Skip to content

Commit ee1a67d

Browse files
authored
Merge pull request #212 from Qoder-Voidd/Verify-StellarHuntsNftClient-mint
Verify StellarHuntsNftClient::mint
2 parents 06678eb + 39a54cf commit ee1a67d

2 files changed

Lines changed: 90 additions & 5 deletions

File tree

onchain/contracts/stellar_hunts/src/bench.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ fn bench_submit_answer_cpu_budget() {
4444
let player = Address::generate(&env);
4545

4646
// Reset the budget so we only measure the submit_answer call itself.
47-
let budget = env.budget();
47+
let mut budget = env.budget();
4848
budget.reset_default();
4949

5050
let ok = client.submit_answer(&player, &1u64, &answer);
5151
assert!(ok);
5252

53-
let cpu = budget.cpu_instruction_count();
54-
let mem = budget.mem_bytes_count();
53+
let cpu = budget.cpu_instruction_cost();
54+
let mem = budget.memory_bytes_cost();
5555

5656
// Log diagnostics when run with --nocapture.
5757
eprintln!(
@@ -101,7 +101,7 @@ fn bench_ten_submit_answers_amortised() {
101101
}
102102

103103
let player = Address::generate(&env);
104-
let budget = env.budget();
104+
let mut budget = env.budget();
105105
budget.reset_default();
106106

107107
for i in 0..per_level {
@@ -110,7 +110,7 @@ fn bench_ten_submit_answers_amortised() {
110110
assert!(ok);
111111
}
112112

113-
let total_cpu = budget.cpu_instruction_count();
113+
let total_cpu = budget.cpu_instruction_cost();
114114
let avg_cpu = total_cpu / (per_level as u64);
115115

116116
eprintln!(

onchain/contracts/stellar_hunts/src/test.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ fn init_with_admin(env: &Env) -> (Address, Address, StellarHuntsClient) {
5555
#[test]
5656
fn test_set_question_per_level_admin_only() {
5757
let env = Env::default();
58+
env.mock_all_auths();
59+
let (_admin, client) = init_with_admin(&env);
5860
let (admin, contract_address, client) = init_with_admin(&env);
5961

6062
env.mock_auths(&[MockAuth {
@@ -94,6 +96,8 @@ fn test_set_question_per_level_unauthorized() {
9496
#[test]
9597
fn test_add_and_get_question() {
9698
let env = Env::default();
99+
env.mock_all_auths();
100+
let (_admin, client) = init_with_admin(&env);
97101
let (admin, contract_address, client) = init_with_admin(&env);
98102

99103
let level = crate::Levels::Easy;
@@ -420,6 +424,87 @@ fn test_require_admin_not_initialized() {
420424
client.set_question_per_level(&5u32);
421425
}
422426

427+
#[test]
428+
fn test_claim_level_completion_nft_retry_safe_on_nft_panic() {
429+
let env = Env::default();
430+
env.mock_all_auths();
431+
432+
let admin = new_admin(&env);
433+
let contract_id = env.register_contract(None, StellarHunts);
434+
let client = StellarHuntsClient::new(&env, &contract_id);
435+
client.init(&admin);
436+
437+
let player = user(&env);
438+
439+
// Register and initialise the NFT contract, granting the game
440+
// contract the minter role.
441+
let nft_id = env.register_contract(None, stellar_hunts_nft::StellarHuntsNft);
442+
let nft_client =
443+
stellar_hunts_nft::StellarHuntsNftClient::new(&env, &nft_id);
444+
nft_client.init(
445+
&admin,
446+
&contract_id,
447+
&soroban_sdk::String::from_str(&env, "ipfs://placeholder/"),
448+
&soroban_sdk::String::from_str(&env, "StellarHuntsBadge"),
449+
&soroban_sdk::String::from_str(&env, "SHB"),
450+
);
451+
452+
// Wire the game contract to the NFT contract.
453+
client.set_nft_contract_address(&nft_id);
454+
455+
// Setup: 1 question per level so the player can complete Easy quickly.
456+
client.set_question_per_level(&1u32);
457+
let level = crate::Levels::Easy;
458+
client.add_question(&level, &b(&env, "Q?"), &b(&env, "A"), &b(&env, "H"));
459+
460+
// Player completes Easy level.
461+
assert!(client.submit_answer(&player, &1u64, &b(&env, "A")));
462+
463+
// ---- First mint: success ----
464+
client.claim_level_completion_nft(&player, &level);
465+
assert!(nft_client.has_level_badge(&player, &level));
466+
467+
// Verify the game contract recorded the mint.
468+
let lp = client.get_player_level_progress(&player, &level);
469+
assert!(lp.nft_minted);
470+
471+
// ---- Simulate out-of-sync state ----
472+
// The NFT contract still holds the badge, but we reset the game
473+
// contract's nft_minted flag as if a previous cross-contract call
474+
// was interrupted before the storage write.
475+
env.as_contract(&contract_id, || {
476+
let lp_key = crate::DataKey::PlayerLevelProgress(player.clone(), level.clone());
477+
let mut lp: crate::LevelProgress =
478+
env.storage().persistent().get(&lp_key).unwrap();
479+
lp.nft_minted = false;
480+
env.storage().persistent().set(&lp_key, &lp);
481+
});
482+
483+
// Confirm the flag was reset.
484+
let lp_reset = client.get_player_level_progress(&player, &level);
485+
assert!(!lp_reset.nft_minted);
486+
487+
// ---- Second mint attempt: should panic ----
488+
// The game contract sees nft_minted == false and proceeds to call
489+
// the NFT contract, which already has the badge -> AlreadyHasBadge.
490+
let should_panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
491+
client.claim_level_completion_nft(&player, &level);
492+
}));
493+
assert!(
494+
should_panic.is_err(),
495+
"expected AlreadyHasBadge panic from NFT contract"
496+
);
497+
498+
// ---- Retry-safe assertion ----
499+
// Because the game contract writes lp.nft_minted = true AFTER the
500+
// cross-contract call, a panic in the NFT contract means the write
501+
// never executes. The flag must remain false so the player (or an
502+
// off-chain retry loop) can safely retry the claim.
503+
let lp_final = client.get_player_level_progress(&player, &level);
504+
assert!(
505+
!lp_final.nft_minted,
506+
"nft_minted must remain false so claim_level_completion_nft is retry-safe"
507+
);
423508
// ---------------------------------------------------------------------
424509
// Summary of negative-auth coverage added:
425510
// • test_set_question_per_level_unauthorized

0 commit comments

Comments
 (0)