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, Serialize, Deserialize)]
754#[serde(rename_all = "camelCase")]
755pub struct SessionSummary {
756 pub correct_simulation: usize,
759 pub incorrect_simulation: usize,
762 pub execution_errors: usize,
764 pub balance_diff: usize,
766 pub log_diff: usize,
768}
769
770impl SessionSummary {
771 pub fn has_deviations(&self) -> bool {
773 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
774 }
775
776 pub fn total_transactions(&self) -> usize {
778 self.correct_simulation
779 + self.incorrect_simulation
780 + self.execution_errors
781 + self.balance_diff
782 + self.log_diff
783 }
784}
785
786impl std::fmt::Display for SessionSummary {
787 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
788 let total = self.total_transactions();
789 write!(
790 f,
791 "Session summary: {total} transactions\n\
792 \x20 - {} correct simulation\n\
793 \x20 - {} incorrect simulation\n\
794 \x20 - {} execution errors\n\
795 \x20 - {} balance diffs\n\
796 \x20 - {} log diffs",
797 self.correct_simulation,
798 self.incorrect_simulation,
799 self.execution_errors,
800 self.balance_diff,
801 self.log_diff,
802 )
803 }
804}
805
806#[derive(Debug, Clone, Serialize, Deserialize)]
808#[serde(rename_all = "camelCase")]
809pub enum BacktestError {
810 InvalidTransactionEncoding {
811 index: usize,
812 error: String,
813 },
814 InvalidTransactionFormat {
815 index: usize,
816 error: String,
817 },
818 InvalidAccountEncoding {
819 address: String,
820 encoding: BinaryEncoding,
821 error: String,
822 },
823 InvalidAccountOwner {
824 address: String,
825 error: String,
826 },
827 InvalidAccountPubkey {
828 address: String,
829 error: String,
830 },
831 NoMoreBlocks,
832 AdvanceSlotFailed {
833 slot: u64,
834 error: String,
835 },
836 FinalizeSlotFailed {
837 slot: u64,
838 error: String,
839 },
840 InvalidRequest {
841 error: String,
842 },
843 Internal {
844 error: String,
845 },
846 InvalidBlockhashFormat {
847 slot: u64,
848 error: String,
849 },
850 InitializingSysvarsFailed {
851 slot: u64,
852 error: String,
853 },
854 ClerkError {
855 error: String,
856 },
857 SimulationError {
858 error: String,
859 },
860 SessionNotFound {
861 session_id: String,
862 },
863 SessionOwnerMismatch,
864 SessionOwnershipBusy {
869 reason: String,
870 },
871}
872
873impl BacktestError {
874 pub fn is_terminal(&self) -> bool {
878 matches!(
879 self,
880 BacktestError::NoMoreBlocks
881 | BacktestError::AdvanceSlotFailed { .. }
882 | BacktestError::FinalizeSlotFailed { .. }
883 | BacktestError::Internal { .. }
884 )
885 }
886}
887
888#[derive(Debug, Clone, Serialize, Deserialize)]
890pub struct AvailableRange {
891 pub bundle_start_slot: u64,
892 pub bundle_start_slot_utc: Option<String>,
893 pub max_bundle_end_slot: Option<u64>,
894 pub max_bundle_end_slot_utc: Option<String>,
895 pub max_bundle_size: Option<u64>,
896}
897
898#[derive(Debug, Clone, Serialize, Deserialize)]
900pub struct BundleBuildRequest {
901 pub start_slot: u64,
902 pub end_slot: u64,
903 #[serde(default)]
904 pub bundle_size: Option<u64>,
905 #[serde(default)]
907 pub idempotency_key: Option<String>,
908}
909
910#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
911#[serde(rename_all = "snake_case")]
912pub enum BundleBuildStatus {
913 Completed,
915 Failed,
919 InProgress,
921 NeedsInvestigation,
925}
926
927impl BundleBuildStatus {
928 pub fn as_str(self) -> &'static str {
930 match self {
931 Self::Completed => "completed",
932 Self::Failed => "failed",
933 Self::InProgress => "in_progress",
934 Self::NeedsInvestigation => "needs_investigation",
935 }
936 }
937}
938
939#[derive(Debug, Clone, Serialize, Deserialize)]
941pub struct BundleBuildStatusResponse {
942 pub request_id: String,
943 pub start_slot: u64,
944 pub end_slot: u64,
945 pub bundle_size: Option<u64>,
946 pub status: BundleBuildStatus,
947 pub created_at_unix_ms: u64,
950}
951
952#[derive(Debug, Clone, Serialize, Deserialize)]
955pub struct SnapshotSlotResponse {
956 pub snapshot_slot: u64,
957}
958
959pub fn split_range(
977 ranges: &[AvailableRange],
978 requested_start: u64,
979 requested_end: u64,
980) -> Result<Vec<(u64, u64)>, String> {
981 if requested_end < requested_start {
982 return Err(format!(
983 "invalid range: start_slot {requested_start} > end_slot {requested_end}"
984 ));
985 }
986
987 let mut ends_by_start: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
992 for r in ranges {
993 if let Some(end) = r.max_bundle_end_slot
994 && end > r.bundle_start_slot
995 {
996 ends_by_start
997 .entry(r.bundle_start_slot)
998 .or_default()
999 .insert(end);
1000 }
1001 }
1002
1003 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
1010 ends.iter()
1011 .next_back()
1012 .is_some_and(|&end| end >= requested_start)
1013 }) else {
1014 return Err(format!(
1015 "start_slot {requested_start} is not covered by any available bundle range"
1016 ));
1017 };
1018
1019 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
1025 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
1026 let mut best: Option<Vec<(u64, u64)>> = None;
1027 for &end in ends {
1028 let candidate = if end >= requested_end {
1029 Some(vec![(start, requested_end)])
1030 } else {
1031 best_from.get(&(end + 1)).map(|rest| {
1032 std::iter::once((start, end))
1033 .chain(rest.iter().copied())
1034 .collect()
1035 })
1036 };
1037 if let Some(candidate) = candidate
1038 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
1039 {
1040 best = Some(candidate);
1041 }
1042 }
1043 if let Some(best) = best {
1044 best_from.insert(start, best);
1045 }
1046 }
1047
1048 best_from.remove(&anchor_start).ok_or_else(|| {
1049 let mut covered_to = anchor_start.saturating_sub(1);
1053 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
1054 if start > covered_to.saturating_add(1) {
1055 break;
1056 }
1057 if let Some(&end) = ends.iter().next_back() {
1058 covered_to = covered_to.max(end);
1059 }
1060 }
1061 if covered_to < requested_end {
1062 format!("gap in coverage at slot {}", covered_to + 1)
1063 } else {
1064 format!(
1065 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available bundle ranges"
1066 )
1067 }
1068 })
1069}
1070
1071impl From<BacktestError> for BacktestResponse {
1072 fn from(error: BacktestError) -> Self {
1073 Self::Error(error)
1074 }
1075}
1076
1077impl std::error::Error for BacktestError {}
1078
1079impl fmt::Display for BacktestError {
1080 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1081 match self {
1082 BacktestError::InvalidTransactionEncoding { index, error } => {
1083 write!(f, "invalid transaction encoding at index {index}: {error}")
1084 }
1085 BacktestError::InvalidTransactionFormat { index, error } => {
1086 write!(f, "invalid transaction format at index {index}: {error}")
1087 }
1088 BacktestError::InvalidAccountEncoding {
1089 address,
1090 encoding,
1091 error,
1092 } => write!(
1093 f,
1094 "invalid encoding for account {address} ({encoding:?}): {error}"
1095 ),
1096 BacktestError::InvalidAccountOwner { address, error } => {
1097 write!(f, "invalid owner for account {address}: {error}")
1098 }
1099 BacktestError::InvalidAccountPubkey { address, error } => {
1100 write!(f, "invalid account pubkey {address}: {error}")
1101 }
1102 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
1103 BacktestError::AdvanceSlotFailed { slot, error } => {
1104 write!(f, "failed to advance to slot {slot}: {error}")
1105 }
1106 BacktestError::FinalizeSlotFailed { slot, error } => {
1107 write!(f, "failed to finalize slot {slot}: {error}")
1108 }
1109 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
1110 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
1111 BacktestError::InvalidBlockhashFormat { slot, error } => {
1112 write!(f, "invalid blockhash at slot {slot}: {error}")
1113 }
1114 BacktestError::InitializingSysvarsFailed { slot, error } => {
1115 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
1116 }
1117 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
1118 BacktestError::SimulationError { error } => {
1119 write!(f, "simulation error: {error}")
1120 }
1121 BacktestError::SessionNotFound { session_id } => {
1122 write!(f, "session not found: {session_id}")
1123 }
1124 BacktestError::SessionOwnerMismatch => {
1125 write!(f, "session owner mismatch")
1126 }
1127 BacktestError::SessionOwnershipBusy { reason } => {
1128 write!(f, "session ownership busy: {reason}")
1129 }
1130 }
1131 }
1132}
1133
1134#[cfg(test)]
1135mod tests {
1136 use super::*;
1137
1138 #[test]
1139 fn fail_fast_divergence_kind_str_round_trips() {
1140 for kind in [
1141 FailFastDivergenceKind::AnyNonBenign,
1142 FailFastDivergenceKind::Tracked,
1143 ] {
1144 assert_eq!(
1145 FailFastDivergenceKind::from_str_opt(kind.as_str()),
1146 Some(kind)
1147 );
1148 }
1149 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
1150 assert_eq!(
1151 FailFastDivergenceKind::default(),
1152 FailFastDivergenceKind::AnyNonBenign
1153 );
1154 }
1155
1156 #[test]
1157 fn bundle_build_request_optional_fields_default_to_none() {
1158 let req: BundleBuildRequest =
1159 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
1160 assert_eq!((req.start_slot, req.end_slot), (1, 2));
1161 assert_eq!(req.bundle_size, None);
1162 assert_eq!(req.idempotency_key, None);
1163 }
1164
1165 #[test]
1166 fn bundle_build_request_parses_optional_fields() {
1167 let req: BundleBuildRequest = serde_json::from_str(
1168 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
1169 )
1170 .expect("parse");
1171 assert_eq!(req.bundle_size, Some(500));
1172 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
1173 }
1174
1175 #[test]
1176 fn bundle_build_status_serde_round_trips_with_snake_case() {
1177 let cases = [
1178 (BundleBuildStatus::Completed, "\"completed\""),
1179 (BundleBuildStatus::Failed, "\"failed\""),
1180 (BundleBuildStatus::InProgress, "\"in_progress\""),
1181 (
1182 BundleBuildStatus::NeedsInvestigation,
1183 "\"needs_investigation\"",
1184 ),
1185 ];
1186 for (status, expected) in cases {
1187 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
1188 assert_eq!(
1189 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
1190 status
1191 );
1192 assert_eq!(status.as_str(), expected.trim_matches('"'));
1193 }
1194 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
1195 }
1196
1197 #[test]
1198 fn bundle_build_status_response_serializes_request_and_status() {
1199 let response = BundleBuildStatusResponse {
1200 request_id: "r".to_string(),
1201 start_slot: 100,
1202 end_slot: 200,
1203 bundle_size: Some(50),
1204 status: BundleBuildStatus::InProgress,
1205 created_at_unix_ms: 0,
1206 };
1207 let json = serde_json::to_value(&response).unwrap();
1208 assert_eq!(json["request_id"].as_str(), Some("r"));
1209 assert_eq!(json["start_slot"].as_u64(), Some(100));
1210 assert_eq!(json["bundle_size"].as_u64(), Some(50));
1211 assert_eq!(json["status"].as_str(), Some("in_progress"));
1212 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
1213 }
1214
1215 fn range(start: u64, end: u64) -> AvailableRange {
1216 AvailableRange {
1217 bundle_start_slot: start,
1218 bundle_start_slot_utc: None,
1219 max_bundle_end_slot: Some(end),
1220 max_bundle_end_slot_utc: None,
1221 max_bundle_size: None,
1222 }
1223 }
1224
1225 #[rstest::rstest]
1229 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
1230 #[case::multi(
1231 vec![range(100, 200), range(201, 300), range(301, 400)],
1232 100, 300, Some(vec![(100, 200), (201, 300)])
1233 )]
1234 #[case::nested(
1237 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
1238 100, 900, Some(vec![(100, 500), (501, 900)])
1239 )]
1240 #[case::prefers_finer_grid(
1244 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
1245 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
1246 )]
1247 #[case::shared_start_prefers_finer(
1251 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
1252 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
1253 )]
1254 #[case::coarse_overlap_prefers_finer_pair(
1258 vec![range(100, 200), range(100, 150), range(151, 160)],
1259 100, 160, Some(vec![(100, 150), (151, 160)])
1260 )]
1261 #[case::falls_back_to_coarse(
1265 vec![range(100, 160), range(100, 120), range(121, 140)],
1266 100, 160, Some(vec![(100, 160)])
1267 )]
1268 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
1270 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
1273 #[case::anchors_then_continues(
1274 vec![range(150, 350), range(351, 600)],
1275 200, 600, Some(vec![(150, 350), (351, 600)])
1276 )]
1277 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
1278 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
1280 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
1282 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
1283 #[case::inverted_range(vec![range(100, 300)], 300, 100, None)]
1285 #[case::user_and_global_ranges_not_collapsed(
1290 vec![range(100, 200), range(100, 150), range(151, 200)],
1291 100, 200, Some(vec![(100, 150), (151, 200)])
1292 )]
1293 fn split_range_cases(
1294 #[case] ranges: Vec<AvailableRange>,
1295 #[case] start: u64,
1296 #[case] end: u64,
1297 #[case] expected: Option<Vec<(u64, u64)>>,
1298 ) {
1299 match expected {
1300 Some(expected) => assert_eq!(split_range(&ranges, start, end).unwrap(), expected),
1301 None => assert!(split_range(&ranges, start, end).is_err()),
1302 }
1303 }
1304
1305 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
1308 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
1309 for r in ranges {
1310 if let Some(end) = r.max_bundle_end_slot
1311 && end > r.bundle_start_slot
1312 {
1313 ends.entry(r.bundle_start_slot).or_default().insert(end);
1314 }
1315 }
1316 ends
1317 }
1318
1319 fn reference_max_split(
1323 ends: &BTreeMap<u64, BTreeSet<u64>>,
1324 cursor: u64,
1325 end: u64,
1326 ) -> Option<Vec<(u64, u64)>> {
1327 ends.get(&cursor)?
1328 .iter()
1329 .filter_map(|&bundle_end| {
1330 if bundle_end >= end {
1331 Some(vec![(cursor, end)])
1332 } else {
1333 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
1334 rest.insert(0, (cursor, bundle_end));
1335 rest
1336 })
1337 }
1338 })
1339 .max_by_key(Vec::len)
1340 }
1341
1342 fn is_valid_split(
1346 split: &[(u64, u64)],
1347 ends: &BTreeMap<u64, BTreeSet<u64>>,
1348 start: u64,
1349 end: u64,
1350 ) -> bool {
1351 split.first().is_some_and(|&(s, _)| s == start)
1352 && split.last().is_some_and(|&(_, e)| e == end)
1353 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
1354 && split.iter().all(|&(s, e)| {
1355 e >= s
1356 && ends
1357 .get(&s)
1358 .and_then(|bundle_ends| bundle_ends.iter().next_back())
1359 .is_some_and(|&max_end| e <= max_end)
1360 })
1361 }
1362
1363 #[test]
1368 fn split_range_matches_reference() {
1369 let mut seed: u64 = 0x9E3779B97F4A7C15;
1370 let mut next = || {
1371 seed = seed
1372 .wrapping_mul(6364136223846793005)
1373 .wrapping_add(1442695040888963407);
1374 seed >> 33
1375 };
1376
1377 for _ in 0..50_000 {
1378 let ranges: Vec<AvailableRange> = (0..next() % 6)
1381 .map(|_| {
1382 let start = next() % 12;
1383 range(start, start + next() % 6) })
1385 .collect();
1386 let start = next() % 12;
1387 let end = start + next() % 6; let got = split_range(&ranges, start, end);
1390 let ends = ends_by_start(&ranges);
1391 let anchor = ends
1394 .range(..=start)
1395 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
1396 .map(|(&s, _)| s);
1397 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
1398
1399 let layout: Vec<_> = ranges
1400 .iter()
1401 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
1402 .collect();
1403 match (&got, &reference) {
1404 (Ok(split), Some(best)) => {
1405 assert!(
1406 is_valid_split(split, &ends, anchor.unwrap(), end),
1407 "invalid split {split:?} for {layout:?} [{start},{end}]"
1408 );
1409 assert_eq!(
1410 split.len(),
1411 best.len(),
1412 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
1413 );
1414 }
1415 (Err(_), None) => {}
1416 _ => panic!(
1417 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
1418 ),
1419 }
1420 }
1421 }
1422}