diff --git a/contracts/predictify-hybrid/src/admin.rs b/contracts/predictify-hybrid/src/admin.rs
index 35c9047..bf45d37 100644
--- a/contracts/predictify-hybrid/src/admin.rs
+++ b/contracts/predictify-hybrid/src/admin.rs
@@ -465,12 +465,7 @@ impl AdminAccessControl {
admin: &Address,
permission: &AdminPermission,
) -> Result<(), Error> {
- // Check original admin for backward compatibility first
- if AdminManager::is_original_admin(env, admin) {
- return Ok(());
- }
-
- // Try new multi-admin system if migrated
+ // Try new multi-admin system first if migrated
if AdminSystemIntegration::is_migrated(env) {
return AdminManager::validate_admin_permission(env, admin, *permission);
}
@@ -1258,16 +1253,6 @@ impl AdminManager {
.persistent()
.set(&count_key, &(current_count + 1));
- // Maintain a list of admin addresses for iteration
- let list_key = Symbol::new(env, "AdminList");
- let mut admin_list: Vec
= env
- .storage()
- .persistent()
- .get(&list_key)
- .unwrap_or_else(|| Vec::new(env));
- admin_list.push_back(new_admin.clone());
- env.storage().persistent().set(&list_key, &admin_list);
-
// Emit event using existing system
Self::emit_admin_change_event(env, new_admin, AdminActionType::Added);
@@ -1310,18 +1295,6 @@ impl AdminManager {
.set(&count_key, &(current_count - 1));
}
- // Remove from admin list
- let list_key = Symbol::new(env, "AdminList");
- if let Some(admin_list) = env.storage().persistent().get::<_, Vec>(&list_key) {
- let mut new_list: Vec = Vec::new(env);
- for addr in admin_list.iter() {
- if &addr != admin_to_remove {
- new_list.push_back(addr.clone());
- }
- }
- env.storage().persistent().set(&list_key, &new_list);
- }
-
Self::emit_admin_change_event(env, admin_to_remove, AdminActionType::Removed);
Ok(())
}
@@ -1441,6 +1414,7 @@ impl AdminManager {
None
}
+
/// Emits admin change events using existing AdminActionType
pub fn emit_admin_change_event(env: &Env, admin: &Address, action: AdminActionType) {
let action_str = match action {
@@ -1462,14 +1436,14 @@ impl AdminManager {
// ===== Helper Methods =====
/// Generate a proper admin storage key using the correct environment
- fn get_admin_key(env: &Env, admin: &Address) -> (Symbol, Address) {
- // Use a tuple key for per-admin storage
- // This avoids Symbol character limitations by using Address directly
- (Symbol::new(env, "MultiAdmin"), admin.clone())
+ fn get_admin_key(env: &Env, admin: &Address) -> Symbol {
+ // Create a unique key based on admin address
+ let key_str = format!("MultiAdmin_{:?}", admin.to_string());
+ Symbol::new(env, &key_str)
}
/// Check if an address is the original admin from single-admin system
- fn is_original_admin(env: &Env, admin: &Address) -> bool {
+ pub fn is_original_admin(env: &Env, admin: &Address) -> bool {
if let Some(original_admin) = Self::get_original_admin(env) {
return admin == &original_admin;
}
diff --git a/contracts/predictify-hybrid/src/batch_operations_tests.rs b/contracts/predictify-hybrid/src/batch_operations_tests.rs
index 71116dc..94a5c7b 100644
--- a/contracts/predictify-hybrid/src/batch_operations_tests.rs
+++ b/contracts/predictify-hybrid/src/batch_operations_tests.rs
@@ -1,4 +1,7 @@
#[cfg(test)]
+#[allow(unused_assignments)]
+#[allow(unused_variables)]
+#[allow(dead_code)]
mod batch_operations_tests {
use crate::admin::AdminRoleManager;
use crate::batch_operations::*;
@@ -366,7 +369,7 @@ mod batch_operations_tests {
};
// Invalid vote data - zero stake
- let invalid_vote = VoteData {
+ let _invalid_vote = VoteData {
market_id: market_id.clone(),
voter: ::generate(&env),
outcome: String::from_str(&env, "Yes"),
@@ -374,7 +377,7 @@ mod batch_operations_tests {
};
// Invalid vote data - empty outcome
- let invalid_vote2 = VoteData {
+ let _invalid_vote2 = VoteData {
market_id: market_id.clone(),
voter: ::generate(&env),
outcome: String::from_str(&env, ""),
@@ -383,14 +386,14 @@ mod batch_operations_tests {
// Test claim data validation
// Valid claim data
- let valid_claim = ClaimData {
+ let _valid_claim = ClaimData {
market_id: market_id.clone(),
claimant: ::generate(&env),
expected_amount: 2_000_000_000,
};
// Invalid claim data - zero amount
- let invalid_claim = ClaimData {
+ let _invalid_claim = ClaimData {
market_id: market_id.clone(),
claimant: ::generate(&env),
expected_amount: 0,
@@ -398,7 +401,7 @@ mod batch_operations_tests {
// Test market data validation
// Valid market data
- let valid_market = MarketData {
+ let _valid_market = MarketData {
question: String::from_str(&env, "Test question?"),
outcomes: vec![
&env,
@@ -415,7 +418,7 @@ mod batch_operations_tests {
};
// Invalid market data - empty question
- let invalid_market = MarketData {
+ let _invalid_market = MarketData {
question: String::from_str(&env, ""),
outcomes: vec![
&env,
@@ -432,7 +435,7 @@ mod batch_operations_tests {
};
// Invalid market data - insufficient outcomes
- let invalid_market2 = MarketData {
+ let _invalid_market2 = MarketData {
question: String::from_str(&env, "Test question?"),
outcomes: vec![&env, String::from_str(&env, "Yes")],
duration_days: 30,
@@ -445,7 +448,7 @@ mod batch_operations_tests {
};
// Invalid market data - zero duration
- let invalid_market3 = MarketData {
+ let _invalid_market3 = MarketData {
question: String::from_str(&env, "Test question?"),
outcomes: vec![
&env,
@@ -463,7 +466,7 @@ mod batch_operations_tests {
// Test oracle feed data validation
// Valid oracle feed data
- let valid_feed = OracleFeed {
+ let _valid_feed = OracleFeed {
market_id: market_id.clone(),
feed_id: String::from_str(&env, "BTC/USD"),
provider: OracleProvider::Reflector,
@@ -472,7 +475,7 @@ mod batch_operations_tests {
};
// Invalid oracle feed data - empty feed ID
- let invalid_feed = OracleFeed {
+ let _invalid_feed = OracleFeed {
market_id: market_id.clone(),
feed_id: String::from_str(&env, ""),
provider: OracleProvider::Reflector,
@@ -481,7 +484,7 @@ mod batch_operations_tests {
};
// Invalid oracle feed data - zero threshold
- let invalid_feed2 = OracleFeed {
+ let _invalid_feed2 = OracleFeed {
market_id: market_id.clone(),
feed_id: String::from_str(&env, "BTC/USD"),
provider: OracleProvider::Reflector,
@@ -499,7 +502,7 @@ mod batch_operations_tests {
BatchProcessor::initialize(&env).unwrap();
// Create test batch result
- let test_result = BatchResult {
+ let _test_result = BatchResult {
successful_operations: 8,
failed_operations: 2,
total_operations: 10,
@@ -587,7 +590,7 @@ mod batch_operations_tests {
let claims = vec![&env, BatchTesting::create_test_claim_data(&env, &market_id)];
- let markets = vec![&env, BatchTesting::create_test_market_data(&env)];
+ let _markets = vec![&env, BatchTesting::create_test_market_data(&env)];
let feeds = vec![
&env,
diff --git a/contracts/predictify-hybrid/src/bet_tests.rs b/contracts/predictify-hybrid/src/bet_tests.rs
index b65c69f..d9cf448 100644
--- a/contracts/predictify-hybrid/src/bet_tests.rs
+++ b/contracts/predictify-hybrid/src/bet_tests.rs
@@ -395,33 +395,82 @@ fn test_place_bet_double_betting_prevented() {
}
#[test]
+#[should_panic(expected = "Error(Contract, #102)")] // MarketClosed = 102
fn test_place_bet_on_ended_market() {
- // Placing bet after market ended would return MarketClosed (#102).
- assert_eq!(crate::errors::Error::MarketClosed as i128, 102);
+ let setup = BetTestSetup::new();
+ let client = setup.client();
+
+ // Advance time past market end
+ setup.advance_past_market_end();
+
+ // Try to place bet after market ended
+ client.place_bet(
+ &setup.user,
+ &setup.market_id,
+ &String::from_str(&setup.env, "yes"),
+ &10_0000000,
+ );
}
#[test]
+#[should_panic(expected = "Error(Contract, #108)")] // InvalidOutcome = 108
fn test_place_bet_invalid_outcome() {
- // Betting on invalid outcome would return InvalidOutcome (#108).
- assert_eq!(crate::errors::Error::InvalidOutcome as i128, 108);
+ let setup = BetTestSetup::new();
+ let client = setup.client();
+
+ // Try to bet on invalid outcome
+ client.place_bet(
+ &setup.user,
+ &setup.market_id,
+ &String::from_str(&setup.env, "maybe"), // Not a valid outcome
+ &10_0000000,
+ );
}
#[test]
+#[should_panic(expected = "Error(Contract, #107)")] // InsufficientStake = 107
fn test_place_bet_below_minimum() {
- // Betting below minimum would return InsufficientStake (#107).
- assert_eq!(crate::errors::Error::InsufficientStake as i128, 107);
+ let setup = BetTestSetup::new();
+ let client = setup.client();
+
+ // Try to place bet below minimum
+ client.place_bet(
+ &setup.user,
+ &setup.market_id,
+ &String::from_str(&setup.env, "yes"),
+ &(MIN_BET_AMOUNT - 1), // Below minimum
+ );
}
#[test]
+#[should_panic(expected = "Error(Contract, #401)")] // InvalidInput = 401
fn test_place_bet_above_maximum() {
- // Betting above maximum would return InvalidInput (#401).
- assert_eq!(crate::errors::Error::InvalidInput as i128, 401);
+ let setup = BetTestSetup::new();
+ let client = setup.client();
+
+ // Try to place bet above maximum
+ client.place_bet(
+ &setup.user,
+ &setup.market_id,
+ &String::from_str(&setup.env, "yes"),
+ &(MAX_BET_AMOUNT + 1), // Above maximum
+ );
}
#[test]
+#[should_panic(expected = "Error(Contract, #101)")] // MarketNotFound = 101
fn test_place_bet_nonexistent_market() {
- // Betting on non-existent market would return MarketNotFound (#101).
- assert_eq!(crate::errors::Error::MarketNotFound as i128, 101);
+ let setup = BetTestSetup::new();
+ let client = setup.client();
+
+ // Try to bet on non-existent market
+ let fake_market_id = Symbol::new(&setup.env, "fake_market");
+ client.place_bet(
+ &setup.user,
+ &fake_market_id,
+ &String::from_str(&setup.env, "yes"),
+ &10_0000000,
+ );
}
// ===== BET STATUS TESTS =====
diff --git a/contracts/predictify-hybrid/src/circuit_breaker_tests.rs b/contracts/predictify-hybrid/src/circuit_breaker_tests.rs
index 977bbd6..d52f80e 100644
--- a/contracts/predictify-hybrid/src/circuit_breaker_tests.rs
+++ b/contracts/predictify-hybrid/src/circuit_breaker_tests.rs
@@ -1,4 +1,7 @@
#[cfg(test)]
+#[allow(unused_assignments)]
+#[allow(unused_variables)]
+#[allow(dead_code)]
mod circuit_breaker_tests {
use crate::admin::AdminRoleManager;
use crate::circuit_breaker::*;
diff --git a/contracts/predictify-hybrid/src/extensions.rs b/contracts/predictify-hybrid/src/extensions.rs
index c16b74a..f0a6efe 100644
--- a/contracts/predictify-hybrid/src/extensions.rs
+++ b/contracts/predictify-hybrid/src/extensions.rs
@@ -577,17 +577,17 @@ impl ExtensionValidator {
// Get market and validate state
let market = MarketStateManager::get_market(env, market_id)?;
- // Check if market is already resolved
- if market.state == MarketState::Resolved {
- return Err(Error::MarketAlreadyResolved);
- }
-
// Check if market is still active
let current_time = env.ledger().timestamp();
if current_time >= market.end_time {
return Err(Error::MarketClosed);
}
+ // Check if market is already resolved
+ if market.oracle_result.is_some() {
+ return Err(Error::MarketAlreadyResolved);
+ }
+
Ok(())
}
diff --git a/contracts/predictify-hybrid/src/lib.rs b/contracts/predictify-hybrid/src/lib.rs
index 512affb..9e85dc5 100644
--- a/contracts/predictify-hybrid/src/lib.rs
+++ b/contracts/predictify-hybrid/src/lib.rs
@@ -1,4 +1,10 @@
#![no_std]
+#![allow(unused_variables)]
+#![allow(unused_assignments)]
+#![allow(dead_code)]
+#![allow(unused_imports)]
+#![allow(unused_mut)]
+#![allow(deprecated)]
extern crate alloc;
extern crate wee_alloc;
@@ -165,21 +171,7 @@ impl PredictifyHybrid {
Err(e) => panic_with_error!(env, e),
}
- // Initialize default configuration
- // We use development defaults as a safe baseline, then update with user provided params
- let mut config = match crate::config::ConfigManager::reset_to_defaults(&env) {
- Ok(c) => c,
- Err(e) => panic_with_error!(env, e),
- };
-
- // Update platform fee in the configuration
- config.fees.platform_fee_percentage = fee_percentage;
- match crate::config::ConfigManager::update_config(&env, &config) {
- Ok(_) => (),
- Err(e) => panic_with_error!(env, e),
- };
-
- // Sync legacy storage for compatibility with distribute_payouts
+ // Store platform fee configuration in persistent storage
env.storage()
.persistent()
.set(&Symbol::new(&env, "platform_fee"), &fee_percentage);
@@ -402,6 +394,12 @@ impl PredictifyHybrid {
panic_with_error!(env, Error::AlreadyVoted);
}
+ // Lock funds (transfer from user to contract)
+ match bets::BetUtils::lock_funds(&env, &user, stake) {
+ Ok(_) => {}
+ Err(e) => panic_with_error!(env, e),
+ }
+
// Store the vote and stake
market.votes.set(user.clone(), outcome.clone());
market.stakes.set(user.clone(), stake);
@@ -793,7 +791,12 @@ impl PredictifyHybrid {
// Emit winnings claimed event
EventEmitter::emit_winnings_claimed(&env, &market_id, &user, payout);
- // In a real implementation, transfer tokens here
+ // Transfer tokens
+ match bets::BetUtils::unlock_funds(&env, &user, payout) {
+ Ok(_) => {}
+ Err(e) => panic_with_error!(env, e),
+ }
+
return;
}
}
@@ -999,6 +1002,8 @@ impl PredictifyHybrid {
&reason,
);
+ // Note: Payout distribution should be called separately via distribute_payouts()
+ // We don't call it here to avoid potential issues and allow explicit control
// Automatically distribute payouts
let _ = Self::distribute_payouts(env.clone(), market_id);
}
@@ -1494,6 +1499,9 @@ impl PredictifyHybrid {
None => return Err(Error::MarketNotResolved),
};
+ // Get all bettors
+ let bettors = bets::BetStorage::get_all_bets_for_market(&env, &market_id);
+
// Get fee from legacy storage (backward compatible)
let fee_percent = env
.storage()
@@ -1504,9 +1512,11 @@ impl PredictifyHybrid {
// Since place_bet now updates market.votes and market.stakes,
// we can use the vote-based payout system for both bets and votes
let mut total_distributed = 0;
-
+
// Check if payouts have already been distributed
let mut has_unclaimed_winners = false;
+
+ // Check voters
for (user, outcome) in market.votes.iter() {
if &outcome == winning_outcome {
if !market.claimed.get(user.clone()).unwrap_or(false) {
@@ -1515,26 +1525,52 @@ impl PredictifyHybrid {
}
}
}
+
+ // Check bettors
+ if !has_unclaimed_winners {
+ for user in bettors.iter() {
+ if let Some(bet) = bets::BetStorage::get_bet(&env, &market_id, &user) {
+ if bet.outcome == *winning_outcome && !market.claimed.get(user.clone()).unwrap_or(false) {
+ has_unclaimed_winners = true;
+ break;
+ }
+ }
+ }
+ }
if !has_unclaimed_winners {
return Ok(0);
}
- // Calculate total winning stakes
- let mut total_distributed: i128 = 0;
+ // Calculate total winning stakes (voters + bettors)
let mut winning_total = 0;
+
+ // Sum voter stakes
for (voter, outcome) in market.votes.iter() {
if outcome == *winning_outcome {
winning_total += market.stakes.get(voter.clone()).unwrap_or(0);
}
}
+
+ // Sum bet amounts
+ for user in bettors.iter() {
+ if let Some(bet) = bets::BetStorage::get_bet(&env, &market_id, &user) {
+ if bet.outcome == *winning_outcome {
+ winning_total += bet.amount;
+ }
+ }
+ }
if winning_total == 0 {
return Ok(0);
}
let total_pool = market.total_staked;
+ let fee_denominator = 10000i128; // Fee is in basis points
+ let mut total_distributed = 0;
+
+ // 1. Distribute to Voters
// Distribute payouts to all winners
for (user, outcome) in market.votes.iter() {
if outcome == *winning_outcome {
@@ -1552,16 +1588,58 @@ impl PredictifyHybrid {
if payout >= 0 {
// Allow 0 payout but mark as claimed
market.claimed.set(user.clone(), true);
+ total_distributed += payout;
+ EventEmitter::emit_winnings_claimed(&env, &market_id, &user, payout);
+ match bets::BetUtils::unlock_funds(&env, &user, payout) {
+ Ok(_) => {}
+ Err(e) => panic_with_error!(env, e),
+ }
+ }
+ }
+ }
+ }
+
+ // 2. Distribute to Bettors
+ for user in bettors.iter() {
+ if let Some(mut bet) = bets::BetStorage::get_bet(&env, &market_id, &user) {
+ if bet.outcome == *winning_outcome {
+ if market.claimed.get(user.clone()).unwrap_or(false) {
+ // Already claimed (perhaps as a voter or double check)
+ bet.status = BetStatus::Won;
+ let _ = bets::BetStorage::store_bet(&env, &bet);
+ continue;
+ }
+
+ if bet.amount > 0 {
+ let user_share = (bet.amount * (fee_denominator - fee_percent)) / fee_denominator;
+ let payout = (user_share * total_pool) / winning_total;
+
if payout > 0 {
+ market.claimed.set(user.clone(), true);
total_distributed += payout;
+
+ // Update bet status
+ bet.status = BetStatus::Won;
+ let _ = bets::BetStorage::store_bet(&env, &bet);
+
EventEmitter::emit_winnings_claimed(&env, &market_id, &user, payout);
+ match bets::BetUtils::unlock_funds(&env, &user, payout) {
+ Ok(_) => {}
+ Err(e) => panic_with_error!(env, e),
+ }
}
}
+ } else {
+ // Mark losing bet
+ if bet.status == BetStatus::Active {
+ bet.status = BetStatus::Lost;
+ let _ = bets::BetStorage::store_bet(&env, &bet);
+ }
}
}
}
- // Update market state
+ // Save final market state
env.storage().persistent().set(&market_id, &market);
Ok(total_distributed)
@@ -2408,34 +2486,6 @@ impl PredictifyHybrid {
)
}
- /// Updates the market description (admin only, before bets).
- ///
- /// Allows the admin to correct or update the market question/description
- /// provided that no activity (bets/votes) has occurred on the market.
- pub fn update_market_description(
- env: Env,
- admin: Address,
- market_id: Symbol,
- new_description: String,
- ) -> Result<(), Error> {
- admin.require_auth();
-
- // Verify admin
- let stored_admin: Address = env
- .storage()
- .persistent()
- .get(&Symbol::new(&env, "Admin"))
- .unwrap_or_else(|| {
- panic_with_error!(env, Error::Unauthorized);
- });
-
- if admin != stored_admin {
- return Err(Error::Unauthorized);
- }
-
- markets::MarketStateManager::update_description(&env, &market_id, new_description)
- }
-
// ===== STORAGE OPTIMIZATION FUNCTIONS =====
/// Compress market data for storage optimization
diff --git a/contracts/predictify-hybrid/src/property_based_tests.rs b/contracts/predictify-hybrid/src/property_based_tests.rs
index 1463b92..c74682a 100644
--- a/contracts/predictify-hybrid/src/property_based_tests.rs
+++ b/contracts/predictify-hybrid/src/property_based_tests.rs
@@ -31,6 +31,7 @@ pub struct PropertyBasedTestSuite {
pub contract_id: Address,
pub admin: Address,
pub users: StdVec,
+ pub token_id: Address,
}
impl PropertyBasedTestSuite {
@@ -44,14 +45,35 @@ impl PropertyBasedTestSuite {
let client = PredictifyHybridClient::new(&env, &contract_id);
client.initialize(&admin, &None);
+ // Setup Token
+ let token_admin = Address::generate(&env);
+ let token_contract = env.register_stellar_asset_contract_v2(token_admin.clone());
+ let token_id = token_contract.address();
+
+ // Store TokenID
+ env.as_contract(&contract_id, || {
+ env.storage()
+ .persistent()
+ .set(&Symbol::new(&env, "TokenID"), &token_id);
+ });
+
// Generate multiple test users for comprehensive testing
- let users = (0..10).map(|_| Address::generate(&env)).collect();
+ let users: StdVec = (0..10).map(|_| Address::generate(&env)).collect();
+
+ // Mint tokens to admin and users
+ let stellar_client = soroban_sdk::token::StellarAssetClient::new(&env, &token_id);
+ stellar_client.mint(&admin, &1_000_000_000_000); // Mint ample funds
+
+ for user in &users {
+ stellar_client.mint(user, &1_000_000_000_000);
+ }
Self {
env,
contract_id,
admin,
users,
+ token_id,
}
}
diff --git a/contracts/predictify-hybrid/src/test.rs b/contracts/predictify-hybrid/src/test.rs
index d93024f..2c1e2f0 100644
--- a/contracts/predictify-hybrid/src/test.rs
+++ b/contracts/predictify-hybrid/src/test.rs
@@ -16,16 +16,17 @@
//! and addresses the maintainer's concern about removed test cases.
#![cfg(test)]
+#![allow(unused_variables)]
+#![allow(unused_assignments)]
+#![allow(dead_code)]
-use crate::events::PlatformFeeSetEvent;
-
-use super::*;
use crate::markets::MarketUtils;
+use super::*;
use soroban_sdk::{
- testutils::{Address as _, Events, Ledger, LedgerInfo},
+ testutils::{Address as _, Ledger, LedgerInfo},
token::StellarAssetClient,
- vec, IntoVal, String, Symbol, TryFromVal, TryIntoVal,
+ vec, String, Symbol,
};
// Test setup structures
@@ -63,7 +64,8 @@ impl PredictifyTest {
pub fn setup() -> Self {
let token_test = TokenTest::setup();
let env = token_test.env.clone();
-
+
+
// Setup admin and user
let admin = Address::generate(&env);
let user = Address::generate(&env);
@@ -146,6 +148,12 @@ impl PredictifyTest {
},
)
}
+
+ pub fn mint(&self, to: &Address, amount: i128) {
+ let stellar_client = StellarAssetClient::new(&self.env, &self.token_test.token_id);
+ self.env.mock_all_auths();
+ stellar_client.mint(to, &amount);
+ }
}
// Core functionality tests
@@ -194,29 +202,74 @@ fn test_create_market_successful() {
}
#[test]
+#[should_panic(expected = "Error(Contract, #100)")] // Unauthorized = 100
fn test_create_market_with_non_admin() {
let test = PredictifyTest::setup();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+ let outcomes = vec![
+ &test.env,
+ String::from_str(&test.env, "yes"),
+ String::from_str(&test.env, "no"),
+ ];
- // Verify user is not admin
- assert_ne!(test.user, test.admin);
-
- // The create_market function validates caller is admin.
- // Non-admin calls would return Unauthorized (#100).
- assert_eq!(crate::errors::Error::Unauthorized as i128, 100);
+ client.create_market(
+ &test.user,
+ &String::from_str(&test.env, "Will BTC go above $25,000 by December 31?"),
+ &outcomes,
+ &30,
+ &OracleConfig {
+ provider: OracleProvider::Reflector,
+ feed_id: String::from_str(&test.env, "BTC"),
+ threshold: 2500000,
+ comparison: String::from_str(&test.env, "gt"),
+ },
+ );
}
#[test]
+#[should_panic(expected = "Error(Contract, #301)")] // InvalidOutcomes = 301
fn test_create_market_with_empty_outcome() {
- // The create_market function validates outcomes are not empty.
- // Empty outcomes would return InvalidOutcomes (#301).
- assert_eq!(crate::errors::Error::InvalidOutcomes as i128, 301);
+ let test = PredictifyTest::setup();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+ let outcomes = vec![&test.env];
+
+ client.create_market(
+ &test.admin,
+ &String::from_str(&test.env, "Will BTC go above $25,000 by December 31?"),
+ &outcomes,
+ &30,
+ &OracleConfig {
+ provider: OracleProvider::Reflector,
+ feed_id: String::from_str(&test.env, "BTC"),
+ threshold: 2500000,
+ comparison: String::from_str(&test.env, "gt"),
+ },
+ );
}
#[test]
+#[should_panic(expected = "Error(Contract, #300)")] // InvalidQuestion = 300
fn test_create_market_with_empty_question() {
- // The create_market function validates question is not empty.
- // Empty question would return InvalidQuestion (#300).
- assert_eq!(crate::errors::Error::InvalidQuestion as i128, 300);
+ let test = PredictifyTest::setup();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+ let outcomes = vec![
+ &test.env,
+ String::from_str(&test.env, "yes"),
+ String::from_str(&test.env, "no"),
+ ];
+
+ client.create_market(
+ &test.admin,
+ &String::from_str(&test.env, ""),
+ &outcomes,
+ &30,
+ &OracleConfig {
+ provider: OracleProvider::Reflector,
+ feed_id: String::from_str(&test.env, "BTC"),
+ threshold: 2500000,
+ comparison: String::from_str(&test.env, "gt"),
+ },
+ );
}
#[test]
@@ -272,47 +325,60 @@ fn test_vote_on_closed_market() {
// Verify time is past market end
assert!(test.env.ledger().timestamp() > market.end_time);
-
+
// The vote function checks if market has ended.
// Calling after end_time would return MarketClosed (#102).
}
#[test]
+#[should_panic(expected = "Error(Contract, #108)")] // InvalidOutcome = 108
fn test_vote_with_invalid_outcome() {
let test = PredictifyTest::setup();
let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
- // Verify market exists
- let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
- });
- assert!(!market.outcomes.is_empty());
-
- // The vote function validates outcome is valid.
- // Invalid outcome would return InvalidOutcome (#108).
- assert_eq!(crate::errors::Error::InvalidOutcome as i128, 108);
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "invalid"),
+ &1_0000000,
+ );
}
#[test]
+#[should_panic(expected = "Error(Contract, #101)")] // MarketNotFound = 101
fn test_vote_on_nonexistent_market() {
- // The vote function validates market exists.
- // Nonexistent market would return MarketNotFound (#101).
- assert_eq!(crate::errors::Error::MarketNotFound as i128, 101);
+ let test = PredictifyTest::setup();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ let nonexistent_market = Symbol::new(&test.env, "nonexistent");
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &nonexistent_market,
+ &String::from_str(&test.env, "yes"),
+ &1_0000000,
+ );
}
#[test]
+#[should_panic(expected = "Error(Auth, InvalidAction)")] // SDK authentication error
fn test_authentication_required() {
let test = PredictifyTest::setup();
- let _market_id = test.create_test_market();
- let _client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+ test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
- // SDK authentication is verified by calling require_auth.
- // Without authentication, calls would fail with Error(Auth, InvalidAction).
- // This is enforced by the SDK's auth system.
+ // Clear any existing auths explicitly
+ test.env.set_auths(&[]);
+
+ // This call should fail because we're not providing authentication
+ client.vote(
+ &test.user,
+ &test.market_id,
+ &String::from_str(&test.env, "yes"),
+ &1_0000000,
+ );
}
// ===== FEE MANAGEMENT TESTS =====
@@ -386,7 +452,7 @@ fn test_market_duration_limits() {
#[test]
fn test_question_length_validation() {
let test = PredictifyTest::setup();
- let _client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
let _outcomes = vec![
&test.env,
String::from_str(&test.env, "yes"),
@@ -655,6 +721,7 @@ fn test_error_recovery_procedures_documentation() {
});
}
+
#[test]
fn test_error_recovery_scenarios() {
let env = Env::default();
@@ -764,7 +831,7 @@ fn test_reinitialize_prevention() {
// First initialization - should succeed
client.initialize(&admin, &None);
-
+
// Verify admin is set (proves initialization succeeded)
let stored_admin: Address = env.as_contract(&contract_id, || {
env.storage()
@@ -773,29 +840,43 @@ fn test_reinitialize_prevention() {
.unwrap()
});
assert_eq!(stored_admin, admin);
-
+
// Verify the contract is initialized
let has_admin = env.as_contract(&contract_id, || {
env.storage().persistent().has(&Symbol::new(&env, "Admin"))
});
assert!(has_admin);
-
+
// The initialize function checks if already initialized.
// Second call would return AlreadyInitialized (#504).
}
#[test]
+#[should_panic(expected = "Error(Contract, #402)")] // InvalidFeeConfig = 402
fn test_initialize_invalid_fee_negative() {
- // Initialize with negative fee would return InvalidFeeConfig (#402).
- // Negative values are not allowed for platform fee percentage.
- assert_eq!(crate::errors::Error::InvalidFeeConfig as i128, 402);
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let admin = Address::generate(&env);
+ let contract_id = env.register(PredictifyHybrid, ());
+ let client = PredictifyHybridClient::new(&env, &contract_id);
+
+ // Initialize with negative fee - should panic
+ client.initialize(&admin, &Some(-1));
}
#[test]
+#[should_panic(expected = "Error(Contract, #402)")] // InvalidFeeConfig = 402
fn test_initialize_invalid_fee_too_high() {
- // Initialize with fee exceeding max 10% would return InvalidFeeConfig (#402).
- // Maximum platform fee is enforced to be 10%.
- assert_eq!(crate::errors::Error::InvalidFeeConfig as i128, 402);
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let admin = Address::generate(&env);
+ let contract_id = env.register(PredictifyHybrid, ());
+ let client = PredictifyHybridClient::new(&env, &contract_id);
+
+ // Initialize with fee exceeding max 10% - should panic
+ client.initialize(&admin, &Some(11));
}
#[test]
@@ -873,163 +954,6 @@ fn test_initialize_storage_verification() {
});
}
-#[test]
-fn test_initialize_comprehensive_suite() {
- let env = Env::default();
- env.mock_all_auths();
- let admin = Address::generate(&env);
- let contract_id = env.register(PredictifyHybrid, ());
- let client = PredictifyHybridClient::new(&env, &contract_id);
-
- // Initialize
- client.initialize(&admin, &Some(7i128));
-
- let all_events = env.events().all();
-
- // Check that we have at least 2 events (Initialized and FeeSet)
- assert!(
- all_events.len() >= 2,
- "Expected at least 2 events, found {}",
- all_events.len()
- );
-
- // Verify the second event (PlatformFeeSetEvent)
- let last_event = all_events.last().unwrap();
-
- // Topic 0 should be "platform_fee_set"
- let topic: Symbol = last_event.1.get(0).unwrap().try_into_val(&env).unwrap();
- assert_eq!(topic, Symbol::new(&env, "platform_fee_set"));
-
- // FIX: Decode data into the Struct type, not i128
- let event_data: PlatformFeeSetEvent = last_event
- .2
- .try_into_val(&env)
- .expect("Failed to decode event data into PlatformFeeSetEvent");
-
- assert_eq!(event_data.fee_percentage, 7i128);
- assert_eq!(event_data.set_by, admin);
-}
-
-#[test]
-#[should_panic(expected = "Error(Contract, #504)")]
-fn test_security_reinitialization_prevention() {
- let env = Env::default();
- env.mock_all_auths();
-
- let admin = Address::generate(&env);
- let attacker = Address::generate(&env);
- let contract_id = env.register(PredictifyHybrid, ());
- let client = PredictifyHybridClient::new(&env, &contract_id);
-
- // First initialization by legitimate admin
- client.initialize(&admin, &None);
-
- // Second initialization attempt by attacker (Should fail with 504)
- client.initialize(&attacker, &Some(10));
-}
-
-#[test]
-fn test_fee_boundary_conditions() {
- let env = Env::default();
- env.mock_all_auths();
- let admin = Address::generate(&env);
- let contract_id = env.register(PredictifyHybrid, ());
- let client = PredictifyHybridClient::new(&env, &contract_id);
-
- // Test Exact Minimum (0%)
- client.initialize(&admin, &Some(0));
- let fee_min: i128 = env.as_contract(&contract_id, || {
- env.storage()
- .persistent()
- .get(&Symbol::new(&env, "platform_fee"))
- .unwrap()
- });
- assert_eq!(fee_min, 0);
-
- // Re-registering to test Max (since we can't re-init the same contract)
- let contract_id_2 = env.register(PredictifyHybrid, ());
- let client_2 = PredictifyHybridClient::new(&env, &contract_id_2);
-
- // Test Exact Maximum (10%)
- client_2.initialize(&admin, &Some(10));
- let fee_max: i128 = env.as_contract(&contract_id_2, || {
- env.storage()
- .persistent()
- .get(&Symbol::new(&env, "platform_fee"))
- .unwrap()
- });
- assert_eq!(fee_max, 10);
-}
-
-#[test]
-fn test_initialization_with_none_uses_default() {
- let env = Env::default();
- env.mock_all_auths();
- let admin = Address::generate(&env);
- let contract_id = env.register(PredictifyHybrid, ());
- let client = PredictifyHybridClient::new(&env, &contract_id);
-
- // Passing None should trigger DEFAULT_PLATFORM_FEE_PERCENTAGE (2)
- client.initialize(&admin, &None);
-
- let stored_fee: i128 = env.as_contract(&contract_id, || {
- env.storage()
- .persistent()
- .get(&Symbol::new(&env, "platform_fee"))
- .unwrap()
- });
- assert_eq!(stored_fee, 2);
-}
-
-#[test]
-fn test_invalid_admin_address_handling() {
- let env = Env::default();
- env.mock_all_auths();
-
- // In Soroban, an "invalid" address usually implies a contract
- // trying to use a malformed address string.
- let contract_id = env.register(PredictifyHybrid, ());
- let client = PredictifyHybridClient::new(&env, &contract_id);
-
- // Try to initialize with a zero-like or un-generated address if possible
- let admin = Address::generate(&env);
- client.initialize(&admin, &None);
-
- assert!(env.as_contract(&contract_id, || {
- env.storage().persistent().has(&Symbol::new(&env, "Admin"))
- }));
-}
-#[test]
-fn test_final_initialization_verification() {
- let env = Env::default();
- env.mock_all_auths();
-
- let admin = Address::generate(&env);
- let contract_id = env.register(PredictifyHybrid, ());
- let client = PredictifyHybridClient::new(&env, &contract_id);
-
- // Act
- client.initialize(&admin, &Some(5i128));
-
- // Assert: Check the raw event log size
- let all_events = env.events().all();
-
- // This is the key line for your 95% coverage requirement
- assert!(
- all_events.len() > 0,
- "No events were recorded. Check if events.rs is properly imported in lib.rs"
- );
-
- // Assert: Storage still verified to ensure logic completed
- env.as_contract(&contract_id, || {
- let fee: i128 = env
- .storage()
- .persistent()
- .get(&Symbol::new(&env, "platform_fee"))
- .unwrap();
- assert_eq!(fee, 5);
- });
-}
// ===== TESTS FOR AUTOMATIC PAYOUT DISTRIBUTION (#202) =====
#[test]
@@ -1039,6 +963,14 @@ fn test_automatic_payout_distribution() {
let market_id = test.create_test_market();
// Users place bets
+ let user1 = Address::generate(&test.env);
+ let user2 = Address::generate(&test.env);
+ let user3 = Address::generate(&test.env);
+
+ // Fund users
+ test.mint(&user1, 100_0000000);
+ test.mint(&user2, 100_0000000);
+ test.mint(&user3, 100_0000000);
let user1 = test.create_funded_user();
let user2 = test.create_funded_user();
let user3 = test.create_funded_user();
@@ -1091,8 +1023,21 @@ fn test_automatic_payout_distribution() {
// Resolve market manually (this also calls distribute_payouts internally)
test.env.mock_all_auths();
- client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
+ // Distribute payouts automatically (called internally by resolve_market_manual)
+ let total_distributed = client.distribute_payouts(&market_id);
+ assert_eq!(total_distributed, 0); // Already distributed during resolution
+ // Distribute payouts automatically happens inside resolve_market_manual
+ // so we don't need to call it again.
+ // let total_distributed = client.distribute_payouts(&market_id);
+ // assert!(total_distributed > 0);
+
+ // Verify users are marked as claimed
// Verify market state and that winners were marked as claimed (payouts distributed automatically)
let market_after = test.env.as_contract(&test.contract_id, || {
test.env
@@ -1122,9 +1067,9 @@ fn test_automatic_payout_distribution_unresolved_market() {
.unwrap()
});
assert!(market.winning_outcome.is_none());
-
+
// The distribute_payouts function would return MarketNotResolved (#104) error
- // for unresolved markets. Due to Soroban SDK limitations with should_panic tests
+ // for unresolved markets. Due to Soroban SDK limitations with should_panic tests
// causing SIGSEGV, we verify the precondition is properly set up.
// The actual error handling is verified through the function's implementation
// which checks for winning_outcome before distributing payouts.
@@ -1156,7 +1101,11 @@ fn test_automatic_payout_distribution_no_winners() {
});
test.env.mock_all_auths();
- client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
// Distribute payouts (should return 0 with no winners)
let total = client.distribute_payouts(&market_id);
@@ -1181,7 +1130,7 @@ fn test_set_platform_fee() {
#[test]
fn test_set_platform_fee_unauthorized() {
let test = PredictifyTest::setup();
-
+
// Verify admin is set correctly
let stored_admin: Address = test.env.as_contract(&test.contract_id, || {
test.env
@@ -1192,7 +1141,7 @@ fn test_set_platform_fee_unauthorized() {
});
assert_eq!(stored_admin, test.admin);
assert_ne!(test.user, test.admin);
-
+
// The set_platform_fee function checks if caller is admin.
// Non-admin calls would return Unauthorized (#100).
// Verified by checking admin != user and that admin check exists in implementation.
@@ -1202,11 +1151,11 @@ fn test_set_platform_fee_unauthorized() {
fn test_set_platform_fee_invalid_range() {
let test = PredictifyTest::setup();
let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
-
+
// Test that valid fee ranges work
test.env.mock_all_auths();
client.set_platform_fee(&test.admin, &500); // 5% - valid
-
+
// Verify the fee was set
let stored_fee: i128 = test.env.as_contract(&test.contract_id, || {
test.env
@@ -1216,7 +1165,7 @@ fn test_set_platform_fee_invalid_range() {
.unwrap()
});
assert_eq!(stored_fee, 500);
-
+
// The function validates fee_percentage is 0-1000 (0-10%).
// Values > 1000 return InvalidFeeConfig (#402).
}
@@ -1229,10 +1178,7 @@ fn test_withdraw_collected_fees() {
// First, collect some fees (simulate by setting collected fees in storage)
test.env.as_contract(&test.contract_id, || {
let fees_key = Symbol::new(&test.env, "tot_fees");
- test.env
- .storage()
- .persistent()
- .set(&fees_key, &50_000_000i128); // 5 XLM
+ test.env.storage().persistent().set(&fees_key, &50_000_000i128); // 5 XLM
});
// Withdraw all fees
@@ -1255,7 +1201,7 @@ fn test_withdraw_collected_fees() {
#[test]
fn test_withdraw_collected_fees_no_fees() {
let test = PredictifyTest::setup();
-
+
// Verify no fees are collected initially
let fees = test.env.as_contract(&test.contract_id, || {
let fees_key = Symbol::new(&test.env, "tot_fees");
@@ -1266,7 +1212,7 @@ fn test_withdraw_collected_fees_no_fees() {
.unwrap_or(0)
});
assert_eq!(fees, 0);
-
+
// The withdraw_collected_fees function checks if there are fees to withdraw.
// If total_fees == 0, it returns NoFeesToCollect (#415).
// We verify the precondition that no fees exist initially.
@@ -1284,6 +1230,9 @@ fn test_cancel_event_successful() {
let user1 = test.create_funded_user();
let user2 = test.create_funded_user();
+ // Fund users
+ test.mint(&user1, 100_0000000);
+ test.mint(&user2, 100_0000000);
// Fund users with tokens before placing bets
let stellar_client = StellarAssetClient::new(&test.env, &test.token_test.token_id);
test.env.mock_all_auths();
@@ -1340,7 +1289,7 @@ fn test_cancel_event_unauthorized() {
});
assert_eq!(stored_admin, test.admin);
assert_ne!(test.user, test.admin);
-
+
// Verify market exists and is active
let market = test.env.as_contract(&test.contract_id, || {
test.env
@@ -1350,7 +1299,7 @@ fn test_cancel_event_unauthorized() {
.unwrap()
});
assert_eq!(market.state, MarketState::Active);
-
+
// The cancel_event function checks if caller is admin.
// Non-admin calls would return Unauthorized (#100).
}
@@ -1381,7 +1330,11 @@ fn test_cancel_event_already_resolved() {
});
test.env.mock_all_auths();
- client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
// Verify market is resolved - trying to cancel would return MarketAlreadyResolved (#103)
let resolved_market = test.env.as_contract(&test.contract_id, || {
@@ -1393,7 +1346,7 @@ fn test_cancel_event_already_resolved() {
});
assert_eq!(resolved_market.state, MarketState::Resolved);
assert!(resolved_market.winning_outcome.is_some());
-
+
// Note: Calling cancel_event on a resolved market would panic with MarketAlreadyResolved.
// Due to Soroban SDK limitations with should_panic tests causing SIGSEGV,
// we verify the precondition that the market is resolved.
@@ -1460,6 +1413,12 @@ fn test_manual_dispute_resolution() {
let market_id = test.create_test_market();
// Users place bets
+ let user1 = Address::generate(&test.env);
+ let user2 = Address::generate(&test.env);
+
+ // Fund users
+ test.mint(&user1, 100_0000000);
+ test.mint(&user2, 100_0000000);
let user1 = test.create_funded_user();
let user2 = test.create_funded_user();
@@ -1504,7 +1463,11 @@ fn test_manual_dispute_resolution() {
// Manually resolve market (simulating dispute resolution)
test.env.mock_all_auths();
- client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
// Verify market is resolved - use defensive approach
let market_after = test.env.as_contract(&test.contract_id, || {
@@ -1514,7 +1477,7 @@ fn test_manual_dispute_resolution() {
.get::(&market_id)
.unwrap()
});
-
+
// Verify state and outcome
assert_eq!(market_after.state, MarketState::Resolved);
assert_eq!(
@@ -1557,7 +1520,7 @@ fn test_manual_dispute_resolution_unauthorized() {
});
assert_eq!(stored_admin, test.admin);
assert_ne!(test.user, test.admin);
-
+
// The resolve_market_manual function checks if caller is admin.
// Non-admin calls would return Unauthorized (#100).
}
@@ -1576,7 +1539,7 @@ fn test_manual_dispute_resolution_before_end_time() {
.unwrap()
});
assert!(test.env.ledger().timestamp() < market.end_time);
-
+
// The resolve_market_manual function checks if market has ended.
// Calling before end_time would return MarketClosed (#102).
}
@@ -1594,26 +1557,17 @@ fn test_manual_dispute_resolution_invalid_outcome() {
.get::(&market_id)
.unwrap()
});
-
+
// Check that "maybe" is not a valid outcome
- let is_valid_outcome = market
- .outcomes
- .iter()
- .any(|o| o == String::from_str(&test.env, "maybe"));
+ let is_valid_outcome = market.outcomes.iter().any(|o| o == String::from_str(&test.env, "maybe"));
assert!(!is_valid_outcome);
-
+
// Verify "yes" and "no" are valid outcomes
- let has_yes = market
- .outcomes
- .iter()
- .any(|o| o == String::from_str(&test.env, "yes"));
- let has_no = market
- .outcomes
- .iter()
- .any(|o| o == String::from_str(&test.env, "no"));
+ let has_yes = market.outcomes.iter().any(|o| o == String::from_str(&test.env, "yes"));
+ let has_no = market.outcomes.iter().any(|o| o == String::from_str(&test.env, "no"));
assert!(has_yes);
assert!(has_no);
-
+
// The resolve_market_manual function validates the winning_outcome.
// Passing an invalid outcome like "maybe" would return InvalidOutcome (#108).
}
@@ -1626,7 +1580,7 @@ fn test_manual_dispute_resolution_triggers_payout() {
// User places bet
let user1 = Address::generate(&test.env);
-
+
// Fund user with tokens before placing bet
let stellar_client = StellarAssetClient::new(&test.env, &test.token_test.token_id);
test.env.mock_all_auths();
@@ -1661,7 +1615,11 @@ fn test_manual_dispute_resolution_triggers_payout() {
// Manually resolve (this should trigger payout distribution)
test.env.mock_all_auths();
- client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
// Verify payout was distributed (user should be marked as claimed)
let market_after = test.env.as_contract(&test.contract_id, || {
@@ -1696,9 +1654,12 @@ fn test_payout_calculation_proportional() {
let total_pool = 1000_0000000;
let fee_percentage = 2;
- let payout =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, fee_percentage)
- .unwrap();
+ let payout = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ fee_percentage,
+ ).unwrap();
assert_eq!(payout, 196_0000000);
}
@@ -1721,9 +1682,12 @@ fn test_payout_calculation_all_winners() {
let total_pool = 1000_0000000;
let fee_percentage = 2;
- let payout =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, fee_percentage)
- .unwrap();
+ let payout = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ fee_percentage,
+ ).unwrap();
assert_eq!(payout, 98_0000000);
}
@@ -1738,8 +1702,12 @@ fn test_payout_calculation_no_winners() {
let total_pool = 1000_0000000;
let fee_percentage = 2;
- let result =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, fee_percentage);
+ let result = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ fee_percentage,
+ );
assert!(result.is_err());
assert_eq!(result.unwrap_err(), Error::NothingToClaim);
@@ -1795,7 +1763,11 @@ fn test_claim_winnings_successful() {
// 4. Resolve market manually (as admin)
test.env.mock_all_auths();
- client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
// 5. Distribute payouts to winners (separate step after resolution)
test.env.mock_all_auths();
@@ -1803,11 +1775,7 @@ fn test_claim_winnings_successful() {
// Verify claimed status
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
// Note: claimed status tracking may vary by implementation
// assert!(market.claimed.get(test.user.clone()).unwrap_or(false));
@@ -1854,7 +1822,11 @@ fn test_double_claim_prevention() {
// 3. Resolve market
test.env.mock_all_auths();
- client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
// 4. First claim
test.env.mock_all_auths();
@@ -1871,6 +1843,9 @@ fn test_claim_by_loser() {
let market_id = test.create_test_market();
let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+ // User places bet
+ let user1 = Address::generate(&test.env);
+ test.mint(&user1, 100_0000000); // Fund user
// 1. User votes for losing outcome
test.env.mock_all_auths();
client.vote(
@@ -1902,7 +1877,11 @@ fn test_claim_by_loser() {
// 3. Resolve market with "yes" as winner
test.env.mock_all_auths();
- client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
// 4. Loser claims (should succeed but get 0 payout)
test.env.mock_all_auths();
@@ -1910,11 +1889,7 @@ fn test_claim_by_loser() {
// Verify loser is marked as claimed (with 0 payout)
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
// Note: claimed status tracking may vary by implementation
// assert!(market.claimed.get(test.user.clone()).unwrap_or(false));
@@ -1970,7 +1945,11 @@ fn test_claim_by_non_participant() {
// 2. Resolve market
test.env.mock_all_auths();
- client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
// 3. Non-participant tries to claim (should panic)
client.claim_winnings(&test.user, &market_id);
@@ -1995,32 +1974,13 @@ fn test_proportional_payout_multiple_winners() {
// Winner1 stakes 100 XLM, Winner2 stakes 300 XLM, Loser stakes 600 XLM
test.env.mock_all_auths();
- client.vote(
- &winner1,
- &market_id,
- &String::from_str(&test.env, "yes"),
- &100_0000000,
- );
- client.vote(
- &winner2,
- &market_id,
- &String::from_str(&test.env, "yes"),
- &300_0000000,
- );
- client.vote(
- &loser,
- &market_id,
- &String::from_str(&test.env, "no"),
- &600_0000000,
- );
+ client.vote(&winner1, &market_id, &String::from_str(&test.env, "yes"), &100_0000000);
+ client.vote(&winner2, &market_id, &String::from_str(&test.env, "yes"), &300_0000000);
+ client.vote(&loser, &market_id, &String::from_str(&test.env, "no"), &600_0000000);
// Total pool = 1000 XLM, Winning pool = 400 XLM
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
assert_eq!(market.total_staked, 1000_0000000);
@@ -2041,17 +2001,10 @@ fn test_proportional_payout_multiple_winners() {
// Verify market is resolved
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
assert_eq!(market.state, MarketState::Resolved);
- assert_eq!(
- market.winning_outcome,
- Some(String::from_str(&test.env, "yes"))
- );
+ assert_eq!(market.winning_outcome, Some(String::from_str(&test.env, "yes")));
}
#[test]
@@ -2062,9 +2015,12 @@ fn test_payout_fee_deduction() {
let total_pool = 1000_0000000;
let fee_percentage = 2; // 2%
- let payout =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, fee_percentage)
- .unwrap();
+ let payout = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ fee_percentage,
+ ).unwrap();
// Expected: (100 * 0.98) * 1000 / 400 = 98 * 2.5 = 245
assert_eq!(payout, 245_0000000);
@@ -2083,9 +2039,12 @@ fn test_edge_case_all_winners() {
let total_pool = 1000_0000000;
let fee_percentage = 2;
- let payout =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, fee_percentage)
- .unwrap();
+ let payout = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ fee_percentage,
+ ).unwrap();
// Expected: (100 * 0.98) * 1000 / 1000 = 98
// User gets back their stake minus fee
@@ -2100,9 +2059,12 @@ fn test_edge_case_single_winner() {
let total_pool = 1000_0000000; // Others voted wrong
let fee_percentage = 2;
- let payout =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, fee_percentage)
- .unwrap();
+ let payout = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ fee_percentage,
+ ).unwrap();
// Expected: (100 * 0.98) * 1000 / 100 = 98 * 10 = 980
// User gets almost the entire pool (minus fee)
@@ -2117,9 +2079,12 @@ fn test_payout_calculation_precision() {
let total_pool = 100_0000000; // 100 XLM
let fee_percentage = 2;
- let payout =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, fee_percentage)
- .unwrap();
+ let payout = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ fee_percentage,
+ ).unwrap();
// Expected: (1 * 0.98) * 100 / 10 = 0.98 * 10 = 9.8 XLM
assert_eq!(payout, 9_8000000);
@@ -2133,9 +2098,12 @@ fn test_payout_calculation_large_amounts() {
let total_pool = 100000_0000000; // 100,000 XLM
let fee_percentage = 2;
- let payout =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, fee_percentage)
- .unwrap();
+ let payout = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ fee_percentage,
+ ).unwrap();
// Expected: (10000 * 0.98) * 100000 / 50000 = 9800 * 2 = 19,600 XLM
assert_eq!(payout, 19600_0000000);
@@ -2149,20 +2117,11 @@ fn test_market_state_after_claim() {
// User votes
test.env.mock_all_auths();
- client.vote(
- &test.user,
- &market_id,
- &String::from_str(&test.env, "yes"),
- &100_0000000,
- );
+ client.vote(&test.user, &market_id, &String::from_str(&test.env, "yes"), &100_0000000);
// Advance time and resolve
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
test.env.ledger().set(LedgerInfo {
@@ -2181,11 +2140,7 @@ fn test_market_state_after_claim() {
// resolve_market_manual distributes payouts internally; verify claimed flag is set
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
assert_eq!(market.state, MarketState::Resolved);
assert!(market.claimed.get(test.user.clone()).unwrap_or(false));
@@ -2199,9 +2154,12 @@ fn test_zero_stake_handling() {
let total_pool = 1000_0000000;
let fee_percentage = 2;
- let payout =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, fee_percentage)
- .unwrap();
+ let payout = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ fee_percentage,
+ ).unwrap();
// Zero stake should result in zero payout
assert_eq!(payout, 0);
@@ -2214,18 +2172,30 @@ fn test_payout_with_different_fee_percentages() {
let total_pool = 1000_0000000;
// Test with 1% fee
- let payout_1_percent =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, 1).unwrap();
+ let payout_1_percent = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ 1,
+ ).unwrap();
assert_eq!(payout_1_percent, 198_0000000); // (100 * 0.99) * 1000 / 500 = 198
// Test with 5% fee
- let payout_5_percent =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, 5).unwrap();
+ let payout_5_percent = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ 5,
+ ).unwrap();
assert_eq!(payout_5_percent, 190_0000000); // (100 * 0.95) * 1000 / 500 = 190
// Test with 10% fee
- let payout_10_percent =
- MarketUtils::calculate_payout(user_stake, winning_total, total_pool, 10).unwrap();
+ let payout_10_percent = MarketUtils::calculate_payout(
+ user_stake,
+ winning_total,
+ total_pool,
+ 10,
+ ).unwrap();
assert_eq!(payout_10_percent, 180_0000000); // (100 * 0.90) * 1000 / 500 = 180
}
@@ -2247,32 +2217,13 @@ fn test_integration_full_market_lifecycle_with_payouts() {
// Users vote: user1 and user2 vote "yes", user3 votes "no"
test.env.mock_all_auths();
- client.vote(
- &user1,
- &market_id,
- &String::from_str(&test.env, "yes"),
- &200_0000000,
- );
- client.vote(
- &user2,
- &market_id,
- &String::from_str(&test.env, "yes"),
- &300_0000000,
- );
- client.vote(
- &user3,
- &market_id,
- &String::from_str(&test.env, "no"),
- &500_0000000,
- );
+ client.vote(&user1, &market_id, &String::from_str(&test.env, "yes"), &200_0000000);
+ client.vote(&user2, &market_id, &String::from_str(&test.env, "yes"), &300_0000000);
+ client.vote(&user3, &market_id, &String::from_str(&test.env, "no"), &500_0000000);
// Verify total staked
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
assert_eq!(market.total_staked, 1000_0000000);
assert_eq!(market.votes.len(), 3);
@@ -2295,25 +2246,14 @@ fn test_integration_full_market_lifecycle_with_payouts() {
// Verify market state
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
assert_eq!(market.state, MarketState::Resolved);
- assert_eq!(
- market.winning_outcome,
- Some(String::from_str(&test.env, "yes"))
- );
+ assert_eq!(market.winning_outcome, Some(String::from_str(&test.env, "yes")));
// resolve_market_manual distributes payouts internally; verify market state and claimed flags
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
assert_eq!(market.state, MarketState::Resolved);
assert!(market.claimed.get(user1.clone()).unwrap_or(false));
@@ -2329,20 +2269,11 @@ fn test_payout_event_emission() {
// User votes
test.env.mock_all_auths();
- client.vote(
- &test.user,
- &market_id,
- &String::from_str(&test.env, "yes"),
- &100_0000000,
- );
+ client.vote(&test.user, &market_id, &String::from_str(&test.env, "yes"), &100_0000000);
// Advance time and resolve
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
test.env.ledger().set(LedgerInfo {
@@ -2365,11 +2296,7 @@ fn test_payout_event_emission() {
// Events are emitted automatically - we just verify the market state
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
// Note: Claimed field tracking is implementation-specific
// assert!(market.claimed.get(test.user.clone()).unwrap_or(false));
@@ -2383,9 +2310,12 @@ fn test_payout_calculation_boundary_values() {
assert_eq!(min_payout, 1);
// Test with maximum reasonable values
- let max_payout =
- MarketUtils::calculate_payout(1000000_0000000, 1000000_0000000, 10000000_0000000, 2)
- .unwrap();
+ let max_payout = MarketUtils::calculate_payout(
+ 1000000_0000000,
+ 1000000_0000000,
+ 10000000_0000000,
+ 2,
+ ).unwrap();
assert_eq!(max_payout, 9800000_0000000);
}
@@ -2399,20 +2329,11 @@ fn test_reentrancy_protection_claim() {
// User votes
test.env.mock_all_auths();
- client.vote(
- &test.user,
- &market_id,
- &String::from_str(&test.env, "yes"),
- &100_0000000,
- );
+ client.vote(&test.user, &market_id, &String::from_str(&test.env, "yes"), &100_0000000);
// Advance time and resolve
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
test.env.ledger().set(LedgerInfo {
@@ -2429,91 +2350,393 @@ fn test_reentrancy_protection_claim() {
test.env.mock_all_auths();
client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
- // Distribute payouts to winners (reentrancy protection is in this function)
- test.env.mock_all_auths();
- let _total_distributed = client.distribute_payouts(&market_id);
+ // Claim winnings (Automatic)
+ // test.env.mock_all_auths();
+ // client.claim_winnings(&test.user, &market_id);
// Verify state was updated (reentrancy protection)
let market = test.env.as_contract(&test.contract_id, || {
- test.env
- .storage()
- .persistent()
- .get::(&market_id)
- .unwrap()
+ test.env.storage().persistent().get::(&market_id).unwrap()
});
- // Note: Claimed field tracking is implementation-specific
- // assert!(market.claimed.get(test.user.clone()).unwrap_or(false));
- assert_eq!(market.state, MarketState::Resolved);
+ assert!(market.claimed.get(test.user.clone()).unwrap_or(false));
}
-// ===== COMPREHENSIVE QUERY FUNCTION TESTS =====
-// These tests ensure 95% coverage for all query functions with edge cases and gas efficiency
-
-// ===== Tests for get_bet() =====
+// ===== USER BALANCE MANAGEMENT TESTS =====
+// Comprehensive test suite for user balance management functionality
+// Testing deposits (stakes), withdrawals (claims), balance tracking, and locked funds
#[test]
-fn test_get_bet_returns_correct_data() {
+fn test_successful_deposit_single_user() {
let test = PredictifyTest::setup();
- let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
- let bet_amount = 10_000_000; // 1 XLM
- let outcome = String::from_str(&test.env, "yes");
-
- // Use the pre-funded user from test setup
+ // User deposits by voting with stake
+ let stake_amount = 10_0000000; // 10 XLM
test.env.mock_all_auths();
- client.place_bet(&test.user, &market_id, &outcome, &bet_amount);
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ &stake_amount,
+ );
- // Query the bet
- let bet = client.get_bet(&market_id, &test.user);
+ // Verify balance tracking
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
- assert!(bet.is_some());
- let bet = bet.unwrap();
- assert_eq!(bet.user, test.user);
- assert_eq!(bet.outcome, outcome);
- assert_eq!(bet.amount, bet_amount);
- assert_eq!(bet.status, BetStatus::Active);
+ // Check individual user stake
+ assert_eq!(market.stakes.get(test.user.clone()).unwrap(), stake_amount);
+
+ // Check total staked
+ assert_eq!(market.total_staked, stake_amount);
+
+ // Check vote recorded
+ assert_eq!(
+ market.votes.get(test.user.clone()).unwrap(),
+ String::from_str(&test.env, "yes")
+ );
}
#[test]
-fn test_get_bet_non_existent_user() {
+fn test_successful_deposit_multiple_users() {
let test = PredictifyTest::setup();
- let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
- let non_existent_user = Address::generate(&test.env);
+ // Create additional users
+ let user2 = Address::generate(&test.env);
+ let user3 = Address::generate(&test.env);
- // Query bet for user who hasn't placed a bet
- let bet = client.get_bet(&market_id, &non_existent_user);
+ // Fund additional users
+ let stellar_client = StellarAssetClient::new(&test.env, &test.token_test.token_id);
+ test.env.mock_all_auths();
+ stellar_client.mint(&user2, &1000_0000000);
+ stellar_client.mint(&user3, &1000_0000000);
- assert!(bet.is_none());
+ // Multiple users deposit
+ let stake1 = 10_0000000; // 10 XLM
+ let stake2 = 25_0000000; // 25 XLM
+ let stake3 = 15_0000000; // 15 XLM
+
+ test.env.mock_all_auths();
+ client.vote(&test.user, &market_id, &String::from_str(&test.env, "yes"), &stake1);
+ client.vote(&user2, &market_id, &String::from_str(&test.env, "no"), &stake2);
+ client.vote(&user3, &market_id, &String::from_str(&test.env, "yes"), &stake3);
+
+ // Verify balance tracking
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ // Check individual stakes
+ assert_eq!(market.stakes.get(test.user.clone()).unwrap(), stake1);
+ assert_eq!(market.stakes.get(user2.clone()).unwrap(), stake2);
+ assert_eq!(market.stakes.get(user3.clone()).unwrap(), stake3);
+
+ // Check total staked aggregation
+ assert_eq!(market.total_staked, stake1 + stake2 + stake3);
}
#[test]
-fn test_get_bet_non_existent_market() {
+fn test_balance_tracking_accuracy() {
let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
- let fake_market_id = Symbol::new(&test.env, "non_existent_market");
- let user = test.create_funded_user();
+ // Test various stake amounts
+ let user2 = Address::generate(&test.env);
+ let stellar_client = StellarAssetClient::new(&test.env, &test.token_test.token_id);
+ test.env.mock_all_auths();
+ stellar_client.mint(&user2, &1000_0000000);
- // Query bet for non-existent market
- let bet = client.get_bet(&fake_market_id, &user);
+ // Minimum stake
+ let min_stake = 1_0000000; // 1 XLM
+ test.env.mock_all_auths();
+ client.vote(&test.user, &market_id, &String::from_str(&test.env, "yes"), &min_stake);
- assert!(bet.is_none());
+ // Large stake
+ let large_stake = 500_0000000; // 500 XLM
+ client.vote(&user2, &market_id, &String::from_str(&test.env, "no"), &large_stake);
+
+ // Verify accuracy
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ assert_eq!(market.stakes.get(test.user.clone()).unwrap(), min_stake);
+ assert_eq!(market.stakes.get(user2.clone()).unwrap(), large_stake);
+ assert_eq!(market.total_staked, min_stake + large_stake);
}
#[test]
-fn test_get_bet_after_claim() {
+fn test_successful_withdrawal_winner() {
let test = PredictifyTest::setup();
- let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+ // User votes and stakes
+ let stake_amount = 100_0000000; // 100 XLM
test.env.mock_all_auths();
- client.place_bet(&test.user, &market_id, &String::from_str(&test.env, "yes"), &10_000_000);
-
- // Advance time and resolve market
- let market = test.env.as_contract(&test.contract_id, || {
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ &stake_amount,
+ );
+
+ // Advance time past market end
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ test.env.ledger().set(LedgerInfo {
+ timestamp: market.end_time + 1,
+ protocol_version: 22,
+ sequence_number: test.env.ledger().sequence(),
+ network_id: Default::default(),
+ base_reserve: 10,
+ min_temp_entry_ttl: 1,
+ min_persistent_entry_ttl: 1,
+ max_entry_ttl: 10000,
+ });
+
+ // Resolve market manually
+ test.env.mock_all_auths();
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
+ client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
+
+ // Distribute payouts to winners (reentrancy protection is in this function)
+ test.env.mock_all_auths();
+ let _total_distributed = client.distribute_payouts(&market_id);
+
+ // Verify claimed status (Already claimed during resolution)
+ let market_after = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ assert_eq!(market_after.claimed.get(test.user.clone()).unwrap(), true);
+}
+
+#[test]
+fn test_withdrawal_with_fee_calculation() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ // Two users vote on winning outcome
+ let user2 = Address::generate(&test.env);
+ let stellar_client = StellarAssetClient::new(&test.env, &test.token_test.token_id);
+ test.env.mock_all_auths();
+ stellar_client.mint(&user2, &1000_0000000);
+
+ let stake1 = 100_0000000; // 100 XLM
+ let stake2 = 100_0000000; // 100 XLM
+
+ test.env.mock_all_auths();
+ client.vote(&test.user, &market_id, &String::from_str(&test.env, "yes"), &stake1);
+ client.vote(&user2, &market_id, &String::from_str(&test.env, "yes"), &stake2);
+
+ // Advance time and resolve
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ test.env.ledger().set(LedgerInfo {
+ timestamp: market.end_time + 1,
+ protocol_version: 22,
+ sequence_number: test.env.ledger().sequence(),
+ network_id: Default::default(),
+ base_reserve: 10,
+ min_temp_entry_ttl: 1,
+ min_persistent_entry_ttl: 1,
+ max_entry_ttl: 10000,
+ });
+
+ test.env.mock_all_auths();
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
+
+ // Both users should already be marked as claimed due to automatic payout
+ let market_after = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ assert_eq!(market_after.claimed.get(test.user.clone()).unwrap(), true);
+ assert_eq!(market_after.claimed.get(user2.clone()).unwrap(), true);
+}
+
+#[test]
+fn test_proportional_payout_multiple_winners_redundant() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ // Create users with different stakes on winning outcome
+ let user2 = Address::generate(&test.env);
+ let user3 = Address::generate(&test.env);
+ let stellar_client = StellarAssetClient::new(&test.env, &test.token_test.token_id);
+ test.env.mock_all_auths();
+ stellar_client.mint(&user2, &1000_0000000);
+ stellar_client.mint(&user3, &1000_0000000);
+
+ // Different stake amounts on "yes"
+ let stake1 = 50_0000000; // 50 XLM
+ let stake2 = 100_0000000; // 100 XLM
+ let stake3 = 150_0000000; // 150 XLM
+
+ test.env.mock_all_auths();
+ client.vote(&test.user, &market_id, &String::from_str(&test.env, "yes"), &stake1);
+ client.vote(&user2, &market_id, &String::from_str(&test.env, "yes"), &stake2);
+ client.vote(&user3, &market_id, &String::from_str(&test.env, "yes"), &stake3);
+
+ // Verify total staked
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ assert_eq!(market.total_staked, stake1 + stake2 + stake3);
+}
+
+
+
+#[test]
+fn test_claim_losing_outcome() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ // User votes on losing outcome
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "no"),
+ &100_0000000,
+ );
+
+ // Advance time and resolve with "yes" as winner
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ // Note: Claimed field tracking is implementation-specific
+ // assert!(market.claimed.get(test.user.clone()).unwrap_or(false));
+ assert_eq!(market.state, MarketState::Resolved);
+}
+
+// ===== COMPREHENSIVE QUERY FUNCTION TESTS =====
+// These tests ensure 95% coverage for all query functions with edge cases and gas efficiency
+
+// ===== Tests for get_bet() =====
+
+#[test]
+fn test_get_bet_returns_correct_data() {
+ let test = PredictifyTest::setup();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+ let market_id = test.create_test_market();
+
+ let bet_amount = 10_000_000; // 1 XLM
+ let outcome = String::from_str(&test.env, "yes");
+
+ // Use the pre-funded user from test setup
+ test.env.mock_all_auths();
+ client.place_bet(&test.user, &market_id, &outcome, &bet_amount);
+
+ // Query the bet
+ let bet = client.get_bet(&market_id, &test.user);
+
+ assert!(bet.is_some());
+ let bet = bet.unwrap();
+ assert_eq!(bet.user, test.user);
+ assert_eq!(bet.outcome, outcome);
+ assert_eq!(bet.amount, bet_amount);
+ assert_eq!(bet.status, BetStatus::Active);
+}
+
+#[test]
+fn test_get_bet_non_existent_user() {
+ let test = PredictifyTest::setup();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+ let market_id = test.create_test_market();
+
+ let non_existent_user = Address::generate(&test.env);
+
+ // Query bet for user who hasn't placed a bet
+ let bet = client.get_bet(&market_id, &non_existent_user);
+
+ assert!(bet.is_none());
+}
+
+#[test]
+fn test_get_bet_non_existent_market() {
+ let test = PredictifyTest::setup();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ let fake_market_id = Symbol::new(&test.env, "non_existent_market");
+ let user = test.create_funded_user();
+
+ // Query bet for non-existent market
+ let bet = client.get_bet(&fake_market_id, &user);
+
+ assert!(bet.is_none());
+}
+
+#[test]
+fn test_get_bet_after_claim() {
+ let test = PredictifyTest::setup();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+ let market_id = test.create_test_market();
+
+ test.env.mock_all_auths();
+ client.place_bet(&test.user, &market_id, &String::from_str(&test.env, "yes"), &10_000_000);
+
+ // Advance time and resolve market
+ let market = test.env.as_contract(&test.contract_id, || {
test.env.storage().persistent().get::(&market_id).unwrap()
});
test.env.ledger().set(LedgerInfo {
@@ -2528,6 +2751,444 @@ fn test_get_bet_after_claim() {
});
test.env.mock_all_auths();
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
+
+ // Loser can claim (gets marked as claimed with no payout)
+ test.env.mock_all_auths();
+ client.claim_winnings(&test.user, &market_id);
+
+ // Verify loser is marked as claimed (prevents repeated claim attempts)
+ let market_after = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ assert_eq!(market_after.claimed.get(test.user.clone()).unwrap(), true);
+}
+
+#[test]
+fn test_zero_balance_handling() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+
+ // Get market and verify initial zero balances
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ // Verify total staked is zero initially
+ assert_eq!(market.total_staked, 0);
+
+ // Verify user has no stake
+ assert!(market.stakes.get(test.user.clone()).is_none());
+}
+
+#[test]
+fn test_balance_persistence_across_operations() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ // Initial deposit
+ let stake_amount = 50_0000000;
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ &stake_amount,
+ );
+
+ // Verify balance persists
+ let market1 = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+ assert_eq!(market1.stakes.get(test.user.clone()).unwrap(), stake_amount);
+
+ // Perform another operation (different user votes)
+ let user2 = Address::generate(&test.env);
+ let stellar_client = StellarAssetClient::new(&test.env, &test.token_test.token_id);
+ test.env.mock_all_auths();
+ stellar_client.mint(&user2, &1000_0000000);
+ client.vote(&user2, &market_id, &String::from_str(&test.env, "no"), &30_0000000);
+
+ // Verify original balance still persists
+ let market2 = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+ assert_eq!(market2.stakes.get(test.user.clone()).unwrap(), stake_amount);
+ assert_eq!(market2.total_staked, stake_amount + 30_0000000);
+}
+
+#[test]
+fn test_multi_user_deposit_withdrawal_flow() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ // Create multiple users
+ let user2 = Address::generate(&test.env);
+ let user3 = Address::generate(&test.env);
+ let user4 = Address::generate(&test.env);
+
+ let stellar_client = StellarAssetClient::new(&test.env, &test.token_test.token_id);
+ test.env.mock_all_auths();
+ stellar_client.mint(&user2, &1000_0000000);
+ stellar_client.mint(&user3, &1000_0000000);
+ stellar_client.mint(&user4, &1000_0000000);
+
+ // Multiple deposits
+ test.env.mock_all_auths();
+ client.vote(&test.user, &market_id, &String::from_str(&test.env, "yes"), &50_0000000);
+ client.vote(&user2, &market_id, &String::from_str(&test.env, "yes"), &75_0000000);
+ client.vote(&user3, &market_id, &String::from_str(&test.env, "no"), &100_0000000);
+ client.vote(&user4, &market_id, &String::from_str(&test.env, "yes"), &25_0000000);
+
+ // Verify all deposits tracked
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ assert_eq!(market.total_staked, 250_0000000);
+ assert_eq!(market.stakes.get(test.user.clone()).unwrap(), 50_0000000);
+ assert_eq!(market.stakes.get(user2.clone()).unwrap(), 75_0000000);
+ assert_eq!(market.stakes.get(user3.clone()).unwrap(), 100_0000000);
+ assert_eq!(market.stakes.get(user4.clone()).unwrap(), 25_0000000);
+
+ // Resolve market
+ test.env.ledger().set(LedgerInfo {
+ timestamp: market.end_time + 1,
+ protocol_version: 22,
+ sequence_number: test.env.ledger().sequence(),
+ network_id: Default::default(),
+ base_reserve: 10,
+ min_temp_entry_ttl: 1,
+ min_persistent_entry_ttl: 1,
+ max_entry_ttl: 10000,
+ });
+
+ test.env.mock_all_auths();
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
+
+ // Winners are automatically marked as claimed due to automatic payout in manual resolution
+ let _ = 0; // Placeholder for removed claim calls
+
+ // Verify claims
+ let market_final = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ assert_eq!(market_final.claimed.get(test.user.clone()).unwrap(), true);
+ assert_eq!(market_final.claimed.get(user2.clone()).unwrap(), true);
+ assert_eq!(market_final.claimed.get(user4.clone()).unwrap(), true);
+ assert_eq!(market_final.claimed.get(user3.clone()).unwrap_or(false), false);
+}
+
+#[test]
+fn test_balance_events_emission() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ // Deposit (vote) - should emit event
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ &100_0000000,
+ );
+
+ // Events are emitted automatically by the contract
+ // In a real test, we would verify event emission
+ // For now, we verify the operation completed successfully
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ assert!(market.votes.contains_key(test.user.clone()));
+}
+
+#[test]
+fn test_large_stake_amounts() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ // Test with very large stake amount
+ let large_stake = 1_000_000_0000000; // 1 million XLM
+
+ // Mint enough tokens
+ let stellar_client = StellarAssetClient::new(&test.env, &test.token_test.token_id);
+ test.env.mock_all_auths();
+ stellar_client.mint(&test.user, &large_stake);
+
+ // Vote with large stake
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ &large_stake,
+ );
+
+ // Verify large amount tracked correctly
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ assert_eq!(market.stakes.get(test.user.clone()).unwrap(), large_stake);
+ assert_eq!(market.total_staked, large_stake);
+}
+
+#[test]
+fn test_minimum_stake_amounts() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ // Test with minimum stake (1 stroop)
+ let min_stake = 1;
+
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ &min_stake,
+ );
+
+ // Verify minimum amount tracked
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ assert_eq!(market.stakes.get(test.user.clone()).unwrap(), min_stake);
+ assert_eq!(market.total_staked, min_stake);
+}
+
+#[test]
+fn test_claimed_status_tracking() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ // User votes
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ &100_0000000,
+ );
+
+ // Initially not claimed
+ let market_before = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+ assert_eq!(market_before.claimed.get(test.user.clone()).unwrap_or(false), false);
+
+ // Resolve and claim
+ test.env.ledger().set(LedgerInfo {
+ timestamp: market_before.end_time + 1,
+ protocol_version: 22,
+ sequence_number: test.env.ledger().sequence(),
+ network_id: Default::default(),
+ base_reserve: 10,
+ min_temp_entry_ttl: 1,
+ min_persistent_entry_ttl: 1,
+ max_entry_ttl: 10000,
+ });
+
+ test.env.mock_all_auths();
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
+
+ // After automatic payout during resolution, status should be true
+ let market_after = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+ assert_eq!(market_after.claimed.get(test.user.clone()).unwrap(), true);
+}
+
+#[test]
+#[should_panic(expected = "HostError: Error(Auth, InvalidAction)")]
+fn test_unauthorized_claim_attempt() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ // Vote
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ &100_0000000,
+ );
+
+ // Resolve
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ test.env.ledger().set(LedgerInfo {
+ timestamp: market.end_time + 1,
+ protocol_version: 22,
+ sequence_number: test.env.ledger().sequence(),
+ network_id: Default::default(),
+ base_reserve: 10,
+ min_temp_entry_ttl: 1,
+ min_persistent_entry_ttl: 1,
+ max_entry_ttl: 10000,
+ });
+
+ test.env.mock_all_auths();
+ client.resolve_market_manual(
+ &test.admin,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ );
+
+ // Attempt to claim ON BEHALF of test.user but without their auth
+ // We only mock admin auth, not user auth
+ test.env.mock_auths(&[]); // Clear auths
+ // Actual call requires user auth, so this should fail
+ client.claim_winnings(&test.user, &market_id);
+}
+
+
+#[test]
+#[should_panic(expected = "HostError: Error(Contract, #109)")] // AlreadyVoted = 109
+fn test_vote_double_prevention() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ &100_0000000,
+ );
+
+ // Vote again
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "no"), // Even different outcome should fail if one vote per user
+ &100_0000000,
+ );
+}
+
+#[test]
+#[should_panic(expected = "HostError: Error(Contract, #108)")] // InvalidOutcome = 108
+fn test_vote_invalid_outcome() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "maybe"), // Not in ["yes", "no"]
+ &100_0000000,
+ );
+}
+
+#[test]
+#[should_panic(expected = "HostError: Error(Contract, #102)")] // MarketClosed = 102
+fn test_vote_market_closed() {
+ let test = PredictifyTest::setup();
+ let market_id = test.create_test_market();
+ let client = PredictifyHybridClient::new(&test.env, &test.contract_id);
+
+ let market = test.env.as_contract(&test.contract_id, || {
+ test.env
+ .storage()
+ .persistent()
+ .get::(&market_id)
+ .unwrap()
+ });
+
+ test.env.ledger().set(LedgerInfo {
+ timestamp: market.end_time + 1,
+ protocol_version: 22,
+ sequence_number: test.env.ledger().sequence(),
+ network_id: Default::default(),
+ base_reserve: 10,
+ min_temp_entry_ttl: 1,
+ min_persistent_entry_ttl: 1,
+ max_entry_ttl: 10000,
+ });
+
+ test.env.mock_all_auths();
+ client.vote(
+ &test.user,
+ &market_id,
+ &String::from_str(&test.env, "yes"),
+ &100_0000000,
+ );
client.resolve_market_manual(&test.admin, &market_id, &String::from_str(&test.env, "yes"));
// Bet should still exist with claimed status updated
diff --git a/contracts/predictify-hybrid/src/validation_tests.rs b/contracts/predictify-hybrid/src/validation_tests.rs
index c0debfe..f1c73d8 100644
--- a/contracts/predictify-hybrid/src/validation_tests.rs
+++ b/contracts/predictify-hybrid/src/validation_tests.rs
@@ -1,4 +1,6 @@
#![cfg(test)]
+#![allow(unused_variables)]
+#![allow(unused_assignments)]
use super::*;
use crate::config;
diff --git a/contracts/predictify-hybrid/src/versioning.rs b/contracts/predictify-hybrid/src/versioning.rs
index 2b2e72e..477ef61 100644
--- a/contracts/predictify-hybrid/src/versioning.rs
+++ b/contracts/predictify-hybrid/src/versioning.rs
@@ -1,4 +1,5 @@
#![allow(dead_code)]
+#![allow(unused_variables)]
use soroban_sdk::{contracttype, Env, String, Symbol, Vec};