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