Η επέκταση Transfer Hook και το Transfer Hook Interface εισάγουν τη δυνατότητα δημιουργίας Mint Accounts που εκτελούν προσαρμοσμένη λογική εντολών σε κάθε μεταφορά token.
Αυτό ξεκλειδώνει πολλές νέες περιπτώσεις χρήσης για μεταφορές token, όπως:
- Επιβολή royalties NFT
- Μαύρη ή λευκή λίστα πορτοφολιών που μπορούν να λαμβάνουν tokens
- Υλοποίηση προσαρμοσμένων χρεώσεων σε μεταφορές token
- Δημιουργία προσαρμοσμένων συμβάντων μεταφοράς token
- Παρακολούθηση στατιστικών για τις μεταφορές token σας
- Και πολλά ακόμα
Για να το επιτύχουν αυτό, οι προγραμματιστές πρέπει να δημιουργήσουν ένα πρόγραμμα που υλοποιεί το Transfer Hook Interface και να αρχικοποιήσουν ένα mint account με ενεργοποιημένη την επέκταση Transfer Hook.
Για κάθε μεταφορά token που αφορά tokens από το mint account, το Token Extensions Program πραγματοποιεί ένα Cross Program Invocation (CPI) για να εκτελέσει μια εντολή στο πρόγραμμα Transfer Hook.
Όταν το Token Extensions Program πραγματοποιεί CPI σε ένα πρόγραμμα Transfer Hook, όλοι οι λογαριασμοί aπό την αρχική μεταφορά μετατρέπονται σε λογαριασμούς μόνο για ανάγνωση. Αυτό σημαίνει ότι τα προνόμια υπογραφής του αποστολέα δεν επεκτείνονται στο πρόγραμμα Transfer Hook.
Αυτή η αρχιτεκτονική απόφαση λαμβάνεται για την αποτροπή κακόβουλης χρήσης προγραμμάτων Transfer Hook.
Σε αυτόν τον οδηγό, θα δημιουργήσουμε ένα πρόγραμμα Transfer Hook χρησιμοποιώντας το πλαίσιο Anchor, ωστόσο, είναι επίσης δυνατό να υλοποιηθεί το Transfer Hook Interface χρησιμοποιώντας ένα native πρόγραμμα. Μάθετε περισσότερα για το πλαίσιο Anchor εδώ: Anchor Framework
Επισκόπηση Transfer Hook Interface
Το Transfer Hook Interface παρέχει έναν τρόπο στους προγραμματιστές να υλοποιούν προσαρμοσμένη λογική εντολών που εκτελείται σε κάθε μεταφορά token για ένα συγκεκριμένο mint account.
Το Transfer Hook Interface ορίζει τις παρακάτω εντολές:
Execute: Μια εντολή που το Token Extensions Program καλεί σε κάθε μεταφορά token.InitializeExtraAccountMetaList(προαιρετικό): Δημιουργεί έναν λογαριασμό που αποθηκεύει μια λίστα πρόσθετων λογαριασμών που απαιτούνται από την προσαρμοσμένη εντολήExecute.UpdateExtraAccountMetaList(προαιρετικό): Ενημερώνει τη λίστα των πρόσθετων λογαριασμών αντικαθιστώντας την υπάρχουσα λίστα.
Δεν είναι τεχνικά απαραίτητο να υλοποιηθεί η εντολή InitializeExtraAccountMetaList
χρησιμοποιώντας το interface. Ο λογαριασμός μπορεί να δημιουργηθεί από οποιαδήποτε εντολή
σε ένα πρόγραμμα Transfer Hook.
Ωστόσο, το Program Derived Address (PDA) για τον λογαριασμό πρέπει να προέρχεται χρησιμοποιώντας τα παρακάτω seeds:
- Η σταθερά συμβολοσειράς "extra-account-metas"
- Η διεύθυνση του mint account
- Το ID του προγράμματος Transfer Hook
const [pda] = PublicKey.findProgramAddressSync([Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()],program.programId // transfer hook program ID);
Αποθηκεύοντας τους επιπλέον λογαριασμούς που απαιτούνται από την εντολή Execute στο
προκαθορισμένο PDA, αυτοί οι λογαριασμοί μπορούν να προστεθούν αυτόματα σε μια εντολή
μεταφοράς token από τον client.
Transfer Hook Hello-world
Αυτό το παράδειγμα είναι το hello world των transfer hooks. Πρόκειται για ένα απλό transfer hook που θα εκτυπώνει ένα μήνυμα σε κάθε μεταφορά token. Ξεκινάμε ανοίγοντας το παράδειγμα στο Solana Playground, ένα διαδικτυακό εργαλείο για τη δημιουργία και ανάπτυξη προγραμμάτων Solana: link
Το παράδειγμα αποτελείται από ένα πρόγραμμα Anchor που υλοποιεί το transfer hook interface και ένα αρχείο δοκιμών για να ελέγξει το πρόγραμμα.
Αυτό το πρόγραμμα θα περιλαμβάνει μόνο 3 εντολές:
initialize_extra_account_meta_list: Δημιουργεί έναν λογαριασμό που αποθηκεύει μια λίστα επιπλέον λογαριασμών που απαιτούνται από την εντολήtransfer_hook. Στο hello world την αφήνουμε κενή.transfer_hook: Αυτή η εντολή καλείται μέσω CPI σε κάθε μεταφορά token για να εκτελέσει μια μεταφορά wrapped SOL token.fallback: Επειδή χρησιμοποιούμε Anchor και το token program είναι ένα native πρόγραμμα, χρειάζεται να προσθέσουμε μια εντολή fallback για να αντιστοιχίσουμε χειροκίνητα το discriminator εντολής και να καλέσουμε την προσαρμοσμένη εντολήtransfer_hook. Δεν χρειάζεται να αλλάξετε αυτή τη συνάρτηση.
Κάθε φορά που μεταφέρεται το token, αυτή η συνάρτηση transfer_hook θα
καλείται από το token program.
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {msg!("Hello Transfer Hook!");Ok(())}
Σε αυτή τη συνάρτηση μπορείτε τώρα να προσθέσετε την επιπλέον λογική σας. Για παράδειγμα, θα μπορούσατε να αποτύχει η μεταφορά κάθε φορά που μεταφέρεται ποσό μεγαλύτερο από 50 ως εξής:
#[error_code]pub enum MyError {#[msg("The amount is too big")]AmountTooBig,}pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {msg!("Hello Transfer Hook!");if amount > 50 {return err!(MyError::AmountTooBig);}Ok(())}
Για να εκτελέσετε το παράδειγμα στο Solana Playground ακολουθήστε αυτόν τον σύνδεσμο: link
Στο τερματικό του Playground, εκτελέστε την εντολή build η οποία θα ενημερώσει την τιμή του
declare_id στο αρχείο lib.rs με ένα νέο παραγόμενο ID προγράμματος. Στη συνέχεια εκτελέστε
την εντολή deploy για να αναπτύξετε το πρόγραμμά σας στο devnet. Όταν το πρόγραμμα
αναπτυχθεί, μπορείτε να εκτελέσετε το αρχείο δοκιμών χρησιμοποιώντας την εντολή test στο τερματικό.
Αυτό θα σας δώσει μια έξοδο παρόμοια με αυτή:
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)
Αν δεν θέλετε να χρησιμοποιήσετε JavaScript για να δημιουργήσετε το token σας, μπορείτε επίσης να χρησιμοποιήσετε την
εντολή spl-token από το Solana CLI αφού αναπτύξετε το πρόγραμμά σας:
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token --transfer-hook yourTransferHookProgramId
Transfer Hook Μετρητή
Το επόμενο παράδειγμα θα σας δείξει πώς μπορείτε να αυξάνετε έναν μετρητή κάθε φορά που μεταφέρεται το token σας. link
Αν θέλετε να προσθέσετε λογική στο transfer hook σας που χρειάζεται επιπλέον λογαριασμούς, πρέπει να τους προσθέσετε στον λογαριασμό ExtraAccountMetaList. Στη δική μας περίπτωση θέλουμε ένα PDA που αποθηκεύει πόσες φορές έχει μεταφερθεί το token.
Αυτό μπορεί να γίνει προσθέτοντας τον παρακάτω κώδικα στην
εντολή initialize_extra_account_meta_list:
let account_metas = vec![ExtraAccountMeta::new_with_seeds(&[Seed::Literal {bytes: "counter".as_bytes().to_vec(),}],false, // is_signertrue, // is_writable)?,];
Χρειάζεται επίσης να δημιουργήσουμε αυτόν τον λογαριασμό όταν αρχικοποιούμε το νέο mint account και να τον παρέχουμε κάθε φορά που μεταφέρουμε το token.
#[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>,}
Και ο λογαριασμός θα περιέχει μια μεταβλητή μετρητή τύπου u64:
#[account]pub struct CounterAccount {counter: u64,}
Τώρα στη συνάρτηση transfer hook μας μπορούμε απλώς να αυξάνουμε αυτόν τον μετρητή κατά ένα κάθε φορά που καλείται:
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {ctx.accounts.counter_account.counter.checked_add(1).unwrap();msg!("This token has been transferred {0} times", ctx.accounts.counter_account.counter);Ok(())}
Στον client αυτοί οι επιπλέον λογαριασμοί προστίθενται αυτόματα από τη βοηθητική συνάρτηση createTransferCheckedWithTransferHookInstruction:
let transferInstructionWithHelper =await createTransferCheckedWithTransferHookInstruction(connection,sourceTokenAccount,mint.publicKey,destinationTokenAccount,wallet.publicKey,amountBigInt,decimals,[],"confirmed",TOKEN_2022_PROGRAM_ID);
Για να εκτελέσετε το παράδειγμα στο Solana Playground ακολουθήστε αυτόν τον σύνδεσμο: link
Και στη συνέχεια πληκτρολογήστε build εκεί, το οποίο θα ενημερώσει την τιμή του declare_id στο
αρχείο lib.rs με ένα νέο παραγόμενο ID προγράμματος. Στη συνέχεια πληκτρολογήστε deploy για να
αναπτύξετε το πρόγραμμά σας στο devnet. Όταν το πρόγραμμα αναπτυχθεί, μπορείτε να εκτελέσετε το
αρχείο δοκιμών πληκτρολογώντας test στο τερματικό.
Αυτό θα σας δώσει την παρακάτω έξοδο. Στην τελευταία συναλλαγή θα μπορείτε να δείτε πόσες φορές έχει μεταφερθεί το token σας:
"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)
Εφόσον εδώ αυξάνουμε έναν μετρητή κάθε φορά που μεταφέρεται το token, πρέπει να διασφαλίσουμε ότι η εντολή transfer hook μπορεί να κληθεί μόνο κατά τη διάρκεια μιας μεταφοράς, διαφορετικά κάποιος θα μπορούσε να καλέσει απευθείας την εντολή transfer hook και να χαλάσει τον μετρητή μας. Αυτός είναι ένας έλεγχος που πρέπει να προσθέσετε σε οποιοδήποτε από τα transfer hooks σας.
Μπορείτε να προσθέσετε τον έλεγχο ως εξής:
fn assert_is_transferring(ctx: &Context<TransferHook>) -> Result<()> {let source_token_info = ctx.accounts.source_token.to_account_info();let mut account_data_ref: RefMut<&mut [u8]> = source_token_info.try_borrow_mut_data()?;let mut account = PodStateWithExtensionsMut::<PodAccount>::unpack(*account_data_ref)?;let account_extension = account.get_extension_mut::<TransferHookAccount>()?;if !bool::from(account_extension.transferring) {return err!(TransferError::IsNotCurrentlyTransferring);}Ok(())}
Και στη συνέχεια να τον καλέσετε στην αρχή της συνάρτησης transfer_hook σας:
#[error_code]pub enum TransferError {#[msg("The token is not currently transferring")]IsNotCurrentlyTransferring,}#[interface(spl_transfer_hook_interface::execute)]pub fn transfer_hook(ctx: Context<TransferHook>, _amount: u64) -> Result<()> {// Fail this instruction if it is not called from within a transfer 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(())}
Transfer Hook με χρέωση wSOL (προχωρημένο παράδειγμα)
Στο επόμενο μέρος αυτού του οδηγού, θα δημιουργήσουμε ένα πιο προχωρημένο πρόγραμμα Transfer Hook χρησιμοποιώντας το πλαίσιο Anchor. Αυτό το πρόγραμμα θα απαιτεί από τον αποστολέα να πληρώνει ένα χρέωμα wSOL για κάθε μεταφορά token.
Οι μεταφορές wSOL θα εκτελούνται χρησιμοποιώντας έναν delegate που είναι ένα PDA προερχόμενο από το πρόγραμμα Transfer Hook. Αυτό είναι απαραίτητο επειδή η υπογραφή του αρχικού αποστολέα της εντολής μεταφοράς token δεν είναι προσβάσιμη στο πρόγραμμα Transfer Hook.
Αυτό το πρόγραμμα θα περιλαμβάνει μόνο 3 εντολές:
initialize_extra_account_meta_list: Δημιουργεί έναν λογαριασμό που αποθηκεύει μια λίστα επιπλέον λογαριασμών που απαιτούνται από την εντολήtransfer_hook.transfer_hook: Αυτή η εντολή καλείται μέσω CPI σε κάθε μεταφορά token για να εκτελέσει μια μεταφορά wrapped SOL token.fallback: Οι εντολές του transfer hook interface έχουν συγκεκριμένα discriminators (αναγνωριστικά εντολών). Σε ένα πρόγραμμα Anchor, μπορούμε να χρησιμοποιήσουμε μια εντολή fallback για να αντιστοιχίσουμε χειροκίνητα το discriminator εντολής και να καλέσουμε την προσαρμοσμένη εντολήtransfer_hook.
Αυτό το πρόγραμμα θα απαιτεί από τον αποστολέα να πληρώνει χρέωμα σε wrapped SOL (wSOL) για κάθε μεταφορά token. Εδώ είναι το τελικό πρόγραμμα.
Ξεκινώντας
Ξεκινήστε ανοίγοντας αυτόν τον σύνδεσμο Solana Playground link και στη συνέχεια κάντε κλικ στο κουμπί "Import" για να αντιγράψετε το έργο.
Ο κώδικας εκκίνησης περιλαμβάνει ένα αρχείο lib.rs και transfer-hook.test.ts τα οποία είναι
στηριγμένα για το πρόγραμμα που θα δημιουργούμε. Στο αρχείο lib.rs θα πρέπει
να βλέπετε τον παρακάτω κώδικα:
use anchor_lang::{prelude::*,system_program::{create_account, CreateAccount},};use anchor_spl::{associated_token::AssociatedToken,token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked},};use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList,};use spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction};declare_id!("E6wu6Nykdra8gXs57Zqo7hY6DLaWugTmD3uuuBmX2Vxt");#[program]pub mod transfer_hook {use super::*;pub fn initialize_extra_account_meta_list(ctx: Context<InitializeExtraAccountMetaList>,) -> Result<()> {Ok(())}pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {Ok(())}pub fn fallback<'info>(program_id: &Pubkey,accounts: &'info [AccountInfo<'info>],data: &[u8],) -> Result<()> {Ok(())}}#[derive(Accounts)]pub struct InitializeExtraAccountMetaList {}#[derive(Accounts)]pub struct TransferHook {}
Αφού εισαγάγετε το έργο, δημιουργήστε το πρόγραμμα χρησιμοποιώντας την εντολή build
στο τερματικό του Playground.
build
Αυτό θα ενημερώσει την τιμή του declare_id στο αρχείο lib.rs με ένα νέο
παραγόμενο ID προγράμματος.
Εντολή Αρχικοποίησης του Λογαριασμού ExtraAccountMetas
Σε αυτό το βήμα, θα υλοποιήσουμε την εντολή initialize_extra_account_meta_list
για το πρόγραμμα Transfer Hook μας. Αυτή η εντολή δημιουργεί έναν
λογαριασμό ExtraAccountMetas, ο οποίος θα αποθηκεύει τους επιπλέον λογαριασμούς που απαιτούνται από
την εντολή transfer_hook μας.
Σε αυτό το παράδειγμα, η εντολή initialize_extra_account_meta_list απαιτεί 7
λογαριασμούς:
payer: Ο λογαριασμός που χρησιμοποιείται για την πληρωμή της δημιουργίας του λογαριασμού ExtraAccountMetas.extra_account_meta_list: Ο λογαριασμός ExtraAccountMetas που δημιουργείται για να αποθηκεύει τη λίστα των λογαριασμών που απαιτούνται από την εντολήtransfer_hookμας.mint: Το mint account που παραπέμπει σε αυτό το πρόγραμμα Transfer Hook. Η διεύθυνση mint είναι ένα απαιτούμενο seed για την παραγωγή του PDAextra_account_meta_list.wsol_mint: Το wrapped SOL mint.token_program: Το αρχικό ID Token Programassociated_token_program: Το ID Associated Token Program.system_program: Το System Program, το οποίο είναι ένας απαιτούμενος λογαριασμός κατά τη δημιουργία νέων λογαριασμών.
Οι διευθύνσεις για mint, wsol_mint και associated_token_program θα
χρησιμοποιηθούν για την παραγωγή των διευθύνσεων για τα wSOL Associated Token Accounts. Αυτοί
οι λογαριασμοί απαιτούνται από την εντολή transfer_hook και θα αποθηκευτούν στον
λογαριασμό ExtraAccountMetas.
Ενημερώστε τη δομή InitializeExtraAccountMetaList αντικαθιστώντας τον παρακάτω
κώδικα εκκίνησης:
#[derive(Accounts)]pub struct InitializeExtraAccountMetaList {}
Με τον κώδικα που παρέχεται παρακάτω:
#[derive(Accounts)]pub struct InitializeExtraAccountMetaList<'info> {#[account(mut)]payer: Signer<'info>,/// CHECK: ExtraAccountMetaList Account, must use these seeds#[account(mut,seeds = [b"extra-account-metas", mint.key().as_ref()],bump)]pub extra_account_meta_list: AccountInfo<'info>,pub mint: InterfaceAccount<'info, Mint>,pub wsol_mint: InterfaceAccount<'info, Mint>,pub token_program: Interface<'info, TokenInterface>,pub associated_token_program: Program<'info, AssociatedToken>,pub system_program: Program<'info, System>,}
Στη συνέχεια, ενημερώστε την εντολή initialize_extra_account_meta_list αντικαθιστώντας
τον παρακάτω κώδικα εκκίνησης:
pub fn initialize_extra_account_meta_list(ctx: Context<InitializeExtraAccountMetaList>,) -> Result<()> {Ok(())}
Με τον παρακάτω κώδικα:
pub fn initialize_extra_account_meta_list(ctx: Context<InitializeExtraAccountMetaList>,) -> Result<()> {// index 0-3 are the accounts required for token transfer (source, mint, destination, owner)// index 4 is address of ExtraAccountMetaList account// The `addExtraAccountsToInstruction` JS helper function resolving 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(())}
Ας εξετάσουμε αναλυτικά την ενημερωμένη λογική της εντολής. Ξεκινάμε με την καταγραφή των επιπλέον λογαριασμών που πρέπει να αποθηκευτούν στον λογαριασμό ExtraAccountMetas.
// index 0-3 are the accounts required for token transfer (source, mint, destination, owner)// index 4 is address of ExtraAccountMetaList account// The `addExtraAccountsToInstruction` JS helper function resolving 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)?,];
Υπάρχουν τρεις μέθοδοι για την αποθήκευση αυτών των λογαριασμών:
- Άμεση αποθήκευση της διεύθυνσης λογαριασμού:
- Διεύθυνση wrapped SOL mint
- ID Token Program
- ID Associated Token Program
// 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,)?,
- Αποθήκευση των seeds για την παραγωγή ενός PDA για το πρόγραμμα Transfer Hook:
- Delegate PDA
// index 8, delegate PDAExtraAccountMeta::new_with_seeds(&[Seed::Literal {bytes: "delegate".as_bytes().to_vec(),}],false, // is_signerfalse, // is_writable)?,
- Αποθηκεύστε τα seeds για να αντλήσετε ένα PDA για ένα πρόγραμμα διαφορετικό από το Transfer Hook
program:
- Εκχώρηση Δικαιωμάτων στο wSOL Associated Token Account του Εκπροσώπου
- 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)?,
Στη συνέχεια, υπολογίζουμε το μέγεθος και το rent που απαιτείται για την αποθήκευση της λίστας ExtraAccountMetas.
// 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);
Στη συνέχεια, κάνουμε ένα CPI στο System Program για να δημιουργήσουμε έναν λογαριασμό και να ορίσουμε το Transfer Hook Program ως ιδιοκτήτη. Τα seeds του PDA συμπεριλαμβάνονται ως signer seeds στο CPI επειδή χρησιμοποιούμε το PDA ως τη διεύθυνση του νέου λογαριασμού.
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,)?;
Μόλις δημιουργήσουμε τον λογαριασμό, αρχικοποιούμε τα δεδομένα του λογαριασμού για να αποθηκεύσουμε τη λίστα ExtraAccountMetas.
// initialize ExtraAccountMetaList account with extra accountsExtraAccountMetaList::init::<ExecuteInstruction>(&mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,&account_metas,)?;
Σε αυτό το παράδειγμα, δεν χρησιμοποιούμε το Transfer Hook interface για τη δημιουργία του λογαριασμού ExtraAccountMetas.
Προσαρμοσμένη Εντολή Transfer Hook
Στη συνέχεια, ας υλοποιήσουμε την προσαρμοσμένη εντολή transfer_hook. Αυτή είναι η εντολή που θα επικαλείται το Token Extension program σε κάθε μεταφορά token.
Σε αυτό το παράδειγμα, θα απαιτούμε μια χρέωση σε wSOL για κάθε μεταφορά token. Για απλότητα, το ποσό της χρέωσης ισούται με το ποσό της μεταφοράς token.
Ενημερώστε τη δομή TransferHook αντικαθιστώντας τον παρακάτω αρχικό κώδικα:
#[derive(Accounts)]pub struct TransferHook {}
Με τον παρακάτω ενημερωμένο κώδικα:
Σημειώστε ότι η σειρά των λογαριασμών σε αυτή τη δομή έχει σημασία. Αυτή είναι η σειρά με την οποία το Token Extensions program παρέχει αυτούς τους λογαριασμούς όταν κάνει CPI σε αυτό το Transfer Hook program.
// 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>,}
Οι πρώτοι 4 λογαριασμοί είναι οι λογαριασμοί που απαιτούνται από την αρχική μεταφορά token.
#[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>,
Ο 5ος λογαριασμός είναι η διεύθυνση του λογαριασμού ExtraAccountMeta που αποθηκεύει τη λίστα των επιπλέον λογαριασμών που απαιτούνται από την εντολή transfer_hook.
/// CHECK: ExtraAccountMetaList Account#[account(seeds = [b"extra-account-metas", mint.key().as_ref()],bump)]pub extra_account_meta_list: UncheckedAccount<'info>,
Οι υπόλοιποι λογαριασμοί είναι οι λογαριασμοί που αναφέρονται στον λογαριασμό ExtraAccountMetas με τη σειρά που τους ορίσαμε στην εντολή initialize_extra_account_meta_list.
pub wsol_mint: InterfaceAccount<'info, Mint>,pub token_program: Interface<'info, TokenInterface>,pub associated_token_program: Program<'info, AssociatedToken>,#[account(mut,seeds = [b"delegate"],bump)]pub delegate: SystemAccount<'info>,#[account(mut,token::mint = wsol_mint,token::authority = delegate,)]pub delegate_wsol_token_account: InterfaceAccount<'info, TokenAccount>,#[account(mut,token::mint = wsol_mint,token::authority = owner,)]pub sender_wsol_token_account: InterfaceAccount<'info, TokenAccount>,
Στη συνέχεια, ενημερώστε την εντολή transfer_hook αντικαθιστώντας τον παρακάτω αρχικό κώδικα:
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {Ok(())}
Με τον παρακάτω ενημερωμένο κώδικα:
// Require SOL fee on transfer, lamport fee is equal to transfer amount// If this fails, the initial token transfer 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(())}
Μέσα στη λογική της εντολής, κάνουμε ένα CPI για τη μεταφορά wSOL από το wSOL token account του αποστολέα. Αυτή η μεταφορά υπογράφεται από τον εκπρόσωπο PDA. Για κάθε μεταφορά token, ο αποστολέας πρέπει πρώτα να εγκρίνει τον εκπρόσωπο για το ποσό μεταφοράς.
Εντολή Fallback
Τέλος, πρέπει να προσθέσουμε μια εντολή fallback στο πρόγραμμα Anchor για να χειριστούμε το CPI από το Token Extensions program.
Αυτό το βήμα απαιτείται λόγω της διαφοράς στον τρόπο που το Anchor παράγει τους διακριτές εντολών σε σύγκριση με αυτούς που χρησιμοποιούνται στις εντολές του Transfer Hook interface. Ο διακριτής εντολής για την εντολή transfer_hook δεν θα ταιριάζει με αυτόν του Transfer Hook interface.
Ενημερώστε την εντολή fallback αντικαθιστώντας τον παρακάτω αρχικό κώδικα:
pub fn fallback<'info>(program_id: &Pubkey,accounts: &'info [AccountInfo<'info>],data: &[u8],) -> Result<()> {Ok(())}
Με τον παρακάτω ενημερωμένο κώδικα:
// fallback instruction handler as workaround to anchor instruction discriminator 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 ελέγχει αν ο διακριτής εντολής για μια εισερχόμενη εντολή ταιριάζει με την εντολή Execute από το Transfer Hook interface. Εάν υπάρχει επιτυχής αντιστοίχιση, επικαλείται την εντολή transfer_hook στο πρόγραμμα Anchor μας.
Επί του παρόντος, υπάρχει μια μη κυκλοφορημένη δυνατότητα του Anchor που απλοποιεί αυτή τη διαδικασία. Θα κατάργούσε την ανάγκη για την εντολή fallback.
Δημιουργία και Ανάπτυξη Προγράμματος
Το Transfer Hook program είναι πλέον ολοκληρωμένο. Βεβαιωθείτε ότι έχετε αρκετό Devnet SOL στο πορτοφόλι Playground σας για να αναπτύξετε το πρόγραμμα.
Για να δημιουργήσετε το πρόγραμμα, χρησιμοποιήστε την παρακάτω εντολή:
build
Στη συνέχεια, αναπτύξτε το πρόγραμμα χρησιμοποιώντας την εντολή:
deploy
Επισκόπηση Αρχείου Δοκιμών
Στη συνέχεια, ας δοκιμάσουμε το πρόγραμμα. Ανοίξτε το αρχείο transfer-hook.test.ts και θα πρέπει να δείτε τον παρακάτω αρχικό κώδικα:
import * as anchor from "@coral-xyz/anchor";import { Program } from "@coral-xyz/anchor";import { TransferHook } from "../target/types/transfer_hook";import {PublicKey,SystemProgram,Transaction,sendAndConfirmTransaction,Keypair,} from "@solana/web3.js";import {ExtensionType,TOKEN_2022_PROGRAM_ID,getMintLen,createInitializeMintInstruction,createInitializeTransferHookInstruction,addExtraAccountsToInstruction,ASSOCIATED_TOKEN_PROGRAM_ID,createAssociatedTokenAccountInstruction,createMintToInstruction,createTransferCheckedInstruction,getAssociatedTokenAddressSync,createApproveInstruction,createSyncNativeInstruction,NATIVE_MINT,TOKEN_PROGRAM_ID,getAccount,getOrCreateAssociatedTokenAccount,} from "@solana/spl-token";import assert from "assert";describe("transfer-hook", () => {// Configure the client to use the local cluster.const provider = anchor.AnchorProvider.env();anchor.setProvider(provider);const program = anchor.workspace.TransferHook as Program<TransferHook>;const wallet = provider.wallet as anchor.Wallet;const connection = provider.connection;// Generate keypair to use as address for the transfer-hook enabled 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 () => {});});
Αρχικά, δημιουργούμε ένα keypair για χρήση ως διεύθυνση για ένα νέο mint account. Χρησιμοποιώντας τη διεύθυνση mint, αντλούμε τις διευθύνσεις Associated Token Account (ATA) που θα χρησιμοποιήσουμε για τη μεταφορά token.
// 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);
Στη συνέχεια, αντλούμε το PDA για τον λογαριασμό ExtraAccountMetas. Αυτός ο λογαριασμός δημιουργείται για να αποθηκεύσει τους επιπλέον λογαριασμούς που απαιτούνται από την προσαρμοσμένη εντολή transfer hook.
// 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 που θα χρησιμοποιηθεί ως εκπρόσωπος. Ο αποστολέας πρέπει να εγκρίνει αυτή τη διεύθυνση ως εκπρόσωπο για το wSOL token account του. Αυτό το PDA εκπροσώπου χρησιμοποιείται για «υπογραφή» της μεταφοράς wSOL στην προσαρμοσμένη εντολή transfer hook.
// PDA delegate to transfer wSOL tokens from senderconst [delegatePDA] = PublicKey.findProgramAddressSync([Buffer.from("delegate")],program.programId);
Επιπλέον, αντλούμε τις διευθύνσεις για τα wSOL token accounts. Η πρώτη διεύθυνση αφορά το wSOL token account του αποστολέα, το οποίο πρέπει να χρηματοδοτηθεί για την κάλυψη της χρέωσης μεταφοράς που απαιτείται από την εντολή transfer hook. Η δεύτερη διεύθυνση αφορά το wSOL token account που ανήκει στο PDA εκπροσώπου. Σε αυτό το παράδειγμα, όλες οι χρεώσεις wSOL αποστέλλονται σε αυτόν τον λογαριασμό.
// 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);
Τέλος, ως μέρος της εγκατάστασης, δημιουργούμε τα wSOL token accounts.
// 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
Για να ξεκινήσετε, δημιουργήστε μια συναλλαγή για τη δημιουργία ενός νέου mint account με ενεργοποιημένη την επέκταση Transfer Hook. Σε αυτή τη συναλλαγή, φροντίστε να ορίσετε το πρόγραμμά μας ως το Transfer Hook program που αποθηκεύεται στην επέκταση.
Η ενεργοποίηση της επέκτασης Transfer Hook επιτρέπει στο Transfer Extension program να καθορίζει ποιο πρόγραμμα θα επικαλείται σε κάθε μεταφορά token.
Αντικαταστήστε τη δοκιμή placeholder:
it("Create Mint Account with Transfer Hook Extension", async () => {});
Με την παρακάτω ενημερωμένη δοκιμή:
it("Create Mint Account with Transfer Hook Extension", async () => {const extensions = [ExtensionType.TransferHook];const mintLen = getMintLen(extensions);const lamports =await provider.connection.getMinimumBalanceForRentExemption(mintLen);const transaction = new Transaction().add(SystemProgram.createAccount({fromPubkey: wallet.publicKey,newAccountPubkey: mint.publicKey,space: mintLen,lamports: lamports,programId: TOKEN_2022_PROGRAM_ID}),createInitializeTransferHookInstruction(mint.publicKey,wallet.publicKey,program.programId, // Transfer Hook Program 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 Accounts
Στη συνέχεια, ως μέρος της εγκατάστασης, δημιουργήστε τα associated token accounts τόσο για τον αποστολέα όσο και για τον παραλήπτη. Επίσης, χρηματοδοτήστε τον λογαριασμό του αποστολέα με μερικά tokens.
Αντικαταστήστε τη δοκιμή placeholder:
it("Create Token Accounts and Mint Tokens", async () => {});
Με την παρακάτω ενημερωμένη δοκιμή:
// Create the two token accounts for the transfer-hook enabled mint// Fund the sender token account with 100 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
Πριν από την αποστολή μιας μεταφοράς token, πρέπει να δημιουργήσουμε τον λογαριασμό ExtraAccountMetas για να αποθηκεύσουμε όλους τους επιπλέον λογαριασμούς που απαιτούνται από την εντολή transfer hook.
Για τη δημιουργία αυτού του λογαριασμού, επικαλούμαστε την εντολή από το πρόγραμμά μας.
Αντικαταστήστε τη δοκιμή placeholder:
it("Create ExtraAccountMetaList Account", async () => {});
Με την παρακάτω ενημερωμένη δοκιμή:
// 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);});
Μεταφορά Tokens
Τέλος, είμαστε έτοιμοι να στείλουμε μια μεταφορά token. Εκτός από την εντολή μεταφοράς, υπάρχουν μερικές επιπλέον εντολές που πρέπει να συμπεριληφθούν.
- Ο αποστολέας πρέπει να μεταφέρει SOL στο wSOL token account του για να καλύψει τη χρέωση που απαιτείται από την εντολή transfer hook.
- Ο αποστολέας πρέπει να εγκρίνει τον εκπρόσωπο PDA για το ποσό της χρέωσης wSOL.
- Συμπεριλάβετε μια εντολή για συγχρονισμό του υπολοίπου wSOL.
- Η εντολή μεταφοράς token πρέπει να περιλαμβάνει όλους τους επιπλέον λογαριασμούς που απαιτούνται από την εντολή transfer hook.
Αντικαταστήστε τη δοκιμή placeholder:
it("Transfer Hook with Extra Account Meta", async () => {});
Με την παρακάτω ενημερωμένη δοκιμή:
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);});
Η εντολή μεταφοράς πρέπει να περιλαμβάνει όλα τα επιπλέον AccountMetas, τη διεύθυνση του λογαριασμού ExtraAccountMetas και τη διεύθυνση του Transfer Hook program.
Εκτέλεση Αρχείου Δοκιμών
Μόλις ενημερώσετε όλες τις δοκιμές, το τελευταίο βήμα είναι να εκτελέσετε τη δοκιμή.
Για να εκτελέσετε το αρχείο δοκιμών, χρησιμοποιήστε την παρακάτω εντολή στο τερματικό:
test
Θα πρέπει να δείτε αποτέλεσμα παρόμοιο με το παρακάτω:
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)
Χρήση δεδομένων token account στο transfer hook
Μερικές φορές μπορεί να θέλετε να χρησιμοποιήσετε δεδομένα λογαριασμού για να αντλήσετε επιπλέον λογαριασμούς στα extra account metas. Αυτό είναι χρήσιμο αν, για παράδειγμα, θέλετε να χρησιμοποιήσετε τον ιδιοκτήτη του token account ως seed για ένα PDA.
Κατά τη δημιουργία του ExtraAccountMeta, μπορείτε να χρησιμοποιήσετε τα δεδομένα οποιουδήποτε λογαριασμού ως επιπλέον seed. Σε αυτή την περίπτωση, θέλουμε να αντλήσουμε έναν λογαριασμό μετρητή από τον ιδιοκτήτη του token account και τη συμβολοσειρά 'counter'. Αυτό σημαίνει ότι θα μπορούμε πάντα να βλέπουμε πόσο συχνά αυτός ο ιδιοκτήτης token account έχει μεταφέρει tokens.
Έτσι το ρυθμίζετε στη συνάρτηση extra_account_metas().
// 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)?])}}
Ας εξετάσουμε τη δομή του token account για να κατανοήσουμε πώς αποθηκεύονται τα δεδομένα του λογαριασμού. Παρακάτω είναι ένα παράδειγμα δομής token account. Μπορούμε λοιπόν να λάβουμε 32 bytes στις θέσεις 32 έως 64 ως τον ιδιοκτήτη του token account, ο οποίος βρίσκεται στο 'account_index: 0'. Το 'account_index' αναφέρεται στον δείκτη του λογαριασμού στον πίνακα λογαριασμών. Στην περίπτωση ενός transfer hook, ο ιδιοκτήτης token account είναι η πρώτη καταχώριση στον πίνακα λογαριασμών. Ο δεύτερος λογαριασμός είναι πάντα το mint και ο τρίτος λογαριασμός είναι ο token account προορισμού. Αυτή η σειρά λογαριασμών είναι ίδια με αυτή του παλιού Token Program.
/// Account data.#[repr(C)]#[derive(Clone, Copy, Debug, Default, PartialEq)]pub struct Account {/// The mint associated with this 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>,}
Στην περίπτωσή μας, θέλουμε να αντλήσουμε έναν λογαριασμό μετρητή από τον ιδιοκτήτη του token account αποστολέα, οπότε όταν δημιουργούμε τους λογαριασμούς ExtraAccountMeta, εκτελούμε init σε αυτόν τον λογαριασμό μετρητή PDA που αντλείται από τον ιδιοκτήτη του token account αποστολέα και τη συμβολοσειρά 'counter'. Όταν αρχικοποιηθεί ο λογαριασμός μετρητή PDA, θα μπορούμε να τον χρησιμοποιούμε μέσα στο transfer hook για αύξηση της τιμής σε κάθε μεταφορά.
struct.```rust#[derive(Accounts)]pub struct InitializeExtraAccountMetaList<'info> {#[account(mut)]payer: Signer<'info>,/// CHECK: ExtraAccountMetaList Account, must use these seeds#[account(init,seeds = [b"extra-account-metas", mint.key().as_ref()],bump,space = ExtraAccountMetaList::size_of(InitializeExtraAccountMetaList::extra_account_metas()?.len())?,payer = payer)]pub extra_account_meta_list: AccountInfo<'info>,pub mint: InterfaceAccount<'info, Mint>,#[account(init, seeds = [b"counter", payer.key().as_ref()], bump, payer = payer, space = 16)]pub counter_account: Account<'info, CounterAccount>,pub token_program: Program<'info, Token2022>,pub associated_token_program: Program<'info, AssociatedToken>,pub system_program: Program<'info, System>,}
Πρέπει επίσης να ορίσουμε αυτόν τον επιπλέον λογαριασμό μετρητή στη δομή TransferHook. Αυτοί είναι οι λογαριασμοί που μεταβιβάζονται στο πρόγραμμα TransferHook μας κάθε φορά που γίνεται μια μεταφορά. Ο πελάτης λαμβάνει αυτούς τους επιπλέον λογαριασμούς από το PDA ExtraAccountsMetaList και τους συμπεριλαμβάνει στην εντολή μεταφοράς token, αλλά εδώ στο πρόγραμμα εξακολουθούμε να πρέπει να το ορίσουμε.
#[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>,}
Στον πελάτη, αυτός ο λογαριασμός δημιουργείται αυτόματα και μπορείτε να τον χρησιμοποιήσετε ως εξής.
const transferInstructionWithHelper =await createTransferCheckedWithTransferHookInstruction(connection,sourceTokenAccount,mint.publicKey,destinationTokenAccount,wallet.publicKey,amountBigInt,decimals,[],"confirmed",TOKEN_2022_PROGRAM_ID);
Η βοηθητική συνάρτηση επιλύει αυτόματα τον λογαριασμό από τον λογαριασμό δεδομένων ExtraAccounts. Ο τρόπος με τον οποίο ο λογαριασμός θα επιλυόταν στον πελάτη είναι ο εξής:
const [counterPDA] = PublicKey.findProgramAddressSync([Buffer.from("counter"), wallet.publicKey.toBuffer()],program.programId);
Σημειώστε ότι ο λογαριασμός μετρητή αντλείται από τον ιδιοκτήτη του token account και πρέπει να αρχικοποιηθεί πριν από τη διενέργεια μεταφοράς. Στην περίπτωση αυτού του παραδείγματος, αρχικοποιούμε τον λογαριασμό μετρητή όταν αρχικοποιούμε τα extra account metas. Επομένως, θα έχουμε ένα PDA μετρητή μόνο για τον ιδιοκτήτη του token account που κάλεσε αυτή τη συνάρτηση. Αν θέλετε να έχετε έναν λογαριασμό μετρητή για κάθε token account του mint σας, θα χρειαστεί να έχετε κάποια λειτουργικότητα για τη δημιουργία αυτών των PDAs εκ των προτέρων. Θα μπορούσε να υπάρχει ένα κουμπί στο dapp σας για εγγραφή σε έναν μετρητή που δημιουργεί αυτόν τον λογαριασμό PDA και από εκεί και πέρα οι χρήστες μπορούν να χρησιμοποιούν αυτό το token μετρητή.
Συμπέρασμα
Η επέκταση Transfer Hook και το Transfer Hook Interface επιτρέπουν τη δημιουργία mint accounts που εκτελούν προσαρμοσμένη λογική εντολών σε κάθε μεταφορά token. Αυτός ο οδηγός χρησιμεύει ως αναφορά για να σας βοηθήσει να δημιουργήσετε τα δικά σας Transfer Hook programs. Μη διστάσετε να είστε δημιουργικοί και να εξερευνήσετε τις δυνατότητες αυτής της νέας λειτουργικότητας!
Is this page helpful?