Skip to content

Commit

Permalink
Add using account data in transfer hook example (#305)
Browse files Browse the repository at this point in the history
* Add using account data in transfer hook example

* Update transfer-hook.md

* minor proofread

* prettier

---------

Co-authored-by: John <[email protected]>
  • Loading branch information
Woody4618 and ZYJLiu authored Aug 7, 2024
1 parent 48d49c6 commit 3a4505e
Showing 1 changed file with 160 additions and 0 deletions.
160 changes: 160 additions & 0 deletions content/guides/token-extensions/transfer-hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,166 @@ Running tests...
4 passing (5s)
```

### 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.

```rust
// 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.

```rust
/// 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>,
}
```

In 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 `init` this 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 within the transfer hook to increment the value in every transfer.

````rust
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>,
}
````

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 gets these additional accounts from the
ExtraAccountsMetaList PDA and includes them in token transfer instruction, but
here in the program we still need to define it.

```rust
#[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 as follows.

```rust
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:

```js
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.

### Conclusion

The Transfer Hook extension and Transfer Hook Interface allow for the creation
Expand Down

0 comments on commit 3a4505e

Please sign in to comment.