如何使用 Anchor 创建和铸造可替代的 SPL 代币

  • QuickNode
  • 发布于 2024-12-16 17:34
  • 阅读 23

本文详细介绍了如何使用Anchor在Solana上创建和铸造可互换的SPL代币,包括项目初始化、代币创建、铸造代币以及测试的步骤。

概述

你是否曾考虑过在用户完成任务时奖励他们一个代币?智能合约允许你创建管理代币分发和使用规则的规范。本指南将教你如何使用 Anchor 在 Solana 上创建和铸造可替代的 SPL 代币。我们将涵盖程序和测试所需的代码,以确保代币在账户之间无缝转移。

你将要做的事情

  • 使用 Anchor 和 Solana Playground 创建 Solana 程序
  • 创建程序指令以创建带有元数据的新 SPL 代币
  • 创建程序指令,以铸造代币到指定账户
  • 编写测试以验证和执行这些指令

你将需要的东西

本指南中使用的依赖

依赖 版本
anchor-lang 0.29.0
anchor-spl 0.29.0
solana-program 1.16.24
spl-token 4.0.0

启动你的项目

通过访问 https://beta.solpg.io/ 在 Solana Playground 中创建一个新项目。Solana Playground 是一个基于浏览器的 Solana 代码编辑器,将使我们能够快速启动此项目。欢迎在自己的代码编辑器中进行操作,但本指南将针对 Solana Playground 的步骤进行调整。首先,点击“创建新项目”:

创建新项目

输入项目名称“token-minter”,并选择“Anchor (Rust)”:

命名项目

设置你的工作区

使用你的 QuickNode 端点连接到 Solana 集群

要在 Solana 上进行构建,你需要一个 API 端点以连接到网络。你可以使用公共节点或部署和管理自己的基础设施;不过,如果你希望获得更快的响应时间,采用我们的服务会省事很多。

QuickNode 现在接受 Solana 付款 🚀

你现在可以 使用 USDC 在 Solana 上支付 QuickNode 计划。作为首个接受 Solana 付款的多链服务提供商,我们正在简化开发者的流程——无论你是创建新账户还是管理现有账户。在这里了解有关使用 Solana 付款的更多信息

看看为什么超过 50% 的 Solana 项目选择 QuickNode,并在 这里 注册一个免费账户。我们将使用一个 Solana Devnet 端点。

拷贝 HTTP Provider 链接:

现在你有了一个端点,返回到 Solana Playground,点击浏览器窗口左下角的设置齿轮 (⚙️) 按钮。你将看到一个“端点”的下拉菜单。打开下拉菜单并选择“自定义”:

端点窗口

将你的 QuickNode 端点粘贴到文本框中并点击“添加”:

粘贴 QuickNode 端点

创建并连接一个钱包

由于这个项目仅用于演示目的,我们可以使用一个“临时”钱包。Solana Playground 使创建一个钱包变得容易。你应该在浏览器窗口左下角看到一个红点“未连接”。点击它:

钱包设置按钮

Solana Playground 将为你生成一个钱包(或者你也可以导入自己的)。可以随意保存以备后用,在准备好时点击继续。一个新钱包将被初始化并连接到 Solana devnet。Solana Playground 会自动向你的新钱包空投一些 SOL,但我们将请求额外一些以确保我们有足够的资金来部署我们的程序。在浏览器终端,你可以使用 Solana CLI 命令。输入 solana airdrop 1 向你的钱包空投 1 SOL。注意:由于 SOL 空投的限制,可能需要在不同的时间多次运行此命令。你也可以从 QuickNode Faucet 领取额外的 SOL。

你的钱包现在应该连接到 devnet,余额大约为 8 SOL(你可能需要从另一个地址发送一些额外的 devnet SOL,以确保你有足够的 SOL 部署到 devnet):

准备好了

你准备好了!让我们开始构建吧!

创建铸造器程序

让我们开始打开 lib.rs 并删除初始代码。一旦你有了一个空白界面,我们就可以开始构建我们的程序。首先,让我们导入一些依赖并构建我们的程序框架。将以下内容添加到文件顶部:

// 1. 导入依赖
use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token::{mint_to, Mint, MintTo, Token, TokenAccount},
    metadata::{
        create_metadata_accounts_v3,
        mpl_token_metadata::types::DataV2,
        CreateMetadataAccountsV3,
        Metadata as Metaplex,
    },
};

// 2. 声明程序 ID(SolPG 将在你部署时自动更新)
declare_id!("11111111111111111111111111111111");

// 3. 定义程序和指令
#[program]
mod token_minter {
    use super::*;
    pub fn init_token(ctx: Context<InitToken>, metadata: InitTokenParams) -> Result<()> {
        // TODO 添加初始化铸币逻辑
        Ok(())
    }

    pub fn mint_tokens(ctx: Context<MintTokens>, quantity: u64) -> Result<()> {
        // TODO 添加铸造代币逻辑
        Ok(())
    }
}

// 4. 定义每个指令的上下文
#[derive(Accounts)]
#[instruction(params: InitTokenParams)]
pub struct InitToken<'info> {
  //TODO: 添加初始化账户上下文
}

#[derive(Accounts)]
pub struct MintTokens<'info> {
  //TODO: 添加铸造代币的账户上下文
}

// 5. 定义初始化代币的参数
#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)]
pub struct InitTokenParams {
    pub name: String,
    pub symbol: String,
    pub uri: String,
    pub decimals: u8,
}

让我们解析一下这里发生的事情:

  1. 我们导入我们程序所需的依赖。我们将使用 Anchor 框架、SPL 代币程序和 Metaplex 代币元数据程序(通过 anchor-spl)。
  2. 我们声明程序 ID。这个 ID 将用于识别我们在 Solana 上的程序。现在它只是一个占位符——Solana Playground 将在我们部署时自动更新它。
  3. 我们定义我们程序和指令的轮廓。我们将创建两个指令:init_tokenmint_tokensinit_token 指令将使用元数据初始化一个新的 SPL 代币,而 mint_tokens 指令将向指定账户铸造代币。
  4. 我们定义每个指令的上下文。上下文包含将用于每个指令的账户。我们将在以下步骤定义这些账户。
  5. 我们为 init_token 指令的参数创建一个结构体。这个结构体包含在 Solana 上创建代币所需的元数据(代币的名称、符号、URI 和小数)。我们使用 AnchorSerializeAnchorDeserialize 属性允许这个结构体被 Anchor 序列化和反序列化。在我们的 init_token 指令中,你会注意到我们将这个结构体作为参数传递。

创建初始化代币上下文和指令

现在我们有了程序的轮廓,我们可以开始填充细节。让我们先从 init_token 指令开始。我们的指令需要一些账户才能执行:

  • 代币的新元数据账户
  • 代币的新铸币账户(我们将其创建为由我们的程序拥有的 PDA)
  • 将为交易和租金豁免支付的支付者账户
  • 用于确定新账户的租金豁免的 Sysvar Rent 账户
  • 我们将要交互的程序:
    • Solana 系统程序
    • SPL 代币程序
    • Metaplex 代币元数据程序

让我们将这些添加到 InitToken 结构体中:

#[derive(Accounts)]
#[instruction(\
    params: InitTokenParams\
)]
pub struct InitToken<'info> {
    /// CHECK: 正在创建的新 Metaplex 账户
    #[account(mut)]
    pub metadata: UncheckedAccount<'info>,
    #[account(\
        init,\
        seeds = [b"mint"],\
        bump,\
        payer = payer,\
        mint::decimals = params.decimals,\
        mint::authority = mint,\
    )]
    pub mint: Account<'info, Mint>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub rent: Sysvar<'info, Rent>,
    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
    pub token_metadata_program: Program<'info, Metaplex>,
}

这里有几点需要注意:

  • Metaplex 代币元数据程序将创建 metadata 账户。我们将使用 UncheckedAccount 类型以表示另一个程序将创建此账户。
  • mint 账户将由我们的程序创建,使用 init。我们将通过 "mint" 这个词为账户提供种子,这意味着每个程序只能创建一个铸币账户(可以根据你的需求使用不同的种子)。我们还将指定支付者、铸币的权限(我们的程序)和铸币的小数(来自我们的指令参数)。你会注意到我们将权限设置为 mint 账户本身。这实际上是将权限授予我们的程序,而不必创建另一个 PDA。

现在,让我们在指令中使用这些账户。将你的 init_token 指令更新为:

    pub fn init_token(ctx: Context<InitToken>, metadata: InitTokenParams) -> Result<()> {
        let seeds = &["mint".as_bytes(), &[ctx.bumps.mint]];
        let signer = [&seeds[..]];

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

        let metadata_ctx = CpiContext::new_with_signer(
            ctx.accounts.token_metadata_program.to_account_info(),
            CreateMetadataAccountsV3 {
                payer: ctx.accounts.payer.to_account_info(),
                update_authority: ctx.accounts.mint.to_account_info(),
                mint: ctx.accounts.mint.to_account_info(),
                metadata: ctx.accounts.metadata.to_account_info(),
                mint_authority: ctx.accounts.mint.to_account_info(),
                system_program: ctx.accounts.system_program.to_account_info(),
                rent: ctx.accounts.rent.to_account_info(),
            },
            &signer
        );

        create_metadata_accounts_v3(
            metadata_ctx,
            token_data,
            false,
            true,
            None,
        )?;

        msg!("代币铸造成功。");

        Ok(())
    }

我们的指令在做以下事情:

  1. 首先,我们声明我们的签名者。我们使用在 InitToken 结构体中定义的 seeds,并通过 ctx.bumps.mint 获取我们的 bump 来创建签名者。
  2. 接下来,我们将我们的 token_data 定义为 DataV2 对象。
  3. 最后,我们使用 CreateMetadataAccountsV3 创建 CPI 上下文,并将其传递给 Metaplex 代币元数据程序中的 create_metadata_accounts_v3 指令。这个指令将为我们的代币创建元数据账户。我们将必要的数据和我们的 CPI 上下文作为参数传递。 _注意:由于我们在 mint 账户中使用了 init,我们不需要从 SPL 代币程序中调用 create_mint 指令。这将在后台由 Anchor 自动处理。_

创建铸造代币的上下文和指令

要铸造代币,我们需要一组类似但略有不同的账户:

  • 代币的铸币账户(应与我们在 init_token 指令中创建的同一铸币账户匹配)
  • 新的代币账户,用于铸造的代币(由接收者或在本例中为支付者拥有)
  • 将为交易和租金豁免支付的支付者账户
  • 用于确定新账户的租金豁免的 Sysvar Rent 账户
  • 我们将要交互的程序:
    • Solana 系统程序
    • SPL 代币程序
    • 关联代币程序

将以下内容添加到你的 MintTokens 结构体:

#[derive(Accounts)]
pub struct MintTokens<'info> {
    #[account(\
        mut,\
        seeds = [b"mint"],\
        bump,\
        mint::authority = mint,\
    )]
    pub mint: Account<'info, Mint>,
    #[account(\
        init_if_needed,\
        payer = payer,\
        associated_token::mint = mint,\
        associated_token::authority = payer,\
    )]
    pub destination: Account<'info, TokenAccount>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub rent: Sysvar<'info, Rent>,
    pub system_program: Program<'info, System>,
    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
}

有几点重要的地方需要注意:

  1. 我们将 mint 设置为 mut 以指示我们将修改账户(在这种情况下是铸造新代币/增加供应)。
  2. 我们为 destination 账户使用 init_if_needed 属性。这将在账户不存在时创建该账户。我们确保代币的铸金和权限分别设置为 mint 账户和 payer 账户。 注意:我们在这个指令中不需要元数据账户或代币元数据程序,因为我们不创建或使用代币的元数据。

最后,你将需要定义 mint_tokens 指令。添加以下代码:

    pub fn mint_tokens(ctx: Context<MintTokens>, quantity: u64) -> Result<()> {
        let seeds = &["mint".as_bytes(), &[ctx.bumps.mint]];
        let signer = [&seeds[..]];

        mint_to(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                MintTo {
                    authority: ctx.accounts.mint.to_account_info(),
                    to: ctx.accounts.destination.to_account_info(),
                    mint: ctx.accounts.mint.to_account_info(),
                },
                &signer,
            ),
            quantity,
        )?;

        Ok(())
    }

在这里,我们使用来自上一步的相同签名者 PDA,并将其传递给 SPL 代币程序中的 mint_to 指令。这个指令将指定数量的代币铸造到目标账户。如果你还记得我们之前的 SPL 代币工作,这个指令期望 quantity 是扩展的小数值。例如,如果代币有两个小数,而我们想要铸造 100 个代币,我们将传递 10,000 作为 quantity 值。

继续构建你的程序,以确保一切正常,通过点击“构建”或在 Solana Playground 终端中输入 anchor build

如果程序成功构建,你可以将其部署到 Solana devnet。如果有任何问题,仔细检查你在上面的指南中的代码或查看我们提供的工作示例。有问题或需要帮助吗?随时在 Discord 上与我们联系。

部署程序

点击页面左侧的工具图标 🛠,然后点击“部署”:

部署

这可能需要一两分钟,但完成后,你应该在浏览器终端看到类似这样的信息:

成功

干得好!让我们测试一下。

测试程序

返回到你修改 lib.rs 文件的主窗口,点击页面左上角的 📑 图标。打开 anchor.test.ts 并用以下内容替换:

describe("测试铸造器", () => {
    // Metaplex 常量
    const METADATA_SEED = "metadata";
    const TOKEN_METADATA_PROGRAM_ID = new web3.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");

    // 我们程序的常量
    const MINT_SEED = "mint";

    // 我们测试的数据
    const payer = pg.wallet.publicKey;
    const metadata = {
      name: "一个测试代币",
      symbol: "TEST",
      uri: "https://5vfxc4tr6xoy23qefqbj4qx2adzkzapneebanhcalf7myvn5gzja.arweave.net/7UtxcnH13Y1uBCwCnkL6APKsge0hAgacQFl-zFW9NlI",
      decimals: 9,
    };
    const mintAmount = 10;
    const [mint] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from(MINT_SEED)],
      pg.PROGRAM_ID
    );

    const [metadataAddress] = web3.PublicKey.findProgramAddressSync(
      [\
        Buffer.from(METADATA_SEED),\
        TOKEN_METADATA_PROGRAM_ID.toBuffer(),\
        mint.toBuffer(),\
      ],
      TOKEN_METADATA_PROGRAM_ID
    );

    // 测试初始化代币
    it("初始化", async () => {

    });

    // 测试铸造代币
    it("铸造代币", async () => {

    });

  });

简化调试的日志

你现在可以访问你的 RPC 端点的日志,帮助你更有效地排查问题。如果你遇到 RPC 调用问题,只需在你的 QuickNode 控制面板中查看日志以快速识别和解决问题。了解更多关于日志历史限制的内容,查看我们的定价页面

在这里,我们创建了测试套件并定义了一些我们将在测试中使用的常量。我们将使用几个种子进行 PDA 挂址推导,Metaplex 代币元数据程序 ID,支付者的公钥,代币的元数据(可以自由使用你自己的),以及我们想要铸造的令牌数量。我们还将定义一个辅助函数来确认交易。

最后,我们推导出两个我们将在测试中使用的 PDA:

  • 代币的 mint PDA。我们使用 MINT_SEED 和程序 ID 在 findProgramAddressSync 方法中推导出它。
  • 将存储我们的代币元数据的 metadataAddress。我们以相同的方式推导它,但这次我们将 METADATA_SEED、Metaplex 代币元数据程序 ID 和 mint PDA 传递给该方法。(来源:Metaplex 文档

测试初始化代币

让我们编写一个初始化测试以创建一个铸币账户(如果尚不存在)。在 it("initialize"...) 块中添加以下测试:

    it("初始化", async () => {

      const info = await pg.connection.getAccountInfo(mint);
      if (info) {
        return; // 如果已初始化,则不尝试再次初始化
      }
      console.log("  找不到铸币账户。尝试进行初始化。");

      const context = {
        metadata: metadataAddress,
        mint,
        payer,
        rent: web3.SYSVAR_RENT_PUBKEY,
        systemProgram: web3.SystemProgram.programId,
        tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
        tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
      };

      const tx = await pg.program.methods
        .initToken(metadata)
        .accounts(context)
        .transaction();

      const txHash = await web3.sendAndConfirmTransaction(pg.connection, tx, [pg.wallet.keypair], {skipPreflight: true});
      console.log(`  https://explorer.solana.com/tx/${txHash}?cluster=devnet`);
      const newInfo = await pg.connection.getAccountInfo(mint);
      assert(newInfo, "  应该初始化铸币账户。");
    });

在这个 initialize 测试中发生了以下事情:

  1. 首先,我们检查铸币账户是否已经存在。如果存在,则不尝试再次初始化,并跳到下一个测试。
  2. 如果铸币账户不存在,我们尝试初始化。
  3. 我们创建一个上下文对象,其中包含我们在指令中将使用的账户。相应的账户应与我们程序中创建的上下文匹配。
  4. 我们执行 init_token(在 TypeScript 中为 initToken)指令,传递元数据和上下文作为参数。rpc() 方法将我们的交易发送到集群,并返回交易哈希。
  5. 最后,我们记录交易 URL 并检查铸币账户是否存在。

测试铸造代币

让我们编写第二个测试以确保 mint_tokens 指令按预期工作。将你的 it("mint tokens"...) 测试块替换为以下代码:

  it("铸造代币", async () => {

    const destination = await anchor.utils.token.associatedAddress({
      mint: mint,
      owner: payer,
    });

    let initialBalance: number;
    try {
      const balance = (await pg.connection.getTokenAccountBalance(destination))
      initialBalance = balance.value.uiAmount;
    } catch {
      // 代币账户尚未初始化,余额为0
      initialBalance = 0;
    }

    const context = {
      mint,
      destination,
      payer,
      rent: web3.SYSVAR_RENT_PUBKEY,
      systemProgram: web3.SystemProgram.programId,
      tokenProgram: anchor.utils.token.TOKEN_PROGRAM_ID,
      associatedTokenProgram: anchor.utils.token.ASSOCIATED_PROGRAM_ID,
    };

    const tx = await pg.program.methods
      .mintTokens(new BN(mintAmount * 10 ** metadata.decimals))
      .accounts(context)
      .transction();
    const txHash = await web3.sendAndConfirmTransaction(pg.connection, tx, [pg.wallet.keypair], {skipPreflight: true});
    console.log(`  https://explorer.solana.com/tx/${txHash}?cluster=devnet`);

    const postBalance = (
      await pg.connection.getTokenAccountBalance(destination)
    ).value.uiAmount;
    assert.equal(
      initialBalance + mintAmount,
      postBalance,
      "最终余额应等于初始余额加铸造数量"
    );
  });

在这个 mint tokens 测试中发生的事情:

  • 首先,我们通过将 mintpayer 公钥传递给 associatedAddress 方法推导出支付者的目标代币账户。
  • 检查目标账户的初始余额(如果不存在则设为 0)
  • 创建一个上下文对象,其中包含我们在指令中将使用的账户。相应的账户应与我们程序中创建的上下文匹配。
  • 执行 mint_tokens 指令(在 TypeScript 中为 mintTokens),将 mintAmount 作为参数传递。rpc() 方法将我们的交易发送到集群,并返回交易哈希。
  • 检查目标账户的最终余额是否等于初始余额加铸造数量。

运行测试

完成测试后,你可以通过点击“🧪 测试”按钮或在 Solana Playground 终端中输入 anchor test 来运行它们。你应该会看到类似如下的信息:

正在运行测试...
  anchor.test.ts:
  测试铸造器
      找不到铸币账户。尝试进行初始化。
      https://explorer.solana.com/tx/4YEzstg3UxWBqfFBcTwjWvg4bdkXw47zLCieBGcr1WNPoZDNZrQxD5H6YL6fSxix3SumYGTYBPFY2vnYxYRAG8J6?cluster=devnet
    ✔ 初始化 (797ms)
      https://explorer.solana.com/tx/2Ws647Z4q5Lsdcm6zSqHqTRpqQZY8nuQaS5Z7fqbw925jjXoztqqydQZsyMA21yF3PecLzVLt936NMo1qfXHoQe1?cluster=devnet
    ✔ 铸造代币 (722ms)
  2 通过 (2s)

干得好!你现在有一个可以创建和铸造代币的工作程序。

你可以重新运行测试函数以铸造额外的代币——由于我们添加了检查铸币账户是否已存在的机制,它将不会尝试再次初始化。或者,你可以点击左侧的“🧪” (测试) 图标,使用 Solana Playground 的 UI 测试铸造指令。

测试

该 UI 可以推导出测试程序所需的所有 PDA 和账户地址。确保在铸造代币时考虑小数(根据代币的小数位数在数量末尾添加额外的零)。

总结

你现在有了一个可以创建和铸造代币的工作程序——这可以应用于任何类型的 Solana 项目:例如,DeFi 用户的奖励、NFT 或游戏物品。继续构建!

如果你遇到了困难,或者有问题,或者只想讨论你正在构建的内容,请在 DiscordTwitter 与我们联系!

我们❤️反馈!

请告诉我们 如果你有任何反馈或新的主题要求。我们很乐意听取你的意见。

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。