通过 Token Extension program,您可以使用元数据扩展来创建 NFT 和数字资产。这些扩展(元数据指针和 token 元数据)让您能够将任意所需的元数据原生存储在链上——全部存放在 token 的 mint account 上可自定义的键值数据存储中,从而降低成本和复杂性。
这对于 Web3 游戏 来说尤为出色,因为我们现在可以在链上键值存储中设置这些「附加元数据字段」,允许游戏在 NFT 本身中保存和访问独特的状态(例如游戏角色的属性或物品栏)。
构建链上程序
在本开发者指南中,我们将演示如何使用 Anchor 程序构建基于 Token Extension 的 NFT 和自定义元数据。该程序将在 NFT 中保存游戏玩家的等级和收集到的资源。
此 NFT 由 Anchor 程序创建,因此从 JavaScript 客户端铸造非常简便。每个 NFT 将通过 Token Metadata 接口提供一些基本结构:
- 默认链上字段 ——
name、symbol和uriuri是指向链下 JSON 文件的链接,该文件包含 NFT 的链下元数据
- 我们还将定义自定义「附加字段」
所有这些字段均使用元数据扩展保存,并指向 NFT 的 mint account,使任何人或任何程序都可以访问。
游戏中的其他应用场景
这类带有可自定义链上元数据的 NFT 为游戏开发者带来了许多有趣的可能性,尤其是因为这些元数据可以由链上程序直接交互或管理。
一些与游戏相关的应用场景包括:
- 保存玩家的等级和经验值
- 当前装备的武器和护甲
- 当前任务进度
- 以此类推,不胜枚举!
铸造 NFT
要创建 NFT,我们需要执行以下步骤:
- 创建一个 mint account
- 初始化 mint account
- 创建元数据指针账户
- 初始化元数据指针账户
- 创建元数据账户
- 初始化元数据账户
- 创建 associated token account
- 将 token 铸造到 associated token account
- 冻结铸造权限
Rust 程序代码
以下是使用 Token Extension program 铸造 NFT 的 Rust 代码:
// calculate the space need for the mint account with the desired extensionslet space = ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::MetadataPointer]).unwrap();// This is the space required for the metadata account.// We put the metadata into the mint account at the end so we// don't need to create and additional account.// Then the metadata pointer points back to the mint account.// Using this technique, only one account is needed for both the mint// information and the metadata.let meta_data_space = 250;let lamports_required = (Rent::get()?).minimum_balance(space + meta_data_space);msg!("Create Mint and metadata account size and cost: {} lamports: {}",space as u64,lamports_required);system_program::create_account(CpiContext::new(ctx.accounts.token_program.to_account_info(),system_program::CreateAccount {from: ctx.accounts.signer.to_account_info(),to: ctx.accounts.mint.to_account_info(),},),lamports_required,space as u64,&ctx.accounts.token_program.key(),)?;// Assign the mint to the token programsystem_program::assign(CpiContext::new(ctx.accounts.token_program.to_account_info(),system_program::Assign {account_to_assign: ctx.accounts.mint.to_account_info(),},),&token_2022::ID,)?;// Initialize the metadata pointer (Need to do this before initializing the mint)let init_meta_data_pointer_ix =spl_token_2022::extension::metadata_pointer::instruction::initialize(&Token2022::id(),&ctx.accounts.mint.key(),Some(ctx.accounts.nft_authority.key()),Some(ctx.accounts.mint.key()),).unwrap();invoke(&init_meta_data_pointer_ix,&[ctx.accounts.mint.to_account_info(),ctx.accounts.nft_authority.to_account_info()],)?;// Initialize the mint cpilet mint_cpi_ix = CpiContext::new(ctx.accounts.token_program.to_account_info(),token_2022::InitializeMint2 {mint: ctx.accounts.mint.to_account_info(),},);token_2022::initialize_mint2(mint_cpi_ix,0,&ctx.accounts.nft_authority.key(),None).unwrap();// We use a PDA as a mint authority for the metadata account because// we want to be able to update the NFT from the program.let seeds = b"nft_authority";let bump = ctx.bumps.nft_authority;let signer: &[&[&[u8]]] = &[&[seeds, &[bump]]];msg!("Init metadata {0}", ctx.accounts.nft_authority.to_account_info().key);// Init the metadata accountlet init_token_meta_data_ix =&spl_token_metadata_interface::instruction::initialize(&spl_token_2022::id(),ctx.accounts.mint.key,ctx.accounts.nft_authority.to_account_info().key,ctx.accounts.mint.key,ctx.accounts.nft_authority.to_account_info().key,"Beaver".to_string(),"BVA".to_string(),"https://arweave.net/MHK3Iopy0GgvDoM7LkkiAdg7pQqExuuWvedApCnzfj0".to_string(),);invoke_signed(init_token_meta_data_ix,&[ctx.accounts.mint.to_account_info().clone(), ctx.accounts.nft_authority.to_account_info().clone()],signer,)?;// Update the metadata account with an additional metadata field in this case the player levelinvoke_signed(&spl_token_metadata_interface::instruction::update_field(&spl_token_2022::id(),ctx.accounts.mint.key,ctx.accounts.nft_authority.to_account_info().key,spl_token_metadata_interface::state::Field::Key("level".to_string()),"1".to_string(),),&[ctx.accounts.mint.to_account_info().clone(),ctx.accounts.nft_authority.to_account_info().clone(),],signer)?;// Create the associated token accountassociated_token::create(CpiContext::new(ctx.accounts.associated_token_program.to_account_info(),associated_token::Create {payer: ctx.accounts.signer.to_account_info(),associated_token: ctx.accounts.token_account.to_account_info(),authority: ctx.accounts.signer.to_account_info(),mint: ctx.accounts.mint.to_account_info(),system_program: ctx.accounts.system_program.to_account_info(),token_program: ctx.accounts.token_program.to_account_info(),},))?;// Mint one token to the associated token account of the playertoken_2022::mint_to(CpiContext::new_with_signer(ctx.accounts.token_program.to_account_info(),token_2022::MintTo {mint: ctx.accounts.mint.to_account_info(),to: ctx.accounts.token_account.to_account_info(),authority: ctx.accounts.nft_authority.to_account_info(),},signer),1,)?;// Freeze the mint authority so no more tokens can be minted to make it an NFTtoken_2022::set_authority(CpiContext::new_with_signer(ctx.accounts.token_program.to_account_info(),token_2022::SetAuthority {current_authority: ctx.accounts.nft_authority.to_account_info(),account_or_mint: ctx.accounts.mint.to_account_info(),},signer),AuthorityType::MintTokens,None,)?;
JavaScript 客户端代码
从客户端调用铸造 NFT 非常简单:
const nftAuthority = PublicKey.findProgramAddressSync([Buffer.from("nft_authority")],program.programId);const mint = new Keypair();const destinationTokenAccount = getAssociatedTokenAddressSync(mint.publicKey,publicKey,false,TOKEN_2022_PROGRAM_ID);const transaction = await program.methods.mintNft().accounts({signer: publicKey,systemProgram: SystemProgram.programId,tokenProgram: TOKEN_2022_PROGRAM_ID,tokenAccount: destinationTokenAccount,mint: mint.publicKey,rent: web3.SYSVAR_RENT_PUBKEY,associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,nftAuthority: nftAuthority[0]}).signers([mint]).transaction();console.log("transaction", transaction);const txSig = await sendTransaction(transaction, connection, {signers: [mint],skipPreflight: true});console.log(`https://explorer.solana.com/tx/${txSig}?cluster=devnet`);
快速入门示例
上述示例基于 Solana Games Preset,它会为您生成一个脚手架项目,其中包含适用于该游戏的 JavaScript 和 Unity 客户端,以及与 Solana Anchor 程序交互的相关配置。
您可以使用以下命令自行运行:
npx create-solana-game gameName
配置本地开发环境
要在本地运行此示例,您需要确保已为 Solana 开发配置好本地环境,包括安装和配置 Anchor CLI。如果尚未完成,可以参照前面提到的配置指南进行操作。
项目结构
Anchor 项目的结构如下:
入口点位于 lib.rs 文件中,在此定义程序 ID 和指令。指令定义在 instructions 文件夹中,状态定义在 state 文件夹中。
调用请求进入 lib.rs 文件后,会被转发到对应的指令处理器。指令处理器再调用状态层来获取并更新数据。
您可以在 instructions 文件夹中找到铸造 NFT 的指令。
├── src│ ├── instructions│ │ ├── chop_tree.rs│ │ ├── init_player.rs│ │ ├── mint_nft.rs│ │ └── update_energy.rs│ ├── state│ │ ├── game_data.rs│ │ ├── mod.rs│ │ └── player_data.rs│ ├── lib.rs│ └── constants.rs│ └── errors.rs
Anchor 程序
完成由 create-solana-game 工具生成的 Anchor 程序的配置:
- 执行
cd program进入程序目录 - 运行
anchor build构建程序 - 运行
anchor deploy部署程序 - 将终端输出的程序 ID 复制到
lib.rs、anchor.toml,以及 Unity 项目中的AnchorService,如使用 JavaScript 则还需复制到anchor.ts文件中 - 再次构建并部署
NextJS 客户端
完成由 create-solana-game 工具生成的 NextJS 客户端的配置:
- 将
programId复制到app/utils/anchor.ts - 执行
cd app进入应用目录 - 运行
yarn install安装 Node 依赖 - 运行
yarn dev启动客户端 - 每次修改 Anchor 程序后,请确保将程序目录中的类型定义复制到客户端以便使用。TypeScript 类型定义文件位于
target/idl文件夹中。
在本地运行此示例
使用 Anchor 的 test 命令并附带 --detach 标志,将启动并配置本地 Solana 测试 validator,使程序处于已部署状态(并在测试完成后保持 validator 持续运行):
cd programanchor test --detach
然后,您可以将 Solana Explorer 设置为使用本地测试 validator(运行 anchor test 命令时启动),以便查看相关交易:
https://explorer.solana.com/?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899
该程序也已部署到网络上,您可以在 devnet 上进行体验。JavaScript 客户端同样提供了铸造 NFT 的按钮。启动 JavaScript 客户端:
cd appyarn installyarn dev
打开 Unity 项目
首先使用 Unity 2021.3.32.f1(或相近版本)打开 Unity 项目,然后打开 GameScene 或 LoginScene 并点击运行。使用左下角的编辑器登录按钮进行登录。
如果无法获取 devnet SOL,您可以从控制台复制您的地址,并参照本指南中的说明:如何获取 devnet SOL
在 Unity 中连接到 Solana 测试 validator
如果您希望避免维护 devnet SOL 的烦恼,可以在 Unity 中连接到本地运行的测试 validator。只需在钱包持有者游戏对象上添加以下链接:
http://localhost:8899ws://localhost:8900
运行 JavaScript 客户端
要启动 JavaScript 客户端并通过浏览器与游戏和程序进行交互:
- 在仓库中打开
app目录 - 安装 Node 依赖
- 运行
dev命令以启动开发服务器
cd appyarn installyarn dev
若要开始修改程序并连接到您自己的程序,请按照以下步骤操作。
Is this page helpful?