Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions contracts/predictify-hybrid/src/reporting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,89 @@ pub struct EventSnapshot {
pub end_time: u64,
}

// ---------------------------------------------------------------------------
// Snapshot Diffing
// ---------------------------------------------------------------------------

/// A snapshot containing multiple event states for offline comparison.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StateSnapshot {
pub stats: PlatformStats,
pub events: Map<Symbol, EventSnapshot>,
}

/// A symmetric diff of two `StateSnapshot`s.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SnapshotDiff {
pub added: Vec<Symbol>,
pub removed: Vec<Symbol>,
pub changed: Vec<Symbol>,
pub fee_delta: i128,
pub total_pool_delta: i128,
}

impl SnapshotDiff {
/// Inverts the diff such that `diff(a, b).invert(env) == diff(b, a)`
pub fn invert(&self, env: &Env) -> Self {
Self {
added: self.removed.clone(),
removed: self.added.clone(),
changed: self.changed.clone(),
fee_delta: -self.fee_delta,
total_pool_delta: -self.total_pool_delta,
}
}
}

impl StateSnapshot {
/// Computes a typed difference between `prev` and `next` `StateSnapshot`s.
/// Returns a deterministic, ordered list of market IDs that were added, removed, or changed.
/// Also includes the fee and total-pool deltas.
pub fn diff(env: &Env, prev: &Self, next: &Self) -> SnapshotDiff {
let mut added = Vec::new(env);
let mut removed = Vec::new(env);
let mut changed = Vec::new(env);

let mut unique_keys: Map<Symbol, ()> = Map::new(env);
for key in prev.events.keys().into_iter() {
unique_keys.set(key.clone(), ());
}
for key in next.events.keys().into_iter() {
unique_keys.set(key.clone(), ());
}

for key in unique_keys.keys().into_iter() {
let val_prev = prev.events.get(key.clone());
let val_next = next.events.get(key.clone());

match (val_prev, val_next) {
(Some(_), None) => removed.push_back(key),
(None, Some(_)) => added.push_back(key),
(Some(p), Some(n)) => {
if p != n {
changed.push_back(key);
}
}
(None, None) => {}
}
}

let fee_delta = next.stats.total_fees_collected.saturating_sub(prev.stats.total_fees_collected);
let pool_delta = next.stats.total_pool_all_events.saturating_sub(prev.stats.total_pool_all_events);

SnapshotDiff {
added,
removed,
changed,
fee_delta,
total_pool_delta: pool_delta,
}
}
}


// ---------------------------------------------------------------------------
// SnapshotEnvelope
// ---------------------------------------------------------------------------
Expand Down
4 changes: 3 additions & 1 deletion contracts/predictify-hybrid/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ pub mod dispute_stake_tests;
pub mod fee_config_commit_reveal_tests;
pub mod reflector_twap_cache_tests;
pub mod dispute_anti_grief_tests;
pub mod oracle_differential_fuzz;
pub mod oracle_differential_fuzz;
pub mod monitoring_mttr_tests;
pub mod snapshot_diffing_tests;
93 changes: 93 additions & 0 deletions contracts/predictify-hybrid/src/tests/snapshot_diffing_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#![cfg(test)]

use crate::reporting::{EventSnapshot, PlatformStats, SnapshotDiff, StateSnapshot};
use crate::types::MarketState;
use soroban_sdk::{Env, Map, String, Symbol, Vec};

fn create_event_snapshot(env: &Env, id: &str, pool: i128) -> EventSnapshot {
EventSnapshot {
id: Symbol::new(env, id),
question: String::from_str(env, "Q"),
outcomes: Vec::new(env),
state: MarketState::Active,
total_pool: pool,
outcome_pools: Map::new(env),
participant_count: 0,
end_time: 0,
}
}

fn create_platform_stats(env: &Env, fees: i128, pool: i128) -> PlatformStats {
PlatformStats {
total_active_events: 0,
total_resolved_events: 0,
total_pool_all_events: pool,
total_fees_collected: fees,
version: String::from_str(env, "1.0.0"),
}
}

#[test]
fn test_state_snapshot_diff() {
let env = Env::default();

let mut events_a = Map::new(&env);
events_a.set(
Symbol::new(&env, "market1"),
create_event_snapshot(&env, "market1", 100),
);
events_a.set(
Symbol::new(&env, "market2"),
create_event_snapshot(&env, "market2", 200),
);
let stats_a = create_platform_stats(&env, 50, 300);

let mut events_b = Map::new(&env);
events_b.set(
Symbol::new(&env, "market1"),
create_event_snapshot(&env, "market1", 100), // unchanged
);
events_b.set(
Symbol::new(&env, "market2"),
create_event_snapshot(&env, "market2", 300), // changed
);
events_b.set(
Symbol::new(&env, "market3"),
create_event_snapshot(&env, "market3", 500), // added
);
let stats_b = create_platform_stats(&env, 70, 900);

let snapshot_a = StateSnapshot { stats: stats_a.clone(), events: events_a };
let snapshot_b = StateSnapshot { stats: stats_b.clone(), events: events_b };

// diff(A, B)
let diff_ab = StateSnapshot::diff(&env, &snapshot_a, &snapshot_b);
assert_eq!(diff_ab.added.len(), 1);
assert!(diff_ab.added.contains(&Symbol::new(&env, "market3")));
assert_eq!(diff_ab.removed.len(), 0);
assert_eq!(diff_ab.changed.len(), 1);
assert!(diff_ab.changed.contains(&Symbol::new(&env, "market2")));
assert_eq!(diff_ab.fee_delta, 20); // 70 - 50
assert_eq!(diff_ab.total_pool_delta, 600); // 900 - 300

// diff(B, A)
let diff_ba = StateSnapshot::diff(&env, &snapshot_b, &snapshot_a);
assert_eq!(diff_ba.added.len(), 0);
assert_eq!(diff_ba.removed.len(), 1);
assert!(diff_ba.removed.contains(&Symbol::new(&env, "market3")));
assert_eq!(diff_ba.changed.len(), 1);
assert!(diff_ba.changed.contains(&Symbol::new(&env, "market2")));
assert_eq!(diff_ba.fee_delta, -20); // 50 - 70
assert_eq!(diff_ba.total_pool_delta, -600); // 300 - 900

// symmetric property: diff(a, b).invert() == diff(b, a)
assert_eq!(diff_ab.invert(&env), diff_ba);

// identity property: diff(A, A) is empty
let diff_aa = StateSnapshot::diff(&env, &snapshot_a, &snapshot_a);
assert_eq!(diff_aa.added.len(), 0);
assert_eq!(diff_aa.removed.len(), 0);
assert_eq!(diff_aa.changed.len(), 0);
assert_eq!(diff_aa.fee_delta, 0);
assert_eq!(diff_aa.total_pool_delta, 0);
}
1 change: 1 addition & 0 deletions docs/CAPABILITIES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Contract Capabilities


The Predictify Hybrid contract exposes a **u64 capabilities bitmap** that allows
clients to discover which features are available without inspecting the Wasm
binary or relying on version-number heuristics.
Expand Down
Loading