如何使用 Anchor 创建和使用 Solana 代币扩展

如何使用 Anchor 创建和使用 Solana 代币扩展

概述

在 2024 年 4 月,Anchor 发布了版本 0.30.0,该版本包括多个重要更改和开发者改进。其中一个显著的变化是为 代币扩展(Token 2022) 引入了代币账户约束。本指南将带你了解这对你的 Solana 项目的意义,我们将通过 Anchor 的示例程序来创建和验证使用代币扩展的代币。

你将要做的事项

  • 了解 Anchor 中的新代币约束
  • 审查并重建 一个示例程序,该程序创建和验证使用代币扩展的代币

需要你具备

本指南使用的依赖项

依赖项 版本
solana-cli 1.18.8
anchor-cli 0.30.0
anchor-lang 0.30.0
anchor-spl 0.30.0

代币扩展回顾

代币扩展(也称为 Token-2022)是在 Solana 上的一个高级代币程序,扩展了原始 SPL 代币程序的功能。它旨在为开发者提供更大的灵活性和附加功能,而不影响现有代币的安全性。提供了多种扩展,包括元数据、转账费用、转账钩子等。在继续之前,请先了解 代币扩展 及其工作原理。

Anchor 中的代币扩展

从 Anchor 0.30.0 开始,你现在可以使用 Anchor 约束 创建和验证使用代币扩展的代币。

扩展 约束
group_pointer authority
group_address
group_member_pointer authority
member_address
metadata_pointer authority
metadata_address
close_authority authority
permanent_delegate delegate
transfer_hook authority
program_id

扩展约束在你的账户上下文结构中使用,格式为 extension 后跟 :: 和扩展名称,再后跟 :: 和约束名称。例如:

    extension::group_pointer::authority = YOUR_AUTH.key()

在创建新代币时,可以使用 init 约束;在验证现有代币时,则可以不使用 init 约束。

Anchor 发布了 一个示例程序,演示如何使用新约束创建和验证代币。让我们创建一个新的 Anchor 项目并添加示例程序,以看看它是如何工作的。

使用 Anchor 创建代币扩展程序

初始化一个新的 Anchor 项目

在开始之前: 请确保安装了 Anchor v.0.30.0 或更高版本。旧版本的 Anchor 无法与新约束一起使用。你可以通过运行 anchor --version 来检查你的 Anchor 版本。如果你已安装 Anchor 版本管理器,可以通过运行 avm update 升级你的 Anchor 版本。

打开终端并运行以下命令初始化一个新的 Anchor 项目:

anchor init token-extensions

这将创建一个名为 token-extensions 的新项目目录,其中包含启动所需的文件和目录。切换到新目录:

cd token-extensions

更新 Cargo.toml

首先,让我们导入所需的依赖项。打开你 programs 目录中的 Cargo.toml 文件(programs/token-extensions/Cargo.toml),并添加以下依赖项:

[dependencies]
anchor-lang = { version = "0.30.0", features = ["init-if-needed"] }
anchor-spl = "0.30.0"
spl-tlv-account-resolution = "0.6.3"
spl-transfer-hook-interface = "0.6.3"
spl-type-length-value = "0.4.3"
spl-pod = "0.2.2"

这里有一些新的依赖项,我们将需要它们来与代币扩展程序进行交互:

接下来,让我们更新我们的 [features]。我们需要关注 Anchor 0.30.0 中的新功能:IDL(接口定义语言)构建。现在在你的程序的 Cargo.toml 定义中需要包含 "idl-build" 功能,以使 IDL 生成正常工作,并且所有生成 IDL 的 crate 都必须要求此功能。在我们的例子中,我们必须在 anchor-spl 依赖项中包含 idl-build 功能。将 Cargo.toml 文件中的 idl-build 功能更新为包含 anchor-spl

    idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]

太好了!现在,我们可以继续下一步。

添加代币扩展工具

在你的程序 src 目录中创建一个新文件 utils.rstoken-extensions/programs/token-extensions/src/utils.rs)。此文件将包含我们用于与代币扩展程序交互的实用函数。将以下导入添加到 utils.rs 文件中:

use anchor_lang::{
    prelude::Result,
    solana_program::{
        account_info::AccountInfo,
        program::invoke,
        pubkey::Pubkey,
        rent::Rent,
        system_instruction::transfer,
        sysvar::Sysvar,
    },
    Lamports,
};
use anchor_spl::token_interface::spl_token_2022::{
    extension::{BaseStateWithExtensions, Extension, StateWithExtensions},
    solana_zk_token_sdk::zk_token_proof_instruction::Pod,
    state::Mint,
};
use spl_tlv_account_resolution::{account::ExtraAccountMeta, state::ExtraAccountMetaList};
use spl_type_length_value::variable_len_pack::VariableLenPack;

pub const APPROVE_ACCOUNT_SEED: &[u8] = b"approve-account";
pub const META_LIST_ACCOUNT_SEED: &[u8] = b"extra-account-metas";

然后在导入下方定义以下实用函数:

pub fn update_account_lamports_to_minimum_balance<'info>(
    account: AccountInfo<'info>,
    payer: AccountInfo<'info>,
    system_program: AccountInfo<'info>,
) -> Result<()> {
    let extra_lamports = Rent::get()?.minimum_balance(account.data_len()) - account.get_lamports();
    if extra_lamports > 0 {
        invoke(
            &transfer(payer.key, account.key, extra_lamports),
            &[payer, account, system_program],
        )?;
    }
    Ok(())
}

pub fn get_mint_extensible_extension_data<T: Extension + VariableLenPack>(
    account: &mut AccountInfo,
) -> Result<T> {
    let mint_data = account.data.borrow();
    let mint_with_extension = StateWithExtensions::<Mint>::unpack(&mint_data)?;
    let extension_data = mint_with_extension.get_variable_len_extension::<T>()?;
    Ok(extension_data)
}

pub fn get_mint_extension_data<T: Extension + Pod>(account: &mut AccountInfo) -> Result<T> {
    let mint_data = account.data.borrow();
    let mint_with_extension = StateWithExtensions::<Mint>::unpack(&mint_data)?;
    let extension_data = *mint_with_extension.get_extension::<T>()?;
    Ok(extension_data)
}

pub fn get_meta_list(approve_account: Option<Pubkey>) -> Vec<ExtraAccountMeta> {
    if let Some(approve_account) = approve_account {
        return vec![ExtraAccountMeta {
            discriminator: 0,
            address_config: approve_account.to_bytes(),
            is_signer: false.into(),
            is_writable: true.into(),
        }];
    }
    vec![]
}

pub fn get_meta_list_size(approve_account: Option<Pubkey>) -> usize {
    // safe because it's either 0 or 1
    ExtraAccountMetaList::size_of(get_meta_list(approve_account).len()).unwrap()
}

以下是这些功能的作用:

  • update_account_lamports_to_minimum_balance - 此函数将更新账户的 lamports 至租赁系统变量所需的最低余额。我们可以使用此函数确保账户在定义其扩展之后,根据其数据长度拥有足够的 lamports 以便免于租金。
  • get_mint_extensible_extension_data - 此函数将从铸币账户获取可扩展(或可变长度)扩展类型的扩展数据。
  • get_mint_extension_data - 此函数将从铸币账户获取固定长度扩展类型的扩展数据。
  • get_meta_list - 此函数将获取批准账户的额外账户元数据(用于转账钩子)。
  • get_meta_list_size - 此函数将获取批准账户的额外账户元数据的大小。

参考: Anchor Token Extensions Sample Program

更新 lib.rs

接下来,让我们更新位于 programs/token-extensions/src 目录中的 lib.rs 文件。用以下代码替换 lib.rs 文件的内容(确保将 YOUR_PROGRAM_ID_HERE 替换为你的程序 ID):

use anchor_lang::prelude::*;

pub mod instructions;
pub mod utils;
pub use instructions::*;
pub use utils::*;

declare_id!("YOUR_PROGRAM_ID_HERE"); // 替换为你的程序 ID

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

    pub fn create_mint_account(
        ctx: Context<CreateMintAccount>,
        args: CreateMintAccountArgs,
    ) -> Result<()> {
        instructions::handler(ctx, args)
    }

    pub fn check_mint_extensions_constraints(
        _ctx: Context<CheckMintExtensionConstraints>,
    ) -> Result<()> {
        Ok(())
    }
}

这是我们程序的主要入口点。它声明了程序 ID 并导入我们之前定义的指令和实用函数。我们将定义两个程序函数:create_mint_accountcheck_mint_extensions_constraints。现在让我们定义它们。

创建新代币指令

在你的程序 src 目录中创建一个新文件 instructions.rstoken-extensions/programs/token-extensions/src/instructions.rs)。该文件将包含我们程序的指令处理程序。将以下导入添加到 instructions.rs 文件中:

use anchor_lang::{prelude::*, solana_program::entrypoint::ProgramResult};

use anchor_spl::{
    associated_token::AssociatedToken,
    token_2022::spl_token_2022::extension::{
        group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer,
        mint_close_authority::MintCloseAuthority, permanent_delegate::PermanentDelegate,
        transfer_hook::TransferHook,
    },
    token_interface::{
        spl_token_metadata_interface::state::TokenMetadata, token_metadata_initialize, Mint,
        Token2022, TokenAccount, TokenMetadataInitialize,
    },
};
use spl_pod::optional_keys::OptionalNonZeroPubkey;

use crate::{
    get_meta_list_size, get_mint_extensible_extension_data, get_mint_extension_data,
    update_account_lamports_to_minimum_balance, META_LIST_ACCOUNT_SEED,
};

这里最显著/相关的是包括 anchor_spl::token_2022::spl_token_2022::extensionanchor_spl::token_interface,这将允许我们与代币扩展程序进行交互。

让我们定义 CreateMintAccount 账户上下文结构和参数结构。将以下代码添加到 instructions.rs 文件中:


#[derive(AnchorDeserialize, AnchorSerialize)]
pub struct CreateMintAccountArgs {
    pub name: String,
    pub symbol: String,
    pub uri: String,
}

#[derive(Accounts)]
#[instruction(args: CreateMintAccountArgs)]
pub struct CreateMintAccount<'info> {
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(mut)]
    /// CHECK: 可以是任何账户
    pub authority: Signer<'info>,
    #[account()]
    /// CHECK: 可以是任何账户
    pub receiver: UncheckedAccount<'info>,
    #[account(
        init,
        signer,
        payer = payer,
        mint::token_program = token_program,
        mint::decimals = 0,
        mint::authority = authority,
        mint::freeze_authority = authority,
        extensions::metadata_pointer::authority = authority,
        extensions::metadata_pointer::metadata_address = mint,
        extensions::group_member_pointer::authority = authority,
        extensions::group_member_pointer::member_address = mint,
        extensions::transfer_hook::authority = authority,
        extensions::transfer_hook::program_id = crate::ID,
        extensions::close_authority::authority = authority,
        extensions::permanent_delegate::delegate = authority,
    )]
    pub mint: Box<InterfaceAccount<'info, Mint>>,
    #[account(
        init,
        payer = payer,
        associated_token::token_program = token_program,
        associated_token::mint = mint,
        associated_token::authority = receiver,
    )]
    pub mint_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
    /// CHECK: 该账户的数据是 TLV 数据的缓冲区
    #[account(
        init,
        space = get_meta_list_size(None),
        seeds = [META_LIST_ACCOUNT_SEED, mint.key().as_ref()],
        bump,
        payer = payer,
    )]
    pub extra_metas_account: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_program: Program<'info, Token2022>,
}

首先要注意的是,我们将 token_program 定义为 Program<'info, Token2022>。该程序将允许我们与代币扩展进行交互。如果你尝试使用原始的 TokenProgram,这些扩展将无法工作。

最显著的变化在于 mint 账户的初始化。我们现在使用新约束定义铸件账户的扩展。如果你以前使用过 Anchor,这应该感觉相当熟悉。我们使用多个 extension 约束来初始化我们的铸币,例如,extensions::metadata_pointer::metadata_address = mint 将元数据地址设置为自身。每个都遵循相同的模式:

extensions::<extension_name>::<constraint_name> = value

因为我们正在定义带有 transfer_hook 扩展的铸件,所以我们还需要创建我们的 extra_metas_account,它将保存批准账户的额外账户元数据。我们将在另一个指南中介绍该账户和转账钩子,但现在,我们只是为该账户设置空间,以便你看到这是如何做到的。

让我们创建一个函数来初始化我们铸造的元数据。将以下的 CreateMintAccount 实现添加到 instructions.rs 文件中:

impl<'info> CreateMintAccount<'info> {
    fn initialize_token_metadata(
        &self,
        name: String,
        symbol: String,
        uri: String,
    ) -> ProgramResult {
        let cpi_accounts = TokenMetadataInitialize {
            token_program_id: self.token_program.to_account_info(),
            mint: self.mint.to_account_info(),
            metadata: self.mint.to_account_info(), // 元数据账户是铸造,因为数据存储在铸币中
            mint_authority: self.authority.to_account_info(),
            update_authority: self.authority.to_account_info(),
        };
        let cpi_ctx = CpiContext::new(self.token_program.to_account_info(), cpi_accounts);
        token_metadata_initialize(cpi_ctx, name, symbol, uri)?;
        Ok(())
    }
}

这是一个简单的 CPI 到代币程序,用于初始化我们铸造的元数据。我们传入namesymboluri作为元数据。

最后,让我们定义CreateMintAccount指令的处理函数。将以下代码添加到instructions.rs文件中:

pub fn handler(ctx: Context<CreateMintAccount>, args: CreateMintAccountArgs) -> Result<()> {
    ctx.accounts.initialize_token_metadata(
        args.name.clone(),
        args.symbol.clone(),
        args.uri.clone(),
    )?;
    ctx.accounts.mint.reload()?;
    update_account_lamports_to_minimum_balance(
        ctx.accounts.mint.to_account_info(),
        ctx.accounts.payer.to_account_info(),
        ctx.accounts.system_program.to_account_info(),
    )?;
    Ok(())
}

你要做的就是执行我们之前定义的initialize_token_metadata函数,重新加载铸造账户,然后使用我们之前创建的辅助函数将铸造账户的 lamports 更新到最低余额。

为了测试实用函数和我们指令的准确性,我们可以按照示例程序获取我们铸造的扩展数据,并运行一些断言(使用 Anchor 的assert_eq!宏)来确保一切按预期工作。如果你愿意,可以更新handler函数以包含这些断言:

pub fn handler(ctx: Context<CreateMintAccount>, args: CreateMintAccountArgs) -> Result<()> {
    ctx.accounts.initialize_token_metadata(
        args.name.clone(),
        args.symbol.clone(),
        args.uri.clone(),
    )?;
    ctx.accounts.mint.reload()?;
    let mint_data = &mut ctx.accounts.mint.to_account_info();
    let metadata = get_mint_extensible_extension_data::<TokenMetadata>(mint_data)?;
    assert_eq!(metadata.mint, ctx.accounts.mint.key());
    assert_eq!(metadata.name, args.name);
    assert_eq!(metadata.symbol, args.symbol);
    assert_eq!(metadata.uri, args.uri);
    let metadata_pointer = get_mint_extension_data::<MetadataPointer>(mint_data)?;
    let mint_key: Option<Pubkey> = Some(ctx.accounts.mint.key());
    let authority_key: Option<Pubkey> = Some(ctx.accounts.authority.key());
    assert_eq!(
        metadata_pointer.metadata_address,
        OptionalNonZeroPubkey::try_from(mint_key)?
    );
    assert_eq!(
        metadata_pointer.authority,
        OptionalNonZeroPubkey::try_from(authority_key)?
    );
    let permanent_delegate = get_mint_extension_data::<PermanentDelegate>(mint_data)?;
    assert_eq!(
        permanent_delegate.delegate,
        OptionalNonZeroPubkey::try_from(authority_key)?
    );
    let close_authority = get_mint_extension_data::<MintCloseAuthority>(mint_data)?;
    assert_eq!(
        close_authority.close_authority,
        OptionalNonZeroPubkey::try_from(authority_key)?
    );
    let transfer_hook = get_mint_extension_data::<TransferHook>(mint_data)?;
    let program_id: Option<Pubkey> = Some(ctx.program_id.key());
    assert_eq!(
        transfer_hook.authority,
        OptionalNonZeroPubkey::try_from(authority_key)?
    );
    assert_eq!(
        transfer_hook.program_id,
        OptionalNonZeroPubkey::try_from(program_id)?
    );
    let group_member_pointer = get_mint_extension_data::<GroupMemberPointer>(mint_data)?;
    assert_eq!(
        group_member_pointer.authority,
        OptionalNonZeroPubkey::try_from(authority_key)?
    );
    assert_eq!(
        group_member_pointer.member_address,
        OptionalNonZeroPubkey::try_from(mint_key)?
    );
    update_account_lamports_to_minimum_balance(
        ctx.accounts.mint.to_account_info(),
        ctx.accounts.payer.to_account_info(),
        ctx.accounts.system_program.to_account_info(),
    )?;
    Ok(())
}

乍一看,似乎有很多内容,但这只是一系列断言,用于确保铸造的扩展数据被正确设置。将其作为参考,以查看如何从动态大小的扩展中获取扩展数据是很有帮助的!

创建检查铸造扩展约束指令

最后,我们只需要定义CheckMintExtensionConstraints指令上下文(如果你还记得,在我们的lib.rs中我们已经定义了指令逻辑,Ok(()))。将以下代码添加到instructions.rs文件中:

#[derive(Accounts)]
#[instruction()]
pub struct CheckMintExtensionConstraints<'info> {
    #[account(mut)]
    /// CHECK: can be any account
    pub authority: Signer<'info>,
    #[account(
        extensions::metadata_pointer::authority = authority,
        extensions::metadata_pointer::metadata_address = mint,
        extensions::group_member_pointer::authority = authority,
        extensions::group_member_pointer::member_address = mint,
        extensions::transfer_hook::authority = authority,
        extensions::transfer_hook::program_id = crate::ID,
        extensions::close_authority::authority = authority,
        extensions::permanent_delegate::delegate = authority,
    )]
    pub mint: Box<InterfaceAccount<'info, Mint>>,
}

Anchor 约束工具将确保我们指令中传入的铸造账户具有正确的扩展。如果你尝试在未设置正确扩展的情况下运行此指令,将会出现约束错误。让我们测试一下!

测试程序

打开 Anchor 生成的测试/token-extensions/tests/token-extensions.ts,并将内容替换为以下代码:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { PublicKey, Keypair } from "@solana/web3.js";
import { TokenExtensions } from "../target/types/token_extensions";
import { ASSOCIATED_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token";
import { assert, expect } from "chai";

const TOKEN_2022_PROGRAM_ID = new anchor.web3.PublicKey(
  "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
);

export function associatedAddress({
  mint,
  owner,
}: {
  mint: PublicKey;
  owner: PublicKey;
}): PublicKey {
  return PublicKey.findProgramAddressSync(
    [owner.toBuffer(), TOKEN_2022_PROGRAM_ID.toBuffer(), mint.toBuffer()],
    ASSOCIATED_PROGRAM_ID
  )[0];
}

describe("token extensions", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.TokenExtensions as Program<TokenExtensions>;
  const payer = Keypair.generate();

  it("airdrop payer", async () => {
    const tx = await provider.connection.requestAirdrop(payer.publicKey, 10000000000);
    let confirmed = false;
    while (!confirmed) {
      await new Promise(resolve => setTimeout(resolve, 1000));
      confirmed = await provider.connection.getSignatureStatuses([tx]);
      if (!confirmed) continue;
      if (confirmed.value[0].err) {
        throw new Error(confirmed.value[0].err.toString());
      }
      if (confirmed.value[0].confirmationStatus === 'confirmed') {
        confirmed = true;
        break;
      }
    }
  });

  let mint = new Keypair();
  it("Create mint account test passes", async () => {
    const [extraMetasAccount] = PublicKey.findProgramAddressSync(
      [
        anchor.utils.bytes.utf8.encode("extra-account-metas"),
        mint.publicKey.toBuffer(),
      ],
      program.programId
    );
    await program.methods
      .createMintAccount({
        name: "quick token",
        symbol: "QT",
        uri: "https://my-token-data.com/metadata.json",
      })
      .accountsStrict({
        payer: payer.publicKey,
        authority: payer.publicKey,
        receiver: payer.publicKey,
        mint: mint.publicKey,
        mintTokenAccount: associatedAddress({
          mint: mint.publicKey,
          owner: payer.publicKey,
        }),
        extraMetasAccount: extraMetasAccount,
        systemProgram: anchor.web3.SystemProgram.programId,
        associatedTokenProgram: ASSOCIATED_PROGRAM_ID,
        tokenProgram: TOKEN_2022_PROGRAM_ID,
      })
      .signers([mint, payer])
      .rpc();
  });

  it("mint extension constraints test passes", async () => {
    try {
      const tx = await program.methods
      .checkMintExtensionsConstraints()
      .accountsStrict({
        authority: payer.publicKey,
        mint: mint.publicKey,
      })
      .signers([payer])
      .rpc();
      assert.ok(tx, "transaction should be processed without error");
    } catch (e) {
      assert.fail('should not throw error');
    }
  });

  it("mint extension constraints fails with invalid authority", async () => {
    const wrongAuth = Keypair.generate();
    try {
      const x = await program.methods
      .checkMintExtensionsConstraints()
      .accountsStrict({
        authority: wrongAuth.publicKey,
        mint: mint.publicKey,
      })
      .signers([payer, wrongAuth])
      .rpc();
      assert.fail('should have thrown an error');
    } catch (e) {
      expect(e, 'should throw error');
    }
  });
});

我们的简单测试环境包括四个测试:

  • airdrop payer - 此测试将向付款账户空投 10 SOL。
  • Create mint account test passes - 此测试将创建一个新的铸造账户和相关的代币账户,然后初始化铸造的元数据。
  • mint extension constraints test passes - 此测试将检查铸造账户的铸造扩展约束。此交易应成功。
  • mint extension constraints fails with invalid authority - 此测试将检查铸造账户的铸造扩展约束,使用错误的权限。为了使测试通过,该交易应失败。

由于我们尚未编译我们的程序并创建 IDL,因此你可能会在程序中看到一些类型错误。当我们运行测试套件时,这将得到解决。

通过在终端中执行以下命令来运行测试:

程序应在几分钟后编译完成(第一次运行时),然后运行测试。如果一切设置正确,你应该会看到以下输出:

  token extensions    ✔ airdrop payer (176ms)    ✔ Create mint account test passes (483ms)    ✔ mint extension constraints test passes (470ms)    ✔ mint extension constraints fails with invalid authority

干得好!

总结

你成功创建了一个新 Anchor 程序,该程序创建了一个带有代币扩展的铸造账户并验证铸造扩展约束。该程序可以作为你构建代币扩展程序的一个很好的参考点。注意:并非所有扩展现在都包含在 Anchor 中,因此请关注将包含更多扩展的 Token 2022 和 Anchor 程序的更新。

如果你遇到困难,有问题,或只是想谈谈你的构建,可以在 DiscordTwitter 给我们留言!

资源

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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