|
| 1 | +//! In-memory fee database for the sandbox environment. |
| 2 | +//! |
| 3 | +//! [`SandboxEnv`] holds 10,000 synthetic fee records spanning a 24-hour window |
| 4 | +//! covering Normal, Rising, Congested, and Spike network scenarios. |
| 5 | +//! No external database dependencies are required. |
| 6 | +
|
| 7 | +use crate::sandbox::fixtures; |
| 8 | + |
| 9 | +/// Network regime represented by a block of fee records. |
| 10 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 11 | +pub enum Regime { |
| 12 | + /// Quiet network — fees 100–500 stroops. |
| 13 | + Normal, |
| 14 | + /// Fees gradually climbing above the normal band. |
| 15 | + Rising, |
| 16 | + /// Sustained high demand; fees significantly elevated. |
| 17 | + Congested, |
| 18 | + /// Short-lived burst; fees spike then recover. |
| 19 | + Spike, |
| 20 | +} |
| 21 | + |
| 22 | +/// A single fee observation stored in the sandbox database. |
| 23 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 24 | +pub struct FeeRecord { |
| 25 | + /// Milliseconds since the Unix epoch. |
| 26 | + pub timestamp_ms: u64, |
| 27 | + /// Fee amount in stroops. |
| 28 | + pub fee_stroops: u64, |
| 29 | + /// Network regime this record belongs to. |
| 30 | + pub regime: Regime, |
| 31 | +} |
| 32 | + |
| 33 | +/// In-memory fee database pre-seeded with 10,000 [`FeeRecord`]s spanning 24 hours. |
| 34 | +/// |
| 35 | +/// Records are partitioned evenly across four [`Regime`] scenarios so tests can |
| 36 | +/// exercise the full range of network behaviour without hitting live infrastructure. |
| 37 | +/// |
| 38 | +/// # Example |
| 39 | +/// ```rust |
| 40 | +/// use stellar_devkit::sandbox::environment::SandboxEnv; |
| 41 | +/// |
| 42 | +/// let mut env = SandboxEnv::new(); |
| 43 | +/// assert_eq!(env.records().len(), 10_000); |
| 44 | +/// env.reset(); |
| 45 | +/// assert_eq!(env.records().len(), 10_000); |
| 46 | +/// ``` |
| 47 | +pub struct SandboxEnv { |
| 48 | + records: Vec<FeeRecord>, |
| 49 | +} |
| 50 | + |
| 51 | +impl SandboxEnv { |
| 52 | + /// Creates a new [`SandboxEnv`] and seeds it with 10,000 records. |
| 53 | + pub fn new() -> Self { |
| 54 | + let mut env = Self { records: Vec::with_capacity(10_000) }; |
| 55 | + env.seed(); |
| 56 | + env |
| 57 | + } |
| 58 | + |
| 59 | + /// Seeds the database with 10,000 fee records spanning 24 hours. |
| 60 | + pub fn seed(&mut self) { |
| 61 | + self.records.clear(); |
| 62 | + const ANCHOR_MS: u64 = 1_753_315_200_000; |
| 63 | + const DAY_MS: u64 = 86_400_000; |
| 64 | + let start_ms = ANCHOR_MS.saturating_sub(DAY_MS); |
| 65 | + let per_regime: u64 = 2_500; |
| 66 | + let regime_duration_ms = DAY_MS / 4; |
| 67 | + |
| 68 | + let scenarios: &[(Regime, u64)] = &[ |
| 69 | + (Regime::Normal, 0), |
| 70 | + (Regime::Rising, 1), |
| 71 | + (Regime::Congested, 2), |
| 72 | + (Regime::Spike, 3), |
| 73 | + ]; |
| 74 | + |
| 75 | + for &(regime, quarter) in scenarios { |
| 76 | + let seg_start = start_ms + quarter * regime_duration_ms; |
| 77 | + let interval_ms = regime_duration_ms / per_regime; |
| 78 | + for i in 0..per_regime { |
| 79 | + let timestamp_ms = seg_start + i * interval_ms; |
| 80 | + let fee_stroops = match regime { |
| 81 | + Regime::Normal => 100 + (i * 400 / per_regime), |
| 82 | + Regime::Rising => 400 + (i * 1_100 / per_regime), |
| 83 | + Regime::Congested => 1_000 + (i * 4_000 / per_regime), |
| 84 | + Regime::Spike => if i % 50 == 0 { 50_000 } else { 100 + (i % 50) * 4 }, |
| 85 | + }; |
| 86 | + self.records.push(FeeRecord { timestamp_ms, fee_stroops, regime }); |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + /// Discards all records and re-seeds the database from scratch. |
| 92 | + pub fn reset(&mut self) { |
| 93 | + self.seed(); |
| 94 | + } |
| 95 | + |
| 96 | + /// Returns a read-only slice of all fee records. |
| 97 | + pub fn records(&self) -> &[FeeRecord] { |
| 98 | + &self.records |
| 99 | + } |
| 100 | + |
| 101 | + /// Returns a mutable reference to the underlying record list. |
| 102 | + pub fn records_mut(&mut self) -> &mut Vec<FeeRecord> { |
| 103 | + &mut self.records |
| 104 | + } |
| 105 | + |
| 106 | + /// Returns only the records belonging to the given [`Regime`]. |
| 107 | + pub fn records_for_regime(&self, regime: Regime) -> Vec<&FeeRecord> { |
| 108 | + self.records.iter().filter(|r| r.regime == regime).collect() |
| 109 | + } |
| 110 | + |
| 111 | + /// Total number of fee records currently in the database. |
| 112 | + pub fn len(&self) -> usize { |
| 113 | + self.records.len() |
| 114 | + } |
| 115 | + |
| 116 | + /// Returns `true` if the database contains no records. |
| 117 | + pub fn is_empty(&self) -> bool { |
| 118 | + self.records.is_empty() |
| 119 | + } |
| 120 | + |
| 121 | + /// Creates a [`SandboxEnv`] seeded from the normal-network fixture. |
| 122 | + pub fn from_normal_fixture() -> Self { |
| 123 | + let raw = fixtures::normal_network(); |
| 124 | + let records = raw |
| 125 | + .into_iter() |
| 126 | + .map(|(timestamp_ms, fee_stroops)| FeeRecord { |
| 127 | + timestamp_ms, |
| 128 | + fee_stroops, |
| 129 | + regime: Regime::Normal, |
| 130 | + }) |
| 131 | + .collect(); |
| 132 | + Self { records } |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +impl Default for SandboxEnv { |
| 137 | + fn default() -> Self { |
| 138 | + Self::new() |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +#[cfg(test)] |
| 143 | +mod tests { |
| 144 | + use super::*; |
| 145 | + |
| 146 | + #[test] |
| 147 | + fn seed_produces_10k_records() { |
| 148 | + let env = SandboxEnv::new(); |
| 149 | + assert_eq!(env.len(), 10_000); |
| 150 | + } |
| 151 | + |
| 152 | + #[test] |
| 153 | + fn each_regime_has_2500_records() { |
| 154 | + let env = SandboxEnv::new(); |
| 155 | + for regime in [Regime::Normal, Regime::Rising, Regime::Congested, Regime::Spike] { |
| 156 | + assert_eq!(env.records_for_regime(regime).len(), 2_500); |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + #[test] |
| 161 | + fn reset_restores_10k_records() { |
| 162 | + let mut env = SandboxEnv::new(); |
| 163 | + env.records_mut().clear(); |
| 164 | + env.reset(); |
| 165 | + assert_eq!(env.len(), 10_000); |
| 166 | + } |
| 167 | + |
| 168 | + #[test] |
| 169 | + fn from_normal_fixture_has_10k_records() { |
| 170 | + let env = SandboxEnv::from_normal_fixture(); |
| 171 | + assert_eq!(env.len(), 10_000); |
| 172 | + } |
| 173 | +} |
0 commit comments