Skip to content

Commit e0fb78f

Browse files
feat: add Quasar variants for compression and oracle examples
Compression examples (cnft-burn, cnft-vault, cutils) ported using raw invoke()/invoke_signed() for Bubblegum and SPL Account Compression CPI, matching the approach used in the Anchor versions. Oracle example (pyth) ported with manual PriceUpdateV2 account data parsing in Quasar's no_std environment.
1 parent 98f12fa commit e0fb78f

File tree

26 files changed

+1252
-0
lines changed

26 files changed

+1252
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "quasar-cnft-burn"
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+
# Direct dependency for invoke_with_bounds — needed for raw CPI with variable
27+
# proof accounts. quasar-lang re-exports types but not the invoke functions.
28+
solana-instruction-view = { version = "2", features = ["cpi"] }
29+
solana-instruction = { version = "3.2.0" }
30+
31+
[dev-dependencies]
32+
quasar-svm = { version = "0.1" }
33+
solana-address = { version = "2.2.0", features = ["decode"] }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[project]
2+
name = "quasar_cnft_burn"
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: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
use crate::*;
2+
use quasar_lang::cpi::{InstructionAccount, InstructionView};
3+
4+
/// Maximum number of proof nodes for the merkle tree.
5+
/// Concurrent merkle trees support up to depth 30, but typical depth is 14-20.
6+
const MAX_PROOF_NODES: usize = 24;
7+
8+
/// Total max accounts for the CPI: 7 fixed + proof nodes.
9+
const MAX_CPI_ACCOUNTS: usize = 7 + MAX_PROOF_NODES;
10+
11+
/// Accounts for burning a compressed NFT via mpl-bubblegum CPI.
12+
#[derive(Accounts)]
13+
pub struct BurnCnft<'info> {
14+
#[account(mut)]
15+
pub leaf_owner: &'info Signer,
16+
/// Tree authority PDA (seeds checked by Bubblegum).
17+
#[account(mut)]
18+
pub tree_authority: &'info UncheckedAccount,
19+
/// Merkle tree account modified by the compression program.
20+
#[account(mut)]
21+
pub merkle_tree: &'info UncheckedAccount,
22+
/// SPL Noop log wrapper.
23+
pub log_wrapper: &'info UncheckedAccount,
24+
/// SPL Account Compression program.
25+
#[account(address = SPL_ACCOUNT_COMPRESSION_ID)]
26+
pub compression_program: &'info UncheckedAccount,
27+
/// mpl-bubblegum program.
28+
#[account(address = MPL_BUBBLEGUM_ID)]
29+
pub bubblegum_program: &'info UncheckedAccount,
30+
pub system_program: &'info Program<System>,
31+
}
32+
33+
impl<'info> BurnCnft<'info> {
34+
pub fn burn_cnft(
35+
&self,
36+
ctx: &CtxWithRemaining<'info, BurnCnft<'info>>,
37+
) -> Result<(), ProgramError> {
38+
// Parse instruction args from raw data:
39+
// root(32) + data_hash(32) + creator_hash(32) + nonce(8) + index(4) = 108 bytes
40+
let data = ctx.data;
41+
if data.len() < 108 {
42+
return Err(ProgramError::InvalidInstructionData);
43+
}
44+
45+
// Build instruction data: discriminator + args
46+
// 8 + 32 + 32 + 32 + 8 + 4 = 116 bytes
47+
let mut ix_data = [0u8; 116];
48+
ix_data[0..8].copy_from_slice(&BURN_DISCRIMINATOR);
49+
ix_data[8..116].copy_from_slice(&data[0..108]);
50+
51+
// Collect remaining accounts (proof nodes) into a stack buffer
52+
let remaining = ctx.remaining_accounts();
53+
let placeholder = self.system_program.to_account_view().clone();
54+
let mut proof_views: [AccountView; MAX_PROOF_NODES] =
55+
core::array::from_fn(|_| placeholder.clone());
56+
let mut proof_count = 0usize;
57+
for result in remaining.iter() {
58+
if proof_count >= MAX_PROOF_NODES {
59+
break;
60+
}
61+
proof_views[proof_count] = result?;
62+
proof_count += 1;
63+
}
64+
65+
let total_accounts = 7 + proof_count;
66+
67+
// Build instruction account metas.
68+
// Layout matches mpl-bubblegum Burn: tree_authority, leaf_owner (signer),
69+
// leaf_delegate (= leaf_owner, not signer), merkle_tree, log_wrapper,
70+
// compression_program, system_program, then proof nodes.
71+
let sys_addr = self.system_program.address();
72+
let mut ix_accounts: [InstructionAccount; MAX_CPI_ACCOUNTS] = core::array::from_fn(|_| {
73+
InstructionAccount::readonly(sys_addr)
74+
});
75+
76+
ix_accounts[0] = InstructionAccount::readonly(self.tree_authority.address());
77+
ix_accounts[1] = InstructionAccount::readonly_signer(self.leaf_owner.address());
78+
// leaf_delegate = leaf_owner, not a signer in this call
79+
ix_accounts[2] = InstructionAccount::readonly(self.leaf_owner.address());
80+
ix_accounts[3] = InstructionAccount::writable(self.merkle_tree.address());
81+
ix_accounts[4] = InstructionAccount::readonly(self.log_wrapper.address());
82+
ix_accounts[5] = InstructionAccount::readonly(self.compression_program.address());
83+
ix_accounts[6] = InstructionAccount::readonly(self.system_program.address());
84+
85+
for i in 0..proof_count {
86+
ix_accounts[7 + i] = InstructionAccount::readonly(proof_views[i].address());
87+
}
88+
89+
// Build account views array for the CPI
90+
let sys_view = self.system_program.to_account_view().clone();
91+
let mut views: [AccountView; MAX_CPI_ACCOUNTS] = core::array::from_fn(|_| sys_view.clone());
92+
93+
views[0] = self.tree_authority.to_account_view().clone();
94+
views[1] = self.leaf_owner.to_account_view().clone();
95+
views[2] = self.leaf_owner.to_account_view().clone(); // leaf_delegate = leaf_owner
96+
views[3] = self.merkle_tree.to_account_view().clone();
97+
views[4] = self.log_wrapper.to_account_view().clone();
98+
views[5] = self.compression_program.to_account_view().clone();
99+
views[6] = self.system_program.to_account_view().clone();
100+
101+
for i in 0..proof_count {
102+
views[7 + i] = proof_views[i].clone();
103+
}
104+
105+
let instruction = InstructionView {
106+
program_id: &MPL_BUBBLEGUM_ID,
107+
data: &ix_data,
108+
accounts: &ix_accounts[..total_accounts],
109+
};
110+
111+
solana_instruction_view::cpi::invoke_with_bounds::<MAX_CPI_ACCOUNTS, AccountView>(
112+
&instruction,
113+
&views[..total_accounts],
114+
)
115+
}
116+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod burn_cnft;
2+
pub use burn_cnft::*;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#![cfg_attr(not(test), no_std)]
2+
3+
use quasar_lang::prelude::*;
4+
5+
mod instructions;
6+
use instructions::*;
7+
#[cfg(test)]
8+
mod tests;
9+
10+
/// Bubblegum Burn instruction discriminator.
11+
const BURN_DISCRIMINATOR: [u8; 8] = [116, 110, 29, 56, 107, 219, 42, 93];
12+
13+
/// mpl-bubblegum program ID (BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY).
14+
const MPL_BUBBLEGUM_ID: Address = Address::new_from_array([
15+
0x98, 0x8b, 0x80, 0xeb, 0x79, 0x35, 0x28, 0x69, 0xb2, 0x24, 0x74, 0x5f, 0x59, 0xdd, 0xbf,
16+
0x8a, 0x26, 0x58, 0xca, 0x13, 0xdc, 0x68, 0x81, 0x21, 0x26, 0x35, 0x1c, 0xae, 0x07, 0xc1,
17+
0xa5, 0xa5,
18+
]);
19+
20+
/// SPL Account Compression program ID (cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK).
21+
const SPL_ACCOUNT_COMPRESSION_ID: Address = Address::new_from_array([
22+
0x09, 0x2a, 0x13, 0xee, 0x95, 0xc4, 0x1c, 0xba, 0x08, 0xa6, 0x7f, 0x5a, 0xc6, 0x7e, 0x8d,
23+
0xf7, 0xe1, 0xda, 0x11, 0x62, 0x5e, 0x1d, 0x64, 0x13, 0x7f, 0x8f, 0x4f, 0x23, 0x83, 0x03,
24+
0x7f, 0x14,
25+
]);
26+
27+
declare_id!("C6qxH8n6mZxrrbtMtYWYSp8JR8vkQ55X1o4EBg7twnMv");
28+
29+
#[program]
30+
mod quasar_cnft_burn {
31+
use super::*;
32+
33+
#[instruction(discriminator = 0)]
34+
pub fn burn_cnft(ctx: CtxWithRemaining<BurnCnft>) -> Result<(), ProgramError> {
35+
ctx.accounts.burn_cnft(&ctx)
36+
}
37+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Compressed NFT operations require external programs (Bubblegum, SPL Account
2+
// Compression) that are not available in the quasar-svm test harness. The build
3+
// itself verifies the CPI instruction construction compiles correctly.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[package]
2+
name = "quasar-cnft-vault"
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+
# Direct dependency for invoke_signed_with_bounds — needed for raw CPI with
27+
# variable proof accounts. quasar-lang re-exports types but not the invoke fns.
28+
solana-instruction-view = { version = "2", features = ["cpi"] }
29+
solana-instruction = { version = "3.2.0" }
30+
31+
[dev-dependencies]
32+
quasar-svm = { version = "0.1" }
33+
solana-address = { version = "2.2.0", features = ["decode"] }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[project]
2+
name = "quasar_cnft_vault"
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pub mod withdraw;
2+
pub use withdraw::*;
3+
4+
pub mod withdraw_two;
5+
pub use withdraw_two::*;

0 commit comments

Comments
 (0)