Skip to content

Commit 091f608

Browse files
tokens: port all 5 previously-skipped examples to Quasar
Port the token examples that were skipped by the previous subagent: 1. spl-token-minter — Token creation with Metaplex metadata CPI via quasar-spl's MetadataCpi trait, plus SPL Token mint_to CPI. 2. nft-minter — Single-instruction NFT minting: mint_to + create_metadata + create_master_edition, all via quasar-spl's metadata support. 3. nft-operations — Collection workflow: create_collection, mint_nft, verify_collection. Uses PDA authority with invoke_signed for all Metaplex CPIs. Uses verify_sized_collection_item. 4. token-swap — Full constant-product AMM with 5 instructions: create_amm, create_pool, deposit_liquidity, withdraw_liquidity, swap. Pure integer math (no fixed-point crate needed). Integer sqrt via Newton's method. 5. external-delegate-token-master — Ethereum-signed token transfers via raw sol_secp256k1_recover syscall + solana-keccak-hasher. Avoids solana-secp256k1-recover crate (pulls std via thiserror). Key patterns: - quasar-spl metadata feature provides MetadataCpi, MetadataProgram - Raw syscall for secp256k1 avoids std conflict - PodU16/PodU64 types generated by #[account] macro need .get()/.into() - Quasar seeds resolve field names to addresses, not account data fields All examples build (cargo build-sbf) and tests pass (cargo test). Metaplex CPI tests are limited since quasar-svm lacks the Metaplex program.
1 parent e134cf3 commit 091f608

29 files changed

Lines changed: 1924 additions & 0 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[package]
2+
name = "quasar-external-delegate-token-master"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# Standalone workspace — not part of the root program-examples workspace.
7+
# Quasar uses a different resolver and dependency tree.
8+
[workspace]
9+
10+
[lints.rust.unexpected_cfgs]
11+
level = "warn"
12+
check-cfg = [
13+
'cfg(target_os, values("solana"))',
14+
]
15+
16+
[lib]
17+
crate-type = ["cdylib", "lib"]
18+
19+
[features]
20+
alloc = []
21+
client = []
22+
debug = []
23+
24+
[dependencies]
25+
quasar-lang = "0.0"
26+
quasar-spl = "0.0"
27+
solana-instruction = { version = "3.2.0" }
28+
solana-define-syscall = "4.0"
29+
solana-keccak-hasher = "3.1"
30+
31+
[dev-dependencies]
32+
quasar-svm = { version = "0.1" }
33+
spl-token-interface = { version = "2.0.0" }
34+
solana-program-pack = { version = "3.1.0" }
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#![cfg_attr(not(test), no_std)]
2+
3+
use quasar_lang::prelude::*;
4+
use quasar_spl::{Token, TokenCpi};
5+
6+
#[cfg(test)]
7+
mod tests;
8+
9+
declare_id!("22222222222222222222222222222222222222222222");
10+
11+
/// User account storing the Solana authority and linked Ethereum address.
12+
#[account(discriminator = 1)]
13+
pub struct UserAccount {
14+
pub authority: Address,
15+
pub ethereum_address: [u8; 20],
16+
}
17+
18+
/// External delegate token master: allows transfers authorised either by
19+
/// the Solana authority or by an Ethereum signature (secp256k1).
20+
#[program]
21+
mod quasar_external_delegate_token_master {
22+
use super::*;
23+
24+
/// Initialize a user account with zero Ethereum address.
25+
#[instruction(discriminator = 0)]
26+
pub fn initialize(ctx: Ctx<Initialize>) -> Result<(), ProgramError> {
27+
ctx.accounts.initialize()
28+
}
29+
30+
/// Set the Ethereum address for signature verification.
31+
#[instruction(discriminator = 1)]
32+
pub fn set_ethereum_address(
33+
ctx: Ctx<SetEthereumAddress>,
34+
ethereum_address: [u8; 20],
35+
) -> Result<(), ProgramError> {
36+
ctx.accounts.set_ethereum_address(ethereum_address)
37+
}
38+
39+
/// Transfer tokens using an Ethereum signature for authorisation.
40+
#[instruction(discriminator = 2)]
41+
pub fn transfer_tokens(
42+
ctx: Ctx<TransferTokens>,
43+
amount: u64,
44+
signature: [u8; 65],
45+
message: [u8; 32],
46+
) -> Result<(), ProgramError> {
47+
ctx.accounts
48+
.transfer_tokens(amount, &signature, &message, &ctx.bumps)
49+
}
50+
51+
/// Transfer tokens using the Solana authority directly.
52+
#[instruction(discriminator = 3)]
53+
pub fn authority_transfer(
54+
ctx: Ctx<AuthorityTransfer>,
55+
amount: u64,
56+
) -> Result<(), ProgramError> {
57+
ctx.accounts.authority_transfer(amount, &ctx.bumps)
58+
}
59+
}
60+
61+
// ---------------------------------------------------------------------------
62+
// Instruction accounts
63+
// ---------------------------------------------------------------------------
64+
65+
#[derive(Accounts)]
66+
pub struct Initialize<'info> {
67+
#[account(mut, init, payer = authority)]
68+
pub user_account: &'info mut Account<UserAccount>,
69+
#[account(mut)]
70+
pub authority: &'info Signer,
71+
pub system_program: &'info Program<System>,
72+
}
73+
74+
impl Initialize<'_> {
75+
#[inline(always)]
76+
pub fn initialize(&mut self) -> Result<(), ProgramError> {
77+
self.user_account
78+
.set_inner(*self.authority.address(), [0u8; 20]);
79+
Ok(())
80+
}
81+
}
82+
83+
#[derive(Accounts)]
84+
pub struct SetEthereumAddress<'info> {
85+
#[account(mut)]
86+
pub user_account: &'info mut Account<UserAccount>,
87+
pub authority: &'info Signer,
88+
}
89+
90+
impl SetEthereumAddress<'_> {
91+
#[inline(always)]
92+
pub fn set_ethereum_address(
93+
&mut self,
94+
ethereum_address: [u8; 20],
95+
) -> Result<(), ProgramError> {
96+
require_keys_eq!(
97+
self.user_account.authority,
98+
*self.authority.address(),
99+
ProgramError::MissingRequiredSignature
100+
);
101+
self.user_account.ethereum_address = ethereum_address;
102+
Ok(())
103+
}
104+
}
105+
106+
#[derive(Accounts)]
107+
pub struct TransferTokens<'info> {
108+
pub user_account: &'info Account<UserAccount>,
109+
pub authority: &'info Signer,
110+
#[account(mut)]
111+
pub user_token_account: &'info mut Account<Token>,
112+
#[account(mut)]
113+
pub recipient_token_account: &'info mut Account<Token>,
114+
/// PDA derived from user_account address.
115+
#[account(seeds = [user_account], bump)]
116+
pub user_pda: &'info UncheckedAccount,
117+
pub token_program: &'info Program<Token>,
118+
}
119+
120+
impl TransferTokens<'_> {
121+
#[inline(always)]
122+
pub fn transfer_tokens(
123+
&self,
124+
amount: u64,
125+
signature: &[u8; 65],
126+
message: &[u8; 32],
127+
bumps: &TransferTokensBumps,
128+
) -> Result<(), ProgramError> {
129+
if !verify_ethereum_signature(
130+
&self.user_account.ethereum_address,
131+
message,
132+
signature,
133+
) {
134+
return Err(ProgramError::Custom(1)); // InvalidSignature
135+
}
136+
137+
let bump = [bumps.user_pda];
138+
let seeds: &[Seed] = &[
139+
Seed::from(self.user_account.address().as_ref()),
140+
Seed::from(&bump as &[u8]),
141+
];
142+
143+
self.token_program
144+
.transfer(
145+
self.user_token_account,
146+
self.recipient_token_account,
147+
self.user_pda,
148+
amount,
149+
)
150+
.invoke_signed(seeds)
151+
}
152+
}
153+
154+
#[derive(Accounts)]
155+
pub struct AuthorityTransfer<'info> {
156+
pub user_account: &'info Account<UserAccount>,
157+
pub authority: &'info Signer,
158+
#[account(mut)]
159+
pub user_token_account: &'info mut Account<Token>,
160+
#[account(mut)]
161+
pub recipient_token_account: &'info mut Account<Token>,
162+
/// PDA derived from user_account address.
163+
#[account(seeds = [user_account], bump)]
164+
pub user_pda: &'info UncheckedAccount,
165+
pub token_program: &'info Program<Token>,
166+
}
167+
168+
impl AuthorityTransfer<'_> {
169+
#[inline(always)]
170+
pub fn authority_transfer(
171+
&self,
172+
amount: u64,
173+
bumps: &AuthorityTransferBumps,
174+
) -> Result<(), ProgramError> {
175+
require_keys_eq!(
176+
self.user_account.authority,
177+
*self.authority.address(),
178+
ProgramError::MissingRequiredSignature
179+
);
180+
181+
let bump = [bumps.user_pda];
182+
let seeds: &[Seed] = &[
183+
Seed::from(self.user_account.address().as_ref()),
184+
Seed::from(&bump as &[u8]),
185+
];
186+
187+
self.token_program
188+
.transfer(
189+
self.user_token_account,
190+
self.recipient_token_account,
191+
self.user_pda,
192+
amount,
193+
)
194+
.invoke_signed(seeds)
195+
}
196+
}
197+
198+
// ---------------------------------------------------------------------------
199+
// Ethereum signature verification using raw syscalls
200+
// ---------------------------------------------------------------------------
201+
202+
fn keccak256(data: &[u8]) -> [u8; 32] {
203+
let hash = solana_keccak_hasher::hash(data);
204+
let bytes: &[u8] = hash.as_ref();
205+
let mut result = [0u8; 32];
206+
result.copy_from_slice(bytes);
207+
result
208+
}
209+
210+
/// Recover secp256k1 public key from a signature, using the raw Solana syscall.
211+
///
212+
/// Returns `None` if recovery fails. The returned key is the 65-byte
213+
/// uncompressed public key (first byte `0x04` is omitted by the syscall,
214+
/// only the 64 bytes of x||y are returned).
215+
fn secp256k1_recover(
216+
message_hash: &[u8; 32],
217+
recovery_id: u8,
218+
signature: &[u8; 64],
219+
) -> Option<[u8; 64]> {
220+
#[cfg(target_os = "solana")]
221+
{
222+
let mut pubkey_result = [0u8; 64];
223+
let rc = unsafe {
224+
solana_define_syscall::definitions::sol_secp256k1_recover(
225+
message_hash.as_ptr(),
226+
recovery_id as u64,
227+
signature.as_ptr(),
228+
pubkey_result.as_mut_ptr(),
229+
)
230+
};
231+
if rc == 0 {
232+
Some(pubkey_result)
233+
} else {
234+
None
235+
}
236+
}
237+
#[cfg(not(target_os = "solana"))]
238+
{
239+
// Off-chain: not implemented (would need a secp256k1 library).
240+
let _ = (message_hash, recovery_id, signature);
241+
None
242+
}
243+
}
244+
245+
fn verify_ethereum_signature(
246+
ethereum_address: &[u8; 20],
247+
message: &[u8; 32],
248+
signature: &[u8; 65],
249+
) -> bool {
250+
let recovery_id = signature[64];
251+
let mut sig = [0u8; 64];
252+
sig.copy_from_slice(&signature[..64]);
253+
254+
if let Some(pubkey_bytes) = secp256k1_recover(message, recovery_id, &sig) {
255+
// Ethereum address = last 20 bytes of keccak256(public_key)
256+
// The syscall returns the 64-byte uncompressed key (sans prefix byte).
257+
let hash = keccak256(&pubkey_bytes);
258+
let mut recovered_address = [0u8; 20];
259+
recovered_address.copy_from_slice(&hash[12..]);
260+
recovered_address == *ethereum_address
261+
} else {
262+
false
263+
}
264+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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_external_delegate_token_master.so").unwrap();
10+
QuasarSvm::new()
11+
.with_program(&crate::ID, &elf)
12+
.with_token_program()
13+
}
14+
15+
fn signer(address: Pubkey) -> Account {
16+
quasar_svm::token::create_keyed_system_account(&address, 5_000_000_000)
17+
}
18+
19+
fn empty(address: Pubkey) -> Account {
20+
Account {
21+
address,
22+
lamports: 0,
23+
data: vec![],
24+
owner: quasar_svm::system_program::ID,
25+
executable: false,
26+
}
27+
}
28+
29+
/// Build initialize instruction data.
30+
/// Wire format: [disc=0]
31+
fn build_initialize_data() -> Vec<u8> {
32+
vec![0u8]
33+
}
34+
35+
#[test]
36+
fn test_initialize() {
37+
let mut svm = setup();
38+
39+
let authority = Pubkey::new_unique();
40+
let user_account = Pubkey::new_unique();
41+
let system_program = quasar_svm::system_program::ID;
42+
43+
let data = build_initialize_data();
44+
45+
let instruction = Instruction {
46+
program_id: crate::ID,
47+
accounts: vec![
48+
solana_instruction::AccountMeta::new(user_account.into(), true),
49+
solana_instruction::AccountMeta::new(authority.into(), true),
50+
solana_instruction::AccountMeta::new_readonly(system_program.into(), false),
51+
],
52+
data,
53+
};
54+
55+
let result = svm.process_instruction(
56+
&instruction,
57+
&[empty(user_account), signer(authority)],
58+
);
59+
60+
assert!(
61+
result.is_ok(),
62+
"initialize failed: {:?}",
63+
result.raw_result
64+
);
65+
println!(" INITIALIZE CU: {}", result.compute_units_consumed);
66+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "quasar-nft-minter"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# Standalone workspace — not part of the root program-examples workspace.
7+
# Quasar uses a different resolver and dependency tree.
8+
[workspace]
9+
10+
[lints.rust.unexpected_cfgs]
11+
level = "warn"
12+
check-cfg = [
13+
'cfg(target_os, values("solana"))',
14+
]
15+
16+
[lib]
17+
crate-type = ["cdylib", "lib"]
18+
19+
[features]
20+
alloc = []
21+
client = []
22+
debug = []
23+
24+
[dependencies]
25+
quasar-lang = "0.0"
26+
quasar-spl = { version = "0.0", features = ["metadata"] }
27+
solana-instruction = { version = "3.2.0" }
28+
29+
[dev-dependencies]
30+
quasar-svm = { version = "0.1" }
31+
spl-token-interface = { version = "2.0.0" }
32+
solana-program-pack = { version = "3.1.0" }

0 commit comments

Comments
 (0)