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, Default, 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 #[serde(default)]
993 pub kind: Option<BundleKind>,
994 #[serde(default)]
997 pub scope: Option<RangeScope>,
998}
999
1000#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1002#[serde(rename_all = "snake_case")]
1003pub enum RangeScope {
1004 Global,
1006 User,
1008}
1009
1010impl RangeScope {
1011 pub fn as_str(self) -> &'static str {
1013 match self {
1014 Self::Global => "global",
1015 Self::User => "user",
1016 }
1017 }
1018}
1019
1020#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1022#[serde(rename_all = "snake_case")]
1023pub enum BundleKind {
1024 #[default]
1026 Blocks,
1027 AccountState,
1030}
1031
1032impl BundleKind {
1033 pub fn as_str(self) -> &'static str {
1035 match self {
1036 Self::Blocks => "blocks",
1037 Self::AccountState => "account_state",
1038 }
1039 }
1040
1041 pub fn from_str_opt(value: &str) -> Option<Self> {
1043 match value {
1044 "blocks" => Some(Self::Blocks),
1045 "account_state" => Some(Self::AccountState),
1046 _ => None,
1047 }
1048 }
1049}
1050
1051#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1055pub struct BundleBuildRequest {
1056 pub start_slot: u64,
1057 pub end_slot: u64,
1058 #[serde(default)]
1059 pub bundle_size: Option<u64>,
1060 #[serde(default)]
1063 pub idempotency_key: Option<String>,
1064 #[serde(default)]
1066 pub kind: BundleKind,
1067}
1068
1069#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1070#[serde(rename_all = "snake_case")]
1071pub enum BundleBuildStatus {
1072 Completed,
1074 Failed,
1076 InProgress,
1078 NeedsInvestigation,
1081}
1082
1083impl BundleBuildStatus {
1084 pub fn as_str(self) -> &'static str {
1086 match self {
1087 Self::Completed => "completed",
1088 Self::Failed => "failed",
1089 Self::InProgress => "in_progress",
1090 Self::NeedsInvestigation => "needs_investigation",
1091 }
1092 }
1093}
1094
1095#[derive(Debug, Clone, Serialize, Deserialize)]
1097pub struct BundleBuildStatusResponse {
1098 pub request_id: String,
1099 pub start_slot: u64,
1100 pub end_slot: u64,
1101 pub bundle_size: Option<u64>,
1102 pub status: BundleBuildStatus,
1103 pub created_at_unix_ms: u64,
1106 #[serde(default)]
1108 pub kind: BundleKind,
1109}
1110
1111#[derive(Debug, Clone, Serialize, Deserialize)]
1113pub struct BundleBuildListResponse {
1114 pub builds: Vec<BundleBuildStatusResponse>,
1115}
1116
1117#[derive(Debug, Clone, Serialize, Deserialize)]
1120pub struct SnapshotSlotResponse {
1121 pub snapshot_slot: u64,
1122}
1123
1124pub fn split_range(
1142 ranges: &[AvailableRange],
1143 requested: SlotRange,
1144) -> Result<Vec<(u64, u64)>, String> {
1145 let (requested_start, requested_end) = (requested.start(), requested.end());
1146
1147 let mut ends_by_start: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
1152 for r in ranges {
1153 if let Some(end) = r.max_bundle_end_slot
1154 && end > r.bundle_start_slot
1155 {
1156 ends_by_start
1157 .entry(r.bundle_start_slot)
1158 .or_default()
1159 .insert(end);
1160 }
1161 }
1162
1163 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
1170 ends.iter()
1171 .next_back()
1172 .is_some_and(|&end| end >= requested_start)
1173 }) else {
1174 return Err(format!(
1175 "start_slot {requested_start} is not covered by any available bundle range"
1176 ));
1177 };
1178
1179 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
1185 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
1186 let mut best: Option<Vec<(u64, u64)>> = None;
1187 for &end in ends {
1188 let candidate = if end >= requested_end {
1189 Some(vec![(start, requested_end)])
1190 } else {
1191 best_from.get(&(end + 1)).map(|rest| {
1192 std::iter::once((start, end))
1193 .chain(rest.iter().copied())
1194 .collect()
1195 })
1196 };
1197 if let Some(candidate) = candidate
1198 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
1199 {
1200 best = Some(candidate);
1201 }
1202 }
1203 if let Some(best) = best {
1204 best_from.insert(start, best);
1205 }
1206 }
1207
1208 best_from.remove(&anchor_start).ok_or_else(|| {
1209 let mut covered_to = anchor_start.saturating_sub(1);
1213 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
1214 if start > covered_to.saturating_add(1) {
1215 break;
1216 }
1217 if let Some(&end) = ends.iter().next_back() {
1218 covered_to = covered_to.max(end);
1219 }
1220 }
1221 if covered_to < requested_end {
1222 format!("gap in coverage at slot {}", covered_to + 1)
1223 } else {
1224 format!(
1225 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available bundle ranges"
1226 )
1227 }
1228 })
1229}
1230
1231impl From<BacktestError> for BacktestResponse {
1232 fn from(error: BacktestError) -> Self {
1233 Self::Error(error)
1234 }
1235}
1236
1237impl std::error::Error for BacktestError {}
1238
1239impl fmt::Display for BacktestError {
1240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1241 match self {
1242 BacktestError::InvalidTransactionEncoding { index, error } => {
1243 write!(f, "invalid transaction encoding at index {index}: {error}")
1244 }
1245 BacktestError::InvalidTransactionFormat { index, error } => {
1246 write!(f, "invalid transaction format at index {index}: {error}")
1247 }
1248 BacktestError::InvalidAccountEncoding {
1249 address,
1250 encoding,
1251 error,
1252 } => write!(
1253 f,
1254 "invalid encoding for account {address} ({encoding:?}): {error}"
1255 ),
1256 BacktestError::InvalidAccountOwner { address, error } => {
1257 write!(f, "invalid owner for account {address}: {error}")
1258 }
1259 BacktestError::InvalidAccountPubkey { address, error } => {
1260 write!(f, "invalid account pubkey {address}: {error}")
1261 }
1262 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
1263 BacktestError::BundleNotFound {
1264 start_slot,
1265 end_slot,
1266 } => write!(
1267 f,
1268 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
1269 ),
1270 BacktestError::AdvanceSlotFailed { slot, error } => {
1271 write!(f, "failed to advance to slot {slot}: {error}")
1272 }
1273 BacktestError::FinalizeSlotFailed { slot, error } => {
1274 write!(f, "failed to finalize slot {slot}: {error}")
1275 }
1276 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
1277 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
1278 BacktestError::InvalidBlockhashFormat { slot, error } => {
1279 write!(f, "invalid blockhash at slot {slot}: {error}")
1280 }
1281 BacktestError::InitializingSysvarsFailed { slot, error } => {
1282 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
1283 }
1284 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
1285 BacktestError::SimulationError { error } => {
1286 write!(f, "simulation error: {error}")
1287 }
1288 BacktestError::SessionNotFound { session_id } => {
1289 write!(f, "session not found: {session_id}")
1290 }
1291 BacktestError::SessionOwnerMismatch => {
1292 write!(f, "session owner mismatch")
1293 }
1294 BacktestError::SessionOwnershipBusy { reason } => {
1295 write!(f, "session ownership busy: {reason}")
1296 }
1297 }
1298 }
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303 use super::*;
1304
1305 #[test]
1306 fn fail_fast_divergence_kind_str_round_trips() {
1307 for kind in [
1308 FailFastDivergenceKind::AnyNonBenign,
1309 FailFastDivergenceKind::Tracked,
1310 ] {
1311 assert_eq!(
1312 FailFastDivergenceKind::from_str_opt(kind.as_str()),
1313 Some(kind)
1314 );
1315 }
1316 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
1317 assert_eq!(
1318 FailFastDivergenceKind::default(),
1319 FailFastDivergenceKind::AnyNonBenign
1320 );
1321 }
1322
1323 #[test]
1324 fn bundle_not_found_is_terminal_and_descriptive() {
1325 let error = BacktestError::BundleNotFound {
1326 start_slot: 100,
1327 end_slot: 600,
1328 };
1329
1330 assert!(error.is_terminal());
1331 assert_eq!(
1332 error.to_string(),
1333 "block bundle not found for start_slot=100, end_slot=600"
1334 );
1335 }
1336
1337 #[test]
1338 fn bundle_build_request_optional_fields_default_to_none() {
1339 let req: BundleBuildRequest =
1340 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
1341 assert_eq!((req.start_slot, req.end_slot), (1, 2));
1342 assert_eq!(req.bundle_size, None);
1343 assert_eq!(req.idempotency_key, None);
1344 }
1345
1346 #[test]
1347 fn bundle_build_request_parses_optional_fields() {
1348 let req: BundleBuildRequest = serde_json::from_str(
1349 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
1350 )
1351 .expect("parse");
1352 assert_eq!(req.bundle_size, Some(500));
1353 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
1354 }
1355
1356 #[test]
1357 fn bundle_build_status_serde_round_trips_with_snake_case() {
1358 let cases = [
1359 (BundleBuildStatus::Completed, "\"completed\""),
1360 (BundleBuildStatus::Failed, "\"failed\""),
1361 (BundleBuildStatus::InProgress, "\"in_progress\""),
1362 (
1363 BundleBuildStatus::NeedsInvestigation,
1364 "\"needs_investigation\"",
1365 ),
1366 ];
1367 for (status, expected) in cases {
1368 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
1369 assert_eq!(
1370 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
1371 status
1372 );
1373 assert_eq!(status.as_str(), expected.trim_matches('"'));
1374 }
1375 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
1376 }
1377
1378 #[test]
1379 fn bundle_build_request_without_kind_deserializes_as_blocks() {
1380 let request: BundleBuildRequest =
1382 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
1383 assert_eq!(request.kind, BundleKind::Blocks);
1384 }
1385
1386 #[test]
1387 fn bundle_kind_round_trips_over_serde_and_as_str() {
1388 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
1389 let json = serde_json::to_string(&kind).unwrap();
1390 assert_eq!(json, format!("\"{}\"", kind.as_str()));
1391 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
1392 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
1395 }
1396 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
1397 assert_eq!(BundleKind::from_str_opt("accountState"), None);
1398 }
1399
1400 #[test]
1401 fn bundle_build_request_carries_kind_through_serde() {
1402 let request = BundleBuildRequest {
1403 start_slot: 100,
1404 end_slot: 200,
1405 kind: BundleKind::AccountState,
1406 ..Default::default()
1407 };
1408 let json = serde_json::to_string(&request).unwrap();
1409 assert!(json.contains(r#""kind":"account_state""#), "{json}");
1410 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
1411 assert_eq!(parsed.kind, BundleKind::AccountState);
1412 }
1413
1414 #[test]
1415 fn bundle_build_status_response_serializes_request_and_status() {
1416 let response = BundleBuildStatusResponse {
1417 request_id: "r".to_string(),
1418 start_slot: 100,
1419 end_slot: 200,
1420 bundle_size: Some(50),
1421 status: BundleBuildStatus::InProgress,
1422 created_at_unix_ms: 0,
1423 kind: BundleKind::Blocks,
1424 };
1425 let json = serde_json::to_value(&response).unwrap();
1426 assert_eq!(json["request_id"].as_str(), Some("r"));
1427 assert_eq!(json["start_slot"].as_u64(), Some(100));
1428 assert_eq!(json["bundle_size"].as_u64(), Some(50));
1429 assert_eq!(json["status"].as_str(), Some("in_progress"));
1430 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
1431 }
1432
1433 #[test]
1434 fn available_range_reads_a_server_that_predates_kind_and_scope() {
1435 let json = r#"{
1437 "bundle_start_slot": 100,
1438 "bundle_start_slot_utc": null,
1439 "max_bundle_end_slot": 200,
1440 "max_bundle_end_slot_utc": null,
1441 "max_bundle_size": 101
1442 }"#;
1443 let parsed: AvailableRange = serde_json::from_str(json).unwrap();
1444 assert_eq!(parsed.bundle_start_slot, 100);
1445 assert_eq!(parsed.kind, None);
1446 assert_eq!(parsed.scope, None);
1447 }
1448
1449 #[test]
1450 fn available_range_ignores_fields_it_does_not_know() {
1451 let json = r#"{"bundle_start_slot": 1, "some_future_field": "x"}"#;
1453 assert_eq!(
1454 serde_json::from_str::<AvailableRange>(json)
1455 .unwrap()
1456 .bundle_start_slot,
1457 1
1458 );
1459 }
1460
1461 #[test]
1462 fn available_range_labels_round_trip() {
1463 let range = AvailableRange {
1464 bundle_start_slot: 100,
1465 max_bundle_end_slot: Some(200),
1466 kind: Some(BundleKind::AccountState),
1467 scope: Some(RangeScope::User),
1468 ..Default::default()
1469 };
1470 let json = serde_json::to_string(&range).unwrap();
1471 assert!(json.contains(r#""kind":"account_state""#), "{json}");
1472 assert!(json.contains(r#""scope":"user""#), "{json}");
1473 let parsed: AvailableRange = serde_json::from_str(&json).unwrap();
1474 assert_eq!(parsed.kind, Some(BundleKind::AccountState));
1475 assert_eq!(parsed.scope, Some(RangeScope::User));
1476 }
1477
1478 fn range(start: u64, end: u64) -> AvailableRange {
1479 AvailableRange {
1480 bundle_start_slot: start,
1481 max_bundle_end_slot: Some(end),
1482 ..Default::default()
1483 }
1484 }
1485
1486 #[rstest::rstest]
1490 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
1491 #[case::multi(
1492 vec![range(100, 200), range(201, 300), range(301, 400)],
1493 100, 300, Some(vec![(100, 200), (201, 300)])
1494 )]
1495 #[case::nested(
1498 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
1499 100, 900, Some(vec![(100, 500), (501, 900)])
1500 )]
1501 #[case::prefers_finer_grid(
1505 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
1506 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
1507 )]
1508 #[case::shared_start_prefers_finer(
1512 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
1513 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
1514 )]
1515 #[case::coarse_overlap_prefers_finer_pair(
1519 vec![range(100, 200), range(100, 150), range(151, 160)],
1520 100, 160, Some(vec![(100, 150), (151, 160)])
1521 )]
1522 #[case::falls_back_to_coarse(
1526 vec![range(100, 160), range(100, 120), range(121, 140)],
1527 100, 160, Some(vec![(100, 160)])
1528 )]
1529 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
1531 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
1534 #[case::anchors_then_continues(
1535 vec![range(150, 350), range(351, 600)],
1536 200, 600, Some(vec![(150, 350), (351, 600)])
1537 )]
1538 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
1539 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
1541 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
1543 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
1544 #[case::user_and_global_ranges_not_collapsed(
1549 vec![range(100, 200), range(100, 150), range(151, 200)],
1550 100, 200, Some(vec![(100, 150), (151, 200)])
1551 )]
1552 fn split_range_cases(
1553 #[case] ranges: Vec<AvailableRange>,
1554 #[case] start: u64,
1555 #[case] end: u64,
1556 #[case] expected: Option<Vec<(u64, u64)>>,
1557 ) {
1558 match expected {
1559 Some(expected) => assert_eq!(
1560 split_range(&ranges, SlotRange::new_unchecked(start, end)).unwrap(),
1561 expected
1562 ),
1563 None => assert!(split_range(&ranges, SlotRange::new_unchecked(start, end)).is_err()),
1564 }
1565 }
1566
1567 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
1570 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
1571 for r in ranges {
1572 if let Some(end) = r.max_bundle_end_slot
1573 && end > r.bundle_start_slot
1574 {
1575 ends.entry(r.bundle_start_slot).or_default().insert(end);
1576 }
1577 }
1578 ends
1579 }
1580
1581 fn reference_max_split(
1585 ends: &BTreeMap<u64, BTreeSet<u64>>,
1586 cursor: u64,
1587 end: u64,
1588 ) -> Option<Vec<(u64, u64)>> {
1589 ends.get(&cursor)?
1590 .iter()
1591 .filter_map(|&bundle_end| {
1592 if bundle_end >= end {
1593 Some(vec![(cursor, end)])
1594 } else {
1595 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
1596 rest.insert(0, (cursor, bundle_end));
1597 rest
1598 })
1599 }
1600 })
1601 .max_by_key(Vec::len)
1602 }
1603
1604 fn is_valid_split(
1608 split: &[(u64, u64)],
1609 ends: &BTreeMap<u64, BTreeSet<u64>>,
1610 start: u64,
1611 end: u64,
1612 ) -> bool {
1613 split.first().is_some_and(|&(s, _)| s == start)
1614 && split.last().is_some_and(|&(_, e)| e == end)
1615 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
1616 && split.iter().all(|&(s, e)| {
1617 e >= s
1618 && ends
1619 .get(&s)
1620 .and_then(|bundle_ends| bundle_ends.iter().next_back())
1621 .is_some_and(|&max_end| e <= max_end)
1622 })
1623 }
1624
1625 #[test]
1630 fn split_range_matches_reference() {
1631 let mut seed: u64 = 0x9E3779B97F4A7C15;
1632 let mut next = || {
1633 seed = seed
1634 .wrapping_mul(6364136223846793005)
1635 .wrapping_add(1442695040888963407);
1636 seed >> 33
1637 };
1638
1639 for _ in 0..50_000 {
1640 let ranges: Vec<AvailableRange> = (0..next() % 6)
1643 .map(|_| {
1644 let start = next() % 12;
1645 range(start, start + next() % 6) })
1647 .collect();
1648 let start = next() % 12;
1649 let end = start + next() % 6; let got = split_range(&ranges, SlotRange::new_unchecked(start, end));
1652 let ends = ends_by_start(&ranges);
1653 let anchor = ends
1656 .range(..=start)
1657 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
1658 .map(|(&s, _)| s);
1659 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
1660
1661 let layout: Vec<_> = ranges
1662 .iter()
1663 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
1664 .collect();
1665 match (&got, &reference) {
1666 (Ok(split), Some(best)) => {
1667 assert!(
1668 is_valid_split(split, &ends, anchor.unwrap(), end),
1669 "invalid split {split:?} for {layout:?} [{start},{end}]"
1670 );
1671 assert_eq!(
1672 split.len(),
1673 best.len(),
1674 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
1675 );
1676 }
1677 (Err(_), None) => {}
1678 _ => panic!(
1679 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
1680 ),
1681 }
1682 }
1683 }
1684}