如何使用 Transfer Hook 扩展

Transfer Hook 扩展和 Transfer Hook 接口引入了创建 mint account 的能力,使其在每次代币转账时执行自定义指令逻辑。

这为代币转账解锁了许多全新的使用场景,例如:

  • 强制执行 NFT 版税
  • 黑名单或白名单可接收代币的钱包
  • 对代币转账实施自定义手续费
  • 创建自定义代币转账事件
  • 追踪代币转账的统计数据
  • 以及更多用途

为实现这一目标,开发者需要构建一个实现 Transfer Hook Interface 的程序,并初始化一个启用了 Transfer Hook 扩展的 mint account。

对于每一笔涉及该 mint account 代币的转账,Token Extensions Program 都会通过 Cross Program Invocation(CPI)调用 Transfer Hook 程序上的指令。

当 Token Extensions Program 通过 CPI 调用 Transfer Hook 程序时,初始转账中的所有账户都将转换为只读账户。这意味着发送方的签名权限不会延伸到 Transfer Hook 程序。

此设计决策旨在防止 Transfer Hook 程序被恶意利用。

在本指南中,我们将使用 Anchor 框架创建一个 Transfer Hook 程序,但也可以使用原生程序来实现 Transfer Hook 接口。在此了解更多关于 Anchor 框架的信息: Anchor Framework

Transfer Hook Interface 概览

Transfer Hook Interface 为开发者提供了一种方式,用于实现针对特定 mint account 在每次代币转账时执行的自定义指令逻辑。

Transfer Hook Interface 规定了以下 指令

  • Execute:Token Extensions Program 在每次代币转账时调用的指令。
  • InitializeExtraAccountMetaList(可选):创建一个账户,用于存储自定义 Execute 指令所需的额外账户列表。
  • UpdateExtraAccountMetaList(可选):通过覆盖现有列表来更新额外账户列表。

从技术上讲,并不强制要求使用该接口来实现 InitializeExtraAccountMetaList 指令。该账户可以由 Transfer Hook 程序上的任何指令创建。

但是,该账户的 Program Derived Address(PDA)必须使用以下 seed 派生:

  • 硬编码字符串 "extra-account-metas"
  • mint account 地址
  • Transfer Hook 程序 ID
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()],
program.programId // transfer hook program ID
);

通过将 Execute 指令所需的额外账户存储在预定义的 PDA 中,这些账户可以从客户端自动添加到代币转账指令中。

Hello-world Transfer Hook

此示例是 Transfer Hook 的 hello world 入门程序。这是一个简单的 Transfer Hook,每次代币转账时仅打印一条消息。我们从在 Solana Playground(一个用于构建和部署 Solana 程序的在线工具)中打开示例开始: link

该示例包含一个实现了 Transfer Hook 接口的 Anchor 程序以及一个用于测试该程序的测试文件。

此程序仅包含 3 条指令:

  1. initialize_extra_account_meta_list:创建一个账户,用于存储 transfer_hook 指令所需的额外账户列表。在 hello world 示例中,我们将其留空。
  2. transfer_hook:此指令在每次代币转账时通过 CPI 调用,用于执行包装 SOL 代币转账。
  3. fallback:由于我们使用的是 Anchor 而 Token Program 是原生程序,因此需要添加一条 fallback 指令来手动匹配指令鉴别器并调用我们自定义的 transfer_hook 指令。无需修改此函数。

每次代币被转账时,Token Program 都会调用此 transfer_hook 函数。

pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
msg!("Hello Transfer Hook!");
Ok(())
}

在此函数中,您现在可以添加额外的逻辑。例如,您可以让转账在转账金额超过 50 时失败,如下所示:

#[error_code]
pub enum MyError {
#[msg("The amount is too big")]
AmountTooBig,
}
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
msg!("Hello Transfer Hook!");
if amount > 50 {
return err!(MyError::AmountTooBig);
}
Ok(())
}

要在 Solana Playground 中运行此示例,请访问以下链接: link

在 Playground 的终端中,运行 build 命令,这将使用新生成的程序 ID 更新 lib.rs 文件中 declare_id 的值。然后运行 deploy 命令将程序部署到 devnet。程序部署完成后,可在终端中使用 test 命令运行测试文件。

这将输出类似如下的结果:

transfer-hook.test.ts:
transfer-hook
Transaction Signature: kB8Hkn8NEavK7xztEhQZXKSeidgEK81PZNmgSSodZFVyzM9o18GwNi4bDWD9Q3cbmh75Vn1jqyinYH3YdgJfnuJ
Create Mint Account with Transfer Hook Extension (539ms)
Transaction Signature: Bf9eYieas6jpV8UxS5upuRv2oMebDdHgDstLMw86ptM7cd4qRpaxRyFYmNZC1WZMcDXP68PoGoApUrrrQKeBbJA
Create Token Accounts and Mint Tokens (744ms)
Transaction Signature: 3oRtCjM6oSdkxQKUyGF3r6hmZGLUpNefihHoGQT5cftRPeQtimvVukLPvb3PSpvLrUsoCWBnz6nSm6ZbPRUhx7UP
Create ExtraAccountMetaList Account (728ms)
Transfer Signature: WNAWK2o7wWpVCqPz2uoMtHRe1F5B1jfW8v4kezdQYqaXE3nRAPfqUFkFHg31uYmpZCjncZUwo4g9ZuhgMC9cS1i
Transfer Hook with Extra Account Meta (1327ms)
4 passing (3s)

如果您不想使用 JavaScript 创建代币,也可以在部署程序后,通过 Solana CLI 使用 spl-token 命令:

spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token --transfer-hook yourTransferHookProgramId

Counter Transfer Hook

下一个示例将展示如何在每次代币转账时递增一个计数器。 link

如果您想在 Transfer Hook 中添加需要额外账户的逻辑,则需要将这些账户添加到 ExtraAccountMetaList 账户中。在本例中,我们需要一个 PDA 来保存代币被转账次数的计数。

这可以通过在 initialize_extra_account_meta_list 指令中添加以下代码来实现:

let account_metas = vec![
ExtraAccountMeta::new_with_seeds(
&[Seed::Literal {
bytes: "counter".as_bytes().to_vec(),
}],
false, // is_signer
true, // is_writable
)?,
];

此外,我们还需要在初始化新的 mint account 时创建此账户,并在每次转账代币时传入该账户。

#[derive(Accounts)]
pub struct InitializeExtraAccountMetaList<'info> {
#[account(mut)]
payer: Signer<'info>,
/// CHECK: ExtraAccountMetaList Account, must use these seeds
#[account(
mut,
seeds = [b"extra-account-metas", mint.key().as_ref()],
bump
)]
pub extra_account_meta_list: AccountInfo<'info>,
pub mint: InterfaceAccount<'info, Mint>,
#[account(
init_if_needed,
seeds = [b"counter"],
bump,
payer = payer,
space = 16
)]
pub counter_account: Account<'info, CounterAccount>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct TransferHook<'info> {
#[account(
token::mint = mint,
token::authority = owner,
)]
pub source_token: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
#[account(
token::mint = mint,
)]
pub destination_token: InterfaceAccount<'info, TokenAccount>,
/// CHECK: source token account owner, can be SystemAccount or PDA owned by another program
pub owner: UncheckedAccount<'info>,
/// CHECK: ExtraAccountMetaList Account,
#[account(
seeds = [b"extra-account-metas", mint.key().as_ref()],
bump
)]
pub extra_account_meta_list: UncheckedAccount<'info>,
#[account(
mut,
seeds = [b"counter"],
bump
)]
pub counter_account: Account<'info, CounterAccount>,
}

该账户将持有一个 u64 类型的计数器变量:

#[account]
pub struct CounterAccount {
counter: u64,
}

现在,在我们的 Transfer Hook 函数中,每次被调用时只需将此计数器加一:

pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
ctx.accounts.counter_account.counter.checked_add(1).unwrap();
msg!("This token has been transferred {0} times", ctx.accounts.counter_account.counter);
Ok(())
}

在客户端中,这些额外账户通过辅助函数 createTransferCheckedWithTransferHookInstruction 自动添加:

let transferInstructionWithHelper =
await createTransferCheckedWithTransferHookInstruction(
connection,
sourceTokenAccount,
mint.publicKey,
destinationTokenAccount,
wallet.publicKey,
amountBigInt,
decimals,
[],
"confirmed",
TOKEN_2022_PROGRAM_ID
);

要在 Solana Playground 中运行此示例,请访问以下链接: link

然后在终端中输入 build,这将使用新生成的程序 ID 更新 lib.rs 文件中 declare_id 的值。然后输入 deploy 将程序部署到 devnet。程序部署完成后,可在终端中输入 test 来运行测试文件。

这将输出如下结果。在最后一笔交易中,您将能看到您的代币被转账的次数:

"This token has been transferred 1 times"
Running tests...
transfer-hook.test.ts:
transfer-hook
Transaction Signature: 48r6effAA4B9RVh13eBXdGjmcPKcm6QwnvodX2dT5nNfJyzoS3AejqatKXyqcmpzPdcmpTjgALnd1xx7v17ggptV
Create Mint Account with Transfer Hook Extension (545ms)
Transaction Signature: nfkBH6cbM5c94od3VG4QmxHkXJzm6VEFxogbQKpd7gERJNgESyu1gEjLJnPiUer59sXnx787eB6hYBkhdkFnzdL
Create Token Accounts and Mint Tokens (354ms)
Extra accounts meta: null
Transaction Signature: 4T6FS3Y95Kjkf9fy5jtCYWo2Wf1SSQKmo6GUK2YqXEcgR4Wrr6aLmnoEBcBNCpEv4ALbJuwu5KtVdxb1S3ynMPJY
Create ExtraAccountMetaList Account (695ms)
Extra accounts meta: 9mifVeGPh7CHyf1NrcUWzzVKMU7g3AwQ6L3md3fMNqju
Counter PDa: 334HLdMwbhSGYf8QWHHmEkeZf6x6caXGF6oxVnCEmaQd
Transfer Signature: 32zoL4oTC3XPVsgeDmT3KsTS4v8U4qe3GPKMF72QX5eSHgAFagKEyvRrGuoP2UEGLpj41Ygm9dSRi5YKghxS24EN
Transfer Hook with Extra Account Meta (776ms)
4 passing (2s)

由于我们在每次代币转账时都会递增计数器,因此需要确保 Transfer Hook 指令只能在转账过程中被调用,否则有人可能直接调用 Transfer Hook 指令来篡改我们的计数器。这是您应该添加到所有 Transfer Hook 中的检查项。

您可以像这样添加该检查:

fn assert_is_transferring(ctx: &Context<TransferHook>) -> Result<()> {
let source_token_info = ctx.accounts.source_token.to_account_info();
let mut account_data_ref: RefMut<&mut [u8]> = source_token_info.try_borrow_mut_data()?;
let mut account = PodStateWithExtensionsMut::<PodAccount>::unpack(*account_data_ref)?;
let account_extension = account.get_extension_mut::<TransferHookAccount>()?;
if !bool::from(account_extension.transferring) {
return err!(TransferError::IsNotCurrentlyTransferring);
}
Ok(())
}

然后在您的 transfer_hook 函数开头调用它:

#[error_code]
pub enum TransferError {
#[msg("The token is not currently transferring")]
IsNotCurrentlyTransferring,
}
#[interface(spl_transfer_hook_interface::execute)]
pub fn transfer_hook(ctx: Context<TransferHook>, _amount: u64) -> Result<()> {
// Fail this instruction if it is not called from within a transfer hook
assert_is_transferring(&ctx)?;
ctx.accounts.counter_account.counter.checked_add(1).unwrap();
msg!("This token has been transferred {0} times", ctx.accounts.counter_account.counter);
Ok(())
}

带 wSOL 转账手续费的 Transfer Hook(进阶示例)

在本指南的下一部分,我们将使用 Anchor 框架构建一个更高级的 Transfer Hook 程序。该程序将要求发送方在每次代币转账时支付 wSOL 手续费。

wSOL 转账将通过一个委托方执行,该委托方是从 Transfer Hook 程序派生的 PDA。这是必要的,因为代币转账指令的初始发送方签名在 Transfer Hook 程序中不可访问。

此程序仅包含 3 条指令:

  1. initialize_extra_account_meta_list:创建一个账户,用于存储 transfer_hook 指令所需的额外账户列表。
  2. transfer_hook:此指令在每次代币转账时通过 CPI 调用,用于执行包装 SOL 代币转账。
  3. fallback:Transfer Hook 接口指令具有特定的鉴别器(指令标识符)。在 Anchor 程序中,我们可以使用 fallback 指令手动匹配指令鉴别器并调用我们自定义的 transfer_hook 指令。

此程序将要求发送方在每次代币转账时支付包装 SOL(wSOL)手续费。以下是 最终程序

快速开始

首先打开此 Solana Playground 链接 ,然后点击「Import」按钮复制该项目。

起始代码包含 lib.rstransfer-hook.test.ts 文件,这些文件是我们将要创建的程序的脚手架。在 lib.rs 文件中,您应该能看到以下代码:

use anchor_lang::{
prelude::*,
system_program::{create_account, CreateAccount},
};
use anchor_spl::{
associated_token::AssociatedToken,
token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked},
};
use spl_tlv_account_resolution::{
account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList,
};
use spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction};
declare_id!("E6wu6Nykdra8gXs57Zqo7hY6DLaWugTmD3uuuBmX2Vxt");
#[program]
pub mod transfer_hook {
use super::*;
pub fn initialize_extra_account_meta_list(
ctx: Context<InitializeExtraAccountMetaList>,
) -> Result<()> {
Ok(())
}
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
Ok(())
}
pub fn fallback<'info>(
program_id: &Pubkey,
accounts: &'info [AccountInfo<'info>],
data: &[u8],
) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializeExtraAccountMetaList {}
#[derive(Accounts)]
pub struct TransferHook {}

导入项目后,在 Playground 终端中使用 build 命令构建程序。

build

这将使用新生成的程序 ID 更新 lib.rs 文件中 declare_id 的值。

初始化 ExtraAccountMetas 账户指令

在此步骤中,我们将为 Transfer Hook 程序实现 initialize_extra_account_meta_list 指令。该指令创建一个 ExtraAccountMetas 账户,用于存储 transfer_hook 指令所需的额外账户。

在此示例中,initialize_extra_account_meta_list 指令需要 7 个账户:

  • payer:用于支付创建 ExtraAccountMetas 账户费用的账户。
  • extra_account_meta_list:创建用于存储 transfer_hook 指令所需账户列表的 ExtraAccountMetas 账户。
  • mint:指向此 Transfer Hook 程序的 mint account。mint 地址是派生 extra_account_meta_list PDA 的必需 seed。
  • wsol_mint:包装 SOL 的 mint。
  • token_program:原始 Token Program ID。
  • associated_token_program:Associated Token Program ID。
  • system_program:System Program,在创建新账户时是必需账户。

mintwsol_mintassociated_token_program 的地址将用于派生 wSOL associated token account 的地址。这些账户由 transfer_hook 指令所需,并将存储在 ExtraAccountMetas 账户中。

通过替换以下起始代码来更新 InitializeExtraAccountMetaList 结构体:

#[derive(Accounts)]
pub struct InitializeExtraAccountMetaList {}

替换为以下提供的代码:

#[derive(Accounts)]
pub struct InitializeExtraAccountMetaList<'info> {
#[account(mut)]
payer: Signer<'info>,
/// CHECK: ExtraAccountMetaList Account, must use these seeds
#[account(
mut,
seeds = [b"extra-account-metas", mint.key().as_ref()],
bump
)]
pub extra_account_meta_list: AccountInfo<'info>,
pub mint: InterfaceAccount<'info, Mint>,
pub wsol_mint: InterfaceAccount<'info, Mint>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}

接下来,通过替换以下起始代码来更新 initialize_extra_account_meta_list 指令:

pub fn initialize_extra_account_meta_list(
ctx: Context<InitializeExtraAccountMetaList>,
) -> Result<()> {
Ok(())
}

替换为以下代码:

pub fn initialize_extra_account_meta_list(
ctx: Context<InitializeExtraAccountMetaList>,
) -> Result<()> {
// index 0-3 are the accounts required for token transfer (source, mint, destination, owner)
// index 4 is address of ExtraAccountMetaList account
// The `addExtraAccountsToInstruction` JS helper function resolving incorrectly
let account_metas = vec![
// index 5, wrapped SOL mint
ExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?,
// index 6, token program
ExtraAccountMeta::new_with_pubkey(&ctx.accounts.token_program.key(), false, false)?,
// index 7, associated token program
ExtraAccountMeta::new_with_pubkey(
&ctx.accounts.associated_token_program.key(),
false,
false,
)?,
// index 8, delegate PDA
ExtraAccountMeta::new_with_seeds(
&[Seed::Literal {
bytes: "delegate".as_bytes().to_vec(),
}],
false, // is_signer
false, // is_writable
)?,
// index 9, delegate wrapped SOL token account
ExtraAccountMeta::new_external_pda_with_seeds(
7, // associated token program index
&[
Seed::AccountKey { index: 8 }, // owner index (delegate PDA)
Seed::AccountKey { index: 6 }, // token program index
Seed::AccountKey { index: 5 }, // wsol mint index
],
false, // is_signer
true, // is_writable
)?,
// index 10, sender wrapped SOL token account
ExtraAccountMeta::new_external_pda_with_seeds(
7, // associated token program index
&[
Seed::AccountKey { index: 3 }, // owner index
Seed::AccountKey { index: 6 }, // token program index
Seed::AccountKey { index: 5 }, // wsol mint index
],
false, // is_signer
true, // is_writable
)?,
];
// calculate account size
let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64;
// calculate minimum required lamports
let lamports = Rent::get()?.minimum_balance(account_size as usize);
let mint = ctx.accounts.mint.key();
let signer_seeds: &[&[&[u8]]] = &[&[
b"extra-account-metas",
&mint.as_ref(),
&[ctx.bumps.extra_account_meta_list],
]];
// create ExtraAccountMetaList account
create_account(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.extra_account_meta_list.to_account_info(),
},
)
.with_signer(signer_seeds),
lamports,
account_size,
ctx.program_id,
)?;
// initialize ExtraAccountMetaList account with extra accounts
ExtraAccountMetaList::init::<ExecuteInstruction>(
&mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,
&account_metas,
)?;
Ok(())
}

让我们逐步了解更新后的指令逻辑。我们首先列出需要存储在 ExtraAccountMetas 账户中的额外账户。

// index 0-3 are the accounts required for token transfer (source, mint, destination, owner)
// index 4 is address of ExtraAccountMetaList account
// The `addExtraAccountsToInstruction` JS helper function resolving incorrectly
let account_metas = vec![
// index 5, wrapped SOL mint
ExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?,
// index 6, token program
ExtraAccountMeta::new_with_pubkey(&ctx.accounts.token_program.key(), false, false)?,
// index 7, associated token program
ExtraAccountMeta::new_with_pubkey(
&ctx.accounts.associated_token_program.key(),
false,
false,
)?,
// index 8, delegate PDA
ExtraAccountMeta::new_with_seeds(
&[Seed::Literal {
bytes: "delegate".as_bytes().to_vec(),
}],
false, // is_signer
true, // is_writable
)?,
// index 9, delegate wrapped SOL token account
ExtraAccountMeta::new_external_pda_with_seeds(
7, // associated token program index
&[
Seed::AccountKey { index: 8 }, // owner index (delegate PDA)
Seed::AccountKey { index: 6 }, // token program index
Seed::AccountKey { index: 5 }, // wsol mint index
],
false, // is_signer
true, // is_writable
)?,
// index 10, sender wrapped SOL token account
ExtraAccountMeta::new_external_pda_with_seeds(
7, // associated token program index
&[
Seed::AccountKey { index: 3 }, // owner index
Seed::AccountKey { index: 6 }, // token program index
Seed::AccountKey { index: 5 }, // wsol mint index
],
false, // is_signer
true, // is_writable
)?,
];

存储这些账户有三种方式:

  1. 直接存储账户地址:
    • 包装 SOL 的 mint 地址
    • Token Program ID
    • Associated Token Program ID
// index 5, wrapped SOL mint
ExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?,
// index 6, token program
ExtraAccountMeta::new_with_pubkey(&ctx.accounts.token_program.key(), false, false)?,
// index 7, associated token program
ExtraAccountMeta::new_with_pubkey(
&ctx.accounts.associated_token_program.key(),
false,
false,
)?,
  1. 存储用于为 Transfer Hook 程序派生 PDA 的 seed:
    • 委托方 PDA
// index 8, delegate PDA
ExtraAccountMeta::new_with_seeds(
&[Seed::Literal {
bytes: "delegate".as_bytes().to_vec(),
}],
false, // is_signer
false, // is_writable
)?,
  1. 存储用于为Transfer Hook程序以外的程序派生PDA的seed:
    • 委托人 wSOL Associated Token Account
    • 发送方 wSOL Associated Token Account
// index 9, delegate wrapped SOL token account
ExtraAccountMeta::new_external_pda_with_seeds(
7, // associated token program index
&[
Seed::AccountKey { index: 8 }, // owner index (delegate PDA)
Seed::AccountKey { index: 6 }, // token program index
Seed::AccountKey { index: 5 }, // wsol mint index
],
false, // is_signer
true, // is_writable
)?,
// index 10, sender wrapped SOL token account
ExtraAccountMeta::new_external_pda_with_seeds(
7, // associated token program index
&[
Seed::AccountKey { index: 3 }, // owner index
Seed::AccountKey { index: 6 }, // token program index
Seed::AccountKey { index: 5 }, // wsol mint index
],
false, // is_signer
true, // is_writable
)?,

接下来,我们计算存储ExtraAccountMetas列表所需的大小和rent。

// calculate account size
let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64;
// calculate minimum required lamports
let lamports = Rent::get()?.minimum_balance(account_size as usize);

接下来,我们向System Program发起CPI,创建一个账户并将Transfer Hook Program设置为所有者。由于我们使用PDA作为新账户的地址,因此在CPI中将PDA的seed作为签名seed包含在内。

let mint = ctx.accounts.mint.key();
let signer_seeds: &[&[&[u8]]] = &[&[
b"extra-account-metas",
&mint.as_ref(),
&[ctx.bumps.extra_account_meta_list],
]];
// create ExtraAccountMetaList account
create_account(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.extra_account_meta_list.to_account_info(),
},
)
.with_signer(signer_seeds),
lamports,
account_size,
ctx.program_id,
)?;

创建账户后,我们初始化账户数据以存储ExtraAccountMetas列表。

// initialize ExtraAccountMetaList account with extra accounts
ExtraAccountMetaList::init::<ExecuteInstruction>(
&mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,
&account_metas,
)?;

在本示例中,我们没有使用Transfer Hook接口来创建ExtraAccountMetas账户。

自定义Transfer Hook指令

接下来,让我们实现自定义的transfer_hook指令。这是Token Extension程序在每次代币转账时调用的指令。

在本示例中,我们将要求每次代币转账时支付一笔以wSOL计价的费用。为简化起见,费用金额等于代币转账金额。

将以下初始代码替换,以更新TransferHook结构体:

#[derive(Accounts)]
pub struct TransferHook {}

替换为以下更新后的代码:

请注意,此结构体中账户的顺序非常重要。这是Token Extensions程序在CPI调用此Transfer Hook程序时提供账户的顺序。

// Order of accounts matters for this struct.
// The first 4 accounts are the accounts required for token transfer (source, mint, destination, owner)
// Remaining accounts are the extra accounts required from the ExtraAccountMetaList account
// These accounts are provided via CPI to this program from the token2022 program
#[derive(Accounts)]
pub struct TransferHook<'info> {
#[account(
token::mint = mint,
token::authority = owner,
)]
pub source_token: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
#[account(
token::mint = mint,
)]
pub destination_token: InterfaceAccount<'info, TokenAccount>,
/// CHECK: source token account owner, can be SystemAccount or PDA owned by another program
pub owner: UncheckedAccount<'info>,
/// CHECK: ExtraAccountMetaList Account,
#[account(
seeds = [b"extra-account-metas", mint.key().as_ref()],
bump
)]
pub extra_account_meta_list: UncheckedAccount<'info>,
pub wsol_mint: InterfaceAccount<'info, Mint>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
#[account(
seeds = [b"delegate"],
bump
)]
pub delegate: SystemAccount<'info>,
#[account(
mut,
token::mint = wsol_mint,
token::authority = delegate,
)]
pub delegate_wsol_token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
token::mint = wsol_mint,
token::authority = owner,
)]
pub sender_wsol_token_account: InterfaceAccount<'info, TokenAccount>,
}

前4个账户是初始代币转账所需的账户。

#[account(
token::mint = mint,
token::authority = owner,
)]
pub source_token: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
#[account(
token::mint = mint,
)]
pub destination_token: InterfaceAccount<'info, TokenAccount>,
/// CHECK: source token account owner, can be SystemAccount or PDA owned by another program
pub owner: UncheckedAccount<'info>,

第5个账户是ExtraAccountMeta账户的地址,该账户存储了我们transfer_hook指令所需的额外账户列表。

/// CHECK: ExtraAccountMetaList Account
#[account(
seeds = [b"extra-account-metas", mint.key().as_ref()],
bump
)]
pub extra_account_meta_list: UncheckedAccount<'info>,

其余账户是ExtraAccountMetas账户中列出的账户,顺序与我们在initialize_extra_account_meta_list指令中定义的顺序一致。

pub wsol_mint: InterfaceAccount<'info, Mint>,
pub token_program: Interface<'info, TokenInterface>,
pub associated_token_program: Program<'info, AssociatedToken>,
#[account(
mut,
seeds = [b"delegate"],
bump
)]
pub delegate: SystemAccount<'info>,
#[account(
mut,
token::mint = wsol_mint,
token::authority = delegate,
)]
pub delegate_wsol_token_account: InterfaceAccount<'info, TokenAccount>,
#[account(
mut,
token::mint = wsol_mint,
token::authority = owner,
)]
pub sender_wsol_token_account: InterfaceAccount<'info, TokenAccount>,

接下来,将以下初始代码替换,以更新transfer_hook指令:

pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
Ok(())
}

替换为以下更新后的代码:

// Require SOL fee on transfer, lamport fee is equal to transfer amount
// If this fails, the initial token transfer fails
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
msg!("Transfer WSOL using delegate PDA");
let signer_seeds: &[&[&[u8]]] = &[&[b"delegate", &[ctx.bumps.delegate]]];
// transfer WSOL from sender to delegate token account using delegate PDA
transfer_checked(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
TransferChecked {
from: ctx.accounts.sender_wsol_token_account.to_account_info(),
mint: ctx.accounts.wsol_mint.to_account_info(),
to: ctx.accounts.delegate_wsol_token_account.to_account_info(),
authority: ctx.accounts.delegate.to_account_info(),
},
)
.with_signer(signer_seeds),
amount,
ctx.accounts.wsol_mint.decimals,
)?;
Ok(())
}

在指令逻辑中,我们发起CPI从发送方的wSOL token account转出wSOL。此次转账由委托人PDA签名授权。每次代币转账时,发送方必须先授权委托人转账相应金额。

Fallback指令

最后,我们需要在Anchor程序中添加一条fallback指令,以处理来自Token Extensions程序的CPI。

此步骤是必要的,因为Anchor生成指令鉴别器的方式与Transfer Hook接口指令所使用的方式存在差异。transfer_hook指令的指令鉴别器与Transfer Hook接口的鉴别器不匹配。

将以下初始代码替换,以更新fallback指令:

pub fn fallback<'info>(
program_id: &Pubkey,
accounts: &'info [AccountInfo<'info>],
data: &[u8],
) -> Result<()> {
Ok(())
}

替换为以下更新后的代码:

// fallback instruction handler as workaround to anchor instruction discriminator check
pub fn fallback<'info>(
program_id: &Pubkey,
accounts: &'info [AccountInfo<'info>],
data: &[u8],
) -> Result<()> {
let instruction = TransferHookInstruction::unpack(data)?;
// match instruction discriminator to transfer hook interface execute instruction
// token2022 program CPIs this instruction on token transfer
match instruction {
TransferHookInstruction::Execute { amount } => {
let amount_bytes = amount.to_le_bytes();
// invoke custom transfer hook instruction on our program
__private::__global::transfer_hook(program_id, accounts, &amount_bytes)
}
_ => return Err(ProgramError::InvalidInstructionData.into()),
}
}

fallback指令会检查传入指令的指令鉴别器是否与Transfer Hook接口中的Execute指令匹配。如果匹配成功,则调用我们Anchor程序中的transfer_hook指令。

目前,Anchor有一个尚未发布的功能可以简化此流程,届时将无需fallback指令。

构建并部署程序

Transfer Hook程序现已完成。请确保您的Playground钱包中有足够的Devnet SOL以部署该程序。

要构建程序,请使用以下命令:

build

接下来,使用以下命令部署程序:

deploy

测试文件概览

接下来,让我们测试该程序。打开transfer-hook.test.ts文件,您应该会看到以下初始代码:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { TransferHook } from "../target/types/transfer_hook";
import {
PublicKey,
SystemProgram,
Transaction,
sendAndConfirmTransaction,
Keypair,
} from "@solana/web3.js";
import {
ExtensionType,
TOKEN_2022_PROGRAM_ID,
getMintLen,
createInitializeMintInstruction,
createInitializeTransferHookInstruction,
addExtraAccountsToInstruction,
ASSOCIATED_TOKEN_PROGRAM_ID,
createAssociatedTokenAccountInstruction,
createMintToInstruction,
createTransferCheckedInstruction,
getAssociatedTokenAddressSync,
createApproveInstruction,
createSyncNativeInstruction,
NATIVE_MINT,
TOKEN_PROGRAM_ID,
getAccount,
getOrCreateAssociatedTokenAccount,
} from "@solana/spl-token";
import assert from "assert";
describe("transfer-hook", () => {
// Configure the client to use the local cluster.
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.TransferHook as Program<TransferHook>;
const wallet = provider.wallet as anchor.Wallet;
const connection = provider.connection;
// Generate keypair to use as address for the transfer-hook enabled mint
const mint = new Keypair();
const decimals = 9;
// Sender token account address
const sourceTokenAccount = getAssociatedTokenAddressSync(
mint.publicKey,
wallet.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
// Recipient token account address
const recipient = Keypair.generate();
const destinationTokenAccount = getAssociatedTokenAddressSync(
mint.publicKey,
recipient.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
// ExtraAccountMetaList address
// Store extra accounts required by the custom transfer hook instruction
const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync(
[Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()],
program.programId
);
// PDA delegate to transfer wSOL tokens from sender
const [delegatePDA] = PublicKey.findProgramAddressSync(
[Buffer.from("delegate")],
program.programId
);
// Sender wSOL token account address
const senderWSolTokenAccount = getAssociatedTokenAddressSync(
NATIVE_MINT, // mint
wallet.publicKey // owner
);
// Delegate PDA wSOL token account address, to receive wSOL tokens from sender
const delegateWSolTokenAccount = getAssociatedTokenAddressSync(
NATIVE_MINT, // mint
delegatePDA, // owner
true // allowOwnerOffCurve
);
// Create the two WSol token accounts as part of setup
before(async () => {
// WSol Token Account for sender
await getOrCreateAssociatedTokenAccount(
connection,
wallet.payer,
NATIVE_MINT,
wallet.publicKey
);
// WSol Token Account for delegate PDA
await getOrCreateAssociatedTokenAccount(
connection,
wallet.payer,
NATIVE_MINT,
delegatePDA,
true
);
});
it("Create Mint Account with Transfer Hook Extension", async () => {});
it("Create Token Accounts and Mint Tokens", async () => {});
it("Create ExtraAccountMetaList Account", async () => {});
it("Transfer Hook with Extra Account Meta", async () => {});
});

首先,我们生成一个keypair,用作新Mint Account的地址。利用mint地址,我们派生出将用于代币转账的Associated Token Account(ATA)地址。

// Generate keypair to use as address for the transfer-hook enabled mint
const mint = new Keypair();
const decimals = 9;
// Sender token account address
const sourceTokenAccount = getAssociatedTokenAddressSync(
mint.publicKey,
wallet.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
// Recipient token account address
const recipient = Keypair.generate();
const destinationTokenAccount = getAssociatedTokenAddressSync(
mint.publicKey,
recipient.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);

接下来,我们为ExtraAccountMetas账户派生PDA。该账户用于存储自定义transfer hook指令所需的额外账户。

// ExtraAccountMetaList address
// Store extra accounts required by the custom transfer hook instruction
const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync(
[Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()],
program.programId
);

我们还需派生一个用作委托人的PDA。发送方必须将此地址授权为其wSOL token account的委托人。该委托人PDA在自定义transfer hook指令中用于为wSOL转账"签名"。

// PDA delegate to transfer wSOL tokens from sender
const [delegatePDA] = PublicKey.findProgramAddressSync(
[Buffer.from("delegate")],
program.programId
);

此外,我们还需派生wSOL token account的地址。第一个地址对应发送方的wSOL token account,需充值资金以支付transfer hook指令所需的转账费用。第二个地址对应由委托人PDA拥有的wSOL token account。在本示例中,所有wSOL费用均发送至该账户。

// Sender wSOL token account address
const senderWSolTokenAccount = getAssociatedTokenAddressSync(
NATIVE_MINT, // mint
wallet.publicKey // owner
);
// Delegate PDA wSOL token account address, to receive wSOL tokens from sender
const delegateWSolTokenAccount = getAssociatedTokenAddressSync(
NATIVE_MINT, // mint
delegatePDA, // owner
true // allowOwnerOffCurve
);

最后,作为设置的一部分,我们创建wSOL token account。

// Create the two WSol token accounts as part of setup
before(async () => {
// WSol Token Account for sender
await getOrCreateAssociatedTokenAccount(
connection,
wallet.payer,
NATIVE_MINT,
wallet.publicKey
);
// WSol Token Account for delegate PDA
await getOrCreateAssociatedTokenAccount(
connection,
wallet.payer,
NATIVE_MINT,
delegatePDA,
true
);
});

创建Mint Account

首先,构建一笔交易,创建一个启用了Transfer Hook扩展的新Mint Account。在此交易中,请务必将我们的程序指定为扩展中存储的Transfer Hook程序。

启用Transfer Hook扩展后,Transfer Extension程序可以确定在每次代币转账时调用哪个程序。

替换占位测试:

it("Create Mint Account with Transfer Hook Extension", async () => {});

替换为以下更新后的测试:

it("Create Mint Account with Transfer Hook Extension", async () => {
const extensions = [ExtensionType.TransferHook];
const mintLen = getMintLen(extensions);
const lamports =
await provider.connection.getMinimumBalanceForRentExemption(mintLen);
const transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: mint.publicKey,
space: mintLen,
lamports: lamports,
programId: TOKEN_2022_PROGRAM_ID
}),
createInitializeTransferHookInstruction(
mint.publicKey,
wallet.publicKey,
program.programId, // Transfer Hook Program ID
TOKEN_2022_PROGRAM_ID
),
createInitializeMintInstruction(
mint.publicKey,
decimals,
wallet.publicKey,
null,
TOKEN_2022_PROGRAM_ID
)
);
const txSig = await sendAndConfirmTransaction(
provider.connection,
transaction,
[wallet.payer, mint]
);
console.log(`Transaction Signature: ${txSig}`);
});

创建Token Account

接下来,作为设置的一部分,为发送方和接收方分别创建Associated Token Account,并向发送方账户充入一些代币。

替换占位测试:

it("Create Token Accounts and Mint Tokens", async () => {});

替换为以下更新后的测试:

// Create the two token accounts for the transfer-hook enabled mint
// Fund the sender token account with 100 tokens
it("Create Token Accounts and Mint Tokens", async () => {
// 100 tokens
const amount = 100 * 10 ** decimals;
const transaction = new Transaction().add(
createAssociatedTokenAccountInstruction(
wallet.publicKey,
sourceTokenAccount,
wallet.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
),
createAssociatedTokenAccountInstruction(
wallet.publicKey,
destinationTokenAccount,
recipient.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
),
createMintToInstruction(
mint.publicKey,
sourceTokenAccount,
wallet.publicKey,
amount,
[],
TOKEN_2022_PROGRAM_ID
)
);
const txSig = await sendAndConfirmTransaction(
connection,
transaction,
[wallet.payer],
{ skipPreflight: true }
);
console.log(`Transaction Signature: ${txSig}`);
});

创建ExtraAccountMeta账户

在发送代币转账之前,我们需要创建ExtraAccountMetas账户,以存储transfer hook指令所需的所有额外账户。

要创建此账户,我们调用程序中的相应指令。

替换占位测试:

it("Create ExtraAccountMetaList Account", async () => {});

替换为以下更新后的测试:

// Account to store extra accounts required by the transfer hook instruction
it("Create ExtraAccountMetaList Account", async () => {
const initializeExtraAccountMetaListInstruction = await program.methods
.initializeExtraAccountMetaList()
.accounts({
payer: wallet.publicKey,
extraAccountMetaList: extraAccountMetaListPDA,
mint: mint.publicKey,
wsolMint: NATIVE_MINT,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID
})
.instruction();
const transaction = new Transaction().add(
initializeExtraAccountMetaListInstruction
);
const txSig = await sendAndConfirmTransaction(
provider.connection,
transaction,
[wallet.payer],
{ skipPreflight: true }
);
console.log("Transaction Signature:", txSig);
});

转账代币

最后,我们准备好发送代币转账。除转账指令外,还需包含几条额外指令。

  • 发送方必须向其wSOL token account转入SOL,以支付transfer hook指令所需的费用。
  • 发送方必须为wSOL费用金额授权委托人PDA。
  • 包含一条同步wSOL余额的指令。
  • 代币转账指令必须包含transfer hook指令所需的所有额外账户。

替换占位测试:

it("Transfer Hook with Extra Account Meta", async () => {});

替换为以下更新后的测试:

it("Transfer Hook with Extra Account Meta", async () => {
// 1 tokens
const amount = 1 * 10 ** decimals;
const amountBigInt = BigInt(amount);
// Instruction for sender to fund their WSol token account
const solTransferInstruction = SystemProgram.transfer({
fromPubkey: wallet.publicKey,
toPubkey: senderWSolTokenAccount,
lamports: amount
});
// Approve delegate PDA to transfer WSol tokens from sender WSol token account
const approveInstruction = createApproveInstruction(
senderWSolTokenAccount,
delegatePDA,
wallet.publicKey,
amount,
[],
TOKEN_PROGRAM_ID
);
// Sync sender WSol token account
const syncWrappedSolInstruction = createSyncNativeInstruction(
senderWSolTokenAccount
);
// This helper function will automatically derive all the additional accounts that were defined in the ExtraAccountMetas account
let transferInstructionWithHelper =
await createTransferCheckedWithTransferHookInstruction(
connection,
sourceTokenAccount,
mint.publicKey,
destinationTokenAccount,
wallet.publicKey,
amountBigInt,
decimals,
[],
"confirmed",
TOKEN_2022_PROGRAM_ID
);
const transaction = new Transaction().add(
solTransferInstruction,
syncWrappedSolInstruction,
approveInstruction,
transferInstructionWithHelper
);
const txSig = await sendAndConfirmTransaction(
connection,
transaction,
[wallet.payer],
{ skipPreflight: true }
);
console.log("Transfer Signature:", txSig);
});

转账指令必须包含所有额外的AccountMeta、ExtraAccountMetas账户的地址以及Transfer Hook程序的地址。

运行测试文件

更新所有测试后,最后一步是运行测试。

要运行测试文件,请在终端中使用以下命令:

test

您应该会看到类似如下的输出:

Running tests...
transfer-hook.test.ts:
transfer-hook
Transaction Signature: 5o12ZTvcSkV8YNqyeQpzRCq4zFSg9VqguQkT9ZSesioj8uzb8dWRheoknuPaRDDqEGdrUBqmRQ2veSUshUicWsqG
✔ Create Mint Account with Transfer Hook Extension (996ms)
Transaction Signature: 4F4Vhi8s1h2reDr6jecvuQFF5XpoofWPpshgAMnfg7jtNZj4HtxbsksFTh28ZjYTaKFpjeturYZKxk5Cj4gBZoy
✔ Create Token Accounts and Mint Tokens (716ms)
Transaction Signature: 3s4Nok6H4qexpGXup3AWC4nGuiqy567rm5rTWLFMXYKxZJensBVVZHCwVDzpwD3XtWjMFHm4TrvQXwKSsp47y5jx
✔ Create ExtraAccountMetaList Account (711ms)
Transfer Signature: 53j9QV5LYUVgV7T7Z99GfYg1Xvp2qbQnHsJbzDK6BR5TPBo9s622KCf3W3BDEL4ECprkZFs5biDRDedfVj6zuDA6
✔ Transfer Hook with Extra Account Meta (925ms)
4 passing (5s)

在transfer hook中使用token account数据

有时您可能希望使用账户数据来派生额外账户中的附加账户。例如,如果您想将token account的所有者作为PDA的seed,这将非常有用。

在创建ExtraAccountMeta时,您可以使用任意账户的数据作为额外seed。在本示例中,我们希望从token account所有者和字符串'counter'派生一个计数器账户。这意味着我们将始终能够看到该token account所有者转账代币的次数。

以下是在extra_account_metas()函数中进行设置的方法。

// Define extra account metas to store on extra_account_meta_list account
impl<'info> InitializeExtraAccountMetaList<'info> {
pub fn extra_account_metas() -> Result<Vec<ExtraAccountMeta>> {
Ok(
vec![
ExtraAccountMeta::new_with_seeds(
&[
Seed::Literal {
bytes: b"counter".to_vec(),
},
Seed::AccountData { account_index: 0, data_index: 32, length: 32 },
],
false, // is_signer
true // is_writable
)?
]
)
}
}

让我们来看一下token account结构体,以了解账户数据的存储方式。下面是一个token account结构的示例。我们可以取位置32到64的32个字节作为token account的所有者,对应'account_index: 0'。'account_index'表示该账户在账户数组中的索引。在transfer hook的情况下,所有者token account是账户数组中的第一个条目,第二个账户始终是mint,第三个账户是目标token account。此账户顺序与旧版token program中的顺序相同。

/// Account data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Account {
/// The mint associated with this account
pub mint: Pubkey,
/// The owner of this account.
pub owner: Pubkey,
/// The amount of tokens this account holds.
pub amount: u64,
pub delegate: COption<Pubkey>,
pub state: AccountState,
pub is_native: COption<u64>,
pub delegated_amount: u64,
pub close_authority: COption<Pubkey>,
}

在我们的示例中,我们希望从发送方token account的所有者派生一个计数器账户,因此在创建ExtraAccountMeta账户时,我们init这个由发送方token account所有者和字符串'counter'派生的PDA计数器账户。初始化PDA计数器账户后,我们便可以在transfer hook中使用它,在每次转账时对其值进行递增。

struct.
```rust
#[derive(Accounts)]
pub struct InitializeExtraAccountMetaList<'info> {
#[account(mut)]
payer: Signer<'info>,
/// CHECK: ExtraAccountMetaList Account, must use these seeds
#[account(
init,
seeds = [b"extra-account-metas", mint.key().as_ref()],
bump,
space = ExtraAccountMetaList::size_of(
InitializeExtraAccountMetaList::extra_account_metas()?.len()
)?,
payer = payer
)]
pub extra_account_meta_list: AccountInfo<'info>,
pub mint: InterfaceAccount<'info, Mint>,
#[account(init, seeds = [b"counter", payer.key().as_ref()], bump, payer = payer, space = 16)]
pub counter_account: Account<'info, CounterAccount>,
pub token_program: Program<'info, Token2022>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}

我们还需要在TransferHook结构体中定义这个额外的计数器账户。这些是每次执行转账时传递给TransferHook程序的账户。客户端从ExtraAccountsMetaList PDA获取这些额外账户,并将其包含在代币转账指令中,但在程序中我们仍需对其进行定义。

#[derive(Accounts)]
pub struct TransferHook<'info> {
#[account(token::mint = mint, token::authority = owner)]
pub source_token: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
#[account(token::mint = mint)]
pub destination_token: InterfaceAccount<'info, TokenAccount>,
/// CHECK: source token account owner, can be SystemAccount or PDA owned by another program
pub owner: UncheckedAccount<'info>,
/// CHECK: ExtraAccountMetaList Account,
#[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)]
pub extra_account_meta_list: UncheckedAccount<'info>,
#[account(seeds = [b"counter", owner.key().as_ref()], bump)]
pub counter_account: Account<'info, CounterAccount>,
}

在客户端中,此账户是自动生成的,您可以按如下方式使用它。

const transferInstructionWithHelper =
await createTransferCheckedWithTransferHookInstruction(
connection,
sourceTokenAccount,
mint.publicKey,
destinationTokenAccount,
wallet.publicKey,
amountBigInt,
decimals,
[],
"confirmed",
TOKEN_2022_PROGRAM_ID
);

该辅助函数会从ExtraAccounts数据账户中自动解析该账户。在客户端中,账户的解析方式如下:

const [counterPDA] = PublicKey.findProgramAddressSync(
[Buffer.from("counter"), wallet.publicKey.toBuffer()],
program.programId
);

请注意,计数器账户是从token account的所有者派生的,需要在执行转账之前完成初始化。在本示例中,我们在初始化extra account metas时初始化计数器账户。因此,我们只会为调用该函数的token account所有者创建一个计数器PDA。如果您希望为您的mint下的每个token account都创建计数器账户,则需要提前实现创建这些PDA的功能。您可以在dapp上设置一个"注册计数器"按钮,用于创建该PDA账户,此后用户便可使用这个计数器代币。

总结

Transfer Hook扩展和Transfer Hook接口允许创建在每次代币转账时执行自定义指令逻辑的Mint Account。本指南可作为参考,帮助您创建自己的Transfer Hook程序。欢迎发挥创意,探索这项新功能的各种可能性!

Is this page helpful?

©️ 2026 Solana 基金会版权所有