diff --git a/.gitignore b/.gitignore index 7a5de424..3c865c56 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ commit_out.txt .git_commit_msg.txt +commit_and_push.ps1 +commit.sh # Rust build artifacts target/ diff --git a/commit.sh b/commit.sh deleted file mode 100644 index 50f56a50..00000000 --- a/commit.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -git add . -git commit -m "docs: markets error catalog" -git push -u origin task/markets-errcat diff --git a/commit_and_push.ps1 b/commit_and_push.ps1 deleted file mode 100644 index 0fe8bf11..00000000 --- a/commit_and_push.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -# Git commit and push script - -cd 'c:\Users\LENOVO PC\Desktop\New folder (7)\hermes' - -# Stage all changes -git add . - -# Create commit -git commit -F '.\.git_commit_msg.txt' - -# Push to origin -git push -u origin feat/canonical-error-registry - -# Display status -git log --oneline -3 diff --git a/contracts/disputes/src/lib.rs b/contracts/disputes/src/lib.rs index f660f708..bd182c42 100644 --- a/contracts/disputes/src/lib.rs +++ b/contracts/disputes/src/lib.rs @@ -167,6 +167,7 @@ public struct EscalationRecord { enum DataKey { DisputeCounter, Dispute(u32), + Escalation(Symbol), } fn load_dispute(env: &Env, id: u32) -> Dispute { @@ -213,17 +214,17 @@ impl DisputesContract { } pub fn escalate_dispute( - _env: Env, + env: Env, policy: EscalationPolicy, market_id: Symbol, stake: u32, turnout: u32, community_outcome: DisputeOutcome, ) -> EscalationRecord { - if !Self::is_escalation_eligible(_env.clone(), policy, stake, turnout) { + if !Self::is_escalation_eligible(env.clone(), policy, stake, turnout) { panic!("market not eligible for escalation"); } - EscalationRecord { + let record = EscalationRecord { market_id, stake, turnout, @@ -232,15 +233,24 @@ impl DisputesContract { uphold_votes: 0, overturn_votes: 0, resolved: false, - } + }; + env.storage() + .persistent() + .set(&DataKey::Escalation(record.market_id.clone()), &record); + record } pub fn council_vote( - _env: Env, + env: Env, policy: EscalationPolicy, - mut record: EscalationRecord, + record: EscalationRecord, vote: u32, ) -> EscalationRecord { + let mut record: EscalationRecord = env + .storage() + .persistent() + .get(&DataKey::Escalation(record.market_id.clone())) + .unwrap_or_else(|| panic!("escalation not found")); if record.resolved { panic!("escalation already resolved"); } @@ -260,9 +270,19 @@ impl DisputesContract { } else if record.overturn_votes >= policy.council_threshold { record.resolved = true; } + env.storage() + .persistent() + .set(&DataKey::Escalation(record.market_id.clone()), &record); record } + /// Returns the persisted escalation record for a market, if one exists. + pub fn get_escalation(env: Env, market_id: Symbol) -> Option { + env.storage() + .persistent() + .get(&DataKey::Escalation(market_id)) + } + pub fn council_outcome(_env: Env, record: EscalationRecord) -> DisputeOutcome { if !record.resolved { panic!("council has not resolved the dispute"); diff --git a/contracts/disputes/tests/dispute_storage.rs b/contracts/disputes/tests/dispute_storage.rs index 2980aa24..cfa07093 100644 --- a/contracts/disputes/tests/dispute_storage.rs +++ b/contracts/disputes/tests/dispute_storage.rs @@ -1,5 +1,6 @@ use disputes::{ DisputeOutcome, DisputeStatus, DisputesContract, DisputesContractClient, + EscalationPolicy, COUNCIL_VOTE_UPHOLD, }; use soroban_sdk::{Env, Symbol}; @@ -33,4 +34,38 @@ fn dispute_lifecycle_is_persisted_and_uses_unique_ids() { let withdrawn = client.withdraw_dispute(&second, &creator); assert_eq!(withdrawn.status, DisputeStatus::Withdrawn); assert_eq!(client.get_dispute(&second.id), Some(withdrawn)); +} + +#[test] +fn council_votes_accumulate_in_persisted_escalation_record() { + let env = Env::default(); + let contract_id = env.register(DisputesContract, ()); + let client = DisputesContractClient::new(&env, &contract_id); + let market_id = Symbol::new(&env, "MARKET1"); + let policy = EscalationPolicy { + min_stake: 100, + quorum: 10, + council_threshold: 2, + }; + + let escalation = client.escalate_dispute( + &policy, + &market_id, + &100, + &9, + &DisputeOutcome::ChallengerWins, + ); + assert_eq!(client.get_escalation(&market_id), Some(escalation.clone())); + + let first_vote = client.council_vote(&policy, &escalation, &COUNCIL_VOTE_UPHOLD); + assert_eq!(first_vote.council_votes, 1); + assert_eq!(first_vote.uphold_votes, 1); + assert!(!first_vote.resolved); + + // A stale caller-supplied record must not discard the vote already stored. + let second_vote = client.council_vote(&policy, &escalation, &COUNCIL_VOTE_UPHOLD); + assert_eq!(second_vote.council_votes, 2); + assert_eq!(second_vote.uphold_votes, 2); + assert!(second_vote.resolved); + assert_eq!(client.get_escalation(&market_id), Some(second_vote)); } \ No newline at end of file diff --git a/contracts/reporting/src/lib.rs b/contracts/reporting/src/lib.rs index 60d03935..3a95bdaf 100644 --- a/contracts/reporting/src/lib.rs +++ b/contracts/reporting/src/lib.rs @@ -295,6 +295,7 @@ impl ReportingContract { /// # Errors /// /// * [`ReportingError::ReportingPaused`] — Reporting is currently paused. + /// * [`ReportingError::ReportNotFound`] — No report with the given ID exists. /// /// # Auth /// @@ -306,7 +307,35 @@ impl ReportingContract { ) -> Result<(), ReportingError> { admin.require_auth(); pause::require_not_paused(&env)?; - let _ = report_id; + + let report = env + .storage() + .persistent() + .get::(&DataKey::Report(report_id)) + .ok_or(ReportingError::ReportNotFound)?; + + let market_key = DataKey::MarketReports(report.market_id); + if let Some(mut market_reports) = env + .storage() + .persistent() + .get::>(&market_key) + { + let mut index = 0; + while index < market_reports.len() { + if market_reports.get(index) == Some(report_id) { + market_reports.remove(index); + break; + } + index += 1; + } + env.storage() + .persistent() + .set(&market_key, &market_reports); + } + + env.storage() + .persistent() + .remove(&DataKey::Report(report_id)); Ok(()) } diff --git a/contracts/reporting/tests/auth_boundary.rs b/contracts/reporting/tests/auth_boundary.rs index 1cc5475e..8fd6886c 100644 --- a/contracts/reporting/tests/auth_boundary.rs +++ b/contracts/reporting/tests/auth_boundary.rs @@ -366,6 +366,11 @@ fn test_delete_report_rejected_without_auth() { fn test_delete_report_accepted_with_admin_auth() { let env = Env::default(); let (client, admin) = register_and_init(&env); + let reporter = Address::generate(&env); + client + .submit_report(&reporter, &1u32, &report_data(&env), &report_hash(&env)) + .unwrap(); + let _ = env.auths(); client.delete_report(&admin, &1u32).unwrap(); diff --git a/contracts/reporting/tests/market_reports_index.rs b/contracts/reporting/tests/market_reports_index.rs index d97b1187..c3b539b3 100644 --- a/contracts/reporting/tests/market_reports_index.rs +++ b/contracts/reporting/tests/market_reports_index.rs @@ -214,6 +214,54 @@ fn test_get_report_not_found() { } } +/// `delete_report` should return `ReportNotFound` for non-existent IDs. +#[test] +fn test_delete_report_not_found() { + let env = Env::default(); + let (client, admin, _reporter) = register_and_init(&env); + + let result = client.try_delete_report(&admin, &999); + match result { + Err(e) => { + let err = e.to_string(); + assert!(err.contains("ReportNotFound") || err.contains("7")); + } + Ok(_) => panic!("Expected ReportNotFound error"), + } +} + +/// Deleting a report should remove it from storage and its market index. +#[test] +fn test_delete_report_removes_report_and_market_index_entry() { + let env = Env::default(); + let (client, admin, reporter) = register_and_init(&env); + let market_id = 42u32; + let report_id = client + .submit_report( + &reporter, + &market_id, + &String::from_str(&env, "Report to delete"), + &String::from_str(&env, "hash"), + ) + .unwrap(); + let remaining_report_id = client + .submit_report( + &reporter, + &market_id, + &String::from_str(&env, "Report to keep"), + &String::from_str(&env, "hash"), + ) + .unwrap(); + + client.delete_report(&admin, &report_id).unwrap(); + + assert!(client.try_get_report(&report_id).is_err()); + assert!(client.get_report(&remaining_report_id).is_ok()); + let market_reports = client.get_reports_by_market(&market_id); + assert_eq!(market_reports.len(), 1); + assert_eq!(market_reports.get(0), Some(remaining_report_id)); +} + // --------------------------------------------------------------------------- // Pause behavior // ---------------------------------------------------------------------------