1use std::{
2 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
3 fmt,
4 num::{NonZeroU64, NonZeroUsize},
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 session_name::{CreateToken, InvalidSessionName, RequestedName};
15use solana_account::AccountSharedData;
16use solana_address::{Address, error::ParseAddressError};
17
18mod session_id;
19mod session_type;
20mod slot_range;
21
22pub use direct_fill::{
23 AMOUNT_WIDTH, DerivedAccount, DirectFillTemplate, ParseDirectFillTemplateError, Seed,
24 TakerSlots, TemplateAccount, TemplateInstruction,
25};
26pub use reroute_report::SwapMode;
27pub use session_id::{ControlSessionId, SessionId};
28pub use session_type::SessionType;
29pub use slot_range::{InvertedSlotRange, SlotRange};
30
31pub mod client_identity;
32pub mod direct_fill;
33pub mod geyser_route;
34pub mod mints;
35pub mod replacement;
36pub mod reroute_report;
37pub mod route_plan;
38pub mod session_name;
39pub mod subscribe_config;
40pub mod usage;
41pub mod ws_compression;
42
43#[allow(
45 clippy::large_enum_variant,
46 reason = "decoded once per message and consumed by value; boxing the create variant would \
47 change every caller's construction for no measurable gain"
48)]
49#[derive(Debug, Serialize, Deserialize)]
50#[serde(tag = "method", content = "params", rename_all = "camelCase")]
51pub enum BacktestRequest {
52 CreateBacktestSession(CreateBacktestSessionRequest),
53 Continue(ContinueParams),
54 ContinueTo(ContinueToParams),
55 ContinueSessionV1(ContinueSessionRequestV1),
56 ContinueToSessionV1(ContinueToSessionRequestV1),
57 CloseBacktestSession,
58 CloseSessionV1(CloseSessionRequestV1),
59 AttachBacktestSession {
60 session_id: SessionId,
61 last_sequence: Option<u64>,
64 },
65 ResumeAttachedSession,
68 AttachParallelControlSessionV2 {
69 control_session_id: ControlSessionId,
70 #[serde(default)]
74 last_sequences: BTreeMap<SessionId, u64>,
75 },
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80#[serde(untagged)]
81pub enum CreateBacktestSessionRequest {
82 V1(CreateBacktestSessionRequestV1),
83 V0(CreateSessionParams),
84}
85
86impl CreateBacktestSessionRequest {
87 pub fn try_into_request_options(
88 self,
89 ) -> Result<CreateBacktestSessionRequestOptions, InvalidSessionName> {
90 match self {
91 Self::V0(request) => Ok(CreateBacktestSessionRequestOptions {
92 request,
93 parallel: false,
94 name: None,
95 }),
96 Self::V1(CreateBacktestSessionRequestV1 {
97 request,
98 parallel,
99 name,
100 create_token,
101 }) => {
102 let name = name
103 .map(|name| {
104 name.try_into()
105 .map(|name| RequestedName { name, create_token })
106 })
107 .transpose()?;
108 Ok(CreateBacktestSessionRequestOptions {
109 request,
110 parallel,
111 name,
112 })
113 }
114 }
115 }
116
117 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
118 match self {
119 Self::V0(request) => (request, false),
120 Self::V1(CreateBacktestSessionRequestV1 {
121 request,
122 parallel,
123 name: _,
124 create_token: _,
125 }) => (request, parallel),
126 }
127 }
128}
129
130impl From<CreateSessionParams> for CreateBacktestSessionRequest {
131 fn from(value: CreateSessionParams) -> Self {
132 Self::V0(value)
133 }
134}
135
136impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
137 fn from(value: CreateBacktestSessionRequestV1) -> Self {
138 Self::V1(value)
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct CreateBacktestSessionRequestV1 {
145 #[serde(flatten)]
146 pub request: CreateSessionParams,
147 #[serde(default)]
148 pub parallel: bool,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub name: Option<String>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub create_token: Option<CreateToken>,
154}
155
156#[derive(Debug, Clone)]
157pub struct CreateBacktestSessionRequestOptions {
158 pub request: CreateSessionParams,
159 pub parallel: bool,
160 pub name: Option<RequestedName>,
161}
162
163#[derive(Debug, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct ContinueSessionRequestV1 {
166 pub session_id: SessionId,
167 pub request: ContinueParams,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct ContinueToSessionRequestV1 {
173 pub session_id: SessionId,
174 pub request: ContinueToParams,
175}
176
177#[derive(Debug, Serialize, Deserialize)]
178#[serde(rename_all = "camelCase")]
179pub struct CloseSessionRequestV1 {
180 pub session_id: SessionId,
181}
182
183#[serde_with::serde_as]
190#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
191#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
192pub enum DiscoveryFilter {
193 ProgramExecuted(#[serde_as(as = "serde_with::DisplayFromStr")] Address),
195 OracleUpdate {
200 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
201 programs: BTreeSet<Address>,
202 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
205 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
206 accounts: BTreeSet<Address>,
207 },
208}
209
210pub struct TxMatchContext {
215 pub invoked_programs: HashSet<Address>,
217 pub moves_tokens: bool,
219 pub written_accounts: Vec<Address>,
222 pub writes_via_unresolved_lookup: bool,
225}
226
227impl DiscoveryFilter {
228 pub fn matches(&self, ctx: &TxMatchContext) -> bool {
231 match self {
232 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
233 Self::OracleUpdate { programs, accounts } => {
234 !ctx.moves_tokens
235 && ctx
236 .invoked_programs
237 .iter()
238 .any(|program| programs.contains(program))
239 && (accounts.is_empty()
240 || ctx.writes_via_unresolved_lookup
241 || ctx
242 .written_accounts
243 .iter()
244 .any(|account| accounts.contains(account)))
245 }
246 }
247 }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
254#[serde(try_from = "[String; 2]", into = "[String; 2]")]
255pub struct MintPair([Address; 2]);
256
257impl MintPair {
258 pub fn new(one: Address, other: Address) -> Self {
259 Self(if one <= other {
260 [one, other]
261 } else {
262 [other, one]
263 })
264 }
265
266 pub fn mints(&self) -> (Address, Address) {
267 (self.0[0], self.0[1])
268 }
269}
270
271#[derive(Debug, thiserror::Error)]
272#[error("invalid mint {mint} in pair")]
273pub struct ParseMintPairError {
274 mint: String,
275 source: ParseAddressError,
276}
277
278impl TryFrom<[String; 2]> for MintPair {
279 type Error = ParseMintPairError;
280
281 fn try_from([one, other]: [String; 2]) -> Result<Self, Self::Error> {
282 fn parse(mint: String) -> Result<Address, ParseMintPairError> {
283 Address::from_str(&mint).map_err(|source| ParseMintPairError { mint, source })
284 }
285 Ok(Self::new(parse(one)?, parse(other)?))
286 }
287}
288
289impl From<MintPair> for [String; 2] {
290 fn from(pair: MintPair) -> Self {
291 let (one, other) = pair.mints();
292 [one.to_string(), other.to_string()]
293 }
294}
295
296#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub struct RerouteFilter {
301 #[serde(default)]
303 pub pairs: BTreeSet<MintPair>,
304}
305
306impl RerouteFilter {
307 pub fn is_empty(&self) -> bool {
310 self.pairs.is_empty()
311 }
312
313 pub fn matches_mints(&self, input: &Address, output: &Address) -> bool {
314 self.pairs.contains(&MintPair::new(*input, *output))
315 }
316
317 pub fn mentions_mint(&self, mint: &Address) -> bool {
320 self.pairs.iter().any(|pair| {
321 let (one, other) = pair.mints();
322 one == *mint || other == *mint
323 })
324 }
325}
326
327#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub enum ActionKind {
332 #[default]
336 Simulate,
337 Send,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346#[serde(tag = "at", rename_all = "camelCase")]
347pub enum ActionAnchor {
348 BeforeSlot {
351 #[serde(default, with = "compressed")]
352 slots: Vec<u64>,
353 },
354 AfterSlot {
357 #[serde(default, with = "compressed")]
358 slots: Vec<u64>,
359 },
360 #[serde(rename_all = "camelCase")]
365 BeforeEverySlot {
366 #[serde(default = "every_slot")]
367 every_n_slots: NonZeroU64,
368 },
369 #[serde(rename_all = "camelCase")]
373 AfterEverySlot {
374 #[serde(default = "every_slot")]
375 every_n_slots: NonZeroU64,
376 },
377 BeforeMatch { filter: DiscoveryFilter },
380 AfterMatch { filter: DiscoveryFilter },
383}
384
385const fn every_slot() -> NonZeroU64 {
386 NonZeroU64::MIN
387}
388
389const MAX_ACTION_TRANSACTIONS_BYTES: u64 = 256 * 1024 * 1024;
393
394mod compressed {
399 use std::io::Read;
400
401 use base64::Engine as _;
402 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
403
404 use super::{BASE64, MAX_ACTION_TRANSACTIONS_BYTES};
405
406 const COMPRESSION_LEVEL: i32 = 0;
408
409 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
410 where
411 T: Serialize,
412 S: Serializer,
413 {
414 let json = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
415 let compressed = zstd::stream::encode_all(json.as_slice(), COMPRESSION_LEVEL)
416 .map_err(serde::ser::Error::custom)?;
417 serializer.serialize_str(&BASE64.encode(compressed))
418 }
419
420 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
421 where
422 T: DeserializeOwned,
423 D: Deserializer<'de>,
424 {
425 let blob = String::deserialize(deserializer)?;
426 let compressed = BASE64
427 .decode(blob.as_bytes())
428 .map_err(serde::de::Error::custom)?;
429 let json = decode_capped(&compressed, MAX_ACTION_TRANSACTIONS_BYTES)
430 .map_err(serde::de::Error::custom)?;
431 serde_json::from_slice(&json).map_err(serde::de::Error::custom)
432 }
433
434 fn decode_capped(compressed: &[u8], cap: u64) -> std::io::Result<Vec<u8>> {
437 let mut json = Vec::new();
438 zstd::stream::read::Decoder::new(compressed)
439 .and_then(|d| Read::take(d, cap + 1).read_to_end(&mut json))?;
440 match json.len() as u64 > cap {
441 true => Err(std::io::Error::other(format!(
442 "decompressed blob is over the {cap} byte ceiling"
443 ))),
444 false => Ok(json),
445 }
446 }
447
448 #[cfg(test)]
449 mod tests {
450 use super::*;
451
452 #[test]
453 fn a_blob_over_the_ceiling_names_it_instead_of_truncating() {
454 let compressed = zstd::stream::encode_all([b'a'; 1024].as_slice(), 0).unwrap();
455 let error = decode_capped(&compressed, 64).unwrap_err();
456 assert!(error.to_string().contains("64 byte ceiling"), "{error}");
457 }
458
459 #[test]
460 fn a_blob_at_the_ceiling_is_kept_whole() {
461 let compressed = zstd::stream::encode_all([b'a'; 64].as_slice(), 0).unwrap();
462 assert_eq!(decode_capped(&compressed, 64).unwrap().len(), 64);
463 }
464 }
465}
466
467#[serde_with::serde_as]
470#[derive(Debug, Clone, Serialize, Deserialize)]
471#[serde(rename_all = "camelCase")]
472pub struct ScheduledAction {
473 pub anchor: ActionAnchor,
474 #[serde(default)]
476 pub kind: ActionKind,
477 #[serde(default, with = "compressed")]
487 pub transactions: Vec<String>,
488 #[serde(default)]
493 pub account_overrides: AccountModifications,
494 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
499 pub feeds_reroute: bool,
500 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
504 #[serde(default)]
505 pub return_accounts: Vec<Address>,
506 #[serde(default)]
508 pub label: Option<String>,
509 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub resolve: Option<TxResolver>,
513}
514
515impl ScheduledAction {
516 pub fn is_computed_override(&self) -> bool {
519 self.feeds_reroute && !self.transactions.is_empty()
520 }
521
522 fn validate_resolve(&self) -> Result<(), TxResolverError> {
523 let Some(resolver) = &self.resolve else {
524 return Ok(());
525 };
526 match self.kind {
527 ActionKind::Send => return Err(TxResolverError::Send),
528 ActionKind::Simulate => {}
529 }
530 match resolver {
531 TxResolver::LiquidityArrays { .. } if self.transactions.len() != 1 => {
532 return Err(TxResolverError::TransactionCount {
533 got: self.transactions.len(),
534 });
535 }
536 TxResolver::QuoteStamp if self.transactions.is_empty() => {
537 return Err(TxResolverError::NoTransactions);
538 }
539 TxResolver::LiquidityArrays { .. } | TxResolver::QuoteStamp => {}
540 }
541 if self.feeds_reroute {
542 return Err(TxResolverError::FeedsReroute);
543 }
544 Ok(())
545 }
546}
547
548pub fn validate_resolvers(actions: &[ScheduledAction]) -> Result<(), ActionResolveError> {
549 actions
550 .iter()
551 .enumerate()
552 .try_for_each(|(action, scheduled)| {
553 scheduled
554 .validate_resolve()
555 .map_err(|source| ActionResolveError { action, source })
556 })
557}
558
559#[serde_with::serde_as]
560#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
561#[serde(
562 tag = "type",
563 rename_all = "camelCase",
564 rename_all_fields = "camelCase"
565)]
566pub enum TxResolver {
567 LiquidityArrays {
572 #[serde_as(as = "serde_with::DisplayFromStr")]
573 pool: Address,
574 #[serde_as(as = "serde_with::DisplayFromStr")]
576 input_mint: Address,
577 array_key_indices: ArrayKeyIndices,
580 },
581 QuoteStamp,
586}
587
588#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
589#[serde(try_from = "Vec<u8>", into = "Vec<u8>")]
590pub struct ArrayKeyIndices {
591 first: u8,
592 rest: Vec<u8>,
593}
594
595impl ArrayKeyIndices {
596 pub fn iter(&self) -> impl Iterator<Item = u8> + '_ {
597 std::iter::once(self.first).chain(self.rest.iter().copied())
598 }
599
600 pub fn count(&self) -> NonZeroUsize {
601 NonZeroUsize::MIN.saturating_add(self.rest.len())
602 }
603}
604
605#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
606pub enum ArrayKeyIndicesError {
607 #[error("no arrayKeyIndices")]
608 Empty,
609 #[error("arrayKeyIndex {index} repeated")]
610 Duplicate { index: u8 },
611}
612
613impl TryFrom<Vec<u8>> for ArrayKeyIndices {
614 type Error = ArrayKeyIndicesError;
615
616 fn try_from(indices: Vec<u8>) -> Result<Self, Self::Error> {
617 let Some((&first, rest)) = indices.split_first() else {
618 return Err(ArrayKeyIndicesError::Empty);
619 };
620 let mut seen = [false; 256];
621 match indices
622 .iter()
623 .find(|&&index| std::mem::replace(&mut seen[usize::from(index)], true))
624 {
625 Some(&index) => Err(ArrayKeyIndicesError::Duplicate { index }),
626 None => Ok(Self {
627 first,
628 rest: rest.to_vec(),
629 }),
630 }
631 }
632}
633
634impl From<ArrayKeyIndices> for Vec<u8> {
635 fn from(indices: ArrayKeyIndices) -> Self {
636 std::iter::once(indices.first).chain(indices.rest).collect()
637 }
638}
639
640#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
641pub enum TxResolverError {
642 #[error("resolve is not valid for kind: send")]
643 Send,
644 #[error("resolve rewrites exactly one transaction, and the action carries {got}")]
645 TransactionCount { got: usize },
646 #[error("resolve needs a transaction to rewrite")]
647 NoTransactions,
648 #[error("resolve cannot be combined with feedsReroute")]
649 FeedsReroute,
650}
651
652#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
653#[error("action {action}: {source}")]
654pub struct ActionResolveError {
655 pub action: usize,
656 #[source]
657 pub source: TxResolverError,
658}
659
660pub const MAX_DIAGNOSTIC_MARKETS: usize = 64;
661pub const MAX_DIAGNOSTIC_USD_VALUES: usize = 16;
662
663#[serde_with::serde_as]
664#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
665#[serde(rename_all = "camelCase")]
666pub struct DiagnosticProbeParams {
667 pub anchor: ActionAnchor,
668 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
669 pub markets: Vec<Address>,
670 pub usd_values: Vec<u32>,
671 #[serde(default)]
672 pub label: Option<String>,
673}
674
675#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
676pub enum DiagnosticProbeError {
677 #[error("a diagnostic probe names no markets")]
678 NoMarkets,
679 #[error("a diagnostic probe names no usdValues")]
680 NoUsdValues,
681 #[error("a diagnostic probe names {got} markets, over the ceiling of {MAX_DIAGNOSTIC_MARKETS}")]
682 TooManyMarkets { got: usize },
683 #[error(
684 "a diagnostic probe names {got} usdValues, over the ceiling of {MAX_DIAGNOSTIC_USD_VALUES}"
685 )]
686 TooManyUsdValues { got: usize },
687 #[error("a diagnostic probe names usdValue 0, which the router reads as no size at all")]
688 ZeroUsdValue,
689 #[error("a diagnostic probe names market {market} more than once")]
690 DuplicateMarket { market: Address },
691 #[error(
692 "a diagnostic probe anchors at {anchor}; probing the router is an await, and only the \
693 slot-end hook has a point for one, so only afterSlot and afterEverySlot are supported"
694 )]
695 UnsupportedAnchor { anchor: &'static str },
696}
697
698impl DiagnosticProbeParams {
699 pub fn fires(&self) -> usize {
701 self.markets.len() * self.usd_values.len()
702 }
703
704 pub fn validate(&self) -> Result<(), DiagnosticProbeError> {
705 if self.markets.is_empty() {
706 return Err(DiagnosticProbeError::NoMarkets);
707 }
708 if self.usd_values.is_empty() {
709 return Err(DiagnosticProbeError::NoUsdValues);
710 }
711 if self.markets.len() > MAX_DIAGNOSTIC_MARKETS {
712 return Err(DiagnosticProbeError::TooManyMarkets {
713 got: self.markets.len(),
714 });
715 }
716 if self.usd_values.len() > MAX_DIAGNOSTIC_USD_VALUES {
717 return Err(DiagnosticProbeError::TooManyUsdValues {
718 got: self.usd_values.len(),
719 });
720 }
721 if self.usd_values.contains(&0) {
722 return Err(DiagnosticProbeError::ZeroUsdValue);
723 }
724 let mut seen = HashSet::with_capacity(self.markets.len());
725 if let Some(market) = self.markets.iter().find(|market| !seen.insert(**market)) {
726 return Err(DiagnosticProbeError::DuplicateMarket { market: *market });
727 }
728 match &self.anchor {
729 ActionAnchor::AfterSlot { .. } | ActionAnchor::AfterEverySlot { .. } => Ok(()),
730 ActionAnchor::BeforeSlot { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
731 anchor: "beforeSlot",
732 }),
733 ActionAnchor::BeforeEverySlot { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
734 anchor: "beforeEverySlot",
735 }),
736 ActionAnchor::BeforeMatch { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
737 anchor: "beforeMatch",
738 }),
739 ActionAnchor::AfterMatch { .. } => Err(DiagnosticProbeError::UnsupportedAnchor {
740 anchor: "afterMatch",
741 }),
742 }
743 }
744}
745
746#[derive(Debug, Clone)]
748enum OverrideContribution {
749 Static(HashMap<Address, AccountSharedData>),
750 Pending,
751 Resolved(HashMap<Address, AccountSharedData>),
752 Failed,
753}
754
755#[derive(Debug, Clone, Default)]
756struct OverrideScheduleInner {
757 entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>,
758 keys: HashSet<Address>,
762}
763
764impl OverrideScheduleInner {
765 fn new(entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>) -> Self {
766 let keys = entries
767 .values()
768 .flatten()
769 .filter_map(|(_, contribution)| match contribution {
770 OverrideContribution::Static(accounts)
771 | OverrideContribution::Resolved(accounts) => Some(accounts.keys().copied()),
772 OverrideContribution::Pending | OverrideContribution::Failed => None,
773 })
774 .flatten()
775 .collect();
776 Self { entries, keys }
777 }
778}
779
780#[derive(Debug, Clone, Default)]
783pub struct OverrideSchedule {
784 inner: Arc<RwLock<OverrideScheduleInner>>,
785}
786
787impl From<BTreeMap<u64, HashMap<Address, AccountSharedData>>> for OverrideSchedule {
788 fn from(entries: BTreeMap<u64, HashMap<Address, AccountSharedData>>) -> Self {
789 Self {
790 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(
791 entries
792 .into_iter()
793 .map(|(slot, accounts)| {
794 (slot, vec![(0, OverrideContribution::Static(accounts))])
795 })
796 .collect(),
797 ))),
798 }
799 }
800}
801
802impl OverrideSchedule {
803 pub fn is_empty(&self) -> bool {
804 self.inner.read().entries.is_empty()
805 }
806
807 pub fn slots(&self) -> impl Iterator<Item = u64> + '_ {
809 self.inner
810 .read()
811 .entries
812 .keys()
813 .copied()
814 .collect::<Vec<_>>()
815 .into_iter()
816 }
817
818 pub fn active_at(&self, slot: u64) -> HashMap<Address, AccountSharedData> {
820 self.inner.read().entries.range(..=slot).fold(
821 HashMap::new(),
822 |mut active, (_, contributions)| {
823 active.extend(fold_contributions(contributions));
824 active
825 },
826 )
827 }
828
829 pub fn active_for(&self, key: &Address, slot: u64) -> Option<AccountSharedData> {
830 let inner = self.inner.read();
831 if !inner.keys.contains(key) {
832 return None;
833 }
834 inner
835 .entries
836 .range(..=slot)
837 .rev()
838 .find_map(|(_, contributions)| {
839 contributions
840 .iter()
841 .rev()
842 .find_map(|(_, contribution)| match contribution {
843 OverrideContribution::Static(accounts)
844 | OverrideContribution::Resolved(accounts) => accounts.get(key).cloned(),
845 OverrideContribution::Pending | OverrideContribution::Failed => None,
846 })
847 })
848 }
849
850 pub fn newly_active(
852 &self,
853 after: Option<u64>,
854 up_to: u64,
855 ) -> Vec<(Address, AccountSharedData)> {
856 let lower = after.map_or(std::ops::Bound::Unbounded, std::ops::Bound::Excluded);
857 self.inner
858 .read()
859 .entries
860 .range((lower, std::ops::Bound::Included(up_to)))
861 .flat_map(|(_, contributions)| fold_contributions(contributions))
862 .collect::<HashMap<_, _>>()
863 .into_iter()
864 .collect()
865 }
866
867 pub fn resolve(
870 &self,
871 slot: u64,
872 action_index: u32,
873 accounts: Option<HashMap<Address, AccountSharedData>>,
874 ) -> Result<(), String> {
875 let mut inner = self.inner.write();
876 let OverrideScheduleInner { entries, keys } = &mut *inner;
877 let contribution = entries
878 .get_mut(&slot)
879 .and_then(|contributions| {
880 contributions
881 .iter_mut()
882 .find(|(index, _)| *index == action_index)
883 .map(|(_, contribution)| contribution)
884 })
885 .ok_or_else(|| {
886 format!("override action {action_index} has no contribution at slot {slot}")
887 })?;
888 if !matches!(contribution, OverrideContribution::Pending) {
889 return Err(format!(
890 "override action {action_index} at slot {slot} is not pending"
891 ));
892 }
893 *contribution = match accounts {
894 Some(accounts) => {
895 keys.extend(accounts.keys().copied());
896 OverrideContribution::Resolved(accounts)
897 }
898 None => OverrideContribution::Failed,
899 };
900 Ok(())
901 }
902}
903
904fn fold_contributions(
905 contributions: &[(u32, OverrideContribution)],
906) -> HashMap<Address, AccountSharedData> {
907 let mut accounts = HashMap::new();
908 for (_, contribution) in contributions {
909 match contribution {
910 OverrideContribution::Static(next) | OverrideContribution::Resolved(next) => {
911 accounts.extend(next.iter().map(|(key, account)| (*key, account.clone())));
912 }
913 OverrideContribution::Pending | OverrideContribution::Failed => {}
914 }
915 }
916 accounts
917}
918
919fn decode_override_actions(actions: &[ScheduledAction]) -> Result<OverrideSchedule, String> {
928 let mut entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>> = BTreeMap::new();
929 for (action_index, action) in actions.iter().enumerate().filter(|(_, action)| {
930 action.feeds_reroute
931 || (action.transactions.is_empty() && !action.account_overrides.0.is_empty())
932 }) {
933 if action.kind == ActionKind::Send {
935 return Err("account overrides are not valid for kind: send".to_string());
936 }
937 if action.transactions.is_empty() && action.account_overrides.0.is_empty() {
938 return Err(
939 "feedsReroute needs something to feed: transactions to simulate, or \
940 accountOverrides"
941 .to_string(),
942 );
943 }
944 let ActionAnchor::BeforeSlot { slots } = &action.anchor else {
945 return Err("scheduled account overrides require a beforeSlot anchor".to_string());
946 };
947 let accounts = decode_account_modifications(&action.account_overrides)?;
948 let contribution = if action.transactions.is_empty() {
949 OverrideContribution::Static(accounts)
950 } else {
951 OverrideContribution::Pending
952 };
953 for slot in slots.iter().copied().collect::<BTreeSet<_>>() {
956 entries
957 .entry(slot)
958 .or_default()
959 .push((action_index as u32, contribution.clone()));
960 }
961 }
962 Ok(OverrideSchedule {
963 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(entries))),
964 })
965}
966
967#[allow(
971 deprecated,
972 reason = "`PROGRAM_OWNERS` remains the canonical loader-owner list"
973)]
974pub fn decode_account_modifications(
975 modifications: &AccountModifications,
976) -> Result<HashMap<Address, AccountSharedData>, String> {
977 modifications
978 .0
979 .iter()
980 .map(|(address, data)| {
981 let account = data
982 .to_account()
983 .map_err(|error| format!("invalid account data for {address}: {error}"))?;
984 if account.executable || solana_account::PROGRAM_OWNERS.contains(&account.owner) {
985 return Err(format!(
986 "overriding executable/program accounts is not supported (account {address})"
987 ));
988 }
989 Ok((*address, AccountSharedData::from(account)))
990 })
991 .collect()
992}
993
994pub const MAX_DISCONNECT_TIMEOUT_SECS: u16 = 900;
998
999#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1002#[serde(rename_all = "lowercase", try_from = "String")]
1003pub enum SwapAggregator {
1004 Jupiter,
1005 Okx,
1006 Titan,
1007 Dflow,
1008}
1009
1010impl SwapAggregator {
1011 pub const fn as_str(self) -> &'static str {
1013 match self {
1014 Self::Jupiter => "jupiter",
1015 Self::Okx => "okx",
1016 Self::Titan => "titan",
1017 Self::Dflow => "dflow",
1018 }
1019 }
1020}
1021
1022impl std::fmt::Display for SwapAggregator {
1023 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1024 f.write_str(self.as_str())
1025 }
1026}
1027
1028#[derive(Debug, thiserror::Error)]
1029pub enum ParseAggregatorError {
1030 #[error("unknown swap aggregator {0:?}; expected one of jupiter, okx, titan, dflow")]
1031 Unknown(String),
1032 #[error("name at least one aggregator (jupiter, okx, titan, dflow)")]
1034 NoAggregator,
1035}
1036
1037impl TryFrom<String> for SwapAggregator {
1038 type Error = ParseAggregatorError;
1039
1040 fn try_from(value: String) -> Result<Self, Self::Error> {
1041 value.parse()
1042 }
1043}
1044
1045impl std::str::FromStr for SwapAggregator {
1046 type Err = ParseAggregatorError;
1047
1048 fn from_str(s: &str) -> Result<Self, Self::Err> {
1049 match s.trim().to_ascii_lowercase().as_str() {
1050 "jupiter" => Ok(Self::Jupiter),
1051 "okx" => Ok(Self::Okx),
1052 "titan" => Ok(Self::Titan),
1053 "dflow" => Ok(Self::Dflow),
1054 _ => Err(ParseAggregatorError::Unknown(s.to_string())),
1055 }
1056 }
1057}
1058
1059#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1063#[serde(transparent)]
1064pub struct RerouteAggregators(BTreeSet<SwapAggregator>);
1065
1066impl Default for RerouteAggregators {
1067 fn default() -> Self {
1068 Self([SwapAggregator::Jupiter].into_iter().collect())
1069 }
1070}
1071
1072impl RerouteAggregators {
1073 pub fn new(aggregators: impl IntoIterator<Item = SwapAggregator>) -> Self {
1074 Self(aggregators.into_iter().collect())
1075 }
1076
1077 pub fn contains(&self, aggregator: SwapAggregator) -> bool {
1078 self.0.contains(&aggregator)
1079 }
1080
1081 pub fn is_empty(&self) -> bool {
1082 self.0.is_empty()
1083 }
1084}
1085
1086impl std::fmt::Display for RerouteAggregators {
1087 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1088 let mut aggregators = self.0.iter();
1089 if let Some(first) = aggregators.next() {
1090 write!(f, "{first}")?;
1091 }
1092 aggregators.try_for_each(|aggregator| write!(f, ",{aggregator}"))
1093 }
1094}
1095
1096impl std::str::FromStr for RerouteAggregators {
1097 type Err = ParseAggregatorError;
1098
1099 fn from_str(s: &str) -> Result<Self, Self::Err> {
1100 let aggregators: BTreeSet<SwapAggregator> = s
1101 .split(',')
1102 .filter(|aggregator| !aggregator.trim().is_empty())
1103 .map(str::parse)
1104 .collect::<Result<_, _>>()?;
1105 if aggregators.is_empty() {
1106 return Err(ParseAggregatorError::NoAggregator);
1107 }
1108 Ok(Self(aggregators))
1109 }
1110}
1111
1112#[serde_with::serde_as]
1120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1121#[serde(rename_all = "camelCase")]
1122pub struct RerouteParams {
1123 #[serde(default)]
1124 pub aggregators: RerouteAggregators,
1125 #[serde(default, skip_serializing_if = "Option::is_none")]
1128 pub filter: Option<RerouteFilter>,
1129 #[serde(default)]
1136 pub detect_failed_l1_swaps: bool,
1137 #[serde(default)]
1140 pub circular_arbs: bool,
1141 #[serde(default = "RerouteParams::default_requote")]
1147 pub requote: bool,
1148 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1157 pub direct_fill: Vec<DirectFillTemplate>,
1158 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
1162 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
1163 pub extra_markets: BTreeSet<Address>,
1164}
1165
1166impl Default for RerouteParams {
1169 fn default() -> Self {
1170 Self {
1171 aggregators: RerouteAggregators::default(),
1172 filter: None,
1173 detect_failed_l1_swaps: false,
1174 circular_arbs: false,
1175 requote: Self::default_requote(),
1176 direct_fill: Vec::new(),
1177 extra_markets: BTreeSet::new(),
1178 }
1179 }
1180}
1181
1182impl std::str::FromStr for MintPair {
1184 type Err = String;
1185
1186 fn from_str(s: &str) -> Result<Self, Self::Err> {
1187 let (base, quote) = s
1188 .split_once(',')
1189 .ok_or_else(|| format!("expected two comma-separated mints, got {s:?}"))?;
1190 Self::try_from([base.trim().to_owned(), quote.trim().to_owned()])
1191 .map_err(|error| error.to_string())
1192 }
1193}
1194
1195impl std::fmt::Display for MintPair {
1196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1197 let (one, other) = self.mints();
1198 write!(f, "{one},{other}")
1199 }
1200}
1201
1202impl RerouteParams {
1203 const fn default_requote() -> bool {
1204 true
1205 }
1206
1207 pub const fn is_noop(&self) -> bool {
1210 !self.requote && self.direct_fill.is_empty()
1211 }
1212}
1213
1214#[serde_with::serde_as]
1216#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
1217#[serde(rename_all = "camelCase")]
1218pub struct CreateSessionParams {
1219 pub start_slot: u64,
1221 pub end_slot: u64,
1223 #[serde(default)]
1226 #[builder(default)]
1227 pub send_summary: bool,
1228 #[serde(default, skip_serializing_if = "Option::is_none")]
1230 pub reroute: Option<RerouteParams>,
1231 #[serde(default)]
1234 pub capacity_wait_timeout_secs: Option<u16>,
1235 #[serde(default)]
1239 pub disconnect_timeout_secs: Option<u16>,
1240 #[serde(default)]
1245 pub extra_compute_units: Option<u32>,
1246 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1253 #[builder(default)]
1254 pub discoveries: Vec<DiscoveryFilter>,
1255 #[serde(default, with = "compressed", skip_serializing_if = "Vec::is_empty")]
1264 #[builder(default)]
1265 pub actions: Vec<ScheduledAction>,
1266 #[serde(default)]
1269 #[builder(default)]
1270 pub replay_account_state: bool,
1271 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1272 #[builder(default)]
1273 pub diagnostic_probes: Vec<DiagnosticProbeParams>,
1274}
1275
1276pub mod replay_support {
1280 use super::{ActionKind, CreateSessionParams};
1281
1282 pub const NO_TRANSACTIONS: &str =
1284 "Create the session without replayAccountState to execute and persist transactions.";
1285
1286 pub const SEND_TRANSACTION: &str = "Use simulateTransaction to run it against the replayed state, or create the session \
1287 without replayAccountState.";
1288
1289 pub const SEND_ACTION: &str = "Use kind: simulate, which runs against the replayed state without committing, post state \
1290 directly with an action carrying accountOverrides and no transactions, or create the \
1291 session without replayAccountState.";
1292
1293 pub fn unsupported(operation: &str, remedy: &str) -> String {
1295 format!(
1296 "{operation} is not supported while account state replay is enabled: replay \
1297 reconstructs account state from the recorded delta bundle instead of executing \
1298 transactions, so this session never executes or persists any. {remedy}"
1299 )
1300 }
1301
1302 pub fn send_action_conflict(params: &CreateSessionParams) -> Option<String> {
1305 (params.replay_account_state
1306 && params
1307 .actions
1308 .iter()
1309 .any(|action| action.kind == ActionKind::Send))
1310 .then(|| unsupported("A scheduled action with kind: send", SEND_ACTION))
1311 }
1312}
1313
1314impl CreateSessionParams {
1315 pub fn requires_sidecar(&self) -> bool {
1318 self.reroute.as_ref().is_some_and(|reroute| reroute.requote)
1319 || !self.diagnostic_probes.is_empty()
1320 }
1321
1322 pub fn requires_diagnostic_endpoint(&self) -> bool {
1326 !self.diagnostic_probes.is_empty()
1327 }
1328
1329 pub fn validate_diagnostic_probes(&self) -> Result<(), DiagnosticProbeError> {
1330 self.diagnostic_probes
1331 .iter()
1332 .try_for_each(DiagnosticProbeParams::validate)
1333 }
1334
1335 pub fn validate_action_resolvers(&self) -> Result<(), ActionResolveError> {
1336 validate_resolvers(&self.actions)
1337 }
1338
1339 pub fn warms_account_universe(&self) -> bool {
1341 self.requires_sidecar()
1342 || self
1343 .reroute
1344 .as_ref()
1345 .is_some_and(|reroute| !reroute.direct_fill.is_empty())
1346 }
1347
1348 pub fn reroute_order_flow(&self) -> bool {
1349 self.reroute.is_some()
1350 }
1351
1352 pub fn override_schedule(&self) -> Result<OverrideSchedule, String> {
1355 decode_override_actions(&self.actions)
1356 }
1357
1358 pub fn reroute_filter(&self) -> Option<&RerouteFilter> {
1360 self.reroute.as_ref()?.filter.as_ref()
1361 }
1362}
1363
1364#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1367#[serde(rename_all = "kebab-case")]
1368pub enum FailFastDivergenceKind {
1369 #[default]
1372 AnyNonBenign,
1373 Tracked,
1376}
1377
1378impl FailFastDivergenceKind {
1379 pub fn as_str(self) -> &'static str {
1382 match self {
1383 Self::AnyNonBenign => "any-non-benign",
1384 Self::Tracked => "tracked",
1385 }
1386 }
1387
1388 pub fn from_str_opt(value: &str) -> Option<Self> {
1390 match value {
1391 "any-non-benign" => Some(Self::AnyNonBenign),
1392 "tracked" => Some(Self::Tracked),
1393 _ => None,
1394 }
1395 }
1396}
1397
1398#[serde_with::serde_as]
1400#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1401pub struct AccountModifications(
1402 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
1403 #[serde(default)]
1404 pub BTreeMap<Address, AccountData>,
1405);
1406
1407impl From<BTreeMap<Address, AccountData>> for AccountModifications {
1408 fn from(value: BTreeMap<Address, AccountData>) -> Self {
1409 Self(value)
1410 }
1411}
1412
1413impl AccountModifications {
1414 pub fn is_empty(&self) -> bool {
1415 self.0.is_empty()
1416 }
1417}
1418
1419#[serde_with::serde_as]
1421#[derive(Debug, Clone, Serialize, Deserialize)]
1422#[serde(rename_all = "camelCase")]
1423pub struct ContinueParams {
1424 #[serde(default = "ContinueParams::default_advance_count")]
1425 pub advance_count: u64,
1427 #[serde(default)]
1428 pub transactions: Vec<String>,
1430 #[serde(default)]
1431 pub modify_account_states: AccountModifications,
1433}
1434
1435impl Default for ContinueParams {
1436 fn default() -> Self {
1437 Self {
1438 advance_count: Self::default_advance_count(),
1439 transactions: Vec::new(),
1440 modify_account_states: AccountModifications(BTreeMap::new()),
1441 }
1442 }
1443}
1444
1445impl ContinueParams {
1446 pub fn default_advance_count() -> u64 {
1447 1
1448 }
1449}
1450
1451#[derive(Debug, Clone, Serialize, Deserialize)]
1457#[serde(rename_all = "camelCase")]
1458pub struct PausedEvent {
1459 pub slot: u64,
1460 #[serde(default, skip_serializing_if = "Option::is_none")]
1461 pub batch_index: Option<u32>,
1462}
1463
1464#[serde_with::serde_as]
1473#[derive(Debug, Clone, Serialize, Deserialize)]
1474#[serde(rename_all = "camelCase")]
1475pub struct DiscoveryBatchEvent {
1476 pub slot: u64,
1477 pub batch_index: u32,
1478 pub matched: Vec<DiscoveryFilter>,
1480 pub transactions: Vec<EncodedBinary>,
1483 #[serde(default)]
1486 pub transaction_indices: Vec<u32>,
1487}
1488
1489#[derive(Debug, Clone, Serialize, Deserialize)]
1491#[serde(rename_all = "camelCase")]
1492pub struct ContinueToParams {
1493 pub slot: u64,
1495 #[serde(default)]
1501 pub batch_index: Option<u32>,
1502}
1503
1504#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1506#[serde(rename_all = "lowercase")]
1507pub enum BinaryEncoding {
1508 Base64,
1509}
1510
1511impl BinaryEncoding {
1512 pub fn encode(self, bytes: &[u8]) -> String {
1513 match self {
1514 Self::Base64 => BASE64.encode(bytes),
1515 }
1516 }
1517
1518 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
1519 match self {
1520 Self::Base64 => BASE64.decode(data),
1521 }
1522 }
1523}
1524
1525#[derive(Debug, Clone, Serialize, Deserialize)]
1527#[serde(rename_all = "camelCase")]
1528pub struct EncodedBinary {
1529 pub data: String,
1531 pub encoding: BinaryEncoding,
1533}
1534
1535impl EncodedBinary {
1536 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
1537 Self { data, encoding }
1538 }
1539
1540 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
1541 Self {
1542 data: encoding.encode(bytes),
1543 encoding,
1544 }
1545 }
1546
1547 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
1548 self.encoding.decode(&self.data)
1549 }
1550}
1551
1552#[serde_with::serde_as]
1554#[derive(Debug, Clone, Serialize, Deserialize)]
1555#[serde(rename_all = "camelCase")]
1556pub struct AccountData {
1557 pub data: EncodedBinary,
1559 pub executable: bool,
1561 pub lamports: u64,
1563 #[serde_as(as = "serde_with::DisplayFromStr")]
1564 pub owner: Address,
1566 pub space: u64,
1568}
1569
1570impl AccountData {
1571 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
1572 Ok(solana_account::Account {
1573 data: self.data.decode()?,
1574 lamports: self.lamports,
1575 owner: self.owner,
1576 executable: self.executable,
1577 rent_epoch: 0,
1578 })
1579 }
1580}
1581
1582#[derive(Debug, Clone, Serialize, Deserialize)]
1584#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1585pub enum BacktestResponse {
1586 SessionCreated {
1587 session_id: SessionId,
1588 rpc_endpoint: String,
1589 #[serde(default, skip_serializing_if = "Option::is_none")]
1590 task_id: Option<String>,
1591 #[serde(default, skip_serializing_if = "Option::is_none")]
1592 session_name: Option<String>,
1593 },
1594 SessionAttached {
1595 session_id: SessionId,
1596 rpc_endpoint: String,
1597 #[serde(default, skip_serializing_if = "Option::is_none")]
1598 task_id: Option<String>,
1599 #[serde(default, skip_serializing_if = "Option::is_none")]
1600 session_name: Option<String>,
1601 },
1602 SessionsCreatedV2 {
1603 control_session_id: ControlSessionId,
1604 #[serde(default, skip_serializing_if = "Option::is_none")]
1605 control_session_name: Option<String>,
1606 session_ids: Vec<SessionId>,
1607 #[serde(default)]
1608 task_ids: Vec<Option<String>>,
1609 #[serde(default)]
1614 start_slots: Vec<u64>,
1615 #[serde(default)]
1616 end_slots: Vec<u64>,
1617 },
1618 ParallelSessionAttachedV2 {
1619 control_session_id: ControlSessionId,
1620 session_ids: Vec<SessionId>,
1621 #[serde(default)]
1622 task_ids: Vec<Option<String>>,
1623 },
1624 ReadyForContinue,
1625 SlotNotification(u64),
1626 Paused(PausedEvent),
1627 DiscoveryBatch(DiscoveryBatchEvent),
1628 Error(BacktestError),
1629 Success,
1630 Completed {
1631 #[serde(skip_serializing_if = "Option::is_none")]
1635 summary: Option<SessionSummary>,
1636 },
1637 Status {
1638 status: BacktestStatus,
1639 },
1640 SessionEventV2 {
1641 session_id: SessionId,
1642 seq_id: u64,
1643 event: SessionEventKind,
1644 },
1645}
1646
1647impl BacktestResponse {
1648 pub fn is_completed(&self) -> bool {
1649 matches!(self, BacktestResponse::Completed { .. })
1650 }
1651
1652 pub fn is_terminal(&self) -> bool {
1653 match self {
1654 BacktestResponse::Completed { .. } => true,
1655 BacktestResponse::Error(e) => e.is_terminal(),
1656 _ => false,
1657 }
1658 }
1659}
1660
1661impl From<BacktestStatus> for BacktestResponse {
1662 fn from(status: BacktestStatus) -> Self {
1663 Self::Status { status }
1664 }
1665}
1666
1667impl From<String> for BacktestResponse {
1668 fn from(message: String) -> Self {
1669 BacktestError::Internal { error: message }.into()
1670 }
1671}
1672
1673impl From<&str> for BacktestResponse {
1674 fn from(message: &str) -> Self {
1675 BacktestError::Internal {
1676 error: message.to_string(),
1677 }
1678 .into()
1679 }
1680}
1681
1682#[derive(Debug, Clone, Serialize, Deserialize)]
1683#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1684pub enum SessionEventKind {
1685 ReadyForContinue,
1686 SlotNotification(u64),
1687 Paused(PausedEvent),
1688 DiscoveryBatch(DiscoveryBatchEvent),
1689 Error(BacktestError),
1690 Success,
1691 Completed {
1692 #[serde(skip_serializing_if = "Option::is_none")]
1693 summary: Option<SessionSummary>,
1694 },
1695 Status {
1696 status: BacktestStatus,
1697 },
1698}
1699
1700impl SessionEventKind {
1701 pub fn is_terminal(&self) -> bool {
1702 match self {
1703 Self::Completed { .. } => true,
1704 Self::Error(e) => e.is_terminal(),
1705 _ => false,
1706 }
1707 }
1708}
1709
1710#[derive(Debug, Clone, Serialize, Deserialize)]
1713#[serde(rename_all = "camelCase")]
1714pub struct SequencedResponse {
1715 pub seq_id: u64,
1716 #[serde(flatten)]
1717 pub response: BacktestResponse,
1718}
1719
1720#[derive(Debug, Clone, Serialize, Deserialize)]
1722#[serde(rename_all = "camelCase")]
1723pub enum BacktestStatus {
1724 StartingRuntime,
1726 DecodedTransactions,
1727 AppliedAccountModifications,
1728 ReadyToExecuteUserTransactions,
1729 ExecutedUserTransactions,
1730 ExecutingBlockTransactions,
1731 ExecutedBlockTransactions,
1732 ProgramAccountsLoaded,
1733}
1734
1735impl std::fmt::Display for BacktestStatus {
1736 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1737 let s = match self {
1738 Self::StartingRuntime => "starting runtime",
1739 Self::DecodedTransactions => "decoded transactions",
1740 Self::AppliedAccountModifications => "applied account modifications",
1741 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
1742 Self::ExecutedUserTransactions => "executed user transactions",
1743 Self::ExecutingBlockTransactions => "executing block transactions",
1744 Self::ExecutedBlockTransactions => "executed block transactions",
1745 Self::ProgramAccountsLoaded => "program accounts loaded",
1746 };
1747 f.write_str(s)
1748 }
1749}
1750
1751#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1756#[serde(rename_all = "camelCase")]
1757pub struct RerouteStatsReport {
1758 #[serde(default)]
1760 pub swaps_detected: u64,
1761 #[serde(default)]
1765 pub swaps_filtered: u64,
1766 #[serde(default)]
1768 pub swaps_rerouted: u64,
1769 #[serde(default)]
1771 pub swaps_simulated: u64,
1772 #[serde(default)]
1774 pub swaps_succeeded: u64,
1775 #[serde(default)]
1777 pub requote_failures: u64,
1778 #[serde(default)]
1780 pub override_setup_failures: u64,
1781 #[serde(default)]
1783 pub metis_unavailable_blocks: u64,
1784 #[serde(default)]
1788 pub quote_attempts_unavailable: u64,
1789 #[serde(default)]
1792 pub direct_fill_matched: u64,
1793 #[serde(default)]
1797 pub direct_fill_outcomes: BTreeMap<String, u64>,
1798 #[serde(default)]
1800 pub direct_fill_scored: u64,
1801 #[serde(default)]
1805 pub direct_fill_bps_total: i64,
1806 #[serde(default)]
1809 pub direct_fill_templates: BTreeMap<String, DirectFillTemplateReport>,
1810 #[serde(default)]
1812 pub originals_scored: u64,
1813 #[serde(default)]
1816 pub originals_bps_total: i64,
1817 #[serde(default)]
1819 pub originals_replayed: u64,
1820 #[serde(default, skip_serializing_if = "Option::is_none")]
1822 pub dataset_uri: Option<String>,
1823}
1824
1825pub const REROUTE_DATASET_RETENTION_DAYS: u32 = 7;
1827
1828#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1831#[serde(rename_all = "camelCase")]
1832pub struct DirectFillTemplateReport {
1833 pub matched: u64,
1834 pub outcomes: BTreeMap<String, u64>,
1835 pub scored: u64,
1836 pub bps_total: i64,
1837}
1838
1839#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1841#[serde(rename_all = "camelCase")]
1842pub struct SessionSummary {
1843 pub correct_simulation: usize,
1846 pub incorrect_simulation: usize,
1849 pub execution_errors: usize,
1851 pub balance_diff: usize,
1853 pub log_diff: usize,
1855 #[serde(default)]
1858 pub unsanitizable: usize,
1859 #[serde(default, skip_serializing_if = "Option::is_none")]
1862 pub reroute_stats: Option<Box<RerouteStatsReport>>,
1863}
1864
1865impl SessionSummary {
1866 pub fn has_deviations(&self) -> bool {
1868 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
1869 }
1870
1871 pub fn total_transactions(&self) -> usize {
1873 self.correct_simulation
1874 + self.incorrect_simulation
1875 + self.execution_errors
1876 + self.balance_diff
1877 + self.log_diff
1878 }
1879
1880 pub fn deviation_details(&self, balance_label: &str, log_label: &str) -> String {
1885 format!(
1886 "{} incorrect simulations, {} execution errors, {} {balance_label}, {} {log_label}",
1887 self.incorrect_simulation, self.execution_errors, self.balance_diff, self.log_diff,
1888 )
1889 }
1890}
1891
1892impl std::fmt::Display for SessionSummary {
1893 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1894 let total = self.total_transactions();
1895 write!(
1896 f,
1897 "Session summary: {total} transactions\n\
1898 \x20 - {} correct simulation\n\
1899 \x20 - {} incorrect simulation\n\
1900 \x20 - {} execution errors\n\
1901 \x20 - {} unsanitizable\n\
1902 \x20 - {} balance diffs\n\
1903 \x20 - {} log diffs",
1904 self.correct_simulation,
1905 self.incorrect_simulation,
1906 self.execution_errors,
1907 self.unsanitizable,
1908 self.balance_diff,
1909 self.log_diff,
1910 )
1911 }
1912}
1913
1914#[derive(Debug, Clone, Serialize, Deserialize)]
1916#[serde(rename_all = "camelCase")]
1917pub enum BacktestError {
1918 InvalidTransactionEncoding {
1919 index: usize,
1920 error: String,
1921 },
1922 InvalidTransactionFormat {
1923 index: usize,
1924 error: String,
1925 },
1926 InvalidAccountEncoding {
1927 address: String,
1928 encoding: BinaryEncoding,
1929 error: String,
1930 },
1931 InvalidAccountOwner {
1932 address: String,
1933 error: String,
1934 },
1935 InvalidAccountPubkey {
1936 address: String,
1937 error: String,
1938 },
1939 NoMoreBlocks,
1940 BundleNotFound {
1941 start_slot: u64,
1942 end_slot: u64,
1943 },
1944 AdvanceSlotFailed {
1945 slot: u64,
1946 error: String,
1947 },
1948 FinalizeSlotFailed {
1949 slot: u64,
1950 error: String,
1951 },
1952 InvalidRequest {
1953 error: String,
1954 },
1955 Internal {
1956 error: String,
1957 },
1958 InvalidBlockhashFormat {
1959 slot: u64,
1960 error: String,
1961 },
1962 InitializingSysvarsFailed {
1963 slot: u64,
1964 error: String,
1965 },
1966 ClerkError {
1967 error: String,
1968 },
1969 SimulationError {
1970 error: String,
1971 },
1972 SessionNotFound {
1973 session_id: String,
1974 },
1975 SessionOwnerMismatch,
1976 SessionOwnershipBusy {
1981 reason: String,
1982 },
1983 NoProducedBlocksInRange {
1989 start_slot: u64,
1990 end_slot: u64,
1991 },
1992 SessionNameTaken {
1994 session_name: String,
1995 },
1996}
1997
1998impl BacktestError {
1999 pub fn is_terminal(&self) -> bool {
2003 matches!(
2004 self,
2005 BacktestError::NoMoreBlocks
2006 | BacktestError::BundleNotFound { .. }
2007 | BacktestError::AdvanceSlotFailed { .. }
2008 | BacktestError::FinalizeSlotFailed { .. }
2009 | BacktestError::Internal { .. }
2010 | BacktestError::NoProducedBlocksInRange { .. }
2011 )
2012 }
2013}
2014
2015#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2017pub struct AvailableRange {
2018 pub bundle_start_slot: u64,
2019 pub bundle_start_slot_utc: Option<String>,
2020 pub max_bundle_end_slot: Option<u64>,
2021 pub max_bundle_end_slot_utc: Option<String>,
2022 pub max_bundle_size: Option<u64>,
2023 #[serde(default)]
2026 pub kind: Option<BundleKind>,
2027 #[serde(default)]
2030 pub scope: Option<RangeScope>,
2031}
2032
2033#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2035#[serde(rename_all = "snake_case")]
2036pub enum RangeScope {
2037 Global,
2039 User,
2041}
2042
2043impl RangeScope {
2044 pub fn as_str(self) -> &'static str {
2046 match self {
2047 Self::Global => "global",
2048 Self::User => "user",
2049 }
2050 }
2051}
2052
2053#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2055#[serde(rename_all = "snake_case")]
2056pub enum BundleKind {
2057 #[default]
2059 Blocks,
2060 AccountState,
2064}
2065
2066pub const REPLAY_RANGE_ADVICE: &str = "replayed state is resolved by the recording that begins \
2070 where the request does, so request a range that begins where one was recorded and ends \
2071 within it";
2072
2073impl BundleKind {
2074 fn coverage_noun(self) -> &'static str {
2076 match self {
2077 Self::Blocks => "bundle range",
2078 Self::AccountState => "account-state bundle",
2079 }
2080 }
2081
2082 fn coverage_nouns(self) -> &'static str {
2084 match self {
2085 Self::Blocks => "bundle ranges",
2086 Self::AccountState => "account-state bundles",
2087 }
2088 }
2089
2090 pub fn as_str(self) -> &'static str {
2092 match self {
2093 Self::Blocks => "blocks",
2094 Self::AccountState => "account_state",
2095 }
2096 }
2097
2098 pub fn from_str_opt(value: &str) -> Option<Self> {
2100 match value {
2101 "blocks" => Some(Self::Blocks),
2102 "account_state" => Some(Self::AccountState),
2103 _ => None,
2104 }
2105 }
2106}
2107
2108impl fmt::Display for BundleKind {
2111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2112 let label = match self {
2113 Self::Blocks => "transactions",
2114 Self::AccountState => "account_state",
2115 };
2116 f.pad(label)
2117 }
2118}
2119
2120#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
2125#[serde(rename_all = "snake_case")]
2126pub enum JobKind {
2127 #[default]
2129 BundleBuild,
2130 Depth,
2132}
2133
2134impl JobKind {
2135 pub fn as_str(self) -> &'static str {
2137 match self {
2138 Self::BundleBuild => "bundle_build",
2139 Self::Depth => "depth",
2140 }
2141 }
2142
2143 pub fn from_str_opt(value: &str) -> Option<Self> {
2145 match value {
2146 "bundle_build" => Some(Self::BundleBuild),
2147 "depth" => Some(Self::Depth),
2148 _ => None,
2149 }
2150 }
2151}
2152
2153#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
2158#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
2159#[serde(rename_all = "snake_case")]
2160pub enum DepthSource {
2161 #[default]
2163 Onchain,
2164 Router,
2166}
2167
2168impl DepthSource {
2169 pub const fn as_str(self) -> &'static str {
2171 match self {
2172 Self::Onchain => "onchain",
2173 Self::Router => "router",
2174 }
2175 }
2176
2177 pub fn from_str_opt(value: &str) -> Option<Self> {
2179 match value {
2180 "onchain" => Some(Self::Onchain),
2181 "router" => Some(Self::Router),
2182 _ => None,
2183 }
2184 }
2185
2186 pub const fn discriminant(self) -> u8 {
2191 match self {
2192 Self::Onchain => 0,
2193 Self::Router => 1,
2194 }
2195 }
2196}
2197
2198impl fmt::Display for DepthSource {
2199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2200 f.write_str(self.as_str())
2201 }
2202}
2203
2204#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
2206#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
2207#[serde(rename_all = "snake_case")]
2208pub enum DepthGranularity {
2209 #[default]
2210 Slot,
2211 Oracle,
2213}
2214
2215impl DepthGranularity {
2216 pub const fn as_str(self) -> &'static str {
2217 match self {
2218 Self::Slot => "slot",
2219 Self::Oracle => "oracle",
2220 }
2221 }
2222
2223 pub fn is_default(&self) -> bool {
2224 *self == Self::default()
2225 }
2226
2227 pub const fn discriminant(self) -> u8 {
2229 match self {
2230 Self::Slot => 0,
2231 Self::Oracle => 1,
2232 }
2233 }
2234}
2235
2236impl fmt::Display for DepthGranularity {
2237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2238 f.write_str(self.as_str())
2239 }
2240}
2241
2242#[derive(Debug, Clone, Serialize, Deserialize)]
2247pub struct DepthCoverageResponse {
2248 pub sources: Vec<DepthSourceCoverage>,
2249}
2250
2251#[derive(Debug, Clone, Serialize, Deserialize)]
2252pub struct DepthSourceCoverage {
2253 pub source: DepthSource,
2255 pub revision: u16,
2258 pub markets: Vec<String>,
2260 pub venues: Vec<String>,
2262 pub size_rungs: usize,
2264 pub completed_ranges: Vec<SlotRange>,
2267}
2268
2269pub const MAX_PROGRAM_OVERRIDES: usize = 8;
2271pub const MAX_ACCOUNT_OVERRIDES: usize = 64;
2273pub const MAX_TOTAL_ELF_BYTES: usize = 8 * 1024 * 1024;
2276pub const MAX_ACCOUNT_OVERRIDE_BYTES: usize = 128 * 1024;
2279
2280#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2286pub struct DepthRequest {
2287 pub start_slot: u64,
2290 pub end_slot: u64,
2292 #[serde(default)]
2295 pub source: DepthSource,
2296 #[serde(default, skip_serializing_if = "DepthGranularity::is_default")]
2297 pub granularity: DepthGranularity,
2298 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2300 pub markets: Vec<String>,
2301 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2303 pub program_overrides: Vec<ProgramOverride>,
2304 #[serde(default, skip_serializing_if = "AccountModifications::is_empty")]
2307 pub account_overrides: AccountModifications,
2308}
2309
2310#[serde_with::serde_as]
2313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2314pub struct ProgramOverride {
2315 #[serde_as(as = "serde_with::DisplayFromStr")]
2316 pub program_id: Address,
2317 #[serde_as(as = "serde_with::base64::Base64")]
2318 pub elf: Vec<u8>,
2319}
2320
2321#[derive(Debug, Clone, Serialize, Deserialize)]
2323pub struct DepthStatusResponse {
2324 pub request_id: String,
2325 pub start_slot: u64,
2326 pub end_slot: u64,
2327 pub source: DepthSource,
2328 #[serde(default, skip_serializing_if = "DepthGranularity::is_default")]
2329 pub granularity: DepthGranularity,
2330 pub status: BundleBuildStatus,
2331 #[serde(default, skip_serializing_if = "Option::is_none")]
2334 pub capture: Option<SlotRange>,
2335 #[serde(default, skip_serializing_if = "Option::is_none")]
2339 pub modification_digest: Option<String>,
2340}
2341
2342#[serde_with::serde_as]
2346#[derive(Debug, Clone, Serialize, Deserialize)]
2347pub struct DepthModifications {
2348 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, serde_with::base64::Base64>")]
2349 pub programs: BTreeMap<Address, Vec<u8>>,
2350 pub accounts: AccountModifications,
2351}
2352
2353#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2357pub struct BundleBuildRequest {
2358 pub start_slot: u64,
2359 pub end_slot: u64,
2360 #[serde(default)]
2361 pub bundle_size: Option<u64>,
2362 #[serde(default)]
2365 pub idempotency_key: Option<String>,
2366 #[serde(default)]
2368 pub kind: BundleKind,
2369}
2370
2371#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2372#[serde(rename_all = "snake_case")]
2373pub enum BundleBuildStatus {
2374 Completed,
2376 Failed,
2378 InProgress,
2380 NeedsInvestigation,
2383}
2384
2385impl BundleBuildStatus {
2386 pub fn as_str(self) -> &'static str {
2388 match self {
2389 Self::Completed => "completed",
2390 Self::Failed => "failed",
2391 Self::InProgress => "in_progress",
2392 Self::NeedsInvestigation => "needs_investigation",
2393 }
2394 }
2395}
2396
2397#[derive(Debug, Clone, Serialize, Deserialize)]
2399pub struct BundleBuildStatusResponse {
2400 pub request_id: String,
2401 pub start_slot: u64,
2402 pub end_slot: u64,
2403 pub bundle_size: Option<u64>,
2404 pub status: BundleBuildStatus,
2405 pub created_at_unix_ms: u64,
2408 #[serde(default)]
2410 pub kind: BundleKind,
2411 #[serde(default)]
2414 pub eta_secs: Option<u64>,
2415}
2416
2417#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2419pub struct CompletedBundle {
2420 pub start_slot: u64,
2421 pub end_slot: u64,
2422 pub kind: BundleKind,
2423 #[serde(default)]
2425 pub completed_at_unix_ms: Option<i64>,
2426 #[serde(default)]
2427 pub size_bytes: Option<i64>,
2428}
2429
2430#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2432pub struct BundleDeleteRequest {
2433 pub start_slot: u64,
2434 pub end_slot: u64,
2435 #[serde(default)]
2437 pub kind: BundleKind,
2438}
2439
2440#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2442pub struct BundleDeleteResponse {
2443 pub start_slot: u64,
2444 pub end_slot: u64,
2445 pub kind: BundleKind,
2446}
2447
2448#[derive(Debug, Clone, Serialize, Deserialize)]
2450pub struct BundleBuildListResponse {
2451 pub builds: Vec<BundleBuildStatusResponse>,
2454 #[serde(default)]
2459 pub bundles: Option<Vec<CompletedBundle>>,
2460}
2461
2462#[derive(Debug, Clone, Serialize, Deserialize)]
2465pub struct SnapshotSlotResponse {
2466 pub snapshot_slot: u64,
2467}
2468
2469#[derive(Debug, Clone, Serialize, Deserialize)]
2472pub struct DatasetCredentialsResponse {
2473 pub access_key_id: String,
2474 pub secret_access_key: String,
2475 pub session_token: String,
2476 pub expiration: Option<chrono::DateTime<chrono::Utc>>,
2477 pub s3_uri: String,
2478 #[serde(default, skip_serializing_if = "Option::is_none")]
2481 pub user_s3_uri: Option<String>,
2482 pub region: Option<String>,
2483}
2484
2485pub fn split_range(
2508 ranges: &[AvailableRange],
2509 requested: SlotRange,
2510 kind: BundleKind,
2511) -> Result<Vec<(u64, u64)>, String> {
2512 split_range_preferring(ranges, requested, kind, Prefer::Most)
2513}
2514
2515pub fn split_range_fewest(
2519 ranges: &[AvailableRange],
2520 requested: SlotRange,
2521 kind: BundleKind,
2522) -> Result<Vec<(u64, u64)>, String> {
2523 split_range_preferring(ranges, requested, kind, Prefer::Fewest)
2524}
2525
2526#[derive(Clone, Copy)]
2528enum Prefer {
2529 Most,
2530 Fewest,
2531}
2532
2533fn split_range_preferring(
2536 ranges: &[AvailableRange],
2537 requested: SlotRange,
2538 kind: BundleKind,
2539 prefer: Prefer,
2540) -> Result<Vec<(u64, u64)>, String> {
2541 let (requested_start, requested_end) = (requested.start(), requested.end());
2542
2543 let ends_by_start = ranges
2550 .iter()
2551 .filter(|r| r.kind.unwrap_or_default() == kind)
2552 .filter_map(|r| {
2553 r.max_bundle_end_slot
2554 .filter(|&end| end >= r.bundle_start_slot)
2555 .map(|end| (r.bundle_start_slot, end))
2556 })
2557 .fold(
2558 BTreeMap::<u64, BTreeSet<u64>>::new(),
2559 |mut ends_by_start, (start, end)| {
2560 ends_by_start.entry(start).or_default().insert(end);
2561 ends_by_start
2562 },
2563 );
2564
2565 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
2573 ends.iter()
2574 .next_back()
2575 .is_some_and(|&end| end >= requested_start)
2576 }) else {
2577 return Err(format!(
2578 "start_slot {requested_start} is not covered by any available {}",
2579 kind.coverage_noun()
2580 ));
2581 };
2582
2583 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
2587 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
2588 let mut best: Option<Vec<(u64, u64)>> = None;
2589 for &end in ends {
2590 let candidate = if end >= requested_end {
2591 Some(vec![(start, requested_end)])
2592 } else {
2593 best_from.get(&(end + 1)).map(|rest| {
2594 std::iter::once((start, end))
2595 .chain(rest.iter().copied())
2596 .collect()
2597 })
2598 };
2599 if let Some(candidate) = candidate
2600 && best.as_ref().is_none_or(|b| match prefer {
2601 Prefer::Most => candidate.len() > b.len(),
2602 Prefer::Fewest => candidate.len() < b.len(),
2603 })
2604 {
2605 best = Some(candidate);
2606 }
2607 }
2608 if let Some(best) = best {
2609 best_from.insert(start, best);
2610 }
2611 }
2612
2613 best_from.remove(&anchor_start).ok_or_else(|| {
2614 let mut covered_to = anchor_start.saturating_sub(1);
2618 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
2619 if start > covered_to.saturating_add(1) {
2620 break;
2621 }
2622 if let Some(&end) = ends.iter().next_back() {
2623 covered_to = covered_to.max(end);
2624 }
2625 }
2626 if covered_to < requested_end {
2627 format!("gap in coverage at slot {}", covered_to + 1)
2628 } else {
2629 format!(
2630 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available {}",
2631 kind.coverage_nouns()
2632 )
2633 }
2634 })
2635}
2636
2637impl From<BacktestError> for BacktestResponse {
2638 fn from(error: BacktestError) -> Self {
2639 Self::Error(error)
2640 }
2641}
2642
2643impl std::error::Error for BacktestError {}
2644
2645impl fmt::Display for BacktestError {
2646 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2647 match self {
2648 BacktestError::InvalidTransactionEncoding { index, error } => {
2649 write!(f, "invalid transaction encoding at index {index}: {error}")
2650 }
2651 BacktestError::InvalidTransactionFormat { index, error } => {
2652 write!(f, "invalid transaction format at index {index}: {error}")
2653 }
2654 BacktestError::InvalidAccountEncoding {
2655 address,
2656 encoding,
2657 error,
2658 } => write!(
2659 f,
2660 "invalid encoding for account {address} ({encoding:?}): {error}"
2661 ),
2662 BacktestError::InvalidAccountOwner { address, error } => {
2663 write!(f, "invalid owner for account {address}: {error}")
2664 }
2665 BacktestError::InvalidAccountPubkey { address, error } => {
2666 write!(f, "invalid account pubkey {address}: {error}")
2667 }
2668 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
2669 BacktestError::BundleNotFound {
2670 start_slot,
2671 end_slot,
2672 } => write!(
2673 f,
2674 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
2675 ),
2676 BacktestError::AdvanceSlotFailed { slot, error } => {
2677 write!(f, "failed to advance to slot {slot}: {error}")
2678 }
2679 BacktestError::FinalizeSlotFailed { slot, error } => {
2680 write!(f, "failed to finalize slot {slot}: {error}")
2681 }
2682 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
2683 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
2684 BacktestError::InvalidBlockhashFormat { slot, error } => {
2685 write!(f, "invalid blockhash at slot {slot}: {error}")
2686 }
2687 BacktestError::InitializingSysvarsFailed { slot, error } => {
2688 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
2689 }
2690 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
2691 BacktestError::SimulationError { error } => {
2692 write!(f, "simulation error: {error}")
2693 }
2694 BacktestError::SessionNotFound { session_id } => {
2695 write!(f, "session not found: {session_id}")
2696 }
2697 BacktestError::SessionOwnerMismatch => {
2698 write!(f, "session owner mismatch")
2699 }
2700 BacktestError::SessionOwnershipBusy { reason } => {
2701 write!(f, "session ownership busy: {reason}")
2702 }
2703 BacktestError::NoProducedBlocksInRange {
2704 start_slot,
2705 end_slot,
2706 } => write!(
2707 f,
2708 "no produced blocks in requested range {start_slot}..={end_slot}; \
2709 slot(s) may be skipped on-chain"
2710 ),
2711 BacktestError::SessionNameTaken { session_name } => {
2712 write!(f, "session name '{session_name}' is already in use")
2713 }
2714 }
2715 }
2716}
2717
2718#[cfg(test)]
2719mod tests {
2720 use rstest::rstest;
2721 use solana_account::ReadableAccount;
2722
2723 use super::*;
2724
2725 #[test]
2728 fn a_sources_discriminant_is_frozen_independently_of_its_label() {
2729 assert_eq!(DepthSource::Onchain.discriminant(), 0);
2730 assert_eq!(DepthSource::Router.discriminant(), 1);
2731 }
2732
2733 #[test]
2736 fn a_source_spells_itself_the_same_way_three_times() {
2737 for source in [DepthSource::Onchain, DepthSource::Router] {
2738 assert_eq!(
2739 serde_json::to_value(source).expect("a source serialises"),
2740 serde_json::json!(source.as_str())
2741 );
2742 assert_eq!(DepthSource::from_str_opt(source.as_str()), Some(source));
2743 }
2744 }
2745
2746 fn address(byte: u8) -> Address {
2747 Address::from([byte; 32])
2748 }
2749
2750 #[rstest]
2751 #[case::one("jupiter", Some(&[SwapAggregator::Jupiter][..]))]
2752 #[case::several("jupiter,okx", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2753 #[case::spaced_and_uppercased(" Jupiter , OKX ", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2754 #[case::repeats_collapse("okx,okx", Some(&[SwapAggregator::Okx][..]))]
2755 #[case::trailing_separator("okx,", Some(&[SwapAggregator::Okx][..]))]
2756 #[case::unknown("jupiter,bogus", None)]
2757 #[case::empty("", None)]
2758 #[case::separators_only(",,", None)]
2759 fn reroute_aggregators_parse_the_shapes_a_flag_carries(
2760 #[case] input: &str,
2761 #[case] expected: Option<&[SwapAggregator]>,
2762 ) {
2763 assert_eq!(
2764 input.parse::<RerouteAggregators>().ok(),
2765 expected.map(|aggregators| RerouteAggregators::new(aggregators.iter().copied()))
2766 );
2767 }
2768
2769 #[test]
2771 fn reroute_aggregators_round_trip_through_their_rendering() {
2772 let aggregators = RerouteAggregators::new([SwapAggregator::Dflow, SwapAggregator::Jupiter]);
2773
2774 assert_eq!(aggregators.to_string(), "jupiter,dflow");
2775 assert_eq!(
2776 aggregators
2777 .to_string()
2778 .parse::<RerouteAggregators>()
2779 .unwrap(),
2780 aggregators
2781 );
2782 assert_eq!(RerouteAggregators::default().to_string(), "jupiter");
2783 }
2784
2785 #[test]
2788 fn aggregators_deserialize_through_the_same_parser_the_flag_uses() {
2789 let params: CreateSessionParams = serde_json::from_value(
2790 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {"aggregators": ["Jupiter", "OKX"]}}),
2791 )
2792 .expect("decode");
2793
2794 assert_eq!(
2795 params.reroute.expect("rerouting requested").aggregators,
2796 RerouteAggregators::new([SwapAggregator::Jupiter, SwapAggregator::Okx])
2797 );
2798 }
2799
2800 #[test]
2803 fn params_without_the_new_reroute_fields_decode_to_the_narrow_defaults() {
2804 let params: CreateSessionParams = serde_json::from_value(
2805 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {}}),
2806 )
2807 .expect("decode");
2808
2809 let reroute = params.reroute.expect("rerouting requested");
2810 assert_eq!(reroute.aggregators, RerouteAggregators::default());
2811 assert!(reroute.aggregators.contains(SwapAggregator::Jupiter));
2812 assert!(!reroute.aggregators.contains(SwapAggregator::Okx));
2813 assert!(!reroute.circular_arbs);
2814 assert!(!reroute.detect_failed_l1_swaps);
2815 }
2816
2817 #[test]
2819 fn a_request_without_reroute_reserves_no_sidecar() {
2820 let params: CreateSessionParams =
2821 serde_json::from_value(serde_json::json!({"startSlot": 1, "endSlot": 2}))
2822 .expect("decode");
2823
2824 assert!(!params.requires_sidecar());
2825 }
2826
2827 #[test]
2832 fn a_per_slot_action_schedule_compresses_across_actions() {
2833 let slots = 2_000u64;
2834 let actions = (0..slots)
2835 .map(|slot| ScheduledAction {
2836 anchor: ActionAnchor::AfterSlot { slots: vec![slot] },
2837 kind: ActionKind::Simulate,
2838 feeds_reroute: false,
2839 transactions: Vec::new(),
2840 account_overrides: AccountModifications(BTreeMap::from([(
2841 address(7),
2842 AccountData {
2843 data: EncodedBinary::new(
2845 BASE64.encode(
2846 [&slot.to_le_bytes()[..], &[0xAB; 2040][..]]
2847 .concat()
2848 .as_slice(),
2849 ),
2850 BinaryEncoding::Base64,
2851 ),
2852 executable: false,
2853 lamports: 15_144_966,
2854 owner: address(9),
2855 space: 2048,
2856 },
2857 )])),
2858 return_accounts: Vec::new(),
2859 label: None,
2860 resolve: None,
2861 })
2862 .collect::<Vec<_>>();
2863 let params = CreateSessionParams::builder()
2864 .start_slot(1)
2865 .end_slot(slots)
2866 .actions(actions.clone())
2867 .build();
2868
2869 let encoded = serde_json::to_string(¶ms).expect("serialize");
2870 let raw = serde_json::to_string(&actions).expect("serialize raw actions");
2871 assert!(
2872 encoded.len() * 8 < raw.len(),
2873 "expected the schedule to compress well; got {} bytes from {} raw",
2874 encoded.len(),
2875 raw.len()
2876 );
2877
2878 let decoded: CreateSessionParams = serde_json::from_str(&encoded).expect("round trip");
2879 assert_eq!(
2880 serde_json::to_string(&decoded.actions).expect("serialize decoded"),
2881 raw
2882 );
2883 }
2884
2885 #[test]
2888 fn an_empty_action_list_is_omitted_entirely() {
2889 let params = CreateSessionParams::builder()
2890 .start_slot(1)
2891 .end_slot(2)
2892 .build();
2893 let json = serde_json::to_value(¶ms).expect("serialize");
2894 assert!(json.get("actions").is_none(), "{json}");
2895 let decoded: CreateSessionParams = serde_json::from_value(json).expect("round trip");
2896 assert!(decoded.actions.is_empty());
2897 }
2898
2899 #[test]
2902 fn send_actions_conflict_with_replay_only_together() {
2903 let params = |replay, kind| {
2904 CreateSessionParams::builder()
2905 .start_slot(1)
2906 .end_slot(2)
2907 .replay_account_state(replay)
2908 .actions(vec![ScheduledAction {
2909 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2910 kind,
2911 feeds_reroute: false,
2912 transactions: vec!["dGVzdA==".to_string()],
2913 account_overrides: Default::default(),
2914 return_accounts: Default::default(),
2915 label: None,
2916 resolve: None,
2917 }])
2918 .build()
2919 };
2920 assert!(replay_support::send_action_conflict(¶ms(false, ActionKind::Send)).is_none());
2921 assert!(
2922 replay_support::send_action_conflict(¶ms(true, ActionKind::Simulate)).is_none()
2923 );
2924 let mut publishing = params(true, ActionKind::Simulate);
2925 publishing.actions[0].feeds_reroute = true;
2926 assert!(replay_support::send_action_conflict(&publishing).is_none());
2927
2928 let conflict = replay_support::send_action_conflict(¶ms(true, ActionKind::Send))
2929 .expect("send under replay conflicts");
2930 assert!(conflict.contains("kind: send"), "{conflict}");
2931 assert!(conflict.contains(replay_support::SEND_ACTION), "{conflict}");
2932 }
2933
2934 fn override_entry(owner: Address, executable: bool) -> AccountData {
2935 AccountData {
2936 data: EncodedBinary::from_bytes(&[1, 2, 3], BinaryEncoding::Base64),
2937 executable,
2938 lamports: 1,
2939 owner,
2940 space: 3,
2941 }
2942 }
2943
2944 fn before() -> ActionAnchor {
2945 ActionAnchor::BeforeSlot { slots: vec![1] }
2946 }
2947
2948 fn valid() -> ScheduledAction {
2949 override_action(before(), address(3), override_entry(address(4), false))
2950 }
2951
2952 fn override_action(
2953 anchor: ActionAnchor,
2954 account: Address,
2955 data: AccountData,
2956 ) -> ScheduledAction {
2957 ScheduledAction {
2958 anchor,
2959 kind: ActionKind::default(),
2960 transactions: Vec::new(),
2961 account_overrides: AccountModifications(BTreeMap::from([(account, data)])),
2962 return_accounts: Vec::new(),
2963 label: None,
2964 feeds_reroute: true,
2965 resolve: None,
2966 }
2967 }
2968
2969 #[test]
2970 fn override_action_round_trips() {
2971 let key = address(1);
2972 let entry = override_entry(address(2), false);
2973 let action = override_action(
2974 ActionAnchor::BeforeSlot { slots: vec![5, 9] },
2975 key,
2976 entry.clone(),
2977 );
2978 let json = serde_json::to_value(&action).unwrap();
2979 assert!(
2980 json["anchor"]["slots"].is_string(),
2981 "slots travel as a compressed blob: {json}"
2982 );
2983 let back: ScheduledAction = serde_json::from_value(json).unwrap();
2984 assert_eq!(back.anchor, ActionAnchor::BeforeSlot { slots: vec![5, 9] });
2985 assert!(back.feeds_reroute);
2986 assert!(!back.is_computed_override(), "no transactions to simulate");
2987 let survived = &back.account_overrides.0[&key];
2988 assert_eq!(survived.lamports, entry.lamports);
2989 assert_eq!(survived.data.data, entry.data.data);
2990 }
2991
2992 #[test]
2993 fn an_every_slot_anchor_round_trips_and_defaults_to_every_slot() {
2994 let anchor = ActionAnchor::AfterEverySlot {
2995 every_n_slots: NonZeroU64::new(10).unwrap(),
2996 };
2997 let json = serde_json::to_value(&anchor).unwrap();
2998 assert_eq!(
2999 json,
3000 serde_json::json!({ "at": "afterEverySlot", "everyNSlots": 10 })
3001 );
3002 assert_eq!(
3003 serde_json::from_value::<ActionAnchor>(json).unwrap(),
3004 anchor
3005 );
3006
3007 assert_eq!(
3008 serde_json::from_value::<ActionAnchor>(serde_json::json!({ "at": "beforeEverySlot" }))
3009 .unwrap(),
3010 ActionAnchor::BeforeEverySlot {
3011 every_n_slots: NonZeroU64::MIN
3012 }
3013 );
3014 assert!(
3015 serde_json::from_value::<ActionAnchor>(
3016 serde_json::json!({ "at": "beforeEverySlot", "everyNSlots": 0 })
3017 )
3018 .is_err(),
3019 "a zero cadence names no slot"
3020 );
3021 }
3022
3023 fn context(
3024 invoked: &[u8],
3025 moves_tokens: bool,
3026 written: &[u8],
3027 writes_via_unresolved_lookup: bool,
3028 ) -> TxMatchContext {
3029 TxMatchContext {
3030 invoked_programs: invoked.iter().copied().map(address).collect(),
3031 moves_tokens,
3032 written_accounts: written.iter().copied().map(address).collect(),
3033 writes_via_unresolved_lookup,
3034 }
3035 }
3036
3037 #[rstest]
3039 #[case::update(&[], context(&[1], false, &[11], false), true)]
3040 #[case::moves_tokens(&[], context(&[1], true, &[11], false), false)]
3041 #[case::second_program_of_the_set(&[], context(&[2], false, &[11], false), true)]
3042 #[case::other_program(&[], context(&[3], false, &[11], false), false)]
3043 #[case::write_to_the_scope(&[10], context(&[1], false, &[11, 10], false), true)]
3044 #[case::write_elsewhere(&[10], context(&[1], false, &[11], false), false)]
3045 #[case::unresolved_write_through_a_lookup_table(&[10], context(&[1], false, &[11], true), true)]
3046 fn an_oracle_update_is_a_token_free_write_by_a_listed_program(
3047 #[case] accounts: &[u8],
3048 #[case] ctx: TxMatchContext,
3049 #[case] expected: bool,
3050 ) {
3051 let filter = DiscoveryFilter::OracleUpdate {
3052 programs: BTreeSet::from([address(1), address(2)]),
3053 accounts: accounts.iter().copied().map(address).collect(),
3054 };
3055 assert_eq!(filter.matches(&ctx), expected);
3056 }
3057
3058 #[rstest]
3059 #[case::unscoped(
3060 &[],
3061 serde_json::json!({ "kind": "oracleUpdate", "value": { "programs": [address(1).to_string()] } })
3062 )]
3063 #[case::scoped(
3064 &[2],
3065 serde_json::json!({
3066 "kind": "oracleUpdate",
3067 "value": { "programs": [address(1).to_string()], "accounts": [address(2).to_string()] }
3068 })
3069 )]
3070 fn an_oracle_update_filter_round_trips_as_kind_and_value(
3071 #[case] accounts: &[u8],
3072 #[case] expected: serde_json::Value,
3073 ) {
3074 let filter = DiscoveryFilter::OracleUpdate {
3075 programs: BTreeSet::from([address(1)]),
3076 accounts: accounts.iter().copied().map(address).collect(),
3077 };
3078 let json = serde_json::to_value(&filter).unwrap();
3079
3080 assert_eq!(json, expected);
3081 assert_eq!(
3082 serde_json::from_value::<DiscoveryFilter>(json).unwrap(),
3083 filter
3084 );
3085 }
3086
3087 #[test]
3092 fn a_token_account_override_is_accepted() {
3093 let action = override_action(
3094 before(),
3095 address(3),
3096 override_entry(spl_token_interface::ID, false),
3097 );
3098 let schedule = decode_override_actions(&[action]).expect("token overrides decode");
3099 assert!(!schedule.active_at(1).is_empty());
3100 }
3101
3102 #[rstest]
3103 #[case::executable(override_action(before(), address(3), override_entry(address(10), true)))]
3104 #[case::program_owned(override_action(
3105 before(),
3106 address(3),
3107 override_entry(solana_account::PROGRAM_OWNERS[0], false),
3108 ))]
3109 #[case::after_slot_anchor(override_action(
3110 ActionAnchor::AfterSlot { slots: vec![1] },
3111 address(3),
3112 override_entry(address(4), false),
3113 ))]
3114 #[case::every_slot_anchor(override_action(
3115 ActionAnchor::BeforeEverySlot { every_n_slots: NonZeroU64::MIN },
3116 address(3),
3117 override_entry(address(4), false),
3118 ))]
3119 #[case::nothing_to_post(ScheduledAction {
3120 account_overrides: AccountModifications::default(),
3121 ..valid()
3122 })]
3123 #[case::send(ScheduledAction {
3125 kind: ActionKind::Send,
3126 ..valid()
3127 })]
3128 #[allow(deprecated, reason = "exercise the loader-owner rejection path")]
3129 fn override_actions_reject_invalid_accounts_and_shapes(#[case] action: ScheduledAction) {
3130 assert!(decode_override_actions(&[action]).is_err());
3131 }
3132
3133 fn liquidity_arrays(array_key_indices: Vec<u8>) -> TxResolver {
3134 TxResolver::LiquidityArrays {
3135 pool: address(1),
3136 input_mint: address(2),
3137 array_key_indices: array_key_indices.try_into().unwrap(),
3138 }
3139 }
3140
3141 fn resolved_action() -> ScheduledAction {
3142 ScheduledAction {
3143 anchor: ActionAnchor::AfterSlot { slots: vec![5] },
3144 kind: ActionKind::Simulate,
3145 transactions: vec!["dGVzdA==".to_string()],
3146 account_overrides: AccountModifications::default(),
3147 feeds_reroute: false,
3148 return_accounts: Vec::new(),
3149 label: None,
3150 resolve: Some(liquidity_arrays(vec![6, 7, 8])),
3151 }
3152 }
3153
3154 fn validate_session_resolvers(actions: Vec<ScheduledAction>) -> Result<(), ActionResolveError> {
3155 CreateSessionParams::builder()
3156 .start_slot(1)
3157 .end_slot(10)
3158 .actions(actions)
3159 .build()
3160 .validate_action_resolvers()
3161 }
3162
3163 #[test]
3164 fn a_liquidity_arrays_resolver_serializes_as_a_type_tagged_camel_case_object() {
3165 let resolver = liquidity_arrays(vec![6, 7, 8]);
3166 let json = serde_json::to_value(&resolver).unwrap();
3167 assert_eq!(
3168 json,
3169 serde_json::json!({
3170 "type": "liquidityArrays",
3171 "pool": address(1).to_string(),
3172 "inputMint": address(2).to_string(),
3173 "arrayKeyIndices": [6, 7, 8],
3174 })
3175 );
3176 assert_eq!(
3177 serde_json::from_value::<TxResolver>(json).unwrap(),
3178 resolver
3179 );
3180 }
3181
3182 #[test]
3183 fn a_quote_stamp_resolver_serializes_as_its_type_tag_alone() {
3184 let json = serde_json::to_value(TxResolver::QuoteStamp).unwrap();
3185 assert_eq!(json, serde_json::json!({ "type": "quoteStamp" }));
3186 assert_eq!(
3187 serde_json::from_value::<TxResolver>(json).unwrap(),
3188 TxResolver::QuoteStamp
3189 );
3190 }
3191
3192 #[test]
3193 fn an_action_without_a_resolver_omits_the_key_and_reads_back_as_none() {
3194 let action = ScheduledAction {
3195 resolve: None,
3196 ..resolved_action()
3197 };
3198 let json = serde_json::to_value(&action).unwrap();
3199 assert!(json.get("resolve").is_none(), "{json}");
3200 let back: ScheduledAction = serde_json::from_value(json).unwrap();
3201 assert_eq!(back.resolve, None);
3202 }
3203
3204 #[test]
3205 fn a_well_formed_resolver_and_an_unresolved_action_pass_validation() {
3206 let unresolved = ScheduledAction {
3207 resolve: None,
3208 kind: ActionKind::Send,
3209 ..resolved_action()
3210 };
3211 assert_eq!(
3212 validate_session_resolvers(vec![unresolved, resolved_action()]),
3213 Ok(())
3214 );
3215 }
3216
3217 #[test]
3218 fn a_quote_stamp_may_rewrite_a_transaction_repeated_per_slot() {
3219 let stamped = ScheduledAction {
3220 anchor: ActionAnchor::BeforeSlot { slots: vec![5, 6] },
3221 transactions: vec!["dGVzdA==".to_string(); 2],
3222 resolve: Some(TxResolver::QuoteStamp),
3223 ..resolved_action()
3224 };
3225 assert_eq!(validate_session_resolvers(vec![stamped]), Ok(()));
3226 }
3227
3228 #[rstest]
3229 #[case::send(
3230 ScheduledAction { kind: ActionKind::Send, ..resolved_action() },
3231 TxResolverError::Send,
3232 )]
3233 #[case::no_transactions(
3234 ScheduledAction { transactions: Vec::new(), ..resolved_action() },
3235 TxResolverError::TransactionCount { got: 0 },
3236 )]
3237 #[case::two_transactions(
3238 ScheduledAction { transactions: vec!["dGVzdA==".to_string(); 2], ..resolved_action() },
3239 TxResolverError::TransactionCount { got: 2 },
3240 )]
3241 #[case::feeds_reroute(
3242 ScheduledAction { feeds_reroute: true, ..resolved_action() },
3243 TxResolverError::FeedsReroute,
3244 )]
3245 #[case::quote_stamp_without_transactions(
3246 ScheduledAction { transactions: Vec::new(), resolve: Some(TxResolver::QuoteStamp), ..resolved_action() },
3247 TxResolverError::NoTransactions,
3248 )]
3249 fn a_resolver_on_an_action_it_cannot_rewrite_is_rejected_at_its_index(
3250 #[case] action: ScheduledAction,
3251 #[case] expected: TxResolverError,
3252 ) {
3253 assert_eq!(
3254 validate_session_resolvers(vec![resolved_action(), action]),
3255 Err(ActionResolveError {
3256 action: 1,
3257 source: expected,
3258 })
3259 );
3260 }
3261
3262 #[rstest]
3263 #[case::empty(Vec::new(), ArrayKeyIndicesError::Empty)]
3264 #[case::duplicate(vec![6, 7, 6], ArrayKeyIndicesError::Duplicate { index: 6 })]
3265 fn array_key_indices_refuse_an_empty_or_repeating_list(
3266 #[case] indices: Vec<u8>,
3267 #[case] expected: ArrayKeyIndicesError,
3268 ) {
3269 assert_eq!(ArrayKeyIndices::try_from(indices), Err(expected));
3270 }
3271
3272 #[test]
3273 fn a_resolver_with_unusable_array_key_indices_fails_to_deserialize() {
3274 let json = serde_json::json!({
3275 "type": "liquidityArrays",
3276 "pool": address(1).to_string(),
3277 "inputMint": address(2).to_string(),
3278 "arrayKeyIndices": [6, 7, 6],
3279 });
3280 assert!(serde_json::from_value::<TxResolver>(json).is_err());
3281 }
3282
3283 fn diagnostic_probe() -> DiagnosticProbeParams {
3284 DiagnosticProbeParams {
3285 anchor: ActionAnchor::AfterEverySlot {
3286 every_n_slots: NonZeroU64::MIN,
3287 },
3288 markets: vec![address(1), address(2)],
3289 usd_values: vec![100, 25_000],
3290 label: Some("depth-0".to_owned()),
3291 }
3292 }
3293
3294 #[rstest]
3295 #[case::no_markets(DiagnosticProbeParams { markets: Vec::new(), ..diagnostic_probe() })]
3296 #[case::no_usd_values(DiagnosticProbeParams { usd_values: Vec::new(), ..diagnostic_probe() })]
3297 #[case::too_many_markets(DiagnosticProbeParams {
3298 markets: (0..=MAX_DIAGNOSTIC_MARKETS as u8).map(address).collect(),
3299 ..diagnostic_probe()
3300 })]
3301 #[case::too_many_usd_values(DiagnosticProbeParams {
3302 usd_values: (1..=MAX_DIAGNOSTIC_USD_VALUES as u32 + 1).collect(),
3303 ..diagnostic_probe()
3304 })]
3305 #[case::zero_usd_value(DiagnosticProbeParams { usd_values: vec![0, 100], ..diagnostic_probe() })]
3306 #[case::duplicate_market(DiagnosticProbeParams {
3307 markets: vec![address(1), address(1)],
3308 ..diagnostic_probe()
3309 })]
3310 #[case::before_slot(DiagnosticProbeParams {
3311 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
3312 ..diagnostic_probe()
3313 })]
3314 #[case::before_every_slot(DiagnosticProbeParams {
3315 anchor: ActionAnchor::BeforeEverySlot { every_n_slots: NonZeroU64::MIN },
3316 ..diagnostic_probe()
3317 })]
3318 fn diagnostic_probes_reject_invalid_shapes(#[case] probe: DiagnosticProbeParams) {
3319 assert!(probe.validate().is_err());
3320 }
3321
3322 #[test]
3323 fn diagnostic_probes_accept_both_supported_anchors() {
3324 assert!(diagnostic_probe().validate().is_ok());
3325 assert!(
3326 DiagnosticProbeParams {
3327 anchor: ActionAnchor::AfterSlot { slots: vec![1, 2] },
3328 ..diagnostic_probe()
3329 }
3330 .validate()
3331 .is_ok()
3332 );
3333 }
3334
3335 #[test]
3337 fn override_actions_accept_a_static_and_a_computed_shape() {
3338 assert!(decode_override_actions(&[valid()]).is_ok());
3339 assert!(
3342 decode_override_actions(&[ScheduledAction {
3343 transactions: vec!["dGVzdA==".to_string()],
3344 ..valid()
3345 }])
3346 .is_ok()
3347 );
3348 }
3349
3350 #[test]
3351 fn override_actions_merge_account_wise_later_wins() {
3352 let entry = |lamports| AccountData {
3353 lamports,
3354 ..override_entry(address(9), false)
3355 };
3356 let slot_action = |slots, account, lamports| {
3357 override_action(ActionAnchor::BeforeSlot { slots }, account, entry(lamports))
3358 };
3359 let actions = [
3360 slot_action(vec![5, 7], address(1), 1),
3361 slot_action(vec![5], address(2), 2),
3362 slot_action(vec![5], address(1), 3),
3363 ScheduledAction {
3364 anchor: ActionAnchor::AfterSlot { slots: vec![5] },
3365 kind: ActionKind::Simulate,
3366 feeds_reroute: false,
3367 transactions: vec!["dGVzdA==".to_string()],
3368 account_overrides: AccountModifications::default(),
3369 return_accounts: Vec::new(),
3370 label: None,
3371 resolve: None,
3372 },
3373 ];
3374 let schedule = decode_override_actions(&actions).unwrap();
3375 assert_eq!(schedule.slots().collect::<Vec<_>>(), vec![5, 7]);
3376 let lamports_at = |key, slot| {
3377 schedule
3378 .active_for(&key, slot)
3379 .map(|account| account.lamports())
3380 };
3381 assert_eq!(lamports_at(address(1), 5), Some(3));
3382 assert_eq!(lamports_at(address(2), 5), Some(2));
3383 assert_eq!(lamports_at(address(1), 7), Some(1));
3384 }
3385
3386 #[test]
3387 fn computed_override_resolution_preserves_action_order_and_failure() {
3388 let key = address(1);
3389 let static_action = |lamports| {
3390 override_action(
3391 ActionAnchor::BeforeSlot { slots: vec![5] },
3392 key,
3393 AccountData {
3394 lamports,
3395 ..override_entry(address(9), false)
3396 },
3397 )
3398 };
3399 let computed_action = || ScheduledAction {
3400 transactions: vec!["dGVzdA==".to_string()],
3401 ..static_action(2)
3402 };
3403
3404 let computed_last =
3405 decode_override_actions(&[static_action(1), computed_action()]).unwrap();
3406 computed_last
3407 .resolve(5, 1, Some(HashMap::from([(key, shared_account(2))])))
3408 .unwrap();
3409 assert_eq!(
3410 computed_last.active_for(&key, 5).map(|a| a.lamports()),
3411 Some(2)
3412 );
3413
3414 let static_last = decode_override_actions(&[computed_action(), static_action(3)]).unwrap();
3415 static_last
3416 .resolve(5, 0, Some(HashMap::from([(key, shared_account(2))])))
3417 .unwrap();
3418 assert_eq!(
3419 static_last.active_for(&key, 5).map(|a| a.lamports()),
3420 Some(3)
3421 );
3422
3423 let failed = decode_override_actions(&[static_action(1), computed_action()]).unwrap();
3424 failed.resolve(5, 1, None).unwrap();
3425 assert_eq!(failed.active_for(&key, 5).map(|a| a.lamports()), Some(1));
3426 assert!(failed.resolve(5, 1, None).is_err(), "resolving twice");
3427 }
3428
3429 fn shared_account(lamports: u64) -> AccountSharedData {
3430 AccountSharedData::from(solana_account::Account {
3431 lamports,
3432 data: vec![1],
3433 owner: address(9),
3434 executable: false,
3435 rent_epoch: 0,
3436 })
3437 }
3438
3439 fn carry_forward_schedule() -> (Address, Address, OverrideSchedule) {
3440 let (a, b) = (address(1), address(2));
3441 let schedule = OverrideSchedule::from(BTreeMap::from([
3442 (10, HashMap::from([(a, shared_account(1))])),
3443 (
3444 20,
3445 HashMap::from([(a, shared_account(2)), (b, shared_account(9))]),
3446 ),
3447 ]));
3448 (a, b, schedule)
3449 }
3450
3451 #[test]
3452 fn override_schedule_resolves_carry_forward_per_account() {
3453 let (a, b, schedule) = carry_forward_schedule();
3454
3455 assert!(schedule.active_for(&a, 9).is_none());
3456 let cases = [
3457 (9, None, None),
3458 (10, Some(1), None),
3459 (15, Some(1), None),
3460 (20, Some(2), Some(9)),
3461 (u64::MAX, Some(2), Some(9)),
3462 ];
3463 for (slot, want_a, want_b) in cases {
3464 assert_eq!(
3465 schedule
3466 .active_for(&a, slot)
3467 .map(|account| account.lamports()),
3468 want_a,
3469 "active_for a at {slot}"
3470 );
3471 let active = schedule.active_at(slot);
3472 assert_eq!(
3473 active.get(&a).map(ReadableAccount::lamports),
3474 want_a,
3475 "active_at a at {slot}"
3476 );
3477 assert_eq!(
3478 active.get(&b).map(ReadableAccount::lamports),
3479 want_b,
3480 "active_at b at {slot}"
3481 );
3482 }
3483 }
3484
3485 #[test]
3486 fn newly_active_yields_final_values_in_window() {
3487 let (a, b, schedule) = carry_forward_schedule();
3488
3489 let newly: HashMap<_, _> = schedule.newly_active(Some(10), 20).into_iter().collect();
3490 assert_eq!(newly.len(), 2);
3491 assert_eq!(newly[&a].lamports(), 2);
3492 assert_eq!(newly[&b].lamports(), 9);
3493 let spanning: HashMap<_, _> = schedule.newly_active(None, 20).into_iter().collect();
3494 assert_eq!(spanning[&a].lamports(), 2, "later slot wins");
3495 assert_eq!(schedule.newly_active(None, 15).len(), 1);
3496 }
3497
3498 #[test]
3499 fn reroute_filter_round_trips_camel_case() {
3500 let filter = RerouteFilter {
3501 pairs: BTreeSet::from([MintPair::new(address(1), address(2))]),
3502 };
3503 let json = serde_json::to_value(&filter).unwrap();
3504 assert!(
3505 json["pairs"][0][0].is_string() && json["pairs"][0][1].is_string(),
3506 "a pair serializes as two base58 strings: {json}"
3507 );
3508 let back: RerouteFilter = serde_json::from_value(json).unwrap();
3509 assert_eq!(back, filter);
3510 }
3511
3512 #[test]
3515 fn mint_pairs_are_normalized_however_they_are_built() {
3516 let (one, other) = (address(1), address(2));
3517 assert_eq!(MintPair::new(one, other), MintPair::new(other, one));
3518
3519 let reversed: MintPair =
3520 serde_json::from_value(serde_json::json!([other.to_string(), one.to_string()]))
3521 .unwrap();
3522 assert_eq!(reversed, MintPair::new(one, other));
3523 }
3524
3525 #[test]
3526 fn fail_fast_divergence_kind_str_round_trips() {
3527 for kind in [
3528 FailFastDivergenceKind::AnyNonBenign,
3529 FailFastDivergenceKind::Tracked,
3530 ] {
3531 assert_eq!(
3532 FailFastDivergenceKind::from_str_opt(kind.as_str()),
3533 Some(kind)
3534 );
3535 }
3536 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
3537 assert_eq!(
3538 FailFastDivergenceKind::default(),
3539 FailFastDivergenceKind::AnyNonBenign
3540 );
3541 }
3542
3543 #[test]
3544 fn bundle_not_found_is_terminal_and_descriptive() {
3545 let error = BacktestError::BundleNotFound {
3546 start_slot: 100,
3547 end_slot: 600,
3548 };
3549
3550 assert!(error.is_terminal());
3551 assert_eq!(
3552 error.to_string(),
3553 "block bundle not found for start_slot=100, end_slot=600"
3554 );
3555 }
3556
3557 #[test]
3558 fn no_produced_blocks_is_terminal_and_descriptive() {
3559 let error = BacktestError::NoProducedBlocksInRange {
3560 start_slot: 100,
3561 end_slot: 100,
3562 };
3563
3564 assert!(error.is_terminal());
3565 assert_eq!(
3568 error.to_string(),
3569 "no produced blocks in requested range 100..=100; slot(s) may be skipped on-chain"
3570 );
3571 }
3572
3573 #[test]
3574 fn session_name_taken_is_not_terminal_and_names_the_session() {
3575 let error = BacktestError::SessionNameTaken {
3576 session_name: "my-run".to_string(),
3577 };
3578
3579 assert!(!error.is_terminal());
3580 assert_eq!(error.to_string(), "session name 'my-run' is already in use");
3581 }
3582
3583 fn create_request(
3584 body: serde_json::Value,
3585 ) -> Result<CreateBacktestSessionRequestOptions, InvalidSessionName> {
3586 serde_json::from_value::<CreateBacktestSessionRequest>(body)
3587 .expect("decode create request")
3588 .try_into_request_options()
3589 }
3590
3591 #[test]
3592 fn create_requests_from_older_clients_decode_without_a_name() {
3593 let v0 = create_request(serde_json::json!({"startSlot": 1, "endSlot": 2})).expect("v0");
3594 assert!(!v0.parallel);
3595 assert_eq!(v0.name, None);
3596
3597 let v1 =
3598 create_request(serde_json::json!({"startSlot": 1, "endSlot": 2, "parallel": true}))
3599 .expect("v1");
3600 assert!(v1.parallel);
3601 assert_eq!(v1.name, None);
3602 }
3603
3604 #[test]
3605 fn named_create_request_without_parallel_keeps_its_name() {
3606 let options = create_request(serde_json::json!({
3607 "startSlot": 1,
3608 "endSlot": 2,
3609 "name": "my-run",
3610 "createToken": "token-1",
3611 }))
3612 .expect("valid name");
3613
3614 assert!(!options.parallel);
3615 assert_eq!(options.request.end_slot, 2);
3616 let requested = options.name.expect("name");
3617 assert_eq!(requested.name.as_str(), "my-run");
3618 assert_eq!(
3619 requested.create_token.as_ref().map(CreateToken::as_str),
3620 Some("token-1")
3621 );
3622 }
3623
3624 #[test]
3625 fn create_request_with_an_invalid_name_is_rejected() {
3626 let error = create_request(serde_json::json!({
3627 "startSlot": 1,
3628 "endSlot": 2,
3629 "name": "my run",
3630 }))
3631 .expect_err("invalid name");
3632 assert!(error.to_string().contains("'my run'"), "{error}");
3633 }
3634
3635 #[test]
3636 fn create_requests_without_a_name_serialize_as_before() {
3637 let params = CreateSessionParams::builder()
3638 .start_slot(1)
3639 .end_slot(2)
3640 .build();
3641 let params_json = serde_json::to_value(¶ms).expect("encode params");
3642
3643 let v0 = serde_json::to_value(CreateBacktestSessionRequest::from(params.clone()))
3644 .expect("encode v0");
3645 assert_eq!(v0, params_json);
3646
3647 let v1 = serde_json::to_value(CreateBacktestSessionRequest::from(
3648 CreateBacktestSessionRequestV1 {
3649 request: params,
3650 parallel: true,
3651 name: None,
3652 create_token: None,
3653 },
3654 ))
3655 .expect("encode v1");
3656 let mut expected = params_json;
3657 expected["parallel"] = true.into();
3658 assert_eq!(v1, expected);
3659 }
3660
3661 #[test]
3662 fn session_responses_omit_an_absent_name_and_decode_without_one() {
3663 let created = BacktestResponse::SessionCreated {
3664 session_id: SessionId::new(),
3665 rpc_endpoint: "/backtest/s".to_string(),
3666 task_id: None,
3667 session_name: None,
3668 };
3669 let json = serde_json::to_string(&created).expect("encode");
3670 assert!(!json.contains("session_name"), "{json}");
3671 let decoded: BacktestResponse = serde_json::from_str(&json).expect("decode");
3672 assert!(matches!(
3673 decoded,
3674 BacktestResponse::SessionCreated {
3675 session_name: None,
3676 ..
3677 }
3678 ));
3679
3680 let parallel = BacktestResponse::SessionsCreatedV2 {
3681 control_session_id: ControlSessionId::new(),
3682 control_session_name: None,
3683 session_ids: Vec::new(),
3684 task_ids: Vec::new(),
3685 start_slots: Vec::new(),
3686 end_slots: Vec::new(),
3687 };
3688 let json = serde_json::to_string(¶llel).expect("encode");
3689 assert!(!json.contains("control_session_name"), "{json}");
3690 let decoded: BacktestResponse = serde_json::from_str(&json).expect("decode");
3691 assert!(matches!(
3692 decoded,
3693 BacktestResponse::SessionsCreatedV2 {
3694 control_session_name: None,
3695 ..
3696 }
3697 ));
3698 }
3699
3700 #[test]
3701 fn bundle_build_request_optional_fields_default_to_none() {
3702 let req: BundleBuildRequest =
3703 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
3704 assert_eq!((req.start_slot, req.end_slot), (1, 2));
3705 assert_eq!(req.bundle_size, None);
3706 assert_eq!(req.idempotency_key, None);
3707 }
3708
3709 #[test]
3710 fn bundle_build_request_parses_optional_fields() {
3711 let req: BundleBuildRequest = serde_json::from_str(
3712 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
3713 )
3714 .expect("parse");
3715 assert_eq!(req.bundle_size, Some(500));
3716 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
3717 }
3718
3719 #[test]
3720 fn bundle_build_status_serde_round_trips_with_snake_case() {
3721 let cases = [
3722 (BundleBuildStatus::Completed, "\"completed\""),
3723 (BundleBuildStatus::Failed, "\"failed\""),
3724 (BundleBuildStatus::InProgress, "\"in_progress\""),
3725 (
3726 BundleBuildStatus::NeedsInvestigation,
3727 "\"needs_investigation\"",
3728 ),
3729 ];
3730 for (status, expected) in cases {
3731 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
3732 assert_eq!(
3733 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
3734 status
3735 );
3736 assert_eq!(status.as_str(), expected.trim_matches('"'));
3737 }
3738 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
3739 }
3740
3741 #[test]
3742 fn bundle_build_request_without_kind_deserializes_as_blocks() {
3743 let request: BundleBuildRequest =
3745 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
3746 assert_eq!(request.kind, BundleKind::Blocks);
3747 }
3748
3749 #[test]
3750 fn bundle_kind_round_trips_over_serde_and_as_str() {
3751 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
3752 let json = serde_json::to_string(&kind).unwrap();
3753 assert_eq!(json, format!("\"{}\"", kind.as_str()));
3754 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
3755 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
3758 }
3759 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
3760 assert_eq!(BundleKind::from_str_opt("accountState"), None);
3761 }
3762
3763 #[test]
3764 fn bundle_build_request_carries_kind_through_serde() {
3765 let request = BundleBuildRequest {
3766 start_slot: 100,
3767 end_slot: 200,
3768 kind: BundleKind::AccountState,
3769 ..Default::default()
3770 };
3771 let json = serde_json::to_string(&request).unwrap();
3772 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3773 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
3774 assert_eq!(parsed.kind, BundleKind::AccountState);
3775 }
3776
3777 #[test]
3778 fn bundle_build_status_response_serializes_request_and_status() {
3779 let response = BundleBuildStatusResponse {
3780 request_id: "r".to_string(),
3781 start_slot: 100,
3782 end_slot: 200,
3783 bundle_size: Some(50),
3784 status: BundleBuildStatus::InProgress,
3785 created_at_unix_ms: 0,
3786 kind: BundleKind::Blocks,
3787 eta_secs: Some(600),
3788 };
3789 let json = serde_json::to_value(&response).unwrap();
3790 assert_eq!(json["request_id"].as_str(), Some("r"));
3791 assert_eq!(json["start_slot"].as_u64(), Some(100));
3792 assert_eq!(json["bundle_size"].as_u64(), Some(50));
3793 assert_eq!(json["status"].as_str(), Some("in_progress"));
3794 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
3795 assert_eq!(json["eta_secs"].as_u64(), Some(600));
3796 }
3797
3798 #[test]
3799 fn an_empty_bundles_list_is_distinct_from_an_absent_one() {
3800 let absent: BundleBuildListResponse = serde_json::from_str(r#"{"builds":[]}"#).unwrap();
3802 let empty: BundleBuildListResponse =
3803 serde_json::from_str(r#"{"builds":[],"bundles":[]}"#).unwrap();
3804 assert_eq!(absent.bundles, None);
3805 assert_eq!(empty.bundles, Some(Vec::new()));
3806 }
3807
3808 #[test]
3809 fn completed_bundle_round_trips() {
3810 let bundle = CompletedBundle {
3811 start_slot: 100,
3812 end_slot: 200,
3813 kind: BundleKind::AccountState,
3814 completed_at_unix_ms: Some(1_700_000_000_000),
3815 size_bytes: Some(4_096),
3816 };
3817 let json = serde_json::to_string(&bundle).unwrap();
3818 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3819 assert_eq!(
3820 serde_json::from_str::<CompletedBundle>(&json).unwrap(),
3821 bundle
3822 );
3823
3824 let sparse = r#"{"start_slot":1,"end_slot":2,"kind":"blocks"}"#;
3825 let parsed: CompletedBundle = serde_json::from_str(sparse).unwrap();
3826 assert_eq!(parsed.completed_at_unix_ms, None);
3827 assert_eq!(parsed.size_bytes, None);
3828 }
3829
3830 #[test]
3831 fn available_range_reads_a_server_that_predates_kind_and_scope() {
3832 let json = r#"{
3834 "bundle_start_slot": 100,
3835 "bundle_start_slot_utc": null,
3836 "max_bundle_end_slot": 200,
3837 "max_bundle_end_slot_utc": null,
3838 "max_bundle_size": 101
3839 }"#;
3840 let parsed: AvailableRange = serde_json::from_str(json).unwrap();
3841 assert_eq!(parsed.bundle_start_slot, 100);
3842 assert_eq!(parsed.kind, None);
3843 assert_eq!(parsed.scope, None);
3844 }
3845
3846 #[test]
3847 fn available_range_ignores_fields_it_does_not_know() {
3848 let json = r#"{"bundle_start_slot": 1, "some_future_field": "x"}"#;
3850 assert_eq!(
3851 serde_json::from_str::<AvailableRange>(json)
3852 .unwrap()
3853 .bundle_start_slot,
3854 1
3855 );
3856 }
3857
3858 #[test]
3859 fn available_range_labels_round_trip() {
3860 let range = AvailableRange {
3861 bundle_start_slot: 100,
3862 max_bundle_end_slot: Some(200),
3863 kind: Some(BundleKind::AccountState),
3864 scope: Some(RangeScope::User),
3865 ..Default::default()
3866 };
3867 let json = serde_json::to_string(&range).unwrap();
3868 assert!(json.contains(r#""kind":"account_state""#), "{json}");
3869 assert!(json.contains(r#""scope":"user""#), "{json}");
3870 let parsed: AvailableRange = serde_json::from_str(&json).unwrap();
3871 assert_eq!(parsed.kind, Some(BundleKind::AccountState));
3872 assert_eq!(parsed.scope, Some(RangeScope::User));
3873 }
3874
3875 #[rstest]
3877 #[case::from_a_server_predating_it(None, None)]
3878 #[case::snapped_outward(Some(SlotRange::new_unchecked(90, 210)), None)]
3879 #[case::modified(Some(SlotRange::new_unchecked(90, 210)), Some("ab"))]
3880 fn a_depth_status_carries_its_optional_fields_only_when_it_has_them(
3881 #[case] capture: Option<SlotRange>,
3882 #[case] digest: Option<&str>,
3883 ) {
3884 let mut wire = serde_json::json!({
3885 "request_id": "r",
3886 "start_slot": 100,
3887 "end_slot": 200,
3888 "source": "onchain",
3889 "status": "in_progress",
3890 });
3891 if let Some(capture) = capture {
3892 wire["capture"] = serde_json::json!({"start": capture.start(), "end": capture.end()});
3893 }
3894 if let Some(digest) = digest {
3895 wire["modification_digest"] = digest.into();
3896 }
3897 let status: DepthStatusResponse =
3898 serde_json::from_value(wire.clone()).expect("deserialize");
3899 assert_eq!(status.capture, capture);
3900 assert_eq!(status.modification_digest.as_deref(), digest);
3901 assert_eq!(serde_json::to_value(&status).expect("serialize"), wire);
3902 }
3903
3904 #[rstest]
3907 #[case::unmodified(
3908 serde_json::json!({"start_slot": 100, "end_slot": 199, "source": "onchain"})
3909 )]
3910 #[case::at_oracle_updates(serde_json::json!({
3911 "start_slot": 100,
3912 "end_slot": 199,
3913 "source": "onchain",
3914 "granularity": "oracle",
3915 }))]
3916 #[case::modified(serde_json::json!({
3917 "start_slot": 100,
3918 "end_slot": 199,
3919 "source": "onchain",
3920 "program_overrides": [{"program_id": address(7).to_string(), "elf": "f0VMRg=="}],
3921 "account_overrides": {
3922 address(8).to_string(): {
3923 "data": {"data": "AQID", "encoding": "base64"},
3924 "executable": false,
3925 "lamports": 1,
3926 "owner": address(9).to_string(),
3927 "space": 3,
3928 },
3929 },
3930 }))]
3931 fn a_depth_request_keeps_its_wire_shape(#[case] wire: serde_json::Value) {
3932 let req: DepthRequest = serde_json::from_value(wire.clone()).expect("deserialize");
3933 assert_eq!(serde_json::to_value(&req).expect("serialize"), wire);
3934 }
3935
3936 #[rstest]
3939 #[case::accounts_only(BTreeMap::new(), r#"{"programs":{},"accounts":{}}"#.to_owned())]
3940 #[case::one_program(
3941 BTreeMap::from([(address(7), b"\x7fELF".to_vec())]),
3942 format!(r#"{{"programs":{{"{}":"f0VMRg=="}},"accounts":{{}}}}"#, address(7))
3943 )]
3944 fn a_depth_modifications_manifest_keeps_its_pinned_shape(
3945 #[case] programs: BTreeMap<Address, Vec<u8>>,
3946 #[case] expected: String,
3947 ) {
3948 let modifications = DepthModifications {
3949 programs,
3950 accounts: AccountModifications::default(),
3951 };
3952 assert_eq!(
3953 serde_json::to_string(&modifications).expect("serialize"),
3954 expected
3955 );
3956 }
3957
3958 fn range(start: u64, end: u64) -> AvailableRange {
3959 AvailableRange {
3960 bundle_start_slot: start,
3961 max_bundle_end_slot: Some(end),
3962 ..Default::default()
3963 }
3964 }
3965
3966 #[rstest::rstest]
3970 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
3971 #[case::multi(
3972 vec![range(100, 200), range(201, 300), range(301, 400)],
3973 100, 300, Some(vec![(100, 200), (201, 300)])
3974 )]
3975 #[case::nested(
3978 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
3979 100, 900, Some(vec![(100, 500), (501, 900)])
3980 )]
3981 #[case::prefers_finer_grid(
3985 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
3986 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
3987 )]
3988 #[case::shared_start_prefers_finer(
3992 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
3993 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
3994 )]
3995 #[case::coarse_overlap_prefers_finer_pair(
3999 vec![range(100, 200), range(100, 150), range(151, 160)],
4000 100, 160, Some(vec![(100, 150), (151, 160)])
4001 )]
4002 #[case::falls_back_to_coarse(
4006 vec![range(100, 160), range(100, 120), range(121, 140)],
4007 100, 160, Some(vec![(100, 160)])
4008 )]
4009 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
4011 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
4014 #[case::anchors_then_continues(
4015 vec![range(150, 350), range(351, 600)],
4016 200, 600, Some(vec![(150, 350), (351, 600)])
4017 )]
4018 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
4019 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
4021 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
4023 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
4024 #[case::user_and_global_ranges_not_collapsed(
4029 vec![range(100, 200), range(100, 150), range(151, 200)],
4030 100, 200, Some(vec![(100, 150), (151, 200)])
4031 )]
4032 fn split_range_cases(
4033 #[case] ranges: Vec<AvailableRange>,
4034 #[case] start: u64,
4035 #[case] end: u64,
4036 #[case] expected: Option<Vec<(u64, u64)>>,
4037 ) {
4038 match expected {
4039 Some(expected) => assert_eq!(
4040 split_range(
4041 &ranges,
4042 SlotRange::new_unchecked(start, end),
4043 BundleKind::Blocks
4044 )
4045 .unwrap(),
4046 expected
4047 ),
4048 None => assert!(
4049 split_range(
4050 &ranges,
4051 SlotRange::new_unchecked(start, end),
4052 BundleKind::Blocks
4053 )
4054 .is_err()
4055 ),
4056 }
4057 }
4058
4059 #[rstest]
4061 #[case::rides_a_coarse_bundle_over_a_finer_pair(
4062 vec![range(100, 200), range(100, 150), range(151, 160)],
4063 100, 160, vec![(100, 160)]
4064 )]
4065 #[case::never_strands_the_walk(
4066 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
4067 100, 160, vec![(100, 120), (121, 140), (141, 160)]
4068 )]
4069 fn split_range_fewest_cases(
4070 #[case] ranges: Vec<AvailableRange>,
4071 #[case] start: u64,
4072 #[case] end: u64,
4073 #[case] expected: Vec<(u64, u64)>,
4074 ) {
4075 assert_eq!(
4076 split_range_fewest(
4077 &ranges,
4078 SlotRange::new_unchecked(start, end),
4079 BundleKind::Blocks
4080 ),
4081 Ok(expected)
4082 );
4083 }
4084
4085 fn account_state_range(start: u64, end: u64) -> AvailableRange {
4086 AvailableRange {
4087 kind: Some(BundleKind::AccountState),
4088 ..range(start, end)
4089 }
4090 }
4091
4092 #[test]
4093 fn a_replay_split_rides_the_recordings_not_a_finer_block_grid() {
4094 let ranges = vec![
4097 range(100, 149),
4098 range(150, 199),
4099 account_state_range(100, 199),
4100 ];
4101 let requested = SlotRange::new_unchecked(100, 199);
4102
4103 assert_eq!(
4104 split_range(&ranges, requested, BundleKind::Blocks).unwrap(),
4105 [(100, 149), (150, 199)]
4106 );
4107 assert_eq!(
4108 split_range(&ranges, requested, BundleKind::AccountState).unwrap(),
4109 [(100, 199)]
4110 );
4111 }
4112
4113 #[test]
4114 fn a_replay_split_tiles_consecutive_recordings() {
4115 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
4116 assert_eq!(
4117 split_range(
4118 &ranges,
4119 SlotRange::new_unchecked(100, 299),
4120 BundleKind::AccountState
4121 )
4122 .unwrap(),
4123 [(100, 199), (200, 299)]
4124 );
4125 }
4126
4127 #[test]
4128 fn a_replay_split_ends_short_of_a_recording() {
4129 let requested = SlotRange::new_unchecked(100, 150);
4132 assert_eq!(
4133 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
4134 [(100, 150)]
4135 );
4136 assert_eq!(
4137 split_range(
4138 &[account_state_range(100, 199)],
4139 requested,
4140 BundleKind::AccountState
4141 )
4142 .unwrap(),
4143 [(100, 150)]
4144 );
4145 }
4146
4147 #[test]
4148 fn a_replay_split_ends_short_of_its_last_recording() {
4149 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
4152 assert_eq!(
4153 split_range(
4154 &ranges,
4155 SlotRange::new_unchecked(100, 250),
4156 BundleKind::AccountState
4157 )
4158 .unwrap(),
4159 [(100, 199), (200, 250)]
4160 );
4161 }
4162
4163 #[test]
4164 fn a_replay_split_tiles_a_one_slot_recording() {
4165 let ranges = vec![account_state_range(100, 249), account_state_range(250, 250)];
4168 assert_eq!(
4169 split_range(
4170 &ranges,
4171 SlotRange::new_unchecked(100, 250),
4172 BundleKind::AccountState
4173 )
4174 .unwrap(),
4175 [(100, 249), (250, 250)]
4176 );
4177 }
4178
4179 #[test]
4180 fn a_replay_split_anchors_a_mid_recording_start() {
4181 let requested = SlotRange::new_unchecked(150, 199);
4185 assert_eq!(
4186 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
4187 [(100, 199)]
4188 );
4189 assert_eq!(
4190 split_range(
4191 &[account_state_range(100, 199)],
4192 requested,
4193 BundleKind::AccountState
4194 )
4195 .unwrap(),
4196 [(100, 199)]
4197 );
4198 }
4199
4200 #[test]
4201 fn a_replay_split_refuses_a_start_no_recording_covers() {
4202 let error = split_range(
4203 &[account_state_range(200, 299)],
4204 SlotRange::new_unchecked(100, 299),
4205 BundleKind::AccountState,
4206 )
4207 .expect_err("no recording covers 100");
4208 assert!(
4209 error.contains("is not covered by any available account-state bundle"),
4210 "{error}"
4211 );
4212 }
4213
4214 #[test]
4215 fn a_replay_split_ignores_block_ranges() {
4216 let error = split_range(
4217 &[range(100, 199)],
4218 SlotRange::new_unchecked(100, 199),
4219 BundleKind::AccountState,
4220 )
4221 .expect_err("owns no recordings");
4222 assert!(error.contains("account-state bundle"), "{error}");
4223 }
4224
4225 #[test]
4226 fn blocks_mode_admits_ranges_from_a_server_that_predates_kind() {
4227 let unlabelled = range(100, 199);
4230 assert_eq!(unlabelled.kind, None);
4231 assert_eq!(
4232 split_range(
4233 &[unlabelled],
4234 SlotRange::new_unchecked(100, 150),
4235 BundleKind::Blocks
4236 )
4237 .unwrap(),
4238 [(100, 150)]
4239 );
4240 }
4241
4242 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
4245 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
4246 for r in ranges {
4247 if let Some(end) = r.max_bundle_end_slot
4248 && end >= r.bundle_start_slot
4249 {
4250 ends.entry(r.bundle_start_slot).or_default().insert(end);
4251 }
4252 }
4253 ends
4254 }
4255
4256 fn split_lengths(
4260 ends: &BTreeMap<u64, BTreeSet<u64>>,
4261 cursor: u64,
4262 end: u64,
4263 ) -> BTreeSet<usize> {
4264 ends.get(&cursor)
4265 .into_iter()
4266 .flatten()
4267 .flat_map(|&bundle_end| {
4268 if bundle_end >= end {
4269 BTreeSet::from([1])
4270 } else {
4271 split_lengths(ends, bundle_end + 1, end)
4272 .into_iter()
4273 .map(|rest| rest + 1)
4274 .collect()
4275 }
4276 })
4277 .collect()
4278 }
4279
4280 fn is_valid_split(
4284 split: &[(u64, u64)],
4285 ends: &BTreeMap<u64, BTreeSet<u64>>,
4286 start: u64,
4287 end: u64,
4288 ) -> bool {
4289 split.first().is_some_and(|&(s, _)| s == start)
4290 && split.last().is_some_and(|&(_, e)| e == end)
4291 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
4292 && split.iter().all(|&(s, e)| {
4293 e >= s
4294 && ends
4295 .get(&s)
4296 .and_then(|bundle_ends| bundle_ends.iter().next_back())
4297 .is_some_and(|&max_end| e <= max_end)
4298 })
4299 }
4300
4301 type SplitFn = fn(&[AvailableRange], SlotRange, BundleKind) -> Result<Vec<(u64, u64)>, String>;
4302 type OptimumFn = fn(&BTreeSet<usize>) -> Option<&usize>;
4303
4304 #[rstest]
4310 #[case::most(split_range as SplitFn, BTreeSet::last as OptimumFn)]
4311 #[case::fewest(split_range_fewest as SplitFn, BTreeSet::first as OptimumFn)]
4312 fn split_range_matches_reference(#[case] walk: SplitFn, #[case] optimum: OptimumFn) {
4313 let mut seed: u64 = 0x9E3779B97F4A7C15;
4314 let mut next = || {
4315 seed = seed
4316 .wrapping_mul(6364136223846793005)
4317 .wrapping_add(1442695040888963407);
4318 seed >> 33
4319 };
4320
4321 for _ in 0..50_000 {
4322 let ranges: Vec<AvailableRange> = (0..next() % 6)
4325 .map(|_| {
4326 let start = next() % 12;
4327 range(start, start + next() % 6) })
4329 .collect();
4330 let start = next() % 12;
4331 let end = start + next() % 6; let got = walk(
4334 &ranges,
4335 SlotRange::new_unchecked(start, end),
4336 BundleKind::Blocks,
4337 );
4338 let ends = ends_by_start(&ranges);
4339 let anchor = ends
4342 .range(..=start)
4343 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
4344 .map(|(&s, _)| s);
4345 let reference = anchor.and_then(|a| optimum(&split_lengths(&ends, a, end)).copied());
4346
4347 let layout: Vec<_> = ranges
4348 .iter()
4349 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
4350 .collect();
4351 match (&got, reference) {
4352 (Ok(split), Some(best)) => {
4353 assert!(
4354 is_valid_split(split, &ends, anchor.unwrap(), end),
4355 "invalid split {split:?} for {layout:?} [{start},{end}]"
4356 );
4357 assert_eq!(
4358 split.len(),
4359 best,
4360 "suboptimal split {split:?} vs {best} sub-ranges for {layout:?} [{start},{end}]"
4361 );
4362 }
4363 (Err(_), None) => {}
4364 _ => panic!(
4365 "disagreement: walk={got:?}, reference={reference:?} sub-ranges for {layout:?} [{start},{end}]"
4366 ),
4367 }
4368 }
4369 }
4370
4371 #[test]
4372 fn a_reroute_without_templates_omits_direct_fill() {
4373 let json = serde_json::to_value(RerouteParams::default()).expect("serialize");
4374 assert!(json.get("directFill").is_none(), "{json}");
4375 }
4376
4377 #[test]
4378 fn extra_markets_cross_the_wire_as_base58() {
4379 let json = serde_json::to_value(RerouteParams::default()).expect("serialize");
4380 assert!(json.get("extraMarkets").is_none(), "{json}");
4381
4382 let reroute = RerouteParams {
4383 extra_markets: BTreeSet::from([address(7)]),
4384 ..Default::default()
4385 };
4386 let json = serde_json::to_value(&reroute).expect("serialize");
4387 assert_eq!(
4388 json["extraMarkets"],
4389 serde_json::json!([address(7).to_string()])
4390 );
4391 assert_eq!(
4392 serde_json::from_value::<RerouteParams>(json).expect("deserialize"),
4393 reroute
4394 );
4395
4396 serde_json::from_value::<RerouteParams>(serde_json::json!({
4397 "extraMarkets": ["not-an-address"]
4398 }))
4399 .expect_err("an invalid address is rejected");
4400 }
4401
4402 fn template(label: &str) -> DirectFillTemplate {
4404 let placeholder = Address::default();
4405 DirectFillTemplate {
4406 label: label.to_owned(),
4407 mode: SwapMode::ExactIn,
4408 input_mint: mints::SOL,
4409 output_mint: mints::USDC,
4410 instruction: TemplateInstruction {
4411 program_id: address(7),
4412 accounts: vec![
4413 TemplateAccount {
4414 address: placeholder,
4415 writable: true,
4416 signer: true,
4417 },
4418 TemplateAccount {
4419 address: placeholder,
4420 writable: true,
4421 signer: false,
4422 },
4423 TemplateAccount {
4424 address: placeholder,
4425 writable: true,
4426 signer: false,
4427 },
4428 TemplateAccount {
4429 address: address(8),
4430 writable: false,
4431 signer: false,
4432 },
4433 ],
4434 data: vec![0; 16],
4435 },
4436 amount_offset: 2,
4437 taker: TakerSlots {
4438 user: vec![0],
4439 input_token_account: vec![1],
4440 output_token_account: vec![2],
4441 derived: Vec::new(),
4442 },
4443 }
4444 }
4445
4446 #[rstest]
4448 #[case::requote_alone(true, 0, false)]
4449 #[case::templates_alone(false, 1, false)]
4450 #[case::both(true, 2, false)]
4451 #[case::neither(false, 0, true)]
4452 fn a_reroute_is_a_noop_only_with_nothing_to_ask(
4453 #[case] requote: bool,
4454 #[case] templates: usize,
4455 #[case] noop: bool,
4456 ) {
4457 let reroute = RerouteParams {
4458 requote,
4459 direct_fill: (0..templates).map(|i| template(&format!("t{i}"))).collect(),
4460 ..Default::default()
4461 };
4462 assert_eq!(reroute.is_noop(), noop);
4463 }
4464
4465 #[rstest]
4467 #[case::templates_without_requote(false, 1, true)]
4468 #[case::requote_without_templates(true, 0, true)]
4469 #[case::nothing(false, 0, false)]
4470 fn the_account_universe_warms_for_templates_as_for_requoting(
4471 #[case] requote: bool,
4472 #[case] templates: usize,
4473 #[case] warms: bool,
4474 ) {
4475 let params = CreateSessionParams::builder()
4476 .start_slot(1)
4477 .end_slot(2)
4478 .reroute(RerouteParams {
4479 requote,
4480 direct_fill: (0..templates).map(|i| template(&format!("t{i}"))).collect(),
4481 ..Default::default()
4482 })
4483 .build();
4484 assert_eq!(params.warms_account_universe(), warms);
4485 }
4486}