diff --git a/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs b/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs index c485708a6..329e70447 100644 --- a/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs +++ b/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/programs/transfer-hook/src/lib.rs @@ -5,23 +5,16 @@ use anchor_spl::{ associated_token::AssociatedToken, token_2022::{ spl_token_2022::{ - extension::{ - transfer_hook::TransferHookAccount, BaseStateWithExtensionsMut, - PodStateWithExtensionsMut, - }, + extension::{transfer_hook::TransferHookAccount, BaseStateWithExtensionsMut, PodStateWithExtensionsMut}, pod::PodAccount, }, Token2022, }, token_interface::{Mint, TokenAccount}, }; -use spl_tlv_account_resolution::{ - account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList, -}; use spl_discriminator::SplDiscriminate; -use spl_transfer_hook_interface::instruction::{ - ExecuteInstruction, InitializeExtraAccountMetaListInstruction, -}; +use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList}; +use spl_transfer_hook_interface::instruction::{ExecuteInstruction, InitializeExtraAccountMetaListInstruction}; declare_id!("1qahDxKHeCLZhbBU2NyMU6vQCQmEUmdeSEBrG5drffK"); @@ -38,9 +31,7 @@ pub mod transfer_hook { use super::*; #[instruction(discriminator = InitializeExtraAccountMetaListInstruction::SPL_DISCRIMINATOR_SLICE)] - pub fn initialize_extra_account_meta_list( - ctx: Context, - ) -> Result<()> { + pub fn initialize_extra_account_meta_list(ctx: Context) -> Result<()> { let extra_account_metas = InitializeExtraAccountMetaList::extra_account_metas()?; // initialize ExtraAccountMetaList account with extra accounts @@ -49,7 +40,8 @@ pub mod transfer_hook { ExtraAccountMetaList::init::( &mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?, &extra_account_metas, - ).map_err(|_| ProgramError::InvalidAccountData)?; + ) + .map_err(|_| ProgramError::InvalidAccountData)?; Ok(()) } @@ -66,14 +58,10 @@ pub mod transfer_hook { } // Increment the transfer count safely - let count = ctx - .accounts - .counter_account - .counter - .checked_add(1) - .ok_or(TransferError::AmountTooBig)?; + let counter_account = &mut ctx.accounts.counter_account; + counter_account.counter = counter_account.counter.checked_add(1).ok_or(TransferError::AmountTooBig)?; - msg!("This token has been transferred {} times", count); + msg!("This token has been transferred {} times", counter_account.counter); Ok(()) } @@ -86,8 +74,8 @@ fn check_is_transferring(ctx: &Context) -> Result<()> { // while anchor-lang 1.0 uses 3.x — structurally identical but different semver types let mut account = PodStateWithExtensionsMut::::unpack(*account_data_ref) .map_err(|_| ProgramError::InvalidAccountData)?; - let account_extension = account.get_extension_mut::() - .map_err(|_| ProgramError::InvalidAccountData)?; + let account_extension = + account.get_extension_mut::().map_err(|_| ProgramError::InvalidAccountData)?; if !bool::from(account_extension.transferring) { return err!(TransferError::IsNotCurrentlyTransferring); @@ -128,18 +116,13 @@ impl<'info> InitializeExtraAccountMetaList<'info> { // while anchor-lang 1.0 uses 3.x — structurally identical but different semver types Ok(vec![ExtraAccountMeta::new_with_seeds( &[ - Seed::Literal { - bytes: b"counter".to_vec(), - }, - Seed::AccountData { - account_index: 0, - data_index: 32, - length: 32, - }, + Seed::Literal { bytes: b"counter".to_vec() }, + Seed::AccountData { account_index: 0, data_index: 32, length: 32 }, ], false, // is_signer true, // is_writable - ).map_err(|_| ProgramError::InvalidArgument)?]) + ) + .map_err(|_| ProgramError::InvalidArgument)?]) } /// Returns the count of extra account metas (avoids the error conversion issue in #[account] attributes) @@ -164,7 +147,7 @@ pub struct TransferHook<'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)] + #[account(mut, seeds = [b"counter", owner.key().as_ref()], bump)] pub counter_account: Account<'info, CounterAccount>, } diff --git a/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/readme.md b/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/readme.md index 812c8ba72..dd77ef18d 100644 --- a/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/readme.md +++ b/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/readme.md @@ -111,7 +111,7 @@ pub struct TransferHook<'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)] + #[account(mut, seeds = [b"counter", owner.key().as_ref()], bump)] pub counter_account: Account<'info, CounterAccount>, } ``` diff --git a/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/tests/transfer-hook.ts b/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/tests/transfer-hook.ts index ae31fc6b5..6f7559eef 100644 --- a/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/tests/transfer-hook.ts +++ b/tokens/token-2022/transfer-hook/account-data-as-seed/anchor/tests/transfer-hook.ts @@ -70,6 +70,9 @@ describe('transfer-hook', () => { program.programId, ); + // Number of transfers sent from the wallet's token account; the counter PDA must track it. + let transfersByWallet = 0; + it('Create Mint Account with Transfer Hook Extension', async () => { const extensions = [ExtensionType.TransferHook]; const mintLen = getMintLen(extensions); @@ -188,6 +191,34 @@ describe('transfer-hook', () => { const txSig = await sendAndConfirmTransaction(connection, transaction, [wallet.payer], { skipPreflight: true }); console.log('Transfer Signature:', txSig); + transfersByWallet += 1; + }); + + it('Counter PDA records every transfer made by the source owner', async () => { + // Distinct amounts so each transfer is a distinct transaction even under the same blockhash. + for (const amount of [2, 3]) { + const transferIx = await createTransferCheckedWithTransferHookInstruction( + connection, + sourceTokenAccount, + mint.publicKey, + destinationTokenAccount, + wallet.publicKey, + BigInt(amount * 10 ** decimals), + decimals, + [], + 'confirmed', + TOKEN_2022_PROGRAM_ID, + ); + await sendAndConfirmTransaction(connection, new Transaction().add(transferIx), [wallet.payer], { + skipPreflight: true, + commitment: 'confirmed', + }); + transfersByWallet += 1; + } + + const counterAccount = await program.account.counterAccount.fetch(counterPDA, 'confirmed'); + expect(transfersByWallet).to.equal(3); + expect(counterAccount.counter.toNumber()).to.equal(transfersByWallet); }); it('Try call transfer hook without transfer', async () => {