diff --git a/contract/contracts/access-control/src/lib.rs b/contract/contracts/access-control/src/lib.rs
index ab68cce5..7e42530f 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 8bc8b48d..f2fa13e6 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 4eef2bfb..b8d51f82 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/src/lib.rs b/contract/contracts/predifi-contract/src/lib.rs
index ce53189d..d0fa7f7b 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 6d481c20..7def0974 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);
+}
+
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 9f0e3376..5a559199 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 005a6146..909f2ba2 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 7fb65f68..16e33f7d 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 b1e5278a..00000000
--- 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 00000000..ef960ddf
--- /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 00000000..9c4f32c8
--- /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 00000000..9085e305
--- /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 00000000..469541e1
--- /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 00000000..b5057d0b
--- /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