La extensión Transfer Hook y la Transfer Hook Interface introducen la capacidad de crear Mint Accounts que ejecutan lógica de instrucciones personalizadas en cada transferencia de tokens.
Esto habilita muchos nuevos casos de uso para las transferencias de tokens, tales como:
- Aplicar regalías de NFT
- Listas negras o blancas de billeteras que pueden recibir tokens
- Implementar comisiones personalizadas en transferencias de tokens
- Crear eventos personalizados de transferencia de tokens
- Rastrear estadísticas sobre tus transferencias de tokens
- Y muchos más
Para lograr esto, los desarrolladores deben construir un programa que implemente la Transfer Hook Interface e inicializar una mint account con la extensión Transfer Hook habilitada.
Por cada transferencia de tokens que involucre tokens de la mint account, el programa Token Extensions realiza un Cross Program Invocation (CPI) para ejecutar una instrucción en el programa Transfer Hook.
Cuando el programa Token Extensions realiza un CPI a un programa Transfer Hook, todas las cuentas de la transferencia inicial se convierten en cuentas de solo lectura. Esto significa que los privilegios de firmante del emisor no se extienden al programa Transfer Hook.
Esta decisión de diseño se toma para prevenir el uso malicioso de los programas Transfer Hook.
En esta guía, crearemos un programa Transfer Hook usando el framework Anchor; sin embargo, también es posible implementar la Transfer Hook Interface usando un programa nativo. Obtén más información sobre el framework Anchor aquí: Anchor Framework
Descripción general de la Transfer Hook Interface
La Transfer Hook Interface proporciona una forma para que los desarrolladores implementen lógica de instrucciones personalizada que se ejecuta en cada transferencia de tokens para una mint account específica.
La Transfer Hook Interface especifica las siguientes instrucciones:
Execute: Una instrucción que el programa Token Extension invoca en cada transferencia de tokens.InitializeExtraAccountMetaList(opcional): Crea una cuenta que almacena una lista de cuentas adicionales requeridas por la instrucción personalizadaExecute.UpdateExtraAccountMetaList(opcional): Actualiza la lista de cuentas adicionales sobrescribiendo la lista existente.
Técnicamente no es obligatorio implementar la instrucción InitializeExtraAccountMetaList
usando la interfaz. La cuenta puede ser creada por cualquier instrucción
en un programa Transfer Hook.
Sin embargo, el Program Derived Address (PDA) para la cuenta debe derivarse usando los siguientes seeds:
- La cadena fija "extra-account-metas"
- La dirección de la mint account
- El ID del programa Transfer Hook
const [pda] = PublicKey.findProgramAddressSync([Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()],program.programId // transfer hook program ID);
Al almacenar las cuentas adicionales requeridas por la instrucción Execute en el
PDA predefinido, estas cuentas pueden añadirse automáticamente a una instrucción
de transferencia de tokens desde el cliente.
Transfer Hook Hello World
Este ejemplo es el hola mundo de los transfer hooks. Es un transfer hook sencillo que simplemente imprimirá un mensaje en cada transferencia de tokens. Comenzamos abriendo el ejemplo en Solana Playground, una herramienta en línea para construir y desplegar programas de Solana: link
El ejemplo consiste en un programa de Anchor que implementa la transfer hook interface y un archivo de pruebas para testear el programa.
Este programa solo incluirá 3 instrucciones:
initialize_extra_account_meta_list: Crea una cuenta que almacena una lista de cuentas adicionales requeridas por la instruccióntransfer_hook. En el hello world dejamos esto vacío.transfer_hook: Esta instrucción es invocada mediante CPI en cada transferencia de tokens para realizar una transferencia de tokens SOL envuelto.fallback: Dado que usamos Anchor y el token program es un programa nativo, necesitamos añadir una instrucción de fallback para hacer coincidir manualmente el discriminador de instrucción e invocar nuestra instrucción personalizadatransfer_hook. No es necesario modificar esta función.
Cada vez que el token sea transferido, la función transfer_hook será
llamada por el token program.
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {msg!("Hello Transfer Hook!");Ok(())}
En esta función ahora puedes añadir tu lógica adicional. Por ejemplo, podrías hacer que la transferencia falle cuando se transfiera una cantidad mayor a 50 de la siguiente manera:
#[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(())}
Para ejecutar el ejemplo en Solana Playground, sigue este enlace: link
En la terminal de Playground, ejecuta el comando build, que actualizará el valor de
declare_id en el archivo lib.rs con un ID de programa recién generado. Luego ejecuta
el comando deploy para desplegar tu programa en devnet. Una vez que el programa esté
desplegado, puedes ejecutar el archivo de pruebas usando el comando test en la terminal.
Esto te dará una salida similar a esta:
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)
Si no deseas usar JavaScript para crear tu token, también puedes usar el
comando spl-token de la CLI de Solana después de desplegar tu programa:
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token --transfer-hook yourTransferHookProgramId
Transfer Hook con contador
El siguiente ejemplo te mostrará cómo incrementar un contador cada vez que tu token haya sido transferido. link
Si deseas añadir lógica a tu transfer hook que necesite cuentas adicionales, debes añadirlas a la cuenta ExtraAccountMetaList. En nuestro caso, queremos un PDA que guarde la cantidad de veces que el token ha sido transferido.
Esto se puede hacer añadiendo el siguiente código a la instrucción
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)?,];
Y también necesitamos crear esta cuenta cuando inicializamos la nueva mint account y debemos pasarla cada vez que transferimos el 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>,}
Y la cuenta contendrá una variable contador de tipo u64:
#[account]pub struct CounterAccount {counter: u64,}
Ahora en nuestra función transfer hook podemos simplemente incrementar este contador en uno cada vez que sea llamada:
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(())}
En el cliente, estas cuentas adicionales se añaden automáticamente mediante la función auxiliar createTransferCheckedWithTransferHookInstruction:
let transferInstructionWithHelper =await createTransferCheckedWithTransferHookInstruction(connection,sourceTokenAccount,mint.publicKey,destinationTokenAccount,wallet.publicKey,amountBigInt,decimals,[],"confirmed",TOKEN_2022_PROGRAM_ID);
Para ejecutar el ejemplo en Solana Playground, sigue este enlace: link
Y luego escribe build, lo que actualizará el valor de declare_id en
el archivo lib.rs con un ID de programa recién generado. Luego escribe deploy para
desplegar tu programa en devnet. Una vez que el programa esté desplegado, puedes ejecutar el
archivo de pruebas escribiendo test en la terminal.
Esto te dará la siguiente salida. En la última transacción podrás ver cuántas veces ha sido transferido tu 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)
Dado que aquí estamos incrementando un contador cada vez que el token es transferido, debemos asegurarnos de que la instrucción transfer hook solo pueda ser llamada durante una transferencia, de lo contrario alguien podría llamar directamente a la instrucción transfer hook y alterar nuestro contador. Esta es una verificación que deberías añadir a cualquiera de tus transfer hooks.
Puedes añadir la verificación de la siguiente manera:
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(())}
Y luego llamarla al inicio de tu función 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 con comisión de transferencia en wSOL (ejemplo avanzado)
En la siguiente parte de esta guía, construiremos un programa Transfer Hook más avanzado usando el framework Anchor. Este programa requerirá que el emisor pague una comisión en wSOL por cada transferencia de tokens.
Las transferencias de wSOL se ejecutarán usando un delegado que es un PDA derivado del programa Transfer Hook. Esto es necesario porque la firma del emisor inicial de la instrucción de transferencia de tokens no es accesible en el programa Transfer Hook.
Este programa solo incluirá 3 instrucciones:
initialize_extra_account_meta_list: Crea una cuenta que almacena una lista de cuentas adicionales requeridas por la instruccióntransfer_hook.transfer_hook: Esta instrucción es invocada mediante CPI en cada transferencia de tokens para realizar una transferencia de tokens SOL envuelto.fallback: Las instrucciones de la transfer hook interface tienen discriminadores específicos (identificadores de instrucción). En un programa Anchor, podemos usar una instrucción de fallback para hacer coincidir manualmente el discriminador de instrucción e invocar nuestra instrucción personalizadatransfer_hook.
Este programa requerirá que el emisor pague una comisión en SOL envuelto (wSOL) en cada transferencia de tokens. Aquí está el programa final.
Primeros pasos
Comienza abriendo este enlace de Solana Playground link y luego haz clic en el botón "Import" para copiar el proyecto.
El código de inicio incluye un archivo lib.rs y transfer-hook.test.ts que están
estructurados para el programa que crearemos. En el archivo lib.rs deberías
ver el siguiente código:
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 {}
Una vez que hayas importado el proyecto, compila el programa usando el comando build
en la terminal de Playground.
build
Esto actualizará el valor de declare_id en el archivo lib.rs con un
ID de programa recién generado.
Instrucción para inicializar la cuenta ExtraAccountMetas
En este paso, implementaremos la instrucción initialize_extra_account_meta_list
para nuestro programa Transfer Hook. Esta instrucción crea una
cuenta ExtraAccountMetas, que almacenará las cuentas adicionales requeridas por
nuestra instrucción transfer_hook.
En este ejemplo, la instrucción initialize_extra_account_meta_list requiere 7
cuentas:
payer: La cuenta utilizada para pagar la creación de la cuenta ExtraAccountMetas.extra_account_meta_list: La cuenta ExtraAccountMetas creada para almacenar la lista de cuentas requeridas por nuestra instruccióntransfer_hook.mint: La mint account que apunta a este programa Transfer Hook. La dirección del mint es un seed obligatorio para derivar el PDAextra_account_meta_list.wsol_mint: El mint de SOL envuelto.token_program: El ID del Token Program original.associated_token_program: El ID del Associated Token Program.system_program: El System Program, que es una cuenta requerida al crear nuevas cuentas.
Las direcciones de mint, wsol_mint y associated_token_program se usarán
para derivar las direcciones de los associated token accounts de wSOL. Estas
cuentas son requeridas por la instrucción transfer_hook y se almacenarán en
la cuenta ExtraAccountMetas.
Actualiza la estructura InitializeExtraAccountMetaList reemplazando el siguiente
código de inicio:
#[derive(Accounts)]pub struct InitializeExtraAccountMetaList {}
Con el código proporcionado a continuación:
#[derive(Accounts)]pub struct InitializeExtraAccountMetaList<'info> {#[account(mut)]payer: Signer<'info>,/// CHECK: ExtraAccountMetaList Account, must use these seeds#[account(mut,seeds = [b"extra-account-metas", mint.key().as_ref()],bump)]pub extra_account_meta_list: AccountInfo<'info>,pub mint: InterfaceAccount<'info, Mint>,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>,}
A continuación, actualiza la instrucción initialize_extra_account_meta_list reemplazando
el siguiente código de inicio:
pub fn initialize_extra_account_meta_list(ctx: Context<InitializeExtraAccountMetaList>,) -> Result<()> {Ok(())}
Con el código a continuación:
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(())}
Repasemos la lógica de la instrucción actualizada. Comenzamos listando las cuentas adicionales que deben almacenarse en la cuenta 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)?,];
Existen tres métodos para almacenar estas cuentas:
- Almacenar directamente la dirección de la cuenta:
- Dirección del mint de SOL envuelto
- ID del Token Program
- ID del 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,)?,
- Almacenar los seeds para derivar un PDA del programa Transfer Hook:
- PDA del delegado
// index 8, delegate PDAExtraAccountMeta::new_with_seeds(&[Seed::Literal {bytes: "delegate".as_bytes().to_vec(),}],false, // is_signerfalse, // is_writable)?,
- Almacena los seeds para derivar un PDA para un programa distinto al programa Transfer Hook:
- Delegar associated token account de wSOL
- associated token account de wSOL del remitente
// 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)?,
A continuación, calculamos el tamaño y el rent necesarios para almacenar la lista de 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);
A continuación, realizamos un CPI al System Program para crear una cuenta y establecer el Token Extensions Program como propietario. Los seeds del PDA se incluyen como seeds firmantes en el CPI porque estamos usando el PDA como la dirección de la nueva cuenta.
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,)?;
Una vez creada la cuenta, inicializamos los datos de la cuenta para almacenar la lista de ExtraAccountMetas.
// initialize ExtraAccountMetaList account with extra accountsExtraAccountMetaList::init::<ExecuteInstruction>(&mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,&account_metas,)?;
En este ejemplo, no estamos usando la interfaz Transfer Hook para crear la cuenta ExtraAccountMetas.
Instrucción Transfer Hook personalizada
A continuación, implementemos la instrucción personalizada transfer_hook. Esta es la instrucción que el Token Extensions Program invocará en cada transferencia de tokens.
En este ejemplo, requeriremos una comisión pagada en wSOL por cada transferencia de tokens. Para simplificar, el importe de la comisión es igual al importe de la transferencia de tokens.
Actualiza el struct TransferHook reemplazando el siguiente código de inicio:
#[derive(Accounts)]pub struct TransferHook {}
Con el código actualizado a continuación:
Ten en cuenta que el orden de las cuentas en este struct es importante. Este es el orden en que el Token Extensions Program proporciona estas cuentas cuando realiza un CPI a este programa Transfer Hook.
// 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>,}
Las primeras 4 cuentas son las requeridas por la transferencia de tokens inicial.
#[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>,
La 5.ª cuenta es la dirección de la cuenta ExtraAccountMeta que almacena la lista de cuentas adicionales requeridas por nuestra instrucción transfer_hook.
/// CHECK: ExtraAccountMetaList Account#[account(seeds = [b"extra-account-metas", mint.key().as_ref()],bump)]pub extra_account_meta_list: UncheckedAccount<'info>,
Las cuentas restantes son las listadas en la cuenta ExtraAccountMetas en el orden en que las definimos en la instrucción 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>,
A continuación, actualiza la instrucción transfer_hook reemplazando el siguiente código de inicio:
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {Ok(())}
Con el código actualizado a continuación:
// 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(())}
Dentro de la lógica de la instrucción, realizamos un CPI para transferir wSOL desde el token account de wSOL del remitente. Esta transferencia se firma mediante el PDA delegado. Por cada transferencia de tokens, el remitente debe aprobar previamente al delegado por el importe de la transferencia.
Instrucción Fallback
Por último, necesitamos agregar una instrucción fallback al programa Anchor para gestionar el CPI proveniente del Token Extensions Program.
Este paso es necesario debido a la diferencia en la forma en que Anchor genera los discriminadores de instrucciones en comparación con los utilizados en las instrucciones de la interfaz Transfer Hook. El discriminador de instrucción para la instrucción transfer_hook no coincidirá con el de la interfaz Transfer Hook.
Actualiza la instrucción fallback reemplazando el siguiente código de inicio:
pub fn fallback<'info>(program_id: &Pubkey,accounts: &'info [AccountInfo<'info>],data: &[u8],) -> Result<()> {Ok(())}
Con el código actualizado a continuación:
// 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()),}}
La instrucción fallback verifica si el discriminador de instrucción de una instrucción entrante coincide con la instrucción Execute de la interfaz Transfer Hook. Si hay una coincidencia exitosa, invoca la instrucción transfer_hook en nuestro programa Anchor.
Actualmente, existe una función de Anchor aún no publicada que simplifica este proceso. Eliminaría la necesidad de la instrucción fallback.
Compilar e implementar el programa
El programa Transfer Hook ya está completo. Asegúrate de tener suficiente SOL en Devnet en tu billetera de Playground para implementar el programa.
Para compilar el programa, usa el siguiente comando:
build
A continuación, implementa el programa con el comando:
deploy
Descripción general del archivo de pruebas
A continuación, probemos el programa. Abre el archivo transfer-hook.test.ts y deberías ver el siguiente código de inicio:
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 () => {});});
Primero, generamos un keypair para usarlo como dirección de un nuevo mint account. Usando la dirección del mint, derivamos las direcciones de associated token account (ATA) que utilizaremos para la transferencia de tokens.
// 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);
A continuación, derivamos el PDA para la cuenta ExtraAccountMetas. Esta cuenta se crea para almacenar las cuentas adicionales requeridas por la instrucción de transfer hook personalizada.
// 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);
También derivamos el PDA que se usará como delegado. El remitente debe aprobar esta dirección como delegado para su token account de wSOL. Este PDA delegado se usa para "firmar" la transferencia de wSOL en la instrucción de transfer hook personalizada.
// PDA delegate to transfer wSOL tokens from senderconst [delegatePDA] = PublicKey.findProgramAddressSync([Buffer.from("delegate")],program.programId);
Además, derivamos las direcciones de los token accounts de wSOL. La primera dirección corresponde al token account de wSOL del remitente, que debe estar financiado para pagar la comisión requerida por la instrucción de transfer hook. La segunda dirección corresponde al token account de wSOL propiedad del PDA delegado. En este ejemplo, todas las comisiones en wSOL se envían a esta cuenta.
// 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);
Finalmente, como parte de la configuración, creamos los token accounts de wSOL.
// 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);});
Crear un mint account
Para comenzar, construye una transacción para crear un nuevo mint account con la extensión Transfer Hook habilitada. En esta transacción, asegúrate de especificar nuestro programa como el Token Extensions Program almacenado en la extensión.
Habilitar la extensión Transfer Hook permite al Token Extensions Program determinar qué programa invocar en cada transferencia de tokens.
Reemplaza la prueba de marcador de posición:
it("Create Mint Account with Transfer Hook Extension", async () => {});
Con la prueba actualizada a continuación:
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}`);});
Crear token accounts
A continuación, como parte de la configuración, crea los associated token accounts tanto para el remitente como para el destinatario. Además, acredita en la cuenta del remitente algunos tokens.
Reemplaza la prueba de marcador de posición:
it("Create Token Accounts and Mint Tokens", async () => {});
Con la prueba actualizada a continuación:
// 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}`);});
Crear la cuenta ExtraAccountMeta
Antes de enviar una transferencia de tokens, necesitamos crear la cuenta ExtraAccountMetas para almacenar todas las cuentas adicionales requeridas por la instrucción de transfer hook.
Para crear esta cuenta, invocamos la instrucción de nuestro programa.
Reemplaza la prueba de marcador de posición:
it("Create ExtraAccountMetaList Account", async () => {});
Con la prueba actualizada a continuación:
// 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);});
Transferir tokens
Por último, estamos listos para enviar una transferencia de tokens. Además de la instrucción de transferencia, hay algunas instrucciones adicionales que deben incluirse.
- El remitente debe transferir SOL a su token account de wSOL para cubrir la comisión requerida por la instrucción de transfer hook.
- El remitente debe aprobar al PDA delegado por el importe de la comisión en wSOL.
- Incluye una instrucción para sincronizar el saldo de wSOL.
- La instrucción de transferencia de tokens debe incluir todas las cuentas adicionales requeridas por la instrucción de transfer hook.
Reemplaza la prueba de marcador de posición:
it("Transfer Hook with Extra Account Meta", async () => {});
Con la prueba actualizada a continuación:
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);});
La instrucción de transferencia debe incluir todos los AccountMetas adicionales, la dirección de la cuenta ExtraAccountMetas y la dirección del programa Transfer Hook.
Ejecutar el archivo de pruebas
Una vez que hayas actualizado todas las pruebas, el paso final es ejecutar la prueba.
Para ejecutar el archivo de pruebas, usa el siguiente comando en el terminal:
test
Deberías ver una salida similar a la siguiente:
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)
Usar datos de token account en transfer hook
A veces puede que desees usar datos de cuenta para derivar cuentas adicionales en los extra account metas. Esto es útil si, por ejemplo, quieres usar el propietario del token account como seed para un PDA.
Al crear el ExtraAccountMeta, puedes usar los datos de cualquier cuenta como seed adicional. En este caso queremos derivar una cuenta contador a partir del propietario del token account y la cadena 'counter'. Esto significa que siempre podremos ver con qué frecuencia ese propietario del token account ha transferido tokens.
Así es como se configura en la función 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)?])}}
Veamos el struct del token account para entender cómo se almacenan los datos de la cuenta. A continuación se muestra un ejemplo de la estructura de un token account. Así podemos tomar 32 bytes en la posición 32 a 64 como el propietario del token account, que se encuentra en 'account_index: 0'. 'account_index` hace referencia al índice de la cuenta en el array de cuentas. En el caso de un transfer hook, el token account propietario es la primera entrada en el array de cuentas. La segunda cuenta es siempre el mint y la tercera cuenta es el token account de destino. Este orden de cuentas es el mismo que en el antiguo 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>,}
En nuestro caso, queremos derivar una cuenta contador a partir del propietario del token account del remitente, por lo que cuando creamos las cuentas ExtraAccountMeta, inicializamos (init) esta cuenta contador PDA derivada del propietario del token account del remitente y la cadena 'counter'. Cuando la cuenta contador PDA esté inicializada, podremos usarla dentro del transfer hook para incrementar el valor en cada transferencia.
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>,}
También necesitamos definir esta cuenta contador adicional en el struct TransferHook. Estas son las cuentas que se pasan a nuestro programa TransferHook cada vez que se realiza una transferencia. El cliente obtiene estas cuentas adicionales del PDA ExtraAccountsMetaList y las incluye en la instrucción de transferencia de tokens, pero aquí en el programa aún necesitamos definirla.
#[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>,}
En el cliente, esta cuenta se genera automáticamente y puedes usarla de la siguiente manera.
const transferInstructionWithHelper =await createTransferCheckedWithTransferHookInstruction(connection,sourceTokenAccount,mint.publicKey,destinationTokenAccount,wallet.publicKey,amountBigInt,decimals,[],"confirmed",TOKEN_2022_PROGRAM_ID);
La función auxiliar resuelve la cuenta automáticamente a partir de la cuenta de datos ExtraAccounts. Así es como se resolvería la cuenta en el cliente:
const [counterPDA] = PublicKey.findProgramAddressSync([Buffer.from("counter"), wallet.publicKey.toBuffer()],program.programId);
Ten en cuenta que la cuenta contador se deriva del propietario del token account y debe inicializarse antes de realizar una transferencia. En este ejemplo, inicializamos la cuenta contador cuando inicializamos los extra account metas. Por lo tanto, solo tendremos un PDA contador para el propietario del token account que llamó a esa función. Si deseas tener una cuenta contador para cada token account de tu mint, necesitarás tener alguna funcionalidad para crear estos PDAs con antelación. Podría haber un botón en tu dapp para registrarse y obtener un contador que cree esta cuenta PDA, y a partir de entonces los usuarios podrán usar este token contador.
Conclusión
La extensión Transfer Hook y la interfaz Transfer Hook permiten la creación de mint accounts que ejecutan lógica de instrucciones personalizada en cada transferencia de tokens. Esta guía sirve como referencia para ayudarte a crear tus propios programas Transfer Hook. ¡Siéntete libre de ser creativo y explorar las capacidades de esta nueva funcionalidad!
Is this page helpful?