Skip to content

Commit d108b90

Browse files
authored
Merge pull request #71 from solana-developers/transfer_hook_examples
Add 4 transfer hook examples
2 parents feb82f2 + 204e31b commit d108b90

File tree

44 files changed

+2200
-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.

44 files changed

+2200
-0
lines changed
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
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
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"
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
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+
};
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+
}
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"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[target.bpfel-unknown-unknown.dependencies.std]
2+
features = []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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+
let account_metas = vec![
31+
ExtraAccountMeta::new_with_seeds(
32+
&[Seed::Literal {
33+
bytes: "counter".as_bytes().to_vec(),
34+
}],
35+
false, // is_signer
36+
true, // is_writable
37+
)?,
38+
];
39+
40+
// calculate account size
41+
let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64;
42+
// calculate minimum required lamports
43+
let lamports = Rent::get()?.minimum_balance(account_size as usize);
44+
45+
let mint = ctx.accounts.mint.key();
46+
let signer_seeds: &[&[&[u8]]] = &[&[
47+
b"extra-account-metas",
48+
&mint.as_ref(),
49+
&[ctx.bumps.extra_account_meta_list],
50+
]];
51+
52+
// create ExtraAccountMetaList account
53+
create_account(
54+
CpiContext::new(
55+
ctx.accounts.system_program.to_account_info(),
56+
CreateAccount {
57+
from: ctx.accounts.payer.to_account_info(),
58+
to: ctx.accounts.extra_account_meta_list.to_account_info(),
59+
},
60+
)
61+
.with_signer(signer_seeds),
62+
lamports,
63+
account_size,
64+
ctx.program_id,
65+
)?;
66+
67+
// initialize ExtraAccountMetaList account with extra accounts
68+
ExtraAccountMetaList::init::<ExecuteInstruction>(
69+
&mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,
70+
&account_metas,
71+
)?;
72+
73+
Ok(())
74+
}
75+
76+
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {
77+
78+
if amount > 50 {
79+
msg!("The amount is too big {0}", amount);
80+
//return err!(MyError::AmountTooBig);
81+
}
82+
83+
ctx.accounts.counter_account.counter.checked_add(1).unwrap();
84+
85+
msg!("This token has been transferred {0} times", ctx.accounts.counter_account.counter);
86+
87+
Ok(())
88+
}
89+
90+
// fallback instruction handler as workaround to anchor instruction discriminator check
91+
pub fn fallback<'info>(
92+
program_id: &Pubkey,
93+
accounts: &'info [AccountInfo<'info>],
94+
data: &[u8],
95+
) -> Result<()> {
96+
let instruction = TransferHookInstruction::unpack(data)?;
97+
98+
// match instruction discriminator to transfer hook interface execute instruction
99+
// token2022 program CPIs this instruction on token transfer
100+
match instruction {
101+
TransferHookInstruction::Execute { amount } => {
102+
let amount_bytes = amount.to_le_bytes();
103+
104+
// invoke custom transfer hook instruction on our program
105+
__private::__global::transfer_hook(program_id, accounts, &amount_bytes)
106+
}
107+
_ => return Err(ProgramError::InvalidInstructionData.into()),
108+
}
109+
}
110+
}
111+
112+
#[derive(Accounts)]
113+
pub struct InitializeExtraAccountMetaList<'info> {
114+
#[account(mut)]
115+
payer: Signer<'info>,
116+
117+
/// CHECK: ExtraAccountMetaList Account, must use these seeds
118+
#[account(
119+
mut,
120+
seeds = [b"extra-account-metas", mint.key().as_ref()],
121+
bump
122+
)]
123+
pub extra_account_meta_list: AccountInfo<'info>,
124+
pub mint: InterfaceAccount<'info, Mint>,
125+
#[account(
126+
init_if_needed,
127+
seeds = [b"counter"],
128+
bump,
129+
payer = payer,
130+
space = 16
131+
)]
132+
pub counter_account: Account<'info, CounterAccount>,
133+
pub token_program: Interface<'info, TokenInterface>,
134+
pub associated_token_program: Program<'info, AssociatedToken>,
135+
pub system_program: Program<'info, System>,
136+
}
137+
138+
// Order of accounts matters for this struct.
139+
// The first 4 accounts are the accounts required for token transfer (source, mint, destination, owner)
140+
// Remaining accounts are the extra accounts required from the ExtraAccountMetaList account
141+
// These accounts are provided via CPI to this program from the token2022 program
142+
#[derive(Accounts)]
143+
pub struct TransferHook<'info> {
144+
#[account(
145+
token::mint = mint,
146+
token::authority = owner,
147+
)]
148+
pub source_token: InterfaceAccount<'info, TokenAccount>,
149+
pub mint: InterfaceAccount<'info, Mint>,
150+
#[account(
151+
token::mint = mint,
152+
)]
153+
pub destination_token: InterfaceAccount<'info, TokenAccount>,
154+
/// CHECK: source token account owner, can be SystemAccount or PDA owned by another program
155+
pub owner: UncheckedAccount<'info>,
156+
/// CHECK: ExtraAccountMetaList Account,
157+
#[account(
158+
seeds = [b"extra-account-metas", mint.key().as_ref()],
159+
bump
160+
)]
161+
pub extra_account_meta_list: UncheckedAccount<'info>,
162+
#[account(
163+
seeds = [b"counter"],
164+
bump
165+
)]
166+
pub counter_account: Account<'info, CounterAccount>,
167+
}
168+
169+
#[account]
170+
pub struct CounterAccount {
171+
counter: u64,
172+
}

0 commit comments

Comments
 (0)