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}
1634
1635#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1638#[serde(rename_all = "camelCase")]
1639pub struct DirectFillTemplateReport {
1640 pub matched: u64,
1641 pub outcomes: BTreeMap<String, u64>,
1642 pub scored: u64,
1643 pub bps_total: i64,
1644}
1645
1646#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1648#[serde(rename_all = "camelCase")]
1649pub struct SessionSummary {
1650 pub correct_simulation: usize,
1653 pub incorrect_simulation: usize,
1656 pub execution_errors: usize,
1658 pub balance_diff: usize,
1660 pub log_diff: usize,
1662 #[serde(default)]
1665 pub unsanitizable: usize,
1666 #[serde(default, skip_serializing_if = "Option::is_none")]
1669 pub reroute_stats: Option<Box<RerouteStatsReport>>,
1670}
1671
1672impl SessionSummary {
1673 pub fn has_deviations(&self) -> bool {
1675 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
1676 }
1677
1678 pub fn total_transactions(&self) -> usize {
1680 self.correct_simulation
1681 + self.incorrect_simulation
1682 + self.execution_errors
1683 + self.balance_diff
1684 + self.log_diff
1685 }
1686
1687 pub fn deviation_details(&self, balance_label: &str, log_label: &str) -> String {
1692 format!(
1693 "{} incorrect simulations, {} execution errors, {} {balance_label}, {} {log_label}",
1694 self.incorrect_simulation, self.execution_errors, self.balance_diff, self.log_diff,
1695 )
1696 }
1697}
1698
1699impl std::fmt::Display for SessionSummary {
1700 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1701 let total = self.total_transactions();
1702 write!(
1703 f,
1704 "Session summary: {total} transactions\n\
1705 \x20 - {} correct simulation\n\
1706 \x20 - {} incorrect simulation\n\
1707 \x20 - {} execution errors\n\
1708 \x20 - {} unsanitizable\n\
1709 \x20 - {} balance diffs\n\
1710 \x20 - {} log diffs",
1711 self.correct_simulation,
1712 self.incorrect_simulation,
1713 self.execution_errors,
1714 self.unsanitizable,
1715 self.balance_diff,
1716 self.log_diff,
1717 )
1718 }
1719}
1720
1721#[derive(Debug, Clone, Serialize, Deserialize)]
1723#[serde(rename_all = "camelCase")]
1724pub enum BacktestError {
1725 InvalidTransactionEncoding {
1726 index: usize,
1727 error: String,
1728 },
1729 InvalidTransactionFormat {
1730 index: usize,
1731 error: String,
1732 },
1733 InvalidAccountEncoding {
1734 address: String,
1735 encoding: BinaryEncoding,
1736 error: String,
1737 },
1738 InvalidAccountOwner {
1739 address: String,
1740 error: String,
1741 },
1742 InvalidAccountPubkey {
1743 address: String,
1744 error: String,
1745 },
1746 NoMoreBlocks,
1747 BundleNotFound {
1748 start_slot: u64,
1749 end_slot: u64,
1750 },
1751 AdvanceSlotFailed {
1752 slot: u64,
1753 error: String,
1754 },
1755 FinalizeSlotFailed {
1756 slot: u64,
1757 error: String,
1758 },
1759 InvalidRequest {
1760 error: String,
1761 },
1762 Internal {
1763 error: String,
1764 },
1765 InvalidBlockhashFormat {
1766 slot: u64,
1767 error: String,
1768 },
1769 InitializingSysvarsFailed {
1770 slot: u64,
1771 error: String,
1772 },
1773 ClerkError {
1774 error: String,
1775 },
1776 SimulationError {
1777 error: String,
1778 },
1779 SessionNotFound {
1780 session_id: String,
1781 },
1782 SessionOwnerMismatch,
1783 SessionOwnershipBusy {
1788 reason: String,
1789 },
1790 NoProducedBlocksInRange {
1796 start_slot: u64,
1797 end_slot: u64,
1798 },
1799}
1800
1801impl BacktestError {
1802 pub fn is_terminal(&self) -> bool {
1806 matches!(
1807 self,
1808 BacktestError::NoMoreBlocks
1809 | BacktestError::BundleNotFound { .. }
1810 | BacktestError::AdvanceSlotFailed { .. }
1811 | BacktestError::FinalizeSlotFailed { .. }
1812 | BacktestError::Internal { .. }
1813 | BacktestError::NoProducedBlocksInRange { .. }
1814 )
1815 }
1816}
1817
1818#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1820pub struct AvailableRange {
1821 pub bundle_start_slot: u64,
1822 pub bundle_start_slot_utc: Option<String>,
1823 pub max_bundle_end_slot: Option<u64>,
1824 pub max_bundle_end_slot_utc: Option<String>,
1825 pub max_bundle_size: Option<u64>,
1826 #[serde(default)]
1829 pub kind: Option<BundleKind>,
1830 #[serde(default)]
1833 pub scope: Option<RangeScope>,
1834}
1835
1836#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1838#[serde(rename_all = "snake_case")]
1839pub enum RangeScope {
1840 Global,
1842 User,
1844}
1845
1846impl RangeScope {
1847 pub fn as_str(self) -> &'static str {
1849 match self {
1850 Self::Global => "global",
1851 Self::User => "user",
1852 }
1853 }
1854}
1855
1856#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1858#[serde(rename_all = "snake_case")]
1859pub enum BundleKind {
1860 #[default]
1862 Blocks,
1863 AccountState,
1867}
1868
1869pub const REPLAY_RANGE_ADVICE: &str = "replayed state is resolved by the recording that begins \
1873 where the request does, so request a range that begins where one was recorded and ends \
1874 within it";
1875
1876impl BundleKind {
1877 fn coverage_noun(self) -> &'static str {
1879 match self {
1880 Self::Blocks => "bundle range",
1881 Self::AccountState => "account-state bundle",
1882 }
1883 }
1884
1885 fn coverage_nouns(self) -> &'static str {
1887 match self {
1888 Self::Blocks => "bundle ranges",
1889 Self::AccountState => "account-state bundles",
1890 }
1891 }
1892
1893 pub fn as_str(self) -> &'static str {
1895 match self {
1896 Self::Blocks => "blocks",
1897 Self::AccountState => "account_state",
1898 }
1899 }
1900
1901 pub fn from_str_opt(value: &str) -> Option<Self> {
1903 match value {
1904 "blocks" => Some(Self::Blocks),
1905 "account_state" => Some(Self::AccountState),
1906 _ => None,
1907 }
1908 }
1909}
1910
1911impl fmt::Display for BundleKind {
1914 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1915 let label = match self {
1916 Self::Blocks => "transactions",
1917 Self::AccountState => "account_state",
1918 };
1919 f.pad(label)
1920 }
1921}
1922
1923#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1928#[serde(rename_all = "snake_case")]
1929pub enum JobKind {
1930 #[default]
1932 BundleBuild,
1933 Depth,
1935}
1936
1937impl JobKind {
1938 pub fn as_str(self) -> &'static str {
1940 match self {
1941 Self::BundleBuild => "bundle_build",
1942 Self::Depth => "depth",
1943 }
1944 }
1945
1946 pub fn from_str_opt(value: &str) -> Option<Self> {
1948 match value {
1949 "bundle_build" => Some(Self::BundleBuild),
1950 "depth" => Some(Self::Depth),
1951 _ => None,
1952 }
1953 }
1954}
1955
1956#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1961#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
1962#[serde(rename_all = "snake_case")]
1963pub enum DepthSource {
1964 #[default]
1966 Onchain,
1967 Router,
1969}
1970
1971impl DepthSource {
1972 pub const fn as_str(self) -> &'static str {
1974 match self {
1975 Self::Onchain => "onchain",
1976 Self::Router => "router",
1977 }
1978 }
1979
1980 pub fn from_str_opt(value: &str) -> Option<Self> {
1982 match value {
1983 "onchain" => Some(Self::Onchain),
1984 "router" => Some(Self::Router),
1985 _ => None,
1986 }
1987 }
1988
1989 pub const fn discriminant(self) -> u8 {
1994 match self {
1995 Self::Onchain => 0,
1996 Self::Router => 1,
1997 }
1998 }
1999}
2000
2001impl fmt::Display for DepthSource {
2002 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2003 f.write_str(self.as_str())
2004 }
2005}
2006
2007#[derive(Debug, Clone, Serialize, Deserialize)]
2012pub struct DepthCoverageResponse {
2013 pub sources: Vec<DepthSourceCoverage>,
2014}
2015
2016#[derive(Debug, Clone, Serialize, Deserialize)]
2017pub struct DepthSourceCoverage {
2018 pub source: DepthSource,
2020 pub revision: u16,
2023 pub markets: Vec<String>,
2025 pub venues: Vec<String>,
2027 pub size_rungs: usize,
2029 pub completed_ranges: Vec<SlotRange>,
2032}
2033
2034#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2036pub struct DepthRequest {
2037 pub start_slot: u64,
2040 pub end_slot: u64,
2042 #[serde(default)]
2045 pub source: DepthSource,
2046 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2048 pub markets: Vec<String>,
2049}
2050
2051#[derive(Debug, Clone, Serialize, Deserialize)]
2053pub struct DepthStatusResponse {
2054 pub request_id: String,
2055 pub start_slot: u64,
2056 pub end_slot: u64,
2057 pub source: DepthSource,
2058 pub status: BundleBuildStatus,
2059}
2060
2061#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2065pub struct BundleBuildRequest {
2066 pub start_slot: u64,
2067 pub end_slot: u64,
2068 #[serde(default)]
2069 pub bundle_size: Option<u64>,
2070 #[serde(default)]
2073 pub idempotency_key: Option<String>,
2074 #[serde(default)]
2076 pub kind: BundleKind,
2077}
2078
2079#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2080#[serde(rename_all = "snake_case")]
2081pub enum BundleBuildStatus {
2082 Completed,
2084 Failed,
2086 InProgress,
2088 NeedsInvestigation,
2091}
2092
2093impl BundleBuildStatus {
2094 pub fn as_str(self) -> &'static str {
2096 match self {
2097 Self::Completed => "completed",
2098 Self::Failed => "failed",
2099 Self::InProgress => "in_progress",
2100 Self::NeedsInvestigation => "needs_investigation",
2101 }
2102 }
2103}
2104
2105#[derive(Debug, Clone, Serialize, Deserialize)]
2107pub struct BundleBuildStatusResponse {
2108 pub request_id: String,
2109 pub start_slot: u64,
2110 pub end_slot: u64,
2111 pub bundle_size: Option<u64>,
2112 pub status: BundleBuildStatus,
2113 pub created_at_unix_ms: u64,
2116 #[serde(default)]
2118 pub kind: BundleKind,
2119 #[serde(default)]
2122 pub eta_secs: Option<u64>,
2123}
2124
2125#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2127pub struct CompletedBundle {
2128 pub start_slot: u64,
2129 pub end_slot: u64,
2130 pub kind: BundleKind,
2131 #[serde(default)]
2133 pub completed_at_unix_ms: Option<i64>,
2134 #[serde(default)]
2135 pub size_bytes: Option<i64>,
2136}
2137
2138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2140pub struct BundleDeleteRequest {
2141 pub start_slot: u64,
2142 pub end_slot: u64,
2143 #[serde(default)]
2145 pub kind: BundleKind,
2146}
2147
2148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2150pub struct BundleDeleteResponse {
2151 pub start_slot: u64,
2152 pub end_slot: u64,
2153 pub kind: BundleKind,
2154}
2155
2156#[derive(Debug, Clone, Serialize, Deserialize)]
2158pub struct BundleBuildListResponse {
2159 pub builds: Vec<BundleBuildStatusResponse>,
2162 #[serde(default)]
2167 pub bundles: Option<Vec<CompletedBundle>>,
2168}
2169
2170#[derive(Debug, Clone, Serialize, Deserialize)]
2173pub struct SnapshotSlotResponse {
2174 pub snapshot_slot: u64,
2175}
2176
2177pub fn split_range(
2200 ranges: &[AvailableRange],
2201 requested: SlotRange,
2202 kind: BundleKind,
2203) -> Result<Vec<(u64, u64)>, String> {
2204 let (requested_start, requested_end) = (requested.start(), requested.end());
2205
2206 let ends_by_start = ranges
2213 .iter()
2214 .filter(|r| r.kind.unwrap_or_default() == kind)
2215 .filter_map(|r| {
2216 r.max_bundle_end_slot
2217 .filter(|&end| end >= r.bundle_start_slot)
2218 .map(|end| (r.bundle_start_slot, end))
2219 })
2220 .fold(
2221 BTreeMap::<u64, BTreeSet<u64>>::new(),
2222 |mut ends_by_start, (start, end)| {
2223 ends_by_start.entry(start).or_default().insert(end);
2224 ends_by_start
2225 },
2226 );
2227
2228 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
2236 ends.iter()
2237 .next_back()
2238 .is_some_and(|&end| end >= requested_start)
2239 }) else {
2240 return Err(format!(
2241 "start_slot {requested_start} is not covered by any available {}",
2242 kind.coverage_noun()
2243 ));
2244 };
2245
2246 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
2252 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
2253 let mut best: Option<Vec<(u64, u64)>> = None;
2254 for &end in ends {
2255 let candidate = if end >= requested_end {
2256 Some(vec![(start, requested_end)])
2257 } else {
2258 best_from.get(&(end + 1)).map(|rest| {
2259 std::iter::once((start, end))
2260 .chain(rest.iter().copied())
2261 .collect()
2262 })
2263 };
2264 if let Some(candidate) = candidate
2265 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
2266 {
2267 best = Some(candidate);
2268 }
2269 }
2270 if let Some(best) = best {
2271 best_from.insert(start, best);
2272 }
2273 }
2274
2275 best_from.remove(&anchor_start).ok_or_else(|| {
2276 let mut covered_to = anchor_start.saturating_sub(1);
2280 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
2281 if start > covered_to.saturating_add(1) {
2282 break;
2283 }
2284 if let Some(&end) = ends.iter().next_back() {
2285 covered_to = covered_to.max(end);
2286 }
2287 }
2288 if covered_to < requested_end {
2289 format!("gap in coverage at slot {}", covered_to + 1)
2290 } else {
2291 format!(
2292 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available {}",
2293 kind.coverage_nouns()
2294 )
2295 }
2296 })
2297}
2298
2299impl From<BacktestError> for BacktestResponse {
2300 fn from(error: BacktestError) -> Self {
2301 Self::Error(error)
2302 }
2303}
2304
2305impl std::error::Error for BacktestError {}
2306
2307impl fmt::Display for BacktestError {
2308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2309 match self {
2310 BacktestError::InvalidTransactionEncoding { index, error } => {
2311 write!(f, "invalid transaction encoding at index {index}: {error}")
2312 }
2313 BacktestError::InvalidTransactionFormat { index, error } => {
2314 write!(f, "invalid transaction format at index {index}: {error}")
2315 }
2316 BacktestError::InvalidAccountEncoding {
2317 address,
2318 encoding,
2319 error,
2320 } => write!(
2321 f,
2322 "invalid encoding for account {address} ({encoding:?}): {error}"
2323 ),
2324 BacktestError::InvalidAccountOwner { address, error } => {
2325 write!(f, "invalid owner for account {address}: {error}")
2326 }
2327 BacktestError::InvalidAccountPubkey { address, error } => {
2328 write!(f, "invalid account pubkey {address}: {error}")
2329 }
2330 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
2331 BacktestError::BundleNotFound {
2332 start_slot,
2333 end_slot,
2334 } => write!(
2335 f,
2336 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
2337 ),
2338 BacktestError::AdvanceSlotFailed { slot, error } => {
2339 write!(f, "failed to advance to slot {slot}: {error}")
2340 }
2341 BacktestError::FinalizeSlotFailed { slot, error } => {
2342 write!(f, "failed to finalize slot {slot}: {error}")
2343 }
2344 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
2345 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
2346 BacktestError::InvalidBlockhashFormat { slot, error } => {
2347 write!(f, "invalid blockhash at slot {slot}: {error}")
2348 }
2349 BacktestError::InitializingSysvarsFailed { slot, error } => {
2350 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
2351 }
2352 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
2353 BacktestError::SimulationError { error } => {
2354 write!(f, "simulation error: {error}")
2355 }
2356 BacktestError::SessionNotFound { session_id } => {
2357 write!(f, "session not found: {session_id}")
2358 }
2359 BacktestError::SessionOwnerMismatch => {
2360 write!(f, "session owner mismatch")
2361 }
2362 BacktestError::SessionOwnershipBusy { reason } => {
2363 write!(f, "session ownership busy: {reason}")
2364 }
2365 BacktestError::NoProducedBlocksInRange {
2366 start_slot,
2367 end_slot,
2368 } => write!(
2369 f,
2370 "no produced blocks in requested range {start_slot}..={end_slot}; \
2371 slot(s) may be skipped on-chain"
2372 ),
2373 }
2374 }
2375}
2376
2377#[cfg(test)]
2378mod tests {
2379 use rstest::rstest;
2380 use solana_account::ReadableAccount;
2381
2382 use super::*;
2383
2384 #[test]
2387 fn a_sources_discriminant_is_frozen_independently_of_its_label() {
2388 assert_eq!(DepthSource::Onchain.discriminant(), 0);
2389 assert_eq!(DepthSource::Router.discriminant(), 1);
2390 }
2391
2392 #[test]
2395 fn a_source_spells_itself_the_same_way_three_times() {
2396 for source in [DepthSource::Onchain, DepthSource::Router] {
2397 assert_eq!(
2398 serde_json::to_value(source).expect("a source serialises"),
2399 serde_json::json!(source.as_str())
2400 );
2401 assert_eq!(DepthSource::from_str_opt(source.as_str()), Some(source));
2402 }
2403 }
2404
2405 fn address(byte: u8) -> Address {
2406 Address::from([byte; 32])
2407 }
2408
2409 #[rstest]
2410 #[case::one("jupiter", Some(&[SwapAggregator::Jupiter][..]))]
2411 #[case::several("jupiter,okx", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2412 #[case::spaced_and_uppercased(" Jupiter , OKX ", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2413 #[case::repeats_collapse("okx,okx", Some(&[SwapAggregator::Okx][..]))]
2414 #[case::trailing_separator("okx,", Some(&[SwapAggregator::Okx][..]))]
2415 #[case::unknown("jupiter,bogus", None)]
2416 #[case::empty("", None)]
2417 #[case::separators_only(",,", None)]
2418 fn reroute_aggregators_parse_the_shapes_a_flag_carries(
2419 #[case] input: &str,
2420 #[case] expected: Option<&[SwapAggregator]>,
2421 ) {
2422 assert_eq!(
2423 input.parse::<RerouteAggregators>().ok(),
2424 expected.map(|aggregators| RerouteAggregators::new(aggregators.iter().copied()))
2425 );
2426 }
2427
2428 #[test]
2430 fn reroute_aggregators_round_trip_through_their_rendering() {
2431 let aggregators = RerouteAggregators::new([SwapAggregator::Dflow, SwapAggregator::Jupiter]);
2432
2433 assert_eq!(aggregators.to_string(), "jupiter,dflow");
2434 assert_eq!(
2435 aggregators
2436 .to_string()
2437 .parse::<RerouteAggregators>()
2438 .unwrap(),
2439 aggregators
2440 );
2441 assert_eq!(RerouteAggregators::default().to_string(), "jupiter");
2442 }
2443
2444 #[test]
2447 fn aggregators_deserialize_through_the_same_parser_the_flag_uses() {
2448 let params: CreateSessionParams = serde_json::from_value(
2449 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {"aggregators": ["Jupiter", "OKX"]}}),
2450 )
2451 .expect("decode");
2452
2453 assert_eq!(
2454 params.reroute.expect("rerouting requested").aggregators,
2455 RerouteAggregators::new([SwapAggregator::Jupiter, SwapAggregator::Okx])
2456 );
2457 }
2458
2459 #[test]
2462 fn params_without_the_new_reroute_fields_decode_to_the_narrow_defaults() {
2463 let params: CreateSessionParams = serde_json::from_value(
2464 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {}}),
2465 )
2466 .expect("decode");
2467
2468 let reroute = params.reroute.expect("rerouting requested");
2469 assert_eq!(reroute.aggregators, RerouteAggregators::default());
2470 assert!(reroute.aggregators.contains(SwapAggregator::Jupiter));
2471 assert!(!reroute.aggregators.contains(SwapAggregator::Okx));
2472 assert!(!reroute.circular_arbs);
2473 assert!(!reroute.detect_failed_l1_swaps);
2474 }
2475
2476 #[test]
2478 fn a_request_without_reroute_reserves_no_sidecar() {
2479 let params: CreateSessionParams =
2480 serde_json::from_value(serde_json::json!({"startSlot": 1, "endSlot": 2}))
2481 .expect("decode");
2482
2483 assert!(!params.requires_sidecar());
2484 }
2485
2486 #[test]
2491 fn a_per_slot_action_schedule_compresses_across_actions() {
2492 let slots = 2_000u64;
2493 let actions = (0..slots)
2494 .map(|slot| ScheduledAction {
2495 anchor: ActionAnchor::AfterSlot { slots: vec![slot] },
2496 kind: ActionKind::Simulate,
2497 feeds_reroute: false,
2498 transactions: Vec::new(),
2499 account_overrides: AccountModifications(BTreeMap::from([(
2500 address(7),
2501 AccountData {
2502 data: EncodedBinary::new(
2504 BASE64.encode(
2505 [&slot.to_le_bytes()[..], &[0xAB; 2040][..]]
2506 .concat()
2507 .as_slice(),
2508 ),
2509 BinaryEncoding::Base64,
2510 ),
2511 executable: false,
2512 lamports: 15_144_966,
2513 owner: address(9),
2514 space: 2048,
2515 },
2516 )])),
2517 return_accounts: Vec::new(),
2518 label: None,
2519 })
2520 .collect::<Vec<_>>();
2521 let params = CreateSessionParams::builder()
2522 .start_slot(1)
2523 .end_slot(slots)
2524 .actions(actions.clone())
2525 .build();
2526
2527 let encoded = serde_json::to_string(¶ms).expect("serialize");
2528 let raw = serde_json::to_string(&actions).expect("serialize raw actions");
2529 assert!(
2530 encoded.len() * 8 < raw.len(),
2531 "expected the schedule to compress well; got {} bytes from {} raw",
2532 encoded.len(),
2533 raw.len()
2534 );
2535
2536 let decoded: CreateSessionParams = serde_json::from_str(&encoded).expect("round trip");
2537 assert_eq!(
2538 serde_json::to_string(&decoded.actions).expect("serialize decoded"),
2539 raw
2540 );
2541 }
2542
2543 #[test]
2546 fn an_empty_action_list_is_omitted_entirely() {
2547 let params = CreateSessionParams::builder()
2548 .start_slot(1)
2549 .end_slot(2)
2550 .build();
2551 let json = serde_json::to_value(¶ms).expect("serialize");
2552 assert!(json.get("actions").is_none(), "{json}");
2553 let decoded: CreateSessionParams = serde_json::from_value(json).expect("round trip");
2554 assert!(decoded.actions.is_empty());
2555 }
2556
2557 #[test]
2560 fn send_actions_conflict_with_replay_only_together() {
2561 let params = |replay, kind| {
2562 CreateSessionParams::builder()
2563 .start_slot(1)
2564 .end_slot(2)
2565 .replay_account_state(replay)
2566 .actions(vec![ScheduledAction {
2567 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2568 kind,
2569 feeds_reroute: false,
2570 transactions: vec!["dGVzdA==".to_string()],
2571 account_overrides: Default::default(),
2572 return_accounts: Default::default(),
2573 label: None,
2574 }])
2575 .build()
2576 };
2577 assert!(replay_support::send_action_conflict(¶ms(false, ActionKind::Send)).is_none());
2578 assert!(
2579 replay_support::send_action_conflict(¶ms(true, ActionKind::Simulate)).is_none()
2580 );
2581 let mut publishing = params(true, ActionKind::Simulate);
2582 publishing.actions[0].feeds_reroute = true;
2583 assert!(replay_support::send_action_conflict(&publishing).is_none());
2584
2585 let conflict = replay_support::send_action_conflict(¶ms(true, ActionKind::Send))
2586 .expect("send under replay conflicts");
2587 assert!(conflict.contains("kind: send"), "{conflict}");
2588 assert!(conflict.contains(replay_support::SEND_ACTION), "{conflict}");
2589 }
2590
2591 fn override_entry(owner: Address, executable: bool) -> AccountData {
2592 AccountData {
2593 data: EncodedBinary::from_bytes(&[1, 2, 3], BinaryEncoding::Base64),
2594 executable,
2595 lamports: 1,
2596 owner,
2597 space: 3,
2598 }
2599 }
2600
2601 fn before() -> ActionAnchor {
2602 ActionAnchor::BeforeSlot { slots: vec![1] }
2603 }
2604
2605 fn valid() -> ScheduledAction {
2606 override_action(before(), address(3), override_entry(address(4), false))
2607 }
2608
2609 fn override_action(
2610 anchor: ActionAnchor,
2611 account: Address,
2612 data: AccountData,
2613 ) -> ScheduledAction {
2614 ScheduledAction {
2615 anchor,
2616 kind: ActionKind::default(),
2617 transactions: Vec::new(),
2618 account_overrides: AccountModifications(BTreeMap::from([(account, data)])),
2619 return_accounts: Vec::new(),
2620 label: None,
2621 feeds_reroute: true,
2622 }
2623 }
2624
2625 #[test]
2626 fn override_action_round_trips() {
2627 let key = address(1);
2628 let entry = override_entry(address(2), false);
2629 let action = override_action(
2630 ActionAnchor::BeforeSlot { slots: vec![5, 9] },
2631 key,
2632 entry.clone(),
2633 );
2634 let json = serde_json::to_value(&action).unwrap();
2635 assert!(
2636 json["anchor"]["slots"].is_string(),
2637 "slots travel as a compressed blob: {json}"
2638 );
2639 let back: ScheduledAction = serde_json::from_value(json).unwrap();
2640 assert_eq!(back.anchor, ActionAnchor::BeforeSlot { slots: vec![5, 9] });
2641 assert!(back.feeds_reroute);
2642 assert!(!back.is_computed_override(), "no transactions to simulate");
2643 let survived = &back.account_overrides.0[&key];
2644 assert_eq!(survived.lamports, entry.lamports);
2645 assert_eq!(survived.data.data, entry.data.data);
2646 }
2647
2648 #[test]
2649 fn an_every_slot_anchor_round_trips_and_defaults_to_every_slot() {
2650 let anchor = ActionAnchor::AfterEverySlot {
2651 every_n_slots: NonZeroU64::new(10).unwrap(),
2652 };
2653 let json = serde_json::to_value(&anchor).unwrap();
2654 assert_eq!(
2655 json,
2656 serde_json::json!({ "at": "afterEverySlot", "everyNSlots": 10 })
2657 );
2658 assert_eq!(
2659 serde_json::from_value::<ActionAnchor>(json).unwrap(),
2660 anchor
2661 );
2662
2663 assert_eq!(
2664 serde_json::from_value::<ActionAnchor>(serde_json::json!({ "at": "beforeEverySlot" }))
2665 .unwrap(),
2666 ActionAnchor::BeforeEverySlot {
2667 every_n_slots: NonZeroU64::MIN
2668 }
2669 );
2670 assert!(
2671 serde_json::from_value::<ActionAnchor>(
2672 serde_json::json!({ "at": "beforeEverySlot", "everyNSlots": 0 })
2673 )
2674 .is_err(),
2675 "a zero cadence names no slot"
2676 );
2677 }
2678
2679 fn context(
2680 invoked: &[u8],
2681 moves_tokens: bool,
2682 written: &[u8],
2683 writes_via_unresolved_lookup: bool,
2684 ) -> TxMatchContext {
2685 TxMatchContext {
2686 invoked_programs: invoked.iter().copied().map(address).collect(),
2687 moves_tokens,
2688 written_accounts: written.iter().copied().map(address).collect(),
2689 writes_via_unresolved_lookup,
2690 }
2691 }
2692
2693 #[rstest]
2695 #[case::update(&[], context(&[1], false, &[11], false), true)]
2696 #[case::moves_tokens(&[], context(&[1], true, &[11], false), false)]
2697 #[case::second_program_of_the_set(&[], context(&[2], false, &[11], false), true)]
2698 #[case::other_program(&[], context(&[3], false, &[11], false), false)]
2699 #[case::write_to_the_scope(&[10], context(&[1], false, &[11, 10], false), true)]
2700 #[case::write_elsewhere(&[10], context(&[1], false, &[11], false), false)]
2701 #[case::unresolved_write_through_a_lookup_table(&[10], context(&[1], false, &[11], true), true)]
2702 fn an_oracle_update_is_a_token_free_write_by_a_listed_program(
2703 #[case] accounts: &[u8],
2704 #[case] ctx: TxMatchContext,
2705 #[case] expected: bool,
2706 ) {
2707 let filter = DiscoveryFilter::OracleUpdate {
2708 programs: BTreeSet::from([address(1), address(2)]),
2709 accounts: accounts.iter().copied().map(address).collect(),
2710 };
2711 assert_eq!(filter.matches(&ctx), expected);
2712 }
2713
2714 #[rstest]
2715 #[case::unscoped(
2716 &[],
2717 serde_json::json!({ "kind": "oracleUpdate", "value": { "programs": [address(1).to_string()] } })
2718 )]
2719 #[case::scoped(
2720 &[2],
2721 serde_json::json!({
2722 "kind": "oracleUpdate",
2723 "value": { "programs": [address(1).to_string()], "accounts": [address(2).to_string()] }
2724 })
2725 )]
2726 fn an_oracle_update_filter_round_trips_as_kind_and_value(
2727 #[case] accounts: &[u8],
2728 #[case] expected: serde_json::Value,
2729 ) {
2730 let filter = DiscoveryFilter::OracleUpdate {
2731 programs: BTreeSet::from([address(1)]),
2732 accounts: accounts.iter().copied().map(address).collect(),
2733 };
2734 let json = serde_json::to_value(&filter).unwrap();
2735
2736 assert_eq!(json, expected);
2737 assert_eq!(
2738 serde_json::from_value::<DiscoveryFilter>(json).unwrap(),
2739 filter
2740 );
2741 }
2742
2743 #[test]
2748 fn a_token_account_override_is_accepted() {
2749 let action = override_action(
2750 before(),
2751 address(3),
2752 override_entry(spl_token_interface::ID, false),
2753 );
2754 let schedule = decode_override_actions(&[action]).expect("token overrides decode");
2755 assert!(!schedule.active_at(1).is_empty());
2756 }
2757
2758 #[rstest]
2759 #[case::executable(override_action(before(), address(3), override_entry(address(10), true)))]
2760 #[case::program_owned(override_action(
2761 before(),
2762 address(3),
2763 override_entry(solana_account::PROGRAM_OWNERS[0], false),
2764 ))]
2765 #[case::after_slot_anchor(override_action(
2766 ActionAnchor::AfterSlot { slots: vec![1] },
2767 address(3),
2768 override_entry(address(4), false),
2769 ))]
2770 #[case::every_slot_anchor(override_action(
2771 ActionAnchor::BeforeEverySlot { every_n_slots: NonZeroU64::MIN },
2772 address(3),
2773 override_entry(address(4), false),
2774 ))]
2775 #[case::nothing_to_post(ScheduledAction {
2776 account_overrides: AccountModifications::default(),
2777 ..valid()
2778 })]
2779 #[case::send(ScheduledAction {
2781 kind: ActionKind::Send,
2782 ..valid()
2783 })]
2784 #[allow(deprecated, reason = "exercise the loader-owner rejection path")]
2785 fn override_actions_reject_invalid_accounts_and_shapes(#[case] action: ScheduledAction) {
2786 assert!(decode_override_actions(&[action]).is_err());
2787 }
2788
2789 fn diagnostic_probe() -> DiagnosticProbeParams {
2790 DiagnosticProbeParams {
2791 anchor: ActionAnchor::AfterEverySlot {
2792 every_n_slots: NonZeroU64::MIN,
2793 },
2794 markets: vec![address(1), address(2)],
2795 usd_values: vec![100, 25_000],
2796 label: Some("depth-0".to_owned()),
2797 }
2798 }
2799
2800 #[rstest]
2801 #[case::no_markets(DiagnosticProbeParams { markets: Vec::new(), ..diagnostic_probe() })]
2802 #[case::no_usd_values(DiagnosticProbeParams { usd_values: Vec::new(), ..diagnostic_probe() })]
2803 #[case::too_many_markets(DiagnosticProbeParams {
2804 markets: (0..=MAX_DIAGNOSTIC_MARKETS as u8).map(address).collect(),
2805 ..diagnostic_probe()
2806 })]
2807 #[case::too_many_usd_values(DiagnosticProbeParams {
2808 usd_values: (1..=MAX_DIAGNOSTIC_USD_VALUES as u32 + 1).collect(),
2809 ..diagnostic_probe()
2810 })]
2811 #[case::zero_usd_value(DiagnosticProbeParams { usd_values: vec![0, 100], ..diagnostic_probe() })]
2812 #[case::duplicate_market(DiagnosticProbeParams {
2813 markets: vec![address(1), address(1)],
2814 ..diagnostic_probe()
2815 })]
2816 #[case::before_slot(DiagnosticProbeParams {
2817 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2818 ..diagnostic_probe()
2819 })]
2820 #[case::before_every_slot(DiagnosticProbeParams {
2821 anchor: ActionAnchor::BeforeEverySlot { every_n_slots: NonZeroU64::MIN },
2822 ..diagnostic_probe()
2823 })]
2824 fn diagnostic_probes_reject_invalid_shapes(#[case] probe: DiagnosticProbeParams) {
2825 assert!(probe.validate().is_err());
2826 }
2827
2828 #[test]
2829 fn diagnostic_probes_accept_both_supported_anchors() {
2830 assert!(diagnostic_probe().validate().is_ok());
2831 assert!(
2832 DiagnosticProbeParams {
2833 anchor: ActionAnchor::AfterSlot { slots: vec![1, 2] },
2834 ..diagnostic_probe()
2835 }
2836 .validate()
2837 .is_ok()
2838 );
2839 }
2840
2841 #[test]
2843 fn override_actions_accept_a_static_and_a_computed_shape() {
2844 assert!(decode_override_actions(&[valid()]).is_ok());
2845 assert!(
2848 decode_override_actions(&[ScheduledAction {
2849 transactions: vec!["dGVzdA==".to_string()],
2850 ..valid()
2851 }])
2852 .is_ok()
2853 );
2854 }
2855
2856 #[test]
2857 fn override_actions_merge_account_wise_later_wins() {
2858 let entry = |lamports| AccountData {
2859 lamports,
2860 ..override_entry(address(9), false)
2861 };
2862 let slot_action = |slots, account, lamports| {
2863 override_action(ActionAnchor::BeforeSlot { slots }, account, entry(lamports))
2864 };
2865 let actions = [
2866 slot_action(vec![5, 7], address(1), 1),
2867 slot_action(vec![5], address(2), 2),
2868 slot_action(vec![5], address(1), 3),
2869 ScheduledAction {
2870 anchor: ActionAnchor::AfterSlot { slots: vec![5] },
2871 kind: ActionKind::Simulate,
2872 feeds_reroute: false,
2873 transactions: vec!["dGVzdA==".to_string()],
2874 account_overrides: AccountModifications::default(),
2875 return_accounts: Vec::new(),
2876 label: None,
2877 },
2878 ];
2879 let schedule = decode_override_actions(&actions).unwrap();
2880 assert_eq!(schedule.slots().collect::<Vec<_>>(), vec![5, 7]);
2881 let lamports_at = |key, slot| {
2882 schedule
2883 .active_for(&key, slot)
2884 .map(|account| account.lamports())
2885 };
2886 assert_eq!(lamports_at(address(1), 5), Some(3));
2887 assert_eq!(lamports_at(address(2), 5), Some(2));
2888 assert_eq!(lamports_at(address(1), 7), Some(1));
2889 }
2890
2891 #[test]
2892 fn computed_override_resolution_preserves_action_order_and_failure() {
2893 let key = address(1);
2894 let static_action = |lamports| {
2895 override_action(
2896 ActionAnchor::BeforeSlot { slots: vec![5] },
2897 key,
2898 AccountData {
2899 lamports,
2900 ..override_entry(address(9), false)
2901 },
2902 )
2903 };
2904 let computed_action = || ScheduledAction {
2905 transactions: vec!["dGVzdA==".to_string()],
2906 ..static_action(2)
2907 };
2908
2909 let computed_last =
2910 decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2911 computed_last
2912 .resolve(5, 1, Some(HashMap::from([(key, shared_account(2))])))
2913 .unwrap();
2914 assert_eq!(
2915 computed_last.active_for(&key, 5).map(|a| a.lamports()),
2916 Some(2)
2917 );
2918
2919 let static_last = decode_override_actions(&[computed_action(), static_action(3)]).unwrap();
2920 static_last
2921 .resolve(5, 0, Some(HashMap::from([(key, shared_account(2))])))
2922 .unwrap();
2923 assert_eq!(
2924 static_last.active_for(&key, 5).map(|a| a.lamports()),
2925 Some(3)
2926 );
2927
2928 let failed = decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2929 failed.resolve(5, 1, None).unwrap();
2930 assert_eq!(failed.active_for(&key, 5).map(|a| a.lamports()), Some(1));
2931 assert!(failed.resolve(5, 1, None).is_err(), "resolving twice");
2932 }
2933
2934 fn shared_account(lamports: u64) -> AccountSharedData {
2935 AccountSharedData::from(solana_account::Account {
2936 lamports,
2937 data: vec![1],
2938 owner: address(9),
2939 executable: false,
2940 rent_epoch: 0,
2941 })
2942 }
2943
2944 fn carry_forward_schedule() -> (Address, Address, OverrideSchedule) {
2945 let (a, b) = (address(1), address(2));
2946 let schedule = OverrideSchedule::from(BTreeMap::from([
2947 (10, HashMap::from([(a, shared_account(1))])),
2948 (
2949 20,
2950 HashMap::from([(a, shared_account(2)), (b, shared_account(9))]),
2951 ),
2952 ]));
2953 (a, b, schedule)
2954 }
2955
2956 #[test]
2957 fn override_schedule_resolves_carry_forward_per_account() {
2958 let (a, b, schedule) = carry_forward_schedule();
2959
2960 assert!(schedule.active_for(&a, 9).is_none());
2961 let cases = [
2962 (9, None, None),
2963 (10, Some(1), None),
2964 (15, Some(1), None),
2965 (20, Some(2), Some(9)),
2966 (u64::MAX, Some(2), Some(9)),
2967 ];
2968 for (slot, want_a, want_b) in cases {
2969 assert_eq!(
2970 schedule
2971 .active_for(&a, slot)
2972 .map(|account| account.lamports()),
2973 want_a,
2974 "active_for a at {slot}"
2975 );
2976 let active = schedule.active_at(slot);
2977 assert_eq!(
2978 active.get(&a).map(ReadableAccount::lamports),
2979 want_a,
2980 "active_at a at {slot}"
2981 );
2982 assert_eq!(
2983 active.get(&b).map(ReadableAccount::lamports),
2984 want_b,
2985 "active_at b at {slot}"
2986 );
2987 }
2988 }
2989
2990 #[test]
2991 fn newly_active_yields_final_values_in_window() {
2992 let (a, b, schedule) = carry_forward_schedule();
2993
2994 let newly: HashMap<_, _> = schedule.newly_active(Some(10), 20).into_iter().collect();
2995 assert_eq!(newly.len(), 2);
2996 assert_eq!(newly[&a].lamports(), 2);
2997 assert_eq!(newly[&b].lamports(), 9);
2998 let spanning: HashMap<_, _> = schedule.newly_active(None, 20).into_iter().collect();
2999 assert_eq!(spanning[&a].lamports(), 2, "later slot wins");
3000 assert_eq!(schedule.newly_active(None, 15).len(), 1);
3001 }
3002
3003 #[test]
3004 fn reroute_filter_round_trips_camel_case() {
3005 let filter = RerouteFilter {
3006 pairs: BTreeSet::from([MintPair::new(address(1), address(2))]),
3007 };
3008 let json = serde_json::to_value(&filter).unwrap();
3009 assert!(
3010 json["pairs"][0][0].is_string() && json["pairs"][0][1].is_string(),
3011 "a pair serializes as two base58 strings: {json}"
3012 );
3013 let back: RerouteFilter = serde_json::from_value(json).unwrap();
3014 assert_eq!(back, filter);
3015 }
3016
3017 #[test]
3020 fn mint_pairs_are_normalized_however_they_are_built() {
3021 let (one, other) = (address(1), address(2));
3022 assert_eq!(MintPair::new(one, other), MintPair::new(other, one));
3023
3024 let reversed: MintPair =
3025 serde_json::from_value(serde_json::json!([other.to_string(), one.to_string()]))
3026 .unwrap();
3027 assert_eq!(reversed, MintPair::new(one, other));
3028 }
3029
3030 #[test]
3031 fn fail_fast_divergence_kind_str_round_trips() {
3032 for kind in [
3033 FailFastDivergenceKind::AnyNonBenign,
3034 FailFastDivergenceKind::Tracked,
3035 ] {
3036 assert_eq!(
3037 FailFastDivergenceKind::from_str_opt(kind.as_str()),
3038 Some(kind)
3039 );
3040 }
3041 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
3042 assert_eq!(
3043 FailFastDivergenceKind::default(),
3044 FailFastDivergenceKind::AnyNonBenign
3045 );
3046 }
3047
3048 #[test]
3049 fn bundle_not_found_is_terminal_and_descriptive() {
3050 let error = BacktestError::BundleNotFound {
3051 start_slot: 100,
3052 end_slot: 600,
3053 };
3054
3055 assert!(error.is_terminal());
3056 assert_eq!(
3057 error.to_string(),
3058 "block bundle not found for start_slot=100, end_slot=600"
3059 );
3060 }
3061
3062 #[test]
3063 fn no_produced_blocks_is_terminal_and_descriptive() {
3064 let error = BacktestError::NoProducedBlocksInRange {
3065 start_slot: 100,
3066 end_slot: 100,
3067 };
3068
3069 assert!(error.is_terminal());
3070 assert_eq!(
3073 error.to_string(),
3074 "no produced blocks in requested range 100..=100; slot(s) may be skipped on-chain"
3075 );
3076 }
3077
3078 #[test]
3079 fn bundle_build_request_optional_fields_default_to_none() {
3080 let req: BundleBuildRequest =
3081 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
3082 assert_eq!((req.start_slot, req.end_slot), (1, 2));
3083 assert_eq!(req.bundle_size, None);
3084 assert_eq!(req.idempotency_key, None);
3085 }
3086
3087 #[test]
3088 fn bundle_build_request_parses_optional_fields() {
3089 let req: BundleBuildRequest = serde_json::from_str(
3090 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
3091 )
3092 .expect("parse");
3093 assert_eq!(req.bundle_size, Some(500));
3094 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
3095 }
3096
3097 #[test]
3098 fn bundle_build_status_serde_round_trips_with_snake_case() {
3099 let cases = [
3100 (BundleBuildStatus::Completed, "\"completed\""),
3101 (BundleBuildStatus::Failed, "\"failed\""),
3102 (BundleBuildStatus::InProgress, "\"in_progress\""),
3103 (
3104 BundleBuildStatus::NeedsInvestigation,
3105 "\"needs_investigation\"",
3106 ),
3107 ];
3108 for (status, expected) in cases {
3109 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
3110 assert_eq!(
3111 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
3112 status
3113 );
3114 assert_eq!(status.as_str(), expected.trim_matches('"'));
3115 }
3116 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
3117 }
3118
3119 #[test]
3120 fn bundle_build_request_without_kind_deserializes_as_blocks() {
3121 let request: BundleBuildRequest =
3123 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
3124 assert_eq!(request.kind, BundleKind::Blocks);
3125 }
3126
3127 #[test]
3128 fn bundle_kind_round_trips_over_serde_and_as_str() {
3129 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
3130 let json = serde_json::to_string(&kind).unwrap();
3131 assert_eq!(json, format!("\"{}\"", kind.as_str()));
3132 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
3133 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
3136 }
3137 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
3138 assert_eq!(BundleKind::from_str_opt("accountState"), None);
3139 }
3140
3141 #[test]
3142 fn bundle_build_request_carries_kind_through_serde() {
3143 let request = BundleBuildRequest {
3144 start_slot: 100,
3145 end_slot: 200,
3146 kind: BundleKind::AccountState,
3147 ..Default::default()
3148 };
3149 let json = serde_json::to_string(&request).unwrap();
3150 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3151 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
3152 assert_eq!(parsed.kind, BundleKind::AccountState);
3153 }
3154
3155 #[test]
3156 fn bundle_build_status_response_serializes_request_and_status() {
3157 let response = BundleBuildStatusResponse {
3158 request_id: "r".to_string(),
3159 start_slot: 100,
3160 end_slot: 200,
3161 bundle_size: Some(50),
3162 status: BundleBuildStatus::InProgress,
3163 created_at_unix_ms: 0,
3164 kind: BundleKind::Blocks,
3165 eta_secs: Some(600),
3166 };
3167 let json = serde_json::to_value(&response).unwrap();
3168 assert_eq!(json["request_id"].as_str(), Some("r"));
3169 assert_eq!(json["start_slot"].as_u64(), Some(100));
3170 assert_eq!(json["bundle_size"].as_u64(), Some(50));
3171 assert_eq!(json["status"].as_str(), Some("in_progress"));
3172 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
3173 assert_eq!(json["eta_secs"].as_u64(), Some(600));
3174 }
3175
3176 #[test]
3177 fn an_empty_bundles_list_is_distinct_from_an_absent_one() {
3178 let absent: BundleBuildListResponse = serde_json::from_str(r#"{"builds":[]}"#).unwrap();
3180 let empty: BundleBuildListResponse =
3181 serde_json::from_str(r#"{"builds":[],"bundles":[]}"#).unwrap();
3182 assert_eq!(absent.bundles, None);
3183 assert_eq!(empty.bundles, Some(Vec::new()));
3184 }
3185
3186 #[test]
3187 fn completed_bundle_round_trips() {
3188 let bundle = CompletedBundle {
3189 start_slot: 100,
3190 end_slot: 200,
3191 kind: BundleKind::AccountState,
3192 completed_at_unix_ms: Some(1_700_000_000_000),
3193 size_bytes: Some(4_096),
3194 };
3195 let json = serde_json::to_string(&bundle).unwrap();
3196 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3197 assert_eq!(
3198 serde_json::from_str::<CompletedBundle>(&json).unwrap(),
3199 bundle
3200 );
3201
3202 let sparse = r#"{"start_slot":1,"end_slot":2,"kind":"blocks"}"#;
3203 let parsed: CompletedBundle = serde_json::from_str(sparse).unwrap();
3204 assert_eq!(parsed.completed_at_unix_ms, None);
3205 assert_eq!(parsed.size_bytes, None);
3206 }
3207
3208 #[test]
3209 fn available_range_reads_a_server_that_predates_kind_and_scope() {
3210 let json = r#"{
3212 "bundle_start_slot": 100,
3213 "bundle_start_slot_utc": null,
3214 "max_bundle_end_slot": 200,
3215 "max_bundle_end_slot_utc": null,
3216 "max_bundle_size": 101
3217 }"#;
3218 let parsed: AvailableRange = serde_json::from_str(json).unwrap();
3219 assert_eq!(parsed.bundle_start_slot, 100);
3220 assert_eq!(parsed.kind, None);
3221 assert_eq!(parsed.scope, None);
3222 }
3223
3224 #[test]
3225 fn available_range_ignores_fields_it_does_not_know() {
3226 let json = r#"{"bundle_start_slot": 1, "some_future_field": "x"}"#;
3228 assert_eq!(
3229 serde_json::from_str::<AvailableRange>(json)
3230 .unwrap()
3231 .bundle_start_slot,
3232 1
3233 );
3234 }
3235
3236 #[test]
3237 fn available_range_labels_round_trip() {
3238 let range = AvailableRange {
3239 bundle_start_slot: 100,
3240 max_bundle_end_slot: Some(200),
3241 kind: Some(BundleKind::AccountState),
3242 scope: Some(RangeScope::User),
3243 ..Default::default()
3244 };
3245 let json = serde_json::to_string(&range).unwrap();
3246 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3247 assert!(json.contains(r#""scope":"user""#), "{json}");
3248 let parsed: AvailableRange = serde_json::from_str(&json).unwrap();
3249 assert_eq!(parsed.kind, Some(BundleKind::AccountState));
3250 assert_eq!(parsed.scope, Some(RangeScope::User));
3251 }
3252
3253 fn range(start: u64, end: u64) -> AvailableRange {
3254 AvailableRange {
3255 bundle_start_slot: start,
3256 max_bundle_end_slot: Some(end),
3257 ..Default::default()
3258 }
3259 }
3260
3261 #[rstest::rstest]
3265 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
3266 #[case::multi(
3267 vec![range(100, 200), range(201, 300), range(301, 400)],
3268 100, 300, Some(vec![(100, 200), (201, 300)])
3269 )]
3270 #[case::nested(
3273 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
3274 100, 900, Some(vec![(100, 500), (501, 900)])
3275 )]
3276 #[case::prefers_finer_grid(
3280 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
3281 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
3282 )]
3283 #[case::shared_start_prefers_finer(
3287 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
3288 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
3289 )]
3290 #[case::coarse_overlap_prefers_finer_pair(
3294 vec![range(100, 200), range(100, 150), range(151, 160)],
3295 100, 160, Some(vec![(100, 150), (151, 160)])
3296 )]
3297 #[case::falls_back_to_coarse(
3301 vec![range(100, 160), range(100, 120), range(121, 140)],
3302 100, 160, Some(vec![(100, 160)])
3303 )]
3304 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
3306 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
3309 #[case::anchors_then_continues(
3310 vec![range(150, 350), range(351, 600)],
3311 200, 600, Some(vec![(150, 350), (351, 600)])
3312 )]
3313 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
3314 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
3316 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
3318 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
3319 #[case::user_and_global_ranges_not_collapsed(
3324 vec![range(100, 200), range(100, 150), range(151, 200)],
3325 100, 200, Some(vec![(100, 150), (151, 200)])
3326 )]
3327 fn split_range_cases(
3328 #[case] ranges: Vec<AvailableRange>,
3329 #[case] start: u64,
3330 #[case] end: u64,
3331 #[case] expected: Option<Vec<(u64, u64)>>,
3332 ) {
3333 match expected {
3334 Some(expected) => assert_eq!(
3335 split_range(
3336 &ranges,
3337 SlotRange::new_unchecked(start, end),
3338 BundleKind::Blocks
3339 )
3340 .unwrap(),
3341 expected
3342 ),
3343 None => assert!(
3344 split_range(
3345 &ranges,
3346 SlotRange::new_unchecked(start, end),
3347 BundleKind::Blocks
3348 )
3349 .is_err()
3350 ),
3351 }
3352 }
3353
3354 fn account_state_range(start: u64, end: u64) -> AvailableRange {
3355 AvailableRange {
3356 kind: Some(BundleKind::AccountState),
3357 ..range(start, end)
3358 }
3359 }
3360
3361 #[test]
3362 fn a_replay_split_rides_the_recordings_not_a_finer_block_grid() {
3363 let ranges = vec![
3366 range(100, 149),
3367 range(150, 199),
3368 account_state_range(100, 199),
3369 ];
3370 let requested = SlotRange::new_unchecked(100, 199);
3371
3372 assert_eq!(
3373 split_range(&ranges, requested, BundleKind::Blocks).unwrap(),
3374 [(100, 149), (150, 199)]
3375 );
3376 assert_eq!(
3377 split_range(&ranges, requested, BundleKind::AccountState).unwrap(),
3378 [(100, 199)]
3379 );
3380 }
3381
3382 #[test]
3383 fn a_replay_split_tiles_consecutive_recordings() {
3384 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
3385 assert_eq!(
3386 split_range(
3387 &ranges,
3388 SlotRange::new_unchecked(100, 299),
3389 BundleKind::AccountState
3390 )
3391 .unwrap(),
3392 [(100, 199), (200, 299)]
3393 );
3394 }
3395
3396 #[test]
3397 fn a_replay_split_ends_short_of_a_recording() {
3398 let requested = SlotRange::new_unchecked(100, 150);
3401 assert_eq!(
3402 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3403 [(100, 150)]
3404 );
3405 assert_eq!(
3406 split_range(
3407 &[account_state_range(100, 199)],
3408 requested,
3409 BundleKind::AccountState
3410 )
3411 .unwrap(),
3412 [(100, 150)]
3413 );
3414 }
3415
3416 #[test]
3417 fn a_replay_split_ends_short_of_its_last_recording() {
3418 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
3421 assert_eq!(
3422 split_range(
3423 &ranges,
3424 SlotRange::new_unchecked(100, 250),
3425 BundleKind::AccountState
3426 )
3427 .unwrap(),
3428 [(100, 199), (200, 250)]
3429 );
3430 }
3431
3432 #[test]
3433 fn a_replay_split_tiles_a_one_slot_recording() {
3434 let ranges = vec![account_state_range(100, 249), account_state_range(250, 250)];
3437 assert_eq!(
3438 split_range(
3439 &ranges,
3440 SlotRange::new_unchecked(100, 250),
3441 BundleKind::AccountState
3442 )
3443 .unwrap(),
3444 [(100, 249), (250, 250)]
3445 );
3446 }
3447
3448 #[test]
3449 fn a_replay_split_anchors_a_mid_recording_start() {
3450 let requested = SlotRange::new_unchecked(150, 199);
3454 assert_eq!(
3455 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3456 [(100, 199)]
3457 );
3458 assert_eq!(
3459 split_range(
3460 &[account_state_range(100, 199)],
3461 requested,
3462 BundleKind::AccountState
3463 )
3464 .unwrap(),
3465 [(100, 199)]
3466 );
3467 }
3468
3469 #[test]
3470 fn a_replay_split_refuses_a_start_no_recording_covers() {
3471 let error = split_range(
3472 &[account_state_range(200, 299)],
3473 SlotRange::new_unchecked(100, 299),
3474 BundleKind::AccountState,
3475 )
3476 .expect_err("no recording covers 100");
3477 assert!(
3478 error.contains("is not covered by any available account-state bundle"),
3479 "{error}"
3480 );
3481 }
3482
3483 #[test]
3484 fn a_replay_split_ignores_block_ranges() {
3485 let error = split_range(
3486 &[range(100, 199)],
3487 SlotRange::new_unchecked(100, 199),
3488 BundleKind::AccountState,
3489 )
3490 .expect_err("owns no recordings");
3491 assert!(error.contains("account-state bundle"), "{error}");
3492 }
3493
3494 #[test]
3495 fn blocks_mode_admits_ranges_from_a_server_that_predates_kind() {
3496 let unlabelled = range(100, 199);
3499 assert_eq!(unlabelled.kind, None);
3500 assert_eq!(
3501 split_range(
3502 &[unlabelled],
3503 SlotRange::new_unchecked(100, 150),
3504 BundleKind::Blocks
3505 )
3506 .unwrap(),
3507 [(100, 150)]
3508 );
3509 }
3510
3511 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
3514 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
3515 for r in ranges {
3516 if let Some(end) = r.max_bundle_end_slot
3517 && end >= r.bundle_start_slot
3518 {
3519 ends.entry(r.bundle_start_slot).or_default().insert(end);
3520 }
3521 }
3522 ends
3523 }
3524
3525 fn reference_max_split(
3529 ends: &BTreeMap<u64, BTreeSet<u64>>,
3530 cursor: u64,
3531 end: u64,
3532 ) -> Option<Vec<(u64, u64)>> {
3533 ends.get(&cursor)?
3534 .iter()
3535 .filter_map(|&bundle_end| {
3536 if bundle_end >= end {
3537 Some(vec![(cursor, end)])
3538 } else {
3539 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
3540 rest.insert(0, (cursor, bundle_end));
3541 rest
3542 })
3543 }
3544 })
3545 .max_by_key(Vec::len)
3546 }
3547
3548 fn is_valid_split(
3552 split: &[(u64, u64)],
3553 ends: &BTreeMap<u64, BTreeSet<u64>>,
3554 start: u64,
3555 end: u64,
3556 ) -> bool {
3557 split.first().is_some_and(|&(s, _)| s == start)
3558 && split.last().is_some_and(|&(_, e)| e == end)
3559 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
3560 && split.iter().all(|&(s, e)| {
3561 e >= s
3562 && ends
3563 .get(&s)
3564 .and_then(|bundle_ends| bundle_ends.iter().next_back())
3565 .is_some_and(|&max_end| e <= max_end)
3566 })
3567 }
3568
3569 #[test]
3574 fn split_range_matches_reference() {
3575 let mut seed: u64 = 0x9E3779B97F4A7C15;
3576 let mut next = || {
3577 seed = seed
3578 .wrapping_mul(6364136223846793005)
3579 .wrapping_add(1442695040888963407);
3580 seed >> 33
3581 };
3582
3583 for _ in 0..50_000 {
3584 let ranges: Vec<AvailableRange> = (0..next() % 6)
3587 .map(|_| {
3588 let start = next() % 12;
3589 range(start, start + next() % 6) })
3591 .collect();
3592 let start = next() % 12;
3593 let end = start + next() % 6; let got = split_range(
3596 &ranges,
3597 SlotRange::new_unchecked(start, end),
3598 BundleKind::Blocks,
3599 );
3600 let ends = ends_by_start(&ranges);
3601 let anchor = ends
3604 .range(..=start)
3605 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
3606 .map(|(&s, _)| s);
3607 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
3608
3609 let layout: Vec<_> = ranges
3610 .iter()
3611 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
3612 .collect();
3613 match (&got, &reference) {
3614 (Ok(split), Some(best)) => {
3615 assert!(
3616 is_valid_split(split, &ends, anchor.unwrap(), end),
3617 "invalid split {split:?} for {layout:?} [{start},{end}]"
3618 );
3619 assert_eq!(
3620 split.len(),
3621 best.len(),
3622 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
3623 );
3624 }
3625 (Err(_), None) => {}
3626 _ => panic!(
3627 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
3628 ),
3629 }
3630 }
3631 }
3632
3633 #[test]
3634 fn a_reroute_without_templates_omits_direct_fill() {
3635 let json = serde_json::to_value(RerouteParams::default()).expect("serialize");
3636 assert!(json.get("directFill").is_none(), "{json}");
3637 }
3638
3639 #[test]
3640 fn extra_markets_cross_the_wire_as_base58() {
3641 let json = serde_json::to_value(RerouteParams::default()).expect("serialize");
3642 assert!(json.get("extraMarkets").is_none(), "{json}");
3643
3644 let reroute = RerouteParams {
3645 extra_markets: BTreeSet::from([address(7)]),
3646 ..Default::default()
3647 };
3648 let json = serde_json::to_value(&reroute).expect("serialize");
3649 assert_eq!(
3650 json["extraMarkets"],
3651 serde_json::json!([address(7).to_string()])
3652 );
3653 assert_eq!(
3654 serde_json::from_value::<RerouteParams>(json).expect("deserialize"),
3655 reroute
3656 );
3657
3658 serde_json::from_value::<RerouteParams>(serde_json::json!({
3659 "extraMarkets": ["not-an-address"]
3660 }))
3661 .expect_err("an invalid address is rejected");
3662 }
3663
3664 fn template(label: &str) -> DirectFillTemplate {
3666 let placeholder = Address::default();
3667 DirectFillTemplate {
3668 label: label.to_owned(),
3669 mode: SwapMode::ExactIn,
3670 input_mint: mints::SOL,
3671 output_mint: mints::USDC,
3672 instruction: TemplateInstruction {
3673 program_id: address(7),
3674 accounts: vec![
3675 TemplateAccount {
3676 address: placeholder,
3677 writable: true,
3678 signer: true,
3679 },
3680 TemplateAccount {
3681 address: placeholder,
3682 writable: true,
3683 signer: false,
3684 },
3685 TemplateAccount {
3686 address: placeholder,
3687 writable: true,
3688 signer: false,
3689 },
3690 TemplateAccount {
3691 address: address(8),
3692 writable: false,
3693 signer: false,
3694 },
3695 ],
3696 data: vec![0; 16],
3697 },
3698 amount_offset: 2,
3699 taker: TakerSlots {
3700 user: vec![0],
3701 input_token_account: vec![1],
3702 output_token_account: vec![2],
3703 },
3704 }
3705 }
3706
3707 #[rstest]
3709 #[case::requote_alone(true, 0, false)]
3710 #[case::templates_alone(false, 1, false)]
3711 #[case::both(true, 2, false)]
3712 #[case::neither(false, 0, true)]
3713 fn a_reroute_is_a_noop_only_with_nothing_to_ask(
3714 #[case] requote: bool,
3715 #[case] templates: usize,
3716 #[case] noop: bool,
3717 ) {
3718 let reroute = RerouteParams {
3719 requote,
3720 direct_fill: (0..templates).map(|i| template(&format!("t{i}"))).collect(),
3721 ..Default::default()
3722 };
3723 assert_eq!(reroute.is_noop(), noop);
3724 }
3725
3726 #[rstest]
3728 #[case::templates_without_requote(false, 1, true)]
3729 #[case::requote_without_templates(true, 0, true)]
3730 #[case::nothing(false, 0, false)]
3731 fn the_account_universe_warms_for_templates_as_for_requoting(
3732 #[case] requote: bool,
3733 #[case] templates: usize,
3734 #[case] warms: bool,
3735 ) {
3736 let params = CreateSessionParams::builder()
3737 .start_slot(1)
3738 .end_slot(2)
3739 .reroute(RerouteParams {
3740 requote,
3741 direct_fill: (0..templates).map(|i| template(&format!("t{i}"))).collect(),
3742 ..Default::default()
3743 })
3744 .build();
3745 assert_eq!(params.warms_account_universe(), warms);
3746 }
3747}