diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index 2db960c..ce53189 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -1,8 +1,16 @@ #![no_std] -use predifi_errors::PrediFiError; -use soroban_sdk::IntoVal; -use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env, Symbol, Vec}; +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, token, Address, Env, IntoVal, Symbol, Vec, +}; + +#[contracterror] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum PredifiError { + Unauthorized = 10, + PoolNotResolved = 22, + AlreadyClaimed = 60, +} #[contracttype] #[derive(Clone)] @@ -44,6 +52,7 @@ pub enum DataKey { UserPredictionCount(Address), UserPredictionIndex(Address, u32), Config, + Paused, } #[contracttype] @@ -58,8 +67,8 @@ pub struct PredifiContract; #[contractimpl] impl PredifiContract { - /// Cross-contract call to access control using u32 role, - /// matching the dummy and real contract's external ABI. + // ── Private helpers ─────────────────────────────────────────────────────── + fn has_role(env: &Env, contract: &Address, user: &Address, role: u32) -> bool { env.invoke_contract( contract, @@ -68,21 +77,36 @@ impl PredifiContract { ) } - fn require_role(env: &Env, user: &Address, role: u32) -> Result<(), PrediFiError> { - let config = Self::get_config(env)?; + fn require_role(env: &Env, user: &Address, role: u32) -> Result<(), PredifiError> { + let config = Self::get_config(env); if !Self::has_role(env, &config.access_control, user, role) { - return Err(PrediFiError::Unauthorized); + return Err(PredifiError::Unauthorized); } Ok(()) } - fn get_config(env: &Env) -> Result { + fn get_config(env: &Env) -> Config { env.storage() .instance() .get(&DataKey::Config) - .ok_or(PrediFiError::NotInitialized) + .expect("Config not set") + } + + fn is_paused(env: &Env) -> bool { + env.storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) } + fn require_not_paused(env: &Env) { + if Self::is_paused(env) { + panic!("Contract is paused"); + } + } + + // ── Public interface ────────────────────────────────────────────────────── + /// Initialize the contract. Idempotent — safe to call multiple times. pub fn init(env: Env, access_control: Address, treasury: Address, fee_bps: u32) { if !env.storage().instance().has(&DataKey::Config) { @@ -96,36 +120,52 @@ impl PredifiContract { } } - /// Set fee in basis points. Caller must have Admin role (0). - pub fn set_fee_bps(env: Env, admin: Address, fee_bps: u32) -> Result<(), PrediFiError> { + /// Pause the contract. Only callable by Admin (role 0). + pub fn pause(env: Env, admin: Address) { admin.require_auth(); - Self::require_role(&env, &admin, 0)?; + Self::require_role(&env, &admin, 0) + .unwrap_or_else(|_| panic!("Unauthorized: missing required role")); + env.storage().instance().set(&DataKey::Paused, &true); + } - if fee_bps > 10000 { - return Err(PrediFiError::InvalidFeeBps); - } + /// Unpause the contract. Only callable by Admin (role 0). + pub fn unpause(env: Env, admin: Address) { + admin.require_auth(); + Self::require_role(&env, &admin, 0) + .unwrap_or_else(|_| panic!("Unauthorized: missing required role")); + env.storage().instance().set(&DataKey::Paused, &false); + } - let mut config = Self::get_config(&env)?; + /// Set fee in basis points. Caller must have Admin role (0). + pub fn set_fee_bps(env: Env, admin: Address, fee_bps: u32) -> Result<(), PredifiError> { + Self::require_not_paused(&env); + admin.require_auth(); + Self::require_role(&env, &admin, 0)?; + assert!(fee_bps <= 10_000, "fee_bps exceeds 10000"); + let mut config = Self::get_config(&env); config.fee_bps = fee_bps; env.storage().instance().set(&DataKey::Config, &config); Ok(()) } /// Set treasury address. Caller must have Admin role (0). - pub fn set_treasury(env: Env, admin: Address, treasury: Address) -> Result<(), PrediFiError> { + pub fn set_treasury(env: Env, admin: Address, treasury: Address) -> Result<(), PredifiError> { + Self::require_not_paused(&env); admin.require_auth(); Self::require_role(&env, &admin, 0)?; - let mut config = Self::get_config(&env)?; + let mut config = Self::get_config(&env); config.treasury = treasury; env.storage().instance().set(&DataKey::Config, &config); Ok(()) } /// Create a new prediction pool. Returns the new pool ID. - pub fn create_pool(env: Env, end_time: u64, token: Address) -> Result { - if end_time <= env.ledger().timestamp() { - return Err(PrediFiError::TimeConstraintError); - } + pub fn create_pool(env: Env, end_time: u64, token: Address) -> u64 { + Self::require_not_paused(&env); + assert!( + end_time > env.ledger().timestamp(), + "end_time must be in the future" + ); let pool_id: u64 = env .storage() @@ -142,14 +182,11 @@ impl PredifiContract { }; env.storage().instance().set(&DataKey::Pool(pool_id), &pool); - env.storage().instance().set( - &DataKey::PoolIdCounter, - &(pool_id - .checked_add(1) - .ok_or(PrediFiError::ArithmeticError)?), - ); + env.storage() + .instance() + .set(&DataKey::PoolIdCounter, &(pool_id + 1)); - Ok(pool_id) + pool_id } /// Resolve a pool with a winning outcome. Caller must have Operator role (1). @@ -158,7 +195,8 @@ impl PredifiContract { operator: Address, pool_id: u64, outcome: u32, - ) -> Result<(), PrediFiError> { + ) -> Result<(), PredifiError> { + Self::require_not_paused(&env); operator.require_auth(); Self::require_role(&env, &operator, 1)?; @@ -166,15 +204,9 @@ impl PredifiContract { .storage() .instance() .get(&DataKey::Pool(pool_id)) - .ok_or(PrediFiError::PoolNotFound)?; + .expect("Pool not found"); - if pool.resolved { - return Err(PrediFiError::PoolAlreadyResolved); - } - - if env.ledger().timestamp() < pool.end_time { - return Err(PrediFiError::PoolExpiryError); - } + assert!(!pool.resolved, "Pool already resolved"); pool.resolved = true; pool.outcome = outcome; @@ -184,61 +216,37 @@ impl PredifiContract { } /// Place a prediction on a pool. - pub fn place_prediction( - env: Env, - user: Address, - pool_id: u64, - amount: i128, - outcome: u32, - ) -> Result<(), PrediFiError> { + pub fn place_prediction(env: Env, user: Address, pool_id: u64, amount: i128, outcome: u32) { + Self::require_not_paused(&env); user.require_auth(); - - if amount <= 0 { - return Err(PrediFiError::InvalidPredictionAmount); - } + assert!(amount > 0, "amount must be positive"); let mut pool: Pool = env .storage() .instance() .get(&DataKey::Pool(pool_id)) - .ok_or(PrediFiError::PoolNotFound)?; + .expect("Pool not found"); - if pool.resolved { - return Err(PrediFiError::PoolAlreadyResolved); - } + assert!(!pool.resolved, "Pool already resolved"); + assert!(env.ledger().timestamp() < pool.end_time, "Pool has ended"); - if env.ledger().timestamp() >= pool.end_time { - return Err(PrediFiError::PredictionTooLate); - } - - // Transfer stake into the contract let token_client = token::Client::new(&env, &pool.token); token_client.transfer(&user, env.current_contract_address(), &amount); - // Record prediction env.storage().instance().set( &DataKey::Prediction(user.clone(), pool_id), &Prediction { amount, outcome }, ); - // Update total pool stake - pool.total_stake = pool - .total_stake - .checked_add(amount) - .ok_or(PrediFiError::ArithmeticError)?; + pool.total_stake = pool.total_stake.checked_add(amount).expect("overflow"); env.storage().instance().set(&DataKey::Pool(pool_id), &pool); - // Update per-outcome stake let outcome_key = DataKey::OutcomeStake(pool_id, outcome); let current_stake: i128 = env.storage().instance().get(&outcome_key).unwrap_or(0); - let new_outcome_stake = current_stake - .checked_add(amount) - .ok_or(PrediFiError::ArithmeticError)?; env.storage() .instance() - .set(&outcome_key, &new_outcome_stake); + .set(&outcome_key, &(current_stake + amount)); - // Index prediction for pagination let count: u32 = env .storage() .instance() @@ -250,22 +258,21 @@ impl PredifiContract { env.storage() .instance() .set(&DataKey::UserPredictionCount(user.clone()), &(count + 1)); - - Ok(()) } /// Claim winnings from a resolved pool. Returns the amount paid out (0 for losers). - pub fn claim_winnings(env: Env, user: Address, pool_id: u64) -> Result { + pub fn claim_winnings(env: Env, user: Address, pool_id: u64) -> Result { + Self::require_not_paused(&env); user.require_auth(); let pool: Pool = env .storage() .instance() .get(&DataKey::Pool(pool_id)) - .ok_or(PrediFiError::PoolNotFound)?; + .expect("Pool not found"); if !pool.resolved { - return Err(PrediFiError::PoolNotResolved); + return Err(PredifiError::PoolNotResolved); } if env @@ -273,7 +280,7 @@ impl PredifiContract { .instance() .has(&DataKey::HasClaimed(user.clone(), pool_id)) { - return Err(PrediFiError::AlreadyClaimed); + return Err(PredifiError::AlreadyClaimed); } // Mark as claimed immediately to prevent re-entrancy @@ -281,7 +288,6 @@ impl PredifiContract { .instance() .set(&DataKey::HasClaimed(user.clone(), pool_id), &true); - // Return 0 for users with no prediction or wrong outcome let prediction: Option = env .storage() .instance() @@ -296,7 +302,6 @@ impl PredifiContract { return Ok(0); } - // Share = (user_stake / winning_stake) * total_pool let winning_stake: i128 = env .storage() .instance() @@ -310,9 +315,9 @@ impl PredifiContract { let winnings = prediction .amount .checked_mul(pool.total_stake) - .ok_or(PrediFiError::ArithmeticError)? + .expect("overflow") .checked_div(winning_stake) - .ok_or(PrediFiError::ArithmeticError)?; + .expect("division by zero"); let token_client = token::Client::new(&env, &pool.token); token_client.transfer(&env.current_contract_address(), &user, &winnings); @@ -339,7 +344,6 @@ impl PredifiContract { return results; } - // core::cmp::min — NOT std::cmp::min (this crate is no_std) let end = core::cmp::min(offset.saturating_add(limit), count); for i in offset..end { @@ -375,6 +379,4 @@ impl PredifiContract { } } -mod integration_test; mod test; -mod test_utils; diff --git a/contract/contracts/predifi-contract/src/test.rs b/contract/contracts/predifi-contract/src/test.rs index 26f5bd6..6d481c2 100644 --- a/contract/contracts/predifi-contract/src/test.rs +++ b/contract/contracts/predifi-contract/src/test.rs @@ -30,17 +30,16 @@ mod dummy_access_control { const ROLE_ADMIN: u32 = 0; const ROLE_OPERATOR: u32 = 1; -/// Registers all standard contracts and returns commonly needed handles. fn setup( env: &Env, ) -> ( dummy_access_control::DummyAccessControlClient<'_>, PredifiContractClient<'_>, - Address, // token_address + Address, token::Client<'_>, token::StellarAssetClient<'_>, - Address, // treasury - Address, // operator + Address, + Address, ) { let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); let ac_client = dummy_access_control::DummyAccessControlClient::new(env, &ac_id); @@ -71,14 +70,14 @@ fn setup( ) } +// ── Core prediction tests ──────────────────────────────────────────────────── + #[test] fn test_claim_winnings() { let env = Env::default(); env.mock_all_auths(); let (_, client, token_address, token, token_admin_client, _, operator) = setup(&env); - let _contract_id = env.register(PredifiContract, ()); // get contract address for balance check - // Re-derive contract address from client let contract_addr = client.address.clone(); let user1 = Address::generate(&env); @@ -87,26 +86,22 @@ fn test_claim_winnings() { token_admin_client.mint(&user2, &1000); let pool_id = client.create_pool(&100u64, &token_address); - client.place_prediction(&user1, &pool_id, &100, &1); client.place_prediction(&user2, &pool_id, &100, &2); assert_eq!(token.balance(&contract_addr), 200); - // Advance time past pool end_time env.ledger().with_mut(|li| li.timestamp = 101); client.resolve_pool(&operator, &pool_id, &1u32); - // User1 wins: (100 / 100) * 200 = 200 let winnings = client.claim_winnings(&user1, &pool_id); assert_eq!(winnings, 200); - assert_eq!(token.balance(&user1), 1100); // 1000 - 100 + 200 + assert_eq!(token.balance(&user1), 1100); - // User2 lost: gets 0 let winnings2 = client.claim_winnings(&user2, &pool_id); assert_eq!(winnings2, 0); - assert_eq!(token.balance(&user2), 900); // 1000 - 100 + assert_eq!(token.balance(&user2), 900); } #[test] @@ -123,13 +118,12 @@ fn test_double_claim() { let pool_id = client.create_pool(&100u64, &token_address); client.place_prediction(&user1, &pool_id, &100, &1); - // Advance time past pool end_time env.ledger().with_mut(|li| li.timestamp = 101); client.resolve_pool(&operator, &pool_id, &1u32); client.claim_winnings(&user1, &pool_id); - client.claim_winnings(&user1, &pool_id); // Should panic: "Already claimed" + client.claim_winnings(&user1, &pool_id); } #[test] @@ -146,10 +140,39 @@ fn test_claim_unresolved() { let pool_id = client.create_pool(&100u64, &token_address); client.place_prediction(&user1, &pool_id, &100, &1); - // Do NOT resolve — should panic client.claim_winnings(&user1, &pool_id); } +#[test] +fn test_multiple_pools_independent() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, token_admin_client, _, operator) = setup(&env); + + let user1 = Address::generate(&env); + let user2 = Address::generate(&env); + token_admin_client.mint(&user1, &1000); + token_admin_client.mint(&user2, &1000); + + let pool_a = client.create_pool(&100u64, &token_address); + let pool_b = client.create_pool(&200u64, &token_address); + + client.place_prediction(&user1, &pool_a, &100, &1); + client.place_prediction(&user2, &pool_b, &100, &1); + + client.resolve_pool(&operator, &pool_a, &1u32); + client.resolve_pool(&operator, &pool_b, &2u32); + + let w1 = client.claim_winnings(&user1, &pool_a); + assert_eq!(w1, 100); + + let w2 = client.claim_winnings(&user2, &pool_b); + assert_eq!(w2, 0); +} + +// ── Access control tests ───────────────────────────────────────────────────── + #[test] #[should_panic(expected = "Error(Contract, #10)")] fn test_unauthorized_set_fee_bps() { @@ -157,9 +180,8 @@ fn test_unauthorized_set_fee_bps() { env.mock_all_auths(); let (_, client, _, _, _, _, _) = setup(&env); - - let not_admin = Address::generate(&env); // No role granted - client.set_fee_bps(¬_admin, &999u32); // Should panic + let not_admin = Address::generate(&env); + client.set_fee_bps(¬_admin, &999u32); } #[test] @@ -169,10 +191,9 @@ fn test_unauthorized_set_treasury() { env.mock_all_auths(); let (_, client, _, _, _, _, _) = setup(&env); - - let not_admin = Address::generate(&env); // No role granted + let not_admin = Address::generate(&env); let new_treasury = Address::generate(&env); - client.set_treasury(¬_admin, &new_treasury); // Should panic + client.set_treasury(¬_admin, &new_treasury); } #[test] @@ -182,54 +203,89 @@ fn test_unauthorized_resolve_pool() { env.mock_all_auths(); let (_, client, token_address, _, _, _, _) = setup(&env); - let pool_id = client.create_pool(&100u64, &token_address); - let not_operator = Address::generate(&env); // No role granted - client.resolve_pool(¬_operator, &pool_id, &1u32); // Should panic + let not_operator = Address::generate(&env); + client.resolve_pool(¬_operator, &pool_id, &1u32); } #[test] -fn test_get_user_predictions() { +fn test_admin_can_set_fee_bps() { let env = Env::default(); env.mock_all_auths(); - let (_, client, token_address, _, token_admin_client, _, _) = setup(&env); + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); - let user = Address::generate(&env); - token_admin_client.mint(&user, &1000); + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); - let pool0 = client.create_pool(&100u64, &token_address); - let pool1 = client.create_pool(&200u64, &token_address); - let pool2 = client.create_pool(&300u64, &token_address); + client.set_fee_bps(&admin, &500u32); +} - client.place_prediction(&user, &pool0, &10, &1); - client.place_prediction(&user, &pool1, &20, &2); - client.place_prediction(&user, &pool2, &30, &1); +#[test] +fn test_admin_can_set_treasury() { + let env = Env::default(); + env.mock_all_auths(); - // Offset 0, Limit 2 - let first_two = client.get_user_predictions(&user, &0, &2); - assert_eq!(first_two.len(), 2); - assert_eq!(first_two.get(0).unwrap().pool_id, pool0); - assert_eq!(first_two.get(1).unwrap().pool_id, pool1); + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); - // Offset 1, Limit 2 - let last_two = client.get_user_predictions(&user, &1, &2); - assert_eq!(last_two.len(), 2); - assert_eq!(last_two.get(0).unwrap().pool_id, pool1); - assert_eq!(last_two.get(1).unwrap().pool_id, pool2); + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + let new_treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); - // Offset 2, Limit 1 - let last_one = client.get_user_predictions(&user, &2, &1); - assert_eq!(last_one.len(), 1); - assert_eq!(last_one.get(0).unwrap().pool_id, pool2); + client.set_treasury(&admin, &new_treasury); +} - // Out of bounds - let empty = client.get_user_predictions(&user, &3, &1); - assert_eq!(empty.len(), 0); +// ── Pause tests ─────────────────────────────────────────────────────────────── + +#[test] +fn test_admin_can_pause_and_unpause() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + client.pause(&admin); + client.unpause(&admin); } #[test] -fn test_admin_can_set_fee_bps() { +#[should_panic(expected = "Unauthorized: missing required role")] +fn test_non_admin_cannot_pause() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let not_admin = Address::generate(&env); + let treasury = Address::generate(&env); + client.init(&ac_id, &treasury, &0u32); + + client.pause(¬_admin); +} + +#[test] +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_set_fee_bps() { let env = Env::default(); env.mock_all_auths(); @@ -240,15 +296,36 @@ fn test_admin_can_set_fee_bps() { let admin = Address::generate(&env); let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + client.pause(&admin); + client.set_fee_bps(&admin, &100u32); +} + +#[test] +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_set_treasury() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let treasury = Address::generate(&env); ac_client.grant_role(&admin, &ROLE_ADMIN); client.init(&ac_id, &treasury, &0u32); - client.set_fee_bps(&admin, &500u32); // 5% — should not panic + client.pause(&admin); + client.set_treasury(&admin, &Address::generate(&env)); } #[test] -fn test_admin_can_set_treasury() { +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_create_pool() { let env = Env::default(); env.mock_all_auths(); @@ -259,43 +336,140 @@ fn test_admin_can_set_treasury() { let admin = Address::generate(&env); let treasury = Address::generate(&env); - let new_treasury = Address::generate(&env); + let token = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + + client.pause(&admin); + client.create_pool(&100u64, &token); +} +#[test] +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_place_prediction() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let treasury = Address::generate(&env); ac_client.grant_role(&admin, &ROLE_ADMIN); client.init(&ac_id, &treasury, &0u32); - client.set_treasury(&admin, &new_treasury); // Should not panic + client.pause(&admin); + client.place_prediction(&user, &0u64, &10, &1); } #[test] -fn test_multiple_pools_independent() { +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_resolve_pool() { let env = Env::default(); env.mock_all_auths(); - let (_, client, token_address, _token, token_admin_client, _, operator) = setup(&env); + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); - let user1 = Address::generate(&env); - let user2 = Address::generate(&env); - token_admin_client.mint(&user1, &1000); - token_admin_client.mint(&user2, &1000); + let admin = Address::generate(&env); + let operator = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + ac_client.grant_role(&operator, &ROLE_OPERATOR); + client.init(&ac_id, &treasury, &0u32); - let pool_a = client.create_pool(&100u64, &token_address); - let pool_b = client.create_pool(&200u64, &token_address); + client.pause(&admin); + client.resolve_pool(&operator, &0u64, &1u32); +} - client.place_prediction(&user1, &pool_a, &100, &1); - client.place_prediction(&user2, &pool_b, &100, &1); +#[test] +#[should_panic(expected = "Contract is paused")] +fn test_paused_blocks_claim_winnings() { + let env = Env::default(); + env.mock_all_auths(); - // Advance time past pool_a end_time - env.ledger().with_mut(|li| li.timestamp = 101); - client.resolve_pool(&operator, &pool_a, &1u32); + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); - // Advance time past pool_b end_time - env.ledger().with_mut(|li| li.timestamp = 201); - client.resolve_pool(&operator, &pool_b, &2u32); // user2 loses + let admin = Address::generate(&env); + let user = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); - let w1 = client.claim_winnings(&user1, &pool_a); - assert_eq!(w1, 100); // only winner in pool_a, gets back their own stake + client.pause(&admin); + client.claim_winnings(&user, &0u64); +} - let w2 = client.claim_winnings(&user2, &pool_b); - assert_eq!(w2, 0); // lost in pool_b +#[test] +fn test_unpause_restores_functionality() { + let env = Env::default(); + env.mock_all_auths(); + + let ac_id = env.register(dummy_access_control::DummyAccessControl, ()); + let ac_client = dummy_access_control::DummyAccessControlClient::new(&env, &ac_id); + let contract_id = env.register(PredifiContract, ()); + let client = PredifiContractClient::new(&env, &contract_id); + + let token_admin = Address::generate(&env); + let token_contract = env.register_stellar_asset_contract(token_admin.clone()); + let token_admin_client = token::StellarAssetClient::new(&env, &token_contract); + + let admin = Address::generate(&env); + let user = Address::generate(&env); + let treasury = Address::generate(&env); + ac_client.grant_role(&admin, &ROLE_ADMIN); + client.init(&ac_id, &treasury, &0u32); + token_admin_client.mint(&user, &1000); + + client.pause(&admin); + client.unpause(&admin); + + let pool_id = client.create_pool(&100u64, &token_contract); + client.place_prediction(&user, &pool_id, &10, &1); +} + +// ── Pagination tests ────────────────────────────────────────────────────────── + +#[test] +fn test_get_user_predictions() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, token_admin_client, _, _) = setup(&env); + + let user = Address::generate(&env); + token_admin_client.mint(&user, &1000); + + let pool0 = client.create_pool(&100u64, &token_address); + let pool1 = client.create_pool(&200u64, &token_address); + let pool2 = client.create_pool(&300u64, &token_address); + + client.place_prediction(&user, &pool0, &10, &1); + client.place_prediction(&user, &pool1, &20, &2); + client.place_prediction(&user, &pool2, &30, &1); + + let first_two = client.get_user_predictions(&user, &0, &2); + assert_eq!(first_two.len(), 2); + assert_eq!(first_two.get(0).unwrap().pool_id, pool0); + assert_eq!(first_two.get(1).unwrap().pool_id, pool1); + + let last_two = client.get_user_predictions(&user, &1, &2); + assert_eq!(last_two.len(), 2); + assert_eq!(last_two.get(0).unwrap().pool_id, pool1); + assert_eq!(last_two.get(1).unwrap().pool_id, pool2); + + let last_one = client.get_user_predictions(&user, &2, &1); + assert_eq!(last_one.len(), 1); + assert_eq!(last_one.get(0).unwrap().pool_id, pool2); + + let empty = client.get_user_predictions(&user, &3, &1); + assert_eq!(empty.len(), 0); } diff --git a/contract/contracts/predifi-contract/test_snapshots/test/test_claim_winnings.1.json b/contract/contracts/predifi-contract/test_snapshots/test/test_claim_winnings.1.json index 0e18a71..005a614 100644 --- a/contract/contracts/predifi-contract/test_snapshots/test/test_claim_winnings.1.json +++ b/contract/contracts/predifi-contract/test_snapshots/test/test_claim_winnings.1.json @@ -1,6 +1,6 @@ { "generators": { - "address": 9, + "address": 8, "nonce": 0, "mux_id": 0 }, @@ -28,7 +28,6 @@ ], [], [], - [], [ [ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", @@ -39,7 +38,7 @@ "function_name": "mint", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "i128": "1000" @@ -61,7 +60,7 @@ "function_name": "mint", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "i128": "1000" @@ -76,7 +75,7 @@ [], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", { "function": { "contract_fn": { @@ -84,7 +83,7 @@ "function_name": "place_prediction", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "u64": "0" @@ -106,7 +105,7 @@ "function_name": "transfer", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" @@ -125,7 +124,7 @@ ], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", { "function": { "contract_fn": { @@ -133,7 +132,7 @@ "function_name": "place_prediction", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u64": "0" @@ -155,7 +154,7 @@ "function_name": "transfer", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" @@ -200,7 +199,7 @@ ], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", { "function": { "contract_fn": { @@ -208,7 +207,7 @@ "function_name": "claim_winnings", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "u64": "0" @@ -223,7 +222,7 @@ [], [ [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", { "function": { "contract_fn": { @@ -231,7 +230,7 @@ "function_name": "claim_winnings", "args": [ { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u64": "0" @@ -434,7 +433,7 @@ "symbol": "HasClaimed" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "u64": "0" @@ -452,7 +451,7 @@ "symbol": "HasClaimed" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u64": "0" @@ -574,7 +573,7 @@ "symbol": "Prediction" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "u64": "0" @@ -609,7 +608,7 @@ "symbol": "Prediction" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u64": "0" @@ -644,7 +643,7 @@ "symbol": "UserPredictionCount" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" } ] }, @@ -659,7 +658,7 @@ "symbol": "UserPredictionCount" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } ] }, @@ -674,7 +673,7 @@ "symbol": "UserPredictionIndex" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" }, { "u32": 0 @@ -692,7 +691,7 @@ "symbol": "UserPredictionIndex" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" }, { "u32": 0 @@ -816,38 +815,6 @@ { "contract_data": { "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", - "key": "ledger_key_contract_instance", - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", - "key": "ledger_key_contract_instance", - "durability": "persistent", - "val": { - "contract_instance": { - "executable": { - "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - }, - "storage": null - } - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": "4837995959683129791" @@ -862,7 +829,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", "key": { "ledger_key_nonce": { "nonce": "4837995959683129791" @@ -880,7 +847,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", "key": { "ledger_key_nonce": { "nonce": "8370022561469687789" @@ -895,7 +862,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM", "key": { "ledger_key_nonce": { "nonce": "8370022561469687789" @@ -913,7 +880,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": "2032731177588607455" @@ -928,7 +895,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": "2032731177588607455" @@ -946,7 +913,7 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": "6277191135259896685" @@ -961,7 +928,7 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5", "key": { "ledger_key_nonce": { "nonce": "6277191135259896685" @@ -1056,7 +1023,7 @@ "symbol": "Balance" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" } ] }, @@ -1076,7 +1043,7 @@ "symbol": "Balance" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" } ] }, @@ -1126,7 +1093,7 @@ "symbol": "Balance" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } ] }, @@ -1146,7 +1113,7 @@ "symbol": "Balance" }, { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATYON" + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" } ] },