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 route_plan;
27pub mod subscribe_config;
28pub mod usage;
29pub mod ws_compression;
30
31#[derive(Debug, Serialize, Deserialize)]
33#[serde(tag = "method", content = "params", rename_all = "camelCase")]
34pub enum BacktestRequest {
35 CreateBacktestSession(CreateBacktestSessionRequest),
36 Continue(ContinueParams),
37 ContinueTo(ContinueToParams),
38 ContinueSessionV1(ContinueSessionRequestV1),
39 ContinueToSessionV1(ContinueToSessionRequestV1),
40 CloseBacktestSession,
41 CloseSessionV1(CloseSessionRequestV1),
42 AttachBacktestSession {
43 session_id: SessionId,
44 last_sequence: Option<u64>,
47 },
48 ResumeAttachedSession,
51 AttachParallelControlSessionV2 {
52 control_session_id: ControlSessionId,
53 #[serde(default)]
57 last_sequences: BTreeMap<SessionId, u64>,
58 },
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(untagged)]
67pub enum CreateBacktestSessionRequest {
68 V1(CreateBacktestSessionRequestV1),
69 V0(CreateSessionParams),
70}
71
72impl CreateBacktestSessionRequest {
73 pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
74 match self {
75 Self::V0(request) => CreateBacktestSessionRequestOptions {
76 request,
77 parallel: false,
78 },
79 Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
80 CreateBacktestSessionRequestOptions { request, parallel }
81 }
82 }
83 }
84
85 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
86 let options = self.into_request_options();
87 (options.request, options.parallel)
88 }
89}
90
91impl From<CreateSessionParams> for CreateBacktestSessionRequest {
92 fn from(value: CreateSessionParams) -> Self {
93 Self::V0(value)
94 }
95}
96
97impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
98 fn from(value: CreateBacktestSessionRequestV1) -> Self {
99 Self::V1(value)
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct CreateBacktestSessionRequestV1 {
106 #[serde(flatten)]
107 pub request: CreateSessionParams,
108 pub parallel: bool,
109}
110
111#[derive(Debug, Clone)]
112pub struct CreateBacktestSessionRequestOptions {
113 pub request: CreateSessionParams,
114 pub parallel: bool,
115}
116
117#[derive(Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct ContinueSessionRequestV1 {
120 pub session_id: SessionId,
121 pub request: ContinueParams,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct ContinueToSessionRequestV1 {
127 pub session_id: SessionId,
128 pub request: ContinueToParams,
129}
130
131#[derive(Debug, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct CloseSessionRequestV1 {
134 pub session_id: SessionId,
135}
136
137#[serde_with::serde_as]
144#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
145#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
146pub enum DiscoveryFilter {
147 ProgramExecuted(#[serde_as(as = "serde_with::DisplayFromStr")] Address),
149}
150
151pub struct TxMatchContext<'a> {
156 pub invoked_programs: &'a HashSet<Address>,
158}
159
160impl DiscoveryFilter {
161 pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
164 match self {
165 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
166 }
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
174#[serde(try_from = "[String; 2]", into = "[String; 2]")]
175pub struct MintPair([Address; 2]);
176
177impl MintPair {
178 pub fn new(one: Address, other: Address) -> Self {
179 Self(if one <= other {
180 [one, other]
181 } else {
182 [other, one]
183 })
184 }
185
186 pub fn mints(&self) -> (Address, Address) {
187 (self.0[0], self.0[1])
188 }
189}
190
191#[derive(Debug, thiserror::Error)]
192#[error("invalid mint {mint} in pair")]
193pub struct ParseMintPairError {
194 mint: String,
195 source: ParseAddressError,
196}
197
198impl TryFrom<[String; 2]> for MintPair {
199 type Error = ParseMintPairError;
200
201 fn try_from([one, other]: [String; 2]) -> Result<Self, Self::Error> {
202 fn parse(mint: String) -> Result<Address, ParseMintPairError> {
203 Address::from_str(&mint).map_err(|source| ParseMintPairError { mint, source })
204 }
205 Ok(Self::new(parse(one)?, parse(other)?))
206 }
207}
208
209impl From<MintPair> for [String; 2] {
210 fn from(pair: MintPair) -> Self {
211 let (one, other) = pair.mints();
212 [one.to_string(), other.to_string()]
213 }
214}
215
216#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(rename_all = "camelCase")]
220pub struct RerouteFilter {
221 #[serde(default)]
223 pub pairs: BTreeSet<MintPair>,
224}
225
226impl RerouteFilter {
227 pub fn is_empty(&self) -> bool {
230 self.pairs.is_empty()
231 }
232
233 pub fn matches_mints(&self, input: &Address, output: &Address) -> bool {
234 self.pairs.contains(&MintPair::new(*input, *output))
235 }
236
237 pub fn mentions_mint(&self, mint: &Address) -> bool {
240 self.pairs.iter().any(|pair| {
241 let (one, other) = pair.mints();
242 one == *mint || other == *mint
243 })
244 }
245}
246
247#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub enum ActionKind {
252 #[default]
256 Simulate,
257 Send,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266#[serde(tag = "at", rename_all = "camelCase")]
267pub enum ActionAnchor {
268 BeforeSlot {
271 #[serde(default, with = "compressed")]
272 slots: Vec<u64>,
273 },
274 AfterSlot {
277 #[serde(default, with = "compressed")]
278 slots: Vec<u64>,
279 },
280 BeforeMatch { filter: DiscoveryFilter },
283 AfterMatch { filter: DiscoveryFilter },
286}
287
288const MAX_ACTION_TRANSACTIONS_BYTES: u64 = 64 * 1024 * 1024;
291
292mod compressed {
297 use std::io::Read;
298
299 use base64::Engine as _;
300 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
301
302 use super::{BASE64, MAX_ACTION_TRANSACTIONS_BYTES};
303
304 const COMPRESSION_LEVEL: i32 = 0;
306
307 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
308 where
309 T: Serialize,
310 S: Serializer,
311 {
312 let json = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
313 let compressed = zstd::stream::encode_all(json.as_slice(), COMPRESSION_LEVEL)
314 .map_err(serde::ser::Error::custom)?;
315 serializer.serialize_str(&BASE64.encode(compressed))
316 }
317
318 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
319 where
320 T: DeserializeOwned,
321 D: Deserializer<'de>,
322 {
323 let blob = String::deserialize(deserializer)?;
324 let compressed = BASE64
325 .decode(blob.as_bytes())
326 .map_err(serde::de::Error::custom)?;
327 let mut json = Vec::new();
328 zstd::stream::read::Decoder::new(compressed.as_slice())
329 .and_then(|d| Read::take(d, MAX_ACTION_TRANSACTIONS_BYTES).read_to_end(&mut json))
330 .map_err(serde::de::Error::custom)?;
331 serde_json::from_slice(&json).map_err(serde::de::Error::custom)
332 }
333}
334
335#[serde_with::serde_as]
343#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct ScheduledAction {
346 pub anchor: ActionAnchor,
347 #[serde(default)]
349 pub kind: ActionKind,
350 #[serde(default, with = "compressed")]
359 pub transactions: Vec<String>,
360 #[serde(default)]
365 pub account_overrides: AccountModifications,
366 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
376 pub feeds_reroute: bool,
377 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
381 #[serde(default)]
382 pub return_accounts: Vec<Address>,
383 #[serde(default)]
385 pub label: Option<String>,
386}
387
388impl ScheduledAction {
389 pub fn is_computed_override(&self) -> bool {
392 self.feeds_reroute && !self.transactions.is_empty()
393 }
394}
395
396#[derive(Debug, Clone)]
398enum OverrideContribution {
399 Static(HashMap<Address, AccountSharedData>),
400 Pending,
401 Resolved(HashMap<Address, AccountSharedData>),
402 Failed,
403}
404
405#[derive(Debug, Clone, Default)]
406struct OverrideScheduleInner {
407 entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>,
408 keys: HashSet<Address>,
412}
413
414impl OverrideScheduleInner {
415 fn new(entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>) -> Self {
416 let keys = entries
417 .values()
418 .flatten()
419 .filter_map(|(_, contribution)| match contribution {
420 OverrideContribution::Static(accounts)
421 | OverrideContribution::Resolved(accounts) => Some(accounts.keys().copied()),
422 OverrideContribution::Pending | OverrideContribution::Failed => None,
423 })
424 .flatten()
425 .collect();
426 Self { entries, keys }
427 }
428}
429
430#[derive(Debug, Clone, Default)]
433pub struct OverrideSchedule {
434 inner: Arc<RwLock<OverrideScheduleInner>>,
435}
436
437impl From<BTreeMap<u64, HashMap<Address, AccountSharedData>>> for OverrideSchedule {
438 fn from(entries: BTreeMap<u64, HashMap<Address, AccountSharedData>>) -> Self {
439 Self {
440 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(
441 entries
442 .into_iter()
443 .map(|(slot, accounts)| {
444 (slot, vec![(0, OverrideContribution::Static(accounts))])
445 })
446 .collect(),
447 ))),
448 }
449 }
450}
451
452impl OverrideSchedule {
453 pub fn is_empty(&self) -> bool {
454 self.inner.read().entries.is_empty()
455 }
456
457 pub fn slots(&self) -> impl Iterator<Item = u64> + '_ {
459 self.inner
460 .read()
461 .entries
462 .keys()
463 .copied()
464 .collect::<Vec<_>>()
465 .into_iter()
466 }
467
468 pub fn active_at(&self, slot: u64) -> HashMap<Address, AccountSharedData> {
470 self.inner.read().entries.range(..=slot).fold(
471 HashMap::new(),
472 |mut active, (_, contributions)| {
473 active.extend(fold_contributions(contributions));
474 active
475 },
476 )
477 }
478
479 pub fn active_for(&self, key: &Address, slot: u64) -> Option<AccountSharedData> {
480 let inner = self.inner.read();
481 if !inner.keys.contains(key) {
482 return None;
483 }
484 inner
485 .entries
486 .range(..=slot)
487 .rev()
488 .find_map(|(_, contributions)| {
489 contributions
490 .iter()
491 .rev()
492 .find_map(|(_, contribution)| match contribution {
493 OverrideContribution::Static(accounts)
494 | OverrideContribution::Resolved(accounts) => accounts.get(key).cloned(),
495 OverrideContribution::Pending | OverrideContribution::Failed => None,
496 })
497 })
498 }
499
500 pub fn newly_active(
502 &self,
503 after: Option<u64>,
504 up_to: u64,
505 ) -> Vec<(Address, AccountSharedData)> {
506 let lower = after.map_or(std::ops::Bound::Unbounded, std::ops::Bound::Excluded);
507 self.inner
508 .read()
509 .entries
510 .range((lower, std::ops::Bound::Included(up_to)))
511 .flat_map(|(_, contributions)| fold_contributions(contributions))
512 .collect::<HashMap<_, _>>()
513 .into_iter()
514 .collect()
515 }
516
517 pub fn resolve(
520 &self,
521 slot: u64,
522 action_index: u32,
523 accounts: Option<HashMap<Address, AccountSharedData>>,
524 ) -> Result<(), String> {
525 let mut inner = self.inner.write();
526 let OverrideScheduleInner { entries, keys } = &mut *inner;
527 let contribution = entries
528 .get_mut(&slot)
529 .and_then(|contributions| {
530 contributions
531 .iter_mut()
532 .find(|(index, _)| *index == action_index)
533 .map(|(_, contribution)| contribution)
534 })
535 .ok_or_else(|| {
536 format!("override action {action_index} has no contribution at slot {slot}")
537 })?;
538 if !matches!(contribution, OverrideContribution::Pending) {
539 return Err(format!(
540 "override action {action_index} at slot {slot} is not pending"
541 ));
542 }
543 *contribution = match accounts {
544 Some(accounts) => {
545 keys.extend(accounts.keys().copied());
546 OverrideContribution::Resolved(accounts)
547 }
548 None => OverrideContribution::Failed,
549 };
550 Ok(())
551 }
552}
553
554fn fold_contributions(
555 contributions: &[(u32, OverrideContribution)],
556) -> HashMap<Address, AccountSharedData> {
557 let mut accounts = HashMap::new();
558 for (_, contribution) in contributions {
559 match contribution {
560 OverrideContribution::Static(next) | OverrideContribution::Resolved(next) => {
561 accounts.extend(next.iter().map(|(key, account)| (*key, account.clone())));
562 }
563 OverrideContribution::Pending | OverrideContribution::Failed => {}
564 }
565 }
566 accounts
567}
568
569fn decode_override_actions(actions: &[ScheduledAction]) -> Result<OverrideSchedule, String> {
578 let mut entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>> = BTreeMap::new();
579 for (action_index, action) in actions.iter().enumerate().filter(|(_, action)| {
580 action.feeds_reroute
581 || (action.transactions.is_empty() && !action.account_overrides.0.is_empty())
582 }) {
583 if action.kind == ActionKind::Send {
585 return Err("account overrides are not valid for kind: send".to_string());
586 }
587 if action.transactions.is_empty() && action.account_overrides.0.is_empty() {
588 return Err(
589 "feedsReroute needs something to feed: transactions to simulate, or \
590 accountOverrides"
591 .to_string(),
592 );
593 }
594 let ActionAnchor::BeforeSlot { slots } = &action.anchor else {
595 return Err("scheduled account overrides require a beforeSlot anchor".to_string());
596 };
597 let accounts = decode_override_accounts(&action.account_overrides)?;
598 let contribution = if action.transactions.is_empty() {
599 OverrideContribution::Static(accounts)
600 } else {
601 OverrideContribution::Pending
602 };
603 for slot in slots.iter().copied().collect::<BTreeSet<_>>() {
606 entries
607 .entry(slot)
608 .or_default()
609 .push((action_index as u32, contribution.clone()));
610 }
611 }
612 Ok(OverrideSchedule {
613 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(entries))),
614 })
615}
616
617#[allow(
621 deprecated,
622 reason = "`PROGRAM_OWNERS` remains the canonical loader-owner list"
623)]
624pub fn decode_account_modifications(
625 modifications: &AccountModifications,
626) -> Result<HashMap<Address, AccountSharedData>, String> {
627 modifications
628 .0
629 .iter()
630 .map(|(address, data)| {
631 let account = data
632 .to_account()
633 .map_err(|error| format!("invalid account data for {address}: {error}"))?;
634 if account.executable || solana_account::PROGRAM_OWNERS.contains(&account.owner) {
635 return Err(format!(
636 "overriding executable/program accounts is not supported (account {address})"
637 ));
638 }
639 Ok((*address, AccountSharedData::from(account)))
640 })
641 .collect()
642}
643
644fn decode_override_accounts(
647 modifications: &AccountModifications,
648) -> Result<HashMap<Address, AccountSharedData>, String> {
649 let accounts = decode_account_modifications(modifications)?;
650 if let Some(address) = modifications
651 .0
652 .iter()
653 .find_map(|(address, data)| TOKEN_PROGRAM_IDS.contains(&data.owner).then_some(address))
654 {
655 return Err(format!(
656 "override for {address}: token accounts cannot be overridden \
657 (prepare-stage reads would desync from simulation)"
658 ));
659 }
660 Ok(accounts)
661}
662
663pub const MAX_DISCONNECT_TIMEOUT_SECS: u16 = 900;
667
668#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
671#[serde(rename_all = "lowercase", try_from = "String")]
672pub enum SwapAggregator {
673 Jupiter,
674 Okx,
675 Titan,
676 Dflow,
677}
678
679impl SwapAggregator {
680 pub const fn as_str(self) -> &'static str {
682 match self {
683 Self::Jupiter => "jupiter",
684 Self::Okx => "okx",
685 Self::Titan => "titan",
686 Self::Dflow => "dflow",
687 }
688 }
689}
690
691impl std::fmt::Display for SwapAggregator {
692 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
693 f.write_str(self.as_str())
694 }
695}
696
697#[derive(Debug, thiserror::Error)]
698pub enum ParseAggregatorError {
699 #[error("unknown swap aggregator {0:?}; expected one of jupiter, okx, titan, dflow")]
700 Unknown(String),
701 #[error("name at least one aggregator (jupiter, okx, titan, dflow)")]
703 NoAggregator,
704}
705
706impl TryFrom<String> for SwapAggregator {
707 type Error = ParseAggregatorError;
708
709 fn try_from(value: String) -> Result<Self, Self::Error> {
710 value.parse()
711 }
712}
713
714impl std::str::FromStr for SwapAggregator {
715 type Err = ParseAggregatorError;
716
717 fn from_str(s: &str) -> Result<Self, Self::Err> {
718 match s.trim().to_ascii_lowercase().as_str() {
719 "jupiter" => Ok(Self::Jupiter),
720 "okx" => Ok(Self::Okx),
721 "titan" => Ok(Self::Titan),
722 "dflow" => Ok(Self::Dflow),
723 _ => Err(ParseAggregatorError::Unknown(s.to_string())),
724 }
725 }
726}
727
728#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
732#[serde(transparent)]
733pub struct RerouteAggregators(BTreeSet<SwapAggregator>);
734
735impl Default for RerouteAggregators {
736 fn default() -> Self {
737 Self([SwapAggregator::Jupiter].into_iter().collect())
738 }
739}
740
741impl RerouteAggregators {
742 pub fn new(aggregators: impl IntoIterator<Item = SwapAggregator>) -> Self {
743 Self(aggregators.into_iter().collect())
744 }
745
746 pub fn contains(&self, aggregator: SwapAggregator) -> bool {
747 self.0.contains(&aggregator)
748 }
749
750 pub fn is_empty(&self) -> bool {
751 self.0.is_empty()
752 }
753}
754
755impl std::fmt::Display for RerouteAggregators {
756 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
757 let mut aggregators = self.0.iter();
758 if let Some(first) = aggregators.next() {
759 write!(f, "{first}")?;
760 }
761 aggregators.try_for_each(|aggregator| write!(f, ",{aggregator}"))
762 }
763}
764
765impl std::str::FromStr for RerouteAggregators {
766 type Err = ParseAggregatorError;
767
768 fn from_str(s: &str) -> Result<Self, Self::Err> {
769 let aggregators: BTreeSet<SwapAggregator> = s
770 .split(',')
771 .filter(|aggregator| !aggregator.trim().is_empty())
772 .map(str::parse)
773 .collect::<Result<_, _>>()?;
774 if aggregators.is_empty() {
775 return Err(ParseAggregatorError::NoAggregator);
776 }
777 Ok(Self(aggregators))
778 }
779}
780
781#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
789#[serde(rename_all = "camelCase")]
790pub struct RerouteParams {
791 #[serde(default)]
792 pub aggregators: RerouteAggregators,
793 #[serde(default, skip_serializing_if = "Option::is_none")]
796 pub filter: Option<RerouteFilter>,
797 #[serde(default)]
804 pub detect_failed_l1_swaps: bool,
805 #[serde(default)]
808 pub circular_arbs: bool,
809 #[serde(default = "RerouteParams::default_requote")]
815 pub requote: bool,
816 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub direct_fill: Option<Box<DirectFillParams>>,
822}
823
824#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
830#[serde(rename_all = "camelCase")]
831pub struct DirectFillParams {
832 pub aggregator: SwapAggregator,
835 pub venue: String,
837 pub pair: MintPair,
839 pub slippage_bps: u16,
842}
843
844impl Default for RerouteParams {
847 fn default() -> Self {
848 Self {
849 aggregators: RerouteAggregators::default(),
850 filter: None,
851 detect_failed_l1_swaps: false,
852 circular_arbs: false,
853 requote: Self::default_requote(),
854 direct_fill: None,
855 }
856 }
857}
858
859impl std::str::FromStr for MintPair {
861 type Err = String;
862
863 fn from_str(s: &str) -> Result<Self, Self::Err> {
864 let (base, quote) = s
865 .split_once(',')
866 .ok_or_else(|| format!("expected two comma-separated mints, got {s:?}"))?;
867 Self::try_from([base.trim().to_owned(), quote.trim().to_owned()])
868 .map_err(|error| error.to_string())
869 }
870}
871
872impl std::fmt::Display for MintPair {
873 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
874 let (one, other) = self.mints();
875 write!(f, "{one},{other}")
876 }
877}
878
879impl RerouteParams {
880 const fn default_requote() -> bool {
881 true
882 }
883
884 pub const fn is_noop(&self) -> bool {
887 !self.requote && self.direct_fill.is_none()
888 }
889}
890
891impl DirectFillParams {
892 pub fn matches_mints(&self, input: &Address, output: &Address) -> bool {
894 MintPair::new(*input, *output) == self.pair
895 }
896}
897
898#[serde_with::serde_as]
900#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
901#[serde(rename_all = "camelCase")]
902pub struct CreateSessionParams {
903 pub start_slot: u64,
905 pub end_slot: u64,
907 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
908 #[serde(default)]
909 #[builder(default)]
910 pub signer_filter: BTreeSet<Address>,
912 #[serde(default)]
915 #[builder(default)]
916 pub send_summary: bool,
917 #[serde(default, skip_serializing_if = "Option::is_none")]
919 pub reroute: Option<RerouteParams>,
920 #[serde(default)]
923 pub capacity_wait_timeout_secs: Option<u16>,
924 #[serde(default)]
928 pub disconnect_timeout_secs: Option<u16>,
929 #[serde(default)]
934 pub extra_compute_units: Option<u32>,
935 #[serde(default, skip_serializing_if = "Vec::is_empty")]
942 #[builder(default)]
943 pub discoveries: Vec<DiscoveryFilter>,
944 #[serde(default, with = "compressed", skip_serializing_if = "Vec::is_empty")]
953 #[builder(default)]
954 pub actions: Vec<ScheduledAction>,
955 #[serde(default)]
958 #[builder(default)]
959 pub replay_account_state: bool,
960}
961
962pub mod replay_support {
966 use super::{ActionKind, CreateSessionParams};
967
968 pub const NO_TRANSACTIONS: &str =
970 "Create the session without replayAccountState to execute and persist transactions.";
971
972 pub const SEND_TRANSACTION: &str = "Use simulateTransaction to run it against the replayed state, or create the session \
973 without replayAccountState.";
974
975 pub const SEND_ACTION: &str = "Use kind: simulate, which runs against the replayed state without committing, post state \
976 directly with an action carrying accountOverrides and no transactions, or create the \
977 session without replayAccountState.";
978
979 pub fn unsupported(operation: &str, remedy: &str) -> String {
981 format!(
982 "{operation} is not supported while account state replay is enabled: replay \
983 reconstructs account state from the recorded delta bundle instead of executing \
984 transactions, so this session never executes or persists any. {remedy}"
985 )
986 }
987
988 pub fn send_action_conflict(params: &CreateSessionParams) -> Option<String> {
991 (params.replay_account_state
992 && params
993 .actions
994 .iter()
995 .any(|action| action.kind == ActionKind::Send))
996 .then(|| unsupported("A scheduled action with kind: send", SEND_ACTION))
997 }
998}
999
1000impl CreateSessionParams {
1001 pub fn requires_sidecar(&self) -> bool {
1004 self.reroute.as_ref().is_some_and(|reroute| reroute.requote)
1005 }
1006
1007 pub fn reroute_order_flow(&self) -> bool {
1008 self.reroute.is_some()
1009 }
1010
1011 pub fn override_schedule(&self) -> Result<OverrideSchedule, String> {
1014 decode_override_actions(&self.actions)
1015 }
1016
1017 pub fn reroute_filter(&self) -> Option<&RerouteFilter> {
1019 self.reroute.as_ref()?.filter.as_ref()
1020 }
1021}
1022
1023#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1026#[serde(rename_all = "kebab-case")]
1027pub enum FailFastDivergenceKind {
1028 #[default]
1031 AnyNonBenign,
1032 Tracked,
1035}
1036
1037impl FailFastDivergenceKind {
1038 pub fn as_str(self) -> &'static str {
1041 match self {
1042 Self::AnyNonBenign => "any-non-benign",
1043 Self::Tracked => "tracked",
1044 }
1045 }
1046
1047 pub fn from_str_opt(value: &str) -> Option<Self> {
1049 match value {
1050 "any-non-benign" => Some(Self::AnyNonBenign),
1051 "tracked" => Some(Self::Tracked),
1052 _ => None,
1053 }
1054 }
1055}
1056
1057#[serde_with::serde_as]
1059#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1060pub struct AccountModifications(
1061 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
1062 #[serde(default)]
1063 pub BTreeMap<Address, AccountData>,
1064);
1065
1066const TOKEN_PROGRAM_IDS: [Address; 2] = [spl_token_interface::ID, spl_token_2022_interface::ID];
1067
1068impl From<BTreeMap<Address, AccountData>> for AccountModifications {
1069 fn from(value: BTreeMap<Address, AccountData>) -> Self {
1070 Self(value)
1071 }
1072}
1073
1074#[serde_with::serde_as]
1076#[derive(Debug, Serialize, Deserialize)]
1077#[serde(rename_all = "camelCase")]
1078pub struct ContinueParams {
1079 #[serde(default = "ContinueParams::default_advance_count")]
1080 pub advance_count: u64,
1082 #[serde(default)]
1083 pub transactions: Vec<String>,
1085 #[serde(default)]
1086 pub modify_account_states: AccountModifications,
1088}
1089
1090impl Default for ContinueParams {
1091 fn default() -> Self {
1092 Self {
1093 advance_count: Self::default_advance_count(),
1094 transactions: Vec::new(),
1095 modify_account_states: AccountModifications(BTreeMap::new()),
1096 }
1097 }
1098}
1099
1100impl ContinueParams {
1101 pub fn default_advance_count() -> u64 {
1102 1
1103 }
1104}
1105
1106#[derive(Debug, Clone, Serialize, Deserialize)]
1112#[serde(rename_all = "camelCase")]
1113pub struct PausedEvent {
1114 pub slot: u64,
1115 #[serde(default, skip_serializing_if = "Option::is_none")]
1116 pub batch_index: Option<u32>,
1117}
1118
1119#[serde_with::serde_as]
1128#[derive(Debug, Clone, Serialize, Deserialize)]
1129#[serde(rename_all = "camelCase")]
1130pub struct DiscoveryBatchEvent {
1131 pub slot: u64,
1132 pub batch_index: u32,
1133 pub matched: Vec<DiscoveryFilter>,
1135 pub transactions: Vec<EncodedBinary>,
1139}
1140
1141#[derive(Debug, Clone, Serialize, Deserialize)]
1143#[serde(rename_all = "camelCase")]
1144pub struct ContinueToParams {
1145 pub slot: u64,
1147 #[serde(default)]
1153 pub batch_index: Option<u32>,
1154}
1155
1156#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1158#[serde(rename_all = "lowercase")]
1159pub enum BinaryEncoding {
1160 Base64,
1161}
1162
1163impl BinaryEncoding {
1164 pub fn encode(self, bytes: &[u8]) -> String {
1165 match self {
1166 Self::Base64 => BASE64.encode(bytes),
1167 }
1168 }
1169
1170 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
1171 match self {
1172 Self::Base64 => BASE64.decode(data),
1173 }
1174 }
1175}
1176
1177#[derive(Debug, Clone, Serialize, Deserialize)]
1179#[serde(rename_all = "camelCase")]
1180pub struct EncodedBinary {
1181 pub data: String,
1183 pub encoding: BinaryEncoding,
1185}
1186
1187impl EncodedBinary {
1188 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
1189 Self { data, encoding }
1190 }
1191
1192 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
1193 Self {
1194 data: encoding.encode(bytes),
1195 encoding,
1196 }
1197 }
1198
1199 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
1200 self.encoding.decode(&self.data)
1201 }
1202}
1203
1204#[serde_with::serde_as]
1206#[derive(Debug, Clone, Serialize, Deserialize)]
1207#[serde(rename_all = "camelCase")]
1208pub struct AccountData {
1209 pub data: EncodedBinary,
1211 pub executable: bool,
1213 pub lamports: u64,
1215 #[serde_as(as = "serde_with::DisplayFromStr")]
1216 pub owner: Address,
1218 pub space: u64,
1220}
1221
1222impl AccountData {
1223 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
1224 Ok(solana_account::Account {
1225 data: self.data.decode()?,
1226 lamports: self.lamports,
1227 owner: self.owner,
1228 executable: self.executable,
1229 rent_epoch: 0,
1230 })
1231 }
1232}
1233
1234#[derive(Debug, Clone, Serialize, Deserialize)]
1236#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1237pub enum BacktestResponse {
1238 SessionCreated {
1239 session_id: SessionId,
1240 rpc_endpoint: String,
1241 #[serde(default, skip_serializing_if = "Option::is_none")]
1242 task_id: Option<String>,
1243 },
1244 SessionAttached {
1245 session_id: SessionId,
1246 rpc_endpoint: String,
1247 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 task_id: Option<String>,
1249 },
1250 SessionsCreatedV2 {
1251 control_session_id: ControlSessionId,
1252 session_ids: Vec<SessionId>,
1253 #[serde(default)]
1254 task_ids: Vec<Option<String>>,
1255 #[serde(default)]
1260 start_slots: Vec<u64>,
1261 #[serde(default)]
1262 end_slots: Vec<u64>,
1263 },
1264 ParallelSessionAttachedV2 {
1265 control_session_id: ControlSessionId,
1266 session_ids: Vec<SessionId>,
1267 #[serde(default)]
1268 task_ids: Vec<Option<String>>,
1269 },
1270 ReadyForContinue,
1271 SlotNotification(u64),
1272 Paused(PausedEvent),
1273 DiscoveryBatch(DiscoveryBatchEvent),
1274 Error(BacktestError),
1275 Success,
1276 Completed {
1277 #[serde(skip_serializing_if = "Option::is_none")]
1281 summary: Option<SessionSummary>,
1282 },
1283 Status {
1284 status: BacktestStatus,
1285 },
1286 SessionEventV2 {
1287 session_id: SessionId,
1288 seq_id: u64,
1289 event: SessionEventKind,
1290 },
1291}
1292
1293impl BacktestResponse {
1294 pub fn is_completed(&self) -> bool {
1295 matches!(self, BacktestResponse::Completed { .. })
1296 }
1297
1298 pub fn is_terminal(&self) -> bool {
1299 match self {
1300 BacktestResponse::Completed { .. } => true,
1301 BacktestResponse::Error(e) => e.is_terminal(),
1302 _ => false,
1303 }
1304 }
1305}
1306
1307impl From<BacktestStatus> for BacktestResponse {
1308 fn from(status: BacktestStatus) -> Self {
1309 Self::Status { status }
1310 }
1311}
1312
1313impl From<String> for BacktestResponse {
1314 fn from(message: String) -> Self {
1315 BacktestError::Internal { error: message }.into()
1316 }
1317}
1318
1319impl From<&str> for BacktestResponse {
1320 fn from(message: &str) -> Self {
1321 BacktestError::Internal {
1322 error: message.to_string(),
1323 }
1324 .into()
1325 }
1326}
1327
1328#[derive(Debug, Clone, Serialize, Deserialize)]
1329#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1330pub enum SessionEventKind {
1331 ReadyForContinue,
1332 SlotNotification(u64),
1333 Paused(PausedEvent),
1334 DiscoveryBatch(DiscoveryBatchEvent),
1335 Error(BacktestError),
1336 Success,
1337 Completed {
1338 #[serde(skip_serializing_if = "Option::is_none")]
1339 summary: Option<SessionSummary>,
1340 },
1341 Status {
1342 status: BacktestStatus,
1343 },
1344}
1345
1346impl SessionEventKind {
1347 pub fn is_terminal(&self) -> bool {
1348 match self {
1349 Self::Completed { .. } => true,
1350 Self::Error(e) => e.is_terminal(),
1351 _ => false,
1352 }
1353 }
1354}
1355
1356#[derive(Debug, Clone, Serialize, Deserialize)]
1359#[serde(rename_all = "camelCase")]
1360pub struct SequencedResponse {
1361 pub seq_id: u64,
1362 #[serde(flatten)]
1363 pub response: BacktestResponse,
1364}
1365
1366#[derive(Debug, Clone, Serialize, Deserialize)]
1368#[serde(rename_all = "camelCase")]
1369pub enum BacktestStatus {
1370 StartingRuntime,
1372 DecodedTransactions,
1373 AppliedAccountModifications,
1374 ReadyToExecuteUserTransactions,
1375 ExecutedUserTransactions,
1376 ExecutingBlockTransactions,
1377 ExecutedBlockTransactions,
1378 ProgramAccountsLoaded,
1379}
1380
1381impl std::fmt::Display for BacktestStatus {
1382 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1383 let s = match self {
1384 Self::StartingRuntime => "starting runtime",
1385 Self::DecodedTransactions => "decoded transactions",
1386 Self::AppliedAccountModifications => "applied account modifications",
1387 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
1388 Self::ExecutedUserTransactions => "executed user transactions",
1389 Self::ExecutingBlockTransactions => "executing block transactions",
1390 Self::ExecutedBlockTransactions => "executed block transactions",
1391 Self::ProgramAccountsLoaded => "program accounts loaded",
1392 };
1393 f.write_str(s)
1394 }
1395}
1396
1397#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1402#[serde(rename_all = "camelCase")]
1403pub struct RerouteStatsReport {
1404 #[serde(default)]
1406 pub swaps_detected: u64,
1407 #[serde(default)]
1411 pub swaps_filtered: u64,
1412 #[serde(default)]
1414 pub swaps_rerouted: u64,
1415 #[serde(default)]
1417 pub swaps_simulated: u64,
1418 #[serde(default)]
1420 pub swaps_succeeded: u64,
1421 #[serde(default)]
1423 pub requote_failures: u64,
1424 #[serde(default)]
1426 pub override_setup_failures: u64,
1427 #[serde(default)]
1429 pub metis_unavailable_blocks: u64,
1430 #[serde(default)]
1434 pub quote_attempts_unavailable: u64,
1435 #[serde(default)]
1437 pub direct_fill_matched: u64,
1438 #[serde(default)]
1440 pub direct_fill_built: u64,
1441 #[serde(default)]
1445 pub direct_fill_rejections: BTreeMap<String, u64>,
1446 #[serde(default)]
1448 pub direct_fill_outcomes: BTreeMap<String, u64>,
1449 #[serde(default)]
1451 pub direct_fill_scored: u64,
1452 #[serde(default)]
1455 pub direct_fill_bps_total: i64,
1456}
1457
1458#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1460#[serde(rename_all = "camelCase")]
1461pub struct SessionSummary {
1462 pub correct_simulation: usize,
1465 pub incorrect_simulation: usize,
1468 pub execution_errors: usize,
1470 pub balance_diff: usize,
1472 pub log_diff: usize,
1474 #[serde(default)]
1477 pub unsanitizable: usize,
1478 #[serde(default, skip_serializing_if = "Option::is_none")]
1481 pub reroute_stats: Option<Box<RerouteStatsReport>>,
1482}
1483
1484impl SessionSummary {
1485 pub fn has_deviations(&self) -> bool {
1487 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
1488 }
1489
1490 pub fn total_transactions(&self) -> usize {
1492 self.correct_simulation
1493 + self.incorrect_simulation
1494 + self.execution_errors
1495 + self.balance_diff
1496 + self.log_diff
1497 }
1498
1499 pub fn deviation_details(&self, balance_label: &str, log_label: &str) -> String {
1504 format!(
1505 "{} incorrect simulations, {} execution errors, {} {balance_label}, {} {log_label}",
1506 self.incorrect_simulation, self.execution_errors, self.balance_diff, self.log_diff,
1507 )
1508 }
1509}
1510
1511impl std::fmt::Display for SessionSummary {
1512 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1513 let total = self.total_transactions();
1514 write!(
1515 f,
1516 "Session summary: {total} transactions\n\
1517 \x20 - {} correct simulation\n\
1518 \x20 - {} incorrect simulation\n\
1519 \x20 - {} execution errors\n\
1520 \x20 - {} unsanitizable\n\
1521 \x20 - {} balance diffs\n\
1522 \x20 - {} log diffs",
1523 self.correct_simulation,
1524 self.incorrect_simulation,
1525 self.execution_errors,
1526 self.unsanitizable,
1527 self.balance_diff,
1528 self.log_diff,
1529 )
1530 }
1531}
1532
1533#[derive(Debug, Clone, Serialize, Deserialize)]
1535#[serde(rename_all = "camelCase")]
1536pub enum BacktestError {
1537 InvalidTransactionEncoding {
1538 index: usize,
1539 error: String,
1540 },
1541 InvalidTransactionFormat {
1542 index: usize,
1543 error: String,
1544 },
1545 InvalidAccountEncoding {
1546 address: String,
1547 encoding: BinaryEncoding,
1548 error: String,
1549 },
1550 InvalidAccountOwner {
1551 address: String,
1552 error: String,
1553 },
1554 InvalidAccountPubkey {
1555 address: String,
1556 error: String,
1557 },
1558 NoMoreBlocks,
1559 BundleNotFound {
1560 start_slot: u64,
1561 end_slot: u64,
1562 },
1563 AdvanceSlotFailed {
1564 slot: u64,
1565 error: String,
1566 },
1567 FinalizeSlotFailed {
1568 slot: u64,
1569 error: String,
1570 },
1571 InvalidRequest {
1572 error: String,
1573 },
1574 Internal {
1575 error: String,
1576 },
1577 InvalidBlockhashFormat {
1578 slot: u64,
1579 error: String,
1580 },
1581 InitializingSysvarsFailed {
1582 slot: u64,
1583 error: String,
1584 },
1585 ClerkError {
1586 error: String,
1587 },
1588 SimulationError {
1589 error: String,
1590 },
1591 SessionNotFound {
1592 session_id: String,
1593 },
1594 SessionOwnerMismatch,
1595 SessionOwnershipBusy {
1600 reason: String,
1601 },
1602 NoProducedBlocksInRange {
1608 start_slot: u64,
1609 end_slot: u64,
1610 },
1611}
1612
1613impl BacktestError {
1614 pub fn is_terminal(&self) -> bool {
1618 matches!(
1619 self,
1620 BacktestError::NoMoreBlocks
1621 | BacktestError::BundleNotFound { .. }
1622 | BacktestError::AdvanceSlotFailed { .. }
1623 | BacktestError::FinalizeSlotFailed { .. }
1624 | BacktestError::Internal { .. }
1625 | BacktestError::NoProducedBlocksInRange { .. }
1626 )
1627 }
1628}
1629
1630#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1632pub struct AvailableRange {
1633 pub bundle_start_slot: u64,
1634 pub bundle_start_slot_utc: Option<String>,
1635 pub max_bundle_end_slot: Option<u64>,
1636 pub max_bundle_end_slot_utc: Option<String>,
1637 pub max_bundle_size: Option<u64>,
1638 #[serde(default)]
1641 pub kind: Option<BundleKind>,
1642 #[serde(default)]
1645 pub scope: Option<RangeScope>,
1646}
1647
1648#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1650#[serde(rename_all = "snake_case")]
1651pub enum RangeScope {
1652 Global,
1654 User,
1656}
1657
1658impl RangeScope {
1659 pub fn as_str(self) -> &'static str {
1661 match self {
1662 Self::Global => "global",
1663 Self::User => "user",
1664 }
1665 }
1666}
1667
1668#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1670#[serde(rename_all = "snake_case")]
1671pub enum BundleKind {
1672 #[default]
1674 Blocks,
1675 AccountState,
1679}
1680
1681pub const REPLAY_RANGE_ADVICE: &str = "replayed state is resolved by the recording that begins \
1685 where the request does, so request a range that begins where one was recorded and ends \
1686 within it";
1687
1688impl BundleKind {
1689 fn coverage_noun(self) -> &'static str {
1691 match self {
1692 Self::Blocks => "bundle range",
1693 Self::AccountState => "account-state bundle you own",
1694 }
1695 }
1696
1697 fn coverage_nouns(self) -> &'static str {
1699 match self {
1700 Self::Blocks => "bundle ranges",
1701 Self::AccountState => "account-state bundles you own",
1702 }
1703 }
1704
1705 pub fn as_str(self) -> &'static str {
1707 match self {
1708 Self::Blocks => "blocks",
1709 Self::AccountState => "account_state",
1710 }
1711 }
1712
1713 pub fn from_str_opt(value: &str) -> Option<Self> {
1715 match value {
1716 "blocks" => Some(Self::Blocks),
1717 "account_state" => Some(Self::AccountState),
1718 _ => None,
1719 }
1720 }
1721}
1722
1723impl fmt::Display for BundleKind {
1726 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1727 let label = match self {
1728 Self::Blocks => "transactions",
1729 Self::AccountState => "account_state",
1730 };
1731 f.pad(label)
1732 }
1733}
1734
1735#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1739pub struct BundleBuildRequest {
1740 pub start_slot: u64,
1741 pub end_slot: u64,
1742 #[serde(default)]
1743 pub bundle_size: Option<u64>,
1744 #[serde(default)]
1747 pub idempotency_key: Option<String>,
1748 #[serde(default)]
1750 pub kind: BundleKind,
1751}
1752
1753#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1754#[serde(rename_all = "snake_case")]
1755pub enum BundleBuildStatus {
1756 Completed,
1758 Failed,
1760 InProgress,
1762 NeedsInvestigation,
1765}
1766
1767impl BundleBuildStatus {
1768 pub fn as_str(self) -> &'static str {
1770 match self {
1771 Self::Completed => "completed",
1772 Self::Failed => "failed",
1773 Self::InProgress => "in_progress",
1774 Self::NeedsInvestigation => "needs_investigation",
1775 }
1776 }
1777}
1778
1779#[derive(Debug, Clone, Serialize, Deserialize)]
1781pub struct BundleBuildStatusResponse {
1782 pub request_id: String,
1783 pub start_slot: u64,
1784 pub end_slot: u64,
1785 pub bundle_size: Option<u64>,
1786 pub status: BundleBuildStatus,
1787 pub created_at_unix_ms: u64,
1790 #[serde(default)]
1792 pub kind: BundleKind,
1793}
1794
1795#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1797pub struct CompletedBundle {
1798 pub start_slot: u64,
1799 pub end_slot: u64,
1800 pub kind: BundleKind,
1801 #[serde(default)]
1803 pub completed_at_unix_ms: Option<i64>,
1804 #[serde(default)]
1805 pub size_bytes: Option<i64>,
1806}
1807
1808#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1810pub struct BundleDeleteRequest {
1811 pub start_slot: u64,
1812 pub end_slot: u64,
1813 #[serde(default)]
1815 pub kind: BundleKind,
1816}
1817
1818#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1820pub struct BundleDeleteResponse {
1821 pub start_slot: u64,
1822 pub end_slot: u64,
1823 pub kind: BundleKind,
1824}
1825
1826#[derive(Debug, Clone, Serialize, Deserialize)]
1828pub struct BundleBuildListResponse {
1829 pub builds: Vec<BundleBuildStatusResponse>,
1832 #[serde(default)]
1837 pub bundles: Option<Vec<CompletedBundle>>,
1838}
1839
1840#[derive(Debug, Clone, Serialize, Deserialize)]
1843pub struct SnapshotSlotResponse {
1844 pub snapshot_slot: u64,
1845}
1846
1847pub fn split_range(
1870 ranges: &[AvailableRange],
1871 requested: SlotRange,
1872 kind: BundleKind,
1873) -> Result<Vec<(u64, u64)>, String> {
1874 let (requested_start, requested_end) = (requested.start(), requested.end());
1875
1876 let ends_by_start = ranges
1883 .iter()
1884 .filter(|r| r.kind.unwrap_or_default() == kind)
1885 .filter_map(|r| {
1886 r.max_bundle_end_slot
1887 .filter(|&end| end >= r.bundle_start_slot)
1888 .map(|end| (r.bundle_start_slot, end))
1889 })
1890 .fold(
1891 BTreeMap::<u64, BTreeSet<u64>>::new(),
1892 |mut ends_by_start, (start, end)| {
1893 ends_by_start.entry(start).or_default().insert(end);
1894 ends_by_start
1895 },
1896 );
1897
1898 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
1906 ends.iter()
1907 .next_back()
1908 .is_some_and(|&end| end >= requested_start)
1909 }) else {
1910 return Err(format!(
1911 "start_slot {requested_start} is not covered by any available {}",
1912 kind.coverage_noun()
1913 ));
1914 };
1915
1916 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
1922 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
1923 let mut best: Option<Vec<(u64, u64)>> = None;
1924 for &end in ends {
1925 let candidate = if end >= requested_end {
1926 Some(vec![(start, requested_end)])
1927 } else {
1928 best_from.get(&(end + 1)).map(|rest| {
1929 std::iter::once((start, end))
1930 .chain(rest.iter().copied())
1931 .collect()
1932 })
1933 };
1934 if let Some(candidate) = candidate
1935 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
1936 {
1937 best = Some(candidate);
1938 }
1939 }
1940 if let Some(best) = best {
1941 best_from.insert(start, best);
1942 }
1943 }
1944
1945 best_from.remove(&anchor_start).ok_or_else(|| {
1946 let mut covered_to = anchor_start.saturating_sub(1);
1950 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
1951 if start > covered_to.saturating_add(1) {
1952 break;
1953 }
1954 if let Some(&end) = ends.iter().next_back() {
1955 covered_to = covered_to.max(end);
1956 }
1957 }
1958 if covered_to < requested_end {
1959 format!("gap in coverage at slot {}", covered_to + 1)
1960 } else {
1961 format!(
1962 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available {}",
1963 kind.coverage_nouns()
1964 )
1965 }
1966 })
1967}
1968
1969impl From<BacktestError> for BacktestResponse {
1970 fn from(error: BacktestError) -> Self {
1971 Self::Error(error)
1972 }
1973}
1974
1975impl std::error::Error for BacktestError {}
1976
1977impl fmt::Display for BacktestError {
1978 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1979 match self {
1980 BacktestError::InvalidTransactionEncoding { index, error } => {
1981 write!(f, "invalid transaction encoding at index {index}: {error}")
1982 }
1983 BacktestError::InvalidTransactionFormat { index, error } => {
1984 write!(f, "invalid transaction format at index {index}: {error}")
1985 }
1986 BacktestError::InvalidAccountEncoding {
1987 address,
1988 encoding,
1989 error,
1990 } => write!(
1991 f,
1992 "invalid encoding for account {address} ({encoding:?}): {error}"
1993 ),
1994 BacktestError::InvalidAccountOwner { address, error } => {
1995 write!(f, "invalid owner for account {address}: {error}")
1996 }
1997 BacktestError::InvalidAccountPubkey { address, error } => {
1998 write!(f, "invalid account pubkey {address}: {error}")
1999 }
2000 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
2001 BacktestError::BundleNotFound {
2002 start_slot,
2003 end_slot,
2004 } => write!(
2005 f,
2006 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
2007 ),
2008 BacktestError::AdvanceSlotFailed { slot, error } => {
2009 write!(f, "failed to advance to slot {slot}: {error}")
2010 }
2011 BacktestError::FinalizeSlotFailed { slot, error } => {
2012 write!(f, "failed to finalize slot {slot}: {error}")
2013 }
2014 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
2015 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
2016 BacktestError::InvalidBlockhashFormat { slot, error } => {
2017 write!(f, "invalid blockhash at slot {slot}: {error}")
2018 }
2019 BacktestError::InitializingSysvarsFailed { slot, error } => {
2020 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
2021 }
2022 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
2023 BacktestError::SimulationError { error } => {
2024 write!(f, "simulation error: {error}")
2025 }
2026 BacktestError::SessionNotFound { session_id } => {
2027 write!(f, "session not found: {session_id}")
2028 }
2029 BacktestError::SessionOwnerMismatch => {
2030 write!(f, "session owner mismatch")
2031 }
2032 BacktestError::SessionOwnershipBusy { reason } => {
2033 write!(f, "session ownership busy: {reason}")
2034 }
2035 BacktestError::NoProducedBlocksInRange {
2036 start_slot,
2037 end_slot,
2038 } => write!(
2039 f,
2040 "no produced blocks in requested range {start_slot}..={end_slot}; \
2041 slot(s) may be skipped on-chain"
2042 ),
2043 }
2044 }
2045}
2046
2047#[cfg(test)]
2048mod tests {
2049 use rstest::rstest;
2050 use solana_account::ReadableAccount;
2051
2052 use super::*;
2053
2054 fn address(byte: u8) -> Address {
2055 Address::from([byte; 32])
2056 }
2057
2058 #[rstest]
2059 #[case::one("jupiter", Some(&[SwapAggregator::Jupiter][..]))]
2060 #[case::several("jupiter,okx", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2061 #[case::spaced_and_uppercased(" Jupiter , OKX ", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2062 #[case::repeats_collapse("okx,okx", Some(&[SwapAggregator::Okx][..]))]
2063 #[case::trailing_separator("okx,", Some(&[SwapAggregator::Okx][..]))]
2064 #[case::unknown("jupiter,bogus", None)]
2065 #[case::empty("", None)]
2066 #[case::separators_only(",,", None)]
2067 fn reroute_aggregators_parse_the_shapes_a_flag_carries(
2068 #[case] input: &str,
2069 #[case] expected: Option<&[SwapAggregator]>,
2070 ) {
2071 assert_eq!(
2072 input.parse::<RerouteAggregators>().ok(),
2073 expected.map(|aggregators| RerouteAggregators::new(aggregators.iter().copied()))
2074 );
2075 }
2076
2077 #[test]
2079 fn reroute_aggregators_round_trip_through_their_rendering() {
2080 let aggregators = RerouteAggregators::new([SwapAggregator::Dflow, SwapAggregator::Jupiter]);
2081
2082 assert_eq!(aggregators.to_string(), "jupiter,dflow");
2083 assert_eq!(
2084 aggregators
2085 .to_string()
2086 .parse::<RerouteAggregators>()
2087 .unwrap(),
2088 aggregators
2089 );
2090 assert_eq!(RerouteAggregators::default().to_string(), "jupiter");
2091 }
2092
2093 #[test]
2096 fn aggregators_deserialize_through_the_same_parser_the_flag_uses() {
2097 let params: CreateSessionParams = serde_json::from_value(
2098 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {"aggregators": ["Jupiter", "OKX"]}}),
2099 )
2100 .expect("decode");
2101
2102 assert_eq!(
2103 params.reroute.expect("rerouting requested").aggregators,
2104 RerouteAggregators::new([SwapAggregator::Jupiter, SwapAggregator::Okx])
2105 );
2106 }
2107
2108 #[test]
2111 fn params_without_the_new_reroute_fields_decode_to_the_narrow_defaults() {
2112 let params: CreateSessionParams = serde_json::from_value(
2113 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {}}),
2114 )
2115 .expect("decode");
2116
2117 let reroute = params.reroute.expect("rerouting requested");
2118 assert_eq!(reroute.aggregators, RerouteAggregators::default());
2119 assert!(reroute.aggregators.contains(SwapAggregator::Jupiter));
2120 assert!(!reroute.aggregators.contains(SwapAggregator::Okx));
2121 assert!(!reroute.circular_arbs);
2122 assert!(!reroute.detect_failed_l1_swaps);
2123 }
2124
2125 #[test]
2127 fn a_request_without_reroute_reserves_no_sidecar() {
2128 let params: CreateSessionParams =
2129 serde_json::from_value(serde_json::json!({"startSlot": 1, "endSlot": 2}))
2130 .expect("decode");
2131
2132 assert!(!params.requires_sidecar());
2133 }
2134
2135 #[test]
2140 fn a_per_slot_action_schedule_compresses_across_actions() {
2141 let slots = 2_000u64;
2142 let actions = (0..slots)
2143 .map(|slot| ScheduledAction {
2144 anchor: ActionAnchor::AfterSlot { slots: vec![slot] },
2145 kind: ActionKind::Simulate,
2146 feeds_reroute: false,
2147 transactions: Vec::new(),
2148 account_overrides: AccountModifications(BTreeMap::from([(
2149 address(7),
2150 AccountData {
2151 data: EncodedBinary::new(
2153 BASE64.encode(
2154 [&slot.to_le_bytes()[..], &[0xAB; 2040][..]]
2155 .concat()
2156 .as_slice(),
2157 ),
2158 BinaryEncoding::Base64,
2159 ),
2160 executable: false,
2161 lamports: 15_144_966,
2162 owner: address(9),
2163 space: 2048,
2164 },
2165 )])),
2166 return_accounts: Vec::new(),
2167 label: None,
2168 })
2169 .collect::<Vec<_>>();
2170 let params = CreateSessionParams::builder()
2171 .start_slot(1)
2172 .end_slot(slots)
2173 .actions(actions.clone())
2174 .build();
2175
2176 let encoded = serde_json::to_string(¶ms).expect("serialize");
2177 let raw = serde_json::to_string(&actions).expect("serialize raw actions");
2178 assert!(
2179 encoded.len() * 8 < raw.len(),
2180 "expected the schedule to compress well; got {} bytes from {} raw",
2181 encoded.len(),
2182 raw.len()
2183 );
2184
2185 let decoded: CreateSessionParams = serde_json::from_str(&encoded).expect("round trip");
2186 assert_eq!(
2187 serde_json::to_string(&decoded.actions).expect("serialize decoded"),
2188 raw
2189 );
2190 }
2191
2192 #[test]
2195 fn an_empty_action_list_is_omitted_entirely() {
2196 let params = CreateSessionParams::builder()
2197 .start_slot(1)
2198 .end_slot(2)
2199 .build();
2200 let json = serde_json::to_value(¶ms).expect("serialize");
2201 assert!(json.get("actions").is_none(), "{json}");
2202 let decoded: CreateSessionParams = serde_json::from_value(json).expect("round trip");
2203 assert!(decoded.actions.is_empty());
2204 }
2205
2206 #[test]
2209 fn send_actions_conflict_with_replay_only_together() {
2210 let params = |replay, kind| {
2211 CreateSessionParams::builder()
2212 .start_slot(1)
2213 .end_slot(2)
2214 .replay_account_state(replay)
2215 .actions(vec![ScheduledAction {
2216 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2217 kind,
2218 feeds_reroute: false,
2219 transactions: vec!["dGVzdA==".to_string()],
2220 account_overrides: Default::default(),
2221 return_accounts: Default::default(),
2222 label: None,
2223 }])
2224 .build()
2225 };
2226 assert!(replay_support::send_action_conflict(¶ms(false, ActionKind::Send)).is_none());
2227 assert!(
2228 replay_support::send_action_conflict(¶ms(true, ActionKind::Simulate)).is_none()
2229 );
2230 let mut publishing = params(true, ActionKind::Simulate);
2231 publishing.actions[0].feeds_reroute = true;
2232 assert!(replay_support::send_action_conflict(&publishing).is_none());
2233
2234 let conflict = replay_support::send_action_conflict(¶ms(true, ActionKind::Send))
2235 .expect("send under replay conflicts");
2236 assert!(conflict.contains("kind: send"), "{conflict}");
2237 assert!(conflict.contains(replay_support::SEND_ACTION), "{conflict}");
2238 }
2239
2240 fn override_entry(owner: Address, executable: bool) -> AccountData {
2241 AccountData {
2242 data: EncodedBinary::from_bytes(&[1, 2, 3], BinaryEncoding::Base64),
2243 executable,
2244 lamports: 1,
2245 owner,
2246 space: 3,
2247 }
2248 }
2249
2250 fn before() -> ActionAnchor {
2251 ActionAnchor::BeforeSlot { slots: vec![1] }
2252 }
2253
2254 fn valid() -> ScheduledAction {
2255 override_action(before(), address(3), override_entry(address(4), false))
2256 }
2257
2258 fn override_action(
2259 anchor: ActionAnchor,
2260 account: Address,
2261 data: AccountData,
2262 ) -> ScheduledAction {
2263 ScheduledAction {
2264 anchor,
2265 kind: ActionKind::default(),
2266 transactions: Vec::new(),
2267 account_overrides: AccountModifications(BTreeMap::from([(account, data)])),
2268 return_accounts: Vec::new(),
2269 label: None,
2270 feeds_reroute: true,
2271 }
2272 }
2273
2274 #[test]
2275 fn override_action_round_trips() {
2276 let key = address(1);
2277 let entry = override_entry(address(2), false);
2278 let action = override_action(
2279 ActionAnchor::BeforeSlot { slots: vec![5, 9] },
2280 key,
2281 entry.clone(),
2282 );
2283 let json = serde_json::to_value(&action).unwrap();
2284 assert!(
2285 json["anchor"]["slots"].is_string(),
2286 "slots travel as a compressed blob: {json}"
2287 );
2288 let back: ScheduledAction = serde_json::from_value(json).unwrap();
2289 assert_eq!(back.anchor, ActionAnchor::BeforeSlot { slots: vec![5, 9] });
2290 assert!(back.feeds_reroute);
2291 assert!(!back.is_computed_override(), "no transactions to simulate");
2292 let survived = &back.account_overrides.0[&key];
2293 assert_eq!(survived.lamports, entry.lamports);
2294 assert_eq!(survived.data.data, entry.data.data);
2295 }
2296
2297 #[rstest]
2298 #[case::executable(override_action(before(), address(3), override_entry(address(10), true)))]
2299 #[case::program_owned(override_action(
2300 before(),
2301 address(3),
2302 override_entry(solana_account::PROGRAM_OWNERS[0], false),
2303 ))]
2304 #[case::token_owned(override_action(
2305 before(),
2306 address(3),
2307 override_entry(TOKEN_PROGRAM_IDS[0], false),
2308 ))]
2309 #[case::after_slot_anchor(override_action(
2310 ActionAnchor::AfterSlot { slots: vec![1] },
2311 address(3),
2312 override_entry(address(4), false),
2313 ))]
2314 #[case::nothing_to_post(ScheduledAction {
2315 account_overrides: AccountModifications::default(),
2316 ..valid()
2317 })]
2318 #[case::send(ScheduledAction {
2320 kind: ActionKind::Send,
2321 ..valid()
2322 })]
2323 #[allow(deprecated, reason = "exercise the loader-owner rejection path")]
2324 fn override_actions_reject_invalid_accounts_and_shapes(#[case] action: ScheduledAction) {
2325 assert!(decode_override_actions(&[action]).is_err());
2326 }
2327
2328 #[test]
2330 fn override_actions_accept_a_static_and_a_computed_shape() {
2331 assert!(decode_override_actions(&[valid()]).is_ok());
2332 assert!(
2335 decode_override_actions(&[ScheduledAction {
2336 transactions: vec!["dGVzdA==".to_string()],
2337 ..valid()
2338 }])
2339 .is_ok()
2340 );
2341 }
2342
2343 #[test]
2344 fn override_actions_merge_account_wise_later_wins() {
2345 let entry = |lamports| AccountData {
2346 lamports,
2347 ..override_entry(address(9), false)
2348 };
2349 let slot_action = |slots, account, lamports| {
2350 override_action(ActionAnchor::BeforeSlot { slots }, account, entry(lamports))
2351 };
2352 let actions = [
2353 slot_action(vec![5, 7], address(1), 1),
2354 slot_action(vec![5], address(2), 2),
2355 slot_action(vec![5], address(1), 3),
2356 ScheduledAction {
2357 anchor: ActionAnchor::AfterSlot { slots: vec![5] },
2358 kind: ActionKind::Simulate,
2359 feeds_reroute: false,
2360 transactions: vec!["dGVzdA==".to_string()],
2361 account_overrides: AccountModifications::default(),
2362 return_accounts: Vec::new(),
2363 label: None,
2364 },
2365 ];
2366 let schedule = decode_override_actions(&actions).unwrap();
2367 assert_eq!(schedule.slots().collect::<Vec<_>>(), vec![5, 7]);
2368 let lamports_at = |key, slot| {
2369 schedule
2370 .active_for(&key, slot)
2371 .map(|account| account.lamports())
2372 };
2373 assert_eq!(lamports_at(address(1), 5), Some(3));
2374 assert_eq!(lamports_at(address(2), 5), Some(2));
2375 assert_eq!(lamports_at(address(1), 7), Some(1));
2376 }
2377
2378 #[test]
2379 fn computed_override_resolution_preserves_action_order_and_failure() {
2380 let key = address(1);
2381 let static_action = |lamports| {
2382 override_action(
2383 ActionAnchor::BeforeSlot { slots: vec![5] },
2384 key,
2385 AccountData {
2386 lamports,
2387 ..override_entry(address(9), false)
2388 },
2389 )
2390 };
2391 let computed_action = || ScheduledAction {
2392 transactions: vec!["dGVzdA==".to_string()],
2393 ..static_action(2)
2394 };
2395
2396 let computed_last =
2397 decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2398 computed_last
2399 .resolve(5, 1, Some(HashMap::from([(key, shared_account(2))])))
2400 .unwrap();
2401 assert_eq!(
2402 computed_last.active_for(&key, 5).map(|a| a.lamports()),
2403 Some(2)
2404 );
2405
2406 let static_last = decode_override_actions(&[computed_action(), static_action(3)]).unwrap();
2407 static_last
2408 .resolve(5, 0, Some(HashMap::from([(key, shared_account(2))])))
2409 .unwrap();
2410 assert_eq!(
2411 static_last.active_for(&key, 5).map(|a| a.lamports()),
2412 Some(3)
2413 );
2414
2415 let failed = decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2416 failed.resolve(5, 1, None).unwrap();
2417 assert_eq!(failed.active_for(&key, 5).map(|a| a.lamports()), Some(1));
2418 assert!(failed.resolve(5, 1, None).is_err(), "resolving twice");
2419 }
2420
2421 fn shared_account(lamports: u64) -> AccountSharedData {
2422 AccountSharedData::from(solana_account::Account {
2423 lamports,
2424 data: vec![1],
2425 owner: address(9),
2426 executable: false,
2427 rent_epoch: 0,
2428 })
2429 }
2430
2431 fn carry_forward_schedule() -> (Address, Address, OverrideSchedule) {
2432 let (a, b) = (address(1), address(2));
2433 let schedule = OverrideSchedule::from(BTreeMap::from([
2434 (10, HashMap::from([(a, shared_account(1))])),
2435 (
2436 20,
2437 HashMap::from([(a, shared_account(2)), (b, shared_account(9))]),
2438 ),
2439 ]));
2440 (a, b, schedule)
2441 }
2442
2443 #[test]
2444 fn override_schedule_resolves_carry_forward_per_account() {
2445 let (a, b, schedule) = carry_forward_schedule();
2446
2447 assert!(schedule.active_for(&a, 9).is_none());
2448 let cases = [
2449 (9, None, None),
2450 (10, Some(1), None),
2451 (15, Some(1), None),
2452 (20, Some(2), Some(9)),
2453 (u64::MAX, Some(2), Some(9)),
2454 ];
2455 for (slot, want_a, want_b) in cases {
2456 assert_eq!(
2457 schedule
2458 .active_for(&a, slot)
2459 .map(|account| account.lamports()),
2460 want_a,
2461 "active_for a at {slot}"
2462 );
2463 let active = schedule.active_at(slot);
2464 assert_eq!(
2465 active.get(&a).map(ReadableAccount::lamports),
2466 want_a,
2467 "active_at a at {slot}"
2468 );
2469 assert_eq!(
2470 active.get(&b).map(ReadableAccount::lamports),
2471 want_b,
2472 "active_at b at {slot}"
2473 );
2474 }
2475 }
2476
2477 #[test]
2478 fn newly_active_yields_final_values_in_window() {
2479 let (a, b, schedule) = carry_forward_schedule();
2480
2481 let newly: HashMap<_, _> = schedule.newly_active(Some(10), 20).into_iter().collect();
2482 assert_eq!(newly.len(), 2);
2483 assert_eq!(newly[&a].lamports(), 2);
2484 assert_eq!(newly[&b].lamports(), 9);
2485 let spanning: HashMap<_, _> = schedule.newly_active(None, 20).into_iter().collect();
2486 assert_eq!(spanning[&a].lamports(), 2, "later slot wins");
2487 assert_eq!(schedule.newly_active(None, 15).len(), 1);
2488 }
2489
2490 #[test]
2491 fn reroute_filter_round_trips_camel_case() {
2492 let filter = RerouteFilter {
2493 pairs: BTreeSet::from([MintPair::new(address(1), address(2))]),
2494 };
2495 let json = serde_json::to_value(&filter).unwrap();
2496 assert!(
2497 json["pairs"][0][0].is_string() && json["pairs"][0][1].is_string(),
2498 "a pair serializes as two base58 strings: {json}"
2499 );
2500 let back: RerouteFilter = serde_json::from_value(json).unwrap();
2501 assert_eq!(back, filter);
2502 }
2503
2504 #[test]
2507 fn mint_pairs_are_normalized_however_they_are_built() {
2508 let (one, other) = (address(1), address(2));
2509 assert_eq!(MintPair::new(one, other), MintPair::new(other, one));
2510
2511 let reversed: MintPair =
2512 serde_json::from_value(serde_json::json!([other.to_string(), one.to_string()]))
2513 .unwrap();
2514 assert_eq!(reversed, MintPair::new(one, other));
2515 }
2516
2517 #[test]
2518 fn fail_fast_divergence_kind_str_round_trips() {
2519 for kind in [
2520 FailFastDivergenceKind::AnyNonBenign,
2521 FailFastDivergenceKind::Tracked,
2522 ] {
2523 assert_eq!(
2524 FailFastDivergenceKind::from_str_opt(kind.as_str()),
2525 Some(kind)
2526 );
2527 }
2528 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
2529 assert_eq!(
2530 FailFastDivergenceKind::default(),
2531 FailFastDivergenceKind::AnyNonBenign
2532 );
2533 }
2534
2535 #[test]
2536 fn bundle_not_found_is_terminal_and_descriptive() {
2537 let error = BacktestError::BundleNotFound {
2538 start_slot: 100,
2539 end_slot: 600,
2540 };
2541
2542 assert!(error.is_terminal());
2543 assert_eq!(
2544 error.to_string(),
2545 "block bundle not found for start_slot=100, end_slot=600"
2546 );
2547 }
2548
2549 #[test]
2550 fn no_produced_blocks_is_terminal_and_descriptive() {
2551 let error = BacktestError::NoProducedBlocksInRange {
2552 start_slot: 100,
2553 end_slot: 100,
2554 };
2555
2556 assert!(error.is_terminal());
2557 assert_eq!(
2560 error.to_string(),
2561 "no produced blocks in requested range 100..=100; slot(s) may be skipped on-chain"
2562 );
2563 }
2564
2565 #[test]
2566 fn bundle_build_request_optional_fields_default_to_none() {
2567 let req: BundleBuildRequest =
2568 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
2569 assert_eq!((req.start_slot, req.end_slot), (1, 2));
2570 assert_eq!(req.bundle_size, None);
2571 assert_eq!(req.idempotency_key, None);
2572 }
2573
2574 #[test]
2575 fn bundle_build_request_parses_optional_fields() {
2576 let req: BundleBuildRequest = serde_json::from_str(
2577 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
2578 )
2579 .expect("parse");
2580 assert_eq!(req.bundle_size, Some(500));
2581 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
2582 }
2583
2584 #[test]
2585 fn bundle_build_status_serde_round_trips_with_snake_case() {
2586 let cases = [
2587 (BundleBuildStatus::Completed, "\"completed\""),
2588 (BundleBuildStatus::Failed, "\"failed\""),
2589 (BundleBuildStatus::InProgress, "\"in_progress\""),
2590 (
2591 BundleBuildStatus::NeedsInvestigation,
2592 "\"needs_investigation\"",
2593 ),
2594 ];
2595 for (status, expected) in cases {
2596 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
2597 assert_eq!(
2598 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
2599 status
2600 );
2601 assert_eq!(status.as_str(), expected.trim_matches('"'));
2602 }
2603 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
2604 }
2605
2606 #[test]
2607 fn bundle_build_request_without_kind_deserializes_as_blocks() {
2608 let request: BundleBuildRequest =
2610 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
2611 assert_eq!(request.kind, BundleKind::Blocks);
2612 }
2613
2614 #[test]
2615 fn bundle_kind_round_trips_over_serde_and_as_str() {
2616 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
2617 let json = serde_json::to_string(&kind).unwrap();
2618 assert_eq!(json, format!("\"{}\"", kind.as_str()));
2619 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
2620 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
2623 }
2624 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
2625 assert_eq!(BundleKind::from_str_opt("accountState"), None);
2626 }
2627
2628 #[test]
2629 fn bundle_build_request_carries_kind_through_serde() {
2630 let request = BundleBuildRequest {
2631 start_slot: 100,
2632 end_slot: 200,
2633 kind: BundleKind::AccountState,
2634 ..Default::default()
2635 };
2636 let json = serde_json::to_string(&request).unwrap();
2637 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2638 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
2639 assert_eq!(parsed.kind, BundleKind::AccountState);
2640 }
2641
2642 #[test]
2643 fn bundle_build_status_response_serializes_request_and_status() {
2644 let response = BundleBuildStatusResponse {
2645 request_id: "r".to_string(),
2646 start_slot: 100,
2647 end_slot: 200,
2648 bundle_size: Some(50),
2649 status: BundleBuildStatus::InProgress,
2650 created_at_unix_ms: 0,
2651 kind: BundleKind::Blocks,
2652 };
2653 let json = serde_json::to_value(&response).unwrap();
2654 assert_eq!(json["request_id"].as_str(), Some("r"));
2655 assert_eq!(json["start_slot"].as_u64(), Some(100));
2656 assert_eq!(json["bundle_size"].as_u64(), Some(50));
2657 assert_eq!(json["status"].as_str(), Some("in_progress"));
2658 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
2659 }
2660
2661 #[test]
2662 fn an_empty_bundles_list_is_distinct_from_an_absent_one() {
2663 let absent: BundleBuildListResponse = serde_json::from_str(r#"{"builds":[]}"#).unwrap();
2665 let empty: BundleBuildListResponse =
2666 serde_json::from_str(r#"{"builds":[],"bundles":[]}"#).unwrap();
2667 assert_eq!(absent.bundles, None);
2668 assert_eq!(empty.bundles, Some(Vec::new()));
2669 }
2670
2671 #[test]
2672 fn completed_bundle_round_trips() {
2673 let bundle = CompletedBundle {
2674 start_slot: 100,
2675 end_slot: 200,
2676 kind: BundleKind::AccountState,
2677 completed_at_unix_ms: Some(1_700_000_000_000),
2678 size_bytes: Some(4_096),
2679 };
2680 let json = serde_json::to_string(&bundle).unwrap();
2681 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2682 assert_eq!(
2683 serde_json::from_str::<CompletedBundle>(&json).unwrap(),
2684 bundle
2685 );
2686
2687 let sparse = r#"{"start_slot":1,"end_slot":2,"kind":"blocks"}"#;
2688 let parsed: CompletedBundle = serde_json::from_str(sparse).unwrap();
2689 assert_eq!(parsed.completed_at_unix_ms, None);
2690 assert_eq!(parsed.size_bytes, None);
2691 }
2692
2693 #[test]
2694 fn available_range_reads_a_server_that_predates_kind_and_scope() {
2695 let json = r#"{
2697 "bundle_start_slot": 100,
2698 "bundle_start_slot_utc": null,
2699 "max_bundle_end_slot": 200,
2700 "max_bundle_end_slot_utc": null,
2701 "max_bundle_size": 101
2702 }"#;
2703 let parsed: AvailableRange = serde_json::from_str(json).unwrap();
2704 assert_eq!(parsed.bundle_start_slot, 100);
2705 assert_eq!(parsed.kind, None);
2706 assert_eq!(parsed.scope, None);
2707 }
2708
2709 #[test]
2710 fn available_range_ignores_fields_it_does_not_know() {
2711 let json = r#"{"bundle_start_slot": 1, "some_future_field": "x"}"#;
2713 assert_eq!(
2714 serde_json::from_str::<AvailableRange>(json)
2715 .unwrap()
2716 .bundle_start_slot,
2717 1
2718 );
2719 }
2720
2721 #[test]
2722 fn available_range_labels_round_trip() {
2723 let range = AvailableRange {
2724 bundle_start_slot: 100,
2725 max_bundle_end_slot: Some(200),
2726 kind: Some(BundleKind::AccountState),
2727 scope: Some(RangeScope::User),
2728 ..Default::default()
2729 };
2730 let json = serde_json::to_string(&range).unwrap();
2731 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2732 assert!(json.contains(r#""scope":"user""#), "{json}");
2733 let parsed: AvailableRange = serde_json::from_str(&json).unwrap();
2734 assert_eq!(parsed.kind, Some(BundleKind::AccountState));
2735 assert_eq!(parsed.scope, Some(RangeScope::User));
2736 }
2737
2738 fn range(start: u64, end: u64) -> AvailableRange {
2739 AvailableRange {
2740 bundle_start_slot: start,
2741 max_bundle_end_slot: Some(end),
2742 ..Default::default()
2743 }
2744 }
2745
2746 #[rstest::rstest]
2750 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
2751 #[case::multi(
2752 vec![range(100, 200), range(201, 300), range(301, 400)],
2753 100, 300, Some(vec![(100, 200), (201, 300)])
2754 )]
2755 #[case::nested(
2758 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
2759 100, 900, Some(vec![(100, 500), (501, 900)])
2760 )]
2761 #[case::prefers_finer_grid(
2765 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
2766 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
2767 )]
2768 #[case::shared_start_prefers_finer(
2772 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
2773 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
2774 )]
2775 #[case::coarse_overlap_prefers_finer_pair(
2779 vec![range(100, 200), range(100, 150), range(151, 160)],
2780 100, 160, Some(vec![(100, 150), (151, 160)])
2781 )]
2782 #[case::falls_back_to_coarse(
2786 vec![range(100, 160), range(100, 120), range(121, 140)],
2787 100, 160, Some(vec![(100, 160)])
2788 )]
2789 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
2791 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
2794 #[case::anchors_then_continues(
2795 vec![range(150, 350), range(351, 600)],
2796 200, 600, Some(vec![(150, 350), (351, 600)])
2797 )]
2798 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
2799 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
2801 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
2803 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
2804 #[case::user_and_global_ranges_not_collapsed(
2809 vec![range(100, 200), range(100, 150), range(151, 200)],
2810 100, 200, Some(vec![(100, 150), (151, 200)])
2811 )]
2812 fn split_range_cases(
2813 #[case] ranges: Vec<AvailableRange>,
2814 #[case] start: u64,
2815 #[case] end: u64,
2816 #[case] expected: Option<Vec<(u64, u64)>>,
2817 ) {
2818 match expected {
2819 Some(expected) => assert_eq!(
2820 split_range(
2821 &ranges,
2822 SlotRange::new_unchecked(start, end),
2823 BundleKind::Blocks
2824 )
2825 .unwrap(),
2826 expected
2827 ),
2828 None => assert!(
2829 split_range(
2830 &ranges,
2831 SlotRange::new_unchecked(start, end),
2832 BundleKind::Blocks
2833 )
2834 .is_err()
2835 ),
2836 }
2837 }
2838
2839 fn account_state_range(start: u64, end: u64) -> AvailableRange {
2840 AvailableRange {
2841 kind: Some(BundleKind::AccountState),
2842 ..range(start, end)
2843 }
2844 }
2845
2846 #[test]
2847 fn a_replay_split_rides_the_recordings_not_a_finer_block_grid() {
2848 let ranges = vec![
2851 range(100, 149),
2852 range(150, 199),
2853 account_state_range(100, 199),
2854 ];
2855 let requested = SlotRange::new_unchecked(100, 199);
2856
2857 assert_eq!(
2858 split_range(&ranges, requested, BundleKind::Blocks).unwrap(),
2859 [(100, 149), (150, 199)]
2860 );
2861 assert_eq!(
2862 split_range(&ranges, requested, BundleKind::AccountState).unwrap(),
2863 [(100, 199)]
2864 );
2865 }
2866
2867 #[test]
2868 fn a_replay_split_tiles_consecutive_recordings() {
2869 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
2870 assert_eq!(
2871 split_range(
2872 &ranges,
2873 SlotRange::new_unchecked(100, 299),
2874 BundleKind::AccountState
2875 )
2876 .unwrap(),
2877 [(100, 199), (200, 299)]
2878 );
2879 }
2880
2881 #[test]
2882 fn a_replay_split_ends_short_of_a_recording() {
2883 let requested = SlotRange::new_unchecked(100, 150);
2886 assert_eq!(
2887 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
2888 [(100, 150)]
2889 );
2890 assert_eq!(
2891 split_range(
2892 &[account_state_range(100, 199)],
2893 requested,
2894 BundleKind::AccountState
2895 )
2896 .unwrap(),
2897 [(100, 150)]
2898 );
2899 }
2900
2901 #[test]
2902 fn a_replay_split_ends_short_of_its_last_recording() {
2903 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
2906 assert_eq!(
2907 split_range(
2908 &ranges,
2909 SlotRange::new_unchecked(100, 250),
2910 BundleKind::AccountState
2911 )
2912 .unwrap(),
2913 [(100, 199), (200, 250)]
2914 );
2915 }
2916
2917 #[test]
2918 fn a_replay_split_tiles_a_one_slot_recording() {
2919 let ranges = vec![account_state_range(100, 249), account_state_range(250, 250)];
2922 assert_eq!(
2923 split_range(
2924 &ranges,
2925 SlotRange::new_unchecked(100, 250),
2926 BundleKind::AccountState
2927 )
2928 .unwrap(),
2929 [(100, 249), (250, 250)]
2930 );
2931 }
2932
2933 #[test]
2934 fn a_replay_split_anchors_a_mid_recording_start() {
2935 let requested = SlotRange::new_unchecked(150, 199);
2939 assert_eq!(
2940 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
2941 [(100, 199)]
2942 );
2943 assert_eq!(
2944 split_range(
2945 &[account_state_range(100, 199)],
2946 requested,
2947 BundleKind::AccountState
2948 )
2949 .unwrap(),
2950 [(100, 199)]
2951 );
2952 }
2953
2954 #[test]
2955 fn a_replay_split_refuses_a_start_no_recording_covers() {
2956 let error = split_range(
2957 &[account_state_range(200, 299)],
2958 SlotRange::new_unchecked(100, 299),
2959 BundleKind::AccountState,
2960 )
2961 .expect_err("no recording covers 100");
2962 assert!(
2963 error.contains("is not covered by any available account-state bundle"),
2964 "{error}"
2965 );
2966 }
2967
2968 #[test]
2969 fn a_replay_split_ignores_block_ranges() {
2970 let error = split_range(
2971 &[range(100, 199)],
2972 SlotRange::new_unchecked(100, 199),
2973 BundleKind::AccountState,
2974 )
2975 .expect_err("owns no recordings");
2976 assert!(error.contains("account-state bundle"), "{error}");
2977 }
2978
2979 #[test]
2980 fn blocks_mode_admits_ranges_from_a_server_that_predates_kind() {
2981 let unlabelled = range(100, 199);
2984 assert_eq!(unlabelled.kind, None);
2985 assert_eq!(
2986 split_range(
2987 &[unlabelled],
2988 SlotRange::new_unchecked(100, 150),
2989 BundleKind::Blocks
2990 )
2991 .unwrap(),
2992 [(100, 150)]
2993 );
2994 }
2995
2996 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
2999 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
3000 for r in ranges {
3001 if let Some(end) = r.max_bundle_end_slot
3002 && end >= r.bundle_start_slot
3003 {
3004 ends.entry(r.bundle_start_slot).or_default().insert(end);
3005 }
3006 }
3007 ends
3008 }
3009
3010 fn reference_max_split(
3014 ends: &BTreeMap<u64, BTreeSet<u64>>,
3015 cursor: u64,
3016 end: u64,
3017 ) -> Option<Vec<(u64, u64)>> {
3018 ends.get(&cursor)?
3019 .iter()
3020 .filter_map(|&bundle_end| {
3021 if bundle_end >= end {
3022 Some(vec![(cursor, end)])
3023 } else {
3024 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
3025 rest.insert(0, (cursor, bundle_end));
3026 rest
3027 })
3028 }
3029 })
3030 .max_by_key(Vec::len)
3031 }
3032
3033 fn is_valid_split(
3037 split: &[(u64, u64)],
3038 ends: &BTreeMap<u64, BTreeSet<u64>>,
3039 start: u64,
3040 end: u64,
3041 ) -> bool {
3042 split.first().is_some_and(|&(s, _)| s == start)
3043 && split.last().is_some_and(|&(_, e)| e == end)
3044 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
3045 && split.iter().all(|&(s, e)| {
3046 e >= s
3047 && ends
3048 .get(&s)
3049 .and_then(|bundle_ends| bundle_ends.iter().next_back())
3050 .is_some_and(|&max_end| e <= max_end)
3051 })
3052 }
3053
3054 #[test]
3059 fn split_range_matches_reference() {
3060 let mut seed: u64 = 0x9E3779B97F4A7C15;
3061 let mut next = || {
3062 seed = seed
3063 .wrapping_mul(6364136223846793005)
3064 .wrapping_add(1442695040888963407);
3065 seed >> 33
3066 };
3067
3068 for _ in 0..50_000 {
3069 let ranges: Vec<AvailableRange> = (0..next() % 6)
3072 .map(|_| {
3073 let start = next() % 12;
3074 range(start, start + next() % 6) })
3076 .collect();
3077 let start = next() % 12;
3078 let end = start + next() % 6; let got = split_range(
3081 &ranges,
3082 SlotRange::new_unchecked(start, end),
3083 BundleKind::Blocks,
3084 );
3085 let ends = ends_by_start(&ranges);
3086 let anchor = ends
3089 .range(..=start)
3090 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
3091 .map(|(&s, _)| s);
3092 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
3093
3094 let layout: Vec<_> = ranges
3095 .iter()
3096 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
3097 .collect();
3098 match (&got, &reference) {
3099 (Ok(split), Some(best)) => {
3100 assert!(
3101 is_valid_split(split, &ends, anchor.unwrap(), end),
3102 "invalid split {split:?} for {layout:?} [{start},{end}]"
3103 );
3104 assert_eq!(
3105 split.len(),
3106 best.len(),
3107 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
3108 );
3109 }
3110 (Err(_), None) => {}
3111 _ => panic!(
3112 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
3113 ),
3114 }
3115 }
3116 }
3117}