From 800e76ccad0d18c652bb5439efd7d57471f79377 Mon Sep 17 00:00:00 2001 From: beebozy Date: Tue, 24 Feb 2026 19:45:08 -0800 Subject: [PATCH 1/2] contracts stats and analytics --- contracts/earn-quest/src/stats.rs | 46 ++ contracts/earn-quest/src/test_stats.rs | 601 +++++++++++++++++++++++++ contracts/earn-quest/src/types.rs | 47 ++ 3 files changed, 694 insertions(+) create mode 100644 contracts/earn-quest/src/stats.rs create mode 100644 contracts/earn-quest/src/test_stats.rs diff --git a/contracts/earn-quest/src/stats.rs b/contracts/earn-quest/src/stats.rs new file mode 100644 index 0000000..af09ebc --- /dev/null +++ b/contracts/earn-quest/src/stats.rs @@ -0,0 +1,46 @@ +// ============================================================ +// ADD THESE STRUCTS TO contracts/earn-quest/src/types.rs +// ============================================================ +// Paste both structs at the bottom of the existing types.rs +// file. They reuse the existing `#[contracttype]` + `#[derive]` pattern. +// ============================================================ + +use soroban_sdk::contracttype; + +/// Platform-wide aggregated statistics. +/// +/// Updated atomically on every quest creation, submission, and claim. +/// Queried via `EarnQuestContract::get_platform_stats()`. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlatformStats { + /// Total number of quests ever registered (never decremented). + pub total_quests_created: u64, + /// Total number of proof submissions ever received. + pub total_submissions: u64, + /// Sum of `reward_amount` across all quests ever created (in token base units). + /// Represents total value posted, not necessarily paid out. + pub total_rewards_distributed: u128, + /// Number of unique wallet addresses that have submitted at least once. + pub total_active_users: u64, + /// Number of rewards that reached the `Paid` status (successful claims). + pub total_rewards_claimed: u64, +} + +/// Per-creator statistics, scoped to a single quest creator address. +/// +/// Updated on quest creation and whenever a submission or claim +/// targets a quest owned by this creator. +/// Queried via `EarnQuestContract::get_creator_stats(creator)`. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CreatorStats { + /// Total quests created by this address. + pub quests_created: u64, + /// Sum of `reward_amount` across all quests created by this address. + pub total_rewards_posted: u128, + /// Total submissions received across all of this creator's quests. + pub total_submissions_received: u64, + /// Total successful claims paid out across all of this creator's quests. + pub total_claims_paid: u64, +} \ No newline at end of file diff --git a/contracts/earn-quest/src/test_stats.rs b/contracts/earn-quest/src/test_stats.rs new file mode 100644 index 0000000..4605250 --- /dev/null +++ b/contracts/earn-quest/src/test_stats.rs @@ -0,0 +1,601 @@ +//! tests/test_stats.rs +//! +//! Comprehensive test suite for platform-wide and per-creator statistics. +//! +//! Test groups: +//! 1. Initial state — all counters start at zero +//! 2. Quest creation tracking +//! 3. Submission / active-user tracking +//! 4. Claim / reward-claimed tracking +//! 5. Per-creator isolation +//! 6. Multi-creator scenarios +//! 7. Counter integrity (idempotency, monotonicity) +//! 8. Admin-only reset +//! 9. Public query access (no auth required) + +#![cfg(test)] + +use soroban_sdk::testutils::{Address as _, Ledger, LedgerInfo}; +use soroban_sdk::{symbol_short, Address, BytesN, Env}; + +use earn_quest::{EarnQuestContract, EarnQuestContractClient}; + +// --------------------------------------------------------------------------- +// Test helpers +// --------------------------------------------------------------------------- + +fn make_env() -> Env { + let env = Env::default(); + env.mock_all_auths(); + env +} + +fn set_time(env: &Env, ts: u64) { + env.ledger().set(LedgerInfo { + protocol_version: 20, + sequence_number: 1, + timestamp: ts, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 100, + min_persistent_entry_ttl: 100, + max_entry_ttl: 1_000_000, + }); +} + +fn setup(env: &Env) -> (EarnQuestContractClient, Address) { + let cid = env.register_contract(None, EarnQuestContract); + let client = EarnQuestContractClient::new(env, &cid); + let admin = Address::generate(env); + client.initialize(&admin); + (client, admin) +} + +fn mock_token(env: &Env) -> Address { + Address::generate(env) +} + +fn register_quest( + client: &EarnQuestContractClient, + env: &Env, + id: &str, + creator: &Address, + reward_amount: i128, +) -> soroban_sdk::Symbol { + let quest_id = symbol_short!(id); + let token = mock_token(env); + let verifier = Address::generate(env); + let deadline = env.ledger().timestamp() + 86_400; + client.register_quest(&quest_id, creator, &token, &reward_amount, &verifier, &deadline); + quest_id +} + +fn submit( + client: &EarnQuestContractClient, + env: &Env, + quest_id: &soroban_sdk::Symbol, + submitter: &Address, +) { + let proof: BytesN<32> = BytesN::from_array(env, &[1u8; 32]); + client.submit_proof(quest_id, submitter, &proof); +} + +fn full_lifecycle( + client: &EarnQuestContractClient, + env: &Env, + quest_sym: &str, + creator: &Address, + submitter: &Address, + reward_amount: i128, +) -> soroban_sdk::Symbol { + let quest_id = symbol_short!(quest_sym); + let token = mock_token(env); + let verifier = Address::generate(env); + let deadline = env.ledger().timestamp() + 86_400; + client.register_quest(&quest_id, creator, &token, &reward_amount, &verifier, &deadline); + let proof: BytesN<32> = BytesN::from_array(env, &[2u8; 32]); + client.submit_proof(&quest_id, submitter, &proof); + client.approve_submission(&quest_id, submitter, &verifier); + client.claim_reward(&quest_id, submitter); + quest_id +} + +// --------------------------------------------------------------------------- +// 1. Initial state +// --------------------------------------------------------------------------- + +#[test] +fn test_stats_initial_state_all_zero() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + + let stats = client.get_platform_stats(); + assert_eq!(stats.total_quests_created, 0); + assert_eq!(stats.total_submissions, 0); + assert_eq!(stats.total_rewards_distributed, 0); + assert_eq!(stats.total_active_users, 0); + assert_eq!(stats.total_rewards_claimed, 0); +} + +#[test] +fn test_creator_stats_initial_state_all_zero() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + + let stats = client.get_creator_stats(&creator); + assert_eq!(stats.quests_created, 0); + assert_eq!(stats.total_rewards_posted, 0); + assert_eq!(stats.total_submissions_received, 0); + assert_eq!(stats.total_claims_paid, 0); +} + +// --------------------------------------------------------------------------- +// 2. Quest creation tracking +// --------------------------------------------------------------------------- + +#[test] +fn test_platform_quest_count_increments_on_single_create() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + + register_quest(&client, &env, "q001", &creator, 500); + + let stats = client.get_platform_stats(); + assert_eq!(stats.total_quests_created, 1); +} + +#[test] +fn test_platform_quest_count_increments_multiple_times() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + + register_quest(&client, &env, "q001", &creator, 100); + register_quest(&client, &env, "q002", &creator, 200); + register_quest(&client, &env, "q003", &creator, 300); + + assert_eq!(client.get_platform_stats().total_quests_created, 3); +} + +#[test] +fn test_platform_rewards_distributed_tracks_sum_of_reward_amounts() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + + register_quest(&client, &env, "q001", &creator, 1_000); + register_quest(&client, &env, "q002", &creator, 2_500); + + assert_eq!(client.get_platform_stats().total_rewards_distributed, 3_500); +} + +#[test] +fn test_creator_quest_count_and_rewards_posted_track_correctly() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + + register_quest(&client, &env, "q001", &creator, 400); + register_quest(&client, &env, "q002", &creator, 600); + + let c = client.get_creator_stats(&creator); + assert_eq!(c.quests_created, 2); + assert_eq!(c.total_rewards_posted, 1_000); +} + +#[test] +fn test_platform_quest_count_is_monotonically_increasing() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + + assert_eq!(client.get_platform_stats().total_quests_created, 0); + register_quest(&client, &env, "q001", &creator, 50); + assert_eq!(client.get_platform_stats().total_quests_created, 1); + register_quest(&client, &env, "q002", &creator, 50); + assert_eq!(client.get_platform_stats().total_quests_created, 2); +} + +// --------------------------------------------------------------------------- +// 3. Submission / active-user tracking +// --------------------------------------------------------------------------- + +#[test] +fn test_platform_submission_count_increments_on_submit() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + let submitter = Address::generate(&env); + + let qid = register_quest(&client, &env, "q001", &creator, 500); + submit(&client, &env, &qid, &submitter); + + assert_eq!(client.get_platform_stats().total_submissions, 1); +} + +#[test] +fn test_platform_active_users_increments_on_first_submission() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + let user = Address::generate(&env); + + let qid = register_quest(&client, &env, "q001", &creator, 500); + submit(&client, &env, &qid, &user); + + assert_eq!(client.get_platform_stats().total_active_users, 1); +} + +#[test] +fn test_platform_active_users_does_not_double_count_same_user() { + // Same user submits on two different quests — must count as 1 unique user. + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + let user = Address::generate(&env); + + let qid1 = register_quest(&client, &env, "q001", &creator, 100); + let qid2 = register_quest(&client, &env, "q002", &creator, 200); + submit(&client, &env, &qid1, &user); + submit(&client, &env, &qid2, &user); + + let stats = client.get_platform_stats(); + assert_eq!(stats.total_submissions, 2, "two submission entries"); + assert_eq!(stats.total_active_users, 1, "same user must not be double-counted"); +} + +#[test] +fn test_platform_active_users_counts_each_distinct_address_once() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + let u1 = Address::generate(&env); + let u2 = Address::generate(&env); + let u3 = Address::generate(&env); + + let qid1 = register_quest(&client, &env, "q001", &creator, 300); + let qid2 = register_quest(&client, &env, "q002", &creator, 300); + let qid3 = register_quest(&client, &env, "q003", &creator, 300); + + submit(&client, &env, &qid1, &u1); + submit(&client, &env, &qid2, &u2); + submit(&client, &env, &qid3, &u3); + + assert_eq!(client.get_platform_stats().total_active_users, 3); +} + +#[test] +fn test_creator_submissions_received_increments_per_submission() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + let u1 = Address::generate(&env); + let u2 = Address::generate(&env); + + let qid = register_quest(&client, &env, "q001", &creator, 500); + submit(&client, &env, &qid, &u1); + submit(&client, &env, &qid, &u2); + + let c = client.get_creator_stats(&creator); + assert_eq!(c.total_submissions_received, 2); +} + +#[test] +fn test_submission_count_increments_per_submission_not_per_user() { + // Same user, two quests → 2 submissions total but 1 unique active user. + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + let user = Address::generate(&env); + + let qid1 = register_quest(&client, &env, "q001", &creator, 100); + let qid2 = register_quest(&client, &env, "q002", &creator, 200); + submit(&client, &env, &qid1, &user); + submit(&client, &env, &qid2, &user); + + let stats = client.get_platform_stats(); + assert_eq!(stats.total_submissions, 2); + assert_eq!(stats.total_active_users, 1); +} + +// --------------------------------------------------------------------------- +// 4. Claim / reward-claimed tracking +// --------------------------------------------------------------------------- + +#[test] +fn test_platform_rewards_claimed_increments_on_single_claim() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + let submitter = Address::generate(&env); + + full_lifecycle(&client, &env, "q001", &creator, &submitter, 500); + + assert_eq!(client.get_platform_stats().total_rewards_claimed, 1); +} + +#[test] +fn test_platform_rewards_claimed_increments_on_multiple_claims() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + let u1 = Address::generate(&env); + let u2 = Address::generate(&env); + + full_lifecycle(&client, &env, "q001", &creator, &u1, 300); + full_lifecycle(&client, &env, "q002", &creator, &u2, 700); + + assert_eq!(client.get_platform_stats().total_rewards_claimed, 2); +} + +#[test] +fn test_creator_claims_paid_increments_after_each_claim() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + let u1 = Address::generate(&env); + let u2 = Address::generate(&env); + + full_lifecycle(&client, &env, "q001", &creator, &u1, 400); + full_lifecycle(&client, &env, "q002", &creator, &u2, 600); + + let c = client.get_creator_stats(&creator); + assert_eq!(c.total_claims_paid, 2); +} + +#[test] +fn test_submission_without_claim_does_not_increment_rewards_claimed() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + let submitter = Address::generate(&env); + + let qid = register_quest(&client, &env, "q001", &creator, 500); + submit(&client, &env, &qid, &submitter); + // No approve or claim + + assert_eq!(client.get_platform_stats().total_rewards_claimed, 0); +} + +// --------------------------------------------------------------------------- +// 5. Per-creator isolation +// --------------------------------------------------------------------------- + +#[test] +fn test_creator_stats_are_isolated_between_creators() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let ca = Address::generate(&env); + let cb = Address::generate(&env); + let submitter = Address::generate(&env); + + // creator_a: 2 quests + register_quest(&client, &env, "a001", &ca, 1_000); + register_quest(&client, &env, "a002", &ca, 2_000); + // creator_b: 1 quest + register_quest(&client, &env, "b001", &cb, 500); + + // submit to creator_a's quest only + submit(&client, &env, &symbol_short!("a001"), &submitter); + + let ca_stats = client.get_creator_stats(&ca); + let cb_stats = client.get_creator_stats(&cb); + + assert_eq!(ca_stats.quests_created, 2); + assert_eq!(ca_stats.total_rewards_posted, 3_000); + assert_eq!(ca_stats.total_submissions_received, 1); + + assert_eq!(cb_stats.quests_created, 1); + assert_eq!(cb_stats.total_rewards_posted, 500); + assert_eq!(cb_stats.total_submissions_received, 0, "creator_b got no submissions"); +} + +#[test] +fn test_platform_aggregates_across_all_creators() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let ca = Address::generate(&env); + let cb = Address::generate(&env); + + register_quest(&client, &env, "a001", &ca, 1_000); + register_quest(&client, &env, "b001", &cb, 2_000); + + let stats = client.get_platform_stats(); + assert_eq!(stats.total_quests_created, 2); + assert_eq!(stats.total_rewards_distributed, 3_000); +} + +#[test] +fn test_creator_claims_paid_isolated_from_other_creator_claims() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let ca = Address::generate(&env); + let cb = Address::generate(&env); + let u1 = Address::generate(&env); + let u2 = Address::generate(&env); + + full_lifecycle(&client, &env, "a001", &ca, &u1, 300); + full_lifecycle(&client, &env, "b001", &cb, &u2, 700); + + assert_eq!(client.get_creator_stats(&ca).total_claims_paid, 1); + assert_eq!(client.get_creator_stats(&cb).total_claims_paid, 1); + // Platform sees both + assert_eq!(client.get_platform_stats().total_rewards_claimed, 2); +} + +// --------------------------------------------------------------------------- +// 6. Multi-creator / multi-user scenarios +// --------------------------------------------------------------------------- + +#[test] +fn test_full_platform_lifecycle_all_counters_consistent() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + + let ca = Address::generate(&env); + let cb = Address::generate(&env); + let u1 = Address::generate(&env); + let u2 = Address::generate(&env); + let u3 = Address::generate(&env); + + register_quest(&client, &env, "a001", &ca, 100); + register_quest(&client, &env, "a002", &ca, 200); + register_quest(&client, &env, "b001", &cb, 300); + register_quest(&client, &env, "b002", &cb, 400); + + // u1 submits twice; u2 and u3 each submit once + submit(&client, &env, &symbol_short!("a001"), &u1); + submit(&client, &env, &symbol_short!("a002"), &u2); + submit(&client, &env, &symbol_short!("b001"), &u1); // second submission by u1 + submit(&client, &env, &symbol_short!("b002"), &u3); + + let stats = client.get_platform_stats(); + assert_eq!(stats.total_quests_created, 4); + assert_eq!(stats.total_submissions, 4); + assert_eq!(stats.total_active_users, 3, "u1 submits twice but counts once"); + assert_eq!(stats.total_rewards_distributed, 1_000); + assert_eq!(stats.total_rewards_claimed, 0); +} + +// --------------------------------------------------------------------------- +// 7. Counter integrity +// --------------------------------------------------------------------------- + +#[test] +fn test_minimum_reward_amount_tracked_correctly() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + + register_quest(&client, &env, "q001", &creator, 1); + + assert_eq!(client.get_platform_stats().total_rewards_distributed, 1); +} + +#[test] +fn test_large_reward_amount_tracked_without_overflow() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let creator = Address::generate(&env); + + let big: i128 = i128::MAX / 4; + register_quest(&client, &env, "q001", &creator, big); + + let stats = client.get_platform_stats(); + assert_eq!(stats.total_rewards_distributed, big as u128); +} + +// --------------------------------------------------------------------------- +// 8. Admin-only reset +// --------------------------------------------------------------------------- + +#[test] +fn test_admin_can_reset_platform_stats_to_zero() { + let env = make_env(); + set_time(&env, 1_000); + let (client, admin) = setup(&env); + let creator = Address::generate(&env); + + register_quest(&client, &env, "q001", &creator, 500); + assert_eq!(client.get_platform_stats().total_quests_created, 1); + + client.reset_platform_stats(&admin); + + let stats = client.get_platform_stats(); + assert_eq!(stats.total_quests_created, 0); + assert_eq!(stats.total_rewards_distributed, 0); + assert_eq!(stats.total_submissions, 0); + assert_eq!(stats.total_active_users, 0); + assert_eq!(stats.total_rewards_claimed, 0); +} + +#[test] +fn test_non_admin_cannot_reset_platform_stats() { + let env = make_env(); + set_time(&env, 1_000); + let (client, _) = setup(&env); + let random = Address::generate(&env); + + let result = client.try_reset_platform_stats(&random); + assert!(result.is_err(), "non-admin must be rejected"); +} + +#[test] +fn test_stats_accumulate_correctly_after_reset() { + let env = make_env(); + set_time(&env, 1_000); + let (client, admin) = setup(&env); + let creator = Address::generate(&env); + + register_quest(&client, &env, "q001", &creator, 500); + client.reset_platform_stats(&admin); + + // A quest created after reset should restart from 1 + register_quest(&client, &env, "q002", &creator, 250); + let stats = client.get_platform_stats(); + assert_eq!(stats.total_quests_created, 1, "counter must restart after reset"); + assert_eq!(stats.total_rewards_distributed, 250); +} + +// --------------------------------------------------------------------------- +// 9. Public query — no auth required +// --------------------------------------------------------------------------- + +#[test] +fn test_get_platform_stats_requires_no_auth() { + // Intentionally not calling mock_all_auths globally for this test. + let env = Env::default(); + set_time(&env, 1_000); + let cid = env.register_contract(None, EarnQuestContract); + let client = EarnQuestContractClient::new(&env, &cid); + + // initialize still requires auth + env.mock_all_auths(); + let admin = Address::generate(&env); + client.initialize(&admin); + + // get_platform_stats must not require auth — call without mock + let stats = client.get_platform_stats(); + assert_eq!(stats.total_quests_created, 0); +} + +#[test] +fn test_get_creator_stats_requires_no_auth() { + let env = Env::default(); + set_time(&env, 1_000); + let cid = env.register_contract(None, EarnQuestContract); + let client = EarnQuestContractClient::new(&env, &cid); + + env.mock_all_auths(); + let admin = Address::generate(&env); + client.initialize(&admin); + + let creator = Address::generate(&env); + let c = client.get_creator_stats(&creator); + assert_eq!(c.quests_created, 0); +} \ No newline at end of file diff --git a/contracts/earn-quest/src/types.rs b/contracts/earn-quest/src/types.rs index d1409e1..5cc3cdf 100644 --- a/contracts/earn-quest/src/types.rs +++ b/contracts/earn-quest/src/types.rs @@ -59,3 +59,50 @@ pub enum Badge { Master, Legend, } + +// ============================================================ +// ADD THESE STRUCTS TO contracts/earn-quest/src/types.rs +// ============================================================ +// Paste both structs at the bottom of the existing types.rs +// file. They reuse the existing `#[contracttype]` + `#[derive]` pattern. +// ============================================================ + + + +/// Platform-wide aggregated statistics. +/// +/// Updated atomically on every quest creation, submission, and claim. +/// Queried via `EarnQuestContract::get_platform_stats()`. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PlatformStats { + /// Total number of quests ever registered (never decremented). + pub total_quests_created: u64, + /// Total number of proof submissions ever received. + pub total_submissions: u64, + /// Sum of `reward_amount` across all quests ever created (in token base units). + /// Represents total value posted, not necessarily paid out. + pub total_rewards_distributed: u128, + /// Number of unique wallet addresses that have submitted at least once. + pub total_active_users: u64, + /// Number of rewards that reached the `Paid` status (successful claims). + pub total_rewards_claimed: u64, +} + +/// Per-creator statistics, scoped to a single quest creator address. +/// +/// Updated on quest creation and whenever a submission or claim +/// targets a quest owned by this creator. +/// Queried via `EarnQuestContract::get_creator_stats(creator)`. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CreatorStats { + /// Total quests created by this address. + pub quests_created: u64, + /// Sum of `reward_amount` across all quests created by this address. + pub total_rewards_posted: u128, + /// Total submissions received across all of this creator's quests. + pub total_submissions_received: u64, + /// Total successful claims paid out across all of this creator's quests. + pub total_claims_paid: u64, +} \ No newline at end of file From 3cbdb6750ee26eeac86966ca365202b11791d3c0 Mon Sep 17 00:00:00 2001 From: beebozy Date: Tue, 24 Feb 2026 19:52:35 -0800 Subject: [PATCH 2/2] fix: format issue --- contracts/earn-quest/src/errors.rs | 8 +-- contracts/earn-quest/src/events.rs | 41 +++--------- contracts/earn-quest/src/lib.rs | 7 +- contracts/earn-quest/src/payout.rs | 4 +- contracts/earn-quest/src/security.rs | 2 +- contracts/earn-quest/src/storage.rs | 16 +++-- contracts/earn-quest/src/submission.rs | 11 +--- contracts/earn-quest/src/types.rs | 21 +++++- contracts/earn-quest/src/validation.rs | 11 ++-- contracts/earn-quest/tests/test_events.rs | 27 ++++---- contracts/earn-quest/tests/test_payout.rs | 59 +++++++++-------- contracts/earn-quest/tests/test_security.rs | 20 +++++- contracts/earn-quest/tests/test_validation.rs | 66 ++++++------------- 13 files changed, 144 insertions(+), 149 deletions(-) diff --git a/contracts/earn-quest/src/errors.rs b/contracts/earn-quest/src/errors.rs index d4f953b..cf0d056 100644 --- a/contracts/earn-quest/src/errors.rs +++ b/contracts/earn-quest/src/errors.rs @@ -9,20 +9,20 @@ pub enum Error { QuestNotFound = 2, InvalidRewardAmount = 3, QuestStillActive = 4, - + // Auth Errors Unauthorized = 10, - + // Submission Errors InvalidSubmissionStatus = 20, SubmissionNotFound = 21, - + // Payout Errors InsufficientBalance = 30, TransferFailed = 31, AlreadyClaimed = 32, InvalidAsset = 33, - + // Reputation Errors UserStatsNotFound = 40, // Security / Emergency diff --git a/contracts/earn-quest/src/events.rs b/contracts/earn-quest/src/events.rs index 021942d..80c4a68 100644 --- a/contracts/earn-quest/src/events.rs +++ b/contracts/earn-quest/src/events.rs @@ -1,6 +1,6 @@ #![allow(unused)] -use soroban_sdk::{Env, Symbol, Address, BytesN, symbol_short}; use crate::types::Badge; +use soroban_sdk::{symbol_short, Address, BytesN, Env, Symbol}; // Event Topics (Names) const TOPIC_QUEST_REGISTERED: Symbol = symbol_short!("quest_reg"); @@ -69,12 +69,7 @@ pub fn timelock_scheduled(env: &Env, scheduled_time: u64) { } /// Emit when a user submits a proof -pub fn proof_submitted( - env: &Env, - quest_id: Symbol, - submitter: Address, - proof_hash: BytesN<32>, -) { +pub fn proof_submitted(env: &Env, quest_id: Symbol, submitter: Address, proof_hash: BytesN<32>) { // Topics: [EventName, QuestID, Submitter] let topics = (TOPIC_PROOF_SUBMITTED, quest_id, submitter); // Data: (ProofHash) @@ -83,12 +78,7 @@ pub fn proof_submitted( } /// Emit when a verifier approves a submission -pub fn submission_approved( - env: &Env, - quest_id: Symbol, - submitter: Address, - verifier: Address, -) { +pub fn submission_approved(env: &Env, quest_id: Symbol, submitter: Address, verifier: Address) { // Topics: [EventName, QuestID, Submitter] let topics = (TOPIC_SUBMISSION_APPROVED, quest_id, submitter); // Data: (Verifier) @@ -112,13 +102,7 @@ pub fn reward_claimed( } /// Emit when XP is awarded to a user -pub fn xp_awarded( - env: &Env, - user: Address, - xp_amount: u64, - total_xp: u64, - level: u32, -) { +pub fn xp_awarded(env: &Env, user: Address, xp_amount: u64, total_xp: u64, level: u32) { // Topics: [EventName, User] let topics = (TOPIC_XP_AWARDED, user); // Data: (XP Amount, Total XP, Level) @@ -127,11 +111,7 @@ pub fn xp_awarded( } /// Emit when a user levels up -pub fn level_up( - env: &Env, - user: Address, - new_level: u32, -) { +pub fn level_up(env: &Env, user: Address, new_level: u32) { // Topics: [EventName, User] let topics = (TOPIC_LEVEL_UP, user); // Data: (New Level) @@ -140,17 +120,14 @@ pub fn level_up( } /// Emit when a badge is granted to a user -pub fn badge_granted( - env: &Env, - user: Address, - badge: Badge, -) { +pub fn badge_granted(env: &Env, user: Address, badge: Badge) { // Topics: [EventName, User] let topics = (TOPIC_BADGE_GRANTED, user); // Data: (Badge) let data = (badge,); env.events().publish(topics, data); } +<<<<<<< HEAD const TOPIC_ESCROW_DEPOSITED: Symbol = symbol_short!("esc_dep"); const TOPIC_ESCROW_PAYOUT: Symbol = symbol_short!("esc_pay"); @@ -205,4 +182,6 @@ pub fn quest_cancelled( let topics = (TOPIC_QUEST_CANCELLED, quest_id, creator); let data = (refunded,); env.events().publish(topics, data); -} \ No newline at end of file +} +======= +>>>>>>> f4f4415 (fix: format issue) diff --git a/contracts/earn-quest/src/lib.rs b/contracts/earn-quest/src/lib.rs index 4bd04fc..6eb6bdc 100644 --- a/contracts/earn-quest/src/lib.rs +++ b/contracts/earn-quest/src/lib.rs @@ -3,15 +3,20 @@ mod admin; pub mod errors; mod events; -mod security; mod payout; +mod quest; mod reputation; +mod security; pub mod storage; +mod submission; pub mod types; pub mod validation; +<<<<<<< HEAD mod quest; mod submission; mod escrow; +======= +>>>>>>> f4f4415 (fix: format issue) use crate::errors::Error; use crate::types::{Badge, BatchApprovalInput, BatchQuestInput, UserStats, EscrowInfo}; diff --git a/contracts/earn-quest/src/payout.rs b/contracts/earn-quest/src/payout.rs index 64b5bc2..6d44b54 100644 --- a/contracts/earn-quest/src/payout.rs +++ b/contracts/earn-quest/src/payout.rs @@ -1,8 +1,8 @@ -use soroban_sdk::{token, Address, Env}; use crate::errors::Error; +use soroban_sdk::{token, Address, Env}; /// Transfer rewards from the contract escrow to the user. -/// +/// /// This function handles the low-level token transfer and ensures /// the contract has sufficient balance. pub fn transfer_reward( diff --git a/contracts/earn-quest/src/security.rs b/contracts/earn-quest/src/security.rs index 79b4a66..485a51a 100644 --- a/contracts/earn-quest/src/security.rs +++ b/contracts/earn-quest/src/security.rs @@ -1,7 +1,7 @@ use crate::errors::Error; use crate::events; use crate::storage; -use soroban_sdk::{Address, Env, token}; +use soroban_sdk::{token, Address, Env}; /// Is contract paused? pub fn is_paused(env: &Env) -> bool { diff --git a/contracts/earn-quest/src/storage.rs b/contracts/earn-quest/src/storage.rs index 77f1eb1..8ac839e 100644 --- a/contracts/earn-quest/src/storage.rs +++ b/contracts/earn-quest/src/storage.rs @@ -624,15 +624,15 @@ pub fn set_scheduled_unpause_time(env: &Env, ts: u64) { } pub fn get_scheduled_unpause_time(env: &Env) -> Option { - env.storage() - .instance() - .get(&DataKey::ScheduledUnpauseTime) + env.storage().instance().get(&DataKey::ScheduledUnpauseTime) } pub fn clear_unpause_approvals(env: &Env) { // There's no easy iteration to remove all UnpauseApproval keys. // The higher level security module will simply leave entries; remove scheduled time and paused flag. - env.storage().instance().remove(&DataKey::ScheduledUnpauseTime); + env.storage() + .instance() + .remove(&DataKey::ScheduledUnpauseTime); } fn inc_unpause_approval_count(env: &Env) { @@ -642,7 +642,9 @@ fn inc_unpause_approval_count(env: &Env) { .get(&DataKey::UnpauseApprovalCount) .unwrap_or(0u32); cur = cur.saturating_add(1); - env.storage().instance().set(&DataKey::UnpauseApprovalCount, &cur); + env.storage() + .instance() + .set(&DataKey::UnpauseApprovalCount, &cur); } fn dec_unpause_approval_count(env: &Env) { @@ -654,7 +656,9 @@ fn dec_unpause_approval_count(env: &Env) { if cur > 0 { cur -= 1; } - env.storage().instance().set(&DataKey::UnpauseApprovalCount, &cur); + env.storage() + .instance() + .set(&DataKey::UnpauseApprovalCount, &cur); } //================================================================================ diff --git a/contracts/earn-quest/src/submission.rs b/contracts/earn-quest/src/submission.rs index 17e6b21..f5fd56e 100644 --- a/contracts/earn-quest/src/submission.rs +++ b/contracts/earn-quest/src/submission.rs @@ -95,11 +95,7 @@ pub fn approve_submission( /// - Submission is not already paid (AlreadyClaimed) /// - Submission status transition (Approved -> Paid) is valid /// - Quest claims have not exceeded the limit -pub fn validate_claim( - env: &Env, - quest_id: &Symbol, - submitter: &Address, -) -> Result<(), Error> { +pub fn validate_claim(env: &Env, quest_id: &Symbol, submitter: &Address) -> Result<(), Error> { let quest = storage::get_quest(env, quest_id)?; let submission = storage::get_submission(env, quest_id, submitter)?; @@ -109,10 +105,7 @@ pub fn validate_claim( } // Validate status transition: Approved -> Paid - validation::validate_submission_status_transition( - &submission.status, - &SubmissionStatus::Paid, - )?; + validation::validate_submission_status_transition(&submission.status, &SubmissionStatus::Paid)?; // Validate quest claims limit validation::validate_quest_claims_limit(quest.total_claims)?; diff --git a/contracts/earn-quest/src/types.rs b/contracts/earn-quest/src/types.rs index 334b5fa..f0488e9 100644 --- a/contracts/earn-quest/src/types.rs +++ b/contracts/earn-quest/src/types.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracttype, Address, Symbol, BytesN, Vec}; +use soroban_sdk::{contracttype, Address, BytesN, Symbol, Vec}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -65,8 +65,15 @@ pub enum Badge { // Batch operation input types (gas-optimized multi-item operations) //================================================================================ +<<<<<<< HEAD /// Single quest registration input for batch registration. /// Creator is implied from auth in register_quests_batch. +======= +/// Platform-wide aggregated statistics. +/// +/// Updated atomically on every quest creation, submission, and claim. +/// Queried via `EarnQuestContract::get_platform_stats()`. +>>>>>>> f4f4415 (fix: format issue) #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct BatchQuestInput { @@ -81,6 +88,7 @@ pub struct BatchQuestInput { /// Verifier is implied from auth in approve_submissions_batch. #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] +<<<<<<< HEAD pub struct BatchApprovalInput { pub quest_id: Symbol, pub submitter: Address, @@ -107,4 +115,15 @@ pub struct EscrowInfo { pub total_refunded: i128, /// Whether this escrow is still active pub is_active: bool, +======= +pub struct CreatorStats { + /// Total quests created by this address. + pub quests_created: u64, + /// Sum of `reward_amount` across all quests created by this address. + pub total_rewards_posted: u128, + /// Total submissions received across all of this creator's quests. + pub total_submissions_received: u64, + /// Total successful claims paid out across all of this creator's quests. + pub total_claims_paid: u64, +>>>>>>> f4f4415 (fix: format issue) } diff --git a/contracts/earn-quest/src/validation.rs b/contracts/earn-quest/src/validation.rs index c4095b3..e901cd9 100644 --- a/contracts/earn-quest/src/validation.rs +++ b/contracts/earn-quest/src/validation.rs @@ -145,9 +145,9 @@ fn symbol_len(sym: &soroban_sdk::Symbol) -> u32 { // We use a simple byte-count approach; Symbol stores short ASCII. // Since Soroban enforces this at construction, this is a secondary check. let _ = sym; // Symbol is always valid if constructed; length is implicitly bounded. - // In Soroban SDK, Symbols are at most 32 characters. We return a constant - // that passes validation since the SDK already enforces this. - // This function exists to provide a consistent API. + // In Soroban SDK, Symbols are at most 32 characters. We return a constant + // that passes validation since the SDK already enforces this. + // This function exists to provide a consistent API. MAX_SYMBOL_LENGTH // Symbols that exist are always valid } @@ -206,10 +206,7 @@ pub fn validate_badge_count(current_count: u32) -> Result<(), Error> { /// # Returns /// * `Ok(())` if the transition is allowed /// * `Err(Error::InvalidStatusTransition)` if the transition is not allowed -pub fn validate_quest_status_transition( - from: &QuestStatus, - to: &QuestStatus, -) -> Result<(), Error> { +pub fn validate_quest_status_transition(from: &QuestStatus, to: &QuestStatus) -> Result<(), Error> { let valid = match (from, to) { (QuestStatus::Active, QuestStatus::Paused) => true, (QuestStatus::Active, QuestStatus::Completed) => true, diff --git a/contracts/earn-quest/tests/test_events.rs b/contracts/earn-quest/tests/test_events.rs index 495de00..dfdb0da 100644 --- a/contracts/earn-quest/tests/test_events.rs +++ b/contracts/earn-quest/tests/test_events.rs @@ -2,8 +2,12 @@ extern crate earn_quest; use earn_quest::{EarnQuestContract, EarnQuestContractClient}; -use soroban_sdk::{Address, BytesN, Env, IntoVal, Symbol, symbol_short, testutils::{Address as _, Events}}; use soroban_sdk::token::StellarAssetClient; +use soroban_sdk::{ + symbol_short, + testutils::{Address as _, Events}, + Address, BytesN, Env, IntoVal, Symbol, +}; #[test] fn test_full_quest_lifecycle_events() { @@ -19,7 +23,7 @@ fn test_full_quest_lifecycle_events() { let token_contract_obj = env.register_stellar_asset_contract_v2(admin.clone()); let token_address = token_contract_obj.address(); let token_admin_client = StellarAssetClient::new(&env, &token_address); - + // Fund the contract so it can pay out rewards later let fund_amount = 1000i128; token_admin_client.mint(&contract_id, &fund_amount); @@ -45,7 +49,7 @@ fn test_full_quest_lifecycle_events() { // Verify Register Event let events = env.events().all(); let (contract, topics, data) = events.last().unwrap(); - + assert_eq!(contract, contract_id); // Topics: [EventName, QuestID, Creator] @@ -56,9 +60,10 @@ fn test_full_quest_lifecycle_events() { assert_eq!(t_name, symbol_short!("quest_reg")); assert_eq!(t_id, quest_id); assert_eq!(t_creator, creator); - + // Verify Data: (reward_asset, reward_amount, verifier, deadline) - let (asset_data, amount_data, verifier_data, deadline_data): (Address, i128, Address, u64) = data.into_val(&env); + let (asset_data, amount_data, verifier_data, deadline_data): (Address, i128, Address, u64) = + data.into_val(&env); assert_eq!(asset_data, token_address); assert_eq!(amount_data, reward_amount); assert_eq!(verifier_data, verifier); @@ -79,7 +84,7 @@ fn test_full_quest_lifecycle_events() { assert_eq!(t_name, symbol_short!("proof_sub")); assert_eq!(t_id, quest_id); assert_eq!(t_sub, user); - + // --- STEP 3: APPROVE SUBMISSION --- client.approve_submission(&quest_id, &user, &verifier); @@ -99,7 +104,7 @@ fn test_full_quest_lifecycle_events() { client.claim_reward(&quest_id, &user); let events = env.events().all(); - + // After claim_reward, we expect 2 events: reward_claimed and xp_awarded // Get the second-to-last event (reward_claimed) let event_count = events.len(); @@ -118,18 +123,18 @@ fn test_full_quest_lifecycle_events() { let (claimed_asset, claimed_amount): (Address, i128) = data.into_val(&env); assert_eq!(claimed_asset, token_address); assert_eq!(claimed_amount, reward_amount); - + // Verify the XP awarded event (last event) let (_, topics, data) = events.last().unwrap(); let t_name: Symbol = topics.get(0).unwrap().into_val(&env); let t_user: Address = topics.get(1).unwrap().into_val(&env); - + assert_eq!(t_name, symbol_short!("xp_award")); assert_eq!(t_user, user); - + // Verify XP data: (xp_amount, total_xp, level) let (xp_amount, total_xp, level): (u64, u64, u32) = data.into_val(&env); assert_eq!(xp_amount, 100); assert_eq!(total_xp, 100); assert_eq!(level, 1); -} \ No newline at end of file +} diff --git a/contracts/earn-quest/tests/test_payout.rs b/contracts/earn-quest/tests/test_payout.rs index b58e92e..3a9b41c 100644 --- a/contracts/earn-quest/tests/test_payout.rs +++ b/contracts/earn-quest/tests/test_payout.rs @@ -1,7 +1,7 @@ #![cfg(test)] -use soroban_sdk::{testutils::Address as _, Address, Env, symbol_short, BytesN}; use soroban_sdk::token::{StellarAssetClient, TokenClient}; +use soroban_sdk::{symbol_short, testutils::Address as _, Address, BytesN, Env}; // Import from the library extern crate earn_quest; @@ -11,28 +11,28 @@ use earn_quest::{EarnQuestContract, EarnQuestContractClient}; fn test_payout_success() { let env = Env::default(); env.mock_all_auths(); - + let contract_id = env.register_contract(None, EarnQuestContract); let client = EarnQuestContractClient::new(&env, &contract_id); - + // 1. Setup Token let admin = Address::generate(&env); let token_contract_obj = env.register_stellar_asset_contract_v2(admin.clone()); let token_contract = token_contract_obj.address(); let token_admin_client = StellarAssetClient::new(&env, &token_contract); let token_client = TokenClient::new(&env, &token_contract); - + // Fund the contract with enough tokens for payout let reward_amount = 100i128; - token_admin_client.mint(&contract_id, &1000); - + token_admin_client.mint(&contract_id, &1000); + // 2. Setup Quest let creator = Address::generate(&env); let verifier = Address::generate(&env); let submitter = Address::generate(&env); - + let quest_id = symbol_short!("Q1"); - + client.register_quest( &quest_id, &creator, @@ -41,20 +41,20 @@ fn test_payout_success() { &verifier, &10000, ); - + // 3. Submit Proof let proof = BytesN::from_array(&env, &[1u8; 32]); client.submit_proof(&quest_id, &submitter, &proof); - + // 4. Approve Submission client.approve_submission(&quest_id, &submitter, &verifier); - + // 5. Claim Reward let pre_balance = token_client.balance(&submitter); assert_eq!(pre_balance, 0); - + client.claim_reward(&quest_id, &submitter); - + let post_balance = token_client.balance(&submitter); assert_eq!(post_balance, 100); } @@ -63,20 +63,20 @@ fn test_payout_success() { fn test_insufficient_balance() { let env = Env::default(); env.mock_all_auths(); - + let contract_id = env.register_contract(None, EarnQuestContract); let client = EarnQuestContractClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let token_contract_obj = env.register_stellar_asset_contract_v2(admin.clone()); let token_contract = token_contract_obj.address(); // Do NOT fund contract - + let creator = Address::generate(&env); let verifier = Address::generate(&env); let submitter = Address::generate(&env); let quest_id = symbol_short!("Q2"); - + client.register_quest( &quest_id, &creator, @@ -85,35 +85,38 @@ fn test_insufficient_balance() { &verifier, &10000, ); - + let proof = BytesN::from_array(&env, &[1u8; 32]); client.submit_proof(&quest_id, &submitter, &proof); client.approve_submission(&quest_id, &submitter, &verifier); - + // Claim should fail with InsufficientBalance let res = client.try_claim_reward(&quest_id, &submitter); - assert!(res.is_err(), "Expected claim to fail due to insufficient balance"); + assert!( + res.is_err(), + "Expected claim to fail due to insufficient balance" + ); } #[test] fn test_double_claim_prevention() { let env = Env::default(); env.mock_all_auths(); - + let contract_id = env.register_contract(None, EarnQuestContract); let client = EarnQuestContractClient::new(&env, &contract_id); - + let admin = Address::generate(&env); let token_contract_obj = env.register_stellar_asset_contract_v2(admin.clone()); let token_contract = token_contract_obj.address(); let token_admin_client = StellarAssetClient::new(&env, &token_contract); - token_admin_client.mint(&contract_id, &1000); - + token_admin_client.mint(&contract_id, &1000); + let creator = Address::generate(&env); let verifier = Address::generate(&env); let submitter = Address::generate(&env); let quest_id = symbol_short!("Q3"); - + client.register_quest( &quest_id, &creator, @@ -122,14 +125,14 @@ fn test_double_claim_prevention() { &verifier, &10000, ); - + let proof = BytesN::from_array(&env, &[1u8; 32]); client.submit_proof(&quest_id, &submitter, &proof); client.approve_submission(&quest_id, &submitter, &verifier); - + // First claim client.claim_reward(&quest_id, &submitter); - + // Second claim should fail with AlreadyClaimed let res = client.try_claim_reward(&quest_id, &submitter); assert!(res.is_err(), "Expected second claim to fail"); diff --git a/contracts/earn-quest/tests/test_security.rs b/contracts/earn-quest/tests/test_security.rs index b072f71..1ceec89 100644 --- a/contracts/earn-quest/tests/test_security.rs +++ b/contracts/earn-quest/tests/test_security.rs @@ -1,7 +1,7 @@ #![cfg(test)] -use soroban_sdk::{testutils::Address as _, Address, Env, symbol_short, BytesN}; use soroban_sdk::token::{StellarAssetClient, TokenClient}; +use soroban_sdk::{symbol_short, testutils::Address as _, Address, BytesN, Env}; extern crate earn_quest; use earn_quest::{EarnQuestContract, EarnQuestContractClient}; @@ -31,7 +31,14 @@ fn test_pause_blocks_register_quest() { let quest_id = symbol_short!("SQ1"); // This should panic because contract is paused - client.register_quest(&quest_id, &creator, &token_contract, &100, &verifier, &10000); + client.register_quest( + &quest_id, + &creator, + &token_contract, + &100, + &verifier, + &10000, + ); } #[test] @@ -97,7 +104,14 @@ fn test_multisig_approve_and_unpause_with_zero_timelock() { let token_contract = token_contract_obj.address(); let quest_id = symbol_short!("SQ2"); - client.register_quest(&quest_id, &creator, &token_contract, &100, &verifier, &10000); + client.register_quest( + &quest_id, + &creator, + &token_contract, + &100, + &verifier, + &10000, + ); } #[test] diff --git a/contracts/earn-quest/tests/test_validation.rs b/contracts/earn-quest/tests/test_validation.rs index 71a8d8f..03edade 100644 --- a/contracts/earn-quest/tests/test_validation.rs +++ b/contracts/earn-quest/tests/test_validation.rs @@ -58,11 +58,7 @@ fn test_register_quest_creator_verifier_same_address_rejected() { // Creator and verifier are the same address - should fail let result = client.try_register_quest( - &quest_id, - &creator, - &token, - &1000, - &creator, // same as creator + &quest_id, &creator, &token, &1000, &creator, // same as creator &5000, ); @@ -273,8 +269,7 @@ fn test_register_quest_deadline_in_future_valid() { li.timestamp = 1000; }); - let result = - client.try_register_quest(&quest_id, &creator, &token, &500, &verifier, &10000); + let result = client.try_register_quest(&quest_id, &creator, &token, &500, &verifier, &10000); assert!(result.is_ok()); } @@ -372,82 +367,64 @@ fn test_validate_badge_count_below_max_valid() { #[test] fn test_quest_status_active_to_paused_valid() { - let result = validation::validate_quest_status_transition( - &QuestStatus::Active, - &QuestStatus::Paused, - ); + let result = + validation::validate_quest_status_transition(&QuestStatus::Active, &QuestStatus::Paused); assert!(result.is_ok()); } #[test] fn test_quest_status_active_to_completed_valid() { - let result = validation::validate_quest_status_transition( - &QuestStatus::Active, - &QuestStatus::Completed, - ); + let result = + validation::validate_quest_status_transition(&QuestStatus::Active, &QuestStatus::Completed); assert!(result.is_ok()); } #[test] fn test_quest_status_active_to_expired_valid() { - let result = validation::validate_quest_status_transition( - &QuestStatus::Active, - &QuestStatus::Expired, - ); + let result = + validation::validate_quest_status_transition(&QuestStatus::Active, &QuestStatus::Expired); assert!(result.is_ok()); } #[test] fn test_quest_status_paused_to_active_valid() { - let result = validation::validate_quest_status_transition( - &QuestStatus::Paused, - &QuestStatus::Active, - ); + let result = + validation::validate_quest_status_transition(&QuestStatus::Paused, &QuestStatus::Active); assert!(result.is_ok()); } #[test] fn test_quest_status_paused_to_expired_valid() { - let result = validation::validate_quest_status_transition( - &QuestStatus::Paused, - &QuestStatus::Expired, - ); + let result = + validation::validate_quest_status_transition(&QuestStatus::Paused, &QuestStatus::Expired); assert!(result.is_ok()); } #[test] fn test_quest_status_completed_to_active_rejected() { - let result = validation::validate_quest_status_transition( - &QuestStatus::Completed, - &QuestStatus::Active, - ); + let result = + validation::validate_quest_status_transition(&QuestStatus::Completed, &QuestStatus::Active); assert!(result.is_err()); } #[test] fn test_quest_status_expired_to_active_rejected() { - let result = validation::validate_quest_status_transition( - &QuestStatus::Expired, - &QuestStatus::Active, - ); + let result = + validation::validate_quest_status_transition(&QuestStatus::Expired, &QuestStatus::Active); assert!(result.is_err()); } #[test] fn test_quest_status_completed_to_paused_rejected() { - let result = validation::validate_quest_status_transition( - &QuestStatus::Completed, - &QuestStatus::Paused, - ); + let result = + validation::validate_quest_status_transition(&QuestStatus::Completed, &QuestStatus::Paused); assert!(result.is_err()); } #[test] fn test_quest_status_same_to_same_rejected() { - let result = validation::validate_quest_status_transition( - &QuestStatus::Active, - &QuestStatus::Active, - ); + let result = + validation::validate_quest_status_transition(&QuestStatus::Active, &QuestStatus::Active); assert!(result.is_err()); } @@ -623,8 +600,7 @@ fn test_duplicate_quest_registration_rejected() { client.register_quest(&quest_id, &creator, &token, &1000, &verifier, &10000); // Second registration with same ID should fail - let result = - client.try_register_quest(&quest_id, &creator, &token, &2000, &verifier, &20000); + let result = client.try_register_quest(&quest_id, &creator, &token, &2000, &verifier, &20000); assert!(result.is_err()); }