Anchor+Metaplex创建NFT

在本文中,我们将介绍如何使用Anchor和Metaplex来铸造Solana NFT

Anchor+Metaplex创建NFT
一键发币: SOL | BNB | ETH | BASE | Blast | ARB | OP | POLYGON | AVAX | FTM | OK

在本文中,我们将使用Anchor(Solana 程序框架)来铸造我们的 NFT。 在此链接中了解有关Anchor框架的更多信息。

1、环境设置

如果你的 Solana 环境已正确设置,可以跳过这部分内容。

对于 Solana 的新手,我们将首先安装作为 CLI 套件提供的 Solana Suite:

sh -c "$(curl -sSfL https://release.solana.com/stable/install)"

根据你的系统,可能会收到安装程序消息:

Please update your PATH environment variable to include the solana programs:

按照安装程序说明进行操作,并通过运行以下命令确认 Solana 套件已正确安装:

solana --version

我们最终将安装Anchor版本管理器提供的 anchor-cli。 在此官方页面上查找指南。

使用cargo安装 avm

cargo install --git https://github.com/coral-xyz/anchor avm --locked --force

使用avm安装anchor cli:

avm install latest
avm use latest

运行如下命令以确认Anchor已正确安装:

anchor --version

在撰写本文时,运行 anchor --version && solana --version 后我的环境如下所示:

anchor-cli 0.28.0
solana-cli 1.16.13 (src:b2a38f65; feat:3949673676, client:SolanaLabs)

一旦一切设置正确,我们就可以开始使用anchor cli 来初始化我们的新项目:

anchor init solana-nft-anchor

进入创建的anchor项目并使用你喜欢的文本编辑器将其打开。 在我们的例子中,使用 vs code,我们将运行:

cd solana-nft-anchor
code .

我们需要的第一件事是为 anchor-lang crate启用 init_if_needed功能。 如果没有自动创建帐户,此功能允许我们创建帐户。

cargo add anchor-lang --features=init-if-needed

我们还将安装具有 metadata功能的 anchor-spl crate。 anchor-spl crate 为我们提供了与代币程序交互所需的几乎所有必要功能,而元数据功能则将与 Metaplex 的令牌元数据程序交互所需的功能纳入范围。

cargo add anchor-spl --features=metadata

运行 anchor build,确保一切都相互协同工作,并且我们不会处理依赖冲突。

我们现在准备开始编写anchor程序来为我们铸造 NFT。 打开 programs/solana-nft-anchor/lib.rs 文件,它应该具有与此类似的内容:

use anchor_lang::prelude::*;

declare_id!("9TEtkW972r8AVyRmQzgyMz8GpG7WJxJ2ZUVZnjFNJgWM"); // should'nt be similar to mine

#[program]
pub mod solana_nft_anchor {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

如果你遇到此错误“结构需要 0 个生命周期参数,但提供了预期的生命周期参数 0 个生命周期参数”,可以忽略它,因为一旦我们开始处理代码,它就会消失。

我们将 Initialize 重命名为 InitNFT,并将在 Accounts 结构中创建 NFT 时将与之交互的账户纳入范围。

我们将纳入范围的第一个帐户将是 signer。 该账户是我们进行的交易的授权者、费用支付者以及交易的签名者。

// snip

#[derive(Accounts)]
pub struct InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[account(mut, signer)]
    signer: AccountInfo<'info>
}

但是 Solana 并行程序执行层(Sealevel)如何区分签名者帐户和普通帐户呢? 我们将其标记为一。 有两种方法可以做到这一点,使用签名者帐户变体或使用我们正在做的anchor约束。 使用 #[account()] 定义的约束是用于简化常见安全检查的内置功能,例如 将帐户标记为可变或不可变。

在上面的代码片段中,我们已将该帐户标记为可变帐户(因为我们将在支付交易时更改帐户余额)并使用 #[account(mut, Signer)] 作为签名者

还要注意的是 Rustdoc 注释的使用  /// CHECK: ok, we are passing in this account ourselves。 这在使用 AccountInfo 包装器时至关重要。 我们将在下一节介绍帐户包装器后讨论这个问题。

2、与代币程序交互

为了创建我们的 NFT,我们需要与代币程序和元数据程序进行交互。 在 Anchor 和 Solana 中工作时,需要显式声明将与之交互的所有帐户。

这将我们带到了我们纳入范围的第二个账户,即 Mint 账户。 代币的铸币账户包含该代币的详细信息,例如铸币权限、冻结权限、总供应量等。

Mint账户
use anchor_spl::token::Mint;

// snip

#[derive(Accounts)]
pub struct InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[account(mut, signer)]
    signer: AccountInfo<'info>,
    #[account(
        init,
        payer = signer,
        mint::decimals = 0,
        mint::authority = signer.key(),
        mint::freeze_authority = signer.key(),
    )]
    mint: Account<'info, Mint>,
}

我们还为我们的mint账户添加了更多限制。 让我们回顾一下它们。

第一个约束 init 就像 system_instruction::create_account() 函数的“包装器”,指示系统程序创建帐户。 此初始化分三个步骤进行:分配空间、转让 Lamports 以供出租并将帐户分配给拥有的程序。

第二个约束出现的地方是 payer = Signer 用于支付帐户创建的租金。 什么是租金? 为了在 Solana 上存储数据,你必须支付某种押金。 这会激励验证器存储你的数据。 如果不付款,你的数据将从区块链中删除。 在这里阅读更多内容。

第三个约束 mint::decimals = 0 设置 NFT 代币的小数位数。 你不可能拥有 0.25 个 NFT! 最后,我们将 mint::authority = signer.key(), mint::freeze_authority = signer.key(), 字段设置为我们的地址。

看这个新添加的账户,声明与第一个账户不同。 我们现在使用 Account 帐户类型而不是 AccountInfo

Anchor AccountInfo 类型是一种定义帐户的方法,不对正在传递的帐户实施任何检查。 我们盲目地相信所传递的帐户是正确的帐户,而不验证数据的结构或帐户的所有者。 因此,我们还必须使用 rustdoc 注释 /// CHECK: ok, we are passing in this account ourselves将其显式标记为可信。

Account帐户类型是一种更安全的帐户声明方式。 它包含 AccountInfo的所有方法,但它验证程序所有权并将底层数据反序列化为指定类型。 上面的帐户检查我们的铸币厂的所有者确实是代币程序 TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,并且该帐户包含mint帐户的所有必需字段。

Mint 账户包含的字段包括:

  • mint_authority - 允许铸造更多代币的地址。 对于我们的 NFT,该字段将设置为零。 任何人都不得铸造更多代币。 (对于无限供应的可替代代币很有用)
  • supply:代币的总供应量。 在我们的例子中,当铸造 NFT 时,供应量将设置为 1。
  • decimals:解释代币余额时要考虑的小数位数。 对于我们的 NFT 来说,这将为零。 为什么? 因为不存在0.5个NFT这样的东西。
  • is_initialized:代币铸造是否已初始化? 确实如此,当我们调用mint指令时。
  • freeze_authority:允许冻结代币账户的地址。

注意:mint账户不持有任何代币。

如果mint账户不持有你的代币,那么它们存储在哪里? 在 Solana 上创建代币时,你需要Token账户来保存新创建的代币。 Token帐户包含以下字段:

  • mint - 与此帐户关联的铸币地址。
  • owner - 对该帐户拥有权限的地址。
  • amount - 该账户中的代币数量。
  • delegate - 可以管理你的代币帐户的另一个地址的地址。 这意味着转移或冻结你的资产。
  • state - 帐户状态。 它是三个可能值的枚举,Uninitialized,表示帐户不存在;Initialized,表示帐户已创建并存在;Frozen,表示帐户已被冻结机构冻结。
  • is_native,此代币是原生 Solana 代币吗?
  • delegated_amount - 委托给上述委托字段的金额。
  • close_authority - 可以关闭此帐户的地址。

然而,这种方法有一个缺点。

第一个缺点。 假设你是一个 NFT 囤积者,已经收集了 1000 个 NFT。 当你的朋友想要从你已经拥有的铸币厂向你发送 NFT 时,他需要知道发送该 NFT 的正确代币账户。 这意味着要跟踪所有这 1000 个代币账户。

第二个缺点。 假设你想向你的非加密货币原生朋友介绍 NFT。 你的朋友以前从未从该收藏中铸造过。 如果你想从他从未铸造过的收藏中向他发送他的第一个 NFT,你的朋友需要拥有该mint账户的 NFT 的代币帐户。 这使得资产转移变得困难且繁琐。 这也意味着空投活动变得不可能。

这就是减少使用 Solana 代币时的摩擦的动机所在,这导致了使用关联代币账户(Associated Token Account)为代币帐户映射到用户钱包的新方法。

关联代币账户是使用地址和铸币账户确定性派生的 PDA。

我们来看看铸造所需的账户:

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token::{Mint, Token, TokenAccount},
};

// snip

#[derive(Accounts)]
pub struct InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[account(mut, signer)]
    pub signer: AccountInfo<'info>,
    #[account(
        init,
        payer = signer,
        mint::decimals = 0,
        mint::authority = signer.key(),
        mint::freeze_authority = signer.key(),
    )]
    pub mint: Account<'info, Mint>, // new
    #[account(
        init_if_needed,
        payer = signer,
        associated_token::mint = mint,
        associated_token::authority = signer,
    )]
    pub associated_token_account: Account<'info, TokenAccount>, // new

    pub token_program: Program<'info, Token>, // new
    pub associated_token_program: Program<'info, AssociatedToken>, // new
    pub system_program: Program<'info, System>, // bew
    pub rent: Sysvar<'info, Rent> // new
}

我们拥有在创建铸币帐户和代币帐户时将与之交互的帐户。

Mint账户由铸币账户类型定义。

我们有一个 Associated_token_account,具有多个anchor约束。 如果我们的钱包中不存在该代币帐户,我们使用 init_if_needed 标志来初始化该代币帐户,并且要使用此功能,你需要定义一个付款人,该付款人将承担与创建新帐户相关的费用。 我们还传入权限和mint作为约束,将mint链接到代币账户。

我还添加了一个空格来作为帐户和程序之间的视觉分隔。 请记住(我知道我听起来像是一张破唱片,但记住这一点很重要),Solana 上的所有内容都是一个帐户。 这些程序也是帐户。 不同之处在于它们的可执行字段标记为 true。 点击这里了解有关 Solana 帐户模型的更多信息。

Anchor 还提供了另一个原语,使程序的使用更加容易,我们使用程序将帐户标记为可执行文件。 这也实现了与帐户类似的检查。 这意味着你无法传递试图将其冒充为令牌程序的恶意程序的地址。 ID 不匹配,程序执行将停止。

我们来谈谈这四个程序。

  • token_program - 代币程序
  • Associated_token_program - 处理 ATA(关联令牌帐户)的创建。
  • system_program - 因为关联的代币程序最终可能需要创建一个新的 ATA,所以我们需要传入这个负责创建所有帐户的程序。
  • rent - 在 Solana 上,当你在区块链上存储数据时需要支付空间费用。 Solana(现在)上的所有账户都需要免租,这意味着需要缴纳 2 年的 sol 来在链上存储数据。 (顺便说一句,没那么贵)。 因此,我们需要为此与rent程序进行交互。

至此,我们现在准备调用指令,在 Solana 上创建我们的第一个 NFT。 让我们首先调用创建铸币和代币账户的指令。 为此,我们需要与代币程序进行交互。 实现此目的的一种方法是使用调用,就像我在上一篇文章中所做的那样,并对它进行跨程序调用。 使用调用很棘手,因为它要求你传递正确的帐户数量以及要与之交互的正确帐户。 。 你错过了一个账户,然后,你的 TX 失败了,你收到一个神秘的错误,告诉你“程序失败,因为你错过了一个账户”,但它无法告诉你错过了哪个账户 🤦。

Anchor 通过使用 CpiContext 结构来简化这一过程,该结构封装了我们在进行 CPI 调用时需要与之交互的所有帐户。 它还已经为常用指令定义了方法。

让我们看一下如何使用 CpiContext 来初始化代币铸造。 要初始化 CpiContext,请调用关联函数 new,该函数接受两个参数。

  • 我们正在执行 cpi 的外部程序。
  • 我们将传入anchor定义的帐户,以使对外部程序的调用成功。 与对外部程序的正常调用相反,使用anchor定义的帐户意味着我们只会声明重要的帐户,而不是几乎永远不会更改的所有程序。

例如,让我们看一下用于初始化新代币mint和使用它的关联代币帐户的代码。

pub fn init_nft(ctx: Context<InitNFT>) -> Result<()> {
        // create mint account
        let cpi_context = CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            MintTo {
                mint: ctx.accounts.mint.to_account_info(),
                to: ctx.accounts.associated_token_account.to_account_info(),
                authority: ctx.accounts.signer.to_account_info(),
            },
        );

        mint_to(cpi_context, 1)?;
        Ok(())
    }

如上所述,我们首先通过调用 CpiContext::new() 方法创建 cpi 上下文。 我们正在与令牌程序交互,因此我们首先将其传入。 对于第二次发送,我们将传入一个结构体,其中包含定义我们将与之交互的帐户。

这是所有代码。

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token::{mint_to, Mint, MintTo, Token, TokenAccount},
};

declare_id!("9TEtkW972r8AVyRmQzgyMz8GpG7WJxJ2ZUVZnjFNJgWM"); // shouldn't be similar to mine

#[program]
pub mod solana_nft_anchor {

    use super::*;

    pub fn init_nft(ctx: Context<InitNFT>) -> Result<()> {
        // create mint account
        let cpi_context = CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            MintTo {
                mint: ctx.accounts.mint.to_account_info(),
                to: ctx.accounts.associated_token_account.to_account_info(),
                authority: ctx.accounts.signer.to_account_info(),
            },
        );

        mint_to(cpi_context, 1)?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[account(mut, signer)]
    pub signer: AccountInfo<'info>,
    #[account(
        init,
        payer = signer,
        mint::decimals = 0,
        mint::authority = signer.key(),
        mint::freeze_authority = signer.key(),
    )]
    pub mint: Account<'info, Mint>,
    #[account(
        init_if_needed,
        payer = signer,
        associated_token::mint = mint,
        associated_token::authority = signer,
    )]
    pub associated_token_account: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

按原样调用此方法将创建我们的 NFT,但是如果没有猴子图像,猴子的不可替代性有什么用呢? 😂。 在下一节中,我们将深入探讨 Metaplex 令牌元数据程序的使用。

3、与 Metaplex 元数据程序交互

Metaplex 提供了一系列工具、智能合约等,旨在使创建和启动 NFT 的过程变得更加容易。

在本指南中,我们将使用他们的代币元数据程序将元数据添加到我们的 spl 代币。

但在此之前,我们需要了解 Metaplex 的幕后工作原理。 请记住 ATA(关联令牌帐户)。 它们是由称为程序派生帐户的程序(智能合约)拥有和控制的特殊类型帐户的一部分。 简单地说,它们是没有对应公钥的公钥。 它们有各种用例,例如签署交易、存储 SOL 和存储数据,如 ATA 中所使用的那样。 它们通常是使用程序的公钥和种子导出的,这些密钥和种子由开发人员选择并作为字节传递到 find_program_address() 函数中。 此 sha256 哈希函数查找不在 ed25519 椭圆曲线上的地址(曲线上的地址是密钥对)。 有关 PDA 的更多详细信息,请参见此处

Metaplex 元数据程序也使用 PDA。 与关联代币程序一样,元数据程序使用 PDA 作为元数据帐户,将其自身附加到 Mint 帐户。 元数据帐户是使用以下种子、元数据、代币元数据程序公钥(即 metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s)以及最后铸币帐户的公钥派生的。 Anchor 有一种不同的方式来派生 PDA,但这里是使用本机 Solana Rust 程序执行上述操作的代码片段。

let metadata_seeds = &[PREFIX.as_bytes(), program_id.as_ref(), mint_pubkey.as_ref()];
let (pubkey, _) = Pubkey::find_program_address(metadata_seeds, &ID);

现在我们知道如何派生元数据帐户,让我们看看该帐户拥有的字段,如下图所示。

Metadata账户

key。 这是第一个字段,它是一个枚举,让 Metaplex 可以识别要使用的“Metaplex 帐户类型”。 它类似于锚鉴别器。 为什么需要它。 代币程序访问不同的帐户,例如 NFT 印刷版的版本帐户、可编程 NFT 的代币记录等。 在我们的元数据帐户示例中,该字段标有 MetadataV1 枚举变体。

以下是这些标记和帐户的详尽列表以及它们用途的解释。

  • Uninitialized:账户尚不存在,需要创建。
  • MetadataV1 :该帐户保存代币元数据(我们当前正在使用的元数据)。
  • MasterEditionV1 和 MasterEditionV2: 主版帐户允许 NFT 打印有限次或无限次。 当我们说印刷时,我们的意思是制作 NFT 的副本
  • EditionV1: 从铸币账户派生的版本账户代表从 Master Edition NFT 复制而来的 NFT。
  • EditionMarker 和 EditionMarkerV2 :此帐户用于在内部跟踪哪些版本已印刷,哪些版本尚未印刷。
  • TokenRecord :仅由可编程 NFT 使用。 代币记录帐户使我们能够将自定义数据附加到代币帐户而不是铸币帐户。
  • MetadataDelegate: 这些帐户用于存储给定元数据帐户的多个委托权限。
  • CollectionAuthorityRecord: 跟踪允许哪些权限设置和/或验证元数据帐户的集合。
  • UseAuthorityRecord: 跟踪允许哪些权限减少元数据帐户上的使用(可能很快就会弃用)字段。
  • TokenOwnedEscrow: 由 NFT 持有者管理的托管账户。
  • ReservationListV1 和 ReservationListV2(已弃用): 用于预订列表。 如果出现在列表中,你可以获得根据你在列表中的位置给出的版本号。

update_authority - 允许更改元数据帐户的地址。

mint - 铸币厂账户的地址。

data - 代币的资产数据。 这包括名称、符号、URI、创作者版税和创作者等值

primary_sale_happened - 指示代币是否已至少被出售一次。

is_mutable - 指示元数据帐户是否可以更改。

edition_nonce - 用于验证打印 NFT 的版本随机数的随机数。

token_standard - 元数据帐户持有的代币类型。 它是一个可选枚举,由以下变体组成:

  • NonFungible - 具有大师版的不可替代版本。
  • FungibleAsset(半同质)- 供应量 > 1 的 spl 代币,但具有 NFT 属性,例如图像和属性数组。
  • Fungible - 具有元数据和供应的代币 > 1。
  • ProgrammableNonFungible - 特殊类型的 NonFungible,始终冻结并强制执行自定义授权规则。
  • collection - 一个可选结构,包含 collectionNft 的公钥,如果不存在则为 None 。

use - 另一个使 NFT 可用的可选字段。 这意味着您可以加载一定数量的“使用”并使用它直到用完为止。有人建议尽快弃用它

collection_details - 可选字段,其中 V1 字段包含 NFT 集合的 NFT 数量。它已被弃用,可能很快就会被删除

programmable_config - 也是一个可选字段,如果设置,则包含可选规则集帐户的地址,该帐户包含与可编程不可替代性相关的约束。

废话不多说,让我们一起构建吧!!!!

在我们的 InitNFT 结构中,我们将把元数据添加到 spl 代币所需的帐户纳入范围。 这些是, metadata_account 用于存储我们的元数据, master_edition_account 用于设置主版 NFT。 我们还没有谈论主版帐户。 它为什么如此重要? 它证明了我们的代币的不可替代性。 它将检查铸币账户上的小数字段是否确实为零,以及供应字段是否设置为 1。它对于确定我们是否可以从 NFT 制作印刷版本也很有用。 我们最终将把 token_metadata_program 纳入范围,它负责处理创建帐户的指令并使用我们提供的字段正确实例化它们。

为了将上述段落写入代码,我们首先安装一个版本 mpl-token-metadata crate。

cargo add mpl-token-metadata@1.13.1


use anchor_spl::{
    associated_token::AssociatedToken,
    metadata::{Metadata}, // new
    token::{mint_to, Mint, MintTo, Token, TokenAccount},
};
use mpl_token_metadata::{
    pda::{find_master_edition_account, find_metadata_account}, // new
};

// snip

#[derive(Accounts)]
pub struct InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[account(mut, signer)]
    pub signer: AccountInfo<'info>,
    #[account(
        init,
        payer = signer,
        mint::decimals = 0,
        mint::authority = signer.key(),
        mint::freeze_authority = signer.key(),
    )]
    pub mint: Account<'info, Mint>,
    #[account(
        init_if_needed,
        payer = signer,
        associated_token::mint = mint,
        associated_token::authority = signer,
    )]
    pub associated_token_account: Account<'info, TokenAccount>,
    /// CHECK - address
    #[account(
        mut,
        address=find_metadata_account(&mint.key()).0,
    )]
    pub metadata_account: AccountInfo<'info>, // new
    /// CHECK: address
    #[account(
        mut,
        address=find_master_edition_account(&mint.key()).0,
    )]
    pub master_edition_account: AccountInfo<'info>, // new

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_metadata_program: Program<'info, Metadata>, // new
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

为了确保传入正确的帐户,我们使用 address="" 约束来确保传入的帐户确实分别是元数据帐户和主版本帐户。 正如我们之前所做的那样,非类型化帐户应该附有 rustdoc 注释 /// CHECK: <reason why the account is untyped>。 你可能想知道为什么我们不使用类型化元数据 Account<'info, MetadataAccount> 和主版 Account<'info, MasterEditionAccount> 帐户。 这是因为Anchor希望预先初始化这些帐户类型。

将所需的帐户和程序纳入范围后,我们现在需要实例化它们。 带有元数据箱的anchor-spl附带了我们可以使用的大量有用的跨程序调用(CPI)指令。

正如我们对 mint 帐户所做的那样,我们将使用 CpiContext::new() 方法来帮助我们确保在对 metadata_program 进行 CPI 调用时拥有所有必需的帐户。

我们将使用 create_metadata_accounts_v3() 创建元数据帐户。 它需要五个参数,

  • 我们的 CpiContext 结构体包含所需的程序 id 和帐户
  • 我们的资产数据恰当地命名为DataV2。 name、symbol、uri...都在这里定义。
  • is_mutable,一个布尔值,决定我们是否可以更改我们的metadata_account。
  • update_authority_is_signer,一个布尔值,表示更新权限是否将成为创建此交易的签名者。
  • 集合详细信息,可选字段包含否。 我们收藏的 NFT 数量。
// snip
use anchor_spl::{
associated_token::AssociatedToken,
metadata::{
create_master_edition_v3, create_metadata_accounts_v3, CreateMasterEditionV3,
CreateMetadataAccountsV3, Metadata, MetadataAccount,
}, // new
token::{mint_to, Mint, MintTo, Token, TokenAccount},
};
use mpl_token_metadata::{
pda::{find_master_edition_account, find_metadata_account},
state::DataV2 // new
};

// snip

pub fn init_nft(
ctx: Context<InitNFT>,
name: String,   // new
symbol: String, // new
uri: String,    // new
) -> Result<()> {
// create mint account
let cpi_context = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.associated_token_account.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
},
);

mint_to(cpi_context, 1)?;

// create metadata account
let cpi_context = CpiContext::new(
    ctx.accounts.token_metadata_program.to_account_info(),
    CreateMetadataAccountsV3 {
        metadata: ctx.accounts.metadata_account.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        mint_authority: ctx.accounts.signer.to_account_info(),
        update_authority: ctx.accounts.signer.to_account_info(),
        payer: ctx.accounts.signer.to_account_info(),
        system_program: ctx.accounts.system_program.to_account_info(),
        rent: ctx.accounts.rent.to_account_info(),
    },
);

let data_v2 = DataV2 {
    name: name,
    symbol: symbol,
    uri: uri,
    seller_fee_basis_points: 0,
    creators: None,
    collection: None,
    uses: None,
};

create_metadata_accounts_v3(cpi_context, data_v2, false, true, None)?;

Ok(())

}

由于 DataV2 结构需要接收资产数据,因此我们更改了 init_nft 函数的定义,以包含我们作为资产数据传递的名称、符号和 uri 参数。 这也与我们获取 DataV2 结构中其他字段的方式相同,例如 seller_fee_basis_points、创建者(如果有多个)……等等。

最后,我们通过调用 create_master_edition_v3 指令创建主版帐户来完成我们的程序,该指令接收初始化主版帐户所需的帐户和可选的 max_supply 参数。 max_supply 获取该 NFT 可以打印的最大版本数。 我们不想让我们的 NFT 制作印刷版,因此我们将其设置为“无”。

use anchor_spl::{
    associated_token::AssociatedToken,
    metadata::{
        create_master_edition_v3, create_metadata_accounts_v3, CreateMasterEditionV3,
        CreateMetadataAccountsV3, Metadata,
    }, // new
    token::{mint_to, Mint, MintTo, Token, TokenAccount},
};

// snip

//create master edition account
let cpi_context = CpiContext::new(
    ctx.accounts.token_metadata_program.to_account_info(),
    CreateMasterEditionV3 {
        edition: ctx.accounts.master_edition_account.to_account_info(),
        mint: ctx.accounts.mint.to_account_info(),
        update_authority: ctx.accounts.signer.to_account_info(),
        mint_authority: ctx.accounts.signer.to_account_info(),
        payer: ctx.accounts.signer.to_account_info(),
        metadata: ctx.accounts.metadata_account.to_account_info(),
        token_program: ctx.accounts.token_program.to_account_info(),
        system_program: ctx.accounts.system_program.to_account_info(),
        rent: ctx.accounts.rent.to_account_info(),
    },
);

create_master_edition_v3(cpi_context, None)?;

在大约 115行代码中,我们编写了一个程序,为我们在链上铸造 NFT。 这是完整的完成程序的样子。

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    metadata::{
        create_master_edition_v3, create_metadata_accounts_v3, CreateMasterEditionV3,
        CreateMetadataAccountsV3, Metadata,
    },
    token::{mint_to, Mint, MintTo, Token, TokenAccount},
};
use mpl_token_metadata::{
    pda::{find_master_edition_account, find_metadata_account},
    state::DataV2,
};

declare_id!("9TEtkW972r8AVyRmQzgyMz8GpG7WJxJ2ZUVZnjFNJgWM"); // shouldn't be similar to mine

#[program]
pub mod solana_nft_anchor {

    use super::*;

    pub fn init_nft(
        ctx: Context<InitNFT>,
        name: String,
        symbol: String,
        uri: String,
    ) -> Result<()> {
        // create mint account
        let cpi_context = CpiContext::new(
            ctx.accounts.token_program.to_account_info(),
            MintTo {
                mint: ctx.accounts.mint.to_account_info(),
                to: ctx.accounts.associated_token_account.to_account_info(),
                authority: ctx.accounts.signer.to_account_info(),
            },
        );

        mint_to(cpi_context, 1)?;

        // create metadata account
        let cpi_context = CpiContext::new(
            ctx.accounts.token_metadata_program.to_account_info(),
            CreateMetadataAccountsV3 {
                metadata: ctx.accounts.metadata_account.to_account_info(),
                mint: ctx.accounts.mint.to_account_info(),
                mint_authority: ctx.accounts.signer.to_account_info(),
                update_authority: ctx.accounts.signer.to_account_info(),
                payer: ctx.accounts.signer.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
                rent: ctx.accounts.rent.to_account_info(),
            },
        );

        let data_v2 = DataV2 {
            name: name,
            symbol: symbol,
            uri: uri,
            seller_fee_basis_points: 0,
            creators: None,
            collection: None,
            uses: None,
        };

        create_metadata_accounts_v3(cpi_context, data_v2, false, true, None)?;

        //create master edition account
        let cpi_context = CpiContext::new(
            ctx.accounts.token_metadata_program.to_account_info(),
            CreateMasterEditionV3 {
                edition: ctx.accounts.master_edition_account.to_account_info(),
                mint: ctx.accounts.mint.to_account_info(),
                update_authority: ctx.accounts.signer.to_account_info(),
                mint_authority: ctx.accounts.signer.to_account_info(),
                payer: ctx.accounts.signer.to_account_info(),
                metadata: ctx.accounts.metadata_account.to_account_info(),
                token_program: ctx.accounts.token_program.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
                rent: ctx.accounts.rent.to_account_info(),
            },
        );

        create_master_edition_v3(cpi_context, None)?;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[account(mut, signer)]
    pub signer: AccountInfo<'info>,
    #[account(
        init,
        payer = signer,
        mint::decimals = 0,
        mint::authority = signer.key(),
        mint::freeze_authority = signer.key(),
    )]
    pub mint: Account<'info, Mint>,
    #[account(
        init_if_needed,
        payer = signer,
        associated_token::mint = mint,
        associated_token::authority = signer
    )]
    pub associated_token_account: Account<'info, TokenAccount>,
    /// CHECK - address
    #[account(
        mut,
        address=find_metadata_account(&mint.key()).0,
    )]
    pub metadata_account: AccountInfo<'info>,
    /// CHECK: address
    #[account(
        mut,
        address=find_master_edition_account(&mint.key()).0,
    )]
    pub master_edition_account: AccountInfo<'info>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_metadata_program: Program<'info, Metadata>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

为了完成程序方面的开发,我们将构建和部署我们的程序。

让我们首先配置 Anchor.toml,以便在运行部署命令时部署到 Devnet。

为此,我们将集群点更改为 devnet:

# snip

[provider]
cluster = "devnet"
# snip

我们现在已准备好部署到 devnet。 继续构建你的程序并使用以下命令部署它。

anchor build

anchor deploy

如果命令相应地运行,你应该会收到部署成功消息。

4、从 TS 客户端调用我们的程序

对于我们的客户,我们将使用 Metaplex 的 umi。 Umi 是“适用于 JavaScript 客户端的 Solana 框架”。

让我们安装我们将要使用的软件包。 Umi 将帮助我们导出元数据帐户和主版本的 PDA,而 spl 代币将帮助我们导出关联的代币帐户。

yarn add @solana/spl-token @metaplex-foundation/mpl-token-metadata @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/umi-signer-wallet-adapters

安装完成后,我们将继续使用 createUmi 函数创建一个新的 Umi 实例,并使用 Umi 接口的代币元数据程序注册我们的本地提供商钱包。

// snip

const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.SolanaNftAnchor as Program<SolanaNftAnchor>;

const signer = provider.wallet;

const umi = createUmi("https://api.devnet.solana.com")
	.use(walletAdapterIdentity(signer))
	.use(mplTokenMetadata());

//snip

设置好配置后,让我们开始调用 init_nft 方法。 我们需要派生关联的代币帐户、元数据帐户和主版本。 通过我们刚刚安装的软件包中的辅助功能,操作变得更加容易。

注意:@solana/web3.js PublicKey 接口与 umi 的 publicKey 接口不兼容,因此如果 umi 函数需要它作为输入,请务必用它包装公钥值。

// generate the mint account
const mint = anchor.web3.Keypair.generate();

// Derive the associated token address account for the mint
const associatedTokenAccount = await getAssociatedTokenAddress(
	mint.publicKey,
	signer.publicKey
);

// derive the metadata account
let metadataAccount = findMetadataPda(umi, {
	mint: publicKey(mint.publicKey),
})[0];

//derive the master edition pda
let masterEditionAccount = findMasterEditionPda(umi, {
	mint: publicKey(mint.publicKey),
})[0];

一旦我们导出了公钥,我们还需要资产数据,即名称、符号和 uri。 我们不会在本教程中进行元数据上传,而是在稍后的单独的 UMI 指南中进行。

现在我们将利用我在上一个教程中使用的数据。

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { SolanaNftAnchor } from "../target/types/solana_nft_anchor";
import { walletAdapterIdentity } from "@metaplex-foundation/umi-signer-wallet-adapters";
import { getAssociatedTokenAddress } from "@solana/spl-token";
import {
	findMasterEditionPda,
	findMetadataPda,
	mplTokenMetadata,
	MPL_TOKEN_METADATA_PROGRAM_ID,
} from "@metaplex-foundation/mpl-token-metadata";
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { publicKey } from "@metaplex-foundation/umi";

import {
	TOKEN_PROGRAM_ID,
	ASSOCIATED_TOKEN_PROGRAM_ID,
} from "@solana/spl-token";

describe("solana-nft-anchor", async () => {
	// Configured the client to use the devnet cluster.
	const provider = anchor.AnchorProvider.env();
	anchor.setProvider(provider);
	const program = anchor.workspace
		.SolanaNftAnchor as Program<SolanaNftAnchor>;

	const signer = provider.wallet;

	const umi = createUmi("https://api.devnet.solana.com")
		.use(walletAdapterIdentity(signer))
		.use(mplTokenMetadata());

	const mint = anchor.web3.Keypair.generate();

	// Derive the associated token address account for the mint
	const associatedTokenAccount = await getAssociatedTokenAddress(
		mint.publicKey,
		signer.publicKey
	);

	// derive the metadata account
	let metadataAccount = findMetadataPda(umi, {
		mint: publicKey(mint.publicKey),
	})[0];

	//derive the master edition pda
	let masterEditionAccount = findMasterEditionPda(umi, {
		mint: publicKey(mint.publicKey),
	})[0];

	const metadata = {
		name: "Kobeni",
		symbol: "kBN",
		uri: "https://raw.githubusercontent.com/687c/solana-nft-native-client/main/metadata.json",
	};

	it("mints nft!", async () => {
		const tx = await program.methods
			.initNft(metadata.name, metadata.symbol, metadata.uri)
			.accounts({
				signer: provider.publicKey,
				mint: mint.publicKey,
				associatedTokenAccount,
				metadataAccount,
				masterEditionAccount,
				tokenProgram: TOKEN_PROGRAM_ID,
				associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
				tokenMetadataProgram: MPL_TOKEN_METADATA_PROGRAM_ID,
				systemProgram: anchor.web3.SystemProgram.programId,
				rent: anchor.web3.SYSVAR_RENT_PUBKEY,
			})
			.signers([mint])
			.rpc();

		console.log(
			`mint nft tx: https://explorer.solana.com/tx/${tx}?cluster=devnet`
		);
		console.log(
			`minted nft: https://explorer.solana.com/address/${mint.publicKey}?cluster=devnet`
		);
	});
});

让我们继续运行测试来铸造我们的 NFT。

anchor test

运行 anchor test将在运行 ts 客户端之前首先构建和部署你的程序。

要跳过此过程,你可以添加 --skip-build--skip-deploy 标志。 我更喜欢使用它,因为我已经在前一阶段构建并部署了该程序。

anchor test --skip-build --skip-deploy

运行上述命令后,你应该会看到新创建的 NFT tx。单击浏览器的交易链接,在代币余额下,你应该看到 +1 的变化。

要查看铸造的 NFT,请打开铸造的 NFT 链接,你应该会看到与此类似的内容。


原文链接:How to Mint Solana NFTs Using Anchor and Metaplex

DefiPlot翻译整理,转载请标明出处

通过 NowPayments 打赏