Skip to content

Commit f79892a

Browse files
Merge pull request #136 from Akshola00/Akshola00/issue59
refac: update market state types and logic
2 parents 1cf9226 + 96323c3 commit f79892a

5 files changed

Lines changed: 194 additions & 44 deletions

File tree

contracts/predictify-hybrid/src/disputes.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,7 @@ mod tests {
10551055
2500000,
10561056
String::from_str(env, "gt"),
10571057
),
1058+
crate::types::MarketState::Active,
10581059
)
10591060
}
10601061

contracts/predictify-hybrid/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ impl PredictifyHybrid {
293293
let state = resolution::ResolutionUtils::get_resolution_state(&env, &market);
294294
let (eligible, reason) = resolution::ResolutionUtils::get_resolution_eligibility(&env, &market);
295295

296+
// Set winning outcome
297+
MarketStateManager::set_winning_outcome(&mut market, final_result.clone(), Some(&market_id));
296298
if !eligible {
297299
validation.is_valid = false;
298300
validation.errors.push_back(reason);

contracts/predictify-hybrid/src/markets.rs

Lines changed: 157 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,11 @@ impl MarketCreator {
3333
let end_time = MarketUtils::calculate_end_time(_env, duration_days);
3434

3535
// Create market instance
36-
let market = Market::new(
37-
_env,
38-
admin.clone(),
39-
question,
40-
outcomes,
41-
end_time,
42-
oracle_config,
43-
);
44-
45-
// Market creation fee is now handled by the fees module
46-
// FeeManager::process_creation_fee(_env, &admin)?;
47-
36+
let market = Market::new(env, admin.clone(), question, outcomes, end_time, oracle_config, MarketState::Active);
37+
38+
// Process market creation fee
39+
MarketUtils::process_creation_fee(env, &admin)?;
40+
4841
// Store market
4942
_env.storage().persistent().set(&market_id, &market);
5043

@@ -193,25 +186,49 @@ impl MarketStateManager {
193186
}
194187

195188
/// Remove market from storage
196-
pub fn remove_market(_env: &Env, market_id: &Symbol) {
197-
_env.storage().persistent().remove(market_id);
189+
pub fn remove_market(env: &Env, market_id: &Symbol) {
190+
let mut market = match Self::get_market(env, market_id) {
191+
Ok(m) => m,
192+
Err(_) => return,
193+
};
194+
if market.state != MarketState::Closed {
195+
MarketStateLogic::validate_state_transition(market.state, MarketState::Closed).unwrap();
196+
let old_state = market.state;
197+
market.state = MarketState::Closed;
198+
MarketStateLogic::emit_state_change_event(env, market_id, old_state, market.state);
199+
Self::update_market(env, market_id, &market);
200+
}
201+
env.storage().persistent().remove(market_id);
198202
}
199203

200204
/// Add vote to market
201-
pub fn add_vote(market: &mut Market, user: Address, outcome: String, stake: i128) {
205+
pub fn add_vote(market: &mut Market, user: Address, outcome: String, stake: i128, market_id: Option<&Symbol>) {
206+
MarketStateLogic::check_function_access_for_state("vote", market.state).unwrap();
202207
market.votes.set(user.clone(), outcome);
203208
market.stakes.set(user.clone(), stake);
204209
market.total_staked += stake;
210+
// No state change for voting
205211
}
206212

207213
/// Add dispute stake to market
208-
pub fn add_dispute_stake(market: &mut Market, user: Address, stake: i128) {
214+
pub fn add_dispute_stake(market: &mut Market, user: Address, stake: i128, market_id: Option<&Symbol>) {
215+
MarketStateLogic::check_function_access_for_state("dispute", market.state).unwrap();
209216
let existing_stake = market.dispute_stakes.get(user.clone()).unwrap_or(0);
210217
market.dispute_stakes.set(user, existing_stake + stake);
218+
// State transition: Ended -> Disputed
219+
if market.state == MarketState::Ended {
220+
MarketStateLogic::validate_state_transition(market.state, MarketState::Disputed).unwrap();
221+
let old_state = market.state;
222+
market.state = MarketState::Disputed;
223+
let env = &market.votes.env();
224+
let owned_event_id = market_id.cloned().unwrap_or_else(|| Symbol::new(env, "unknown_market_id"));
225+
MarketStateLogic::emit_state_change_event(env, &owned_event_id, old_state, market.state);
226+
}
211227
}
212228

213229
/// Mark user as claimed
214-
pub fn mark_claimed(market: &mut Market, user: Address) {
230+
pub fn mark_claimed(market: &mut Market, user: Address, _market_id: Option<&Symbol>) {
231+
MarketStateLogic::check_function_access_for_state("claim", market.state).unwrap();
215232
market.claimed.set(user, true);
216233
}
217234

@@ -221,12 +238,32 @@ impl MarketStateManager {
221238
}
222239

223240
/// Set winning outcome
224-
pub fn set_winning_outcome(market: &mut Market, outcome: String) {
241+
pub fn set_winning_outcome(market: &mut Market, outcome: String, market_id: Option<&Symbol>) {
242+
MarketStateLogic::check_function_access_for_state("resolve", market.state).unwrap();
243+
let old_state = market.state;
225244
market.winning_outcome = Some(outcome);
245+
// State transition: Ended/Disputed -> Resolved
246+
if market.state == MarketState::Ended || market.state == MarketState::Disputed {
247+
MarketStateLogic::validate_state_transition(market.state, MarketState::Resolved).unwrap();
248+
market.state = MarketState::Resolved;
249+
let env = &market.votes.env();
250+
let owned_event_id = market_id.cloned().unwrap_or_else(|| Symbol::new(env, "unknown_market_id"));
251+
MarketStateLogic::emit_state_change_event(env, &owned_event_id, old_state, market.state);
252+
}
226253
}
227254

228255
/// Mark fees as collected
229-
pub fn mark_fees_collected(market: &mut Market) {
256+
pub fn mark_fees_collected(market: &mut Market, market_id: Option<&Symbol>) {
257+
MarketStateLogic::check_function_access_for_state("close", market.state).unwrap();
258+
let old_state = market.state;
259+
// State transition: Resolved -> Closed
260+
if market.state == MarketState::Resolved {
261+
MarketStateLogic::validate_state_transition(market.state, MarketState::Closed).unwrap();
262+
market.state = MarketState::Closed;
263+
let env = &market.votes.env();
264+
let owned_event_id = market_id.cloned().unwrap_or_else(|| Symbol::new(env, "unknown_market_id"));
265+
MarketStateLogic::emit_state_change_event(env, &owned_event_id, old_state, market.state);
266+
}
230267
market.fee_collected = true;
231268
}
232269

@@ -527,8 +564,9 @@ impl MarketTestHelpers {
527564
token_client.transfer(&user, &_env.current_contract_address(), &stake);
528565

529566
// Add vote
530-
MarketStateManager::add_vote(&mut market, user, outcome, stake);
531-
MarketStateManager::update_market(_env, market_id, &market);
567+
MarketStateManager::add_vote(&mut market, user, outcome, stake, None);
568+
MarketStateManager::update_market(env, market_id, &market);
569+
532570

533571
Ok(())
534572
}
@@ -554,13 +592,108 @@ impl MarketTestHelpers {
554592
MarketUtils::determine_final_result(_env, &oracle_result, &community_consensus);
555593

556594
// Set winning outcome
557-
MarketStateManager::set_winning_outcome(&mut market, final_result.clone());
558-
MarketStateManager::update_market(_env, market_id, &market);
595+
MarketStateManager::set_winning_outcome(&mut market, final_result.clone(), None);
596+
MarketStateManager::update_market(env, market_id, &market);
559597

560598
Ok(final_result)
561599
}
562600
}
563601

602+
// ===== MARKET STATE LOGIC =====
603+
604+
pub struct MarketStateLogic;
605+
606+
impl MarketStateLogic {
607+
/// Validate allowed state transitions
608+
pub fn validate_state_transition(from: MarketState, to: MarketState) -> Result<(), Error> {
609+
use MarketState::*;
610+
let allowed = match from {
611+
Active => matches!(to, Ended | Cancelled | Closed | Disputed),
612+
Ended => matches!(to, Resolved | Disputed | Closed | Cancelled),
613+
Disputed => matches!(to, Resolved | Closed | Cancelled),
614+
Resolved => matches!(to, Closed),
615+
Closed => false,
616+
Cancelled => false,
617+
};
618+
if allowed {
619+
Ok(())
620+
} else {
621+
Err(Error::InvalidState)
622+
}
623+
}
624+
625+
/// Check if a function is allowed in the given state
626+
pub fn check_function_access_for_state(function: &str, state: MarketState) -> Result<(), Error> {
627+
use MarketState::*;
628+
let allowed = match function {
629+
"vote" => matches!(state, Active),
630+
"dispute" => matches!(state, Ended),
631+
"resolve" => matches!(state, Ended | Disputed),
632+
"claim" => matches!(state, Resolved),
633+
"close" => matches!(state, Resolved | Cancelled | Closed),
634+
_ => true, // By default allow
635+
};
636+
if allowed {
637+
Ok(())
638+
} else {
639+
Err(Error::MarketClosed)
640+
}
641+
}
642+
643+
/// Emit a state change event (placeholder: use env.events().publish)
644+
pub fn emit_state_change_event(env: &Env, market_id: &Symbol, old_state: MarketState, new_state: MarketState) {
645+
env.events().publish(("market_state_change", market_id), (old_state, new_state));
646+
}
647+
648+
/// Validate that the market's state is consistent with its data
649+
pub fn validate_market_state_consistency(env: &Env, market: &Market) -> Result<(), Error> {
650+
use MarketState::*;
651+
let now = env.ledger().timestamp();
652+
match market.state {
653+
Active => {
654+
if market.end_time <= now {
655+
return Err(Error::InvalidState);
656+
}
657+
if market.winning_outcome.is_some() {
658+
return Err(Error::InvalidState);
659+
}
660+
}
661+
Ended => {
662+
if market.end_time > now {
663+
return Err(Error::InvalidState);
664+
}
665+
if market.winning_outcome.is_some() {
666+
return Err(Error::InvalidState);
667+
}
668+
}
669+
Disputed => {
670+
if market.dispute_stakes.is_empty() {
671+
return Err(Error::InvalidState);
672+
}
673+
}
674+
Resolved => {
675+
if market.winning_outcome.is_none() {
676+
return Err(Error::InvalidState);
677+
}
678+
}
679+
Closed | Cancelled => {}
680+
}
681+
Ok(())
682+
}
683+
684+
/// Get the current state of a market
685+
pub fn get_market_state(env: &Env, market_id: &Symbol) -> Result<MarketState, Error> {
686+
let market = MarketStateManager::get_market(env, market_id)?;
687+
Ok(market.state)
688+
}
689+
690+
/// Check if a market can transition to a target state
691+
pub fn can_transition_to_state(env: &Env, market_id: &Symbol, target_state: MarketState) -> Result<bool, Error> {
692+
let market = MarketStateManager::get_market(env, market_id)?;
693+
Ok(MarketStateLogic::validate_state_transition(market.state, target_state).is_ok())
694+
}
695+
}
696+
564697
// ===== MODULE TESTS =====
565698

566699
#[cfg(test)]
@@ -660,6 +793,7 @@ mod tests {
660793
25_000_00,
661794
String::from_str(&env, "gt"),
662795
),
796+
MarketState::Active,
663797
);
664798

665799
// Test market stats

contracts/predictify-hybrid/src/types.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ pub struct Market {
168168
pub winning_outcome: Option<String>,
169169
/// Whether fees have been collected
170170
pub fee_collected: bool,
171+
/// Explicit market state
172+
pub state: MarketState,
171173
/// Market extension history
172174
pub extension_history: Vec<MarketExtension>,
173175
/// Total extension days applied
@@ -226,6 +228,7 @@ impl MarketExtension {
226228

227229
Ok(())
228230
}
231+
229232
}
230233

231234
impl Market {
@@ -237,6 +240,7 @@ impl Market {
237240
outcomes: Vec<String>,
238241
end_time: u64,
239242
oracle_config: OracleConfig,
243+
state: MarketState,
240244
) -> Self {
241245
Self {
242246
admin,
@@ -252,9 +256,11 @@ impl Market {
252256
dispute_stakes: Map::new(env),
253257
winning_outcome: None,
254258
fee_collected: false,
259+
state,
255260
extension_history: vec![env],
256261
total_extension_days: 0,
257262
max_extension_days: 30, // Default maximum extension days
263+
258264
}
259265
}
260266

@@ -687,6 +693,7 @@ impl VoteParams {
687693
// ===== UTILITY TYPES =====
688694

689695
/// Market state enumeration
696+
#[contracttype]
690697
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
691698
pub enum MarketState {
692699
/// Market is active and accepting votes
@@ -695,20 +702,18 @@ pub enum MarketState {
695702
Ended,
696703
/// Market has been resolved
697704
Resolved,
705+
/// Market is in dispute
706+
Disputed,
698707
/// Market has been closed
699708
Closed,
709+
/// Market has been cancelled
710+
Cancelled,
700711
}
701712

702713
impl MarketState {
703-
/// Get state from market
714+
/// Get state from market (legacy, for migration)
704715
pub fn from_market(market: &Market, current_time: u64) -> Self {
705-
if market.is_resolved() {
706-
MarketState::Resolved
707-
} else if market.has_ended(current_time) {
708-
MarketState::Ended
709-
} else {
710-
MarketState::Active
711-
}
716+
market.state
712717
}
713718

714719
/// Check if market is active
@@ -718,10 +723,7 @@ impl MarketState {
718723

719724
/// Check if market has ended
720725
pub fn has_ended(&self) -> bool {
721-
matches!(
722-
self,
723-
MarketState::Ended | MarketState::Resolved | MarketState::Closed
724-
)
726+
matches!(self, MarketState::Ended | MarketState::Resolved | MarketState::Closed | MarketState::Cancelled)
725727
}
726728

727729
/// Check if market is resolved
@@ -893,6 +895,7 @@ mod tests {
893895
outcomes,
894896
env.ledger().timestamp() + 86400,
895897
oracle_config,
898+
MarketState::Active,
896899
);
897900

898901
assert!(market.is_active(env.ledger().timestamp()));
@@ -933,6 +936,7 @@ mod tests {
933936
outcomes,
934937
env.ledger().timestamp() + 86400,
935938
oracle_config,
939+
MarketState::Active,
936940
);
937941

938942
let state = MarketState::from_market(&market, env.ledger().timestamp());

0 commit comments

Comments
 (0)