1pub mod usage;
2
3use std::{
4 collections::{BTreeMap, BTreeSet, HashSet},
5 fmt,
6};
7
8use base64::{
9 DecodeError as Base64DecodeError, Engine as _, engine::general_purpose::STANDARD as BASE64,
10};
11use serde::{Deserialize, Serialize};
12use solana_address::Address;
13
14#[derive(Debug, Serialize, Deserialize)]
16#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
17#[serde(tag = "method", content = "params", rename_all = "camelCase")]
18#[cfg_attr(feature = "ts-rs", ts(export))]
19pub enum BacktestRequest {
20 CreateBacktestSession(CreateBacktestSessionRequest),
21 Continue(ContinueParams),
22 ContinueTo(ContinueToParams),
23 ContinueSessionV1(ContinueSessionRequestV1),
24 ContinueToSessionV1(ContinueToSessionRequestV1),
25 CloseBacktestSession,
26 CloseSessionV1(CloseSessionRequestV1),
27 AttachBacktestSession {
28 session_id: String,
29 last_sequence: Option<u64>,
32 },
33 ResumeAttachedSession,
36 AttachParallelControlSessionV2 {
37 control_session_id: String,
38 #[serde(default)]
42 last_sequences: BTreeMap<String, u64>,
43 },
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
51#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
52#[serde(untagged)]
53#[cfg_attr(feature = "ts-rs", ts(export))]
54pub enum CreateBacktestSessionRequest {
55 V1(CreateBacktestSessionRequestV1),
56 V0(CreateSessionParams),
57}
58
59impl CreateBacktestSessionRequest {
60 pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
61 match self {
62 Self::V0(request) => CreateBacktestSessionRequestOptions {
63 request,
64 parallel: false,
65 },
66 Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
67 CreateBacktestSessionRequestOptions { request, parallel }
68 }
69 }
70 }
71
72 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
73 let options = self.into_request_options();
74 (options.request, options.parallel)
75 }
76}
77
78impl From<CreateSessionParams> for CreateBacktestSessionRequest {
79 fn from(value: CreateSessionParams) -> Self {
80 Self::V0(value)
81 }
82}
83
84impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
85 fn from(value: CreateBacktestSessionRequestV1) -> Self {
86 Self::V1(value)
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
92#[serde(rename_all = "camelCase")]
93#[cfg_attr(feature = "ts-rs", ts(export))]
94pub struct CreateBacktestSessionRequestV1 {
95 #[serde(flatten)]
96 pub request: CreateSessionParams,
97 pub parallel: bool,
98}
99
100#[derive(Debug, Clone)]
101pub struct CreateBacktestSessionRequestOptions {
102 pub request: CreateSessionParams,
103 pub parallel: bool,
104}
105
106#[derive(Debug, Serialize, Deserialize)]
107#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
108#[serde(rename_all = "camelCase")]
109#[cfg_attr(feature = "ts-rs", ts(export))]
110pub struct ContinueSessionRequestV1 {
111 pub session_id: String,
112 pub request: ContinueParams,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
117#[serde(rename_all = "camelCase")]
118#[cfg_attr(feature = "ts-rs", ts(export))]
119pub struct ContinueToSessionRequestV1 {
120 pub session_id: String,
121 pub request: ContinueToParams,
122}
123
124#[derive(Debug, Serialize, Deserialize)]
125#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
126#[serde(rename_all = "camelCase")]
127#[cfg_attr(feature = "ts-rs", ts(export))]
128pub struct CloseSessionRequestV1 {
129 pub session_id: String,
130}
131
132#[serde_with::serde_as]
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
141#[cfg_attr(feature = "ts-rs", ts(export))]
142#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
143pub enum DiscoveryFilter {
144 ProgramExecuted(
146 #[serde_as(as = "serde_with::DisplayFromStr")]
147 #[cfg_attr(feature = "ts-rs", ts(as = "String"))]
148 Address,
149 ),
150}
151
152pub struct TxMatchContext<'a> {
157 pub invoked_programs: &'a HashSet<Address>,
159}
160
161impl DiscoveryFilter {
162 pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
165 match self {
166 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
167 }
168 }
169}
170
171#[serde_with::serde_as]
173#[derive(Debug, Clone, Serialize, Deserialize)]
174#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
175#[serde(rename_all = "camelCase")]
176pub struct CreateSessionParams {
177 pub start_slot: u64,
179 pub end_slot: u64,
181 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
182 #[serde(default)]
183 #[cfg_attr(feature = "ts-rs", ts(as = "Vec<String>"))]
184 pub signer_filter: BTreeSet<Address>,
186 #[serde(default)]
189 pub send_summary: bool,
190 #[serde(default)]
193 #[cfg_attr(feature = "ts-rs", ts(optional))]
194 pub capacity_wait_timeout_secs: Option<u16>,
195 #[serde(default)]
199 pub disconnect_timeout_secs: Option<u16>,
200 #[serde(default)]
205 pub extra_compute_units: Option<u32>,
206 #[serde(default)]
208 pub agents: Vec<AgentParams>,
209 #[serde(default, skip_serializing_if = "Vec::is_empty")]
216 pub discoveries: Vec<DiscoveryFilter>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
222#[serde(rename_all = "camelCase")]
223#[cfg_attr(feature = "ts-rs", ts(export))]
224pub enum AgentType {
225 Arb,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
231#[serde(rename_all = "camelCase")]
232#[cfg_attr(feature = "ts-rs", ts(export))]
233pub struct ArbRouteParams {
234 pub base_mint: String,
235 pub temp_mint: String,
236 #[serde(default)]
237 pub buy_dexes: Vec<String>,
238 #[serde(default)]
239 pub sell_dexes: Vec<String>,
240 pub min_input: u64,
241 pub max_input: u64,
242 #[serde(default)]
243 pub min_profit: u64,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
249#[serde(rename_all = "camelCase")]
250#[cfg_attr(feature = "ts-rs", ts(export))]
251pub struct AgentParams {
252 pub agent_type: AgentType,
253 pub wallet: Option<String>,
254 pub keypair: Option<String>,
256 pub seed_sol_lamports: Option<u64>,
257 #[serde(default)]
258 pub seed_token_accounts: BTreeMap<String, u64>,
259 #[serde(default)]
260 pub arb_routes: Vec<ArbRouteParams>,
261}
262
263#[serde_with::serde_as]
265#[derive(Debug, Serialize, Deserialize, Default)]
266#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
267pub struct AccountModifications(
268 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
269 #[serde(default)]
270 #[cfg_attr(feature = "ts-rs", ts(as = "BTreeMap<String, AccountData>"))]
271 pub BTreeMap<Address, AccountData>,
272);
273
274#[serde_with::serde_as]
276#[derive(Debug, Serialize, Deserialize)]
277#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
278#[serde(rename_all = "camelCase")]
279pub struct ContinueParams {
280 #[serde(default = "ContinueParams::default_advance_count")]
281 pub advance_count: u64,
283 #[serde(default)]
284 pub transactions: Vec<String>,
286 #[serde(default)]
287 pub modify_account_states: AccountModifications,
289}
290
291impl Default for ContinueParams {
292 fn default() -> Self {
293 Self {
294 advance_count: Self::default_advance_count(),
295 transactions: Vec::new(),
296 modify_account_states: AccountModifications(BTreeMap::new()),
297 }
298 }
299}
300
301impl ContinueParams {
302 pub fn default_advance_count() -> u64 {
303 1
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
313#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
314#[cfg_attr(feature = "ts-rs", ts(export))]
315#[serde(rename_all = "camelCase")]
316pub struct PausedEvent {
317 pub slot: u64,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub batch_index: Option<u32>,
320}
321
322#[serde_with::serde_as]
331#[derive(Debug, Clone, Serialize, Deserialize)]
332#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
333#[cfg_attr(feature = "ts-rs", ts(export))]
334#[serde(rename_all = "camelCase")]
335pub struct DiscoveryBatchEvent {
336 pub slot: u64,
337 pub batch_index: u32,
338 pub matched: Vec<DiscoveryFilter>,
340 pub transactions: Vec<EncodedBinary>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
349#[cfg_attr(feature = "ts-rs", ts(export))]
350#[serde(rename_all = "camelCase")]
351pub struct ContinueToParams {
352 pub slot: u64,
354 #[serde(default)]
360 pub batch_index: Option<u32>,
361}
362
363#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
365#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
366#[serde(rename_all = "lowercase")]
367pub enum BinaryEncoding {
368 Base64,
369}
370
371impl BinaryEncoding {
372 pub fn encode(self, bytes: &[u8]) -> String {
373 match self {
374 Self::Base64 => BASE64.encode(bytes),
375 }
376 }
377
378 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
379 match self {
380 Self::Base64 => BASE64.decode(data),
381 }
382 }
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
388#[serde(rename_all = "camelCase")]
389pub struct EncodedBinary {
390 pub data: String,
392 pub encoding: BinaryEncoding,
394}
395
396impl EncodedBinary {
397 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
398 Self { data, encoding }
399 }
400
401 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
402 Self {
403 data: encoding.encode(bytes),
404 encoding,
405 }
406 }
407
408 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
409 self.encoding.decode(&self.data)
410 }
411}
412
413#[serde_with::serde_as]
415#[derive(Debug, Serialize, Deserialize)]
416#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
417#[serde(rename_all = "camelCase")]
418pub struct AccountData {
419 pub data: EncodedBinary,
421 pub executable: bool,
423 pub lamports: u64,
425 #[serde_as(as = "serde_with::DisplayFromStr")]
426 #[cfg_attr(feature = "ts-rs", ts(as = "String"))]
427 pub owner: Address,
429 pub space: u64,
431}
432
433#[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 BacktestResponse {
439 SessionCreated {
440 session_id: String,
441 rpc_endpoint: String,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 task_id: Option<String>,
444 },
445 SessionAttached {
446 session_id: String,
447 rpc_endpoint: String,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
449 task_id: Option<String>,
450 },
451 SessionsCreated {
452 session_ids: Vec<String>,
453 },
454 SessionsCreatedV2 {
455 control_session_id: String,
456 session_ids: Vec<String>,
457 #[serde(default)]
458 task_ids: Vec<Option<String>>,
459 },
460 ParallelSessionAttachedV2 {
461 control_session_id: String,
462 session_ids: Vec<String>,
463 #[serde(default)]
464 task_ids: Vec<Option<String>>,
465 },
466 ReadyForContinue,
467 SlotNotification(u64),
468 Paused(PausedEvent),
469 DiscoveryBatch(DiscoveryBatchEvent),
470 Error(BacktestError),
471 Success,
472 Completed {
473 #[serde(skip_serializing_if = "Option::is_none")]
477 summary: Option<SessionSummary>,
478 #[serde(default, skip_serializing_if = "Option::is_none")]
479 agent_stats: Option<Vec<AgentStatsReport>>,
480 },
481 Status {
482 status: BacktestStatus,
483 },
484 SessionEventV1 {
485 session_id: String,
486 event: SessionEventV1,
487 },
488 SessionEventV2 {
489 session_id: String,
490 seq_id: u64,
491 event: SessionEventKind,
492 },
493}
494
495impl BacktestResponse {
496 pub fn is_completed(&self) -> bool {
497 matches!(self, BacktestResponse::Completed { .. })
498 }
499
500 pub fn is_terminal(&self) -> bool {
501 match self {
502 BacktestResponse::Completed { .. } => true,
503 BacktestResponse::Error(e) => matches!(
504 e,
505 BacktestError::NoMoreBlocks
506 | BacktestError::AdvanceSlotFailed { .. }
507 | BacktestError::FinalizeSlotFailed { .. }
508 | BacktestError::Internal { .. }
509 ),
510 _ => false,
511 }
512 }
513}
514
515impl From<BacktestStatus> for BacktestResponse {
516 fn from(status: BacktestStatus) -> Self {
517 Self::Status { status }
518 }
519}
520
521impl From<String> for BacktestResponse {
522 fn from(message: String) -> Self {
523 BacktestError::Internal { error: message }.into()
524 }
525}
526
527impl From<&str> for BacktestResponse {
528 fn from(message: &str) -> Self {
529 BacktestError::Internal {
530 error: message.to_string(),
531 }
532 .into()
533 }
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize)]
537#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
538#[serde(tag = "method", content = "params", rename_all = "camelCase")]
539#[cfg_attr(feature = "ts-rs", ts(export))]
540pub enum SessionEventV1 {
541 ReadyForContinue,
542 SlotNotification(u64),
543 Paused(PausedEvent),
544 DiscoveryBatch(DiscoveryBatchEvent),
545 Error(BacktestError),
546 Success,
547 Completed {
548 #[serde(skip_serializing_if = "Option::is_none")]
549 summary: Option<SessionSummary>,
550 #[serde(default, skip_serializing_if = "Option::is_none")]
551 agent_stats: Option<Vec<AgentStatsReport>>,
552 },
553 Status {
554 status: BacktestStatus,
555 },
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
559#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
560#[serde(tag = "method", content = "params", rename_all = "camelCase")]
561#[cfg_attr(feature = "ts-rs", ts(export))]
562pub enum SessionEventKind {
563 ReadyForContinue,
564 SlotNotification(u64),
565 Paused(PausedEvent),
566 DiscoveryBatch(DiscoveryBatchEvent),
567 Error(BacktestError),
568 Success,
569 Completed {
570 #[serde(skip_serializing_if = "Option::is_none")]
571 summary: Option<SessionSummary>,
572 },
573 Status {
574 status: BacktestStatus,
575 },
576}
577
578impl SessionEventKind {
579 pub fn is_terminal(&self) -> bool {
580 match self {
581 Self::Completed { .. } => true,
582 Self::Error(e) => matches!(
583 e,
584 BacktestError::NoMoreBlocks
585 | BacktestError::AdvanceSlotFailed { .. }
586 | BacktestError::FinalizeSlotFailed { .. }
587 | BacktestError::Internal { .. }
588 ),
589 _ => false,
590 }
591 }
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
597#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
598#[serde(rename_all = "camelCase")]
599#[cfg_attr(feature = "ts-rs", ts(export))]
600pub struct SequencedResponse {
601 pub seq_id: u64,
602 #[serde(flatten)]
603 pub response: BacktestResponse,
604}
605
606#[derive(Debug, Clone, Serialize, Deserialize)]
608#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
609#[serde(rename_all = "camelCase")]
610pub enum BacktestStatus {
611 StartingRuntime,
613 DecodedTransactions,
614 AppliedAccountModifications,
615 ReadyToExecuteUserTransactions,
616 ExecutedUserTransactions,
617 ExecutingBlockTransactions,
618 ExecutedBlockTransactions,
619 ProgramAccountsLoaded,
620}
621
622impl std::fmt::Display for BacktestStatus {
623 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624 let s = match self {
625 Self::StartingRuntime => "starting runtime",
626 Self::DecodedTransactions => "decoded transactions",
627 Self::AppliedAccountModifications => "applied account modifications",
628 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
629 Self::ExecutedUserTransactions => "executed user transactions",
630 Self::ExecutingBlockTransactions => "executing block transactions",
631 Self::ExecutedBlockTransactions => "executed block transactions",
632 Self::ProgramAccountsLoaded => "program accounts loaded",
633 };
634 f.write_str(s)
635 }
636}
637
638#[derive(Debug, Clone, Default, Serialize, Deserialize)]
640#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
641#[serde(rename_all = "camelCase")]
642#[cfg_attr(feature = "ts-rs", ts(export))]
643pub struct AgentStatsReport {
644 pub name: String,
645 pub slots_processed: u64,
646 pub opportunities_found: u64,
647 pub opportunities_skipped: u64,
648 pub no_routes: u64,
649 pub txs_produced: u64,
650 pub expected_gain_by_mint: BTreeMap<String, i64>,
652 #[serde(default)]
654 pub txs_submitted: u64,
655 #[serde(default)]
657 pub txs_failed: u64,
658 #[serde(default)]
660 pub txs_simulation_rejected: u64,
661 #[serde(default)]
663 pub txs_simulation_failed: u64,
664}
665
666#[derive(Debug, Clone, Default, Serialize, Deserialize)]
668#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
669#[serde(rename_all = "camelCase")]
670pub struct SessionSummary {
671 pub correct_simulation: usize,
674 pub incorrect_simulation: usize,
677 pub execution_errors: usize,
679 pub balance_diff: usize,
681 pub log_diff: usize,
683}
684
685impl SessionSummary {
686 pub fn has_deviations(&self) -> bool {
688 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
689 }
690
691 pub fn total_transactions(&self) -> usize {
693 self.correct_simulation
694 + self.incorrect_simulation
695 + self.execution_errors
696 + self.balance_diff
697 + self.log_diff
698 }
699}
700
701impl std::fmt::Display for SessionSummary {
702 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
703 let total = self.total_transactions();
704 write!(
705 f,
706 "Session summary: {total} transactions\n\
707 \x20 - {} correct simulation\n\
708 \x20 - {} incorrect simulation\n\
709 \x20 - {} execution errors\n\
710 \x20 - {} balance diffs\n\
711 \x20 - {} log diffs",
712 self.correct_simulation,
713 self.incorrect_simulation,
714 self.execution_errors,
715 self.balance_diff,
716 self.log_diff,
717 )
718 }
719}
720
721#[derive(Debug, Clone, Serialize, Deserialize)]
723#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
724#[serde(rename_all = "camelCase")]
725pub enum BacktestError {
726 InvalidTransactionEncoding {
727 index: usize,
728 error: String,
729 },
730 InvalidTransactionFormat {
731 index: usize,
732 error: String,
733 },
734 InvalidAccountEncoding {
735 address: String,
736 encoding: BinaryEncoding,
737 error: String,
738 },
739 InvalidAccountOwner {
740 address: String,
741 error: String,
742 },
743 InvalidAccountPubkey {
744 address: String,
745 error: String,
746 },
747 NoMoreBlocks,
748 AdvanceSlotFailed {
749 slot: u64,
750 error: String,
751 },
752 FinalizeSlotFailed {
753 slot: u64,
754 error: String,
755 },
756 InvalidRequest {
757 error: String,
758 },
759 Internal {
760 error: String,
761 },
762 InvalidBlockhashFormat {
763 slot: u64,
764 error: String,
765 },
766 InitializingSysvarsFailed {
767 slot: u64,
768 error: String,
769 },
770 ClerkError {
771 error: String,
772 },
773 SimulationError {
774 error: String,
775 },
776 SessionNotFound {
777 session_id: String,
778 },
779 SessionOwnerMismatch,
780 SessionOwnershipBusy {
785 reason: String,
786 },
787}
788
789#[derive(Debug, Clone, Serialize, Deserialize)]
791pub struct AvailableRange {
792 pub bundle_start_slot: u64,
793 pub bundle_start_slot_utc: Option<String>,
794 pub max_bundle_end_slot: Option<u64>,
795 pub max_bundle_end_slot_utc: Option<String>,
796 pub max_bundle_size: Option<u64>,
797}
798
799pub fn split_range(
805 ranges: &[AvailableRange],
806 requested_start: u64,
807 requested_end: u64,
808) -> Result<Vec<(u64, u64)>, String> {
809 if requested_end < requested_start {
810 return Err(format!(
811 "invalid range: start_slot {requested_start} > end_slot {requested_end}"
812 ));
813 }
814
815 let mut candidates: Vec<(u64, u64)> = ranges
816 .iter()
817 .filter_map(|r| Some((r.bundle_start_slot, r.max_bundle_end_slot?)))
818 .filter(|(start, end)| {
819 end > start
820 && *start >= requested_start
821 && *start < requested_end
822 && *end > requested_start
823 })
824 .collect();
825
826 candidates.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
827 candidates.dedup_by_key(|(start, _)| *start);
828
829 if candidates.is_empty() || candidates.first().unwrap().0 != requested_start {
830 return Err(format!(
831 "start_slot {requested_start} is not covered by any available bundle range"
832 ));
833 }
834
835 let mut result: Vec<(u64, u64)> = Vec::new();
836 let mut current_slot = requested_start;
837 let mut i = 0;
838 let mut best_end = 0u64;
839
840 while current_slot <= requested_end {
841 while i < candidates.len() && candidates[i].0 <= current_slot {
842 best_end = best_end.max(candidates[i].1);
843 i += 1;
844 }
845 if best_end < current_slot {
846 return Err(format!("gap in coverage at slot {current_slot}"));
847 }
848 let range_end = best_end.min(requested_end);
849 result.push((current_slot, range_end));
850 current_slot = range_end + 1;
851 }
852
853 Ok(result)
854}
855
856impl From<BacktestError> for BacktestResponse {
857 fn from(error: BacktestError) -> Self {
858 Self::Error(error)
859 }
860}
861
862impl std::error::Error for BacktestError {}
863
864impl fmt::Display for BacktestError {
865 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
866 match self {
867 BacktestError::InvalidTransactionEncoding { index, error } => {
868 write!(f, "invalid transaction encoding at index {index}: {error}")
869 }
870 BacktestError::InvalidTransactionFormat { index, error } => {
871 write!(f, "invalid transaction format at index {index}: {error}")
872 }
873 BacktestError::InvalidAccountEncoding {
874 address,
875 encoding,
876 error,
877 } => write!(
878 f,
879 "invalid encoding for account {address} ({encoding:?}): {error}"
880 ),
881 BacktestError::InvalidAccountOwner { address, error } => {
882 write!(f, "invalid owner for account {address}: {error}")
883 }
884 BacktestError::InvalidAccountPubkey { address, error } => {
885 write!(f, "invalid account pubkey {address}: {error}")
886 }
887 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
888 BacktestError::AdvanceSlotFailed { slot, error } => {
889 write!(f, "failed to advance to slot {slot}: {error}")
890 }
891 BacktestError::FinalizeSlotFailed { slot, error } => {
892 write!(f, "failed to finalize slot {slot}: {error}")
893 }
894 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
895 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
896 BacktestError::InvalidBlockhashFormat { slot, error } => {
897 write!(f, "invalid blockhash at slot {slot}: {error}")
898 }
899 BacktestError::InitializingSysvarsFailed { slot, error } => {
900 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
901 }
902 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
903 BacktestError::SimulationError { error } => {
904 write!(f, "simulation error: {error}")
905 }
906 BacktestError::SessionNotFound { session_id } => {
907 write!(f, "session not found: {session_id}")
908 }
909 BacktestError::SessionOwnerMismatch => {
910 write!(f, "session owner mismatch")
911 }
912 BacktestError::SessionOwnershipBusy { reason } => {
913 write!(f, "session ownership busy: {reason}")
914 }
915 }
916 }
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922
923 fn range(start: u64, end: u64) -> AvailableRange {
924 AvailableRange {
925 bundle_start_slot: start,
926 bundle_start_slot_utc: None,
927 max_bundle_end_slot: Some(end),
928 max_bundle_end_slot_utc: None,
929 max_bundle_size: None,
930 }
931 }
932
933 #[test]
934 fn split_range_valid() {
935 let ranges = vec![range(100, 300)];
937 assert_eq!(split_range(&ranges, 100, 300).unwrap(), vec![(100, 300)]);
938
939 let ranges = vec![range(100, 200), range(201, 300), range(301, 400)];
941 assert_eq!(
942 split_range(&ranges, 100, 300).unwrap(),
943 vec![(100, 200), (201, 300)]
944 );
945
946 let ranges = vec![
950 range(100, 500),
951 range(110, 150),
952 range(150, 190),
953 range(501, 900),
954 ];
955 assert_eq!(
956 split_range(&ranges, 100, 900).unwrap(),
957 vec![(100, 500), (501, 900)]
958 );
959 }
960
961 #[test]
962 fn split_range_err() {
963 let ranges = vec![range(200, 400)];
965 assert!(split_range(&ranges, 100, 400).is_err());
966
967 let ranges = vec![range(200, 400)];
968 assert!(split_range(&ranges, 300, 400).is_err());
969
970 let ranges = vec![range(100, 200)];
972 assert!(split_range(&ranges, 100, 300).is_err());
973
974 let ranges = vec![range(100, 200), range(210, 300)];
976 assert!(split_range(&ranges, 100, 300).is_err());
977
978 let ranges = vec![range(100, 300)];
980 assert!(split_range(&ranges, 300, 100).is_err());
981 }
982}