1use std::{
2 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
3 fmt,
4 str::FromStr,
5 sync::Arc,
6};
7
8use base64::{
9 DecodeError as Base64DecodeError, Engine as _, engine::general_purpose::STANDARD as BASE64,
10};
11use parking_lot::RwLock;
12use serde::{Deserialize, Serialize};
13use solana_account::AccountSharedData;
14use solana_address::{Address, error::ParseAddressError};
15
16mod session_id;
17mod session_type;
18mod slot_range;
19
20pub use session_id::{ControlSessionId, SessionId};
21pub use session_type::SessionType;
22pub use slot_range::{InvertedSlotRange, SlotRange};
23
24pub mod client_identity;
25pub mod geyser_route;
26pub mod route_plan;
27pub mod subscribe_config;
28pub mod usage;
29pub mod ws_compression;
30
31#[derive(Debug, Serialize, Deserialize)]
33#[serde(tag = "method", content = "params", rename_all = "camelCase")]
34pub enum BacktestRequest {
35 CreateBacktestSession(CreateBacktestSessionRequest),
36 Continue(ContinueParams),
37 ContinueTo(ContinueToParams),
38 ContinueSessionV1(ContinueSessionRequestV1),
39 ContinueToSessionV1(ContinueToSessionRequestV1),
40 CloseBacktestSession,
41 CloseSessionV1(CloseSessionRequestV1),
42 AttachBacktestSession {
43 session_id: SessionId,
44 last_sequence: Option<u64>,
47 },
48 ResumeAttachedSession,
51 AttachParallelControlSessionV2 {
52 control_session_id: ControlSessionId,
53 #[serde(default)]
57 last_sequences: BTreeMap<SessionId, u64>,
58 },
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(untagged)]
67pub enum CreateBacktestSessionRequest {
68 V1(CreateBacktestSessionRequestV1),
69 V0(CreateSessionParams),
70}
71
72impl CreateBacktestSessionRequest {
73 pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
74 match self {
75 Self::V0(request) => CreateBacktestSessionRequestOptions {
76 request,
77 parallel: false,
78 },
79 Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
80 CreateBacktestSessionRequestOptions { request, parallel }
81 }
82 }
83 }
84
85 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
86 let options = self.into_request_options();
87 (options.request, options.parallel)
88 }
89}
90
91impl From<CreateSessionParams> for CreateBacktestSessionRequest {
92 fn from(value: CreateSessionParams) -> Self {
93 Self::V0(value)
94 }
95}
96
97impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
98 fn from(value: CreateBacktestSessionRequestV1) -> Self {
99 Self::V1(value)
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub struct CreateBacktestSessionRequestV1 {
106 #[serde(flatten)]
107 pub request: CreateSessionParams,
108 pub parallel: bool,
109}
110
111#[derive(Debug, Clone)]
112pub struct CreateBacktestSessionRequestOptions {
113 pub request: CreateSessionParams,
114 pub parallel: bool,
115}
116
117#[derive(Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct ContinueSessionRequestV1 {
120 pub session_id: SessionId,
121 pub request: ContinueParams,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct ContinueToSessionRequestV1 {
127 pub session_id: SessionId,
128 pub request: ContinueToParams,
129}
130
131#[derive(Debug, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct CloseSessionRequestV1 {
134 pub session_id: SessionId,
135}
136
137#[serde_with::serde_as]
144#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
145#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
146pub enum DiscoveryFilter {
147 ProgramExecuted(#[serde_as(as = "serde_with::DisplayFromStr")] Address),
149}
150
151pub struct TxMatchContext<'a> {
156 pub invoked_programs: &'a HashSet<Address>,
158}
159
160impl DiscoveryFilter {
161 pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
164 match self {
165 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
166 }
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
174#[serde(try_from = "[String; 2]", into = "[String; 2]")]
175pub struct MintPair([Address; 2]);
176
177impl MintPair {
178 pub fn new(one: Address, other: Address) -> Self {
179 Self(if one <= other {
180 [one, other]
181 } else {
182 [other, one]
183 })
184 }
185
186 pub fn mints(&self) -> (Address, Address) {
187 (self.0[0], self.0[1])
188 }
189}
190
191#[derive(Debug, thiserror::Error)]
192#[error("invalid mint {mint} in pair")]
193pub struct ParseMintPairError {
194 mint: String,
195 source: ParseAddressError,
196}
197
198impl TryFrom<[String; 2]> for MintPair {
199 type Error = ParseMintPairError;
200
201 fn try_from([one, other]: [String; 2]) -> Result<Self, Self::Error> {
202 fn parse(mint: String) -> Result<Address, ParseMintPairError> {
203 Address::from_str(&mint).map_err(|source| ParseMintPairError { mint, source })
204 }
205 Ok(Self::new(parse(one)?, parse(other)?))
206 }
207}
208
209impl From<MintPair> for [String; 2] {
210 fn from(pair: MintPair) -> Self {
211 let (one, other) = pair.mints();
212 [one.to_string(), other.to_string()]
213 }
214}
215
216#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(rename_all = "camelCase")]
220pub struct RerouteFilter {
221 #[serde(default)]
223 pub pairs: BTreeSet<MintPair>,
224}
225
226impl RerouteFilter {
227 pub fn is_empty(&self) -> bool {
230 self.pairs.is_empty()
231 }
232
233 pub fn matches_mints(&self, input: &Address, output: &Address) -> bool {
234 self.pairs.contains(&MintPair::new(*input, *output))
235 }
236
237 pub fn mentions_mint(&self, mint: &Address) -> bool {
240 self.pairs.iter().any(|pair| {
241 let (one, other) = pair.mints();
242 one == *mint || other == *mint
243 })
244 }
245}
246
247#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub enum ActionKind {
252 #[default]
256 Simulate,
257 Send,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266#[serde(tag = "at", rename_all = "camelCase")]
267pub enum ActionAnchor {
268 BeforeSlot {
271 #[serde(default, with = "compressed")]
272 slots: Vec<u64>,
273 },
274 AfterSlot {
277 #[serde(default, with = "compressed")]
278 slots: Vec<u64>,
279 },
280 BeforeMatch { filter: DiscoveryFilter },
283 AfterMatch { filter: DiscoveryFilter },
286}
287
288const MAX_ACTION_TRANSACTIONS_BYTES: u64 = 64 * 1024 * 1024;
291
292mod compressed {
297 use std::io::Read;
298
299 use base64::Engine as _;
300 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
301
302 use super::{BASE64, MAX_ACTION_TRANSACTIONS_BYTES};
303
304 const COMPRESSION_LEVEL: i32 = 0;
306
307 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
308 where
309 T: Serialize,
310 S: Serializer,
311 {
312 let json = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
313 let compressed = zstd::stream::encode_all(json.as_slice(), COMPRESSION_LEVEL)
314 .map_err(serde::ser::Error::custom)?;
315 serializer.serialize_str(&BASE64.encode(compressed))
316 }
317
318 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
319 where
320 T: DeserializeOwned,
321 D: Deserializer<'de>,
322 {
323 let blob = String::deserialize(deserializer)?;
324 let compressed = BASE64
325 .decode(blob.as_bytes())
326 .map_err(serde::de::Error::custom)?;
327 let mut json = Vec::new();
328 zstd::stream::read::Decoder::new(compressed.as_slice())
329 .and_then(|d| Read::take(d, MAX_ACTION_TRANSACTIONS_BYTES).read_to_end(&mut json))
330 .map_err(serde::de::Error::custom)?;
331 serde_json::from_slice(&json).map_err(serde::de::Error::custom)
332 }
333}
334
335#[serde_with::serde_as]
343#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct ScheduledAction {
346 pub anchor: ActionAnchor,
347 #[serde(default)]
349 pub kind: ActionKind,
350 #[serde(default, with = "compressed")]
359 pub transactions: Vec<String>,
360 #[serde(default)]
365 pub account_overrides: AccountModifications,
366 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
376 pub feeds_reroute: bool,
377 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
381 #[serde(default)]
382 pub return_accounts: Vec<Address>,
383 #[serde(default)]
385 pub label: Option<String>,
386}
387
388impl ScheduledAction {
389 pub fn is_computed_override(&self) -> bool {
392 self.feeds_reroute && !self.transactions.is_empty()
393 }
394}
395
396#[derive(Debug, Clone)]
398enum OverrideContribution {
399 Static(HashMap<Address, AccountSharedData>),
400 Pending,
401 Resolved(HashMap<Address, AccountSharedData>),
402 Failed,
403}
404
405#[derive(Debug, Clone, Default)]
406struct OverrideScheduleInner {
407 entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>,
408 keys: HashSet<Address>,
412}
413
414impl OverrideScheduleInner {
415 fn new(entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>) -> Self {
416 let keys = entries
417 .values()
418 .flatten()
419 .filter_map(|(_, contribution)| match contribution {
420 OverrideContribution::Static(accounts)
421 | OverrideContribution::Resolved(accounts) => Some(accounts.keys().copied()),
422 OverrideContribution::Pending | OverrideContribution::Failed => None,
423 })
424 .flatten()
425 .collect();
426 Self { entries, keys }
427 }
428}
429
430#[derive(Debug, Clone, Default)]
433pub struct OverrideSchedule {
434 inner: Arc<RwLock<OverrideScheduleInner>>,
435}
436
437impl From<BTreeMap<u64, HashMap<Address, AccountSharedData>>> for OverrideSchedule {
438 fn from(entries: BTreeMap<u64, HashMap<Address, AccountSharedData>>) -> Self {
439 Self {
440 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(
441 entries
442 .into_iter()
443 .map(|(slot, accounts)| {
444 (slot, vec![(0, OverrideContribution::Static(accounts))])
445 })
446 .collect(),
447 ))),
448 }
449 }
450}
451
452impl OverrideSchedule {
453 pub fn is_empty(&self) -> bool {
454 self.inner.read().entries.is_empty()
455 }
456
457 pub fn slots(&self) -> impl Iterator<Item = u64> + '_ {
459 self.inner
460 .read()
461 .entries
462 .keys()
463 .copied()
464 .collect::<Vec<_>>()
465 .into_iter()
466 }
467
468 pub fn active_at(&self, slot: u64) -> HashMap<Address, AccountSharedData> {
470 self.inner.read().entries.range(..=slot).fold(
471 HashMap::new(),
472 |mut active, (_, contributions)| {
473 active.extend(fold_contributions(contributions));
474 active
475 },
476 )
477 }
478
479 pub fn active_for(&self, key: &Address, slot: u64) -> Option<AccountSharedData> {
480 let inner = self.inner.read();
481 if !inner.keys.contains(key) {
482 return None;
483 }
484 inner
485 .entries
486 .range(..=slot)
487 .rev()
488 .find_map(|(_, contributions)| {
489 contributions
490 .iter()
491 .rev()
492 .find_map(|(_, contribution)| match contribution {
493 OverrideContribution::Static(accounts)
494 | OverrideContribution::Resolved(accounts) => accounts.get(key).cloned(),
495 OverrideContribution::Pending | OverrideContribution::Failed => None,
496 })
497 })
498 }
499
500 pub fn newly_active(
502 &self,
503 after: Option<u64>,
504 up_to: u64,
505 ) -> Vec<(Address, AccountSharedData)> {
506 let lower = after.map_or(std::ops::Bound::Unbounded, std::ops::Bound::Excluded);
507 self.inner
508 .read()
509 .entries
510 .range((lower, std::ops::Bound::Included(up_to)))
511 .flat_map(|(_, contributions)| fold_contributions(contributions))
512 .collect::<HashMap<_, _>>()
513 .into_iter()
514 .collect()
515 }
516
517 pub fn resolve(
520 &self,
521 slot: u64,
522 action_index: u32,
523 accounts: Option<HashMap<Address, AccountSharedData>>,
524 ) -> Result<(), String> {
525 let mut inner = self.inner.write();
526 let OverrideScheduleInner { entries, keys } = &mut *inner;
527 let contribution = entries
528 .get_mut(&slot)
529 .and_then(|contributions| {
530 contributions
531 .iter_mut()
532 .find(|(index, _)| *index == action_index)
533 .map(|(_, contribution)| contribution)
534 })
535 .ok_or_else(|| {
536 format!("override action {action_index} has no contribution at slot {slot}")
537 })?;
538 if !matches!(contribution, OverrideContribution::Pending) {
539 return Err(format!(
540 "override action {action_index} at slot {slot} is not pending"
541 ));
542 }
543 *contribution = match accounts {
544 Some(accounts) => {
545 keys.extend(accounts.keys().copied());
546 OverrideContribution::Resolved(accounts)
547 }
548 None => OverrideContribution::Failed,
549 };
550 Ok(())
551 }
552}
553
554fn fold_contributions(
555 contributions: &[(u32, OverrideContribution)],
556) -> HashMap<Address, AccountSharedData> {
557 let mut accounts = HashMap::new();
558 for (_, contribution) in contributions {
559 match contribution {
560 OverrideContribution::Static(next) | OverrideContribution::Resolved(next) => {
561 accounts.extend(next.iter().map(|(key, account)| (*key, account.clone())));
562 }
563 OverrideContribution::Pending | OverrideContribution::Failed => {}
564 }
565 }
566 accounts
567}
568
569fn decode_override_actions(actions: &[ScheduledAction]) -> Result<OverrideSchedule, String> {
578 let mut entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>> = BTreeMap::new();
579 for (action_index, action) in actions.iter().enumerate().filter(|(_, action)| {
580 action.feeds_reroute
581 || (action.transactions.is_empty() && !action.account_overrides.0.is_empty())
582 }) {
583 if action.kind == ActionKind::Send {
585 return Err("account overrides are not valid for kind: send".to_string());
586 }
587 if action.transactions.is_empty() && action.account_overrides.0.is_empty() {
588 return Err(
589 "feedsReroute needs something to feed: transactions to simulate, or \
590 accountOverrides"
591 .to_string(),
592 );
593 }
594 let ActionAnchor::BeforeSlot { slots } = &action.anchor else {
595 return Err("scheduled account overrides require a beforeSlot anchor".to_string());
596 };
597 let accounts = decode_override_accounts(&action.account_overrides)?;
598 let contribution = if action.transactions.is_empty() {
599 OverrideContribution::Static(accounts)
600 } else {
601 OverrideContribution::Pending
602 };
603 for slot in slots.iter().copied().collect::<BTreeSet<_>>() {
606 entries
607 .entry(slot)
608 .or_default()
609 .push((action_index as u32, contribution.clone()));
610 }
611 }
612 Ok(OverrideSchedule {
613 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(entries))),
614 })
615}
616
617#[allow(
621 deprecated,
622 reason = "`PROGRAM_OWNERS` remains the canonical loader-owner list"
623)]
624pub fn decode_account_modifications(
625 modifications: &AccountModifications,
626) -> Result<HashMap<Address, AccountSharedData>, String> {
627 modifications
628 .0
629 .iter()
630 .map(|(address, data)| {
631 let account = data
632 .to_account()
633 .map_err(|error| format!("invalid account data for {address}: {error}"))?;
634 if account.executable || solana_account::PROGRAM_OWNERS.contains(&account.owner) {
635 return Err(format!(
636 "overriding executable/program accounts is not supported (account {address})"
637 ));
638 }
639 Ok((*address, AccountSharedData::from(account)))
640 })
641 .collect()
642}
643
644fn decode_override_accounts(
647 modifications: &AccountModifications,
648) -> Result<HashMap<Address, AccountSharedData>, String> {
649 let accounts = decode_account_modifications(modifications)?;
650 if let Some(address) = modifications
651 .0
652 .iter()
653 .find_map(|(address, data)| TOKEN_PROGRAM_IDS.contains(&data.owner).then_some(address))
654 {
655 return Err(format!(
656 "override for {address}: token accounts cannot be overridden \
657 (prepare-stage reads would desync from simulation)"
658 ));
659 }
660 Ok(accounts)
661}
662
663pub const MAX_DISCONNECT_TIMEOUT_SECS: u16 = 900;
667
668#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
671#[serde(rename_all = "lowercase", try_from = "String")]
672pub enum SwapAggregator {
673 Jupiter,
674 Okx,
675 Titan,
676 Dflow,
677}
678
679impl SwapAggregator {
680 pub const fn as_str(self) -> &'static str {
682 match self {
683 Self::Jupiter => "jupiter",
684 Self::Okx => "okx",
685 Self::Titan => "titan",
686 Self::Dflow => "dflow",
687 }
688 }
689}
690
691impl std::fmt::Display for SwapAggregator {
692 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
693 f.write_str(self.as_str())
694 }
695}
696
697#[derive(Debug, thiserror::Error)]
698pub enum ParseAggregatorError {
699 #[error("unknown swap aggregator {0:?}; expected one of jupiter, okx, titan, dflow")]
700 Unknown(String),
701 #[error("name at least one aggregator (jupiter, okx, titan, dflow)")]
703 NoAggregator,
704}
705
706impl TryFrom<String> for SwapAggregator {
707 type Error = ParseAggregatorError;
708
709 fn try_from(value: String) -> Result<Self, Self::Error> {
710 value.parse()
711 }
712}
713
714impl std::str::FromStr for SwapAggregator {
715 type Err = ParseAggregatorError;
716
717 fn from_str(s: &str) -> Result<Self, Self::Err> {
718 match s.trim().to_ascii_lowercase().as_str() {
719 "jupiter" => Ok(Self::Jupiter),
720 "okx" => Ok(Self::Okx),
721 "titan" => Ok(Self::Titan),
722 "dflow" => Ok(Self::Dflow),
723 _ => Err(ParseAggregatorError::Unknown(s.to_string())),
724 }
725 }
726}
727
728#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
732#[serde(transparent)]
733pub struct RerouteAggregators(BTreeSet<SwapAggregator>);
734
735impl Default for RerouteAggregators {
736 fn default() -> Self {
737 Self([SwapAggregator::Jupiter].into_iter().collect())
738 }
739}
740
741impl RerouteAggregators {
742 pub fn new(aggregators: impl IntoIterator<Item = SwapAggregator>) -> Self {
743 Self(aggregators.into_iter().collect())
744 }
745
746 pub fn contains(&self, aggregator: SwapAggregator) -> bool {
747 self.0.contains(&aggregator)
748 }
749
750 pub fn is_empty(&self) -> bool {
751 self.0.is_empty()
752 }
753}
754
755impl std::fmt::Display for RerouteAggregators {
756 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
757 let mut aggregators = self.0.iter();
758 if let Some(first) = aggregators.next() {
759 write!(f, "{first}")?;
760 }
761 aggregators.try_for_each(|aggregator| write!(f, ",{aggregator}"))
762 }
763}
764
765impl std::str::FromStr for RerouteAggregators {
766 type Err = ParseAggregatorError;
767
768 fn from_str(s: &str) -> Result<Self, Self::Err> {
769 let aggregators: BTreeSet<SwapAggregator> = s
770 .split(',')
771 .filter(|aggregator| !aggregator.trim().is_empty())
772 .map(str::parse)
773 .collect::<Result<_, _>>()?;
774 if aggregators.is_empty() {
775 return Err(ParseAggregatorError::NoAggregator);
776 }
777 Ok(Self(aggregators))
778 }
779}
780
781#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
789#[serde(rename_all = "camelCase")]
790pub struct RerouteParams {
791 #[serde(default)]
792 pub aggregators: RerouteAggregators,
793 #[serde(default, skip_serializing_if = "Option::is_none")]
796 pub filter: Option<RerouteFilter>,
797 #[serde(default)]
804 pub detect_failed_l1_swaps: bool,
805 #[serde(default)]
808 pub circular_arbs: bool,
809 #[serde(default = "RerouteParams::default_requote")]
815 pub requote: bool,
816 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub direct_fill: Option<Box<DirectFillParams>>,
822}
823
824#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
830#[serde(rename_all = "camelCase")]
831pub struct DirectFillParams {
832 pub aggregator: SwapAggregator,
835 pub venue: String,
837 pub pair: MintPair,
839 #[serde(default = "DirectFillParams::default_slippage_bps")]
842 pub slippage_bps: u16,
843 pub market: DirectFillMarket,
845}
846
847impl DirectFillParams {
848 const fn default_slippage_bps() -> u16 {
851 50
852 }
853}
854
855#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
863#[serde(try_from = "DirectFillMarketWire", into = "DirectFillMarketWire")]
864pub struct DirectFillMarket {
865 pub mints: [Address; 2],
868 pub accounts: Vec<DirectFillAccount>,
870}
871
872#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
874#[serde(try_from = "DirectFillAccountWire", into = "DirectFillAccountWire")]
875pub struct DirectFillAccount {
876 pub address: Address,
877 pub writable: bool,
880}
881
882#[derive(Debug, Clone, Serialize, Deserialize)]
883#[serde(rename_all = "camelCase")]
884struct DirectFillMarketWire {
885 mints: [String; 2],
886 accounts: Vec<DirectFillAccount>,
887}
888
889#[derive(Debug, Clone, Serialize, Deserialize)]
890#[serde(rename_all = "camelCase")]
891struct DirectFillAccountWire {
892 address: String,
893 #[serde(default)]
894 writable: bool,
895}
896
897#[derive(Debug, thiserror::Error, PartialEq, Eq)]
899pub enum ParseDirectFillMarketError {
900 #[error("invalid address {address} in the venue's market")]
901 Address { address: String },
902 #[error("a venue's account run must hold 1..=255 accounts, got {count}")]
904 RunLength { count: usize },
905 #[error("a market quotes two different mints, got {mint} twice")]
907 RepeatedMint { mint: String },
908}
909
910impl TryFrom<DirectFillAccountWire> for DirectFillAccount {
911 type Error = ParseDirectFillMarketError;
912
913 fn try_from(wire: DirectFillAccountWire) -> Result<Self, Self::Error> {
914 Ok(Self {
915 address: Address::from_str(&wire.address).map_err(|_| {
916 ParseDirectFillMarketError::Address {
917 address: wire.address.clone(),
918 }
919 })?,
920 writable: wire.writable,
921 })
922 }
923}
924
925impl From<DirectFillAccount> for DirectFillAccountWire {
926 fn from(account: DirectFillAccount) -> Self {
927 Self {
928 address: account.address.to_string(),
929 writable: account.writable,
930 }
931 }
932}
933
934impl TryFrom<DirectFillMarketWire> for DirectFillMarket {
935 type Error = ParseDirectFillMarketError;
936
937 fn try_from(wire: DirectFillMarketWire) -> Result<Self, Self::Error> {
938 let [one, other] = wire.mints;
939 let parse = |mint: &str| {
940 Address::from_str(mint).map_err(|_| ParseDirectFillMarketError::Address {
941 address: mint.to_owned(),
942 })
943 };
944 let mints = [parse(&one)?, parse(&other)?];
945 if mints[0] == mints[1] {
946 return Err(ParseDirectFillMarketError::RepeatedMint { mint: one });
947 }
948 if wire.accounts.is_empty() || wire.accounts.len() > usize::from(u8::MAX) {
949 return Err(ParseDirectFillMarketError::RunLength {
950 count: wire.accounts.len(),
951 });
952 }
953 Ok(Self {
954 mints,
955 accounts: wire.accounts,
956 })
957 }
958}
959
960impl From<DirectFillMarket> for DirectFillMarketWire {
961 fn from(market: DirectFillMarket) -> Self {
962 Self {
963 mints: [market.mints[0].to_string(), market.mints[1].to_string()],
964 accounts: market.accounts,
965 }
966 }
967}
968
969impl Default for RerouteParams {
972 fn default() -> Self {
973 Self {
974 aggregators: RerouteAggregators::default(),
975 filter: None,
976 detect_failed_l1_swaps: false,
977 circular_arbs: false,
978 requote: Self::default_requote(),
979 direct_fill: None,
980 }
981 }
982}
983
984impl std::str::FromStr for MintPair {
986 type Err = String;
987
988 fn from_str(s: &str) -> Result<Self, Self::Err> {
989 let (base, quote) = s
990 .split_once(',')
991 .ok_or_else(|| format!("expected two comma-separated mints, got {s:?}"))?;
992 Self::try_from([base.trim().to_owned(), quote.trim().to_owned()])
993 .map_err(|error| error.to_string())
994 }
995}
996
997impl std::fmt::Display for MintPair {
998 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
999 let (one, other) = self.mints();
1000 write!(f, "{one},{other}")
1001 }
1002}
1003
1004impl RerouteParams {
1005 const fn default_requote() -> bool {
1006 true
1007 }
1008
1009 pub const fn is_noop(&self) -> bool {
1012 !self.requote && self.direct_fill.is_none()
1013 }
1014}
1015
1016impl DirectFillParams {
1017 pub fn matches_mints(&self, input: &Address, output: &Address) -> bool {
1019 MintPair::new(*input, *output) == self.pair
1020 }
1021}
1022
1023#[serde_with::serde_as]
1025#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
1026#[serde(rename_all = "camelCase")]
1027pub struct CreateSessionParams {
1028 pub start_slot: u64,
1030 pub end_slot: u64,
1032 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
1033 #[serde(default)]
1034 #[builder(default)]
1035 pub signer_filter: BTreeSet<Address>,
1037 #[serde(default)]
1040 #[builder(default)]
1041 pub send_summary: bool,
1042 #[serde(default, skip_serializing_if = "Option::is_none")]
1044 pub reroute: Option<RerouteParams>,
1045 #[serde(default)]
1048 pub capacity_wait_timeout_secs: Option<u16>,
1049 #[serde(default)]
1053 pub disconnect_timeout_secs: Option<u16>,
1054 #[serde(default)]
1059 pub extra_compute_units: Option<u32>,
1060 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1067 #[builder(default)]
1068 pub discoveries: Vec<DiscoveryFilter>,
1069 #[serde(default, with = "compressed", skip_serializing_if = "Vec::is_empty")]
1078 #[builder(default)]
1079 pub actions: Vec<ScheduledAction>,
1080 #[serde(default)]
1083 #[builder(default)]
1084 pub replay_account_state: bool,
1085}
1086
1087pub mod replay_support {
1091 use super::{ActionKind, CreateSessionParams};
1092
1093 pub const NO_TRANSACTIONS: &str =
1095 "Create the session without replayAccountState to execute and persist transactions.";
1096
1097 pub const SEND_TRANSACTION: &str = "Use simulateTransaction to run it against the replayed state, or create the session \
1098 without replayAccountState.";
1099
1100 pub const SEND_ACTION: &str = "Use kind: simulate, which runs against the replayed state without committing, post state \
1101 directly with an action carrying accountOverrides and no transactions, or create the \
1102 session without replayAccountState.";
1103
1104 pub fn unsupported(operation: &str, remedy: &str) -> String {
1106 format!(
1107 "{operation} is not supported while account state replay is enabled: replay \
1108 reconstructs account state from the recorded delta bundle instead of executing \
1109 transactions, so this session never executes or persists any. {remedy}"
1110 )
1111 }
1112
1113 pub fn send_action_conflict(params: &CreateSessionParams) -> Option<String> {
1116 (params.replay_account_state
1117 && params
1118 .actions
1119 .iter()
1120 .any(|action| action.kind == ActionKind::Send))
1121 .then(|| unsupported("A scheduled action with kind: send", SEND_ACTION))
1122 }
1123}
1124
1125impl CreateSessionParams {
1126 pub fn requires_sidecar(&self) -> bool {
1129 self.reroute.as_ref().is_some_and(|reroute| reroute.requote)
1130 }
1131
1132 pub fn reroute_order_flow(&self) -> bool {
1133 self.reroute.is_some()
1134 }
1135
1136 pub fn override_schedule(&self) -> Result<OverrideSchedule, String> {
1139 decode_override_actions(&self.actions)
1140 }
1141
1142 pub fn reroute_filter(&self) -> Option<&RerouteFilter> {
1144 self.reroute.as_ref()?.filter.as_ref()
1145 }
1146}
1147
1148#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1151#[serde(rename_all = "kebab-case")]
1152pub enum FailFastDivergenceKind {
1153 #[default]
1156 AnyNonBenign,
1157 Tracked,
1160}
1161
1162impl FailFastDivergenceKind {
1163 pub fn as_str(self) -> &'static str {
1166 match self {
1167 Self::AnyNonBenign => "any-non-benign",
1168 Self::Tracked => "tracked",
1169 }
1170 }
1171
1172 pub fn from_str_opt(value: &str) -> Option<Self> {
1174 match value {
1175 "any-non-benign" => Some(Self::AnyNonBenign),
1176 "tracked" => Some(Self::Tracked),
1177 _ => None,
1178 }
1179 }
1180}
1181
1182#[serde_with::serde_as]
1184#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1185pub struct AccountModifications(
1186 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
1187 #[serde(default)]
1188 pub BTreeMap<Address, AccountData>,
1189);
1190
1191const TOKEN_PROGRAM_IDS: [Address; 2] = [spl_token_interface::ID, spl_token_2022_interface::ID];
1192
1193impl From<BTreeMap<Address, AccountData>> for AccountModifications {
1194 fn from(value: BTreeMap<Address, AccountData>) -> Self {
1195 Self(value)
1196 }
1197}
1198
1199#[serde_with::serde_as]
1201#[derive(Debug, Serialize, Deserialize)]
1202#[serde(rename_all = "camelCase")]
1203pub struct ContinueParams {
1204 #[serde(default = "ContinueParams::default_advance_count")]
1205 pub advance_count: u64,
1207 #[serde(default)]
1208 pub transactions: Vec<String>,
1210 #[serde(default)]
1211 pub modify_account_states: AccountModifications,
1213}
1214
1215impl Default for ContinueParams {
1216 fn default() -> Self {
1217 Self {
1218 advance_count: Self::default_advance_count(),
1219 transactions: Vec::new(),
1220 modify_account_states: AccountModifications(BTreeMap::new()),
1221 }
1222 }
1223}
1224
1225impl ContinueParams {
1226 pub fn default_advance_count() -> u64 {
1227 1
1228 }
1229}
1230
1231#[derive(Debug, Clone, Serialize, Deserialize)]
1237#[serde(rename_all = "camelCase")]
1238pub struct PausedEvent {
1239 pub slot: u64,
1240 #[serde(default, skip_serializing_if = "Option::is_none")]
1241 pub batch_index: Option<u32>,
1242}
1243
1244#[serde_with::serde_as]
1253#[derive(Debug, Clone, Serialize, Deserialize)]
1254#[serde(rename_all = "camelCase")]
1255pub struct DiscoveryBatchEvent {
1256 pub slot: u64,
1257 pub batch_index: u32,
1258 pub matched: Vec<DiscoveryFilter>,
1260 pub transactions: Vec<EncodedBinary>,
1264}
1265
1266#[derive(Debug, Clone, Serialize, Deserialize)]
1268#[serde(rename_all = "camelCase")]
1269pub struct ContinueToParams {
1270 pub slot: u64,
1272 #[serde(default)]
1278 pub batch_index: Option<u32>,
1279}
1280
1281#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1283#[serde(rename_all = "lowercase")]
1284pub enum BinaryEncoding {
1285 Base64,
1286}
1287
1288impl BinaryEncoding {
1289 pub fn encode(self, bytes: &[u8]) -> String {
1290 match self {
1291 Self::Base64 => BASE64.encode(bytes),
1292 }
1293 }
1294
1295 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
1296 match self {
1297 Self::Base64 => BASE64.decode(data),
1298 }
1299 }
1300}
1301
1302#[derive(Debug, Clone, Serialize, Deserialize)]
1304#[serde(rename_all = "camelCase")]
1305pub struct EncodedBinary {
1306 pub data: String,
1308 pub encoding: BinaryEncoding,
1310}
1311
1312impl EncodedBinary {
1313 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
1314 Self { data, encoding }
1315 }
1316
1317 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
1318 Self {
1319 data: encoding.encode(bytes),
1320 encoding,
1321 }
1322 }
1323
1324 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
1325 self.encoding.decode(&self.data)
1326 }
1327}
1328
1329#[serde_with::serde_as]
1331#[derive(Debug, Clone, Serialize, Deserialize)]
1332#[serde(rename_all = "camelCase")]
1333pub struct AccountData {
1334 pub data: EncodedBinary,
1336 pub executable: bool,
1338 pub lamports: u64,
1340 #[serde_as(as = "serde_with::DisplayFromStr")]
1341 pub owner: Address,
1343 pub space: u64,
1345}
1346
1347impl AccountData {
1348 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
1349 Ok(solana_account::Account {
1350 data: self.data.decode()?,
1351 lamports: self.lamports,
1352 owner: self.owner,
1353 executable: self.executable,
1354 rent_epoch: 0,
1355 })
1356 }
1357}
1358
1359#[derive(Debug, Clone, Serialize, Deserialize)]
1361#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1362pub enum BacktestResponse {
1363 SessionCreated {
1364 session_id: SessionId,
1365 rpc_endpoint: String,
1366 #[serde(default, skip_serializing_if = "Option::is_none")]
1367 task_id: Option<String>,
1368 },
1369 SessionAttached {
1370 session_id: SessionId,
1371 rpc_endpoint: String,
1372 #[serde(default, skip_serializing_if = "Option::is_none")]
1373 task_id: Option<String>,
1374 },
1375 SessionsCreatedV2 {
1376 control_session_id: ControlSessionId,
1377 session_ids: Vec<SessionId>,
1378 #[serde(default)]
1379 task_ids: Vec<Option<String>>,
1380 #[serde(default)]
1385 start_slots: Vec<u64>,
1386 #[serde(default)]
1387 end_slots: Vec<u64>,
1388 },
1389 ParallelSessionAttachedV2 {
1390 control_session_id: ControlSessionId,
1391 session_ids: Vec<SessionId>,
1392 #[serde(default)]
1393 task_ids: Vec<Option<String>>,
1394 },
1395 ReadyForContinue,
1396 SlotNotification(u64),
1397 Paused(PausedEvent),
1398 DiscoveryBatch(DiscoveryBatchEvent),
1399 Error(BacktestError),
1400 Success,
1401 Completed {
1402 #[serde(skip_serializing_if = "Option::is_none")]
1406 summary: Option<SessionSummary>,
1407 },
1408 Status {
1409 status: BacktestStatus,
1410 },
1411 SessionEventV2 {
1412 session_id: SessionId,
1413 seq_id: u64,
1414 event: SessionEventKind,
1415 },
1416}
1417
1418impl BacktestResponse {
1419 pub fn is_completed(&self) -> bool {
1420 matches!(self, BacktestResponse::Completed { .. })
1421 }
1422
1423 pub fn is_terminal(&self) -> bool {
1424 match self {
1425 BacktestResponse::Completed { .. } => true,
1426 BacktestResponse::Error(e) => e.is_terminal(),
1427 _ => false,
1428 }
1429 }
1430}
1431
1432impl From<BacktestStatus> for BacktestResponse {
1433 fn from(status: BacktestStatus) -> Self {
1434 Self::Status { status }
1435 }
1436}
1437
1438impl From<String> for BacktestResponse {
1439 fn from(message: String) -> Self {
1440 BacktestError::Internal { error: message }.into()
1441 }
1442}
1443
1444impl From<&str> for BacktestResponse {
1445 fn from(message: &str) -> Self {
1446 BacktestError::Internal {
1447 error: message.to_string(),
1448 }
1449 .into()
1450 }
1451}
1452
1453#[derive(Debug, Clone, Serialize, Deserialize)]
1454#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1455pub enum SessionEventKind {
1456 ReadyForContinue,
1457 SlotNotification(u64),
1458 Paused(PausedEvent),
1459 DiscoveryBatch(DiscoveryBatchEvent),
1460 Error(BacktestError),
1461 Success,
1462 Completed {
1463 #[serde(skip_serializing_if = "Option::is_none")]
1464 summary: Option<SessionSummary>,
1465 },
1466 Status {
1467 status: BacktestStatus,
1468 },
1469}
1470
1471impl SessionEventKind {
1472 pub fn is_terminal(&self) -> bool {
1473 match self {
1474 Self::Completed { .. } => true,
1475 Self::Error(e) => e.is_terminal(),
1476 _ => false,
1477 }
1478 }
1479}
1480
1481#[derive(Debug, Clone, Serialize, Deserialize)]
1484#[serde(rename_all = "camelCase")]
1485pub struct SequencedResponse {
1486 pub seq_id: u64,
1487 #[serde(flatten)]
1488 pub response: BacktestResponse,
1489}
1490
1491#[derive(Debug, Clone, Serialize, Deserialize)]
1493#[serde(rename_all = "camelCase")]
1494pub enum BacktestStatus {
1495 StartingRuntime,
1497 DecodedTransactions,
1498 AppliedAccountModifications,
1499 ReadyToExecuteUserTransactions,
1500 ExecutedUserTransactions,
1501 ExecutingBlockTransactions,
1502 ExecutedBlockTransactions,
1503 ProgramAccountsLoaded,
1504}
1505
1506impl std::fmt::Display for BacktestStatus {
1507 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1508 let s = match self {
1509 Self::StartingRuntime => "starting runtime",
1510 Self::DecodedTransactions => "decoded transactions",
1511 Self::AppliedAccountModifications => "applied account modifications",
1512 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
1513 Self::ExecutedUserTransactions => "executed user transactions",
1514 Self::ExecutingBlockTransactions => "executing block transactions",
1515 Self::ExecutedBlockTransactions => "executed block transactions",
1516 Self::ProgramAccountsLoaded => "program accounts loaded",
1517 };
1518 f.write_str(s)
1519 }
1520}
1521
1522#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1527#[serde(rename_all = "camelCase")]
1528pub struct RerouteStatsReport {
1529 #[serde(default)]
1531 pub swaps_detected: u64,
1532 #[serde(default)]
1536 pub swaps_filtered: u64,
1537 #[serde(default)]
1539 pub swaps_rerouted: u64,
1540 #[serde(default)]
1542 pub swaps_simulated: u64,
1543 #[serde(default)]
1545 pub swaps_succeeded: u64,
1546 #[serde(default)]
1548 pub requote_failures: u64,
1549 #[serde(default)]
1551 pub override_setup_failures: u64,
1552 #[serde(default)]
1554 pub metis_unavailable_blocks: u64,
1555 #[serde(default)]
1559 pub quote_attempts_unavailable: u64,
1560 #[serde(default)]
1562 pub direct_fill_matched: u64,
1563 #[serde(default)]
1565 pub direct_fill_built: u64,
1566 #[serde(default)]
1570 pub direct_fill_rejections: BTreeMap<String, u64>,
1571 #[serde(default)]
1573 pub direct_fill_outcomes: BTreeMap<String, u64>,
1574 #[serde(default)]
1576 pub direct_fill_scored: u64,
1577 #[serde(default)]
1581 pub direct_fill_bps_total: i64,
1582}
1583
1584#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1586#[serde(rename_all = "camelCase")]
1587pub struct SessionSummary {
1588 pub correct_simulation: usize,
1591 pub incorrect_simulation: usize,
1594 pub execution_errors: usize,
1596 pub balance_diff: usize,
1598 pub log_diff: usize,
1600 #[serde(default)]
1603 pub unsanitizable: usize,
1604 #[serde(default, skip_serializing_if = "Option::is_none")]
1607 pub reroute_stats: Option<Box<RerouteStatsReport>>,
1608}
1609
1610impl SessionSummary {
1611 pub fn has_deviations(&self) -> bool {
1613 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
1614 }
1615
1616 pub fn total_transactions(&self) -> usize {
1618 self.correct_simulation
1619 + self.incorrect_simulation
1620 + self.execution_errors
1621 + self.balance_diff
1622 + self.log_diff
1623 }
1624
1625 pub fn deviation_details(&self, balance_label: &str, log_label: &str) -> String {
1630 format!(
1631 "{} incorrect simulations, {} execution errors, {} {balance_label}, {} {log_label}",
1632 self.incorrect_simulation, self.execution_errors, self.balance_diff, self.log_diff,
1633 )
1634 }
1635}
1636
1637impl std::fmt::Display for SessionSummary {
1638 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1639 let total = self.total_transactions();
1640 write!(
1641 f,
1642 "Session summary: {total} transactions\n\
1643 \x20 - {} correct simulation\n\
1644 \x20 - {} incorrect simulation\n\
1645 \x20 - {} execution errors\n\
1646 \x20 - {} unsanitizable\n\
1647 \x20 - {} balance diffs\n\
1648 \x20 - {} log diffs",
1649 self.correct_simulation,
1650 self.incorrect_simulation,
1651 self.execution_errors,
1652 self.unsanitizable,
1653 self.balance_diff,
1654 self.log_diff,
1655 )
1656 }
1657}
1658
1659#[derive(Debug, Clone, Serialize, Deserialize)]
1661#[serde(rename_all = "camelCase")]
1662pub enum BacktestError {
1663 InvalidTransactionEncoding {
1664 index: usize,
1665 error: String,
1666 },
1667 InvalidTransactionFormat {
1668 index: usize,
1669 error: String,
1670 },
1671 InvalidAccountEncoding {
1672 address: String,
1673 encoding: BinaryEncoding,
1674 error: String,
1675 },
1676 InvalidAccountOwner {
1677 address: String,
1678 error: String,
1679 },
1680 InvalidAccountPubkey {
1681 address: String,
1682 error: String,
1683 },
1684 NoMoreBlocks,
1685 BundleNotFound {
1686 start_slot: u64,
1687 end_slot: u64,
1688 },
1689 AdvanceSlotFailed {
1690 slot: u64,
1691 error: String,
1692 },
1693 FinalizeSlotFailed {
1694 slot: u64,
1695 error: String,
1696 },
1697 InvalidRequest {
1698 error: String,
1699 },
1700 Internal {
1701 error: String,
1702 },
1703 InvalidBlockhashFormat {
1704 slot: u64,
1705 error: String,
1706 },
1707 InitializingSysvarsFailed {
1708 slot: u64,
1709 error: String,
1710 },
1711 ClerkError {
1712 error: String,
1713 },
1714 SimulationError {
1715 error: String,
1716 },
1717 SessionNotFound {
1718 session_id: String,
1719 },
1720 SessionOwnerMismatch,
1721 SessionOwnershipBusy {
1726 reason: String,
1727 },
1728 NoProducedBlocksInRange {
1734 start_slot: u64,
1735 end_slot: u64,
1736 },
1737}
1738
1739impl BacktestError {
1740 pub fn is_terminal(&self) -> bool {
1744 matches!(
1745 self,
1746 BacktestError::NoMoreBlocks
1747 | BacktestError::BundleNotFound { .. }
1748 | BacktestError::AdvanceSlotFailed { .. }
1749 | BacktestError::FinalizeSlotFailed { .. }
1750 | BacktestError::Internal { .. }
1751 | BacktestError::NoProducedBlocksInRange { .. }
1752 )
1753 }
1754}
1755
1756#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1758pub struct AvailableRange {
1759 pub bundle_start_slot: u64,
1760 pub bundle_start_slot_utc: Option<String>,
1761 pub max_bundle_end_slot: Option<u64>,
1762 pub max_bundle_end_slot_utc: Option<String>,
1763 pub max_bundle_size: Option<u64>,
1764 #[serde(default)]
1767 pub kind: Option<BundleKind>,
1768 #[serde(default)]
1771 pub scope: Option<RangeScope>,
1772}
1773
1774#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1776#[serde(rename_all = "snake_case")]
1777pub enum RangeScope {
1778 Global,
1780 User,
1782}
1783
1784impl RangeScope {
1785 pub fn as_str(self) -> &'static str {
1787 match self {
1788 Self::Global => "global",
1789 Self::User => "user",
1790 }
1791 }
1792}
1793
1794#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1796#[serde(rename_all = "snake_case")]
1797pub enum BundleKind {
1798 #[default]
1800 Blocks,
1801 AccountState,
1805}
1806
1807pub const REPLAY_RANGE_ADVICE: &str = "replayed state is resolved by the recording that begins \
1811 where the request does, so request a range that begins where one was recorded and ends \
1812 within it";
1813
1814impl BundleKind {
1815 fn coverage_noun(self) -> &'static str {
1817 match self {
1818 Self::Blocks => "bundle range",
1819 Self::AccountState => "account-state bundle you own",
1820 }
1821 }
1822
1823 fn coverage_nouns(self) -> &'static str {
1825 match self {
1826 Self::Blocks => "bundle ranges",
1827 Self::AccountState => "account-state bundles you own",
1828 }
1829 }
1830
1831 pub fn as_str(self) -> &'static str {
1833 match self {
1834 Self::Blocks => "blocks",
1835 Self::AccountState => "account_state",
1836 }
1837 }
1838
1839 pub fn from_str_opt(value: &str) -> Option<Self> {
1841 match value {
1842 "blocks" => Some(Self::Blocks),
1843 "account_state" => Some(Self::AccountState),
1844 _ => None,
1845 }
1846 }
1847}
1848
1849impl fmt::Display for BundleKind {
1852 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1853 let label = match self {
1854 Self::Blocks => "transactions",
1855 Self::AccountState => "account_state",
1856 };
1857 f.pad(label)
1858 }
1859}
1860
1861#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1865pub struct BundleBuildRequest {
1866 pub start_slot: u64,
1867 pub end_slot: u64,
1868 #[serde(default)]
1869 pub bundle_size: Option<u64>,
1870 #[serde(default)]
1873 pub idempotency_key: Option<String>,
1874 #[serde(default)]
1876 pub kind: BundleKind,
1877}
1878
1879#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1880#[serde(rename_all = "snake_case")]
1881pub enum BundleBuildStatus {
1882 Completed,
1884 Failed,
1886 InProgress,
1888 NeedsInvestigation,
1891}
1892
1893impl BundleBuildStatus {
1894 pub fn as_str(self) -> &'static str {
1896 match self {
1897 Self::Completed => "completed",
1898 Self::Failed => "failed",
1899 Self::InProgress => "in_progress",
1900 Self::NeedsInvestigation => "needs_investigation",
1901 }
1902 }
1903}
1904
1905#[derive(Debug, Clone, Serialize, Deserialize)]
1907pub struct BundleBuildStatusResponse {
1908 pub request_id: String,
1909 pub start_slot: u64,
1910 pub end_slot: u64,
1911 pub bundle_size: Option<u64>,
1912 pub status: BundleBuildStatus,
1913 pub created_at_unix_ms: u64,
1916 #[serde(default)]
1918 pub kind: BundleKind,
1919}
1920
1921#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1923pub struct CompletedBundle {
1924 pub start_slot: u64,
1925 pub end_slot: u64,
1926 pub kind: BundleKind,
1927 #[serde(default)]
1929 pub completed_at_unix_ms: Option<i64>,
1930 #[serde(default)]
1931 pub size_bytes: Option<i64>,
1932}
1933
1934#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1936pub struct BundleDeleteRequest {
1937 pub start_slot: u64,
1938 pub end_slot: u64,
1939 #[serde(default)]
1941 pub kind: BundleKind,
1942}
1943
1944#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1946pub struct BundleDeleteResponse {
1947 pub start_slot: u64,
1948 pub end_slot: u64,
1949 pub kind: BundleKind,
1950}
1951
1952#[derive(Debug, Clone, Serialize, Deserialize)]
1954pub struct BundleBuildListResponse {
1955 pub builds: Vec<BundleBuildStatusResponse>,
1958 #[serde(default)]
1963 pub bundles: Option<Vec<CompletedBundle>>,
1964}
1965
1966#[derive(Debug, Clone, Serialize, Deserialize)]
1969pub struct SnapshotSlotResponse {
1970 pub snapshot_slot: u64,
1971}
1972
1973pub fn split_range(
1996 ranges: &[AvailableRange],
1997 requested: SlotRange,
1998 kind: BundleKind,
1999) -> Result<Vec<(u64, u64)>, String> {
2000 let (requested_start, requested_end) = (requested.start(), requested.end());
2001
2002 let ends_by_start = ranges
2009 .iter()
2010 .filter(|r| r.kind.unwrap_or_default() == kind)
2011 .filter_map(|r| {
2012 r.max_bundle_end_slot
2013 .filter(|&end| end >= r.bundle_start_slot)
2014 .map(|end| (r.bundle_start_slot, end))
2015 })
2016 .fold(
2017 BTreeMap::<u64, BTreeSet<u64>>::new(),
2018 |mut ends_by_start, (start, end)| {
2019 ends_by_start.entry(start).or_default().insert(end);
2020 ends_by_start
2021 },
2022 );
2023
2024 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
2032 ends.iter()
2033 .next_back()
2034 .is_some_and(|&end| end >= requested_start)
2035 }) else {
2036 return Err(format!(
2037 "start_slot {requested_start} is not covered by any available {}",
2038 kind.coverage_noun()
2039 ));
2040 };
2041
2042 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
2048 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
2049 let mut best: Option<Vec<(u64, u64)>> = None;
2050 for &end in ends {
2051 let candidate = if end >= requested_end {
2052 Some(vec![(start, requested_end)])
2053 } else {
2054 best_from.get(&(end + 1)).map(|rest| {
2055 std::iter::once((start, end))
2056 .chain(rest.iter().copied())
2057 .collect()
2058 })
2059 };
2060 if let Some(candidate) = candidate
2061 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
2062 {
2063 best = Some(candidate);
2064 }
2065 }
2066 if let Some(best) = best {
2067 best_from.insert(start, best);
2068 }
2069 }
2070
2071 best_from.remove(&anchor_start).ok_or_else(|| {
2072 let mut covered_to = anchor_start.saturating_sub(1);
2076 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
2077 if start > covered_to.saturating_add(1) {
2078 break;
2079 }
2080 if let Some(&end) = ends.iter().next_back() {
2081 covered_to = covered_to.max(end);
2082 }
2083 }
2084 if covered_to < requested_end {
2085 format!("gap in coverage at slot {}", covered_to + 1)
2086 } else {
2087 format!(
2088 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available {}",
2089 kind.coverage_nouns()
2090 )
2091 }
2092 })
2093}
2094
2095impl From<BacktestError> for BacktestResponse {
2096 fn from(error: BacktestError) -> Self {
2097 Self::Error(error)
2098 }
2099}
2100
2101impl std::error::Error for BacktestError {}
2102
2103impl fmt::Display for BacktestError {
2104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2105 match self {
2106 BacktestError::InvalidTransactionEncoding { index, error } => {
2107 write!(f, "invalid transaction encoding at index {index}: {error}")
2108 }
2109 BacktestError::InvalidTransactionFormat { index, error } => {
2110 write!(f, "invalid transaction format at index {index}: {error}")
2111 }
2112 BacktestError::InvalidAccountEncoding {
2113 address,
2114 encoding,
2115 error,
2116 } => write!(
2117 f,
2118 "invalid encoding for account {address} ({encoding:?}): {error}"
2119 ),
2120 BacktestError::InvalidAccountOwner { address, error } => {
2121 write!(f, "invalid owner for account {address}: {error}")
2122 }
2123 BacktestError::InvalidAccountPubkey { address, error } => {
2124 write!(f, "invalid account pubkey {address}: {error}")
2125 }
2126 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
2127 BacktestError::BundleNotFound {
2128 start_slot,
2129 end_slot,
2130 } => write!(
2131 f,
2132 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
2133 ),
2134 BacktestError::AdvanceSlotFailed { slot, error } => {
2135 write!(f, "failed to advance to slot {slot}: {error}")
2136 }
2137 BacktestError::FinalizeSlotFailed { slot, error } => {
2138 write!(f, "failed to finalize slot {slot}: {error}")
2139 }
2140 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
2141 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
2142 BacktestError::InvalidBlockhashFormat { slot, error } => {
2143 write!(f, "invalid blockhash at slot {slot}: {error}")
2144 }
2145 BacktestError::InitializingSysvarsFailed { slot, error } => {
2146 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
2147 }
2148 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
2149 BacktestError::SimulationError { error } => {
2150 write!(f, "simulation error: {error}")
2151 }
2152 BacktestError::SessionNotFound { session_id } => {
2153 write!(f, "session not found: {session_id}")
2154 }
2155 BacktestError::SessionOwnerMismatch => {
2156 write!(f, "session owner mismatch")
2157 }
2158 BacktestError::SessionOwnershipBusy { reason } => {
2159 write!(f, "session ownership busy: {reason}")
2160 }
2161 BacktestError::NoProducedBlocksInRange {
2162 start_slot,
2163 end_slot,
2164 } => write!(
2165 f,
2166 "no produced blocks in requested range {start_slot}..={end_slot}; \
2167 slot(s) may be skipped on-chain"
2168 ),
2169 }
2170 }
2171}
2172
2173#[cfg(test)]
2174mod tests {
2175 use rstest::rstest;
2176 use solana_account::ReadableAccount;
2177
2178 use super::*;
2179
2180 fn address(byte: u8) -> Address {
2181 Address::from([byte; 32])
2182 }
2183
2184 #[rstest]
2185 #[case::one("jupiter", Some(&[SwapAggregator::Jupiter][..]))]
2186 #[case::several("jupiter,okx", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2187 #[case::spaced_and_uppercased(" Jupiter , OKX ", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2188 #[case::repeats_collapse("okx,okx", Some(&[SwapAggregator::Okx][..]))]
2189 #[case::trailing_separator("okx,", Some(&[SwapAggregator::Okx][..]))]
2190 #[case::unknown("jupiter,bogus", None)]
2191 #[case::empty("", None)]
2192 #[case::separators_only(",,", None)]
2193 fn reroute_aggregators_parse_the_shapes_a_flag_carries(
2194 #[case] input: &str,
2195 #[case] expected: Option<&[SwapAggregator]>,
2196 ) {
2197 assert_eq!(
2198 input.parse::<RerouteAggregators>().ok(),
2199 expected.map(|aggregators| RerouteAggregators::new(aggregators.iter().copied()))
2200 );
2201 }
2202
2203 #[test]
2205 fn reroute_aggregators_round_trip_through_their_rendering() {
2206 let aggregators = RerouteAggregators::new([SwapAggregator::Dflow, SwapAggregator::Jupiter]);
2207
2208 assert_eq!(aggregators.to_string(), "jupiter,dflow");
2209 assert_eq!(
2210 aggregators
2211 .to_string()
2212 .parse::<RerouteAggregators>()
2213 .unwrap(),
2214 aggregators
2215 );
2216 assert_eq!(RerouteAggregators::default().to_string(), "jupiter");
2217 }
2218
2219 #[test]
2222 fn aggregators_deserialize_through_the_same_parser_the_flag_uses() {
2223 let params: CreateSessionParams = serde_json::from_value(
2224 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {"aggregators": ["Jupiter", "OKX"]}}),
2225 )
2226 .expect("decode");
2227
2228 assert_eq!(
2229 params.reroute.expect("rerouting requested").aggregators,
2230 RerouteAggregators::new([SwapAggregator::Jupiter, SwapAggregator::Okx])
2231 );
2232 }
2233
2234 #[test]
2237 fn params_without_the_new_reroute_fields_decode_to_the_narrow_defaults() {
2238 let params: CreateSessionParams = serde_json::from_value(
2239 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {}}),
2240 )
2241 .expect("decode");
2242
2243 let reroute = params.reroute.expect("rerouting requested");
2244 assert_eq!(reroute.aggregators, RerouteAggregators::default());
2245 assert!(reroute.aggregators.contains(SwapAggregator::Jupiter));
2246 assert!(!reroute.aggregators.contains(SwapAggregator::Okx));
2247 assert!(!reroute.circular_arbs);
2248 assert!(!reroute.detect_failed_l1_swaps);
2249 }
2250
2251 #[test]
2253 fn a_request_without_reroute_reserves_no_sidecar() {
2254 let params: CreateSessionParams =
2255 serde_json::from_value(serde_json::json!({"startSlot": 1, "endSlot": 2}))
2256 .expect("decode");
2257
2258 assert!(!params.requires_sidecar());
2259 }
2260
2261 #[test]
2266 fn a_per_slot_action_schedule_compresses_across_actions() {
2267 let slots = 2_000u64;
2268 let actions = (0..slots)
2269 .map(|slot| ScheduledAction {
2270 anchor: ActionAnchor::AfterSlot { slots: vec![slot] },
2271 kind: ActionKind::Simulate,
2272 feeds_reroute: false,
2273 transactions: Vec::new(),
2274 account_overrides: AccountModifications(BTreeMap::from([(
2275 address(7),
2276 AccountData {
2277 data: EncodedBinary::new(
2279 BASE64.encode(
2280 [&slot.to_le_bytes()[..], &[0xAB; 2040][..]]
2281 .concat()
2282 .as_slice(),
2283 ),
2284 BinaryEncoding::Base64,
2285 ),
2286 executable: false,
2287 lamports: 15_144_966,
2288 owner: address(9),
2289 space: 2048,
2290 },
2291 )])),
2292 return_accounts: Vec::new(),
2293 label: None,
2294 })
2295 .collect::<Vec<_>>();
2296 let params = CreateSessionParams::builder()
2297 .start_slot(1)
2298 .end_slot(slots)
2299 .actions(actions.clone())
2300 .build();
2301
2302 let encoded = serde_json::to_string(¶ms).expect("serialize");
2303 let raw = serde_json::to_string(&actions).expect("serialize raw actions");
2304 assert!(
2305 encoded.len() * 8 < raw.len(),
2306 "expected the schedule to compress well; got {} bytes from {} raw",
2307 encoded.len(),
2308 raw.len()
2309 );
2310
2311 let decoded: CreateSessionParams = serde_json::from_str(&encoded).expect("round trip");
2312 assert_eq!(
2313 serde_json::to_string(&decoded.actions).expect("serialize decoded"),
2314 raw
2315 );
2316 }
2317
2318 #[test]
2321 fn an_empty_action_list_is_omitted_entirely() {
2322 let params = CreateSessionParams::builder()
2323 .start_slot(1)
2324 .end_slot(2)
2325 .build();
2326 let json = serde_json::to_value(¶ms).expect("serialize");
2327 assert!(json.get("actions").is_none(), "{json}");
2328 let decoded: CreateSessionParams = serde_json::from_value(json).expect("round trip");
2329 assert!(decoded.actions.is_empty());
2330 }
2331
2332 #[test]
2335 fn send_actions_conflict_with_replay_only_together() {
2336 let params = |replay, kind| {
2337 CreateSessionParams::builder()
2338 .start_slot(1)
2339 .end_slot(2)
2340 .replay_account_state(replay)
2341 .actions(vec![ScheduledAction {
2342 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2343 kind,
2344 feeds_reroute: false,
2345 transactions: vec!["dGVzdA==".to_string()],
2346 account_overrides: Default::default(),
2347 return_accounts: Default::default(),
2348 label: None,
2349 }])
2350 .build()
2351 };
2352 assert!(replay_support::send_action_conflict(¶ms(false, ActionKind::Send)).is_none());
2353 assert!(
2354 replay_support::send_action_conflict(¶ms(true, ActionKind::Simulate)).is_none()
2355 );
2356 let mut publishing = params(true, ActionKind::Simulate);
2357 publishing.actions[0].feeds_reroute = true;
2358 assert!(replay_support::send_action_conflict(&publishing).is_none());
2359
2360 let conflict = replay_support::send_action_conflict(¶ms(true, ActionKind::Send))
2361 .expect("send under replay conflicts");
2362 assert!(conflict.contains("kind: send"), "{conflict}");
2363 assert!(conflict.contains(replay_support::SEND_ACTION), "{conflict}");
2364 }
2365
2366 fn override_entry(owner: Address, executable: bool) -> AccountData {
2367 AccountData {
2368 data: EncodedBinary::from_bytes(&[1, 2, 3], BinaryEncoding::Base64),
2369 executable,
2370 lamports: 1,
2371 owner,
2372 space: 3,
2373 }
2374 }
2375
2376 fn before() -> ActionAnchor {
2377 ActionAnchor::BeforeSlot { slots: vec![1] }
2378 }
2379
2380 fn valid() -> ScheduledAction {
2381 override_action(before(), address(3), override_entry(address(4), false))
2382 }
2383
2384 fn override_action(
2385 anchor: ActionAnchor,
2386 account: Address,
2387 data: AccountData,
2388 ) -> ScheduledAction {
2389 ScheduledAction {
2390 anchor,
2391 kind: ActionKind::default(),
2392 transactions: Vec::new(),
2393 account_overrides: AccountModifications(BTreeMap::from([(account, data)])),
2394 return_accounts: Vec::new(),
2395 label: None,
2396 feeds_reroute: true,
2397 }
2398 }
2399
2400 #[test]
2401 fn override_action_round_trips() {
2402 let key = address(1);
2403 let entry = override_entry(address(2), false);
2404 let action = override_action(
2405 ActionAnchor::BeforeSlot { slots: vec![5, 9] },
2406 key,
2407 entry.clone(),
2408 );
2409 let json = serde_json::to_value(&action).unwrap();
2410 assert!(
2411 json["anchor"]["slots"].is_string(),
2412 "slots travel as a compressed blob: {json}"
2413 );
2414 let back: ScheduledAction = serde_json::from_value(json).unwrap();
2415 assert_eq!(back.anchor, ActionAnchor::BeforeSlot { slots: vec![5, 9] });
2416 assert!(back.feeds_reroute);
2417 assert!(!back.is_computed_override(), "no transactions to simulate");
2418 let survived = &back.account_overrides.0[&key];
2419 assert_eq!(survived.lamports, entry.lamports);
2420 assert_eq!(survived.data.data, entry.data.data);
2421 }
2422
2423 #[rstest]
2424 #[case::executable(override_action(before(), address(3), override_entry(address(10), true)))]
2425 #[case::program_owned(override_action(
2426 before(),
2427 address(3),
2428 override_entry(solana_account::PROGRAM_OWNERS[0], false),
2429 ))]
2430 #[case::token_owned(override_action(
2431 before(),
2432 address(3),
2433 override_entry(TOKEN_PROGRAM_IDS[0], false),
2434 ))]
2435 #[case::after_slot_anchor(override_action(
2436 ActionAnchor::AfterSlot { slots: vec![1] },
2437 address(3),
2438 override_entry(address(4), false),
2439 ))]
2440 #[case::nothing_to_post(ScheduledAction {
2441 account_overrides: AccountModifications::default(),
2442 ..valid()
2443 })]
2444 #[case::send(ScheduledAction {
2446 kind: ActionKind::Send,
2447 ..valid()
2448 })]
2449 #[allow(deprecated, reason = "exercise the loader-owner rejection path")]
2450 fn override_actions_reject_invalid_accounts_and_shapes(#[case] action: ScheduledAction) {
2451 assert!(decode_override_actions(&[action]).is_err());
2452 }
2453
2454 #[test]
2456 fn override_actions_accept_a_static_and_a_computed_shape() {
2457 assert!(decode_override_actions(&[valid()]).is_ok());
2458 assert!(
2461 decode_override_actions(&[ScheduledAction {
2462 transactions: vec!["dGVzdA==".to_string()],
2463 ..valid()
2464 }])
2465 .is_ok()
2466 );
2467 }
2468
2469 #[test]
2470 fn override_actions_merge_account_wise_later_wins() {
2471 let entry = |lamports| AccountData {
2472 lamports,
2473 ..override_entry(address(9), false)
2474 };
2475 let slot_action = |slots, account, lamports| {
2476 override_action(ActionAnchor::BeforeSlot { slots }, account, entry(lamports))
2477 };
2478 let actions = [
2479 slot_action(vec![5, 7], address(1), 1),
2480 slot_action(vec![5], address(2), 2),
2481 slot_action(vec![5], address(1), 3),
2482 ScheduledAction {
2483 anchor: ActionAnchor::AfterSlot { slots: vec![5] },
2484 kind: ActionKind::Simulate,
2485 feeds_reroute: false,
2486 transactions: vec!["dGVzdA==".to_string()],
2487 account_overrides: AccountModifications::default(),
2488 return_accounts: Vec::new(),
2489 label: None,
2490 },
2491 ];
2492 let schedule = decode_override_actions(&actions).unwrap();
2493 assert_eq!(schedule.slots().collect::<Vec<_>>(), vec![5, 7]);
2494 let lamports_at = |key, slot| {
2495 schedule
2496 .active_for(&key, slot)
2497 .map(|account| account.lamports())
2498 };
2499 assert_eq!(lamports_at(address(1), 5), Some(3));
2500 assert_eq!(lamports_at(address(2), 5), Some(2));
2501 assert_eq!(lamports_at(address(1), 7), Some(1));
2502 }
2503
2504 #[test]
2505 fn computed_override_resolution_preserves_action_order_and_failure() {
2506 let key = address(1);
2507 let static_action = |lamports| {
2508 override_action(
2509 ActionAnchor::BeforeSlot { slots: vec![5] },
2510 key,
2511 AccountData {
2512 lamports,
2513 ..override_entry(address(9), false)
2514 },
2515 )
2516 };
2517 let computed_action = || ScheduledAction {
2518 transactions: vec!["dGVzdA==".to_string()],
2519 ..static_action(2)
2520 };
2521
2522 let computed_last =
2523 decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2524 computed_last
2525 .resolve(5, 1, Some(HashMap::from([(key, shared_account(2))])))
2526 .unwrap();
2527 assert_eq!(
2528 computed_last.active_for(&key, 5).map(|a| a.lamports()),
2529 Some(2)
2530 );
2531
2532 let static_last = decode_override_actions(&[computed_action(), static_action(3)]).unwrap();
2533 static_last
2534 .resolve(5, 0, Some(HashMap::from([(key, shared_account(2))])))
2535 .unwrap();
2536 assert_eq!(
2537 static_last.active_for(&key, 5).map(|a| a.lamports()),
2538 Some(3)
2539 );
2540
2541 let failed = decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2542 failed.resolve(5, 1, None).unwrap();
2543 assert_eq!(failed.active_for(&key, 5).map(|a| a.lamports()), Some(1));
2544 assert!(failed.resolve(5, 1, None).is_err(), "resolving twice");
2545 }
2546
2547 fn shared_account(lamports: u64) -> AccountSharedData {
2548 AccountSharedData::from(solana_account::Account {
2549 lamports,
2550 data: vec![1],
2551 owner: address(9),
2552 executable: false,
2553 rent_epoch: 0,
2554 })
2555 }
2556
2557 fn carry_forward_schedule() -> (Address, Address, OverrideSchedule) {
2558 let (a, b) = (address(1), address(2));
2559 let schedule = OverrideSchedule::from(BTreeMap::from([
2560 (10, HashMap::from([(a, shared_account(1))])),
2561 (
2562 20,
2563 HashMap::from([(a, shared_account(2)), (b, shared_account(9))]),
2564 ),
2565 ]));
2566 (a, b, schedule)
2567 }
2568
2569 #[test]
2570 fn override_schedule_resolves_carry_forward_per_account() {
2571 let (a, b, schedule) = carry_forward_schedule();
2572
2573 assert!(schedule.active_for(&a, 9).is_none());
2574 let cases = [
2575 (9, None, None),
2576 (10, Some(1), None),
2577 (15, Some(1), None),
2578 (20, Some(2), Some(9)),
2579 (u64::MAX, Some(2), Some(9)),
2580 ];
2581 for (slot, want_a, want_b) in cases {
2582 assert_eq!(
2583 schedule
2584 .active_for(&a, slot)
2585 .map(|account| account.lamports()),
2586 want_a,
2587 "active_for a at {slot}"
2588 );
2589 let active = schedule.active_at(slot);
2590 assert_eq!(
2591 active.get(&a).map(ReadableAccount::lamports),
2592 want_a,
2593 "active_at a at {slot}"
2594 );
2595 assert_eq!(
2596 active.get(&b).map(ReadableAccount::lamports),
2597 want_b,
2598 "active_at b at {slot}"
2599 );
2600 }
2601 }
2602
2603 #[test]
2604 fn newly_active_yields_final_values_in_window() {
2605 let (a, b, schedule) = carry_forward_schedule();
2606
2607 let newly: HashMap<_, _> = schedule.newly_active(Some(10), 20).into_iter().collect();
2608 assert_eq!(newly.len(), 2);
2609 assert_eq!(newly[&a].lamports(), 2);
2610 assert_eq!(newly[&b].lamports(), 9);
2611 let spanning: HashMap<_, _> = schedule.newly_active(None, 20).into_iter().collect();
2612 assert_eq!(spanning[&a].lamports(), 2, "later slot wins");
2613 assert_eq!(schedule.newly_active(None, 15).len(), 1);
2614 }
2615
2616 #[test]
2617 fn reroute_filter_round_trips_camel_case() {
2618 let filter = RerouteFilter {
2619 pairs: BTreeSet::from([MintPair::new(address(1), address(2))]),
2620 };
2621 let json = serde_json::to_value(&filter).unwrap();
2622 assert!(
2623 json["pairs"][0][0].is_string() && json["pairs"][0][1].is_string(),
2624 "a pair serializes as two base58 strings: {json}"
2625 );
2626 let back: RerouteFilter = serde_json::from_value(json).unwrap();
2627 assert_eq!(back, filter);
2628 }
2629
2630 #[test]
2633 fn mint_pairs_are_normalized_however_they_are_built() {
2634 let (one, other) = (address(1), address(2));
2635 assert_eq!(MintPair::new(one, other), MintPair::new(other, one));
2636
2637 let reversed: MintPair =
2638 serde_json::from_value(serde_json::json!([other.to_string(), one.to_string()]))
2639 .unwrap();
2640 assert_eq!(reversed, MintPair::new(one, other));
2641 }
2642
2643 #[test]
2644 fn fail_fast_divergence_kind_str_round_trips() {
2645 for kind in [
2646 FailFastDivergenceKind::AnyNonBenign,
2647 FailFastDivergenceKind::Tracked,
2648 ] {
2649 assert_eq!(
2650 FailFastDivergenceKind::from_str_opt(kind.as_str()),
2651 Some(kind)
2652 );
2653 }
2654 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
2655 assert_eq!(
2656 FailFastDivergenceKind::default(),
2657 FailFastDivergenceKind::AnyNonBenign
2658 );
2659 }
2660
2661 #[test]
2662 fn bundle_not_found_is_terminal_and_descriptive() {
2663 let error = BacktestError::BundleNotFound {
2664 start_slot: 100,
2665 end_slot: 600,
2666 };
2667
2668 assert!(error.is_terminal());
2669 assert_eq!(
2670 error.to_string(),
2671 "block bundle not found for start_slot=100, end_slot=600"
2672 );
2673 }
2674
2675 #[test]
2676 fn no_produced_blocks_is_terminal_and_descriptive() {
2677 let error = BacktestError::NoProducedBlocksInRange {
2678 start_slot: 100,
2679 end_slot: 100,
2680 };
2681
2682 assert!(error.is_terminal());
2683 assert_eq!(
2686 error.to_string(),
2687 "no produced blocks in requested range 100..=100; slot(s) may be skipped on-chain"
2688 );
2689 }
2690
2691 #[test]
2692 fn bundle_build_request_optional_fields_default_to_none() {
2693 let req: BundleBuildRequest =
2694 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
2695 assert_eq!((req.start_slot, req.end_slot), (1, 2));
2696 assert_eq!(req.bundle_size, None);
2697 assert_eq!(req.idempotency_key, None);
2698 }
2699
2700 #[test]
2701 fn bundle_build_request_parses_optional_fields() {
2702 let req: BundleBuildRequest = serde_json::from_str(
2703 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
2704 )
2705 .expect("parse");
2706 assert_eq!(req.bundle_size, Some(500));
2707 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
2708 }
2709
2710 #[test]
2711 fn bundle_build_status_serde_round_trips_with_snake_case() {
2712 let cases = [
2713 (BundleBuildStatus::Completed, "\"completed\""),
2714 (BundleBuildStatus::Failed, "\"failed\""),
2715 (BundleBuildStatus::InProgress, "\"in_progress\""),
2716 (
2717 BundleBuildStatus::NeedsInvestigation,
2718 "\"needs_investigation\"",
2719 ),
2720 ];
2721 for (status, expected) in cases {
2722 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
2723 assert_eq!(
2724 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
2725 status
2726 );
2727 assert_eq!(status.as_str(), expected.trim_matches('"'));
2728 }
2729 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
2730 }
2731
2732 #[test]
2733 fn bundle_build_request_without_kind_deserializes_as_blocks() {
2734 let request: BundleBuildRequest =
2736 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
2737 assert_eq!(request.kind, BundleKind::Blocks);
2738 }
2739
2740 #[test]
2741 fn bundle_kind_round_trips_over_serde_and_as_str() {
2742 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
2743 let json = serde_json::to_string(&kind).unwrap();
2744 assert_eq!(json, format!("\"{}\"", kind.as_str()));
2745 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
2746 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
2749 }
2750 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
2751 assert_eq!(BundleKind::from_str_opt("accountState"), None);
2752 }
2753
2754 #[test]
2755 fn bundle_build_request_carries_kind_through_serde() {
2756 let request = BundleBuildRequest {
2757 start_slot: 100,
2758 end_slot: 200,
2759 kind: BundleKind::AccountState,
2760 ..Default::default()
2761 };
2762 let json = serde_json::to_string(&request).unwrap();
2763 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2764 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
2765 assert_eq!(parsed.kind, BundleKind::AccountState);
2766 }
2767
2768 #[test]
2769 fn bundle_build_status_response_serializes_request_and_status() {
2770 let response = BundleBuildStatusResponse {
2771 request_id: "r".to_string(),
2772 start_slot: 100,
2773 end_slot: 200,
2774 bundle_size: Some(50),
2775 status: BundleBuildStatus::InProgress,
2776 created_at_unix_ms: 0,
2777 kind: BundleKind::Blocks,
2778 };
2779 let json = serde_json::to_value(&response).unwrap();
2780 assert_eq!(json["request_id"].as_str(), Some("r"));
2781 assert_eq!(json["start_slot"].as_u64(), Some(100));
2782 assert_eq!(json["bundle_size"].as_u64(), Some(50));
2783 assert_eq!(json["status"].as_str(), Some("in_progress"));
2784 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
2785 }
2786
2787 #[test]
2788 fn an_empty_bundles_list_is_distinct_from_an_absent_one() {
2789 let absent: BundleBuildListResponse = serde_json::from_str(r#"{"builds":[]}"#).unwrap();
2791 let empty: BundleBuildListResponse =
2792 serde_json::from_str(r#"{"builds":[],"bundles":[]}"#).unwrap();
2793 assert_eq!(absent.bundles, None);
2794 assert_eq!(empty.bundles, Some(Vec::new()));
2795 }
2796
2797 #[test]
2798 fn completed_bundle_round_trips() {
2799 let bundle = CompletedBundle {
2800 start_slot: 100,
2801 end_slot: 200,
2802 kind: BundleKind::AccountState,
2803 completed_at_unix_ms: Some(1_700_000_000_000),
2804 size_bytes: Some(4_096),
2805 };
2806 let json = serde_json::to_string(&bundle).unwrap();
2807 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2808 assert_eq!(
2809 serde_json::from_str::<CompletedBundle>(&json).unwrap(),
2810 bundle
2811 );
2812
2813 let sparse = r#"{"start_slot":1,"end_slot":2,"kind":"blocks"}"#;
2814 let parsed: CompletedBundle = serde_json::from_str(sparse).unwrap();
2815 assert_eq!(parsed.completed_at_unix_ms, None);
2816 assert_eq!(parsed.size_bytes, None);
2817 }
2818
2819 #[test]
2820 fn available_range_reads_a_server_that_predates_kind_and_scope() {
2821 let json = r#"{
2823 "bundle_start_slot": 100,
2824 "bundle_start_slot_utc": null,
2825 "max_bundle_end_slot": 200,
2826 "max_bundle_end_slot_utc": null,
2827 "max_bundle_size": 101
2828 }"#;
2829 let parsed: AvailableRange = serde_json::from_str(json).unwrap();
2830 assert_eq!(parsed.bundle_start_slot, 100);
2831 assert_eq!(parsed.kind, None);
2832 assert_eq!(parsed.scope, None);
2833 }
2834
2835 #[test]
2836 fn available_range_ignores_fields_it_does_not_know() {
2837 let json = r#"{"bundle_start_slot": 1, "some_future_field": "x"}"#;
2839 assert_eq!(
2840 serde_json::from_str::<AvailableRange>(json)
2841 .unwrap()
2842 .bundle_start_slot,
2843 1
2844 );
2845 }
2846
2847 #[test]
2848 fn available_range_labels_round_trip() {
2849 let range = AvailableRange {
2850 bundle_start_slot: 100,
2851 max_bundle_end_slot: Some(200),
2852 kind: Some(BundleKind::AccountState),
2853 scope: Some(RangeScope::User),
2854 ..Default::default()
2855 };
2856 let json = serde_json::to_string(&range).unwrap();
2857 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2858 assert!(json.contains(r#""scope":"user""#), "{json}");
2859 let parsed: AvailableRange = serde_json::from_str(&json).unwrap();
2860 assert_eq!(parsed.kind, Some(BundleKind::AccountState));
2861 assert_eq!(parsed.scope, Some(RangeScope::User));
2862 }
2863
2864 fn range(start: u64, end: u64) -> AvailableRange {
2865 AvailableRange {
2866 bundle_start_slot: start,
2867 max_bundle_end_slot: Some(end),
2868 ..Default::default()
2869 }
2870 }
2871
2872 #[rstest::rstest]
2876 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
2877 #[case::multi(
2878 vec![range(100, 200), range(201, 300), range(301, 400)],
2879 100, 300, Some(vec![(100, 200), (201, 300)])
2880 )]
2881 #[case::nested(
2884 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
2885 100, 900, Some(vec![(100, 500), (501, 900)])
2886 )]
2887 #[case::prefers_finer_grid(
2891 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
2892 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
2893 )]
2894 #[case::shared_start_prefers_finer(
2898 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
2899 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
2900 )]
2901 #[case::coarse_overlap_prefers_finer_pair(
2905 vec![range(100, 200), range(100, 150), range(151, 160)],
2906 100, 160, Some(vec![(100, 150), (151, 160)])
2907 )]
2908 #[case::falls_back_to_coarse(
2912 vec![range(100, 160), range(100, 120), range(121, 140)],
2913 100, 160, Some(vec![(100, 160)])
2914 )]
2915 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
2917 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
2920 #[case::anchors_then_continues(
2921 vec![range(150, 350), range(351, 600)],
2922 200, 600, Some(vec![(150, 350), (351, 600)])
2923 )]
2924 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
2925 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
2927 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
2929 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
2930 #[case::user_and_global_ranges_not_collapsed(
2935 vec![range(100, 200), range(100, 150), range(151, 200)],
2936 100, 200, Some(vec![(100, 150), (151, 200)])
2937 )]
2938 fn split_range_cases(
2939 #[case] ranges: Vec<AvailableRange>,
2940 #[case] start: u64,
2941 #[case] end: u64,
2942 #[case] expected: Option<Vec<(u64, u64)>>,
2943 ) {
2944 match expected {
2945 Some(expected) => assert_eq!(
2946 split_range(
2947 &ranges,
2948 SlotRange::new_unchecked(start, end),
2949 BundleKind::Blocks
2950 )
2951 .unwrap(),
2952 expected
2953 ),
2954 None => assert!(
2955 split_range(
2956 &ranges,
2957 SlotRange::new_unchecked(start, end),
2958 BundleKind::Blocks
2959 )
2960 .is_err()
2961 ),
2962 }
2963 }
2964
2965 fn account_state_range(start: u64, end: u64) -> AvailableRange {
2966 AvailableRange {
2967 kind: Some(BundleKind::AccountState),
2968 ..range(start, end)
2969 }
2970 }
2971
2972 #[test]
2973 fn a_replay_split_rides_the_recordings_not_a_finer_block_grid() {
2974 let ranges = vec![
2977 range(100, 149),
2978 range(150, 199),
2979 account_state_range(100, 199),
2980 ];
2981 let requested = SlotRange::new_unchecked(100, 199);
2982
2983 assert_eq!(
2984 split_range(&ranges, requested, BundleKind::Blocks).unwrap(),
2985 [(100, 149), (150, 199)]
2986 );
2987 assert_eq!(
2988 split_range(&ranges, requested, BundleKind::AccountState).unwrap(),
2989 [(100, 199)]
2990 );
2991 }
2992
2993 #[test]
2994 fn a_replay_split_tiles_consecutive_recordings() {
2995 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
2996 assert_eq!(
2997 split_range(
2998 &ranges,
2999 SlotRange::new_unchecked(100, 299),
3000 BundleKind::AccountState
3001 )
3002 .unwrap(),
3003 [(100, 199), (200, 299)]
3004 );
3005 }
3006
3007 #[test]
3008 fn a_replay_split_ends_short_of_a_recording() {
3009 let requested = SlotRange::new_unchecked(100, 150);
3012 assert_eq!(
3013 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3014 [(100, 150)]
3015 );
3016 assert_eq!(
3017 split_range(
3018 &[account_state_range(100, 199)],
3019 requested,
3020 BundleKind::AccountState
3021 )
3022 .unwrap(),
3023 [(100, 150)]
3024 );
3025 }
3026
3027 #[test]
3028 fn a_replay_split_ends_short_of_its_last_recording() {
3029 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
3032 assert_eq!(
3033 split_range(
3034 &ranges,
3035 SlotRange::new_unchecked(100, 250),
3036 BundleKind::AccountState
3037 )
3038 .unwrap(),
3039 [(100, 199), (200, 250)]
3040 );
3041 }
3042
3043 #[test]
3044 fn a_replay_split_tiles_a_one_slot_recording() {
3045 let ranges = vec![account_state_range(100, 249), account_state_range(250, 250)];
3048 assert_eq!(
3049 split_range(
3050 &ranges,
3051 SlotRange::new_unchecked(100, 250),
3052 BundleKind::AccountState
3053 )
3054 .unwrap(),
3055 [(100, 249), (250, 250)]
3056 );
3057 }
3058
3059 #[test]
3060 fn a_replay_split_anchors_a_mid_recording_start() {
3061 let requested = SlotRange::new_unchecked(150, 199);
3065 assert_eq!(
3066 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3067 [(100, 199)]
3068 );
3069 assert_eq!(
3070 split_range(
3071 &[account_state_range(100, 199)],
3072 requested,
3073 BundleKind::AccountState
3074 )
3075 .unwrap(),
3076 [(100, 199)]
3077 );
3078 }
3079
3080 #[test]
3081 fn a_replay_split_refuses_a_start_no_recording_covers() {
3082 let error = split_range(
3083 &[account_state_range(200, 299)],
3084 SlotRange::new_unchecked(100, 299),
3085 BundleKind::AccountState,
3086 )
3087 .expect_err("no recording covers 100");
3088 assert!(
3089 error.contains("is not covered by any available account-state bundle"),
3090 "{error}"
3091 );
3092 }
3093
3094 #[test]
3095 fn a_replay_split_ignores_block_ranges() {
3096 let error = split_range(
3097 &[range(100, 199)],
3098 SlotRange::new_unchecked(100, 199),
3099 BundleKind::AccountState,
3100 )
3101 .expect_err("owns no recordings");
3102 assert!(error.contains("account-state bundle"), "{error}");
3103 }
3104
3105 #[test]
3106 fn blocks_mode_admits_ranges_from_a_server_that_predates_kind() {
3107 let unlabelled = range(100, 199);
3110 assert_eq!(unlabelled.kind, None);
3111 assert_eq!(
3112 split_range(
3113 &[unlabelled],
3114 SlotRange::new_unchecked(100, 150),
3115 BundleKind::Blocks
3116 )
3117 .unwrap(),
3118 [(100, 150)]
3119 );
3120 }
3121
3122 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
3125 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
3126 for r in ranges {
3127 if let Some(end) = r.max_bundle_end_slot
3128 && end >= r.bundle_start_slot
3129 {
3130 ends.entry(r.bundle_start_slot).or_default().insert(end);
3131 }
3132 }
3133 ends
3134 }
3135
3136 fn reference_max_split(
3140 ends: &BTreeMap<u64, BTreeSet<u64>>,
3141 cursor: u64,
3142 end: u64,
3143 ) -> Option<Vec<(u64, u64)>> {
3144 ends.get(&cursor)?
3145 .iter()
3146 .filter_map(|&bundle_end| {
3147 if bundle_end >= end {
3148 Some(vec![(cursor, end)])
3149 } else {
3150 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
3151 rest.insert(0, (cursor, bundle_end));
3152 rest
3153 })
3154 }
3155 })
3156 .max_by_key(Vec::len)
3157 }
3158
3159 fn is_valid_split(
3163 split: &[(u64, u64)],
3164 ends: &BTreeMap<u64, BTreeSet<u64>>,
3165 start: u64,
3166 end: u64,
3167 ) -> bool {
3168 split.first().is_some_and(|&(s, _)| s == start)
3169 && split.last().is_some_and(|&(_, e)| e == end)
3170 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
3171 && split.iter().all(|&(s, e)| {
3172 e >= s
3173 && ends
3174 .get(&s)
3175 .and_then(|bundle_ends| bundle_ends.iter().next_back())
3176 .is_some_and(|&max_end| e <= max_end)
3177 })
3178 }
3179
3180 #[test]
3185 fn split_range_matches_reference() {
3186 let mut seed: u64 = 0x9E3779B97F4A7C15;
3187 let mut next = || {
3188 seed = seed
3189 .wrapping_mul(6364136223846793005)
3190 .wrapping_add(1442695040888963407);
3191 seed >> 33
3192 };
3193
3194 for _ in 0..50_000 {
3195 let ranges: Vec<AvailableRange> = (0..next() % 6)
3198 .map(|_| {
3199 let start = next() % 12;
3200 range(start, start + next() % 6) })
3202 .collect();
3203 let start = next() % 12;
3204 let end = start + next() % 6; let got = split_range(
3207 &ranges,
3208 SlotRange::new_unchecked(start, end),
3209 BundleKind::Blocks,
3210 );
3211 let ends = ends_by_start(&ranges);
3212 let anchor = ends
3215 .range(..=start)
3216 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
3217 .map(|(&s, _)| s);
3218 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
3219
3220 let layout: Vec<_> = ranges
3221 .iter()
3222 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
3223 .collect();
3224 match (&got, &reference) {
3225 (Ok(split), Some(best)) => {
3226 assert!(
3227 is_valid_split(split, &ends, anchor.unwrap(), end),
3228 "invalid split {split:?} for {layout:?} [{start},{end}]"
3229 );
3230 assert_eq!(
3231 split.len(),
3232 best.len(),
3233 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
3234 );
3235 }
3236 (Err(_), None) => {}
3237 _ => panic!(
3238 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
3239 ),
3240 }
3241 }
3242 }
3243}