1pub mod subscribe_config;
2pub mod usage;
3
4use std::{
5 collections::{BTreeMap, BTreeSet, HashSet},
6 fmt,
7};
8
9use base64::{
10 DecodeError as Base64DecodeError, Engine as _, engine::general_purpose::STANDARD as BASE64,
11};
12use serde::{Deserialize, Serialize};
13use solana_address::Address;
14
15pub mod ws_compression;
16
17#[derive(Debug, Serialize, Deserialize)]
19#[serde(tag = "method", content = "params", rename_all = "camelCase")]
20pub enum BacktestRequest {
21 CreateBacktestSession(CreateBacktestSessionRequest),
22 Continue(ContinueParams),
23 ContinueTo(ContinueToParams),
24 ContinueSessionV1(ContinueSessionRequestV1),
25 ContinueToSessionV1(ContinueToSessionRequestV1),
26 CloseBacktestSession,
27 CloseSessionV1(CloseSessionRequestV1),
28 AttachBacktestSession {
29 session_id: String,
30 last_sequence: Option<u64>,
33 },
34 ResumeAttachedSession,
37 AttachParallelControlSessionV2 {
38 control_session_id: String,
39 #[serde(default)]
43 last_sequences: BTreeMap<String, u64>,
44 },
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(untagged)]
53pub enum CreateBacktestSessionRequest {
54 V1(CreateBacktestSessionRequestV1),
55 V0(CreateSessionParams),
56}
57
58impl CreateBacktestSessionRequest {
59 pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
60 match self {
61 Self::V0(request) => CreateBacktestSessionRequestOptions {
62 request,
63 parallel: false,
64 },
65 Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
66 CreateBacktestSessionRequestOptions { request, parallel }
67 }
68 }
69 }
70
71 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
72 let options = self.into_request_options();
73 (options.request, options.parallel)
74 }
75}
76
77impl From<CreateSessionParams> for CreateBacktestSessionRequest {
78 fn from(value: CreateSessionParams) -> Self {
79 Self::V0(value)
80 }
81}
82
83impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
84 fn from(value: CreateBacktestSessionRequestV1) -> Self {
85 Self::V1(value)
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct CreateBacktestSessionRequestV1 {
92 #[serde(flatten)]
93 pub request: CreateSessionParams,
94 pub parallel: bool,
95}
96
97#[derive(Debug, Clone)]
98pub struct CreateBacktestSessionRequestOptions {
99 pub request: CreateSessionParams,
100 pub parallel: bool,
101}
102
103#[derive(Debug, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct ContinueSessionRequestV1 {
106 pub session_id: String,
107 pub request: ContinueParams,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct ContinueToSessionRequestV1 {
113 pub session_id: String,
114 pub request: ContinueToParams,
115}
116
117#[derive(Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct CloseSessionRequestV1 {
120 pub session_id: String,
121}
122
123#[serde_with::serde_as]
130#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
131#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
132pub enum DiscoveryFilter {
133 ProgramExecuted(#[serde_as(as = "serde_with::DisplayFromStr")] Address),
135}
136
137pub struct TxMatchContext<'a> {
142 pub invoked_programs: &'a HashSet<Address>,
144}
145
146impl DiscoveryFilter {
147 pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
150 match self {
151 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
152 }
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub enum ActionKind {
159 Simulate,
160 Send,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
165#[serde(tag = "at", rename_all = "camelCase")]
166pub enum ActionAnchor {
167 #[default]
170 AfterSlot,
171 BeforeMatch { filter: DiscoveryFilter },
173 AfterMatch { filter: DiscoveryFilter },
175}
176
177#[serde_with::serde_as]
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct ScheduledAction {
183 #[serde(default)]
184 pub anchor: ActionAnchor,
185 pub kind: ActionKind,
186 pub transaction: String,
188 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
191 #[serde(default)]
192 pub return_accounts: Vec<Address>,
193 #[serde(default)]
195 pub label: Option<String>,
196}
197
198#[serde_with::serde_as]
200#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
201#[serde(rename_all = "camelCase")]
202pub struct CreateSessionParams {
203 pub start_slot: u64,
205 pub end_slot: u64,
207 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
208 #[serde(default)]
209 #[builder(default)]
210 pub signer_filter: BTreeSet<Address>,
212 #[serde(default)]
215 #[builder(default)]
216 pub send_summary: bool,
217 #[serde(default)]
220 pub capacity_wait_timeout_secs: Option<u16>,
221 #[serde(default)]
225 pub disconnect_timeout_secs: Option<u16>,
226 #[serde(default)]
231 pub extra_compute_units: Option<u32>,
232 #[serde(default)]
234 #[builder(default)]
235 pub agents: Vec<AgentParams>,
236 #[serde(default, skip_serializing_if = "Vec::is_empty")]
243 #[builder(default)]
244 pub discoveries: Vec<DiscoveryFilter>,
245 #[serde(default, skip_serializing_if = "Vec::is_empty")]
248 #[builder(default)]
249 pub actions: Vec<ScheduledAction>,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
255#[serde(rename_all = "kebab-case")]
256pub enum FailFastDivergenceKind {
257 #[default]
260 AnyNonBenign,
261 Tracked,
264}
265
266impl FailFastDivergenceKind {
267 pub fn as_str(self) -> &'static str {
270 match self {
271 Self::AnyNonBenign => "any-non-benign",
272 Self::Tracked => "tracked",
273 }
274 }
275
276 pub fn from_str_opt(value: &str) -> Option<Self> {
278 match value {
279 "any-non-benign" => Some(Self::AnyNonBenign),
280 "tracked" => Some(Self::Tracked),
281 _ => None,
282 }
283 }
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub enum AgentType {
290 Arb,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct ArbRouteParams {
297 pub base_mint: String,
298 pub temp_mint: String,
299 #[serde(default)]
300 pub buy_dexes: Vec<String>,
301 #[serde(default)]
302 pub sell_dexes: Vec<String>,
303 pub min_input: u64,
304 pub max_input: u64,
305 #[serde(default)]
306 pub min_profit: u64,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct AgentParams {
313 pub agent_type: AgentType,
314 pub wallet: Option<String>,
315 pub keypair: Option<String>,
317 pub seed_sol_lamports: Option<u64>,
318 #[serde(default)]
319 pub seed_token_accounts: BTreeMap<String, u64>,
320 #[serde(default)]
321 pub arb_routes: Vec<ArbRouteParams>,
322}
323
324#[serde_with::serde_as]
326#[derive(Debug, Serialize, Deserialize, Default)]
327pub struct AccountModifications(
328 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
329 #[serde(default)]
330 pub BTreeMap<Address, AccountData>,
331);
332
333#[serde_with::serde_as]
335#[derive(Debug, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct ContinueParams {
338 #[serde(default = "ContinueParams::default_advance_count")]
339 pub advance_count: u64,
341 #[serde(default)]
342 pub transactions: Vec<String>,
344 #[serde(default)]
345 pub modify_account_states: AccountModifications,
347}
348
349impl Default for ContinueParams {
350 fn default() -> Self {
351 Self {
352 advance_count: Self::default_advance_count(),
353 transactions: Vec::new(),
354 modify_account_states: AccountModifications(BTreeMap::new()),
355 }
356 }
357}
358
359impl ContinueParams {
360 pub fn default_advance_count() -> u64 {
361 1
362 }
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
371#[serde(rename_all = "camelCase")]
372pub struct PausedEvent {
373 pub slot: u64,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub batch_index: Option<u32>,
376}
377
378#[serde_with::serde_as]
387#[derive(Debug, Clone, Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct DiscoveryBatchEvent {
390 pub slot: u64,
391 pub batch_index: u32,
392 pub matched: Vec<DiscoveryFilter>,
394 pub transactions: Vec<EncodedBinary>,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402#[serde(rename_all = "camelCase")]
403pub struct ContinueToParams {
404 pub slot: u64,
406 #[serde(default)]
412 pub batch_index: Option<u32>,
413}
414
415#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
417#[serde(rename_all = "lowercase")]
418pub enum BinaryEncoding {
419 Base64,
420}
421
422impl BinaryEncoding {
423 pub fn encode(self, bytes: &[u8]) -> String {
424 match self {
425 Self::Base64 => BASE64.encode(bytes),
426 }
427 }
428
429 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
430 match self {
431 Self::Base64 => BASE64.decode(data),
432 }
433 }
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438#[serde(rename_all = "camelCase")]
439pub struct EncodedBinary {
440 pub data: String,
442 pub encoding: BinaryEncoding,
444}
445
446impl EncodedBinary {
447 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
448 Self { data, encoding }
449 }
450
451 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
452 Self {
453 data: encoding.encode(bytes),
454 encoding,
455 }
456 }
457
458 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
459 self.encoding.decode(&self.data)
460 }
461}
462
463#[serde_with::serde_as]
465#[derive(Debug, Serialize, Deserialize)]
466#[serde(rename_all = "camelCase")]
467pub struct AccountData {
468 pub data: EncodedBinary,
470 pub executable: bool,
472 pub lamports: u64,
474 #[serde_as(as = "serde_with::DisplayFromStr")]
475 pub owner: Address,
477 pub space: u64,
479}
480
481impl AccountData {
482 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
483 Ok(solana_account::Account {
484 data: self.data.decode()?,
485 lamports: self.lamports,
486 owner: self.owner,
487 executable: self.executable,
488 rent_epoch: 0,
489 })
490 }
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize)]
495#[serde(tag = "method", content = "params", rename_all = "camelCase")]
496pub enum BacktestResponse {
497 SessionCreated {
498 session_id: String,
499 rpc_endpoint: String,
500 #[serde(default, skip_serializing_if = "Option::is_none")]
501 task_id: Option<String>,
502 },
503 SessionAttached {
504 session_id: String,
505 rpc_endpoint: String,
506 #[serde(default, skip_serializing_if = "Option::is_none")]
507 task_id: Option<String>,
508 },
509 SessionsCreated {
510 session_ids: Vec<String>,
511 },
512 SessionsCreatedV2 {
513 control_session_id: String,
514 session_ids: Vec<String>,
515 #[serde(default)]
516 task_ids: Vec<Option<String>>,
517 #[serde(default)]
522 start_slots: Vec<u64>,
523 #[serde(default)]
524 end_slots: Vec<u64>,
525 },
526 ParallelSessionAttachedV2 {
527 control_session_id: String,
528 session_ids: Vec<String>,
529 #[serde(default)]
530 task_ids: Vec<Option<String>>,
531 },
532 ReadyForContinue,
533 SlotNotification(u64),
534 Paused(PausedEvent),
535 DiscoveryBatch(DiscoveryBatchEvent),
536 Error(BacktestError),
537 Success,
538 Completed {
539 #[serde(skip_serializing_if = "Option::is_none")]
543 summary: Option<SessionSummary>,
544 #[serde(default, skip_serializing_if = "Option::is_none")]
545 agent_stats: Option<Vec<AgentStatsReport>>,
546 },
547 Status {
548 status: BacktestStatus,
549 },
550 SessionEventV1 {
551 session_id: String,
552 event: SessionEventV1,
553 },
554 SessionEventV2 {
555 session_id: String,
556 seq_id: u64,
557 event: SessionEventKind,
558 },
559}
560
561impl BacktestResponse {
562 pub fn is_completed(&self) -> bool {
563 matches!(self, BacktestResponse::Completed { .. })
564 }
565
566 pub fn is_terminal(&self) -> bool {
567 match self {
568 BacktestResponse::Completed { .. } => true,
569 BacktestResponse::Error(e) => matches!(
570 e,
571 BacktestError::NoMoreBlocks
572 | BacktestError::AdvanceSlotFailed { .. }
573 | BacktestError::FinalizeSlotFailed { .. }
574 | BacktestError::Internal { .. }
575 ),
576 _ => false,
577 }
578 }
579}
580
581impl From<BacktestStatus> for BacktestResponse {
582 fn from(status: BacktestStatus) -> Self {
583 Self::Status { status }
584 }
585}
586
587impl From<String> for BacktestResponse {
588 fn from(message: String) -> Self {
589 BacktestError::Internal { error: message }.into()
590 }
591}
592
593impl From<&str> for BacktestResponse {
594 fn from(message: &str) -> Self {
595 BacktestError::Internal {
596 error: message.to_string(),
597 }
598 .into()
599 }
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
603#[serde(tag = "method", content = "params", rename_all = "camelCase")]
604pub enum SessionEventV1 {
605 ReadyForContinue,
606 SlotNotification(u64),
607 Paused(PausedEvent),
608 DiscoveryBatch(DiscoveryBatchEvent),
609 Error(BacktestError),
610 Success,
611 Completed {
612 #[serde(skip_serializing_if = "Option::is_none")]
613 summary: Option<SessionSummary>,
614 #[serde(default, skip_serializing_if = "Option::is_none")]
615 agent_stats: Option<Vec<AgentStatsReport>>,
616 },
617 Status {
618 status: BacktestStatus,
619 },
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize)]
623#[serde(tag = "method", content = "params", rename_all = "camelCase")]
624pub enum SessionEventKind {
625 ReadyForContinue,
626 SlotNotification(u64),
627 Paused(PausedEvent),
628 DiscoveryBatch(DiscoveryBatchEvent),
629 Error(BacktestError),
630 Success,
631 Completed {
632 #[serde(skip_serializing_if = "Option::is_none")]
633 summary: Option<SessionSummary>,
634 },
635 Status {
636 status: BacktestStatus,
637 },
638}
639
640impl SessionEventKind {
641 pub fn is_terminal(&self) -> bool {
642 match self {
643 Self::Completed { .. } => true,
644 Self::Error(e) => matches!(
645 e,
646 BacktestError::NoMoreBlocks
647 | BacktestError::AdvanceSlotFailed { .. }
648 | BacktestError::FinalizeSlotFailed { .. }
649 | BacktestError::Internal { .. }
650 ),
651 _ => false,
652 }
653 }
654}
655
656#[derive(Debug, Clone, Serialize, Deserialize)]
659#[serde(rename_all = "camelCase")]
660pub struct SequencedResponse {
661 pub seq_id: u64,
662 #[serde(flatten)]
663 pub response: BacktestResponse,
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize)]
668#[serde(rename_all = "camelCase")]
669pub enum BacktestStatus {
670 StartingRuntime,
672 DecodedTransactions,
673 AppliedAccountModifications,
674 ReadyToExecuteUserTransactions,
675 ExecutedUserTransactions,
676 ExecutingBlockTransactions,
677 ExecutedBlockTransactions,
678 ProgramAccountsLoaded,
679}
680
681impl std::fmt::Display for BacktestStatus {
682 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
683 let s = match self {
684 Self::StartingRuntime => "starting runtime",
685 Self::DecodedTransactions => "decoded transactions",
686 Self::AppliedAccountModifications => "applied account modifications",
687 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
688 Self::ExecutedUserTransactions => "executed user transactions",
689 Self::ExecutingBlockTransactions => "executing block transactions",
690 Self::ExecutedBlockTransactions => "executed block transactions",
691 Self::ProgramAccountsLoaded => "program accounts loaded",
692 };
693 f.write_str(s)
694 }
695}
696
697#[derive(Debug, Clone, Default, Serialize, Deserialize)]
699#[serde(rename_all = "camelCase")]
700pub struct AgentStatsReport {
701 pub name: String,
702 pub slots_processed: u64,
703 pub opportunities_found: u64,
704 pub opportunities_skipped: u64,
705 pub no_routes: u64,
706 pub txs_produced: u64,
707 pub expected_gain_by_mint: BTreeMap<String, i64>,
709 #[serde(default)]
711 pub txs_submitted: u64,
712 #[serde(default)]
714 pub txs_failed: u64,
715 #[serde(default)]
717 pub txs_simulation_rejected: u64,
718 #[serde(default)]
720 pub txs_simulation_failed: u64,
721}
722
723#[derive(Debug, Clone, Default, Serialize, Deserialize)]
725#[serde(rename_all = "camelCase")]
726pub struct SessionSummary {
727 pub correct_simulation: usize,
730 pub incorrect_simulation: usize,
733 pub execution_errors: usize,
735 pub balance_diff: usize,
737 pub log_diff: usize,
739}
740
741impl SessionSummary {
742 pub fn has_deviations(&self) -> bool {
744 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
745 }
746
747 pub fn total_transactions(&self) -> usize {
749 self.correct_simulation
750 + self.incorrect_simulation
751 + self.execution_errors
752 + self.balance_diff
753 + self.log_diff
754 }
755}
756
757impl std::fmt::Display for SessionSummary {
758 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
759 let total = self.total_transactions();
760 write!(
761 f,
762 "Session summary: {total} transactions\n\
763 \x20 - {} correct simulation\n\
764 \x20 - {} incorrect simulation\n\
765 \x20 - {} execution errors\n\
766 \x20 - {} balance diffs\n\
767 \x20 - {} log diffs",
768 self.correct_simulation,
769 self.incorrect_simulation,
770 self.execution_errors,
771 self.balance_diff,
772 self.log_diff,
773 )
774 }
775}
776
777#[derive(Debug, Clone, Serialize, Deserialize)]
779#[serde(rename_all = "camelCase")]
780pub enum BacktestError {
781 InvalidTransactionEncoding {
782 index: usize,
783 error: String,
784 },
785 InvalidTransactionFormat {
786 index: usize,
787 error: String,
788 },
789 InvalidAccountEncoding {
790 address: String,
791 encoding: BinaryEncoding,
792 error: String,
793 },
794 InvalidAccountOwner {
795 address: String,
796 error: String,
797 },
798 InvalidAccountPubkey {
799 address: String,
800 error: String,
801 },
802 NoMoreBlocks,
803 AdvanceSlotFailed {
804 slot: u64,
805 error: String,
806 },
807 FinalizeSlotFailed {
808 slot: u64,
809 error: String,
810 },
811 InvalidRequest {
812 error: String,
813 },
814 Internal {
815 error: String,
816 },
817 InvalidBlockhashFormat {
818 slot: u64,
819 error: String,
820 },
821 InitializingSysvarsFailed {
822 slot: u64,
823 error: String,
824 },
825 ClerkError {
826 error: String,
827 },
828 SimulationError {
829 error: String,
830 },
831 SessionNotFound {
832 session_id: String,
833 },
834 SessionOwnerMismatch,
835 SessionOwnershipBusy {
840 reason: String,
841 },
842}
843
844#[derive(Debug, Clone, Serialize, Deserialize)]
846pub struct AvailableRange {
847 pub bundle_start_slot: u64,
848 pub bundle_start_slot_utc: Option<String>,
849 pub max_bundle_end_slot: Option<u64>,
850 pub max_bundle_end_slot_utc: Option<String>,
851 pub max_bundle_size: Option<u64>,
852}
853
854pub fn split_range(
872 ranges: &[AvailableRange],
873 requested_start: u64,
874 requested_end: u64,
875) -> Result<Vec<(u64, u64)>, String> {
876 if requested_end < requested_start {
877 return Err(format!(
878 "invalid range: start_slot {requested_start} > end_slot {requested_end}"
879 ));
880 }
881
882 let mut ends_by_start: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
887 for r in ranges {
888 if let Some(end) = r.max_bundle_end_slot
889 && end > r.bundle_start_slot
890 {
891 ends_by_start
892 .entry(r.bundle_start_slot)
893 .or_default()
894 .insert(end);
895 }
896 }
897
898 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
905 ends.iter()
906 .next_back()
907 .is_some_and(|&end| end >= requested_start)
908 }) else {
909 return Err(format!(
910 "start_slot {requested_start} is not covered by any available bundle range"
911 ));
912 };
913
914 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
920 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
921 let mut best: Option<Vec<(u64, u64)>> = None;
922 for &end in ends {
923 let candidate = if end >= requested_end {
924 Some(vec![(start, requested_end)])
925 } else {
926 best_from.get(&(end + 1)).map(|rest| {
927 std::iter::once((start, end))
928 .chain(rest.iter().copied())
929 .collect()
930 })
931 };
932 if let Some(candidate) = candidate
933 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
934 {
935 best = Some(candidate);
936 }
937 }
938 if let Some(best) = best {
939 best_from.insert(start, best);
940 }
941 }
942
943 best_from.remove(&anchor_start).ok_or_else(|| {
944 let mut covered_to = anchor_start.saturating_sub(1);
948 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
949 if start > covered_to.saturating_add(1) {
950 break;
951 }
952 if let Some(&end) = ends.iter().next_back() {
953 covered_to = covered_to.max(end);
954 }
955 }
956 if covered_to < requested_end {
957 format!("gap in coverage at slot {}", covered_to + 1)
958 } else {
959 format!(
960 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available bundle ranges"
961 )
962 }
963 })
964}
965
966impl From<BacktestError> for BacktestResponse {
967 fn from(error: BacktestError) -> Self {
968 Self::Error(error)
969 }
970}
971
972impl std::error::Error for BacktestError {}
973
974impl fmt::Display for BacktestError {
975 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
976 match self {
977 BacktestError::InvalidTransactionEncoding { index, error } => {
978 write!(f, "invalid transaction encoding at index {index}: {error}")
979 }
980 BacktestError::InvalidTransactionFormat { index, error } => {
981 write!(f, "invalid transaction format at index {index}: {error}")
982 }
983 BacktestError::InvalidAccountEncoding {
984 address,
985 encoding,
986 error,
987 } => write!(
988 f,
989 "invalid encoding for account {address} ({encoding:?}): {error}"
990 ),
991 BacktestError::InvalidAccountOwner { address, error } => {
992 write!(f, "invalid owner for account {address}: {error}")
993 }
994 BacktestError::InvalidAccountPubkey { address, error } => {
995 write!(f, "invalid account pubkey {address}: {error}")
996 }
997 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
998 BacktestError::AdvanceSlotFailed { slot, error } => {
999 write!(f, "failed to advance to slot {slot}: {error}")
1000 }
1001 BacktestError::FinalizeSlotFailed { slot, error } => {
1002 write!(f, "failed to finalize slot {slot}: {error}")
1003 }
1004 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
1005 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
1006 BacktestError::InvalidBlockhashFormat { slot, error } => {
1007 write!(f, "invalid blockhash at slot {slot}: {error}")
1008 }
1009 BacktestError::InitializingSysvarsFailed { slot, error } => {
1010 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
1011 }
1012 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
1013 BacktestError::SimulationError { error } => {
1014 write!(f, "simulation error: {error}")
1015 }
1016 BacktestError::SessionNotFound { session_id } => {
1017 write!(f, "session not found: {session_id}")
1018 }
1019 BacktestError::SessionOwnerMismatch => {
1020 write!(f, "session owner mismatch")
1021 }
1022 BacktestError::SessionOwnershipBusy { reason } => {
1023 write!(f, "session ownership busy: {reason}")
1024 }
1025 }
1026 }
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031 use super::*;
1032
1033 #[test]
1034 fn fail_fast_divergence_kind_str_round_trips() {
1035 for kind in [
1036 FailFastDivergenceKind::AnyNonBenign,
1037 FailFastDivergenceKind::Tracked,
1038 ] {
1039 assert_eq!(
1040 FailFastDivergenceKind::from_str_opt(kind.as_str()),
1041 Some(kind)
1042 );
1043 }
1044 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
1045 assert_eq!(
1046 FailFastDivergenceKind::default(),
1047 FailFastDivergenceKind::AnyNonBenign
1048 );
1049 }
1050
1051 fn range(start: u64, end: u64) -> AvailableRange {
1052 AvailableRange {
1053 bundle_start_slot: start,
1054 bundle_start_slot_utc: None,
1055 max_bundle_end_slot: Some(end),
1056 max_bundle_end_slot_utc: None,
1057 max_bundle_size: None,
1058 }
1059 }
1060
1061 #[rstest::rstest]
1065 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
1066 #[case::multi(
1067 vec![range(100, 200), range(201, 300), range(301, 400)],
1068 100, 300, Some(vec![(100, 200), (201, 300)])
1069 )]
1070 #[case::nested(
1073 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
1074 100, 900, Some(vec![(100, 500), (501, 900)])
1075 )]
1076 #[case::prefers_finer_grid(
1080 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
1081 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
1082 )]
1083 #[case::shared_start_prefers_finer(
1087 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
1088 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
1089 )]
1090 #[case::falls_back_to_coarse(
1094 vec![range(100, 160), range(100, 120), range(121, 140)],
1095 100, 160, Some(vec![(100, 160)])
1096 )]
1097 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
1099 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
1102 #[case::anchors_then_continues(
1103 vec![range(150, 350), range(351, 600)],
1104 200, 600, Some(vec![(150, 350), (351, 600)])
1105 )]
1106 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
1107 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
1109 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
1111 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
1112 #[case::inverted_range(vec![range(100, 300)], 300, 100, None)]
1114 fn split_range_cases(
1115 #[case] ranges: Vec<AvailableRange>,
1116 #[case] start: u64,
1117 #[case] end: u64,
1118 #[case] expected: Option<Vec<(u64, u64)>>,
1119 ) {
1120 match expected {
1121 Some(expected) => assert_eq!(split_range(&ranges, start, end).unwrap(), expected),
1122 None => assert!(split_range(&ranges, start, end).is_err()),
1123 }
1124 }
1125
1126 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
1129 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
1130 for r in ranges {
1131 if let Some(end) = r.max_bundle_end_slot
1132 && end > r.bundle_start_slot
1133 {
1134 ends.entry(r.bundle_start_slot).or_default().insert(end);
1135 }
1136 }
1137 ends
1138 }
1139
1140 fn reference_max_split(
1144 ends: &BTreeMap<u64, BTreeSet<u64>>,
1145 cursor: u64,
1146 end: u64,
1147 ) -> Option<Vec<(u64, u64)>> {
1148 ends.get(&cursor)?
1149 .iter()
1150 .filter_map(|&bundle_end| {
1151 if bundle_end >= end {
1152 Some(vec![(cursor, end)])
1153 } else {
1154 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
1155 rest.insert(0, (cursor, bundle_end));
1156 rest
1157 })
1158 }
1159 })
1160 .max_by_key(Vec::len)
1161 }
1162
1163 fn is_valid_split(
1167 split: &[(u64, u64)],
1168 ends: &BTreeMap<u64, BTreeSet<u64>>,
1169 start: u64,
1170 end: u64,
1171 ) -> bool {
1172 split.first().is_some_and(|&(s, _)| s == start)
1173 && split.last().is_some_and(|&(_, e)| e == end)
1174 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
1175 && split.iter().all(|&(s, e)| {
1176 e >= s
1177 && ends
1178 .get(&s)
1179 .and_then(|bundle_ends| bundle_ends.iter().next_back())
1180 .is_some_and(|&max_end| e <= max_end)
1181 })
1182 }
1183
1184 #[test]
1189 fn split_range_matches_reference() {
1190 let mut seed: u64 = 0x9E3779B97F4A7C15;
1191 let mut next = || {
1192 seed = seed
1193 .wrapping_mul(6364136223846793005)
1194 .wrapping_add(1442695040888963407);
1195 seed >> 33
1196 };
1197
1198 for _ in 0..50_000 {
1199 let ranges: Vec<AvailableRange> = (0..next() % 6)
1202 .map(|_| {
1203 let start = next() % 12;
1204 range(start, start + next() % 6) })
1206 .collect();
1207 let start = next() % 12;
1208 let end = start + next() % 6; let got = split_range(&ranges, start, end);
1211 let ends = ends_by_start(&ranges);
1212 let anchor = ends
1215 .range(..=start)
1216 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
1217 .map(|(&s, _)| s);
1218 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
1219
1220 let layout: Vec<_> = ranges
1221 .iter()
1222 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
1223 .collect();
1224 match (&got, &reference) {
1225 (Ok(split), Some(best)) => {
1226 assert!(
1227 is_valid_split(split, &ends, anchor.unwrap(), end),
1228 "invalid split {split:?} for {layout:?} [{start},{end}]"
1229 );
1230 assert_eq!(
1231 split.len(),
1232 best.len(),
1233 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
1234 );
1235 }
1236 (Err(_), None) => {}
1237 _ => panic!(
1238 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
1239 ),
1240 }
1241 }
1242 }
1243}