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 },
441 SessionAttached {
442 session_id: String,
443 rpc_endpoint: String,
444 },
445 SessionsCreated {
446 session_ids: Vec<String>,
447 },
448 SessionsCreatedV2 {
449 control_session_id: String,
450 session_ids: Vec<String>,
451 },
452 ParallelSessionAttachedV2 {
453 control_session_id: String,
454 session_ids: Vec<String>,
455 },
456 ReadyForContinue,
457 SlotNotification(u64),
458 Paused(PausedEvent),
459 DiscoveryBatch(DiscoveryBatchEvent),
460 Error(BacktestError),
461 Success,
462 Completed {
463 #[serde(skip_serializing_if = "Option::is_none")]
467 summary: Option<SessionSummary>,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
469 agent_stats: Option<Vec<AgentStatsReport>>,
470 },
471 Status {
472 status: BacktestStatus,
473 },
474 SessionEventV1 {
475 session_id: String,
476 event: SessionEventV1,
477 },
478 SessionEventV2 {
479 session_id: String,
480 seq_id: u64,
481 event: SessionEventKind,
482 },
483}
484
485impl BacktestResponse {
486 pub fn is_completed(&self) -> bool {
487 matches!(self, BacktestResponse::Completed { .. })
488 }
489
490 pub fn is_terminal(&self) -> bool {
491 match self {
492 BacktestResponse::Completed { .. } => true,
493 BacktestResponse::Error(e) => matches!(
494 e,
495 BacktestError::NoMoreBlocks
496 | BacktestError::AdvanceSlotFailed { .. }
497 | BacktestError::Internal { .. }
498 ),
499 _ => false,
500 }
501 }
502}
503
504impl From<BacktestStatus> for BacktestResponse {
505 fn from(status: BacktestStatus) -> Self {
506 Self::Status { status }
507 }
508}
509
510impl From<String> for BacktestResponse {
511 fn from(message: String) -> Self {
512 BacktestError::Internal { error: message }.into()
513 }
514}
515
516impl From<&str> for BacktestResponse {
517 fn from(message: &str) -> Self {
518 BacktestError::Internal {
519 error: message.to_string(),
520 }
521 .into()
522 }
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
526#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
527#[serde(tag = "method", content = "params", rename_all = "camelCase")]
528#[cfg_attr(feature = "ts-rs", ts(export))]
529pub enum SessionEventV1 {
530 ReadyForContinue,
531 SlotNotification(u64),
532 Paused(PausedEvent),
533 DiscoveryBatch(DiscoveryBatchEvent),
534 Error(BacktestError),
535 Success,
536 Completed {
537 #[serde(skip_serializing_if = "Option::is_none")]
538 summary: Option<SessionSummary>,
539 #[serde(default, skip_serializing_if = "Option::is_none")]
540 agent_stats: Option<Vec<AgentStatsReport>>,
541 },
542 Status {
543 status: BacktestStatus,
544 },
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
548#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
549#[serde(tag = "method", content = "params", rename_all = "camelCase")]
550#[cfg_attr(feature = "ts-rs", ts(export))]
551pub enum SessionEventKind {
552 ReadyForContinue,
553 SlotNotification(u64),
554 Paused(PausedEvent),
555 DiscoveryBatch(DiscoveryBatchEvent),
556 Error(BacktestError),
557 Success,
558 Completed {
559 #[serde(skip_serializing_if = "Option::is_none")]
560 summary: Option<SessionSummary>,
561 },
562 Status {
563 status: BacktestStatus,
564 },
565}
566
567impl SessionEventKind {
568 pub fn is_terminal(&self) -> bool {
569 match self {
570 Self::Completed { .. } => true,
571 Self::Error(e) => matches!(
572 e,
573 BacktestError::NoMoreBlocks
574 | BacktestError::AdvanceSlotFailed { .. }
575 | BacktestError::Internal { .. }
576 ),
577 _ => false,
578 }
579 }
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize)]
585#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
586#[serde(rename_all = "camelCase")]
587#[cfg_attr(feature = "ts-rs", ts(export))]
588pub struct SequencedResponse {
589 pub seq_id: u64,
590 #[serde(flatten)]
591 pub response: BacktestResponse,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
596#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
597#[serde(rename_all = "camelCase")]
598pub enum BacktestStatus {
599 StartingRuntime,
601 DecodedTransactions,
602 AppliedAccountModifications,
603 ReadyToExecuteUserTransactions,
604 ExecutedUserTransactions,
605 ExecutingBlockTransactions,
606 ExecutedBlockTransactions,
607 ProgramAccountsLoaded,
608}
609
610impl std::fmt::Display for BacktestStatus {
611 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
612 let s = match self {
613 Self::StartingRuntime => "starting runtime",
614 Self::DecodedTransactions => "decoded transactions",
615 Self::AppliedAccountModifications => "applied account modifications",
616 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
617 Self::ExecutedUserTransactions => "executed user transactions",
618 Self::ExecutingBlockTransactions => "executing block transactions",
619 Self::ExecutedBlockTransactions => "executed block transactions",
620 Self::ProgramAccountsLoaded => "program accounts loaded",
621 };
622 f.write_str(s)
623 }
624}
625
626#[derive(Debug, Clone, Default, Serialize, Deserialize)]
628#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
629#[serde(rename_all = "camelCase")]
630#[cfg_attr(feature = "ts-rs", ts(export))]
631pub struct AgentStatsReport {
632 pub name: String,
633 pub slots_processed: u64,
634 pub opportunities_found: u64,
635 pub opportunities_skipped: u64,
636 pub no_routes: u64,
637 pub txs_produced: u64,
638 pub expected_gain_by_mint: BTreeMap<String, i64>,
640 #[serde(default)]
642 pub txs_submitted: u64,
643 #[serde(default)]
645 pub txs_failed: u64,
646 #[serde(default)]
648 pub txs_simulation_rejected: u64,
649 #[serde(default)]
651 pub txs_simulation_failed: u64,
652}
653
654#[derive(Debug, Clone, Default, Serialize, Deserialize)]
656#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
657#[serde(rename_all = "camelCase")]
658pub struct SessionSummary {
659 pub correct_simulation: usize,
662 pub incorrect_simulation: usize,
665 pub execution_errors: usize,
667 pub balance_diff: usize,
669 pub log_diff: usize,
671}
672
673impl SessionSummary {
674 pub fn has_deviations(&self) -> bool {
676 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
677 }
678
679 pub fn total_transactions(&self) -> usize {
681 self.correct_simulation
682 + self.incorrect_simulation
683 + self.execution_errors
684 + self.balance_diff
685 + self.log_diff
686 }
687}
688
689impl std::fmt::Display for SessionSummary {
690 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
691 let total = self.total_transactions();
692 write!(
693 f,
694 "Session summary: {total} transactions\n\
695 \x20 - {} correct simulation\n\
696 \x20 - {} incorrect simulation\n\
697 \x20 - {} execution errors\n\
698 \x20 - {} balance diffs\n\
699 \x20 - {} log diffs",
700 self.correct_simulation,
701 self.incorrect_simulation,
702 self.execution_errors,
703 self.balance_diff,
704 self.log_diff,
705 )
706 }
707}
708
709#[derive(Debug, Clone, Serialize, Deserialize)]
711#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
712#[serde(rename_all = "camelCase")]
713pub enum BacktestError {
714 InvalidTransactionEncoding {
715 index: usize,
716 error: String,
717 },
718 InvalidTransactionFormat {
719 index: usize,
720 error: String,
721 },
722 InvalidAccountEncoding {
723 address: String,
724 encoding: BinaryEncoding,
725 error: String,
726 },
727 InvalidAccountOwner {
728 address: String,
729 error: String,
730 },
731 InvalidAccountPubkey {
732 address: String,
733 error: String,
734 },
735 NoMoreBlocks,
736 AdvanceSlotFailed {
737 slot: u64,
738 error: String,
739 },
740 InvalidRequest {
741 error: String,
742 },
743 Internal {
744 error: String,
745 },
746 InvalidBlockhashFormat {
747 slot: u64,
748 error: String,
749 },
750 InitializingSysvarsFailed {
751 slot: u64,
752 error: String,
753 },
754 ClerkError {
755 error: String,
756 },
757 SimulationError {
758 error: String,
759 },
760 SessionNotFound {
761 session_id: String,
762 },
763 SessionOwnerMismatch,
764}
765
766#[derive(Debug, Clone, Serialize, Deserialize)]
768pub struct AvailableRange {
769 pub bundle_start_slot: u64,
770 pub bundle_start_slot_utc: Option<String>,
771 pub max_bundle_end_slot: Option<u64>,
772 pub max_bundle_end_slot_utc: Option<String>,
773 pub max_bundle_size: Option<u64>,
774}
775
776pub fn split_range(
782 ranges: &[AvailableRange],
783 requested_start: u64,
784 requested_end: u64,
785) -> Result<Vec<(u64, u64)>, String> {
786 if requested_end < requested_start {
787 return Err(format!(
788 "invalid range: start_slot {requested_start} > end_slot {requested_end}"
789 ));
790 }
791
792 let mut candidates: Vec<(u64, u64)> = ranges
793 .iter()
794 .filter_map(|r| Some((r.bundle_start_slot, r.max_bundle_end_slot?)))
795 .filter(|(start, end)| {
796 end > start
797 && *start >= requested_start
798 && *start < requested_end
799 && *end > requested_start
800 })
801 .collect();
802
803 candidates.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
804 candidates.dedup_by_key(|(start, _)| *start);
805
806 if candidates.is_empty() || candidates.first().unwrap().0 != requested_start {
807 return Err(format!(
808 "start_slot {requested_start} is not covered by any available bundle range"
809 ));
810 }
811
812 if candidates.last().unwrap().1 < requested_end {
813 return Err(format!(
814 "end_slot {requested_end} is not covered by any available bundle range"
815 ));
816 }
817
818 let mut bundles: Vec<(u64, u64)> = Vec::with_capacity(candidates.len());
819 for (i, (start, mut end)) in candidates.iter().copied().enumerate() {
820 if let Some((next_start, _)) = candidates.get(i + 1).copied() {
821 if next_start <= end + 1 {
822 end = next_start.saturating_sub(1);
823 } else {
824 return Err(format!("gap between bundle ranges: {end} and {next_start}"));
825 }
826 }
827 bundles.push((start, end));
828 }
829
830 let result: Vec<(u64, u64)> = bundles
831 .into_iter()
832 .map(|(start, range_end)| (start.max(requested_start), range_end.min(requested_end)))
833 .collect();
834
835 Ok(result)
836}
837
838impl From<BacktestError> for BacktestResponse {
839 fn from(error: BacktestError) -> Self {
840 Self::Error(error)
841 }
842}
843
844impl std::error::Error for BacktestError {}
845
846impl fmt::Display for BacktestError {
847 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
848 match self {
849 BacktestError::InvalidTransactionEncoding { index, error } => {
850 write!(f, "invalid transaction encoding at index {index}: {error}")
851 }
852 BacktestError::InvalidTransactionFormat { index, error } => {
853 write!(f, "invalid transaction format at index {index}: {error}")
854 }
855 BacktestError::InvalidAccountEncoding {
856 address,
857 encoding,
858 error,
859 } => write!(
860 f,
861 "invalid encoding for account {address} ({encoding:?}): {error}"
862 ),
863 BacktestError::InvalidAccountOwner { address, error } => {
864 write!(f, "invalid owner for account {address}: {error}")
865 }
866 BacktestError::InvalidAccountPubkey { address, error } => {
867 write!(f, "invalid account pubkey {address}: {error}")
868 }
869 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
870 BacktestError::AdvanceSlotFailed { slot, error } => {
871 write!(f, "failed to advance to slot {slot}: {error}")
872 }
873 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
874 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
875 BacktestError::InvalidBlockhashFormat { slot, error } => {
876 write!(f, "invalid blockhash at slot {slot}: {error}")
877 }
878 BacktestError::InitializingSysvarsFailed { slot, error } => {
879 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
880 }
881 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
882 BacktestError::SimulationError { error } => {
883 write!(f, "simulation error: {error}")
884 }
885 BacktestError::SessionNotFound { session_id } => {
886 write!(f, "session not found: {session_id}")
887 }
888 BacktestError::SessionOwnerMismatch => {
889 write!(f, "session owner mismatch")
890 }
891 }
892 }
893}
894
895#[cfg(test)]
896mod tests {
897 use super::*;
898
899 fn range(start: u64, end: u64) -> AvailableRange {
900 AvailableRange {
901 bundle_start_slot: start,
902 bundle_start_slot_utc: None,
903 max_bundle_end_slot: Some(end),
904 max_bundle_end_slot_utc: None,
905 max_bundle_size: None,
906 }
907 }
908
909 #[test]
910 fn split_range_valid() {
911 let ranges = vec![range(100, 300)];
913 assert_eq!(split_range(&ranges, 100, 300).unwrap(), vec![(100, 300)]);
914
915 let ranges = vec![range(100, 200), range(201, 300), range(301, 400)];
917 assert_eq!(
918 split_range(&ranges, 100, 300).unwrap(),
919 vec![(100, 200), (201, 300)]
920 );
921 }
922
923 #[test]
924 fn split_range_err() {
925 let ranges = vec![range(200, 400)];
927 assert!(split_range(&ranges, 100, 400).is_err());
928
929 let ranges = vec![range(200, 400)];
930 assert!(split_range(&ranges, 300, 400).is_err());
931
932 let ranges = vec![range(100, 200)];
934 assert!(split_range(&ranges, 100, 300).is_err());
935
936 let ranges = vec![range(100, 200), range(210, 300)];
938 assert!(split_range(&ranges, 100, 300).is_err());
939
940 let ranges = vec![range(100, 300)];
942 assert!(split_range(&ranges, 300, 100).is_err());
943 }
944}