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, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct ScheduledAction {
183 #[serde(default)]
184 pub anchor: ActionAnchor,
185 pub kind: ActionKind,
186 pub transactions: Vec<String>,
190 #[serde(default)]
194 pub account_overrides: AccountModifications,
195 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
199 #[serde(default)]
200 pub return_accounts: Vec<Address>,
201 #[serde(default)]
203 pub label: Option<String>,
204}
205
206#[serde_with::serde_as]
208#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
209#[serde(rename_all = "camelCase")]
210pub struct CreateSessionParams {
211 pub start_slot: u64,
213 pub end_slot: u64,
215 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
216 #[serde(default)]
217 #[builder(default)]
218 pub signer_filter: BTreeSet<Address>,
220 #[serde(default)]
223 #[builder(default)]
224 pub send_summary: bool,
225 #[serde(default)]
228 pub capacity_wait_timeout_secs: Option<u16>,
229 #[serde(default)]
233 pub disconnect_timeout_secs: Option<u16>,
234 #[serde(default)]
239 pub extra_compute_units: Option<u32>,
240 #[serde(default)]
242 #[builder(default)]
243 pub agents: Vec<AgentParams>,
244 #[serde(default, skip_serializing_if = "Vec::is_empty")]
251 #[builder(default)]
252 pub discoveries: Vec<DiscoveryFilter>,
253 #[serde(default, skip_serializing_if = "Vec::is_empty")]
256 #[builder(default)]
257 pub actions: Vec<ScheduledAction>,
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
263#[serde(rename_all = "kebab-case")]
264pub enum FailFastDivergenceKind {
265 #[default]
268 AnyNonBenign,
269 Tracked,
272}
273
274impl FailFastDivergenceKind {
275 pub fn as_str(self) -> &'static str {
278 match self {
279 Self::AnyNonBenign => "any-non-benign",
280 Self::Tracked => "tracked",
281 }
282 }
283
284 pub fn from_str_opt(value: &str) -> Option<Self> {
286 match value {
287 "any-non-benign" => Some(Self::AnyNonBenign),
288 "tracked" => Some(Self::Tracked),
289 _ => None,
290 }
291 }
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub enum AgentType {
298 Arb,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub struct ArbRouteParams {
305 pub base_mint: String,
306 pub temp_mint: String,
307 #[serde(default)]
308 pub buy_dexes: Vec<String>,
309 #[serde(default)]
310 pub sell_dexes: Vec<String>,
311 pub min_input: u64,
312 pub max_input: u64,
313 #[serde(default)]
314 pub min_profit: u64,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct AgentParams {
321 pub agent_type: AgentType,
322 pub wallet: Option<String>,
323 pub keypair: Option<String>,
325 pub seed_sol_lamports: Option<u64>,
326 #[serde(default)]
327 pub seed_token_accounts: BTreeMap<String, u64>,
328 #[serde(default)]
329 pub arb_routes: Vec<ArbRouteParams>,
330}
331
332#[serde_with::serde_as]
334#[derive(Debug, Clone, Serialize, Deserialize, Default)]
335pub struct AccountModifications(
336 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
337 #[serde(default)]
338 pub BTreeMap<Address, AccountData>,
339);
340
341#[serde_with::serde_as]
343#[derive(Debug, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct ContinueParams {
346 #[serde(default = "ContinueParams::default_advance_count")]
347 pub advance_count: u64,
349 #[serde(default)]
350 pub transactions: Vec<String>,
352 #[serde(default)]
353 pub modify_account_states: AccountModifications,
355}
356
357impl Default for ContinueParams {
358 fn default() -> Self {
359 Self {
360 advance_count: Self::default_advance_count(),
361 transactions: Vec::new(),
362 modify_account_states: AccountModifications(BTreeMap::new()),
363 }
364 }
365}
366
367impl ContinueParams {
368 pub fn default_advance_count() -> u64 {
369 1
370 }
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
379#[serde(rename_all = "camelCase")]
380pub struct PausedEvent {
381 pub slot: u64,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
383 pub batch_index: Option<u32>,
384}
385
386#[serde_with::serde_as]
395#[derive(Debug, Clone, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct DiscoveryBatchEvent {
398 pub slot: u64,
399 pub batch_index: u32,
400 pub matched: Vec<DiscoveryFilter>,
402 pub transactions: Vec<EncodedBinary>,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct ContinueToParams {
412 pub slot: u64,
414 #[serde(default)]
420 pub batch_index: Option<u32>,
421}
422
423#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
425#[serde(rename_all = "lowercase")]
426pub enum BinaryEncoding {
427 Base64,
428}
429
430impl BinaryEncoding {
431 pub fn encode(self, bytes: &[u8]) -> String {
432 match self {
433 Self::Base64 => BASE64.encode(bytes),
434 }
435 }
436
437 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
438 match self {
439 Self::Base64 => BASE64.decode(data),
440 }
441 }
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize)]
446#[serde(rename_all = "camelCase")]
447pub struct EncodedBinary {
448 pub data: String,
450 pub encoding: BinaryEncoding,
452}
453
454impl EncodedBinary {
455 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
456 Self { data, encoding }
457 }
458
459 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
460 Self {
461 data: encoding.encode(bytes),
462 encoding,
463 }
464 }
465
466 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
467 self.encoding.decode(&self.data)
468 }
469}
470
471#[serde_with::serde_as]
473#[derive(Debug, Clone, Serialize, Deserialize)]
474#[serde(rename_all = "camelCase")]
475pub struct AccountData {
476 pub data: EncodedBinary,
478 pub executable: bool,
480 pub lamports: u64,
482 #[serde_as(as = "serde_with::DisplayFromStr")]
483 pub owner: Address,
485 pub space: u64,
487}
488
489impl AccountData {
490 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
491 Ok(solana_account::Account {
492 data: self.data.decode()?,
493 lamports: self.lamports,
494 owner: self.owner,
495 executable: self.executable,
496 rent_epoch: 0,
497 })
498 }
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
503#[serde(tag = "method", content = "params", rename_all = "camelCase")]
504pub enum BacktestResponse {
505 SessionCreated {
506 session_id: String,
507 rpc_endpoint: String,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
509 task_id: Option<String>,
510 },
511 SessionAttached {
512 session_id: String,
513 rpc_endpoint: String,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
515 task_id: Option<String>,
516 },
517 SessionsCreated {
518 session_ids: Vec<String>,
519 },
520 SessionsCreatedV2 {
521 control_session_id: String,
522 session_ids: Vec<String>,
523 #[serde(default)]
524 task_ids: Vec<Option<String>>,
525 #[serde(default)]
530 start_slots: Vec<u64>,
531 #[serde(default)]
532 end_slots: Vec<u64>,
533 },
534 ParallelSessionAttachedV2 {
535 control_session_id: String,
536 session_ids: Vec<String>,
537 #[serde(default)]
538 task_ids: Vec<Option<String>>,
539 },
540 ReadyForContinue,
541 SlotNotification(u64),
542 Paused(PausedEvent),
543 DiscoveryBatch(DiscoveryBatchEvent),
544 Error(BacktestError),
545 Success,
546 Completed {
547 #[serde(skip_serializing_if = "Option::is_none")]
551 summary: Option<SessionSummary>,
552 #[serde(default, skip_serializing_if = "Option::is_none")]
553 agent_stats: Option<Vec<AgentStatsReport>>,
554 },
555 Status {
556 status: BacktestStatus,
557 },
558 SessionEventV1 {
559 session_id: String,
560 event: SessionEventV1,
561 },
562 SessionEventV2 {
563 session_id: String,
564 seq_id: u64,
565 event: SessionEventKind,
566 },
567}
568
569impl BacktestResponse {
570 pub fn is_completed(&self) -> bool {
571 matches!(self, BacktestResponse::Completed { .. })
572 }
573
574 pub fn is_terminal(&self) -> bool {
575 match self {
576 BacktestResponse::Completed { .. } => true,
577 BacktestResponse::Error(e) => matches!(
578 e,
579 BacktestError::NoMoreBlocks
580 | BacktestError::AdvanceSlotFailed { .. }
581 | BacktestError::FinalizeSlotFailed { .. }
582 | BacktestError::Internal { .. }
583 ),
584 _ => false,
585 }
586 }
587}
588
589impl From<BacktestStatus> for BacktestResponse {
590 fn from(status: BacktestStatus) -> Self {
591 Self::Status { status }
592 }
593}
594
595impl From<String> for BacktestResponse {
596 fn from(message: String) -> Self {
597 BacktestError::Internal { error: message }.into()
598 }
599}
600
601impl From<&str> for BacktestResponse {
602 fn from(message: &str) -> Self {
603 BacktestError::Internal {
604 error: message.to_string(),
605 }
606 .into()
607 }
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize)]
611#[serde(tag = "method", content = "params", rename_all = "camelCase")]
612pub enum SessionEventV1 {
613 ReadyForContinue,
614 SlotNotification(u64),
615 Paused(PausedEvent),
616 DiscoveryBatch(DiscoveryBatchEvent),
617 Error(BacktestError),
618 Success,
619 Completed {
620 #[serde(skip_serializing_if = "Option::is_none")]
621 summary: Option<SessionSummary>,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
623 agent_stats: Option<Vec<AgentStatsReport>>,
624 },
625 Status {
626 status: BacktestStatus,
627 },
628}
629
630#[derive(Debug, Clone, Serialize, Deserialize)]
631#[serde(tag = "method", content = "params", rename_all = "camelCase")]
632pub enum SessionEventKind {
633 ReadyForContinue,
634 SlotNotification(u64),
635 Paused(PausedEvent),
636 DiscoveryBatch(DiscoveryBatchEvent),
637 Error(BacktestError),
638 Success,
639 Completed {
640 #[serde(skip_serializing_if = "Option::is_none")]
641 summary: Option<SessionSummary>,
642 },
643 Status {
644 status: BacktestStatus,
645 },
646}
647
648impl SessionEventKind {
649 pub fn is_terminal(&self) -> bool {
650 match self {
651 Self::Completed { .. } => true,
652 Self::Error(e) => matches!(
653 e,
654 BacktestError::NoMoreBlocks
655 | BacktestError::AdvanceSlotFailed { .. }
656 | BacktestError::FinalizeSlotFailed { .. }
657 | BacktestError::Internal { .. }
658 ),
659 _ => false,
660 }
661 }
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize)]
667#[serde(rename_all = "camelCase")]
668pub struct SequencedResponse {
669 pub seq_id: u64,
670 #[serde(flatten)]
671 pub response: BacktestResponse,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize)]
676#[serde(rename_all = "camelCase")]
677pub enum BacktestStatus {
678 StartingRuntime,
680 DecodedTransactions,
681 AppliedAccountModifications,
682 ReadyToExecuteUserTransactions,
683 ExecutedUserTransactions,
684 ExecutingBlockTransactions,
685 ExecutedBlockTransactions,
686 ProgramAccountsLoaded,
687}
688
689impl std::fmt::Display for BacktestStatus {
690 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
691 let s = match self {
692 Self::StartingRuntime => "starting runtime",
693 Self::DecodedTransactions => "decoded transactions",
694 Self::AppliedAccountModifications => "applied account modifications",
695 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
696 Self::ExecutedUserTransactions => "executed user transactions",
697 Self::ExecutingBlockTransactions => "executing block transactions",
698 Self::ExecutedBlockTransactions => "executed block transactions",
699 Self::ProgramAccountsLoaded => "program accounts loaded",
700 };
701 f.write_str(s)
702 }
703}
704
705#[derive(Debug, Clone, Default, Serialize, Deserialize)]
707#[serde(rename_all = "camelCase")]
708pub struct AgentStatsReport {
709 pub name: String,
710 pub slots_processed: u64,
711 pub opportunities_found: u64,
712 pub opportunities_skipped: u64,
713 pub no_routes: u64,
714 pub txs_produced: u64,
715 pub expected_gain_by_mint: BTreeMap<String, i64>,
717 #[serde(default)]
719 pub txs_submitted: u64,
720 #[serde(default)]
722 pub txs_failed: u64,
723 #[serde(default)]
725 pub txs_simulation_rejected: u64,
726 #[serde(default)]
728 pub txs_simulation_failed: u64,
729}
730
731#[derive(Debug, Clone, Default, Serialize, Deserialize)]
733#[serde(rename_all = "camelCase")]
734pub struct SessionSummary {
735 pub correct_simulation: usize,
738 pub incorrect_simulation: usize,
741 pub execution_errors: usize,
743 pub balance_diff: usize,
745 pub log_diff: usize,
747}
748
749impl SessionSummary {
750 pub fn has_deviations(&self) -> bool {
752 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
753 }
754
755 pub fn total_transactions(&self) -> usize {
757 self.correct_simulation
758 + self.incorrect_simulation
759 + self.execution_errors
760 + self.balance_diff
761 + self.log_diff
762 }
763}
764
765impl std::fmt::Display for SessionSummary {
766 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
767 let total = self.total_transactions();
768 write!(
769 f,
770 "Session summary: {total} transactions\n\
771 \x20 - {} correct simulation\n\
772 \x20 - {} incorrect simulation\n\
773 \x20 - {} execution errors\n\
774 \x20 - {} balance diffs\n\
775 \x20 - {} log diffs",
776 self.correct_simulation,
777 self.incorrect_simulation,
778 self.execution_errors,
779 self.balance_diff,
780 self.log_diff,
781 )
782 }
783}
784
785#[derive(Debug, Clone, Serialize, Deserialize)]
787#[serde(rename_all = "camelCase")]
788pub enum BacktestError {
789 InvalidTransactionEncoding {
790 index: usize,
791 error: String,
792 },
793 InvalidTransactionFormat {
794 index: usize,
795 error: String,
796 },
797 InvalidAccountEncoding {
798 address: String,
799 encoding: BinaryEncoding,
800 error: String,
801 },
802 InvalidAccountOwner {
803 address: String,
804 error: String,
805 },
806 InvalidAccountPubkey {
807 address: String,
808 error: String,
809 },
810 NoMoreBlocks,
811 AdvanceSlotFailed {
812 slot: u64,
813 error: String,
814 },
815 FinalizeSlotFailed {
816 slot: u64,
817 error: String,
818 },
819 InvalidRequest {
820 error: String,
821 },
822 Internal {
823 error: String,
824 },
825 InvalidBlockhashFormat {
826 slot: u64,
827 error: String,
828 },
829 InitializingSysvarsFailed {
830 slot: u64,
831 error: String,
832 },
833 ClerkError {
834 error: String,
835 },
836 SimulationError {
837 error: String,
838 },
839 SessionNotFound {
840 session_id: String,
841 },
842 SessionOwnerMismatch,
843 SessionOwnershipBusy {
848 reason: String,
849 },
850}
851
852#[derive(Debug, Clone, Serialize, Deserialize)]
854pub struct AvailableRange {
855 pub bundle_start_slot: u64,
856 pub bundle_start_slot_utc: Option<String>,
857 pub max_bundle_end_slot: Option<u64>,
858 pub max_bundle_end_slot_utc: Option<String>,
859 pub max_bundle_size: Option<u64>,
860}
861
862#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct BundleBuildRequest {
865 pub start_slot: u64,
866 pub end_slot: u64,
867 #[serde(default)]
868 pub bundle_size: Option<u64>,
869 #[serde(default)]
871 pub idempotency_key: Option<String>,
872}
873
874#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
875#[serde(rename_all = "snake_case")]
876pub enum BundleBuildStatus {
877 Completed,
878 Failed,
879 InProgress,
880 NeedsInvestigation,
881}
882
883#[derive(Debug, Clone, Serialize, Deserialize)]
885pub struct BundleBuildStatusResponse {
886 pub request_id: String,
887 pub start_slot: u64,
888 pub end_slot: u64,
889 pub bundle_size: Option<u64>,
890 pub status: BundleBuildStatus,
891}
892
893pub fn split_range(
911 ranges: &[AvailableRange],
912 requested_start: u64,
913 requested_end: u64,
914) -> Result<Vec<(u64, u64)>, String> {
915 if requested_end < requested_start {
916 return Err(format!(
917 "invalid range: start_slot {requested_start} > end_slot {requested_end}"
918 ));
919 }
920
921 let mut ends_by_start: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
926 for r in ranges {
927 if let Some(end) = r.max_bundle_end_slot
928 && end > r.bundle_start_slot
929 {
930 ends_by_start
931 .entry(r.bundle_start_slot)
932 .or_default()
933 .insert(end);
934 }
935 }
936
937 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
944 ends.iter()
945 .next_back()
946 .is_some_and(|&end| end >= requested_start)
947 }) else {
948 return Err(format!(
949 "start_slot {requested_start} is not covered by any available bundle range"
950 ));
951 };
952
953 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
959 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
960 let mut best: Option<Vec<(u64, u64)>> = None;
961 for &end in ends {
962 let candidate = if end >= requested_end {
963 Some(vec![(start, requested_end)])
964 } else {
965 best_from.get(&(end + 1)).map(|rest| {
966 std::iter::once((start, end))
967 .chain(rest.iter().copied())
968 .collect()
969 })
970 };
971 if let Some(candidate) = candidate
972 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
973 {
974 best = Some(candidate);
975 }
976 }
977 if let Some(best) = best {
978 best_from.insert(start, best);
979 }
980 }
981
982 best_from.remove(&anchor_start).ok_or_else(|| {
983 let mut covered_to = anchor_start.saturating_sub(1);
987 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
988 if start > covered_to.saturating_add(1) {
989 break;
990 }
991 if let Some(&end) = ends.iter().next_back() {
992 covered_to = covered_to.max(end);
993 }
994 }
995 if covered_to < requested_end {
996 format!("gap in coverage at slot {}", covered_to + 1)
997 } else {
998 format!(
999 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available bundle ranges"
1000 )
1001 }
1002 })
1003}
1004
1005impl From<BacktestError> for BacktestResponse {
1006 fn from(error: BacktestError) -> Self {
1007 Self::Error(error)
1008 }
1009}
1010
1011impl std::error::Error for BacktestError {}
1012
1013impl fmt::Display for BacktestError {
1014 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1015 match self {
1016 BacktestError::InvalidTransactionEncoding { index, error } => {
1017 write!(f, "invalid transaction encoding at index {index}: {error}")
1018 }
1019 BacktestError::InvalidTransactionFormat { index, error } => {
1020 write!(f, "invalid transaction format at index {index}: {error}")
1021 }
1022 BacktestError::InvalidAccountEncoding {
1023 address,
1024 encoding,
1025 error,
1026 } => write!(
1027 f,
1028 "invalid encoding for account {address} ({encoding:?}): {error}"
1029 ),
1030 BacktestError::InvalidAccountOwner { address, error } => {
1031 write!(f, "invalid owner for account {address}: {error}")
1032 }
1033 BacktestError::InvalidAccountPubkey { address, error } => {
1034 write!(f, "invalid account pubkey {address}: {error}")
1035 }
1036 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
1037 BacktestError::AdvanceSlotFailed { slot, error } => {
1038 write!(f, "failed to advance to slot {slot}: {error}")
1039 }
1040 BacktestError::FinalizeSlotFailed { slot, error } => {
1041 write!(f, "failed to finalize slot {slot}: {error}")
1042 }
1043 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
1044 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
1045 BacktestError::InvalidBlockhashFormat { slot, error } => {
1046 write!(f, "invalid blockhash at slot {slot}: {error}")
1047 }
1048 BacktestError::InitializingSysvarsFailed { slot, error } => {
1049 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
1050 }
1051 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
1052 BacktestError::SimulationError { error } => {
1053 write!(f, "simulation error: {error}")
1054 }
1055 BacktestError::SessionNotFound { session_id } => {
1056 write!(f, "session not found: {session_id}")
1057 }
1058 BacktestError::SessionOwnerMismatch => {
1059 write!(f, "session owner mismatch")
1060 }
1061 BacktestError::SessionOwnershipBusy { reason } => {
1062 write!(f, "session ownership busy: {reason}")
1063 }
1064 }
1065 }
1066}
1067
1068#[cfg(test)]
1069mod tests {
1070 use super::*;
1071
1072 #[test]
1073 fn fail_fast_divergence_kind_str_round_trips() {
1074 for kind in [
1075 FailFastDivergenceKind::AnyNonBenign,
1076 FailFastDivergenceKind::Tracked,
1077 ] {
1078 assert_eq!(
1079 FailFastDivergenceKind::from_str_opt(kind.as_str()),
1080 Some(kind)
1081 );
1082 }
1083 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
1084 assert_eq!(
1085 FailFastDivergenceKind::default(),
1086 FailFastDivergenceKind::AnyNonBenign
1087 );
1088 }
1089
1090 #[test]
1091 fn bundle_build_request_optional_fields_default_to_none() {
1092 let req: BundleBuildRequest =
1093 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
1094 assert_eq!((req.start_slot, req.end_slot), (1, 2));
1095 assert_eq!(req.bundle_size, None);
1096 assert_eq!(req.idempotency_key, None);
1097 }
1098
1099 #[test]
1100 fn bundle_build_request_parses_optional_fields() {
1101 let req: BundleBuildRequest = serde_json::from_str(
1102 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
1103 )
1104 .expect("parse");
1105 assert_eq!(req.bundle_size, Some(500));
1106 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
1107 }
1108
1109 #[test]
1110 fn bundle_build_status_serde_round_trips_with_snake_case() {
1111 let cases = [
1112 (BundleBuildStatus::Completed, "\"completed\""),
1113 (BundleBuildStatus::Failed, "\"failed\""),
1114 (BundleBuildStatus::InProgress, "\"in_progress\""),
1115 (
1116 BundleBuildStatus::NeedsInvestigation,
1117 "\"needs_investigation\"",
1118 ),
1119 ];
1120 for (status, expected) in cases {
1121 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
1122 assert_eq!(
1123 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
1124 status
1125 );
1126 }
1127 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
1128 }
1129
1130 #[test]
1131 fn bundle_build_status_response_serializes_request_and_status() {
1132 let response = BundleBuildStatusResponse {
1133 request_id: "r".to_string(),
1134 start_slot: 100,
1135 end_slot: 200,
1136 bundle_size: Some(50),
1137 status: BundleBuildStatus::InProgress,
1138 };
1139 let json = serde_json::to_value(&response).unwrap();
1140 assert_eq!(json["request_id"].as_str(), Some("r"));
1141 assert_eq!(json["start_slot"].as_u64(), Some(100));
1142 assert_eq!(json["bundle_size"].as_u64(), Some(50));
1143 assert_eq!(json["status"].as_str(), Some("in_progress"));
1144 assert!(json.get("flow_run_id").is_none());
1145 }
1146
1147 fn range(start: u64, end: u64) -> AvailableRange {
1148 AvailableRange {
1149 bundle_start_slot: start,
1150 bundle_start_slot_utc: None,
1151 max_bundle_end_slot: Some(end),
1152 max_bundle_end_slot_utc: None,
1153 max_bundle_size: None,
1154 }
1155 }
1156
1157 #[rstest::rstest]
1161 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
1162 #[case::multi(
1163 vec![range(100, 200), range(201, 300), range(301, 400)],
1164 100, 300, Some(vec![(100, 200), (201, 300)])
1165 )]
1166 #[case::nested(
1169 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
1170 100, 900, Some(vec![(100, 500), (501, 900)])
1171 )]
1172 #[case::prefers_finer_grid(
1176 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
1177 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
1178 )]
1179 #[case::shared_start_prefers_finer(
1183 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
1184 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
1185 )]
1186 #[case::coarse_overlap_prefers_finer_pair(
1190 vec![range(100, 200), range(100, 150), range(151, 160)],
1191 100, 160, Some(vec![(100, 150), (151, 160)])
1192 )]
1193 #[case::falls_back_to_coarse(
1197 vec![range(100, 160), range(100, 120), range(121, 140)],
1198 100, 160, Some(vec![(100, 160)])
1199 )]
1200 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
1202 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
1205 #[case::anchors_then_continues(
1206 vec![range(150, 350), range(351, 600)],
1207 200, 600, Some(vec![(150, 350), (351, 600)])
1208 )]
1209 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
1210 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
1212 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
1214 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
1215 #[case::inverted_range(vec![range(100, 300)], 300, 100, None)]
1217 #[case::user_and_global_ranges_not_collapsed(
1222 vec![range(100, 200), range(100, 150), range(151, 200)],
1223 100, 200, Some(vec![(100, 150), (151, 200)])
1224 )]
1225 fn split_range_cases(
1226 #[case] ranges: Vec<AvailableRange>,
1227 #[case] start: u64,
1228 #[case] end: u64,
1229 #[case] expected: Option<Vec<(u64, u64)>>,
1230 ) {
1231 match expected {
1232 Some(expected) => assert_eq!(split_range(&ranges, start, end).unwrap(), expected),
1233 None => assert!(split_range(&ranges, start, end).is_err()),
1234 }
1235 }
1236
1237 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
1240 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
1241 for r in ranges {
1242 if let Some(end) = r.max_bundle_end_slot
1243 && end > r.bundle_start_slot
1244 {
1245 ends.entry(r.bundle_start_slot).or_default().insert(end);
1246 }
1247 }
1248 ends
1249 }
1250
1251 fn reference_max_split(
1255 ends: &BTreeMap<u64, BTreeSet<u64>>,
1256 cursor: u64,
1257 end: u64,
1258 ) -> Option<Vec<(u64, u64)>> {
1259 ends.get(&cursor)?
1260 .iter()
1261 .filter_map(|&bundle_end| {
1262 if bundle_end >= end {
1263 Some(vec![(cursor, end)])
1264 } else {
1265 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
1266 rest.insert(0, (cursor, bundle_end));
1267 rest
1268 })
1269 }
1270 })
1271 .max_by_key(Vec::len)
1272 }
1273
1274 fn is_valid_split(
1278 split: &[(u64, u64)],
1279 ends: &BTreeMap<u64, BTreeSet<u64>>,
1280 start: u64,
1281 end: u64,
1282 ) -> bool {
1283 split.first().is_some_and(|&(s, _)| s == start)
1284 && split.last().is_some_and(|&(_, e)| e == end)
1285 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
1286 && split.iter().all(|&(s, e)| {
1287 e >= s
1288 && ends
1289 .get(&s)
1290 .and_then(|bundle_ends| bundle_ends.iter().next_back())
1291 .is_some_and(|&max_end| e <= max_end)
1292 })
1293 }
1294
1295 #[test]
1300 fn split_range_matches_reference() {
1301 let mut seed: u64 = 0x9E3779B97F4A7C15;
1302 let mut next = || {
1303 seed = seed
1304 .wrapping_mul(6364136223846793005)
1305 .wrapping_add(1442695040888963407);
1306 seed >> 33
1307 };
1308
1309 for _ in 0..50_000 {
1310 let ranges: Vec<AvailableRange> = (0..next() % 6)
1313 .map(|_| {
1314 let start = next() % 12;
1315 range(start, start + next() % 6) })
1317 .collect();
1318 let start = next() % 12;
1319 let end = start + next() % 6; let got = split_range(&ranges, start, end);
1322 let ends = ends_by_start(&ranges);
1323 let anchor = ends
1326 .range(..=start)
1327 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
1328 .map(|(&s, _)| s);
1329 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
1330
1331 let layout: Vec<_> = ranges
1332 .iter()
1333 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
1334 .collect();
1335 match (&got, &reference) {
1336 (Ok(split), Some(best)) => {
1337 assert!(
1338 is_valid_split(split, &ends, anchor.unwrap(), end),
1339 "invalid split {split:?} for {layout:?} [{start},{end}]"
1340 );
1341 assert_eq!(
1342 split.len(),
1343 best.len(),
1344 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
1345 );
1346 }
1347 (Err(_), None) => {}
1348 _ => panic!(
1349 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
1350 ),
1351 }
1352 }
1353 }
1354}