Ayush 40aed81963 chore: bumped versions (#378) hace 4 meses
..
migrations 8139019dd2 Example on how to use account data as seeds (#111) hace 1 año
programs 40aed81963 chore: bumped versions (#378) hace 4 meses
tests 8139019dd2 Example on how to use account data as seeds (#111) hace 1 año
.gitignore 8139019dd2 Example on how to use account data as seeds (#111) hace 1 año
.prettierignore 8139019dd2 Example on how to use account data as seeds (#111) hace 1 año
.prettierrc 8139019dd2 Example on how to use account data as seeds (#111) hace 1 año
Anchor.toml 40aed81963 chore: bumped versions (#378) hace 4 meses
Cargo.toml 8139019dd2 Example on how to use account data as seeds (#111) hace 1 año
package.json 8139019dd2 Example on how to use account data as seeds (#111) hace 1 año
pnpm-lock.yaml 8139019dd2 Example on how to use account data as seeds (#111) hace 1 año
readme.md 8139019dd2 Example on how to use account data as seeds (#111) hace 1 año
tsconfig.json 8139019dd2 Example on how to use account data as seeds (#111) hace 1 año

readme.md

Using token account data in transfer hook

Sometimes you may want to use account data to derive additional accounts in the extra account metas. This is useful if, for example, you want to use the token account's owner as a seed for a PDA.

When creating the ExtraAccountMeta you can use the data of any account as an extra seed. In this case we want to derive a counter account from the token account owner and the string 'counter'. This means we will be always able to see how often that token account owner has transferred tokens.

This is how you set it up in the extra_account_metas() function.

// Define extra account metas to store on extra_account_meta_list account
impl<'info> InitializeExtraAccountMetaList<'info> {
    pub fn extra_account_metas() -> Result<Vec<ExtraAccountMeta>> {
        Ok(
            vec![
                ExtraAccountMeta::new_with_seeds(
                    &[
                        Seed::Literal {
                            bytes: b"counter".to_vec(),
                        },
                        Seed::AccountData { account_index: 0, data_index: 32, length: 32 },
                    ],
                    false, // is_signer
                    true // is_writable
                )?
            ]
        )
    }
}

Let's look at the token account struct to understand how the account data is stored. Below is an example of a token account structure. So we can take 32 bytes at position 32 to 64 as the owner of the token account, which is at 'account_index: 0'. 'account_index` refers to the index of the account in the accounts array. In the case of a transfer hook, the owner token account is the first entry in the accounts array. The second account is always the mint and the third account is the destination token account. This account order is the same as in the old token program.

/// Account data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Account {
    /// The mint associated with this account
    pub mint: Pubkey,
    /// The owner of this account.
    pub owner: Pubkey,
    /// The amount of tokens this account holds.
    pub amount: u64,
    pub delegate: COption<Pubkey>,
    pub state: AccountState,
    pub is_native: COption<u64>,
    pub delegated_amount: u64,
    pub close_authority: COption<Pubkey>,
}

I our case we want to derive a counter account from the owner of the sender token account so when we create the ExtraAccountMeta accounts we initthis PDA counter account that is derived from the sender token account owner and the string 'counter'. When the PDA counter account is initialized we will be able to use it with in the transfer hook to increase it in every transfer.

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

We also need to define this extra counter account in the TransferHook struct. These are the accounts that are passed to our TransferHook program every time a transfer is done. The client get the additional accounts from the ExtraAccountsMetaList PDA but here in the program we still need to define it.

#[derive(Accounts)]
pub struct TransferHook<'info> {
    #[account(token::mint = mint, token::authority = owner)]
    pub source_token: InterfaceAccount<'info, TokenAccount>,
    pub mint: InterfaceAccount<'info, Mint>,
    #[account(token::mint = mint)]
    pub destination_token: InterfaceAccount<'info, TokenAccount>,
    /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program
    pub owner: UncheckedAccount<'info>,
    /// CHECK: ExtraAccountMetaList Account,
    #[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)]
    pub extra_account_meta_list: UncheckedAccount<'info>,
    #[account(seeds = [b"counter", owner.key().as_ref()], bump)]
    pub counter_account: Account<'info, CounterAccount>,
}

In the client this account is auto generated and you can use it like this.

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

The helper function is resolving the account automatically from the ExtraAccounts data account. How the account would be resolved in the client is like this:

const [counterPDA] = PublicKey.findProgramAddressSync(
  [Buffer.from("counter"), wallet.publicKey.toBuffer()],
  program.programId,
);

Note that the counter account is derived from the owner of the token account and needs to be initialized before doing a transfer. In the case of this example we initialize the counter account when we initialize the extra account metas. So we will only have a counter PDA for the owner of the token account that called that function. If you want to have a counter account for every token account for your mint out there you will need to have some functionality to create these PDAs before hand. There could be a button on your dapp to sign up for a counter that creates this PDA account and from then on the users can use this counter token.