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
12mod session_id;
13mod slot_range;
14
15pub use session_id::{ControlSessionId, SessionId};
16pub use slot_range::{InvertedSlotRange, SlotRange};
17
18pub mod subscribe_config;
19pub mod usage;
20pub mod ws_compression;
21
22#[derive(Debug, Serialize, Deserialize)]
24#[serde(tag = "method", content = "params", rename_all = "camelCase")]
25pub enum BacktestRequest {
26 CreateBacktestSession(CreateBacktestSessionRequest),
27 Continue(ContinueParams),
28 ContinueTo(ContinueToParams),
29 ContinueSessionV1(ContinueSessionRequestV1),
30 ContinueToSessionV1(ContinueToSessionRequestV1),
31 CloseBacktestSession,
32 CloseSessionV1(CloseSessionRequestV1),
33 AttachBacktestSession {
34 session_id: SessionId,
35 last_sequence: Option<u64>,
38 },
39 ResumeAttachedSession,
42 AttachParallelControlSessionV2 {
43 control_session_id: ControlSessionId,
44 #[serde(default)]
48 last_sequences: BTreeMap<SessionId, u64>,
49 },
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(untagged)]
58pub enum CreateBacktestSessionRequest {
59 V1(CreateBacktestSessionRequestV1),
60 V0(CreateSessionParams),
61}
62
63impl CreateBacktestSessionRequest {
64 pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
65 match self {
66 Self::V0(request) => CreateBacktestSessionRequestOptions {
67 request,
68 parallel: false,
69 },
70 Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
71 CreateBacktestSessionRequestOptions { request, parallel }
72 }
73 }
74 }
75
76 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
77 let options = self.into_request_options();
78 (options.request, options.parallel)
79 }
80}
81
82impl From<CreateSessionParams> for CreateBacktestSessionRequest {
83 fn from(value: CreateSessionParams) -> Self {
84 Self::V0(value)
85 }
86}
87
88impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
89 fn from(value: CreateBacktestSessionRequestV1) -> Self {
90 Self::V1(value)
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct CreateBacktestSessionRequestV1 {
97 #[serde(flatten)]
98 pub request: CreateSessionParams,
99 pub parallel: bool,
100}
101
102#[derive(Debug, Clone)]
103pub struct CreateBacktestSessionRequestOptions {
104 pub request: CreateSessionParams,
105 pub parallel: bool,
106}
107
108#[derive(Debug, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase")]
110pub struct ContinueSessionRequestV1 {
111 pub session_id: SessionId,
112 pub request: ContinueParams,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct ContinueToSessionRequestV1 {
118 pub session_id: SessionId,
119 pub request: ContinueToParams,
120}
121
122#[derive(Debug, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct CloseSessionRequestV1 {
125 pub session_id: SessionId,
126}
127
128#[serde_with::serde_as]
135#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
136#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
137pub enum DiscoveryFilter {
138 ProgramExecuted(#[serde_as(as = "serde_with::DisplayFromStr")] Address),
140}
141
142pub struct TxMatchContext<'a> {
147 pub invoked_programs: &'a HashSet<Address>,
149}
150
151impl DiscoveryFilter {
152 pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
155 match self {
156 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
157 }
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub enum ActionKind {
164 Simulate,
165 Send,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(tag = "at", rename_all = "camelCase")]
171pub enum ActionAnchor {
172 AfterSlot {
175 #[serde(default, with = "compressed")]
176 slots: Vec<u64>,
177 },
178 BeforeMatch { filter: DiscoveryFilter },
181 AfterMatch { filter: DiscoveryFilter },
184}
185
186const MAX_ACTION_TRANSACTIONS_BYTES: u64 = 64 * 1024 * 1024;
189
190mod compressed {
195 use std::io::Read;
196
197 use base64::Engine as _;
198 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
199
200 use super::{BASE64, MAX_ACTION_TRANSACTIONS_BYTES};
201
202 const COMPRESSION_LEVEL: i32 = 0;
204
205 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
206 where
207 T: Serialize,
208 S: Serializer,
209 {
210 let json = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
211 let compressed = zstd::stream::encode_all(json.as_slice(), COMPRESSION_LEVEL)
212 .map_err(serde::ser::Error::custom)?;
213 serializer.serialize_str(&BASE64.encode(compressed))
214 }
215
216 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
217 where
218 T: DeserializeOwned,
219 D: Deserializer<'de>,
220 {
221 let blob = String::deserialize(deserializer)?;
222 let compressed = BASE64
223 .decode(blob.as_bytes())
224 .map_err(serde::de::Error::custom)?;
225 let mut json = Vec::new();
226 zstd::stream::read::Decoder::new(compressed.as_slice())
227 .and_then(|d| Read::take(d, MAX_ACTION_TRANSACTIONS_BYTES).read_to_end(&mut json))
228 .map_err(serde::de::Error::custom)?;
229 serde_json::from_slice(&json).map_err(serde::de::Error::custom)
230 }
231}
232
233#[serde_with::serde_as]
236#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct ScheduledAction {
239 pub anchor: ActionAnchor,
240 pub kind: ActionKind,
241 #[serde(default, with = "compressed")]
249 pub transactions: Vec<String>,
250 #[serde(default)]
254 pub account_overrides: AccountModifications,
255 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
259 #[serde(default)]
260 pub return_accounts: Vec<Address>,
261 #[serde(default)]
263 pub label: Option<String>,
264}
265
266#[serde_with::serde_as]
268#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
269#[serde(rename_all = "camelCase")]
270pub struct CreateSessionParams {
271 pub start_slot: u64,
273 pub end_slot: u64,
275 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
276 #[serde(default)]
277 #[builder(default)]
278 pub signer_filter: BTreeSet<Address>,
280 #[serde(default)]
283 #[builder(default)]
284 pub send_summary: bool,
285 #[serde(default)]
289 #[builder(default)]
290 pub reroute_order_flow: bool,
291 #[serde(default)]
294 pub capacity_wait_timeout_secs: Option<u16>,
295 #[serde(default)]
299 pub disconnect_timeout_secs: Option<u16>,
300 #[serde(default)]
305 pub extra_compute_units: Option<u32>,
306 #[serde(default)]
308 #[builder(default)]
309 pub agents: Vec<AgentParams>,
310 #[serde(default, skip_serializing_if = "Vec::is_empty")]
317 #[builder(default)]
318 pub discoveries: Vec<DiscoveryFilter>,
319 #[serde(default, skip_serializing_if = "Vec::is_empty")]
322 #[builder(default)]
323 pub actions: Vec<ScheduledAction>,
324 #[serde(default)]
327 #[builder(default)]
328 pub replay_account_state: bool,
329 #[serde(default = "default_true")]
336 #[builder(default = true)]
337 pub detect_failed_l1_swaps: bool,
338}
339
340const fn default_true() -> bool {
342 true
343}
344
345pub const fn sidecar_required(agents_present: bool, reroute_order_flow: bool) -> bool {
349 agents_present || reroute_order_flow
350}
351
352impl CreateSessionParams {
353 pub fn requires_sidecar(&self) -> bool {
355 sidecar_required(!self.agents.is_empty(), self.reroute_order_flow)
356 }
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
362#[serde(rename_all = "kebab-case")]
363pub enum FailFastDivergenceKind {
364 #[default]
367 AnyNonBenign,
368 Tracked,
371}
372
373impl FailFastDivergenceKind {
374 pub fn as_str(self) -> &'static str {
377 match self {
378 Self::AnyNonBenign => "any-non-benign",
379 Self::Tracked => "tracked",
380 }
381 }
382
383 pub fn from_str_opt(value: &str) -> Option<Self> {
385 match value {
386 "any-non-benign" => Some(Self::AnyNonBenign),
387 "tracked" => Some(Self::Tracked),
388 _ => None,
389 }
390 }
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize)]
395#[serde(rename_all = "camelCase")]
396pub enum AgentType {
397 Arb,
398}
399
400#[serde_with::serde_as]
402#[derive(Debug, Clone, Serialize, Deserialize)]
403#[serde(rename_all = "camelCase")]
404pub struct ArbRouteParams {
405 #[serde_as(as = "serde_with::DisplayFromStr")]
406 pub base_mint: Address,
407 #[serde_as(as = "serde_with::DisplayFromStr")]
408 pub temp_mint: Address,
409 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
410 #[serde(default)]
411 pub buy_dexes: Vec<Address>,
412 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
413 #[serde(default)]
414 pub sell_dexes: Vec<Address>,
415 pub min_input: u64,
416 pub max_input: u64,
417 #[serde(default)]
418 pub min_profit: u64,
419}
420
421#[serde_with::serde_as]
423#[derive(Debug, Clone, Serialize, Deserialize)]
424#[serde(rename_all = "camelCase")]
425pub struct AgentParams {
426 pub agent_type: AgentType,
427 #[serde_as(as = "Option<serde_with::DisplayFromStr>")]
428 pub wallet: Option<Address>,
429 pub keypair: Option<String>,
431 pub seed_sol_lamports: Option<u64>,
432 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
433 #[serde(default)]
434 pub seed_token_accounts: BTreeMap<Address, u64>,
435 #[serde(default)]
436 pub arb_routes: Vec<ArbRouteParams>,
437}
438
439#[serde_with::serde_as]
441#[derive(Debug, Clone, Serialize, Deserialize, Default)]
442pub struct AccountModifications(
443 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
444 #[serde(default)]
445 pub BTreeMap<Address, AccountData>,
446);
447
448impl From<BTreeMap<Address, AccountData>> for AccountModifications {
449 fn from(value: BTreeMap<Address, AccountData>) -> Self {
450 Self(value)
451 }
452}
453
454#[serde_with::serde_as]
456#[derive(Debug, Serialize, Deserialize)]
457#[serde(rename_all = "camelCase")]
458pub struct ContinueParams {
459 #[serde(default = "ContinueParams::default_advance_count")]
460 pub advance_count: u64,
462 #[serde(default)]
463 pub transactions: Vec<String>,
465 #[serde(default)]
466 pub modify_account_states: AccountModifications,
468}
469
470impl Default for ContinueParams {
471 fn default() -> Self {
472 Self {
473 advance_count: Self::default_advance_count(),
474 transactions: Vec::new(),
475 modify_account_states: AccountModifications(BTreeMap::new()),
476 }
477 }
478}
479
480impl ContinueParams {
481 pub fn default_advance_count() -> u64 {
482 1
483 }
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
492#[serde(rename_all = "camelCase")]
493pub struct PausedEvent {
494 pub slot: u64,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub batch_index: Option<u32>,
497}
498
499#[serde_with::serde_as]
508#[derive(Debug, Clone, Serialize, Deserialize)]
509#[serde(rename_all = "camelCase")]
510pub struct DiscoveryBatchEvent {
511 pub slot: u64,
512 pub batch_index: u32,
513 pub matched: Vec<DiscoveryFilter>,
515 pub transactions: Vec<EncodedBinary>,
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize)]
523#[serde(rename_all = "camelCase")]
524pub struct ContinueToParams {
525 pub slot: u64,
527 #[serde(default)]
533 pub batch_index: Option<u32>,
534}
535
536#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
538#[serde(rename_all = "lowercase")]
539pub enum BinaryEncoding {
540 Base64,
541}
542
543impl BinaryEncoding {
544 pub fn encode(self, bytes: &[u8]) -> String {
545 match self {
546 Self::Base64 => BASE64.encode(bytes),
547 }
548 }
549
550 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
551 match self {
552 Self::Base64 => BASE64.decode(data),
553 }
554 }
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
559#[serde(rename_all = "camelCase")]
560pub struct EncodedBinary {
561 pub data: String,
563 pub encoding: BinaryEncoding,
565}
566
567impl EncodedBinary {
568 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
569 Self { data, encoding }
570 }
571
572 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
573 Self {
574 data: encoding.encode(bytes),
575 encoding,
576 }
577 }
578
579 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
580 self.encoding.decode(&self.data)
581 }
582}
583
584#[serde_with::serde_as]
586#[derive(Debug, Clone, Serialize, Deserialize)]
587#[serde(rename_all = "camelCase")]
588pub struct AccountData {
589 pub data: EncodedBinary,
591 pub executable: bool,
593 pub lamports: u64,
595 #[serde_as(as = "serde_with::DisplayFromStr")]
596 pub owner: Address,
598 pub space: u64,
600}
601
602impl AccountData {
603 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
604 Ok(solana_account::Account {
605 data: self.data.decode()?,
606 lamports: self.lamports,
607 owner: self.owner,
608 executable: self.executable,
609 rent_epoch: 0,
610 })
611 }
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize)]
616#[serde(tag = "method", content = "params", rename_all = "camelCase")]
617pub enum BacktestResponse {
618 SessionCreated {
619 session_id: SessionId,
620 rpc_endpoint: String,
621 #[serde(default, skip_serializing_if = "Option::is_none")]
622 task_id: Option<String>,
623 },
624 SessionAttached {
625 session_id: SessionId,
626 rpc_endpoint: String,
627 #[serde(default, skip_serializing_if = "Option::is_none")]
628 task_id: Option<String>,
629 },
630 SessionsCreatedV2 {
631 control_session_id: ControlSessionId,
632 session_ids: Vec<SessionId>,
633 #[serde(default)]
634 task_ids: Vec<Option<String>>,
635 #[serde(default)]
640 start_slots: Vec<u64>,
641 #[serde(default)]
642 end_slots: Vec<u64>,
643 },
644 ParallelSessionAttachedV2 {
645 control_session_id: ControlSessionId,
646 session_ids: Vec<SessionId>,
647 #[serde(default)]
648 task_ids: Vec<Option<String>>,
649 },
650 ReadyForContinue,
651 SlotNotification(u64),
652 Paused(PausedEvent),
653 DiscoveryBatch(DiscoveryBatchEvent),
654 Error(BacktestError),
655 Success,
656 Completed {
657 #[serde(skip_serializing_if = "Option::is_none")]
661 summary: Option<SessionSummary>,
662 #[serde(default, skip_serializing_if = "Option::is_none")]
663 agent_stats: Option<Vec<AgentStatsReport>>,
664 },
665 Status {
666 status: BacktestStatus,
667 },
668 SessionEventV2 {
669 session_id: SessionId,
670 seq_id: u64,
671 event: SessionEventKind,
672 },
673}
674
675impl BacktestResponse {
676 pub fn is_completed(&self) -> bool {
677 matches!(self, BacktestResponse::Completed { .. })
678 }
679
680 pub fn is_terminal(&self) -> bool {
681 match self {
682 BacktestResponse::Completed { .. } => true,
683 BacktestResponse::Error(e) => e.is_terminal(),
684 _ => false,
685 }
686 }
687}
688
689impl From<BacktestStatus> for BacktestResponse {
690 fn from(status: BacktestStatus) -> Self {
691 Self::Status { status }
692 }
693}
694
695impl From<String> for BacktestResponse {
696 fn from(message: String) -> Self {
697 BacktestError::Internal { error: message }.into()
698 }
699}
700
701impl From<&str> for BacktestResponse {
702 fn from(message: &str) -> Self {
703 BacktestError::Internal {
704 error: message.to_string(),
705 }
706 .into()
707 }
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize)]
711#[serde(tag = "method", content = "params", rename_all = "camelCase")]
712pub enum SessionEventKind {
713 ReadyForContinue,
714 SlotNotification(u64),
715 Paused(PausedEvent),
716 DiscoveryBatch(DiscoveryBatchEvent),
717 Error(BacktestError),
718 Success,
719 Completed {
720 #[serde(skip_serializing_if = "Option::is_none")]
721 summary: Option<SessionSummary>,
722 },
723 Status {
724 status: BacktestStatus,
725 },
726}
727
728impl SessionEventKind {
729 pub fn is_terminal(&self) -> bool {
730 match self {
731 Self::Completed { .. } => true,
732 Self::Error(e) => e.is_terminal(),
733 _ => false,
734 }
735 }
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize)]
741#[serde(rename_all = "camelCase")]
742pub struct SequencedResponse {
743 pub seq_id: u64,
744 #[serde(flatten)]
745 pub response: BacktestResponse,
746}
747
748#[derive(Debug, Clone, Serialize, Deserialize)]
750#[serde(rename_all = "camelCase")]
751pub enum BacktestStatus {
752 StartingRuntime,
754 DecodedTransactions,
755 AppliedAccountModifications,
756 ReadyToExecuteUserTransactions,
757 ExecutedUserTransactions,
758 ExecutingBlockTransactions,
759 ExecutedBlockTransactions,
760 ProgramAccountsLoaded,
761}
762
763impl std::fmt::Display for BacktestStatus {
764 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
765 let s = match self {
766 Self::StartingRuntime => "starting runtime",
767 Self::DecodedTransactions => "decoded transactions",
768 Self::AppliedAccountModifications => "applied account modifications",
769 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
770 Self::ExecutedUserTransactions => "executed user transactions",
771 Self::ExecutingBlockTransactions => "executing block transactions",
772 Self::ExecutedBlockTransactions => "executed block transactions",
773 Self::ProgramAccountsLoaded => "program accounts loaded",
774 };
775 f.write_str(s)
776 }
777}
778
779#[derive(Debug, Clone, Default, Serialize, Deserialize)]
781#[serde(rename_all = "camelCase")]
782pub struct AgentStatsReport {
783 pub name: String,
784 pub slots_processed: u64,
785 pub opportunities_found: u64,
786 pub opportunities_skipped: u64,
787 pub no_routes: u64,
788 pub txs_produced: u64,
789 pub expected_gain_by_mint: BTreeMap<String, i64>,
791 #[serde(default)]
793 pub txs_submitted: u64,
794 #[serde(default)]
796 pub txs_failed: u64,
797 #[serde(default)]
799 pub txs_simulation_rejected: u64,
800 #[serde(default)]
802 pub txs_simulation_failed: u64,
803}
804
805#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
809#[serde(rename_all = "camelCase")]
810pub struct RerouteStatsReport {
811 #[serde(default)]
813 pub swaps_detected: u64,
814 #[serde(default)]
816 pub swaps_rerouted: u64,
817 #[serde(default)]
819 pub swaps_simulated: u64,
820 #[serde(default)]
822 pub swaps_succeeded: u64,
823 #[serde(default)]
825 pub requote_failures: u64,
826 #[serde(default)]
828 pub metis_unavailable_blocks: u64,
829}
830
831#[derive(Debug, Clone, Default, Serialize, Deserialize)]
833#[serde(rename_all = "camelCase")]
834pub struct SessionSummary {
835 pub correct_simulation: usize,
838 pub incorrect_simulation: usize,
841 pub execution_errors: usize,
843 pub balance_diff: usize,
845 pub log_diff: usize,
847 #[serde(default)]
850 pub unsanitizable: usize,
851 #[serde(default, skip_serializing_if = "Option::is_none")]
854 pub reroute_stats: Option<Box<RerouteStatsReport>>,
855}
856
857impl SessionSummary {
858 pub fn has_deviations(&self) -> bool {
860 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
861 }
862
863 pub fn total_transactions(&self) -> usize {
865 self.correct_simulation
866 + self.incorrect_simulation
867 + self.execution_errors
868 + self.balance_diff
869 + self.log_diff
870 }
871}
872
873impl std::fmt::Display for SessionSummary {
874 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
875 let total = self.total_transactions();
876 write!(
877 f,
878 "Session summary: {total} transactions\n\
879 \x20 - {} correct simulation\n\
880 \x20 - {} incorrect simulation\n\
881 \x20 - {} execution errors\n\
882 \x20 - {} unsanitizable\n\
883 \x20 - {} balance diffs\n\
884 \x20 - {} log diffs",
885 self.correct_simulation,
886 self.incorrect_simulation,
887 self.execution_errors,
888 self.unsanitizable,
889 self.balance_diff,
890 self.log_diff,
891 )
892 }
893}
894
895#[derive(Debug, Clone, Serialize, Deserialize)]
897#[serde(rename_all = "camelCase")]
898pub enum BacktestError {
899 InvalidTransactionEncoding {
900 index: usize,
901 error: String,
902 },
903 InvalidTransactionFormat {
904 index: usize,
905 error: String,
906 },
907 InvalidAccountEncoding {
908 address: String,
909 encoding: BinaryEncoding,
910 error: String,
911 },
912 InvalidAccountOwner {
913 address: String,
914 error: String,
915 },
916 InvalidAccountPubkey {
917 address: String,
918 error: String,
919 },
920 NoMoreBlocks,
921 BundleNotFound {
922 start_slot: u64,
923 end_slot: u64,
924 },
925 AdvanceSlotFailed {
926 slot: u64,
927 error: String,
928 },
929 FinalizeSlotFailed {
930 slot: u64,
931 error: String,
932 },
933 InvalidRequest {
934 error: String,
935 },
936 Internal {
937 error: String,
938 },
939 InvalidBlockhashFormat {
940 slot: u64,
941 error: String,
942 },
943 InitializingSysvarsFailed {
944 slot: u64,
945 error: String,
946 },
947 ClerkError {
948 error: String,
949 },
950 SimulationError {
951 error: String,
952 },
953 SessionNotFound {
954 session_id: String,
955 },
956 SessionOwnerMismatch,
957 SessionOwnershipBusy {
962 reason: String,
963 },
964}
965
966impl BacktestError {
967 pub fn is_terminal(&self) -> bool {
971 matches!(
972 self,
973 BacktestError::NoMoreBlocks
974 | BacktestError::BundleNotFound { .. }
975 | BacktestError::AdvanceSlotFailed { .. }
976 | BacktestError::FinalizeSlotFailed { .. }
977 | BacktestError::Internal { .. }
978 )
979 }
980}
981
982#[derive(Debug, Clone, Serialize, Deserialize)]
984pub struct AvailableRange {
985 pub bundle_start_slot: u64,
986 pub bundle_start_slot_utc: Option<String>,
987 pub max_bundle_end_slot: Option<u64>,
988 pub max_bundle_end_slot_utc: Option<String>,
989 pub max_bundle_size: Option<u64>,
990}
991
992#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
994#[serde(rename_all = "snake_case")]
995pub enum BundleKind {
996 #[default]
998 Blocks,
999 AccountState,
1002}
1003
1004impl BundleKind {
1005 pub fn as_str(self) -> &'static str {
1007 match self {
1008 Self::Blocks => "blocks",
1009 Self::AccountState => "account_state",
1010 }
1011 }
1012
1013 pub fn from_str_opt(value: &str) -> Option<Self> {
1015 match value {
1016 "blocks" => Some(Self::Blocks),
1017 "account_state" => Some(Self::AccountState),
1018 _ => None,
1019 }
1020 }
1021}
1022
1023#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1027pub struct BundleBuildRequest {
1028 pub start_slot: u64,
1029 pub end_slot: u64,
1030 #[serde(default)]
1031 pub bundle_size: Option<u64>,
1032 #[serde(default)]
1035 pub idempotency_key: Option<String>,
1036 #[serde(default)]
1038 pub kind: BundleKind,
1039}
1040
1041#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1042#[serde(rename_all = "snake_case")]
1043pub enum BundleBuildStatus {
1044 Completed,
1046 Failed,
1048 InProgress,
1050 NeedsInvestigation,
1053}
1054
1055impl BundleBuildStatus {
1056 pub fn as_str(self) -> &'static str {
1058 match self {
1059 Self::Completed => "completed",
1060 Self::Failed => "failed",
1061 Self::InProgress => "in_progress",
1062 Self::NeedsInvestigation => "needs_investigation",
1063 }
1064 }
1065}
1066
1067#[derive(Debug, Clone, Serialize, Deserialize)]
1069pub struct BundleBuildStatusResponse {
1070 pub request_id: String,
1071 pub start_slot: u64,
1072 pub end_slot: u64,
1073 pub bundle_size: Option<u64>,
1074 pub status: BundleBuildStatus,
1075 pub created_at_unix_ms: u64,
1078 #[serde(default)]
1080 pub kind: BundleKind,
1081}
1082
1083#[derive(Debug, Clone, Serialize, Deserialize)]
1085pub struct BundleBuildListResponse {
1086 pub builds: Vec<BundleBuildStatusResponse>,
1087}
1088
1089#[derive(Debug, Clone, Serialize, Deserialize)]
1092pub struct SnapshotSlotResponse {
1093 pub snapshot_slot: u64,
1094}
1095
1096pub fn split_range(
1114 ranges: &[AvailableRange],
1115 requested: SlotRange,
1116) -> Result<Vec<(u64, u64)>, String> {
1117 let (requested_start, requested_end) = (requested.start(), requested.end());
1118
1119 let mut ends_by_start: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
1124 for r in ranges {
1125 if let Some(end) = r.max_bundle_end_slot
1126 && end > r.bundle_start_slot
1127 {
1128 ends_by_start
1129 .entry(r.bundle_start_slot)
1130 .or_default()
1131 .insert(end);
1132 }
1133 }
1134
1135 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
1142 ends.iter()
1143 .next_back()
1144 .is_some_and(|&end| end >= requested_start)
1145 }) else {
1146 return Err(format!(
1147 "start_slot {requested_start} is not covered by any available bundle range"
1148 ));
1149 };
1150
1151 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
1157 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
1158 let mut best: Option<Vec<(u64, u64)>> = None;
1159 for &end in ends {
1160 let candidate = if end >= requested_end {
1161 Some(vec![(start, requested_end)])
1162 } else {
1163 best_from.get(&(end + 1)).map(|rest| {
1164 std::iter::once((start, end))
1165 .chain(rest.iter().copied())
1166 .collect()
1167 })
1168 };
1169 if let Some(candidate) = candidate
1170 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
1171 {
1172 best = Some(candidate);
1173 }
1174 }
1175 if let Some(best) = best {
1176 best_from.insert(start, best);
1177 }
1178 }
1179
1180 best_from.remove(&anchor_start).ok_or_else(|| {
1181 let mut covered_to = anchor_start.saturating_sub(1);
1185 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
1186 if start > covered_to.saturating_add(1) {
1187 break;
1188 }
1189 if let Some(&end) = ends.iter().next_back() {
1190 covered_to = covered_to.max(end);
1191 }
1192 }
1193 if covered_to < requested_end {
1194 format!("gap in coverage at slot {}", covered_to + 1)
1195 } else {
1196 format!(
1197 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available bundle ranges"
1198 )
1199 }
1200 })
1201}
1202
1203impl From<BacktestError> for BacktestResponse {
1204 fn from(error: BacktestError) -> Self {
1205 Self::Error(error)
1206 }
1207}
1208
1209impl std::error::Error for BacktestError {}
1210
1211impl fmt::Display for BacktestError {
1212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1213 match self {
1214 BacktestError::InvalidTransactionEncoding { index, error } => {
1215 write!(f, "invalid transaction encoding at index {index}: {error}")
1216 }
1217 BacktestError::InvalidTransactionFormat { index, error } => {
1218 write!(f, "invalid transaction format at index {index}: {error}")
1219 }
1220 BacktestError::InvalidAccountEncoding {
1221 address,
1222 encoding,
1223 error,
1224 } => write!(
1225 f,
1226 "invalid encoding for account {address} ({encoding:?}): {error}"
1227 ),
1228 BacktestError::InvalidAccountOwner { address, error } => {
1229 write!(f, "invalid owner for account {address}: {error}")
1230 }
1231 BacktestError::InvalidAccountPubkey { address, error } => {
1232 write!(f, "invalid account pubkey {address}: {error}")
1233 }
1234 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
1235 BacktestError::BundleNotFound {
1236 start_slot,
1237 end_slot,
1238 } => write!(
1239 f,
1240 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
1241 ),
1242 BacktestError::AdvanceSlotFailed { slot, error } => {
1243 write!(f, "failed to advance to slot {slot}: {error}")
1244 }
1245 BacktestError::FinalizeSlotFailed { slot, error } => {
1246 write!(f, "failed to finalize slot {slot}: {error}")
1247 }
1248 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
1249 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
1250 BacktestError::InvalidBlockhashFormat { slot, error } => {
1251 write!(f, "invalid blockhash at slot {slot}: {error}")
1252 }
1253 BacktestError::InitializingSysvarsFailed { slot, error } => {
1254 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
1255 }
1256 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
1257 BacktestError::SimulationError { error } => {
1258 write!(f, "simulation error: {error}")
1259 }
1260 BacktestError::SessionNotFound { session_id } => {
1261 write!(f, "session not found: {session_id}")
1262 }
1263 BacktestError::SessionOwnerMismatch => {
1264 write!(f, "session owner mismatch")
1265 }
1266 BacktestError::SessionOwnershipBusy { reason } => {
1267 write!(f, "session ownership busy: {reason}")
1268 }
1269 }
1270 }
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275 use super::*;
1276
1277 #[test]
1278 fn fail_fast_divergence_kind_str_round_trips() {
1279 for kind in [
1280 FailFastDivergenceKind::AnyNonBenign,
1281 FailFastDivergenceKind::Tracked,
1282 ] {
1283 assert_eq!(
1284 FailFastDivergenceKind::from_str_opt(kind.as_str()),
1285 Some(kind)
1286 );
1287 }
1288 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
1289 assert_eq!(
1290 FailFastDivergenceKind::default(),
1291 FailFastDivergenceKind::AnyNonBenign
1292 );
1293 }
1294
1295 #[test]
1296 fn bundle_not_found_is_terminal_and_descriptive() {
1297 let error = BacktestError::BundleNotFound {
1298 start_slot: 100,
1299 end_slot: 600,
1300 };
1301
1302 assert!(error.is_terminal());
1303 assert_eq!(
1304 error.to_string(),
1305 "block bundle not found for start_slot=100, end_slot=600"
1306 );
1307 }
1308
1309 #[test]
1310 fn bundle_build_request_optional_fields_default_to_none() {
1311 let req: BundleBuildRequest =
1312 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
1313 assert_eq!((req.start_slot, req.end_slot), (1, 2));
1314 assert_eq!(req.bundle_size, None);
1315 assert_eq!(req.idempotency_key, None);
1316 }
1317
1318 #[test]
1319 fn bundle_build_request_parses_optional_fields() {
1320 let req: BundleBuildRequest = serde_json::from_str(
1321 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
1322 )
1323 .expect("parse");
1324 assert_eq!(req.bundle_size, Some(500));
1325 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
1326 }
1327
1328 #[test]
1329 fn bundle_build_status_serde_round_trips_with_snake_case() {
1330 let cases = [
1331 (BundleBuildStatus::Completed, "\"completed\""),
1332 (BundleBuildStatus::Failed, "\"failed\""),
1333 (BundleBuildStatus::InProgress, "\"in_progress\""),
1334 (
1335 BundleBuildStatus::NeedsInvestigation,
1336 "\"needs_investigation\"",
1337 ),
1338 ];
1339 for (status, expected) in cases {
1340 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
1341 assert_eq!(
1342 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
1343 status
1344 );
1345 assert_eq!(status.as_str(), expected.trim_matches('"'));
1346 }
1347 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
1348 }
1349
1350 #[test]
1351 fn bundle_build_request_without_kind_deserializes_as_blocks() {
1352 let request: BundleBuildRequest =
1354 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
1355 assert_eq!(request.kind, BundleKind::Blocks);
1356 }
1357
1358 #[test]
1359 fn bundle_kind_round_trips_over_serde_and_as_str() {
1360 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
1361 let json = serde_json::to_string(&kind).unwrap();
1362 assert_eq!(json, format!("\"{}\"", kind.as_str()));
1363 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
1364 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
1367 }
1368 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
1369 assert_eq!(BundleKind::from_str_opt("accountState"), None);
1370 }
1371
1372 #[test]
1373 fn bundle_build_request_carries_kind_through_serde() {
1374 let request = BundleBuildRequest {
1375 start_slot: 100,
1376 end_slot: 200,
1377 kind: BundleKind::AccountState,
1378 ..Default::default()
1379 };
1380 let json = serde_json::to_string(&request).unwrap();
1381 assert!(json.contains(r#""kind":"account_state""#), "{json}");
1382 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
1383 assert_eq!(parsed.kind, BundleKind::AccountState);
1384 }
1385
1386 #[test]
1387 fn bundle_build_status_response_serializes_request_and_status() {
1388 let response = BundleBuildStatusResponse {
1389 request_id: "r".to_string(),
1390 start_slot: 100,
1391 end_slot: 200,
1392 bundle_size: Some(50),
1393 status: BundleBuildStatus::InProgress,
1394 created_at_unix_ms: 0,
1395 kind: BundleKind::Blocks,
1396 };
1397 let json = serde_json::to_value(&response).unwrap();
1398 assert_eq!(json["request_id"].as_str(), Some("r"));
1399 assert_eq!(json["start_slot"].as_u64(), Some(100));
1400 assert_eq!(json["bundle_size"].as_u64(), Some(50));
1401 assert_eq!(json["status"].as_str(), Some("in_progress"));
1402 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
1403 }
1404
1405 fn range(start: u64, end: u64) -> AvailableRange {
1406 AvailableRange {
1407 bundle_start_slot: start,
1408 bundle_start_slot_utc: None,
1409 max_bundle_end_slot: Some(end),
1410 max_bundle_end_slot_utc: None,
1411 max_bundle_size: None,
1412 }
1413 }
1414
1415 #[rstest::rstest]
1419 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
1420 #[case::multi(
1421 vec![range(100, 200), range(201, 300), range(301, 400)],
1422 100, 300, Some(vec![(100, 200), (201, 300)])
1423 )]
1424 #[case::nested(
1427 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
1428 100, 900, Some(vec![(100, 500), (501, 900)])
1429 )]
1430 #[case::prefers_finer_grid(
1434 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
1435 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
1436 )]
1437 #[case::shared_start_prefers_finer(
1441 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
1442 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
1443 )]
1444 #[case::coarse_overlap_prefers_finer_pair(
1448 vec![range(100, 200), range(100, 150), range(151, 160)],
1449 100, 160, Some(vec![(100, 150), (151, 160)])
1450 )]
1451 #[case::falls_back_to_coarse(
1455 vec![range(100, 160), range(100, 120), range(121, 140)],
1456 100, 160, Some(vec![(100, 160)])
1457 )]
1458 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
1460 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
1463 #[case::anchors_then_continues(
1464 vec![range(150, 350), range(351, 600)],
1465 200, 600, Some(vec![(150, 350), (351, 600)])
1466 )]
1467 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
1468 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
1470 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
1472 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
1473 #[case::user_and_global_ranges_not_collapsed(
1478 vec![range(100, 200), range(100, 150), range(151, 200)],
1479 100, 200, Some(vec![(100, 150), (151, 200)])
1480 )]
1481 fn split_range_cases(
1482 #[case] ranges: Vec<AvailableRange>,
1483 #[case] start: u64,
1484 #[case] end: u64,
1485 #[case] expected: Option<Vec<(u64, u64)>>,
1486 ) {
1487 match expected {
1488 Some(expected) => assert_eq!(
1489 split_range(&ranges, SlotRange::new_unchecked(start, end)).unwrap(),
1490 expected
1491 ),
1492 None => assert!(split_range(&ranges, SlotRange::new_unchecked(start, end)).is_err()),
1493 }
1494 }
1495
1496 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
1499 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
1500 for r in ranges {
1501 if let Some(end) = r.max_bundle_end_slot
1502 && end > r.bundle_start_slot
1503 {
1504 ends.entry(r.bundle_start_slot).or_default().insert(end);
1505 }
1506 }
1507 ends
1508 }
1509
1510 fn reference_max_split(
1514 ends: &BTreeMap<u64, BTreeSet<u64>>,
1515 cursor: u64,
1516 end: u64,
1517 ) -> Option<Vec<(u64, u64)>> {
1518 ends.get(&cursor)?
1519 .iter()
1520 .filter_map(|&bundle_end| {
1521 if bundle_end >= end {
1522 Some(vec![(cursor, end)])
1523 } else {
1524 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
1525 rest.insert(0, (cursor, bundle_end));
1526 rest
1527 })
1528 }
1529 })
1530 .max_by_key(Vec::len)
1531 }
1532
1533 fn is_valid_split(
1537 split: &[(u64, u64)],
1538 ends: &BTreeMap<u64, BTreeSet<u64>>,
1539 start: u64,
1540 end: u64,
1541 ) -> bool {
1542 split.first().is_some_and(|&(s, _)| s == start)
1543 && split.last().is_some_and(|&(_, e)| e == end)
1544 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
1545 && split.iter().all(|&(s, e)| {
1546 e >= s
1547 && ends
1548 .get(&s)
1549 .and_then(|bundle_ends| bundle_ends.iter().next_back())
1550 .is_some_and(|&max_end| e <= max_end)
1551 })
1552 }
1553
1554 #[test]
1559 fn split_range_matches_reference() {
1560 let mut seed: u64 = 0x9E3779B97F4A7C15;
1561 let mut next = || {
1562 seed = seed
1563 .wrapping_mul(6364136223846793005)
1564 .wrapping_add(1442695040888963407);
1565 seed >> 33
1566 };
1567
1568 for _ in 0..50_000 {
1569 let ranges: Vec<AvailableRange> = (0..next() % 6)
1572 .map(|_| {
1573 let start = next() % 12;
1574 range(start, start + next() % 6) })
1576 .collect();
1577 let start = next() % 12;
1578 let end = start + next() % 6; let got = split_range(&ranges, SlotRange::new_unchecked(start, end));
1581 let ends = ends_by_start(&ranges);
1582 let anchor = ends
1585 .range(..=start)
1586 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
1587 .map(|(&s, _)| s);
1588 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
1589
1590 let layout: Vec<_> = ranges
1591 .iter()
1592 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
1593 .collect();
1594 match (&got, &reference) {
1595 (Ok(split), Some(best)) => {
1596 assert!(
1597 is_valid_split(split, &ends, anchor.unwrap(), end),
1598 "invalid split {split:?} for {layout:?} [{start},{end}]"
1599 );
1600 assert_eq!(
1601 split.len(),
1602 best.len(),
1603 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
1604 );
1605 }
1606 (Err(_), None) => {}
1607 _ => panic!(
1608 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
1609 ),
1610 }
1611 }
1612 }
1613}