Transfer Hook uzantısı nasıl kullanılır

Transfer Hook uzantısı ve Transfer Hook Interface, her token transferinde özel instruction mantığını çalıştıran Mint Account'lar oluşturma imkanı sunar.

Bu, token transferleri için birçok yeni kullanım senaryosunun önünü açar, örneğin:

  • NFT telif ücretlerini zorunlu kılma
  • Token alabilecek cüzdanlar için kara veya beyaz liste oluşturma
  • Token transferlerinde özel ücretler uygulama
  • Özel token transfer olayları oluşturma
  • Token transferlerinize ait istatistikleri takip etme
  • Ve çok daha fazlası

Bunu başarmak için geliştiricilerin Transfer Hook Interface'i uygulayan bir program oluşturması ve Transfer Hook uzantısı etkinleştirilmiş bir Mint Account başlatması gerekir.

Mint Account'a ait tokenları içeren her token transferinde, Token Extensions programı Transfer Hook programı üzerindeki bir instruction'ı çalıştırmak için Cross Program Invocation (CPI) yapar.

Token Extensions programı bir Transfer Hook programına CPI yaptığında, ilk transferdeki tüm hesaplar salt okunur hesaplara dönüştürülür. Bu, göndericinin imza yetkilerinin Transfer Hook programına geçmediği anlamına gelir.

Bu tasarım kararı, Transfer Hook programlarının kötü amaçlı kullanımını önlemek için alınmıştır.

Bu rehberde Anchor framework'ünü kullanarak bir Transfer Hook programı oluşturacağız; ancak Transfer Hook Interface'i yerel (native) bir program ile de uygulamak mümkündür. Anchor framework hakkında daha fazla bilgiyi burada bulabilirsiniz: Anchor Framework

Transfer Hook Interface'e Genel Bakış

Transfer Hook Interface, geliştiricilerin belirli bir Mint Account için her token transferinde çalıştırılan özel instruction mantığı uygulamasının bir yolunu sunar.

Transfer Hook Interface aşağıdaki instruction'ları tanımlar:

  • Execute: Token Extensions programının her token transferinde çağırdığı bir instruction.
  • InitializeExtraAccountMetaList (isteğe bağlı): Özel Execute instruction'ının ihtiyaç duyduğu ek hesapların listesini saklayan bir hesap oluşturur.
  • UpdateExtraAccountMetaList (isteğe bağlı): Mevcut listenin üzerine yazarak ek hesaplar listesini günceller.

InitializeExtraAccountMetaList instruction'ını interface üzerinden uygulamak teknik olarak zorunlu değildir. Hesap, bir Transfer Hook programındaki herhangi bir instruction ile oluşturulabilir.

Ancak hesabın Program Derived Address (PDA) adresi, aşağıdaki seed'ler kullanılarak türetilmelidir:

  • Sabit kodlanmış "extra-account-metas" dizesi
  • Mint Account adresi
  • Transfer Hook programının ID'si
const [pda] = await PublicKey.findProgramAddress(
[Buffer.from("extra-account-metas"), mint.publicKey.toBytes()],
program.programId // transfer hook program ID
);

Execute instruction'ının ihtiyaç duyduğu ek hesapları önceden tanımlanmış PDA'da saklayarak, bu hesaplar istemciden bir token transfer instruction'ına otomatik olarak eklenebilir.

Hello-world Transfer hook

Bu örnek, transfer hook'ların hello world'üdür. Her token transferinde sadece bir mesaj yazdıran basit bir transfer hook'tur. Başlangıç olarak örneği, solana programları oluşturmak ve dağıtmak için kullanılan çevrimiçi bir araç olan Solana Playground'da açıyoruz: link

Örnek, transfer hook interface'ini uygulayan bir Anchor programından ve programı test etmek için bir test dosyasından oluşur.

Bu program yalnızca 3 instruction içerecektir:

  1. initialize_extra_account_meta_list: transfer_hook instruction'ının ihtiyaç duyduğu ek hesapların listesini saklayan bir hesap oluşturur. Hello world örneğinde bunu boş bırakıyoruz.
  2. transfer_hook: Bu instruction, her token transferinde CPI ile çağrılır ve sarmalanmış SOL token transferi gerçekleştirir.
  3. fallback: Anchor kullandığımız ve Token Program yerel (native) bir program olduğu için, instruction discriminator'ını elle eşleştirmek ve özel transfer_hook instruction'ımızı çağırmak üzere bir fallback instruction eklememiz gerekir. Bu fonksiyonu değiştirmenize gerek yok.

Token her transfer edildiğinde bu transfer_hook fonksiyonu Token Program tarafından çağrılacaktır.

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

Bu fonksiyona artık ek mantığınızı ekleyebilirsiniz. Örneğin, 50'den büyük bir miktar transfer edildiğinde transferin başarısız olmasını şu şekilde sağlayabilirsiniz:

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

Örneği Solana Playground'da çalıştırmak için şu bağlantıyı izleyin: link

Playground'un terminalinde build komutunu çalıştırın; bu komut lib.rs dosyasındaki declare_id değerini yeni oluşturulan bir program ID'si ile güncelleyecektir. Ardından programınızı devnet'e dağıtmak için deploy komutunu çalıştırın. Program dağıtıldığında, terminalde test komutunu kullanarak test dosyasını çalıştırabilirsiniz.

Bu size aşağıdakine benzer bir çıktı verecektir:

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)

Tokenınızı oluşturmak için javascript kullanmak istemiyorsanız, programınızı dağıttıktan sonra Solana CLI'daki spl-token komutunu da kullanabilirsiniz:

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

Counter Transfer hook

Sonraki örnek, tokenınız her transfer edildiğinde bir sayacı nasıl artırabileceğinizi gösterecek. link

Transfer hook'unuza ek hesaplar gerektiren mantık eklemek istiyorsanız, bunları ExtraAccountMetaList hesabına eklemeniz gerekir. Buradaki örneğimizde, tokenın kaç kez transfer edildiğinin sayısını tutan bir PDA istiyoruz.

Bu, initialize_extra_account_meta_list instruction'ına aşağıdaki kod eklenerek yapılabilir:

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

Ayrıca yeni mint account'u başlatırken bu hesabı da oluşturmamız ve token her transfer edildiğinde bu hesabı geçirmemiz gerekir.

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

Hesap, u64 türünde bir sayaç değişkeni tutacaktır:

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

Artık transfer hook fonksiyonumuzda, her çağrıldığında bu sayacı bir artırabiliriz:

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

İstemcide bu ek hesaplar bir yardımcı fonksiyon tarafından otomatik olarak eklenir: web3.js v3 için @solana-program/token-2022 içindeki getTransferCheckedWithTransferHookInstructionAsync, ya da eski web3.js için @solana/spl-token içindeki createTransferCheckedWithTransferHookInstruction. v3 yardımcısı bir Kit RPC alır; bu nedenle Connection ile aynı endpoint için @solana/kit içindeki createSolanaRpc ile bir tane oluşturun.

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

Örneği Solana Playground'da çalıştırmak için şu bağlantıyı izleyin: link

Ardından orada build yazın; bu, lib.rs dosyasındaki declare_id değerini yeni oluşturulan bir program ID'si ile güncelleyecektir. Sonra programınızı devnet'e dağıtmak için deploy yazın. Program dağıtıldığında, terminalde test yazarak test dosyasını çalıştırabilirsiniz.

Bu size aşağıdaki çıktıyı verecektir. Son transaction'da tokenınızın kaç kez transfer edildiğini görebileceksiniz:

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

Burada token her transfer edildiğinde bir sayacı artırdığımız için, transfer hook instruction'ının yalnızca bir transfer sırasında çağrılabildiğinden emin olmamız gerekir; aksi takdirde biri transfer hook instruction'ını doğrudan çağırıp sayacımızı bozabilir. Bu, tüm transfer hook'larınıza eklemeniz gereken bir kontroldür.

Kontrolü şu şekilde ekleyebilirsiniz:

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

Ardından transfer_hook fonksiyonunuzun başında çağırın:

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

wSOL Transfer Ücretli Transfer Hook (ileri seviye örnek)

Rehberin bir sonraki bölümünde, Anchor framework'ünü kullanarak daha gelişmiş bir Transfer Hook programı oluşturacağız. Bu program, göndericinin her token transferi için wSOL ücreti ödemesini zorunlu kılacak.

wSOL transferleri, Transfer Hook programından türetilmiş bir PDA olan bir delegate kullanılarak gerçekleştirilecek. Bunun nedeni, token transfer instruction'ının ilk göndericisine ait imzaya Transfer Hook programında erişilememesidir.

Bu program yalnızca 3 instruction içerecektir:

  1. initialize_extra_account_meta_list: transfer_hook instruction'ının ihtiyaç duyduğu ek hesapların listesini saklayan bir hesap oluşturur.
  2. transfer_hook: Bu instruction, her token transferinde CPI ile çağrılır ve sarmalanmış SOL token transferi gerçekleştirir.
  3. fallback: Transfer hook interface instruction'larının belirli discriminator'ları (instruction tanımlayıcıları) vardır. Bir Anchor programında, instruction discriminator'ını elle eşleştirmek ve özel transfer_hook instruction'ımızı çağırmak için bir fallback instruction kullanabiliriz.

Bu program, göndericinin her token transferinde sarmalanmış SOL (wSOL) cinsinden bir ücret ödemesini zorunlu kılacak. İşte nihai program.

Başlarken

Bu Solana Playground linkini açarak başlayın, ardından projeyi kopyalamak için "Import" düğmesine tıklayın.

Başlangıç kodu, oluşturacağımız program için iskeleti hazırlanmış bir lib.rs ve transfer-hook.test.ts dosyası içerir. lib.rs dosyasında aşağıdaki kodu görmelisiniz:

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

Projeyi içe aktardıktan sonra, Playground terminalinde build komutunu kullanarak programı derleyin.

build

Bu, lib.rs dosyasındaki declare_id değerini yeni oluşturulan bir program ID'si ile güncelleyecektir.

ExtraAccountMetas Hesabını Başlatma Instruction'ı

Bu adımda, Transfer Hook programımız için initialize_extra_account_meta_list instruction'ını uygulayacağız. Bu instruction, transfer_hook instruction'ımızın ihtiyaç duyduğu ek hesapları saklayacak bir ExtraAccountMetas hesabı oluşturur.

Bu örnekte initialize_extra_account_meta_list instruction'ı 7 hesap gerektirir:

  • payer: ExtraAccountMetas hesabının oluşturulma ücretini ödemek için kullanılan hesap.
  • extra_account_meta_list: transfer_hook instruction'ımızın ihtiyaç duyduğu hesapların listesini saklamak için oluşturulan ExtraAccountMetas hesabı.
  • mint: Bu Transfer Hook programına işaret eden Mint Account. Mint adresi, extra_account_meta_list PDA'sını türetmek için gerekli bir seed'dir.
  • wsol_mint: Sarmalanmış SOL mint'i.
  • token_program: Orijinal Token Program ID'si
  • associated_token_program: Associated Token Program ID'si.
  • system_program: Yeni hesaplar oluşturulurken gerekli bir hesap olan System Program.

mint, wsol_mint ve associated_token_program adresleri, wSOL Associated Token Account adreslerini türetmek için kullanılacak. Bu hesaplar transfer_hook instruction'ı için gereklidir ve ExtraAccountMetas hesabında saklanacaktır.

Aşağıdaki başlangıç kodunu değiştirerek InitializeExtraAccountMetaList struct'ını güncelleyin:

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

Aşağıda verilen kodla:

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

Ardından, aşağıdaki başlangıç kodunu değiştirerek initialize_extra_account_meta_list instruction'ını güncelleyin:

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

Aşağıdaki kod ile:

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

Güncellenen instruction mantığını inceleyelim. Öncelikle ExtraAccountMetas hesabında saklanması gereken ek hesapları listeleyerek başlıyoruz.

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

Bu hesapları saklamak için üç yöntem vardır:

  1. Hesap adresini doğrudan saklayın:
    • Wrapped SOL mint adresi
    • Token Program ID'si
    • Associated Token Program ID'si
// index 5, wrapped SOL mint
ExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?,
// index 6, token program
ExtraAccountMeta::new_with_pubkey(&ctx.accounts.token_program.key(), false, false)?,
// index 7, associated token program
ExtraAccountMeta::new_with_pubkey(
&ctx.accounts.associated_token_program.key(),
false,
false,
)?,
  1. Transfer Hook programı için bir PDA türetmek üzere seed'leri saklayın:
    • 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. Transfer Hook programı dışındaki bir program için PDA türetmek üzere seed'leri saklayın:
    • Delegate wSOL Associated Token Account
    • Gönderen 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
)?,

Ardından, ExtraAccountMetas listesini saklamak için gereken boyutu ve rent'i hesaplıyoruz.

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

Ardından, bir hesap oluşturmak ve sahibi olarak Transfer Hook Program'ı ayarlamak için System Program'a bir CPI yapıyoruz. Yeni hesabın adresi olarak PDA'yı kullandığımız için PDA seed'leri CPI üzerinde signer seed olarak dahil edilir.

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

Hesabı oluşturduktan sonra, ExtraAccountMetas listesini saklamak için hesap verilerini başlatıyoruz.

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

Bu örnekte, ExtraAccountMetas hesabını oluşturmak için Transfer Hook arayüzünü kullanmıyoruz.

Özel Transfer Hook Instruction'ı

Şimdi özel transfer_hook instruction'ını uygulayalım. Token Extensions programının her token transferinde çağıracağı instruction budur.

Bu örnekte, her token transferi için wSOL ile ödenen bir ücret talep edeceğiz. Basitlik açısından ücret tutarı, token transfer tutarına eşittir.

Aşağıdaki başlangıç kodunu değiştirerek TransferHook struct'ını güncelleyin:

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

Aşağıdaki güncellenmiş kod ile:

Bu struct'taki hesapların sırasının önemli olduğunu unutmayın. Token Extensions programı, bu Transfer Hook programına CPI yaptığında hesapları bu sırayla sağlar.

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

İlk 4 hesap, başlangıçtaki token transferi için gereken hesaplardır.

#[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>,
  1. hesap, transfer_hook instruction'ımızın ihtiyaç duyduğu ek hesapların listesini saklayan ExtraAccountMeta hesabının adresidir.
/// CHECK: ExtraAccountMetaList Account
#[account(
seeds = [b"extra-account-metas", mint.key().as_ref()],
bump
)]
pub extra_account_meta_list: UncheckedAccount<'info>,

Kalan hesaplar, ExtraAccountMetas hesabında initialize_extra_account_meta_list instruction'ında tanımladığımız sırayla listelenen hesaplardır.

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

Ardından, aşağıdaki başlangıç kodunu değiştirerek transfer_hook instruction'ını güncelleyin:

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

Aşağıdaki güncellenmiş kod ile:

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

Instruction mantığı içinde, göndericinin wSOL token account'undan wSOL aktarmak için bir CPI yapıyoruz. Bu transfer, delegate PDA kullanılarak imzalanır. Her token transferi için gönderici önce delegate'i transfer tutarı kadar onaylamalıdır.

Fallback Instruction

Son olarak, Token Extensions programından gelen CPI'ı işlemek için Anchor programına bir fallback instruction eklememiz gerekiyor.

Bu adım, Anchor'ın instruction discriminator'larını oluşturma biçimi ile Transfer Hook arayüzü instruction'larında kullanılanlar arasındaki farktan dolayı gereklidir. transfer_hook instruction'ının discriminator'ı, Transfer Hook arayüzündeki ile eşleşmeyecektir.

Aşağıdaki başlangıç kodunu değiştirerek fallback instruction'ını güncelleyin:

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

Aşağıdaki güncellenmiş kod ile:

// 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, gelen bir instruction'ın discriminator'ının Transfer Hook arayüzündeki Execute instruction'ı ile eşleşip eşleşmediğini kontrol eder. Başarılı bir eşleşme olursa Anchor programımızdaki transfer_hook instruction'ını çağırır.

Şu anda bu süreci basitleştiren, henüz yayımlanmamış bir Anchor özelliği bulunuyor. Bu özellik fallback instruction ihtiyacını ortadan kaldıracak.

Programı Derleme ve Dağıtma

Transfer Hook programı artık tamamlandı. Programı dağıtmak için Playground cüzdanınızda yeterli Devnet SOL olduğundan emin olun.

Programı derlemek için aşağıdaki komutu kullanın:

build

Ardından programı şu komutla dağıtın:

deploy

Test Dosyasına Genel Bakış

Şimdi programı test edelim. transfer-hook.test.ts dosyasını açın; aşağıdaki başlangıç kodunu görmelisiniz:

Aşağıdaki test dosyası, Anchor'ın TypeScript istemcisi (AnchorProvider, program.methods ve sunduğu Wallet) eski web3.js türleri üzerine kurulu olduğundan, eski web3.js (@solana/web3.js v1) ve @solana/spl-token kullanır. web3.js v3 ve @solana-program/token-2022 ile yazılmış istemci tarafı gönderim yolu için Transfer Hook Entegrasyon Kılavuzu'na bakın.

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

Öncelikle, yeni bir Mint Account için adres olarak kullanılacak bir keypair oluşturuyoruz. Mint adresini kullanarak, token transferi için kullanacağımız Associated Token Account (ATA) adreslerini türetiyoruz.

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

Ardından ExtraAccountMetas hesabı için PDA'yı türetiyoruz. Bu hesap, özel transfer hook instruction'ı için gereken ek hesapları saklamak üzere oluşturulur.

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

Ayrıca delegate olarak kullanılacak PDA'yı da türetiyoruz. Gönderici, bu adresi wSOL token account'u için delegate olarak onaylamalıdır. Bu delegate PDA, özel transfer hook instruction'ında wSOL transferini "imzalamak" için kullanılır.

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

Ek olarak, wSOL token account'larının adreslerini türetiyoruz. İlk adres, göndericinin wSOL token account'una aittir ve transfer hook instruction'ının gerektirdiği transfer ücretini ödemek için fonlanması gerekir. İkinci adres ise delegate PDA'ya ait wSOL token account'udur. Bu örnekte tüm wSOL ücretleri bu hesaba gönderilir.

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

Son olarak, kurulumun bir parçası olarak wSOL token account'larını oluşturuyoruz.

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

Mint Account Oluşturma

Başlamak için, Transfer Hook extension'ı etkinleştirilmiş yeni bir Mint Account oluşturacak bir transaction hazırlayın. Bu transaction'da, extension'da saklanan Transfer Hook programı olarak programımızı belirttiğinizden emin olun.

Transfer Hook extension'ını etkinleştirmek, Transfer Extension programının her token transferinde hangi programı çağıracağını belirlemesini sağlar.

Yer tutucu testi değiştirin:

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

Aşağıdaki güncellenmiş test ile:

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'ları Oluşturma

Ardından, kurulumun bir parçası olarak hem gönderici hem de alıcı için Associated Token Account'ları oluşturun. Ayrıca göndericinin hesabını bir miktar token ile fonlayın.

Yer tutucu testi değiştirin:

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

Aşağıdaki güncellenmiş test ile:

// 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 Hesabı Oluşturma

Bir token transferi göndermeden önce, transfer hook instruction'ının gerektirdiği tüm ek hesapları saklamak için ExtraAccountMetas hesabını oluşturmamız gerekir.

Bu hesabı oluşturmak için programımızdaki instruction'ı çağırıyoruz.

Yer tutucu testi değiştirin:

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

Aşağıdaki güncellenmiş test ile:

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

Token Transferi

Son olarak, bir token transferi göndermeye hazırız. Transfer instruction'ına ek olarak dahil edilmesi gereken birkaç instruction daha vardır.

  • Gönderici, transfer hook instruction'ının gerektirdiği ücreti karşılamak için wSOL token account'una SOL aktarmalıdır.
  • Gönderici, wSOL ücreti tutarı için delegate PDA'yı onaylamalıdır.
  • wSOL bakiyesini senkronize eden bir instruction ekleyin.
  • Token transfer instruction'ı, transfer hook instruction'ının gerektirdiği tüm ek hesapları içermelidir.

Yer tutucu testi değiştirin:

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

Aşağıdaki güncellenmiş test ile:

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

Transfer instruction'ı; tüm ek AccountMeta'ları, ExtraAccountMetas hesabının adresini ve Transfer Hook programının adresini içermelidir.

Test Dosyasını Çalıştırma

Tüm testleri güncelledikten sonra son adım testi çalıştırmaktır.

Test dosyasını çalıştırmak için terminalde aşağıdaki komutu kullanın:

test

Aşağıdakine benzer bir çıktı görmelisiniz:

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

Transfer hook'ta token account verilerini kullanma

Bazen extra account meta'lardaki ek hesapları türetmek için hesap verilerini kullanmak isteyebilirsiniz. Örneğin, token account'un sahibini bir PDA için seed olarak kullanmak istediğinizde bu işe yarar.

ExtraAccountMeta'yı oluştururken herhangi bir hesabın verisini ek seed olarak kullanabilirsiniz. Bu örnekte, token account sahibinden ve 'counter' dizesinden bir counter hesabı türetmek istiyoruz. Bu, ilgili token account sahibinin ne sıklıkla token transfer ettiğini her zaman görebileceğimiz anlamına gelir.

extra_account_metas() fonksiyonunda bunu şöyle ayarlarsınız.

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

Hesap verilerinin nasıl saklandığını anlamak için token account struct'ına bakalım. Aşağıda bir token account yapısı örneği verilmiştir. Böylece 32. ile 64. pozisyon arasındaki 32 byte'ı, 'account_index: 0' konumundaki token account'un sahibi olarak alabiliriz. 'account_index', hesabın accounts dizisindeki indeksini ifade eder. Transfer hook durumunda, sahip token account'u accounts dizisindeki ilk girdidir. İkinci hesap her zaman mint, üçüncü hesap ise hedef token account'tur. Bu hesap sırası eski token programındakiyle aynıdır.

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

Bizim durumumuzda, gönderici token account'un sahibinden bir counter hesabı türetmek istiyoruz. Bu yüzden ExtraAccountMeta hesaplarını oluştururken, gönderici token account sahibinden ve 'counter' dizesinden türetilen bu PDA counter hesabını init ediyoruz. PDA counter hesabı başlatıldığında, her transferde değeri artırmak için transfer hook içinde kullanabileceğiz.

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

Bu ek counter hesabını TransferHook struct'ında da tanımlamamız gerekir. Bunlar, her transfer yapıldığında TransferHook programımıza iletilen hesaplardır. İstemci bu ek hesapları ExtraAccountsMetaList PDA'sından alır ve token transfer instruction'ına dahil eder, ancak programda yine de bunu tanımlamamız gerekir.

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

İstemcide bu hesap otomatik olarak oluşturulur ve şu şekilde kullanabilirsiniz.

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

Yardımcı fonksiyon, hesabı ExtraAccounts veri hesabından otomatik olarak çözümler. Hesabın istemcide nasıl çözümleneceği şöyledir:

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

Counter hesabının token account'un sahibinden türetildiğini ve transfer yapmadan önce başlatılması gerektiğini unutmayın. Bu örnekte counter hesabını extra account meta'ları başlatırken başlatıyoruz. Dolayısıyla yalnızca ilgili fonksiyonu çağıran token account sahibi için bir counter PDA'sı olacak. Mint'iniz için var olan her token account'a bir counter hesabı istiyorsanız, bu PDA'ları önceden oluşturacak bir işlevsellik sağlamanız gerekir. Dapp'inizde, bu PDA hesabını oluşturan bir counter'a kaydolma butonu olabilir; ardından kullanıcılar bu counter token'ı kullanabilir.

Sonuç

Transfer Hook uzantısı ve Transfer Hook Interface, her token transferinde özel talimat mantığı yürüten Mint Account'ların oluşturulmasına olanak tanır. Bu rehber, kendi Transfer Hook programlarınızı oluşturmanıza yardımcı olacak bir referans niteliğindedir. Yaratıcı olmaktan ve bu yeni işlevselliğin sunduğu imkânları keşfetmekten çekinmeyin!

Is this page helpful?

© 2026 Solana Vakfı. Tüm hakları saklıdır.