{"content":{"title":"如何使用 Anchor 创建和使用 Solana 代币扩展","body":">- 原文链接：[www.quicknode.com/guides...](https://www.quicknode.com/guides/solana-development/anchor/token-2022)\r\n>- 译者：[AI翻译官](https://learnblockchain.cn/people/19584)，校对：[翻译小组](https://learnblockchain.cn/people/412)\r\n>- 本文链接：[learnblockchain.cn/article…](https://learnblockchain.cn/article/10705)\r\n    \r\n## 概述\r\n\r\n在 2024 年 4 月，Anchor [发布了版本 0.30.0](https://www.anchor-lang.com/release-notes/0.30.0)，该版本包括多个重要更改和开发者改进。其中一个显著的变化是为 [代币扩展（Token 2022）](https://www.quicknode.com/guides/solana-development/spl-tokens/token-2022/overview) 引入了代币账户约束。本指南将带你了解这对你的 Solana 项目的意义，我们将通过 Anchor 的示例程序来创建和验证使用代币扩展的代币。\r\n\r\n### 你将要做的事项\r\n\r\n*   了解 Anchor 中的新代币约束\r\n*   审查并重建 [一个示例程序](https://github.com/coral-xyz/anchor/tree/v0.30.0/tests/spl/token-extensions)，该程序创建和验证使用代币扩展的代币\r\n\r\n### 需要你具备\r\n\r\n*   有使用 Anchor 的基本经验 ( [指南：使用 Anchor 入门](https://learnblockchain.cn/article/10706) )\r\n*   对 [Solana 代币扩展](https://www.quicknode.com/guides/solana-development/spl-tokens/token-2022/overview) 的经验\r\n*   对 Rust 编程语言的经验\r\n*   对 JavaScript/TypeScript 的基本知识\r\n*   安装了 [Anchor](https://www.anchor-lang.com/docs/installation) v.0.30.0 或更高版本\r\n\r\n### 本指南使用的依赖项\r\n\r\n| 依赖项 | 版本 |\r\n| --- | --- |\r\n| solana-cli | 1.18.8 |\r\n| anchor-cli | 0.30.0 |\r\n| anchor-lang | 0.30.0 |\r\n| anchor-spl | 0.30.0 |\r\n\r\n### 代币扩展回顾\r\n\r\n代币扩展（也称为 Token-2022）是在 Solana 上的一个高级代币程序，扩展了原始 SPL 代币程序的功能。它旨在为开发者提供更大的灵活性和附加功能，而不影响现有代币的安全性。提供了多种扩展，包括元数据、转账费用、转账钩子等。在继续之前，请先了解 [代币扩展](https://www.quicknode.com/guides/solana-development/spl-tokens/token-2022/overview) 及其工作原理。\r\n\r\n## Anchor 中的代币扩展\r\n\r\n从 Anchor 0.30.0 开始，你现在可以使用 [Anchor 约束](https://www.quicknode.com/guides/solana-development/anchor/how-to-use-constraints-in-anchor) 创建和验证使用代币扩展的代币。\r\n\r\n| 扩展 | 约束 |\r\n| --- | --- |\r\n| group_pointer | authority |\r\n| group_address |\r\n| group_member_pointer | authority |\r\n| member_address |\r\n| metadata_pointer | authority |\r\n| metadata_address |\r\n| close_authority | authority |\r\n| permanent_delegate | delegate |\r\n| transfer_hook | authority |\r\n| program_id |\r\n\r\n扩展约束在你的账户上下文结构中使用，格式为 `extension` 后跟 `::` 和扩展名称，再后跟 `::` 和约束名称。例如：\r\n\r\n```\r\n    extension::group_pointer::authority = YOUR_AUTH.key()\r\n```\r\n \r\n在创建新代币时，可以使用 `init` 约束；在验证现有代币时，则可以不使用 `init` 约束。\r\n\r\nAnchor 发布了 [一个示例程序](https://github.com/coral-xyz/anchor/tree/v0.30.0/tests/spl/token-extensions)，演示如何使用新约束创建和验证代币。让我们创建一个新的 Anchor 项目并添加示例程序，以看看它是如何工作的。\r\n\r\n## 使用 Anchor 创建代币扩展程序\r\n\r\n### 初始化一个新的 Anchor 项目\r\n\r\n> 在开始之前: \r\n请确保安装了 [Anchor](https://www.anchor-lang.com/docs/installation) v.0.30.0 或更高版本。旧版本的 Anchor 无法与新约束一起使用。你可以通过运行 `anchor --version` 来检查你的 Anchor 版本。如果你已安装 Anchor 版本管理器，可以通过运行 `avm update` 升级你的 Anchor 版本。\r\n\r\n打开终端并运行以下命令初始化一个新的 Anchor 项目：\r\n\r\n    anchor init token-extensions\r\n\r\n这将创建一个名为 `token-extensions` 的新项目目录，其中包含启动所需的文件和目录。切换到新目录：\r\n```\r\ncd token-extensions\r\n\r\n```\r\n\r\n### 更新 `Cargo.toml`\r\n\r\n首先，让我们导入所需的依赖项。打开你 `programs` 目录中的 `Cargo.toml` 文件（programs/token-extensions/Cargo.toml），并添加以下依赖项：\r\n\r\n```rust\r\n[dependencies]\r\nanchor-lang = { version = \"0.30.0\", features = [\"init-if-needed\"] }\r\nanchor-spl = \"0.30.0\"\r\nspl-tlv-account-resolution = \"0.6.3\"\r\nspl-transfer-hook-interface = \"0.6.3\"\r\nspl-type-length-value = \"0.4.3\"\r\nspl-pod = \"0.2.2\"\r\n```\r\n\r\n这里有一些新的依赖项，我们将需要它们来与代币扩展程序进行交互：\r\n\r\n*   [`spl-tlv-account-resolution`](https://crates.io/crates/spl-tlv-account-resolution) - 一个用于编码和解码账户中的 TLV（类型-长度-值）数据的库。\r\n*   [`spl-transfer-hook-interface`](https://crates.io/crates/spl-transfer-hook-interface) - 一个用于实现转账钩子的库。\r\n*   [`spl-type-length-value`](https://crates.io/crates/spl-type-length-value) - 一个用于编码和解码 TLV 数据的库。\r\n*   [`spl-pod`](https://crates.io/crates/spl-pod) - 一个用于编码和解码普通旧数据（POD）数据的库。\r\n\r\n接下来，让我们更新我们的 `[features]`。我们需要关注 Anchor 0.30.0 中的新功能：IDL（接口定义语言）构建。现在在你的程序的 Cargo.toml 定义中需要包含 \"idl-build\" 功能，以使 IDL 生成正常工作，并且所有生成 IDL 的 crate 都必须要求此功能。在我们的例子中，我们必须在 `anchor-spl` 依赖项中包含 `idl-build` 功能。将 `Cargo.toml` 文件中的 `idl-build` 功能更新为包含 `anchor-spl`：\r\n\r\n```\r\n    idl-build = [\"anchor-lang/idl-build\", \"anchor-spl/idl-build\"]\r\n```\r\n\r\n太好了！现在，我们可以继续下一步。\r\n\r\n### 添加代币扩展工具\r\n\r\n在你的程序 `src` 目录中创建一个新文件 `utils.rs`（`token-extensions/programs/token-extensions/src/utils.rs`）。此文件将包含我们用于与代币扩展程序交互的实用函数。将以下导入添加到 `utils.rs` 文件中：\r\n\r\n```rust\r\nuse anchor_lang::{\r\n    prelude::Result,\r\n    solana_program::{\r\n        account_info::AccountInfo,\r\n        program::invoke,\r\n        pubkey::Pubkey,\r\n        rent::Rent,\r\n        system_instruction::transfer,\r\n        sysvar::Sysvar,\r\n    },\r\n    Lamports,\r\n};\r\nuse anchor_spl::token_interface::spl_token_2022::{\r\n    extension::{BaseStateWithExtensions, Extension, StateWithExtensions},\r\n    solana_zk_token_sdk::zk_token_proof_instruction::Pod,\r\n    state::Mint,\r\n};\r\nuse spl_tlv_account_resolution::{account::ExtraAccountMeta, state::ExtraAccountMetaList};\r\nuse spl_type_length_value::variable_len_pack::VariableLenPack;\r\n\r\npub const APPROVE_ACCOUNT_SEED: &[u8] = b\"approve-account\";\r\npub const META_LIST_ACCOUNT_SEED: &[u8] = b\"extra-account-metas\";\r\n```\r\n\r\n然后在导入下方定义以下实用函数：\r\n\r\n```rust\r\npub fn update_account_lamports_to_minimum_balance<'info>(\r\n    account: AccountInfo<'info>,\r\n    payer: AccountInfo<'info>,\r\n    system_program: AccountInfo<'info>,\r\n) -> Result<()> {\r\n    let extra_lamports = Rent::get()?.minimum_balance(account.data_len()) - account.get_lamports();\r\n    if extra_lamports > 0 {\r\n        invoke(\r\n            &transfer(payer.key, account.key, extra_lamports),\r\n            &[payer, account, system_program],\r\n        )?;\r\n    }\r\n    Ok(())\r\n}\r\n\r\npub fn get_mint_extensible_extension_data<T: Extension + VariableLenPack>(\r\n    account: &mut AccountInfo,\r\n) -> Result<T> {\r\n    let mint_data = account.data.borrow();\r\n    let mint_with_extension = StateWithExtensions::<Mint>::unpack(&mint_data)?;\r\n    let extension_data = mint_with_extension.get_variable_len_extension::<T>()?;\r\n    Ok(extension_data)\r\n}\r\n\r\npub fn get_mint_extension_data<T: Extension + Pod>(account: &mut AccountInfo) -> Result<T> {\r\n    let mint_data = account.data.borrow();\r\n    let mint_with_extension = StateWithExtensions::<Mint>::unpack(&mint_data)?;\r\n    let extension_data = *mint_with_extension.get_extension::<T>()?;\r\n    Ok(extension_data)\r\n}\r\n\r\n\r\npub fn get_meta_list(approve_account: Option<Pubkey>) -> Vec<ExtraAccountMeta> {\r\n    if let Some(approve_account) = approve_account {\r\n        return vec![ExtraAccountMeta {\r\n            discriminator: 0,\r\n            address_config: approve_account.to_bytes(),\r\n            is_signer: false.into(),\r\n            is_writable: true.into(),\r\n        }];\r\n    }\r\n    vec![]\r\n}\r\n\r\npub fn get_meta_list_size(approve_account: Option<Pubkey>) -> usize {\r\n    // safe because it's either 0 or 1\r\n    ExtraAccountMetaList::size_of(get_meta_list(approve_account).len()).unwrap()\r\n}\r\n```\r\n\r\n以下是这些功能的作用：\r\n\r\n*   `update_account_lamports_to_minimum_balance` - 此函数将更新账户的 lamports 至租赁系统变量所需的最低余额。我们可以使用此函数确保账户在定义其扩展之后，根据其数据长度拥有足够的 lamports 以便免于租金。\r\n*   `get_mint_extensible_extension_data` - 此函数将从铸币账户获取可扩展（或可变长度）扩展类型的扩展数据。\r\n*   `get_mint_extension_data` - 此函数将从铸币账户获取固定长度扩展类型的扩展数据。\r\n*   `get_meta_list` - 此函数将获取批准账户的额外账户元数据（用于转账钩子）。\r\n*   `get_meta_list_size` - 此函数将获取批准账户的额外账户元数据的大小。\r\n\r\n*参考： [Anchor Token Extensions Sample Program](https://github.com/coral-xyz/anchor/blob/v0.30.0/tests/spl/token-extensions/programs/token-extensions/src/utils.rs)*\r\n\r\n### 更新 `lib.rs` \r\n\r\n接下来，让我们更新位于 `programs/token-extensions/src` 目录中的 `lib.rs` 文件。用以下代码替换 `lib.rs` 文件的内容（**确保将 `YOUR_PROGRAM_ID_HERE` 替换为你的程序 ID**）：\r\n\r\n```rust\r\nuse anchor_lang::prelude::*;\r\n\r\npub mod instructions;\r\npub mod utils;\r\npub use instructions::*;\r\npub use utils::*;\r\n\r\ndeclare_id!(\"YOUR_PROGRAM_ID_HERE\"); // 替换为你的程序 ID\r\n\r\n#[program]\r\npub mod token_extensions {\r\n    use super::*;\r\n\r\n    pub fn create_mint_account(\r\n        ctx: Context<CreateMintAccount>,\r\n        args: CreateMintAccountArgs,\r\n    ) -> Result<()> {\r\n        instructions::handler(ctx, args)\r\n    }\r\n\r\n    pub fn check_mint_extensions_constraints(\r\n        _ctx: Context<CheckMintExtensionConstraints>,\r\n    ) -> Result<()> {\r\n        Ok(())\r\n    }\r\n}\r\n```\r\n\r\n这是我们程序的主要入口点。它声明了程序 ID 并导入我们之前定义的指令和实用函数。我们将定义两个程序函数：`create_mint_account` 和 `check_mint_extensions_constraints`。现在让我们定义它们。\r\n\r\n### 创建新代币指令 \r\n在你的程序 `src` 目录中创建一个新文件 `instructions.rs`（`token-extensions/programs/token-extensions/src/instructions.rs`）。该文件将包含我们程序的指令处理程序。将以下导入添加到 `instructions.rs` 文件中：\r\n\r\n```rust\r\nuse anchor_lang::{prelude::*, solana_program::entrypoint::ProgramResult};\r\n\r\nuse anchor_spl::{\r\n    associated_token::AssociatedToken,\r\n    token_2022::spl_token_2022::extension::{\r\n        group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer,\r\n        mint_close_authority::MintCloseAuthority, permanent_delegate::PermanentDelegate,\r\n        transfer_hook::TransferHook,\r\n    },\r\n    token_interface::{\r\n        spl_token_metadata_interface::state::TokenMetadata, token_metadata_initialize, Mint,\r\n        Token2022, TokenAccount, TokenMetadataInitialize,\r\n    },\r\n};\r\nuse spl_pod::optional_keys::OptionalNonZeroPubkey;\r\n\r\nuse crate::{\r\n    get_meta_list_size, get_mint_extensible_extension_data, get_mint_extension_data,\r\n    update_account_lamports_to_minimum_balance, META_LIST_ACCOUNT_SEED,\r\n};\r\n```\r\n\r\n这里最显著/相关的是包括 `anchor_spl::token_2022::spl_token_2022::extension` 和 `anchor_spl::token_interface`，这将允许我们与代币扩展程序进行交互。\r\n\r\n让我们定义 `CreateMintAccount` 账户上下文结构和参数结构。将以下代码添加到 `instructions.rs` 文件中：\r\n\r\n```rust\r\n\r\n#[derive(AnchorDeserialize, AnchorSerialize)]\r\npub struct CreateMintAccountArgs {\r\n    pub name: String,\r\n    pub symbol: String,\r\n    pub uri: String,\r\n}\r\n\r\n#[derive(Accounts)]\r\n#[instruction(args: CreateMintAccountArgs)]\r\npub struct CreateMintAccount<'info> {\r\n    #[account(mut)]\r\n    pub payer: Signer<'info>,\r\n    #[account(mut)]\r\n    /// CHECK: 可以是任何账户\r\n    pub authority: Signer<'info>,\r\n    #[account()]\r\n    /// CHECK: 可以是任何账户\r\n    pub receiver: UncheckedAccount<'info>,\r\n    #[account(\r\n        init,\r\n        signer,\r\n        payer = payer,\r\n        mint::token_program = token_program,\r\n        mint::decimals = 0,\r\n        mint::authority = authority,\r\n        mint::freeze_authority = authority,\r\n        extensions::metadata_pointer::authority = authority,\r\n        extensions::metadata_pointer::metadata_address = mint,\r\n        extensions::group_member_pointer::authority = authority,\r\n        extensions::group_member_pointer::member_address = mint,\r\n        extensions::transfer_hook::authority = authority,\r\n        extensions::transfer_hook::program_id = crate::ID,\r\n        extensions::close_authority::authority = authority,\r\n        extensions::permanent_delegate::delegate = authority,\r\n    )]\r\n    pub mint: Box<InterfaceAccount<'info, Mint>>,\r\n    #[account(\r\n        init,\r\n        payer = payer,\r\n        associated_token::token_program = token_program,\r\n        associated_token::mint = mint,\r\n        associated_token::authority = receiver,\r\n    )]\r\n    pub mint_token_account: Box<InterfaceAccount<'info, TokenAccount>>,\r\n    /// CHECK: 该账户的数据是 TLV 数据的缓冲区\r\n    #[account(\r\n        init,\r\n        space = get_meta_list_size(None),\r\n        seeds = [META_LIST_ACCOUNT_SEED, mint.key().as_ref()],\r\n        bump,\r\n        payer = payer,\r\n    )]\r\n    pub extra_metas_account: UncheckedAccount<'info>,\r\n    pub system_program: Program<'info, System>,\r\n    pub associated_token_program: Program<'info, AssociatedToken>,\r\n    pub token_program: Program<'info, Token2022>,\r\n}\r\n```\r\n\r\n首先要注意的是，我们将 `token_program` 定义为 `Program<'info, Token2022>`。该程序将允许我们与代币扩展进行交互。如果你尝试使用原始的 `TokenProgram`，这些扩展将无法工作。\r\n\r\n最显著的变化在于 `mint` 账户的初始化。我们现在使用新约束定义铸件账户的扩展。如果你以前使用过 Anchor，这应该感觉相当熟悉。我们使用多个 `extension` 约束来初始化我们的铸币，例如，`extensions::metadata_pointer::metadata_address = mint` 将元数据地址设置为自身。每个都遵循相同的模式：\r\n\r\n    extensions::<extension_name>::<constraint_name> = value\r\n\r\n因为我们正在定义带有 `transfer_hook` 扩展的铸件，所以我们还需要创建我们的 `extra_metas_account`，它将保存批准账户的额外账户元数据。我们将在另一个指南中介绍该账户和转账钩子，但现在，我们只是为该账户设置空间，以便你看到这是如何做到的。\r\n\r\n让我们创建一个函数来初始化我们铸造的元数据。将以下的 `CreateMintAccount` 实现添加到 `instructions.rs` 文件中：\r\n\r\n```rust\r\nimpl<'info> CreateMintAccount<'info> {\r\n    fn initialize_token_metadata(\r\n        &self,\r\n        name: String,\r\n        symbol: String,\r\n        uri: String,\r\n    ) -> ProgramResult {\r\n        let cpi_accounts = TokenMetadataInitialize {\r\n            token_program_id: self.token_program.to_account_info(),\r\n            mint: self.mint.to_account_info(),\r\n            metadata: self.mint.to_account_info(), // 元数据账户是铸造，因为数据存储在铸币中\r\n            mint_authority: self.authority.to_account_info(),\r\n            update_authority: self.authority.to_account_info(),\r\n        };\r\n        let cpi_ctx = CpiContext::new(self.token_program.to_account_info(), cpi_accounts);\r\n        token_metadata_initialize(cpi_ctx, name, symbol, uri)?;\r\n        Ok(())\r\n    }\r\n}\r\n```\r\n\r\n这是一个简单的 CPI 到代币程序，用于初始化我们铸造的元数据。我们传入`name`、`symbol`和`uri`作为元数据。\r\n\r\n最后，让我们定义`CreateMintAccount`指令的处理函数。将以下代码添加到`instructions.rs`文件中：\r\n\r\n```rust\r\npub fn handler(ctx: Context<CreateMintAccount>, args: CreateMintAccountArgs) -> Result<()> {\r\n    ctx.accounts.initialize_token_metadata(\r\n        args.name.clone(),\r\n        args.symbol.clone(),\r\n        args.uri.clone(),\r\n    )?;\r\n    ctx.accounts.mint.reload()?;\r\n    update_account_lamports_to_minimum_balance(\r\n        ctx.accounts.mint.to_account_info(),\r\n        ctx.accounts.payer.to_account_info(),\r\n        ctx.accounts.system_program.to_account_info(),\r\n    )?;\r\n    Ok(())\r\n}\r\n```\r\n\r\n你要做的就是执行我们之前定义的`initialize_token_metadata`函数，重新加载铸造账户，然后使用我们之前创建的辅助函数将铸造账户的 lamports 更新到最低余额。\r\n\r\n为了测试实用函数和我们指令的准确性，我们可以按照示例程序获取我们铸造的扩展数据，并运行一些断言（使用 Anchor 的`assert_eq!`宏）来确保一切按预期工作。如果你愿意，可以更新`handler`函数以包含这些断言：\r\n\r\n```rust\r\npub fn handler(ctx: Context<CreateMintAccount>, args: CreateMintAccountArgs) -> Result<()> {\r\n    ctx.accounts.initialize_token_metadata(\r\n        args.name.clone(),\r\n        args.symbol.clone(),\r\n        args.uri.clone(),\r\n    )?;\r\n    ctx.accounts.mint.reload()?;\r\n    let mint_data = &mut ctx.accounts.mint.to_account_info();\r\n    let metadata = get_mint_extensible_extension_data::<TokenMetadata>(mint_data)?;\r\n    assert_eq!(metadata.mint, ctx.accounts.mint.key());\r\n    assert_eq!(metadata.name, args.name);\r\n    assert_eq!(metadata.symbol, args.symbol);\r\n    assert_eq!(metadata.uri, args.uri);\r\n    let metadata_pointer = get_mint_extension_data::<MetadataPointer>(mint_data)?;\r\n    let mint_key: Option<Pubkey> = Some(ctx.accounts.mint.key());\r\n    let authority_key: Option<Pubkey> = Some(ctx.accounts.authority.key());\r\n    assert_eq!(\r\n        metadata_pointer.metadata_address,\r\n        OptionalNonZeroPubkey::try_from(mint_key)?\r\n    );\r\n    assert_eq!(\r\n        metadata_pointer.authority,\r\n        OptionalNonZeroPubkey::try_from(authority_key)?\r\n    );\r\n    let permanent_delegate = get_mint_extension_data::<PermanentDelegate>(mint_data)?;\r\n    assert_eq!(\r\n        permanent_delegate.delegate,\r\n        OptionalNonZeroPubkey::try_from(authority_key)?\r\n    );\r\n    let close_authority = get_mint_extension_data::<MintCloseAuthority>(mint_data)?;\r\n    assert_eq!(\r\n        close_authority.close_authority,\r\n        OptionalNonZeroPubkey::try_from(authority_key)?\r\n    );\r\n    let transfer_hook = get_mint_extension_data::<TransferHook>(mint_data)?;\r\n    let program_id: Option<Pubkey> = Some(ctx.program_id.key());\r\n    assert_eq!(\r\n        transfer_hook.authority,\r\n        OptionalNonZeroPubkey::try_from(authority_key)?\r\n    );\r\n    assert_eq!(\r\n        transfer_hook.program_id,\r\n        OptionalNonZeroPubkey::try_from(program_id)?\r\n    );\r\n    let group_member_pointer = get_mint_extension_data::<GroupMemberPointer>(mint_data)?;\r\n    assert_eq!(\r\n        group_member_pointer.authority,\r\n        OptionalNonZeroPubkey::try_from(authority_key)?\r\n    );\r\n    assert_eq!(\r\n        group_member_pointer.member_address,\r\n        OptionalNonZeroPubkey::try_from(mint_key)?\r\n    );\r\n    update_account_lamports_to_minimum_balance(\r\n        ctx.accounts.mint.to_account_info(),\r\n        ctx.accounts.payer.to_account_info(),\r\n        ctx.accounts.system_program.to_account_info(),\r\n    )?;\r\n    Ok(())\r\n}\r\n```\r\n\r\n乍一看，似乎有很多内容，但这只是一系列断言，用于确保铸造的扩展数据被正确设置。将其作为参考，以查看如何从动态大小的扩展中获取扩展数据是很有帮助的！\r\n\r\n### 创建检查铸造扩展约束指令\r\n\r\n最后，我们只需要定义`CheckMintExtensionConstraints`指令上下文（如果你还记得，在我们的`lib.rs`中我们已经定义了指令逻辑，`Ok(())`）。将以下代码添加到`instructions.rs`文件中：\r\n\r\n```rust\r\n#[derive(Accounts)]\r\n#[instruction()]\r\npub struct CheckMintExtensionConstraints<'info> {\r\n    #[account(mut)]\r\n    /// CHECK: can be any account\r\n    pub authority: Signer<'info>,\r\n    #[account(\r\n        extensions::metadata_pointer::authority = authority,\r\n        extensions::metadata_pointer::metadata_address = mint,\r\n        extensions::group_member_pointer::authority = authority,\r\n        extensions::group_member_pointer::member_address = mint,\r\n        extensions::transfer_hook::authority = authority,\r\n        extensions::transfer_hook::program_id = crate::ID,\r\n        extensions::close_authority::authority = authority,\r\n        extensions::permanent_delegate::delegate = authority,\r\n    )]\r\n    pub mint: Box<InterfaceAccount<'info, Mint>>,\r\n}\r\n```\r\n\r\nAnchor 约束工具将确保我们指令中传入的铸造账户具有正确的扩展。如果你尝试在未设置正确扩展的情况下运行此指令，将会出现约束错误。让我们测试一下！\r\n\r\n## 测试程序\r\n\r\n打开 Anchor 生成的测试`/token-extensions/tests/token-extensions.ts`，并将内容替换为以下代码：\r\n\r\n```typescript\r\nimport * as anchor from \"@coral-xyz/anchor\";\r\nimport { Program } from \"@coral-xyz/anchor\";\r\nimport { PublicKey, Keypair } from \"@solana/web3.js\";\r\nimport { TokenExtensions } from \"../target/types/token_extensions\";\r\nimport { ASSOCIATED_PROGRAM_ID } from \"@coral-xyz/anchor/dist/cjs/utils/token\";\r\nimport { assert, expect } from \"chai\";\r\n\r\nconst TOKEN_2022_PROGRAM_ID = new anchor.web3.PublicKey(\r\n  \"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb\"\r\n);\r\n\r\nexport function associatedAddress({\r\n  mint,\r\n  owner,\r\n}: {\r\n  mint: PublicKey;\r\n  owner: PublicKey;\r\n}): PublicKey {\r\n  return PublicKey.findProgramAddressSync(\r\n    [owner.toBuffer(), TOKEN_2022_PROGRAM_ID.toBuffer(), mint.toBuffer()],\r\n    ASSOCIATED_PROGRAM_ID\r\n  )[0];\r\n}\r\n\r\ndescribe(\"token extensions\", () => {\r\n  const provider = anchor.AnchorProvider.env();\r\n  anchor.setProvider(provider);\r\n  const program = anchor.workspace.TokenExtensions as Program<TokenExtensions>;\r\n  const payer = Keypair.generate();\r\n\r\n  it(\"airdrop payer\", async () => {\r\n    const tx = await provider.connection.requestAirdrop(payer.publicKey, 10000000000);\r\n    let confirmed = false;\r\n    while (!confirmed) {\r\n      await new Promise(resolve => setTimeout(resolve, 1000));\r\n      confirmed = await provider.connection.getSignatureStatuses([tx]);\r\n      if (!confirmed) continue;\r\n      if (confirmed.value[0].err) {\r\n        throw new Error(confirmed.value[0].err.toString());\r\n      }\r\n      if (confirmed.value[0].confirmationStatus === 'confirmed') {\r\n        confirmed = true;\r\n        break;\r\n      }\r\n    }\r\n  });\r\n\r\n  let mint = new Keypair();\r\n  it(\"Create mint account test passes\", async () => {\r\n    const [extraMetasAccount] = PublicKey.findProgramAddressSync(\r\n      [\r\n        anchor.utils.bytes.utf8.encode(\"extra-account-metas\"),\r\n        mint.publicKey.toBuffer(),\r\n      ],\r\n      program.programId\r\n    );\r\n    await program.methods\r\n      .createMintAccount({\r\n        name: \"quick token\",\r\n        symbol: \"QT\",\r\n        uri: \"https://my-token-data.com/metadata.json\",\r\n      })\r\n      .accountsStrict({\r\n        payer: payer.publicKey,\r\n        authority: payer.publicKey,\r\n        receiver: payer.publicKey,\r\n        mint: mint.publicKey,\r\n        mintTokenAccount: associatedAddress({\r\n          mint: mint.publicKey,\r\n          owner: payer.publicKey,\r\n        }),\r\n        extraMetasAccount: extraMetasAccount,\r\n        systemProgram: anchor.web3.SystemProgram.programId,\r\n        associatedTokenProgram: ASSOCIATED_PROGRAM_ID,\r\n        tokenProgram: TOKEN_2022_PROGRAM_ID,\r\n      })\r\n      .signers([mint, payer])\r\n      .rpc();\r\n  });\r\n\r\n  it(\"mint extension constraints test passes\", async () => {\r\n    try {\r\n      const tx = await program.methods\r\n      .checkMintExtensionsConstraints()\r\n      .accountsStrict({\r\n        authority: payer.publicKey,\r\n        mint: mint.publicKey,\r\n      })\r\n      .signers([payer])\r\n      .rpc();\r\n      assert.ok(tx, \"transaction should be processed without error\");\r\n    } catch (e) {\r\n      assert.fail('should not throw error');\r\n    }\r\n  });\r\n\r\n  it(\"mint extension constraints fails with invalid authority\", async () => {\r\n    const wrongAuth = Keypair.generate();\r\n    try {\r\n      const x = await program.methods\r\n      .checkMintExtensionsConstraints()\r\n      .accountsStrict({\r\n        authority: wrongAuth.publicKey,\r\n        mint: mint.publicKey,\r\n      })\r\n      .signers([payer, wrongAuth])\r\n      .rpc();\r\n      assert.fail('should have thrown an error');\r\n    } catch (e) {\r\n      expect(e, 'should throw error');\r\n    }\r\n  });\r\n});\r\n```\r\n\r\n我们的简单测试环境包括四个测试：\r\n\r\n*   `airdrop payer` - 此测试将向付款账户空投 10 SOL。\r\n*   `Create mint account test passes` - 此测试将创建一个新的铸造账户和相关的代币账户，然后初始化铸造的元数据。\r\n*   `mint extension constraints test passes` - 此测试将检查铸造账户的铸造扩展约束。此交易应成功。\r\n*   `mint extension constraints fails with invalid authority` - 此测试将检查铸造账户的铸造扩展约束，使用错误的权限。为了使测试通过，该交易应失败。\r\n\r\n由于我们尚未编译我们的程序并创建 IDL，因此你可能会在程序中看到一些类型错误。当我们运行测试套件时，这将得到解决。\r\n\r\n通过在终端中执行以下命令来运行测试：\r\n\r\n程序应在几分钟后编译完成（第一次运行时），然后运行测试。如果一切设置正确，你应该会看到以下输出：\r\n\r\n      token extensions    ✔ airdrop payer (176ms)    ✔ Create mint account test passes (483ms)    ✔ mint extension constraints test passes (470ms)    ✔ mint extension constraints fails with invalid authority\r\n\r\n干得好！\r\n\r\n## 总结\r\n\r\n你成功创建了一个新 Anchor 程序，该程序创建了一个带有代币扩展的铸造账户并验证铸造扩展约束。该程序可以作为你构建代币扩展程序的一个很好的参考点。*注意：并非所有扩展现在都包含在 Anchor 中，因此请关注将包含更多扩展的 Token 2022 和 Anchor 程序的更新。*\r\n\r\n如果你遇到困难，有问题，或只是想谈谈你的构建，可以在 [Discord](https://discord.gg/quicknode) 或 [Twitter](https://twitter.com/QuickNode) 给我们留言！\r\n\r\n## 资源\r\n\r\n*   [指南：Solana 代币扩展](https://www.quicknode.com/guides/solana-development/spl-tokens/token-2022/overview)\r\n*   [Anchor GitHub 仓库](https://github.com/coral-xyz/anchor)\r\n*   [Anchor 文档](https://www.anchor-lang.com/)\r\n*   [Anchor 0.30.0 发布说明](https://www.anchor-lang.com/release-notes/0.30.0)\r\n*   [Anchor / Token 2022 示例程序](https://github.com/coral-xyz/anchor/tree/v0.30.0/tests/spl/token-extensions)\r\n\r\n> 我是 [AI 翻译官](https://learnblockchain.cn/people/19584)，为大家转译优秀英文文章，如有翻译不通的地方，在[这里](https://github.com/lbc-team/Pioneer/blob/master/translations/10705.md)修改，还请包涵～"},"author":{"user":"https://learnblockchain.cn/people/25306","address":null},"history":"bafkreieovvmtwemmz32mnaonmvxe7dqp46box3so4gqwyafp2rtgjtmxz4","timestamp":1738983049,"version":1}