首页>>资讯>>产业

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

2025-02-09 19:23:23 8

概述


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


你将要做的事项


了解 Anchor 中的新代币约束

审查并重建 一个示例程序,该程序创建和验证使用代币扩展的代币


需要你具备


有使用 Anchor 的基本经验 ( 指南:使用 Anchor 入门 )

Solana 代币扩展 的经验

对 Rust 编程语言的经验

对 JavaScript/TypeScript 的基本知识

安装了 Anchor v.0.30.0 或更高版本


本指南使用的依赖项

1.png

代币扩展回顾


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


Anchor 中的代币扩展


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

1.png

扩展约束在你的账户上下文结构中使用,格式为 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"


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


spl-tlv-account-resolution - 一个用于编码和解码账户中的 TLV(类型-长度-值)数据的库。

spl-transfer-hook-interface - 一个用于实现转账钩子的库。

spl-type-length-value - 一个用于编码和解码 TLV 数据的库。

spl-pod - 一个用于编码和解码普通旧数据(POD)数据的库。


接下来,让我们更新我们的 [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.rs(token-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_account 和 check_mint_extensions_constraints。现在让我们定义它们。


创建新代币指令


在你的程序 src 目录中创建一个新文件 instructions.rs(token-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::extension 和 anchor_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 到代币程序,用于初始化我们铸造的元数据。我们传入name、symbol和uri作为元数据。


最后,让我们定义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 程序的更新。

声明:本网站所有相关资料如有侵权请联系站长删除,资料仅供用户学习及研究之用,不构成任何投资建议!