Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compression/cnft-burn/anchor/programs/cnft-burn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ borsh = { version = "1", features = ["derive"] }

[dev-dependencies]
litesvm = "0.13.1"
solana-kite = "0.4.0"
solana-instruction = "3.0.0"
solana-keypair = "3.0.1"
solana-pubkey = "3.0.0"
Expand Down
49 changes: 20 additions & 29 deletions compression/cnft-burn/anchor/programs/cnft-burn/tests/test_burn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ use {
solana_instruction::{account_meta::AccountMeta, Instruction},
solana_keccak_hasher::hashv,
solana_keypair::Keypair,
solana_message::Message,
solana_kite::{create_wallet, send_transaction_from_instructions},
solana_pubkey::{pubkey, Pubkey},
solana_signer::Signer,
solana_transaction::Transaction,
};

// ---- Program IDs ----------------------------------------------------------
Expand Down Expand Up @@ -239,19 +238,6 @@ fn read_current_root(data: &[u8]) -> [u8; 32] {

// ---- Helpers ---------------------------------------------------------------

fn send(
svm: &mut LiteSVM,
ixs: Vec<Instruction>,
payer: &Keypair,
signers: &[&Keypair],
) -> Result<(), Box<litesvm::types::FailedTransactionMetadata>> {
let msg = Message::new(&ixs, Some(&payer.pubkey()));
let blockhash = svm.latest_blockhash();
let mut tx = Transaction::new_unsigned(msg);
tx.sign(signers, blockhash);
svm.send_transaction(tx).map(|_| ()).map_err(Box::new)
}

#[test]
fn test_burn_cnft() {
let mut svm = LiteSVM::new();
Expand Down Expand Up @@ -279,15 +265,8 @@ fn test_burn_cnft() {
.unwrap();

// Fund payer and leaf_owner.
let payer = Keypair::new();
let leaf_owner = Keypair::new();
svm.airdrop(&payer.pubkey(), 100 * solana_native_token::LAMPORTS_PER_SOL)
.unwrap();
svm.airdrop(
&leaf_owner.pubkey(),
10 * solana_native_token::LAMPORTS_PER_SOL,
)
.unwrap();
let payer = create_wallet(&mut svm, 100 * solana_native_token::LAMPORTS_PER_SOL).unwrap();
let leaf_owner = create_wallet(&mut svm, 10 * solana_native_token::LAMPORTS_PER_SOL).unwrap();

// Create the Merkle tree account, owned by the compression program.
let merkle_tree = Keypair::new();
Expand Down Expand Up @@ -334,11 +313,11 @@ fn test_burn_cnft() {
},
};

send(
send_transaction_from_instructions(
&mut svm,
vec![create_acc, create_tree_ix],
&payer,
&[&payer, &merkle_tree],
&payer.pubkey(),
)
.expect("create_tree_config should succeed");

Expand Down Expand Up @@ -383,7 +362,8 @@ fn test_burn_cnft() {
d
},
};
send(&mut svm, vec![mint_ix], &payer, &[&payer]).expect("mint_v1 should succeed");
send_transaction_from_instructions(&mut svm, vec![mint_ix], &[&payer], &payer.pubkey())
.expect("mint_v1 should succeed");

// Recompute data_hash and creator_hash exactly as Bubblegum does.
let data_hash = hash_metadata(&metadata);
Expand Down Expand Up @@ -432,7 +412,13 @@ fn test_burn_cnft() {
data: burn_data.clone(),
};

send(&mut svm, vec![burn_ix], &leaf_owner, &[&leaf_owner]).expect("burn_cnft should succeed");
send_transaction_from_instructions(
&mut svm,
vec![burn_ix],
&[&leaf_owner],
&leaf_owner.pubkey(),
)
.expect("burn_cnft should succeed");

// After burning, leaf 0 is zeroed. The root the test cached is now stale,
// so a second burn with the same (root, hashes) must fail.
Expand All @@ -441,7 +427,12 @@ fn test_burn_cnft() {
accounts: burn_accounts,
data: burn_data,
};
let second = send(&mut svm, vec![burn_ix2], &leaf_owner, &[&leaf_owner]);
let second = send_transaction_from_instructions(
&mut svm,
vec![burn_ix2],
&[&leaf_owner],
&leaf_owner.pubkey(),
);
assert!(
second.is_err(),
"second burn must fail: the leaf was already burned"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"

[dev-dependencies]
litesvm = "0.13.1"
solana-kite = "0.4.0"
solana-instruction = "3.0.0"
solana-keypair = "3.0.1"
solana-pubkey = "3.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ use {
solana_instruction::{account_meta::AccountMeta, Instruction},
solana_keccak_hasher::hashv,
solana_keypair::Keypair,
solana_message::Message,
solana_kite::{create_wallet, send_transaction_from_instructions, SolanaKiteError},
solana_pubkey::{pubkey, Pubkey},
solana_signer::Signer,
solana_transaction::Transaction,
};

// ---- Program IDs ----------------------------------------------------------
Expand Down Expand Up @@ -249,30 +248,14 @@ fn read_current_root(data: &[u8]) -> [u8; 32] {

// ---- Transaction helpers ----------------------------------------------------

fn send(
svm: &mut LiteSVM,
ixs: Vec<Instruction>,
payer: &Keypair,
signers: &[&Keypair],
) -> Result<(), Box<litesvm::types::FailedTransactionMetadata>> {
let msg = Message::new(&ixs, Some(&payer.pubkey()));
let blockhash = svm.latest_blockhash();
let mut tx = Transaction::new_unsigned(msg);
tx.sign(signers, blockhash);
svm.send_transaction(tx).map(|_| ()).map_err(Box::new)
}

/// Assert a failed transaction carries the given program error.
fn assert_custom_error(
result: Result<(), Box<litesvm::types::FailedTransactionMetadata>>,
expected: VaultError,
) {
fn assert_custom_error(result: Result<(), SolanaKiteError>, expected: VaultError) {
let failed = result.expect_err("transaction should fail");
// v2's `#[error_code]` makes the enum `#[repr(u32)]` and only generates
// `From<VaultError> for anchor_lang::Error`, so the on-wire custom code is
// the discriminant plus the default 6000 offset.
let expected_code = expected as u32 + 6000;
let error_text = format!("{:?}", failed.err);
let error_text = format!("{failed:?}");
assert!(
error_text.contains(&format!("Custom({expected_code})")),
"expected Custom({expected_code}), got: {error_text}"
Expand Down Expand Up @@ -325,16 +308,9 @@ fn setup_vault() -> VaultTestContext {
)
.unwrap();

let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 100 * solana_native_token::LAMPORTS_PER_SOL)
.unwrap();
let payer = create_wallet(&mut svm, 100 * solana_native_token::LAMPORTS_PER_SOL).unwrap();

let authority = Keypair::new();
svm.airdrop(
&authority.pubkey(),
10 * solana_native_token::LAMPORTS_PER_SOL,
)
.unwrap();
let authority = create_wallet(&mut svm, 10 * solana_native_token::LAMPORTS_PER_SOL).unwrap();

// The vault PDA that stores the authority, owns the cNFTs (as Bubblegum
// leaf owner) and signs the transfer CPI.
Expand All @@ -357,11 +333,11 @@ fn setup_vault() -> VaultTestContext {
vault_pda,
};
let authority_keypair = svm_context.authority.insecure_clone();
send(
send_transaction_from_instructions(
&mut svm_context.svm,
vec![initialize_ix],
&authority_keypair,
&[&authority_keypair],
&authority_keypair.pubkey(),
)
.expect("initialize_vault should succeed");

Expand Down Expand Up @@ -419,11 +395,11 @@ fn create_tree_with_vault_cnft(context: &mut VaultTestContext) -> TreeWithVaultC
},
};

send(
send_transaction_from_instructions(
&mut context.svm,
vec![create_acc, create_tree_ix],
&payer,
&[&payer, &merkle_tree],
&payer.pubkey(),
)
.expect("create_tree_config should succeed");

Expand Down Expand Up @@ -469,7 +445,8 @@ fn create_tree_with_vault_cnft(context: &mut VaultTestContext) -> TreeWithVaultC
d
},
};
send(&mut context.svm, vec![mint_ix], &payer, &[&payer]).expect("mint_v1 should succeed");
send_transaction_from_instructions(&mut context.svm, vec![mint_ix], &[&payer], &payer.pubkey())
.expect("mint_v1 should succeed");

// Recompute data_hash and creator_hash exactly as Bubblegum does.
let data_hash = hash_metadata(&metadata);
Expand Down Expand Up @@ -612,22 +589,22 @@ fn test_withdraw_cnft_by_authority() {

// The stored authority signs, so the withdraw succeeds (the vault PDA
// signs the Bubblegum CPI via invoke_signed inside the program).
send(
send_transaction_from_instructions(
&mut context.svm,
vec![withdraw_ix.clone()],
&authority,
&[&authority],
&authority.pubkey(),
)
.expect("withdraw_cnft signed by the vault authority should succeed");

// After transfer, leaf 0's owner changed (vault -> recipient), so the root
// moved. A second withdraw replaying the same (root, hashes) must fail: the
// cached root is stale and the leaf no longer hashes to it for the vault.
let second = send(
let second = send_transaction_from_instructions(
&mut context.svm,
vec![withdraw_ix],
&authority,
&[&authority],
&authority.pubkey(),
);
assert!(
second.is_err(),
Expand All @@ -643,19 +620,18 @@ fn test_withdraw_cnft_rejected_for_non_authority() {

// An attacker funds and signs their own withdraw attempt; the vault's
// stored authority did not sign.
let attacker = Keypair::new();
context
.svm
.airdrop(
&attacker.pubkey(),
10 * solana_native_token::LAMPORTS_PER_SOL,
)
.unwrap();
let attacker =
create_wallet(&mut context.svm, 10 * solana_native_token::LAMPORTS_PER_SOL).unwrap();

let withdraw_ix =
build_withdraw_cnft_instruction(&context, attacker.pubkey(), &tree, recipient.pubkey());

let result = send(&mut context.svm, vec![withdraw_ix], &attacker, &[&attacker]);
let result = send_transaction_from_instructions(
&mut context.svm,
vec![withdraw_ix],
&[&attacker],
&attacker.pubkey(),
);
assert_custom_error(result, VaultError::InvalidWithdrawAuthority);
}

Expand All @@ -677,19 +653,24 @@ fn test_withdraw_two_cnfts_by_authority() {
MAX_DEPTH as u8,
);

send(
send_transaction_from_instructions(
&mut context.svm,
vec![withdraw_ix],
&authority,
&[&authority],
&authority.pubkey(),
)
.expect("withdraw_two_cnfts signed by the vault authority should succeed");

// Both trees' roots moved, so both cNFTs left the vault: replaying the
// single-tree withdraw against either tree with the cached roots fails.
let replay1 =
build_withdraw_cnft_instruction(&context, authority.pubkey(), &tree1, recipient.pubkey());
let replay = send(&mut context.svm, vec![replay1], &authority, &[&authority]);
let replay = send_transaction_from_instructions(
&mut context.svm,
vec![replay1],
&[&authority],
&authority.pubkey(),
);
assert!(
replay.is_err(),
"cNFT#1 already left the vault, replay must fail"
Expand Down Expand Up @@ -720,11 +701,11 @@ fn test_withdraw_two_cnfts_rejects_out_of_range_proof_length() {
0,
);

let result = send(
let result = send_transaction_from_instructions(
&mut context.svm,
vec![withdraw_ix],
&authority,
&[&authority],
&authority.pubkey(),
);
assert_custom_error(result, VaultError::ProofLengthMismatch);
}
Expand All @@ -749,11 +730,11 @@ fn test_withdraw_two_cnfts_rejects_inconsistent_proof_lengths() {
MAX_DEPTH as u8,
);

let result = send(
let result = send_transaction_from_instructions(
&mut context.svm,
vec![withdraw_ix],
&authority,
&[&authority],
&authority.pubkey(),
);
assert_custom_error(result, VaultError::ProofLengthMismatch);
}
1 change: 1 addition & 0 deletions compression/cutils/anchor/programs/cutils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(target_os, values("solana"

[dev-dependencies]
litesvm = "0.13.1"
solana-kite = "0.4.0"
solana-instruction = "3.0.0"
solana-keypair = "3.0.1"
solana-pubkey = "3.0.0"
Expand Down
Loading
Loading