simulator-api 0.11.0

Wire-protocol types for the Solana simulator backtest WebSocket API
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Response body of `GET /usage`.
#[derive(Debug, Serialize, Deserialize)]
pub struct UsageReport {
    pub api_key_name: String,
    pub since: DateTime<Utc>,
    pub until: DateTime<Utc>,
    pub sessions: SessionCounts,
    pub compute: ComputeTotals,
    /// Present only for keys with the `build_bundle` feature.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub bundle_builds: Option<BundleBuildUsage>,
}

/// Bundle-build usage for the window.
#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub struct BundleBuildUsage {
    pub requested: u64,
}

/// Raw `backtest_session_*` point counts for this api key in the window, by
/// outcome — not session-deduplicated, so the three need not balance (a
/// pre-start failure has no `started`; a cross-manager handoff re-emits
/// `started`). Billing uses the compute totals, which derive only from the
/// terminal completed/failed points.
#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub struct SessionCounts {
    pub started: u64,
    pub completed: u64,
    pub failed: u64,
}

/// Aggregated compute totals for a usage window.
#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub struct ComputeTotals {
    pub executed_slot_count: u64,
    pub session_duration_ms: u64,
}

impl SessionCounts {
    pub fn saturating_add(self, other: Self) -> Self {
        Self {
            started: self.started.saturating_add(other.started),
            completed: self.completed.saturating_add(other.completed),
            failed: self.failed.saturating_add(other.failed),
        }
    }
}

impl ComputeTotals {
    pub fn saturating_add(self, other: Self) -> Self {
        Self {
            executed_slot_count: self
                .executed_slot_count
                .saturating_add(other.executed_slot_count),
            session_duration_ms: self
                .session_duration_ms
                .saturating_add(other.session_duration_ms),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn report(bundle_builds: Option<BundleBuildUsage>) -> UsageReport {
        UsageReport {
            api_key_name: "k".to_string(),
            since: "2026-01-01T00:00:00Z".parse().unwrap(),
            until: "2026-01-02T00:00:00Z".parse().unwrap(),
            sessions: SessionCounts::default(),
            compute: ComputeTotals::default(),
            bundle_builds,
        }
    }

    #[test]
    fn usage_report_absent_bundle_builds_round_trips_to_none() {
        let json = serde_json::to_string(&report(None)).unwrap();
        assert!(!json.contains("bundle_builds"));
        let parsed: UsageReport = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.bundle_builds, None);
    }

    #[test]
    fn usage_report_present_bundle_builds_round_trips() {
        let json = serde_json::to_string(&report(Some(BundleBuildUsage { requested: 3 }))).unwrap();
        let parsed: UsageReport = serde_json::from_str(&json).unwrap();
        assert_eq!(
            parsed.bundle_builds,
            Some(BundleBuildUsage { requested: 3 })
        );
    }
}