1use std::{
2 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
3 fmt,
4 str::FromStr,
5 sync::Arc,
6};
7
8use base64::{
9 DecodeError as Base64DecodeError, Engine as _, engine::general_purpose::STANDARD as BASE64,
10};
11use parking_lot::RwLock;
12use serde::{Deserialize, Serialize};
13use solana_account::AccountSharedData;
14use solana_address::{Address, error::ParseAddressError};
15
16mod session_id;
17mod session_type;
18mod slot_range;
19
20pub use session_id::{ControlSessionId, SessionId};
21pub use session_type::SessionType;
22pub use slot_range::{InvertedSlotRange, SlotRange};
23
24pub mod client_identity;
25pub mod geyser_route;
26pub mod subscribe_config;
27pub mod usage;
28pub mod ws_compression;
29
30#[derive(Debug, Serialize, Deserialize)]
32#[serde(tag = "method", content = "params", rename_all = "camelCase")]
33pub enum BacktestRequest {
34 CreateBacktestSession(CreateBacktestSessionRequest),
35 Continue(ContinueParams),
36 ContinueTo(ContinueToParams),
37 ContinueSessionV1(ContinueSessionRequestV1),
38 ContinueToSessionV1(ContinueToSessionRequestV1),
39 CloseBacktestSession,
40 CloseSessionV1(CloseSessionRequestV1),
41 AttachBacktestSession {
42 session_id: SessionId,
43 last_sequence: Option<u64>,
46 },
47 ResumeAttachedSession,
50 AttachParallelControlSessionV2 {
51 control_session_id: ControlSessionId,
52 #[serde(default)]
56 last_sequences: BTreeMap<SessionId, u64>,
57 },
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(untagged)]
66pub enum CreateBacktestSessionRequest {
67 V1(CreateBacktestSessionRequestV1),
68 V0(CreateSessionParams),
69}
70
71impl CreateBacktestSessionRequest {
72 pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
73 match self {
74 Self::V0(request) => CreateBacktestSessionRequestOptions {
75 request,
76 parallel: false,
77 },
78 Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
79 CreateBacktestSessionRequestOptions { request, parallel }
80 }
81 }
82 }
83
84 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
85 let options = self.into_request_options();
86 (options.request, options.parallel)
87 }
88}
89
90impl From<CreateSessionParams> for CreateBacktestSessionRequest {
91 fn from(value: CreateSessionParams) -> Self {
92 Self::V0(value)
93 }
94}
95
96impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
97 fn from(value: CreateBacktestSessionRequestV1) -> Self {
98 Self::V1(value)
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct CreateBacktestSessionRequestV1 {
105 #[serde(flatten)]
106 pub request: CreateSessionParams,
107 pub parallel: bool,
108}
109
110#[derive(Debug, Clone)]
111pub struct CreateBacktestSessionRequestOptions {
112 pub request: CreateSessionParams,
113 pub parallel: bool,
114}
115
116#[derive(Debug, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct ContinueSessionRequestV1 {
119 pub session_id: SessionId,
120 pub request: ContinueParams,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct ContinueToSessionRequestV1 {
126 pub session_id: SessionId,
127 pub request: ContinueToParams,
128}
129
130#[derive(Debug, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct CloseSessionRequestV1 {
133 pub session_id: SessionId,
134}
135
136#[serde_with::serde_as]
143#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
144#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
145pub enum DiscoveryFilter {
146 ProgramExecuted(#[serde_as(as = "serde_with::DisplayFromStr")] Address),
148}
149
150pub struct TxMatchContext<'a> {
155 pub invoked_programs: &'a HashSet<Address>,
157}
158
159impl DiscoveryFilter {
160 pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
163 match self {
164 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
173#[serde(try_from = "[String; 2]", into = "[String; 2]")]
174pub struct MintPair([Address; 2]);
175
176impl MintPair {
177 pub fn new(one: Address, other: Address) -> Self {
178 Self(if one <= other {
179 [one, other]
180 } else {
181 [other, one]
182 })
183 }
184
185 pub fn mints(&self) -> (Address, Address) {
186 (self.0[0], self.0[1])
187 }
188}
189
190#[derive(Debug, thiserror::Error)]
191#[error("invalid mint {mint} in pair")]
192pub struct ParseMintPairError {
193 mint: String,
194 source: ParseAddressError,
195}
196
197impl TryFrom<[String; 2]> for MintPair {
198 type Error = ParseMintPairError;
199
200 fn try_from([one, other]: [String; 2]) -> Result<Self, Self::Error> {
201 fn parse(mint: String) -> Result<Address, ParseMintPairError> {
202 Address::from_str(&mint).map_err(|source| ParseMintPairError { mint, source })
203 }
204 Ok(Self::new(parse(one)?, parse(other)?))
205 }
206}
207
208impl From<MintPair> for [String; 2] {
209 fn from(pair: MintPair) -> Self {
210 let (one, other) = pair.mints();
211 [one.to_string(), other.to_string()]
212 }
213}
214
215#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(rename_all = "camelCase")]
219pub struct RerouteFilter {
220 #[serde(default)]
222 pub pairs: BTreeSet<MintPair>,
223}
224
225impl RerouteFilter {
226 pub fn is_empty(&self) -> bool {
229 self.pairs.is_empty()
230 }
231
232 pub fn matches_mints(&self, input: &Address, output: &Address) -> bool {
233 self.pairs.contains(&MintPair::new(*input, *output))
234 }
235
236 pub fn mentions_mint(&self, mint: &Address) -> bool {
239 self.pairs.iter().any(|pair| {
240 let (one, other) = pair.mints();
241 one == *mint || other == *mint
242 })
243 }
244}
245
246#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250pub enum ActionKind {
251 #[default]
255 Simulate,
256 Send,
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(tag = "at", rename_all = "camelCase")]
266pub enum ActionAnchor {
267 BeforeSlot {
270 #[serde(default, with = "compressed")]
271 slots: Vec<u64>,
272 },
273 AfterSlot {
276 #[serde(default, with = "compressed")]
277 slots: Vec<u64>,
278 },
279 BeforeMatch { filter: DiscoveryFilter },
282 AfterMatch { filter: DiscoveryFilter },
285}
286
287const MAX_ACTION_TRANSACTIONS_BYTES: u64 = 64 * 1024 * 1024;
290
291mod compressed {
296 use std::io::Read;
297
298 use base64::Engine as _;
299 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
300
301 use super::{BASE64, MAX_ACTION_TRANSACTIONS_BYTES};
302
303 const COMPRESSION_LEVEL: i32 = 0;
305
306 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
307 where
308 T: Serialize,
309 S: Serializer,
310 {
311 let json = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
312 let compressed = zstd::stream::encode_all(json.as_slice(), COMPRESSION_LEVEL)
313 .map_err(serde::ser::Error::custom)?;
314 serializer.serialize_str(&BASE64.encode(compressed))
315 }
316
317 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
318 where
319 T: DeserializeOwned,
320 D: Deserializer<'de>,
321 {
322 let blob = String::deserialize(deserializer)?;
323 let compressed = BASE64
324 .decode(blob.as_bytes())
325 .map_err(serde::de::Error::custom)?;
326 let mut json = Vec::new();
327 zstd::stream::read::Decoder::new(compressed.as_slice())
328 .and_then(|d| Read::take(d, MAX_ACTION_TRANSACTIONS_BYTES).read_to_end(&mut json))
329 .map_err(serde::de::Error::custom)?;
330 serde_json::from_slice(&json).map_err(serde::de::Error::custom)
331 }
332}
333
334#[serde_with::serde_as]
342#[derive(Debug, Clone, Serialize, Deserialize)]
343#[serde(rename_all = "camelCase")]
344pub struct ScheduledAction {
345 pub anchor: ActionAnchor,
346 #[serde(default)]
348 pub kind: ActionKind,
349 #[serde(default, with = "compressed")]
358 pub transactions: Vec<String>,
359 #[serde(default)]
364 pub account_overrides: AccountModifications,
365 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
375 pub feeds_reroute: bool,
376 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
380 #[serde(default)]
381 pub return_accounts: Vec<Address>,
382 #[serde(default)]
384 pub label: Option<String>,
385}
386
387impl ScheduledAction {
388 pub fn is_computed_override(&self) -> bool {
391 self.feeds_reroute && !self.transactions.is_empty()
392 }
393}
394
395#[derive(Debug, Clone)]
397enum OverrideContribution {
398 Static(HashMap<Address, AccountSharedData>),
399 Pending,
400 Resolved(HashMap<Address, AccountSharedData>),
401 Failed,
402}
403
404#[derive(Debug, Clone, Default)]
405struct OverrideScheduleInner {
406 entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>,
407 keys: HashSet<Address>,
411}
412
413impl OverrideScheduleInner {
414 fn new(entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>) -> Self {
415 let keys = entries
416 .values()
417 .flatten()
418 .filter_map(|(_, contribution)| match contribution {
419 OverrideContribution::Static(accounts)
420 | OverrideContribution::Resolved(accounts) => Some(accounts.keys().copied()),
421 OverrideContribution::Pending | OverrideContribution::Failed => None,
422 })
423 .flatten()
424 .collect();
425 Self { entries, keys }
426 }
427}
428
429#[derive(Debug, Clone, Default)]
432pub struct OverrideSchedule {
433 inner: Arc<RwLock<OverrideScheduleInner>>,
434}
435
436impl From<BTreeMap<u64, HashMap<Address, AccountSharedData>>> for OverrideSchedule {
437 fn from(entries: BTreeMap<u64, HashMap<Address, AccountSharedData>>) -> Self {
438 Self {
439 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(
440 entries
441 .into_iter()
442 .map(|(slot, accounts)| {
443 (slot, vec![(0, OverrideContribution::Static(accounts))])
444 })
445 .collect(),
446 ))),
447 }
448 }
449}
450
451impl OverrideSchedule {
452 pub fn is_empty(&self) -> bool {
453 self.inner.read().entries.is_empty()
454 }
455
456 pub fn slots(&self) -> impl Iterator<Item = u64> + '_ {
458 self.inner
459 .read()
460 .entries
461 .keys()
462 .copied()
463 .collect::<Vec<_>>()
464 .into_iter()
465 }
466
467 pub fn active_at(&self, slot: u64) -> HashMap<Address, AccountSharedData> {
469 self.inner.read().entries.range(..=slot).fold(
470 HashMap::new(),
471 |mut active, (_, contributions)| {
472 active.extend(fold_contributions(contributions));
473 active
474 },
475 )
476 }
477
478 pub fn active_for(&self, key: &Address, slot: u64) -> Option<AccountSharedData> {
479 let inner = self.inner.read();
480 if !inner.keys.contains(key) {
481 return None;
482 }
483 inner
484 .entries
485 .range(..=slot)
486 .rev()
487 .find_map(|(_, contributions)| {
488 contributions
489 .iter()
490 .rev()
491 .find_map(|(_, contribution)| match contribution {
492 OverrideContribution::Static(accounts)
493 | OverrideContribution::Resolved(accounts) => accounts.get(key).cloned(),
494 OverrideContribution::Pending | OverrideContribution::Failed => None,
495 })
496 })
497 }
498
499 pub fn newly_active(
501 &self,
502 after: Option<u64>,
503 up_to: u64,
504 ) -> Vec<(Address, AccountSharedData)> {
505 let lower = after.map_or(std::ops::Bound::Unbounded, std::ops::Bound::Excluded);
506 self.inner
507 .read()
508 .entries
509 .range((lower, std::ops::Bound::Included(up_to)))
510 .flat_map(|(_, contributions)| fold_contributions(contributions))
511 .collect::<HashMap<_, _>>()
512 .into_iter()
513 .collect()
514 }
515
516 pub fn resolve(
519 &self,
520 slot: u64,
521 action_index: u32,
522 accounts: Option<HashMap<Address, AccountSharedData>>,
523 ) -> Result<(), String> {
524 let mut inner = self.inner.write();
525 let OverrideScheduleInner { entries, keys } = &mut *inner;
526 let contribution = entries
527 .get_mut(&slot)
528 .and_then(|contributions| {
529 contributions
530 .iter_mut()
531 .find(|(index, _)| *index == action_index)
532 .map(|(_, contribution)| contribution)
533 })
534 .ok_or_else(|| {
535 format!("override action {action_index} has no contribution at slot {slot}")
536 })?;
537 if !matches!(contribution, OverrideContribution::Pending) {
538 return Err(format!(
539 "override action {action_index} at slot {slot} is not pending"
540 ));
541 }
542 *contribution = match accounts {
543 Some(accounts) => {
544 keys.extend(accounts.keys().copied());
545 OverrideContribution::Resolved(accounts)
546 }
547 None => OverrideContribution::Failed,
548 };
549 Ok(())
550 }
551}
552
553fn fold_contributions(
554 contributions: &[(u32, OverrideContribution)],
555) -> HashMap<Address, AccountSharedData> {
556 let mut accounts = HashMap::new();
557 for (_, contribution) in contributions {
558 match contribution {
559 OverrideContribution::Static(next) | OverrideContribution::Resolved(next) => {
560 accounts.extend(next.iter().map(|(key, account)| (*key, account.clone())));
561 }
562 OverrideContribution::Pending | OverrideContribution::Failed => {}
563 }
564 }
565 accounts
566}
567
568fn decode_override_actions(actions: &[ScheduledAction]) -> Result<OverrideSchedule, String> {
577 let mut entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>> = BTreeMap::new();
578 for (action_index, action) in actions.iter().enumerate().filter(|(_, action)| {
579 action.feeds_reroute
580 || (action.transactions.is_empty() && !action.account_overrides.0.is_empty())
581 }) {
582 if action.kind == ActionKind::Send {
584 return Err("account overrides are not valid for kind: send".to_string());
585 }
586 if action.transactions.is_empty() && action.account_overrides.0.is_empty() {
587 return Err(
588 "feedsReroute needs something to feed: transactions to simulate, or \
589 accountOverrides"
590 .to_string(),
591 );
592 }
593 let ActionAnchor::BeforeSlot { slots } = &action.anchor else {
594 return Err("scheduled account overrides require a beforeSlot anchor".to_string());
595 };
596 let accounts = decode_override_accounts(&action.account_overrides)?;
597 let contribution = if action.transactions.is_empty() {
598 OverrideContribution::Static(accounts)
599 } else {
600 OverrideContribution::Pending
601 };
602 for slot in slots.iter().copied().collect::<BTreeSet<_>>() {
605 entries
606 .entry(slot)
607 .or_default()
608 .push((action_index as u32, contribution.clone()));
609 }
610 }
611 Ok(OverrideSchedule {
612 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(entries))),
613 })
614}
615
616#[allow(
620 deprecated,
621 reason = "`PROGRAM_OWNERS` remains the canonical loader-owner list"
622)]
623pub fn decode_account_modifications(
624 modifications: &AccountModifications,
625) -> Result<HashMap<Address, AccountSharedData>, String> {
626 modifications
627 .0
628 .iter()
629 .map(|(address, data)| {
630 let account = data
631 .to_account()
632 .map_err(|error| format!("invalid account data for {address}: {error}"))?;
633 if account.executable || solana_account::PROGRAM_OWNERS.contains(&account.owner) {
634 return Err(format!(
635 "overriding executable/program accounts is not supported (account {address})"
636 ));
637 }
638 Ok((*address, AccountSharedData::from(account)))
639 })
640 .collect()
641}
642
643fn decode_override_accounts(
646 modifications: &AccountModifications,
647) -> Result<HashMap<Address, AccountSharedData>, String> {
648 let accounts = decode_account_modifications(modifications)?;
649 if let Some(address) = modifications
650 .0
651 .iter()
652 .find_map(|(address, data)| TOKEN_PROGRAM_IDS.contains(&data.owner).then_some(address))
653 {
654 return Err(format!(
655 "override for {address}: token accounts cannot be overridden \
656 (prepare-stage reads would desync from simulation)"
657 ));
658 }
659 Ok(accounts)
660}
661
662pub const MAX_DISCONNECT_TIMEOUT_SECS: u16 = 900;
666
667#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
670#[serde(rename_all = "lowercase", try_from = "String")]
671pub enum SwapVenue {
672 Jupiter,
673 Okx,
674 Titan,
675 Dflow,
676}
677
678impl SwapVenue {
679 pub const fn as_str(self) -> &'static str {
681 match self {
682 Self::Jupiter => "jupiter",
683 Self::Okx => "okx",
684 Self::Titan => "titan",
685 Self::Dflow => "dflow",
686 }
687 }
688}
689
690impl std::fmt::Display for SwapVenue {
691 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
692 f.write_str(self.as_str())
693 }
694}
695
696#[derive(Debug, thiserror::Error)]
697pub enum ParseVenueError {
698 #[error("unknown swap venue {0:?}; expected one of jupiter, okx, titan, dflow")]
699 Unknown(String),
700 #[error("name at least one venue (jupiter, okx, titan, dflow)")]
702 NoVenues,
703}
704
705impl TryFrom<String> for SwapVenue {
706 type Error = ParseVenueError;
707
708 fn try_from(value: String) -> Result<Self, Self::Error> {
709 value.parse()
710 }
711}
712
713impl std::str::FromStr for SwapVenue {
714 type Err = ParseVenueError;
715
716 fn from_str(s: &str) -> Result<Self, Self::Err> {
717 match s.trim().to_ascii_lowercase().as_str() {
718 "jupiter" => Ok(Self::Jupiter),
719 "okx" => Ok(Self::Okx),
720 "titan" => Ok(Self::Titan),
721 "dflow" => Ok(Self::Dflow),
722 _ => Err(ParseVenueError::Unknown(s.to_string())),
723 }
724 }
725}
726
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
731#[serde(transparent)]
732pub struct RerouteVenues(BTreeSet<SwapVenue>);
733
734impl Default for RerouteVenues {
735 fn default() -> Self {
736 Self([SwapVenue::Jupiter].into_iter().collect())
737 }
738}
739
740impl RerouteVenues {
741 pub fn new(venues: impl IntoIterator<Item = SwapVenue>) -> Self {
742 Self(venues.into_iter().collect())
743 }
744
745 pub fn contains(&self, venue: SwapVenue) -> bool {
746 self.0.contains(&venue)
747 }
748
749 pub fn is_empty(&self) -> bool {
750 self.0.is_empty()
751 }
752}
753
754impl std::fmt::Display for RerouteVenues {
755 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
756 let mut venues = self.0.iter();
757 if let Some(first) = venues.next() {
758 write!(f, "{first}")?;
759 }
760 venues.try_for_each(|venue| write!(f, ",{venue}"))
761 }
762}
763
764impl std::str::FromStr for RerouteVenues {
765 type Err = ParseVenueError;
766
767 fn from_str(s: &str) -> Result<Self, Self::Err> {
768 let venues: BTreeSet<SwapVenue> = s
769 .split(',')
770 .filter(|venue| !venue.trim().is_empty())
771 .map(str::parse)
772 .collect::<Result<_, _>>()?;
773 if venues.is_empty() {
774 return Err(ParseVenueError::NoVenues);
775 }
776 Ok(Self(venues))
777 }
778}
779
780#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
788#[serde(rename_all = "camelCase")]
789pub struct RerouteParams {
790 #[serde(default)]
791 pub venues: RerouteVenues,
792 #[serde(default, skip_serializing_if = "Option::is_none")]
795 pub filter: Option<RerouteFilter>,
796 #[serde(default)]
803 pub detect_failed_l1_swaps: bool,
804 #[serde(default)]
807 pub circular_arbs: bool,
808}
809
810#[serde_with::serde_as]
812#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
813#[serde(rename_all = "camelCase")]
814pub struct CreateSessionParams {
815 pub start_slot: u64,
817 pub end_slot: u64,
819 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
820 #[serde(default)]
821 #[builder(default)]
822 pub signer_filter: BTreeSet<Address>,
824 #[serde(default)]
827 #[builder(default)]
828 pub send_summary: bool,
829 #[serde(default, skip_serializing_if = "Option::is_none")]
831 pub reroute: Option<RerouteParams>,
832 #[serde(default)]
835 pub capacity_wait_timeout_secs: Option<u16>,
836 #[serde(default)]
840 pub disconnect_timeout_secs: Option<u16>,
841 #[serde(default)]
846 pub extra_compute_units: Option<u32>,
847 #[serde(default, skip_serializing_if = "Vec::is_empty")]
854 #[builder(default)]
855 pub discoveries: Vec<DiscoveryFilter>,
856 #[serde(default, with = "compressed", skip_serializing_if = "Vec::is_empty")]
865 #[builder(default)]
866 pub actions: Vec<ScheduledAction>,
867 #[serde(default)]
870 #[builder(default)]
871 pub replay_account_state: bool,
872}
873
874pub mod replay_support {
878 use super::{ActionKind, CreateSessionParams};
879
880 pub const NO_TRANSACTIONS: &str =
882 "Create the session without replayAccountState to execute and persist transactions.";
883
884 pub const SEND_TRANSACTION: &str = "Use simulateTransaction to run it against the replayed state, or create the session \
885 without replayAccountState.";
886
887 pub const SEND_ACTION: &str = "Use kind: simulate, which runs against the replayed state without committing, post state \
888 directly with an action carrying accountOverrides and no transactions, or create the \
889 session without replayAccountState.";
890
891 pub fn unsupported(operation: &str, remedy: &str) -> String {
893 format!(
894 "{operation} is not supported while account state replay is enabled: replay \
895 reconstructs account state from the recorded delta bundle instead of executing \
896 transactions, so this session never executes or persists any. {remedy}"
897 )
898 }
899
900 pub fn send_action_conflict(params: &CreateSessionParams) -> Option<String> {
903 (params.replay_account_state
904 && params
905 .actions
906 .iter()
907 .any(|action| action.kind == ActionKind::Send))
908 .then(|| unsupported("A scheduled action with kind: send", SEND_ACTION))
909 }
910}
911
912impl CreateSessionParams {
913 pub fn requires_sidecar(&self) -> bool {
915 self.reroute_order_flow()
916 }
917
918 pub fn reroute_order_flow(&self) -> bool {
919 self.reroute.is_some()
920 }
921
922 pub fn override_schedule(&self) -> Result<OverrideSchedule, String> {
925 decode_override_actions(&self.actions)
926 }
927
928 pub fn reroute_filter(&self) -> Option<&RerouteFilter> {
930 self.reroute.as_ref()?.filter.as_ref()
931 }
932}
933
934#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
937#[serde(rename_all = "kebab-case")]
938pub enum FailFastDivergenceKind {
939 #[default]
942 AnyNonBenign,
943 Tracked,
946}
947
948impl FailFastDivergenceKind {
949 pub fn as_str(self) -> &'static str {
952 match self {
953 Self::AnyNonBenign => "any-non-benign",
954 Self::Tracked => "tracked",
955 }
956 }
957
958 pub fn from_str_opt(value: &str) -> Option<Self> {
960 match value {
961 "any-non-benign" => Some(Self::AnyNonBenign),
962 "tracked" => Some(Self::Tracked),
963 _ => None,
964 }
965 }
966}
967
968#[serde_with::serde_as]
970#[derive(Debug, Clone, Serialize, Deserialize, Default)]
971pub struct AccountModifications(
972 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
973 #[serde(default)]
974 pub BTreeMap<Address, AccountData>,
975);
976
977const TOKEN_PROGRAM_IDS: [Address; 2] = [spl_token_interface::ID, spl_token_2022_interface::ID];
978
979impl From<BTreeMap<Address, AccountData>> for AccountModifications {
980 fn from(value: BTreeMap<Address, AccountData>) -> Self {
981 Self(value)
982 }
983}
984
985#[serde_with::serde_as]
987#[derive(Debug, Serialize, Deserialize)]
988#[serde(rename_all = "camelCase")]
989pub struct ContinueParams {
990 #[serde(default = "ContinueParams::default_advance_count")]
991 pub advance_count: u64,
993 #[serde(default)]
994 pub transactions: Vec<String>,
996 #[serde(default)]
997 pub modify_account_states: AccountModifications,
999}
1000
1001impl Default for ContinueParams {
1002 fn default() -> Self {
1003 Self {
1004 advance_count: Self::default_advance_count(),
1005 transactions: Vec::new(),
1006 modify_account_states: AccountModifications(BTreeMap::new()),
1007 }
1008 }
1009}
1010
1011impl ContinueParams {
1012 pub fn default_advance_count() -> u64 {
1013 1
1014 }
1015}
1016
1017#[derive(Debug, Clone, Serialize, Deserialize)]
1023#[serde(rename_all = "camelCase")]
1024pub struct PausedEvent {
1025 pub slot: u64,
1026 #[serde(default, skip_serializing_if = "Option::is_none")]
1027 pub batch_index: Option<u32>,
1028}
1029
1030#[serde_with::serde_as]
1039#[derive(Debug, Clone, Serialize, Deserialize)]
1040#[serde(rename_all = "camelCase")]
1041pub struct DiscoveryBatchEvent {
1042 pub slot: u64,
1043 pub batch_index: u32,
1044 pub matched: Vec<DiscoveryFilter>,
1046 pub transactions: Vec<EncodedBinary>,
1050}
1051
1052#[derive(Debug, Clone, Serialize, Deserialize)]
1054#[serde(rename_all = "camelCase")]
1055pub struct ContinueToParams {
1056 pub slot: u64,
1058 #[serde(default)]
1064 pub batch_index: Option<u32>,
1065}
1066
1067#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1069#[serde(rename_all = "lowercase")]
1070pub enum BinaryEncoding {
1071 Base64,
1072}
1073
1074impl BinaryEncoding {
1075 pub fn encode(self, bytes: &[u8]) -> String {
1076 match self {
1077 Self::Base64 => BASE64.encode(bytes),
1078 }
1079 }
1080
1081 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
1082 match self {
1083 Self::Base64 => BASE64.decode(data),
1084 }
1085 }
1086}
1087
1088#[derive(Debug, Clone, Serialize, Deserialize)]
1090#[serde(rename_all = "camelCase")]
1091pub struct EncodedBinary {
1092 pub data: String,
1094 pub encoding: BinaryEncoding,
1096}
1097
1098impl EncodedBinary {
1099 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
1100 Self { data, encoding }
1101 }
1102
1103 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
1104 Self {
1105 data: encoding.encode(bytes),
1106 encoding,
1107 }
1108 }
1109
1110 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
1111 self.encoding.decode(&self.data)
1112 }
1113}
1114
1115#[serde_with::serde_as]
1117#[derive(Debug, Clone, Serialize, Deserialize)]
1118#[serde(rename_all = "camelCase")]
1119pub struct AccountData {
1120 pub data: EncodedBinary,
1122 pub executable: bool,
1124 pub lamports: u64,
1126 #[serde_as(as = "serde_with::DisplayFromStr")]
1127 pub owner: Address,
1129 pub space: u64,
1131}
1132
1133impl AccountData {
1134 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
1135 Ok(solana_account::Account {
1136 data: self.data.decode()?,
1137 lamports: self.lamports,
1138 owner: self.owner,
1139 executable: self.executable,
1140 rent_epoch: 0,
1141 })
1142 }
1143}
1144
1145#[derive(Debug, Clone, Serialize, Deserialize)]
1147#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1148pub enum BacktestResponse {
1149 SessionCreated {
1150 session_id: SessionId,
1151 rpc_endpoint: String,
1152 #[serde(default, skip_serializing_if = "Option::is_none")]
1153 task_id: Option<String>,
1154 },
1155 SessionAttached {
1156 session_id: SessionId,
1157 rpc_endpoint: String,
1158 #[serde(default, skip_serializing_if = "Option::is_none")]
1159 task_id: Option<String>,
1160 },
1161 SessionsCreatedV2 {
1162 control_session_id: ControlSessionId,
1163 session_ids: Vec<SessionId>,
1164 #[serde(default)]
1165 task_ids: Vec<Option<String>>,
1166 #[serde(default)]
1171 start_slots: Vec<u64>,
1172 #[serde(default)]
1173 end_slots: Vec<u64>,
1174 },
1175 ParallelSessionAttachedV2 {
1176 control_session_id: ControlSessionId,
1177 session_ids: Vec<SessionId>,
1178 #[serde(default)]
1179 task_ids: Vec<Option<String>>,
1180 },
1181 ReadyForContinue,
1182 SlotNotification(u64),
1183 Paused(PausedEvent),
1184 DiscoveryBatch(DiscoveryBatchEvent),
1185 Error(BacktestError),
1186 Success,
1187 Completed {
1188 #[serde(skip_serializing_if = "Option::is_none")]
1192 summary: Option<SessionSummary>,
1193 },
1194 Status {
1195 status: BacktestStatus,
1196 },
1197 SessionEventV2 {
1198 session_id: SessionId,
1199 seq_id: u64,
1200 event: SessionEventKind,
1201 },
1202}
1203
1204impl BacktestResponse {
1205 pub fn is_completed(&self) -> bool {
1206 matches!(self, BacktestResponse::Completed { .. })
1207 }
1208
1209 pub fn is_terminal(&self) -> bool {
1210 match self {
1211 BacktestResponse::Completed { .. } => true,
1212 BacktestResponse::Error(e) => e.is_terminal(),
1213 _ => false,
1214 }
1215 }
1216}
1217
1218impl From<BacktestStatus> for BacktestResponse {
1219 fn from(status: BacktestStatus) -> Self {
1220 Self::Status { status }
1221 }
1222}
1223
1224impl From<String> for BacktestResponse {
1225 fn from(message: String) -> Self {
1226 BacktestError::Internal { error: message }.into()
1227 }
1228}
1229
1230impl From<&str> for BacktestResponse {
1231 fn from(message: &str) -> Self {
1232 BacktestError::Internal {
1233 error: message.to_string(),
1234 }
1235 .into()
1236 }
1237}
1238
1239#[derive(Debug, Clone, Serialize, Deserialize)]
1240#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1241pub enum SessionEventKind {
1242 ReadyForContinue,
1243 SlotNotification(u64),
1244 Paused(PausedEvent),
1245 DiscoveryBatch(DiscoveryBatchEvent),
1246 Error(BacktestError),
1247 Success,
1248 Completed {
1249 #[serde(skip_serializing_if = "Option::is_none")]
1250 summary: Option<SessionSummary>,
1251 },
1252 Status {
1253 status: BacktestStatus,
1254 },
1255}
1256
1257impl SessionEventKind {
1258 pub fn is_terminal(&self) -> bool {
1259 match self {
1260 Self::Completed { .. } => true,
1261 Self::Error(e) => e.is_terminal(),
1262 _ => false,
1263 }
1264 }
1265}
1266
1267#[derive(Debug, Clone, Serialize, Deserialize)]
1270#[serde(rename_all = "camelCase")]
1271pub struct SequencedResponse {
1272 pub seq_id: u64,
1273 #[serde(flatten)]
1274 pub response: BacktestResponse,
1275}
1276
1277#[derive(Debug, Clone, Serialize, Deserialize)]
1279#[serde(rename_all = "camelCase")]
1280pub enum BacktestStatus {
1281 StartingRuntime,
1283 DecodedTransactions,
1284 AppliedAccountModifications,
1285 ReadyToExecuteUserTransactions,
1286 ExecutedUserTransactions,
1287 ExecutingBlockTransactions,
1288 ExecutedBlockTransactions,
1289 ProgramAccountsLoaded,
1290}
1291
1292impl std::fmt::Display for BacktestStatus {
1293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1294 let s = match self {
1295 Self::StartingRuntime => "starting runtime",
1296 Self::DecodedTransactions => "decoded transactions",
1297 Self::AppliedAccountModifications => "applied account modifications",
1298 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
1299 Self::ExecutedUserTransactions => "executed user transactions",
1300 Self::ExecutingBlockTransactions => "executing block transactions",
1301 Self::ExecutedBlockTransactions => "executed block transactions",
1302 Self::ProgramAccountsLoaded => "program accounts loaded",
1303 };
1304 f.write_str(s)
1305 }
1306}
1307
1308#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1312#[serde(rename_all = "camelCase")]
1313pub struct RerouteStatsReport {
1314 #[serde(default)]
1316 pub swaps_detected: u64,
1317 #[serde(default)]
1321 pub swaps_filtered: u64,
1322 #[serde(default)]
1324 pub swaps_rerouted: u64,
1325 #[serde(default)]
1327 pub swaps_simulated: u64,
1328 #[serde(default)]
1330 pub swaps_succeeded: u64,
1331 #[serde(default)]
1333 pub requote_failures: u64,
1334 #[serde(default)]
1336 pub override_setup_failures: u64,
1337 #[serde(default)]
1339 pub metis_unavailable_blocks: u64,
1340 #[serde(default)]
1344 pub quote_attempts_unavailable: u64,
1345}
1346
1347#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1349#[serde(rename_all = "camelCase")]
1350pub struct SessionSummary {
1351 pub correct_simulation: usize,
1354 pub incorrect_simulation: usize,
1357 pub execution_errors: usize,
1359 pub balance_diff: usize,
1361 pub log_diff: usize,
1363 #[serde(default)]
1366 pub unsanitizable: usize,
1367 #[serde(default, skip_serializing_if = "Option::is_none")]
1370 pub reroute_stats: Option<Box<RerouteStatsReport>>,
1371}
1372
1373impl SessionSummary {
1374 pub fn has_deviations(&self) -> bool {
1376 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
1377 }
1378
1379 pub fn total_transactions(&self) -> usize {
1381 self.correct_simulation
1382 + self.incorrect_simulation
1383 + self.execution_errors
1384 + self.balance_diff
1385 + self.log_diff
1386 }
1387
1388 pub fn deviation_details(&self, balance_label: &str, log_label: &str) -> String {
1393 format!(
1394 "{} incorrect simulations, {} execution errors, {} {balance_label}, {} {log_label}",
1395 self.incorrect_simulation, self.execution_errors, self.balance_diff, self.log_diff,
1396 )
1397 }
1398}
1399
1400impl std::fmt::Display for SessionSummary {
1401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1402 let total = self.total_transactions();
1403 write!(
1404 f,
1405 "Session summary: {total} transactions\n\
1406 \x20 - {} correct simulation\n\
1407 \x20 - {} incorrect simulation\n\
1408 \x20 - {} execution errors\n\
1409 \x20 - {} unsanitizable\n\
1410 \x20 - {} balance diffs\n\
1411 \x20 - {} log diffs",
1412 self.correct_simulation,
1413 self.incorrect_simulation,
1414 self.execution_errors,
1415 self.unsanitizable,
1416 self.balance_diff,
1417 self.log_diff,
1418 )
1419 }
1420}
1421
1422#[derive(Debug, Clone, Serialize, Deserialize)]
1424#[serde(rename_all = "camelCase")]
1425pub enum BacktestError {
1426 InvalidTransactionEncoding {
1427 index: usize,
1428 error: String,
1429 },
1430 InvalidTransactionFormat {
1431 index: usize,
1432 error: String,
1433 },
1434 InvalidAccountEncoding {
1435 address: String,
1436 encoding: BinaryEncoding,
1437 error: String,
1438 },
1439 InvalidAccountOwner {
1440 address: String,
1441 error: String,
1442 },
1443 InvalidAccountPubkey {
1444 address: String,
1445 error: String,
1446 },
1447 NoMoreBlocks,
1448 BundleNotFound {
1449 start_slot: u64,
1450 end_slot: u64,
1451 },
1452 AdvanceSlotFailed {
1453 slot: u64,
1454 error: String,
1455 },
1456 FinalizeSlotFailed {
1457 slot: u64,
1458 error: String,
1459 },
1460 InvalidRequest {
1461 error: String,
1462 },
1463 Internal {
1464 error: String,
1465 },
1466 InvalidBlockhashFormat {
1467 slot: u64,
1468 error: String,
1469 },
1470 InitializingSysvarsFailed {
1471 slot: u64,
1472 error: String,
1473 },
1474 ClerkError {
1475 error: String,
1476 },
1477 SimulationError {
1478 error: String,
1479 },
1480 SessionNotFound {
1481 session_id: String,
1482 },
1483 SessionOwnerMismatch,
1484 SessionOwnershipBusy {
1489 reason: String,
1490 },
1491 NoProducedBlocksInRange {
1497 start_slot: u64,
1498 end_slot: u64,
1499 },
1500}
1501
1502impl BacktestError {
1503 pub fn is_terminal(&self) -> bool {
1507 matches!(
1508 self,
1509 BacktestError::NoMoreBlocks
1510 | BacktestError::BundleNotFound { .. }
1511 | BacktestError::AdvanceSlotFailed { .. }
1512 | BacktestError::FinalizeSlotFailed { .. }
1513 | BacktestError::Internal { .. }
1514 | BacktestError::NoProducedBlocksInRange { .. }
1515 )
1516 }
1517}
1518
1519#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1521pub struct AvailableRange {
1522 pub bundle_start_slot: u64,
1523 pub bundle_start_slot_utc: Option<String>,
1524 pub max_bundle_end_slot: Option<u64>,
1525 pub max_bundle_end_slot_utc: Option<String>,
1526 pub max_bundle_size: Option<u64>,
1527 #[serde(default)]
1530 pub kind: Option<BundleKind>,
1531 #[serde(default)]
1534 pub scope: Option<RangeScope>,
1535}
1536
1537#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1539#[serde(rename_all = "snake_case")]
1540pub enum RangeScope {
1541 Global,
1543 User,
1545}
1546
1547impl RangeScope {
1548 pub fn as_str(self) -> &'static str {
1550 match self {
1551 Self::Global => "global",
1552 Self::User => "user",
1553 }
1554 }
1555}
1556
1557#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1559#[serde(rename_all = "snake_case")]
1560pub enum BundleKind {
1561 #[default]
1563 Blocks,
1564 AccountState,
1568}
1569
1570pub const REPLAY_RANGE_ADVICE: &str = "replayed state is resolved by the recording that begins \
1574 where the request does, so request a range that begins where one was recorded and ends \
1575 within it";
1576
1577impl BundleKind {
1578 fn coverage_noun(self) -> &'static str {
1580 match self {
1581 Self::Blocks => "bundle range",
1582 Self::AccountState => "account-state bundle you own",
1583 }
1584 }
1585
1586 fn coverage_nouns(self) -> &'static str {
1588 match self {
1589 Self::Blocks => "bundle ranges",
1590 Self::AccountState => "account-state bundles you own",
1591 }
1592 }
1593
1594 pub fn as_str(self) -> &'static str {
1596 match self {
1597 Self::Blocks => "blocks",
1598 Self::AccountState => "account_state",
1599 }
1600 }
1601
1602 pub fn from_str_opt(value: &str) -> Option<Self> {
1604 match value {
1605 "blocks" => Some(Self::Blocks),
1606 "account_state" => Some(Self::AccountState),
1607 _ => None,
1608 }
1609 }
1610}
1611
1612impl fmt::Display for BundleKind {
1615 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1616 let label = match self {
1617 Self::Blocks => "transactions",
1618 Self::AccountState => "account_state",
1619 };
1620 f.pad(label)
1621 }
1622}
1623
1624#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1628pub struct BundleBuildRequest {
1629 pub start_slot: u64,
1630 pub end_slot: u64,
1631 #[serde(default)]
1632 pub bundle_size: Option<u64>,
1633 #[serde(default)]
1636 pub idempotency_key: Option<String>,
1637 #[serde(default)]
1639 pub kind: BundleKind,
1640}
1641
1642#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1643#[serde(rename_all = "snake_case")]
1644pub enum BundleBuildStatus {
1645 Completed,
1647 Failed,
1649 InProgress,
1651 NeedsInvestigation,
1654}
1655
1656impl BundleBuildStatus {
1657 pub fn as_str(self) -> &'static str {
1659 match self {
1660 Self::Completed => "completed",
1661 Self::Failed => "failed",
1662 Self::InProgress => "in_progress",
1663 Self::NeedsInvestigation => "needs_investigation",
1664 }
1665 }
1666}
1667
1668#[derive(Debug, Clone, Serialize, Deserialize)]
1670pub struct BundleBuildStatusResponse {
1671 pub request_id: String,
1672 pub start_slot: u64,
1673 pub end_slot: u64,
1674 pub bundle_size: Option<u64>,
1675 pub status: BundleBuildStatus,
1676 pub created_at_unix_ms: u64,
1679 #[serde(default)]
1681 pub kind: BundleKind,
1682}
1683
1684#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1686pub struct CompletedBundle {
1687 pub start_slot: u64,
1688 pub end_slot: u64,
1689 pub kind: BundleKind,
1690 #[serde(default)]
1692 pub completed_at_unix_ms: Option<i64>,
1693 #[serde(default)]
1694 pub size_bytes: Option<i64>,
1695}
1696
1697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1699pub struct BundleDeleteRequest {
1700 pub start_slot: u64,
1701 pub end_slot: u64,
1702 #[serde(default)]
1704 pub kind: BundleKind,
1705}
1706
1707#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1709pub struct BundleDeleteResponse {
1710 pub start_slot: u64,
1711 pub end_slot: u64,
1712 pub kind: BundleKind,
1713}
1714
1715#[derive(Debug, Clone, Serialize, Deserialize)]
1717pub struct BundleBuildListResponse {
1718 pub builds: Vec<BundleBuildStatusResponse>,
1721 #[serde(default)]
1726 pub bundles: Option<Vec<CompletedBundle>>,
1727}
1728
1729#[derive(Debug, Clone, Serialize, Deserialize)]
1732pub struct SnapshotSlotResponse {
1733 pub snapshot_slot: u64,
1734}
1735
1736pub fn split_range(
1759 ranges: &[AvailableRange],
1760 requested: SlotRange,
1761 kind: BundleKind,
1762) -> Result<Vec<(u64, u64)>, String> {
1763 let (requested_start, requested_end) = (requested.start(), requested.end());
1764
1765 let ends_by_start = ranges
1772 .iter()
1773 .filter(|r| r.kind.unwrap_or_default() == kind)
1774 .filter_map(|r| {
1775 r.max_bundle_end_slot
1776 .filter(|&end| end >= r.bundle_start_slot)
1777 .map(|end| (r.bundle_start_slot, end))
1778 })
1779 .fold(
1780 BTreeMap::<u64, BTreeSet<u64>>::new(),
1781 |mut ends_by_start, (start, end)| {
1782 ends_by_start.entry(start).or_default().insert(end);
1783 ends_by_start
1784 },
1785 );
1786
1787 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
1795 ends.iter()
1796 .next_back()
1797 .is_some_and(|&end| end >= requested_start)
1798 }) else {
1799 return Err(format!(
1800 "start_slot {requested_start} is not covered by any available {}",
1801 kind.coverage_noun()
1802 ));
1803 };
1804
1805 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
1811 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
1812 let mut best: Option<Vec<(u64, u64)>> = None;
1813 for &end in ends {
1814 let candidate = if end >= requested_end {
1815 Some(vec![(start, requested_end)])
1816 } else {
1817 best_from.get(&(end + 1)).map(|rest| {
1818 std::iter::once((start, end))
1819 .chain(rest.iter().copied())
1820 .collect()
1821 })
1822 };
1823 if let Some(candidate) = candidate
1824 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
1825 {
1826 best = Some(candidate);
1827 }
1828 }
1829 if let Some(best) = best {
1830 best_from.insert(start, best);
1831 }
1832 }
1833
1834 best_from.remove(&anchor_start).ok_or_else(|| {
1835 let mut covered_to = anchor_start.saturating_sub(1);
1839 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
1840 if start > covered_to.saturating_add(1) {
1841 break;
1842 }
1843 if let Some(&end) = ends.iter().next_back() {
1844 covered_to = covered_to.max(end);
1845 }
1846 }
1847 if covered_to < requested_end {
1848 format!("gap in coverage at slot {}", covered_to + 1)
1849 } else {
1850 format!(
1851 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available {}",
1852 kind.coverage_nouns()
1853 )
1854 }
1855 })
1856}
1857
1858impl From<BacktestError> for BacktestResponse {
1859 fn from(error: BacktestError) -> Self {
1860 Self::Error(error)
1861 }
1862}
1863
1864impl std::error::Error for BacktestError {}
1865
1866impl fmt::Display for BacktestError {
1867 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1868 match self {
1869 BacktestError::InvalidTransactionEncoding { index, error } => {
1870 write!(f, "invalid transaction encoding at index {index}: {error}")
1871 }
1872 BacktestError::InvalidTransactionFormat { index, error } => {
1873 write!(f, "invalid transaction format at index {index}: {error}")
1874 }
1875 BacktestError::InvalidAccountEncoding {
1876 address,
1877 encoding,
1878 error,
1879 } => write!(
1880 f,
1881 "invalid encoding for account {address} ({encoding:?}): {error}"
1882 ),
1883 BacktestError::InvalidAccountOwner { address, error } => {
1884 write!(f, "invalid owner for account {address}: {error}")
1885 }
1886 BacktestError::InvalidAccountPubkey { address, error } => {
1887 write!(f, "invalid account pubkey {address}: {error}")
1888 }
1889 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
1890 BacktestError::BundleNotFound {
1891 start_slot,
1892 end_slot,
1893 } => write!(
1894 f,
1895 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
1896 ),
1897 BacktestError::AdvanceSlotFailed { slot, error } => {
1898 write!(f, "failed to advance to slot {slot}: {error}")
1899 }
1900 BacktestError::FinalizeSlotFailed { slot, error } => {
1901 write!(f, "failed to finalize slot {slot}: {error}")
1902 }
1903 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
1904 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
1905 BacktestError::InvalidBlockhashFormat { slot, error } => {
1906 write!(f, "invalid blockhash at slot {slot}: {error}")
1907 }
1908 BacktestError::InitializingSysvarsFailed { slot, error } => {
1909 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
1910 }
1911 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
1912 BacktestError::SimulationError { error } => {
1913 write!(f, "simulation error: {error}")
1914 }
1915 BacktestError::SessionNotFound { session_id } => {
1916 write!(f, "session not found: {session_id}")
1917 }
1918 BacktestError::SessionOwnerMismatch => {
1919 write!(f, "session owner mismatch")
1920 }
1921 BacktestError::SessionOwnershipBusy { reason } => {
1922 write!(f, "session ownership busy: {reason}")
1923 }
1924 BacktestError::NoProducedBlocksInRange {
1925 start_slot,
1926 end_slot,
1927 } => write!(
1928 f,
1929 "no produced blocks in requested range {start_slot}..={end_slot}; \
1930 slot(s) may be skipped on-chain"
1931 ),
1932 }
1933 }
1934}
1935
1936#[cfg(test)]
1937mod tests {
1938 use rstest::rstest;
1939 use solana_account::ReadableAccount;
1940
1941 use super::*;
1942
1943 fn address(byte: u8) -> Address {
1944 Address::from([byte; 32])
1945 }
1946
1947 #[rstest]
1948 #[case::one("jupiter", Some(&[SwapVenue::Jupiter][..]))]
1949 #[case::several("jupiter,okx", Some(&[SwapVenue::Jupiter, SwapVenue::Okx][..]))]
1950 #[case::spaced_and_uppercased(" Jupiter , OKX ", Some(&[SwapVenue::Jupiter, SwapVenue::Okx][..]))]
1951 #[case::repeats_collapse("okx,okx", Some(&[SwapVenue::Okx][..]))]
1952 #[case::trailing_separator("okx,", Some(&[SwapVenue::Okx][..]))]
1953 #[case::unknown("jupiter,bogus", None)]
1954 #[case::empty("", None)]
1955 #[case::separators_only(",,", None)]
1956 fn reroute_venues_parse_the_shapes_a_flag_carries(
1957 #[case] input: &str,
1958 #[case] expected: Option<&[SwapVenue]>,
1959 ) {
1960 assert_eq!(
1961 input.parse::<RerouteVenues>().ok(),
1962 expected.map(|venues| RerouteVenues::new(venues.iter().copied()))
1963 );
1964 }
1965
1966 #[test]
1968 fn reroute_venues_round_trip_through_their_rendering() {
1969 let venues = RerouteVenues::new([SwapVenue::Dflow, SwapVenue::Jupiter]);
1970
1971 assert_eq!(venues.to_string(), "jupiter,dflow");
1972 assert_eq!(venues.to_string().parse::<RerouteVenues>().unwrap(), venues);
1973 assert_eq!(RerouteVenues::default().to_string(), "jupiter");
1974 }
1975
1976 #[test]
1979 fn venues_deserialize_through_the_same_parser_the_flag_uses() {
1980 let params: CreateSessionParams = serde_json::from_value(
1981 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {"venues": ["Jupiter", "OKX"]}}),
1982 )
1983 .expect("decode");
1984
1985 assert_eq!(
1986 params.reroute.expect("rerouting requested").venues,
1987 RerouteVenues::new([SwapVenue::Jupiter, SwapVenue::Okx])
1988 );
1989 }
1990
1991 #[test]
1994 fn params_without_the_new_reroute_fields_decode_to_the_narrow_defaults() {
1995 let params: CreateSessionParams = serde_json::from_value(
1996 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {}}),
1997 )
1998 .expect("decode");
1999
2000 let reroute = params.reroute.expect("rerouting requested");
2001 assert_eq!(reroute.venues, RerouteVenues::default());
2002 assert!(reroute.venues.contains(SwapVenue::Jupiter));
2003 assert!(!reroute.venues.contains(SwapVenue::Okx));
2004 assert!(!reroute.circular_arbs);
2005 assert!(!reroute.detect_failed_l1_swaps);
2006 }
2007
2008 #[test]
2010 fn a_request_without_reroute_reserves_no_sidecar() {
2011 let params: CreateSessionParams =
2012 serde_json::from_value(serde_json::json!({"startSlot": 1, "endSlot": 2}))
2013 .expect("decode");
2014
2015 assert!(!params.requires_sidecar());
2016 }
2017
2018 #[test]
2023 fn a_per_slot_action_schedule_compresses_across_actions() {
2024 let slots = 2_000u64;
2025 let actions = (0..slots)
2026 .map(|slot| ScheduledAction {
2027 anchor: ActionAnchor::AfterSlot { slots: vec![slot] },
2028 kind: ActionKind::Simulate,
2029 feeds_reroute: false,
2030 transactions: Vec::new(),
2031 account_overrides: AccountModifications(BTreeMap::from([(
2032 address(7),
2033 AccountData {
2034 data: EncodedBinary::new(
2036 BASE64.encode(
2037 [&slot.to_le_bytes()[..], &[0xAB; 2040][..]]
2038 .concat()
2039 .as_slice(),
2040 ),
2041 BinaryEncoding::Base64,
2042 ),
2043 executable: false,
2044 lamports: 15_144_966,
2045 owner: address(9),
2046 space: 2048,
2047 },
2048 )])),
2049 return_accounts: Vec::new(),
2050 label: None,
2051 })
2052 .collect::<Vec<_>>();
2053 let params = CreateSessionParams::builder()
2054 .start_slot(1)
2055 .end_slot(slots)
2056 .actions(actions.clone())
2057 .build();
2058
2059 let encoded = serde_json::to_string(¶ms).expect("serialize");
2060 let raw = serde_json::to_string(&actions).expect("serialize raw actions");
2061 assert!(
2062 encoded.len() * 8 < raw.len(),
2063 "expected the schedule to compress well; got {} bytes from {} raw",
2064 encoded.len(),
2065 raw.len()
2066 );
2067
2068 let decoded: CreateSessionParams = serde_json::from_str(&encoded).expect("round trip");
2069 assert_eq!(
2070 serde_json::to_string(&decoded.actions).expect("serialize decoded"),
2071 raw
2072 );
2073 }
2074
2075 #[test]
2078 fn an_empty_action_list_is_omitted_entirely() {
2079 let params = CreateSessionParams::builder()
2080 .start_slot(1)
2081 .end_slot(2)
2082 .build();
2083 let json = serde_json::to_value(¶ms).expect("serialize");
2084 assert!(json.get("actions").is_none(), "{json}");
2085 let decoded: CreateSessionParams = serde_json::from_value(json).expect("round trip");
2086 assert!(decoded.actions.is_empty());
2087 }
2088
2089 #[test]
2092 fn send_actions_conflict_with_replay_only_together() {
2093 let params = |replay, kind| {
2094 CreateSessionParams::builder()
2095 .start_slot(1)
2096 .end_slot(2)
2097 .replay_account_state(replay)
2098 .actions(vec![ScheduledAction {
2099 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2100 kind,
2101 feeds_reroute: false,
2102 transactions: vec!["dGVzdA==".to_string()],
2103 account_overrides: Default::default(),
2104 return_accounts: Default::default(),
2105 label: None,
2106 }])
2107 .build()
2108 };
2109 assert!(replay_support::send_action_conflict(¶ms(false, ActionKind::Send)).is_none());
2110 assert!(
2111 replay_support::send_action_conflict(¶ms(true, ActionKind::Simulate)).is_none()
2112 );
2113 let mut publishing = params(true, ActionKind::Simulate);
2114 publishing.actions[0].feeds_reroute = true;
2115 assert!(replay_support::send_action_conflict(&publishing).is_none());
2116
2117 let conflict = replay_support::send_action_conflict(¶ms(true, ActionKind::Send))
2118 .expect("send under replay conflicts");
2119 assert!(conflict.contains("kind: send"), "{conflict}");
2120 assert!(conflict.contains(replay_support::SEND_ACTION), "{conflict}");
2121 }
2122
2123 fn override_entry(owner: Address, executable: bool) -> AccountData {
2124 AccountData {
2125 data: EncodedBinary::from_bytes(&[1, 2, 3], BinaryEncoding::Base64),
2126 executable,
2127 lamports: 1,
2128 owner,
2129 space: 3,
2130 }
2131 }
2132
2133 fn before() -> ActionAnchor {
2134 ActionAnchor::BeforeSlot { slots: vec![1] }
2135 }
2136
2137 fn valid() -> ScheduledAction {
2138 override_action(before(), address(3), override_entry(address(4), false))
2139 }
2140
2141 fn override_action(
2142 anchor: ActionAnchor,
2143 account: Address,
2144 data: AccountData,
2145 ) -> ScheduledAction {
2146 ScheduledAction {
2147 anchor,
2148 kind: ActionKind::default(),
2149 transactions: Vec::new(),
2150 account_overrides: AccountModifications(BTreeMap::from([(account, data)])),
2151 return_accounts: Vec::new(),
2152 label: None,
2153 feeds_reroute: true,
2154 }
2155 }
2156
2157 #[test]
2158 fn override_action_round_trips() {
2159 let key = address(1);
2160 let entry = override_entry(address(2), false);
2161 let action = override_action(
2162 ActionAnchor::BeforeSlot { slots: vec![5, 9] },
2163 key,
2164 entry.clone(),
2165 );
2166 let json = serde_json::to_value(&action).unwrap();
2167 assert!(
2168 json["anchor"]["slots"].is_string(),
2169 "slots travel as a compressed blob: {json}"
2170 );
2171 let back: ScheduledAction = serde_json::from_value(json).unwrap();
2172 assert_eq!(back.anchor, ActionAnchor::BeforeSlot { slots: vec![5, 9] });
2173 assert!(back.feeds_reroute);
2174 assert!(!back.is_computed_override(), "no transactions to simulate");
2175 let survived = &back.account_overrides.0[&key];
2176 assert_eq!(survived.lamports, entry.lamports);
2177 assert_eq!(survived.data.data, entry.data.data);
2178 }
2179
2180 #[rstest]
2181 #[case::executable(override_action(before(), address(3), override_entry(address(10), true)))]
2182 #[case::program_owned(override_action(
2183 before(),
2184 address(3),
2185 override_entry(solana_account::PROGRAM_OWNERS[0], false),
2186 ))]
2187 #[case::token_owned(override_action(
2188 before(),
2189 address(3),
2190 override_entry(TOKEN_PROGRAM_IDS[0], false),
2191 ))]
2192 #[case::after_slot_anchor(override_action(
2193 ActionAnchor::AfterSlot { slots: vec![1] },
2194 address(3),
2195 override_entry(address(4), false),
2196 ))]
2197 #[case::nothing_to_post(ScheduledAction {
2198 account_overrides: AccountModifications::default(),
2199 ..valid()
2200 })]
2201 #[case::send(ScheduledAction {
2203 kind: ActionKind::Send,
2204 ..valid()
2205 })]
2206 #[allow(deprecated, reason = "exercise the loader-owner rejection path")]
2207 fn override_actions_reject_invalid_accounts_and_shapes(#[case] action: ScheduledAction) {
2208 assert!(decode_override_actions(&[action]).is_err());
2209 }
2210
2211 #[test]
2213 fn override_actions_accept_a_static_and_a_computed_shape() {
2214 assert!(decode_override_actions(&[valid()]).is_ok());
2215 assert!(
2218 decode_override_actions(&[ScheduledAction {
2219 transactions: vec!["dGVzdA==".to_string()],
2220 ..valid()
2221 }])
2222 .is_ok()
2223 );
2224 }
2225
2226 #[test]
2227 fn override_actions_merge_account_wise_later_wins() {
2228 let entry = |lamports| AccountData {
2229 lamports,
2230 ..override_entry(address(9), false)
2231 };
2232 let slot_action = |slots, account, lamports| {
2233 override_action(ActionAnchor::BeforeSlot { slots }, account, entry(lamports))
2234 };
2235 let actions = [
2236 slot_action(vec![5, 7], address(1), 1),
2237 slot_action(vec![5], address(2), 2),
2238 slot_action(vec![5], address(1), 3),
2239 ScheduledAction {
2240 anchor: ActionAnchor::AfterSlot { slots: vec![5] },
2241 kind: ActionKind::Simulate,
2242 feeds_reroute: false,
2243 transactions: vec!["dGVzdA==".to_string()],
2244 account_overrides: AccountModifications::default(),
2245 return_accounts: Vec::new(),
2246 label: None,
2247 },
2248 ];
2249 let schedule = decode_override_actions(&actions).unwrap();
2250 assert_eq!(schedule.slots().collect::<Vec<_>>(), vec![5, 7]);
2251 let lamports_at = |key, slot| {
2252 schedule
2253 .active_for(&key, slot)
2254 .map(|account| account.lamports())
2255 };
2256 assert_eq!(lamports_at(address(1), 5), Some(3));
2257 assert_eq!(lamports_at(address(2), 5), Some(2));
2258 assert_eq!(lamports_at(address(1), 7), Some(1));
2259 }
2260
2261 #[test]
2262 fn computed_override_resolution_preserves_action_order_and_failure() {
2263 let key = address(1);
2264 let static_action = |lamports| {
2265 override_action(
2266 ActionAnchor::BeforeSlot { slots: vec![5] },
2267 key,
2268 AccountData {
2269 lamports,
2270 ..override_entry(address(9), false)
2271 },
2272 )
2273 };
2274 let computed_action = || ScheduledAction {
2275 transactions: vec!["dGVzdA==".to_string()],
2276 ..static_action(2)
2277 };
2278
2279 let computed_last =
2280 decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2281 computed_last
2282 .resolve(5, 1, Some(HashMap::from([(key, shared_account(2))])))
2283 .unwrap();
2284 assert_eq!(
2285 computed_last.active_for(&key, 5).map(|a| a.lamports()),
2286 Some(2)
2287 );
2288
2289 let static_last = decode_override_actions(&[computed_action(), static_action(3)]).unwrap();
2290 static_last
2291 .resolve(5, 0, Some(HashMap::from([(key, shared_account(2))])))
2292 .unwrap();
2293 assert_eq!(
2294 static_last.active_for(&key, 5).map(|a| a.lamports()),
2295 Some(3)
2296 );
2297
2298 let failed = decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2299 failed.resolve(5, 1, None).unwrap();
2300 assert_eq!(failed.active_for(&key, 5).map(|a| a.lamports()), Some(1));
2301 assert!(failed.resolve(5, 1, None).is_err(), "resolving twice");
2302 }
2303
2304 fn shared_account(lamports: u64) -> AccountSharedData {
2305 AccountSharedData::from(solana_account::Account {
2306 lamports,
2307 data: vec![1],
2308 owner: address(9),
2309 executable: false,
2310 rent_epoch: 0,
2311 })
2312 }
2313
2314 fn carry_forward_schedule() -> (Address, Address, OverrideSchedule) {
2315 let (a, b) = (address(1), address(2));
2316 let schedule = OverrideSchedule::from(BTreeMap::from([
2317 (10, HashMap::from([(a, shared_account(1))])),
2318 (
2319 20,
2320 HashMap::from([(a, shared_account(2)), (b, shared_account(9))]),
2321 ),
2322 ]));
2323 (a, b, schedule)
2324 }
2325
2326 #[test]
2327 fn override_schedule_resolves_carry_forward_per_account() {
2328 let (a, b, schedule) = carry_forward_schedule();
2329
2330 assert!(schedule.active_for(&a, 9).is_none());
2331 let cases = [
2332 (9, None, None),
2333 (10, Some(1), None),
2334 (15, Some(1), None),
2335 (20, Some(2), Some(9)),
2336 (u64::MAX, Some(2), Some(9)),
2337 ];
2338 for (slot, want_a, want_b) in cases {
2339 assert_eq!(
2340 schedule
2341 .active_for(&a, slot)
2342 .map(|account| account.lamports()),
2343 want_a,
2344 "active_for a at {slot}"
2345 );
2346 let active = schedule.active_at(slot);
2347 assert_eq!(
2348 active.get(&a).map(ReadableAccount::lamports),
2349 want_a,
2350 "active_at a at {slot}"
2351 );
2352 assert_eq!(
2353 active.get(&b).map(ReadableAccount::lamports),
2354 want_b,
2355 "active_at b at {slot}"
2356 );
2357 }
2358 }
2359
2360 #[test]
2361 fn newly_active_yields_final_values_in_window() {
2362 let (a, b, schedule) = carry_forward_schedule();
2363
2364 let newly: HashMap<_, _> = schedule.newly_active(Some(10), 20).into_iter().collect();
2365 assert_eq!(newly.len(), 2);
2366 assert_eq!(newly[&a].lamports(), 2);
2367 assert_eq!(newly[&b].lamports(), 9);
2368 let spanning: HashMap<_, _> = schedule.newly_active(None, 20).into_iter().collect();
2369 assert_eq!(spanning[&a].lamports(), 2, "later slot wins");
2370 assert_eq!(schedule.newly_active(None, 15).len(), 1);
2371 }
2372
2373 #[test]
2374 fn reroute_filter_round_trips_camel_case() {
2375 let filter = RerouteFilter {
2376 pairs: BTreeSet::from([MintPair::new(address(1), address(2))]),
2377 };
2378 let json = serde_json::to_value(&filter).unwrap();
2379 assert!(
2380 json["pairs"][0][0].is_string() && json["pairs"][0][1].is_string(),
2381 "a pair serializes as two base58 strings: {json}"
2382 );
2383 let back: RerouteFilter = serde_json::from_value(json).unwrap();
2384 assert_eq!(back, filter);
2385 }
2386
2387 #[test]
2390 fn mint_pairs_are_normalized_however_they_are_built() {
2391 let (one, other) = (address(1), address(2));
2392 assert_eq!(MintPair::new(one, other), MintPair::new(other, one));
2393
2394 let reversed: MintPair =
2395 serde_json::from_value(serde_json::json!([other.to_string(), one.to_string()]))
2396 .unwrap();
2397 assert_eq!(reversed, MintPair::new(one, other));
2398 }
2399
2400 #[test]
2401 fn fail_fast_divergence_kind_str_round_trips() {
2402 for kind in [
2403 FailFastDivergenceKind::AnyNonBenign,
2404 FailFastDivergenceKind::Tracked,
2405 ] {
2406 assert_eq!(
2407 FailFastDivergenceKind::from_str_opt(kind.as_str()),
2408 Some(kind)
2409 );
2410 }
2411 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
2412 assert_eq!(
2413 FailFastDivergenceKind::default(),
2414 FailFastDivergenceKind::AnyNonBenign
2415 );
2416 }
2417
2418 #[test]
2419 fn bundle_not_found_is_terminal_and_descriptive() {
2420 let error = BacktestError::BundleNotFound {
2421 start_slot: 100,
2422 end_slot: 600,
2423 };
2424
2425 assert!(error.is_terminal());
2426 assert_eq!(
2427 error.to_string(),
2428 "block bundle not found for start_slot=100, end_slot=600"
2429 );
2430 }
2431
2432 #[test]
2433 fn no_produced_blocks_is_terminal_and_descriptive() {
2434 let error = BacktestError::NoProducedBlocksInRange {
2435 start_slot: 100,
2436 end_slot: 100,
2437 };
2438
2439 assert!(error.is_terminal());
2440 assert_eq!(
2443 error.to_string(),
2444 "no produced blocks in requested range 100..=100; slot(s) may be skipped on-chain"
2445 );
2446 }
2447
2448 #[test]
2449 fn bundle_build_request_optional_fields_default_to_none() {
2450 let req: BundleBuildRequest =
2451 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
2452 assert_eq!((req.start_slot, req.end_slot), (1, 2));
2453 assert_eq!(req.bundle_size, None);
2454 assert_eq!(req.idempotency_key, None);
2455 }
2456
2457 #[test]
2458 fn bundle_build_request_parses_optional_fields() {
2459 let req: BundleBuildRequest = serde_json::from_str(
2460 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
2461 )
2462 .expect("parse");
2463 assert_eq!(req.bundle_size, Some(500));
2464 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
2465 }
2466
2467 #[test]
2468 fn bundle_build_status_serde_round_trips_with_snake_case() {
2469 let cases = [
2470 (BundleBuildStatus::Completed, "\"completed\""),
2471 (BundleBuildStatus::Failed, "\"failed\""),
2472 (BundleBuildStatus::InProgress, "\"in_progress\""),
2473 (
2474 BundleBuildStatus::NeedsInvestigation,
2475 "\"needs_investigation\"",
2476 ),
2477 ];
2478 for (status, expected) in cases {
2479 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
2480 assert_eq!(
2481 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
2482 status
2483 );
2484 assert_eq!(status.as_str(), expected.trim_matches('"'));
2485 }
2486 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
2487 }
2488
2489 #[test]
2490 fn bundle_build_request_without_kind_deserializes_as_blocks() {
2491 let request: BundleBuildRequest =
2493 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
2494 assert_eq!(request.kind, BundleKind::Blocks);
2495 }
2496
2497 #[test]
2498 fn bundle_kind_round_trips_over_serde_and_as_str() {
2499 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
2500 let json = serde_json::to_string(&kind).unwrap();
2501 assert_eq!(json, format!("\"{}\"", kind.as_str()));
2502 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
2503 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
2506 }
2507 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
2508 assert_eq!(BundleKind::from_str_opt("accountState"), None);
2509 }
2510
2511 #[test]
2512 fn bundle_build_request_carries_kind_through_serde() {
2513 let request = BundleBuildRequest {
2514 start_slot: 100,
2515 end_slot: 200,
2516 kind: BundleKind::AccountState,
2517 ..Default::default()
2518 };
2519 let json = serde_json::to_string(&request).unwrap();
2520 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2521 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
2522 assert_eq!(parsed.kind, BundleKind::AccountState);
2523 }
2524
2525 #[test]
2526 fn bundle_build_status_response_serializes_request_and_status() {
2527 let response = BundleBuildStatusResponse {
2528 request_id: "r".to_string(),
2529 start_slot: 100,
2530 end_slot: 200,
2531 bundle_size: Some(50),
2532 status: BundleBuildStatus::InProgress,
2533 created_at_unix_ms: 0,
2534 kind: BundleKind::Blocks,
2535 };
2536 let json = serde_json::to_value(&response).unwrap();
2537 assert_eq!(json["request_id"].as_str(), Some("r"));
2538 assert_eq!(json["start_slot"].as_u64(), Some(100));
2539 assert_eq!(json["bundle_size"].as_u64(), Some(50));
2540 assert_eq!(json["status"].as_str(), Some("in_progress"));
2541 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
2542 }
2543
2544 #[test]
2545 fn an_empty_bundles_list_is_distinct_from_an_absent_one() {
2546 let absent: BundleBuildListResponse = serde_json::from_str(r#"{"builds":[]}"#).unwrap();
2548 let empty: BundleBuildListResponse =
2549 serde_json::from_str(r#"{"builds":[],"bundles":[]}"#).unwrap();
2550 assert_eq!(absent.bundles, None);
2551 assert_eq!(empty.bundles, Some(Vec::new()));
2552 }
2553
2554 #[test]
2555 fn completed_bundle_round_trips() {
2556 let bundle = CompletedBundle {
2557 start_slot: 100,
2558 end_slot: 200,
2559 kind: BundleKind::AccountState,
2560 completed_at_unix_ms: Some(1_700_000_000_000),
2561 size_bytes: Some(4_096),
2562 };
2563 let json = serde_json::to_string(&bundle).unwrap();
2564 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2565 assert_eq!(
2566 serde_json::from_str::<CompletedBundle>(&json).unwrap(),
2567 bundle
2568 );
2569
2570 let sparse = r#"{"start_slot":1,"end_slot":2,"kind":"blocks"}"#;
2571 let parsed: CompletedBundle = serde_json::from_str(sparse).unwrap();
2572 assert_eq!(parsed.completed_at_unix_ms, None);
2573 assert_eq!(parsed.size_bytes, None);
2574 }
2575
2576 #[test]
2577 fn available_range_reads_a_server_that_predates_kind_and_scope() {
2578 let json = r#"{
2580 "bundle_start_slot": 100,
2581 "bundle_start_slot_utc": null,
2582 "max_bundle_end_slot": 200,
2583 "max_bundle_end_slot_utc": null,
2584 "max_bundle_size": 101
2585 }"#;
2586 let parsed: AvailableRange = serde_json::from_str(json).unwrap();
2587 assert_eq!(parsed.bundle_start_slot, 100);
2588 assert_eq!(parsed.kind, None);
2589 assert_eq!(parsed.scope, None);
2590 }
2591
2592 #[test]
2593 fn available_range_ignores_fields_it_does_not_know() {
2594 let json = r#"{"bundle_start_slot": 1, "some_future_field": "x"}"#;
2596 assert_eq!(
2597 serde_json::from_str::<AvailableRange>(json)
2598 .unwrap()
2599 .bundle_start_slot,
2600 1
2601 );
2602 }
2603
2604 #[test]
2605 fn available_range_labels_round_trip() {
2606 let range = AvailableRange {
2607 bundle_start_slot: 100,
2608 max_bundle_end_slot: Some(200),
2609 kind: Some(BundleKind::AccountState),
2610 scope: Some(RangeScope::User),
2611 ..Default::default()
2612 };
2613 let json = serde_json::to_string(&range).unwrap();
2614 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2615 assert!(json.contains(r#""scope":"user""#), "{json}");
2616 let parsed: AvailableRange = serde_json::from_str(&json).unwrap();
2617 assert_eq!(parsed.kind, Some(BundleKind::AccountState));
2618 assert_eq!(parsed.scope, Some(RangeScope::User));
2619 }
2620
2621 fn range(start: u64, end: u64) -> AvailableRange {
2622 AvailableRange {
2623 bundle_start_slot: start,
2624 max_bundle_end_slot: Some(end),
2625 ..Default::default()
2626 }
2627 }
2628
2629 #[rstest::rstest]
2633 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
2634 #[case::multi(
2635 vec![range(100, 200), range(201, 300), range(301, 400)],
2636 100, 300, Some(vec![(100, 200), (201, 300)])
2637 )]
2638 #[case::nested(
2641 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
2642 100, 900, Some(vec![(100, 500), (501, 900)])
2643 )]
2644 #[case::prefers_finer_grid(
2648 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
2649 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
2650 )]
2651 #[case::shared_start_prefers_finer(
2655 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
2656 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
2657 )]
2658 #[case::coarse_overlap_prefers_finer_pair(
2662 vec![range(100, 200), range(100, 150), range(151, 160)],
2663 100, 160, Some(vec![(100, 150), (151, 160)])
2664 )]
2665 #[case::falls_back_to_coarse(
2669 vec![range(100, 160), range(100, 120), range(121, 140)],
2670 100, 160, Some(vec![(100, 160)])
2671 )]
2672 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
2674 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
2677 #[case::anchors_then_continues(
2678 vec![range(150, 350), range(351, 600)],
2679 200, 600, Some(vec![(150, 350), (351, 600)])
2680 )]
2681 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
2682 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
2684 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
2686 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
2687 #[case::user_and_global_ranges_not_collapsed(
2692 vec![range(100, 200), range(100, 150), range(151, 200)],
2693 100, 200, Some(vec![(100, 150), (151, 200)])
2694 )]
2695 fn split_range_cases(
2696 #[case] ranges: Vec<AvailableRange>,
2697 #[case] start: u64,
2698 #[case] end: u64,
2699 #[case] expected: Option<Vec<(u64, u64)>>,
2700 ) {
2701 match expected {
2702 Some(expected) => assert_eq!(
2703 split_range(
2704 &ranges,
2705 SlotRange::new_unchecked(start, end),
2706 BundleKind::Blocks
2707 )
2708 .unwrap(),
2709 expected
2710 ),
2711 None => assert!(
2712 split_range(
2713 &ranges,
2714 SlotRange::new_unchecked(start, end),
2715 BundleKind::Blocks
2716 )
2717 .is_err()
2718 ),
2719 }
2720 }
2721
2722 fn account_state_range(start: u64, end: u64) -> AvailableRange {
2723 AvailableRange {
2724 kind: Some(BundleKind::AccountState),
2725 ..range(start, end)
2726 }
2727 }
2728
2729 #[test]
2730 fn a_replay_split_rides_the_recordings_not_a_finer_block_grid() {
2731 let ranges = vec![
2734 range(100, 149),
2735 range(150, 199),
2736 account_state_range(100, 199),
2737 ];
2738 let requested = SlotRange::new_unchecked(100, 199);
2739
2740 assert_eq!(
2741 split_range(&ranges, requested, BundleKind::Blocks).unwrap(),
2742 [(100, 149), (150, 199)]
2743 );
2744 assert_eq!(
2745 split_range(&ranges, requested, BundleKind::AccountState).unwrap(),
2746 [(100, 199)]
2747 );
2748 }
2749
2750 #[test]
2751 fn a_replay_split_tiles_consecutive_recordings() {
2752 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
2753 assert_eq!(
2754 split_range(
2755 &ranges,
2756 SlotRange::new_unchecked(100, 299),
2757 BundleKind::AccountState
2758 )
2759 .unwrap(),
2760 [(100, 199), (200, 299)]
2761 );
2762 }
2763
2764 #[test]
2765 fn a_replay_split_ends_short_of_a_recording() {
2766 let requested = SlotRange::new_unchecked(100, 150);
2769 assert_eq!(
2770 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
2771 [(100, 150)]
2772 );
2773 assert_eq!(
2774 split_range(
2775 &[account_state_range(100, 199)],
2776 requested,
2777 BundleKind::AccountState
2778 )
2779 .unwrap(),
2780 [(100, 150)]
2781 );
2782 }
2783
2784 #[test]
2785 fn a_replay_split_ends_short_of_its_last_recording() {
2786 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
2789 assert_eq!(
2790 split_range(
2791 &ranges,
2792 SlotRange::new_unchecked(100, 250),
2793 BundleKind::AccountState
2794 )
2795 .unwrap(),
2796 [(100, 199), (200, 250)]
2797 );
2798 }
2799
2800 #[test]
2801 fn a_replay_split_tiles_a_one_slot_recording() {
2802 let ranges = vec![account_state_range(100, 249), account_state_range(250, 250)];
2805 assert_eq!(
2806 split_range(
2807 &ranges,
2808 SlotRange::new_unchecked(100, 250),
2809 BundleKind::AccountState
2810 )
2811 .unwrap(),
2812 [(100, 249), (250, 250)]
2813 );
2814 }
2815
2816 #[test]
2817 fn a_replay_split_anchors_a_mid_recording_start() {
2818 let requested = SlotRange::new_unchecked(150, 199);
2822 assert_eq!(
2823 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
2824 [(100, 199)]
2825 );
2826 assert_eq!(
2827 split_range(
2828 &[account_state_range(100, 199)],
2829 requested,
2830 BundleKind::AccountState
2831 )
2832 .unwrap(),
2833 [(100, 199)]
2834 );
2835 }
2836
2837 #[test]
2838 fn a_replay_split_refuses_a_start_no_recording_covers() {
2839 let error = split_range(
2840 &[account_state_range(200, 299)],
2841 SlotRange::new_unchecked(100, 299),
2842 BundleKind::AccountState,
2843 )
2844 .expect_err("no recording covers 100");
2845 assert!(
2846 error.contains("is not covered by any available account-state bundle"),
2847 "{error}"
2848 );
2849 }
2850
2851 #[test]
2852 fn a_replay_split_ignores_block_ranges() {
2853 let error = split_range(
2854 &[range(100, 199)],
2855 SlotRange::new_unchecked(100, 199),
2856 BundleKind::AccountState,
2857 )
2858 .expect_err("owns no recordings");
2859 assert!(error.contains("account-state bundle"), "{error}");
2860 }
2861
2862 #[test]
2863 fn blocks_mode_admits_ranges_from_a_server_that_predates_kind() {
2864 let unlabelled = range(100, 199);
2867 assert_eq!(unlabelled.kind, None);
2868 assert_eq!(
2869 split_range(
2870 &[unlabelled],
2871 SlotRange::new_unchecked(100, 150),
2872 BundleKind::Blocks
2873 )
2874 .unwrap(),
2875 [(100, 150)]
2876 );
2877 }
2878
2879 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
2882 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
2883 for r in ranges {
2884 if let Some(end) = r.max_bundle_end_slot
2885 && end >= r.bundle_start_slot
2886 {
2887 ends.entry(r.bundle_start_slot).or_default().insert(end);
2888 }
2889 }
2890 ends
2891 }
2892
2893 fn reference_max_split(
2897 ends: &BTreeMap<u64, BTreeSet<u64>>,
2898 cursor: u64,
2899 end: u64,
2900 ) -> Option<Vec<(u64, u64)>> {
2901 ends.get(&cursor)?
2902 .iter()
2903 .filter_map(|&bundle_end| {
2904 if bundle_end >= end {
2905 Some(vec![(cursor, end)])
2906 } else {
2907 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
2908 rest.insert(0, (cursor, bundle_end));
2909 rest
2910 })
2911 }
2912 })
2913 .max_by_key(Vec::len)
2914 }
2915
2916 fn is_valid_split(
2920 split: &[(u64, u64)],
2921 ends: &BTreeMap<u64, BTreeSet<u64>>,
2922 start: u64,
2923 end: u64,
2924 ) -> bool {
2925 split.first().is_some_and(|&(s, _)| s == start)
2926 && split.last().is_some_and(|&(_, e)| e == end)
2927 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
2928 && split.iter().all(|&(s, e)| {
2929 e >= s
2930 && ends
2931 .get(&s)
2932 .and_then(|bundle_ends| bundle_ends.iter().next_back())
2933 .is_some_and(|&max_end| e <= max_end)
2934 })
2935 }
2936
2937 #[test]
2942 fn split_range_matches_reference() {
2943 let mut seed: u64 = 0x9E3779B97F4A7C15;
2944 let mut next = || {
2945 seed = seed
2946 .wrapping_mul(6364136223846793005)
2947 .wrapping_add(1442695040888963407);
2948 seed >> 33
2949 };
2950
2951 for _ in 0..50_000 {
2952 let ranges: Vec<AvailableRange> = (0..next() % 6)
2955 .map(|_| {
2956 let start = next() % 12;
2957 range(start, start + next() % 6) })
2959 .collect();
2960 let start = next() % 12;
2961 let end = start + next() % 6; let got = split_range(
2964 &ranges,
2965 SlotRange::new_unchecked(start, end),
2966 BundleKind::Blocks,
2967 );
2968 let ends = ends_by_start(&ranges);
2969 let anchor = ends
2972 .range(..=start)
2973 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
2974 .map(|(&s, _)| s);
2975 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
2976
2977 let layout: Vec<_> = ranges
2978 .iter()
2979 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
2980 .collect();
2981 match (&got, &reference) {
2982 (Ok(split), Some(best)) => {
2983 assert!(
2984 is_valid_split(split, &ends, anchor.unwrap(), end),
2985 "invalid split {split:?} for {layout:?} [{start},{end}]"
2986 );
2987 assert_eq!(
2988 split.len(),
2989 best.len(),
2990 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
2991 );
2992 }
2993 (Err(_), None) => {}
2994 _ => panic!(
2995 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
2996 ),
2997 }
2998 }
2999 }
3000}