

在这一篇文章中,我们将学习如何编写一个自定义合约,并且只需要四个步骤就可以生成NFT。
关于Solana开发的一些提示
在Solana的开发过程中,我们会遇到很多奇怪的自定义错误和bug,因为Solana的开发生态系统不像以太坊的开发生态系统那么大,所以修复它们会非常困难和令人沮丧。但不用担心。当我们陷入困境时,我们只需要在正确的地方寻找解决方案。
项目概述
我们将使用的工具:
- Solana CLI工具——Solana 官方 CLI工具集
- Anchor Framework—— 开发Solana项目的高级框架
- Solana/web3.js——web3.js的Solana版本
- Solana/ spla -token ——一个使用spl代币的包
- Mocha——一个JS测试工具
现在开始
准备工作
在CLI中使用如下命令设置网络为devnet:
solana config set –url devnet
要确认它是否工作,然后输入cmd后检查输出:
Config File: /Users/anoushkkharangate/.config/solana/cli/config.yml RPC URL: https://api.devnet.solana.com WebSocket URL: wss://api.devnet.solana.com/ (computed) Keypair Path: /Users/anoushkkharangate/.config/solana/id.json Commitment: confirmed
接下来,如果还没有设置的话,我们是需要设置文件系统钱包的,并使用命令Solana airdrop 1添加一些devnet sol。
最后,使用anchor CLI 命令行创建一个anchor 项目:
anchor init <name-of-your-project>
确保Anchor.toml也被设置为devnet。
[features] seeds = false [programs.devnet] metaplex_anchor_nft = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" [registry] url = "https://anchor.projectserum.com" [provider] cluster = "devnet" wallet = "/Users/<user-name>/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
步骤1. 导入依赖项
在我们的项目中,必须有一个名为programs的文件夹。转到programs/<your-project-name>/Cargo.toml
并添加这些依赖项。确保使用0.22.1版本,并且可以使用avm来更改它。
[dependencies]
anchor-lang = “0.22.1”
anchor-spl = “0.22.1”
mpl-token-metadata = {version = “1.2.5”, features = [“no-entrypoint”]}
不要使用最新版本的anchor ,因为anchor会延迟使用最新版本的Solana程序进行自我更新,所以它可能会导致mpl crate 和 anchor crate 的依赖需求之间的冲突。
然后转到lib.rssrc 中的文件并导入这些:
use anchor_lang::prelude::*;
use anchor_lang::solana_program::program::invoke;
use anchor_spl::token;
use anchor_spl::token::{MintTo, Token};
use mpl_token_metadata::instruction::{create_master_edition_v3, create_metadata_accounts_v2};
现在我们可以编写mint函数了!
步骤2. 编写Mint函数结构
首先,让我们为mint函数创建accounts结构
#[derive(Accounts)] pub struct MintNFT<'info> { #[account(mut)] pub mint_authority: Signer<'info>, /// CHECK: This is not dangerous because we don't read or write from this account #[account(mut)] pub mint: UncheckedAccount<'info>, // #[account(mut)] pub token_program: Program<'info, Token>, /// CHECK: This is not dangerous because we don't read or write from this account #[account(mut)] pub metadata: UncheckedAccount<'info>, /// CHECK: This is not dangerous because we don't read or write from this account #[account(mut)] pub token_account: UncheckedAccount<'info>, /// CHECK: This is not dangerous because we don't read or write from this account pub token_metadata_program: UncheckedAccount<'info>, /// CHECK: This is not dangerous because we don't read or write from this account #[account(mut)] pub payer: AccountInfo<'info>, pub system_program: Program<'info, System>, /// CHECK: This is not dangerous because we don't read or write from this account pub rent: AccountInfo<'info>, /// CHECK: This is not dangerous because we don't read or write from this account #[account(mut)] pub master_edition: UncheckedAccount<'info>, }
不要担心未检查的帐户,因为我们将把它们传递到Metaplex程序,它将为我们检查它们。
为了在Anchor中使用未检查帐户,我们需要在每个帐户上面添加这个注释:
/// CHECK: This is not dangerous because we don’t read or write from this account
步骤3. Mint函数
让我们创建一个函数,使用我们刚刚创建的结构来生成代币:
pub fn mint_nft( ctx: Context<MintNFT>, creator_key: Pubkey, uri: String, title: String, ) -> Result<()> { msg!("Initializing Mint NFT"); let cpi_accounts = MintTo { mint: ctx.accounts.mint.to_account_info(), to: ctx.accounts.token_account.to_account_info(), authority: ctx.accounts.payer.to_account_info(), }; msg!("CPI Accounts Assigned"); let cpi_program = ctx.accounts.token_program.to_account_info(); msg!("CPI Program Assigned"); let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); msg!("CPI Context Assigned"); token::mint_to(cpi_ctx, 1)?; msg!("Token Minted !!!"); let account_info = vec![ ctx.accounts.metadata.to_account_info(), ctx.accounts.mint.to_account_info(), ctx.accounts.mint_authority.to_account_info(), ctx.accounts.payer.to_account_info(), ctx.accounts.token_metadata_program.to_account_info(), ctx.accounts.token_program.to_account_info(), ctx.accounts.system_program.to_account_info(), ctx.accounts.rent.to_account_info(), ]; msg!("Account Info Assigned"); let creator = vec![ mpl_token_metadata::state::Creator { address: creator_key, verified: false, share: 100, }, mpl_token_metadata::state::Creator { address: ctx.accounts.mint_authority.key(), verified: false, share: 0, }, ]; msg!("Creator Assigned"); let symbol = std::string::ToString::to_string("symb"); invoke( &create_metadata_accounts_v2( ctx.accounts.token_metadata_program.key(), ctx.accounts.metadata.key(), ctx.accounts.mint.key(), ctx.accounts.mint_authority.key(), ctx.accounts.payer.key(), ctx.accounts.payer.key(), title, symbol, uri, Some(creator), 1, true, false, None, None, ), account_info.as_slice(), )?; msg!("Metadata Account Created !!!"); let master_edition_infos = vec![ ctx.accounts.master_edition.to_account_info(), ctx.accounts.mint.to_account_info(), ctx.accounts.mint_authority.to_account_info(), ctx.accounts.payer.to_account_info(), ctx.accounts.metadata.to_account_info(), ctx.accounts.token_metadata_program.to_account_info(), ctx.accounts.token_program.to_account_info(), ctx.accounts.system_program.to_account_info(), ctx.accounts.rent.to_account_info(), ]; msg!("Master Edition Account Infos Assigned"); invoke( &create_master_edition_v3( ctx.accounts.token_metadata_program.key(), ctx.accounts.master_edition.key(), ctx.accounts.mint.key(), ctx.accounts.payer.key(), ctx.accounts.mint_authority.key(), ctx.accounts.metadata.key(), ctx.accounts.payer.key(), Some(0), ), master_edition_infos.as_slice(), )?; msg!("Master Edition Nft Minted !!!"); Ok(()) } }
如果想调试我们的程序,最好使用msg!()来记录我们想要检查的任何值。它接受字符串,所以我们必须使用std::string::ToString来转换。我们的日志将显示在终端或.anchor/program-logs/<program-id>中

该creator数组需要有铸造 NFT 的人作为其中的一部分,但我们可以将份额设置为 0,所以这并不重要。以下是代码:
let creator = vec![ mpl_token_metadata::state::Creator { address: creator_key, verified: false, share: 100, }, mpl_token_metadata::state::Creator { address: ctx.accounts.mint_authority.key(), verified: false, share: 0, }, ];
现在还没有实现集合,因为它不在本指南的范围内,但我们可以通过使用以下方法来实现:
mpl_token_metadata::instruction::set_and_verify_collection
关于为什么我把这里的最大供给量设为0。在Metaplex中,如果代币是独一无二的,那么我们必须将其最大供应量设置为零,因为要求的总供应-实际供应(1-1)等于0
&create_master_edition_v3( ctx.accounts.token_metadata_program.key(), ctx.accounts.master_edition.key(), ctx.accounts.mint.key(), ctx.accounts.payer.key(), ctx.accounts.mint_authority.key(), ctx.accounts.metadata.key(), ctx.accounts.payer.key(), Some(0), // max supply 0 ),
一旦我们写了这个函数,运行anchor build && anchor deploy,我们应该会看到部署的程序ID。
将此程序 ID 粘贴到Anchor.toml
和lib.rs
文件中,我们将看到此默认 ID 的任何位置Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS
步骤4. 调用Mint函数
在做任何事情之前,确保我们已经导入了@solana/web3.js和@solana/spl-token。在tests/<test-file>.ts
添加这些导入和常量:
import { TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, getAssociatedTokenAddress, createInitializeMintInstruction, MINT_SIZE, } from "@solana/spl-token"; import { LAMPORTS_PER_SOL } from "@solana/web3.js"; const { PublicKey, SystemProgram } = anchor.web3; q const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey( "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" ); const lamports: number = await program.provider.connection.getMinimumBalanceForRentExemption( MINT_SIZE ); const getMetadata = async ( mint: anchor.web3.PublicKey ): Promise<anchor.web3.PublicKey> => { return ( await anchor.web3.PublicKey.findProgramAddress( [ Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), ], TOKEN_METADATA_PROGRAM_ID ) )[0]; }; const getMasterEdition = async ( mint: anchor.web3.PublicKey ): Promise<anchor.web3.PublicKey> => { return ( await anchor.web3.PublicKey.findProgramAddress( [ Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from("edition"), ], TOKEN_METADATA_PROGRAM_ID ) )[0]; }; const mintKey: anchor.web3.Keypair = anchor.web3.Keypair.generate();
现在让我们制作代币和与其关联的代币账户,如下所示:
const NftTokenAccount = await getAssociatedTokenAddress( mintKey.publicKey, program.provider.wallet.publicKey ); console.log("NFT Account: ", NftTokenAccount.toBase58()); const mint_tx = new anchor.web3.Transaction().add( anchor.web3.SystemProgram.createAccount({ fromPubkey: program.provider.wallet.publicKey, newAccountPubkey: mintKey.publicKey, space: MINT_SIZE, programId: TOKEN_PROGRAM_ID, lamports, }), createInitializeMintInstruction( mintKey.publicKey, 0, program.provider.wallet.publicKey, program.provider.wallet.publicKey ), createAssociatedTokenAccountInstruction( program.provider.wallet.publicKey, NftTokenAccount, program.provider.wallet.publicKey, mintKey.publicKey ) ); const res = await program.provider.send(mint_tx, [mintKey]); console.log( await program.provider.connection.getParsedAccountInfo(mintKey.publicKey) ); console.log("Account: ", res); console.log("Mint key: ", mintKey.publicKey.toString()); console.log("User: ", program.provider.wallet.publicKey.toString()); const metadataAddress = await getMetadata(mintKey.publicKey); const masterEdition = await getMasterEdition(mintKey.publicKey); console.log("Metadata address: ", metadataAddress.toBase58()); console.log("MasterEdition: ", masterEdition.toBase58());
注意:mint 和 freeze 权限必须相同,否则就不起作用。 createInitializeMintInstruction( mintKey.publicKey, 0, program.provider.wallet.publicKey,// mint auth program.provider.wallet.publicKey // freeze auth ),
现在,调用mint函数并传递所有的数据和帐户。
const tx = await program.rpc.mintNft( mintKey.publicKey, "https://arweave.net/y5e5DJsiwH0s_ayfMwYk-SnrZtVZzHLQDSTZ5dNRUHA", "NFT Title", { accounts: { mintAuthority: program.provider.wallet.publicKey, mint: mintKey.publicKey, tokenAccount: NftTokenAccount, tokenProgram: TOKEN_PROGRAM_ID, metadata: metadataAddress, tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID, payer: program.provider.wallet.publicKey, systemProgram: SystemProgram.programId, rent: anchor.web3.SYSVAR_RENT_PUBKEY, masterEdition: masterEdition, }, } ); console.log("Your transaction signature", tx);
现在只需运行anchor 测试,我们应该就能够创建我们的NFT。
账户:
4swrfmnovhckxy3gdgagbxzwpffuvyxwpwsgxqbyvozg1m63nzhxyprm7ktqajsdtphn2ivypr6jqfxelsb6a1nx Mint key:DehGx61vZPYNaMWm9KYdP91UYXXLu1XKoc2CCu3NZFNb
用户:7ctwnydtnbb3p9eviqszkuekjcknmcaassmc7nbtvkue
元数据地址:7ut8ymzgqzaxvrdro8jlkkpnuccdeqxsfznv1hjzc3bo MasterEdition:Au76v2ZDnWSLj23TCu9NRVEYWrbVUq6DAGNnCuALaN6o 交易签名是kwest87h3dz5gw5cdl1jtirkwcxjknzyvqshatligxz4hqgsda7ew6rrhqbj2tqqfrwzzfvhfbu1cpyyh7whh
✔初始化!(6950毫秒)
1通过(7) ✨
9.22秒完成。
如果我们得到任何带有十六进制值(如0x1)的自定义程序错误,将十六进制值转换为纯文本,然后转到metaplex github并使用浏览器搜索数字+第1次出现的单词“error(”。
总结
我希望这篇指南对所有Solana爱好者有用。
关于
ChinaDeFi – ChinaDeFi.com 是一个研究驱动的DeFi创新组织,同时我们也是区块链开发团队。每天从全球超过500个优质信息源的近900篇内容中,寻找思考更具深度、梳理更为系统的内容,以最快的速度同步到中国市场提供决策辅助材料。
Layer 2道友 – 欢迎对Layer 2感兴趣的区块链技术爱好者、研究分析人与Gavin(微信: chinadefi)联系,共同探讨Layer 2带来的落地机遇。敬请关注我们的微信公众号 “去中心化金融社区”。
