Transfer Hook -laajennuksen käyttäminen

Transfer Hook -laajennus ja Transfer Hook Interface mahdollistavat sellaisten Mint Account -tilien luomisen, jotka suorittavat mukautettua instruktiologiikkaa jokaisen token-siirron yhteydessä.

Tämä avaa monia uusia käyttötapauksia token-siirroille, kuten:

  • NFT-rojaltien valvominen
  • Token-vastaanottajien walletien mustalistaus tai sallittujen listaus
  • Mukautettujen maksujen toteuttaminen token-siirtoihin
  • Mukautettujen token-siirtotapahtumien luominen
  • Token-siirtojen tilastojen seuranta
  • Ja monia muita

Tämän toteuttamiseksi kehittäjien on rakennettava ohjelma, joka toteuttaa Transfer Hook Interfacen ja alustettava Mint Account, jossa Transfer Hook -laajennus on otettu käyttöön.

Jokaisen token-siirron yhteydessä, johon liittyy Mint Account -tilin tokeneita, Token Extensions program tekee Cross Program Invocation -kutsun (CPI) suorittaakseen instruktion Transfer Hook -ohjelmassa.

Kun Token Extensions program tekee CPI-kutsun Transfer Hook -ohjelmaan, kaikki alkuperäisen siirron tilit muunnetaan vain luku -tileiksi. Tämä tarkoittaa, että lähettäjän allekirjoitusoikeudet eivät ulotu Transfer Hook -ohjelmaan.

Tämä suunnitteluratkaisu on tehty Transfer Hook -ohjelmien väärinkäytön estämiseksi.

Tässä oppaassa luomme Transfer Hook -ohjelman Anchor-kehyksellä, mutta Transfer Hook Interfacen voi toteuttaa myös natiivilla ohjelmalla. Lisätietoja Anchor-kehyksestä: Anchor Framework

Transfer Hook Interfacen yleiskatsaus

Transfer Hook Interface tarjoaa kehittäjille tavan toteuttaa mukautettua instruktiologiikkaa, joka suoritetaan jokaisen tietyn Mint Account -tilin token-siirron yhteydessä.

Transfer Hook Interface määrittelee seuraavat instruktiot:

  • Execute: Instruktio, jonka Token Extensions program kutsuu jokaisen token-siirron yhteydessä.
  • InitializeExtraAccountMetaList (valinnainen): Luo tilin, joka tallentaa listan mukautetun Execute-instruktion vaatimista lisätileistä.
  • UpdateExtraAccountMetaList (valinnainen): Päivittää lisätilien listan korvaamalla nykyisen listan.

Teknisesti InitializeExtraAccountMetaList-instruktion toteuttaminen rajapinnan kautta ei ole pakollista. Tilin voi luoda mikä tahansa Transfer Hook -ohjelman instruktio.

Tilin Program Derived Address (PDA) on kuitenkin johdettava käyttämällä seuraavia seed-arvoja:

  • Kovakoodattu merkkijono "extra-account-metas"
  • Mint Account -tilin osoite
  • Transfer Hook -ohjelman ID
const [pda] = await PublicKey.findProgramAddress(
[Buffer.from("extra-account-metas"), mint.publicKey.toBytes()],
program.programId // transfer hook program ID
);

Tallentamalla Execute-instruktion vaatimat lisätilit ennalta määritettyyn PDA-osoitteeseen nämä tilit voidaan lisätä automaattisesti token-siirron instruktioon asiakasohjelmassa.

Hello World -Transfer Hook

Tämä esimerkki on transfer hookien Hello World. Se on yksinkertainen transfer hook, joka tulostaa viestin jokaisen token-siirron yhteydessä. Aloitamme avaamalla esimerkin Solana Playgroundissa, verkkotyökalussa Solana-ohjelmien rakentamiseen ja julkaisemiseen: linkki

Esimerkki koostuu Anchor-ohjelmasta, joka toteuttaa transfer hook -rajapinnan, sekä testitiedostosta ohjelman testaamiseen.

Tämä ohjelma sisältää vain 3 instruktiota:

  1. initialize_extra_account_meta_list: Luo tilin, joka tallentaa listan transfer_hook-instruktion vaatimista lisätileistä. Hello World -esimerkissä jätämme sen tyhjäksi.
  2. transfer_hook: Tämä instruktio kutsutaan CPI:n kautta jokaisen token-siirron yhteydessä wrapped SOL -token-siirron suorittamiseksi.
  3. fallback: Koska käytämme Anchoria ja Token Program on natiivi ohjelma, meidän on lisättävä fallback-instruktio, joka täsmää instruktion discriminatorin käsin ja kutsuu mukautettua transfer_hook-instruktiotamme. Tätä funktiota ei tarvitse muuttaa.

Aina kun tokenia siirretään, Token Program kutsuu tätä transfer_hook-funktiota.

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

Tähän funktioon voit lisätä oman lisälogiikkasi. Voit esimerkiksi antaa siirron epäonnistua aina, kun siirrettävä määrä on suurempi kuin 50, näin:

#[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(())
}

Voit ajaa esimerkin Solana Playgroundissa tästä linkistä: linkki

Suorita Playgroundin terminaalissa build-komento, joka päivittää declare_id-arvon lib.rs-tiedostossa uudella, generoidulla ohjelma-ID:llä. Suorita sitten deploy-komento julkaistaksesi ohjelmasi devnetiin. Kun ohjelma on julkaistu, voit ajaa testitiedoston käyttämällä terminaalissa test-komentoa.

Tämän jälkeen saat samankaltaisen tulosteen kuin tässä:

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)

Jos et halua käyttää javascriptiä tokenin luomiseen, voit myös käyttää Solana CLI:n spl-token-komentoa ohjelmasi julkaisemisen jälkeen:

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

Counter Transfer Hook

Seuraava esimerkki näyttää, miten voit kasvattaa laskuria aina, kun tokenia on siirretty. linkki

Jos haluat lisätä transfer hookiisi logiikkaa, joka tarvitsee lisätilejä, sinun on lisättävä ne ExtraAccountMetaList-tiliin. Tässä tapauksessa haluamme PDA:n, joka tallentaa, kuinka monta kertaa tokenia on siirretty.

Tämä onnistuu lisäämällä seuraava koodi initialize_extra_account_meta_list-instruktioon:

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

Meidän on myös luotava tämä tili, kun alustamme uuden mint accountin, ja välitettävä se jokaisen token-siirron yhteydessä.

#[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>,
}

Ja tili sisältää u64-laskurimuuttujan:

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

Nyt transfer hook -funktiossamme voimme yksinkertaisesti kasvattaa tätä laskuria yhdellä aina, kun funktiota kutsutaan:

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(())
}

Asiakasohjelmassa nämä lisätilit lisätään automaattisesti apufunktiolla: getTransferCheckedWithTransferHookInstructionAsync kirjastosta @solana-program/token-2022 web3.js v3:ssa tai createTransferCheckedWithTransferHookInstruction kirjastosta @solana/spl-token vanhassa web3.js:ssä. V3-apufunktio ottaa parametrina Kit RPC:n, joten rakenna sellainen samalle endpointille kuin Connection käyttää, funktiolla createSolanaRpc kirjastosta @solana/kit.

const rpc = createSolanaRpc(connection.rpcEndpoint);
let transferInstructionWithHelper =
await getTransferCheckedWithTransferHookInstructionAsync(
{ rpc },
{
source: sourceTokenAccount.toBase58(),
mint: mint.publicKey.toBase58(),
destination: destinationTokenAccount.toBase58(),
authority: wallet, // a v3 Keypair, passed as the transfer's signer
amount: amountBigInt,
decimals
}
);

Voit ajaa esimerkin Solana Playgroundissa tästä linkistä: linkki

Kirjoita siellä build, joka päivittää declare_id-arvon lib.rs-tiedostossa uudella, generoidulla ohjelma-ID:llä. Kirjoita sitten deploy julkaistaksesi ohjelmasi devnetiin. Kun ohjelma on julkaistu, voit ajaa testitiedoston kirjoittamalla terminaaliin test.

Tämän jälkeen saat seuraavan tulosteen. Viimeisestä transaktiosta näet, kuinka monta kertaa tokenia on siirretty:

"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)

Koska kasvatamme tässä laskuria aina, kun tokenia siirretään, meidän on varmistettava, että transfer hook -instruktiota voi kutsua vain siirron aikana. Muuten kuka tahansa voisi kutsua transfer hook -instruktiota suoraan ja sotkea laskurimme. Tämä on tarkistus, joka kannattaa lisätä kaikkiin transfer hookeihisi.

Voit lisätä tarkistuksen näin:

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(())
}

Ja kutsu sitä transfer_hook-funktiosi alussa:

#[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(())
}

Transfer Hook wSOl-siirtomaksulla (edistynyt esimerkki)

Oppaan seuraavassa osassa rakennamme edistyneemmän Transfer Hook -ohjelman Anchor-kehyksellä. Tämä ohjelma edellyttää, että lähettäjä maksaa wSOL-maksun jokaisesta token-siirrosta.

wSOL-siirrot suoritetaan delegaatilla, joka on Transfer Hook -ohjelmasta johdettu PDA. Tämä on tarpeen, koska token-siirtoinstruktion alkuperäisen lähettäjän allekirjoitus ei ole käytettävissä Transfer Hook -ohjelmassa.

Tämä ohjelma sisältää vain 3 instruktiota:

  1. initialize_extra_account_meta_list: Luo tilin, joka tallentaa listan transfer_hook-instruktion vaatimista lisätileistä.
  2. transfer_hook: Tämä instruktio kutsutaan CPI:n kautta jokaisen token-siirron yhteydessä wrapped SOL -token-siirron suorittamiseksi.
  3. fallback: Transfer hook -rajapinnan instruktioilla on tietyt discriminatorit (instruktioiden tunnisteet). Anchor-ohjelmassa voimme käyttää fallback-instruktiota täsmäämään instruktion discriminatorin käsin ja kutsumaan mukautettua transfer_hook-instruktiotamme.

Tämä ohjelma edellyttää, että lähettäjä maksaa maksun wrapped SOL:ssa (wSOL) jokaisesta token-siirrosta. Tässä on valmis ohjelma.

Aloitus

Aloita avaamalla tämä Solana Playground -linkki ja kopioi projekti napsauttamalla "Import"-painiketta.

Aloituskoodi sisältää lib.rs- ja transfer-hook.test.ts-tiedostot, jotka on runkoistettu luomaamme ohjelmaa varten. lib.rs-tiedostossa pitäisi olla seuraava koodi:

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 {}

Kun olet tuonut projektin, rakenna ohjelma käyttämällä build-komentoa Playgroundin terminaalissa.

build

Tämä päivittää declare_id-arvon lib.rs-tiedostossa uudella, generoidulla ohjelma-ID:llä.

ExtraAccountMetas-tilin alustusinstruktio

Tässä vaiheessa toteutamme Transfer Hook -ohjelmamme initialize_extra_account_meta_list-instruktion. Tämä instruktio luo ExtraAccountMetas-tilin, joka tallentaa transfer_hook-instruktiomme vaatimat lisätilit.

Tässä esimerkissä initialize_extra_account_meta_list-instruktio vaatii 7 tiliä:

  • payer: Tili, jolla maksetaan ExtraAccountMetas-tilin luominen.
  • extra_account_meta_list: ExtraAccountMetas-tili, joka luodaan tallentamaan transfer_hook-instruktiomme vaatimien tilien lista.
  • mint: Mint Account, joka osoittaa tähän Transfer Hook -ohjelmaan. Mint-osoite on pakollinen seed-arvo extra_account_meta_list-PDA:n johtamisessa.
  • wsol_mint: Wrapped SOL -mint.
  • token_program: Alkuperäisen Token Programin ID
  • associated_token_program: Associated Token Programin ID.
  • system_program: System Program, joka on pakollinen tili uusien tilien luomisessa.

Osoitteita mint, wsol_mint ja associated_token_program käytetään johtamaan wSOL associated token account -tilien osoitteet. Nämä tilit ovat transfer_hook-instruktion vaatimia, ja ne tallennetaan ExtraAccountMetas-tilille.

Päivitä InitializeExtraAccountMetaList-struct korvaamalla seuraava aloituskoodi:

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

Alla annetulla koodilla:

#[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>,
}

Päivitä seuraavaksi initialize_extra_account_meta_list-instruktio korvaamalla seuraava aloituskoodi:

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

Alla olevalla koodilla:

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(())
}

Käydään läpi päivitetty instruktion logiikka. Aloitamme luettelemalla lisätilit, jotka täytyy tallentaa ExtraAccountMetas-tilille.

// 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
)?,
];

Näiden tilien tallentamiseen on kolme tapaa:

  1. Tallenna tilin osoite suoraan:
    • Wrapped SOL mint -osoite
    • 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. Tallenna seed-arvot, joilla johdetaan PDA Transfer Hook -ohjelmalle:
    • Delegate-PDA
// index 8, delegate PDA
ExtraAccountMeta::new_with_seeds(
&[Seed::Literal {
bytes: "delegate".as_bytes().to_vec(),
}],
false, // is_signer
false, // is_writable
)?,
  1. Tallenna seed-arvot, joilla johdetaan PDA muulle ohjelmalle kuin Transfer Hook -ohjelmalle:
    • Delegaatin wSOL Associated Token Account
    • Lähettäjän 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
)?,

Seuraavaksi laskemme ExtraAccountMetas-luettelon tallentamiseen tarvittavan koon ja rent-maksun.

// 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);

Seuraavaksi teemme CPI-kutsun System Program -ohjelmaan luodaksemme tilin ja asettaaksemme Transfer Hook -ohjelman omistajaksi. PDA:n seed-arvot sisällytetään CPI-kutsuun allekirjoittajan seed-arvoina, koska käytämme PDA:ta uuden tilin osoitteena.

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,
)?;

Kun tili on luotu, alustamme tilin datan tallentamaan ExtraAccountMetas-luettelon.

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

Tässä esimerkissä emme käytä Transfer Hook -rajapintaa ExtraAccountMetas-tilin luomiseen.

Mukautettu Transfer Hook -instruktio

Toteutetaan seuraavaksi mukautettu transfer_hook-instruktio. Token Extension -ohjelma kutsuu tätä instruktiota jokaisen token-siirron yhteydessä.

Tässä esimerkissä jokaisesta token-siirrosta vaaditaan wSOL-muodossa maksettava maksu. Yksinkertaisuuden vuoksi maksun määrä on sama kuin siirrettävien tokenien määrä.

Päivitä TransferHook-struct korvaamalla seuraava aloituskoodi:

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

Alla olevalla päivitetyllä koodilla:

Huomaa, että tämän structin tilien järjestys on tärkeä. Token Extensions program antaa nämä tilit tässä järjestyksessä, kun se tekee CPI-kutsun tähän Transfer Hook -ohjelmaan.

// 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>,
}

Neljä ensimmäistä tiliä ovat alkuperäisen token-siirron vaatimat tilit.

#[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>,

Viides tili on ExtraAccountMeta-tilin osoite, johon on tallennettu transfer_hook-instruktiomme vaatimien lisätilien luettelo.

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

Loput tilit ovat ExtraAccountMetas-tilillä lueteltuja tilejä siinä järjestyksessä, jossa ne määriteltiin initialize_extra_account_meta_list-instruktiossa.

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>,

Päivitä seuraavaksi transfer_hook-instruktio korvaamalla seuraava aloituskoodi:

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

Alla olevalla päivitetyllä koodilla:

// 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(())
}

Instruktion logiikassa teemme CPI-kutsun, jolla siirretään wSOL lähettäjän wSOL token account -tililtä. Siirto allekirjoitetaan delegate-PDA:lla. Jokaisessa token-siirrossa lähettäjän on ensin hyväksyttävä delegate siirtomäärälle.

Fallback-instruktio

Lopuksi meidän täytyy lisätä Anchor-ohjelmaan fallback-instruktio, joka käsittelee Token Extensions -ohjelman tekemän CPI-kutsun.

Tämä vaihe on tarpeen, koska Anchor generoi instruktioiden discriminatorit eri tavalla kuin Transfer Hook -rajapinnan instruktioissa käytetään. transfer_hook-instruktion discriminator ei vastaa Transfer Hook -rajapinnan discriminatoria.

Päivitä fallback-instruktio korvaamalla seuraava aloituskoodi:

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

Alla olevalla päivitetyllä koodilla:

// 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-instruktio tarkistaa, vastaako saapuvan instruktion discriminator Transfer Hook -rajapinnan Execute-instruktiota. Jos vastaavuus löytyy, se kutsuu Anchor-ohjelmamme transfer_hook-instruktiota.

Anchor-kehyksessä on tällä hetkellä julkaisematon ominaisuus, joka yksinkertaistaa tätä prosessia. Se poistaisi tarpeen fallback-instruktiolle.

Rakenna ja julkaise ohjelma

Transfer Hook -ohjelma on nyt valmis. Varmista, että Playground-lompakossasi on riittävästi Devnet SOL -varoja ohjelman julkaisua varten.

Rakenna ohjelma seuraavalla komennolla:

build

Julkaise seuraavaksi ohjelma komennolla:

deploy

Testitiedoston yleiskatsaus

Testataan seuraavaksi ohjelma. Avaa transfer-hook.test.ts-tiedosto, jossa pitäisi näkyä seuraava aloituskoodi:

Alla oleva testitiedosto käyttää legacy web3.js -kirjastoa (@solana/web3.js v1) ja @solana/spl-token-pakettia, koska TypeScript-asiakasohjelma, jonka Anchor tarjoaa (AnchorProvider, program.methods ja sen tarjoama Wallet), perustuu legacy web3.js -tyyppeihin. Esimerkin client-puolen lähetyspolusta, joka on kirjoitettu web3.js v3 -kirjastolla ja @solana-program/token-2022-paketilla, löydät oppaasta Transfer Hook -integrointiopas.

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,
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 () => {});
});

Ensin generoimme keypair-avainparin, jota käytetään uuden Mint Account -tilin osoitteena. Mint-osoitteen avulla johdamme Associated Token Account (ATA) -osoitteet, joita käytämme token-siirrossa.

// 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
);

Seuraavaksi johdamme ExtraAccountMetas-tilin PDA:n. Tämä tili luodaan tallentamaan mukautetun transfer hook -instruktion vaatimat lisätilit.

// 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
);

Johdamme myös PDA:n, jota käytetään delegatena. Lähettäjän on hyväksyttävä tämä osoite wSOL token account -tilinsä delegatena. Tätä delegate-PDA:ta käytetään "allekirjoittamaan" wSOL-siirto mukautetussa transfer hook -instruktiossa.

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

Lisäksi johdamme wSOL token account -tilien osoitteet. Ensimmäinen osoite on lähettäjän wSOL token account -tilille, joka täytyy rahoittaa, jotta transfer hook -instruktion vaatima siirtomaksu voidaan maksaa. Toinen osoite on delegate-PDA:n omistamalle wSOL token account -tilille. Tässä esimerkissä kaikki wSOL-maksut lähetetään tälle tilille.

// 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
);

Lopuksi osana valmistelua luomme wSOL token account -tilit.

// 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
);
});

Luo Mint Account

Aloita rakentamalla transaktio, joka luo uuden Mint Account -tilin Transfer Hook -laajennus käytössä. Varmista tässä transaktiossa, että määrität ohjelmamme laajennukseen tallennettavaksi Transfer Hook -ohjelmaksi.

Transfer Hook -laajennuksen käyttöönotto mahdollistaa sen, että Transfer Extension -ohjelma voi määrittää, mitä ohjelmaa kutsutaan jokaisen token-siirron yhteydessä.

Korvaa paikkamerkkitesti:

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

Alla olevalla päivitetyllä testillä:

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 -tilien luominen

Luo seuraavaksi osana valmistelua Associated Token Account -tilit sekä lähettäjälle että vastaanottajalle. Rahoita lisäksi lähettäjän tili joillakin tokeneilla.

Korvaa paikkamerkkitesti:

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

Alla olevalla päivitetyllä testillä:

// 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}`);
});

Luo ExtraAccountMeta-tili

Ennen token-siirron lähettämistä meidän täytyy luoda ExtraAccountMetas-tili, johon tallennetaan kaikki transfer hook -instruktion vaatimat lisätilit.

Tämän tilin luomiseksi kutsumme ohjelmamme instruktiota.

Korvaa paikkamerkkitesti:

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

Alla olevalla päivitetyllä testillä:

// 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);
});

Siirrä tokeneita

Nyt olemme valmiita lähettämään token-siirron. Siirtoinstruktion lisäksi mukaan täytyy sisällyttää muutama muu instruktio.

  • Lähettäjän on siirrettävä SOL-varoja wSOL token account -tililleen kattamaan transfer hook -instruktion vaatima maksu.
  • Lähettäjän on hyväksyttävä delegate-PDA wSOL-maksun määrälle.
  • Sisällytä instruktio, joka synkronoi wSOL-saldon.
  • Token-siirtoinstruktioon on sisällytettävä kaikki transfer hook -instruktion vaatimat lisätilit.

Korvaa paikkamerkkitesti:

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

Alla olevalla päivitetyllä testillä:

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);
});

Siirtoinstruktioon on sisällytettävä kaikki ylimääräiset AccountMetat, ExtraAccountMetas-tilin osoite ja Transfer Hook -ohjelman osoite.

Suorita testitiedosto

Kun olet päivittänyt kaikki testit, viimeinen vaihe on testin suorittaminen.

Suorita testitiedosto antamalla päätteessä seuraava komento:

test

Sinun pitäisi nähdä seuraavanlainen tuloste:

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)

Token account -datan käyttäminen transfer hookissa

Joskus tilin dataa halutaan käyttää lisätilien johtamiseen extra account metas -tiedoissa. Tämä on hyödyllistä esimerkiksi silloin, kun token account -tilin omistajaa halutaan käyttää PDA:n seed-arvona.

ExtraAccountMetaa luodessasi voit käyttää minkä tahansa tilin dataa ylimääräisenä seed-arvona. Tässä tapauksessa haluamme johtaa counter-tilin token account -tilin omistajasta ja merkkijonosta 'counter'. Näin voimme aina nähdä, kuinka usein kyseisen token account -tilin omistaja on siirtänyt tokeneita.

Näin se määritetään extra_account_metas()-funktiossa.

// 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
)?
]
)
}
}

Tarkastellaan token account -structia ymmärtääksemme, miten tilin data tallennetaan. Alla on esimerkki token account -rakenteesta. Voimme siis ottaa 32 tavua kohdasta 32–64 token account -tilin omistajaksi, joka on kohdassa 'account_index: 0'. 'account_index' viittaa tilin indeksiin accounts-taulukossa. Transfer hookin tapauksessa omistajan token account on accounts-taulukon ensimmäinen alkio. Toinen tili on aina mint ja kolmas tili on kohde-token account. Tämä tilien järjestys on sama kuin vanhassa Token Program -ohjelmassa.

/// 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>,
}

Meidän tapauksessamme haluamme johtaa counter-tilin lähettäjän token account -tilin omistajasta, joten kun luomme ExtraAccountMeta-tilit, alustamme (init) tämän PDA-counter-tilin, joka johdetaan lähettäjän token account -tilin omistajasta ja merkkijonosta 'counter'. Kun PDA-counter-tili on alustettu, voimme käyttää sitä transfer hookissa arvon kasvattamiseen jokaisessa siirrossa.

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>,
}

Meidän on myös määriteltävä tämä ylimääräinen counter-tili TransferHook-structissa. Nämä ovat tilit, jotka välitetään TransferHook-ohjelmallemme aina, kun siirto tehdään. Asiakas hakee nämä lisätilit ExtraAccountsMetaList-PDA:sta ja sisällyttää ne token-siirtoinstruktioon, mutta ohjelmassa se täytyy silti määrittää.

#[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>,
}

Asiakkaassa tämä tili generoidaan automaattisesti, ja voit käyttää sitä seuraavasti.

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

Apufunktio ratkaisee tilin automaattisesti ExtraAccounts-datatililtä. Tili ratkaistaisiin asiakkaassa seuraavasti:

const [counterPDA] = await PublicKey.findProgramAddress(
[Buffer.from("counter"), wallet.publicKey.toBytes()],
program.programId
);

Huomaa, että counter-tili johdetaan token account -tilin omistajasta, ja se täytyy alustaa ennen siirron tekemistä. Tässä esimerkissä alustamme counter-tilin samalla, kun alustamme extra account metas -tilin. Näin counter-PDA on vain sen token account -tilin omistajalle, joka kutsui kyseistä funktiota. Jos haluat counter-tilin jokaiselle tokenillesi (mint) kuuluvalle token account -tilille, tarvitset toiminnallisuuden, jolla nämä PDA:t luodaan etukäteen. Dappisi voisi sisältää painikkeen, jolla rekisteröidytään counteriin ja joka luo tämän PDA-tilin, ja siitä lähtien käyttäjät voivat käyttää tätä counter-tokenia.

Yhteenveto

Transfer Hook -laajennus ja Transfer Hook Interface -rajapinta mahdollistavat sellaisten Mint Account -tilien luomisen, jotka suorittavat mukautettua ohjelmalogiikkaa jokaisen tokensiirron yhteydessä. Tämä opas toimii apuna omien Transfer Hook -ohjelmiesi luomisessa. Käytä rohkeasti luovuuttasi ja tutustu tämän uuden toiminnallisuuden mahdollisuuksiin!

Is this page helpful?

© 2026 Solana Foundation. Kaikki oikeudet pidätetään.