Transfer Hook -laajennuksen käyttäminen

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

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

  • NFT-rojaltien pakottaminen
  • Tokeneita vastaanottavien lompakoiden estolistat tai sallittujen lista
  • Mukautettujen maksujen toteuttaminen token-siirroille
  • Mukautettujen token-siirtotapahtumien luominen
  • Token-siirtojen tilastojen seuraaminen
  • Ja monia muita

Tämän toteuttamiseksi kehittäjien täytyy rakentaa ohjelma, joka toteuttaa Transfer Hook Interfacen ja alustaa Mint Account, jossa Transfer Hook -laajennus on käytössä.

Jokaisen sellaisen token-siirron yhteydessä, jossa siirretään Mint Accountin tokeneita, Token Extensions program tekee Cross Program Invocation (CPI) -kutsun suorittaakseen käskyn 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ä suunnittelupäätös on tehty Transfer Hook -ohjelmien väärinkäytön estämiseksi.

Tässä oppaassa luomme Transfer Hook -ohjelman Anchor-kehyksellä. Transfer Hook Interface on kuitenkin mahdollista toteuttaa myös natiiviohjelmalla. Lue lisää Anchor-kehyksestä täältä: Anchor Framework

Transfer Hook Interfacen yleiskatsaus

Transfer Hook Interface tarjoaa kehittäjille tavan toteuttaa mukautettua käskylogiikkaa, joka suoritetaan jokaisen tietyn Mint Accountin token-siirron yhteydessä.

Transfer Hook Interface määrittelee seuraavat käskyt:

  • Execute: Käsky, jonka Token Extensions program kutsuu jokaisen token-siirron yhteydessä.
  • InitializeExtraAccountMetaList (valinnainen): Luo tilin, joka tallentaa luettelon mukautetun Execute-käskyn tarvitsemista lisätileistä.
  • UpdateExtraAccountMetaList (valinnainen): Päivittää lisätilien luettelon korvaamalla olemassa olevan luettelon.

InitializeExtraAccountMetaList-käskyn toteuttaminen rajapinnan avulla ei ole tekninen vaatimus. Tilin voi luoda mikä tahansa Transfer Hook -ohjelman käsky.

Tilin Program Derived Address (PDA) täytyy kuitenkin johtaa käyttäen seuraavia seed-arvoja:

  • Kovakoodattu merkkijono "extra-account-metas"
  • Mint Accountin 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-käskyn tarvitsemat lisätilit ennalta määritettyyn PDA:han nämä tilit voidaan lisätä automaattisesti token-siirtokäskyyn 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 Interfacen, sekä testitiedostosta ohjelman testaamiseen.

Tämä ohjelma sisältää vain 3 käskyä:

  1. initialize_extra_account_meta_list: Luo tilin, joka tallentaa luettelon transfer_hook-käskyn tarvitsemista lisätileistä. Hello world -esimerkissä jätämme sen tyhjäksi.
  2. transfer_hook: Tämä käsky kutsutaan CPI:n kautta jokaisen token-siirron yhteydessä wrapped SOL -token-siirron suorittamiseksi.
  3. fallback: Koska käytämme Anchoria ja Token Program on natiiviohjelma, meidän täytyy lisätä fallback-käsky, joka täsmää käskyn discriminatorin manuaalisesti ja kutsuu mukautettua transfer_hook-käskyämme. 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 nyt lisätä oman lisälogiikkasi. Voisit 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ämän linkin kautta: linkki

Aja Playgroundin terminaalissa build-komento, joka päivittää declare_id-arvon lib.rs-tiedostossa uudella, generoidulla ohjelma-ID:llä. Aja 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 tulosteen, joka on samankaltainen kuin tämä:

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ä tokenisi luomiseen, voit ohjelmasi julkaisemisen jälkeen käyttää myös Solana CLI:n spl-token-komentoa:

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

Counter Transfer Hook

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

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

Tämä voidaan tehdä lisäämällä seuraava koodi initialize_extra_account_meta_list-käskyyn:

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

Meidän täytyy myös luoda tämä tili, kun alustamme uuden Mint Accountin, ja välittää se mukaan joka kerta, kun tokenia siirretään.

#[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-tyyppisen laskurimuuttujan:

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

Nyt transfer hook -funktiossamme voimme kasvattaa tätä laskuria yhdellä joka kerta, 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 paketista @solana-program/token-2022 web3.js v3:lla tai createTransferCheckedWithTransferHookInstruction paketista @solana/spl-token vanhalla web3.js:llä. V3-apufunktio ottaa parametrikseen Kit RPC:n, joten luo sellainen samalle päätepisteelle kuin Connection käyttämällä createSolanaRpc-funktiota paketista @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ämän linkin kautta: 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 tokeniasi 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 täytyy varmistaa, että transfer hook -käskyä voi kutsua vain siirron aikana. Muuten joku voisi kutsua transfer hook -käskyä suoraan ja sotkea laskurimme. Tämä on tarkistus, joka sinun 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 vaatii lähettäjää maksamaan wSOL-maksun jokaisesta token-siirrosta.

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

Tämä ohjelma sisältää vain 3 käskyä:

  1. initialize_extra_account_meta_list: Luo tilin, joka tallentaa luettelon transfer_hook-käskyn tarvitsemista lisätileistä.
  2. transfer_hook: Tämä käsky kutsutaan CPI:n kautta jokaisen token-siirron yhteydessä wrapped SOL -token-siirron suorittamiseksi.
  3. fallback: Transfer hook interfacen käskyillä on omat discriminatorinsa (käskytunnisteet). Anchor-ohjelmassa voimme käyttää fallback-käskyä täsmäämään käskyn discriminatorin manuaalisesti ja kutsumaan mukautettua transfer_hook-käskyämme.

Tämä ohjelma vaatii lähettäjää maksamaan maksun wrapped SOL (wSOL) -tokenina jokaisesta token-siirrosta. Tässä on valmis ohjelma.

Aloittaminen

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 pohjustettu 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 Playgroundin terminaalissa build-komennolla.

build

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

ExtraAccountMetas-tilin alustuskäsky

Tässä vaiheessa toteutamme Transfer Hook -ohjelmamme initialize_extra_account_meta_list-käskyn. Tämä käsky luo ExtraAccountMetas-tilin, joka tallentaa transfer_hook-käskymme tarvitsemat lisätilit.

Tässä esimerkissä initialize_extra_account_meta_list-käsky vaatii 7 tiliä:

  • payer: Tili, jolla maksetaan ExtraAccountMetas-tilin luominen.
  • extra_account_meta_list: ExtraAccountMetas-tili, joka luodaan tallentamaan transfer_hook-käskymme tarvitsemien tilien luettelo.
  • mint: Mint Account, joka osoittaa tähän Transfer Hook -ohjelmaan. Mint-osoite on pakollinen seed-arvo extra_account_meta_list-PDA:n johtamiseen.
  • wsol_mint: Wrapped SOL -mint.
  • token_program: Alkuperäisen Token Program -ohjelman ID.
  • associated_token_program: Associated Token Program -ohjelman ID.
  • system_program: System Program, joka on pakollinen tili uusia tilejä luotaessa.

Osoitteita mint, wsol_mint ja associated_token_program käytetään wSOL-tilien (associated token account) osoitteiden johtamiseen. Nämä tilit ovat transfer_hook-käskyn vaatimia, ja ne tallennetaan ExtraAccountMetas-tilille.

Päivitä InitializeExtraAccountMetaList-struct korvaamalla seuraava aloituskoodi:

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

Alla olevalla 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-käsky korvaamalla seuraava aloituskoodi:

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

Seuraavalla 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 instruction-logiikka. Aloitamme listaamalla 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 -mintin 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 seeds, 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 seeds, joilla johdetaan PDA muulle ohjelmalle kuin Transfer Hook -ohjelmalle:
    • Delegaten 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-listan tallentamiseen tarvittavan koon ja rent-määrän.

// 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 Programiin, jolla luodaan tili ja asetetaan Transfer Hook -ohjelma omistajaksi. PDA seeds sisällytetään CPI-kutsuun signer seeds -arvoina, koska PDA:ta käytetään 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-listan.

// 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 -instruction

Toteutetaan seuraavaksi mukautettu transfer_hook-instruction. Token Extensions Program kutsuu tätä instructionia jokaisen token-siirron yhteydessä.

Tässä esimerkissä jokaisesta token-siirrosta vaaditaan wSOL-muodossa maksettava maksu. Yksinkertaisuuden vuoksi maksun suuruus on sama kuin siirrettävä tokenmäärä.

Päivitä TransferHook-struct korvaamalla seuraava aloituskoodi:

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

Alla olevalla päivitetyllä koodilla:

Huomaa, että tilien järjestyksellä tässä structissa on merkitystä. Tässä järjestyksessä Token Extensions Program antaa nämä tilit, 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, joka tallentaa transfer_hook-instructionimme vaatimien lisätilien listan.

/// 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ä listatut tilit siinä järjestyksessä, jossa ne määriteltiin initialize_extra_account_meta_list-instructionissa.

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

Instructionin logiikassa teemme CPI-kutsun, jolla siirretään wSOL:ia lähettäjän wSOL token account -tililtä. Siirto allekirjoitetaan delegate PDA:lla. Jokaisessa token-siirrossa lähettäjän täytyy ensin hyväksyä delegate siirrettävälle määrälle.

Fallback-instruction

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

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

Päivitä fallback-instruction 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-instruction tarkistaa, vastaako saapuvan instructionin discriminator Transfer Hook -rajapinnan Execute-instructionia. Jos vastaavuus löytyy, se kutsuu Anchor-ohjelmamme transfer_hook-instructionia.

Anchorissa on tällä hetkellä julkaisematon ominaisuus, joka yksinkertaistaa tätä prosessia. Se poistaisi tarpeen fallback-instructionille.

Rakenna ja julkaise ohjelma

Transfer Hook -ohjelma on nyt valmis. Varmista, että Playground-lompakossasi on tarpeeksi Devnet-SOL:ia ohjelman julkaisemiseen.

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:ää (@solana/web3.js v1) ja @solana/spl-token-pakettia, koska Anchorin TypeScript-client (AnchorProvider, program.methods ja sen tarjoama Wallet) perustuu legacy web3.js -tyyppeihin. Client-puolen lähetyspolusta, joka on kirjoitettu web3.js v3:lla ja @solana-program/token-2022:lla, katso 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,
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 () => {});
});

Ensin luomme keypairin, jota käytetään uuden Mint Accountin 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 -instructionin 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 täytyy hyväksyä tämä osoite wSOL token accountinsa delegateksi. Tätä delegate PDA:ta käytetään "allekirjoittamaan" wSOL-siirto mukautetussa transfer hook -instructionissa.

// 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, jolle täytyy lisätä varoja, jotta transfer hook -instructionin vaatima siirtomaksu voidaan maksaa. Toinen osoite on delegate PDA:n omistama wSOL token account. 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
);
});

Mint Accountin luominen

Aloita rakentamalla transaktio, joka luo uuden Mint Accountin 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 sallii Token Extensions Programin selvittää, 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. Lisää lisäksi lähettäjän tilille tokeneita.

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

ExtraAccountMeta-tilin luominen

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

Tämän tilin luomiseksi kutsumme ohjelmamme instructionia.

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

Tokenien siirto

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

  • Lähettäjän täytyy siirtää SOL:ia wSOL token accountilleen kattaakseen transfer hook -instructionin vaatiman maksun.
  • Lähettäjän täytyy hyväksyä delegate PDA wSOL-maksun määrälle.
  • Sisällytä instruction wSOL-saldon synkronoimiseksi.
  • Token-siirto-instructionin täytyy sisältää kaikki transfer hook -instructionin 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);
});

Siirto-instructionin täytyy sisältää kaikki lisä-AccountMetat, ExtraAccountMetas-tilin osoite ja Transfer Hook -ohjelman osoite.

Testitiedoston ajaminen

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

Aja testitiedosto antamalla terminaalissa seuraava komento:

test

Sinun pitäisi nähdä seuraavankaltainen 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 voi haluta käyttää lisätilien johtamiseen extra account metoissa. Tästä on hyötyä esimerkiksi silloin, kun haluat käyttää token account -tilin omistajaa PDA:n seedinä.

Kun luot ExtraAccountMetan, voit käyttää minkä tahansa tilin dataa ylimääräisenä seedinä. Tässä tapauksessa haluamme johtaa counter-tilin token account -tilin omistajasta ja merkkijonosta 'counter'. Näin näemme aina, kuinka usein kyseisen token accountin omistaja on siirtänyt tokeneita.

Näin tämä 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 -tilin rakenteesta. Voimme 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. Tilien järjestys on sama kuin vanhassa Token Programissa.

/// 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, teemme init-toiminnon tälle PDA counter -tilille, 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 täytyy määritellä tämä ylimääräinen counter-tili myös TransferHook-structissa. Nämä ovat tilit, jotka välitetään TransferHook-ohjelmallemme aina siirron yhteydessä. Client hakee nämä lisätilit ExtraAccountsMetaList PDA:sta ja sisällyttää ne token-siirto-instructioniin, mutta ohjelmassa ne täytyy silti määritellä.

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

Clientissa tämä tili generoituu automaattisesti, ja sitä voi käyttää 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 clientissa 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, kun alustamme extra account metat. Counter-PDA on siis vain sen token account -tilin omistajalle, joka kutsui kyseistä funktiota. Jos haluat counter-tilin jokaiselle mintillesi kuuluvalle token account -tilille, tarvitset toiminnon näiden PDA:iden luomiseen etukäteen. Dapp-sovelluksessasi voisi olla painike, 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 mahdollistavat sellaisten Mint Account -tilien luomisen, jotka suorittavat mukautettua ohjelmalogiikkaa jokaisen token-siirron yhteydessä. Tämä opas toimii apuna ja viitteenä, kun luot omia Transfer Hook -ohjelmiasi. Anna luovuutesi lentää ja tutustu rohkeasti tämän uuden toiminnallisuuden mahdollisuuksiin!

Is this page helpful?

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