Skip to content

Commit 98f12fa

Browse files
feat: add Quasar variants for 6 transfer-hook token-2022 examples
Transfer hook examples ported: hello-world, counter, account-data-as-seed, transfer-cost, transfer-switch, whitelist. allow-block-list-token skipped (most complex, multi-program interaction).
1 parent e1c9257 commit 98f12fa

File tree

24 files changed

+2506
-0
lines changed

24 files changed

+2506
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[package]
2+
name = "quasar-transfer-hook-account-data-as-seed"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[workspace]
7+
8+
[lints.rust.unexpected_cfgs]
9+
level = "warn"
10+
check-cfg = [
11+
'cfg(target_os, values("solana"))',
12+
]
13+
14+
[lib]
15+
crate-type = ["cdylib", "lib"]
16+
17+
[features]
18+
alloc = []
19+
client = []
20+
debug = []
21+
22+
[dependencies]
23+
quasar-lang = "0.0"
24+
quasar-spl = "0.0"
25+
solana-instruction = { version = "3.2.0" }
26+
27+
[dev-dependencies]
28+
quasar-svm = { version = "0.1" }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[project]
2+
name = "quasar_transfer_hook_account_data_as_seed"
3+
4+
[toolchain]
5+
type = "solana"
6+
7+
[testing]
8+
language = "rust"
9+
10+
[testing.rust]
11+
framework = "quasar-svm"
12+
13+
[testing.rust.test]
14+
program = "cargo"
15+
args = [
16+
"test",
17+
"tests::",
18+
]
19+
20+
[clients]
21+
languages = ["rust"]
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
#![cfg_attr(not(test), no_std)]
2+
3+
use quasar_lang::sysvars::Sysvar;
4+
use quasar_lang::{
5+
cpi::Seed,
6+
prelude::*,
7+
};
8+
9+
#[cfg(test)]
10+
mod tests;
11+
12+
declare_id!("22222222222222222222222222222222222222222222");
13+
14+
/// SPL Transfer Hook Interface discriminators (SHA-256 prefix).
15+
#[allow(dead_code)]
16+
const EXECUTE_DISCRIMINATOR: [u8; 8] = [105, 37, 101, 197, 75, 251, 102, 26];
17+
18+
/// Transfer hook that uses account data as a PDA seed. The counter PDA is
19+
/// seeded by ["counter", owner_pubkey] where the owner pubkey is read from
20+
/// the source token account data at runtime by the Token-2022 program.
21+
#[program]
22+
mod quasar_transfer_hook_account_data_as_seed {
23+
use super::*;
24+
25+
/// Create the ExtraAccountMetaList PDA (with 1 extra account: counter PDA
26+
/// whose seed includes account data from the source token account) and the
27+
/// counter PDA itself.
28+
/// Discriminator = sha256("spl-transfer-hook-interface:initialize-extra-account-metas")[:8]
29+
#[instruction(discriminator = [43, 34, 13, 49, 167, 88, 235, 235])]
30+
pub fn initialize_extra_account_meta_list(
31+
ctx: Ctx<InitializeExtraAccountMetaList>,
32+
) -> Result<(), ProgramError> {
33+
ctx.accounts.initialize_extra_account_meta_list()
34+
}
35+
36+
/// Transfer hook handler — increments a per-owner counter on each transfer.
37+
/// Discriminator = sha256("spl-transfer-hook-interface:execute")[:8]
38+
#[instruction(discriminator = [105, 37, 101, 197, 75, 251, 102, 26])]
39+
pub fn transfer_hook(ctx: Ctx<TransferHook>, _amount: u64) -> Result<(), ProgramError> {
40+
ctx.accounts.transfer_hook()
41+
}
42+
}
43+
44+
// ---------------------------------------------------------------------------
45+
// InitializeExtraAccountMetaList
46+
// ---------------------------------------------------------------------------
47+
48+
#[derive(Accounts)]
49+
pub struct InitializeExtraAccountMetaList<'info> {
50+
#[account(mut)]
51+
pub payer: &'info Signer,
52+
/// ExtraAccountMetaList PDA: ["extra-account-metas", mint]
53+
#[account(mut)]
54+
pub extra_account_meta_list: &'info mut UncheckedAccount,
55+
pub mint: &'info UncheckedAccount,
56+
/// Counter PDA: ["counter", payer_key]
57+
#[account(mut)]
58+
pub counter_account: &'info mut UncheckedAccount,
59+
pub system_program: &'info Program<System>,
60+
}
61+
62+
impl InitializeExtraAccountMetaList<'_> {
63+
#[inline(always)]
64+
pub fn initialize_extra_account_meta_list(&self) -> Result<(), ProgramError> {
65+
// ExtraAccountMetaList with 1 extra account.
66+
// ExtraAccountMeta for a PDA with seeds [Literal("counter"), AccountData(0, 32, 32)]:
67+
// The AccountData seed resolves the owner pubkey from account_index=0
68+
// (source_token) at data_index=32 (owner field offset), length=32.
69+
//
70+
// TLV layout:
71+
// [8 bytes: Execute discriminator]
72+
// [4 bytes: data length]
73+
// [4 bytes: PodSlice count = 1]
74+
// [35 bytes: ExtraAccountMeta entry]
75+
// Total = 51 bytes
76+
let meta_list_size: u64 = 51;
77+
let lamports = Rent::get()?.try_minimum_balance(meta_list_size as usize)?;
78+
79+
let mint_address = self.mint.to_account_view().address();
80+
let (expected_pda, bump) = Address::find_program_address(
81+
&[b"extra-account-metas", mint_address.as_ref()],
82+
&crate::ID,
83+
);
84+
85+
if self.extra_account_meta_list.to_account_view().address() != &expected_pda {
86+
return Err(ProgramError::InvalidSeeds);
87+
}
88+
89+
let bump_bytes = [bump];
90+
let seeds = [
91+
Seed::from(b"extra-account-metas" as &[u8]),
92+
Seed::from(mint_address.as_ref()),
93+
Seed::from(&bump_bytes as &[u8]),
94+
];
95+
96+
self.system_program
97+
.create_account(
98+
self.payer,
99+
&*self.extra_account_meta_list,
100+
lamports,
101+
meta_list_size,
102+
&crate::ID,
103+
)
104+
.invoke_signed(&seeds)?;
105+
106+
// Write TLV data
107+
let view = unsafe {
108+
&mut *(self.extra_account_meta_list as *const UncheckedAccount as *mut UncheckedAccount
109+
as *mut AccountView)
110+
};
111+
let mut data = view.try_borrow_mut()?;
112+
113+
data[0..8].copy_from_slice(&EXECUTE_DISCRIMINATOR);
114+
data[8..12].copy_from_slice(&39u32.to_le_bytes()); // data length: 4 + 35
115+
data[12..16].copy_from_slice(&1u32.to_le_bytes()); // count = 1
116+
117+
// ExtraAccountMeta for counter PDA seeded by ["counter", AccountData(0, 32, 32)]
118+
data[16] = 1; // discriminator: PDA from seeds
119+
let mut config = [0u8; 32];
120+
config[0] = 2; // number of seeds
121+
// Seed 0: Literal "counter"
122+
config[1] = 0; // seed type: literal
123+
config[2] = 7; // seed length
124+
config[3..10].copy_from_slice(b"counter");
125+
// Seed 1: AccountData(account_index=0, data_index=32, length=32)
126+
config[10] = 1; // seed type: account data
127+
config[11] = 0; // account_index
128+
config[12] = 32; // data_index
129+
config[13] = 32; // length
130+
data[17..49].copy_from_slice(&config);
131+
data[49] = 0; // is_signer = false
132+
data[50] = 1; // is_writable = true
133+
134+
// Create the counter PDA (seeded by payer key for this init)
135+
let payer_address = self.payer.to_account_view().address();
136+
let counter_size: u64 = 16;
137+
let counter_lamports = Rent::get()?.try_minimum_balance(counter_size as usize)?;
138+
139+
let (counter_pda, counter_bump) =
140+
Address::find_program_address(&[b"counter", payer_address.as_ref()], &crate::ID);
141+
142+
if self.counter_account.to_account_view().address() != &counter_pda {
143+
return Err(ProgramError::InvalidSeeds);
144+
}
145+
146+
let counter_bump_bytes = [counter_bump];
147+
let counter_seeds = [
148+
Seed::from(b"counter" as &[u8]),
149+
Seed::from(payer_address.as_ref()),
150+
Seed::from(&counter_bump_bytes as &[u8]),
151+
];
152+
153+
self.system_program
154+
.create_account(
155+
self.payer,
156+
&*self.counter_account,
157+
counter_lamports,
158+
counter_size,
159+
&crate::ID,
160+
)
161+
.invoke_signed(&counter_seeds)?;
162+
163+
log("Extra account meta list and counter initialized");
164+
Ok(())
165+
}
166+
}
167+
168+
// ---------------------------------------------------------------------------
169+
// TransferHook
170+
// ---------------------------------------------------------------------------
171+
172+
#[derive(Accounts)]
173+
pub struct TransferHook<'info> {
174+
pub source_token: &'info UncheckedAccount,
175+
pub mint: &'info UncheckedAccount,
176+
pub destination_token: &'info UncheckedAccount,
177+
pub owner: &'info UncheckedAccount,
178+
pub extra_account_meta_list: &'info UncheckedAccount,
179+
/// Counter PDA resolved by Token-2022 using account data seeds
180+
#[account(mut)]
181+
pub counter_account: &'info mut UncheckedAccount,
182+
}
183+
184+
impl TransferHook<'_> {
185+
#[inline(always)]
186+
pub fn transfer_hook(&self) -> Result<(), ProgramError> {
187+
let view = unsafe {
188+
&mut *(self.counter_account as *const UncheckedAccount as *mut UncheckedAccount
189+
as *mut AccountView)
190+
};
191+
let mut data = view.try_borrow_mut()?;
192+
193+
if data.len() < 16 {
194+
return Err(ProgramError::AccountDataTooSmall);
195+
}
196+
197+
let mut counter_bytes = [0u8; 8];
198+
counter_bytes.copy_from_slice(&data[8..16]);
199+
let counter = u64::from_le_bytes(counter_bytes);
200+
201+
let new_counter = counter
202+
.checked_add(1)
203+
.ok_or(ProgramError::ArithmeticOverflow)?;
204+
205+
data[8..16].copy_from_slice(&new_counter.to_le_bytes());
206+
207+
log("Transfer hook: per-owner counter incremented");
208+
Ok(())
209+
}
210+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
extern crate std;
2+
use {
3+
alloc::vec,
4+
quasar_svm::{Account, Instruction, Pubkey, QuasarSvm},
5+
std::println,
6+
};
7+
8+
fn setup() -> QuasarSvm {
9+
let elf = std::fs::read("target/deploy/quasar_transfer_hook_account_data_as_seed.so").unwrap();
10+
QuasarSvm::new().with_program(&crate::ID, &elf)
11+
}
12+
13+
fn signer(address: Pubkey) -> Account {
14+
quasar_svm::token::create_keyed_system_account(&address, 10_000_000_000)
15+
}
16+
17+
fn empty(address: Pubkey) -> Account {
18+
Account {
19+
address,
20+
lamports: 0,
21+
data: vec![],
22+
owner: quasar_svm::system_program::ID,
23+
executable: false,
24+
}
25+
}
26+
27+
#[test]
28+
fn test_initialize_and_transfer_hook() {
29+
let mut svm = setup();
30+
31+
let payer = Pubkey::new_unique();
32+
let mint = Pubkey::new_unique();
33+
let system_program = quasar_svm::system_program::ID;
34+
35+
let (meta_list_pda, _) = Pubkey::find_program_address(
36+
&[b"extra-account-metas", mint.as_ref()],
37+
&crate::ID.into(),
38+
);
39+
40+
let (counter_pda, _) = Pubkey::find_program_address(
41+
&[b"counter", payer.as_ref()],
42+
&crate::ID.into(),
43+
);
44+
45+
// Initialize
46+
let init_data = vec![43, 34, 13, 49, 167, 88, 235, 235];
47+
let init_ix = Instruction {
48+
program_id: crate::ID,
49+
accounts: vec![
50+
solana_instruction::AccountMeta::new(payer.into(), true),
51+
solana_instruction::AccountMeta::new(meta_list_pda.into(), false),
52+
solana_instruction::AccountMeta::new_readonly(mint.into(), false),
53+
solana_instruction::AccountMeta::new(counter_pda.into(), false),
54+
solana_instruction::AccountMeta::new_readonly(system_program.into(), false),
55+
],
56+
data: init_data,
57+
};
58+
59+
let result = svm.process_instruction(
60+
&init_ix,
61+
&[signer(payer), empty(meta_list_pda), empty(mint), empty(counter_pda)],
62+
);
63+
result.print_logs();
64+
assert!(result.is_ok(), "init failed: {:?}", result.raw_result);
65+
println!(" INIT CU: {}", result.compute_units_consumed);
66+
67+
// Transfer hook
68+
let source_token = Pubkey::new_unique();
69+
let destination_token = Pubkey::new_unique();
70+
let owner = Pubkey::new_unique();
71+
72+
let mut hook_data = vec![105, 37, 101, 197, 75, 251, 102, 26];
73+
hook_data.extend_from_slice(&1u64.to_le_bytes());
74+
75+
let hook_ix = Instruction {
76+
program_id: crate::ID,
77+
accounts: vec![
78+
solana_instruction::AccountMeta::new_readonly(source_token.into(), false),
79+
solana_instruction::AccountMeta::new_readonly(mint.into(), false),
80+
solana_instruction::AccountMeta::new_readonly(destination_token.into(), false),
81+
solana_instruction::AccountMeta::new_readonly(owner.into(), false),
82+
solana_instruction::AccountMeta::new_readonly(meta_list_pda.into(), false),
83+
solana_instruction::AccountMeta::new(counter_pda.into(), false),
84+
],
85+
data: hook_data,
86+
};
87+
88+
let result = svm.process_instruction(
89+
&hook_ix,
90+
&[empty(source_token), empty(destination_token), signer(owner)],
91+
);
92+
result.print_logs();
93+
assert!(result.is_ok(), "transfer_hook failed: {:?}", result.raw_result);
94+
println!(" TRANSFER_HOOK CU: {}", result.compute_units_consumed);
95+
96+
let counter_account = svm.get_account(&counter_pda).expect("counter missing");
97+
let counter = u64::from_le_bytes(counter_account.data[8..16].try_into().unwrap());
98+
assert_eq!(counter, 1, "counter should be 1");
99+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[package]
2+
name = "quasar-transfer-hook-counter"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[workspace]
7+
8+
[lints.rust.unexpected_cfgs]
9+
level = "warn"
10+
check-cfg = [
11+
'cfg(target_os, values("solana"))',
12+
]
13+
14+
[lib]
15+
crate-type = ["cdylib", "lib"]
16+
17+
[features]
18+
alloc = []
19+
client = []
20+
debug = []
21+
22+
[dependencies]
23+
quasar-lang = "0.0"
24+
quasar-spl = "0.0"
25+
solana-instruction = { version = "3.2.0" }
26+
27+
[dev-dependencies]
28+
quasar-svm = { version = "0.1" }

0 commit comments

Comments
 (0)