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