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
12pub mod subscribe_config;
13pub mod usage;
14pub mod ws_compression;
15
16#[derive(Debug, Serialize, Deserialize)]
18#[serde(tag = "method", content = "params", rename_all = "camelCase")]
19pub enum BacktestRequest {
20 CreateBacktestSession(CreateBacktestSessionRequest),
21 Continue(ContinueParams),
22 ContinueTo(ContinueToParams),
23 ContinueSessionV1(ContinueSessionRequestV1),
24 ContinueToSessionV1(ContinueToSessionRequestV1),
25 CloseBacktestSession,
26 CloseSessionV1(CloseSessionRequestV1),
27 AttachBacktestSession {
28 session_id: String,
29 last_sequence: Option<u64>,
32 },
33 ResumeAttachedSession,
36 AttachParallelControlSessionV2 {
37 control_session_id: String,
38 #[serde(default)]
42 last_sequences: BTreeMap<String, u64>,
43 },
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(untagged)]
52pub enum CreateBacktestSessionRequest {
53 V1(CreateBacktestSessionRequestV1),
54 V0(CreateSessionParams),
55}
56
57impl CreateBacktestSessionRequest {
58 pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
59 match self {
60 Self::V0(request) => CreateBacktestSessionRequestOptions {
61 request,
62 parallel: false,
63 },
64 Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
65 CreateBacktestSessionRequestOptions { request, parallel }
66 }
67 }
68 }
69
70 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
71 let options = self.into_request_options();
72 (options.request, options.parallel)
73 }
74}
75
76impl From<CreateSessionParams> for CreateBacktestSessionRequest {
77 fn from(value: CreateSessionParams) -> Self {
78 Self::V0(value)
79 }
80}
81
82impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
83 fn from(value: CreateBacktestSessionRequestV1) -> Self {
84 Self::V1(value)
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct CreateBacktestSessionRequestV1 {
91 #[serde(flatten)]
92 pub request: CreateSessionParams,
93 pub parallel: bool,
94}
95
96#[derive(Debug, Clone)]
97pub struct CreateBacktestSessionRequestOptions {
98 pub request: CreateSessionParams,
99 pub parallel: bool,
100}
101
102#[derive(Debug, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct ContinueSessionRequestV1 {
105 pub session_id: String,
106 pub request: ContinueParams,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct ContinueToSessionRequestV1 {
112 pub session_id: String,
113 pub request: ContinueToParams,
114}
115
116#[derive(Debug, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct CloseSessionRequestV1 {
119 pub session_id: String,
120}
121
122#[serde_with::serde_as]
129#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
130#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
131pub enum DiscoveryFilter {
132 ProgramExecuted(#[serde_as(as = "serde_with::DisplayFromStr")] Address),
134}
135
136pub struct TxMatchContext<'a> {
141 pub invoked_programs: &'a HashSet<Address>,
143}
144
145impl DiscoveryFilter {
146 pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
149 match self {
150 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
151 }
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub enum ActionKind {
158 Simulate,
159 Send,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(tag = "at", rename_all = "camelCase")]
165pub enum ActionAnchor {
166 AfterSlot {
169 #[serde(default, with = "compressed")]
170 slots: Vec<u64>,
171 },
172 BeforeMatch { filter: DiscoveryFilter },
175 AfterMatch { filter: DiscoveryFilter },
178}
179
180const MAX_ACTION_TRANSACTIONS_BYTES: u64 = 64 * 1024 * 1024;
183
184mod compressed {
189 use std::io::Read;
190
191 use base64::Engine as _;
192 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
193
194 use super::{BASE64, MAX_ACTION_TRANSACTIONS_BYTES};
195
196 const COMPRESSION_LEVEL: i32 = 0;
198
199 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
200 where
201 T: Serialize,
202 S: Serializer,
203 {
204 let json = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
205 let compressed = zstd::stream::encode_all(json.as_slice(), COMPRESSION_LEVEL)
206 .map_err(serde::ser::Error::custom)?;
207 serializer.serialize_str(&BASE64.encode(compressed))
208 }
209
210 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
211 where
212 T: DeserializeOwned,
213 D: Deserializer<'de>,
214 {
215 let blob = String::deserialize(deserializer)?;
216 let compressed = BASE64
217 .decode(blob.as_bytes())
218 .map_err(serde::de::Error::custom)?;
219 let mut json = Vec::new();
220 zstd::stream::read::Decoder::new(compressed.as_slice())
221 .and_then(|d| Read::take(d, MAX_ACTION_TRANSACTIONS_BYTES).read_to_end(&mut json))
222 .map_err(serde::de::Error::custom)?;
223 serde_json::from_slice(&json).map_err(serde::de::Error::custom)
224 }
225}
226
227#[serde_with::serde_as]
230#[derive(Debug, Clone, Serialize, Deserialize)]
231#[serde(rename_all = "camelCase")]
232pub struct ScheduledAction {
233 pub anchor: ActionAnchor,
234 pub kind: ActionKind,
235 #[serde(default, with = "compressed")]
243 pub transactions: Vec<String>,
244 #[serde(default)]
248 pub account_overrides: AccountModifications,
249 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
253 #[serde(default)]
254 pub return_accounts: Vec<Address>,
255 #[serde(default)]
257 pub label: Option<String>,
258}
259
260#[serde_with::serde_as]
262#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
263#[serde(rename_all = "camelCase")]
264pub struct CreateSessionParams {
265 pub start_slot: u64,
267 pub end_slot: u64,
269 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
270 #[serde(default)]
271 #[builder(default)]
272 pub signer_filter: BTreeSet<Address>,
274 #[serde(default)]
277 #[builder(default)]
278 pub send_summary: bool,
279 #[serde(default)]
283 #[builder(default)]
284 pub reroute_order_flow: bool,
285 #[serde(default)]
288 pub capacity_wait_timeout_secs: Option<u16>,
289 #[serde(default)]
293 pub disconnect_timeout_secs: Option<u16>,
294 #[serde(default)]
299 pub extra_compute_units: Option<u32>,
300 #[serde(default)]
302 #[builder(default)]
303 pub agents: Vec<AgentParams>,
304 #[serde(default, skip_serializing_if = "Vec::is_empty")]
311 #[builder(default)]
312 pub discoveries: Vec<DiscoveryFilter>,
313 #[serde(default, skip_serializing_if = "Vec::is_empty")]
316 #[builder(default)]
317 pub actions: Vec<ScheduledAction>,
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
323#[serde(rename_all = "kebab-case")]
324pub enum FailFastDivergenceKind {
325 #[default]
328 AnyNonBenign,
329 Tracked,
332}
333
334impl FailFastDivergenceKind {
335 pub fn as_str(self) -> &'static str {
338 match self {
339 Self::AnyNonBenign => "any-non-benign",
340 Self::Tracked => "tracked",
341 }
342 }
343
344 pub fn from_str_opt(value: &str) -> Option<Self> {
346 match value {
347 "any-non-benign" => Some(Self::AnyNonBenign),
348 "tracked" => Some(Self::Tracked),
349 _ => None,
350 }
351 }
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub enum AgentType {
358 Arb,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
363#[serde(rename_all = "camelCase")]
364pub struct ArbRouteParams {
365 pub base_mint: String,
366 pub temp_mint: String,
367 #[serde(default)]
368 pub buy_dexes: Vec<String>,
369 #[serde(default)]
370 pub sell_dexes: Vec<String>,
371 pub min_input: u64,
372 pub max_input: u64,
373 #[serde(default)]
374 pub min_profit: u64,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379#[serde(rename_all = "camelCase")]
380pub struct AgentParams {
381 pub agent_type: AgentType,
382 pub wallet: Option<String>,
383 pub keypair: Option<String>,
385 pub seed_sol_lamports: Option<u64>,
386 #[serde(default)]
387 pub seed_token_accounts: BTreeMap<String, u64>,
388 #[serde(default)]
389 pub arb_routes: Vec<ArbRouteParams>,
390}
391
392#[serde_with::serde_as]
394#[derive(Debug, Clone, Serialize, Deserialize, Default)]
395pub struct AccountModifications(
396 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
397 #[serde(default)]
398 pub BTreeMap<Address, AccountData>,
399);
400
401#[serde_with::serde_as]
403#[derive(Debug, Serialize, Deserialize)]
404#[serde(rename_all = "camelCase")]
405pub struct ContinueParams {
406 #[serde(default = "ContinueParams::default_advance_count")]
407 pub advance_count: u64,
409 #[serde(default)]
410 pub transactions: Vec<String>,
412 #[serde(default)]
413 pub modify_account_states: AccountModifications,
415}
416
417impl Default for ContinueParams {
418 fn default() -> Self {
419 Self {
420 advance_count: Self::default_advance_count(),
421 transactions: Vec::new(),
422 modify_account_states: AccountModifications(BTreeMap::new()),
423 }
424 }
425}
426
427impl ContinueParams {
428 pub fn default_advance_count() -> u64 {
429 1
430 }
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct PausedEvent {
441 pub slot: u64,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub batch_index: Option<u32>,
444}
445
446#[serde_with::serde_as]
455#[derive(Debug, Clone, Serialize, Deserialize)]
456#[serde(rename_all = "camelCase")]
457pub struct DiscoveryBatchEvent {
458 pub slot: u64,
459 pub batch_index: u32,
460 pub matched: Vec<DiscoveryFilter>,
462 pub transactions: Vec<EncodedBinary>,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize)]
470#[serde(rename_all = "camelCase")]
471pub struct ContinueToParams {
472 pub slot: u64,
474 #[serde(default)]
480 pub batch_index: Option<u32>,
481}
482
483#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
485#[serde(rename_all = "lowercase")]
486pub enum BinaryEncoding {
487 Base64,
488}
489
490impl BinaryEncoding {
491 pub fn encode(self, bytes: &[u8]) -> String {
492 match self {
493 Self::Base64 => BASE64.encode(bytes),
494 }
495 }
496
497 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
498 match self {
499 Self::Base64 => BASE64.decode(data),
500 }
501 }
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize)]
506#[serde(rename_all = "camelCase")]
507pub struct EncodedBinary {
508 pub data: String,
510 pub encoding: BinaryEncoding,
512}
513
514impl EncodedBinary {
515 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
516 Self { data, encoding }
517 }
518
519 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
520 Self {
521 data: encoding.encode(bytes),
522 encoding,
523 }
524 }
525
526 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
527 self.encoding.decode(&self.data)
528 }
529}
530
531#[serde_with::serde_as]
533#[derive(Debug, Clone, Serialize, Deserialize)]
534#[serde(rename_all = "camelCase")]
535pub struct AccountData {
536 pub data: EncodedBinary,
538 pub executable: bool,
540 pub lamports: u64,
542 #[serde_as(as = "serde_with::DisplayFromStr")]
543 pub owner: Address,
545 pub space: u64,
547}
548
549impl AccountData {
550 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
551 Ok(solana_account::Account {
552 data: self.data.decode()?,
553 lamports: self.lamports,
554 owner: self.owner,
555 executable: self.executable,
556 rent_epoch: 0,
557 })
558 }
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize)]
563#[serde(tag = "method", content = "params", rename_all = "camelCase")]
564pub enum BacktestResponse {
565 SessionCreated {
566 session_id: String,
567 rpc_endpoint: String,
568 #[serde(default, skip_serializing_if = "Option::is_none")]
569 task_id: Option<String>,
570 },
571 SessionAttached {
572 session_id: String,
573 rpc_endpoint: String,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
575 task_id: Option<String>,
576 },
577 SessionsCreatedV2 {
578 control_session_id: String,
579 session_ids: Vec<String>,
580 #[serde(default)]
581 task_ids: Vec<Option<String>>,
582 #[serde(default)]
587 start_slots: Vec<u64>,
588 #[serde(default)]
589 end_slots: Vec<u64>,
590 },
591 ParallelSessionAttachedV2 {
592 control_session_id: String,
593 session_ids: Vec<String>,
594 #[serde(default)]
595 task_ids: Vec<Option<String>>,
596 },
597 ReadyForContinue,
598 SlotNotification(u64),
599 Paused(PausedEvent),
600 DiscoveryBatch(DiscoveryBatchEvent),
601 Error(BacktestError),
602 Success,
603 Completed {
604 #[serde(skip_serializing_if = "Option::is_none")]
608 summary: Option<SessionSummary>,
609 #[serde(default, skip_serializing_if = "Option::is_none")]
610 agent_stats: Option<Vec<AgentStatsReport>>,
611 },
612 Status {
613 status: BacktestStatus,
614 },
615 SessionEventV2 {
616 session_id: String,
617 seq_id: u64,
618 event: SessionEventKind,
619 },
620}
621
622impl BacktestResponse {
623 pub fn is_completed(&self) -> bool {
624 matches!(self, BacktestResponse::Completed { .. })
625 }
626
627 pub fn is_terminal(&self) -> bool {
628 match self {
629 BacktestResponse::Completed { .. } => true,
630 BacktestResponse::Error(e) => e.is_terminal(),
631 _ => false,
632 }
633 }
634}
635
636impl From<BacktestStatus> for BacktestResponse {
637 fn from(status: BacktestStatus) -> Self {
638 Self::Status { status }
639 }
640}
641
642impl From<String> for BacktestResponse {
643 fn from(message: String) -> Self {
644 BacktestError::Internal { error: message }.into()
645 }
646}
647
648impl From<&str> for BacktestResponse {
649 fn from(message: &str) -> Self {
650 BacktestError::Internal {
651 error: message.to_string(),
652 }
653 .into()
654 }
655}
656
657#[derive(Debug, Clone, Serialize, Deserialize)]
658#[serde(tag = "method", content = "params", rename_all = "camelCase")]
659pub enum SessionEventKind {
660 ReadyForContinue,
661 SlotNotification(u64),
662 Paused(PausedEvent),
663 DiscoveryBatch(DiscoveryBatchEvent),
664 Error(BacktestError),
665 Success,
666 Completed {
667 #[serde(skip_serializing_if = "Option::is_none")]
668 summary: Option<SessionSummary>,
669 },
670 Status {
671 status: BacktestStatus,
672 },
673}
674
675impl SessionEventKind {
676 pub fn is_terminal(&self) -> bool {
677 match self {
678 Self::Completed { .. } => true,
679 Self::Error(e) => e.is_terminal(),
680 _ => false,
681 }
682 }
683}
684
685#[derive(Debug, Clone, Serialize, Deserialize)]
688#[serde(rename_all = "camelCase")]
689pub struct SequencedResponse {
690 pub seq_id: u64,
691 #[serde(flatten)]
692 pub response: BacktestResponse,
693}
694
695#[derive(Debug, Clone, Serialize, Deserialize)]
697#[serde(rename_all = "camelCase")]
698pub enum BacktestStatus {
699 StartingRuntime,
701 DecodedTransactions,
702 AppliedAccountModifications,
703 ReadyToExecuteUserTransactions,
704 ExecutedUserTransactions,
705 ExecutingBlockTransactions,
706 ExecutedBlockTransactions,
707 ProgramAccountsLoaded,
708}
709
710impl std::fmt::Display for BacktestStatus {
711 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
712 let s = match self {
713 Self::StartingRuntime => "starting runtime",
714 Self::DecodedTransactions => "decoded transactions",
715 Self::AppliedAccountModifications => "applied account modifications",
716 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
717 Self::ExecutedUserTransactions => "executed user transactions",
718 Self::ExecutingBlockTransactions => "executing block transactions",
719 Self::ExecutedBlockTransactions => "executed block transactions",
720 Self::ProgramAccountsLoaded => "program accounts loaded",
721 };
722 f.write_str(s)
723 }
724}
725
726#[derive(Debug, Clone, Default, Serialize, Deserialize)]
728#[serde(rename_all = "camelCase")]
729pub struct AgentStatsReport {
730 pub name: String,
731 pub slots_processed: u64,
732 pub opportunities_found: u64,
733 pub opportunities_skipped: u64,
734 pub no_routes: u64,
735 pub txs_produced: u64,
736 pub expected_gain_by_mint: BTreeMap<String, i64>,
738 #[serde(default)]
740 pub txs_submitted: u64,
741 #[serde(default)]
743 pub txs_failed: u64,
744 #[serde(default)]
746 pub txs_simulation_rejected: u64,
747 #[serde(default)]
749 pub txs_simulation_failed: u64,
750}
751
752#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
756#[serde(rename_all = "camelCase")]
757pub struct RerouteStatsReport {
758 pub swaps_rerouted: u64,
760 #[serde(default)]
762 pub swaps_simulated: u64,
763 #[serde(default)]
765 pub swaps_succeeded: u64,
766 pub requote_failures: u64,
768}
769
770#[derive(Debug, Clone, Default, Serialize, Deserialize)]
772#[serde(rename_all = "camelCase")]
773pub struct SessionSummary {
774 pub correct_simulation: usize,
777 pub incorrect_simulation: usize,
780 pub execution_errors: usize,
782 pub balance_diff: usize,
784 pub log_diff: usize,
786 #[serde(default)]
789 pub unsanitizable: usize,
790 #[serde(default, skip_serializing_if = "Option::is_none")]
793 pub reroute_stats: Option<Box<RerouteStatsReport>>,
794}
795
796impl SessionSummary {
797 pub fn has_deviations(&self) -> bool {
799 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
800 }
801
802 pub fn total_transactions(&self) -> usize {
804 self.correct_simulation
805 + self.incorrect_simulation
806 + self.execution_errors
807 + self.balance_diff
808 + self.log_diff
809 }
810}
811
812impl std::fmt::Display for SessionSummary {
813 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
814 let total = self.total_transactions();
815 write!(
816 f,
817 "Session summary: {total} transactions\n\
818 \x20 - {} correct simulation\n\
819 \x20 - {} incorrect simulation\n\
820 \x20 - {} execution errors\n\
821 \x20 - {} unsanitizable\n\
822 \x20 - {} balance diffs\n\
823 \x20 - {} log diffs",
824 self.correct_simulation,
825 self.incorrect_simulation,
826 self.execution_errors,
827 self.unsanitizable,
828 self.balance_diff,
829 self.log_diff,
830 )
831 }
832}
833
834#[derive(Debug, Clone, Serialize, Deserialize)]
836#[serde(rename_all = "camelCase")]
837pub enum BacktestError {
838 InvalidTransactionEncoding {
839 index: usize,
840 error: String,
841 },
842 InvalidTransactionFormat {
843 index: usize,
844 error: String,
845 },
846 InvalidAccountEncoding {
847 address: String,
848 encoding: BinaryEncoding,
849 error: String,
850 },
851 InvalidAccountOwner {
852 address: String,
853 error: String,
854 },
855 InvalidAccountPubkey {
856 address: String,
857 error: String,
858 },
859 NoMoreBlocks,
860 AdvanceSlotFailed {
861 slot: u64,
862 error: String,
863 },
864 FinalizeSlotFailed {
865 slot: u64,
866 error: String,
867 },
868 InvalidRequest {
869 error: String,
870 },
871 Internal {
872 error: String,
873 },
874 InvalidBlockhashFormat {
875 slot: u64,
876 error: String,
877 },
878 InitializingSysvarsFailed {
879 slot: u64,
880 error: String,
881 },
882 ClerkError {
883 error: String,
884 },
885 SimulationError {
886 error: String,
887 },
888 SessionNotFound {
889 session_id: String,
890 },
891 SessionOwnerMismatch,
892 SessionOwnershipBusy {
897 reason: String,
898 },
899}
900
901impl BacktestError {
902 pub fn is_terminal(&self) -> bool {
906 matches!(
907 self,
908 BacktestError::NoMoreBlocks
909 | BacktestError::AdvanceSlotFailed { .. }
910 | BacktestError::FinalizeSlotFailed { .. }
911 | BacktestError::Internal { .. }
912 )
913 }
914}
915
916#[derive(Debug, Clone, Serialize, Deserialize)]
918pub struct AvailableRange {
919 pub bundle_start_slot: u64,
920 pub bundle_start_slot_utc: Option<String>,
921 pub max_bundle_end_slot: Option<u64>,
922 pub max_bundle_end_slot_utc: Option<String>,
923 pub max_bundle_size: Option<u64>,
924}
925
926#[derive(Debug, Clone, Serialize, Deserialize)]
928pub struct BundleBuildRequest {
929 pub start_slot: u64,
930 pub end_slot: u64,
931 #[serde(default)]
932 pub bundle_size: Option<u64>,
933 #[serde(default)]
935 pub idempotency_key: Option<String>,
936}
937
938#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
939#[serde(rename_all = "snake_case")]
940pub enum BundleBuildStatus {
941 Completed,
943 Failed,
947 InProgress,
949 NeedsInvestigation,
953}
954
955impl BundleBuildStatus {
956 pub fn as_str(self) -> &'static str {
958 match self {
959 Self::Completed => "completed",
960 Self::Failed => "failed",
961 Self::InProgress => "in_progress",
962 Self::NeedsInvestigation => "needs_investigation",
963 }
964 }
965}
966
967#[derive(Debug, Clone, Serialize, Deserialize)]
969pub struct BundleBuildStatusResponse {
970 pub request_id: String,
971 pub start_slot: u64,
972 pub end_slot: u64,
973 pub bundle_size: Option<u64>,
974 pub status: BundleBuildStatus,
975 pub created_at_unix_ms: u64,
978}
979
980#[derive(Debug, Clone, Serialize, Deserialize)]
983pub struct SnapshotSlotResponse {
984 pub snapshot_slot: u64,
985}
986
987pub fn split_range(
1005 ranges: &[AvailableRange],
1006 requested_start: u64,
1007 requested_end: u64,
1008) -> Result<Vec<(u64, u64)>, String> {
1009 if requested_end < requested_start {
1010 return Err(format!(
1011 "invalid range: start_slot {requested_start} > end_slot {requested_end}"
1012 ));
1013 }
1014
1015 let mut ends_by_start: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
1020 for r in ranges {
1021 if let Some(end) = r.max_bundle_end_slot
1022 && end > r.bundle_start_slot
1023 {
1024 ends_by_start
1025 .entry(r.bundle_start_slot)
1026 .or_default()
1027 .insert(end);
1028 }
1029 }
1030
1031 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
1038 ends.iter()
1039 .next_back()
1040 .is_some_and(|&end| end >= requested_start)
1041 }) else {
1042 return Err(format!(
1043 "start_slot {requested_start} is not covered by any available bundle range"
1044 ));
1045 };
1046
1047 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
1053 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
1054 let mut best: Option<Vec<(u64, u64)>> = None;
1055 for &end in ends {
1056 let candidate = if end >= requested_end {
1057 Some(vec![(start, requested_end)])
1058 } else {
1059 best_from.get(&(end + 1)).map(|rest| {
1060 std::iter::once((start, end))
1061 .chain(rest.iter().copied())
1062 .collect()
1063 })
1064 };
1065 if let Some(candidate) = candidate
1066 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
1067 {
1068 best = Some(candidate);
1069 }
1070 }
1071 if let Some(best) = best {
1072 best_from.insert(start, best);
1073 }
1074 }
1075
1076 best_from.remove(&anchor_start).ok_or_else(|| {
1077 let mut covered_to = anchor_start.saturating_sub(1);
1081 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
1082 if start > covered_to.saturating_add(1) {
1083 break;
1084 }
1085 if let Some(&end) = ends.iter().next_back() {
1086 covered_to = covered_to.max(end);
1087 }
1088 }
1089 if covered_to < requested_end {
1090 format!("gap in coverage at slot {}", covered_to + 1)
1091 } else {
1092 format!(
1093 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available bundle ranges"
1094 )
1095 }
1096 })
1097}
1098
1099impl From<BacktestError> for BacktestResponse {
1100 fn from(error: BacktestError) -> Self {
1101 Self::Error(error)
1102 }
1103}
1104
1105impl std::error::Error for BacktestError {}
1106
1107impl fmt::Display for BacktestError {
1108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1109 match self {
1110 BacktestError::InvalidTransactionEncoding { index, error } => {
1111 write!(f, "invalid transaction encoding at index {index}: {error}")
1112 }
1113 BacktestError::InvalidTransactionFormat { index, error } => {
1114 write!(f, "invalid transaction format at index {index}: {error}")
1115 }
1116 BacktestError::InvalidAccountEncoding {
1117 address,
1118 encoding,
1119 error,
1120 } => write!(
1121 f,
1122 "invalid encoding for account {address} ({encoding:?}): {error}"
1123 ),
1124 BacktestError::InvalidAccountOwner { address, error } => {
1125 write!(f, "invalid owner for account {address}: {error}")
1126 }
1127 BacktestError::InvalidAccountPubkey { address, error } => {
1128 write!(f, "invalid account pubkey {address}: {error}")
1129 }
1130 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
1131 BacktestError::AdvanceSlotFailed { slot, error } => {
1132 write!(f, "failed to advance to slot {slot}: {error}")
1133 }
1134 BacktestError::FinalizeSlotFailed { slot, error } => {
1135 write!(f, "failed to finalize slot {slot}: {error}")
1136 }
1137 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
1138 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
1139 BacktestError::InvalidBlockhashFormat { slot, error } => {
1140 write!(f, "invalid blockhash at slot {slot}: {error}")
1141 }
1142 BacktestError::InitializingSysvarsFailed { slot, error } => {
1143 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
1144 }
1145 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
1146 BacktestError::SimulationError { error } => {
1147 write!(f, "simulation error: {error}")
1148 }
1149 BacktestError::SessionNotFound { session_id } => {
1150 write!(f, "session not found: {session_id}")
1151 }
1152 BacktestError::SessionOwnerMismatch => {
1153 write!(f, "session owner mismatch")
1154 }
1155 BacktestError::SessionOwnershipBusy { reason } => {
1156 write!(f, "session ownership busy: {reason}")
1157 }
1158 }
1159 }
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164 use super::*;
1165
1166 #[test]
1167 fn fail_fast_divergence_kind_str_round_trips() {
1168 for kind in [
1169 FailFastDivergenceKind::AnyNonBenign,
1170 FailFastDivergenceKind::Tracked,
1171 ] {
1172 assert_eq!(
1173 FailFastDivergenceKind::from_str_opt(kind.as_str()),
1174 Some(kind)
1175 );
1176 }
1177 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
1178 assert_eq!(
1179 FailFastDivergenceKind::default(),
1180 FailFastDivergenceKind::AnyNonBenign
1181 );
1182 }
1183
1184 #[test]
1185 fn bundle_build_request_optional_fields_default_to_none() {
1186 let req: BundleBuildRequest =
1187 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
1188 assert_eq!((req.start_slot, req.end_slot), (1, 2));
1189 assert_eq!(req.bundle_size, None);
1190 assert_eq!(req.idempotency_key, None);
1191 }
1192
1193 #[test]
1194 fn bundle_build_request_parses_optional_fields() {
1195 let req: BundleBuildRequest = serde_json::from_str(
1196 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
1197 )
1198 .expect("parse");
1199 assert_eq!(req.bundle_size, Some(500));
1200 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
1201 }
1202
1203 #[test]
1204 fn bundle_build_status_serde_round_trips_with_snake_case() {
1205 let cases = [
1206 (BundleBuildStatus::Completed, "\"completed\""),
1207 (BundleBuildStatus::Failed, "\"failed\""),
1208 (BundleBuildStatus::InProgress, "\"in_progress\""),
1209 (
1210 BundleBuildStatus::NeedsInvestigation,
1211 "\"needs_investigation\"",
1212 ),
1213 ];
1214 for (status, expected) in cases {
1215 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
1216 assert_eq!(
1217 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
1218 status
1219 );
1220 assert_eq!(status.as_str(), expected.trim_matches('"'));
1221 }
1222 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
1223 }
1224
1225 #[test]
1226 fn bundle_build_status_response_serializes_request_and_status() {
1227 let response = BundleBuildStatusResponse {
1228 request_id: "r".to_string(),
1229 start_slot: 100,
1230 end_slot: 200,
1231 bundle_size: Some(50),
1232 status: BundleBuildStatus::InProgress,
1233 created_at_unix_ms: 0,
1234 };
1235 let json = serde_json::to_value(&response).unwrap();
1236 assert_eq!(json["request_id"].as_str(), Some("r"));
1237 assert_eq!(json["start_slot"].as_u64(), Some(100));
1238 assert_eq!(json["bundle_size"].as_u64(), Some(50));
1239 assert_eq!(json["status"].as_str(), Some("in_progress"));
1240 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
1241 }
1242
1243 fn range(start: u64, end: u64) -> AvailableRange {
1244 AvailableRange {
1245 bundle_start_slot: start,
1246 bundle_start_slot_utc: None,
1247 max_bundle_end_slot: Some(end),
1248 max_bundle_end_slot_utc: None,
1249 max_bundle_size: None,
1250 }
1251 }
1252
1253 #[rstest::rstest]
1257 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
1258 #[case::multi(
1259 vec![range(100, 200), range(201, 300), range(301, 400)],
1260 100, 300, Some(vec![(100, 200), (201, 300)])
1261 )]
1262 #[case::nested(
1265 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
1266 100, 900, Some(vec![(100, 500), (501, 900)])
1267 )]
1268 #[case::prefers_finer_grid(
1272 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
1273 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
1274 )]
1275 #[case::shared_start_prefers_finer(
1279 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
1280 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
1281 )]
1282 #[case::coarse_overlap_prefers_finer_pair(
1286 vec![range(100, 200), range(100, 150), range(151, 160)],
1287 100, 160, Some(vec![(100, 150), (151, 160)])
1288 )]
1289 #[case::falls_back_to_coarse(
1293 vec![range(100, 160), range(100, 120), range(121, 140)],
1294 100, 160, Some(vec![(100, 160)])
1295 )]
1296 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
1298 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
1301 #[case::anchors_then_continues(
1302 vec![range(150, 350), range(351, 600)],
1303 200, 600, Some(vec![(150, 350), (351, 600)])
1304 )]
1305 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
1306 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
1308 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
1310 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
1311 #[case::inverted_range(vec![range(100, 300)], 300, 100, None)]
1313 #[case::user_and_global_ranges_not_collapsed(
1318 vec![range(100, 200), range(100, 150), range(151, 200)],
1319 100, 200, Some(vec![(100, 150), (151, 200)])
1320 )]
1321 fn split_range_cases(
1322 #[case] ranges: Vec<AvailableRange>,
1323 #[case] start: u64,
1324 #[case] end: u64,
1325 #[case] expected: Option<Vec<(u64, u64)>>,
1326 ) {
1327 match expected {
1328 Some(expected) => assert_eq!(split_range(&ranges, start, end).unwrap(), expected),
1329 None => assert!(split_range(&ranges, start, end).is_err()),
1330 }
1331 }
1332
1333 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
1336 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
1337 for r in ranges {
1338 if let Some(end) = r.max_bundle_end_slot
1339 && end > r.bundle_start_slot
1340 {
1341 ends.entry(r.bundle_start_slot).or_default().insert(end);
1342 }
1343 }
1344 ends
1345 }
1346
1347 fn reference_max_split(
1351 ends: &BTreeMap<u64, BTreeSet<u64>>,
1352 cursor: u64,
1353 end: u64,
1354 ) -> Option<Vec<(u64, u64)>> {
1355 ends.get(&cursor)?
1356 .iter()
1357 .filter_map(|&bundle_end| {
1358 if bundle_end >= end {
1359 Some(vec![(cursor, end)])
1360 } else {
1361 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
1362 rest.insert(0, (cursor, bundle_end));
1363 rest
1364 })
1365 }
1366 })
1367 .max_by_key(Vec::len)
1368 }
1369
1370 fn is_valid_split(
1374 split: &[(u64, u64)],
1375 ends: &BTreeMap<u64, BTreeSet<u64>>,
1376 start: u64,
1377 end: u64,
1378 ) -> bool {
1379 split.first().is_some_and(|&(s, _)| s == start)
1380 && split.last().is_some_and(|&(_, e)| e == end)
1381 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
1382 && split.iter().all(|&(s, e)| {
1383 e >= s
1384 && ends
1385 .get(&s)
1386 .and_then(|bundle_ends| bundle_ends.iter().next_back())
1387 .is_some_and(|&max_end| e <= max_end)
1388 })
1389 }
1390
1391 #[test]
1396 fn split_range_matches_reference() {
1397 let mut seed: u64 = 0x9E3779B97F4A7C15;
1398 let mut next = || {
1399 seed = seed
1400 .wrapping_mul(6364136223846793005)
1401 .wrapping_add(1442695040888963407);
1402 seed >> 33
1403 };
1404
1405 for _ in 0..50_000 {
1406 let ranges: Vec<AvailableRange> = (0..next() % 6)
1409 .map(|_| {
1410 let start = next() % 12;
1411 range(start, start + next() % 6) })
1413 .collect();
1414 let start = next() % 12;
1415 let end = start + next() % 6; let got = split_range(&ranges, start, end);
1418 let ends = ends_by_start(&ranges);
1419 let anchor = ends
1422 .range(..=start)
1423 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
1424 .map(|(&s, _)| s);
1425 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
1426
1427 let layout: Vec<_> = ranges
1428 .iter()
1429 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
1430 .collect();
1431 match (&got, &reference) {
1432 (Ok(split), Some(best)) => {
1433 assert!(
1434 is_valid_split(split, &ends, anchor.unwrap(), end),
1435 "invalid split {split:?} for {layout:?} [{start},{end}]"
1436 );
1437 assert_eq!(
1438 split.len(),
1439 best.len(),
1440 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
1441 );
1442 }
1443 (Err(_), None) => {}
1444 _ => panic!(
1445 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
1446 ),
1447 }
1448 }
1449 }
1450}