如何使用 Anchor 创建和使用 Solana 代币扩展
- 原文链接:www.quicknode.com/guides...
- 译者:AI翻译官,校对:翻译小组
- 本文链接:learnblockchain.cn/article…
在 2024 年 4 月,Anchor 发布了版本 0.30.0,该版本包括多个重要更改和开发者改进。其中一个显著的变化是为 代币扩展(Token 2022) 引入了代币账户约束。本指南将带你了解这对你的 Solana 项目的意义,我们将通过 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 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 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 程序的更新。
如果你遇到困难,有问题,或只是想谈谈你的构建,可以在 Discord 或 Twitter 给我们留言!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!