@@ -24,7 +24,7 @@ use soroban_sdk::{contracttype, symbol_short, Address, Env, Map, String, Symbol,
2424use crate :: errors:: Error ;
2525use crate :: events:: EventEmitter ;
2626use crate :: markets:: { MarketStateManager , MarketUtils , MarketValidator } ;
27- use crate :: types:: { Bet , BetLimits , BetStatus , BetStats , Market , MarketState } ;
27+ use crate :: types:: { Bet , BetLimits , BetStats , BetStatus , Market , MarketState } ;
2828// use crate::validation;
2929
3030// ===== CONSTANTS =====
@@ -517,6 +517,8 @@ impl BetManager {
517517 for i in 0 ..bet_count {
518518 if let Some ( bet_key) = bets. get ( i) {
519519 if let Some ( mut bet) = BetStorage :: get_bet ( env, market_id, & bet_key) {
520+ let old_status = bet. status ;
521+
520522 // Determine if bet won or lost (check if outcome is in winning outcomes)
521523 if winning_outcomes. contains ( & bet. outcome ) {
522524 bet. mark_as_won ( ) ;
@@ -527,8 +529,36 @@ impl BetManager {
527529 // Update bet status
528530 BetStorage :: store_bet ( env, & bet) ?;
529531
530- // Skip event emission to avoid potential segfaults
531- // Events can be emitted separately if needed
532+ if old_status != bet. status {
533+ let old_status_str = match old_status {
534+ crate :: types:: BetStatus :: Active => String :: from_str ( env, "Active" ) ,
535+ crate :: types:: BetStatus :: Won => String :: from_str ( env, "Won" ) ,
536+ crate :: types:: BetStatus :: Lost => String :: from_str ( env, "Lost" ) ,
537+ crate :: types:: BetStatus :: Refunded => String :: from_str ( env, "Refunded" ) ,
538+ crate :: types:: BetStatus :: Cancelled => {
539+ String :: from_str ( env, "Cancelled" )
540+ }
541+ } ;
542+ let new_status_str = match bet. status {
543+ crate :: types:: BetStatus :: Active => String :: from_str ( env, "Active" ) ,
544+ crate :: types:: BetStatus :: Won => String :: from_str ( env, "Won" ) ,
545+ crate :: types:: BetStatus :: Lost => String :: from_str ( env, "Lost" ) ,
546+ crate :: types:: BetStatus :: Refunded => String :: from_str ( env, "Refunded" ) ,
547+ crate :: types:: BetStatus :: Cancelled => {
548+ String :: from_str ( env, "Cancelled" )
549+ }
550+ } ;
551+
552+ // Resolution status is visible on-ledger for indexers and auditors.
553+ EventEmitter :: emit_bet_status_updated (
554+ env,
555+ market_id,
556+ & bet. user ,
557+ & old_status_str,
558+ & new_status_str,
559+ None ,
560+ ) ;
561+ }
532562 }
533563 }
534564 }
@@ -640,6 +670,116 @@ impl BetManager {
640670
641671 Ok ( payout)
642672 }
673+ /// Cancel a bet before the market deadline and refund the user.
674+ ///
675+ /// This function allows users to cancel their active bets before the market
676+ /// deadline, receiving a full refund of their locked funds.
677+ ///
678+ /// # Parameters
679+ ///
680+ /// - `env` - The Soroban environment
681+ /// - `user` - Address of the user cancelling the bet
682+ /// - `market_id` - Symbol identifying the market
683+ ///
684+ /// # Returns
685+ ///
686+ /// Returns `Ok(())` on successful cancellation and refund,
687+ /// or `Err(Error)` if cancellation fails.
688+ ///
689+ /// # Errors
690+ ///
691+ /// - `Error::NothingToClaim` - User has no bet on this market
692+ /// - `Error::MarketNotFound` - Market does not exist
693+ /// - `Error::MarketClosed` - Market deadline has passed
694+ /// - `Error::InvalidState` - Bet is not in Active status
695+ ///
696+ /// # Security
697+ ///
698+ /// - Requires user authentication via `require_auth()`
699+ /// - Only the bettor can cancel their own bet
700+ /// - Can only cancel before market deadline
701+ /// - Funds are refunded atomically with status update
702+ ///
703+ /// # Example
704+ ///
705+ /// ```rust
706+ /// BetManager::cancel_bet(
707+ /// &env,
708+ /// user.clone(),
709+ /// Symbol::new(&env, "BTC_100K"),
710+ /// )?;
711+ /// ```
712+ pub fn cancel_bet ( env : & Env , user : Address , market_id : Symbol ) -> Result < ( ) , Error > {
713+ // Require authentication from the user
714+ user. require_auth ( ) ;
715+
716+ // Get user's bet
717+ let mut bet = BetStorage :: get_bet ( env, & market_id, & user) . ok_or ( Error :: NothingToClaim ) ?;
718+
719+ // Ensure bet is active
720+ if !bet. is_active ( ) {
721+ return Err ( Error :: InvalidState ) ;
722+ }
723+
724+ // Get market and validate it hasn't ended
725+ let market = MarketStateManager :: get_market ( env, & market_id) ?;
726+ let current_time = env. ledger ( ) . timestamp ( ) ;
727+
728+ if current_time >= market. end_time {
729+ return Err ( Error :: MarketClosed ) ;
730+ }
731+
732+ // Refund the locked funds
733+ BetUtils :: unlock_funds ( env, & user, bet. amount ) ?;
734+
735+ // Mark bet as cancelled
736+ bet. status = BetStatus :: Cancelled ;
737+ BetStorage :: store_bet ( env, & bet) ?;
738+
739+ // Update market betting stats
740+ Self :: update_market_bet_stats_on_cancel ( env, & market_id, & bet. outcome , bet. amount ) ?;
741+
742+ // Emit bet cancelled event
743+ EventEmitter :: emit_bet_status_updated (
744+ env,
745+ & market_id,
746+ & user,
747+ & String :: from_str ( env, "Active" ) ,
748+ & String :: from_str ( env, "Cancelled" ) ,
749+ Some ( bet. amount ) ,
750+ ) ;
751+
752+ Ok ( ( ) )
753+ }
754+
755+ /// Update market betting statistics after a bet cancellation.
756+ fn update_market_bet_stats_on_cancel (
757+ env : & Env ,
758+ market_id : & Symbol ,
759+ outcome : & String ,
760+ amount : i128 ,
761+ ) -> Result < ( ) , Error > {
762+ let mut stats = BetStorage :: get_market_bet_stats ( env, market_id) ;
763+
764+ // Update totals
765+ stats. total_bets = stats. total_bets . saturating_sub ( 1 ) ;
766+ stats. total_amount_locked = stats. total_amount_locked . saturating_sub ( amount) ;
767+ stats. unique_bettors = stats. unique_bettors . saturating_sub ( 1 ) ;
768+
769+ // Update outcome totals
770+ let current_outcome_total = stats. outcome_totals . get ( outcome. clone ( ) ) . unwrap_or ( 0 ) ;
771+ let new_total = current_outcome_total. saturating_sub ( amount) ;
772+ if new_total > 0 {
773+ stats. outcome_totals . set ( outcome. clone ( ) , new_total) ;
774+ } else {
775+ stats. outcome_totals . remove ( outcome. clone ( ) ) ;
776+ }
777+
778+ // Store updated stats
779+ BetStorage :: store_market_bet_stats ( env, market_id, & stats) ?;
780+
781+ Ok ( ( ) )
782+ }
643783}
644784
645785// ===== BET STORAGE =====
@@ -829,7 +969,7 @@ impl BetValidator {
829969 let limits = get_effective_bet_limits ( env, market_id) ;
830970 // Temporarily disabled due to validation module being disabled
831971 // validation::validate_bet_amount_against_limits(amount, &limits)
832-
972+
833973 // Simple validation for now
834974 if amount < limits. min_bet || amount > limits. max_bet {
835975 return Err ( Error :: InvalidInput ) ;
0 commit comments