Skip to content

Commit f7f094e

Browse files
committed
Add 4 transfer hook examples
1 parent 944bb7d commit f7f094e

File tree

45 files changed

+2184
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2184
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
.anchor
3+
.DS_Store
4+
target
5+
**/*.rs.bk
6+
node_modules
7+
test-ledger
8+
.yarn
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
.anchor
3+
.DS_Store
4+
target
5+
node_modules
6+
dist
7+
build
8+
test-ledger
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[toolchain]
2+
3+
[features]
4+
seeds = false
5+
skip-lint = false
6+
7+
[programs.localnet]
8+
transfer_hook = "DrWbQtYJGtsoRwzKqAbHKHKsCJJfpysudF39GBVFSxub"
9+
10+
[registry]
11+
url = "https://api.apr.dev"
12+
13+
[provider]
14+
cluster = "Localnet"
15+
wallet = "~/.config/solana/id.json"
16+
17+
[scripts]
18+
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[workspace]
2+
members = [
3+
"programs/*"
4+
]
5+
6+
[profile.release]
7+
overflow-checks = true
8+
lto = "fat"
9+
codegen-units = 1
10+
[profile.release.build-override]
11+
opt-level = 3
12+
incremental = false
13+
codegen-units = 1
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Migrations are an early feature. Currently, they're nothing more than this
2+
// single deploy script that's invoked from the CLI, injecting a provider
3+
// configured from the workspace's Anchor.toml.
4+
5+
const anchor = require("@coral-xyz/anchor");
6+
7+
module.exports = async function (provider) {
8+
// Configure client to use the provider.
9+
anchor.setProvider(provider);
10+
11+
// Add your deploy script here.
12+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"scripts": {
3+
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
4+
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
5+
},
6+
"dependencies": {
7+
"@coral-xyz/anchor": "^0.29.0",
8+
"@solana/spl-token": "^0.4.0",
9+
"@solana/web3.js": "^1.89.1"
10+
},
11+
"devDependencies": {
12+
"@types/bn.js": "^5.1.0",
13+
"@types/chai": "^4.3.0",
14+
"@types/mocha": "^9.0.0",
15+
"chai": "^4.3.4",
16+
"mocha": "^9.0.3",
17+
"prettier": "^2.6.2",
18+
"ts-mocha": "^10.0.0",
19+
"typescript": "^4.3.5"
20+
}
21+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[190,198,254,219,195,198,188,248,91,58,36,89,114,143,221,246,55,38,153,95,43,99,203,221,16,226,83,243,224,154,57,42,97,126,109,17,158,56,137,34,19,105,170,85,64,116,76,234,202,180,168,197,104,240,143,161,175,114,99,93,98,123,237,240]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "transfer-hook"
3+
version = "0.1.0"
4+
description = "Created with Anchor"
5+
edition = "2021"
6+
7+
[lib]
8+
crate-type = ["cdylib", "lib"]
9+
name = "transfer_hook"
10+
11+
[features]
12+
no-entrypoint = []
13+
no-idl = []
14+
no-log-ix-name = []
15+
cpi = ["no-entrypoint"]
16+
default = []
17+
18+
[dependencies]
19+
anchor-lang = {version = "0.29.0", features = ["init-if-needed"]}
20+
anchor-spl = "0.29.0"
21+
solana-program = "=1.17.17"
22+
23+
spl-transfer-hook-interface = "0.5.0"
24+
spl-tlv-account-resolution = "0.5.0"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[target.bpfel-unknown-unknown.dependencies.std]
2+
features = []
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use anchor_lang::{
2+
prelude::*,
3+
system_program::{create_account, CreateAccount},
4+
};
5+
use anchor_spl::{
6+
associated_token::AssociatedToken,
7+
token_interface::{Mint, TokenAccount, TokenInterface},
8+
};
9+
use spl_tlv_account_resolution::{
10+
account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList,
11+
};
12+
use spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction};
13+
14+
declare_id!("DrWbQtYJGtsoRwzKqAbHKHKsCJJfpysudF39GBVFSxub");
15+
16+
#[error_code]
17+
pub enum MyError {
18+
#[msg("The amount is too big")]
19+
AmountTooBig,
20+
}
21+
22+
#[program]
23+
pub mod transfer_hook {
24+
use super::*;
25+
26+
pub fn initialize_extra_account_meta_list(
27+
ctx: Context<InitializeExtraAccountMetaList>,
28+
) -> Result<()> {
29+
30+
// The `addExtraAccountsToInstruction` JS helper function resolving incorrectly
31+
let account_metas = vec![
32+
ExtraAccountMeta::new_with_seeds(
33+
&[Seed::Literal {
34+
bytes: "counter".as_bytes().to_vec(),
35+
}],
36+
false, // is_signer
37+
true, // is_writable
38+
)?,
39+
];
40+
41+
// calculate account size
42+
let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64;
43+
// calculate minimum required lamports
44+
let lamports = Rent::get()?.minimum_balance(account_size as usize);
45+
46+
let mint = ctx.accounts.mint.key();
47+
let signer_seeds: &[&[&[u8]]] = &[&[
48+
b"extra-account-metas",
49+
&mint.as_ref(),
50+
&[ctx.bumps.extra_account_meta_list],
51+
]];
52+
53+
// create ExtraAccountMetaList account
54+
create_account(
55+
CpiContext::new(
56+
ctx.accounts.system_program.to_account_info(),
57+
CreateAccount {
58+
from: ctx.accounts.payer.to_account_info(),
59+
to: ctx.accounts.extra_account_meta_list.to_account_info(),
60+
},
61+
)
62+
.with_signer(signer_seeds),
63+
lamports,
64+
account_size,
65+
ctx.program_id,
66+
)?;
67+
68+
// initialize ExtraAccountMetaList account with extra accounts
69+
ExtraAccountMetaList::init::<ExecuteInstruction>(
70+
&mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,
71+
&account_metas,
72+
)?;
73+
74+
Ok(())
75+
}
76+
77+
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
78+
79+
if amount > 50 {
80+
msg!("The amount is too big {0}", amount);
81+
// return err!(MyError::AmountTooBig);
82+
}
83+
84+
ctx.accounts.counter_account.counter.checked_add(1).unwrap();
85+
86+
msg!("This token has been transfered {0} times", ctx.accounts.counter_account.counter);
87+
88+
Ok(())
89+
}
90+
91+
// fallback instruction handler as workaround to anchor instruction discriminator check
92+
pub fn fallback<'info>(
93+
program_id: &Pubkey,
94+
accounts: &'info [AccountInfo<'info>],
95+
data: &[u8],
96+
) -> Result<()> {
97+
let instruction = TransferHookInstruction::unpack(data)?;
98+
99+
// match instruction discriminator to transfer hook interface execute instruction
100+
// token2022 program CPIs this instruction on token transfer
101+
match instruction {
102+
TransferHookInstruction::Execute { amount } => {
103+
let amount_bytes = amount.to_le_bytes();
104+
105+
// invoke custom transfer hook instruction on our program
106+
__private::__global::transfer_hook(program_id, accounts, &amount_bytes)
107+
}
108+
_ => return Err(ProgramError::InvalidInstructionData.into()),
109+
}
110+
}
111+
}
112+
113+
#[derive(Accounts)]
114+
pub struct InitializeExtraAccountMetaList<'info> {
115+
#[account(mut)]
116+
payer: Signer<'info>,
117+
118+
/// CHECK: ExtraAccountMetaList Account, must use these seeds
119+
#[account(
120+
mut,
121+
seeds = [b"extra-account-metas", mint.key().as_ref()],
122+
bump
123+
)]
124+
pub extra_account_meta_list: AccountInfo<'info>,
125+
pub mint: InterfaceAccount<'info, Mint>,
126+
#[account(
127+
init_if_needed,
128+
seeds = [b"counter"],
129+
bump,
130+
payer = payer,
131+
space = 16
132+
)]
133+
pub counter_account: Account<'info, CounterAccount>,
134+
pub token_program: Interface<'info, TokenInterface>,
135+
pub associated_token_program: Program<'info, AssociatedToken>,
136+
pub system_program: Program<'info, System>,
137+
}
138+
139+
// Order of accounts matters for this struct.
140+
// The first 4 accounts are the accounts required for token transfer (source, mint, destination, owner)
141+
// Remaining accounts are the extra accounts required from the ExtraAccountMetaList account
142+
// These accounts are provided via CPI to this program from the token2022 program
143+
#[derive(Accounts)]
144+
pub struct TransferHook<'info> {
145+
#[account(
146+
token::mint = mint,
147+
token::authority = owner,
148+
)]
149+
pub source_token: InterfaceAccount<'info, TokenAccount>,
150+
pub mint: InterfaceAccount<'info, Mint>,
151+
#[account(
152+
token::mint = mint,
153+
)]
154+
pub destination_token: InterfaceAccount<'info, TokenAccount>,
155+
/// CHECK: source token account owner, can be SystemAccount or PDA owned by another program
156+
pub owner: UncheckedAccount<'info>,
157+
/// CHECK: ExtraAccountMetaList Account,
158+
#[account(
159+
seeds = [b"extra-account-metas", mint.key().as_ref()],
160+
bump
161+
)]
162+
pub extra_account_meta_list: UncheckedAccount<'info>,
163+
#[account(
164+
seeds = [b"counter"],
165+
bump
166+
)]
167+
pub counter_account: Account<'info, CounterAccount>,
168+
}
169+
170+
#[account]
171+
pub struct CounterAccount {
172+
counter: u64,
173+
}

0 commit comments

Comments
 (0)