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ı): ÖzelExecuteinstruction'ı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:
initialize_extra_account_meta_list:transfer_hookinstruction'ının ihtiyaç duyduğu ek hesapların listesini saklayan bir hesap oluşturur. Hello world örneğinde bunu boş bırakıyoruz.transfer_hook: Bu instruction, her token transferinde CPI ile çağrılır ve sarmalanmış SOL token transferi gerçekleştirir.fallback: Anchor kullandığımız ve Token Program yerel (native) bir program olduğu için, instruction discriminator'ını elle eşleştirmek ve özeltransfer_hookinstruction'ı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-hookTransaction 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_signertrue, // 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 programpub 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 signeramount: 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-hookTransaction Signature: 48r6effAA4B9RVh13eBXdGjmcPKcm6QwnvodX2dT5nNfJyzoS3AejqatKXyqcmpzPdcmpTjgALnd1xx7v17ggptV✔ Create Mint Account with Transfer Hook Extension (545ms)Transaction Signature: nfkBH6cbM5c94od3VG4QmxHkXJzm6VEFxogbQKpd7gERJNgESyu1gEjLJnPiUer59sXnx787eB6hYBkhdkFnzdL✔ Create Token Accounts and Mint Tokens (354ms)Extra accounts meta: nullTransaction Signature: 4T6FS3Y95Kjkf9fy5jtCYWo2Wf1SSQKmo6GUK2YqXEcgR4Wrr6aLmnoEBcBNCpEv4ALbJuwu5KtVdxb1S3ynMPJY✔ Create ExtraAccountMetaList Account (695ms)Extra accounts meta: 9mifVeGPh7CHyf1NrcUWzzVKMU7g3AwQ6L3md3fMNqjuCounter PDa: 334HLdMwbhSGYf8QWHHmEkeZf6x6caXGF6oxVnCEmaQdTransfer 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 hookassert_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:
initialize_extra_account_meta_list:transfer_hookinstruction'ının ihtiyaç duyduğu ek hesapların listesini saklayan bir hesap oluşturur.transfer_hook: Bu instruction, her token transferinde CPI ile çağrılır ve sarmalanmış SOL token transferi gerçekleştirir.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 özeltransfer_hookinstruction'ı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_hookinstruction'ı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_listPDA'sını türetmek için gerekli bir seed'dir.wsol_mint: Sarmalanmış SOL mint'i.token_program: Orijinal Token Program ID'siassociated_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 incorrectlylet account_metas = vec![// index 5, wrapped SOL mintExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?,// index 6, token programExtraAccountMeta::new_with_pubkey(&ctx.accounts.token_program.key(), false, false)?,// index 7, associated token programExtraAccountMeta::new_with_pubkey(&ctx.accounts.associated_token_program.key(),false,false,)?,// index 8, delegate PDAExtraAccountMeta::new_with_seeds(&[Seed::Literal {bytes: "delegate".as_bytes().to_vec(),}],false, // is_signerfalse, // is_writable)?,// index 9, delegate wrapped SOL token accountExtraAccountMeta::new_external_pda_with_seeds(7, // associated token program index&[Seed::AccountKey { index: 8 }, // owner index (delegate PDA)Seed::AccountKey { index: 6 }, // token program indexSeed::AccountKey { index: 5 }, // wsol mint index],false, // is_signertrue, // is_writable)?,// index 10, sender wrapped SOL token accountExtraAccountMeta::new_external_pda_with_seeds(7, // associated token program index&[Seed::AccountKey { index: 3 }, // owner indexSeed::AccountKey { index: 6 }, // token program indexSeed::AccountKey { index: 5 }, // wsol mint index],false, // is_signertrue, // is_writable)?,];// calculate account sizelet account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64;// calculate minimum required lamportslet 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 accountcreate_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 accountsExtraAccountMetaList::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 incorrectlylet account_metas = vec![// index 5, wrapped SOL mintExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?,// index 6, token programExtraAccountMeta::new_with_pubkey(&ctx.accounts.token_program.key(), false, false)?,// index 7, associated token programExtraAccountMeta::new_with_pubkey(&ctx.accounts.associated_token_program.key(),false,false,)?,// index 8, delegate PDAExtraAccountMeta::new_with_seeds(&[Seed::Literal {bytes: "delegate".as_bytes().to_vec(),}],false, // is_signertrue, // is_writable)?,// index 9, delegate wrapped SOL token accountExtraAccountMeta::new_external_pda_with_seeds(7, // associated token program index&[Seed::AccountKey { index: 8 }, // owner index (delegate PDA)Seed::AccountKey { index: 6 }, // token program indexSeed::AccountKey { index: 5 }, // wsol mint index],false, // is_signertrue, // is_writable)?,// index 10, sender wrapped SOL token accountExtraAccountMeta::new_external_pda_with_seeds(7, // associated token program index&[Seed::AccountKey { index: 3 }, // owner indexSeed::AccountKey { index: 6 }, // token program indexSeed::AccountKey { index: 5 }, // wsol mint index],false, // is_signertrue, // is_writable)?,];
Bu hesapları saklamak için üç yöntem vardır:
- Hesap adresini doğrudan saklayın:
- Wrapped SOL mint adresi
- Token Program ID'si
- Associated Token Program ID'si
// index 5, wrapped SOL mintExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?,// index 6, token programExtraAccountMeta::new_with_pubkey(&ctx.accounts.token_program.key(), false, false)?,// index 7, associated token programExtraAccountMeta::new_with_pubkey(&ctx.accounts.associated_token_program.key(),false,false,)?,
- Transfer Hook programı için bir PDA türetmek üzere seed'leri saklayın:
- Delegate PDA
// index 8, delegate PDAExtraAccountMeta::new_with_seeds(&[Seed::Literal {bytes: "delegate".as_bytes().to_vec(),}],false, // is_signerfalse, // is_writable)?,
- 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 accountExtraAccountMeta::new_external_pda_with_seeds(7, // associated token program index&[Seed::AccountKey { index: 8 }, // owner index (delegate PDA)Seed::AccountKey { index: 6 }, // token program indexSeed::AccountKey { index: 5 }, // wsol mint index],false, // is_signertrue, // is_writable)?,// index 10, sender wrapped SOL token accountExtraAccountMeta::new_external_pda_with_seeds(7, // associated token program index&[Seed::AccountKey { index: 3 }, // owner indexSeed::AccountKey { index: 6 }, // token program indexSeed::AccountKey { index: 5 }, // wsol mint index],false, // is_signertrue, // is_writable)?,
Ardından, ExtraAccountMetas listesini saklamak için gereken boyutu ve rent'i hesaplıyoruz.
// calculate account sizelet account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64;// calculate minimum required lamportslet 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 accountcreate_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 accountsExtraAccountMetaList::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 programpub 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 programpub owner: UncheckedAccount<'info>,
- hesap,
transfer_hookinstruction'ı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 failspub 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 PDAtransfer_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 checkpub 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 transfermatch 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 mintconst mint = new Keypair();const decimals = 9;// Sender token account addressconst sourceTokenAccount = getAssociatedTokenAddressSync(mint.publicKey,wallet.publicKey,false,TOKEN_2022_PROGRAM_ID,ASSOCIATED_TOKEN_PROGRAM_ID);// Recipient token account addressconst 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 instructionconst [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync([Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()],program.programId);// PDA delegate to transfer wSOL tokens from senderconst [delegatePDA] = PublicKey.findProgramAddressSync([Buffer.from("delegate")],program.programId);// Sender wSOL token account addressconst senderWSolTokenAccount = getAssociatedTokenAddressSync(NATIVE_MINT, // mintwallet.publicKey // owner);// Delegate PDA wSOL token account address, to receive wSOL tokens from senderconst delegateWSolTokenAccount = getAssociatedTokenAddressSync(NATIVE_MINT, // mintdelegatePDA, // ownertrue // allowOwnerOffCurve);// Create the two WSol token accounts as part of setupbefore(async () => {// WSol Token Account for senderawait getOrCreateAssociatedTokenAccount(connection,wallet.payer,NATIVE_MINT,wallet.publicKey);// WSol Token Account for delegate PDAawait 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 mintconst mint = new Keypair();const decimals = 9;// Sender token account addressconst sourceTokenAccount = getAssociatedTokenAddressSync(mint.publicKey,wallet.publicKey,false,TOKEN_2022_PROGRAM_ID,ASSOCIATED_TOKEN_PROGRAM_ID);// Recipient token account addressconst 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 instructionconst [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 senderconst [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 addressconst senderWSolTokenAccount = getAssociatedTokenAddressSync(NATIVE_MINT, // mintwallet.publicKey // owner);// Delegate PDA wSOL token account address, to receive wSOL tokens from senderconst delegateWSolTokenAccount = getAssociatedTokenAddressSync(NATIVE_MINT, // mintdelegatePDA, // ownertrue // allowOwnerOffCurve);
Son olarak, kurulumun bir parçası olarak wSOL token account'larını oluşturuyoruz.
// Create the two WSol token accounts as part of setupbefore(async () => {// WSol Token Account for senderawait getOrCreateAssociatedTokenAccount(connection,wallet.payer,NATIVE_MINT,wallet.publicKey);// WSol Token Account for delegate PDAawait 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 IDTOKEN_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 tokensit("Create Token Accounts and Mint Tokens", async () => {// 100 tokensconst 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 instructionit("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 tokensconst amount = 1 * 10 ** decimals;const amountBigInt = BigInt(amount);// Instruction for sender to fund their WSol token accountconst solTransferInstruction = SystemProgram.transfer({fromPubkey: wallet.publicKey,toPubkey: senderWSolTokenAccount,lamports: amount});// Approve delegate PDA to transfer WSol tokens from sender WSol token accountconst approveInstruction = createApproveInstruction(senderWSolTokenAccount,delegatePDA,wallet.publicKey,amount,[],TOKEN_PROGRAM_ID);// Sync sender WSol token accountconst syncWrappedSolInstruction = createSyncNativeInstruction(senderWSolTokenAccount);// This helper function will automatically derive all the additional accounts that were defined in the ExtraAccountMetas accountlet 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-hookTransaction 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 accountimpl<'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_signertrue // 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 accountpub 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 programpub 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?