1use std::{
2 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
3 fmt,
4 num::NonZeroU64,
5 str::FromStr,
6 sync::Arc,
7};
8
9use base64::{
10 DecodeError as Base64DecodeError, Engine as _, engine::general_purpose::STANDARD as BASE64,
11};
12use parking_lot::RwLock;
13use serde::{Deserialize, Serialize};
14use solana_account::AccountSharedData;
15use solana_address::{Address, error::ParseAddressError};
16
17mod session_id;
18mod session_type;
19mod slot_range;
20
21pub use direct_fill::{
22 AMOUNT_WIDTH, DirectFillTemplate, ParseDirectFillTemplateError, TakerSlots, TemplateAccount,
23 TemplateInstruction,
24};
25pub use reroute_report::SwapMode;
26pub use session_id::{ControlSessionId, SessionId};
27pub use session_type::SessionType;
28pub use slot_range::{InvertedSlotRange, SlotRange};
29
30pub mod client_identity;
31pub mod direct_fill;
32pub mod geyser_route;
33pub mod mints;
34pub mod replacement;
35pub mod reroute_report;
36pub mod route_plan;
37pub mod subscribe_config;
38pub mod usage;
39pub mod ws_compression;
40
41#[derive(Debug, Serialize, Deserialize)]
43#[serde(tag = "method", content = "params", rename_all = "camelCase")]
44pub enum BacktestRequest {
45 CreateBacktestSession(CreateBacktestSessionRequest),
46 Continue(ContinueParams),
47 ContinueTo(ContinueToParams),
48 ContinueSessionV1(ContinueSessionRequestV1),
49 ContinueToSessionV1(ContinueToSessionRequestV1),
50 CloseBacktestSession,
51 CloseSessionV1(CloseSessionRequestV1),
52 AttachBacktestSession {
53 session_id: SessionId,
54 last_sequence: Option<u64>,
57 },
58 ResumeAttachedSession,
61 AttachParallelControlSessionV2 {
62 control_session_id: ControlSessionId,
63 #[serde(default)]
67 last_sequences: BTreeMap<SessionId, u64>,
68 },
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(untagged)]
77pub enum CreateBacktestSessionRequest {
78 V1(CreateBacktestSessionRequestV1),
79 V0(CreateSessionParams),
80}
81
82impl CreateBacktestSessionRequest {
83 pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
84 match self {
85 Self::V0(request) => CreateBacktestSessionRequestOptions {
86 request,
87 parallel: false,
88 },
89 Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
90 CreateBacktestSessionRequestOptions { request, parallel }
91 }
92 }
93 }
94
95 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
96 let options = self.into_request_options();
97 (options.request, options.parallel)
98 }
99}
100
101impl From<CreateSessionParams> for CreateBacktestSessionRequest {
102 fn from(value: CreateSessionParams) -> Self {
103 Self::V0(value)
104 }
105}
106
107impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
108 fn from(value: CreateBacktestSessionRequestV1) -> Self {
109 Self::V1(value)
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct CreateBacktestSessionRequestV1 {
116 #[serde(flatten)]
117 pub request: CreateSessionParams,
118 pub parallel: bool,
119}
120
121#[derive(Debug, Clone)]
122pub struct CreateBacktestSessionRequestOptions {
123 pub request: CreateSessionParams,
124 pub parallel: bool,
125}
126
127#[derive(Debug, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct ContinueSessionRequestV1 {
130 pub session_id: SessionId,
131 pub request: ContinueParams,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct ContinueToSessionRequestV1 {
137 pub session_id: SessionId,
138 pub request: ContinueToParams,
139}
140
141#[derive(Debug, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct CloseSessionRequestV1 {
144 pub session_id: SessionId,
145}
146
147#[serde_with::serde_as]
154#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
155#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
156pub enum DiscoveryFilter {
157 ProgramExecuted(#[serde_as(as = "serde_with::DisplayFromStr")] Address),
159}
160
161pub struct TxMatchContext<'a> {
166 pub invoked_programs: &'a HashSet<Address>,
168}
169
170impl DiscoveryFilter {
171 pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
174 match self {
175 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
176 }
177 }
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
184#[serde(try_from = "[String; 2]", into = "[String; 2]")]
185pub struct MintPair([Address; 2]);
186
187impl MintPair {
188 pub fn new(one: Address, other: Address) -> Self {
189 Self(if one <= other {
190 [one, other]
191 } else {
192 [other, one]
193 })
194 }
195
196 pub fn mints(&self) -> (Address, Address) {
197 (self.0[0], self.0[1])
198 }
199}
200
201#[derive(Debug, thiserror::Error)]
202#[error("invalid mint {mint} in pair")]
203pub struct ParseMintPairError {
204 mint: String,
205 source: ParseAddressError,
206}
207
208impl TryFrom<[String; 2]> for MintPair {
209 type Error = ParseMintPairError;
210
211 fn try_from([one, other]: [String; 2]) -> Result<Self, Self::Error> {
212 fn parse(mint: String) -> Result<Address, ParseMintPairError> {
213 Address::from_str(&mint).map_err(|source| ParseMintPairError { mint, source })
214 }
215 Ok(Self::new(parse(one)?, parse(other)?))
216 }
217}
218
219impl From<MintPair> for [String; 2] {
220 fn from(pair: MintPair) -> Self {
221 let (one, other) = pair.mints();
222 [one.to_string(), other.to_string()]
223 }
224}
225
226#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct RerouteFilter {
231 #[serde(default)]
233 pub pairs: BTreeSet<MintPair>,
234}
235
236impl RerouteFilter {
237 pub fn is_empty(&self) -> bool {
240 self.pairs.is_empty()
241 }
242
243 pub fn matches_mints(&self, input: &Address, output: &Address) -> bool {
244 self.pairs.contains(&MintPair::new(*input, *output))
245 }
246
247 pub fn mentions_mint(&self, mint: &Address) -> bool {
250 self.pairs.iter().any(|pair| {
251 let (one, other) = pair.mints();
252 one == *mint || other == *mint
253 })
254 }
255}
256
257#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
260#[serde(rename_all = "camelCase")]
261pub enum ActionKind {
262 #[default]
266 Simulate,
267 Send,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(tag = "at", rename_all = "camelCase")]
277pub enum ActionAnchor {
278 BeforeSlot {
281 #[serde(default, with = "compressed")]
282 slots: Vec<u64>,
283 },
284 AfterSlot {
287 #[serde(default, with = "compressed")]
288 slots: Vec<u64>,
289 },
290 #[serde(rename_all = "camelCase")]
295 BeforeEverySlot {
296 #[serde(default = "every_slot")]
297 every_n_slots: NonZeroU64,
298 },
299 #[serde(rename_all = "camelCase")]
303 AfterEverySlot {
304 #[serde(default = "every_slot")]
305 every_n_slots: NonZeroU64,
306 },
307 BeforeMatch { filter: DiscoveryFilter },
310 AfterMatch { filter: DiscoveryFilter },
313}
314
315const fn every_slot() -> NonZeroU64 {
316 NonZeroU64::MIN
317}
318
319const MAX_ACTION_TRANSACTIONS_BYTES: u64 = 64 * 1024 * 1024;
322
323mod compressed {
328 use std::io::Read;
329
330 use base64::Engine as _;
331 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
332
333 use super::{BASE64, MAX_ACTION_TRANSACTIONS_BYTES};
334
335 const COMPRESSION_LEVEL: i32 = 0;
337
338 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
339 where
340 T: Serialize,
341 S: Serializer,
342 {
343 let json = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
344 let compressed = zstd::stream::encode_all(json.as_slice(), COMPRESSION_LEVEL)
345 .map_err(serde::ser::Error::custom)?;
346 serializer.serialize_str(&BASE64.encode(compressed))
347 }
348
349 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
350 where
351 T: DeserializeOwned,
352 D: Deserializer<'de>,
353 {
354 let blob = String::deserialize(deserializer)?;
355 let compressed = BASE64
356 .decode(blob.as_bytes())
357 .map_err(serde::de::Error::custom)?;
358 let mut json = Vec::new();
359 zstd::stream::read::Decoder::new(compressed.as_slice())
360 .and_then(|d| Read::take(d, MAX_ACTION_TRANSACTIONS_BYTES).read_to_end(&mut json))
361 .map_err(serde::de::Error::custom)?;
362 serde_json::from_slice(&json).map_err(serde::de::Error::custom)
363 }
364}
365
366#[serde_with::serde_as]
374#[derive(Debug, Clone, Serialize, Deserialize)]
375#[serde(rename_all = "camelCase")]
376pub struct ScheduledAction {
377 pub anchor: ActionAnchor,
378 #[serde(default)]
380 pub kind: ActionKind,
381 #[serde(default, with = "compressed")]
391 pub transactions: Vec<String>,
392 #[serde(default)]
397 pub account_overrides: AccountModifications,
398 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
408 pub feeds_reroute: bool,
409 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
413 #[serde(default)]
414 pub return_accounts: Vec<Address>,
415 #[serde(default)]
417 pub label: Option<String>,
418}
419
420impl ScheduledAction {
421 pub fn is_computed_override(&self) -> bool {
424 self.feeds_reroute && !self.transactions.is_empty()
425 }
426}
427
428pub const MAX_DIAGNOSTIC_MARKETS: usize = 64;
429pub const MAX_DIAGNOSTIC_USD_VALUES: usize = 16;
430
431#[serde_with::serde_as]
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
433#[serde(rename_all = "camelCase")]
434pub struct DiagnosticProbeParams {
435 pub anchor: ActionAnchor,
436 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
437 pub markets: Vec<Address>,
438 pub usd_values: Vec<u32>,
439 #[serde(default)]
440 pub label: Option<String>,
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
444pub enum DiagnosticProbeError {
445 #[error("a diagnostic probe names no markets")]
446 NoMarkets,
447 #[error("a diagnostic probe names no usdValues")]
448 NoUsdValues,
449 #[error("a diagnostic probe names {got} markets, over the ceiling of {MAX_DIAGNOSTIC_MARKETS}")]
450 TooManyMarkets { got: usize },
451 #[error(
452 "a diagnostic probe names {got} usdValues, over the ceiling of {MAX_DIAGNOSTIC_USD_VALUES}"
453 )]
454 TooManyUsdValues { got: usize },
455 #[error("a diagnostic probe names usdValue 0, which the router reads as no size at all")]
456 ZeroUsdValue,
457 #[error("a diagnostic probe names market {market} more than once")]
458 DuplicateMarket { market: Address },
459 #[error(
460 "a diagnostic probe anchors at {anchor}; probing the router is an await, and only the \
461 slot-end hook has a point for one, so only afterSlot and afterEverySlot are supported"
462 )]
463 UnsupportedAnchor { anchor: &'static str },
464}
465
466impl DiagnosticProbeParams {
467 pub fn fires(&self) -> usize {
469 self.markets.len() * self.usd_values.len()
470 }
471
472 pub fn validate(&self) -> Result<(), DiagnosticProbeError> {
473 if self.markets.is_empty() {
474 return Err(DiagnosticProbeError::NoMarkets);
475 }
476 if self.usd_values.is_empty() {
477 return Err(DiagnosticProbeError::NoUsdValues);
478 }
479 if self.markets.len() > MAX_DIAGNOSTIC_MARKETS {
480 return Err(DiagnosticProbeError::TooManyMarkets {
481 got: self.markets.len(),
482 });
483 }
484 if self.usd_values.len() > MAX_DIAGNOSTIC_USD_VALUES {
485 return Err(DiagnosticProbeError::TooManyUsdValues {
486 got: self.usd_values.len(),
487 });
488 }
489 if self.usd_values.contains(&0) {
490 return Err(DiagnosticProbeError::ZeroUsdValue);
491 }
492 let mut seen = HashSet::with_capacity(self.markets.len());
493 if let Some(market) = self.markets.iter().find(|market| !seen.insert(**market)) {
494 return Err(DiagnosticProbeError::DuplicateMarket { market: *market });
495 }
496 match &self.anchor {
497 ActionAnchor::AfterSlot { .. } | ActionAnchor::AfterEverySlot { .. } => Ok(()),
498 ActionAnchor::BeforeSlot { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
499 anchor: "beforeSlot",
500 }),
501 ActionAnchor::BeforeEverySlot { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
502 anchor: "beforeEverySlot",
503 }),
504 ActionAnchor::BeforeMatch { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
505 anchor: "beforeMatch",
506 }),
507 ActionAnchor::AfterMatch { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
508 anchor: "afterMatch",
509 }),
510 }
511 }
512}
513
514#[derive(Debug, Clone)]
516enum OverrideContribution {
517 Static(HashMap<Address, AccountSharedData>),
518 Pending,
519 Resolved(HashMap<Address, AccountSharedData>),
520 Failed,
521}
522
523#[derive(Debug, Clone, Default)]
524struct OverrideScheduleInner {
525 entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>,
526 keys: HashSet<Address>,
530}
531
532impl OverrideScheduleInner {
533 fn new(entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>) -> Self {
534 let keys = entries
535 .values()
536 .flatten()
537 .filter_map(|(_, contribution)| match contribution {
538 OverrideContribution::Static(accounts)
539 | OverrideContribution::Resolved(accounts) => Some(accounts.keys().copied()),
540 OverrideContribution::Pending | OverrideContribution::Failed => None,
541 })
542 .flatten()
543 .collect();
544 Self { entries, keys }
545 }
546}
547
548#[derive(Debug, Clone, Default)]
551pub struct OverrideSchedule {
552 inner: Arc<RwLock<OverrideScheduleInner>>,
553}
554
555impl From<BTreeMap<u64, HashMap<Address, AccountSharedData>>> for OverrideSchedule {
556 fn from(entries: BTreeMap<u64, HashMap<Address, AccountSharedData>>) -> Self {
557 Self {
558 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(
559 entries
560 .into_iter()
561 .map(|(slot, accounts)| {
562 (slot, vec![(0, OverrideContribution::Static(accounts))])
563 })
564 .collect(),
565 ))),
566 }
567 }
568}
569
570impl OverrideSchedule {
571 pub fn is_empty(&self) -> bool {
572 self.inner.read().entries.is_empty()
573 }
574
575 pub fn slots(&self) -> impl Iterator<Item = u64> + '_ {
577 self.inner
578 .read()
579 .entries
580 .keys()
581 .copied()
582 .collect::<Vec<_>>()
583 .into_iter()
584 }
585
586 pub fn active_at(&self, slot: u64) -> HashMap<Address, AccountSharedData> {
588 self.inner.read().entries.range(..=slot).fold(
589 HashMap::new(),
590 |mut active, (_, contributions)| {
591 active.extend(fold_contributions(contributions));
592 active
593 },
594 )
595 }
596
597 pub fn active_for(&self, key: &Address, slot: u64) -> Option<AccountSharedData> {
598 let inner = self.inner.read();
599 if !inner.keys.contains(key) {
600 return None;
601 }
602 inner
603 .entries
604 .range(..=slot)
605 .rev()
606 .find_map(|(_, contributions)| {
607 contributions
608 .iter()
609 .rev()
610 .find_map(|(_, contribution)| match contribution {
611 OverrideContribution::Static(accounts)
612 | OverrideContribution::Resolved(accounts) => accounts.get(key).cloned(),
613 OverrideContribution::Pending | OverrideContribution::Failed => None,
614 })
615 })
616 }
617
618 pub fn newly_active(
620 &self,
621 after: Option<u64>,
622 up_to: u64,
623 ) -> Vec<(Address, AccountSharedData)> {
624 let lower = after.map_or(std::ops::Bound::Unbounded, std::ops::Bound::Excluded);
625 self.inner
626 .read()
627 .entries
628 .range((lower, std::ops::Bound::Included(up_to)))
629 .flat_map(|(_, contributions)| fold_contributions(contributions))
630 .collect::<HashMap<_, _>>()
631 .into_iter()
632 .collect()
633 }
634
635 pub fn resolve(
638 &self,
639 slot: u64,
640 action_index: u32,
641 accounts: Option<HashMap<Address, AccountSharedData>>,
642 ) -> Result<(), String> {
643 let mut inner = self.inner.write();
644 let OverrideScheduleInner { entries, keys } = &mut *inner;
645 let contribution = entries
646 .get_mut(&slot)
647 .and_then(|contributions| {
648 contributions
649 .iter_mut()
650 .find(|(index, _)| *index == action_index)
651 .map(|(_, contribution)| contribution)
652 })
653 .ok_or_else(|| {
654 format!("override action {action_index} has no contribution at slot {slot}")
655 })?;
656 if !matches!(contribution, OverrideContribution::Pending) {
657 return Err(format!(
658 "override action {action_index} at slot {slot} is not pending"
659 ));
660 }
661 *contribution = match accounts {
662 Some(accounts) => {
663 keys.extend(accounts.keys().copied());
664 OverrideContribution::Resolved(accounts)
665 }
666 None => OverrideContribution::Failed,
667 };
668 Ok(())
669 }
670}
671
672fn fold_contributions(
673 contributions: &[(u32, OverrideContribution)],
674) -> HashMap<Address, AccountSharedData> {
675 let mut accounts = HashMap::new();
676 for (_, contribution) in contributions {
677 match contribution {
678 OverrideContribution::Static(next) | OverrideContribution::Resolved(next) => {
679 accounts.extend(next.iter().map(|(key, account)| (*key, account.clone())));
680 }
681 OverrideContribution::Pending | OverrideContribution::Failed => {}
682 }
683 }
684 accounts
685}
686
687fn decode_override_actions(actions: &[ScheduledAction]) -> Result<OverrideSchedule, String> {
696 let mut entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>> = BTreeMap::new();
697 for (action_index, action) in actions.iter().enumerate().filter(|(_, action)| {
698 action.feeds_reroute
699 || (action.transactions.is_empty() && !action.account_overrides.0.is_empty())
700 }) {
701 if action.kind == ActionKind::Send {
703 return Err("account overrides are not valid for kind: send".to_string());
704 }
705 if action.transactions.is_empty() && action.account_overrides.0.is_empty() {
706 return Err(
707 "feedsReroute needs something to feed: transactions to simulate, or \
708 accountOverrides"
709 .to_string(),
710 );
711 }
712 let ActionAnchor::BeforeSlot { slots } = &action.anchor else {
713 return Err("scheduled account overrides require a beforeSlot anchor".to_string());
714 };
715 let accounts = decode_account_modifications(&action.account_overrides)?;
716 let contribution = if action.transactions.is_empty() {
717 OverrideContribution::Static(accounts)
718 } else {
719 OverrideContribution::Pending
720 };
721 for slot in slots.iter().copied().collect::<BTreeSet<_>>() {
724 entries
725 .entry(slot)
726 .or_default()
727 .push((action_index as u32, contribution.clone()));
728 }
729 }
730 Ok(OverrideSchedule {
731 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(entries))),
732 })
733}
734
735#[allow(
739 deprecated,
740 reason = "`PROGRAM_OWNERS` remains the canonical loader-owner list"
741)]
742pub fn decode_account_modifications(
743 modifications: &AccountModifications,
744) -> Result<HashMap<Address, AccountSharedData>, String> {
745 modifications
746 .0
747 .iter()
748 .map(|(address, data)| {
749 let account = data
750 .to_account()
751 .map_err(|error| format!("invalid account data for {address}: {error}"))?;
752 if account.executable || solana_account::PROGRAM_OWNERS.contains(&account.owner) {
753 return Err(format!(
754 "overriding executable/program accounts is not supported (account {address})"
755 ));
756 }
757 Ok((*address, AccountSharedData::from(account)))
758 })
759 .collect()
760}
761
762pub const MAX_DISCONNECT_TIMEOUT_SECS: u16 = 900;
766
767#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
770#[serde(rename_all = "lowercase", try_from = "String")]
771pub enum SwapAggregator {
772 Jupiter,
773 Okx,
774 Titan,
775 Dflow,
776}
777
778impl SwapAggregator {
779 pub const fn as_str(self) -> &'static str {
781 match self {
782 Self::Jupiter => "jupiter",
783 Self::Okx => "okx",
784 Self::Titan => "titan",
785 Self::Dflow => "dflow",
786 }
787 }
788}
789
790impl std::fmt::Display for SwapAggregator {
791 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
792 f.write_str(self.as_str())
793 }
794}
795
796#[derive(Debug, thiserror::Error)]
797pub enum ParseAggregatorError {
798 #[error("unknown swap aggregator {0:?}; expected one of jupiter, okx, titan, dflow")]
799 Unknown(String),
800 #[error("name at least one aggregator (jupiter, okx, titan, dflow)")]
802 NoAggregator,
803}
804
805impl TryFrom<String> for SwapAggregator {
806 type Error = ParseAggregatorError;
807
808 fn try_from(value: String) -> Result<Self, Self::Error> {
809 value.parse()
810 }
811}
812
813impl std::str::FromStr for SwapAggregator {
814 type Err = ParseAggregatorError;
815
816 fn from_str(s: &str) -> Result<Self, Self::Err> {
817 match s.trim().to_ascii_lowercase().as_str() {
818 "jupiter" => Ok(Self::Jupiter),
819 "okx" => Ok(Self::Okx),
820 "titan" => Ok(Self::Titan),
821 "dflow" => Ok(Self::Dflow),
822 _ => Err(ParseAggregatorError::Unknown(s.to_string())),
823 }
824 }
825}
826
827#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
831#[serde(transparent)]
832pub struct RerouteAggregators(BTreeSet<SwapAggregator>);
833
834impl Default for RerouteAggregators {
835 fn default() -> Self {
836 Self([SwapAggregator::Jupiter].into_iter().collect())
837 }
838}
839
840impl RerouteAggregators {
841 pub fn new(aggregators: impl IntoIterator<Item = SwapAggregator>) -> Self {
842 Self(aggregators.into_iter().collect())
843 }
844
845 pub fn contains(&self, aggregator: SwapAggregator) -> bool {
846 self.0.contains(&aggregator)
847 }
848
849 pub fn is_empty(&self) -> bool {
850 self.0.is_empty()
851 }
852}
853
854impl std::fmt::Display for RerouteAggregators {
855 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
856 let mut aggregators = self.0.iter();
857 if let Some(first) = aggregators.next() {
858 write!(f, "{first}")?;
859 }
860 aggregators.try_for_each(|aggregator| write!(f, ",{aggregator}"))
861 }
862}
863
864impl std::str::FromStr for RerouteAggregators {
865 type Err = ParseAggregatorError;
866
867 fn from_str(s: &str) -> Result<Self, Self::Err> {
868 let aggregators: BTreeSet<SwapAggregator> = s
869 .split(',')
870 .filter(|aggregator| !aggregator.trim().is_empty())
871 .map(str::parse)
872 .collect::<Result<_, _>>()?;
873 if aggregators.is_empty() {
874 return Err(ParseAggregatorError::NoAggregator);
875 }
876 Ok(Self(aggregators))
877 }
878}
879
880#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
888#[serde(rename_all = "camelCase")]
889pub struct RerouteParams {
890 #[serde(default)]
891 pub aggregators: RerouteAggregators,
892 #[serde(default, skip_serializing_if = "Option::is_none")]
895 pub filter: Option<RerouteFilter>,
896 #[serde(default)]
903 pub detect_failed_l1_swaps: bool,
904 #[serde(default)]
907 pub circular_arbs: bool,
908 #[serde(default = "RerouteParams::default_requote")]
914 pub requote: bool,
915 #[serde(default, skip_serializing_if = "Vec::is_empty")]
924 pub direct_fill: Vec<DirectFillTemplate>,
925}
926
927impl Default for RerouteParams {
930 fn default() -> Self {
931 Self {
932 aggregators: RerouteAggregators::default(),
933 filter: None,
934 detect_failed_l1_swaps: false,
935 circular_arbs: false,
936 requote: Self::default_requote(),
937 direct_fill: Vec::new(),
938 }
939 }
940}
941
942impl std::str::FromStr for MintPair {
944 type Err = String;
945
946 fn from_str(s: &str) -> Result<Self, Self::Err> {
947 let (base, quote) = s
948 .split_once(',')
949 .ok_or_else(|| format!("expected two comma-separated mints, got {s:?}"))?;
950 Self::try_from([base.trim().to_owned(), quote.trim().to_owned()])
951 .map_err(|error| error.to_string())
952 }
953}
954
955impl std::fmt::Display for MintPair {
956 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
957 let (one, other) = self.mints();
958 write!(f, "{one},{other}")
959 }
960}
961
962impl RerouteParams {
963 const fn default_requote() -> bool {
964 true
965 }
966
967 pub const fn is_noop(&self) -> bool {
970 !self.requote && self.direct_fill.is_empty()
971 }
972}
973
974#[serde_with::serde_as]
976#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
977#[serde(rename_all = "camelCase")]
978pub struct CreateSessionParams {
979 pub start_slot: u64,
981 pub end_slot: u64,
983 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
984 #[serde(default)]
985 #[builder(default)]
986 pub signer_filter: BTreeSet<Address>,
988 #[serde(default)]
991 #[builder(default)]
992 pub send_summary: bool,
993 #[serde(default, skip_serializing_if = "Option::is_none")]
995 pub reroute: Option<RerouteParams>,
996 #[serde(default)]
999 pub capacity_wait_timeout_secs: Option<u16>,
1000 #[serde(default)]
1004 pub disconnect_timeout_secs: Option<u16>,
1005 #[serde(default)]
1010 pub extra_compute_units: Option<u32>,
1011 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1018 #[builder(default)]
1019 pub discoveries: Vec<DiscoveryFilter>,
1020 #[serde(default, with = "compressed", skip_serializing_if = "Vec::is_empty")]
1029 #[builder(default)]
1030 pub actions: Vec<ScheduledAction>,
1031 #[serde(default)]
1034 #[builder(default)]
1035 pub replay_account_state: bool,
1036 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1037 #[builder(default)]
1038 pub diagnostic_probes: Vec<DiagnosticProbeParams>,
1039}
1040
1041pub mod replay_support {
1045 use super::{ActionKind, CreateSessionParams};
1046
1047 pub const NO_TRANSACTIONS: &str =
1049 "Create the session without replayAccountState to execute and persist transactions.";
1050
1051 pub const SEND_TRANSACTION: &str = "Use simulateTransaction to run it against the replayed state, or create the session \
1052 without replayAccountState.";
1053
1054 pub const SEND_ACTION: &str = "Use kind: simulate, which runs against the replayed state without committing, post state \
1055 directly with an action carrying accountOverrides and no transactions, or create the \
1056 session without replayAccountState.";
1057
1058 pub fn unsupported(operation: &str, remedy: &str) -> String {
1060 format!(
1061 "{operation} is not supported while account state replay is enabled: replay \
1062 reconstructs account state from the recorded delta bundle instead of executing \
1063 transactions, so this session never executes or persists any. {remedy}"
1064 )
1065 }
1066
1067 pub fn send_action_conflict(params: &CreateSessionParams) -> Option<String> {
1070 (params.replay_account_state
1071 && params
1072 .actions
1073 .iter()
1074 .any(|action| action.kind == ActionKind::Send))
1075 .then(|| unsupported("A scheduled action with kind: send", SEND_ACTION))
1076 }
1077}
1078
1079impl CreateSessionParams {
1080 pub fn requires_sidecar(&self) -> bool {
1083 self.reroute.as_ref().is_some_and(|reroute| reroute.requote)
1084 || !self.diagnostic_probes.is_empty()
1085 }
1086
1087 pub fn requires_diagnostic_endpoint(&self) -> bool {
1091 !self.diagnostic_probes.is_empty()
1092 }
1093
1094 pub fn validate_diagnostic_probes(&self) -> Result<(), DiagnosticProbeError> {
1095 self.diagnostic_probes
1096 .iter()
1097 .try_for_each(DiagnosticProbeParams::validate)
1098 }
1099
1100 pub fn warms_account_universe(&self) -> bool {
1102 self.requires_sidecar()
1103 || self
1104 .reroute
1105 .as_ref()
1106 .is_some_and(|reroute| !reroute.direct_fill.is_empty())
1107 }
1108
1109 pub fn reroute_order_flow(&self) -> bool {
1110 self.reroute.is_some()
1111 }
1112
1113 pub fn override_schedule(&self) -> Result<OverrideSchedule, String> {
1116 decode_override_actions(&self.actions)
1117 }
1118
1119 pub fn reroute_filter(&self) -> Option<&RerouteFilter> {
1121 self.reroute.as_ref()?.filter.as_ref()
1122 }
1123}
1124
1125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1128#[serde(rename_all = "kebab-case")]
1129pub enum FailFastDivergenceKind {
1130 #[default]
1133 AnyNonBenign,
1134 Tracked,
1137}
1138
1139impl FailFastDivergenceKind {
1140 pub fn as_str(self) -> &'static str {
1143 match self {
1144 Self::AnyNonBenign => "any-non-benign",
1145 Self::Tracked => "tracked",
1146 }
1147 }
1148
1149 pub fn from_str_opt(value: &str) -> Option<Self> {
1151 match value {
1152 "any-non-benign" => Some(Self::AnyNonBenign),
1153 "tracked" => Some(Self::Tracked),
1154 _ => None,
1155 }
1156 }
1157}
1158
1159#[serde_with::serde_as]
1161#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1162pub struct AccountModifications(
1163 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
1164 #[serde(default)]
1165 pub BTreeMap<Address, AccountData>,
1166);
1167
1168impl From<BTreeMap<Address, AccountData>> for AccountModifications {
1169 fn from(value: BTreeMap<Address, AccountData>) -> Self {
1170 Self(value)
1171 }
1172}
1173
1174#[serde_with::serde_as]
1176#[derive(Debug, Serialize, Deserialize)]
1177#[serde(rename_all = "camelCase")]
1178pub struct ContinueParams {
1179 #[serde(default = "ContinueParams::default_advance_count")]
1180 pub advance_count: u64,
1182 #[serde(default)]
1183 pub transactions: Vec<String>,
1185 #[serde(default)]
1186 pub modify_account_states: AccountModifications,
1188}
1189
1190impl Default for ContinueParams {
1191 fn default() -> Self {
1192 Self {
1193 advance_count: Self::default_advance_count(),
1194 transactions: Vec::new(),
1195 modify_account_states: AccountModifications(BTreeMap::new()),
1196 }
1197 }
1198}
1199
1200impl ContinueParams {
1201 pub fn default_advance_count() -> u64 {
1202 1
1203 }
1204}
1205
1206#[derive(Debug, Clone, Serialize, Deserialize)]
1212#[serde(rename_all = "camelCase")]
1213pub struct PausedEvent {
1214 pub slot: u64,
1215 #[serde(default, skip_serializing_if = "Option::is_none")]
1216 pub batch_index: Option<u32>,
1217}
1218
1219#[serde_with::serde_as]
1228#[derive(Debug, Clone, Serialize, Deserialize)]
1229#[serde(rename_all = "camelCase")]
1230pub struct DiscoveryBatchEvent {
1231 pub slot: u64,
1232 pub batch_index: u32,
1233 pub matched: Vec<DiscoveryFilter>,
1235 pub transactions: Vec<EncodedBinary>,
1239}
1240
1241#[derive(Debug, Clone, Serialize, Deserialize)]
1243#[serde(rename_all = "camelCase")]
1244pub struct ContinueToParams {
1245 pub slot: u64,
1247 #[serde(default)]
1253 pub batch_index: Option<u32>,
1254}
1255
1256#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1258#[serde(rename_all = "lowercase")]
1259pub enum BinaryEncoding {
1260 Base64,
1261}
1262
1263impl BinaryEncoding {
1264 pub fn encode(self, bytes: &[u8]) -> String {
1265 match self {
1266 Self::Base64 => BASE64.encode(bytes),
1267 }
1268 }
1269
1270 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
1271 match self {
1272 Self::Base64 => BASE64.decode(data),
1273 }
1274 }
1275}
1276
1277#[derive(Debug, Clone, Serialize, Deserialize)]
1279#[serde(rename_all = "camelCase")]
1280pub struct EncodedBinary {
1281 pub data: String,
1283 pub encoding: BinaryEncoding,
1285}
1286
1287impl EncodedBinary {
1288 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
1289 Self { data, encoding }
1290 }
1291
1292 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
1293 Self {
1294 data: encoding.encode(bytes),
1295 encoding,
1296 }
1297 }
1298
1299 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
1300 self.encoding.decode(&self.data)
1301 }
1302}
1303
1304#[serde_with::serde_as]
1306#[derive(Debug, Clone, Serialize, Deserialize)]
1307#[serde(rename_all = "camelCase")]
1308pub struct AccountData {
1309 pub data: EncodedBinary,
1311 pub executable: bool,
1313 pub lamports: u64,
1315 #[serde_as(as = "serde_with::DisplayFromStr")]
1316 pub owner: Address,
1318 pub space: u64,
1320}
1321
1322impl AccountData {
1323 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
1324 Ok(solana_account::Account {
1325 data: self.data.decode()?,
1326 lamports: self.lamports,
1327 owner: self.owner,
1328 executable: self.executable,
1329 rent_epoch: 0,
1330 })
1331 }
1332}
1333
1334#[derive(Debug, Clone, Serialize, Deserialize)]
1336#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1337pub enum BacktestResponse {
1338 SessionCreated {
1339 session_id: SessionId,
1340 rpc_endpoint: String,
1341 #[serde(default, skip_serializing_if = "Option::is_none")]
1342 task_id: Option<String>,
1343 },
1344 SessionAttached {
1345 session_id: SessionId,
1346 rpc_endpoint: String,
1347 #[serde(default, skip_serializing_if = "Option::is_none")]
1348 task_id: Option<String>,
1349 },
1350 SessionsCreatedV2 {
1351 control_session_id: ControlSessionId,
1352 session_ids: Vec<SessionId>,
1353 #[serde(default)]
1354 task_ids: Vec<Option<String>>,
1355 #[serde(default)]
1360 start_slots: Vec<u64>,
1361 #[serde(default)]
1362 end_slots: Vec<u64>,
1363 },
1364 ParallelSessionAttachedV2 {
1365 control_session_id: ControlSessionId,
1366 session_ids: Vec<SessionId>,
1367 #[serde(default)]
1368 task_ids: Vec<Option<String>>,
1369 },
1370 ReadyForContinue,
1371 SlotNotification(u64),
1372 Paused(PausedEvent),
1373 DiscoveryBatch(DiscoveryBatchEvent),
1374 Error(BacktestError),
1375 Success,
1376 Completed {
1377 #[serde(skip_serializing_if = "Option::is_none")]
1381 summary: Option<SessionSummary>,
1382 },
1383 Status {
1384 status: BacktestStatus,
1385 },
1386 SessionEventV2 {
1387 session_id: SessionId,
1388 seq_id: u64,
1389 event: SessionEventKind,
1390 },
1391}
1392
1393impl BacktestResponse {
1394 pub fn is_completed(&self) -> bool {
1395 matches!(self, BacktestResponse::Completed { .. })
1396 }
1397
1398 pub fn is_terminal(&self) -> bool {
1399 match self {
1400 BacktestResponse::Completed { .. } => true,
1401 BacktestResponse::Error(e) => e.is_terminal(),
1402 _ => false,
1403 }
1404 }
1405}
1406
1407impl From<BacktestStatus> for BacktestResponse {
1408 fn from(status: BacktestStatus) -> Self {
1409 Self::Status { status }
1410 }
1411}
1412
1413impl From<String> for BacktestResponse {
1414 fn from(message: String) -> Self {
1415 BacktestError::Internal { error: message }.into()
1416 }
1417}
1418
1419impl From<&str> for BacktestResponse {
1420 fn from(message: &str) -> Self {
1421 BacktestError::Internal {
1422 error: message.to_string(),
1423 }
1424 .into()
1425 }
1426}
1427
1428#[derive(Debug, Clone, Serialize, Deserialize)]
1429#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1430pub enum SessionEventKind {
1431 ReadyForContinue,
1432 SlotNotification(u64),
1433 Paused(PausedEvent),
1434 DiscoveryBatch(DiscoveryBatchEvent),
1435 Error(BacktestError),
1436 Success,
1437 Completed {
1438 #[serde(skip_serializing_if = "Option::is_none")]
1439 summary: Option<SessionSummary>,
1440 },
1441 Status {
1442 status: BacktestStatus,
1443 },
1444}
1445
1446impl SessionEventKind {
1447 pub fn is_terminal(&self) -> bool {
1448 match self {
1449 Self::Completed { .. } => true,
1450 Self::Error(e) => e.is_terminal(),
1451 _ => false,
1452 }
1453 }
1454}
1455
1456#[derive(Debug, Clone, Serialize, Deserialize)]
1459#[serde(rename_all = "camelCase")]
1460pub struct SequencedResponse {
1461 pub seq_id: u64,
1462 #[serde(flatten)]
1463 pub response: BacktestResponse,
1464}
1465
1466#[derive(Debug, Clone, Serialize, Deserialize)]
1468#[serde(rename_all = "camelCase")]
1469pub enum BacktestStatus {
1470 StartingRuntime,
1472 DecodedTransactions,
1473 AppliedAccountModifications,
1474 ReadyToExecuteUserTransactions,
1475 ExecutedUserTransactions,
1476 ExecutingBlockTransactions,
1477 ExecutedBlockTransactions,
1478 ProgramAccountsLoaded,
1479}
1480
1481impl std::fmt::Display for BacktestStatus {
1482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1483 let s = match self {
1484 Self::StartingRuntime => "starting runtime",
1485 Self::DecodedTransactions => "decoded transactions",
1486 Self::AppliedAccountModifications => "applied account modifications",
1487 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
1488 Self::ExecutedUserTransactions => "executed user transactions",
1489 Self::ExecutingBlockTransactions => "executing block transactions",
1490 Self::ExecutedBlockTransactions => "executed block transactions",
1491 Self::ProgramAccountsLoaded => "program accounts loaded",
1492 };
1493 f.write_str(s)
1494 }
1495}
1496
1497#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1502#[serde(rename_all = "camelCase")]
1503pub struct RerouteStatsReport {
1504 #[serde(default)]
1506 pub swaps_detected: u64,
1507 #[serde(default)]
1511 pub swaps_filtered: u64,
1512 #[serde(default)]
1514 pub swaps_rerouted: u64,
1515 #[serde(default)]
1517 pub swaps_simulated: u64,
1518 #[serde(default)]
1520 pub swaps_succeeded: u64,
1521 #[serde(default)]
1523 pub requote_failures: u64,
1524 #[serde(default)]
1526 pub override_setup_failures: u64,
1527 #[serde(default)]
1529 pub metis_unavailable_blocks: u64,
1530 #[serde(default)]
1534 pub quote_attempts_unavailable: u64,
1535 #[serde(default)]
1538 pub direct_fill_matched: u64,
1539 #[serde(default)]
1543 pub direct_fill_outcomes: BTreeMap<String, u64>,
1544 #[serde(default)]
1546 pub direct_fill_scored: u64,
1547 #[serde(default)]
1551 pub direct_fill_bps_total: i64,
1552 #[serde(default)]
1555 pub direct_fill_templates: BTreeMap<String, DirectFillTemplateReport>,
1556 #[serde(default)]
1558 pub originals_scored: u64,
1559 #[serde(default)]
1562 pub originals_bps_total: i64,
1563 #[serde(default)]
1565 pub originals_replayed: u64,
1566}
1567
1568#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1571#[serde(rename_all = "camelCase")]
1572pub struct DirectFillTemplateReport {
1573 pub matched: u64,
1574 pub outcomes: BTreeMap<String, u64>,
1575 pub scored: u64,
1576 pub bps_total: i64,
1577}
1578
1579#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1581#[serde(rename_all = "camelCase")]
1582pub struct SessionSummary {
1583 pub correct_simulation: usize,
1586 pub incorrect_simulation: usize,
1589 pub execution_errors: usize,
1591 pub balance_diff: usize,
1593 pub log_diff: usize,
1595 #[serde(default)]
1598 pub unsanitizable: usize,
1599 #[serde(default, skip_serializing_if = "Option::is_none")]
1602 pub reroute_stats: Option<Box<RerouteStatsReport>>,
1603}
1604
1605impl SessionSummary {
1606 pub fn has_deviations(&self) -> bool {
1608 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
1609 }
1610
1611 pub fn total_transactions(&self) -> usize {
1613 self.correct_simulation
1614 + self.incorrect_simulation
1615 + self.execution_errors
1616 + self.balance_diff
1617 + self.log_diff
1618 }
1619
1620 pub fn deviation_details(&self, balance_label: &str, log_label: &str) -> String {
1625 format!(
1626 "{} incorrect simulations, {} execution errors, {} {balance_label}, {} {log_label}",
1627 self.incorrect_simulation, self.execution_errors, self.balance_diff, self.log_diff,
1628 )
1629 }
1630}
1631
1632impl std::fmt::Display for SessionSummary {
1633 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1634 let total = self.total_transactions();
1635 write!(
1636 f,
1637 "Session summary: {total} transactions\n\
1638 \x20 - {} correct simulation\n\
1639 \x20 - {} incorrect simulation\n\
1640 \x20 - {} execution errors\n\
1641 \x20 - {} unsanitizable\n\
1642 \x20 - {} balance diffs\n\
1643 \x20 - {} log diffs",
1644 self.correct_simulation,
1645 self.incorrect_simulation,
1646 self.execution_errors,
1647 self.unsanitizable,
1648 self.balance_diff,
1649 self.log_diff,
1650 )
1651 }
1652}
1653
1654#[derive(Debug, Clone, Serialize, Deserialize)]
1656#[serde(rename_all = "camelCase")]
1657pub enum BacktestError {
1658 InvalidTransactionEncoding {
1659 index: usize,
1660 error: String,
1661 },
1662 InvalidTransactionFormat {
1663 index: usize,
1664 error: String,
1665 },
1666 InvalidAccountEncoding {
1667 address: String,
1668 encoding: BinaryEncoding,
1669 error: String,
1670 },
1671 InvalidAccountOwner {
1672 address: String,
1673 error: String,
1674 },
1675 InvalidAccountPubkey {
1676 address: String,
1677 error: String,
1678 },
1679 NoMoreBlocks,
1680 BundleNotFound {
1681 start_slot: u64,
1682 end_slot: u64,
1683 },
1684 AdvanceSlotFailed {
1685 slot: u64,
1686 error: String,
1687 },
1688 FinalizeSlotFailed {
1689 slot: u64,
1690 error: String,
1691 },
1692 InvalidRequest {
1693 error: String,
1694 },
1695 Internal {
1696 error: String,
1697 },
1698 InvalidBlockhashFormat {
1699 slot: u64,
1700 error: String,
1701 },
1702 InitializingSysvarsFailed {
1703 slot: u64,
1704 error: String,
1705 },
1706 ClerkError {
1707 error: String,
1708 },
1709 SimulationError {
1710 error: String,
1711 },
1712 SessionNotFound {
1713 session_id: String,
1714 },
1715 SessionOwnerMismatch,
1716 SessionOwnershipBusy {
1721 reason: String,
1722 },
1723 NoProducedBlocksInRange {
1729 start_slot: u64,
1730 end_slot: u64,
1731 },
1732}
1733
1734impl BacktestError {
1735 pub fn is_terminal(&self) -> bool {
1739 matches!(
1740 self,
1741 BacktestError::NoMoreBlocks
1742 | BacktestError::BundleNotFound { .. }
1743 | BacktestError::AdvanceSlotFailed { .. }
1744 | BacktestError::FinalizeSlotFailed { .. }
1745 | BacktestError::Internal { .. }
1746 | BacktestError::NoProducedBlocksInRange { .. }
1747 )
1748 }
1749}
1750
1751#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1753pub struct AvailableRange {
1754 pub bundle_start_slot: u64,
1755 pub bundle_start_slot_utc: Option<String>,
1756 pub max_bundle_end_slot: Option<u64>,
1757 pub max_bundle_end_slot_utc: Option<String>,
1758 pub max_bundle_size: Option<u64>,
1759 #[serde(default)]
1762 pub kind: Option<BundleKind>,
1763 #[serde(default)]
1766 pub scope: Option<RangeScope>,
1767}
1768
1769#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1771#[serde(rename_all = "snake_case")]
1772pub enum RangeScope {
1773 Global,
1775 User,
1777}
1778
1779impl RangeScope {
1780 pub fn as_str(self) -> &'static str {
1782 match self {
1783 Self::Global => "global",
1784 Self::User => "user",
1785 }
1786 }
1787}
1788
1789#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1791#[serde(rename_all = "snake_case")]
1792pub enum BundleKind {
1793 #[default]
1795 Blocks,
1796 AccountState,
1800}
1801
1802pub const REPLAY_RANGE_ADVICE: &str = "replayed state is resolved by the recording that begins \
1806 where the request does, so request a range that begins where one was recorded and ends \
1807 within it";
1808
1809impl BundleKind {
1810 fn coverage_noun(self) -> &'static str {
1812 match self {
1813 Self::Blocks => "bundle range",
1814 Self::AccountState => "account-state bundle you own",
1815 }
1816 }
1817
1818 fn coverage_nouns(self) -> &'static str {
1820 match self {
1821 Self::Blocks => "bundle ranges",
1822 Self::AccountState => "account-state bundles you own",
1823 }
1824 }
1825
1826 pub fn as_str(self) -> &'static str {
1828 match self {
1829 Self::Blocks => "blocks",
1830 Self::AccountState => "account_state",
1831 }
1832 }
1833
1834 pub fn from_str_opt(value: &str) -> Option<Self> {
1836 match value {
1837 "blocks" => Some(Self::Blocks),
1838 "account_state" => Some(Self::AccountState),
1839 _ => None,
1840 }
1841 }
1842}
1843
1844impl fmt::Display for BundleKind {
1847 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1848 let label = match self {
1849 Self::Blocks => "transactions",
1850 Self::AccountState => "account_state",
1851 };
1852 f.pad(label)
1853 }
1854}
1855
1856#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1861#[serde(rename_all = "snake_case")]
1862pub enum JobKind {
1863 #[default]
1865 BundleBuild,
1866 Depth,
1868}
1869
1870impl JobKind {
1871 pub fn as_str(self) -> &'static str {
1873 match self {
1874 Self::BundleBuild => "bundle_build",
1875 Self::Depth => "depth",
1876 }
1877 }
1878
1879 pub fn from_str_opt(value: &str) -> Option<Self> {
1881 match value {
1882 "bundle_build" => Some(Self::BundleBuild),
1883 "depth" => Some(Self::Depth),
1884 _ => None,
1885 }
1886 }
1887}
1888
1889#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1894#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
1895#[serde(rename_all = "snake_case")]
1896pub enum DepthSource {
1897 #[default]
1899 Onchain,
1900 Router,
1902}
1903
1904impl DepthSource {
1905 pub const fn as_str(self) -> &'static str {
1907 match self {
1908 Self::Onchain => "onchain",
1909 Self::Router => "router",
1910 }
1911 }
1912
1913 pub fn from_str_opt(value: &str) -> Option<Self> {
1915 match value {
1916 "onchain" => Some(Self::Onchain),
1917 "router" => Some(Self::Router),
1918 _ => None,
1919 }
1920 }
1921
1922 pub const fn discriminant(self) -> u8 {
1927 match self {
1928 Self::Onchain => 0,
1929 Self::Router => 1,
1930 }
1931 }
1932}
1933
1934impl fmt::Display for DepthSource {
1935 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1936 f.write_str(self.as_str())
1937 }
1938}
1939
1940#[derive(Debug, Clone, Serialize, Deserialize)]
1945pub struct DepthCoverageResponse {
1946 pub sources: Vec<DepthSourceCoverage>,
1947}
1948
1949#[derive(Debug, Clone, Serialize, Deserialize)]
1950pub struct DepthSourceCoverage {
1951 pub source: DepthSource,
1953 pub revision: u16,
1956 pub markets: Vec<String>,
1958 pub venues: Vec<String>,
1960 pub size_rungs: usize,
1962 pub completed_ranges: Vec<SlotRange>,
1965}
1966
1967#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1969pub struct DepthRequest {
1970 pub start_slot: u64,
1973 pub end_slot: u64,
1975 #[serde(default)]
1978 pub source: DepthSource,
1979 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1981 pub markets: Vec<String>,
1982}
1983
1984#[derive(Debug, Clone, Serialize, Deserialize)]
1986pub struct DepthStatusResponse {
1987 pub request_id: String,
1988 pub start_slot: u64,
1989 pub end_slot: u64,
1990 pub source: DepthSource,
1991 pub status: BundleBuildStatus,
1992}
1993
1994#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1998pub struct BundleBuildRequest {
1999 pub start_slot: u64,
2000 pub end_slot: u64,
2001 #[serde(default)]
2002 pub bundle_size: Option<u64>,
2003 #[serde(default)]
2006 pub idempotency_key: Option<String>,
2007 #[serde(default)]
2009 pub kind: BundleKind,
2010}
2011
2012#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2013#[serde(rename_all = "snake_case")]
2014pub enum BundleBuildStatus {
2015 Completed,
2017 Failed,
2019 InProgress,
2021 NeedsInvestigation,
2024}
2025
2026impl BundleBuildStatus {
2027 pub fn as_str(self) -> &'static str {
2029 match self {
2030 Self::Completed => "completed",
2031 Self::Failed => "failed",
2032 Self::InProgress => "in_progress",
2033 Self::NeedsInvestigation => "needs_investigation",
2034 }
2035 }
2036}
2037
2038#[derive(Debug, Clone, Serialize, Deserialize)]
2040pub struct BundleBuildStatusResponse {
2041 pub request_id: String,
2042 pub start_slot: u64,
2043 pub end_slot: u64,
2044 pub bundle_size: Option<u64>,
2045 pub status: BundleBuildStatus,
2046 pub created_at_unix_ms: u64,
2049 #[serde(default)]
2051 pub kind: BundleKind,
2052 #[serde(default)]
2055 pub eta_secs: Option<u64>,
2056}
2057
2058#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2060pub struct CompletedBundle {
2061 pub start_slot: u64,
2062 pub end_slot: u64,
2063 pub kind: BundleKind,
2064 #[serde(default)]
2066 pub completed_at_unix_ms: Option<i64>,
2067 #[serde(default)]
2068 pub size_bytes: Option<i64>,
2069}
2070
2071#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2073pub struct BundleDeleteRequest {
2074 pub start_slot: u64,
2075 pub end_slot: u64,
2076 #[serde(default)]
2078 pub kind: BundleKind,
2079}
2080
2081#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2083pub struct BundleDeleteResponse {
2084 pub start_slot: u64,
2085 pub end_slot: u64,
2086 pub kind: BundleKind,
2087}
2088
2089#[derive(Debug, Clone, Serialize, Deserialize)]
2091pub struct BundleBuildListResponse {
2092 pub builds: Vec<BundleBuildStatusResponse>,
2095 #[serde(default)]
2100 pub bundles: Option<Vec<CompletedBundle>>,
2101}
2102
2103#[derive(Debug, Clone, Serialize, Deserialize)]
2106pub struct SnapshotSlotResponse {
2107 pub snapshot_slot: u64,
2108}
2109
2110pub fn split_range(
2133 ranges: &[AvailableRange],
2134 requested: SlotRange,
2135 kind: BundleKind,
2136) -> Result<Vec<(u64, u64)>, String> {
2137 let (requested_start, requested_end) = (requested.start(), requested.end());
2138
2139 let ends_by_start = ranges
2146 .iter()
2147 .filter(|r| r.kind.unwrap_or_default() == kind)
2148 .filter_map(|r| {
2149 r.max_bundle_end_slot
2150 .filter(|&end| end >= r.bundle_start_slot)
2151 .map(|end| (r.bundle_start_slot, end))
2152 })
2153 .fold(
2154 BTreeMap::<u64, BTreeSet<u64>>::new(),
2155 |mut ends_by_start, (start, end)| {
2156 ends_by_start.entry(start).or_default().insert(end);
2157 ends_by_start
2158 },
2159 );
2160
2161 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
2169 ends.iter()
2170 .next_back()
2171 .is_some_and(|&end| end >= requested_start)
2172 }) else {
2173 return Err(format!(
2174 "start_slot {requested_start} is not covered by any available {}",
2175 kind.coverage_noun()
2176 ));
2177 };
2178
2179 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
2185 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
2186 let mut best: Option<Vec<(u64, u64)>> = None;
2187 for &end in ends {
2188 let candidate = if end >= requested_end {
2189 Some(vec![(start, requested_end)])
2190 } else {
2191 best_from.get(&(end + 1)).map(|rest| {
2192 std::iter::once((start, end))
2193 .chain(rest.iter().copied())
2194 .collect()
2195 })
2196 };
2197 if let Some(candidate) = candidate
2198 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
2199 {
2200 best = Some(candidate);
2201 }
2202 }
2203 if let Some(best) = best {
2204 best_from.insert(start, best);
2205 }
2206 }
2207
2208 best_from.remove(&anchor_start).ok_or_else(|| {
2209 let mut covered_to = anchor_start.saturating_sub(1);
2213 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
2214 if start > covered_to.saturating_add(1) {
2215 break;
2216 }
2217 if let Some(&end) = ends.iter().next_back() {
2218 covered_to = covered_to.max(end);
2219 }
2220 }
2221 if covered_to < requested_end {
2222 format!("gap in coverage at slot {}", covered_to + 1)
2223 } else {
2224 format!(
2225 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available {}",
2226 kind.coverage_nouns()
2227 )
2228 }
2229 })
2230}
2231
2232impl From<BacktestError> for BacktestResponse {
2233 fn from(error: BacktestError) -> Self {
2234 Self::Error(error)
2235 }
2236}
2237
2238impl std::error::Error for BacktestError {}
2239
2240impl fmt::Display for BacktestError {
2241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2242 match self {
2243 BacktestError::InvalidTransactionEncoding { index, error } => {
2244 write!(f, "invalid transaction encoding at index {index}: {error}")
2245 }
2246 BacktestError::InvalidTransactionFormat { index, error } => {
2247 write!(f, "invalid transaction format at index {index}: {error}")
2248 }
2249 BacktestError::InvalidAccountEncoding {
2250 address,
2251 encoding,
2252 error,
2253 } => write!(
2254 f,
2255 "invalid encoding for account {address} ({encoding:?}): {error}"
2256 ),
2257 BacktestError::InvalidAccountOwner { address, error } => {
2258 write!(f, "invalid owner for account {address}: {error}")
2259 }
2260 BacktestError::InvalidAccountPubkey { address, error } => {
2261 write!(f, "invalid account pubkey {address}: {error}")
2262 }
2263 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
2264 BacktestError::BundleNotFound {
2265 start_slot,
2266 end_slot,
2267 } => write!(
2268 f,
2269 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
2270 ),
2271 BacktestError::AdvanceSlotFailed { slot, error } => {
2272 write!(f, "failed to advance to slot {slot}: {error}")
2273 }
2274 BacktestError::FinalizeSlotFailed { slot, error } => {
2275 write!(f, "failed to finalize slot {slot}: {error}")
2276 }
2277 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
2278 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
2279 BacktestError::InvalidBlockhashFormat { slot, error } => {
2280 write!(f, "invalid blockhash at slot {slot}: {error}")
2281 }
2282 BacktestError::InitializingSysvarsFailed { slot, error } => {
2283 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
2284 }
2285 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
2286 BacktestError::SimulationError { error } => {
2287 write!(f, "simulation error: {error}")
2288 }
2289 BacktestError::SessionNotFound { session_id } => {
2290 write!(f, "session not found: {session_id}")
2291 }
2292 BacktestError::SessionOwnerMismatch => {
2293 write!(f, "session owner mismatch")
2294 }
2295 BacktestError::SessionOwnershipBusy { reason } => {
2296 write!(f, "session ownership busy: {reason}")
2297 }
2298 BacktestError::NoProducedBlocksInRange {
2299 start_slot,
2300 end_slot,
2301 } => write!(
2302 f,
2303 "no produced blocks in requested range {start_slot}..={end_slot}; \
2304 slot(s) may be skipped on-chain"
2305 ),
2306 }
2307 }
2308}
2309
2310#[cfg(test)]
2311mod tests {
2312 use rstest::rstest;
2313 use solana_account::ReadableAccount;
2314
2315 use super::*;
2316
2317 #[test]
2320 fn a_sources_discriminant_is_frozen_independently_of_its_label() {
2321 assert_eq!(DepthSource::Onchain.discriminant(), 0);
2322 assert_eq!(DepthSource::Router.discriminant(), 1);
2323 }
2324
2325 #[test]
2328 fn a_source_spells_itself_the_same_way_three_times() {
2329 for source in [DepthSource::Onchain, DepthSource::Router] {
2330 assert_eq!(
2331 serde_json::to_value(source).expect("a source serialises"),
2332 serde_json::json!(source.as_str())
2333 );
2334 assert_eq!(DepthSource::from_str_opt(source.as_str()), Some(source));
2335 }
2336 }
2337
2338 fn address(byte: u8) -> Address {
2339 Address::from([byte; 32])
2340 }
2341
2342 #[rstest]
2343 #[case::one("jupiter", Some(&[SwapAggregator::Jupiter][..]))]
2344 #[case::several("jupiter,okx", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2345 #[case::spaced_and_uppercased(" Jupiter , OKX ", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2346 #[case::repeats_collapse("okx,okx", Some(&[SwapAggregator::Okx][..]))]
2347 #[case::trailing_separator("okx,", Some(&[SwapAggregator::Okx][..]))]
2348 #[case::unknown("jupiter,bogus", None)]
2349 #[case::empty("", None)]
2350 #[case::separators_only(",,", None)]
2351 fn reroute_aggregators_parse_the_shapes_a_flag_carries(
2352 #[case] input: &str,
2353 #[case] expected: Option<&[SwapAggregator]>,
2354 ) {
2355 assert_eq!(
2356 input.parse::<RerouteAggregators>().ok(),
2357 expected.map(|aggregators| RerouteAggregators::new(aggregators.iter().copied()))
2358 );
2359 }
2360
2361 #[test]
2363 fn reroute_aggregators_round_trip_through_their_rendering() {
2364 let aggregators = RerouteAggregators::new([SwapAggregator::Dflow, SwapAggregator::Jupiter]);
2365
2366 assert_eq!(aggregators.to_string(), "jupiter,dflow");
2367 assert_eq!(
2368 aggregators
2369 .to_string()
2370 .parse::<RerouteAggregators>()
2371 .unwrap(),
2372 aggregators
2373 );
2374 assert_eq!(RerouteAggregators::default().to_string(), "jupiter");
2375 }
2376
2377 #[test]
2380 fn aggregators_deserialize_through_the_same_parser_the_flag_uses() {
2381 let params: CreateSessionParams = serde_json::from_value(
2382 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {"aggregators": ["Jupiter", "OKX"]}}),
2383 )
2384 .expect("decode");
2385
2386 assert_eq!(
2387 params.reroute.expect("rerouting requested").aggregators,
2388 RerouteAggregators::new([SwapAggregator::Jupiter, SwapAggregator::Okx])
2389 );
2390 }
2391
2392 #[test]
2395 fn params_without_the_new_reroute_fields_decode_to_the_narrow_defaults() {
2396 let params: CreateSessionParams = serde_json::from_value(
2397 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {}}),
2398 )
2399 .expect("decode");
2400
2401 let reroute = params.reroute.expect("rerouting requested");
2402 assert_eq!(reroute.aggregators, RerouteAggregators::default());
2403 assert!(reroute.aggregators.contains(SwapAggregator::Jupiter));
2404 assert!(!reroute.aggregators.contains(SwapAggregator::Okx));
2405 assert!(!reroute.circular_arbs);
2406 assert!(!reroute.detect_failed_l1_swaps);
2407 }
2408
2409 #[test]
2411 fn a_request_without_reroute_reserves_no_sidecar() {
2412 let params: CreateSessionParams =
2413 serde_json::from_value(serde_json::json!({"startSlot": 1, "endSlot": 2}))
2414 .expect("decode");
2415
2416 assert!(!params.requires_sidecar());
2417 }
2418
2419 #[test]
2424 fn a_per_slot_action_schedule_compresses_across_actions() {
2425 let slots = 2_000u64;
2426 let actions = (0..slots)
2427 .map(|slot| ScheduledAction {
2428 anchor: ActionAnchor::AfterSlot { slots: vec![slot] },
2429 kind: ActionKind::Simulate,
2430 feeds_reroute: false,
2431 transactions: Vec::new(),
2432 account_overrides: AccountModifications(BTreeMap::from([(
2433 address(7),
2434 AccountData {
2435 data: EncodedBinary::new(
2437 BASE64.encode(
2438 [&slot.to_le_bytes()[..], &[0xAB; 2040][..]]
2439 .concat()
2440 .as_slice(),
2441 ),
2442 BinaryEncoding::Base64,
2443 ),
2444 executable: false,
2445 lamports: 15_144_966,
2446 owner: address(9),
2447 space: 2048,
2448 },
2449 )])),
2450 return_accounts: Vec::new(),
2451 label: None,
2452 })
2453 .collect::<Vec<_>>();
2454 let params = CreateSessionParams::builder()
2455 .start_slot(1)
2456 .end_slot(slots)
2457 .actions(actions.clone())
2458 .build();
2459
2460 let encoded = serde_json::to_string(¶ms).expect("serialize");
2461 let raw = serde_json::to_string(&actions).expect("serialize raw actions");
2462 assert!(
2463 encoded.len() * 8 < raw.len(),
2464 "expected the schedule to compress well; got {} bytes from {} raw",
2465 encoded.len(),
2466 raw.len()
2467 );
2468
2469 let decoded: CreateSessionParams = serde_json::from_str(&encoded).expect("round trip");
2470 assert_eq!(
2471 serde_json::to_string(&decoded.actions).expect("serialize decoded"),
2472 raw
2473 );
2474 }
2475
2476 #[test]
2479 fn an_empty_action_list_is_omitted_entirely() {
2480 let params = CreateSessionParams::builder()
2481 .start_slot(1)
2482 .end_slot(2)
2483 .build();
2484 let json = serde_json::to_value(¶ms).expect("serialize");
2485 assert!(json.get("actions").is_none(), "{json}");
2486 let decoded: CreateSessionParams = serde_json::from_value(json).expect("round trip");
2487 assert!(decoded.actions.is_empty());
2488 }
2489
2490 #[test]
2493 fn send_actions_conflict_with_replay_only_together() {
2494 let params = |replay, kind| {
2495 CreateSessionParams::builder()
2496 .start_slot(1)
2497 .end_slot(2)
2498 .replay_account_state(replay)
2499 .actions(vec![ScheduledAction {
2500 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2501 kind,
2502 feeds_reroute: false,
2503 transactions: vec!["dGVzdA==".to_string()],
2504 account_overrides: Default::default(),
2505 return_accounts: Default::default(),
2506 label: None,
2507 }])
2508 .build()
2509 };
2510 assert!(replay_support::send_action_conflict(¶ms(false, ActionKind::Send)).is_none());
2511 assert!(
2512 replay_support::send_action_conflict(¶ms(true, ActionKind::Simulate)).is_none()
2513 );
2514 let mut publishing = params(true, ActionKind::Simulate);
2515 publishing.actions[0].feeds_reroute = true;
2516 assert!(replay_support::send_action_conflict(&publishing).is_none());
2517
2518 let conflict = replay_support::send_action_conflict(¶ms(true, ActionKind::Send))
2519 .expect("send under replay conflicts");
2520 assert!(conflict.contains("kind: send"), "{conflict}");
2521 assert!(conflict.contains(replay_support::SEND_ACTION), "{conflict}");
2522 }
2523
2524 fn override_entry(owner: Address, executable: bool) -> AccountData {
2525 AccountData {
2526 data: EncodedBinary::from_bytes(&[1, 2, 3], BinaryEncoding::Base64),
2527 executable,
2528 lamports: 1,
2529 owner,
2530 space: 3,
2531 }
2532 }
2533
2534 fn before() -> ActionAnchor {
2535 ActionAnchor::BeforeSlot { slots: vec![1] }
2536 }
2537
2538 fn valid() -> ScheduledAction {
2539 override_action(before(), address(3), override_entry(address(4), false))
2540 }
2541
2542 fn override_action(
2543 anchor: ActionAnchor,
2544 account: Address,
2545 data: AccountData,
2546 ) -> ScheduledAction {
2547 ScheduledAction {
2548 anchor,
2549 kind: ActionKind::default(),
2550 transactions: Vec::new(),
2551 account_overrides: AccountModifications(BTreeMap::from([(account, data)])),
2552 return_accounts: Vec::new(),
2553 label: None,
2554 feeds_reroute: true,
2555 }
2556 }
2557
2558 #[test]
2559 fn override_action_round_trips() {
2560 let key = address(1);
2561 let entry = override_entry(address(2), false);
2562 let action = override_action(
2563 ActionAnchor::BeforeSlot { slots: vec![5, 9] },
2564 key,
2565 entry.clone(),
2566 );
2567 let json = serde_json::to_value(&action).unwrap();
2568 assert!(
2569 json["anchor"]["slots"].is_string(),
2570 "slots travel as a compressed blob: {json}"
2571 );
2572 let back: ScheduledAction = serde_json::from_value(json).unwrap();
2573 assert_eq!(back.anchor, ActionAnchor::BeforeSlot { slots: vec![5, 9] });
2574 assert!(back.feeds_reroute);
2575 assert!(!back.is_computed_override(), "no transactions to simulate");
2576 let survived = &back.account_overrides.0[&key];
2577 assert_eq!(survived.lamports, entry.lamports);
2578 assert_eq!(survived.data.data, entry.data.data);
2579 }
2580
2581 #[test]
2582 fn an_every_slot_anchor_round_trips_and_defaults_to_every_slot() {
2583 let anchor = ActionAnchor::AfterEverySlot {
2584 every_n_slots: NonZeroU64::new(10).unwrap(),
2585 };
2586 let json = serde_json::to_value(&anchor).unwrap();
2587 assert_eq!(
2588 json,
2589 serde_json::json!({ "at": "afterEverySlot", "everyNSlots": 10 })
2590 );
2591 assert_eq!(
2592 serde_json::from_value::<ActionAnchor>(json).unwrap(),
2593 anchor
2594 );
2595
2596 assert_eq!(
2597 serde_json::from_value::<ActionAnchor>(serde_json::json!({ "at": "beforeEverySlot" }))
2598 .unwrap(),
2599 ActionAnchor::BeforeEverySlot {
2600 every_n_slots: NonZeroU64::MIN
2601 }
2602 );
2603 assert!(
2604 serde_json::from_value::<ActionAnchor>(
2605 serde_json::json!({ "at": "beforeEverySlot", "everyNSlots": 0 })
2606 )
2607 .is_err(),
2608 "a zero cadence names no slot"
2609 );
2610 }
2611
2612 #[test]
2617 fn a_token_account_override_is_accepted() {
2618 let action = override_action(
2619 before(),
2620 address(3),
2621 override_entry(spl_token_interface::ID, false),
2622 );
2623 let schedule = decode_override_actions(&[action]).expect("token overrides decode");
2624 assert!(!schedule.active_at(1).is_empty());
2625 }
2626
2627 #[rstest]
2628 #[case::executable(override_action(before(), address(3), override_entry(address(10), true)))]
2629 #[case::program_owned(override_action(
2630 before(),
2631 address(3),
2632 override_entry(solana_account::PROGRAM_OWNERS[0], false),
2633 ))]
2634 #[case::after_slot_anchor(override_action(
2635 ActionAnchor::AfterSlot { slots: vec![1] },
2636 address(3),
2637 override_entry(address(4), false),
2638 ))]
2639 #[case::every_slot_anchor(override_action(
2640 ActionAnchor::BeforeEverySlot { every_n_slots: NonZeroU64::MIN },
2641 address(3),
2642 override_entry(address(4), false),
2643 ))]
2644 #[case::nothing_to_post(ScheduledAction {
2645 account_overrides: AccountModifications::default(),
2646 ..valid()
2647 })]
2648 #[case::send(ScheduledAction {
2650 kind: ActionKind::Send,
2651 ..valid()
2652 })]
2653 #[allow(deprecated, reason = "exercise the loader-owner rejection path")]
2654 fn override_actions_reject_invalid_accounts_and_shapes(#[case] action: ScheduledAction) {
2655 assert!(decode_override_actions(&[action]).is_err());
2656 }
2657
2658 fn diagnostic_probe() -> DiagnosticProbeParams {
2659 DiagnosticProbeParams {
2660 anchor: ActionAnchor::AfterEverySlot {
2661 every_n_slots: NonZeroU64::MIN,
2662 },
2663 markets: vec![address(1), address(2)],
2664 usd_values: vec![100, 25_000],
2665 label: Some("depth-0".to_owned()),
2666 }
2667 }
2668
2669 #[rstest]
2670 #[case::no_markets(DiagnosticProbeParams { markets: Vec::new(), ..diagnostic_probe() })]
2671 #[case::no_usd_values(DiagnosticProbeParams { usd_values: Vec::new(), ..diagnostic_probe() })]
2672 #[case::too_many_markets(DiagnosticProbeParams {
2673 markets: (0..=MAX_DIAGNOSTIC_MARKETS as u8).map(address).collect(),
2674 ..diagnostic_probe()
2675 })]
2676 #[case::too_many_usd_values(DiagnosticProbeParams {
2677 usd_values: (1..=MAX_DIAGNOSTIC_USD_VALUES as u32 + 1).collect(),
2678 ..diagnostic_probe()
2679 })]
2680 #[case::zero_usd_value(DiagnosticProbeParams { usd_values: vec![0, 100], ..diagnostic_probe() })]
2681 #[case::duplicate_market(DiagnosticProbeParams {
2682 markets: vec![address(1), address(1)],
2683 ..diagnostic_probe()
2684 })]
2685 #[case::before_slot(DiagnosticProbeParams {
2686 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2687 ..diagnostic_probe()
2688 })]
2689 #[case::before_every_slot(DiagnosticProbeParams {
2690 anchor: ActionAnchor::BeforeEverySlot { every_n_slots: NonZeroU64::MIN },
2691 ..diagnostic_probe()
2692 })]
2693 fn diagnostic_probes_reject_invalid_shapes(#[case] probe: DiagnosticProbeParams) {
2694 assert!(probe.validate().is_err());
2695 }
2696
2697 #[test]
2698 fn diagnostic_probes_accept_both_supported_anchors() {
2699 assert!(diagnostic_probe().validate().is_ok());
2700 assert!(
2701 DiagnosticProbeParams {
2702 anchor: ActionAnchor::AfterSlot { slots: vec![1, 2] },
2703 ..diagnostic_probe()
2704 }
2705 .validate()
2706 .is_ok()
2707 );
2708 }
2709
2710 #[test]
2712 fn override_actions_accept_a_static_and_a_computed_shape() {
2713 assert!(decode_override_actions(&[valid()]).is_ok());
2714 assert!(
2717 decode_override_actions(&[ScheduledAction {
2718 transactions: vec!["dGVzdA==".to_string()],
2719 ..valid()
2720 }])
2721 .is_ok()
2722 );
2723 }
2724
2725 #[test]
2726 fn override_actions_merge_account_wise_later_wins() {
2727 let entry = |lamports| AccountData {
2728 lamports,
2729 ..override_entry(address(9), false)
2730 };
2731 let slot_action = |slots, account, lamports| {
2732 override_action(ActionAnchor::BeforeSlot { slots }, account, entry(lamports))
2733 };
2734 let actions = [
2735 slot_action(vec![5, 7], address(1), 1),
2736 slot_action(vec![5], address(2), 2),
2737 slot_action(vec![5], address(1), 3),
2738 ScheduledAction {
2739 anchor: ActionAnchor::AfterSlot { slots: vec![5] },
2740 kind: ActionKind::Simulate,
2741 feeds_reroute: false,
2742 transactions: vec!["dGVzdA==".to_string()],
2743 account_overrides: AccountModifications::default(),
2744 return_accounts: Vec::new(),
2745 label: None,
2746 },
2747 ];
2748 let schedule = decode_override_actions(&actions).unwrap();
2749 assert_eq!(schedule.slots().collect::<Vec<_>>(), vec![5, 7]);
2750 let lamports_at = |key, slot| {
2751 schedule
2752 .active_for(&key, slot)
2753 .map(|account| account.lamports())
2754 };
2755 assert_eq!(lamports_at(address(1), 5), Some(3));
2756 assert_eq!(lamports_at(address(2), 5), Some(2));
2757 assert_eq!(lamports_at(address(1), 7), Some(1));
2758 }
2759
2760 #[test]
2761 fn computed_override_resolution_preserves_action_order_and_failure() {
2762 let key = address(1);
2763 let static_action = |lamports| {
2764 override_action(
2765 ActionAnchor::BeforeSlot { slots: vec![5] },
2766 key,
2767 AccountData {
2768 lamports,
2769 ..override_entry(address(9), false)
2770 },
2771 )
2772 };
2773 let computed_action = || ScheduledAction {
2774 transactions: vec!["dGVzdA==".to_string()],
2775 ..static_action(2)
2776 };
2777
2778 let computed_last =
2779 decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2780 computed_last
2781 .resolve(5, 1, Some(HashMap::from([(key, shared_account(2))])))
2782 .unwrap();
2783 assert_eq!(
2784 computed_last.active_for(&key, 5).map(|a| a.lamports()),
2785 Some(2)
2786 );
2787
2788 let static_last = decode_override_actions(&[computed_action(), static_action(3)]).unwrap();
2789 static_last
2790 .resolve(5, 0, Some(HashMap::from([(key, shared_account(2))])))
2791 .unwrap();
2792 assert_eq!(
2793 static_last.active_for(&key, 5).map(|a| a.lamports()),
2794 Some(3)
2795 );
2796
2797 let failed = decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2798 failed.resolve(5, 1, None).unwrap();
2799 assert_eq!(failed.active_for(&key, 5).map(|a| a.lamports()), Some(1));
2800 assert!(failed.resolve(5, 1, None).is_err(), "resolving twice");
2801 }
2802
2803 fn shared_account(lamports: u64) -> AccountSharedData {
2804 AccountSharedData::from(solana_account::Account {
2805 lamports,
2806 data: vec![1],
2807 owner: address(9),
2808 executable: false,
2809 rent_epoch: 0,
2810 })
2811 }
2812
2813 fn carry_forward_schedule() -> (Address, Address, OverrideSchedule) {
2814 let (a, b) = (address(1), address(2));
2815 let schedule = OverrideSchedule::from(BTreeMap::from([
2816 (10, HashMap::from([(a, shared_account(1))])),
2817 (
2818 20,
2819 HashMap::from([(a, shared_account(2)), (b, shared_account(9))]),
2820 ),
2821 ]));
2822 (a, b, schedule)
2823 }
2824
2825 #[test]
2826 fn override_schedule_resolves_carry_forward_per_account() {
2827 let (a, b, schedule) = carry_forward_schedule();
2828
2829 assert!(schedule.active_for(&a, 9).is_none());
2830 let cases = [
2831 (9, None, None),
2832 (10, Some(1), None),
2833 (15, Some(1), None),
2834 (20, Some(2), Some(9)),
2835 (u64::MAX, Some(2), Some(9)),
2836 ];
2837 for (slot, want_a, want_b) in cases {
2838 assert_eq!(
2839 schedule
2840 .active_for(&a, slot)
2841 .map(|account| account.lamports()),
2842 want_a,
2843 "active_for a at {slot}"
2844 );
2845 let active = schedule.active_at(slot);
2846 assert_eq!(
2847 active.get(&a).map(ReadableAccount::lamports),
2848 want_a,
2849 "active_at a at {slot}"
2850 );
2851 assert_eq!(
2852 active.get(&b).map(ReadableAccount::lamports),
2853 want_b,
2854 "active_at b at {slot}"
2855 );
2856 }
2857 }
2858
2859 #[test]
2860 fn newly_active_yields_final_values_in_window() {
2861 let (a, b, schedule) = carry_forward_schedule();
2862
2863 let newly: HashMap<_, _> = schedule.newly_active(Some(10), 20).into_iter().collect();
2864 assert_eq!(newly.len(), 2);
2865 assert_eq!(newly[&a].lamports(), 2);
2866 assert_eq!(newly[&b].lamports(), 9);
2867 let spanning: HashMap<_, _> = schedule.newly_active(None, 20).into_iter().collect();
2868 assert_eq!(spanning[&a].lamports(), 2, "later slot wins");
2869 assert_eq!(schedule.newly_active(None, 15).len(), 1);
2870 }
2871
2872 #[test]
2873 fn reroute_filter_round_trips_camel_case() {
2874 let filter = RerouteFilter {
2875 pairs: BTreeSet::from([MintPair::new(address(1), address(2))]),
2876 };
2877 let json = serde_json::to_value(&filter).unwrap();
2878 assert!(
2879 json["pairs"][0][0].is_string() && json["pairs"][0][1].is_string(),
2880 "a pair serializes as two base58 strings: {json}"
2881 );
2882 let back: RerouteFilter = serde_json::from_value(json).unwrap();
2883 assert_eq!(back, filter);
2884 }
2885
2886 #[test]
2889 fn mint_pairs_are_normalized_however_they_are_built() {
2890 let (one, other) = (address(1), address(2));
2891 assert_eq!(MintPair::new(one, other), MintPair::new(other, one));
2892
2893 let reversed: MintPair =
2894 serde_json::from_value(serde_json::json!([other.to_string(), one.to_string()]))
2895 .unwrap();
2896 assert_eq!(reversed, MintPair::new(one, other));
2897 }
2898
2899 #[test]
2900 fn fail_fast_divergence_kind_str_round_trips() {
2901 for kind in [
2902 FailFastDivergenceKind::AnyNonBenign,
2903 FailFastDivergenceKind::Tracked,
2904 ] {
2905 assert_eq!(
2906 FailFastDivergenceKind::from_str_opt(kind.as_str()),
2907 Some(kind)
2908 );
2909 }
2910 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
2911 assert_eq!(
2912 FailFastDivergenceKind::default(),
2913 FailFastDivergenceKind::AnyNonBenign
2914 );
2915 }
2916
2917 #[test]
2918 fn bundle_not_found_is_terminal_and_descriptive() {
2919 let error = BacktestError::BundleNotFound {
2920 start_slot: 100,
2921 end_slot: 600,
2922 };
2923
2924 assert!(error.is_terminal());
2925 assert_eq!(
2926 error.to_string(),
2927 "block bundle not found for start_slot=100, end_slot=600"
2928 );
2929 }
2930
2931 #[test]
2932 fn no_produced_blocks_is_terminal_and_descriptive() {
2933 let error = BacktestError::NoProducedBlocksInRange {
2934 start_slot: 100,
2935 end_slot: 100,
2936 };
2937
2938 assert!(error.is_terminal());
2939 assert_eq!(
2942 error.to_string(),
2943 "no produced blocks in requested range 100..=100; slot(s) may be skipped on-chain"
2944 );
2945 }
2946
2947 #[test]
2948 fn bundle_build_request_optional_fields_default_to_none() {
2949 let req: BundleBuildRequest =
2950 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
2951 assert_eq!((req.start_slot, req.end_slot), (1, 2));
2952 assert_eq!(req.bundle_size, None);
2953 assert_eq!(req.idempotency_key, None);
2954 }
2955
2956 #[test]
2957 fn bundle_build_request_parses_optional_fields() {
2958 let req: BundleBuildRequest = serde_json::from_str(
2959 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
2960 )
2961 .expect("parse");
2962 assert_eq!(req.bundle_size, Some(500));
2963 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
2964 }
2965
2966 #[test]
2967 fn bundle_build_status_serde_round_trips_with_snake_case() {
2968 let cases = [
2969 (BundleBuildStatus::Completed, "\"completed\""),
2970 (BundleBuildStatus::Failed, "\"failed\""),
2971 (BundleBuildStatus::InProgress, "\"in_progress\""),
2972 (
2973 BundleBuildStatus::NeedsInvestigation,
2974 "\"needs_investigation\"",
2975 ),
2976 ];
2977 for (status, expected) in cases {
2978 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
2979 assert_eq!(
2980 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
2981 status
2982 );
2983 assert_eq!(status.as_str(), expected.trim_matches('"'));
2984 }
2985 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
2986 }
2987
2988 #[test]
2989 fn bundle_build_request_without_kind_deserializes_as_blocks() {
2990 let request: BundleBuildRequest =
2992 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
2993 assert_eq!(request.kind, BundleKind::Blocks);
2994 }
2995
2996 #[test]
2997 fn bundle_kind_round_trips_over_serde_and_as_str() {
2998 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
2999 let json = serde_json::to_string(&kind).unwrap();
3000 assert_eq!(json, format!("\"{}\"", kind.as_str()));
3001 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
3002 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
3005 }
3006 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
3007 assert_eq!(BundleKind::from_str_opt("accountState"), None);
3008 }
3009
3010 #[test]
3011 fn bundle_build_request_carries_kind_through_serde() {
3012 let request = BundleBuildRequest {
3013 start_slot: 100,
3014 end_slot: 200,
3015 kind: BundleKind::AccountState,
3016 ..Default::default()
3017 };
3018 let json = serde_json::to_string(&request).unwrap();
3019 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3020 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
3021 assert_eq!(parsed.kind, BundleKind::AccountState);
3022 }
3023
3024 #[test]
3025 fn bundle_build_status_response_serializes_request_and_status() {
3026 let response = BundleBuildStatusResponse {
3027 request_id: "r".to_string(),
3028 start_slot: 100,
3029 end_slot: 200,
3030 bundle_size: Some(50),
3031 status: BundleBuildStatus::InProgress,
3032 created_at_unix_ms: 0,
3033 kind: BundleKind::Blocks,
3034 eta_secs: Some(600),
3035 };
3036 let json = serde_json::to_value(&response).unwrap();
3037 assert_eq!(json["request_id"].as_str(), Some("r"));
3038 assert_eq!(json["start_slot"].as_u64(), Some(100));
3039 assert_eq!(json["bundle_size"].as_u64(), Some(50));
3040 assert_eq!(json["status"].as_str(), Some("in_progress"));
3041 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
3042 assert_eq!(json["eta_secs"].as_u64(), Some(600));
3043 }
3044
3045 #[test]
3046 fn an_empty_bundles_list_is_distinct_from_an_absent_one() {
3047 let absent: BundleBuildListResponse = serde_json::from_str(r#"{"builds":[]}"#).unwrap();
3049 let empty: BundleBuildListResponse =
3050 serde_json::from_str(r#"{"builds":[],"bundles":[]}"#).unwrap();
3051 assert_eq!(absent.bundles, None);
3052 assert_eq!(empty.bundles, Some(Vec::new()));
3053 }
3054
3055 #[test]
3056 fn completed_bundle_round_trips() {
3057 let bundle = CompletedBundle {
3058 start_slot: 100,
3059 end_slot: 200,
3060 kind: BundleKind::AccountState,
3061 completed_at_unix_ms: Some(1_700_000_000_000),
3062 size_bytes: Some(4_096),
3063 };
3064 let json = serde_json::to_string(&bundle).unwrap();
3065 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3066 assert_eq!(
3067 serde_json::from_str::<CompletedBundle>(&json).unwrap(),
3068 bundle
3069 );
3070
3071 let sparse = r#"{"start_slot":1,"end_slot":2,"kind":"blocks"}"#;
3072 let parsed: CompletedBundle = serde_json::from_str(sparse).unwrap();
3073 assert_eq!(parsed.completed_at_unix_ms, None);
3074 assert_eq!(parsed.size_bytes, None);
3075 }
3076
3077 #[test]
3078 fn available_range_reads_a_server_that_predates_kind_and_scope() {
3079 let json = r#"{
3081 "bundle_start_slot": 100,
3082 "bundle_start_slot_utc": null,
3083 "max_bundle_end_slot": 200,
3084 "max_bundle_end_slot_utc": null,
3085 "max_bundle_size": 101
3086 }"#;
3087 let parsed: AvailableRange = serde_json::from_str(json).unwrap();
3088 assert_eq!(parsed.bundle_start_slot, 100);
3089 assert_eq!(parsed.kind, None);
3090 assert_eq!(parsed.scope, None);
3091 }
3092
3093 #[test]
3094 fn available_range_ignores_fields_it_does_not_know() {
3095 let json = r#"{"bundle_start_slot": 1, "some_future_field": "x"}"#;
3097 assert_eq!(
3098 serde_json::from_str::<AvailableRange>(json)
3099 .unwrap()
3100 .bundle_start_slot,
3101 1
3102 );
3103 }
3104
3105 #[test]
3106 fn available_range_labels_round_trip() {
3107 let range = AvailableRange {
3108 bundle_start_slot: 100,
3109 max_bundle_end_slot: Some(200),
3110 kind: Some(BundleKind::AccountState),
3111 scope: Some(RangeScope::User),
3112 ..Default::default()
3113 };
3114 let json = serde_json::to_string(&range).unwrap();
3115 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3116 assert!(json.contains(r#""scope":"user""#), "{json}");
3117 let parsed: AvailableRange = serde_json::from_str(&json).unwrap();
3118 assert_eq!(parsed.kind, Some(BundleKind::AccountState));
3119 assert_eq!(parsed.scope, Some(RangeScope::User));
3120 }
3121
3122 fn range(start: u64, end: u64) -> AvailableRange {
3123 AvailableRange {
3124 bundle_start_slot: start,
3125 max_bundle_end_slot: Some(end),
3126 ..Default::default()
3127 }
3128 }
3129
3130 #[rstest::rstest]
3134 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
3135 #[case::multi(
3136 vec![range(100, 200), range(201, 300), range(301, 400)],
3137 100, 300, Some(vec![(100, 200), (201, 300)])
3138 )]
3139 #[case::nested(
3142 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
3143 100, 900, Some(vec![(100, 500), (501, 900)])
3144 )]
3145 #[case::prefers_finer_grid(
3149 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
3150 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
3151 )]
3152 #[case::shared_start_prefers_finer(
3156 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
3157 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
3158 )]
3159 #[case::coarse_overlap_prefers_finer_pair(
3163 vec![range(100, 200), range(100, 150), range(151, 160)],
3164 100, 160, Some(vec![(100, 150), (151, 160)])
3165 )]
3166 #[case::falls_back_to_coarse(
3170 vec![range(100, 160), range(100, 120), range(121, 140)],
3171 100, 160, Some(vec![(100, 160)])
3172 )]
3173 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
3175 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
3178 #[case::anchors_then_continues(
3179 vec![range(150, 350), range(351, 600)],
3180 200, 600, Some(vec![(150, 350), (351, 600)])
3181 )]
3182 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
3183 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
3185 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
3187 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
3188 #[case::user_and_global_ranges_not_collapsed(
3193 vec![range(100, 200), range(100, 150), range(151, 200)],
3194 100, 200, Some(vec![(100, 150), (151, 200)])
3195 )]
3196 fn split_range_cases(
3197 #[case] ranges: Vec<AvailableRange>,
3198 #[case] start: u64,
3199 #[case] end: u64,
3200 #[case] expected: Option<Vec<(u64, u64)>>,
3201 ) {
3202 match expected {
3203 Some(expected) => assert_eq!(
3204 split_range(
3205 &ranges,
3206 SlotRange::new_unchecked(start, end),
3207 BundleKind::Blocks
3208 )
3209 .unwrap(),
3210 expected
3211 ),
3212 None => assert!(
3213 split_range(
3214 &ranges,
3215 SlotRange::new_unchecked(start, end),
3216 BundleKind::Blocks
3217 )
3218 .is_err()
3219 ),
3220 }
3221 }
3222
3223 fn account_state_range(start: u64, end: u64) -> AvailableRange {
3224 AvailableRange {
3225 kind: Some(BundleKind::AccountState),
3226 ..range(start, end)
3227 }
3228 }
3229
3230 #[test]
3231 fn a_replay_split_rides_the_recordings_not_a_finer_block_grid() {
3232 let ranges = vec![
3235 range(100, 149),
3236 range(150, 199),
3237 account_state_range(100, 199),
3238 ];
3239 let requested = SlotRange::new_unchecked(100, 199);
3240
3241 assert_eq!(
3242 split_range(&ranges, requested, BundleKind::Blocks).unwrap(),
3243 [(100, 149), (150, 199)]
3244 );
3245 assert_eq!(
3246 split_range(&ranges, requested, BundleKind::AccountState).unwrap(),
3247 [(100, 199)]
3248 );
3249 }
3250
3251 #[test]
3252 fn a_replay_split_tiles_consecutive_recordings() {
3253 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
3254 assert_eq!(
3255 split_range(
3256 &ranges,
3257 SlotRange::new_unchecked(100, 299),
3258 BundleKind::AccountState
3259 )
3260 .unwrap(),
3261 [(100, 199), (200, 299)]
3262 );
3263 }
3264
3265 #[test]
3266 fn a_replay_split_ends_short_of_a_recording() {
3267 let requested = SlotRange::new_unchecked(100, 150);
3270 assert_eq!(
3271 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3272 [(100, 150)]
3273 );
3274 assert_eq!(
3275 split_range(
3276 &[account_state_range(100, 199)],
3277 requested,
3278 BundleKind::AccountState
3279 )
3280 .unwrap(),
3281 [(100, 150)]
3282 );
3283 }
3284
3285 #[test]
3286 fn a_replay_split_ends_short_of_its_last_recording() {
3287 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
3290 assert_eq!(
3291 split_range(
3292 &ranges,
3293 SlotRange::new_unchecked(100, 250),
3294 BundleKind::AccountState
3295 )
3296 .unwrap(),
3297 [(100, 199), (200, 250)]
3298 );
3299 }
3300
3301 #[test]
3302 fn a_replay_split_tiles_a_one_slot_recording() {
3303 let ranges = vec![account_state_range(100, 249), account_state_range(250, 250)];
3306 assert_eq!(
3307 split_range(
3308 &ranges,
3309 SlotRange::new_unchecked(100, 250),
3310 BundleKind::AccountState
3311 )
3312 .unwrap(),
3313 [(100, 249), (250, 250)]
3314 );
3315 }
3316
3317 #[test]
3318 fn a_replay_split_anchors_a_mid_recording_start() {
3319 let requested = SlotRange::new_unchecked(150, 199);
3323 assert_eq!(
3324 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3325 [(100, 199)]
3326 );
3327 assert_eq!(
3328 split_range(
3329 &[account_state_range(100, 199)],
3330 requested,
3331 BundleKind::AccountState
3332 )
3333 .unwrap(),
3334 [(100, 199)]
3335 );
3336 }
3337
3338 #[test]
3339 fn a_replay_split_refuses_a_start_no_recording_covers() {
3340 let error = split_range(
3341 &[account_state_range(200, 299)],
3342 SlotRange::new_unchecked(100, 299),
3343 BundleKind::AccountState,
3344 )
3345 .expect_err("no recording covers 100");
3346 assert!(
3347 error.contains("is not covered by any available account-state bundle"),
3348 "{error}"
3349 );
3350 }
3351
3352 #[test]
3353 fn a_replay_split_ignores_block_ranges() {
3354 let error = split_range(
3355 &[range(100, 199)],
3356 SlotRange::new_unchecked(100, 199),
3357 BundleKind::AccountState,
3358 )
3359 .expect_err("owns no recordings");
3360 assert!(error.contains("account-state bundle"), "{error}");
3361 }
3362
3363 #[test]
3364 fn blocks_mode_admits_ranges_from_a_server_that_predates_kind() {
3365 let unlabelled = range(100, 199);
3368 assert_eq!(unlabelled.kind, None);
3369 assert_eq!(
3370 split_range(
3371 &[unlabelled],
3372 SlotRange::new_unchecked(100, 150),
3373 BundleKind::Blocks
3374 )
3375 .unwrap(),
3376 [(100, 150)]
3377 );
3378 }
3379
3380 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
3383 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
3384 for r in ranges {
3385 if let Some(end) = r.max_bundle_end_slot
3386 && end >= r.bundle_start_slot
3387 {
3388 ends.entry(r.bundle_start_slot).or_default().insert(end);
3389 }
3390 }
3391 ends
3392 }
3393
3394 fn reference_max_split(
3398 ends: &BTreeMap<u64, BTreeSet<u64>>,
3399 cursor: u64,
3400 end: u64,
3401 ) -> Option<Vec<(u64, u64)>> {
3402 ends.get(&cursor)?
3403 .iter()
3404 .filter_map(|&bundle_end| {
3405 if bundle_end >= end {
3406 Some(vec![(cursor, end)])
3407 } else {
3408 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
3409 rest.insert(0, (cursor, bundle_end));
3410 rest
3411 })
3412 }
3413 })
3414 .max_by_key(Vec::len)
3415 }
3416
3417 fn is_valid_split(
3421 split: &[(u64, u64)],
3422 ends: &BTreeMap<u64, BTreeSet<u64>>,
3423 start: u64,
3424 end: u64,
3425 ) -> bool {
3426 split.first().is_some_and(|&(s, _)| s == start)
3427 && split.last().is_some_and(|&(_, e)| e == end)
3428 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
3429 && split.iter().all(|&(s, e)| {
3430 e >= s
3431 && ends
3432 .get(&s)
3433 .and_then(|bundle_ends| bundle_ends.iter().next_back())
3434 .is_some_and(|&max_end| e <= max_end)
3435 })
3436 }
3437
3438 #[test]
3443 fn split_range_matches_reference() {
3444 let mut seed: u64 = 0x9E3779B97F4A7C15;
3445 let mut next = || {
3446 seed = seed
3447 .wrapping_mul(6364136223846793005)
3448 .wrapping_add(1442695040888963407);
3449 seed >> 33
3450 };
3451
3452 for _ in 0..50_000 {
3453 let ranges: Vec<AvailableRange> = (0..next() % 6)
3456 .map(|_| {
3457 let start = next() % 12;
3458 range(start, start + next() % 6) })
3460 .collect();
3461 let start = next() % 12;
3462 let end = start + next() % 6; let got = split_range(
3465 &ranges,
3466 SlotRange::new_unchecked(start, end),
3467 BundleKind::Blocks,
3468 );
3469 let ends = ends_by_start(&ranges);
3470 let anchor = ends
3473 .range(..=start)
3474 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
3475 .map(|(&s, _)| s);
3476 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
3477
3478 let layout: Vec<_> = ranges
3479 .iter()
3480 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
3481 .collect();
3482 match (&got, &reference) {
3483 (Ok(split), Some(best)) => {
3484 assert!(
3485 is_valid_split(split, &ends, anchor.unwrap(), end),
3486 "invalid split {split:?} for {layout:?} [{start},{end}]"
3487 );
3488 assert_eq!(
3489 split.len(),
3490 best.len(),
3491 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
3492 );
3493 }
3494 (Err(_), None) => {}
3495 _ => panic!(
3496 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
3497 ),
3498 }
3499 }
3500 }
3501
3502 #[test]
3503 fn a_reroute_without_templates_omits_direct_fill() {
3504 let json = serde_json::to_value(RerouteParams::default()).expect("serialize");
3505 assert!(json.get("directFill").is_none(), "{json}");
3506 }
3507
3508 fn template(label: &str) -> DirectFillTemplate {
3510 let placeholder = Address::default();
3511 DirectFillTemplate {
3512 label: label.to_owned(),
3513 mode: SwapMode::ExactIn,
3514 input_mint: mints::SOL,
3515 output_mint: mints::USDC,
3516 instruction: TemplateInstruction {
3517 program_id: address(7),
3518 accounts: vec![
3519 TemplateAccount {
3520 address: placeholder,
3521 writable: true,
3522 signer: true,
3523 },
3524 TemplateAccount {
3525 address: placeholder,
3526 writable: true,
3527 signer: false,
3528 },
3529 TemplateAccount {
3530 address: placeholder,
3531 writable: true,
3532 signer: false,
3533 },
3534 TemplateAccount {
3535 address: address(8),
3536 writable: false,
3537 signer: false,
3538 },
3539 ],
3540 data: vec![0; 16],
3541 },
3542 amount_offset: 2,
3543 taker: TakerSlots {
3544 user: vec![0],
3545 input_token_account: vec![1],
3546 output_token_account: vec![2],
3547 },
3548 }
3549 }
3550
3551 #[rstest]
3553 #[case::requote_alone(true, 0, false)]
3554 #[case::templates_alone(false, 1, false)]
3555 #[case::both(true, 2, false)]
3556 #[case::neither(false, 0, true)]
3557 fn a_reroute_is_a_noop_only_with_nothing_to_ask(
3558 #[case] requote: bool,
3559 #[case] templates: usize,
3560 #[case] noop: bool,
3561 ) {
3562 let reroute = RerouteParams {
3563 requote,
3564 direct_fill: (0..templates).map(|i| template(&format!("t{i}"))).collect(),
3565 ..Default::default()
3566 };
3567 assert_eq!(reroute.is_noop(), noop);
3568 }
3569
3570 #[rstest]
3572 #[case::templates_without_requote(false, 1, true)]
3573 #[case::requote_without_templates(true, 0, true)]
3574 #[case::nothing(false, 0, false)]
3575 fn the_account_universe_warms_for_templates_as_for_requoting(
3576 #[case] requote: bool,
3577 #[case] templates: usize,
3578 #[case] warms: bool,
3579 ) {
3580 let params = CreateSessionParams::builder()
3581 .start_slot(1)
3582 .end_slot(2)
3583 .reroute(RerouteParams {
3584 requote,
3585 direct_fill: (0..templates).map(|i| template(&format!("t{i}"))).collect(),
3586 ..Default::default()
3587 })
3588 .build();
3589 assert_eq!(params.warms_account_universe(), warms);
3590 }
3591}