Aptos NFT 铸造实战:从创建 Collection 到带 Proof-of-Knowledge 挑战的生产级铸造合约
Aptos NFT 铸造实战从创建 Collection 到带 Proof-of-Knowledge 挑战的生产级铸造合约【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core本文基于 aptos-core 仓库中 mint_nft 教程 的完整四部分示例代码展开带你从零创建一个 NFT Collection 与 Token逐步演进为使用 Resource Account 自动签名、支持 Admin 配置开关、并通过签名挑战防止机器人刷量的生产级 Mint 合约。读完本文你将掌握 Aptos Move 中create_collection/create_tokendata/mint_token的核心参数含义、Resource Account 与SignerCapability的用法、Admin 权限模式的实现方式以及如何用 ed25519 签名验证和自定义事件为 NFT 铸造项目做生产加固。教程总体结构与运行方式README 说明本教程的目标是创建一个 collection 和 token然后把 token mint 给一个 receiver完整操作说明分布在四个子目录的 Move 源码注释中。目录按递进关系组织目录模块名核心主题1-Create-NFTcreate_nft创建 Collection/Token 并 mint 给 receiver双 signer 方案2-Using-Resource-Accountcreate_nft_with_resource_account引入 Resource Account模块可编程签名3-Adding-Admincreate_nft_with_resource_and_admin_accounts增加 Admin 账号与配置更新函数4-Getting-Production-Readycreate_nft_getting_production_ready事件、Proof-of-Knowledge 挑战、单元测试四个示例包的结构一致以 1-Create-NFT/Move.toml 为例[package] name Examples version 0.0.0 [dependencies] AptosFramework { git https://github.com/aptos-labs/aptos-framework.git, subdir aptos-framework, rev mainnet } AptosToken { git https://github.com/aptos-labs/aptos-framework.git, subdir aptos-token, rev mainnet } [addresses] aptos_framework 0x1从依赖声明看示例包通过 git 依赖引用aptos-framework与aptos-token框架包锁定在mainnet修订版所有代币相关能力都来自 0x1 地址下的aptos_token::token模块。第三、四部分的 Move.toml 额外声明了一个命名地址# replace the admin_addr with the actual admin address we created using CLI admin_addr 0xbeef这个admin_addr占位符必须在发布前替换为真实 Admin 地址因为后续 Admin 函数的权限校验正是与admin_addr比对完成的。第一部分创建 Collection、Token 并 Mint 给 Receiver两种 NFT 类型create_nft.move 的文件头注释先厘清了两类 NFT 的区别这是理解后面所有设计的前提Event ticket / certificate活动门票/凭证类所有从同一个 base token 铸造出来的 NFT 共享相同的token_data_id和图片每一次铸造相当于 base token 的一个印刷版本printing edition。典型场景是活动门票每张票可以带有expiration_sec: u64、is_ticket_used: bool等属性mint 时设置过期时间、标记未使用核销时把is_ticket_used更新为 true。本教程全程以这类 NFT 为例Pfp NFT每个 token 都有唯一的token_data_id和图片没有印刷版概念常见于 NFT 交易平台的藏品本质是艺术品所有权的凭证。init_moduleCollection 与 TokenData 的创建参数模块发布时自动调用init_module其中演示了token::create_collection与token::create_tokendata的关键参数fun init_module(source_account: signer) { let collection_name string::utf8(bCollection name); let description string::utf8(bDescription); let collection_uri string::utf8(bCollection uri); let token_name string::utf8(bToken name); let token_uri string::utf8(bToken uri); // This means that the supply of the token will not be tracked. let maximum_supply 0; // 是否允许修改 CollectionData 的 description、uri、maximum 三个字段 let mutate_setting vectorbool[ false, false, false ]; token::create_collection(source_account, collection_name, description, collection_uri, maximum_supply, mutate_setting); let token_data_id token::create_tokendata( source_account, collection_name, token_name, string::utf8(b), // description 0, // supply 上限0 表示不追踪 token_uri, signer::address_of(source_account), // royalty_account 1, // royalty_basis_points1/10000即 0.01% 0, // current_supply token::create_token_mutability_config( vectorbool[ false, false, false, false, true ] // 依次对应token maximum、uri、royalty、description、properties 是否可修改 ), // property maps属性键值对这里记录接收者地址 vectorString[string::utf8(bgiven_to)], vectorvectoru8[b], vectorString[ string::utf8(baddress) ], ); move_to(source_account, ModuleData { token_data_id }); }几个值得注意的参数语义均与源码注释一致maximum_supply 0不追踪供应量链上不会校验 mint 总量Collection 的mutate_setting是三个布尔值分别控制 description、uri、maximum 是否允许后续修改示例中全部关闭Token 的 mutability 配置是五个布尔值依次为 maximum、uri、royalty、description、properties 是否可修改。示例只开启了最后一项properties这是后续用 property map 记录given_to字段的前提property map 采用三组平行 vector键名、键值、嵌套键名的方式初始化示例在given_to.address下记录接收者信息。同时模块用一个has key资源保存铸造上下文struct ModuleData has key { token_data_id: TokenDataId, }发布与双 signer 铸造源码注释给出的操作流程为# 1. 创建账户创建默认账户 aptos init # 2. 在目录 aptos-core/aptos-move/move-examples/mint_nft/1-Create-NFT 下发布模块 aptos move publish --named-addresses mint_nft[默认账户地址]默认账户地址可通过查看~/.aptos/config.yaml获得。发布成功后输出transaction_hash、gas_used、vm_status: Executed successfully等字段可凭交易哈希在 Aptos Explorer 的Changes页签查看模块变更。铸造函数delayed_mint_event_ticket(module_owner, receiver)要求模块 owner 与 receiver 两个账户同时签名内部逻辑为校验 owner 地址等于mint_nft否则以error::permission_denied(ENOT_AUTHORIZED)中断错误码常量ENOT_AUTHORIZED: u64 1然后调用token::mint_token铸造 1 个 token、token::direct_transfer转给 receiver最后调用token::mutate_token_properties把given_to.address属性更新为 receiver 的 BCS 编码地址public entry fun delayed_mint_event_ticket(module_owner: signer, receiver: signer) acquires ModuleData { assert!(signer::address_of(module_owner) mint_nft, error::permission_denied(ENOT_AUTHORIZED)); let module_data borrow_global_mutModuleData(mint_nft); let token_id token::mint_token(module_owner, module_data.token_data_id, 1); token::direct_transfer(module_owner, receiver, token_id, 1); let (creator_address, collection, name) token::get_token_data_id_fields(module_data.token_data_id); token::mutate_token_properties( module_owner, signer::address_of(receiver), creator_address, collection, name, 0, 1, vectorString[string::utf8(bgiven_to)], vectorvectoru8[bcs::to_bytes(signer::address_of(receiver))], vectorString[ string::utf8(baddress) ], ); }这里有一个刻意保留的缺陷同一份token_data_id被反复复用每次铸造只递增 property version——这正是印刷版门票的实现方式相同 token data、不同 property version 的 token。而双 signer 签名在现实中极不实用要么自己实现延迟执行要么让两把钥匙同时在线签名。源码注释明确说这一部分不实际执行 mint 命令问题将在第二部分解决。第二部分Resource Account 让模块可编程签名create_nft_with_resource_account.move 引入了 Resource Account资源账户一种由开发者控制、独立于用户账户管理资源的特性典型用途是发布模块和自动签名。通用区块链语境下它也就是 PDA / 智能合约账户。流程上的变化# 1. 新增一个 nft-receiver 账户 aptos init --profile nft-receiver # 2. 在目录 aptos-core/aptos-move/move-examples/mint_nft/2-Using-Resource-Account 下 # 用 Resource Account 发布包seed 为任意种子如 1235 aptos move create-resource-account-and-publish-package --seed [seed] \ --address-name mint_nft --profile default \ --named-addresses source_addr[默认账户地址]发布命令会打印 Resource Account 的派生地址并请求确认。此时 mint 只需 receiver 单账户签名aptos move run --function-id [resource account 地址]::create_nft_with_resource_account::mint_event_ticket --profile nft-receiver代码层面ModuleData新增了对资源账户签名能力的保存struct ModuleData has key { // Storing the signer capability here, so the module can programmatically sign for transactions signer_cap: SignerCapability, token_data_id: TokenDataId, }在init_module中通过resource_account::retrieve_resource_account_cap(resource_signer, source_addr)取回SignerCapability。源码注释解释了其安全含义调用该函数会把资源账户的认证密钥authentication key旋转为 0源账户从此放弃对该资源账户的控制——调用之前资源账户的认证密钥与源账户一致源账户还能控制它取回能力后模块完全自主也无法再通过更新模块的方式改逻辑。铸造函数因此只需一个 signerpublic entry fun mint_event_ticket(receiver: signer) acquires ModuleData { let module_data borrow_global_mutModuleData(mint_nft); // 用模块内保存的 SignerCapability 构造资源账户的 signer let resource_signer account::create_signer_with_capability(module_data.signer_cap); let token_id token::mint_token(resource_signer, module_data.token_data_id, 1); token::direct_transfer(resource_signer, receiver, token_id, 1); // 同样调用 mutate_token_properties 递增 property version此处传入空属性 vector ... }account::create_signer_with_capability用能力对象变出一个可以代表资源账户签名的 signermint_token和direct_transfer都无需任何人工手动签名。新的问题也随之出现既然模块发布在资源账户下、不可更新如何更新模块自身的配置这就是第三部分的主题。第三部分Admin 账号与可更新的铸造配置create_nft_with_resource_and_admin_accounts.move 在保持资源账户自治不可变的前提下引入 Admin 账户来修改配置——ModuleData增加两个字段struct ModuleData has key { signer_cap: SignerCapability, token_data_id: TokenDataId, expiration_timestamp: u64, // 铸造截止时间秒级时间戳 minting_enabled: bool, // 铸造总开关 }init_module初始化时minting_enabled: false、expiration_timestamp: 10000000000即默认禁止铸造。模块定义了三个错误码const ENOT_AUTHORIZED: u64 1; // 调用者不是 Admin const ECOLLECTION_EXPIRED: u64 2; // 铸造已过期 const EMINTING_DISABLED: u64 3; // 铸造被禁用mint_event_ticket在铸造前增加了两道断言assert!(timestamp::now_seconds() module_data.expiration_timestamp, error::permission_denied(ECOLLECTION_EXPIRED)); assert!(module_data.minting_enabled, error::permission_denied(EMINTING_DISABLED));两个 Admin 函数通过Move.toml中的admin_addr校验调用者身份public entry fun set_minting_enabled(caller: signer, minting_enabled: bool) acquires ModuleData { let caller_address signer::address_of(caller); assert!(caller_address admin_addr, error::permission_denied(ENOT_AUTHORIZED)); let module_data borrow_global_mutModuleData(mint_nft); module_data.minting_enabled minting_enabled; } public entry fun set_timestamp(caller: signer, expiration_timestamp: u64) acquires ModuleData { let caller_address signer::address_of(caller); assert!(caller_address admin_addr, error::permission_denied(ENOT_AUTHORIZED)); let module_data borrow_global_mutModuleData(mint_nft); module_data.expiration_timestamp expiration_timestamp; }操作序列源码注释中给出的完整实战流程# 1. 创建 admin 账户并把 Move.toml 中 admin_addr 占位符替换为真实地址 aptos init --profile admin # 2. 发布模块与第二部分相同的 create-resource-account-and-publish-package 命令 aptos move create-resource-account-and-publish-package --seed [seed] \ --address-name mint_nft --profile default --named-addresses source_addr[默认账户地址] # 3a. 直接 mint —— 预期失败因为 init 时 minting_enabled 为 false aptos move run --function-id [资源账户地址]::create_nft_with_resource_and_admin_accounts::mint_event_ticket --profile nft-receiver # 输出: # { Error: Simulation failed with status: Move abort in 0x...::create_nft_with_resource_and_admin_accounts: EMINTING_DISABLED(0x50003): The collection minting is disabled } # 3b. 用 admin 账户打开铸造开关 aptos move run --function-id [资源账户地址]::create_nft_with_resource_and_admin_accounts::set_minting_enabled --args bool:true --profile admin # 3c. 再次 mint —— 成功 aptos move run --function-id [资源账户地址]::create_nft_with_resource_and_admin_accounts::mint_event_ticket --profile nft-receiver从失败输出中的EMINTING_DISABLED(0x50003)可以看出 Move 错误码的编码方式模块自定义错误码位于0x50000起的区间模块内常量3对应0x50003同理ECOLLECTION_EXPIRED对应0x50002标准库invalid_argument类错误则落在0x10000区间第四部分测试中会见到0x10006。第四部分生产级加固——事件、Proof-of-Knowledge 挑战与单元测试最后一部分 create_nft_getting_production_ready.move 补齐了三样生产要素自定义事件、签名挑战防刷、单元测试。自定义事件#[event] struct TokenMinting has drop, store { token_receiver_address: address, token_data_id: TokenDataId, }mint_event_ticket成功转账后event::emit链上即可按事件追踪谁在什么时候收到了哪个 token便于后端记录铸造账本。Proof-of-Knowledge 签名挑战防刷的核心思路只有持有特定私钥的认证后端才能触发 mint。挑战结构体由接收者账户的序列号、地址与 token data 三元组构成struct MintProofChallenge has drop { receiver_account_sequence_number: u64, receiver_account_address: address, token_data_id: TokenDataId, }验证函数verify_proof_of_knowledge在链上实时读取接收者当前序列号account::get_sequence_number(receiver_addr)重组挑战再用ed25519::signature_verify_strict_t严格校验签名fun verify_proof_of_knowledge( receiver_addr: address, mint_proof_signature: vectoru8, token_data_id: TokenDataId, public_key: ValidatedPublicKey ) { let sequence_number account::get_sequence_number(receiver_addr); let proof_challenge MintProofChallenge { receiver_account_sequence_number: sequence_number, receiver_account_address: receiver_addr, token_data_id, }; let signature ed25519::new_signature_from_bytes(mint_proof_signature); let unvalidated_public_key ed25519::public_key_to_unvalidated(public_key); assert!( ed25519::signature_verify_strict_t(signature, unvalidated_public_key, proof_challenge), error::invalid_argument(EINVALID_PROOF_OF_KNOWLEDGE) ); }源码注释解释了设计动机由于挑战中绑定了接收者当前的receiver_account_sequence_number签名是一次性的——攻击者即使截获一次合法签名也无法重放重放时该账户序列号已经变化同时无法通过批量刷接口来批量 mint因为每次请求都需要后端重新签名。ModuleData中保存的public_key是ValidatedPublicKeyinit_module先写入一个硬编码占位公钥发布后由 Admin 通过set_public_key更新为真实值public entry fun set_public_key(caller: signer, pk_bytes: vectoru8) acquires ModuleData { let caller_address signer::address_of(caller); assert!(caller_address admin_addr, error::permission_denied(ENOT_AUTHORIZED)); let module_data borrow_global_mutModuleData(mint_nft); module_data.public_key std::option::extract(mut ed25519::new_validated_public_key_from_bytes(pk_bytes)); }该部分完整的错误码表ENOT_AUTHORIZED 1、ECOLLECTION_EXPIRED 2、EMINTING_DISABLED 3、EWRONG_PUBLIC_KEY 4、EINVALID_SCHEME 5、EINVALID_PROOF_OF_KNOWLEDGE 6。操作要点来自源码注释# 3.a 生成用于验证的密钥对 aptos key generate --key-type ed25519 --output-file output.key # 3.b 用 admin 账户把新生成的公钥写入 ModuleData公钥内容见 output.key.pub aptos move run --function-id [资源账户地址]::create_nft_getting_production_ready::set_public_key \ --args hex:[公钥hex] --profile admin # 3.c 生成合法签名修改 aptos-move/e2e-move-tests/src/tests/mint_nft.rs 中 # generate_nft_tutorial_part4_signature 函数里的 resource_address、nft_receiver、 # admin_private_key、receiver_account_sequence_number 为实际值后运行 cargo test generate_nft_tutorial_part4_signature -- --nocapture # 3.d 携带签名 mint签名作为 hex 参数传入由 nft-receiver 账户提交交易 aptos move run --function-id [资源账户地址]::create_nft_getting_production_ready::mint_event_ticket \ --args hex:[上一步生成的签名hex] --profile nft-receiver其中接收者账户的当前序列号可在 Aptos Explorer 中查看该账户的Info页签获得。签名生成的 Rust 侧实现在 e2e-move-tests/src/tests/mint_nft.rs测试函数把MintProofChallenge结构体 BCS 编码后用 admin 私钥sign_arbitrary_message签名并打印 hex。注意 Rust 侧的结构体还额外携带了account_address、module_name、struct_name三个字段——这正是 BCS 序列化 Move 结构体时的类型标签type tag与链上 Move 结构体的序列化布局保持一致这也是挑战消息能被signature_verify_strict_t校验通过的细节所在。内置 Move 单元测试第四部分模块自带了完整的单元测试覆盖用#[test_only]的set_up_test搭建环境设置全局时间、模拟资源账户创建、以 admin 身份设置公钥五个用例分别验证测试函数验证场景期望结果test_happy_path两个不同接收者先后合法 mint成功且第二个 token 的 property version 正确递增为 2test_minting_expired时间超过expiration_timestamp后 mint#[expected_failure(abort_code 0x50002)]即ECOLLECTION_EXPIREDtest_update_expiration_timeadmin 把过期时间改为早于当前时间后再 mint预期0x50002test_update_minting_enabledadmin 关闭开关后再 mint预期0x50003EMINTING_DISABLEDtest_invalid_signature篡改签名首字节后再 mint预期0x10006invalid_argument区间 EINVALID_PROOF_OF_KNOWLEDGEtest_happy_path中用token::withdraw_token把 mint 到的 token 从接收者账户取出、再deposit_token放回去token 不可 drop并用token::create_token_id_raw直接构造 property version 为 1 和 2 的 token id 来断言两次铸造的版次递增——这为印刷版机制提供了可执行的验证依据。关键要点小结NFT 版式选择门票/凭证类 NFT 复用同一TokenDataId、以 property version 区分版次PFP 类则为每个 token 创建独立token_data_id。本教程示例全部采用前者参数语义maximum_supply 0表示不追踪供应collection 的三个 mutability 位对应 description/uri/maximumtoken 的五个位对应 maximum/uri/royalty/description/properties开启 properties 修改是写入 property map 的前提Resource Account 是双刃剑retrieve_resource_account_cap取回SignerCapability即放弃源账户控制权换来的是模块内create_signer_with_capability的可编程签名能力随之而来的配置更新需求用 Admin 地址 命名地址比对解决防刷机制的本质挑战消息绑定接收者实时序列号 地址 TokenDataId后端 ed25519 签名一次性有效链上用signature_verify_strict_t严格校验签名不匹配即EINVALID_PROOF_OF_KNOWLEDGE0x10006中断可观测性#[event]的TokenMinting事件让后端可以可靠追踪每次铸造错误码约定模块自定义错误从0x50000起偏移0x50002/0x50003invalid_argument类错误从0x10000起0x10006第四部分的expected_failure注解把这一编码规则写进了测试断言。所有示例代码位于 aptos-move/move-examples/mint_nft 下配合各 Move 文件头部注释中的完整命令输出gas 范围、transaction_hash、vm_status等可以在本地或 devnet 环境完整复现从发布到 mint 的全流程。【免费下载链接】aptos-coreAptos is a layer 1 blockchain built to support the widespread use of blockchain through better technology and user experience.项目地址: https://gitcode.com/GitHub_Trending/ap/aptos-core创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考