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 OracleUpdate {
164 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
165 programs: BTreeSet<Address>,
166 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
169 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
170 accounts: BTreeSet<Address>,
171 },
172}
173
174pub struct TxMatchContext {
179 pub invoked_programs: HashSet<Address>,
181 pub moves_tokens: bool,
183 pub written_accounts: Vec<Address>,
186 pub writes_via_unresolved_lookup: bool,
189}
190
191impl DiscoveryFilter {
192 pub fn matches(&self, ctx: &TxMatchContext) -> bool {
195 match self {
196 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
197 Self::OracleUpdate { programs, accounts } => {
198 !ctx.moves_tokens
199 && ctx
200 .invoked_programs
201 .iter()
202 .any(|program| programs.contains(program))
203 && (accounts.is_empty()
204 || ctx.writes_via_unresolved_lookup
205 || ctx
206 .written_accounts
207 .iter()
208 .any(|account| accounts.contains(account)))
209 }
210 }
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
218#[serde(try_from = "[String; 2]", into = "[String; 2]")]
219pub struct MintPair([Address; 2]);
220
221impl MintPair {
222 pub fn new(one: Address, other: Address) -> Self {
223 Self(if one <= other {
224 [one, other]
225 } else {
226 [other, one]
227 })
228 }
229
230 pub fn mints(&self) -> (Address, Address) {
231 (self.0[0], self.0[1])
232 }
233}
234
235#[derive(Debug, thiserror::Error)]
236#[error("invalid mint {mint} in pair")]
237pub struct ParseMintPairError {
238 mint: String,
239 source: ParseAddressError,
240}
241
242impl TryFrom<[String; 2]> for MintPair {
243 type Error = ParseMintPairError;
244
245 fn try_from([one, other]: [String; 2]) -> Result<Self, Self::Error> {
246 fn parse(mint: String) -> Result<Address, ParseMintPairError> {
247 Address::from_str(&mint).map_err(|source| ParseMintPairError { mint, source })
248 }
249 Ok(Self::new(parse(one)?, parse(other)?))
250 }
251}
252
253impl From<MintPair> for [String; 2] {
254 fn from(pair: MintPair) -> Self {
255 let (one, other) = pair.mints();
256 [one.to_string(), other.to_string()]
257 }
258}
259
260#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264pub struct RerouteFilter {
265 #[serde(default)]
267 pub pairs: BTreeSet<MintPair>,
268}
269
270impl RerouteFilter {
271 pub fn is_empty(&self) -> bool {
274 self.pairs.is_empty()
275 }
276
277 pub fn matches_mints(&self, input: &Address, output: &Address) -> bool {
278 self.pairs.contains(&MintPair::new(*input, *output))
279 }
280
281 pub fn mentions_mint(&self, mint: &Address) -> bool {
284 self.pairs.iter().any(|pair| {
285 let (one, other) = pair.mints();
286 one == *mint || other == *mint
287 })
288 }
289}
290
291#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(rename_all = "camelCase")]
295pub enum ActionKind {
296 #[default]
300 Simulate,
301 Send,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
310#[serde(tag = "at", rename_all = "camelCase")]
311pub enum ActionAnchor {
312 BeforeSlot {
315 #[serde(default, with = "compressed")]
316 slots: Vec<u64>,
317 },
318 AfterSlot {
321 #[serde(default, with = "compressed")]
322 slots: Vec<u64>,
323 },
324 #[serde(rename_all = "camelCase")]
329 BeforeEverySlot {
330 #[serde(default = "every_slot")]
331 every_n_slots: NonZeroU64,
332 },
333 #[serde(rename_all = "camelCase")]
337 AfterEverySlot {
338 #[serde(default = "every_slot")]
339 every_n_slots: NonZeroU64,
340 },
341 BeforeMatch { filter: DiscoveryFilter },
344 AfterMatch { filter: DiscoveryFilter },
347}
348
349const fn every_slot() -> NonZeroU64 {
350 NonZeroU64::MIN
351}
352
353const MAX_ACTION_TRANSACTIONS_BYTES: u64 = 256 * 1024 * 1024;
357
358mod compressed {
363 use std::io::Read;
364
365 use base64::Engine as _;
366 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
367
368 use super::{BASE64, MAX_ACTION_TRANSACTIONS_BYTES};
369
370 const COMPRESSION_LEVEL: i32 = 0;
372
373 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
374 where
375 T: Serialize,
376 S: Serializer,
377 {
378 let json = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
379 let compressed = zstd::stream::encode_all(json.as_slice(), COMPRESSION_LEVEL)
380 .map_err(serde::ser::Error::custom)?;
381 serializer.serialize_str(&BASE64.encode(compressed))
382 }
383
384 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
385 where
386 T: DeserializeOwned,
387 D: Deserializer<'de>,
388 {
389 let blob = String::deserialize(deserializer)?;
390 let compressed = BASE64
391 .decode(blob.as_bytes())
392 .map_err(serde::de::Error::custom)?;
393 let json = decode_capped(&compressed, MAX_ACTION_TRANSACTIONS_BYTES)
394 .map_err(serde::de::Error::custom)?;
395 serde_json::from_slice(&json).map_err(serde::de::Error::custom)
396 }
397
398 fn decode_capped(compressed: &[u8], cap: u64) -> std::io::Result<Vec<u8>> {
401 let mut json = Vec::new();
402 zstd::stream::read::Decoder::new(compressed)
403 .and_then(|d| Read::take(d, cap + 1).read_to_end(&mut json))?;
404 match json.len() as u64 > cap {
405 true => Err(std::io::Error::other(format!(
406 "decompressed blob is over the {cap} byte ceiling"
407 ))),
408 false => Ok(json),
409 }
410 }
411
412 #[cfg(test)]
413 mod tests {
414 use super::*;
415
416 #[test]
417 fn a_blob_over_the_ceiling_names_it_instead_of_truncating() {
418 let compressed = zstd::stream::encode_all([b'a'; 1024].as_slice(), 0).unwrap();
419 let error = decode_capped(&compressed, 64).unwrap_err();
420 assert!(error.to_string().contains("64 byte ceiling"), "{error}");
421 }
422
423 #[test]
424 fn a_blob_at_the_ceiling_is_kept_whole() {
425 let compressed = zstd::stream::encode_all([b'a'; 64].as_slice(), 0).unwrap();
426 assert_eq!(decode_capped(&compressed, 64).unwrap().len(), 64);
427 }
428 }
429}
430
431#[serde_with::serde_as]
434#[derive(Debug, Clone, Serialize, Deserialize)]
435#[serde(rename_all = "camelCase")]
436pub struct ScheduledAction {
437 pub anchor: ActionAnchor,
438 #[serde(default)]
440 pub kind: ActionKind,
441 #[serde(default, with = "compressed")]
451 pub transactions: Vec<String>,
452 #[serde(default)]
457 pub account_overrides: AccountModifications,
458 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
463 pub feeds_reroute: bool,
464 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
468 #[serde(default)]
469 pub return_accounts: Vec<Address>,
470 #[serde(default)]
472 pub label: Option<String>,
473}
474
475impl ScheduledAction {
476 pub fn is_computed_override(&self) -> bool {
479 self.feeds_reroute && !self.transactions.is_empty()
480 }
481}
482
483pub const MAX_DIAGNOSTIC_MARKETS: usize = 64;
484pub const MAX_DIAGNOSTIC_USD_VALUES: usize = 16;
485
486#[serde_with::serde_as]
487#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
488#[serde(rename_all = "camelCase")]
489pub struct DiagnosticProbeParams {
490 pub anchor: ActionAnchor,
491 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
492 pub markets: Vec<Address>,
493 pub usd_values: Vec<u32>,
494 #[serde(default)]
495 pub label: Option<String>,
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
499pub enum DiagnosticProbeError {
500 #[error("a diagnostic probe names no markets")]
501 NoMarkets,
502 #[error("a diagnostic probe names no usdValues")]
503 NoUsdValues,
504 #[error("a diagnostic probe names {got} markets, over the ceiling of {MAX_DIAGNOSTIC_MARKETS}")]
505 TooManyMarkets { got: usize },
506 #[error(
507 "a diagnostic probe names {got} usdValues, over the ceiling of {MAX_DIAGNOSTIC_USD_VALUES}"
508 )]
509 TooManyUsdValues { got: usize },
510 #[error("a diagnostic probe names usdValue 0, which the router reads as no size at all")]
511 ZeroUsdValue,
512 #[error("a diagnostic probe names market {market} more than once")]
513 DuplicateMarket { market: Address },
514 #[error(
515 "a diagnostic probe anchors at {anchor}; probing the router is an await, and only the \
516 slot-end hook has a point for one, so only afterSlot and afterEverySlot are supported"
517 )]
518 UnsupportedAnchor { anchor: &'static str },
519}
520
521impl DiagnosticProbeParams {
522 pub fn fires(&self) -> usize {
524 self.markets.len() * self.usd_values.len()
525 }
526
527 pub fn validate(&self) -> Result<(), DiagnosticProbeError> {
528 if self.markets.is_empty() {
529 return Err(DiagnosticProbeError::NoMarkets);
530 }
531 if self.usd_values.is_empty() {
532 return Err(DiagnosticProbeError::NoUsdValues);
533 }
534 if self.markets.len() > MAX_DIAGNOSTIC_MARKETS {
535 return Err(DiagnosticProbeError::TooManyMarkets {
536 got: self.markets.len(),
537 });
538 }
539 if self.usd_values.len() > MAX_DIAGNOSTIC_USD_VALUES {
540 return Err(DiagnosticProbeError::TooManyUsdValues {
541 got: self.usd_values.len(),
542 });
543 }
544 if self.usd_values.contains(&0) {
545 return Err(DiagnosticProbeError::ZeroUsdValue);
546 }
547 let mut seen = HashSet::with_capacity(self.markets.len());
548 if let Some(market) = self.markets.iter().find(|market| !seen.insert(**market)) {
549 return Err(DiagnosticProbeError::DuplicateMarket { market: *market });
550 }
551 match &self.anchor {
552 ActionAnchor::AfterSlot { .. } | ActionAnchor::AfterEverySlot { .. } => Ok(()),
553 ActionAnchor::BeforeSlot { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
554 anchor: "beforeSlot",
555 }),
556 ActionAnchor::BeforeEverySlot { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
557 anchor: "beforeEverySlot",
558 }),
559 ActionAnchor::BeforeMatch { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
560 anchor: "beforeMatch",
561 }),
562 ActionAnchor::AfterMatch { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
563 anchor: "afterMatch",
564 }),
565 }
566 }
567}
568
569#[derive(Debug, Clone)]
571enum OverrideContribution {
572 Static(HashMap<Address, AccountSharedData>),
573 Pending,
574 Resolved(HashMap<Address, AccountSharedData>),
575 Failed,
576}
577
578#[derive(Debug, Clone, Default)]
579struct OverrideScheduleInner {
580 entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>,
581 keys: HashSet<Address>,
585}
586
587impl OverrideScheduleInner {
588 fn new(entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>) -> Self {
589 let keys = entries
590 .values()
591 .flatten()
592 .filter_map(|(_, contribution)| match contribution {
593 OverrideContribution::Static(accounts)
594 | OverrideContribution::Resolved(accounts) => Some(accounts.keys().copied()),
595 OverrideContribution::Pending | OverrideContribution::Failed => None,
596 })
597 .flatten()
598 .collect();
599 Self { entries, keys }
600 }
601}
602
603#[derive(Debug, Clone, Default)]
606pub struct OverrideSchedule {
607 inner: Arc<RwLock<OverrideScheduleInner>>,
608}
609
610impl From<BTreeMap<u64, HashMap<Address, AccountSharedData>>> for OverrideSchedule {
611 fn from(entries: BTreeMap<u64, HashMap<Address, AccountSharedData>>) -> Self {
612 Self {
613 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(
614 entries
615 .into_iter()
616 .map(|(slot, accounts)| {
617 (slot, vec![(0, OverrideContribution::Static(accounts))])
618 })
619 .collect(),
620 ))),
621 }
622 }
623}
624
625impl OverrideSchedule {
626 pub fn is_empty(&self) -> bool {
627 self.inner.read().entries.is_empty()
628 }
629
630 pub fn slots(&self) -> impl Iterator<Item = u64> + '_ {
632 self.inner
633 .read()
634 .entries
635 .keys()
636 .copied()
637 .collect::<Vec<_>>()
638 .into_iter()
639 }
640
641 pub fn active_at(&self, slot: u64) -> HashMap<Address, AccountSharedData> {
643 self.inner.read().entries.range(..=slot).fold(
644 HashMap::new(),
645 |mut active, (_, contributions)| {
646 active.extend(fold_contributions(contributions));
647 active
648 },
649 )
650 }
651
652 pub fn active_for(&self, key: &Address, slot: u64) -> Option<AccountSharedData> {
653 let inner = self.inner.read();
654 if !inner.keys.contains(key) {
655 return None;
656 }
657 inner
658 .entries
659 .range(..=slot)
660 .rev()
661 .find_map(|(_, contributions)| {
662 contributions
663 .iter()
664 .rev()
665 .find_map(|(_, contribution)| match contribution {
666 OverrideContribution::Static(accounts)
667 | OverrideContribution::Resolved(accounts) => accounts.get(key).cloned(),
668 OverrideContribution::Pending | OverrideContribution::Failed => None,
669 })
670 })
671 }
672
673 pub fn newly_active(
675 &self,
676 after: Option<u64>,
677 up_to: u64,
678 ) -> Vec<(Address, AccountSharedData)> {
679 let lower = after.map_or(std::ops::Bound::Unbounded, std::ops::Bound::Excluded);
680 self.inner
681 .read()
682 .entries
683 .range((lower, std::ops::Bound::Included(up_to)))
684 .flat_map(|(_, contributions)| fold_contributions(contributions))
685 .collect::<HashMap<_, _>>()
686 .into_iter()
687 .collect()
688 }
689
690 pub fn resolve(
693 &self,
694 slot: u64,
695 action_index: u32,
696 accounts: Option<HashMap<Address, AccountSharedData>>,
697 ) -> Result<(), String> {
698 let mut inner = self.inner.write();
699 let OverrideScheduleInner { entries, keys } = &mut *inner;
700 let contribution = entries
701 .get_mut(&slot)
702 .and_then(|contributions| {
703 contributions
704 .iter_mut()
705 .find(|(index, _)| *index == action_index)
706 .map(|(_, contribution)| contribution)
707 })
708 .ok_or_else(|| {
709 format!("override action {action_index} has no contribution at slot {slot}")
710 })?;
711 if !matches!(contribution, OverrideContribution::Pending) {
712 return Err(format!(
713 "override action {action_index} at slot {slot} is not pending"
714 ));
715 }
716 *contribution = match accounts {
717 Some(accounts) => {
718 keys.extend(accounts.keys().copied());
719 OverrideContribution::Resolved(accounts)
720 }
721 None => OverrideContribution::Failed,
722 };
723 Ok(())
724 }
725}
726
727fn fold_contributions(
728 contributions: &[(u32, OverrideContribution)],
729) -> HashMap<Address, AccountSharedData> {
730 let mut accounts = HashMap::new();
731 for (_, contribution) in contributions {
732 match contribution {
733 OverrideContribution::Static(next) | OverrideContribution::Resolved(next) => {
734 accounts.extend(next.iter().map(|(key, account)| (*key, account.clone())));
735 }
736 OverrideContribution::Pending | OverrideContribution::Failed => {}
737 }
738 }
739 accounts
740}
741
742fn decode_override_actions(actions: &[ScheduledAction]) -> Result<OverrideSchedule, String> {
751 let mut entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>> = BTreeMap::new();
752 for (action_index, action) in actions.iter().enumerate().filter(|(_, action)| {
753 action.feeds_reroute
754 || (action.transactions.is_empty() && !action.account_overrides.0.is_empty())
755 }) {
756 if action.kind == ActionKind::Send {
758 return Err("account overrides are not valid for kind: send".to_string());
759 }
760 if action.transactions.is_empty() && action.account_overrides.0.is_empty() {
761 return Err(
762 "feedsReroute needs something to feed: transactions to simulate, or \
763 accountOverrides"
764 .to_string(),
765 );
766 }
767 let ActionAnchor::BeforeSlot { slots } = &action.anchor else {
768 return Err("scheduled account overrides require a beforeSlot anchor".to_string());
769 };
770 let accounts = decode_account_modifications(&action.account_overrides)?;
771 let contribution = if action.transactions.is_empty() {
772 OverrideContribution::Static(accounts)
773 } else {
774 OverrideContribution::Pending
775 };
776 for slot in slots.iter().copied().collect::<BTreeSet<_>>() {
779 entries
780 .entry(slot)
781 .or_default()
782 .push((action_index as u32, contribution.clone()));
783 }
784 }
785 Ok(OverrideSchedule {
786 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(entries))),
787 })
788}
789
790#[allow(
794 deprecated,
795 reason = "`PROGRAM_OWNERS` remains the canonical loader-owner list"
796)]
797pub fn decode_account_modifications(
798 modifications: &AccountModifications,
799) -> Result<HashMap<Address, AccountSharedData>, String> {
800 modifications
801 .0
802 .iter()
803 .map(|(address, data)| {
804 let account = data
805 .to_account()
806 .map_err(|error| format!("invalid account data for {address}: {error}"))?;
807 if account.executable || solana_account::PROGRAM_OWNERS.contains(&account.owner) {
808 return Err(format!(
809 "overriding executable/program accounts is not supported (account {address})"
810 ));
811 }
812 Ok((*address, AccountSharedData::from(account)))
813 })
814 .collect()
815}
816
817pub const MAX_DISCONNECT_TIMEOUT_SECS: u16 = 900;
821
822#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
825#[serde(rename_all = "lowercase", try_from = "String")]
826pub enum SwapAggregator {
827 Jupiter,
828 Okx,
829 Titan,
830 Dflow,
831}
832
833impl SwapAggregator {
834 pub const fn as_str(self) -> &'static str {
836 match self {
837 Self::Jupiter => "jupiter",
838 Self::Okx => "okx",
839 Self::Titan => "titan",
840 Self::Dflow => "dflow",
841 }
842 }
843}
844
845impl std::fmt::Display for SwapAggregator {
846 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
847 f.write_str(self.as_str())
848 }
849}
850
851#[derive(Debug, thiserror::Error)]
852pub enum ParseAggregatorError {
853 #[error("unknown swap aggregator {0:?}; expected one of jupiter, okx, titan, dflow")]
854 Unknown(String),
855 #[error("name at least one aggregator (jupiter, okx, titan, dflow)")]
857 NoAggregator,
858}
859
860impl TryFrom<String> for SwapAggregator {
861 type Error = ParseAggregatorError;
862
863 fn try_from(value: String) -> Result<Self, Self::Error> {
864 value.parse()
865 }
866}
867
868impl std::str::FromStr for SwapAggregator {
869 type Err = ParseAggregatorError;
870
871 fn from_str(s: &str) -> Result<Self, Self::Err> {
872 match s.trim().to_ascii_lowercase().as_str() {
873 "jupiter" => Ok(Self::Jupiter),
874 "okx" => Ok(Self::Okx),
875 "titan" => Ok(Self::Titan),
876 "dflow" => Ok(Self::Dflow),
877 _ => Err(ParseAggregatorError::Unknown(s.to_string())),
878 }
879 }
880}
881
882#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
886#[serde(transparent)]
887pub struct RerouteAggregators(BTreeSet<SwapAggregator>);
888
889impl Default for RerouteAggregators {
890 fn default() -> Self {
891 Self([SwapAggregator::Jupiter].into_iter().collect())
892 }
893}
894
895impl RerouteAggregators {
896 pub fn new(aggregators: impl IntoIterator<Item = SwapAggregator>) -> Self {
897 Self(aggregators.into_iter().collect())
898 }
899
900 pub fn contains(&self, aggregator: SwapAggregator) -> bool {
901 self.0.contains(&aggregator)
902 }
903
904 pub fn is_empty(&self) -> bool {
905 self.0.is_empty()
906 }
907}
908
909impl std::fmt::Display for RerouteAggregators {
910 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
911 let mut aggregators = self.0.iter();
912 if let Some(first) = aggregators.next() {
913 write!(f, "{first}")?;
914 }
915 aggregators.try_for_each(|aggregator| write!(f, ",{aggregator}"))
916 }
917}
918
919impl std::str::FromStr for RerouteAggregators {
920 type Err = ParseAggregatorError;
921
922 fn from_str(s: &str) -> Result<Self, Self::Err> {
923 let aggregators: BTreeSet<SwapAggregator> = s
924 .split(',')
925 .filter(|aggregator| !aggregator.trim().is_empty())
926 .map(str::parse)
927 .collect::<Result<_, _>>()?;
928 if aggregators.is_empty() {
929 return Err(ParseAggregatorError::NoAggregator);
930 }
931 Ok(Self(aggregators))
932 }
933}
934
935#[serde_with::serde_as]
943#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
944#[serde(rename_all = "camelCase")]
945pub struct RerouteParams {
946 #[serde(default)]
947 pub aggregators: RerouteAggregators,
948 #[serde(default, skip_serializing_if = "Option::is_none")]
951 pub filter: Option<RerouteFilter>,
952 #[serde(default)]
959 pub detect_failed_l1_swaps: bool,
960 #[serde(default)]
963 pub circular_arbs: bool,
964 #[serde(default = "RerouteParams::default_requote")]
970 pub requote: bool,
971 #[serde(default, skip_serializing_if = "Vec::is_empty")]
980 pub direct_fill: Vec<DirectFillTemplate>,
981 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
985 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
986 pub extra_markets: BTreeSet<Address>,
987}
988
989impl Default for RerouteParams {
992 fn default() -> Self {
993 Self {
994 aggregators: RerouteAggregators::default(),
995 filter: None,
996 detect_failed_l1_swaps: false,
997 circular_arbs: false,
998 requote: Self::default_requote(),
999 direct_fill: Vec::new(),
1000 extra_markets: BTreeSet::new(),
1001 }
1002 }
1003}
1004
1005impl std::str::FromStr for MintPair {
1007 type Err = String;
1008
1009 fn from_str(s: &str) -> Result<Self, Self::Err> {
1010 let (base, quote) = s
1011 .split_once(',')
1012 .ok_or_else(|| format!("expected two comma-separated mints, got {s:?}"))?;
1013 Self::try_from([base.trim().to_owned(), quote.trim().to_owned()])
1014 .map_err(|error| error.to_string())
1015 }
1016}
1017
1018impl std::fmt::Display for MintPair {
1019 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1020 let (one, other) = self.mints();
1021 write!(f, "{one},{other}")
1022 }
1023}
1024
1025impl RerouteParams {
1026 const fn default_requote() -> bool {
1027 true
1028 }
1029
1030 pub const fn is_noop(&self) -> bool {
1033 !self.requote && self.direct_fill.is_empty()
1034 }
1035}
1036
1037#[serde_with::serde_as]
1039#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
1040#[serde(rename_all = "camelCase")]
1041pub struct CreateSessionParams {
1042 pub start_slot: u64,
1044 pub end_slot: u64,
1046 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
1047 #[serde(default)]
1048 #[builder(default)]
1049 pub signer_filter: BTreeSet<Address>,
1051 #[serde(default)]
1054 #[builder(default)]
1055 pub send_summary: bool,
1056 #[serde(default, skip_serializing_if = "Option::is_none")]
1058 pub reroute: Option<RerouteParams>,
1059 #[serde(default)]
1062 pub capacity_wait_timeout_secs: Option<u16>,
1063 #[serde(default)]
1067 pub disconnect_timeout_secs: Option<u16>,
1068 #[serde(default)]
1073 pub extra_compute_units: Option<u32>,
1074 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1081 #[builder(default)]
1082 pub discoveries: Vec<DiscoveryFilter>,
1083 #[serde(default, with = "compressed", skip_serializing_if = "Vec::is_empty")]
1092 #[builder(default)]
1093 pub actions: Vec<ScheduledAction>,
1094 #[serde(default)]
1097 #[builder(default)]
1098 pub replay_account_state: bool,
1099 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1100 #[builder(default)]
1101 pub diagnostic_probes: Vec<DiagnosticProbeParams>,
1102}
1103
1104pub mod replay_support {
1108 use super::{ActionKind, CreateSessionParams};
1109
1110 pub const NO_TRANSACTIONS: &str =
1112 "Create the session without replayAccountState to execute and persist transactions.";
1113
1114 pub const SEND_TRANSACTION: &str = "Use simulateTransaction to run it against the replayed state, or create the session \
1115 without replayAccountState.";
1116
1117 pub const SEND_ACTION: &str = "Use kind: simulate, which runs against the replayed state without committing, post state \
1118 directly with an action carrying accountOverrides and no transactions, or create the \
1119 session without replayAccountState.";
1120
1121 pub fn unsupported(operation: &str, remedy: &str) -> String {
1123 format!(
1124 "{operation} is not supported while account state replay is enabled: replay \
1125 reconstructs account state from the recorded delta bundle instead of executing \
1126 transactions, so this session never executes or persists any. {remedy}"
1127 )
1128 }
1129
1130 pub fn send_action_conflict(params: &CreateSessionParams) -> Option<String> {
1133 (params.replay_account_state
1134 && params
1135 .actions
1136 .iter()
1137 .any(|action| action.kind == ActionKind::Send))
1138 .then(|| unsupported("A scheduled action with kind: send", SEND_ACTION))
1139 }
1140}
1141
1142impl CreateSessionParams {
1143 pub fn requires_sidecar(&self) -> bool {
1146 self.reroute.as_ref().is_some_and(|reroute| reroute.requote)
1147 || !self.diagnostic_probes.is_empty()
1148 }
1149
1150 pub fn requires_diagnostic_endpoint(&self) -> bool {
1154 !self.diagnostic_probes.is_empty()
1155 }
1156
1157 pub fn validate_diagnostic_probes(&self) -> Result<(), DiagnosticProbeError> {
1158 self.diagnostic_probes
1159 .iter()
1160 .try_for_each(DiagnosticProbeParams::validate)
1161 }
1162
1163 pub fn warms_account_universe(&self) -> bool {
1165 self.requires_sidecar()
1166 || self
1167 .reroute
1168 .as_ref()
1169 .is_some_and(|reroute| !reroute.direct_fill.is_empty())
1170 }
1171
1172 pub fn reroute_order_flow(&self) -> bool {
1173 self.reroute.is_some()
1174 }
1175
1176 pub fn override_schedule(&self) -> Result<OverrideSchedule, String> {
1179 decode_override_actions(&self.actions)
1180 }
1181
1182 pub fn reroute_filter(&self) -> Option<&RerouteFilter> {
1184 self.reroute.as_ref()?.filter.as_ref()
1185 }
1186}
1187
1188#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1191#[serde(rename_all = "kebab-case")]
1192pub enum FailFastDivergenceKind {
1193 #[default]
1196 AnyNonBenign,
1197 Tracked,
1200}
1201
1202impl FailFastDivergenceKind {
1203 pub fn as_str(self) -> &'static str {
1206 match self {
1207 Self::AnyNonBenign => "any-non-benign",
1208 Self::Tracked => "tracked",
1209 }
1210 }
1211
1212 pub fn from_str_opt(value: &str) -> Option<Self> {
1214 match value {
1215 "any-non-benign" => Some(Self::AnyNonBenign),
1216 "tracked" => Some(Self::Tracked),
1217 _ => None,
1218 }
1219 }
1220}
1221
1222#[serde_with::serde_as]
1224#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1225pub struct AccountModifications(
1226 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
1227 #[serde(default)]
1228 pub BTreeMap<Address, AccountData>,
1229);
1230
1231impl From<BTreeMap<Address, AccountData>> for AccountModifications {
1232 fn from(value: BTreeMap<Address, AccountData>) -> Self {
1233 Self(value)
1234 }
1235}
1236
1237#[serde_with::serde_as]
1239#[derive(Debug, Serialize, Deserialize)]
1240#[serde(rename_all = "camelCase")]
1241pub struct ContinueParams {
1242 #[serde(default = "ContinueParams::default_advance_count")]
1243 pub advance_count: u64,
1245 #[serde(default)]
1246 pub transactions: Vec<String>,
1248 #[serde(default)]
1249 pub modify_account_states: AccountModifications,
1251}
1252
1253impl Default for ContinueParams {
1254 fn default() -> Self {
1255 Self {
1256 advance_count: Self::default_advance_count(),
1257 transactions: Vec::new(),
1258 modify_account_states: AccountModifications(BTreeMap::new()),
1259 }
1260 }
1261}
1262
1263impl ContinueParams {
1264 pub fn default_advance_count() -> u64 {
1265 1
1266 }
1267}
1268
1269#[derive(Debug, Clone, Serialize, Deserialize)]
1275#[serde(rename_all = "camelCase")]
1276pub struct PausedEvent {
1277 pub slot: u64,
1278 #[serde(default, skip_serializing_if = "Option::is_none")]
1279 pub batch_index: Option<u32>,
1280}
1281
1282#[serde_with::serde_as]
1291#[derive(Debug, Clone, Serialize, Deserialize)]
1292#[serde(rename_all = "camelCase")]
1293pub struct DiscoveryBatchEvent {
1294 pub slot: u64,
1295 pub batch_index: u32,
1296 pub matched: Vec<DiscoveryFilter>,
1298 pub transactions: Vec<EncodedBinary>,
1302 #[serde(default)]
1305 pub transaction_indices: Vec<u32>,
1306}
1307
1308#[derive(Debug, Clone, Serialize, Deserialize)]
1310#[serde(rename_all = "camelCase")]
1311pub struct ContinueToParams {
1312 pub slot: u64,
1314 #[serde(default)]
1320 pub batch_index: Option<u32>,
1321}
1322
1323#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1325#[serde(rename_all = "lowercase")]
1326pub enum BinaryEncoding {
1327 Base64,
1328}
1329
1330impl BinaryEncoding {
1331 pub fn encode(self, bytes: &[u8]) -> String {
1332 match self {
1333 Self::Base64 => BASE64.encode(bytes),
1334 }
1335 }
1336
1337 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
1338 match self {
1339 Self::Base64 => BASE64.decode(data),
1340 }
1341 }
1342}
1343
1344#[derive(Debug, Clone, Serialize, Deserialize)]
1346#[serde(rename_all = "camelCase")]
1347pub struct EncodedBinary {
1348 pub data: String,
1350 pub encoding: BinaryEncoding,
1352}
1353
1354impl EncodedBinary {
1355 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
1356 Self { data, encoding }
1357 }
1358
1359 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
1360 Self {
1361 data: encoding.encode(bytes),
1362 encoding,
1363 }
1364 }
1365
1366 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
1367 self.encoding.decode(&self.data)
1368 }
1369}
1370
1371#[serde_with::serde_as]
1373#[derive(Debug, Clone, Serialize, Deserialize)]
1374#[serde(rename_all = "camelCase")]
1375pub struct AccountData {
1376 pub data: EncodedBinary,
1378 pub executable: bool,
1380 pub lamports: u64,
1382 #[serde_as(as = "serde_with::DisplayFromStr")]
1383 pub owner: Address,
1385 pub space: u64,
1387}
1388
1389impl AccountData {
1390 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
1391 Ok(solana_account::Account {
1392 data: self.data.decode()?,
1393 lamports: self.lamports,
1394 owner: self.owner,
1395 executable: self.executable,
1396 rent_epoch: 0,
1397 })
1398 }
1399}
1400
1401#[derive(Debug, Clone, Serialize, Deserialize)]
1403#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1404pub enum BacktestResponse {
1405 SessionCreated {
1406 session_id: SessionId,
1407 rpc_endpoint: String,
1408 #[serde(default, skip_serializing_if = "Option::is_none")]
1409 task_id: Option<String>,
1410 },
1411 SessionAttached {
1412 session_id: SessionId,
1413 rpc_endpoint: String,
1414 #[serde(default, skip_serializing_if = "Option::is_none")]
1415 task_id: Option<String>,
1416 },
1417 SessionsCreatedV2 {
1418 control_session_id: ControlSessionId,
1419 session_ids: Vec<SessionId>,
1420 #[serde(default)]
1421 task_ids: Vec<Option<String>>,
1422 #[serde(default)]
1427 start_slots: Vec<u64>,
1428 #[serde(default)]
1429 end_slots: Vec<u64>,
1430 },
1431 ParallelSessionAttachedV2 {
1432 control_session_id: ControlSessionId,
1433 session_ids: Vec<SessionId>,
1434 #[serde(default)]
1435 task_ids: Vec<Option<String>>,
1436 },
1437 ReadyForContinue,
1438 SlotNotification(u64),
1439 Paused(PausedEvent),
1440 DiscoveryBatch(DiscoveryBatchEvent),
1441 Error(BacktestError),
1442 Success,
1443 Completed {
1444 #[serde(skip_serializing_if = "Option::is_none")]
1448 summary: Option<SessionSummary>,
1449 },
1450 Status {
1451 status: BacktestStatus,
1452 },
1453 SessionEventV2 {
1454 session_id: SessionId,
1455 seq_id: u64,
1456 event: SessionEventKind,
1457 },
1458}
1459
1460impl BacktestResponse {
1461 pub fn is_completed(&self) -> bool {
1462 matches!(self, BacktestResponse::Completed { .. })
1463 }
1464
1465 pub fn is_terminal(&self) -> bool {
1466 match self {
1467 BacktestResponse::Completed { .. } => true,
1468 BacktestResponse::Error(e) => e.is_terminal(),
1469 _ => false,
1470 }
1471 }
1472}
1473
1474impl From<BacktestStatus> for BacktestResponse {
1475 fn from(status: BacktestStatus) -> Self {
1476 Self::Status { status }
1477 }
1478}
1479
1480impl From<String> for BacktestResponse {
1481 fn from(message: String) -> Self {
1482 BacktestError::Internal { error: message }.into()
1483 }
1484}
1485
1486impl From<&str> for BacktestResponse {
1487 fn from(message: &str) -> Self {
1488 BacktestError::Internal {
1489 error: message.to_string(),
1490 }
1491 .into()
1492 }
1493}
1494
1495#[derive(Debug, Clone, Serialize, Deserialize)]
1496#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1497pub enum SessionEventKind {
1498 ReadyForContinue,
1499 SlotNotification(u64),
1500 Paused(PausedEvent),
1501 DiscoveryBatch(DiscoveryBatchEvent),
1502 Error(BacktestError),
1503 Success,
1504 Completed {
1505 #[serde(skip_serializing_if = "Option::is_none")]
1506 summary: Option<SessionSummary>,
1507 },
1508 Status {
1509 status: BacktestStatus,
1510 },
1511}
1512
1513impl SessionEventKind {
1514 pub fn is_terminal(&self) -> bool {
1515 match self {
1516 Self::Completed { .. } => true,
1517 Self::Error(e) => e.is_terminal(),
1518 _ => false,
1519 }
1520 }
1521}
1522
1523#[derive(Debug, Clone, Serialize, Deserialize)]
1526#[serde(rename_all = "camelCase")]
1527pub struct SequencedResponse {
1528 pub seq_id: u64,
1529 #[serde(flatten)]
1530 pub response: BacktestResponse,
1531}
1532
1533#[derive(Debug, Clone, Serialize, Deserialize)]
1535#[serde(rename_all = "camelCase")]
1536pub enum BacktestStatus {
1537 StartingRuntime,
1539 DecodedTransactions,
1540 AppliedAccountModifications,
1541 ReadyToExecuteUserTransactions,
1542 ExecutedUserTransactions,
1543 ExecutingBlockTransactions,
1544 ExecutedBlockTransactions,
1545 ProgramAccountsLoaded,
1546}
1547
1548impl std::fmt::Display for BacktestStatus {
1549 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1550 let s = match self {
1551 Self::StartingRuntime => "starting runtime",
1552 Self::DecodedTransactions => "decoded transactions",
1553 Self::AppliedAccountModifications => "applied account modifications",
1554 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
1555 Self::ExecutedUserTransactions => "executed user transactions",
1556 Self::ExecutingBlockTransactions => "executing block transactions",
1557 Self::ExecutedBlockTransactions => "executed block transactions",
1558 Self::ProgramAccountsLoaded => "program accounts loaded",
1559 };
1560 f.write_str(s)
1561 }
1562}
1563
1564#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1569#[serde(rename_all = "camelCase")]
1570pub struct RerouteStatsReport {
1571 #[serde(default)]
1573 pub swaps_detected: u64,
1574 #[serde(default)]
1578 pub swaps_filtered: u64,
1579 #[serde(default)]
1581 pub swaps_rerouted: u64,
1582 #[serde(default)]
1584 pub swaps_simulated: u64,
1585 #[serde(default)]
1587 pub swaps_succeeded: u64,
1588 #[serde(default)]
1590 pub requote_failures: u64,
1591 #[serde(default)]
1593 pub override_setup_failures: u64,
1594 #[serde(default)]
1596 pub metis_unavailable_blocks: u64,
1597 #[serde(default)]
1601 pub quote_attempts_unavailable: u64,
1602 #[serde(default)]
1605 pub direct_fill_matched: u64,
1606 #[serde(default)]
1610 pub direct_fill_outcomes: BTreeMap<String, u64>,
1611 #[serde(default)]
1613 pub direct_fill_scored: u64,
1614 #[serde(default)]
1618 pub direct_fill_bps_total: i64,
1619 #[serde(default)]
1622 pub direct_fill_templates: BTreeMap<String, DirectFillTemplateReport>,
1623 #[serde(default)]
1625 pub originals_scored: u64,
1626 #[serde(default)]
1629 pub originals_bps_total: i64,
1630 #[serde(default)]
1632 pub originals_replayed: u64,
1633 #[serde(default, skip_serializing_if = "Option::is_none")]
1635 pub dataset_uri: Option<String>,
1636}
1637
1638pub const REROUTE_DATASET_RETENTION_DAYS: u32 = 7;
1640
1641#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1644#[serde(rename_all = "camelCase")]
1645pub struct DirectFillTemplateReport {
1646 pub matched: u64,
1647 pub outcomes: BTreeMap<String, u64>,
1648 pub scored: u64,
1649 pub bps_total: i64,
1650}
1651
1652#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1654#[serde(rename_all = "camelCase")]
1655pub struct SessionSummary {
1656 pub correct_simulation: usize,
1659 pub incorrect_simulation: usize,
1662 pub execution_errors: usize,
1664 pub balance_diff: usize,
1666 pub log_diff: usize,
1668 #[serde(default)]
1671 pub unsanitizable: usize,
1672 #[serde(default, skip_serializing_if = "Option::is_none")]
1675 pub reroute_stats: Option<Box<RerouteStatsReport>>,
1676}
1677
1678impl SessionSummary {
1679 pub fn has_deviations(&self) -> bool {
1681 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
1682 }
1683
1684 pub fn total_transactions(&self) -> usize {
1686 self.correct_simulation
1687 + self.incorrect_simulation
1688 + self.execution_errors
1689 + self.balance_diff
1690 + self.log_diff
1691 }
1692
1693 pub fn deviation_details(&self, balance_label: &str, log_label: &str) -> String {
1698 format!(
1699 "{} incorrect simulations, {} execution errors, {} {balance_label}, {} {log_label}",
1700 self.incorrect_simulation, self.execution_errors, self.balance_diff, self.log_diff,
1701 )
1702 }
1703}
1704
1705impl std::fmt::Display for SessionSummary {
1706 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1707 let total = self.total_transactions();
1708 write!(
1709 f,
1710 "Session summary: {total} transactions\n\
1711 \x20 - {} correct simulation\n\
1712 \x20 - {} incorrect simulation\n\
1713 \x20 - {} execution errors\n\
1714 \x20 - {} unsanitizable\n\
1715 \x20 - {} balance diffs\n\
1716 \x20 - {} log diffs",
1717 self.correct_simulation,
1718 self.incorrect_simulation,
1719 self.execution_errors,
1720 self.unsanitizable,
1721 self.balance_diff,
1722 self.log_diff,
1723 )
1724 }
1725}
1726
1727#[derive(Debug, Clone, Serialize, Deserialize)]
1729#[serde(rename_all = "camelCase")]
1730pub enum BacktestError {
1731 InvalidTransactionEncoding {
1732 index: usize,
1733 error: String,
1734 },
1735 InvalidTransactionFormat {
1736 index: usize,
1737 error: String,
1738 },
1739 InvalidAccountEncoding {
1740 address: String,
1741 encoding: BinaryEncoding,
1742 error: String,
1743 },
1744 InvalidAccountOwner {
1745 address: String,
1746 error: String,
1747 },
1748 InvalidAccountPubkey {
1749 address: String,
1750 error: String,
1751 },
1752 NoMoreBlocks,
1753 BundleNotFound {
1754 start_slot: u64,
1755 end_slot: u64,
1756 },
1757 AdvanceSlotFailed {
1758 slot: u64,
1759 error: String,
1760 },
1761 FinalizeSlotFailed {
1762 slot: u64,
1763 error: String,
1764 },
1765 InvalidRequest {
1766 error: String,
1767 },
1768 Internal {
1769 error: String,
1770 },
1771 InvalidBlockhashFormat {
1772 slot: u64,
1773 error: String,
1774 },
1775 InitializingSysvarsFailed {
1776 slot: u64,
1777 error: String,
1778 },
1779 ClerkError {
1780 error: String,
1781 },
1782 SimulationError {
1783 error: String,
1784 },
1785 SessionNotFound {
1786 session_id: String,
1787 },
1788 SessionOwnerMismatch,
1789 SessionOwnershipBusy {
1794 reason: String,
1795 },
1796 NoProducedBlocksInRange {
1802 start_slot: u64,
1803 end_slot: u64,
1804 },
1805}
1806
1807impl BacktestError {
1808 pub fn is_terminal(&self) -> bool {
1812 matches!(
1813 self,
1814 BacktestError::NoMoreBlocks
1815 | BacktestError::BundleNotFound { .. }
1816 | BacktestError::AdvanceSlotFailed { .. }
1817 | BacktestError::FinalizeSlotFailed { .. }
1818 | BacktestError::Internal { .. }
1819 | BacktestError::NoProducedBlocksInRange { .. }
1820 )
1821 }
1822}
1823
1824#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1826pub struct AvailableRange {
1827 pub bundle_start_slot: u64,
1828 pub bundle_start_slot_utc: Option<String>,
1829 pub max_bundle_end_slot: Option<u64>,
1830 pub max_bundle_end_slot_utc: Option<String>,
1831 pub max_bundle_size: Option<u64>,
1832 #[serde(default)]
1835 pub kind: Option<BundleKind>,
1836 #[serde(default)]
1839 pub scope: Option<RangeScope>,
1840}
1841
1842#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1844#[serde(rename_all = "snake_case")]
1845pub enum RangeScope {
1846 Global,
1848 User,
1850}
1851
1852impl RangeScope {
1853 pub fn as_str(self) -> &'static str {
1855 match self {
1856 Self::Global => "global",
1857 Self::User => "user",
1858 }
1859 }
1860}
1861
1862#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1864#[serde(rename_all = "snake_case")]
1865pub enum BundleKind {
1866 #[default]
1868 Blocks,
1869 AccountState,
1873}
1874
1875pub const REPLAY_RANGE_ADVICE: &str = "replayed state is resolved by the recording that begins \
1879 where the request does, so request a range that begins where one was recorded and ends \
1880 within it";
1881
1882impl BundleKind {
1883 fn coverage_noun(self) -> &'static str {
1885 match self {
1886 Self::Blocks => "bundle range",
1887 Self::AccountState => "account-state bundle",
1888 }
1889 }
1890
1891 fn coverage_nouns(self) -> &'static str {
1893 match self {
1894 Self::Blocks => "bundle ranges",
1895 Self::AccountState => "account-state bundles",
1896 }
1897 }
1898
1899 pub fn as_str(self) -> &'static str {
1901 match self {
1902 Self::Blocks => "blocks",
1903 Self::AccountState => "account_state",
1904 }
1905 }
1906
1907 pub fn from_str_opt(value: &str) -> Option<Self> {
1909 match value {
1910 "blocks" => Some(Self::Blocks),
1911 "account_state" => Some(Self::AccountState),
1912 _ => None,
1913 }
1914 }
1915}
1916
1917impl fmt::Display for BundleKind {
1920 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1921 let label = match self {
1922 Self::Blocks => "transactions",
1923 Self::AccountState => "account_state",
1924 };
1925 f.pad(label)
1926 }
1927}
1928
1929#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1934#[serde(rename_all = "snake_case")]
1935pub enum JobKind {
1936 #[default]
1938 BundleBuild,
1939 Depth,
1941}
1942
1943impl JobKind {
1944 pub fn as_str(self) -> &'static str {
1946 match self {
1947 Self::BundleBuild => "bundle_build",
1948 Self::Depth => "depth",
1949 }
1950 }
1951
1952 pub fn from_str_opt(value: &str) -> Option<Self> {
1954 match value {
1955 "bundle_build" => Some(Self::BundleBuild),
1956 "depth" => Some(Self::Depth),
1957 _ => None,
1958 }
1959 }
1960}
1961
1962#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1967#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
1968#[serde(rename_all = "snake_case")]
1969pub enum DepthSource {
1970 #[default]
1972 Onchain,
1973 Router,
1975}
1976
1977impl DepthSource {
1978 pub const fn as_str(self) -> &'static str {
1980 match self {
1981 Self::Onchain => "onchain",
1982 Self::Router => "router",
1983 }
1984 }
1985
1986 pub fn from_str_opt(value: &str) -> Option<Self> {
1988 match value {
1989 "onchain" => Some(Self::Onchain),
1990 "router" => Some(Self::Router),
1991 _ => None,
1992 }
1993 }
1994
1995 pub const fn discriminant(self) -> u8 {
2000 match self {
2001 Self::Onchain => 0,
2002 Self::Router => 1,
2003 }
2004 }
2005}
2006
2007impl fmt::Display for DepthSource {
2008 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2009 f.write_str(self.as_str())
2010 }
2011}
2012
2013#[derive(Debug, Clone, Serialize, Deserialize)]
2018pub struct DepthCoverageResponse {
2019 pub sources: Vec<DepthSourceCoverage>,
2020}
2021
2022#[derive(Debug, Clone, Serialize, Deserialize)]
2023pub struct DepthSourceCoverage {
2024 pub source: DepthSource,
2026 pub revision: u16,
2029 pub markets: Vec<String>,
2031 pub venues: Vec<String>,
2033 pub size_rungs: usize,
2035 pub completed_ranges: Vec<SlotRange>,
2038}
2039
2040#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2042pub struct DepthRequest {
2043 pub start_slot: u64,
2046 pub end_slot: u64,
2048 #[serde(default)]
2051 pub source: DepthSource,
2052 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2054 pub markets: Vec<String>,
2055}
2056
2057#[derive(Debug, Clone, Serialize, Deserialize)]
2059pub struct DepthStatusResponse {
2060 pub request_id: String,
2061 pub start_slot: u64,
2062 pub end_slot: u64,
2063 pub source: DepthSource,
2064 pub status: BundleBuildStatus,
2065}
2066
2067#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2071pub struct BundleBuildRequest {
2072 pub start_slot: u64,
2073 pub end_slot: u64,
2074 #[serde(default)]
2075 pub bundle_size: Option<u64>,
2076 #[serde(default)]
2079 pub idempotency_key: Option<String>,
2080 #[serde(default)]
2082 pub kind: BundleKind,
2083}
2084
2085#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2086#[serde(rename_all = "snake_case")]
2087pub enum BundleBuildStatus {
2088 Completed,
2090 Failed,
2092 InProgress,
2094 NeedsInvestigation,
2097}
2098
2099impl BundleBuildStatus {
2100 pub fn as_str(self) -> &'static str {
2102 match self {
2103 Self::Completed => "completed",
2104 Self::Failed => "failed",
2105 Self::InProgress => "in_progress",
2106 Self::NeedsInvestigation => "needs_investigation",
2107 }
2108 }
2109}
2110
2111#[derive(Debug, Clone, Serialize, Deserialize)]
2113pub struct BundleBuildStatusResponse {
2114 pub request_id: String,
2115 pub start_slot: u64,
2116 pub end_slot: u64,
2117 pub bundle_size: Option<u64>,
2118 pub status: BundleBuildStatus,
2119 pub created_at_unix_ms: u64,
2122 #[serde(default)]
2124 pub kind: BundleKind,
2125 #[serde(default)]
2128 pub eta_secs: Option<u64>,
2129}
2130
2131#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2133pub struct CompletedBundle {
2134 pub start_slot: u64,
2135 pub end_slot: u64,
2136 pub kind: BundleKind,
2137 #[serde(default)]
2139 pub completed_at_unix_ms: Option<i64>,
2140 #[serde(default)]
2141 pub size_bytes: Option<i64>,
2142}
2143
2144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2146pub struct BundleDeleteRequest {
2147 pub start_slot: u64,
2148 pub end_slot: u64,
2149 #[serde(default)]
2151 pub kind: BundleKind,
2152}
2153
2154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2156pub struct BundleDeleteResponse {
2157 pub start_slot: u64,
2158 pub end_slot: u64,
2159 pub kind: BundleKind,
2160}
2161
2162#[derive(Debug, Clone, Serialize, Deserialize)]
2164pub struct BundleBuildListResponse {
2165 pub builds: Vec<BundleBuildStatusResponse>,
2168 #[serde(default)]
2173 pub bundles: Option<Vec<CompletedBundle>>,
2174}
2175
2176#[derive(Debug, Clone, Serialize, Deserialize)]
2179pub struct SnapshotSlotResponse {
2180 pub snapshot_slot: u64,
2181}
2182
2183pub fn split_range(
2206 ranges: &[AvailableRange],
2207 requested: SlotRange,
2208 kind: BundleKind,
2209) -> Result<Vec<(u64, u64)>, String> {
2210 let (requested_start, requested_end) = (requested.start(), requested.end());
2211
2212 let ends_by_start = ranges
2219 .iter()
2220 .filter(|r| r.kind.unwrap_or_default() == kind)
2221 .filter_map(|r| {
2222 r.max_bundle_end_slot
2223 .filter(|&end| end >= r.bundle_start_slot)
2224 .map(|end| (r.bundle_start_slot, end))
2225 })
2226 .fold(
2227 BTreeMap::<u64, BTreeSet<u64>>::new(),
2228 |mut ends_by_start, (start, end)| {
2229 ends_by_start.entry(start).or_default().insert(end);
2230 ends_by_start
2231 },
2232 );
2233
2234 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
2242 ends.iter()
2243 .next_back()
2244 .is_some_and(|&end| end >= requested_start)
2245 }) else {
2246 return Err(format!(
2247 "start_slot {requested_start} is not covered by any available {}",
2248 kind.coverage_noun()
2249 ));
2250 };
2251
2252 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
2258 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
2259 let mut best: Option<Vec<(u64, u64)>> = None;
2260 for &end in ends {
2261 let candidate = if end >= requested_end {
2262 Some(vec![(start, requested_end)])
2263 } else {
2264 best_from.get(&(end + 1)).map(|rest| {
2265 std::iter::once((start, end))
2266 .chain(rest.iter().copied())
2267 .collect()
2268 })
2269 };
2270 if let Some(candidate) = candidate
2271 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
2272 {
2273 best = Some(candidate);
2274 }
2275 }
2276 if let Some(best) = best {
2277 best_from.insert(start, best);
2278 }
2279 }
2280
2281 best_from.remove(&anchor_start).ok_or_else(|| {
2282 let mut covered_to = anchor_start.saturating_sub(1);
2286 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
2287 if start > covered_to.saturating_add(1) {
2288 break;
2289 }
2290 if let Some(&end) = ends.iter().next_back() {
2291 covered_to = covered_to.max(end);
2292 }
2293 }
2294 if covered_to < requested_end {
2295 format!("gap in coverage at slot {}", covered_to + 1)
2296 } else {
2297 format!(
2298 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available {}",
2299 kind.coverage_nouns()
2300 )
2301 }
2302 })
2303}
2304
2305impl From<BacktestError> for BacktestResponse {
2306 fn from(error: BacktestError) -> Self {
2307 Self::Error(error)
2308 }
2309}
2310
2311impl std::error::Error for BacktestError {}
2312
2313impl fmt::Display for BacktestError {
2314 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2315 match self {
2316 BacktestError::InvalidTransactionEncoding { index, error } => {
2317 write!(f, "invalid transaction encoding at index {index}: {error}")
2318 }
2319 BacktestError::InvalidTransactionFormat { index, error } => {
2320 write!(f, "invalid transaction format at index {index}: {error}")
2321 }
2322 BacktestError::InvalidAccountEncoding {
2323 address,
2324 encoding,
2325 error,
2326 } => write!(
2327 f,
2328 "invalid encoding for account {address} ({encoding:?}): {error}"
2329 ),
2330 BacktestError::InvalidAccountOwner { address, error } => {
2331 write!(f, "invalid owner for account {address}: {error}")
2332 }
2333 BacktestError::InvalidAccountPubkey { address, error } => {
2334 write!(f, "invalid account pubkey {address}: {error}")
2335 }
2336 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
2337 BacktestError::BundleNotFound {
2338 start_slot,
2339 end_slot,
2340 } => write!(
2341 f,
2342 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
2343 ),
2344 BacktestError::AdvanceSlotFailed { slot, error } => {
2345 write!(f, "failed to advance to slot {slot}: {error}")
2346 }
2347 BacktestError::FinalizeSlotFailed { slot, error } => {
2348 write!(f, "failed to finalize slot {slot}: {error}")
2349 }
2350 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
2351 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
2352 BacktestError::InvalidBlockhashFormat { slot, error } => {
2353 write!(f, "invalid blockhash at slot {slot}: {error}")
2354 }
2355 BacktestError::InitializingSysvarsFailed { slot, error } => {
2356 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
2357 }
2358 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
2359 BacktestError::SimulationError { error } => {
2360 write!(f, "simulation error: {error}")
2361 }
2362 BacktestError::SessionNotFound { session_id } => {
2363 write!(f, "session not found: {session_id}")
2364 }
2365 BacktestError::SessionOwnerMismatch => {
2366 write!(f, "session owner mismatch")
2367 }
2368 BacktestError::SessionOwnershipBusy { reason } => {
2369 write!(f, "session ownership busy: {reason}")
2370 }
2371 BacktestError::NoProducedBlocksInRange {
2372 start_slot,
2373 end_slot,
2374 } => write!(
2375 f,
2376 "no produced blocks in requested range {start_slot}..={end_slot}; \
2377 slot(s) may be skipped on-chain"
2378 ),
2379 }
2380 }
2381}
2382
2383#[cfg(test)]
2384mod tests {
2385 use rstest::rstest;
2386 use solana_account::ReadableAccount;
2387
2388 use super::*;
2389
2390 #[test]
2393 fn a_sources_discriminant_is_frozen_independently_of_its_label() {
2394 assert_eq!(DepthSource::Onchain.discriminant(), 0);
2395 assert_eq!(DepthSource::Router.discriminant(), 1);
2396 }
2397
2398 #[test]
2401 fn a_source_spells_itself_the_same_way_three_times() {
2402 for source in [DepthSource::Onchain, DepthSource::Router] {
2403 assert_eq!(
2404 serde_json::to_value(source).expect("a source serialises"),
2405 serde_json::json!(source.as_str())
2406 );
2407 assert_eq!(DepthSource::from_str_opt(source.as_str()), Some(source));
2408 }
2409 }
2410
2411 fn address(byte: u8) -> Address {
2412 Address::from([byte; 32])
2413 }
2414
2415 #[rstest]
2416 #[case::one("jupiter", Some(&[SwapAggregator::Jupiter][..]))]
2417 #[case::several("jupiter,okx", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2418 #[case::spaced_and_uppercased(" Jupiter , OKX ", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2419 #[case::repeats_collapse("okx,okx", Some(&[SwapAggregator::Okx][..]))]
2420 #[case::trailing_separator("okx,", Some(&[SwapAggregator::Okx][..]))]
2421 #[case::unknown("jupiter,bogus", None)]
2422 #[case::empty("", None)]
2423 #[case::separators_only(",,", None)]
2424 fn reroute_aggregators_parse_the_shapes_a_flag_carries(
2425 #[case] input: &str,
2426 #[case] expected: Option<&[SwapAggregator]>,
2427 ) {
2428 assert_eq!(
2429 input.parse::<RerouteAggregators>().ok(),
2430 expected.map(|aggregators| RerouteAggregators::new(aggregators.iter().copied()))
2431 );
2432 }
2433
2434 #[test]
2436 fn reroute_aggregators_round_trip_through_their_rendering() {
2437 let aggregators = RerouteAggregators::new([SwapAggregator::Dflow, SwapAggregator::Jupiter]);
2438
2439 assert_eq!(aggregators.to_string(), "jupiter,dflow");
2440 assert_eq!(
2441 aggregators
2442 .to_string()
2443 .parse::<RerouteAggregators>()
2444 .unwrap(),
2445 aggregators
2446 );
2447 assert_eq!(RerouteAggregators::default().to_string(), "jupiter");
2448 }
2449
2450 #[test]
2453 fn aggregators_deserialize_through_the_same_parser_the_flag_uses() {
2454 let params: CreateSessionParams = serde_json::from_value(
2455 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {"aggregators": ["Jupiter", "OKX"]}}),
2456 )
2457 .expect("decode");
2458
2459 assert_eq!(
2460 params.reroute.expect("rerouting requested").aggregators,
2461 RerouteAggregators::new([SwapAggregator::Jupiter, SwapAggregator::Okx])
2462 );
2463 }
2464
2465 #[test]
2468 fn params_without_the_new_reroute_fields_decode_to_the_narrow_defaults() {
2469 let params: CreateSessionParams = serde_json::from_value(
2470 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {}}),
2471 )
2472 .expect("decode");
2473
2474 let reroute = params.reroute.expect("rerouting requested");
2475 assert_eq!(reroute.aggregators, RerouteAggregators::default());
2476 assert!(reroute.aggregators.contains(SwapAggregator::Jupiter));
2477 assert!(!reroute.aggregators.contains(SwapAggregator::Okx));
2478 assert!(!reroute.circular_arbs);
2479 assert!(!reroute.detect_failed_l1_swaps);
2480 }
2481
2482 #[test]
2484 fn a_request_without_reroute_reserves_no_sidecar() {
2485 let params: CreateSessionParams =
2486 serde_json::from_value(serde_json::json!({"startSlot": 1, "endSlot": 2}))
2487 .expect("decode");
2488
2489 assert!(!params.requires_sidecar());
2490 }
2491
2492 #[test]
2497 fn a_per_slot_action_schedule_compresses_across_actions() {
2498 let slots = 2_000u64;
2499 let actions = (0..slots)
2500 .map(|slot| ScheduledAction {
2501 anchor: ActionAnchor::AfterSlot { slots: vec![slot] },
2502 kind: ActionKind::Simulate,
2503 feeds_reroute: false,
2504 transactions: Vec::new(),
2505 account_overrides: AccountModifications(BTreeMap::from([(
2506 address(7),
2507 AccountData {
2508 data: EncodedBinary::new(
2510 BASE64.encode(
2511 [&slot.to_le_bytes()[..], &[0xAB; 2040][..]]
2512 .concat()
2513 .as_slice(),
2514 ),
2515 BinaryEncoding::Base64,
2516 ),
2517 executable: false,
2518 lamports: 15_144_966,
2519 owner: address(9),
2520 space: 2048,
2521 },
2522 )])),
2523 return_accounts: Vec::new(),
2524 label: None,
2525 })
2526 .collect::<Vec<_>>();
2527 let params = CreateSessionParams::builder()
2528 .start_slot(1)
2529 .end_slot(slots)
2530 .actions(actions.clone())
2531 .build();
2532
2533 let encoded = serde_json::to_string(¶ms).expect("serialize");
2534 let raw = serde_json::to_string(&actions).expect("serialize raw actions");
2535 assert!(
2536 encoded.len() * 8 < raw.len(),
2537 "expected the schedule to compress well; got {} bytes from {} raw",
2538 encoded.len(),
2539 raw.len()
2540 );
2541
2542 let decoded: CreateSessionParams = serde_json::from_str(&encoded).expect("round trip");
2543 assert_eq!(
2544 serde_json::to_string(&decoded.actions).expect("serialize decoded"),
2545 raw
2546 );
2547 }
2548
2549 #[test]
2552 fn an_empty_action_list_is_omitted_entirely() {
2553 let params = CreateSessionParams::builder()
2554 .start_slot(1)
2555 .end_slot(2)
2556 .build();
2557 let json = serde_json::to_value(¶ms).expect("serialize");
2558 assert!(json.get("actions").is_none(), "{json}");
2559 let decoded: CreateSessionParams = serde_json::from_value(json).expect("round trip");
2560 assert!(decoded.actions.is_empty());
2561 }
2562
2563 #[test]
2566 fn send_actions_conflict_with_replay_only_together() {
2567 let params = |replay, kind| {
2568 CreateSessionParams::builder()
2569 .start_slot(1)
2570 .end_slot(2)
2571 .replay_account_state(replay)
2572 .actions(vec![ScheduledAction {
2573 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2574 kind,
2575 feeds_reroute: false,
2576 transactions: vec!["dGVzdA==".to_string()],
2577 account_overrides: Default::default(),
2578 return_accounts: Default::default(),
2579 label: None,
2580 }])
2581 .build()
2582 };
2583 assert!(replay_support::send_action_conflict(¶ms(false, ActionKind::Send)).is_none());
2584 assert!(
2585 replay_support::send_action_conflict(¶ms(true, ActionKind::Simulate)).is_none()
2586 );
2587 let mut publishing = params(true, ActionKind::Simulate);
2588 publishing.actions[0].feeds_reroute = true;
2589 assert!(replay_support::send_action_conflict(&publishing).is_none());
2590
2591 let conflict = replay_support::send_action_conflict(¶ms(true, ActionKind::Send))
2592 .expect("send under replay conflicts");
2593 assert!(conflict.contains("kind: send"), "{conflict}");
2594 assert!(conflict.contains(replay_support::SEND_ACTION), "{conflict}");
2595 }
2596
2597 fn override_entry(owner: Address, executable: bool) -> AccountData {
2598 AccountData {
2599 data: EncodedBinary::from_bytes(&[1, 2, 3], BinaryEncoding::Base64),
2600 executable,
2601 lamports: 1,
2602 owner,
2603 space: 3,
2604 }
2605 }
2606
2607 fn before() -> ActionAnchor {
2608 ActionAnchor::BeforeSlot { slots: vec![1] }
2609 }
2610
2611 fn valid() -> ScheduledAction {
2612 override_action(before(), address(3), override_entry(address(4), false))
2613 }
2614
2615 fn override_action(
2616 anchor: ActionAnchor,
2617 account: Address,
2618 data: AccountData,
2619 ) -> ScheduledAction {
2620 ScheduledAction {
2621 anchor,
2622 kind: ActionKind::default(),
2623 transactions: Vec::new(),
2624 account_overrides: AccountModifications(BTreeMap::from([(account, data)])),
2625 return_accounts: Vec::new(),
2626 label: None,
2627 feeds_reroute: true,
2628 }
2629 }
2630
2631 #[test]
2632 fn override_action_round_trips() {
2633 let key = address(1);
2634 let entry = override_entry(address(2), false);
2635 let action = override_action(
2636 ActionAnchor::BeforeSlot { slots: vec![5, 9] },
2637 key,
2638 entry.clone(),
2639 );
2640 let json = serde_json::to_value(&action).unwrap();
2641 assert!(
2642 json["anchor"]["slots"].is_string(),
2643 "slots travel as a compressed blob: {json}"
2644 );
2645 let back: ScheduledAction = serde_json::from_value(json).unwrap();
2646 assert_eq!(back.anchor, ActionAnchor::BeforeSlot { slots: vec![5, 9] });
2647 assert!(back.feeds_reroute);
2648 assert!(!back.is_computed_override(), "no transactions to simulate");
2649 let survived = &back.account_overrides.0[&key];
2650 assert_eq!(survived.lamports, entry.lamports);
2651 assert_eq!(survived.data.data, entry.data.data);
2652 }
2653
2654 #[test]
2655 fn an_every_slot_anchor_round_trips_and_defaults_to_every_slot() {
2656 let anchor = ActionAnchor::AfterEverySlot {
2657 every_n_slots: NonZeroU64::new(10).unwrap(),
2658 };
2659 let json = serde_json::to_value(&anchor).unwrap();
2660 assert_eq!(
2661 json,
2662 serde_json::json!({ "at": "afterEverySlot", "everyNSlots": 10 })
2663 );
2664 assert_eq!(
2665 serde_json::from_value::<ActionAnchor>(json).unwrap(),
2666 anchor
2667 );
2668
2669 assert_eq!(
2670 serde_json::from_value::<ActionAnchor>(serde_json::json!({ "at": "beforeEverySlot" }))
2671 .unwrap(),
2672 ActionAnchor::BeforeEverySlot {
2673 every_n_slots: NonZeroU64::MIN
2674 }
2675 );
2676 assert!(
2677 serde_json::from_value::<ActionAnchor>(
2678 serde_json::json!({ "at": "beforeEverySlot", "everyNSlots": 0 })
2679 )
2680 .is_err(),
2681 "a zero cadence names no slot"
2682 );
2683 }
2684
2685 fn context(
2686 invoked: &[u8],
2687 moves_tokens: bool,
2688 written: &[u8],
2689 writes_via_unresolved_lookup: bool,
2690 ) -> TxMatchContext {
2691 TxMatchContext {
2692 invoked_programs: invoked.iter().copied().map(address).collect(),
2693 moves_tokens,
2694 written_accounts: written.iter().copied().map(address).collect(),
2695 writes_via_unresolved_lookup,
2696 }
2697 }
2698
2699 #[rstest]
2701 #[case::update(&[], context(&[1], false, &[11], false), true)]
2702 #[case::moves_tokens(&[], context(&[1], true, &[11], false), false)]
2703 #[case::second_program_of_the_set(&[], context(&[2], false, &[11], false), true)]
2704 #[case::other_program(&[], context(&[3], false, &[11], false), false)]
2705 #[case::write_to_the_scope(&[10], context(&[1], false, &[11, 10], false), true)]
2706 #[case::write_elsewhere(&[10], context(&[1], false, &[11], false), false)]
2707 #[case::unresolved_write_through_a_lookup_table(&[10], context(&[1], false, &[11], true), true)]
2708 fn an_oracle_update_is_a_token_free_write_by_a_listed_program(
2709 #[case] accounts: &[u8],
2710 #[case] ctx: TxMatchContext,
2711 #[case] expected: bool,
2712 ) {
2713 let filter = DiscoveryFilter::OracleUpdate {
2714 programs: BTreeSet::from([address(1), address(2)]),
2715 accounts: accounts.iter().copied().map(address).collect(),
2716 };
2717 assert_eq!(filter.matches(&ctx), expected);
2718 }
2719
2720 #[rstest]
2721 #[case::unscoped(
2722 &[],
2723 serde_json::json!({ "kind": "oracleUpdate", "value": { "programs": [address(1).to_string()] } })
2724 )]
2725 #[case::scoped(
2726 &[2],
2727 serde_json::json!({
2728 "kind": "oracleUpdate",
2729 "value": { "programs": [address(1).to_string()], "accounts": [address(2).to_string()] }
2730 })
2731 )]
2732 fn an_oracle_update_filter_round_trips_as_kind_and_value(
2733 #[case] accounts: &[u8],
2734 #[case] expected: serde_json::Value,
2735 ) {
2736 let filter = DiscoveryFilter::OracleUpdate {
2737 programs: BTreeSet::from([address(1)]),
2738 accounts: accounts.iter().copied().map(address).collect(),
2739 };
2740 let json = serde_json::to_value(&filter).unwrap();
2741
2742 assert_eq!(json, expected);
2743 assert_eq!(
2744 serde_json::from_value::<DiscoveryFilter>(json).unwrap(),
2745 filter
2746 );
2747 }
2748
2749 #[test]
2754 fn a_token_account_override_is_accepted() {
2755 let action = override_action(
2756 before(),
2757 address(3),
2758 override_entry(spl_token_interface::ID, false),
2759 );
2760 let schedule = decode_override_actions(&[action]).expect("token overrides decode");
2761 assert!(!schedule.active_at(1).is_empty());
2762 }
2763
2764 #[rstest]
2765 #[case::executable(override_action(before(), address(3), override_entry(address(10), true)))]
2766 #[case::program_owned(override_action(
2767 before(),
2768 address(3),
2769 override_entry(solana_account::PROGRAM_OWNERS[0], false),
2770 ))]
2771 #[case::after_slot_anchor(override_action(
2772 ActionAnchor::AfterSlot { slots: vec![1] },
2773 address(3),
2774 override_entry(address(4), false),
2775 ))]
2776 #[case::every_slot_anchor(override_action(
2777 ActionAnchor::BeforeEverySlot { every_n_slots: NonZeroU64::MIN },
2778 address(3),
2779 override_entry(address(4), false),
2780 ))]
2781 #[case::nothing_to_post(ScheduledAction {
2782 account_overrides: AccountModifications::default(),
2783 ..valid()
2784 })]
2785 #[case::send(ScheduledAction {
2787 kind: ActionKind::Send,
2788 ..valid()
2789 })]
2790 #[allow(deprecated, reason = "exercise the loader-owner rejection path")]
2791 fn override_actions_reject_invalid_accounts_and_shapes(#[case] action: ScheduledAction) {
2792 assert!(decode_override_actions(&[action]).is_err());
2793 }
2794
2795 fn diagnostic_probe() -> DiagnosticProbeParams {
2796 DiagnosticProbeParams {
2797 anchor: ActionAnchor::AfterEverySlot {
2798 every_n_slots: NonZeroU64::MIN,
2799 },
2800 markets: vec![address(1), address(2)],
2801 usd_values: vec![100, 25_000],
2802 label: Some("depth-0".to_owned()),
2803 }
2804 }
2805
2806 #[rstest]
2807 #[case::no_markets(DiagnosticProbeParams { markets: Vec::new(), ..diagnostic_probe() })]
2808 #[case::no_usd_values(DiagnosticProbeParams { usd_values: Vec::new(), ..diagnostic_probe() })]
2809 #[case::too_many_markets(DiagnosticProbeParams {
2810 markets: (0..=MAX_DIAGNOSTIC_MARKETS as u8).map(address).collect(),
2811 ..diagnostic_probe()
2812 })]
2813 #[case::too_many_usd_values(DiagnosticProbeParams {
2814 usd_values: (1..=MAX_DIAGNOSTIC_USD_VALUES as u32 + 1).collect(),
2815 ..diagnostic_probe()
2816 })]
2817 #[case::zero_usd_value(DiagnosticProbeParams { usd_values: vec![0, 100], ..diagnostic_probe() })]
2818 #[case::duplicate_market(DiagnosticProbeParams {
2819 markets: vec![address(1), address(1)],
2820 ..diagnostic_probe()
2821 })]
2822 #[case::before_slot(DiagnosticProbeParams {
2823 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2824 ..diagnostic_probe()
2825 })]
2826 #[case::before_every_slot(DiagnosticProbeParams {
2827 anchor: ActionAnchor::BeforeEverySlot { every_n_slots: NonZeroU64::MIN },
2828 ..diagnostic_probe()
2829 })]
2830 fn diagnostic_probes_reject_invalid_shapes(#[case] probe: DiagnosticProbeParams) {
2831 assert!(probe.validate().is_err());
2832 }
2833
2834 #[test]
2835 fn diagnostic_probes_accept_both_supported_anchors() {
2836 assert!(diagnostic_probe().validate().is_ok());
2837 assert!(
2838 DiagnosticProbeParams {
2839 anchor: ActionAnchor::AfterSlot { slots: vec![1, 2] },
2840 ..diagnostic_probe()
2841 }
2842 .validate()
2843 .is_ok()
2844 );
2845 }
2846
2847 #[test]
2849 fn override_actions_accept_a_static_and_a_computed_shape() {
2850 assert!(decode_override_actions(&[valid()]).is_ok());
2851 assert!(
2854 decode_override_actions(&[ScheduledAction {
2855 transactions: vec!["dGVzdA==".to_string()],
2856 ..valid()
2857 }])
2858 .is_ok()
2859 );
2860 }
2861
2862 #[test]
2863 fn override_actions_merge_account_wise_later_wins() {
2864 let entry = |lamports| AccountData {
2865 lamports,
2866 ..override_entry(address(9), false)
2867 };
2868 let slot_action = |slots, account, lamports| {
2869 override_action(ActionAnchor::BeforeSlot { slots }, account, entry(lamports))
2870 };
2871 let actions = [
2872 slot_action(vec![5, 7], address(1), 1),
2873 slot_action(vec![5], address(2), 2),
2874 slot_action(vec![5], address(1), 3),
2875 ScheduledAction {
2876 anchor: ActionAnchor::AfterSlot { slots: vec![5] },
2877 kind: ActionKind::Simulate,
2878 feeds_reroute: false,
2879 transactions: vec!["dGVzdA==".to_string()],
2880 account_overrides: AccountModifications::default(),
2881 return_accounts: Vec::new(),
2882 label: None,
2883 },
2884 ];
2885 let schedule = decode_override_actions(&actions).unwrap();
2886 assert_eq!(schedule.slots().collect::<Vec<_>>(), vec![5, 7]);
2887 let lamports_at = |key, slot| {
2888 schedule
2889 .active_for(&key, slot)
2890 .map(|account| account.lamports())
2891 };
2892 assert_eq!(lamports_at(address(1), 5), Some(3));
2893 assert_eq!(lamports_at(address(2), 5), Some(2));
2894 assert_eq!(lamports_at(address(1), 7), Some(1));
2895 }
2896
2897 #[test]
2898 fn computed_override_resolution_preserves_action_order_and_failure() {
2899 let key = address(1);
2900 let static_action = |lamports| {
2901 override_action(
2902 ActionAnchor::BeforeSlot { slots: vec![5] },
2903 key,
2904 AccountData {
2905 lamports,
2906 ..override_entry(address(9), false)
2907 },
2908 )
2909 };
2910 let computed_action = || ScheduledAction {
2911 transactions: vec!["dGVzdA==".to_string()],
2912 ..static_action(2)
2913 };
2914
2915 let computed_last =
2916 decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2917 computed_last
2918 .resolve(5, 1, Some(HashMap::from([(key, shared_account(2))])))
2919 .unwrap();
2920 assert_eq!(
2921 computed_last.active_for(&key, 5).map(|a| a.lamports()),
2922 Some(2)
2923 );
2924
2925 let static_last = decode_override_actions(&[computed_action(), static_action(3)]).unwrap();
2926 static_last
2927 .resolve(5, 0, Some(HashMap::from([(key, shared_account(2))])))
2928 .unwrap();
2929 assert_eq!(
2930 static_last.active_for(&key, 5).map(|a| a.lamports()),
2931 Some(3)
2932 );
2933
2934 let failed = decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2935 failed.resolve(5, 1, None).unwrap();
2936 assert_eq!(failed.active_for(&key, 5).map(|a| a.lamports()), Some(1));
2937 assert!(failed.resolve(5, 1, None).is_err(), "resolving twice");
2938 }
2939
2940 fn shared_account(lamports: u64) -> AccountSharedData {
2941 AccountSharedData::from(solana_account::Account {
2942 lamports,
2943 data: vec![1],
2944 owner: address(9),
2945 executable: false,
2946 rent_epoch: 0,
2947 })
2948 }
2949
2950 fn carry_forward_schedule() -> (Address, Address, OverrideSchedule) {
2951 let (a, b) = (address(1), address(2));
2952 let schedule = OverrideSchedule::from(BTreeMap::from([
2953 (10, HashMap::from([(a, shared_account(1))])),
2954 (
2955 20,
2956 HashMap::from([(a, shared_account(2)), (b, shared_account(9))]),
2957 ),
2958 ]));
2959 (a, b, schedule)
2960 }
2961
2962 #[test]
2963 fn override_schedule_resolves_carry_forward_per_account() {
2964 let (a, b, schedule) = carry_forward_schedule();
2965
2966 assert!(schedule.active_for(&a, 9).is_none());
2967 let cases = [
2968 (9, None, None),
2969 (10, Some(1), None),
2970 (15, Some(1), None),
2971 (20, Some(2), Some(9)),
2972 (u64::MAX, Some(2), Some(9)),
2973 ];
2974 for (slot, want_a, want_b) in cases {
2975 assert_eq!(
2976 schedule
2977 .active_for(&a, slot)
2978 .map(|account| account.lamports()),
2979 want_a,
2980 "active_for a at {slot}"
2981 );
2982 let active = schedule.active_at(slot);
2983 assert_eq!(
2984 active.get(&a).map(ReadableAccount::lamports),
2985 want_a,
2986 "active_at a at {slot}"
2987 );
2988 assert_eq!(
2989 active.get(&b).map(ReadableAccount::lamports),
2990 want_b,
2991 "active_at b at {slot}"
2992 );
2993 }
2994 }
2995
2996 #[test]
2997 fn newly_active_yields_final_values_in_window() {
2998 let (a, b, schedule) = carry_forward_schedule();
2999
3000 let newly: HashMap<_, _> = schedule.newly_active(Some(10), 20).into_iter().collect();
3001 assert_eq!(newly.len(), 2);
3002 assert_eq!(newly[&a].lamports(), 2);
3003 assert_eq!(newly[&b].lamports(), 9);
3004 let spanning: HashMap<_, _> = schedule.newly_active(None, 20).into_iter().collect();
3005 assert_eq!(spanning[&a].lamports(), 2, "later slot wins");
3006 assert_eq!(schedule.newly_active(None, 15).len(), 1);
3007 }
3008
3009 #[test]
3010 fn reroute_filter_round_trips_camel_case() {
3011 let filter = RerouteFilter {
3012 pairs: BTreeSet::from([MintPair::new(address(1), address(2))]),
3013 };
3014 let json = serde_json::to_value(&filter).unwrap();
3015 assert!(
3016 json["pairs"][0][0].is_string() && json["pairs"][0][1].is_string(),
3017 "a pair serializes as two base58 strings: {json}"
3018 );
3019 let back: RerouteFilter = serde_json::from_value(json).unwrap();
3020 assert_eq!(back, filter);
3021 }
3022
3023 #[test]
3026 fn mint_pairs_are_normalized_however_they_are_built() {
3027 let (one, other) = (address(1), address(2));
3028 assert_eq!(MintPair::new(one, other), MintPair::new(other, one));
3029
3030 let reversed: MintPair =
3031 serde_json::from_value(serde_json::json!([other.to_string(), one.to_string()]))
3032 .unwrap();
3033 assert_eq!(reversed, MintPair::new(one, other));
3034 }
3035
3036 #[test]
3037 fn fail_fast_divergence_kind_str_round_trips() {
3038 for kind in [
3039 FailFastDivergenceKind::AnyNonBenign,
3040 FailFastDivergenceKind::Tracked,
3041 ] {
3042 assert_eq!(
3043 FailFastDivergenceKind::from_str_opt(kind.as_str()),
3044 Some(kind)
3045 );
3046 }
3047 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
3048 assert_eq!(
3049 FailFastDivergenceKind::default(),
3050 FailFastDivergenceKind::AnyNonBenign
3051 );
3052 }
3053
3054 #[test]
3055 fn bundle_not_found_is_terminal_and_descriptive() {
3056 let error = BacktestError::BundleNotFound {
3057 start_slot: 100,
3058 end_slot: 600,
3059 };
3060
3061 assert!(error.is_terminal());
3062 assert_eq!(
3063 error.to_string(),
3064 "block bundle not found for start_slot=100, end_slot=600"
3065 );
3066 }
3067
3068 #[test]
3069 fn no_produced_blocks_is_terminal_and_descriptive() {
3070 let error = BacktestError::NoProducedBlocksInRange {
3071 start_slot: 100,
3072 end_slot: 100,
3073 };
3074
3075 assert!(error.is_terminal());
3076 assert_eq!(
3079 error.to_string(),
3080 "no produced blocks in requested range 100..=100; slot(s) may be skipped on-chain"
3081 );
3082 }
3083
3084 #[test]
3085 fn bundle_build_request_optional_fields_default_to_none() {
3086 let req: BundleBuildRequest =
3087 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
3088 assert_eq!((req.start_slot, req.end_slot), (1, 2));
3089 assert_eq!(req.bundle_size, None);
3090 assert_eq!(req.idempotency_key, None);
3091 }
3092
3093 #[test]
3094 fn bundle_build_request_parses_optional_fields() {
3095 let req: BundleBuildRequest = serde_json::from_str(
3096 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
3097 )
3098 .expect("parse");
3099 assert_eq!(req.bundle_size, Some(500));
3100 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
3101 }
3102
3103 #[test]
3104 fn bundle_build_status_serde_round_trips_with_snake_case() {
3105 let cases = [
3106 (BundleBuildStatus::Completed, "\"completed\""),
3107 (BundleBuildStatus::Failed, "\"failed\""),
3108 (BundleBuildStatus::InProgress, "\"in_progress\""),
3109 (
3110 BundleBuildStatus::NeedsInvestigation,
3111 "\"needs_investigation\"",
3112 ),
3113 ];
3114 for (status, expected) in cases {
3115 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
3116 assert_eq!(
3117 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
3118 status
3119 );
3120 assert_eq!(status.as_str(), expected.trim_matches('"'));
3121 }
3122 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
3123 }
3124
3125 #[test]
3126 fn bundle_build_request_without_kind_deserializes_as_blocks() {
3127 let request: BundleBuildRequest =
3129 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
3130 assert_eq!(request.kind, BundleKind::Blocks);
3131 }
3132
3133 #[test]
3134 fn bundle_kind_round_trips_over_serde_and_as_str() {
3135 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
3136 let json = serde_json::to_string(&kind).unwrap();
3137 assert_eq!(json, format!("\"{}\"", kind.as_str()));
3138 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
3139 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
3142 }
3143 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
3144 assert_eq!(BundleKind::from_str_opt("accountState"), None);
3145 }
3146
3147 #[test]
3148 fn bundle_build_request_carries_kind_through_serde() {
3149 let request = BundleBuildRequest {
3150 start_slot: 100,
3151 end_slot: 200,
3152 kind: BundleKind::AccountState,
3153 ..Default::default()
3154 };
3155 let json = serde_json::to_string(&request).unwrap();
3156 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3157 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
3158 assert_eq!(parsed.kind, BundleKind::AccountState);
3159 }
3160
3161 #[test]
3162 fn bundle_build_status_response_serializes_request_and_status() {
3163 let response = BundleBuildStatusResponse {
3164 request_id: "r".to_string(),
3165 start_slot: 100,
3166 end_slot: 200,
3167 bundle_size: Some(50),
3168 status: BundleBuildStatus::InProgress,
3169 created_at_unix_ms: 0,
3170 kind: BundleKind::Blocks,
3171 eta_secs: Some(600),
3172 };
3173 let json = serde_json::to_value(&response).unwrap();
3174 assert_eq!(json["request_id"].as_str(), Some("r"));
3175 assert_eq!(json["start_slot"].as_u64(), Some(100));
3176 assert_eq!(json["bundle_size"].as_u64(), Some(50));
3177 assert_eq!(json["status"].as_str(), Some("in_progress"));
3178 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
3179 assert_eq!(json["eta_secs"].as_u64(), Some(600));
3180 }
3181
3182 #[test]
3183 fn an_empty_bundles_list_is_distinct_from_an_absent_one() {
3184 let absent: BundleBuildListResponse = serde_json::from_str(r#"{"builds":[]}"#).unwrap();
3186 let empty: BundleBuildListResponse =
3187 serde_json::from_str(r#"{"builds":[],"bundles":[]}"#).unwrap();
3188 assert_eq!(absent.bundles, None);
3189 assert_eq!(empty.bundles, Some(Vec::new()));
3190 }
3191
3192 #[test]
3193 fn completed_bundle_round_trips() {
3194 let bundle = CompletedBundle {
3195 start_slot: 100,
3196 end_slot: 200,
3197 kind: BundleKind::AccountState,
3198 completed_at_unix_ms: Some(1_700_000_000_000),
3199 size_bytes: Some(4_096),
3200 };
3201 let json = serde_json::to_string(&bundle).unwrap();
3202 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3203 assert_eq!(
3204 serde_json::from_str::<CompletedBundle>(&json).unwrap(),
3205 bundle
3206 );
3207
3208 let sparse = r#"{"start_slot":1,"end_slot":2,"kind":"blocks"}"#;
3209 let parsed: CompletedBundle = serde_json::from_str(sparse).unwrap();
3210 assert_eq!(parsed.completed_at_unix_ms, None);
3211 assert_eq!(parsed.size_bytes, None);
3212 }
3213
3214 #[test]
3215 fn available_range_reads_a_server_that_predates_kind_and_scope() {
3216 let json = r#"{
3218 "bundle_start_slot": 100,
3219 "bundle_start_slot_utc": null,
3220 "max_bundle_end_slot": 200,
3221 "max_bundle_end_slot_utc": null,
3222 "max_bundle_size": 101
3223 }"#;
3224 let parsed: AvailableRange = serde_json::from_str(json).unwrap();
3225 assert_eq!(parsed.bundle_start_slot, 100);
3226 assert_eq!(parsed.kind, None);
3227 assert_eq!(parsed.scope, None);
3228 }
3229
3230 #[test]
3231 fn available_range_ignores_fields_it_does_not_know() {
3232 let json = r#"{"bundle_start_slot": 1, "some_future_field": "x"}"#;
3234 assert_eq!(
3235 serde_json::from_str::<AvailableRange>(json)
3236 .unwrap()
3237 .bundle_start_slot,
3238 1
3239 );
3240 }
3241
3242 #[test]
3243 fn available_range_labels_round_trip() {
3244 let range = AvailableRange {
3245 bundle_start_slot: 100,
3246 max_bundle_end_slot: Some(200),
3247 kind: Some(BundleKind::AccountState),
3248 scope: Some(RangeScope::User),
3249 ..Default::default()
3250 };
3251 let json = serde_json::to_string(&range).unwrap();
3252 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3253 assert!(json.contains(r#""scope":"user""#), "{json}");
3254 let parsed: AvailableRange = serde_json::from_str(&json).unwrap();
3255 assert_eq!(parsed.kind, Some(BundleKind::AccountState));
3256 assert_eq!(parsed.scope, Some(RangeScope::User));
3257 }
3258
3259 fn range(start: u64, end: u64) -> AvailableRange {
3260 AvailableRange {
3261 bundle_start_slot: start,
3262 max_bundle_end_slot: Some(end),
3263 ..Default::default()
3264 }
3265 }
3266
3267 #[rstest::rstest]
3271 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
3272 #[case::multi(
3273 vec![range(100, 200), range(201, 300), range(301, 400)],
3274 100, 300, Some(vec![(100, 200), (201, 300)])
3275 )]
3276 #[case::nested(
3279 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
3280 100, 900, Some(vec![(100, 500), (501, 900)])
3281 )]
3282 #[case::prefers_finer_grid(
3286 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
3287 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
3288 )]
3289 #[case::shared_start_prefers_finer(
3293 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
3294 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
3295 )]
3296 #[case::coarse_overlap_prefers_finer_pair(
3300 vec![range(100, 200), range(100, 150), range(151, 160)],
3301 100, 160, Some(vec![(100, 150), (151, 160)])
3302 )]
3303 #[case::falls_back_to_coarse(
3307 vec![range(100, 160), range(100, 120), range(121, 140)],
3308 100, 160, Some(vec![(100, 160)])
3309 )]
3310 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
3312 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
3315 #[case::anchors_then_continues(
3316 vec![range(150, 350), range(351, 600)],
3317 200, 600, Some(vec![(150, 350), (351, 600)])
3318 )]
3319 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
3320 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
3322 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
3324 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
3325 #[case::user_and_global_ranges_not_collapsed(
3330 vec![range(100, 200), range(100, 150), range(151, 200)],
3331 100, 200, Some(vec![(100, 150), (151, 200)])
3332 )]
3333 fn split_range_cases(
3334 #[case] ranges: Vec<AvailableRange>,
3335 #[case] start: u64,
3336 #[case] end: u64,
3337 #[case] expected: Option<Vec<(u64, u64)>>,
3338 ) {
3339 match expected {
3340 Some(expected) => assert_eq!(
3341 split_range(
3342 &ranges,
3343 SlotRange::new_unchecked(start, end),
3344 BundleKind::Blocks
3345 )
3346 .unwrap(),
3347 expected
3348 ),
3349 None => assert!(
3350 split_range(
3351 &ranges,
3352 SlotRange::new_unchecked(start, end),
3353 BundleKind::Blocks
3354 )
3355 .is_err()
3356 ),
3357 }
3358 }
3359
3360 fn account_state_range(start: u64, end: u64) -> AvailableRange {
3361 AvailableRange {
3362 kind: Some(BundleKind::AccountState),
3363 ..range(start, end)
3364 }
3365 }
3366
3367 #[test]
3368 fn a_replay_split_rides_the_recordings_not_a_finer_block_grid() {
3369 let ranges = vec![
3372 range(100, 149),
3373 range(150, 199),
3374 account_state_range(100, 199),
3375 ];
3376 let requested = SlotRange::new_unchecked(100, 199);
3377
3378 assert_eq!(
3379 split_range(&ranges, requested, BundleKind::Blocks).unwrap(),
3380 [(100, 149), (150, 199)]
3381 );
3382 assert_eq!(
3383 split_range(&ranges, requested, BundleKind::AccountState).unwrap(),
3384 [(100, 199)]
3385 );
3386 }
3387
3388 #[test]
3389 fn a_replay_split_tiles_consecutive_recordings() {
3390 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
3391 assert_eq!(
3392 split_range(
3393 &ranges,
3394 SlotRange::new_unchecked(100, 299),
3395 BundleKind::AccountState
3396 )
3397 .unwrap(),
3398 [(100, 199), (200, 299)]
3399 );
3400 }
3401
3402 #[test]
3403 fn a_replay_split_ends_short_of_a_recording() {
3404 let requested = SlotRange::new_unchecked(100, 150);
3407 assert_eq!(
3408 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3409 [(100, 150)]
3410 );
3411 assert_eq!(
3412 split_range(
3413 &[account_state_range(100, 199)],
3414 requested,
3415 BundleKind::AccountState
3416 )
3417 .unwrap(),
3418 [(100, 150)]
3419 );
3420 }
3421
3422 #[test]
3423 fn a_replay_split_ends_short_of_its_last_recording() {
3424 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
3427 assert_eq!(
3428 split_range(
3429 &ranges,
3430 SlotRange::new_unchecked(100, 250),
3431 BundleKind::AccountState
3432 )
3433 .unwrap(),
3434 [(100, 199), (200, 250)]
3435 );
3436 }
3437
3438 #[test]
3439 fn a_replay_split_tiles_a_one_slot_recording() {
3440 let ranges = vec![account_state_range(100, 249), account_state_range(250, 250)];
3443 assert_eq!(
3444 split_range(
3445 &ranges,
3446 SlotRange::new_unchecked(100, 250),
3447 BundleKind::AccountState
3448 )
3449 .unwrap(),
3450 [(100, 249), (250, 250)]
3451 );
3452 }
3453
3454 #[test]
3455 fn a_replay_split_anchors_a_mid_recording_start() {
3456 let requested = SlotRange::new_unchecked(150, 199);
3460 assert_eq!(
3461 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3462 [(100, 199)]
3463 );
3464 assert_eq!(
3465 split_range(
3466 &[account_state_range(100, 199)],
3467 requested,
3468 BundleKind::AccountState
3469 )
3470 .unwrap(),
3471 [(100, 199)]
3472 );
3473 }
3474
3475 #[test]
3476 fn a_replay_split_refuses_a_start_no_recording_covers() {
3477 let error = split_range(
3478 &[account_state_range(200, 299)],
3479 SlotRange::new_unchecked(100, 299),
3480 BundleKind::AccountState,
3481 )
3482 .expect_err("no recording covers 100");
3483 assert!(
3484 error.contains("is not covered by any available account-state bundle"),
3485 "{error}"
3486 );
3487 }
3488
3489 #[test]
3490 fn a_replay_split_ignores_block_ranges() {
3491 let error = split_range(
3492 &[range(100, 199)],
3493 SlotRange::new_unchecked(100, 199),
3494 BundleKind::AccountState,
3495 )
3496 .expect_err("owns no recordings");
3497 assert!(error.contains("account-state bundle"), "{error}");
3498 }
3499
3500 #[test]
3501 fn blocks_mode_admits_ranges_from_a_server_that_predates_kind() {
3502 let unlabelled = range(100, 199);
3505 assert_eq!(unlabelled.kind, None);
3506 assert_eq!(
3507 split_range(
3508 &[unlabelled],
3509 SlotRange::new_unchecked(100, 150),
3510 BundleKind::Blocks
3511 )
3512 .unwrap(),
3513 [(100, 150)]
3514 );
3515 }
3516
3517 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
3520 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
3521 for r in ranges {
3522 if let Some(end) = r.max_bundle_end_slot
3523 && end >= r.bundle_start_slot
3524 {
3525 ends.entry(r.bundle_start_slot).or_default().insert(end);
3526 }
3527 }
3528 ends
3529 }
3530
3531 fn reference_max_split(
3535 ends: &BTreeMap<u64, BTreeSet<u64>>,
3536 cursor: u64,
3537 end: u64,
3538 ) -> Option<Vec<(u64, u64)>> {
3539 ends.get(&cursor)?
3540 .iter()
3541 .filter_map(|&bundle_end| {
3542 if bundle_end >= end {
3543 Some(vec![(cursor, end)])
3544 } else {
3545 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
3546 rest.insert(0, (cursor, bundle_end));
3547 rest
3548 })
3549 }
3550 })
3551 .max_by_key(Vec::len)
3552 }
3553
3554 fn is_valid_split(
3558 split: &[(u64, u64)],
3559 ends: &BTreeMap<u64, BTreeSet<u64>>,
3560 start: u64,
3561 end: u64,
3562 ) -> bool {
3563 split.first().is_some_and(|&(s, _)| s == start)
3564 && split.last().is_some_and(|&(_, e)| e == end)
3565 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
3566 && split.iter().all(|&(s, e)| {
3567 e >= s
3568 && ends
3569 .get(&s)
3570 .and_then(|bundle_ends| bundle_ends.iter().next_back())
3571 .is_some_and(|&max_end| e <= max_end)
3572 })
3573 }
3574
3575 #[test]
3580 fn split_range_matches_reference() {
3581 let mut seed: u64 = 0x9E3779B97F4A7C15;
3582 let mut next = || {
3583 seed = seed
3584 .wrapping_mul(6364136223846793005)
3585 .wrapping_add(1442695040888963407);
3586 seed >> 33
3587 };
3588
3589 for _ in 0..50_000 {
3590 let ranges: Vec<AvailableRange> = (0..next() % 6)
3593 .map(|_| {
3594 let start = next() % 12;
3595 range(start, start + next() % 6) })
3597 .collect();
3598 let start = next() % 12;
3599 let end = start + next() % 6; let got = split_range(
3602 &ranges,
3603 SlotRange::new_unchecked(start, end),
3604 BundleKind::Blocks,
3605 );
3606 let ends = ends_by_start(&ranges);
3607 let anchor = ends
3610 .range(..=start)
3611 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
3612 .map(|(&s, _)| s);
3613 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
3614
3615 let layout: Vec<_> = ranges
3616 .iter()
3617 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
3618 .collect();
3619 match (&got, &reference) {
3620 (Ok(split), Some(best)) => {
3621 assert!(
3622 is_valid_split(split, &ends, anchor.unwrap(), end),
3623 "invalid split {split:?} for {layout:?} [{start},{end}]"
3624 );
3625 assert_eq!(
3626 split.len(),
3627 best.len(),
3628 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
3629 );
3630 }
3631 (Err(_), None) => {}
3632 _ => panic!(
3633 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
3634 ),
3635 }
3636 }
3637 }
3638
3639 #[test]
3640 fn a_reroute_without_templates_omits_direct_fill() {
3641 let json = serde_json::to_value(RerouteParams::default()).expect("serialize");
3642 assert!(json.get("directFill").is_none(), "{json}");
3643 }
3644
3645 #[test]
3646 fn extra_markets_cross_the_wire_as_base58() {
3647 let json = serde_json::to_value(RerouteParams::default()).expect("serialize");
3648 assert!(json.get("extraMarkets").is_none(), "{json}");
3649
3650 let reroute = RerouteParams {
3651 extra_markets: BTreeSet::from([address(7)]),
3652 ..Default::default()
3653 };
3654 let json = serde_json::to_value(&reroute).expect("serialize");
3655 assert_eq!(
3656 json["extraMarkets"],
3657 serde_json::json!([address(7).to_string()])
3658 );
3659 assert_eq!(
3660 serde_json::from_value::<RerouteParams>(json).expect("deserialize"),
3661 reroute
3662 );
3663
3664 serde_json::from_value::<RerouteParams>(serde_json::json!({
3665 "extraMarkets": ["not-an-address"]
3666 }))
3667 .expect_err("an invalid address is rejected");
3668 }
3669
3670 fn template(label: &str) -> DirectFillTemplate {
3672 let placeholder = Address::default();
3673 DirectFillTemplate {
3674 label: label.to_owned(),
3675 mode: SwapMode::ExactIn,
3676 input_mint: mints::SOL,
3677 output_mint: mints::USDC,
3678 instruction: TemplateInstruction {
3679 program_id: address(7),
3680 accounts: vec![
3681 TemplateAccount {
3682 address: placeholder,
3683 writable: true,
3684 signer: true,
3685 },
3686 TemplateAccount {
3687 address: placeholder,
3688 writable: true,
3689 signer: false,
3690 },
3691 TemplateAccount {
3692 address: placeholder,
3693 writable: true,
3694 signer: false,
3695 },
3696 TemplateAccount {
3697 address: address(8),
3698 writable: false,
3699 signer: false,
3700 },
3701 ],
3702 data: vec![0; 16],
3703 },
3704 amount_offset: 2,
3705 taker: TakerSlots {
3706 user: vec![0],
3707 input_token_account: vec![1],
3708 output_token_account: vec![2],
3709 },
3710 }
3711 }
3712
3713 #[rstest]
3715 #[case::requote_alone(true, 0, false)]
3716 #[case::templates_alone(false, 1, false)]
3717 #[case::both(true, 2, false)]
3718 #[case::neither(false, 0, true)]
3719 fn a_reroute_is_a_noop_only_with_nothing_to_ask(
3720 #[case] requote: bool,
3721 #[case] templates: usize,
3722 #[case] noop: bool,
3723 ) {
3724 let reroute = RerouteParams {
3725 requote,
3726 direct_fill: (0..templates).map(|i| template(&format!("t{i}"))).collect(),
3727 ..Default::default()
3728 };
3729 assert_eq!(reroute.is_noop(), noop);
3730 }
3731
3732 #[rstest]
3734 #[case::templates_without_requote(false, 1, true)]
3735 #[case::requote_without_templates(true, 0, true)]
3736 #[case::nothing(false, 0, false)]
3737 fn the_account_universe_warms_for_templates_as_for_requoting(
3738 #[case] requote: bool,
3739 #[case] templates: usize,
3740 #[case] warms: bool,
3741 ) {
3742 let params = CreateSessionParams::builder()
3743 .start_slot(1)
3744 .end_slot(2)
3745 .reroute(RerouteParams {
3746 requote,
3747 direct_fill: (0..templates).map(|i| template(&format!("t{i}"))).collect(),
3748 ..Default::default()
3749 })
3750 .build();
3751 assert_eq!(params.warms_account_universe(), warms);
3752 }
3753}