From 56e11025619593f3035efea0069a24872a73017b Mon Sep 17 00:00:00 2001 From: dinahmaccodes Date: Sat, 21 Feb 2026 13:15:26 +0100 Subject: [PATCH 1/2] feat: add explicit state machine model for market lifecycle - Add MarketState enum (Active, Resolved, Canceled) - Update Pool struct to use state instead of resolved flag - Add state transition guards and validation - Implement cancel_pool function - Add PoolCanceledEvent --- .../contracts/predifi-contract/src/lib.rs | 455 +++++++++++++++--- .../contracts/predifi-contract/src/test.rs | 371 +++++++++++++- 2 files changed, 741 insertions(+), 85 deletions(-) diff --git a/contract/contracts/predifi-contract/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs index ce53189..d0fa7f7 100644 --- a/contract/contracts/predifi-contract/src/lib.rs +++ b/contract/contracts/predifi-contract/src/lib.rs @@ -1,25 +1,50 @@ #![no_std] use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, token, Address, Env, IntoVal, Symbol, Vec, + contract, contracterror, contractevent, contractimpl, contracttype, token, Address, Env, + IntoVal, String, Symbol, Vec, }; +const DAY_IN_LEDGERS: u32 = 17280; +const BUMP_THRESHOLD: u32 = 14 * DAY_IN_LEDGERS; +const BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; + +/// Represents the explicit state machine for prediction markets. +/// Valid transitions: +/// - Active -> Resolved +/// - Active -> Canceled +/// - Resolved (terminal state - no transitions allowed) +/// - Canceled (terminal state - no transitions allowed) +#[contracttype] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum MarketState { + Active = 0, + Resolved = 1, + Canceled = 2, +} + #[contracterror] #[derive(Copy, Clone, Debug, PartialEq)] pub enum PredifiError { Unauthorized = 10, PoolNotResolved = 22, AlreadyClaimed = 60, + InvalidStateTransition = 70, + MarketAlreadyClosed = 71, } #[contracttype] #[derive(Clone)] pub struct Pool { pub end_time: u64, - pub resolved: bool, + pub state: MarketState, pub outcome: u32, pub token: Address, pub total_stake: i128, + /// A short human-readable description of the event being predicted. + pub description: String, + /// A URL (e.g. IPFS CIDv1) pointing to extended metadata for this pool. + pub metadata_url: String, } #[contracttype] @@ -37,7 +62,7 @@ pub struct UserPredictionDetail { pub amount: i128, pub user_outcome: u32, pub pool_end_time: u64, - pub pool_resolved: bool, + pub pool_state: MarketState, pub pool_outcome: u32, } @@ -53,6 +78,7 @@ pub enum DataKey { UserPredictionIndex(Address, u32), Config, Paused, + PoolState(u64), } #[contracttype] @@ -62,6 +88,86 @@ pub struct Prediction { pub outcome: u32, } +// ── Events ─────────────────────────────────────────────────────────────────── + +#[contractevent(topics = ["init"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InitEvent { + pub access_control: Address, + pub treasury: Address, + pub fee_bps: u32, +} + +#[contractevent(topics = ["pause"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PauseEvent { + pub admin: Address, +} + +#[contractevent(topics = ["unpause"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct UnpauseEvent { + pub admin: Address, +} + +#[contractevent(topics = ["fee_update"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FeeUpdateEvent { + pub admin: Address, + pub fee_bps: u32, +} + +#[contractevent(topics = ["treasury_update"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TreasuryUpdateEvent { + pub admin: Address, + pub treasury: Address, +} + +#[contractevent(topics = ["pool_created"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PoolCreatedEvent { + pub pool_id: u64, + pub end_time: u64, + pub token: Address, + /// Metadata URL included so off-chain indexers can immediately fetch context. + pub metadata_url: String, +} + +#[contractevent(topics = ["pool_resolved"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PoolResolvedEvent { + pub pool_id: u64, + pub operator: Address, + pub outcome: u32, +} + +#[contractevent(topics = ["pool_canceled"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PoolCanceledEvent { + pub pool_id: u64, + pub operator: Address, +} + +#[contractevent(topics = ["prediction_placed"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PredictionPlacedEvent { + pub pool_id: u64, + pub user: Address, + pub amount: i128, + pub outcome: u32, +} + +#[contractevent(topics = ["winnings_claimed"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WinningsClaimedEvent { + pub pool_id: u64, + pub user: Address, + pub amount: i128, +} + +// ───────────────────────────────────────────────────────────────────────────── + #[contract] pub struct PredifiContract; @@ -69,6 +175,18 @@ pub struct PredifiContract; impl PredifiContract { // ── Private helpers ─────────────────────────────────────────────────────── + fn extend_instance(env: &Env) { + env.storage() + .instance() + .extend_ttl(BUMP_THRESHOLD, BUMP_AMOUNT); + } + + fn extend_persistent(env: &Env, key: &DataKey) { + env.storage() + .persistent() + .extend_ttl(key, BUMP_THRESHOLD, BUMP_AMOUNT); + } + fn has_role(env: &Env, contract: &Address, user: &Address, role: u32) -> bool { env.invoke_contract( contract, @@ -86,17 +204,23 @@ impl PredifiContract { } fn get_config(env: &Env) -> Config { - env.storage() + let config = env + .storage() .instance() .get(&DataKey::Config) - .expect("Config not set") + .expect("Config not set"); + Self::extend_instance(env); + config } fn is_paused(env: &Env) -> bool { - env.storage() + let paused = env + .storage() .instance() .get(&DataKey::Paused) - .unwrap_or(false) + .unwrap_or(false); + Self::extend_instance(env); + paused } fn require_not_paused(env: &Env) { @@ -105,6 +229,56 @@ impl PredifiContract { } } + /// Get the current state of a market. Defaults to Active if not explicitly stored. + fn get_market_state(env: &Env, pool_id: u64) -> MarketState { + let state_key = DataKey::PoolState(pool_id); + let state = env + .storage() + .persistent() + .get(&state_key) + .unwrap_or(MarketState::Active); + Self::extend_persistent(env, &state_key); + state + } + + /// Validate and set the market state with explicit state transition rules. + /// Returns InvalidStateTransition if transition is not allowed. + fn set_market_state( + env: &Env, + pool_id: u64, + new_state: MarketState, + ) -> Result<(), PredifiError> { + let current_state = Self::get_market_state(env, pool_id); + + // Validate state transition + let transition_valid = match (current_state, new_state) { + // Active can transition to Resolved or Canceled + (MarketState::Active, MarketState::Resolved) => true, + (MarketState::Active, MarketState::Canceled) => true, + // Terminal states cannot transition + (MarketState::Resolved, _) => false, + (MarketState::Canceled, _) => false, + // Same state is not allowed + (s1, s2) if s1 == s2 => false, + _ => false, + }; + + if !transition_valid { + return Err(PredifiError::InvalidStateTransition); + } + + let state_key = DataKey::PoolState(pool_id); + env.storage().persistent().set(&state_key, &new_state); + Self::extend_persistent(env, &state_key); + Ok(()) + } + + /// Check if market is in a terminal state (resolved or canceled). + fn is_market_closed(env: &Env, pool_id: u64) -> bool { + let state = Self::get_market_state(env, pool_id); + state == MarketState::Resolved || state == MarketState::Canceled + } + // ── Public interface ────────────────────────────────────────────────────── /// Initialize the contract. Idempotent — safe to call multiple times. @@ -112,11 +286,19 @@ impl PredifiContract { if !env.storage().instance().has(&DataKey::Config) { let config = Config { fee_bps, - treasury, - access_control, + treasury: treasury.clone(), + access_control: access_control.clone(), }; env.storage().instance().set(&DataKey::Config, &config); env.storage().instance().set(&DataKey::PoolIdCounter, &0u64); + Self::extend_instance(&env); + + InitEvent { + access_control, + treasury, + fee_bps, + } + .publish(&env); } } @@ -126,6 +308,9 @@ impl PredifiContract { Self::require_role(&env, &admin, 0) .unwrap_or_else(|_| panic!("Unauthorized: missing required role")); env.storage().instance().set(&DataKey::Paused, &true); + Self::extend_instance(&env); + + PauseEvent { admin }.publish(&env); } /// Unpause the contract. Only callable by Admin (role 0). @@ -134,6 +319,9 @@ impl PredifiContract { Self::require_role(&env, &admin, 0) .unwrap_or_else(|_| panic!("Unauthorized: missing required role")); env.storage().instance().set(&DataKey::Paused, &false); + Self::extend_instance(&env); + + UnpauseEvent { admin }.publish(&env); } /// Set fee in basis points. Caller must have Admin role (0). @@ -145,6 +333,9 @@ impl PredifiContract { let mut config = Self::get_config(&env); config.fee_bps = fee_bps; env.storage().instance().set(&DataKey::Config, &config); + Self::extend_instance(&env); + + FeeUpdateEvent { admin, fee_bps }.publish(&env); Ok(()) } @@ -154,37 +345,76 @@ impl PredifiContract { admin.require_auth(); Self::require_role(&env, &admin, 0)?; let mut config = Self::get_config(&env); - config.treasury = treasury; + config.treasury = treasury.clone(); env.storage().instance().set(&DataKey::Config, &config); + Self::extend_instance(&env); + + TreasuryUpdateEvent { admin, treasury }.publish(&env); Ok(()) } /// Create a new prediction pool. Returns the new pool ID. - pub fn create_pool(env: Env, end_time: u64, token: Address) -> u64 { + /// + /// # Arguments + /// * `end_time` - Unix timestamp after which no more predictions are accepted. + /// * `token` - The Stellar token contract address used for staking. + /// * `description` - Short human-readable description of the event (max 256 bytes). + /// * `metadata_url` - URL pointing to extended metadata, e.g. an IPFS link (max 512 bytes). + pub fn create_pool( + env: Env, + end_time: u64, + token: Address, + description: String, + metadata_url: String, + ) -> u64 { Self::require_not_paused(&env); assert!( end_time > env.ledger().timestamp(), "end_time must be in the future" ); + assert!(description.len() <= 256, "description exceeds 256 bytes"); + assert!(metadata_url.len() <= 512, "metadata_url exceeds 512 bytes"); let pool_id: u64 = env .storage() .instance() .get(&DataKey::PoolIdCounter) .unwrap_or(0); + Self::extend_instance(&env); let pool = Pool { end_time, - resolved: false, + state: MarketState::Active, outcome: 0, - token, + token: token.clone(), total_stake: 0, + description, + metadata_url: metadata_url.clone(), }; - env.storage().instance().set(&DataKey::Pool(pool_id), &pool); + let pool_key = DataKey::Pool(pool_id); + env.storage().persistent().set(&pool_key, &pool); + Self::extend_persistent(&env, &pool_key); + + // Initialize state as Active + let state_key = DataKey::PoolState(pool_id); + env.storage() + .persistent() + .set(&state_key, &MarketState::Active); + Self::extend_persistent(&env, &state_key); + env.storage() .instance() .set(&DataKey::PoolIdCounter, &(pool_id + 1)); + Self::extend_instance(&env); + + PoolCreatedEvent { + pool_id, + end_time, + token, + metadata_url, + } + .publish(&env); pool_id } @@ -200,98 +430,164 @@ impl PredifiContract { operator.require_auth(); Self::require_role(&env, &operator, 1)?; + let pool_key = DataKey::Pool(pool_id); let mut pool: Pool = env .storage() - .instance() - .get(&DataKey::Pool(pool_id)) + .persistent() + .get(&pool_key) .expect("Pool not found"); + Self::extend_persistent(&env, &pool_key); - assert!(!pool.resolved, "Pool already resolved"); + // Validate state transition: only Active -> Resolved is allowed + // This will return InvalidStateTransition if not allowed + Self::set_market_state(&env, pool_id, MarketState::Resolved)?; - pool.resolved = true; + // Update pool with resolved state and outcome + pool.state = MarketState::Resolved; pool.outcome = outcome; - env.storage().instance().set(&DataKey::Pool(pool_id), &pool); + // Atomically update pool storage + env.storage().persistent().set(&pool_key, &pool); + Self::extend_persistent(&env, &pool_key); + + PoolResolvedEvent { + pool_id, + operator, + outcome, + } + .publish(&env); + Ok(()) + } + + /// Cancel a pool. Caller must have Operator role (1). + /// This transitions the market from Active -> Canceled. + pub fn cancel_pool(env: Env, operator: Address, pool_id: u64) -> Result<(), PredifiError> { + Self::require_not_paused(&env); + operator.require_auth(); + Self::require_role(&env, &operator, 1)?; + + let pool_key = DataKey::Pool(pool_id); + let mut pool: Pool = env + .storage() + .persistent() + .get(&pool_key) + .expect("Pool not found"); + Self::extend_persistent(&env, &pool_key); + + // Validate state transition: only Active -> Canceled is allowed + // This will return InvalidStateTransition if not allowed + Self::set_market_state(&env, pool_id, MarketState::Canceled)?; + + // Update pool state + pool.state = MarketState::Canceled; + + // Atomically update pool storage + env.storage().persistent().set(&pool_key, &pool); + Self::extend_persistent(&env, &pool_key); + + PoolCanceledEvent { + pool_id, + operator, + } + .publish(&env); Ok(()) } /// Place a prediction on a pool. + #[allow(clippy::needless_borrows_for_generic_args)] pub fn place_prediction(env: Env, user: Address, pool_id: u64, amount: i128, outcome: u32) { Self::require_not_paused(&env); user.require_auth(); assert!(amount > 0, "amount must be positive"); + let pool_key = DataKey::Pool(pool_id); let mut pool: Pool = env .storage() - .instance() - .get(&DataKey::Pool(pool_id)) + .persistent() + .get(&pool_key) .expect("Pool not found"); + Self::extend_persistent(&env, &pool_key); + + // Guard: Ensure market is not in terminal state + if Self::is_market_closed(&env, pool_id) { + panic!("Cannot place prediction on a closed market"); + } - assert!(!pool.resolved, "Pool already resolved"); assert!(env.ledger().timestamp() < pool.end_time, "Pool has ended"); let token_client = token::Client::new(&env, &pool.token); - token_client.transfer(&user, env.current_contract_address(), &amount); + token_client.transfer(&user, &env.current_contract_address(), &amount); - env.storage().instance().set( - &DataKey::Prediction(user.clone(), pool_id), - &Prediction { amount, outcome }, - ); + let pred_key = DataKey::Prediction(user.clone(), pool_id); + env.storage() + .persistent() + .set(&pred_key, &Prediction { amount, outcome }); + Self::extend_persistent(&env, &pred_key); pool.total_stake = pool.total_stake.checked_add(amount).expect("overflow"); - env.storage().instance().set(&DataKey::Pool(pool_id), &pool); + env.storage().persistent().set(&pool_key, &pool); + Self::extend_persistent(&env, &pool_key); let outcome_key = DataKey::OutcomeStake(pool_id, outcome); - let current_stake: i128 = env.storage().instance().get(&outcome_key).unwrap_or(0); + let current_stake: i128 = env.storage().persistent().get(&outcome_key).unwrap_or(0); env.storage() - .instance() + .persistent() .set(&outcome_key, &(current_stake + amount)); + Self::extend_persistent(&env, &outcome_key); - let count: u32 = env - .storage() - .instance() - .get(&DataKey::UserPredictionCount(user.clone())) - .unwrap_or(0); - env.storage() - .instance() - .set(&DataKey::UserPredictionIndex(user.clone(), count), &pool_id); - env.storage() - .instance() - .set(&DataKey::UserPredictionCount(user.clone()), &(count + 1)); + let count_key = DataKey::UserPredictionCount(user.clone()); + let count: u32 = env.storage().persistent().get(&count_key).unwrap_or(0); + + let index_key = DataKey::UserPredictionIndex(user.clone(), count); + env.storage().persistent().set(&index_key, &pool_id); + Self::extend_persistent(&env, &index_key); + + env.storage().persistent().set(&count_key, &(count + 1)); + Self::extend_persistent(&env, &count_key); + + PredictionPlacedEvent { + pool_id, + user, + amount, + outcome, + } + .publish(&env); } /// Claim winnings from a resolved pool. Returns the amount paid out (0 for losers). + #[allow(clippy::needless_borrows_for_generic_args)] pub fn claim_winnings(env: Env, user: Address, pool_id: u64) -> Result { Self::require_not_paused(&env); user.require_auth(); + let pool_key = DataKey::Pool(pool_id); let pool: Pool = env .storage() - .instance() - .get(&DataKey::Pool(pool_id)) + .persistent() + .get(&pool_key) .expect("Pool not found"); + Self::extend_persistent(&env, &pool_key); - if !pool.resolved { + // Guard: Ensure pool is in Resolved state + if pool.state != MarketState::Resolved { return Err(PredifiError::PoolNotResolved); } - if env - .storage() - .instance() - .has(&DataKey::HasClaimed(user.clone(), pool_id)) - { + let claimed_key = DataKey::HasClaimed(user.clone(), pool_id); + if env.storage().persistent().has(&claimed_key) { return Err(PredifiError::AlreadyClaimed); } // Mark as claimed immediately to prevent re-entrancy - env.storage() - .instance() - .set(&DataKey::HasClaimed(user.clone(), pool_id), &true); + env.storage().persistent().set(&claimed_key, &true); + Self::extend_persistent(&env, &claimed_key); - let prediction: Option = env - .storage() - .instance() - .get(&DataKey::Prediction(user.clone(), pool_id)); + let pred_key = DataKey::Prediction(user.clone(), pool_id); + let prediction: Option = env.storage().persistent().get(&pred_key); + + if env.storage().persistent().has(&pred_key) { + Self::extend_persistent(&env, &pred_key); + } let prediction = match prediction { Some(p) => p, @@ -302,11 +598,11 @@ impl PredifiContract { return Ok(0); } - let winning_stake: i128 = env - .storage() - .instance() - .get(&DataKey::OutcomeStake(pool_id, pool.outcome)) - .unwrap_or(0); + let outcome_key = DataKey::OutcomeStake(pool_id, pool.outcome); + let winning_stake: i128 = env.storage().persistent().get(&outcome_key).unwrap_or(0); + if env.storage().persistent().has(&outcome_key) { + Self::extend_persistent(&env, &outcome_key); + } if winning_stake == 0 { return Ok(0); @@ -322,6 +618,13 @@ impl PredifiContract { let token_client = token::Client::new(&env, &pool.token); token_client.transfer(&env.current_contract_address(), &user, &winnings); + WinningsClaimedEvent { + pool_id, + user, + amount: winnings, + } + .publish(&env); + Ok(winnings) } @@ -332,11 +635,11 @@ impl PredifiContract { offset: u32, limit: u32, ) -> Vec { - let count: u32 = env - .storage() - .instance() - .get(&DataKey::UserPredictionCount(user.clone())) - .unwrap_or(0); + let count_key = DataKey::UserPredictionCount(user.clone()); + let count: u32 = env.storage().persistent().get(&count_key).unwrap_or(0); + if env.storage().persistent().has(&count_key) { + Self::extend_persistent(&env, &count_key); + } let mut results = Vec::new(&env); @@ -347,30 +650,36 @@ impl PredifiContract { let end = core::cmp::min(offset.saturating_add(limit), count); for i in offset..end { + let index_key = DataKey::UserPredictionIndex(user.clone(), i); let pool_id: u64 = env .storage() - .instance() - .get(&DataKey::UserPredictionIndex(user.clone(), i)) + .persistent() + .get(&index_key) .expect("index not found"); + Self::extend_persistent(&env, &index_key); + let pred_key = DataKey::Prediction(user.clone(), pool_id); let prediction: Prediction = env .storage() - .instance() - .get(&DataKey::Prediction(user.clone(), pool_id)) + .persistent() + .get(&pred_key) .expect("prediction not found"); + Self::extend_persistent(&env, &pred_key); + let pool_key = DataKey::Pool(pool_id); let pool: Pool = env .storage() - .instance() - .get(&DataKey::Pool(pool_id)) + .persistent() + .get(&pool_key) .expect("pool not found"); + Self::extend_persistent(&env, &pool_key); results.push_back(UserPredictionDetail { pool_id, amount: prediction.amount, user_outcome: prediction.outcome, pool_end_time: pool.end_time, - pool_resolved: pool.resolved, + pool_state: pool.state, pool_outcome: pool.outcome, }); } diff --git a/contract/contracts/predifi-contract/src/test.rs b/contract/contracts/predifi-contract/src/test.rs index 6d481c2..7def097 100644 --- a/contract/contracts/predifi-contract/src/test.rs +++ b/contract/contracts/predifi-contract/src/test.rs @@ -4,7 +4,7 @@ use super::*; use soroban_sdk::{ testutils::{Address as _, Ledger}, - token, Address, Env, + token, Address, Env, String, }; mod dummy_access_control { @@ -85,7 +85,15 @@ fn test_claim_winnings() { token_admin_client.mint(&user1, &1000); token_admin_client.mint(&user2, &1000); - let pool_id = client.create_pool(&100u64, &token_address); + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); client.place_prediction(&user1, &pool_id, &100, &1); client.place_prediction(&user2, &pool_id, &100, &2); @@ -115,7 +123,15 @@ fn test_double_claim() { let user1 = Address::generate(&env); token_admin_client.mint(&user1, &1000); - let pool_id = client.create_pool(&100u64, &token_address); + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); client.place_prediction(&user1, &pool_id, &100, &1); env.ledger().with_mut(|li| li.timestamp = 101); @@ -137,7 +153,15 @@ fn test_claim_unresolved() { let user1 = Address::generate(&env); token_admin_client.mint(&user1, &1000); - let pool_id = client.create_pool(&100u64, &token_address); + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); client.place_prediction(&user1, &pool_id, &100, &1); client.claim_winnings(&user1, &pool_id); @@ -155,8 +179,24 @@ fn test_multiple_pools_independent() { 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); + let pool_a = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + let pool_b = client.create_pool( + &200u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); client.place_prediction(&user1, &pool_a, &100, &1); client.place_prediction(&user2, &pool_b, &100, &1); @@ -203,7 +243,15 @@ 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 pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); let not_operator = Address::generate(&env); client.resolve_pool(¬_operator, &pool_id, &1u32); } @@ -341,7 +389,15 @@ fn test_paused_blocks_create_pool() { client.init(&ac_id, &treasury, &0u32); client.pause(&admin); - client.create_pool(&100u64, &token); + client.create_pool( + &100u64, + &token, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); } #[test] @@ -432,7 +488,15 @@ fn test_unpause_restores_functionality() { client.pause(&admin); client.unpause(&admin); - let pool_id = client.create_pool(&100u64, &token_contract); + let pool_id = client.create_pool( + &100u64, + &token_contract, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); client.place_prediction(&user, &pool_id, &10, &1); } @@ -448,9 +512,33 @@ fn test_get_user_predictions() { 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); + let pool0 = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + let pool1 = client.create_pool( + &200u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + let pool2 = client.create_pool( + &300u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); client.place_prediction(&user, &pool0, &10, &1); client.place_prediction(&user, &pool1, &20, &2); @@ -473,3 +561,262 @@ fn test_get_user_predictions() { let empty = client.get_user_predictions(&user, &3, &1); assert_eq!(empty.len(), 0); } + +// ── State Machine / State Transition Tests ─────────────────────────────────── + +#[test] +#[should_panic(expected = "Cannot place prediction on a closed market")] +fn test_cannot_predict_on_resolved_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, token_admin_client, _, operator) = setup(&env); + + let user1 = Address::generate(&env); + token_admin_client.mint(&user1, &1000); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + env.ledger().with_mut(|li| li.timestamp = 101); + client.resolve_pool(&operator, &pool_id, &1u32); + + // Should fail: cannot predict on resolved pool + client.place_prediction(&user1, &pool_id, &100, &1); +} + +#[test] +#[should_panic(expected = "Error(Contract, #70)")] +fn test_cannot_resolve_already_resolved_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, _, _, operator) = setup(&env); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + env.ledger().with_mut(|li| li.timestamp = 101); + client.resolve_pool(&operator, &pool_id, &1u32); + + // Should fail: cannot resolve already resolved pool (invalid state transition) + client.resolve_pool(&operator, &pool_id, &2u32); +} + +#[test] +#[should_panic(expected = "Cannot place prediction on a closed market")] +fn test_cannot_predict_on_canceled_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, token_admin_client, _, operator) = setup(&env); + + let user1 = Address::generate(&env); + token_admin_client.mint(&user1, &1000); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + env.ledger().with_mut(|li| li.timestamp = 101); + client.cancel_pool(&operator, &pool_id); + + // Should fail: cannot predict on canceled pool + client.place_prediction(&user1, &pool_id, &100, &1); +} + +#[test] +#[should_panic(expected = "Error(Contract, #70)")] +fn test_cannot_cancel_already_canceled_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, _, _, operator) = setup(&env); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + env.ledger().with_mut(|li| li.timestamp = 101); + client.cancel_pool(&operator, &pool_id); + + // Should fail: cannot cancel already canceled pool (invalid state transition) + client.cancel_pool(&operator, &pool_id); +} + +#[test] +#[should_panic(expected = "Error(Contract, #70)")] +fn test_cannot_resolve_canceled_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, _, _, operator) = setup(&env); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + env.ledger().with_mut(|li| li.timestamp = 101); + client.cancel_pool(&operator, &pool_id); + + // Should fail: cannot resolve canceled pool (invalid state transition) + client.resolve_pool(&operator, &pool_id, &1u32); +} + +#[test] +#[should_panic(expected = "Error(Contract, #70)")] +fn test_cannot_cancel_resolved_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, _, _, operator) = setup(&env); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + env.ledger().with_mut(|li| li.timestamp = 101); + client.resolve_pool(&operator, &pool_id, &1u32); + + // Should fail: cannot cancel resolved pool (invalid state transition) + client.cancel_pool(&operator, &pool_id); +} + +#[test] +fn test_valid_transition_active_to_resolved() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, _, _, operator) = setup(&env); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + env.ledger().with_mut(|li| li.timestamp = 101); + + // This should succeed: valid transition from Active to Resolved + client.resolve_pool(&operator, &pool_id, &1u32); +} + +#[test] +fn test_valid_transition_active_to_canceled() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, _, _, operator) = setup(&env); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + env.ledger().with_mut(|li| li.timestamp = 101); + + // This should succeed: valid transition from Active to Canceled + client.cancel_pool(&operator, &pool_id); +} + +#[test] +#[should_panic(expected = "Error(Contract, #10)")] +fn test_unauthorized_cancel_pool() { + let env = Env::default(); + env.mock_all_auths(); + + let (_, client, token_address, _, _, _, _) = setup(&env); + let not_operator = Address::generate(&env); + + let pool_id = client.create_pool( + &100u64, + &token_address, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + env.ledger().with_mut(|li| li.timestamp = 101); + client.cancel_pool(¬_operator, &pool_id); +} + +#[test] +fn test_get_user_predictions_includes_state() { + 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, + &String::from_str(&env, "Test Pool"), + &String::from_str( + &env, + "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ), + ); + + client.place_prediction(&user, &pool0, &10, &1); + + let predictions = client.get_user_predictions(&user, &0, &10); + assert_eq!(predictions.len(), 1); + + let detail = predictions.get(0).unwrap(); + assert_eq!(detail.pool_id, pool0); + // Verify the new state field exists and is correct + assert_eq!(detail.pool_state, MarketState::Active); +} + From 2a82ed3182309a4d7cf699386c75490624a9f6b4 Mon Sep 17 00:00:00 2001 From: dinahmaccodes Date: Sat, 21 Feb 2026 13:16:23 +0100 Subject: [PATCH 2/2] chore: update snapshots and integration tests for state machine changes --- contract/contracts/access-control/src/lib.rs | 201 ++-- contract/contracts/access-control/src/test.rs | 4 +- .../predifi-contract/src/integration_test.rs | 8 +- .../test/test_claim_unresolved.1.json | 508 +++++++--- .../test/test_claim_winnings.1.json | 947 +++++++++++++----- .../test/test_double_claim.1.json | 577 ++++++++--- .../test/test_resolution_window.1.json | 400 -------- docs/contract-reference.md | 478 +++++++++ docs/oracles.md | 243 +++++ docs/prediction-lifecycle.md | 292 ++++++ docs/quickstart.md | 199 ++++ docs/troubleshooting.md | 508 ++++++++++ 12 files changed, 3274 insertions(+), 1091 deletions(-) delete mode 100644 contract/contracts/predifi-contract/test_snapshots/test/test_resolution_window.1.json create mode 100644 docs/contract-reference.md create mode 100644 docs/oracles.md create mode 100644 docs/prediction-lifecycle.md create mode 100644 docs/quickstart.md create mode 100644 docs/troubleshooting.md diff --git a/contract/contracts/access-control/src/lib.rs b/contract/contracts/access-control/src/lib.rs index ab68cce..7e42530 100644 --- a/contract/contracts/access-control/src/lib.rs +++ b/contract/contracts/access-control/src/lib.rs @@ -1,6 +1,6 @@ #![no_std] use predifi_errors::PrediFiError; -use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; +use soroban_sdk::{contract, contractevent, contractimpl, contracttype, Address, Env}; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] @@ -12,31 +12,67 @@ pub enum Role { User = 4, } +#[contractevent(topics = ["admin_init"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AdminInitEvent { + pub admin: Address, +} + +#[contractevent(topics = ["role_assigned"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RoleAssignedEvent { + pub admin: Address, + pub user: Address, + pub role: Role, +} + +#[contractevent(topics = ["role_revoked"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RoleRevokedEvent { + pub admin: Address, + pub user: Address, + pub role: Role, +} + +#[contractevent(topics = ["role_transferred"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RoleTransferredEvent { + pub admin: Address, + pub from: Address, + pub to: Address, + pub role: Role, +} + +#[contractevent(topics = ["admin_transferred"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AdminTransferredEvent { + pub admin: Address, + pub new_admin: Address, +} + +#[contractevent(topics = ["all_roles_revoked"])] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AllRolesRevokedEvent { + pub admin: Address, + pub user: Address, +} + #[contracttype] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PoolStatus { - /// The pool is open for predictions. Active, - /// The event has occurred and the outcome is determined. Resolved, - /// The pool is closed for new predictions but not yet resolved. Closed, - /// The outcome is being disputed. Disputed, } #[contracttype] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PoolCategory { - /// Sports-related predictions. Sports, - /// Political predictions. Politics, - /// Financial predictions. Finance, - /// Entertainment predictions. Entertainment, - /// Other categories. Other, } @@ -53,31 +89,18 @@ pub struct AccessControl; #[contractimpl] impl AccessControl { - /// Initialize the contract with an initial admin address. - /// - /// # Arguments - /// * `admin` - The address to be appointed as the initial super admin. - /// - /// # Errors - /// * Panics with `"AlreadyInitializedOrConfigNotSet"` if the contract has already been initialized. pub fn init(env: Env, admin: Address) { if env.storage().instance().has(&DataKey::Admin) { soroban_sdk::panic_with_error!(&env, PrediFiError::AlreadyInitializedOrConfigNotSet); } env.storage().instance().set(&DataKey::Admin, &admin); - // Also grant the Admin role to the admin address. env.storage() .persistent() - .set(&DataKey::Role(admin, Role::Admin), &()); + .set(&DataKey::Role(admin.clone(), Role::Admin), &()); + + AdminInitEvent { admin }.publish(&env); } - /// Returns the current super admin address. - /// - /// # Returns - /// The address of the current super admin. - /// - /// # Errors - /// * Panics with `"NotInitialized"` if the contract hasn't been initialized yet. pub fn get_admin(env: Env) -> Address { env.storage() .instance() @@ -85,17 +108,6 @@ impl AccessControl { .expect("NotInitialized") } - /// Assigns a specific role to a user. - /// - /// Only the current super admin can call this function. - /// - /// # Arguments - /// * `admin_caller` - The address of the admin calling the function. - /// * `user` - The address to receive the role. - /// * `role` - The role to be assigned. - /// - /// # Errors - /// * `Unauthorized` - If the caller is not the super admin. pub fn assign_role( env: Env, admin_caller: Address, @@ -111,22 +123,17 @@ impl AccessControl { env.storage() .persistent() - .set(&DataKey::Role(user, role), &()); + .set(&DataKey::Role(user.clone(), role.clone()), &()); + + RoleAssignedEvent { + admin: admin_caller, + user, + role, + } + .publish(&env); Ok(()) } - /// Revokes a specific role from a user. - /// - /// Only the current super admin can call this function. - /// - /// # Arguments - /// * `admin_caller` - The address of the admin calling the function. - /// * `user` - The address from which the role will be revoked. - /// * `role` - The role to be revoked. - /// - /// # Errors - /// * `Unauthorized` - If the caller is not the super admin. - /// * `InsufficientPermissions` - If the user doesn't have the specified role. pub fn revoke_role( env: Env, admin_caller: Address, @@ -150,35 +157,21 @@ impl AccessControl { env.storage() .persistent() - .remove(&DataKey::Role(user, role)); + .remove(&DataKey::Role(user.clone(), role.clone())); + + RoleRevokedEvent { + admin: admin_caller, + user, + role, + } + .publish(&env); Ok(()) } - /// Checks if a user has a specific role. - /// - /// # Arguments - /// * `user` - The address to check. - /// * `role` - The role to check for. - /// - /// # Returns - /// `true` if the user has the role, `false` otherwise. pub fn has_role(env: Env, user: Address, role: Role) -> bool { env.storage().persistent().has(&DataKey::Role(user, role)) } - /// Transfers a role from one address to another. - /// - /// Only the current super admin can call this function. - /// - /// # Arguments - /// * `admin_caller` - The address of the admin calling the function. - /// * `from` - The address currently holding the role. - /// * `to` - The address to receive the role. - /// * `role` - The role to be transferred. - /// - /// # Errors - /// * `Unauthorized` - If the caller is not the super admin. - /// * `InsufficientPermissions` - If the `from` address doesn't have the specified role. pub fn transfer_role( env: Env, admin_caller: Address, @@ -203,23 +196,21 @@ impl AccessControl { env.storage() .persistent() - .remove(&DataKey::Role(from, role.clone())); + .remove(&DataKey::Role(from.clone(), role.clone())); env.storage() .persistent() - .set(&DataKey::Role(to, role), &()); + .set(&DataKey::Role(to.clone(), role.clone()), &()); + + RoleTransferredEvent { + admin: admin_caller, + from, + to, + role, + } + .publish(&env); Ok(()) } - /// Transfers the super admin status to a new address. - /// - /// Only the current super admin can call this function. - /// - /// # Arguments - /// * `admin_caller` - The address of the current admin. - /// * `new_admin` - The address to become the new super admin. - /// - /// # Errors - /// * `Unauthorized` - If the caller is not the current super admin. pub fn transfer_admin( env: Env, admin_caller: Address, @@ -232,27 +223,24 @@ impl AccessControl { return Err(PrediFiError::Unauthorized); } - // Update the admin address. env.storage().instance().set(&DataKey::Admin, &new_admin); - // Transfer the Admin role record. env.storage() .persistent() .remove(&DataKey::Role(current_admin, Role::Admin)); env.storage() .persistent() - .set(&DataKey::Role(new_admin, Role::Admin), &()); + .set(&DataKey::Role(new_admin.clone(), Role::Admin), &()); + + AdminTransferredEvent { + admin: admin_caller, + new_admin, + } + .publish(&env); Ok(()) } - /// Checks if a user is the current super admin. - /// - /// # Arguments - /// * `user` - The address to check. - /// - /// # Returns - /// `true` if the user is the current super admin, `false` otherwise. pub fn is_admin(env: Env, user: Address) -> bool { let stored: Option
= env.storage().instance().get(&DataKey::Admin); match stored { @@ -261,16 +249,6 @@ impl AccessControl { } } - /// Revokes all roles from a user. - /// - /// Only the current super admin can call this function. - /// - /// # Arguments - /// * `admin_caller` - The address of the admin calling the function. - /// * `user` - The address from which all roles will be revoked. - /// - /// # Errors - /// * `Unauthorized` - If the caller is not the super admin. pub fn revoke_all_roles( env: Env, admin_caller: Address, @@ -283,7 +261,6 @@ impl AccessControl { return Err(PrediFiError::Unauthorized); } - // Revoke all possible roles. for role in [ Role::Admin, Role::Operator, @@ -299,17 +276,15 @@ impl AccessControl { } } + AllRolesRevokedEvent { + admin: admin_caller, + user, + } + .publish(&env); + Ok(()) } - /// Checks if a user has any of the specified roles. - /// - /// # Arguments - /// * `user` - The address to check. - /// * `roles` - A vector of roles to check. - /// - /// # Returns - /// `true` if the user has at least one of the specified roles, `false` otherwise. pub fn has_any_role(env: Env, user: Address, roles: soroban_sdk::Vec) -> bool { for role in roles.iter() { if env diff --git a/contract/contracts/access-control/src/test.rs b/contract/contracts/access-control/src/test.rs index 8bc8b48..f2fa13e 100644 --- a/contract/contracts/access-control/src/test.rs +++ b/contract/contracts/access-control/src/test.rs @@ -123,7 +123,7 @@ fn test_unauthorized_assignment() { // non_admin tries to assign a role let result = client.try_assign_role(&non_admin, &user, &Role::Operator); - assert_eq!(result, Err(Ok(PrediFiError::Unauthorized.into()))); + assert_eq!(result, Err(Ok(PrediFiError::Unauthorized))); } #[test] fn test_is_admin() { @@ -191,7 +191,7 @@ fn test_revoke_all_roles_unauthorized() { // Non-admin tries to revoke all roles let result = client.try_revoke_all_roles(&non_admin, &user); - assert_eq!(result, Err(Ok(PrediFiError::Unauthorized.into()))); + assert_eq!(result, Err(Ok(PrediFiError::Unauthorized))); } #[test] diff --git a/contract/contracts/predifi-contract/src/integration_test.rs b/contract/contracts/predifi-contract/src/integration_test.rs index 4eef2bf..b8d51f8 100644 --- a/contract/contracts/predifi-contract/src/integration_test.rs +++ b/contract/contracts/predifi-contract/src/integration_test.rs @@ -4,7 +4,7 @@ use super::*; use crate::test_utils::TokenTestContext; use soroban_sdk::{ testutils::{Address as _, Ledger}, - Address, Env, + Address, Env, String, }; mod dummy_access_control { @@ -74,7 +74,7 @@ fn test_full_market_lifecycle() { // 1. Create Pool let end_time = 1000u64; - let pool_id = client.create_pool(&end_time, &token_ctx.token_address); + let pool_id = client.create_pool(&end_time, &token_ctx.token_address, &String::from_str(&env, "Test Pool"), &String::from_str(&env, "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")); // 2. Place Predictions client.place_prediction(&user1, &pool_id, &100, &1); // User 1 bets 100 on Outcome 1 @@ -131,7 +131,7 @@ fn test_multi_user_betting_and_balance_verification() { token_ctx.mint(&user, 5000); } - let pool_id = client.create_pool(&2000u64, &token_ctx.token_address); + let pool_id = client.create_pool(&2000u64, &token_ctx.token_address, &String::from_str(&env, "Test Pool"), &String::from_str(&env, "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")); // Bets: // U0: 500 on 1 @@ -182,7 +182,7 @@ fn test_market_resolution_multiple_winners() { token_ctx.mint(&user2, 1000); token_ctx.mint(&user3, 1000); - let pool_id = client.create_pool(&1500u64, &token_ctx.token_address); + let pool_id = client.create_pool(&1500u64, &token_ctx.token_address, &String::from_str(&env, "Test Pool"), &String::from_str(&env, "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi")); // Bets: // U1: 200 on 1 diff --git a/contract/contracts/predifi-contract/test_snapshots/test/test_claim_unresolved.1.json b/contract/contracts/predifi-contract/test_snapshots/test/test_claim_unresolved.1.json index 9f0e337..5a55919 100644 --- a/contract/contracts/predifi-contract/test_snapshots/test/test_claim_unresolved.1.json +++ b/contract/contracts/predifi-contract/test_snapshots/test/test_claim_unresolved.1.json @@ -224,6 +224,368 @@ 4095 ] ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "OutcomeStake" + }, + { + "u64": "0" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "OutcomeStake" + }, + { + "u64": "0" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "i128": "100" + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Pool" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Pool" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "description" + }, + "val": { + "string": "Test Pool" + } + }, + { + "key": { + "symbol": "end_time" + }, + "val": { + "u64": "100" + } + }, + { + "key": { + "symbol": "metadata_url" + }, + "val": { + "string": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" + } + }, + { + "key": { + "symbol": "outcome" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "token" + }, + "val": { + "address": "CCABDO7UZXYE4W6GVSEGSNNZTKSLFQGKXXQTH6OX7M7GKZ4Z6CUJNGZN" + } + }, + { + "key": { + "symbol": "total_stake" + }, + "val": { + "i128": "100" + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PoolState" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PoolState" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 0 + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Prediction" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Prediction" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": "100" + } + }, + { + "key": { + "symbol": "outcome" + }, + "val": { + "u32": 1 + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionCount" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionCount" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionIndex" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionIndex" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "u64": "0" + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], [ { "contract_data": { @@ -284,80 +646,6 @@ ] } }, - { - "key": { - "vec": [ - { - "symbol": "OutcomeStake" - }, - { - "u64": "0" - }, - { - "u32": 1 - } - ] - }, - "val": { - "i128": "100" - } - }, - { - "key": { - "vec": [ - { - "symbol": "Pool" - }, - { - "u64": "0" - } - ] - }, - "val": { - "map": [ - { - "key": { - "symbol": "end_time" - }, - "val": { - "u64": "100" - } - }, - { - "key": { - "symbol": "outcome" - }, - "val": { - "u32": 0 - } - }, - { - "key": { - "symbol": "resolved" - }, - "val": { - "bool": false - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CCABDO7UZXYE4W6GVSEGSNNZTKSLFQGKXXQTH6OX7M7GKZ4Z6CUJNGZN" - } - }, - { - "key": { - "symbol": "total_stake" - }, - "val": { - "i128": "100" - } - } - ] - } - }, { "key": { "vec": [ @@ -369,74 +657,6 @@ "val": { "u64": "1" } - }, - { - "key": { - "vec": [ - { - "symbol": "Prediction" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" - }, - { - "u64": "0" - } - ] - }, - "val": { - "map": [ - { - "key": { - "symbol": "amount" - }, - "val": { - "i128": "100" - } - }, - { - "key": { - "symbol": "outcome" - }, - "val": { - "u32": 1 - } - } - ] - } - }, - { - "key": { - "vec": [ - { - "symbol": "UserPredictionCount" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" - } - ] - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "vec": [ - { - "symbol": "UserPredictionIndex" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" - }, - { - "u32": 0 - } - ] - }, - "val": { - "u64": "0" - } } ] } @@ -445,7 +665,7 @@ }, "ext": "v0" }, - 4095 + 518400 ] ], [ @@ -784,7 +1004,7 @@ }, "ext": "v0" }, - 4095 + 518400 ] ] ] 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 005a614..909f2ba 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 @@ -366,6 +366,685 @@ 4095 ] ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "HasClaimed" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "HasClaimed" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "HasClaimed" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "HasClaimed" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "OutcomeStake" + }, + { + "u64": "0" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "OutcomeStake" + }, + { + "u64": "0" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "i128": "100" + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "OutcomeStake" + }, + { + "u64": "0" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "OutcomeStake" + }, + { + "u64": "0" + }, + { + "u32": 2 + } + ] + }, + "durability": "persistent", + "val": { + "i128": "100" + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Pool" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Pool" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "description" + }, + "val": { + "string": "Test Pool" + } + }, + { + "key": { + "symbol": "end_time" + }, + "val": { + "u64": "100" + } + }, + { + "key": { + "symbol": "metadata_url" + }, + "val": { + "string": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" + } + }, + { + "key": { + "symbol": "outcome" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "token" + }, + "val": { + "address": "CCABDO7UZXYE4W6GVSEGSNNZTKSLFQGKXXQTH6OX7M7GKZ4Z6CUJNGZN" + } + }, + { + "key": { + "symbol": "total_stake" + }, + "val": { + "i128": "200" + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PoolState" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PoolState" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Prediction" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Prediction" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": "100" + } + }, + { + "key": { + "symbol": "outcome" + }, + "val": { + "u32": 1 + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Prediction" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Prediction" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": "100" + } + }, + { + "key": { + "symbol": "outcome" + }, + "val": { + "u32": 2 + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionCount" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionCount" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionCount" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionCount" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionIndex" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionIndex" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "u64": "0" + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionIndex" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionIndex" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "u64": "0" + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], [ { "contract_data": { @@ -426,134 +1105,6 @@ ] } }, - { - "key": { - "vec": [ - { - "symbol": "HasClaimed" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" - }, - { - "u64": "0" - } - ] - }, - "val": { - "bool": true - } - }, - { - "key": { - "vec": [ - { - "symbol": "HasClaimed" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" - }, - { - "u64": "0" - } - ] - }, - "val": { - "bool": true - } - }, - { - "key": { - "vec": [ - { - "symbol": "OutcomeStake" - }, - { - "u64": "0" - }, - { - "u32": 1 - } - ] - }, - "val": { - "i128": "100" - } - }, - { - "key": { - "vec": [ - { - "symbol": "OutcomeStake" - }, - { - "u64": "0" - }, - { - "u32": 2 - } - ] - }, - "val": { - "i128": "100" - } - }, - { - "key": { - "vec": [ - { - "symbol": "Pool" - }, - { - "u64": "0" - } - ] - }, - "val": { - "map": [ - { - "key": { - "symbol": "end_time" - }, - "val": { - "u64": "100" - } - }, - { - "key": { - "symbol": "outcome" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "resolved" - }, - "val": { - "bool": true - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CCABDO7UZXYE4W6GVSEGSNNZTKSLFQGKXXQTH6OX7M7GKZ4Z6CUJNGZN" - } - }, - { - "key": { - "symbol": "total_stake" - }, - "val": { - "i128": "200" - } - } - ] - } - }, { "key": { "vec": [ @@ -565,142 +1116,6 @@ "val": { "u64": "1" } - }, - { - "key": { - "vec": [ - { - "symbol": "Prediction" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" - }, - { - "u64": "0" - } - ] - }, - "val": { - "map": [ - { - "key": { - "symbol": "amount" - }, - "val": { - "i128": "100" - } - }, - { - "key": { - "symbol": "outcome" - }, - "val": { - "u32": 1 - } - } - ] - } - }, - { - "key": { - "vec": [ - { - "symbol": "Prediction" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" - }, - { - "u64": "0" - } - ] - }, - "val": { - "map": [ - { - "key": { - "symbol": "amount" - }, - "val": { - "i128": "100" - } - }, - { - "key": { - "symbol": "outcome" - }, - "val": { - "u32": 2 - } - } - ] - } - }, - { - "key": { - "vec": [ - { - "symbol": "UserPredictionCount" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" - } - ] - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "vec": [ - { - "symbol": "UserPredictionCount" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" - } - ] - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "vec": [ - { - "symbol": "UserPredictionIndex" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" - }, - { - "u32": 0 - } - ] - }, - "val": { - "u64": "0" - } - }, - { - "key": { - "vec": [ - { - "symbol": "UserPredictionIndex" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARQG5" - }, - { - "u32": 0 - } - ] - }, - "val": { - "u64": "0" - } } ] } @@ -709,7 +1124,7 @@ }, "ext": "v0" }, - 4095 + 518400 ] ], [ @@ -1283,7 +1698,7 @@ }, "ext": "v0" }, - 4095 + 518400 ] ] ] diff --git a/contract/contracts/predifi-contract/test_snapshots/test/test_double_claim.1.json b/contract/contracts/predifi-contract/test_snapshots/test/test_double_claim.1.json index 7fb65f6..16e33f7 100644 --- a/contract/contracts/predifi-contract/test_snapshots/test/test_double_claim.1.json +++ b/contract/contracts/predifi-contract/test_snapshots/test/test_double_claim.1.json @@ -271,6 +271,419 @@ 4095 ] ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "HasClaimed" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "HasClaimed" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "bool": true + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "OutcomeStake" + }, + { + "u64": "0" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "OutcomeStake" + }, + { + "u64": "0" + }, + { + "u32": 1 + } + ] + }, + "durability": "persistent", + "val": { + "i128": "100" + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Pool" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Pool" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "description" + }, + "val": { + "string": "Test Pool" + } + }, + { + "key": { + "symbol": "end_time" + }, + "val": { + "u64": "100" + } + }, + { + "key": { + "symbol": "metadata_url" + }, + "val": { + "string": "ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi" + } + }, + { + "key": { + "symbol": "outcome" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "state" + }, + "val": { + "u32": 1 + } + }, + { + "key": { + "symbol": "token" + }, + "val": { + "address": "CCABDO7UZXYE4W6GVSEGSNNZTKSLFQGKXXQTH6OX7M7GKZ4Z6CUJNGZN" + } + }, + { + "key": { + "symbol": "total_stake" + }, + "val": { + "i128": "100" + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PoolState" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "PoolState" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Prediction" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "Prediction" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u64": "0" + } + ] + }, + "durability": "persistent", + "val": { + "map": [ + { + "key": { + "symbol": "amount" + }, + "val": { + "i128": "100" + } + }, + { + "key": { + "symbol": "outcome" + }, + "val": { + "u32": 1 + } + } + ] + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionCount" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionCount" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionIndex" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "key": { + "vec": [ + { + "symbol": "UserPredictionIndex" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" + }, + { + "u32": 0 + } + ] + }, + "durability": "persistent", + "val": { + "u64": "0" + } + } + }, + "ext": "v0" + }, + 518400 + ] + ], [ { "contract_data": { @@ -331,98 +744,6 @@ ] } }, - { - "key": { - "vec": [ - { - "symbol": "HasClaimed" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" - }, - { - "u64": "0" - } - ] - }, - "val": { - "bool": true - } - }, - { - "key": { - "vec": [ - { - "symbol": "OutcomeStake" - }, - { - "u64": "0" - }, - { - "u32": 1 - } - ] - }, - "val": { - "i128": "100" - } - }, - { - "key": { - "vec": [ - { - "symbol": "Pool" - }, - { - "u64": "0" - } - ] - }, - "val": { - "map": [ - { - "key": { - "symbol": "end_time" - }, - "val": { - "u64": "100" - } - }, - { - "key": { - "symbol": "outcome" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "resolved" - }, - "val": { - "bool": true - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CCABDO7UZXYE4W6GVSEGSNNZTKSLFQGKXXQTH6OX7M7GKZ4Z6CUJNGZN" - } - }, - { - "key": { - "symbol": "total_stake" - }, - "val": { - "i128": "100" - } - } - ] - } - }, { "key": { "vec": [ @@ -434,74 +755,6 @@ "val": { "u64": "1" } - }, - { - "key": { - "vec": [ - { - "symbol": "Prediction" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" - }, - { - "u64": "0" - } - ] - }, - "val": { - "map": [ - { - "key": { - "symbol": "amount" - }, - "val": { - "i128": "100" - } - }, - { - "key": { - "symbol": "outcome" - }, - "val": { - "u32": 1 - } - } - ] - } - }, - { - "key": { - "vec": [ - { - "symbol": "UserPredictionCount" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" - } - ] - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "vec": [ - { - "symbol": "UserPredictionIndex" - }, - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOLZM" - }, - { - "u32": 0 - } - ] - }, - "val": { - "u64": "0" - } } ] } @@ -510,7 +763,7 @@ }, "ext": "v0" }, - 4095 + 518400 ] ], [ @@ -915,7 +1168,7 @@ }, "ext": "v0" }, - 4095 + 518400 ] ] ] diff --git a/contract/contracts/predifi-contract/test_snapshots/test/test_resolution_window.1.json b/contract/contracts/predifi-contract/test_snapshots/test/test_resolution_window.1.json deleted file mode 100644 index b1e5278..0000000 --- a/contract/contracts/predifi-contract/test_snapshots/test/test_resolution_window.1.json +++ /dev/null @@ -1,400 +0,0 @@ -{ - "generators": { - "address": 3, - "nonce": 0, - "mux_id": 0 - }, - "auth": [ - [], - [ - [ - "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", - { - "function": { - "contract_fn": { - "contract_address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", - "function_name": "set_admin", - "args": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ] - } - }, - "sub_invocations": [] - } - ] - ], - [], - [], - [], - [], - [], - [], - [] - ], - "ledger": { - "protocol_version": 23, - "sequence_number": 0, - "timestamp": 605800, - "network_id": "0000000000000000000000000000000000000000000000000000000000000000", - "base_reserve": 0, - "min_persistent_entry_ttl": 4096, - "min_temp_entry_ttl": 16, - "max_entry_ttl": 6312000, - "ledger_entries": [ - [ - { - "account": { - "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "account": { - "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", - "balance": "0", - "seq_num": "0", - "num_sub_entries": 0, - "inflation_dest": null, - "flags": 0, - "home_domain": "", - "thresholds": "01010101", - "signers": [], - "ext": "v0" - } - }, - "ext": "v0" - }, - null - ] - ], - [ - { - "contract_data": { - "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", - "key": { - "ledger_key_nonce": { - "nonce": "801925984706572462" - } - }, - "durability": "temporary" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V", - "key": { - "ledger_key_nonce": { - "nonce": "801925984706572462" - } - }, - "durability": "temporary", - "val": "void" - } - }, - "ext": "v0" - }, - 6311999 - ] - ], - [ - { - "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": "ledger_key_contract_instance", - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", - "key": "ledger_key_contract_instance", - "durability": "persistent", - "val": { - "contract_instance": { - "executable": { - "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - }, - "storage": [ - { - "key": { - "vec": [ - { - "symbol": "Pool" - }, - { - "u64": "0" - } - ] - }, - "val": { - "map": [ - { - "key": { - "symbol": "end_time" - }, - "val": { - "u64": "1000" - } - }, - { - "key": { - "symbol": "outcome" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "resolved" - }, - "val": { - "bool": true - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" - } - }, - { - "key": { - "symbol": "total_stake" - }, - "val": { - "i128": "0" - } - } - ] - } - }, - { - "key": { - "vec": [ - { - "symbol": "Pool" - }, - { - "u64": "1" - } - ] - }, - "val": { - "map": [ - { - "key": { - "symbol": "end_time" - }, - "val": { - "u64": "1000" - } - }, - { - "key": { - "symbol": "outcome" - }, - "val": { - "u32": 1 - } - }, - { - "key": { - "symbol": "resolved" - }, - "val": { - "bool": true - } - }, - { - "key": { - "symbol": "token" - }, - "val": { - "address": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF" - } - }, - { - "key": { - "symbol": "total_stake" - }, - "val": { - "i128": "0" - } - } - ] - } - }, - { - "key": { - "vec": [ - { - "symbol": "PoolIdCounter" - } - ] - }, - "val": { - "u64": "2" - } - } - ] - } - } - } - }, - "ext": "v0" - }, - 4095 - ] - ], - [ - { - "contract_data": { - "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", - "key": "ledger_key_contract_instance", - "durability": "persistent" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_data": { - "ext": "v0", - "contract": "CBUSYNQKASUYFWYC3M2GUEDMX4AIVWPALDBYJPNK6554BREHTGZ2IUNF", - "key": "ledger_key_contract_instance", - "durability": "persistent", - "val": { - "contract_instance": { - "executable": "stellar_asset", - "storage": [ - { - "key": { - "symbol": "METADATA" - }, - "val": { - "map": [ - { - "key": { - "symbol": "decimal" - }, - "val": { - "u32": 7 - } - }, - { - "key": { - "symbol": "name" - }, - "val": { - "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGO6V" - } - }, - { - "key": { - "symbol": "symbol" - }, - "val": { - "string": "aaa" - } - } - ] - } - }, - { - "key": { - "vec": [ - { - "symbol": "Admin" - } - ] - }, - "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - }, - { - "key": { - "vec": [ - { - "symbol": "AssetInfo" - } - ] - }, - "val": { - "vec": [ - { - "symbol": "AlphaNum4" - }, - { - "map": [ - { - "key": { - "symbol": "asset_code" - }, - "val": { - "string": "aaa\\0" - } - }, - { - "key": { - "symbol": "issuer" - }, - "val": { - "bytes": "0000000000000000000000000000000000000000000000000000000000000003" - } - } - ] - } - ] - } - } - ] - } - } - } - }, - "ext": "v0" - }, - 120960 - ] - ], - [ - { - "contract_code": { - "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - } - }, - [ - { - "last_modified_ledger_seq": 0, - "data": { - "contract_code": { - "ext": "v0", - "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "code": "" - } - }, - "ext": "v0" - }, - 4095 - ] - ] - ] - }, - "events": [] -} \ No newline at end of file diff --git a/docs/contract-reference.md b/docs/contract-reference.md new file mode 100644 index 0000000..ef960dd --- /dev/null +++ b/docs/contract-reference.md @@ -0,0 +1,478 @@ +# Contract Reference + +Complete reference for all PrediFi contract methods, events, and data structures. + +## Core Functions + +### `init` + +Initialize the contract with configuration parameters. + +```rust +pub fn init( + env: Env, + access_control: Address, + treasury: Address, + fee_bps: u32 +) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `access_control` | `Address` | Access control contract address | +| `treasury` | `Address` | Treasury address for fee collection | +| `fee_bps` | `u32` | Fee in basis points (max 10000 = 100%) | + +**Returns:** None + +**Events:** `InitEvent` + +**Notes:** +- Idempotent - safe to call multiple times +- Only sets config if not already initialized + +--- + +### `create_pool` + +Create a new prediction market pool. + +```rust +pub fn create_pool( + env: Env, + end_time: u64, + token: Address, + description: String, + metadata_url: String +) -> u64 +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `end_time` | `u64` | Unix timestamp after which predictions close | +| `token` | `Address` | Stellar token contract for staking | +| `description` | `String` | Event description (max 256 bytes) | +| `metadata_url` | `String` | Extended metadata URL (max 512 bytes) | + +**Returns:** `u64` - New pool ID + +**Events:** `PoolCreatedEvent` + +**Validations:** +- `end_time` must be in the future +- `description` length ≤ 256 bytes +- `metadata_url` length ≤ 512 bytes + +**Example:** + +```rust +let pool_id = contract.create_pool( + env, + 1735689600, // Dec 31, 2024 + token_address, + String::from_str(&env, "Will BTC hit $100k?"), + String::from_str(&env, "ipfs://QmXxx...") +); +``` + +--- + +### `place_prediction` + +Place a prediction on an active pool. + +```rust +pub fn place_prediction( + env: Env, + user: Address, + pool_id: u64, + amount: i128, + outcome: u32 +) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `user` | `Address` | User placing the prediction | +| `pool_id` | `u64` | Pool to predict on | +| `amount` | `i128` | Prediction amount (in token's smallest unit) | +| `outcome` | `u32` | Outcome index (0, 1, 2, etc.) | + +**Returns:** None + +**Events:** `PredictionPlacedEvent` + +**Validations:** +- Pool must exist +- Pool must not be resolved +- Current time < pool.end_time +- Amount > 0 +- User must have sufficient token balance + +**Token Transfer:** +- Transfers `amount` tokens from user to contract + +**Example:** + +```rust +contract.place_prediction( + env, + user_address, + pool_id, + 1000000000, // 100 tokens + 1 // Outcome: "Yes" +); +``` + +--- + +### `resolve_pool` + +Resolve a pool with the winning outcome. Requires Operator role (1). + +```rust +pub fn resolve_pool( + env: Env, + operator: Address, + pool_id: u64, + outcome: u32 +) -> Result<(), PredifiError> +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `operator` | `Address` | Operator address (must have role 1) | +| `pool_id` | `u64` | Pool to resolve | +| `outcome` | `u32` | Winning outcome index | + +**Returns:** `Result<(), PredifiError>` + +**Events:** `PoolResolvedEvent` + +**Validations:** +- Operator must have role 1 +- Pool must exist +- Pool must not already be resolved + +**Errors:** +- `Unauthorized` - Operator lacks required role +- `PoolNotFound` - Pool doesn't exist +- `PoolAlreadyResolved` - Pool already resolved + +**Example:** + +```rust +contract.resolve_pool( + env, + operator_address, + pool_id, + 1 // Winning outcome +)?; +``` + +--- + +### `claim_winnings` + +Claim winnings from a resolved pool. + +```rust +pub fn claim_winnings( + env: Env, + user: Address, + pool_id: u64 +) -> Result +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `user` | `Address` | User claiming winnings | +| `pool_id` | `u64` | Pool to claim from | + +**Returns:** `Result` - Amount claimed (0 if didn't win) + +**Events:** `WinningsClaimedEvent` + +**Validations:** +- Pool must be resolved +- User must not have already claimed +- User must have placed a prediction + +**Reward Calculation:** + +``` +winnings = (user_stake / winning_outcome_total_stake) × total_pool_stake +``` + +**Errors:** +- `PoolNotResolved` - Pool not yet resolved +- `AlreadyClaimed` - User already claimed winnings + +**Example:** + +```rust +let winnings = contract.claim_winnings( + env, + user_address, + pool_id +)?; + +if winnings > 0 { + // User won and received winnings +} +``` + +--- + +### `get_user_predictions` + +Get a paginated list of a user's predictions. + +```rust +pub fn get_user_predictions( + env: Env, + user: Address, + offset: u32, + limit: u32 +) -> Vec +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `user` | `Address` | User address | +| `offset` | `u32` | Pagination offset | +| `limit` | `u32` | Maximum results to return | + +**Returns:** `Vec` + +**Example:** + +```rust +let predictions = contract.get_user_predictions( + env, + user_address, + 0, // offset + 10 // limit +); +``` + +--- + +## Admin Functions + +### `pause` + +Pause all contract operations. Requires Admin role (0). + +```rust +pub fn pause(env: Env, admin: Address) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `admin` | `Address` | Admin address (must have role 0) | + +**Events:** `PauseEvent` + +--- + +### `unpause` + +Resume contract operations. Requires Admin role (0). + +```rust +pub fn unpause(env: Env, admin: Address) +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `admin` | `Address` | Admin address (must have role 0) | + +**Events:** `UnpauseEvent` + +--- + +### `set_fee_bps` + +Update protocol fee. Requires Admin role (0). + +```rust +pub fn set_fee_bps( + env: Env, + admin: Address, + fee_bps: u32 +) -> Result<(), PredifiError> +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `admin` | `Address` | Admin address | +| `fee_bps` | `u32` | New fee in basis points (max 10000) | + +**Events:** `FeeUpdateEvent` + +--- + +### `set_treasury` + +Update treasury address. Requires Admin role (0). + +```rust +pub fn set_treasury( + env: Env, + admin: Address, + treasury: Address +) -> Result<(), PredifiError> +``` + +**Parameters:** + +| Parameter | Type | Description | +|-----------|------|-------------| +| `admin` | `Address` | Admin address | +| `treasury` | `Address` | New treasury address | + +**Events:** `TreasuryUpdateEvent` + +--- + +## Data Structures + +### `Pool` + +```rust +pub struct Pool { + pub end_time: u64, + pub resolved: bool, + pub outcome: u32, + pub token: Address, + pub total_stake: i128, + pub description: String, + pub metadata_url: String, +} +``` + +### `Prediction` + +```rust +pub struct Prediction { + pub amount: i128, + pub outcome: u32, +} +``` + +### `UserPredictionDetail` + +```rust +pub struct UserPredictionDetail { + pub pool_id: u64, + pub amount: i128, + pub user_outcome: u32, + pub pool_end_time: u64, + pub pool_resolved: bool, + pub pool_outcome: u32, +} +``` + +### `Config` + +```rust +pub struct Config { + pub fee_bps: u32, + pub treasury: Address, + pub access_control: Address, +} +``` + +--- + +## Events + +### `PoolCreatedEvent` + +Emitted when a new pool is created. + +```rust +pub struct PoolCreatedEvent { + pub pool_id: u64, + pub end_time: u64, + pub token: Address, + pub metadata_url: String, +} +``` + +### `PredictionPlacedEvent` + +Emitted when a user places a prediction. + +```rust +pub struct PredictionPlacedEvent { + pub pool_id: u64, + pub user: Address, + pub amount: i128, + pub outcome: u32, +} +``` + +### `PoolResolvedEvent` + +Emitted when a pool is resolved. + +```rust +pub struct PoolResolvedEvent { + pub pool_id: u64, + pub operator: Address, + pub outcome: u32, +} +``` + +### `WinningsClaimedEvent` + +Emitted when a user claims winnings. + +```rust +pub struct WinningsClaimedEvent { + pub pool_id: u64, + pub user: Address, + pub amount: i128, +} +``` + +--- + +## Error Codes + +See [Troubleshooting](./troubleshooting.md) for complete error reference. + +| Code | Error | Description | +|------|-------|-------------| +| 10 | `Unauthorized` | Caller lacks required role | +| 22 | `PoolNotResolved` | Pool not yet resolved | +| 60 | `AlreadyClaimed` | User already claimed winnings | + +--- + +## Next Steps + +- Start with [Quickstart](./quickstart.md) +- Understand [Prediction Lifecycle](./prediction-lifecycle.md) +- Learn about [Oracles](./oracles.md) +- Review [Troubleshooting](./troubleshooting.md) diff --git a/docs/oracles.md b/docs/oracles.md new file mode 100644 index 0000000..9c4f32c --- /dev/null +++ b/docs/oracles.md @@ -0,0 +1,243 @@ +# Verifiable Oracles + +PrediFi uses **Stork Network** to ensure objective, verifiable market outcomes. This document explains how oracle resolution works and how to verify outcomes. + +## Overview + +PrediFi markets resolve based on verifiable, on-chain data from Stork Network oracles. This eliminates the need for trusted third parties and ensures outcomes are objective and auditable. + +## Oracle Resolution Flow + +```mermaid +sequenceDiagram + participant P as Pool + participant O as Operator + participant S as Stork Network + participant C as Contract + + P->>P: end_time reached + P->>O: Pool ready for resolution + O->>S: Query oracle data + S-->>O: Return verified outcome + O->>O: Verify outcome against criteria + O->>C: resolve_pool(pool_id, outcome) + C->>C: Validate operator role + C->>C: Set pool.resolved = true + C->>C: Set pool.outcome = outcome + C->>C: Emit PoolResolvedEvent + C-->>O: Success +``` + +## Stork Network Integration + +Stork Network provides decentralized oracle services for Stellar/Soroban: + +1. **Data Sources**: Aggregates data from multiple trusted sources +2. **Verification**: Cryptographic proofs ensure data integrity +3. **On-Chain**: Outcomes are verifiable on-chain via contract calls + +### Querying Oracle Data + +Operators query Stork Network before resolving pools: + +```typescript +// Example: Query Stork Network for outcome +async function queryOracle(poolId: number, metadataUrl: string) { + // Parse metadata URL to get oracle query parameters + const oracleParams = parseMetadata(metadataUrl); + + // Query Stork Network + const response = await fetch(`https://stork.network/api/query`, { + method: 'POST', + body: JSON.stringify({ + pool_id: poolId, + query: oracleParams.query, + timestamp: oracleParams.end_time + }) + }); + + const data = await response.json(); + return data.outcome; // Returns outcome index (0, 1, 2, etc.) +} +``` + +## Resolution Process + +### Step 1: Pool End Time Reached + +Once `pool.end_time` passes, the pool is closed to new predictions: + +```rust +// Pool is closed when: +env.ledger().timestamp() >= pool.end_time +``` + +### Step 2: Operator Queries Oracle + +An operator (with role 1) queries Stork Network for the verified outcome: + +```typescript +const outcome = await queryOracle(poolId, pool.metadata_url); +``` + +### Step 3: Operator Resolves Pool + +The operator calls `resolve_pool()` with the verified outcome: + +```rust +contract.resolve_pool( + env, + operator_address, + pool_id, + outcome // Verified outcome from oracle +)?; +``` + +### Step 4: Contract Validation + +The contract validates: + +- Operator has role 1 (Operator role) +- Pool exists and is not already resolved +- Pool's `end_time` has passed + +### Step 5: Outcome Set + +Once validated, the contract: + +1. Sets `pool.resolved = true` +2. Sets `pool.outcome = outcome` +3. Emits `PoolResolvedEvent` +4. Makes pool eligible for claims + +## Verifying Outcomes + +### On-Chain Verification + +All outcomes are stored on-chain and can be verified: + +```typescript +async function verifyOutcome(poolId: number) { + const pool = await contract.call('get_pool', { + pool_id: nativeToScVal(poolId, { type: 'u64' }) + }); + + if (!pool.resolved) { + throw new Error('Pool not yet resolved'); + } + + return { + resolved: pool.resolved, + outcome: pool.outcome, + resolvedAt: pool.resolved_at // If available + }; +} +``` + +### Off-Chain Verification + +You can verify outcomes against Stork Network data: + +```typescript +async function verifyAgainstOracle(poolId: number, contractOutcome: number) { + const oracleOutcome = await queryOracle(poolId); + + if (oracleOutcome !== contractOutcome) { + throw new Error('Outcome mismatch between contract and oracle'); + } + + return true; // Verified +} +``` + +## Oracle Data Sources + +Stork Network aggregates data from multiple sources: + +| Source Type | Examples | Use Case | +|-------------|----------|----------| +| **APIs** | Sports APIs, Financial APIs | Real-time event data | +| **Blockchain** | Other chains, Cross-chain data | Multi-chain events | +| **Feeds** | Price feeds, News feeds | Market data | + +## Security Considerations + +### Operator Trust + +Operators are required to have role 1, but they cannot: + +- Change outcomes after resolution +- Resolve pools before `end_time` +- Manipulate oracle data (Stork Network prevents this) + +### Oracle Reliability + +Stork Network provides: + +- **Redundancy**: Multiple data sources +- **Verification**: Cryptographic proofs +- **Transparency**: All queries are logged + +### Dispute Resolution + +If an outcome seems incorrect: + +1. Check the `PoolResolvedEvent` for the operator +2. Verify against Stork Network data +3. Review the pool's `metadata_url` for resolution criteria +4. Contact the protocol team if discrepancies are found + +## Best Practices + +### For Operators + +- Always verify oracle data before resolving +- Wait for sufficient confirmation from Stork Network +- Double-check outcome indices match pool structure +- Monitor for oracle updates before resolution deadline + +### For Users + +- Review pool `metadata_url` to understand resolution criteria +- Verify outcomes on-chain after resolution +- Check `PoolResolvedEvent` for resolution details +- Report any discrepancies immediately + +## Example: Resolving a Sports Market + +```typescript +// Pool: "Will Team A win the match?" +// Metadata URL contains match ID and resolution criteria + +async function resolveSportsMarket(poolId: number) { + // 1. Get pool details + const pool = await getPool(poolId); + + // 2. Wait for end_time + await waitUntil(pool.end_time); + + // 3. Query Stork Network for match result + const matchResult = await queryStorkNetwork({ + type: 'sports', + match_id: pool.metadata.match_id, + source: 'sports_api' + }); + + // 4. Map result to outcome index + // Outcome 0: "No", Outcome 1: "Yes" + const outcome = matchResult.team_a_won ? 1 : 0; + + // 5. Resolve pool + await contract.resolve_pool( + operatorAddress, + poolId, + outcome + ); +} +``` + +## Next Steps + +- Learn about [Pool Resolution](./prediction-lifecycle.md#phase-3-resolution) +- Explore [Contract Methods](./contract-reference.md#admin-functions) +- Review [Error Handling](./troubleshooting.md#oracle-errors) diff --git a/docs/prediction-lifecycle.md b/docs/prediction-lifecycle.md new file mode 100644 index 0000000..9085e30 --- /dev/null +++ b/docs/prediction-lifecycle.md @@ -0,0 +1,292 @@ +# Prediction Lifecycle + +Understanding how predictions flow through the PrediFi protocol—from market creation to reward distribution. + +## Overview + +Every prediction market on PrediFi follows a structured lifecycle with four distinct phases: + +1. **Creation** - A new market is created with defined parameters +2. **Trading** - Users place predictions and add liquidity +3. **Resolution** - The market outcome is determined via oracle +4. **Settlement** - Winners claim their rewards + +```mermaid +graph TD + A[Market Creator] -->|create_pool| B[Pool Created] + B -->|place_prediction| C[Users Add Predictions] + C -->|place_prediction| C + C -->|end_time reached| D[Pool Closed] + D -->|resolve_pool| E[Oracle Resolves Outcome] + E -->|claim_winnings| F[Winners Claim Rewards] + F -->|End| G[Lifecycle Complete] + + style B fill:#e1f5ff + style C fill:#fff4e1 + style E fill:#e8f5e9 + style F fill:#f3e5f5 +``` + +## Phase 1: Market Creation + +A market creator calls `create_pool()` to establish a new prediction market. + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `end_time` | `u64` | Unix timestamp after which no predictions are accepted | +| `token` | `Address` | Stellar token contract address for staking | +| `description` | `String` | Human-readable event description (max 256 bytes) | +| `metadata_url` | `String` | URL to extended metadata, e.g., IPFS link (max 512 bytes) | + +### Example + +```rust +let pool_id = contract.create_pool( + env, + 1735689600, // Unix timestamp + token_address, + String::from_str(&env, "Will Bitcoin reach $100k by 2025?"), + String::from_str(&env, "ipfs://QmXxx...") +); +``` + +### What Happens + +- A new `Pool` struct is created with `resolved: false` +- Pool ID is auto-incremented and returned +- `PoolCreatedEvent` is emitted for off-chain indexers +- Pool is stored in persistent storage with TTL extension + +:::info +**Pool IDs**: Pool IDs are sequential integers starting from 0. Each new pool increments the counter. +::: + +## Phase 2: Trading & Liquidity + +Users place predictions by calling `place_prediction()` before the pool's `end_time`. + +### Prediction Flow + +```mermaid +sequenceDiagram + participant U as User + participant C as Contract + participant T as Token Contract + + U->>C: place_prediction(pool_id, amount, outcome) + C->>C: Validate pool state + C->>C: Check end_time not passed + C->>T: transfer(user, contract, amount) + C->>C: Update total_stake + C->>C: Update outcome_stake[outcome] + C->>C: Store user prediction + C->>C: Emit PredictionPlacedEvent + C-->>U: Success +``` + +### Key Validations + +- Pool must not be resolved (`pool.resolved == false`) +- Current time must be before `pool.end_time` +- Amount must be positive (`amount > 0`) +- User must have sufficient token balance + +### Staking Mechanism + +When a user places a prediction: + +1. **Token Transfer**: Tokens are transferred from user to contract +2. **Stake Tracking**: Total stake and outcome-specific stake are updated +3. **Prediction Storage**: User's prediction is stored with amount and outcome +4. **Indexing**: Prediction is added to user's prediction list for quick lookup + +### Example + +```rust +contract.place_prediction( + env, + user_address, + pool_id, + 1000000000, // 100 tokens (in smallest unit) + 1 // Outcome: "Yes" +); +``` + +:::tip +**Multiple Predictions**: Users can place multiple predictions on the same pool, including different outcomes. Each prediction is tracked separately. +::: + +## Phase 3: Resolution + +After `end_time` passes, an operator (with role 1) calls `resolve_pool()` to set the winning outcome. + +### Resolution Process + +```mermaid +graph LR + A[Pool End Time Reached] --> B[Operator Calls resolve_pool] + B --> C{Validate Operator Role} + C -->|Authorized| D[Set pool.resolved = true] + C -->|Unauthorized| E[Error: Unauthorized] + D --> F[Set pool.outcome = winning_outcome] + F --> G[Emit PoolResolvedEvent] + G --> H[Pool Ready for Claims] + + style D fill:#e8f5e9 + style F fill:#e8f5e9 + style E fill:#ffebee +``` + +### Oracle Integration + +PrediFi uses **Stork Network** for verifiable, objective market resolution: + +1. **Oracle Query**: Off-chain service queries Stork Network for outcome data +2. **Verification**: Outcome is verified against on-chain criteria +3. **Resolution**: Operator calls `resolve_pool()` with verified outcome +4. **Immutability**: Once resolved, outcome cannot be changed + +See [Verifiable Oracles](./oracles.md) for detailed oracle mechanics. + +### Example + +```rust +// Operator resolves pool with outcome 1 ("Yes") +contract.resolve_pool( + env, + operator_address, + pool_id, + 1 // Winning outcome +)?; +``` + +:::warning +**Irreversible**: Once a pool is resolved, the outcome cannot be changed. Ensure oracle data is accurate before resolving. +::: + +## Phase 4: Settlement & Claims + +Winners call `claim_winnings()` to receive their proportional share of the total pool. + +### Reward Calculation + +Rewards are calculated using a **proportional distribution** model: + +``` +winnings = (user_stake / winning_outcome_total_stake) × total_pool_stake +``` + +### Claim Flow + +```mermaid +sequenceDiagram + participant U as User + participant C as Contract + participant T as Token Contract + + U->>C: claim_winnings(pool_id) + C->>C: Check pool.resolved == true + C->>C: Check not already claimed + C->>C: Get user prediction + C->>C: Check prediction.outcome == pool.outcome + C->>C: Calculate winnings proportionally + C->>T: transfer(contract, user, winnings) + C->>C: Mark as claimed + C->C: Emit WinningsClaimedEvent + C-->>U: Return winnings amount +``` + +### Example + +```rust +let winnings = contract.claim_winnings( + env, + user_address, + pool_id +)?; + +// Returns 0 if user didn't win or already claimed +if winnings > 0 { + println!("Claimed {} tokens", winnings); +} +``` + +### Edge Cases + +- **Losers**: Users who predicted the wrong outcome receive 0 tokens +- **No Prediction**: Users who never placed a prediction receive 0 tokens +- **Already Claimed**: Subsequent calls return `AlreadyClaimed` error +- **Unresolved Pool**: Returns `PoolNotResolved` error + +:::info +**Fee Structure**: Protocol fees are deducted from the total pool before distribution. Winners receive their proportional share of the net pool (after fees). +::: + +## Incentive Alignment + +The protocol aligns incentives through transparent, on-chain mechanics: + +```mermaid +graph TB + A[Market Participants] --> B{Incentive Type} + B -->|Liquidity Providers| C[Earn from Trading Fees] + B -->|Correct Predictors| D[Win Proportional Rewards] + B -->|Operators| E[Maintain Oracle Integrity] + + C --> F[Protocol Sustainability] + D --> F + E --> F + + style F fill:#e8f5e9 +``` + +### Key Principles + +1. **Transparency**: All pool data, predictions, and resolutions are on-chain +2. **Proportional Rewards**: Winners share pool proportionally to their stake +3. **No Central Authority**: Resolution relies on verifiable oracles, not trusted parties +4. **Immutability**: Once resolved, outcomes cannot be manipulated + +## State Transitions + +```mermaid +stateDiagram-v2 + [*] --> Created: create_pool() + Created --> Active: Users place predictions + Active --> Active: More predictions + Active --> Closed: end_time reached + Closed --> Resolved: resolve_pool() + Resolved --> Claimed: claim_winnings() + Claimed --> [*] + + note right of Created + resolved: false + total_stake: 0 + end note + + note right of Resolved + resolved: true + outcome: set + end note +``` + +## Events Timeline + +Every phase emits events for off-chain indexing and monitoring: + +| Event | Phase | Emitted When | +|-------|-------|--------------| +| `PoolCreatedEvent` | Creation | Pool is created | +| `PredictionPlacedEvent` | Trading | User places prediction | +| `PoolResolvedEvent` | Resolution | Operator resolves pool | +| `WinningsClaimedEvent` | Settlement | Winner claims rewards | + +See [Contract Reference](./contract-reference.md) for complete event schemas. + +## Next Steps + +- Learn about [Oracle Resolution](./oracles.md) +- Explore [Contract Methods](./contract-reference.md) +- Review [Error Handling](./troubleshooting.md) diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..469541e --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,199 @@ +# Quickstart: Make Your First Prediction in 5 Minutes + +Get started with PrediFi by placing your first prediction. This guide walks you through connecting your wallet, finding a market, and placing a bet. + +## Prerequisites + +- A Stellar wallet (e.g., [Freighter](https://freighter.app/)) +- XLM or supported token for gas fees +- Basic familiarity with Stellar/Soroban + +:::tip +**Testnet First**: Start on Stellar testnet to experiment without real funds. Get testnet XLM from the [Stellar Laboratory](https://laboratory.stellar.org/#account-creator?network=test). +::: + +## Step 1: Install the Soroban SDK + +```bash +npm install @stellar/stellar-sdk +``` + +Or with TypeScript: + +```bash +npm install @stellar/stellar-sdk @types/node +``` + +## Step 2: Connect Your Wallet + +```typescript +import { Contract, Networks, nativeToScVal } from '@stellar/stellar-sdk'; + +// Connect to Stellar network +const network = Networks.TESTNET; // or Networks.PUBLIC for mainnet +const server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); + +// Contract address (replace with actual deployed contract) +const contractId = 'YOUR_CONTRACT_ID_HERE'; + +// Initialize contract client +const contract = new Contract(contractId); +``` + +:::info +**Contract Addresses**: Contract addresses differ between testnet and mainnet. Check the [deployment guide](./deployment.md) for current addresses. +::: + +## Step 3: Find an Active Pool + +```typescript +// Get pool details +async function getPool(poolId: number) { + const result = await contract.call('get_pool', { + pool_id: nativeToScVal(poolId, { type: 'u64' }) + }); + + return { + endTime: result.end_time, + resolved: result.resolved, + outcome: result.outcome, + totalStake: result.total_stake, + description: result.description + }; +} + +// Example: Get pool #1 +const pool = await getPool(1); +console.log('Pool:', pool.description); +``` + +## Step 4: Place Your Prediction + +```typescript +import { Keypair, TransactionBuilder, Operation } from '@stellar/stellar-sdk'; + +async function placePrediction( + poolId: number, + amount: number, // in smallest unit (e.g., stroops for XLM) + outcome: number, // 0 for "No", 1 for "Yes", etc. + sourceKeypair: Keypair +) { + // Build transaction + const account = await server.loadAccount(sourceKeypair.publicKey()); + + const transaction = new TransactionBuilder(account, { + fee: '100', // Base fee + networkPassphrase: network + }) + .addOperation( + contract.call('place_prediction', { + user: sourceKeypair.publicKey(), + pool_id: nativeToScVal(poolId, { type: 'u64' }), + amount: nativeToScVal(amount, { type: 'i128' }), + outcome: nativeToScVal(outcome, { type: 'u32' }) + }) + ) + .setTimeout(30) + .build(); + + // Sign and submit + transaction.sign(sourceKeypair); + const result = await server.submitTransaction(transaction); + + return result; +} + +// Example: Predict "Yes" (outcome 1) with 100 tokens +const keypair = Keypair.fromSecret('YOUR_SECRET_KEY'); +await placePrediction(1, 1000000000, 1, keypair); +``` + +:::warning +**Amount Format**: Amounts are in the smallest unit of the token. For XLM, use stroops (1 XLM = 10,000,000 stroops). For other tokens, check the token's decimal precision. +::: + +## Step 5: Check Your Prediction Status + +```typescript +async function getUserPredictions(userAddress: string, offset = 0, limit = 10) { + const result = await contract.call('get_user_predictions', { + user: userAddress, + offset: nativeToScVal(offset, { type: 'u32' }), + limit: nativeToScVal(limit, { type: 'u32' }) + }); + + return result.map((pred: any) => ({ + poolId: pred.pool_id, + amount: pred.amount, + outcome: pred.user_outcome, + poolResolved: pred.pool_resolved, + poolOutcome: pred.pool_outcome + })); +} + +const predictions = await getUserPredictions(keypair.publicKey()); +console.log('Your predictions:', predictions); +``` + +## Complete Example + +Here's a complete working example: + +```typescript +import { + Contract, + Networks, + Keypair, + Server, + TransactionBuilder, + nativeToScVal +} from '@stellar/stellar-sdk'; + +const network = Networks.TESTNET; +const server = new Server('https://horizon-testnet.stellar.org'); +const contractId = 'YOUR_CONTRACT_ID'; +const contract = new Contract(contractId); + +// Load your wallet +const keypair = Keypair.fromSecret('YOUR_SECRET_KEY'); +const account = await server.loadAccount(keypair.publicKey()); + +// Get pool info +const pool = await contract.call('get_pool', { + pool_id: nativeToScVal(1, { type: 'u64' }) +}); + +console.log(`Pool: ${pool.description}`); +console.log(`Ends at: ${new Date(pool.end_time * 1000)}`); + +// Place prediction +const tx = new TransactionBuilder(account, { + fee: '100', + networkPassphrase: network +}) + .addOperation( + contract.call('place_prediction', { + user: keypair.publicKey(), + pool_id: nativeToScVal(1, { type: 'u64' }), + amount: nativeToScVal(1000000000, { type: 'i128' }), // 100 tokens + outcome: nativeToScVal(1, { type: 'u32' }) // "Yes" + }) + ) + .setTimeout(30) + .build(); + +tx.sign(keypair); +const result = await server.submitTransaction(tx); +console.log('Transaction hash:', result.hash); +``` + +## Next Steps + +- Learn about the [Prediction Lifecycle](./prediction-lifecycle.md) +- Explore [Contract Reference](./contract-reference.md) +- Understand [Oracle Resolution](./oracles.md) +- Check [Troubleshooting](./troubleshooting.md) for common issues + +:::tip +**Need Help?** Join our [Telegram community](https://t.me/predifi_onchain_build/1) for support and updates. +::: diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..b5057d0 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,508 @@ +# Troubleshooting + +Common errors, solutions, and debugging tips for PrediFi integration. + +## Error Code Reference + +PrediFi uses comprehensive error codes for precise error handling. All errors implement the `PredifiError` enum. + +### Error Categories + +| Range | Category | Description | +|-------|----------|-------------| +| 1-5 | Initialization | Contract setup errors | +| 10-15 | Authorization | Access control errors | +| 20-30 | Pool State | Pool lifecycle errors | +| 40-50 | Prediction | Betting errors | +| 60-70 | Claiming | Reward claim errors | +| 80-85 | Timestamp | Time validation errors | +| 90-100 | Validation | Input validation errors | +| 110-118 | Arithmetic | Math operation errors | +| 120-129 | Storage | Data persistence errors | +| 150-159 | Token | Token transfer errors | +| 160-169 | Oracle | Oracle resolution errors | +| 180-189 | Admin | Admin operation errors | + +--- + +## Common Errors + +### Initialization Errors + +#### `NotInitialized` (Code: 1) + +**Message:** "Contract has not been initialized yet" + +**Cause:** Contract `init()` function hasn't been called. + +**Solution:** + +```rust +// Call init before using contract +contract.init( + env, + access_control_address, + treasury_address, + 100 // 1% fee (100 basis points) +); +``` + +--- + +### Authorization Errors + +#### `Unauthorized` (Code: 10) + +**Message:** "The caller is not authorized to perform this action" + +**Cause:** User lacks required role for the operation. + +**Solution:** + +- For `resolve_pool()`: Ensure caller has Operator role (1) +- For admin functions: Ensure caller has Admin role (0) +- Check access control contract for role assignments + +**Example:** + +```rust +// Check if user has operator role +let has_role = access_control.has_role(user, 1); +if !has_role { + return Err(PredifiError::Unauthorized); +} +``` + +--- + +### Pool State Errors + +#### `PoolNotFound` (Code: 20) + +**Message:** "The specified pool was not found" + +**Cause:** Pool ID doesn't exist. + +**Solution:** + +```typescript +// Verify pool exists before operations +const pool = await contract.call('get_pool', { + pool_id: nativeToScVal(poolId, { type: 'u64' }) +}); + +if (!pool) { + throw new Error('Pool not found'); +} +``` + +#### `PoolAlreadyResolved` (Code: 21) + +**Message:** "The pool has already been resolved" + +**Cause:** Attempting to resolve or modify an already resolved pool. + +**Solution:** Check pool state before operations: + +```rust +let pool = get_pool(&env, pool_id)?; +if pool.resolved { + return Err(PredifiError::PoolAlreadyResolved); +} +``` + +#### `PoolNotResolved` (Code: 22) + +**Message:** "The pool has not been resolved yet" + +**Cause:** Attempting to claim winnings from unresolved pool. + +**Solution:** Wait for pool resolution: + +```typescript +// Check if pool is resolved +const pool = await getPool(poolId); +if (!pool.resolved) { + console.log('Pool not yet resolved. Waiting...'); + return; +} + +// Now safe to claim +await claimWinnings(poolId); +``` + +--- + +### Prediction Errors + +#### `InvalidPredictionAmount` (Code: 42) + +**Message:** "The prediction amount is invalid (e.g., zero or negative)" + +**Cause:** Amount is zero or negative. + +**Solution:** + +```rust +// Validate amount before calling +if amount <= 0 { + return Err(PredifiError::InvalidPredictionAmount); +} +``` + +#### `PredictionTooLate` (Code: 43) + +**Message:** "Cannot place prediction after pool end time" + +**Cause:** Pool's `end_time` has passed. + +**Solution:** + +```typescript +// Check pool end time +const pool = await getPool(poolId); +const now = Date.now() / 1000; // Unix timestamp + +if (now >= pool.end_time) { + throw new Error('Pool has closed for predictions'); +} +``` + +#### `InsufficientBalanceOrStakeLimit` (Code: 44) + +**Message:** "The user has insufficient balance or stake limit violation" + +**Cause:** User doesn't have enough tokens or exceeds stake limit. + +**Solution:** + +```typescript +// Check balance before prediction +const balance = await tokenContract.balance(userAddress); +if (balance < amount) { + throw new Error('Insufficient balance'); +} + +// Check stake limits if applicable +const totalStake = await getUserTotalStake(userAddress); +if (totalStake + amount > MAX_STAKE) { + throw new Error('Stake limit exceeded'); +} +``` + +--- + +### Claiming Errors + +#### `AlreadyClaimed` (Code: 60) + +**Message:** "The user has already claimed winnings for this pool" + +**Cause:** User already claimed winnings from this pool. + +**Solution:** + +```typescript +// Check if already claimed before calling +const hasClaimed = await checkIfClaimed(userAddress, poolId); +if (hasClaimed) { + console.log('Already claimed'); + return; +} + +// Safe to claim +await claimWinnings(poolId); +``` + +#### `NotAWinner` (Code: 61) + +**Message:** "The user did not win this pool" + +**Cause:** User's prediction outcome doesn't match pool outcome. + +**Note:** This doesn't throw an error - `claim_winnings()` returns 0 for losers. + +**Solution:** + +```rust +let winnings = contract.claim_winnings(env, user, pool_id)?; +if winnings == 0 { + // User didn't win or already claimed +} +``` + +--- + +### Timestamp Errors + +#### `InvalidTimestamp` (Code: 80) + +**Message:** "The provided timestamp is invalid or time constraints not met" + +**Cause:** Timestamp validation failed (e.g., end_time in the past). + +**Solution:** + +```rust +// Validate timestamp +let current_time = env.ledger().timestamp(); +if end_time <= current_time { + return Err(PredifiError::InvalidTimestamp); +} +``` + +--- + +### Validation Errors + +#### `InvalidData` (Code: 90) + +**Message:** "The provided data is invalid" + +**Cause:** General data validation failure. + +**Solution:** Check all input parameters match expected types and constraints. + +#### `InvalidAddressOrToken` (Code: 91) + +**Message:** "The provided address or token is invalid" + +**Cause:** Invalid Stellar address or token contract. + +**Solution:** + +```typescript +// Validate address format +function isValidAddress(address: string): boolean { + // Stellar addresses are 56 characters, start with G + return /^G[A-Z0-9]{55}$/.test(address); +} + +if (!isValidAddress(tokenAddress)) { + throw new Error('Invalid token address'); +} +``` + +--- + +### Arithmetic Errors + +#### `ArithmeticError` (Code: 110) + +**Message:** "An arithmetic overflow, underflow, or division by zero occurred" + +**Cause:** Math operation failed (overflow, underflow, or division by zero). + +**Solution:** Use checked arithmetic: + +```rust +// Use checked operations +let total = stake_a + .checked_add(stake_b) + .ok_or(PredifiError::ArithmeticError)?; + +let winnings = amount + .checked_mul(pool.total_stake) + .ok_or(PredifiError::ArithmeticError)? + .checked_div(winning_stake) + .ok_or(PredifiError::ArithmeticError)?; +``` + +--- + +### Token Errors + +#### `TokenError` (Code: 150) + +**Message:** "Token transfer, approval, or contract call failed" + +**Cause:** Token contract call failed (insufficient balance, approval, etc.). + +**Solution:** + +```typescript +// Check balance and approval before transfer +const balance = await token.balance(userAddress); +if (balance < amount) { + throw new Error('Insufficient balance'); +} + +// Ensure contract has approval (if needed) +await token.approve(userAddress, contractAddress, amount); +``` + +--- + +### Oracle Errors + +#### `OracleError` (Code: 160) + +**Message:** "Oracle error or stale data detected" + +**Cause:** Oracle data is unavailable or stale. + +**Solution:** + +```typescript +// Verify oracle data freshness +const oracleData = await queryOracle(poolId); +const dataAge = Date.now() - oracleData.timestamp; + +if (dataAge > MAX_DATA_AGE) { + throw new Error('Oracle data is stale'); +} +``` + +#### `ResolutionError` (Code: 161) + +**Message:** "Resolution error or unauthorized resolver" + +**Cause:** Resolution attempt failed or unauthorized. + +**Solution:** Ensure operator has role 1 and pool is ready for resolution. + +--- + +## RPC & Network Issues + +### Transaction Timeout + +**Symptom:** Transaction hangs or times out. + +**Solutions:** + +1. **Increase timeout:** +```typescript +const tx = new TransactionBuilder(account, { + timeout: 60 // Increase from default 30 +}) +``` + +2. **Check network status:** +```typescript +const server = new Server('https://horizon-testnet.stellar.org'); +const health = await server.health(); +console.log('Network status:', health); +``` + +3. **Retry with exponential backoff:** +```typescript +async function retryWithBackoff(fn, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await fn(); + } catch (error) { + if (i === maxRetries - 1) throw error; + await sleep(2 ** i * 1000); // Exponential backoff + } + } +} +``` + +### Connection Errors + +**Symptom:** Cannot connect to Stellar network. + +**Solutions:** + +- Verify network endpoint is correct +- Check firewall/proxy settings +- Try alternative Horizon server +- Verify internet connection + +### Gas/Fee Estimation + +**Symptom:** Transaction fails with insufficient fee. + +**Solution:** + +```typescript +// Get recommended fee +const feeStats = await server.feeStats(); +const recommendedFee = feeStats.fee_charged.mode; + +const tx = new TransactionBuilder(account, { + fee: recommendedFee.toString() +}); +``` + +--- + +## Debugging Tips + +### 1. Enable Verbose Logging + +```typescript +// Enable detailed logging +const server = new Server('https://horizon-testnet.stellar.org', { + allowHttp: true +}); + +server.on('request', (req) => { + console.log('Request:', req); +}); + +server.on('response', (res) => { + console.log('Response:', res); +}); +``` + +### 2. Check Contract State + +```typescript +// Verify contract is initialized +const config = await contract.call('get_config'); +console.log('Config:', config); + +// Check if paused +const paused = await contract.call('is_paused'); +console.log('Paused:', paused); +``` + +### 3. Validate Pool State + +```typescript +// Get full pool state +const pool = await getPool(poolId); +console.log('Pool state:', { + id: poolId, + endTime: new Date(pool.end_time * 1000), + resolved: pool.resolved, + outcome: pool.outcome, + totalStake: pool.total_stake +}); +``` + +### 4. Monitor Events + +```typescript +// Listen for contract events +const events = await server.effects() + .forAccount(contractAddress) + .order('desc') + .limit(10) + .call(); + +events.records.forEach(event => { + console.log('Event:', event); +}); +``` + +--- + +## Getting Help + +If you encounter issues not covered here: + +1. **Check Error Codes:** Review the [Error Code Reference](#error-code-reference) above +2. **Review Documentation:** See [Contract Reference](./contract-reference.md) +3. **Community Support:** Join [Telegram](https://t.me/predifi_onchain_build/1) +4. **Open an Issue:** [GitHub Issues](https://github.com/Web3Novalabs/predifi/issues) + +--- + +## Next Steps + +- Review [Quickstart](./quickstart.md) for basic usage +- Explore [Contract Reference](./contract-reference.md) for API details +- Understand [Prediction Lifecycle](./prediction-lifecycle.md) for flow