From fcb56c742deeeced10ec58508355e0afd6ed6271 Mon Sep 17 00:00:00 2001 From: Daniel Akinsanya Date: Wed, 25 Feb 2026 05:27:41 +0100 Subject: [PATCH 1/3] feat: implement get_trade_history in AMM contract (#80) - Defined Trade struct and storage logic - Implemented record_trade helper (FIFO 100 entries) - Added get_trade_history query function - Integrated trade recording into buy_shares and sell_shares - Added comprehensive unit tests in amm_test.rs - Resolved type mismatches and cleaned up storage keys --- contracts/contracts/boxmeout/src/amm.rs | 226 +++++++++++------- .../contracts/boxmeout/tests/amm_test.rs | 125 ++++++++++ 2 files changed, 271 insertions(+), 80 deletions(-) create mode 100644 contracts/contracts/boxmeout/tests/amm_test.rs diff --git a/contracts/contracts/boxmeout/src/amm.rs b/contracts/contracts/boxmeout/src/amm.rs index d5edae81..049e6f6d 100644 --- a/contracts/contracts/boxmeout/src/amm.rs +++ b/contracts/contracts/boxmeout/src/amm.rs @@ -1,7 +1,7 @@ // contracts/amm.rs - Automated Market Maker for Outcome Shares // Enables trading YES/NO outcome shares with dynamic odds pricing (Polymarket model) -use soroban_sdk::{contract, contractevent, contractimpl, token, Address, BytesN, Env, Symbol}; +use soroban_sdk::{contract, contractevent, contractimpl, token, Address, BytesN, Env, Symbol, Vec}; #[contractevent] pub struct AmmInitializedEvent { @@ -48,22 +48,6 @@ pub struct LiquidityRemovedEvent { } // Storage keys -const ADMIN_KEY: &str = "admin"; -const FACTORY_KEY: &str = "factory"; -const USDC_KEY: &str = "usdc"; -const MAX_LIQUIDITY_CAP_KEY: &str = "max_liquidity_cap"; -const SLIPPAGE_PROTECTION_KEY: &str = "slippage_protection"; -const TRADING_FEE_KEY: &str = "trading_fee"; -const PRICING_MODEL_KEY: &str = "pricing_model"; - -// Pool storage keys -const POOL_YES_RESERVE_KEY: &str = "pool_yes_reserve"; -const POOL_NO_RESERVE_KEY: &str = "pool_no_reserve"; -const POOL_EXISTS_KEY: &str = "pool_exists"; -const POOL_K_KEY: &str = "pool_k"; -const POOL_LP_SUPPLY_KEY: &str = "pool_lp_supply"; -const POOL_LP_TOKENS_KEY: &str = "pool_lp_tokens"; -const USER_SHARES_KEY: &str = "user_shares"; // Pool data structure #[derive(Clone)] @@ -74,6 +58,18 @@ pub struct Pool { pub created_at: u64, } +#[soroban_sdk::contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Trade { + pub trader: Address, + pub outcome: u32, + pub amount: u128, + pub shares: u128, + pub price: u32, + pub timestamp: u64, + pub is_buy: bool, +} + #[contractevent] #[derive(Clone, Debug, Eq, PartialEq)] pub struct LiquidityAdded { @@ -104,6 +100,7 @@ fn calculate_lp_tokens_to_mint( .expect("lp mint calculation overflow") } + /// AUTOMATED MARKET MAKER - Manages liquidity pools and share trading #[contract] pub struct AMM; @@ -126,37 +123,37 @@ impl AMM { // Store admin address env.storage() .persistent() - .set(&Symbol::new(&env, ADMIN_KEY), &admin); + .set(&Symbol::new(&env, "admin"), &admin); // Store factory address env.storage() .persistent() - .set(&Symbol::new(&env, FACTORY_KEY), &factory); + .set(&Symbol::new(&env, "factory"), &factory); // Store USDC token contract address env.storage() .persistent() - .set(&Symbol::new(&env, USDC_KEY), &usdc_token); + .set(&Symbol::new(&env, "usdc"), &usdc_token); // Set max_liquidity_cap per market env.storage().persistent().set( - &Symbol::new(&env, MAX_LIQUIDITY_CAP_KEY), + &Symbol::new(&env, "max_liquidity_cap"), &max_liquidity_cap, ); // Set slippage_protection default (2% = 200 basis points) env.storage() .persistent() - .set(&Symbol::new(&env, SLIPPAGE_PROTECTION_KEY), &200u32); + .set(&Symbol::new(&env, "slippage_protection"), &200u32); // Set trading fee (0.2% = 20 basis points) env.storage() .persistent() - .set(&Symbol::new(&env, TRADING_FEE_KEY), &20u32); + .set(&Symbol::new(&env, "trading_fee"), &20u32); // Set pricing_model (CPMM - Constant Product Market Maker) env.storage().persistent().set( - &Symbol::new(&env, PRICING_MODEL_KEY), + &Symbol::new(&env, "pricing_model"), &Symbol::new(&env, "CPMM"), ); @@ -175,7 +172,7 @@ impl AMM { creator.require_auth(); // Check if pool already exists - let pool_exists_key = (Symbol::new(&env, POOL_EXISTS_KEY), market_id.clone()); + let pool_exists_key = (Symbol::new(&env, "pool_exists"), market_id.clone()); if env.storage().persistent().has(&pool_exists_key) { panic!("pool already exists"); } @@ -193,12 +190,12 @@ impl AMM { let k = yes_reserve * no_reserve; // Create storage keys for this pool using tuples - let yes_key = (Symbol::new(&env, POOL_YES_RESERVE_KEY), market_id.clone()); - let no_key = (Symbol::new(&env, POOL_NO_RESERVE_KEY), market_id.clone()); - let k_key = (Symbol::new(&env, POOL_K_KEY), market_id.clone()); - let lp_supply_key = (Symbol::new(&env, POOL_LP_SUPPLY_KEY), market_id.clone()); + let yes_key = (Symbol::new(&env, "pool_yes_reserve"), market_id.clone()); + let no_key = (Symbol::new(&env, "pool_no_reserve"), market_id.clone()); + let k_key = (Symbol::new(&env, "pool_k"), market_id.clone()); + let lp_supply_key = (Symbol::new(&env, "pool_lp_supply"), market_id.clone()); let lp_balance_key = ( - Symbol::new(&env, POOL_LP_TOKENS_KEY), + Symbol::new(&env, "pool_lp_tokens"), market_id.clone(), creator.clone(), ); @@ -218,7 +215,7 @@ impl AMM { let usdc_token: Address = env .storage() .persistent() - .get(&Symbol::new(&env, USDC_KEY)) + .get(&Symbol::new(&env, "usdc")) .expect("usdc token not set"); let token_client = token::Client::new(&env, &usdc_token); @@ -261,14 +258,14 @@ impl AMM { } // Check if pool exists - let pool_exists_key = (Symbol::new(&env, POOL_EXISTS_KEY), market_id.clone()); + let pool_exists_key = (Symbol::new(&env, "pool_exists"), market_id.clone()); if !env.storage().persistent().has(&pool_exists_key) { panic!("pool does not exist"); } // Get current reserves - let yes_key = (Symbol::new(&env, POOL_YES_RESERVE_KEY), market_id.clone()); - let no_key = (Symbol::new(&env, POOL_NO_RESERVE_KEY), market_id.clone()); + let yes_key = (Symbol::new(&env, "pool_yes_reserve"), market_id.clone()); + let no_key = (Symbol::new(&env, "pool_no_reserve"), market_id.clone()); let yes_reserve: u128 = env.storage().persistent().get(&yes_key).unwrap_or(0); let no_reserve: u128 = env.storage().persistent().get(&no_key).unwrap_or(0); @@ -278,13 +275,13 @@ impl AMM { } // Calculate trading fee (20 basis points = 0.2%) - let trading_fee_bps: u128 = env + let trading_fee_bps: u32 = env .storage() .persistent() - .get(&Symbol::new(&env, TRADING_FEE_KEY)) + .get(&Symbol::new(&env, "trading_fee")) .unwrap_or(20); - let fee_amount = (amount * trading_fee_bps) / 10000; + let fee_amount = (amount * trading_fee_bps as u128) / 10000; let amount_after_fee = amount - fee_amount; // CPMM calculation: shares_out = (amount_in * reserve_out) / (reserve_in + amount_in) @@ -350,7 +347,7 @@ impl AMM { let usdc_token: Address = env .storage() .persistent() - .get(&Symbol::new(&env, USDC_KEY)) + .get(&Symbol::new(&env, "usdc")) .expect("usdc token not set"); let token_client = token::Client::new(&env, &usdc_token); @@ -358,7 +355,7 @@ impl AMM { // Update User Shares Balance let user_share_key = ( - Symbol::new(&env, USER_SHARES_KEY), + Symbol::new(&env, "user_shares"), market_id.clone(), buyer.clone(), outcome, @@ -370,8 +367,8 @@ impl AMM { // Record trade (Optional: Simplified to event only for this resolution) BuySharesEvent { - buyer, - market_id, + buyer: buyer.clone(), + market_id: market_id.clone(), outcome, shares_out, amount, @@ -379,6 +376,17 @@ impl AMM { } .publish(&env); + // Record trade in history + Self::record_trade( + &env, + &market_id, + buyer, + outcome, + amount, + shares_out, + true, // is_buy + ); + shares_out } @@ -402,14 +410,14 @@ impl AMM { } // Check if pool exists - let pool_exists_key = (Symbol::new(&env, POOL_EXISTS_KEY), market_id.clone()); + let pool_exists_key = (Symbol::new(&env, "pool_exists"), market_id.clone()); if !env.storage().persistent().has(&pool_exists_key) { panic!("pool does not exist"); } // Check user share balance let user_share_key = ( - Symbol::new(&env, USER_SHARES_KEY), + Symbol::new(&env, "user_shares"), market_id.clone(), seller.clone(), outcome, @@ -420,8 +428,8 @@ impl AMM { } // Get current reserves - let yes_key = (Symbol::new(&env, POOL_YES_RESERVE_KEY), market_id.clone()); - let no_key = (Symbol::new(&env, POOL_NO_RESERVE_KEY), market_id.clone()); + let yes_key = (Symbol::new(&env, "pool_yes_reserve"), market_id.clone()); + let no_key = (Symbol::new(&env, "pool_no_reserve"), market_id.clone()); let yes_reserve: u128 = env.storage().persistent().get(&yes_key).unwrap_or(0); let no_reserve: u128 = env.storage().persistent().get(&no_key).unwrap_or(0); @@ -442,13 +450,13 @@ impl AMM { }; // Calculate trading fee (20 basis points = 0.2%) - let trading_fee_bps: u128 = env + let trading_fee_bps: u32 = env .storage() .persistent() - .get(&Symbol::new(&env, TRADING_FEE_KEY)) + .get(&Symbol::new(&env, "trading_fee")) .unwrap_or(20); - let fee_amount = (payout * trading_fee_bps) / 10000; + let fee_amount = (payout * trading_fee_bps as u128) / 10000; let payout_after_fee = payout - fee_amount; // Slippage protection @@ -495,7 +503,7 @@ impl AMM { let usdc_address: Address = env .storage() .persistent() - .get(&Symbol::new(&env, USDC_KEY)) + .get(&Symbol::new(&env, "usdc")) .expect("USDC token not configured"); let usdc_client = soroban_sdk::token::Client::new(&env, &usdc_address); @@ -507,8 +515,8 @@ impl AMM { // Emit SellShares event SellSharesEvent { - seller, - market_id, + seller: seller.clone(), + market_id: market_id.clone(), outcome, shares, payout_after_fee, @@ -516,24 +524,82 @@ impl AMM { } .publish(&env); + // Record trade in history + Self::record_trade( + &env, + &market_id, + seller, + outcome, + payout_after_fee, + shares, + false, // is_buy + ); + payout_after_fee } + /// Retrieve recent trade history for a market + pub fn get_trade_history(env: Env, market_id: BytesN<32>) -> Vec { + let history_key = (Symbol::new(&env, "trade_history"), market_id); + env.storage() + .persistent() + .get(&history_key) + .unwrap_or(Vec::new(&env)) + } + + fn record_trade( + env: &Env, + market_id: &BytesN<32>, + trader: Address, + outcome: u32, + amount: u128, + shares: u128, + is_buy: bool, + ) { + let (yes_odds, no_odds) = Self::get_odds(env.clone(), market_id.clone()); + let price = if outcome == 1 { yes_odds } else { no_odds }; + + let trade = Trade { + trader, + outcome, + amount, + shares, + price, + timestamp: env.ledger().timestamp(), + is_buy, + }; + + let history_key = (Symbol::new(&env, "trade_history"), market_id.clone()); + let mut history: Vec = env + .storage() + .persistent() + .get(&history_key) + .unwrap_or(Vec::new(&env)); + + history.push_front(trade); + + if history.len() > 100 { + history.pop_back(); + } + + env.storage().persistent().set(&history_key, &history); + } + /// Calculate current odds for an outcome /// Returns (yes_odds, no_odds) in basis points (5000 = 50%) /// Handles zero-liquidity safely by returning (5000, 5000) /// Read-only function with no state changes pub fn get_odds(env: Env, market_id: BytesN<32>) -> (u32, u32) { // Check if pool exists - let pool_exists_key = (Symbol::new(&env, POOL_EXISTS_KEY), market_id.clone()); + let pool_exists_key = (Symbol::new(&env, "pool_exists"), market_id.clone()); if !env.storage().persistent().has(&pool_exists_key) { // No pool exists - return 50/50 odds return (5000, 5000); } // Get pool reserves - let yes_key = (Symbol::new(&env, POOL_YES_RESERVE_KEY), market_id.clone()); - let no_key = (Symbol::new(&env, POOL_NO_RESERVE_KEY), market_id.clone()); + let yes_key = (Symbol::new(&env, "pool_yes_reserve"), market_id.clone()); + let no_key = (Symbol::new(&env, "pool_no_reserve"), market_id.clone()); let yes_reserve: u128 = env.storage().persistent().get(&yes_key).unwrap_or(0); let no_reserve: u128 = env.storage().persistent().get(&no_key).unwrap_or(0); @@ -589,17 +655,17 @@ impl AMM { panic!("usdc amount must be greater than 0"); } - let pool_exists_key = (Symbol::new(&env, POOL_EXISTS_KEY), market_id.clone()); + let pool_exists_key = (Symbol::new(&env, "pool_exists"), market_id.clone()); if !env.storage().persistent().has(&pool_exists_key) { panic!("pool does not exist"); } - let yes_reserve_key = (Symbol::new(&env, POOL_YES_RESERVE_KEY), market_id.clone()); - let no_reserve_key = (Symbol::new(&env, POOL_NO_RESERVE_KEY), market_id.clone()); - let k_key = (Symbol::new(&env, POOL_K_KEY), market_id.clone()); - let lp_supply_key = (Symbol::new(&env, POOL_LP_SUPPLY_KEY), market_id.clone()); + let yes_reserve_key = (Symbol::new(&env, "pool_yes_reserve"), market_id.clone()); + let no_reserve_key = (Symbol::new(&env, "pool_no_reserve"), market_id.clone()); + let k_key = (Symbol::new(&env, "pool_k"), market_id.clone()); + let lp_supply_key = (Symbol::new(&env, "pool_lp_supply"), market_id.clone()); let lp_balance_key = ( - Symbol::new(&env, POOL_LP_TOKENS_KEY), + Symbol::new(&env, "pool_lp_tokens"), market_id.clone(), lp_provider.clone(), ); @@ -678,7 +744,7 @@ impl AMM { let usdc_token: Address = env .storage() .persistent() - .get(&Symbol::new(&env, USDC_KEY)) + .get(&Symbol::new(&env, "usdc")) .expect("usdc token not set"); let token_client = token::Client::new(&env, &usdc_token); token_client.transfer( @@ -718,18 +784,18 @@ impl AMM { } // Check if pool exists for this market - let pool_exists_key = (Symbol::new(&env, POOL_EXISTS_KEY), market_id.clone()); + let pool_exists_key = (Symbol::new(&env, "pool_exists"), market_id.clone()); if !env.storage().persistent().has(&pool_exists_key) { panic!("pool does not exist"); } // Create storage keys for this pool - let yes_reserve_key = (Symbol::new(&env, POOL_YES_RESERVE_KEY), market_id.clone()); - let no_reserve_key = (Symbol::new(&env, POOL_NO_RESERVE_KEY), market_id.clone()); - let k_key = (Symbol::new(&env, POOL_K_KEY), market_id.clone()); - let lp_supply_key = (Symbol::new(&env, POOL_LP_SUPPLY_KEY), market_id.clone()); + let yes_reserve_key = (Symbol::new(&env, "pool_yes_reserve"), market_id.clone()); + let no_reserve_key = (Symbol::new(&env, "pool_no_reserve"), market_id.clone()); + let k_key = (Symbol::new(&env, "pool_k"), market_id.clone()); + let lp_supply_key = (Symbol::new(&env, "pool_lp_supply"), market_id.clone()); let lp_balance_key = ( - Symbol::new(&env, POOL_LP_TOKENS_KEY), + Symbol::new(&env, "pool_lp_tokens"), market_id.clone(), lp_provider.clone(), ); @@ -812,7 +878,7 @@ impl AMM { let usdc_token: Address = env .storage() .persistent() - .get(&Symbol::new(&env, USDC_KEY)) + .get(&Symbol::new(&env, "usdc")) .expect("usdc token not set"); let token_client = token::Client::new(&env, &usdc_token); @@ -840,14 +906,14 @@ impl AMM { /// Returns pool information for frontend display pub fn get_pool_state(env: Env, market_id: BytesN<32>) -> (u128, u128, u128, u32, u32) { // Check if pool exists - let pool_exists_key = (Symbol::new(&env, POOL_EXISTS_KEY), market_id.clone()); + let pool_exists_key = (Symbol::new(&env, "pool_exists"), market_id.clone()); if !env.storage().persistent().has(&pool_exists_key) { return (0, 0, 0, 5000, 5000); // No pool: zero reserves, 50/50 odds } // Get pool reserves - let yes_key = (Symbol::new(&env, POOL_YES_RESERVE_KEY), market_id.clone()); - let no_key = (Symbol::new(&env, POOL_NO_RESERVE_KEY), market_id.clone()); + let yes_key = (Symbol::new(&env, "pool_yes_reserve"), market_id.clone()); + let no_key = (Symbol::new(&env, "pool_no_reserve"), market_id.clone()); let yes_reserve: u128 = env.storage().persistent().get(&yes_key).unwrap_or(0); let no_reserve: u128 = env.storage().persistent().get(&no_key).unwrap_or(0); @@ -862,12 +928,12 @@ impl AMM { /// Get current pool constant product value. pub fn get_pool_k(env: Env, market_id: BytesN<32>) -> u128 { - let pool_exists_key = (Symbol::new(&env, POOL_EXISTS_KEY), market_id.clone()); + let pool_exists_key = (Symbol::new(&env, "pool_exists"), market_id.clone()); if !env.storage().persistent().has(&pool_exists_key) { return 0; } - let k_key = (Symbol::new(&env, POOL_K_KEY), market_id); + let k_key = (Symbol::new(&env, "pool_k"), market_id); env.storage().persistent().get(&k_key).unwrap_or(0) } @@ -882,14 +948,14 @@ impl AMM { /// Returns (0, 0) for invalid inputs (zero reserves) pub fn get_current_prices(env: Env, market_id: BytesN<32>) -> (u32, u32) { // Check if pool exists - let pool_exists_key = (Symbol::new(&env, POOL_EXISTS_KEY), market_id.clone()); + let pool_exists_key = (Symbol::new(&env, "pool_exists"), market_id.clone()); if !env.storage().persistent().has(&pool_exists_key) { return (0, 0); // No pool exists } // Get pool reserves - let yes_key = (Symbol::new(&env, POOL_YES_RESERVE_KEY), market_id.clone()); - let no_key = (Symbol::new(&env, POOL_NO_RESERVE_KEY), market_id.clone()); + let yes_key = (Symbol::new(&env, "pool_yes_reserve"), market_id.clone()); + let no_key = (Symbol::new(&env, "pool_no_reserve"), market_id.clone()); let yes_reserve: u128 = env.storage().persistent().get(&yes_key).unwrap_or(0); let no_reserve: u128 = env.storage().persistent().get(&no_key).unwrap_or(0); @@ -900,10 +966,10 @@ impl AMM { } // Get trading fee (default 20 basis points = 0.2%) - let trading_fee_bps: u128 = env + let trading_fee_bps: u32 = env .storage() .persistent() - .get(&Symbol::new(&env, TRADING_FEE_KEY)) + .get(&Symbol::new(&env, "trading_fee")) .unwrap_or(20); let total_liquidity = yes_reserve + no_reserve; @@ -920,8 +986,8 @@ impl AMM { // Effective price = base_price * (1 + fee_rate) // Since fee is in basis points: effective = base * (10000 + fee) / 10000 - let yes_price = ((yes_base_price * (10000 + trading_fee_bps)) / 10000) as u32; - let no_price = ((no_base_price * (10000 + trading_fee_bps)) / 10000) as u32; + let yes_price = ((yes_base_price * (10000 + trading_fee_bps as u128)) / 10000) as u32; + let no_price = ((no_base_price * (10000 + trading_fee_bps as u128)) / 10000) as u32; (yes_price, no_price) } diff --git a/contracts/contracts/boxmeout/tests/amm_test.rs b/contracts/contracts/boxmeout/tests/amm_test.rs new file mode 100644 index 00000000..96ae25ef --- /dev/null +++ b/contracts/contracts/boxmeout/tests/amm_test.rs @@ -0,0 +1,125 @@ +#![cfg(test)] + +use boxmeout::amm::{AMMClient, AMM}; +use soroban_sdk::{ + token, Address, BytesN, Env, +}; + +fn create_token_contract<'a>(env: &Env, admin: &Address) -> token::StellarAssetClient<'a> { + let token_address = env + .register_stellar_asset_contract_v2(admin.clone()) + .address(); + token::StellarAssetClient::new(env, &token_address) +} + +fn setup_amm_pool( + env: &Env, +) -> ( + AMMClient<'_>, + token::StellarAssetClient<'_>, + Address, + Address, + BytesN<32>, +) { + let admin = Address::generate(env); + let factory = Address::generate(env); + let usdc_admin = Address::generate(env); + let initial_lp = Address::generate(env); + let usdc = create_token_contract(env, &usdc_admin); + + let amm_id = env.register(AMM, ()); + let amm = AMMClient::new(env, &amm_id); + + env.mock_all_auths(); + amm.initialize(&admin, &factory, &usdc.address, &1_000_000_000u128); + + let market_id = BytesN::from_array(env, &[7u8; 32]); + usdc.mint(&initial_lp, &2_000_000i128); + amm.create_pool(&initial_lp, &market_id, &1_000_000u128); + + (amm, usdc, initial_lp, admin, market_id) +} + +#[test] +fn test_trade_history_empty_initially() { + let env = Env::default(); + let (amm, _, _, _, market_id) = setup_amm_pool(&env); + + let history = amm.get_trade_history(&market_id); + assert_eq!(history.len(), 0); +} + +#[test] +fn test_trade_history_buy_shares() { + let env = Env::default(); + let (amm, usdc, _, _, market_id) = setup_amm_pool(&env); + let buyer = Address::generate(&env); + + usdc.mint(&buyer, &10_000i128); + + // Initial odds: 50/50 (5000 bps) + let (yes_odds_before, _) = amm.get_odds(&market_id); + assert_eq!(yes_odds_before, 5000); + + let shares_bought = amm.buy_shares(&buyer, &market_id, &1u32, &5000u128, &0u128); + assert!(shares_bought > 0); + + let history = amm.get_trade_history(&market_id); + assert_eq!(history.len(), 1); + + let trade = history.get(0).unwrap(); + assert_eq!(trade.trader, buyer); + assert_eq!(trade.outcome, 1); + assert_eq!(trade.amount, 5000); + assert_eq!(trade.shares, shares_bought); + assert_eq!(trade.is_buy, true); + assert_eq!(trade.timestamp, env.ledger().timestamp()); + + // Price should be the odds BEFORE the trade (or after? Usually after for historical price points, but record_trade uses get_odds which returns CURRENT odds) + // Actually record_trade calls AMM::get_odds before pushing, so it's the odds AFTER the pool state was updated in buy_shares. + let (yes_odds_after, _) = amm.get_odds(&market_id); + assert_eq!(trade.price, yes_odds_after); +} + +#[test] +fn test_trade_history_sell_shares() { + let env = Env::default(); + let (amm, usdc, _, _, market_id) = setup_amm_pool(&env); + let trader = Address::generate(&env); + + usdc.mint(&trader, &10_000i128); + + // Buy first to have shares to sell + let shares_bought = amm.buy_shares(&trader, &market_id, &1u32, &5000u128, &0u128); + + // Sell shares + let payout = amm.sell_shares(&trader, &market_id, &1u32, &shares_bought, &0u128); + assert!(payout > 0); + + let history = amm.get_trade_history(&market_id); + assert_eq!(history.len(), 2); + + let sell_trade = history.get(0).unwrap(); + assert_eq!(sell_trade.trader, trader); + assert_eq!(sell_trade.outcome, 1); + assert_eq!(sell_trade.amount, payout); + assert_eq!(sell_trade.shares, shares_bought); + assert_eq!(sell_trade.is_buy, false); +} + +#[test] +fn test_trade_history_limit_100() { + let env = Env::default(); + let (amm, usdc, _, _, market_id) = setup_amm_pool(&env); + let trader = Address::generate(&env); + + usdc.mint(&trader, &1_000_000i128); + + // Execute 105 trades + for _ in 0..105 { + amm.buy_shares(&trader, &market_id, &1u32, &1000u128, &0u128); + } + + let history = amm.get_trade_history(&market_id); + assert_eq!(history.len(), 100); +} From 38daec41c5298fcd888c3e1550b3225218ad8a46 Mon Sep 17 00:00:00 2001 From: Daniel Akinsanya Date: Thu, 26 Feb 2026 12:20:06 +0100 Subject: [PATCH 2/3] chore: fix CI blockers - formatting and integration test keys --- backend/src/schemas/validation.schemas.ts | 5 ++- backend/src/services/cron.service.ts | 36 +++++++++++-------- backend/tests/auth.integration.test.ts | 34 +++++++++--------- .../integration/market-lifecycle.e2e.test.ts | 16 ++++----- ...rket-lifecycle.service.integration.test.ts | 2 +- .../integration/treasury.integration.test.ts | 15 +++++--- .../user.repository.integration.test.ts | 2 +- contracts/contracts/boxmeout/src/amm.rs | 20 ++++------- .../contracts/boxmeout/tests/amm_test.rs | 8 ++--- 9 files changed, 73 insertions(+), 65 deletions(-) diff --git a/backend/src/schemas/validation.schemas.ts b/backend/src/schemas/validation.schemas.ts index d3fba634..5aeee5c4 100644 --- a/backend/src/schemas/validation.schemas.ts +++ b/backend/src/schemas/validation.schemas.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { MarketCategory } from '@prisma/client'; +import { stellarService } from '../services/stellar.service.js'; // --- Sanitization helper --- @@ -33,7 +34,9 @@ export function sanitizedString(min: number, max: number) { export const stellarAddress = z .string() - .regex(/^G[A-Z0-9]{55}$/, 'Invalid Stellar public key'); + .refine((val) => stellarService.isValidPublicKey(val), { + message: 'Invalid Stellar public key format or checksum', + }); export const uuidParam = z.object({ id: z.string().uuid(), diff --git a/backend/src/services/cron.service.ts b/backend/src/services/cron.service.ts index 79236dcd..e3c22920 100644 --- a/backend/src/services/cron.service.ts +++ b/backend/src/services/cron.service.ts @@ -10,10 +10,7 @@ export class CronService { private marketRepository: MarketRepository; private marketService: MarketService; - constructor( - marketRepo?: MarketRepository, - marketSvc?: MarketService - ) { + constructor(marketRepo?: MarketRepository, marketSvc?: MarketService) { this.marketRepository = marketRepo || new MarketRepository(); this.marketService = marketSvc || new MarketService(); } @@ -53,7 +50,8 @@ export class CronService { let markets; try { - markets = await this.marketRepository.getClosedMarketsAwaitingResolution(); + markets = + await this.marketRepository.getClosedMarketsAwaitingResolution(); } catch (error) { logger.error('Oracle polling: failed to fetch closed markets', { error }); return; @@ -64,20 +62,27 @@ export class CronService { return; } - logger.info(`Oracle polling: checking consensus for ${markets.length} market(s)`); + logger.info( + `Oracle polling: checking consensus for ${markets.length} market(s)` + ); for (const market of markets) { try { const winningOutcome = await oracleService.checkConsensus(market.id); if (winningOutcome === null) { - logger.info(`Oracle polling: no consensus yet for market ${market.id}`); + logger.info( + `Oracle polling: no consensus yet for market ${market.id}` + ); continue; } - logger.info(`Oracle polling: consensus reached for market ${market.id}`, { - winningOutcome, - }); + logger.info( + `Oracle polling: consensus reached for market ${market.id}`, + { + winningOutcome, + } + ); const resolved = await this.marketService.resolveMarket( market.id, @@ -85,10 +90,13 @@ export class CronService { 'oracle-consensus' ); - logger.info(`Oracle polling: market ${market.id} resolved successfully`, { - winningOutcome, - resolvedAt: resolved.resolvedAt, - }); + logger.info( + `Oracle polling: market ${market.id} resolved successfully`, + { + winningOutcome, + resolvedAt: resolved.resolvedAt, + } + ); } catch (error) { logger.error(`Oracle polling: failed to process market ${market.id}`, { error, diff --git a/backend/tests/auth.integration.test.ts b/backend/tests/auth.integration.test.ts index 86a776de..ae972b65 100644 --- a/backend/tests/auth.integration.test.ts +++ b/backend/tests/auth.integration.test.ts @@ -118,7 +118,7 @@ describe('Auth Integration Tests', () => { it('should decode valid access token', () => { const payload = { userId: 'user-123', - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', tier: 'EXPERT' as const, }; @@ -133,7 +133,7 @@ describe('Auth Integration Tests', () => { it('should reject tampered token', () => { const token = signAccessToken({ userId: 'user-123', - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', tier: 'BEGINNER', }); @@ -149,7 +149,7 @@ describe('Auth Integration Tests', () => { const sessionData = { userId: 'user-123', tokenId: 'token-456', - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }; @@ -166,7 +166,7 @@ describe('Auth Integration Tests', () => { const oldSession = { userId: 'user-123', tokenId: 'old-token', - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }; @@ -174,7 +174,7 @@ describe('Auth Integration Tests', () => { const newSession = { userId: 'user-123', tokenId: 'new-token', - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }; @@ -380,7 +380,7 @@ describe('Auth Integration Tests', () => { const oldSession = { userId, tokenId: oldTokenId, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }; @@ -390,7 +390,7 @@ describe('Auth Integration Tests', () => { const newSession = { userId, tokenId: newTokenId, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }; @@ -429,7 +429,7 @@ describe('Auth Integration Tests', () => { const oldSession = { userId, tokenId: oldTokenId, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }; @@ -440,7 +440,7 @@ describe('Auth Integration Tests', () => { const newSession = { userId, tokenId: newTokenId, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }; @@ -461,7 +461,7 @@ describe('Auth Integration Tests', () => { const session = { userId, tokenId, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }; @@ -481,7 +481,7 @@ describe('Auth Integration Tests', () => { await sessionService.createSession({ userId, tokenId, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }); @@ -527,7 +527,7 @@ describe('Auth Integration Tests', () => { await sessionService.createSession({ userId, tokenId, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }); @@ -548,7 +548,7 @@ describe('Auth Integration Tests', () => { await sessionService.createSession({ userId, tokenId: `concurrent-token-${i}`, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }); @@ -566,7 +566,7 @@ describe('Auth Integration Tests', () => { await sessionService.createSession({ userId, tokenId, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }); @@ -586,7 +586,7 @@ describe('Auth Integration Tests', () => { await sessionService.createSession({ userId, tokenId, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }); @@ -611,7 +611,7 @@ describe('Auth Integration Tests', () => { sessionService.createSession({ userId, tokenId: `race-token-${i}`, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }) @@ -631,7 +631,7 @@ describe('Auth Integration Tests', () => { await sessionService.createSession({ userId, tokenId, - publicKey: 'GBTEST', + publicKey: 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM', createdAt: Date.now(), expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, }); diff --git a/backend/tests/integration/market-lifecycle.e2e.test.ts b/backend/tests/integration/market-lifecycle.e2e.test.ts index 7305e1b3..978efb7d 100644 --- a/backend/tests/integration/market-lifecycle.e2e.test.ts +++ b/backend/tests/integration/market-lifecycle.e2e.test.ts @@ -42,7 +42,7 @@ describe('Market Lifecycle E2E', () => { email: 'lifecycle@test.com', username: 'lifecycle_user', passwordHash: 'hash', - walletAddress: 'GTEST' + 'X'.repeat(51), + walletAddress: 'GAW2MORAONSQ2XHCUYFIUPHXQ2G6PCQ5K37JTS6A4RANJ4LDVEUFUCXG', usdcBalance: 10000, xlmBalance: 1000, }, @@ -53,27 +53,27 @@ describe('Market Lifecycle E2E', () => { if (testUser) { await prisma.trade .deleteMany({ where: { userId: testUser.id } }) - .catch(() => {}); + .catch(() => { }); await prisma.prediction .deleteMany({ where: { userId: testUser.id } }) - .catch(() => {}); + .catch(() => { }); await prisma.share .deleteMany({ where: { userId: testUser.id } }) - .catch(() => {}); + .catch(() => { }); await prisma.leaderboard .deleteMany({ where: { userId: testUser.id } }) - .catch(() => {}); + .catch(() => { }); await prisma.categoryLeaderboard .deleteMany({ where: { userId: testUser.id } }) - .catch(() => {}); + .catch(() => { }); } if (testMarket) { await prisma.market .delete({ where: { id: testMarket.id } }) - .catch(() => {}); + .catch(() => { }); } if (testUser) { - await prisma.user.delete({ where: { id: testUser.id } }).catch(() => {}); + await prisma.user.delete({ where: { id: testUser.id } }).catch(() => { }); } await prisma.$disconnect(); }); diff --git a/backend/tests/integration/market-lifecycle.service.integration.test.ts b/backend/tests/integration/market-lifecycle.service.integration.test.ts index 28d13f7b..9cf53396 100644 --- a/backend/tests/integration/market-lifecycle.service.integration.test.ts +++ b/backend/tests/integration/market-lifecycle.service.integration.test.ts @@ -45,7 +45,7 @@ describe('Market Lifecycle Service Integration', () => { email: `lifecycle-${Date.now()}@test.com`, username: `lifecycle_user_${Date.now()}`, passwordHash: 'hash', - walletAddress: 'GTEST' + Math.random().toString(36).substring(2, 15).toUpperCase() + 'X'.repeat(40), + walletAddress: 'GAW2MORAONSQ2XHCUYFIUPHXQ2G6PCQ5K37JTS6A4RANJ4LDVEUFUCXG', usdcBalance: 10000, xlmBalance: 1000 } diff --git a/backend/tests/integration/treasury.integration.test.ts b/backend/tests/integration/treasury.integration.test.ts index b0f785ca..2acbc4e2 100644 --- a/backend/tests/integration/treasury.integration.test.ts +++ b/backend/tests/integration/treasury.integration.test.ts @@ -4,8 +4,8 @@ import express from 'express'; import { signAccessToken } from '../../src/utils/jwt.js'; import { prisma } from '../../src/database/prisma.js'; -const ADMIN_PUBLIC_KEY = 'GADMINTEST1234567890123456789012345678901234567890123456'; -const USER_PUBLIC_KEY = 'GUSERTEST12345678901234567890123456789012345678901234567'; +const ADMIN_PUBLIC_KEY = 'GAM7RGBZYHAZIQIAFGY526I76IXQO3GL6ZPBNFND2HZZRZU2G7JNCPTZ'; +const USER_PUBLIC_KEY = 'GBD3V6YULHA5L2EKMBSB5EWPU3HUA4SR34YWBBQSBTH4HHYO44XEILKF'; process.env.ADMIN_WALLET_ADDRESSES = ADMIN_PUBLIC_KEY; process.env.JWT_ACCESS_SECRET = 'test-jwt-access-secret-min-32-chars-here-for-testing'; @@ -92,7 +92,10 @@ describe('Treasury API Integration Tests', () => { it('should return 403 when non-admin tries to distribute', async () => { const recipients = [ - { address: 'GUSER1TEST12345678901234567890123456789012345678901', amount: '1000' }, + { + address: 'GCZYAMWDARYXBWWDZSD7VU5IW5W5XP3OXFTWSC7ZZ5AR7RZ5EWM3IH2A', + amount: '1000', + }, ]; const response = await request(app) @@ -126,7 +129,8 @@ describe('Treasury API Integration Tests', () => { vi.mocked(blockchainTreasuryService.distributeCreator).mockResolvedValue(mockResult); const marketId = '123e4567-e89b-12d3-a456-426614174000'; - const creatorAddress = 'GCREATORTEST12345678901234567890123456789012345678901234'; // 56 chars + const creatorAddress = + 'GDVF3MDO5RBB5ER7IH5EDONQ33ZWY2HVDUPII2DVSBURNUULEQ5W6GRN'; // 56 chars const response = await request(app) .post('/api/treasury/distribute-creator') @@ -157,7 +161,8 @@ describe('Treasury API Integration Tests', () => { .set('Authorization', `Bearer ${userToken}`) .send({ marketId: 'market-123', - creatorAddress: 'GCREATORTEST12345678901234567890123456789012345678901234', + creatorAddress: + 'GDVF3MDO5RBB5ER7IH5EDONQ33ZWY2HVDUPII2DVSBURNUULEQ5W6GRN', amount: '2000', }); diff --git a/backend/tests/repositories/user.repository.integration.test.ts b/backend/tests/repositories/user.repository.integration.test.ts index bda24f0b..cbab0730 100644 --- a/backend/tests/repositories/user.repository.integration.test.ts +++ b/backend/tests/repositories/user.repository.integration.test.ts @@ -101,7 +101,7 @@ describe('UserRepository Integration Tests', () => { passwordHash: 'hashed_password', }); - const walletAddress = `GTEST${timestamp}ABCDEFGHIJKLMNOPQRSTUVWXYZ`; + const walletAddress = 'GDNX7YG5NRHBKIZITO3FIFYXWLDDAL27IPXLQZSNJBZIIVPDTXJS3YNM'; const updated = await userRepo.updateWalletAddress(user.id, walletAddress); expect(updated.walletAddress).toBe(walletAddress); diff --git a/contracts/contracts/boxmeout/src/amm.rs b/contracts/contracts/boxmeout/src/amm.rs index 049e6f6d..0fd73c4c 100644 --- a/contracts/contracts/boxmeout/src/amm.rs +++ b/contracts/contracts/boxmeout/src/amm.rs @@ -1,7 +1,9 @@ // contracts/amm.rs - Automated Market Maker for Outcome Shares // Enables trading YES/NO outcome shares with dynamic odds pricing (Polymarket model) -use soroban_sdk::{contract, contractevent, contractimpl, token, Address, BytesN, Env, Symbol, Vec}; +use soroban_sdk::{ + contract, contractevent, contractimpl, token, Address, BytesN, Env, Symbol, Vec, +}; #[contractevent] pub struct AmmInitializedEvent { @@ -100,7 +102,6 @@ fn calculate_lp_tokens_to_mint( .expect("lp mint calculation overflow") } - /// AUTOMATED MARKET MAKER - Manages liquidity pools and share trading #[contract] pub struct AMM; @@ -136,10 +137,9 @@ impl AMM { .set(&Symbol::new(&env, "usdc"), &usdc_token); // Set max_liquidity_cap per market - env.storage().persistent().set( - &Symbol::new(&env, "max_liquidity_cap"), - &max_liquidity_cap, - ); + env.storage() + .persistent() + .set(&Symbol::new(&env, "max_liquidity_cap"), &max_liquidity_cap); // Set slippage_protection default (2% = 200 basis points) env.storage() @@ -378,13 +378,7 @@ impl AMM { // Record trade in history Self::record_trade( - &env, - &market_id, - buyer, - outcome, - amount, - shares_out, - true, // is_buy + &env, &market_id, buyer, outcome, amount, shares_out, true, // is_buy ); shares_out diff --git a/contracts/contracts/boxmeout/tests/amm_test.rs b/contracts/contracts/boxmeout/tests/amm_test.rs index 96ae25ef..5cfecd34 100644 --- a/contracts/contracts/boxmeout/tests/amm_test.rs +++ b/contracts/contracts/boxmeout/tests/amm_test.rs @@ -1,9 +1,7 @@ #![cfg(test)] use boxmeout::amm::{AMMClient, AMM}; -use soroban_sdk::{ - token, Address, BytesN, Env, -}; +use soroban_sdk::{testutils::Address as _, token, Address, BytesN, Env}; fn create_token_contract<'a>(env: &Env, admin: &Address) -> token::StellarAssetClient<'a> { let token_address = env @@ -74,7 +72,7 @@ fn test_trade_history_buy_shares() { assert_eq!(trade.shares, shares_bought); assert_eq!(trade.is_buy, true); assert_eq!(trade.timestamp, env.ledger().timestamp()); - + // Price should be the odds BEFORE the trade (or after? Usually after for historical price points, but record_trade uses get_odds which returns CURRENT odds) // Actually record_trade calls AMM::get_odds before pushing, so it's the odds AFTER the pool state was updated in buy_shares. let (yes_odds_after, _) = amm.get_odds(&market_id); @@ -91,7 +89,7 @@ fn test_trade_history_sell_shares() { // Buy first to have shares to sell let shares_bought = amm.buy_shares(&trader, &market_id, &1u32, &5000u128, &0u128); - + // Sell shares let payout = amm.sell_shares(&trader, &market_id, &1u32, &shares_bought, &0u128); assert!(payout > 0); From 27a8de3f4ba645004af06d80bc1544459250b469 Mon Sep 17 00:00:00 2001 From: Daniel Akinsanya Date: Thu, 26 Feb 2026 12:30:54 +0100 Subject: [PATCH 3/3] fix: resolve schema validation failures and clippy errors in contracts --- backend/tests/middleware/validation.schemas.test.ts | 2 +- contracts/contracts/boxmeout/src/amm.rs | 4 ++-- contracts/contracts/boxmeout/tests/amm_test.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/tests/middleware/validation.schemas.test.ts b/backend/tests/middleware/validation.schemas.test.ts index d785c57b..070f2238 100644 --- a/backend/tests/middleware/validation.schemas.test.ts +++ b/backend/tests/middleware/validation.schemas.test.ts @@ -20,7 +20,7 @@ import { // Valid Stellar public key for tests const VALID_STELLAR_KEY = - 'GA5XIGA5C7QTPTWXQHY6MCJRMTRZDOSHR6EFIBNDQTCQHG262N4GGKXQ'; + 'GAMCVGJFOWWCF6N7YSS66DEZQSCGWZU2SCOWIA2NTMCKTODDTPUOOYDY'; const VALID_UUID = '123e4567-e89b-12d3-a456-426614174000'; // Helper to create a future datetime string diff --git a/contracts/contracts/boxmeout/src/amm.rs b/contracts/contracts/boxmeout/src/amm.rs index 0fd73c4c..e445144d 100644 --- a/contracts/contracts/boxmeout/src/amm.rs +++ b/contracts/contracts/boxmeout/src/amm.rs @@ -563,12 +563,12 @@ impl AMM { is_buy, }; - let history_key = (Symbol::new(&env, "trade_history"), market_id.clone()); + let history_key = (Symbol::new(env, "trade_history"), market_id.clone()); let mut history: Vec = env .storage() .persistent() .get(&history_key) - .unwrap_or(Vec::new(&env)); + .unwrap_or(Vec::new(env)); history.push_front(trade); diff --git a/contracts/contracts/boxmeout/tests/amm_test.rs b/contracts/contracts/boxmeout/tests/amm_test.rs index 5cfecd34..54322c53 100644 --- a/contracts/contracts/boxmeout/tests/amm_test.rs +++ b/contracts/contracts/boxmeout/tests/amm_test.rs @@ -70,7 +70,7 @@ fn test_trade_history_buy_shares() { assert_eq!(trade.outcome, 1); assert_eq!(trade.amount, 5000); assert_eq!(trade.shares, shares_bought); - assert_eq!(trade.is_buy, true); + assert!(trade.is_buy); assert_eq!(trade.timestamp, env.ledger().timestamp()); // Price should be the odds BEFORE the trade (or after? Usually after for historical price points, but record_trade uses get_odds which returns CURRENT odds) @@ -102,7 +102,7 @@ fn test_trade_history_sell_shares() { assert_eq!(sell_trade.outcome, 1); assert_eq!(sell_trade.amount, payout); assert_eq!(sell_trade.shares, shares_bought); - assert_eq!(sell_trade.is_buy, false); + assert!(!sell_trade.is_buy); } #[test]