Skip to main content

simulator_api/
lib.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    fmt,
4};
5
6use base64::{
7    DecodeError as Base64DecodeError, Engine as _, engine::general_purpose::STANDARD as BASE64,
8};
9use serde::{Deserialize, Serialize};
10use solana_address::Address;
11
12/// Backtest RPC methods exposed to the client.
13#[derive(Debug, Serialize, Deserialize)]
14#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
15#[serde(tag = "method", content = "params", rename_all = "camelCase")]
16#[cfg_attr(feature = "ts-rs", ts(export))]
17pub enum BacktestRequest {
18    CreateBacktestSession(CreateBacktestSessionRequest),
19    Continue(ContinueParams),
20    ContinueSessionV1(ContinueSessionRequestV1),
21    CloseBacktestSession,
22    CloseSessionV1(CloseSessionRequestV1),
23    AttachBacktestSession {
24        session_id: String,
25        /// Last sequence number the client received. Responses after this sequence
26        /// will be replayed from the session's buffer. None = replay entire buffer.
27        last_sequence: Option<u64>,
28    },
29    /// Sent after reattaching and rebuilding any dependent subscriptions.
30    /// Allows the manager to resume a session that was paused for handoff.
31    ResumeAttachedSession,
32    AttachParallelControlSessionV2 {
33        control_session_id: String,
34        /// Last per-session sequence number received by the client. Responses after
35        /// these sequence numbers will be replayed from the manager's per-session
36        /// replay store. Missing sessions replay their entire retained history.
37        #[serde(default)]
38        last_sequences: BTreeMap<String, u64>,
39    },
40}
41
42/// Versioned payload for `CreateBacktestSession`.
43///
44/// - `V0` keeps backwards-compatible shape by using `CreateSessionParams` directly.
45/// - `V1` keeps the same shape and adds `parallel`.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
48#[serde(untagged)]
49#[cfg_attr(feature = "ts-rs", ts(export))]
50pub enum CreateBacktestSessionRequest {
51    V1(CreateBacktestSessionRequestV1),
52    V0(CreateSessionParams),
53}
54
55impl CreateBacktestSessionRequest {
56    pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
57        match self {
58            Self::V0(request) => CreateBacktestSessionRequestOptions {
59                request,
60                parallel: false,
61            },
62            Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
63                CreateBacktestSessionRequestOptions { request, parallel }
64            }
65        }
66    }
67
68    pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
69        let options = self.into_request_options();
70        (options.request, options.parallel)
71    }
72}
73
74impl From<CreateSessionParams> for CreateBacktestSessionRequest {
75    fn from(value: CreateSessionParams) -> Self {
76        Self::V0(value)
77    }
78}
79
80impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
81    fn from(value: CreateBacktestSessionRequestV1) -> Self {
82        Self::V1(value)
83    }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
88#[serde(rename_all = "camelCase")]
89#[cfg_attr(feature = "ts-rs", ts(export))]
90pub struct CreateBacktestSessionRequestV1 {
91    #[serde(flatten)]
92    pub request: CreateSessionParams,
93    pub parallel: bool,
94}
95
96#[derive(Debug, Clone)]
97pub struct CreateBacktestSessionRequestOptions {
98    pub request: CreateSessionParams,
99    pub parallel: bool,
100}
101
102#[derive(Debug, Serialize, Deserialize)]
103#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
104#[serde(rename_all = "camelCase")]
105#[cfg_attr(feature = "ts-rs", ts(export))]
106pub struct ContinueSessionRequestV1 {
107    pub session_id: String,
108    pub request: ContinueParams,
109}
110
111#[derive(Debug, Serialize, Deserialize)]
112#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
113#[serde(rename_all = "camelCase")]
114#[cfg_attr(feature = "ts-rs", ts(export))]
115pub struct CloseSessionRequestV1 {
116    pub session_id: String,
117}
118
119/// Parameters required to start a new backtest session.
120#[serde_with::serde_as]
121#[derive(Debug, Clone, Serialize, Deserialize)]
122#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
123#[serde(rename_all = "camelCase")]
124pub struct CreateSessionParams {
125    /// First slot (inclusive) to replay.
126    pub start_slot: u64,
127    /// Last slot (inclusive) to replay.
128    pub end_slot: u64,
129    #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
130    #[serde(default)]
131    #[cfg_attr(feature = "ts-rs", ts(as = "Vec<String>"))]
132    /// Skip transactions signed by these addresses.
133    pub signer_filter: BTreeSet<Address>,
134    #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
135    #[serde(default)]
136    #[cfg_attr(feature = "ts-rs", ts(as = "Vec<String>"))]
137    /// Programs to preload before executing.
138    pub preload_programs: BTreeSet<Address>,
139    /// Account bundle IDs to preload before executing.
140    #[serde(default)]
141    pub preload_account_bundles: Vec<String>,
142    /// When true, include a session summary with transaction statistics in client-facing
143    /// `Completed` responses. Summary generation remains enabled internally for metrics.
144    #[serde(default)]
145    pub send_summary: bool,
146    /// Maximum seconds to wait for ECS capacity-related startup retries before
147    /// failing session creation. If not set (or 0), capacity errors fail immediately.
148    #[serde(default)]
149    #[cfg_attr(feature = "ts-rs", ts(optional))]
150    pub capacity_wait_timeout_secs: Option<u16>,
151    /// Maximum seconds to keep the session alive after the control websocket disconnects.
152    /// If not set (or 0), the session tears down immediately on disconnect.
153    /// Maximum value: 900 (15 minutes).
154    #[serde(default)]
155    pub disconnect_timeout_secs: Option<u16>,
156    /// Extra compute units to add to each transaction's `SetComputeUnitLimit` budget.
157    /// Useful when replaying with an account override whose program uses more CU than
158    /// the original, causing otherwise-healthy transactions to run out of budget.
159    /// Only applied when a `SetComputeUnitLimit` instruction is already present.
160    #[serde(default)]
161    pub extra_compute_units: Option<u32>,
162    /// Agent configurations to run as sidecars alongside this session.
163    #[serde(default)]
164    pub agents: Vec<AgentParams>,
165}
166
167/// Available agent types for sidecar participation in backtest sessions.
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
170#[serde(rename_all = "camelCase")]
171#[cfg_attr(feature = "ts-rs", ts(export))]
172pub enum AgentType {
173    Arb,
174}
175
176/// Parameters for a circular arbitrage route.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
179#[serde(rename_all = "camelCase")]
180#[cfg_attr(feature = "ts-rs", ts(export))]
181pub struct ArbRouteParams {
182    pub base_mint: String,
183    pub temp_mint: String,
184    #[serde(default)]
185    pub buy_dexes: Vec<String>,
186    #[serde(default)]
187    pub sell_dexes: Vec<String>,
188    pub min_input: u64,
189    pub max_input: u64,
190    #[serde(default)]
191    pub min_profit: u64,
192}
193
194/// Configuration for an agent to run alongside a backtest session.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
197#[serde(rename_all = "camelCase")]
198#[cfg_attr(feature = "ts-rs", ts(export))]
199pub struct AgentParams {
200    pub agent_type: AgentType,
201    pub wallet: Option<String>,
202    /// Base58-encoded 64-byte keypair for signing transactions (compatible with `solana-keygen`).
203    pub keypair: Option<String>,
204    pub seed_sol_lamports: Option<u64>,
205    #[serde(default)]
206    pub seed_token_accounts: BTreeMap<String, u64>,
207    #[serde(default)]
208    pub arb_routes: Vec<ArbRouteParams>,
209}
210
211/// Account state modifications to apply.
212#[serde_with::serde_as]
213#[derive(Debug, Serialize, Deserialize, Default)]
214#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
215pub struct AccountModifications(
216    #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
217    #[serde(default)]
218    #[cfg_attr(feature = "ts-rs", ts(as = "BTreeMap<String, AccountData>"))]
219    pub BTreeMap<Address, AccountData>,
220);
221
222/// Arguments used to continue an existing session.
223#[serde_with::serde_as]
224#[derive(Debug, Serialize, Deserialize)]
225#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
226#[serde(rename_all = "camelCase")]
227pub struct ContinueParams {
228    #[serde(default = "ContinueParams::default_advance_count")]
229    /// Number of blocks to advance before waiting.
230    pub advance_count: u64,
231    #[serde(default)]
232    /// Base64-encoded transactions to execute before advancing.
233    pub transactions: Vec<String>,
234    #[serde(default)]
235    /// Account state overrides to apply ahead of execution.
236    pub modify_account_states: AccountModifications,
237}
238
239impl Default for ContinueParams {
240    fn default() -> Self {
241        Self {
242            advance_count: Self::default_advance_count(),
243            transactions: Vec::new(),
244            modify_account_states: AccountModifications(BTreeMap::new()),
245        }
246    }
247}
248
249impl ContinueParams {
250    pub fn default_advance_count() -> u64 {
251        1
252    }
253}
254
255/// Supported binary encodings for account/transaction payloads.
256#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
257#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
258#[serde(rename_all = "lowercase")]
259pub enum BinaryEncoding {
260    Base64,
261}
262
263impl BinaryEncoding {
264    pub fn encode(self, bytes: &[u8]) -> String {
265        match self {
266            Self::Base64 => BASE64.encode(bytes),
267        }
268    }
269
270    pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
271        match self {
272            Self::Base64 => BASE64.decode(data),
273        }
274    }
275}
276
277/// A blob paired with the encoding needed to decode it.
278#[derive(Debug, Clone, Serialize, Deserialize)]
279#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
280#[serde(rename_all = "camelCase")]
281pub struct EncodedBinary {
282    /// Encoded payload.
283    pub data: String,
284    /// Encoding scheme used for the payload.
285    pub encoding: BinaryEncoding,
286}
287
288impl EncodedBinary {
289    pub fn new(data: String, encoding: BinaryEncoding) -> Self {
290        Self { data, encoding }
291    }
292
293    pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
294        Self {
295            data: encoding.encode(bytes),
296            encoding,
297        }
298    }
299
300    pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
301        self.encoding.decode(&self.data)
302    }
303}
304
305/// Account snapshot used to seed or modify state in a session.
306#[serde_with::serde_as]
307#[derive(Debug, Serialize, Deserialize)]
308#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
309#[serde(rename_all = "camelCase")]
310pub struct AccountData {
311    /// Account data bytes and encoding.
312    pub data: EncodedBinary,
313    /// Whether the account is executable.
314    pub executable: bool,
315    /// Lamport balance.
316    pub lamports: u64,
317    #[serde_as(as = "serde_with::DisplayFromStr")]
318    #[cfg_attr(feature = "ts-rs", ts(as = "String"))]
319    /// Account owner pubkey.
320    pub owner: Address,
321    /// Allocated space in bytes.
322    pub space: u64,
323}
324
325/// Responses returned over the backtest RPC channel.
326#[derive(Debug, Clone, Serialize, Deserialize)]
327#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
328#[serde(tag = "method", content = "params", rename_all = "camelCase")]
329#[cfg_attr(feature = "ts-rs", ts(export))]
330pub enum BacktestResponse {
331    SessionCreated {
332        session_id: String,
333        rpc_endpoint: String,
334    },
335    SessionAttached {
336        session_id: String,
337        rpc_endpoint: String,
338    },
339    SessionsCreated {
340        session_ids: Vec<String>,
341    },
342    SessionsCreatedV2 {
343        control_session_id: String,
344        session_ids: Vec<String>,
345    },
346    ParallelSessionAttachedV2 {
347        control_session_id: String,
348        session_ids: Vec<String>,
349    },
350    ReadyForContinue,
351    SlotNotification(u64),
352    Error(BacktestError),
353    Success,
354    Completed {
355        /// Session summary with transaction statistics.
356        /// The session always computes this summary, but management may omit it from
357        /// client-facing responses unless `send_summary` was requested at session creation.
358        #[serde(skip_serializing_if = "Option::is_none")]
359        summary: Option<SessionSummary>,
360        #[serde(default, skip_serializing_if = "Option::is_none")]
361        agent_stats: Option<Vec<AgentStatsReport>>,
362    },
363    Status {
364        status: BacktestStatus,
365    },
366    SessionEventV1 {
367        session_id: String,
368        event: SessionEventV1,
369    },
370    SessionEventV2 {
371        session_id: String,
372        seq_id: u64,
373        event: SessionEventKind,
374    },
375}
376
377impl BacktestResponse {
378    pub fn is_completed(&self) -> bool {
379        matches!(self, BacktestResponse::Completed { .. })
380    }
381
382    pub fn is_terminal(&self) -> bool {
383        match self {
384            BacktestResponse::Completed { .. } => true,
385            BacktestResponse::Error(e) => matches!(
386                e,
387                BacktestError::NoMoreBlocks
388                    | BacktestError::AdvanceSlotFailed { .. }
389                    | BacktestError::Internal { .. }
390            ),
391            _ => false,
392        }
393    }
394}
395
396impl From<BacktestStatus> for BacktestResponse {
397    fn from(status: BacktestStatus) -> Self {
398        Self::Status { status }
399    }
400}
401
402impl From<String> for BacktestResponse {
403    fn from(message: String) -> Self {
404        BacktestError::Internal { error: message }.into()
405    }
406}
407
408impl From<&str> for BacktestResponse {
409    fn from(message: &str) -> Self {
410        BacktestError::Internal {
411            error: message.to_string(),
412        }
413        .into()
414    }
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
418#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
419#[serde(tag = "method", content = "params", rename_all = "camelCase")]
420#[cfg_attr(feature = "ts-rs", ts(export))]
421pub enum SessionEventV1 {
422    ReadyForContinue,
423    SlotNotification(u64),
424    Error(BacktestError),
425    Success,
426    Completed {
427        #[serde(skip_serializing_if = "Option::is_none")]
428        summary: Option<SessionSummary>,
429        #[serde(default, skip_serializing_if = "Option::is_none")]
430        agent_stats: Option<Vec<AgentStatsReport>>,
431    },
432    Status {
433        status: BacktestStatus,
434    },
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
438#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
439#[serde(tag = "method", content = "params", rename_all = "camelCase")]
440#[cfg_attr(feature = "ts-rs", ts(export))]
441pub enum SessionEventKind {
442    ReadyForContinue,
443    SlotNotification(u64),
444    Error(BacktestError),
445    Success,
446    Completed {
447        #[serde(skip_serializing_if = "Option::is_none")]
448        summary: Option<SessionSummary>,
449    },
450    Status {
451        status: BacktestStatus,
452    },
453}
454
455impl SessionEventKind {
456    pub fn is_terminal(&self) -> bool {
457        match self {
458            Self::Completed { .. } => true,
459            Self::Error(e) => matches!(
460                e,
461                BacktestError::NoMoreBlocks
462                    | BacktestError::AdvanceSlotFailed { .. }
463                    | BacktestError::Internal { .. }
464            ),
465            _ => false,
466        }
467    }
468}
469
470/// Wire format wrapper for responses sent over the control websocket.
471/// Sessions with `disconnect_timeout_secs > 0` use this to track client position.
472#[derive(Debug, Clone, Serialize, Deserialize)]
473#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
474#[serde(rename_all = "camelCase")]
475#[cfg_attr(feature = "ts-rs", ts(export))]
476pub struct SequencedResponse {
477    pub seq_id: u64,
478    #[serde(flatten)]
479    pub response: BacktestResponse,
480}
481
482/// High-level progress states during a `Continue` call.
483#[derive(Debug, Clone, Serialize, Deserialize)]
484#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
485#[serde(rename_all = "camelCase")]
486pub enum BacktestStatus {
487    /// Runtime startup is in progress.
488    StartingRuntime,
489    DecodedTransactions,
490    AppliedAccountModifications,
491    ReadyToExecuteUserTransactions,
492    ExecutedUserTransactions,
493    ExecutingBlockTransactions,
494    ExecutedBlockTransactions,
495    ProgramAccountsLoaded,
496}
497
498/// Structured stats reported by an agent during a backtest session.
499#[derive(Debug, Clone, Default, Serialize, Deserialize)]
500#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
501#[serde(rename_all = "camelCase")]
502#[cfg_attr(feature = "ts-rs", ts(export))]
503pub struct AgentStatsReport {
504    pub name: String,
505    pub slots_processed: u64,
506    pub opportunities_found: u64,
507    pub opportunities_skipped: u64,
508    pub no_routes: u64,
509    pub txs_produced: u64,
510    /// Cumulative expected profit per base mint, keyed by mint address.
511    pub expected_gain_by_mint: BTreeMap<String, i64>,
512    /// Transactions successfully executed by the sidecar.
513    #[serde(default)]
514    pub txs_submitted: u64,
515    /// Transactions that failed execution.
516    #[serde(default)]
517    pub txs_failed: u64,
518    /// Transactions rejected by preflight simulation (unprofitable).
519    #[serde(default)]
520    pub txs_simulation_rejected: u64,
521    /// Preflight simulation RPC calls that errored.
522    #[serde(default)]
523    pub txs_simulation_failed: u64,
524}
525
526/// Summary of transaction execution statistics for a completed backtest session.
527#[derive(Debug, Clone, Default, Serialize, Deserialize)]
528#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
529#[serde(rename_all = "camelCase")]
530pub struct SessionSummary {
531    /// Number of simulations where simulator outcome matched on-chain outcome
532    /// (`true_success + true_failure`).
533    pub correct_simulation: usize,
534    /// Number of simulations where simulator outcome did not match on-chain outcome
535    /// (`false_success + false_failure`).
536    pub incorrect_simulation: usize,
537    /// Number of transactions that had execution errors in simulation.
538    pub execution_errors: usize,
539    /// Number of transactions with different balance diffs.
540    pub balance_diff: usize,
541    /// Number of transactions with different log diffs.
542    pub log_diff: usize,
543}
544
545impl SessionSummary {
546    /// Returns true if there were any execution deviations (errors or mismatched results).
547    pub fn has_deviations(&self) -> bool {
548        self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
549    }
550
551    /// Total number of transactions processed.
552    pub fn total_transactions(&self) -> usize {
553        self.correct_simulation
554            + self.incorrect_simulation
555            + self.execution_errors
556            + self.balance_diff
557            + self.log_diff
558    }
559}
560
561impl std::fmt::Display for SessionSummary {
562    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
563        let total = self.total_transactions();
564        write!(
565            f,
566            "Session summary: {total} transactions\n\
567             \x20  - {} correct simulation\n\
568             \x20  - {} incorrect simulation\n\
569             \x20  - {} execution errors\n\
570             \x20  - {} balance diffs\n\
571             \x20  - {} log diffs",
572            self.correct_simulation,
573            self.incorrect_simulation,
574            self.execution_errors,
575            self.balance_diff,
576            self.log_diff,
577        )
578    }
579}
580
581/// Error variants surfaced to backtest RPC clients.
582#[derive(Debug, Clone, Serialize, Deserialize)]
583#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
584#[serde(rename_all = "camelCase")]
585pub enum BacktestError {
586    InvalidTransactionEncoding {
587        index: usize,
588        error: String,
589    },
590    InvalidTransactionFormat {
591        index: usize,
592        error: String,
593    },
594    InvalidAccountEncoding {
595        address: String,
596        encoding: BinaryEncoding,
597        error: String,
598    },
599    InvalidAccountOwner {
600        address: String,
601        error: String,
602    },
603    InvalidAccountPubkey {
604        address: String,
605        error: String,
606    },
607    NoMoreBlocks,
608    AdvanceSlotFailed {
609        slot: u64,
610        error: String,
611    },
612    InvalidRequest {
613        error: String,
614    },
615    Internal {
616        error: String,
617    },
618    InvalidBlockhashFormat {
619        slot: u64,
620        error: String,
621    },
622    InitializingSysvarsFailed {
623        slot: u64,
624        error: String,
625    },
626    ClerkError {
627        error: String,
628    },
629    SimulationError {
630        error: String,
631    },
632    SessionNotFound {
633        session_id: String,
634    },
635    SessionOwnerMismatch,
636}
637
638/// One contiguous block range available on the history clerk.
639#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct AvailableRange {
641    pub bundle_start_slot: u64,
642    pub bundle_start_slot_utc: Option<String>,
643    pub max_bundle_end_slot: Option<u64>,
644    pub max_bundle_end_slot_utc: Option<String>,
645    pub max_bundle_size: Option<u64>,
646}
647
648/// Split a user-requested `[start_slot, end_slot]` range across the available
649/// bundle ranges, returning a list of non-overlapping `(start, end)` pairs — one
650/// per bundle that intersects the requested range.
651///
652/// Returns an error if the requested start slot is not covered by any range.
653pub fn split_range(
654    ranges: &[AvailableRange],
655    requested_start: u64,
656    requested_end: u64,
657) -> Result<Vec<(u64, u64)>, String> {
658    if requested_end < requested_start {
659        return Err(format!(
660            "invalid range: start_slot {requested_start} > end_slot {requested_end}"
661        ));
662    }
663
664    let mut candidates: Vec<(u64, u64)> = ranges
665        .iter()
666        .filter_map(|r| Some((r.bundle_start_slot, r.max_bundle_end_slot?)))
667        .filter(|(start, end)| end >= start)
668        .collect();
669
670    candidates.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
671    candidates.dedup_by_key(|(start, _)| *start);
672
673    if candidates.is_empty() {
674        return Err("no available bundle ranges found on server".to_string());
675    }
676
677    let mut non_overlapping: Vec<(u64, u64)> = Vec::with_capacity(candidates.len());
678    for (i, (start, mut end)) in candidates.iter().copied().enumerate() {
679        if let Some((next_start, _)) = candidates.get(i + 1).copied()
680            && next_start <= end
681        {
682            end = next_start.saturating_sub(1);
683        }
684        if end >= start {
685            non_overlapping.push((start, end));
686        }
687    }
688
689    let anchor = non_overlapping
690        .iter()
691        .enumerate()
692        .rev()
693        .find(|(_, (s, e))| *s <= requested_start && *e >= requested_start)
694        .map(|(i, _)| i)
695        .ok_or_else(|| {
696            format!("start_slot {requested_start} is not covered by any available bundle range")
697        })?;
698
699    let mut result = Vec::new();
700    for (start, range_end) in non_overlapping.into_iter().skip(anchor) {
701        if start > requested_end {
702            break;
703        }
704        let end = range_end.min(requested_end);
705        if end >= start {
706            result.push((start, end));
707        }
708        if end == requested_end {
709            break;
710        }
711    }
712
713    if result.is_empty() {
714        return Err(format!(
715            "no available bundle ranges intersect requested range [{requested_start}-{requested_end}]"
716        ));
717    }
718
719    Ok(result)
720}
721
722impl From<BacktestError> for BacktestResponse {
723    fn from(error: BacktestError) -> Self {
724        Self::Error(error)
725    }
726}
727
728impl std::error::Error for BacktestError {}
729
730impl fmt::Display for BacktestError {
731    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
732        match self {
733            BacktestError::InvalidTransactionEncoding { index, error } => {
734                write!(f, "invalid transaction encoding at index {index}: {error}")
735            }
736            BacktestError::InvalidTransactionFormat { index, error } => {
737                write!(f, "invalid transaction format at index {index}: {error}")
738            }
739            BacktestError::InvalidAccountEncoding {
740                address,
741                encoding,
742                error,
743            } => write!(
744                f,
745                "invalid encoding for account {address} ({encoding:?}): {error}"
746            ),
747            BacktestError::InvalidAccountOwner { address, error } => {
748                write!(f, "invalid owner for account {address}: {error}")
749            }
750            BacktestError::InvalidAccountPubkey { address, error } => {
751                write!(f, "invalid account pubkey {address}: {error}")
752            }
753            BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
754            BacktestError::AdvanceSlotFailed { slot, error } => {
755                write!(f, "failed to advance to slot {slot}: {error}")
756            }
757            BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
758            BacktestError::Internal { error } => write!(f, "internal error: {error}"),
759            BacktestError::InvalidBlockhashFormat { slot, error } => {
760                write!(f, "invalid blockhash at slot {slot}: {error}")
761            }
762            BacktestError::InitializingSysvarsFailed { slot, error } => {
763                write!(f, "failed to initialize sysvars at slot {slot}: {error}")
764            }
765            BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
766            BacktestError::SimulationError { error } => {
767                write!(f, "simulation error: {error}")
768            }
769            BacktestError::SessionNotFound { session_id } => {
770                write!(f, "session not found: {session_id}")
771            }
772            BacktestError::SessionOwnerMismatch => {
773                write!(f, "session owner mismatch")
774            }
775        }
776    }
777}