Skip to content

Commit ee743c8

Browse files
refactor: extract instruction handlers for transfer-hook (transfer-cost, whitelist)
Move instruction handlers into src/instructions/ directory following Anchor 1.0 conventions. Shared types and helpers remain in lib.rs.
1 parent 45fabba commit ee743c8

9 files changed

Lines changed: 378 additions & 324 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_spl::token_interface::Mint;
3+
use spl_tlv_account_resolution::state::ExtraAccountMetaList;
4+
use spl_transfer_hook_interface::instruction::ExecuteInstruction;
5+
6+
use crate::{handle_extra_account_metas, handle_extra_account_metas_count, CounterAccount};
7+
8+
#[derive(Accounts)]
9+
pub struct InitializeExtraAccountMetaList<'info> {
10+
#[account(mut)]
11+
payer: Signer<'info>,
12+
13+
/// CHECK: ExtraAccountMetaList Account, must use these seeds
14+
#[account(
15+
init,
16+
seeds = [b"extra-account-metas", mint.key().as_ref()],
17+
bump,
18+
// size_of returns Result with spl's ProgramError — unwrap is safe for known-good input
19+
space = ExtraAccountMetaList::size_of(
20+
handle_extra_account_metas_count()
21+
).unwrap(),
22+
payer = payer
23+
)]
24+
pub extra_account_meta_list: UncheckedAccount<'info>,
25+
pub mint: InterfaceAccount<'info, Mint>,
26+
#[account(init, seeds = [b"counter"], bump, payer = payer, space = CounterAccount::DISCRIMINATOR.len() + CounterAccount::INIT_SPACE)]
27+
pub counter_account: Account<'info, CounterAccount>,
28+
pub system_program: Program<'info, System>,
29+
}
30+
31+
pub fn handler(mut context: Context<InitializeExtraAccountMetaList>) -> Result<()> {
32+
let extra_account_metas = handle_extra_account_metas()?;
33+
34+
// initialize ExtraAccountMetaList account with extra accounts
35+
ExtraAccountMetaList::init::<ExecuteInstruction>(
36+
&mut context.accounts.extra_account_meta_list.try_borrow_mut_data()?,
37+
&extra_account_metas,
38+
)
39+
.map_err(|_| ProgramError::InvalidAccountData)?;
40+
41+
Ok(())
42+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pub mod initialize_extra_account_meta_list;
2+
pub mod transfer_hook;
3+
4+
pub use initialize_extra_account_meta_list::*;
5+
pub use transfer_hook::*;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use anchor_lang::prelude::*;
2+
use anchor_spl::{
3+
associated_token::AssociatedToken,
4+
token::Token,
5+
token_interface::{transfer_checked, Mint, TokenAccount, TransferChecked},
6+
};
7+
8+
use crate::{check_is_transferring, CounterAccount, TransferError};
9+
10+
// Order of accounts matters for this struct.
11+
// The first 4 accounts are the accounts required for token transfer (source, mint, destination, owner)
12+
// Remaining accounts are the extra accounts required from the ExtraAccountMetaList account
13+
// These accounts are provided via CPI to this program from the token2022 program
14+
//
15+
// Box<InterfaceAccount> used for source_token, destination_token, wsol_mint,
16+
// delegate_wsol_token_account, and sender_wsol_token_account to avoid exceeding
17+
// the 4096-byte BPF stack frame limit in try_accounts deserialization.
18+
// This struct has 12 accounts — without Box, the generated code uses ~4160 bytes of stack.
19+
#[derive(Accounts)]
20+
pub struct TransferHook<'info> {
21+
#[account(token::mint = mint, token::authority = owner)]
22+
pub source_token: Box<InterfaceAccount<'info, TokenAccount>>,
23+
pub mint: Box<InterfaceAccount<'info, Mint>>,
24+
#[account(token::mint = mint)]
25+
pub destination_token: Box<InterfaceAccount<'info, TokenAccount>>,
26+
/// CHECK: source token account owner, can be SystemAccount or PDA owned by another program
27+
pub owner: UncheckedAccount<'info>,
28+
/// CHECK: ExtraAccountMetaList Account,
29+
#[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)]
30+
pub extra_account_meta_list: UncheckedAccount<'info>,
31+
pub wsol_mint: Box<InterfaceAccount<'info, Mint>>,
32+
pub token_program: Program<'info, Token>,
33+
pub associated_token_program: Program<'info, AssociatedToken>,
34+
#[account(
35+
mut,
36+
seeds = [b"delegate"],
37+
bump
38+
)]
39+
pub delegate: SystemAccount<'info>,
40+
#[account(
41+
mut,
42+
token::mint = wsol_mint,
43+
token::authority = delegate,
44+
)]
45+
pub delegate_wsol_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
46+
#[account(
47+
mut,
48+
token::mint = wsol_mint,
49+
token::authority = owner,
50+
)]
51+
pub sender_wsol_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
52+
#[account(seeds = [b"counter"], bump)]
53+
pub counter_account: Account<'info, CounterAccount>,
54+
}
55+
56+
pub fn handler(context: Context<TransferHook>, amount: u64) -> Result<()> {
57+
// Fail this instruction if it is not called from within a transfer hook
58+
check_is_transferring(&context)?;
59+
60+
if amount > 50 {
61+
msg!("The amount is too big {0}", amount);
62+
}
63+
64+
context.accounts.counter_account.counter += 1;
65+
66+
msg!(
67+
"This token has been transferred {0} times",
68+
context.accounts.counter_account.counter
69+
);
70+
71+
msg!(
72+
"Is writable mint {0}",
73+
context.accounts.mint.to_account_info().is_writable
74+
);
75+
msg!(
76+
"Is destination mint {0}",
77+
context.accounts.destination_token.to_account_info().is_writable
78+
);
79+
msg!(
80+
"Is source mint {0}",
81+
context.accounts.source_token.to_account_info().is_writable
82+
);
83+
84+
let signer_seeds: &[&[&[u8]]] = &[&[b"delegate", &[context.bumps.delegate]]];
85+
86+
// Transfer WSOL from sender to delegate token account using delegate PDA
87+
// transfer lamports amount equal to token transfer amount
88+
transfer_checked(
89+
CpiContext::new(
90+
context.accounts.token_program.key(),
91+
TransferChecked {
92+
from: context.accounts.sender_wsol_token_account.to_account_info(),
93+
mint: context.accounts.wsol_mint.to_account_info(),
94+
to: context.accounts.delegate_wsol_token_account.to_account_info(),
95+
authority: context.accounts.delegate.to_account_info(),
96+
},
97+
)
98+
.with_signer(signer_seeds),
99+
amount,
100+
context.accounts.wsol_mint.decimals,
101+
)?;
102+
Ok(())
103+
}

0 commit comments

Comments
 (0)