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 = 256 * 1024 * 1024;
323
324mod compressed {
329 use std::io::Read;
330
331 use base64::Engine as _;
332 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
333
334 use super::{BASE64, MAX_ACTION_TRANSACTIONS_BYTES};
335
336 const COMPRESSION_LEVEL: i32 = 0;
338
339 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
340 where
341 T: Serialize,
342 S: Serializer,
343 {
344 let json = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
345 let compressed = zstd::stream::encode_all(json.as_slice(), COMPRESSION_LEVEL)
346 .map_err(serde::ser::Error::custom)?;
347 serializer.serialize_str(&BASE64.encode(compressed))
348 }
349
350 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
351 where
352 T: DeserializeOwned,
353 D: Deserializer<'de>,
354 {
355 let blob = String::deserialize(deserializer)?;
356 let compressed = BASE64
357 .decode(blob.as_bytes())
358 .map_err(serde::de::Error::custom)?;
359 let json = decode_capped(&compressed, MAX_ACTION_TRANSACTIONS_BYTES)
360 .map_err(serde::de::Error::custom)?;
361 serde_json::from_slice(&json).map_err(serde::de::Error::custom)
362 }
363
364 fn decode_capped(compressed: &[u8], cap: u64) -> std::io::Result<Vec<u8>> {
367 let mut json = Vec::new();
368 zstd::stream::read::Decoder::new(compressed)
369 .and_then(|d| Read::take(d, cap + 1).read_to_end(&mut json))?;
370 match json.len() as u64 > cap {
371 true => Err(std::io::Error::other(format!(
372 "decompressed blob is over the {cap} byte ceiling"
373 ))),
374 false => Ok(json),
375 }
376 }
377
378 #[cfg(test)]
379 mod tests {
380 use super::*;
381
382 #[test]
383 fn a_blob_over_the_ceiling_names_it_instead_of_truncating() {
384 let compressed = zstd::stream::encode_all([b'a'; 1024].as_slice(), 0).unwrap();
385 let error = decode_capped(&compressed, 64).unwrap_err();
386 assert!(error.to_string().contains("64 byte ceiling"), "{error}");
387 }
388
389 #[test]
390 fn a_blob_at_the_ceiling_is_kept_whole() {
391 let compressed = zstd::stream::encode_all([b'a'; 64].as_slice(), 0).unwrap();
392 assert_eq!(decode_capped(&compressed, 64).unwrap().len(), 64);
393 }
394 }
395}
396
397#[serde_with::serde_as]
400#[derive(Debug, Clone, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402pub struct ScheduledAction {
403 pub anchor: ActionAnchor,
404 #[serde(default)]
406 pub kind: ActionKind,
407 #[serde(default, with = "compressed")]
417 pub transactions: Vec<String>,
418 #[serde(default)]
423 pub account_overrides: AccountModifications,
424 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
429 pub feeds_reroute: bool,
430 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
434 #[serde(default)]
435 pub return_accounts: Vec<Address>,
436 #[serde(default)]
438 pub label: Option<String>,
439}
440
441impl ScheduledAction {
442 pub fn is_computed_override(&self) -> bool {
445 self.feeds_reroute && !self.transactions.is_empty()
446 }
447}
448
449pub const MAX_DIAGNOSTIC_MARKETS: usize = 64;
450pub const MAX_DIAGNOSTIC_USD_VALUES: usize = 16;
451
452#[serde_with::serde_as]
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
454#[serde(rename_all = "camelCase")]
455pub struct DiagnosticProbeParams {
456 pub anchor: ActionAnchor,
457 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
458 pub markets: Vec<Address>,
459 pub usd_values: Vec<u32>,
460 #[serde(default)]
461 pub label: Option<String>,
462}
463
464#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
465pub enum DiagnosticProbeError {
466 #[error("a diagnostic probe names no markets")]
467 NoMarkets,
468 #[error("a diagnostic probe names no usdValues")]
469 NoUsdValues,
470 #[error("a diagnostic probe names {got} markets, over the ceiling of {MAX_DIAGNOSTIC_MARKETS}")]
471 TooManyMarkets { got: usize },
472 #[error(
473 "a diagnostic probe names {got} usdValues, over the ceiling of {MAX_DIAGNOSTIC_USD_VALUES}"
474 )]
475 TooManyUsdValues { got: usize },
476 #[error("a diagnostic probe names usdValue 0, which the router reads as no size at all")]
477 ZeroUsdValue,
478 #[error("a diagnostic probe names market {market} more than once")]
479 DuplicateMarket { market: Address },
480 #[error(
481 "a diagnostic probe anchors at {anchor}; probing the router is an await, and only the \
482 slot-end hook has a point for one, so only afterSlot and afterEverySlot are supported"
483 )]
484 UnsupportedAnchor { anchor: &'static str },
485}
486
487impl DiagnosticProbeParams {
488 pub fn fires(&self) -> usize {
490 self.markets.len() * self.usd_values.len()
491 }
492
493 pub fn validate(&self) -> Result<(), DiagnosticProbeError> {
494 if self.markets.is_empty() {
495 return Err(DiagnosticProbeError::NoMarkets);
496 }
497 if self.usd_values.is_empty() {
498 return Err(DiagnosticProbeError::NoUsdValues);
499 }
500 if self.markets.len() > MAX_DIAGNOSTIC_MARKETS {
501 return Err(DiagnosticProbeError::TooManyMarkets {
502 got: self.markets.len(),
503 });
504 }
505 if self.usd_values.len() > MAX_DIAGNOSTIC_USD_VALUES {
506 return Err(DiagnosticProbeError::TooManyUsdValues {
507 got: self.usd_values.len(),
508 });
509 }
510 if self.usd_values.contains(&0) {
511 return Err(DiagnosticProbeError::ZeroUsdValue);
512 }
513 let mut seen = HashSet::with_capacity(self.markets.len());
514 if let Some(market) = self.markets.iter().find(|market| !seen.insert(**market)) {
515 return Err(DiagnosticProbeError::DuplicateMarket { market: *market });
516 }
517 match &self.anchor {
518 ActionAnchor::AfterSlot { .. } | ActionAnchor::AfterEverySlot { .. } => Ok(()),
519 ActionAnchor::BeforeSlot { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
520 anchor: "beforeSlot",
521 }),
522 ActionAnchor::BeforeEverySlot { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
523 anchor: "beforeEverySlot",
524 }),
525 ActionAnchor::BeforeMatch { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
526 anchor: "beforeMatch",
527 }),
528 ActionAnchor::AfterMatch { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
529 anchor: "afterMatch",
530 }),
531 }
532 }
533}
534
535#[derive(Debug, Clone)]
537enum OverrideContribution {
538 Static(HashMap<Address, AccountSharedData>),
539 Pending,
540 Resolved(HashMap<Address, AccountSharedData>),
541 Failed,
542}
543
544#[derive(Debug, Clone, Default)]
545struct OverrideScheduleInner {
546 entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>,
547 keys: HashSet<Address>,
551}
552
553impl OverrideScheduleInner {
554 fn new(entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>) -> Self {
555 let keys = entries
556 .values()
557 .flatten()
558 .filter_map(|(_, contribution)| match contribution {
559 OverrideContribution::Static(accounts)
560 | OverrideContribution::Resolved(accounts) => Some(accounts.keys().copied()),
561 OverrideContribution::Pending | OverrideContribution::Failed => None,
562 })
563 .flatten()
564 .collect();
565 Self { entries, keys }
566 }
567}
568
569#[derive(Debug, Clone, Default)]
572pub struct OverrideSchedule {
573 inner: Arc<RwLock<OverrideScheduleInner>>,
574}
575
576impl From<BTreeMap<u64, HashMap<Address, AccountSharedData>>> for OverrideSchedule {
577 fn from(entries: BTreeMap<u64, HashMap<Address, AccountSharedData>>) -> Self {
578 Self {
579 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(
580 entries
581 .into_iter()
582 .map(|(slot, accounts)| {
583 (slot, vec![(0, OverrideContribution::Static(accounts))])
584 })
585 .collect(),
586 ))),
587 }
588 }
589}
590
591impl OverrideSchedule {
592 pub fn is_empty(&self) -> bool {
593 self.inner.read().entries.is_empty()
594 }
595
596 pub fn slots(&self) -> impl Iterator<Item = u64> + '_ {
598 self.inner
599 .read()
600 .entries
601 .keys()
602 .copied()
603 .collect::<Vec<_>>()
604 .into_iter()
605 }
606
607 pub fn active_at(&self, slot: u64) -> HashMap<Address, AccountSharedData> {
609 self.inner.read().entries.range(..=slot).fold(
610 HashMap::new(),
611 |mut active, (_, contributions)| {
612 active.extend(fold_contributions(contributions));
613 active
614 },
615 )
616 }
617
618 pub fn active_for(&self, key: &Address, slot: u64) -> Option<AccountSharedData> {
619 let inner = self.inner.read();
620 if !inner.keys.contains(key) {
621 return None;
622 }
623 inner
624 .entries
625 .range(..=slot)
626 .rev()
627 .find_map(|(_, contributions)| {
628 contributions
629 .iter()
630 .rev()
631 .find_map(|(_, contribution)| match contribution {
632 OverrideContribution::Static(accounts)
633 | OverrideContribution::Resolved(accounts) => accounts.get(key).cloned(),
634 OverrideContribution::Pending | OverrideContribution::Failed => None,
635 })
636 })
637 }
638
639 pub fn newly_active(
641 &self,
642 after: Option<u64>,
643 up_to: u64,
644 ) -> Vec<(Address, AccountSharedData)> {
645 let lower = after.map_or(std::ops::Bound::Unbounded, std::ops::Bound::Excluded);
646 self.inner
647 .read()
648 .entries
649 .range((lower, std::ops::Bound::Included(up_to)))
650 .flat_map(|(_, contributions)| fold_contributions(contributions))
651 .collect::<HashMap<_, _>>()
652 .into_iter()
653 .collect()
654 }
655
656 pub fn resolve(
659 &self,
660 slot: u64,
661 action_index: u32,
662 accounts: Option<HashMap<Address, AccountSharedData>>,
663 ) -> Result<(), String> {
664 let mut inner = self.inner.write();
665 let OverrideScheduleInner { entries, keys } = &mut *inner;
666 let contribution = entries
667 .get_mut(&slot)
668 .and_then(|contributions| {
669 contributions
670 .iter_mut()
671 .find(|(index, _)| *index == action_index)
672 .map(|(_, contribution)| contribution)
673 })
674 .ok_or_else(|| {
675 format!("override action {action_index} has no contribution at slot {slot}")
676 })?;
677 if !matches!(contribution, OverrideContribution::Pending) {
678 return Err(format!(
679 "override action {action_index} at slot {slot} is not pending"
680 ));
681 }
682 *contribution = match accounts {
683 Some(accounts) => {
684 keys.extend(accounts.keys().copied());
685 OverrideContribution::Resolved(accounts)
686 }
687 None => OverrideContribution::Failed,
688 };
689 Ok(())
690 }
691}
692
693fn fold_contributions(
694 contributions: &[(u32, OverrideContribution)],
695) -> HashMap<Address, AccountSharedData> {
696 let mut accounts = HashMap::new();
697 for (_, contribution) in contributions {
698 match contribution {
699 OverrideContribution::Static(next) | OverrideContribution::Resolved(next) => {
700 accounts.extend(next.iter().map(|(key, account)| (*key, account.clone())));
701 }
702 OverrideContribution::Pending | OverrideContribution::Failed => {}
703 }
704 }
705 accounts
706}
707
708fn decode_override_actions(actions: &[ScheduledAction]) -> Result<OverrideSchedule, String> {
717 let mut entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>> = BTreeMap::new();
718 for (action_index, action) in actions.iter().enumerate().filter(|(_, action)| {
719 action.feeds_reroute
720 || (action.transactions.is_empty() && !action.account_overrides.0.is_empty())
721 }) {
722 if action.kind == ActionKind::Send {
724 return Err("account overrides are not valid for kind: send".to_string());
725 }
726 if action.transactions.is_empty() && action.account_overrides.0.is_empty() {
727 return Err(
728 "feedsReroute needs something to feed: transactions to simulate, or \
729 accountOverrides"
730 .to_string(),
731 );
732 }
733 let ActionAnchor::BeforeSlot { slots } = &action.anchor else {
734 return Err("scheduled account overrides require a beforeSlot anchor".to_string());
735 };
736 let accounts = decode_account_modifications(&action.account_overrides)?;
737 let contribution = if action.transactions.is_empty() {
738 OverrideContribution::Static(accounts)
739 } else {
740 OverrideContribution::Pending
741 };
742 for slot in slots.iter().copied().collect::<BTreeSet<_>>() {
745 entries
746 .entry(slot)
747 .or_default()
748 .push((action_index as u32, contribution.clone()));
749 }
750 }
751 Ok(OverrideSchedule {
752 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(entries))),
753 })
754}
755
756#[allow(
760 deprecated,
761 reason = "`PROGRAM_OWNERS` remains the canonical loader-owner list"
762)]
763pub fn decode_account_modifications(
764 modifications: &AccountModifications,
765) -> Result<HashMap<Address, AccountSharedData>, String> {
766 modifications
767 .0
768 .iter()
769 .map(|(address, data)| {
770 let account = data
771 .to_account()
772 .map_err(|error| format!("invalid account data for {address}: {error}"))?;
773 if account.executable || solana_account::PROGRAM_OWNERS.contains(&account.owner) {
774 return Err(format!(
775 "overriding executable/program accounts is not supported (account {address})"
776 ));
777 }
778 Ok((*address, AccountSharedData::from(account)))
779 })
780 .collect()
781}
782
783pub const MAX_DISCONNECT_TIMEOUT_SECS: u16 = 900;
787
788#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
791#[serde(rename_all = "lowercase", try_from = "String")]
792pub enum SwapAggregator {
793 Jupiter,
794 Okx,
795 Titan,
796 Dflow,
797}
798
799impl SwapAggregator {
800 pub const fn as_str(self) -> &'static str {
802 match self {
803 Self::Jupiter => "jupiter",
804 Self::Okx => "okx",
805 Self::Titan => "titan",
806 Self::Dflow => "dflow",
807 }
808 }
809}
810
811impl std::fmt::Display for SwapAggregator {
812 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
813 f.write_str(self.as_str())
814 }
815}
816
817#[derive(Debug, thiserror::Error)]
818pub enum ParseAggregatorError {
819 #[error("unknown swap aggregator {0:?}; expected one of jupiter, okx, titan, dflow")]
820 Unknown(String),
821 #[error("name at least one aggregator (jupiter, okx, titan, dflow)")]
823 NoAggregator,
824}
825
826impl TryFrom<String> for SwapAggregator {
827 type Error = ParseAggregatorError;
828
829 fn try_from(value: String) -> Result<Self, Self::Error> {
830 value.parse()
831 }
832}
833
834impl std::str::FromStr for SwapAggregator {
835 type Err = ParseAggregatorError;
836
837 fn from_str(s: &str) -> Result<Self, Self::Err> {
838 match s.trim().to_ascii_lowercase().as_str() {
839 "jupiter" => Ok(Self::Jupiter),
840 "okx" => Ok(Self::Okx),
841 "titan" => Ok(Self::Titan),
842 "dflow" => Ok(Self::Dflow),
843 _ => Err(ParseAggregatorError::Unknown(s.to_string())),
844 }
845 }
846}
847
848#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
852#[serde(transparent)]
853pub struct RerouteAggregators(BTreeSet<SwapAggregator>);
854
855impl Default for RerouteAggregators {
856 fn default() -> Self {
857 Self([SwapAggregator::Jupiter].into_iter().collect())
858 }
859}
860
861impl RerouteAggregators {
862 pub fn new(aggregators: impl IntoIterator<Item = SwapAggregator>) -> Self {
863 Self(aggregators.into_iter().collect())
864 }
865
866 pub fn contains(&self, aggregator: SwapAggregator) -> bool {
867 self.0.contains(&aggregator)
868 }
869
870 pub fn is_empty(&self) -> bool {
871 self.0.is_empty()
872 }
873}
874
875impl std::fmt::Display for RerouteAggregators {
876 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
877 let mut aggregators = self.0.iter();
878 if let Some(first) = aggregators.next() {
879 write!(f, "{first}")?;
880 }
881 aggregators.try_for_each(|aggregator| write!(f, ",{aggregator}"))
882 }
883}
884
885impl std::str::FromStr for RerouteAggregators {
886 type Err = ParseAggregatorError;
887
888 fn from_str(s: &str) -> Result<Self, Self::Err> {
889 let aggregators: BTreeSet<SwapAggregator> = s
890 .split(',')
891 .filter(|aggregator| !aggregator.trim().is_empty())
892 .map(str::parse)
893 .collect::<Result<_, _>>()?;
894 if aggregators.is_empty() {
895 return Err(ParseAggregatorError::NoAggregator);
896 }
897 Ok(Self(aggregators))
898 }
899}
900
901#[serde_with::serde_as]
909#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
910#[serde(rename_all = "camelCase")]
911pub struct RerouteParams {
912 #[serde(default)]
913 pub aggregators: RerouteAggregators,
914 #[serde(default, skip_serializing_if = "Option::is_none")]
917 pub filter: Option<RerouteFilter>,
918 #[serde(default)]
925 pub detect_failed_l1_swaps: bool,
926 #[serde(default)]
929 pub circular_arbs: bool,
930 #[serde(default = "RerouteParams::default_requote")]
936 pub requote: bool,
937 #[serde(default, skip_serializing_if = "Vec::is_empty")]
946 pub direct_fill: Vec<DirectFillTemplate>,
947 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
951 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
952 pub extra_markets: BTreeSet<Address>,
953}
954
955impl Default for RerouteParams {
958 fn default() -> Self {
959 Self {
960 aggregators: RerouteAggregators::default(),
961 filter: None,
962 detect_failed_l1_swaps: false,
963 circular_arbs: false,
964 requote: Self::default_requote(),
965 direct_fill: Vec::new(),
966 extra_markets: BTreeSet::new(),
967 }
968 }
969}
970
971impl std::str::FromStr for MintPair {
973 type Err = String;
974
975 fn from_str(s: &str) -> Result<Self, Self::Err> {
976 let (base, quote) = s
977 .split_once(',')
978 .ok_or_else(|| format!("expected two comma-separated mints, got {s:?}"))?;
979 Self::try_from([base.trim().to_owned(), quote.trim().to_owned()])
980 .map_err(|error| error.to_string())
981 }
982}
983
984impl std::fmt::Display for MintPair {
985 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
986 let (one, other) = self.mints();
987 write!(f, "{one},{other}")
988 }
989}
990
991impl RerouteParams {
992 const fn default_requote() -> bool {
993 true
994 }
995
996 pub const fn is_noop(&self) -> bool {
999 !self.requote && self.direct_fill.is_empty()
1000 }
1001}
1002
1003#[serde_with::serde_as]
1005#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
1006#[serde(rename_all = "camelCase")]
1007pub struct CreateSessionParams {
1008 pub start_slot: u64,
1010 pub end_slot: u64,
1012 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
1013 #[serde(default)]
1014 #[builder(default)]
1015 pub signer_filter: BTreeSet<Address>,
1017 #[serde(default)]
1020 #[builder(default)]
1021 pub send_summary: bool,
1022 #[serde(default, skip_serializing_if = "Option::is_none")]
1024 pub reroute: Option<RerouteParams>,
1025 #[serde(default)]
1028 pub capacity_wait_timeout_secs: Option<u16>,
1029 #[serde(default)]
1033 pub disconnect_timeout_secs: Option<u16>,
1034 #[serde(default)]
1039 pub extra_compute_units: Option<u32>,
1040 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1047 #[builder(default)]
1048 pub discoveries: Vec<DiscoveryFilter>,
1049 #[serde(default, with = "compressed", skip_serializing_if = "Vec::is_empty")]
1058 #[builder(default)]
1059 pub actions: Vec<ScheduledAction>,
1060 #[serde(default)]
1063 #[builder(default)]
1064 pub replay_account_state: bool,
1065 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1066 #[builder(default)]
1067 pub diagnostic_probes: Vec<DiagnosticProbeParams>,
1068}
1069
1070pub mod replay_support {
1074 use super::{ActionKind, CreateSessionParams};
1075
1076 pub const NO_TRANSACTIONS: &str =
1078 "Create the session without replayAccountState to execute and persist transactions.";
1079
1080 pub const SEND_TRANSACTION: &str = "Use simulateTransaction to run it against the replayed state, or create the session \
1081 without replayAccountState.";
1082
1083 pub const SEND_ACTION: &str = "Use kind: simulate, which runs against the replayed state without committing, post state \
1084 directly with an action carrying accountOverrides and no transactions, or create the \
1085 session without replayAccountState.";
1086
1087 pub fn unsupported(operation: &str, remedy: &str) -> String {
1089 format!(
1090 "{operation} is not supported while account state replay is enabled: replay \
1091 reconstructs account state from the recorded delta bundle instead of executing \
1092 transactions, so this session never executes or persists any. {remedy}"
1093 )
1094 }
1095
1096 pub fn send_action_conflict(params: &CreateSessionParams) -> Option<String> {
1099 (params.replay_account_state
1100 && params
1101 .actions
1102 .iter()
1103 .any(|action| action.kind == ActionKind::Send))
1104 .then(|| unsupported("A scheduled action with kind: send", SEND_ACTION))
1105 }
1106}
1107
1108impl CreateSessionParams {
1109 pub fn requires_sidecar(&self) -> bool {
1112 self.reroute.as_ref().is_some_and(|reroute| reroute.requote)
1113 || !self.diagnostic_probes.is_empty()
1114 }
1115
1116 pub fn requires_diagnostic_endpoint(&self) -> bool {
1120 !self.diagnostic_probes.is_empty()
1121 }
1122
1123 pub fn validate_diagnostic_probes(&self) -> Result<(), DiagnosticProbeError> {
1124 self.diagnostic_probes
1125 .iter()
1126 .try_for_each(DiagnosticProbeParams::validate)
1127 }
1128
1129 pub fn warms_account_universe(&self) -> bool {
1131 self.requires_sidecar()
1132 || self
1133 .reroute
1134 .as_ref()
1135 .is_some_and(|reroute| !reroute.direct_fill.is_empty())
1136 }
1137
1138 pub fn reroute_order_flow(&self) -> bool {
1139 self.reroute.is_some()
1140 }
1141
1142 pub fn override_schedule(&self) -> Result<OverrideSchedule, String> {
1145 decode_override_actions(&self.actions)
1146 }
1147
1148 pub fn reroute_filter(&self) -> Option<&RerouteFilter> {
1150 self.reroute.as_ref()?.filter.as_ref()
1151 }
1152}
1153
1154#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1157#[serde(rename_all = "kebab-case")]
1158pub enum FailFastDivergenceKind {
1159 #[default]
1162 AnyNonBenign,
1163 Tracked,
1166}
1167
1168impl FailFastDivergenceKind {
1169 pub fn as_str(self) -> &'static str {
1172 match self {
1173 Self::AnyNonBenign => "any-non-benign",
1174 Self::Tracked => "tracked",
1175 }
1176 }
1177
1178 pub fn from_str_opt(value: &str) -> Option<Self> {
1180 match value {
1181 "any-non-benign" => Some(Self::AnyNonBenign),
1182 "tracked" => Some(Self::Tracked),
1183 _ => None,
1184 }
1185 }
1186}
1187
1188#[serde_with::serde_as]
1190#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1191pub struct AccountModifications(
1192 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
1193 #[serde(default)]
1194 pub BTreeMap<Address, AccountData>,
1195);
1196
1197impl From<BTreeMap<Address, AccountData>> for AccountModifications {
1198 fn from(value: BTreeMap<Address, AccountData>) -> Self {
1199 Self(value)
1200 }
1201}
1202
1203#[serde_with::serde_as]
1205#[derive(Debug, Serialize, Deserialize)]
1206#[serde(rename_all = "camelCase")]
1207pub struct ContinueParams {
1208 #[serde(default = "ContinueParams::default_advance_count")]
1209 pub advance_count: u64,
1211 #[serde(default)]
1212 pub transactions: Vec<String>,
1214 #[serde(default)]
1215 pub modify_account_states: AccountModifications,
1217}
1218
1219impl Default for ContinueParams {
1220 fn default() -> Self {
1221 Self {
1222 advance_count: Self::default_advance_count(),
1223 transactions: Vec::new(),
1224 modify_account_states: AccountModifications(BTreeMap::new()),
1225 }
1226 }
1227}
1228
1229impl ContinueParams {
1230 pub fn default_advance_count() -> u64 {
1231 1
1232 }
1233}
1234
1235#[derive(Debug, Clone, Serialize, Deserialize)]
1241#[serde(rename_all = "camelCase")]
1242pub struct PausedEvent {
1243 pub slot: u64,
1244 #[serde(default, skip_serializing_if = "Option::is_none")]
1245 pub batch_index: Option<u32>,
1246}
1247
1248#[serde_with::serde_as]
1257#[derive(Debug, Clone, Serialize, Deserialize)]
1258#[serde(rename_all = "camelCase")]
1259pub struct DiscoveryBatchEvent {
1260 pub slot: u64,
1261 pub batch_index: u32,
1262 pub matched: Vec<DiscoveryFilter>,
1264 pub transactions: Vec<EncodedBinary>,
1268}
1269
1270#[derive(Debug, Clone, Serialize, Deserialize)]
1272#[serde(rename_all = "camelCase")]
1273pub struct ContinueToParams {
1274 pub slot: u64,
1276 #[serde(default)]
1282 pub batch_index: Option<u32>,
1283}
1284
1285#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1287#[serde(rename_all = "lowercase")]
1288pub enum BinaryEncoding {
1289 Base64,
1290}
1291
1292impl BinaryEncoding {
1293 pub fn encode(self, bytes: &[u8]) -> String {
1294 match self {
1295 Self::Base64 => BASE64.encode(bytes),
1296 }
1297 }
1298
1299 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
1300 match self {
1301 Self::Base64 => BASE64.decode(data),
1302 }
1303 }
1304}
1305
1306#[derive(Debug, Clone, Serialize, Deserialize)]
1308#[serde(rename_all = "camelCase")]
1309pub struct EncodedBinary {
1310 pub data: String,
1312 pub encoding: BinaryEncoding,
1314}
1315
1316impl EncodedBinary {
1317 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
1318 Self { data, encoding }
1319 }
1320
1321 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
1322 Self {
1323 data: encoding.encode(bytes),
1324 encoding,
1325 }
1326 }
1327
1328 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
1329 self.encoding.decode(&self.data)
1330 }
1331}
1332
1333#[serde_with::serde_as]
1335#[derive(Debug, Clone, Serialize, Deserialize)]
1336#[serde(rename_all = "camelCase")]
1337pub struct AccountData {
1338 pub data: EncodedBinary,
1340 pub executable: bool,
1342 pub lamports: u64,
1344 #[serde_as(as = "serde_with::DisplayFromStr")]
1345 pub owner: Address,
1347 pub space: u64,
1349}
1350
1351impl AccountData {
1352 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
1353 Ok(solana_account::Account {
1354 data: self.data.decode()?,
1355 lamports: self.lamports,
1356 owner: self.owner,
1357 executable: self.executable,
1358 rent_epoch: 0,
1359 })
1360 }
1361}
1362
1363#[derive(Debug, Clone, Serialize, Deserialize)]
1365#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1366pub enum BacktestResponse {
1367 SessionCreated {
1368 session_id: SessionId,
1369 rpc_endpoint: String,
1370 #[serde(default, skip_serializing_if = "Option::is_none")]
1371 task_id: Option<String>,
1372 },
1373 SessionAttached {
1374 session_id: SessionId,
1375 rpc_endpoint: String,
1376 #[serde(default, skip_serializing_if = "Option::is_none")]
1377 task_id: Option<String>,
1378 },
1379 SessionsCreatedV2 {
1380 control_session_id: ControlSessionId,
1381 session_ids: Vec<SessionId>,
1382 #[serde(default)]
1383 task_ids: Vec<Option<String>>,
1384 #[serde(default)]
1389 start_slots: Vec<u64>,
1390 #[serde(default)]
1391 end_slots: Vec<u64>,
1392 },
1393 ParallelSessionAttachedV2 {
1394 control_session_id: ControlSessionId,
1395 session_ids: Vec<SessionId>,
1396 #[serde(default)]
1397 task_ids: Vec<Option<String>>,
1398 },
1399 ReadyForContinue,
1400 SlotNotification(u64),
1401 Paused(PausedEvent),
1402 DiscoveryBatch(DiscoveryBatchEvent),
1403 Error(BacktestError),
1404 Success,
1405 Completed {
1406 #[serde(skip_serializing_if = "Option::is_none")]
1410 summary: Option<SessionSummary>,
1411 },
1412 Status {
1413 status: BacktestStatus,
1414 },
1415 SessionEventV2 {
1416 session_id: SessionId,
1417 seq_id: u64,
1418 event: SessionEventKind,
1419 },
1420}
1421
1422impl BacktestResponse {
1423 pub fn is_completed(&self) -> bool {
1424 matches!(self, BacktestResponse::Completed { .. })
1425 }
1426
1427 pub fn is_terminal(&self) -> bool {
1428 match self {
1429 BacktestResponse::Completed { .. } => true,
1430 BacktestResponse::Error(e) => e.is_terminal(),
1431 _ => false,
1432 }
1433 }
1434}
1435
1436impl From<BacktestStatus> for BacktestResponse {
1437 fn from(status: BacktestStatus) -> Self {
1438 Self::Status { status }
1439 }
1440}
1441
1442impl From<String> for BacktestResponse {
1443 fn from(message: String) -> Self {
1444 BacktestError::Internal { error: message }.into()
1445 }
1446}
1447
1448impl From<&str> for BacktestResponse {
1449 fn from(message: &str) -> Self {
1450 BacktestError::Internal {
1451 error: message.to_string(),
1452 }
1453 .into()
1454 }
1455}
1456
1457#[derive(Debug, Clone, Serialize, Deserialize)]
1458#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1459pub enum SessionEventKind {
1460 ReadyForContinue,
1461 SlotNotification(u64),
1462 Paused(PausedEvent),
1463 DiscoveryBatch(DiscoveryBatchEvent),
1464 Error(BacktestError),
1465 Success,
1466 Completed {
1467 #[serde(skip_serializing_if = "Option::is_none")]
1468 summary: Option<SessionSummary>,
1469 },
1470 Status {
1471 status: BacktestStatus,
1472 },
1473}
1474
1475impl SessionEventKind {
1476 pub fn is_terminal(&self) -> bool {
1477 match self {
1478 Self::Completed { .. } => true,
1479 Self::Error(e) => e.is_terminal(),
1480 _ => false,
1481 }
1482 }
1483}
1484
1485#[derive(Debug, Clone, Serialize, Deserialize)]
1488#[serde(rename_all = "camelCase")]
1489pub struct SequencedResponse {
1490 pub seq_id: u64,
1491 #[serde(flatten)]
1492 pub response: BacktestResponse,
1493}
1494
1495#[derive(Debug, Clone, Serialize, Deserialize)]
1497#[serde(rename_all = "camelCase")]
1498pub enum BacktestStatus {
1499 StartingRuntime,
1501 DecodedTransactions,
1502 AppliedAccountModifications,
1503 ReadyToExecuteUserTransactions,
1504 ExecutedUserTransactions,
1505 ExecutingBlockTransactions,
1506 ExecutedBlockTransactions,
1507 ProgramAccountsLoaded,
1508}
1509
1510impl std::fmt::Display for BacktestStatus {
1511 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1512 let s = match self {
1513 Self::StartingRuntime => "starting runtime",
1514 Self::DecodedTransactions => "decoded transactions",
1515 Self::AppliedAccountModifications => "applied account modifications",
1516 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
1517 Self::ExecutedUserTransactions => "executed user transactions",
1518 Self::ExecutingBlockTransactions => "executing block transactions",
1519 Self::ExecutedBlockTransactions => "executed block transactions",
1520 Self::ProgramAccountsLoaded => "program accounts loaded",
1521 };
1522 f.write_str(s)
1523 }
1524}
1525
1526#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1531#[serde(rename_all = "camelCase")]
1532pub struct RerouteStatsReport {
1533 #[serde(default)]
1535 pub swaps_detected: u64,
1536 #[serde(default)]
1540 pub swaps_filtered: u64,
1541 #[serde(default)]
1543 pub swaps_rerouted: u64,
1544 #[serde(default)]
1546 pub swaps_simulated: u64,
1547 #[serde(default)]
1549 pub swaps_succeeded: u64,
1550 #[serde(default)]
1552 pub requote_failures: u64,
1553 #[serde(default)]
1555 pub override_setup_failures: u64,
1556 #[serde(default)]
1558 pub metis_unavailable_blocks: u64,
1559 #[serde(default)]
1563 pub quote_attempts_unavailable: u64,
1564 #[serde(default)]
1567 pub direct_fill_matched: u64,
1568 #[serde(default)]
1572 pub direct_fill_outcomes: BTreeMap<String, u64>,
1573 #[serde(default)]
1575 pub direct_fill_scored: u64,
1576 #[serde(default)]
1580 pub direct_fill_bps_total: i64,
1581 #[serde(default)]
1584 pub direct_fill_templates: BTreeMap<String, DirectFillTemplateReport>,
1585 #[serde(default)]
1587 pub originals_scored: u64,
1588 #[serde(default)]
1591 pub originals_bps_total: i64,
1592 #[serde(default)]
1594 pub originals_replayed: u64,
1595}
1596
1597#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1600#[serde(rename_all = "camelCase")]
1601pub struct DirectFillTemplateReport {
1602 pub matched: u64,
1603 pub outcomes: BTreeMap<String, u64>,
1604 pub scored: u64,
1605 pub bps_total: i64,
1606}
1607
1608#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1610#[serde(rename_all = "camelCase")]
1611pub struct SessionSummary {
1612 pub correct_simulation: usize,
1615 pub incorrect_simulation: usize,
1618 pub execution_errors: usize,
1620 pub balance_diff: usize,
1622 pub log_diff: usize,
1624 #[serde(default)]
1627 pub unsanitizable: usize,
1628 #[serde(default, skip_serializing_if = "Option::is_none")]
1631 pub reroute_stats: Option<Box<RerouteStatsReport>>,
1632}
1633
1634impl SessionSummary {
1635 pub fn has_deviations(&self) -> bool {
1637 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
1638 }
1639
1640 pub fn total_transactions(&self) -> usize {
1642 self.correct_simulation
1643 + self.incorrect_simulation
1644 + self.execution_errors
1645 + self.balance_diff
1646 + self.log_diff
1647 }
1648
1649 pub fn deviation_details(&self, balance_label: &str, log_label: &str) -> String {
1654 format!(
1655 "{} incorrect simulations, {} execution errors, {} {balance_label}, {} {log_label}",
1656 self.incorrect_simulation, self.execution_errors, self.balance_diff, self.log_diff,
1657 )
1658 }
1659}
1660
1661impl std::fmt::Display for SessionSummary {
1662 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1663 let total = self.total_transactions();
1664 write!(
1665 f,
1666 "Session summary: {total} transactions\n\
1667 \x20 - {} correct simulation\n\
1668 \x20 - {} incorrect simulation\n\
1669 \x20 - {} execution errors\n\
1670 \x20 - {} unsanitizable\n\
1671 \x20 - {} balance diffs\n\
1672 \x20 - {} log diffs",
1673 self.correct_simulation,
1674 self.incorrect_simulation,
1675 self.execution_errors,
1676 self.unsanitizable,
1677 self.balance_diff,
1678 self.log_diff,
1679 )
1680 }
1681}
1682
1683#[derive(Debug, Clone, Serialize, Deserialize)]
1685#[serde(rename_all = "camelCase")]
1686pub enum BacktestError {
1687 InvalidTransactionEncoding {
1688 index: usize,
1689 error: String,
1690 },
1691 InvalidTransactionFormat {
1692 index: usize,
1693 error: String,
1694 },
1695 InvalidAccountEncoding {
1696 address: String,
1697 encoding: BinaryEncoding,
1698 error: String,
1699 },
1700 InvalidAccountOwner {
1701 address: String,
1702 error: String,
1703 },
1704 InvalidAccountPubkey {
1705 address: String,
1706 error: String,
1707 },
1708 NoMoreBlocks,
1709 BundleNotFound {
1710 start_slot: u64,
1711 end_slot: u64,
1712 },
1713 AdvanceSlotFailed {
1714 slot: u64,
1715 error: String,
1716 },
1717 FinalizeSlotFailed {
1718 slot: u64,
1719 error: String,
1720 },
1721 InvalidRequest {
1722 error: String,
1723 },
1724 Internal {
1725 error: String,
1726 },
1727 InvalidBlockhashFormat {
1728 slot: u64,
1729 error: String,
1730 },
1731 InitializingSysvarsFailed {
1732 slot: u64,
1733 error: String,
1734 },
1735 ClerkError {
1736 error: String,
1737 },
1738 SimulationError {
1739 error: String,
1740 },
1741 SessionNotFound {
1742 session_id: String,
1743 },
1744 SessionOwnerMismatch,
1745 SessionOwnershipBusy {
1750 reason: String,
1751 },
1752 NoProducedBlocksInRange {
1758 start_slot: u64,
1759 end_slot: u64,
1760 },
1761}
1762
1763impl BacktestError {
1764 pub fn is_terminal(&self) -> bool {
1768 matches!(
1769 self,
1770 BacktestError::NoMoreBlocks
1771 | BacktestError::BundleNotFound { .. }
1772 | BacktestError::AdvanceSlotFailed { .. }
1773 | BacktestError::FinalizeSlotFailed { .. }
1774 | BacktestError::Internal { .. }
1775 | BacktestError::NoProducedBlocksInRange { .. }
1776 )
1777 }
1778}
1779
1780#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1782pub struct AvailableRange {
1783 pub bundle_start_slot: u64,
1784 pub bundle_start_slot_utc: Option<String>,
1785 pub max_bundle_end_slot: Option<u64>,
1786 pub max_bundle_end_slot_utc: Option<String>,
1787 pub max_bundle_size: Option<u64>,
1788 #[serde(default)]
1791 pub kind: Option<BundleKind>,
1792 #[serde(default)]
1795 pub scope: Option<RangeScope>,
1796}
1797
1798#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1800#[serde(rename_all = "snake_case")]
1801pub enum RangeScope {
1802 Global,
1804 User,
1806}
1807
1808impl RangeScope {
1809 pub fn as_str(self) -> &'static str {
1811 match self {
1812 Self::Global => "global",
1813 Self::User => "user",
1814 }
1815 }
1816}
1817
1818#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1820#[serde(rename_all = "snake_case")]
1821pub enum BundleKind {
1822 #[default]
1824 Blocks,
1825 AccountState,
1829}
1830
1831pub const REPLAY_RANGE_ADVICE: &str = "replayed state is resolved by the recording that begins \
1835 where the request does, so request a range that begins where one was recorded and ends \
1836 within it";
1837
1838impl BundleKind {
1839 fn coverage_noun(self) -> &'static str {
1841 match self {
1842 Self::Blocks => "bundle range",
1843 Self::AccountState => "account-state bundle",
1844 }
1845 }
1846
1847 fn coverage_nouns(self) -> &'static str {
1849 match self {
1850 Self::Blocks => "bundle ranges",
1851 Self::AccountState => "account-state bundles",
1852 }
1853 }
1854
1855 pub fn as_str(self) -> &'static str {
1857 match self {
1858 Self::Blocks => "blocks",
1859 Self::AccountState => "account_state",
1860 }
1861 }
1862
1863 pub fn from_str_opt(value: &str) -> Option<Self> {
1865 match value {
1866 "blocks" => Some(Self::Blocks),
1867 "account_state" => Some(Self::AccountState),
1868 _ => None,
1869 }
1870 }
1871}
1872
1873impl fmt::Display for BundleKind {
1876 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1877 let label = match self {
1878 Self::Blocks => "transactions",
1879 Self::AccountState => "account_state",
1880 };
1881 f.pad(label)
1882 }
1883}
1884
1885#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1890#[serde(rename_all = "snake_case")]
1891pub enum JobKind {
1892 #[default]
1894 BundleBuild,
1895 Depth,
1897}
1898
1899impl JobKind {
1900 pub fn as_str(self) -> &'static str {
1902 match self {
1903 Self::BundleBuild => "bundle_build",
1904 Self::Depth => "depth",
1905 }
1906 }
1907
1908 pub fn from_str_opt(value: &str) -> Option<Self> {
1910 match value {
1911 "bundle_build" => Some(Self::BundleBuild),
1912 "depth" => Some(Self::Depth),
1913 _ => None,
1914 }
1915 }
1916}
1917
1918#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1923#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
1924#[serde(rename_all = "snake_case")]
1925pub enum DepthSource {
1926 #[default]
1928 Onchain,
1929 Router,
1931}
1932
1933impl DepthSource {
1934 pub const fn as_str(self) -> &'static str {
1936 match self {
1937 Self::Onchain => "onchain",
1938 Self::Router => "router",
1939 }
1940 }
1941
1942 pub fn from_str_opt(value: &str) -> Option<Self> {
1944 match value {
1945 "onchain" => Some(Self::Onchain),
1946 "router" => Some(Self::Router),
1947 _ => None,
1948 }
1949 }
1950
1951 pub const fn discriminant(self) -> u8 {
1956 match self {
1957 Self::Onchain => 0,
1958 Self::Router => 1,
1959 }
1960 }
1961}
1962
1963impl fmt::Display for DepthSource {
1964 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1965 f.write_str(self.as_str())
1966 }
1967}
1968
1969#[derive(Debug, Clone, Serialize, Deserialize)]
1974pub struct DepthCoverageResponse {
1975 pub sources: Vec<DepthSourceCoverage>,
1976}
1977
1978#[derive(Debug, Clone, Serialize, Deserialize)]
1979pub struct DepthSourceCoverage {
1980 pub source: DepthSource,
1982 pub revision: u16,
1985 pub markets: Vec<String>,
1987 pub venues: Vec<String>,
1989 pub size_rungs: usize,
1991 pub completed_ranges: Vec<SlotRange>,
1994}
1995
1996#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1998pub struct DepthRequest {
1999 pub start_slot: u64,
2002 pub end_slot: u64,
2004 #[serde(default)]
2007 pub source: DepthSource,
2008 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2010 pub markets: Vec<String>,
2011}
2012
2013#[derive(Debug, Clone, Serialize, Deserialize)]
2015pub struct DepthStatusResponse {
2016 pub request_id: String,
2017 pub start_slot: u64,
2018 pub end_slot: u64,
2019 pub source: DepthSource,
2020 pub status: BundleBuildStatus,
2021}
2022
2023#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2027pub struct BundleBuildRequest {
2028 pub start_slot: u64,
2029 pub end_slot: u64,
2030 #[serde(default)]
2031 pub bundle_size: Option<u64>,
2032 #[serde(default)]
2035 pub idempotency_key: Option<String>,
2036 #[serde(default)]
2038 pub kind: BundleKind,
2039}
2040
2041#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2042#[serde(rename_all = "snake_case")]
2043pub enum BundleBuildStatus {
2044 Completed,
2046 Failed,
2048 InProgress,
2050 NeedsInvestigation,
2053}
2054
2055impl BundleBuildStatus {
2056 pub fn as_str(self) -> &'static str {
2058 match self {
2059 Self::Completed => "completed",
2060 Self::Failed => "failed",
2061 Self::InProgress => "in_progress",
2062 Self::NeedsInvestigation => "needs_investigation",
2063 }
2064 }
2065}
2066
2067#[derive(Debug, Clone, Serialize, Deserialize)]
2069pub struct BundleBuildStatusResponse {
2070 pub request_id: String,
2071 pub start_slot: u64,
2072 pub end_slot: u64,
2073 pub bundle_size: Option<u64>,
2074 pub status: BundleBuildStatus,
2075 pub created_at_unix_ms: u64,
2078 #[serde(default)]
2080 pub kind: BundleKind,
2081 #[serde(default)]
2084 pub eta_secs: Option<u64>,
2085}
2086
2087#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2089pub struct CompletedBundle {
2090 pub start_slot: u64,
2091 pub end_slot: u64,
2092 pub kind: BundleKind,
2093 #[serde(default)]
2095 pub completed_at_unix_ms: Option<i64>,
2096 #[serde(default)]
2097 pub size_bytes: Option<i64>,
2098}
2099
2100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2102pub struct BundleDeleteRequest {
2103 pub start_slot: u64,
2104 pub end_slot: u64,
2105 #[serde(default)]
2107 pub kind: BundleKind,
2108}
2109
2110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2112pub struct BundleDeleteResponse {
2113 pub start_slot: u64,
2114 pub end_slot: u64,
2115 pub kind: BundleKind,
2116}
2117
2118#[derive(Debug, Clone, Serialize, Deserialize)]
2120pub struct BundleBuildListResponse {
2121 pub builds: Vec<BundleBuildStatusResponse>,
2124 #[serde(default)]
2129 pub bundles: Option<Vec<CompletedBundle>>,
2130}
2131
2132#[derive(Debug, Clone, Serialize, Deserialize)]
2135pub struct SnapshotSlotResponse {
2136 pub snapshot_slot: u64,
2137}
2138
2139pub fn split_range(
2162 ranges: &[AvailableRange],
2163 requested: SlotRange,
2164 kind: BundleKind,
2165) -> Result<Vec<(u64, u64)>, String> {
2166 let (requested_start, requested_end) = (requested.start(), requested.end());
2167
2168 let ends_by_start = ranges
2175 .iter()
2176 .filter(|r| r.kind.unwrap_or_default() == kind)
2177 .filter_map(|r| {
2178 r.max_bundle_end_slot
2179 .filter(|&end| end >= r.bundle_start_slot)
2180 .map(|end| (r.bundle_start_slot, end))
2181 })
2182 .fold(
2183 BTreeMap::<u64, BTreeSet<u64>>::new(),
2184 |mut ends_by_start, (start, end)| {
2185 ends_by_start.entry(start).or_default().insert(end);
2186 ends_by_start
2187 },
2188 );
2189
2190 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
2198 ends.iter()
2199 .next_back()
2200 .is_some_and(|&end| end >= requested_start)
2201 }) else {
2202 return Err(format!(
2203 "start_slot {requested_start} is not covered by any available {}",
2204 kind.coverage_noun()
2205 ));
2206 };
2207
2208 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
2214 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
2215 let mut best: Option<Vec<(u64, u64)>> = None;
2216 for &end in ends {
2217 let candidate = if end >= requested_end {
2218 Some(vec![(start, requested_end)])
2219 } else {
2220 best_from.get(&(end + 1)).map(|rest| {
2221 std::iter::once((start, end))
2222 .chain(rest.iter().copied())
2223 .collect()
2224 })
2225 };
2226 if let Some(candidate) = candidate
2227 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
2228 {
2229 best = Some(candidate);
2230 }
2231 }
2232 if let Some(best) = best {
2233 best_from.insert(start, best);
2234 }
2235 }
2236
2237 best_from.remove(&anchor_start).ok_or_else(|| {
2238 let mut covered_to = anchor_start.saturating_sub(1);
2242 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
2243 if start > covered_to.saturating_add(1) {
2244 break;
2245 }
2246 if let Some(&end) = ends.iter().next_back() {
2247 covered_to = covered_to.max(end);
2248 }
2249 }
2250 if covered_to < requested_end {
2251 format!("gap in coverage at slot {}", covered_to + 1)
2252 } else {
2253 format!(
2254 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available {}",
2255 kind.coverage_nouns()
2256 )
2257 }
2258 })
2259}
2260
2261impl From<BacktestError> for BacktestResponse {
2262 fn from(error: BacktestError) -> Self {
2263 Self::Error(error)
2264 }
2265}
2266
2267impl std::error::Error for BacktestError {}
2268
2269impl fmt::Display for BacktestError {
2270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2271 match self {
2272 BacktestError::InvalidTransactionEncoding { index, error } => {
2273 write!(f, "invalid transaction encoding at index {index}: {error}")
2274 }
2275 BacktestError::InvalidTransactionFormat { index, error } => {
2276 write!(f, "invalid transaction format at index {index}: {error}")
2277 }
2278 BacktestError::InvalidAccountEncoding {
2279 address,
2280 encoding,
2281 error,
2282 } => write!(
2283 f,
2284 "invalid encoding for account {address} ({encoding:?}): {error}"
2285 ),
2286 BacktestError::InvalidAccountOwner { address, error } => {
2287 write!(f, "invalid owner for account {address}: {error}")
2288 }
2289 BacktestError::InvalidAccountPubkey { address, error } => {
2290 write!(f, "invalid account pubkey {address}: {error}")
2291 }
2292 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
2293 BacktestError::BundleNotFound {
2294 start_slot,
2295 end_slot,
2296 } => write!(
2297 f,
2298 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
2299 ),
2300 BacktestError::AdvanceSlotFailed { slot, error } => {
2301 write!(f, "failed to advance to slot {slot}: {error}")
2302 }
2303 BacktestError::FinalizeSlotFailed { slot, error } => {
2304 write!(f, "failed to finalize slot {slot}: {error}")
2305 }
2306 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
2307 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
2308 BacktestError::InvalidBlockhashFormat { slot, error } => {
2309 write!(f, "invalid blockhash at slot {slot}: {error}")
2310 }
2311 BacktestError::InitializingSysvarsFailed { slot, error } => {
2312 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
2313 }
2314 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
2315 BacktestError::SimulationError { error } => {
2316 write!(f, "simulation error: {error}")
2317 }
2318 BacktestError::SessionNotFound { session_id } => {
2319 write!(f, "session not found: {session_id}")
2320 }
2321 BacktestError::SessionOwnerMismatch => {
2322 write!(f, "session owner mismatch")
2323 }
2324 BacktestError::SessionOwnershipBusy { reason } => {
2325 write!(f, "session ownership busy: {reason}")
2326 }
2327 BacktestError::NoProducedBlocksInRange {
2328 start_slot,
2329 end_slot,
2330 } => write!(
2331 f,
2332 "no produced blocks in requested range {start_slot}..={end_slot}; \
2333 slot(s) may be skipped on-chain"
2334 ),
2335 }
2336 }
2337}
2338
2339#[cfg(test)]
2340mod tests {
2341 use rstest::rstest;
2342 use solana_account::ReadableAccount;
2343
2344 use super::*;
2345
2346 #[test]
2349 fn a_sources_discriminant_is_frozen_independently_of_its_label() {
2350 assert_eq!(DepthSource::Onchain.discriminant(), 0);
2351 assert_eq!(DepthSource::Router.discriminant(), 1);
2352 }
2353
2354 #[test]
2357 fn a_source_spells_itself_the_same_way_three_times() {
2358 for source in [DepthSource::Onchain, DepthSource::Router] {
2359 assert_eq!(
2360 serde_json::to_value(source).expect("a source serialises"),
2361 serde_json::json!(source.as_str())
2362 );
2363 assert_eq!(DepthSource::from_str_opt(source.as_str()), Some(source));
2364 }
2365 }
2366
2367 fn address(byte: u8) -> Address {
2368 Address::from([byte; 32])
2369 }
2370
2371 #[rstest]
2372 #[case::one("jupiter", Some(&[SwapAggregator::Jupiter][..]))]
2373 #[case::several("jupiter,okx", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2374 #[case::spaced_and_uppercased(" Jupiter , OKX ", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2375 #[case::repeats_collapse("okx,okx", Some(&[SwapAggregator::Okx][..]))]
2376 #[case::trailing_separator("okx,", Some(&[SwapAggregator::Okx][..]))]
2377 #[case::unknown("jupiter,bogus", None)]
2378 #[case::empty("", None)]
2379 #[case::separators_only(",,", None)]
2380 fn reroute_aggregators_parse_the_shapes_a_flag_carries(
2381 #[case] input: &str,
2382 #[case] expected: Option<&[SwapAggregator]>,
2383 ) {
2384 assert_eq!(
2385 input.parse::<RerouteAggregators>().ok(),
2386 expected.map(|aggregators| RerouteAggregators::new(aggregators.iter().copied()))
2387 );
2388 }
2389
2390 #[test]
2392 fn reroute_aggregators_round_trip_through_their_rendering() {
2393 let aggregators = RerouteAggregators::new([SwapAggregator::Dflow, SwapAggregator::Jupiter]);
2394
2395 assert_eq!(aggregators.to_string(), "jupiter,dflow");
2396 assert_eq!(
2397 aggregators
2398 .to_string()
2399 .parse::<RerouteAggregators>()
2400 .unwrap(),
2401 aggregators
2402 );
2403 assert_eq!(RerouteAggregators::default().to_string(), "jupiter");
2404 }
2405
2406 #[test]
2409 fn aggregators_deserialize_through_the_same_parser_the_flag_uses() {
2410 let params: CreateSessionParams = serde_json::from_value(
2411 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {"aggregators": ["Jupiter", "OKX"]}}),
2412 )
2413 .expect("decode");
2414
2415 assert_eq!(
2416 params.reroute.expect("rerouting requested").aggregators,
2417 RerouteAggregators::new([SwapAggregator::Jupiter, SwapAggregator::Okx])
2418 );
2419 }
2420
2421 #[test]
2424 fn params_without_the_new_reroute_fields_decode_to_the_narrow_defaults() {
2425 let params: CreateSessionParams = serde_json::from_value(
2426 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {}}),
2427 )
2428 .expect("decode");
2429
2430 let reroute = params.reroute.expect("rerouting requested");
2431 assert_eq!(reroute.aggregators, RerouteAggregators::default());
2432 assert!(reroute.aggregators.contains(SwapAggregator::Jupiter));
2433 assert!(!reroute.aggregators.contains(SwapAggregator::Okx));
2434 assert!(!reroute.circular_arbs);
2435 assert!(!reroute.detect_failed_l1_swaps);
2436 }
2437
2438 #[test]
2440 fn a_request_without_reroute_reserves_no_sidecar() {
2441 let params: CreateSessionParams =
2442 serde_json::from_value(serde_json::json!({"startSlot": 1, "endSlot": 2}))
2443 .expect("decode");
2444
2445 assert!(!params.requires_sidecar());
2446 }
2447
2448 #[test]
2453 fn a_per_slot_action_schedule_compresses_across_actions() {
2454 let slots = 2_000u64;
2455 let actions = (0..slots)
2456 .map(|slot| ScheduledAction {
2457 anchor: ActionAnchor::AfterSlot { slots: vec![slot] },
2458 kind: ActionKind::Simulate,
2459 feeds_reroute: false,
2460 transactions: Vec::new(),
2461 account_overrides: AccountModifications(BTreeMap::from([(
2462 address(7),
2463 AccountData {
2464 data: EncodedBinary::new(
2466 BASE64.encode(
2467 [&slot.to_le_bytes()[..], &[0xAB; 2040][..]]
2468 .concat()
2469 .as_slice(),
2470 ),
2471 BinaryEncoding::Base64,
2472 ),
2473 executable: false,
2474 lamports: 15_144_966,
2475 owner: address(9),
2476 space: 2048,
2477 },
2478 )])),
2479 return_accounts: Vec::new(),
2480 label: None,
2481 })
2482 .collect::<Vec<_>>();
2483 let params = CreateSessionParams::builder()
2484 .start_slot(1)
2485 .end_slot(slots)
2486 .actions(actions.clone())
2487 .build();
2488
2489 let encoded = serde_json::to_string(¶ms).expect("serialize");
2490 let raw = serde_json::to_string(&actions).expect("serialize raw actions");
2491 assert!(
2492 encoded.len() * 8 < raw.len(),
2493 "expected the schedule to compress well; got {} bytes from {} raw",
2494 encoded.len(),
2495 raw.len()
2496 );
2497
2498 let decoded: CreateSessionParams = serde_json::from_str(&encoded).expect("round trip");
2499 assert_eq!(
2500 serde_json::to_string(&decoded.actions).expect("serialize decoded"),
2501 raw
2502 );
2503 }
2504
2505 #[test]
2508 fn an_empty_action_list_is_omitted_entirely() {
2509 let params = CreateSessionParams::builder()
2510 .start_slot(1)
2511 .end_slot(2)
2512 .build();
2513 let json = serde_json::to_value(¶ms).expect("serialize");
2514 assert!(json.get("actions").is_none(), "{json}");
2515 let decoded: CreateSessionParams = serde_json::from_value(json).expect("round trip");
2516 assert!(decoded.actions.is_empty());
2517 }
2518
2519 #[test]
2522 fn send_actions_conflict_with_replay_only_together() {
2523 let params = |replay, kind| {
2524 CreateSessionParams::builder()
2525 .start_slot(1)
2526 .end_slot(2)
2527 .replay_account_state(replay)
2528 .actions(vec![ScheduledAction {
2529 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2530 kind,
2531 feeds_reroute: false,
2532 transactions: vec!["dGVzdA==".to_string()],
2533 account_overrides: Default::default(),
2534 return_accounts: Default::default(),
2535 label: None,
2536 }])
2537 .build()
2538 };
2539 assert!(replay_support::send_action_conflict(¶ms(false, ActionKind::Send)).is_none());
2540 assert!(
2541 replay_support::send_action_conflict(¶ms(true, ActionKind::Simulate)).is_none()
2542 );
2543 let mut publishing = params(true, ActionKind::Simulate);
2544 publishing.actions[0].feeds_reroute = true;
2545 assert!(replay_support::send_action_conflict(&publishing).is_none());
2546
2547 let conflict = replay_support::send_action_conflict(¶ms(true, ActionKind::Send))
2548 .expect("send under replay conflicts");
2549 assert!(conflict.contains("kind: send"), "{conflict}");
2550 assert!(conflict.contains(replay_support::SEND_ACTION), "{conflict}");
2551 }
2552
2553 fn override_entry(owner: Address, executable: bool) -> AccountData {
2554 AccountData {
2555 data: EncodedBinary::from_bytes(&[1, 2, 3], BinaryEncoding::Base64),
2556 executable,
2557 lamports: 1,
2558 owner,
2559 space: 3,
2560 }
2561 }
2562
2563 fn before() -> ActionAnchor {
2564 ActionAnchor::BeforeSlot { slots: vec![1] }
2565 }
2566
2567 fn valid() -> ScheduledAction {
2568 override_action(before(), address(3), override_entry(address(4), false))
2569 }
2570
2571 fn override_action(
2572 anchor: ActionAnchor,
2573 account: Address,
2574 data: AccountData,
2575 ) -> ScheduledAction {
2576 ScheduledAction {
2577 anchor,
2578 kind: ActionKind::default(),
2579 transactions: Vec::new(),
2580 account_overrides: AccountModifications(BTreeMap::from([(account, data)])),
2581 return_accounts: Vec::new(),
2582 label: None,
2583 feeds_reroute: true,
2584 }
2585 }
2586
2587 #[test]
2588 fn override_action_round_trips() {
2589 let key = address(1);
2590 let entry = override_entry(address(2), false);
2591 let action = override_action(
2592 ActionAnchor::BeforeSlot { slots: vec![5, 9] },
2593 key,
2594 entry.clone(),
2595 );
2596 let json = serde_json::to_value(&action).unwrap();
2597 assert!(
2598 json["anchor"]["slots"].is_string(),
2599 "slots travel as a compressed blob: {json}"
2600 );
2601 let back: ScheduledAction = serde_json::from_value(json).unwrap();
2602 assert_eq!(back.anchor, ActionAnchor::BeforeSlot { slots: vec![5, 9] });
2603 assert!(back.feeds_reroute);
2604 assert!(!back.is_computed_override(), "no transactions to simulate");
2605 let survived = &back.account_overrides.0[&key];
2606 assert_eq!(survived.lamports, entry.lamports);
2607 assert_eq!(survived.data.data, entry.data.data);
2608 }
2609
2610 #[test]
2611 fn an_every_slot_anchor_round_trips_and_defaults_to_every_slot() {
2612 let anchor = ActionAnchor::AfterEverySlot {
2613 every_n_slots: NonZeroU64::new(10).unwrap(),
2614 };
2615 let json = serde_json::to_value(&anchor).unwrap();
2616 assert_eq!(
2617 json,
2618 serde_json::json!({ "at": "afterEverySlot", "everyNSlots": 10 })
2619 );
2620 assert_eq!(
2621 serde_json::from_value::<ActionAnchor>(json).unwrap(),
2622 anchor
2623 );
2624
2625 assert_eq!(
2626 serde_json::from_value::<ActionAnchor>(serde_json::json!({ "at": "beforeEverySlot" }))
2627 .unwrap(),
2628 ActionAnchor::BeforeEverySlot {
2629 every_n_slots: NonZeroU64::MIN
2630 }
2631 );
2632 assert!(
2633 serde_json::from_value::<ActionAnchor>(
2634 serde_json::json!({ "at": "beforeEverySlot", "everyNSlots": 0 })
2635 )
2636 .is_err(),
2637 "a zero cadence names no slot"
2638 );
2639 }
2640
2641 #[test]
2646 fn a_token_account_override_is_accepted() {
2647 let action = override_action(
2648 before(),
2649 address(3),
2650 override_entry(spl_token_interface::ID, false),
2651 );
2652 let schedule = decode_override_actions(&[action]).expect("token overrides decode");
2653 assert!(!schedule.active_at(1).is_empty());
2654 }
2655
2656 #[rstest]
2657 #[case::executable(override_action(before(), address(3), override_entry(address(10), true)))]
2658 #[case::program_owned(override_action(
2659 before(),
2660 address(3),
2661 override_entry(solana_account::PROGRAM_OWNERS[0], false),
2662 ))]
2663 #[case::after_slot_anchor(override_action(
2664 ActionAnchor::AfterSlot { slots: vec![1] },
2665 address(3),
2666 override_entry(address(4), false),
2667 ))]
2668 #[case::every_slot_anchor(override_action(
2669 ActionAnchor::BeforeEverySlot { every_n_slots: NonZeroU64::MIN },
2670 address(3),
2671 override_entry(address(4), false),
2672 ))]
2673 #[case::nothing_to_post(ScheduledAction {
2674 account_overrides: AccountModifications::default(),
2675 ..valid()
2676 })]
2677 #[case::send(ScheduledAction {
2679 kind: ActionKind::Send,
2680 ..valid()
2681 })]
2682 #[allow(deprecated, reason = "exercise the loader-owner rejection path")]
2683 fn override_actions_reject_invalid_accounts_and_shapes(#[case] action: ScheduledAction) {
2684 assert!(decode_override_actions(&[action]).is_err());
2685 }
2686
2687 fn diagnostic_probe() -> DiagnosticProbeParams {
2688 DiagnosticProbeParams {
2689 anchor: ActionAnchor::AfterEverySlot {
2690 every_n_slots: NonZeroU64::MIN,
2691 },
2692 markets: vec![address(1), address(2)],
2693 usd_values: vec![100, 25_000],
2694 label: Some("depth-0".to_owned()),
2695 }
2696 }
2697
2698 #[rstest]
2699 #[case::no_markets(DiagnosticProbeParams { markets: Vec::new(), ..diagnostic_probe() })]
2700 #[case::no_usd_values(DiagnosticProbeParams { usd_values: Vec::new(), ..diagnostic_probe() })]
2701 #[case::too_many_markets(DiagnosticProbeParams {
2702 markets: (0..=MAX_DIAGNOSTIC_MARKETS as u8).map(address).collect(),
2703 ..diagnostic_probe()
2704 })]
2705 #[case::too_many_usd_values(DiagnosticProbeParams {
2706 usd_values: (1..=MAX_DIAGNOSTIC_USD_VALUES as u32 + 1).collect(),
2707 ..diagnostic_probe()
2708 })]
2709 #[case::zero_usd_value(DiagnosticProbeParams { usd_values: vec![0, 100], ..diagnostic_probe() })]
2710 #[case::duplicate_market(DiagnosticProbeParams {
2711 markets: vec![address(1), address(1)],
2712 ..diagnostic_probe()
2713 })]
2714 #[case::before_slot(DiagnosticProbeParams {
2715 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2716 ..diagnostic_probe()
2717 })]
2718 #[case::before_every_slot(DiagnosticProbeParams {
2719 anchor: ActionAnchor::BeforeEverySlot { every_n_slots: NonZeroU64::MIN },
2720 ..diagnostic_probe()
2721 })]
2722 fn diagnostic_probes_reject_invalid_shapes(#[case] probe: DiagnosticProbeParams) {
2723 assert!(probe.validate().is_err());
2724 }
2725
2726 #[test]
2727 fn diagnostic_probes_accept_both_supported_anchors() {
2728 assert!(diagnostic_probe().validate().is_ok());
2729 assert!(
2730 DiagnosticProbeParams {
2731 anchor: ActionAnchor::AfterSlot { slots: vec![1, 2] },
2732 ..diagnostic_probe()
2733 }
2734 .validate()
2735 .is_ok()
2736 );
2737 }
2738
2739 #[test]
2741 fn override_actions_accept_a_static_and_a_computed_shape() {
2742 assert!(decode_override_actions(&[valid()]).is_ok());
2743 assert!(
2746 decode_override_actions(&[ScheduledAction {
2747 transactions: vec!["dGVzdA==".to_string()],
2748 ..valid()
2749 }])
2750 .is_ok()
2751 );
2752 }
2753
2754 #[test]
2755 fn override_actions_merge_account_wise_later_wins() {
2756 let entry = |lamports| AccountData {
2757 lamports,
2758 ..override_entry(address(9), false)
2759 };
2760 let slot_action = |slots, account, lamports| {
2761 override_action(ActionAnchor::BeforeSlot { slots }, account, entry(lamports))
2762 };
2763 let actions = [
2764 slot_action(vec![5, 7], address(1), 1),
2765 slot_action(vec![5], address(2), 2),
2766 slot_action(vec![5], address(1), 3),
2767 ScheduledAction {
2768 anchor: ActionAnchor::AfterSlot { slots: vec![5] },
2769 kind: ActionKind::Simulate,
2770 feeds_reroute: false,
2771 transactions: vec!["dGVzdA==".to_string()],
2772 account_overrides: AccountModifications::default(),
2773 return_accounts: Vec::new(),
2774 label: None,
2775 },
2776 ];
2777 let schedule = decode_override_actions(&actions).unwrap();
2778 assert_eq!(schedule.slots().collect::<Vec<_>>(), vec![5, 7]);
2779 let lamports_at = |key, slot| {
2780 schedule
2781 .active_for(&key, slot)
2782 .map(|account| account.lamports())
2783 };
2784 assert_eq!(lamports_at(address(1), 5), Some(3));
2785 assert_eq!(lamports_at(address(2), 5), Some(2));
2786 assert_eq!(lamports_at(address(1), 7), Some(1));
2787 }
2788
2789 #[test]
2790 fn computed_override_resolution_preserves_action_order_and_failure() {
2791 let key = address(1);
2792 let static_action = |lamports| {
2793 override_action(
2794 ActionAnchor::BeforeSlot { slots: vec![5] },
2795 key,
2796 AccountData {
2797 lamports,
2798 ..override_entry(address(9), false)
2799 },
2800 )
2801 };
2802 let computed_action = || ScheduledAction {
2803 transactions: vec!["dGVzdA==".to_string()],
2804 ..static_action(2)
2805 };
2806
2807 let computed_last =
2808 decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2809 computed_last
2810 .resolve(5, 1, Some(HashMap::from([(key, shared_account(2))])))
2811 .unwrap();
2812 assert_eq!(
2813 computed_last.active_for(&key, 5).map(|a| a.lamports()),
2814 Some(2)
2815 );
2816
2817 let static_last = decode_override_actions(&[computed_action(), static_action(3)]).unwrap();
2818 static_last
2819 .resolve(5, 0, Some(HashMap::from([(key, shared_account(2))])))
2820 .unwrap();
2821 assert_eq!(
2822 static_last.active_for(&key, 5).map(|a| a.lamports()),
2823 Some(3)
2824 );
2825
2826 let failed = decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2827 failed.resolve(5, 1, None).unwrap();
2828 assert_eq!(failed.active_for(&key, 5).map(|a| a.lamports()), Some(1));
2829 assert!(failed.resolve(5, 1, None).is_err(), "resolving twice");
2830 }
2831
2832 fn shared_account(lamports: u64) -> AccountSharedData {
2833 AccountSharedData::from(solana_account::Account {
2834 lamports,
2835 data: vec![1],
2836 owner: address(9),
2837 executable: false,
2838 rent_epoch: 0,
2839 })
2840 }
2841
2842 fn carry_forward_schedule() -> (Address, Address, OverrideSchedule) {
2843 let (a, b) = (address(1), address(2));
2844 let schedule = OverrideSchedule::from(BTreeMap::from([
2845 (10, HashMap::from([(a, shared_account(1))])),
2846 (
2847 20,
2848 HashMap::from([(a, shared_account(2)), (b, shared_account(9))]),
2849 ),
2850 ]));
2851 (a, b, schedule)
2852 }
2853
2854 #[test]
2855 fn override_schedule_resolves_carry_forward_per_account() {
2856 let (a, b, schedule) = carry_forward_schedule();
2857
2858 assert!(schedule.active_for(&a, 9).is_none());
2859 let cases = [
2860 (9, None, None),
2861 (10, Some(1), None),
2862 (15, Some(1), None),
2863 (20, Some(2), Some(9)),
2864 (u64::MAX, Some(2), Some(9)),
2865 ];
2866 for (slot, want_a, want_b) in cases {
2867 assert_eq!(
2868 schedule
2869 .active_for(&a, slot)
2870 .map(|account| account.lamports()),
2871 want_a,
2872 "active_for a at {slot}"
2873 );
2874 let active = schedule.active_at(slot);
2875 assert_eq!(
2876 active.get(&a).map(ReadableAccount::lamports),
2877 want_a,
2878 "active_at a at {slot}"
2879 );
2880 assert_eq!(
2881 active.get(&b).map(ReadableAccount::lamports),
2882 want_b,
2883 "active_at b at {slot}"
2884 );
2885 }
2886 }
2887
2888 #[test]
2889 fn newly_active_yields_final_values_in_window() {
2890 let (a, b, schedule) = carry_forward_schedule();
2891
2892 let newly: HashMap<_, _> = schedule.newly_active(Some(10), 20).into_iter().collect();
2893 assert_eq!(newly.len(), 2);
2894 assert_eq!(newly[&a].lamports(), 2);
2895 assert_eq!(newly[&b].lamports(), 9);
2896 let spanning: HashMap<_, _> = schedule.newly_active(None, 20).into_iter().collect();
2897 assert_eq!(spanning[&a].lamports(), 2, "later slot wins");
2898 assert_eq!(schedule.newly_active(None, 15).len(), 1);
2899 }
2900
2901 #[test]
2902 fn reroute_filter_round_trips_camel_case() {
2903 let filter = RerouteFilter {
2904 pairs: BTreeSet::from([MintPair::new(address(1), address(2))]),
2905 };
2906 let json = serde_json::to_value(&filter).unwrap();
2907 assert!(
2908 json["pairs"][0][0].is_string() && json["pairs"][0][1].is_string(),
2909 "a pair serializes as two base58 strings: {json}"
2910 );
2911 let back: RerouteFilter = serde_json::from_value(json).unwrap();
2912 assert_eq!(back, filter);
2913 }
2914
2915 #[test]
2918 fn mint_pairs_are_normalized_however_they_are_built() {
2919 let (one, other) = (address(1), address(2));
2920 assert_eq!(MintPair::new(one, other), MintPair::new(other, one));
2921
2922 let reversed: MintPair =
2923 serde_json::from_value(serde_json::json!([other.to_string(), one.to_string()]))
2924 .unwrap();
2925 assert_eq!(reversed, MintPair::new(one, other));
2926 }
2927
2928 #[test]
2929 fn fail_fast_divergence_kind_str_round_trips() {
2930 for kind in [
2931 FailFastDivergenceKind::AnyNonBenign,
2932 FailFastDivergenceKind::Tracked,
2933 ] {
2934 assert_eq!(
2935 FailFastDivergenceKind::from_str_opt(kind.as_str()),
2936 Some(kind)
2937 );
2938 }
2939 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
2940 assert_eq!(
2941 FailFastDivergenceKind::default(),
2942 FailFastDivergenceKind::AnyNonBenign
2943 );
2944 }
2945
2946 #[test]
2947 fn bundle_not_found_is_terminal_and_descriptive() {
2948 let error = BacktestError::BundleNotFound {
2949 start_slot: 100,
2950 end_slot: 600,
2951 };
2952
2953 assert!(error.is_terminal());
2954 assert_eq!(
2955 error.to_string(),
2956 "block bundle not found for start_slot=100, end_slot=600"
2957 );
2958 }
2959
2960 #[test]
2961 fn no_produced_blocks_is_terminal_and_descriptive() {
2962 let error = BacktestError::NoProducedBlocksInRange {
2963 start_slot: 100,
2964 end_slot: 100,
2965 };
2966
2967 assert!(error.is_terminal());
2968 assert_eq!(
2971 error.to_string(),
2972 "no produced blocks in requested range 100..=100; slot(s) may be skipped on-chain"
2973 );
2974 }
2975
2976 #[test]
2977 fn bundle_build_request_optional_fields_default_to_none() {
2978 let req: BundleBuildRequest =
2979 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
2980 assert_eq!((req.start_slot, req.end_slot), (1, 2));
2981 assert_eq!(req.bundle_size, None);
2982 assert_eq!(req.idempotency_key, None);
2983 }
2984
2985 #[test]
2986 fn bundle_build_request_parses_optional_fields() {
2987 let req: BundleBuildRequest = serde_json::from_str(
2988 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
2989 )
2990 .expect("parse");
2991 assert_eq!(req.bundle_size, Some(500));
2992 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
2993 }
2994
2995 #[test]
2996 fn bundle_build_status_serde_round_trips_with_snake_case() {
2997 let cases = [
2998 (BundleBuildStatus::Completed, "\"completed\""),
2999 (BundleBuildStatus::Failed, "\"failed\""),
3000 (BundleBuildStatus::InProgress, "\"in_progress\""),
3001 (
3002 BundleBuildStatus::NeedsInvestigation,
3003 "\"needs_investigation\"",
3004 ),
3005 ];
3006 for (status, expected) in cases {
3007 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
3008 assert_eq!(
3009 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
3010 status
3011 );
3012 assert_eq!(status.as_str(), expected.trim_matches('"'));
3013 }
3014 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
3015 }
3016
3017 #[test]
3018 fn bundle_build_request_without_kind_deserializes_as_blocks() {
3019 let request: BundleBuildRequest =
3021 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
3022 assert_eq!(request.kind, BundleKind::Blocks);
3023 }
3024
3025 #[test]
3026 fn bundle_kind_round_trips_over_serde_and_as_str() {
3027 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
3028 let json = serde_json::to_string(&kind).unwrap();
3029 assert_eq!(json, format!("\"{}\"", kind.as_str()));
3030 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
3031 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
3034 }
3035 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
3036 assert_eq!(BundleKind::from_str_opt("accountState"), None);
3037 }
3038
3039 #[test]
3040 fn bundle_build_request_carries_kind_through_serde() {
3041 let request = BundleBuildRequest {
3042 start_slot: 100,
3043 end_slot: 200,
3044 kind: BundleKind::AccountState,
3045 ..Default::default()
3046 };
3047 let json = serde_json::to_string(&request).unwrap();
3048 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3049 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
3050 assert_eq!(parsed.kind, BundleKind::AccountState);
3051 }
3052
3053 #[test]
3054 fn bundle_build_status_response_serializes_request_and_status() {
3055 let response = BundleBuildStatusResponse {
3056 request_id: "r".to_string(),
3057 start_slot: 100,
3058 end_slot: 200,
3059 bundle_size: Some(50),
3060 status: BundleBuildStatus::InProgress,
3061 created_at_unix_ms: 0,
3062 kind: BundleKind::Blocks,
3063 eta_secs: Some(600),
3064 };
3065 let json = serde_json::to_value(&response).unwrap();
3066 assert_eq!(json["request_id"].as_str(), Some("r"));
3067 assert_eq!(json["start_slot"].as_u64(), Some(100));
3068 assert_eq!(json["bundle_size"].as_u64(), Some(50));
3069 assert_eq!(json["status"].as_str(), Some("in_progress"));
3070 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
3071 assert_eq!(json["eta_secs"].as_u64(), Some(600));
3072 }
3073
3074 #[test]
3075 fn an_empty_bundles_list_is_distinct_from_an_absent_one() {
3076 let absent: BundleBuildListResponse = serde_json::from_str(r#"{"builds":[]}"#).unwrap();
3078 let empty: BundleBuildListResponse =
3079 serde_json::from_str(r#"{"builds":[],"bundles":[]}"#).unwrap();
3080 assert_eq!(absent.bundles, None);
3081 assert_eq!(empty.bundles, Some(Vec::new()));
3082 }
3083
3084 #[test]
3085 fn completed_bundle_round_trips() {
3086 let bundle = CompletedBundle {
3087 start_slot: 100,
3088 end_slot: 200,
3089 kind: BundleKind::AccountState,
3090 completed_at_unix_ms: Some(1_700_000_000_000),
3091 size_bytes: Some(4_096),
3092 };
3093 let json = serde_json::to_string(&bundle).unwrap();
3094 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3095 assert_eq!(
3096 serde_json::from_str::<CompletedBundle>(&json).unwrap(),
3097 bundle
3098 );
3099
3100 let sparse = r#"{"start_slot":1,"end_slot":2,"kind":"blocks"}"#;
3101 let parsed: CompletedBundle = serde_json::from_str(sparse).unwrap();
3102 assert_eq!(parsed.completed_at_unix_ms, None);
3103 assert_eq!(parsed.size_bytes, None);
3104 }
3105
3106 #[test]
3107 fn available_range_reads_a_server_that_predates_kind_and_scope() {
3108 let json = r#"{
3110 "bundle_start_slot": 100,
3111 "bundle_start_slot_utc": null,
3112 "max_bundle_end_slot": 200,
3113 "max_bundle_end_slot_utc": null,
3114 "max_bundle_size": 101
3115 }"#;
3116 let parsed: AvailableRange = serde_json::from_str(json).unwrap();
3117 assert_eq!(parsed.bundle_start_slot, 100);
3118 assert_eq!(parsed.kind, None);
3119 assert_eq!(parsed.scope, None);
3120 }
3121
3122 #[test]
3123 fn available_range_ignores_fields_it_does_not_know() {
3124 let json = r#"{"bundle_start_slot": 1, "some_future_field": "x"}"#;
3126 assert_eq!(
3127 serde_json::from_str::<AvailableRange>(json)
3128 .unwrap()
3129 .bundle_start_slot,
3130 1
3131 );
3132 }
3133
3134 #[test]
3135 fn available_range_labels_round_trip() {
3136 let range = AvailableRange {
3137 bundle_start_slot: 100,
3138 max_bundle_end_slot: Some(200),
3139 kind: Some(BundleKind::AccountState),
3140 scope: Some(RangeScope::User),
3141 ..Default::default()
3142 };
3143 let json = serde_json::to_string(&range).unwrap();
3144 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3145 assert!(json.contains(r#""scope":"user""#), "{json}");
3146 let parsed: AvailableRange = serde_json::from_str(&json).unwrap();
3147 assert_eq!(parsed.kind, Some(BundleKind::AccountState));
3148 assert_eq!(parsed.scope, Some(RangeScope::User));
3149 }
3150
3151 fn range(start: u64, end: u64) -> AvailableRange {
3152 AvailableRange {
3153 bundle_start_slot: start,
3154 max_bundle_end_slot: Some(end),
3155 ..Default::default()
3156 }
3157 }
3158
3159 #[rstest::rstest]
3163 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
3164 #[case::multi(
3165 vec![range(100, 200), range(201, 300), range(301, 400)],
3166 100, 300, Some(vec![(100, 200), (201, 300)])
3167 )]
3168 #[case::nested(
3171 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
3172 100, 900, Some(vec![(100, 500), (501, 900)])
3173 )]
3174 #[case::prefers_finer_grid(
3178 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
3179 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
3180 )]
3181 #[case::shared_start_prefers_finer(
3185 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
3186 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
3187 )]
3188 #[case::coarse_overlap_prefers_finer_pair(
3192 vec![range(100, 200), range(100, 150), range(151, 160)],
3193 100, 160, Some(vec![(100, 150), (151, 160)])
3194 )]
3195 #[case::falls_back_to_coarse(
3199 vec![range(100, 160), range(100, 120), range(121, 140)],
3200 100, 160, Some(vec![(100, 160)])
3201 )]
3202 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
3204 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
3207 #[case::anchors_then_continues(
3208 vec![range(150, 350), range(351, 600)],
3209 200, 600, Some(vec![(150, 350), (351, 600)])
3210 )]
3211 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
3212 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
3214 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
3216 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
3217 #[case::user_and_global_ranges_not_collapsed(
3222 vec![range(100, 200), range(100, 150), range(151, 200)],
3223 100, 200, Some(vec![(100, 150), (151, 200)])
3224 )]
3225 fn split_range_cases(
3226 #[case] ranges: Vec<AvailableRange>,
3227 #[case] start: u64,
3228 #[case] end: u64,
3229 #[case] expected: Option<Vec<(u64, u64)>>,
3230 ) {
3231 match expected {
3232 Some(expected) => assert_eq!(
3233 split_range(
3234 &ranges,
3235 SlotRange::new_unchecked(start, end),
3236 BundleKind::Blocks
3237 )
3238 .unwrap(),
3239 expected
3240 ),
3241 None => assert!(
3242 split_range(
3243 &ranges,
3244 SlotRange::new_unchecked(start, end),
3245 BundleKind::Blocks
3246 )
3247 .is_err()
3248 ),
3249 }
3250 }
3251
3252 fn account_state_range(start: u64, end: u64) -> AvailableRange {
3253 AvailableRange {
3254 kind: Some(BundleKind::AccountState),
3255 ..range(start, end)
3256 }
3257 }
3258
3259 #[test]
3260 fn a_replay_split_rides_the_recordings_not_a_finer_block_grid() {
3261 let ranges = vec![
3264 range(100, 149),
3265 range(150, 199),
3266 account_state_range(100, 199),
3267 ];
3268 let requested = SlotRange::new_unchecked(100, 199);
3269
3270 assert_eq!(
3271 split_range(&ranges, requested, BundleKind::Blocks).unwrap(),
3272 [(100, 149), (150, 199)]
3273 );
3274 assert_eq!(
3275 split_range(&ranges, requested, BundleKind::AccountState).unwrap(),
3276 [(100, 199)]
3277 );
3278 }
3279
3280 #[test]
3281 fn a_replay_split_tiles_consecutive_recordings() {
3282 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
3283 assert_eq!(
3284 split_range(
3285 &ranges,
3286 SlotRange::new_unchecked(100, 299),
3287 BundleKind::AccountState
3288 )
3289 .unwrap(),
3290 [(100, 199), (200, 299)]
3291 );
3292 }
3293
3294 #[test]
3295 fn a_replay_split_ends_short_of_a_recording() {
3296 let requested = SlotRange::new_unchecked(100, 150);
3299 assert_eq!(
3300 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3301 [(100, 150)]
3302 );
3303 assert_eq!(
3304 split_range(
3305 &[account_state_range(100, 199)],
3306 requested,
3307 BundleKind::AccountState
3308 )
3309 .unwrap(),
3310 [(100, 150)]
3311 );
3312 }
3313
3314 #[test]
3315 fn a_replay_split_ends_short_of_its_last_recording() {
3316 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
3319 assert_eq!(
3320 split_range(
3321 &ranges,
3322 SlotRange::new_unchecked(100, 250),
3323 BundleKind::AccountState
3324 )
3325 .unwrap(),
3326 [(100, 199), (200, 250)]
3327 );
3328 }
3329
3330 #[test]
3331 fn a_replay_split_tiles_a_one_slot_recording() {
3332 let ranges = vec![account_state_range(100, 249), account_state_range(250, 250)];
3335 assert_eq!(
3336 split_range(
3337 &ranges,
3338 SlotRange::new_unchecked(100, 250),
3339 BundleKind::AccountState
3340 )
3341 .unwrap(),
3342 [(100, 249), (250, 250)]
3343 );
3344 }
3345
3346 #[test]
3347 fn a_replay_split_anchors_a_mid_recording_start() {
3348 let requested = SlotRange::new_unchecked(150, 199);
3352 assert_eq!(
3353 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3354 [(100, 199)]
3355 );
3356 assert_eq!(
3357 split_range(
3358 &[account_state_range(100, 199)],
3359 requested,
3360 BundleKind::AccountState
3361 )
3362 .unwrap(),
3363 [(100, 199)]
3364 );
3365 }
3366
3367 #[test]
3368 fn a_replay_split_refuses_a_start_no_recording_covers() {
3369 let error = split_range(
3370 &[account_state_range(200, 299)],
3371 SlotRange::new_unchecked(100, 299),
3372 BundleKind::AccountState,
3373 )
3374 .expect_err("no recording covers 100");
3375 assert!(
3376 error.contains("is not covered by any available account-state bundle"),
3377 "{error}"
3378 );
3379 }
3380
3381 #[test]
3382 fn a_replay_split_ignores_block_ranges() {
3383 let error = split_range(
3384 &[range(100, 199)],
3385 SlotRange::new_unchecked(100, 199),
3386 BundleKind::AccountState,
3387 )
3388 .expect_err("owns no recordings");
3389 assert!(error.contains("account-state bundle"), "{error}");
3390 }
3391
3392 #[test]
3393 fn blocks_mode_admits_ranges_from_a_server_that_predates_kind() {
3394 let unlabelled = range(100, 199);
3397 assert_eq!(unlabelled.kind, None);
3398 assert_eq!(
3399 split_range(
3400 &[unlabelled],
3401 SlotRange::new_unchecked(100, 150),
3402 BundleKind::Blocks
3403 )
3404 .unwrap(),
3405 [(100, 150)]
3406 );
3407 }
3408
3409 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
3412 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
3413 for r in ranges {
3414 if let Some(end) = r.max_bundle_end_slot
3415 && end >= r.bundle_start_slot
3416 {
3417 ends.entry(r.bundle_start_slot).or_default().insert(end);
3418 }
3419 }
3420 ends
3421 }
3422
3423 fn reference_max_split(
3427 ends: &BTreeMap<u64, BTreeSet<u64>>,
3428 cursor: u64,
3429 end: u64,
3430 ) -> Option<Vec<(u64, u64)>> {
3431 ends.get(&cursor)?
3432 .iter()
3433 .filter_map(|&bundle_end| {
3434 if bundle_end >= end {
3435 Some(vec![(cursor, end)])
3436 } else {
3437 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
3438 rest.insert(0, (cursor, bundle_end));
3439 rest
3440 })
3441 }
3442 })
3443 .max_by_key(Vec::len)
3444 }
3445
3446 fn is_valid_split(
3450 split: &[(u64, u64)],
3451 ends: &BTreeMap<u64, BTreeSet<u64>>,
3452 start: u64,
3453 end: u64,
3454 ) -> bool {
3455 split.first().is_some_and(|&(s, _)| s == start)
3456 && split.last().is_some_and(|&(_, e)| e == end)
3457 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
3458 && split.iter().all(|&(s, e)| {
3459 e >= s
3460 && ends
3461 .get(&s)
3462 .and_then(|bundle_ends| bundle_ends.iter().next_back())
3463 .is_some_and(|&max_end| e <= max_end)
3464 })
3465 }
3466
3467 #[test]
3472 fn split_range_matches_reference() {
3473 let mut seed: u64 = 0x9E3779B97F4A7C15;
3474 let mut next = || {
3475 seed = seed
3476 .wrapping_mul(6364136223846793005)
3477 .wrapping_add(1442695040888963407);
3478 seed >> 33
3479 };
3480
3481 for _ in 0..50_000 {
3482 let ranges: Vec<AvailableRange> = (0..next() % 6)
3485 .map(|_| {
3486 let start = next() % 12;
3487 range(start, start + next() % 6) })
3489 .collect();
3490 let start = next() % 12;
3491 let end = start + next() % 6; let got = split_range(
3494 &ranges,
3495 SlotRange::new_unchecked(start, end),
3496 BundleKind::Blocks,
3497 );
3498 let ends = ends_by_start(&ranges);
3499 let anchor = ends
3502 .range(..=start)
3503 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
3504 .map(|(&s, _)| s);
3505 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
3506
3507 let layout: Vec<_> = ranges
3508 .iter()
3509 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
3510 .collect();
3511 match (&got, &reference) {
3512 (Ok(split), Some(best)) => {
3513 assert!(
3514 is_valid_split(split, &ends, anchor.unwrap(), end),
3515 "invalid split {split:?} for {layout:?} [{start},{end}]"
3516 );
3517 assert_eq!(
3518 split.len(),
3519 best.len(),
3520 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
3521 );
3522 }
3523 (Err(_), None) => {}
3524 _ => panic!(
3525 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
3526 ),
3527 }
3528 }
3529 }
3530
3531 #[test]
3532 fn a_reroute_without_templates_omits_direct_fill() {
3533 let json = serde_json::to_value(RerouteParams::default()).expect("serialize");
3534 assert!(json.get("directFill").is_none(), "{json}");
3535 }
3536
3537 #[test]
3538 fn extra_markets_cross_the_wire_as_base58() {
3539 let json = serde_json::to_value(RerouteParams::default()).expect("serialize");
3540 assert!(json.get("extraMarkets").is_none(), "{json}");
3541
3542 let reroute = RerouteParams {
3543 extra_markets: BTreeSet::from([address(7)]),
3544 ..Default::default()
3545 };
3546 let json = serde_json::to_value(&reroute).expect("serialize");
3547 assert_eq!(
3548 json["extraMarkets"],
3549 serde_json::json!([address(7).to_string()])
3550 );
3551 assert_eq!(
3552 serde_json::from_value::<RerouteParams>(json).expect("deserialize"),
3553 reroute
3554 );
3555
3556 serde_json::from_value::<RerouteParams>(serde_json::json!({
3557 "extraMarkets": ["not-an-address"]
3558 }))
3559 .expect_err("an invalid address is rejected");
3560 }
3561
3562 fn template(label: &str) -> DirectFillTemplate {
3564 let placeholder = Address::default();
3565 DirectFillTemplate {
3566 label: label.to_owned(),
3567 mode: SwapMode::ExactIn,
3568 input_mint: mints::SOL,
3569 output_mint: mints::USDC,
3570 instruction: TemplateInstruction {
3571 program_id: address(7),
3572 accounts: vec![
3573 TemplateAccount {
3574 address: placeholder,
3575 writable: true,
3576 signer: true,
3577 },
3578 TemplateAccount {
3579 address: placeholder,
3580 writable: true,
3581 signer: false,
3582 },
3583 TemplateAccount {
3584 address: placeholder,
3585 writable: true,
3586 signer: false,
3587 },
3588 TemplateAccount {
3589 address: address(8),
3590 writable: false,
3591 signer: false,
3592 },
3593 ],
3594 data: vec![0; 16],
3595 },
3596 amount_offset: 2,
3597 taker: TakerSlots {
3598 user: vec![0],
3599 input_token_account: vec![1],
3600 output_token_account: vec![2],
3601 },
3602 }
3603 }
3604
3605 #[rstest]
3607 #[case::requote_alone(true, 0, false)]
3608 #[case::templates_alone(false, 1, false)]
3609 #[case::both(true, 2, false)]
3610 #[case::neither(false, 0, true)]
3611 fn a_reroute_is_a_noop_only_with_nothing_to_ask(
3612 #[case] requote: bool,
3613 #[case] templates: usize,
3614 #[case] noop: bool,
3615 ) {
3616 let reroute = RerouteParams {
3617 requote,
3618 direct_fill: (0..templates).map(|i| template(&format!("t{i}"))).collect(),
3619 ..Default::default()
3620 };
3621 assert_eq!(reroute.is_noop(), noop);
3622 }
3623
3624 #[rstest]
3626 #[case::templates_without_requote(false, 1, true)]
3627 #[case::requote_without_templates(true, 0, true)]
3628 #[case::nothing(false, 0, false)]
3629 fn the_account_universe_warms_for_templates_as_for_requoting(
3630 #[case] requote: bool,
3631 #[case] templates: usize,
3632 #[case] warms: bool,
3633 ) {
3634 let params = CreateSessionParams::builder()
3635 .start_slot(1)
3636 .end_slot(2)
3637 .reroute(RerouteParams {
3638 requote,
3639 direct_fill: (0..templates).map(|i| template(&format!("t{i}"))).collect(),
3640 ..Default::default()
3641 })
3642 .build();
3643 assert_eq!(params.warms_account_universe(), warms);
3644 }
3645}