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 mints;
27pub mod reroute_report;
28pub mod route_plan;
29pub mod subscribe_config;
30pub mod usage;
31pub mod ws_compression;
32
33#[derive(Debug, Serialize, Deserialize)]
35#[serde(tag = "method", content = "params", rename_all = "camelCase")]
36pub enum BacktestRequest {
37 CreateBacktestSession(CreateBacktestSessionRequest),
38 Continue(ContinueParams),
39 ContinueTo(ContinueToParams),
40 ContinueSessionV1(ContinueSessionRequestV1),
41 ContinueToSessionV1(ContinueToSessionRequestV1),
42 CloseBacktestSession,
43 CloseSessionV1(CloseSessionRequestV1),
44 AttachBacktestSession {
45 session_id: SessionId,
46 last_sequence: Option<u64>,
49 },
50 ResumeAttachedSession,
53 AttachParallelControlSessionV2 {
54 control_session_id: ControlSessionId,
55 #[serde(default)]
59 last_sequences: BTreeMap<SessionId, u64>,
60 },
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
68#[serde(untagged)]
69pub enum CreateBacktestSessionRequest {
70 V1(CreateBacktestSessionRequestV1),
71 V0(CreateSessionParams),
72}
73
74impl CreateBacktestSessionRequest {
75 pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
76 match self {
77 Self::V0(request) => CreateBacktestSessionRequestOptions {
78 request,
79 parallel: false,
80 },
81 Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
82 CreateBacktestSessionRequestOptions { request, parallel }
83 }
84 }
85 }
86
87 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
88 let options = self.into_request_options();
89 (options.request, options.parallel)
90 }
91}
92
93impl From<CreateSessionParams> for CreateBacktestSessionRequest {
94 fn from(value: CreateSessionParams) -> Self {
95 Self::V0(value)
96 }
97}
98
99impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
100 fn from(value: CreateBacktestSessionRequestV1) -> Self {
101 Self::V1(value)
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct CreateBacktestSessionRequestV1 {
108 #[serde(flatten)]
109 pub request: CreateSessionParams,
110 pub parallel: bool,
111}
112
113#[derive(Debug, Clone)]
114pub struct CreateBacktestSessionRequestOptions {
115 pub request: CreateSessionParams,
116 pub parallel: bool,
117}
118
119#[derive(Debug, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct ContinueSessionRequestV1 {
122 pub session_id: SessionId,
123 pub request: ContinueParams,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct ContinueToSessionRequestV1 {
129 pub session_id: SessionId,
130 pub request: ContinueToParams,
131}
132
133#[derive(Debug, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct CloseSessionRequestV1 {
136 pub session_id: SessionId,
137}
138
139#[serde_with::serde_as]
146#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
147#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
148pub enum DiscoveryFilter {
149 ProgramExecuted(#[serde_as(as = "serde_with::DisplayFromStr")] Address),
151}
152
153pub struct TxMatchContext<'a> {
158 pub invoked_programs: &'a HashSet<Address>,
160}
161
162impl DiscoveryFilter {
163 pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
166 match self {
167 Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
176#[serde(try_from = "[String; 2]", into = "[String; 2]")]
177pub struct MintPair([Address; 2]);
178
179impl MintPair {
180 pub fn new(one: Address, other: Address) -> Self {
181 Self(if one <= other {
182 [one, other]
183 } else {
184 [other, one]
185 })
186 }
187
188 pub fn mints(&self) -> (Address, Address) {
189 (self.0[0], self.0[1])
190 }
191}
192
193#[derive(Debug, thiserror::Error)]
194#[error("invalid mint {mint} in pair")]
195pub struct ParseMintPairError {
196 mint: String,
197 source: ParseAddressError,
198}
199
200impl TryFrom<[String; 2]> for MintPair {
201 type Error = ParseMintPairError;
202
203 fn try_from([one, other]: [String; 2]) -> Result<Self, Self::Error> {
204 fn parse(mint: String) -> Result<Address, ParseMintPairError> {
205 Address::from_str(&mint).map_err(|source| ParseMintPairError { mint, source })
206 }
207 Ok(Self::new(parse(one)?, parse(other)?))
208 }
209}
210
211impl From<MintPair> for [String; 2] {
212 fn from(pair: MintPair) -> Self {
213 let (one, other) = pair.mints();
214 [one.to_string(), other.to_string()]
215 }
216}
217
218#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub struct RerouteFilter {
223 #[serde(default)]
225 pub pairs: BTreeSet<MintPair>,
226}
227
228impl RerouteFilter {
229 pub fn is_empty(&self) -> bool {
232 self.pairs.is_empty()
233 }
234
235 pub fn matches_mints(&self, input: &Address, output: &Address) -> bool {
236 self.pairs.contains(&MintPair::new(*input, *output))
237 }
238
239 pub fn mentions_mint(&self, mint: &Address) -> bool {
242 self.pairs.iter().any(|pair| {
243 let (one, other) = pair.mints();
244 one == *mint || other == *mint
245 })
246 }
247}
248
249#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub enum ActionKind {
254 #[default]
258 Simulate,
259 Send,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268#[serde(tag = "at", rename_all = "camelCase")]
269pub enum ActionAnchor {
270 BeforeSlot {
273 #[serde(default, with = "compressed")]
274 slots: Vec<u64>,
275 },
276 AfterSlot {
279 #[serde(default, with = "compressed")]
280 slots: Vec<u64>,
281 },
282 BeforeMatch { filter: DiscoveryFilter },
285 AfterMatch { filter: DiscoveryFilter },
288}
289
290const MAX_ACTION_TRANSACTIONS_BYTES: u64 = 64 * 1024 * 1024;
293
294mod compressed {
299 use std::io::Read;
300
301 use base64::Engine as _;
302 use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
303
304 use super::{BASE64, MAX_ACTION_TRANSACTIONS_BYTES};
305
306 const COMPRESSION_LEVEL: i32 = 0;
308
309 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
310 where
311 T: Serialize,
312 S: Serializer,
313 {
314 let json = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
315 let compressed = zstd::stream::encode_all(json.as_slice(), COMPRESSION_LEVEL)
316 .map_err(serde::ser::Error::custom)?;
317 serializer.serialize_str(&BASE64.encode(compressed))
318 }
319
320 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
321 where
322 T: DeserializeOwned,
323 D: Deserializer<'de>,
324 {
325 let blob = String::deserialize(deserializer)?;
326 let compressed = BASE64
327 .decode(blob.as_bytes())
328 .map_err(serde::de::Error::custom)?;
329 let mut json = Vec::new();
330 zstd::stream::read::Decoder::new(compressed.as_slice())
331 .and_then(|d| Read::take(d, MAX_ACTION_TRANSACTIONS_BYTES).read_to_end(&mut json))
332 .map_err(serde::de::Error::custom)?;
333 serde_json::from_slice(&json).map_err(serde::de::Error::custom)
334 }
335}
336
337#[serde_with::serde_as]
345#[derive(Debug, Clone, Serialize, Deserialize)]
346#[serde(rename_all = "camelCase")]
347pub struct ScheduledAction {
348 pub anchor: ActionAnchor,
349 #[serde(default)]
351 pub kind: ActionKind,
352 #[serde(default, with = "compressed")]
361 pub transactions: Vec<String>,
362 #[serde(default)]
367 pub account_overrides: AccountModifications,
368 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
378 pub feeds_reroute: bool,
379 #[serde_as(as = "Vec<serde_with::DisplayFromStr>")]
383 #[serde(default)]
384 pub return_accounts: Vec<Address>,
385 #[serde(default)]
387 pub label: Option<String>,
388}
389
390impl ScheduledAction {
391 pub fn is_computed_override(&self) -> bool {
394 self.feeds_reroute && !self.transactions.is_empty()
395 }
396}
397
398#[derive(Debug, Clone)]
400enum OverrideContribution {
401 Static(HashMap<Address, AccountSharedData>),
402 Pending,
403 Resolved(HashMap<Address, AccountSharedData>),
404 Failed,
405}
406
407#[derive(Debug, Clone, Default)]
408struct OverrideScheduleInner {
409 entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>,
410 keys: HashSet<Address>,
414}
415
416impl OverrideScheduleInner {
417 fn new(entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>>) -> Self {
418 let keys = entries
419 .values()
420 .flatten()
421 .filter_map(|(_, contribution)| match contribution {
422 OverrideContribution::Static(accounts)
423 | OverrideContribution::Resolved(accounts) => Some(accounts.keys().copied()),
424 OverrideContribution::Pending | OverrideContribution::Failed => None,
425 })
426 .flatten()
427 .collect();
428 Self { entries, keys }
429 }
430}
431
432#[derive(Debug, Clone, Default)]
435pub struct OverrideSchedule {
436 inner: Arc<RwLock<OverrideScheduleInner>>,
437}
438
439impl From<BTreeMap<u64, HashMap<Address, AccountSharedData>>> for OverrideSchedule {
440 fn from(entries: BTreeMap<u64, HashMap<Address, AccountSharedData>>) -> Self {
441 Self {
442 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(
443 entries
444 .into_iter()
445 .map(|(slot, accounts)| {
446 (slot, vec![(0, OverrideContribution::Static(accounts))])
447 })
448 .collect(),
449 ))),
450 }
451 }
452}
453
454impl OverrideSchedule {
455 pub fn is_empty(&self) -> bool {
456 self.inner.read().entries.is_empty()
457 }
458
459 pub fn slots(&self) -> impl Iterator<Item = u64> + '_ {
461 self.inner
462 .read()
463 .entries
464 .keys()
465 .copied()
466 .collect::<Vec<_>>()
467 .into_iter()
468 }
469
470 pub fn active_at(&self, slot: u64) -> HashMap<Address, AccountSharedData> {
472 self.inner.read().entries.range(..=slot).fold(
473 HashMap::new(),
474 |mut active, (_, contributions)| {
475 active.extend(fold_contributions(contributions));
476 active
477 },
478 )
479 }
480
481 pub fn active_for(&self, key: &Address, slot: u64) -> Option<AccountSharedData> {
482 let inner = self.inner.read();
483 if !inner.keys.contains(key) {
484 return None;
485 }
486 inner
487 .entries
488 .range(..=slot)
489 .rev()
490 .find_map(|(_, contributions)| {
491 contributions
492 .iter()
493 .rev()
494 .find_map(|(_, contribution)| match contribution {
495 OverrideContribution::Static(accounts)
496 | OverrideContribution::Resolved(accounts) => accounts.get(key).cloned(),
497 OverrideContribution::Pending | OverrideContribution::Failed => None,
498 })
499 })
500 }
501
502 pub fn newly_active(
504 &self,
505 after: Option<u64>,
506 up_to: u64,
507 ) -> Vec<(Address, AccountSharedData)> {
508 let lower = after.map_or(std::ops::Bound::Unbounded, std::ops::Bound::Excluded);
509 self.inner
510 .read()
511 .entries
512 .range((lower, std::ops::Bound::Included(up_to)))
513 .flat_map(|(_, contributions)| fold_contributions(contributions))
514 .collect::<HashMap<_, _>>()
515 .into_iter()
516 .collect()
517 }
518
519 pub fn resolve(
522 &self,
523 slot: u64,
524 action_index: u32,
525 accounts: Option<HashMap<Address, AccountSharedData>>,
526 ) -> Result<(), String> {
527 let mut inner = self.inner.write();
528 let OverrideScheduleInner { entries, keys } = &mut *inner;
529 let contribution = entries
530 .get_mut(&slot)
531 .and_then(|contributions| {
532 contributions
533 .iter_mut()
534 .find(|(index, _)| *index == action_index)
535 .map(|(_, contribution)| contribution)
536 })
537 .ok_or_else(|| {
538 format!("override action {action_index} has no contribution at slot {slot}")
539 })?;
540 if !matches!(contribution, OverrideContribution::Pending) {
541 return Err(format!(
542 "override action {action_index} at slot {slot} is not pending"
543 ));
544 }
545 *contribution = match accounts {
546 Some(accounts) => {
547 keys.extend(accounts.keys().copied());
548 OverrideContribution::Resolved(accounts)
549 }
550 None => OverrideContribution::Failed,
551 };
552 Ok(())
553 }
554}
555
556fn fold_contributions(
557 contributions: &[(u32, OverrideContribution)],
558) -> HashMap<Address, AccountSharedData> {
559 let mut accounts = HashMap::new();
560 for (_, contribution) in contributions {
561 match contribution {
562 OverrideContribution::Static(next) | OverrideContribution::Resolved(next) => {
563 accounts.extend(next.iter().map(|(key, account)| (*key, account.clone())));
564 }
565 OverrideContribution::Pending | OverrideContribution::Failed => {}
566 }
567 }
568 accounts
569}
570
571fn decode_override_actions(actions: &[ScheduledAction]) -> Result<OverrideSchedule, String> {
580 let mut entries: BTreeMap<u64, Vec<(u32, OverrideContribution)>> = BTreeMap::new();
581 for (action_index, action) in actions.iter().enumerate().filter(|(_, action)| {
582 action.feeds_reroute
583 || (action.transactions.is_empty() && !action.account_overrides.0.is_empty())
584 }) {
585 if action.kind == ActionKind::Send {
587 return Err("account overrides are not valid for kind: send".to_string());
588 }
589 if action.transactions.is_empty() && action.account_overrides.0.is_empty() {
590 return Err(
591 "feedsReroute needs something to feed: transactions to simulate, or \
592 accountOverrides"
593 .to_string(),
594 );
595 }
596 let ActionAnchor::BeforeSlot { slots } = &action.anchor else {
597 return Err("scheduled account overrides require a beforeSlot anchor".to_string());
598 };
599 let accounts = decode_account_modifications(&action.account_overrides)?;
600 let contribution = if action.transactions.is_empty() {
601 OverrideContribution::Static(accounts)
602 } else {
603 OverrideContribution::Pending
604 };
605 for slot in slots.iter().copied().collect::<BTreeSet<_>>() {
608 entries
609 .entry(slot)
610 .or_default()
611 .push((action_index as u32, contribution.clone()));
612 }
613 }
614 Ok(OverrideSchedule {
615 inner: Arc::new(RwLock::new(OverrideScheduleInner::new(entries))),
616 })
617}
618
619#[allow(
623 deprecated,
624 reason = "`PROGRAM_OWNERS` remains the canonical loader-owner list"
625)]
626pub fn decode_account_modifications(
627 modifications: &AccountModifications,
628) -> Result<HashMap<Address, AccountSharedData>, String> {
629 modifications
630 .0
631 .iter()
632 .map(|(address, data)| {
633 let account = data
634 .to_account()
635 .map_err(|error| format!("invalid account data for {address}: {error}"))?;
636 if account.executable || solana_account::PROGRAM_OWNERS.contains(&account.owner) {
637 return Err(format!(
638 "overriding executable/program accounts is not supported (account {address})"
639 ));
640 }
641 Ok((*address, AccountSharedData::from(account)))
642 })
643 .collect()
644}
645
646pub const MAX_DISCONNECT_TIMEOUT_SECS: u16 = 900;
650
651#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
654#[serde(rename_all = "lowercase", try_from = "String")]
655pub enum SwapAggregator {
656 Jupiter,
657 Okx,
658 Titan,
659 Dflow,
660}
661
662impl SwapAggregator {
663 pub const fn as_str(self) -> &'static str {
665 match self {
666 Self::Jupiter => "jupiter",
667 Self::Okx => "okx",
668 Self::Titan => "titan",
669 Self::Dflow => "dflow",
670 }
671 }
672}
673
674impl std::fmt::Display for SwapAggregator {
675 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
676 f.write_str(self.as_str())
677 }
678}
679
680#[derive(Debug, thiserror::Error)]
681pub enum ParseAggregatorError {
682 #[error("unknown swap aggregator {0:?}; expected one of jupiter, okx, titan, dflow")]
683 Unknown(String),
684 #[error("name at least one aggregator (jupiter, okx, titan, dflow)")]
686 NoAggregator,
687}
688
689impl TryFrom<String> for SwapAggregator {
690 type Error = ParseAggregatorError;
691
692 fn try_from(value: String) -> Result<Self, Self::Error> {
693 value.parse()
694 }
695}
696
697impl std::str::FromStr for SwapAggregator {
698 type Err = ParseAggregatorError;
699
700 fn from_str(s: &str) -> Result<Self, Self::Err> {
701 match s.trim().to_ascii_lowercase().as_str() {
702 "jupiter" => Ok(Self::Jupiter),
703 "okx" => Ok(Self::Okx),
704 "titan" => Ok(Self::Titan),
705 "dflow" => Ok(Self::Dflow),
706 _ => Err(ParseAggregatorError::Unknown(s.to_string())),
707 }
708 }
709}
710
711#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
715#[serde(transparent)]
716pub struct RerouteAggregators(BTreeSet<SwapAggregator>);
717
718impl Default for RerouteAggregators {
719 fn default() -> Self {
720 Self([SwapAggregator::Jupiter].into_iter().collect())
721 }
722}
723
724impl RerouteAggregators {
725 pub fn new(aggregators: impl IntoIterator<Item = SwapAggregator>) -> Self {
726 Self(aggregators.into_iter().collect())
727 }
728
729 pub fn contains(&self, aggregator: SwapAggregator) -> bool {
730 self.0.contains(&aggregator)
731 }
732
733 pub fn is_empty(&self) -> bool {
734 self.0.is_empty()
735 }
736}
737
738impl std::fmt::Display for RerouteAggregators {
739 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
740 let mut aggregators = self.0.iter();
741 if let Some(first) = aggregators.next() {
742 write!(f, "{first}")?;
743 }
744 aggregators.try_for_each(|aggregator| write!(f, ",{aggregator}"))
745 }
746}
747
748impl std::str::FromStr for RerouteAggregators {
749 type Err = ParseAggregatorError;
750
751 fn from_str(s: &str) -> Result<Self, Self::Err> {
752 let aggregators: BTreeSet<SwapAggregator> = s
753 .split(',')
754 .filter(|aggregator| !aggregator.trim().is_empty())
755 .map(str::parse)
756 .collect::<Result<_, _>>()?;
757 if aggregators.is_empty() {
758 return Err(ParseAggregatorError::NoAggregator);
759 }
760 Ok(Self(aggregators))
761 }
762}
763
764#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
772#[serde(rename_all = "camelCase")]
773pub struct RerouteParams {
774 #[serde(default)]
775 pub aggregators: RerouteAggregators,
776 #[serde(default, skip_serializing_if = "Option::is_none")]
779 pub filter: Option<RerouteFilter>,
780 #[serde(default)]
787 pub detect_failed_l1_swaps: bool,
788 #[serde(default)]
791 pub circular_arbs: bool,
792 #[serde(default = "RerouteParams::default_requote")]
798 pub requote: bool,
799 #[serde(default, skip_serializing_if = "Option::is_none")]
804 pub direct_fill: Option<Box<DirectFillParams>>,
805}
806
807#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
813#[serde(rename_all = "camelCase")]
814pub struct DirectFillParams {
815 pub aggregator: SwapAggregator,
818 pub venue: String,
820 pub pair: MintPair,
822 #[serde(default = "DirectFillParams::default_slippage_bps")]
825 pub slippage_bps: u16,
826 pub market: DirectFillMarket,
828}
829
830impl DirectFillParams {
831 const fn default_slippage_bps() -> u16 {
834 50
835 }
836}
837
838#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
846#[serde(try_from = "DirectFillMarketWire", into = "DirectFillMarketWire")]
847pub struct DirectFillMarket {
848 pub mints: [Address; 2],
851 pub accounts: Vec<DirectFillAccount>,
853}
854
855#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
857#[serde(try_from = "DirectFillAccountWire", into = "DirectFillAccountWire")]
858pub struct DirectFillAccount {
859 pub address: Address,
860 pub writable: bool,
863}
864
865#[derive(Debug, Clone, Serialize, Deserialize)]
866#[serde(rename_all = "camelCase")]
867struct DirectFillMarketWire {
868 mints: [String; 2],
869 accounts: Vec<DirectFillAccount>,
870}
871
872#[derive(Debug, Clone, Serialize, Deserialize)]
873#[serde(rename_all = "camelCase")]
874struct DirectFillAccountWire {
875 address: String,
876 #[serde(default)]
877 writable: bool,
878}
879
880#[derive(Debug, thiserror::Error, PartialEq, Eq)]
882pub enum ParseDirectFillMarketError {
883 #[error("invalid address {address} in the venue's market")]
884 Address { address: String },
885 #[error("a venue's account run must hold 1..=255 accounts, got {count}")]
887 RunLength { count: usize },
888 #[error("a market quotes two different mints, got {mint} twice")]
890 RepeatedMint { mint: String },
891}
892
893impl TryFrom<DirectFillAccountWire> for DirectFillAccount {
894 type Error = ParseDirectFillMarketError;
895
896 fn try_from(wire: DirectFillAccountWire) -> Result<Self, Self::Error> {
897 Ok(Self {
898 address: Address::from_str(&wire.address).map_err(|_| {
899 ParseDirectFillMarketError::Address {
900 address: wire.address.clone(),
901 }
902 })?,
903 writable: wire.writable,
904 })
905 }
906}
907
908impl From<DirectFillAccount> for DirectFillAccountWire {
909 fn from(account: DirectFillAccount) -> Self {
910 Self {
911 address: account.address.to_string(),
912 writable: account.writable,
913 }
914 }
915}
916
917impl TryFrom<DirectFillMarketWire> for DirectFillMarket {
918 type Error = ParseDirectFillMarketError;
919
920 fn try_from(wire: DirectFillMarketWire) -> Result<Self, Self::Error> {
921 let [one, other] = wire.mints;
922 let parse = |mint: &str| {
923 Address::from_str(mint).map_err(|_| ParseDirectFillMarketError::Address {
924 address: mint.to_owned(),
925 })
926 };
927 let mints = [parse(&one)?, parse(&other)?];
928 if mints[0] == mints[1] {
929 return Err(ParseDirectFillMarketError::RepeatedMint { mint: one });
930 }
931 if wire.accounts.is_empty() || wire.accounts.len() > usize::from(u8::MAX) {
932 return Err(ParseDirectFillMarketError::RunLength {
933 count: wire.accounts.len(),
934 });
935 }
936 Ok(Self {
937 mints,
938 accounts: wire.accounts,
939 })
940 }
941}
942
943impl From<DirectFillMarket> for DirectFillMarketWire {
944 fn from(market: DirectFillMarket) -> Self {
945 Self {
946 mints: [market.mints[0].to_string(), market.mints[1].to_string()],
947 accounts: market.accounts,
948 }
949 }
950}
951
952impl Default for RerouteParams {
955 fn default() -> Self {
956 Self {
957 aggregators: RerouteAggregators::default(),
958 filter: None,
959 detect_failed_l1_swaps: false,
960 circular_arbs: false,
961 requote: Self::default_requote(),
962 direct_fill: None,
963 }
964 }
965}
966
967impl std::str::FromStr for MintPair {
969 type Err = String;
970
971 fn from_str(s: &str) -> Result<Self, Self::Err> {
972 let (base, quote) = s
973 .split_once(',')
974 .ok_or_else(|| format!("expected two comma-separated mints, got {s:?}"))?;
975 Self::try_from([base.trim().to_owned(), quote.trim().to_owned()])
976 .map_err(|error| error.to_string())
977 }
978}
979
980impl std::fmt::Display for MintPair {
981 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
982 let (one, other) = self.mints();
983 write!(f, "{one},{other}")
984 }
985}
986
987impl RerouteParams {
988 const fn default_requote() -> bool {
989 true
990 }
991
992 pub const fn is_noop(&self) -> bool {
995 !self.requote && self.direct_fill.is_none()
996 }
997}
998
999impl DirectFillParams {
1000 pub fn matches_mints(&self, input: &Address, output: &Address) -> bool {
1002 MintPair::new(*input, *output) == self.pair
1003 }
1004}
1005
1006#[serde_with::serde_as]
1008#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
1009#[serde(rename_all = "camelCase")]
1010pub struct CreateSessionParams {
1011 pub start_slot: u64,
1013 pub end_slot: u64,
1015 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
1016 #[serde(default)]
1017 #[builder(default)]
1018 pub signer_filter: BTreeSet<Address>,
1020 #[serde(default)]
1023 #[builder(default)]
1024 pub send_summary: bool,
1025 #[serde(default, skip_serializing_if = "Option::is_none")]
1027 pub reroute: Option<RerouteParams>,
1028 #[serde(default)]
1031 pub capacity_wait_timeout_secs: Option<u16>,
1032 #[serde(default)]
1036 pub disconnect_timeout_secs: Option<u16>,
1037 #[serde(default)]
1042 pub extra_compute_units: Option<u32>,
1043 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1050 #[builder(default)]
1051 pub discoveries: Vec<DiscoveryFilter>,
1052 #[serde(default, with = "compressed", skip_serializing_if = "Vec::is_empty")]
1061 #[builder(default)]
1062 pub actions: Vec<ScheduledAction>,
1063 #[serde(default)]
1066 #[builder(default)]
1067 pub replay_account_state: bool,
1068}
1069
1070pub mod replay_support {
1074 use super::{ActionKind, CreateSessionParams};
1075
1076 pub const NO_TRANSACTIONS: &str =
1078 "Create the session without replayAccountState to execute and persist transactions.";
1079
1080 pub const SEND_TRANSACTION: &str = "Use simulateTransaction to run it against the replayed state, or create the session \
1081 without replayAccountState.";
1082
1083 pub const SEND_ACTION: &str = "Use kind: simulate, which runs against the replayed state without committing, post state \
1084 directly with an action carrying accountOverrides and no transactions, or create the \
1085 session without replayAccountState.";
1086
1087 pub fn unsupported(operation: &str, remedy: &str) -> String {
1089 format!(
1090 "{operation} is not supported while account state replay is enabled: replay \
1091 reconstructs account state from the recorded delta bundle instead of executing \
1092 transactions, so this session never executes or persists any. {remedy}"
1093 )
1094 }
1095
1096 pub fn send_action_conflict(params: &CreateSessionParams) -> Option<String> {
1099 (params.replay_account_state
1100 && params
1101 .actions
1102 .iter()
1103 .any(|action| action.kind == ActionKind::Send))
1104 .then(|| unsupported("A scheduled action with kind: send", SEND_ACTION))
1105 }
1106}
1107
1108impl CreateSessionParams {
1109 pub fn requires_sidecar(&self) -> bool {
1112 self.reroute.as_ref().is_some_and(|reroute| reroute.requote)
1113 }
1114
1115 pub fn reroute_order_flow(&self) -> bool {
1116 self.reroute.is_some()
1117 }
1118
1119 pub fn override_schedule(&self) -> Result<OverrideSchedule, String> {
1122 decode_override_actions(&self.actions)
1123 }
1124
1125 pub fn reroute_filter(&self) -> Option<&RerouteFilter> {
1127 self.reroute.as_ref()?.filter.as_ref()
1128 }
1129}
1130
1131#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1134#[serde(rename_all = "kebab-case")]
1135pub enum FailFastDivergenceKind {
1136 #[default]
1139 AnyNonBenign,
1140 Tracked,
1143}
1144
1145impl FailFastDivergenceKind {
1146 pub fn as_str(self) -> &'static str {
1149 match self {
1150 Self::AnyNonBenign => "any-non-benign",
1151 Self::Tracked => "tracked",
1152 }
1153 }
1154
1155 pub fn from_str_opt(value: &str) -> Option<Self> {
1157 match value {
1158 "any-non-benign" => Some(Self::AnyNonBenign),
1159 "tracked" => Some(Self::Tracked),
1160 _ => None,
1161 }
1162 }
1163}
1164
1165#[serde_with::serde_as]
1167#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1168pub struct AccountModifications(
1169 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
1170 #[serde(default)]
1171 pub BTreeMap<Address, AccountData>,
1172);
1173
1174impl From<BTreeMap<Address, AccountData>> for AccountModifications {
1175 fn from(value: BTreeMap<Address, AccountData>) -> Self {
1176 Self(value)
1177 }
1178}
1179
1180#[serde_with::serde_as]
1182#[derive(Debug, Serialize, Deserialize)]
1183#[serde(rename_all = "camelCase")]
1184pub struct ContinueParams {
1185 #[serde(default = "ContinueParams::default_advance_count")]
1186 pub advance_count: u64,
1188 #[serde(default)]
1189 pub transactions: Vec<String>,
1191 #[serde(default)]
1192 pub modify_account_states: AccountModifications,
1194}
1195
1196impl Default for ContinueParams {
1197 fn default() -> Self {
1198 Self {
1199 advance_count: Self::default_advance_count(),
1200 transactions: Vec::new(),
1201 modify_account_states: AccountModifications(BTreeMap::new()),
1202 }
1203 }
1204}
1205
1206impl ContinueParams {
1207 pub fn default_advance_count() -> u64 {
1208 1
1209 }
1210}
1211
1212#[derive(Debug, Clone, Serialize, Deserialize)]
1218#[serde(rename_all = "camelCase")]
1219pub struct PausedEvent {
1220 pub slot: u64,
1221 #[serde(default, skip_serializing_if = "Option::is_none")]
1222 pub batch_index: Option<u32>,
1223}
1224
1225#[serde_with::serde_as]
1234#[derive(Debug, Clone, Serialize, Deserialize)]
1235#[serde(rename_all = "camelCase")]
1236pub struct DiscoveryBatchEvent {
1237 pub slot: u64,
1238 pub batch_index: u32,
1239 pub matched: Vec<DiscoveryFilter>,
1241 pub transactions: Vec<EncodedBinary>,
1245}
1246
1247#[derive(Debug, Clone, Serialize, Deserialize)]
1249#[serde(rename_all = "camelCase")]
1250pub struct ContinueToParams {
1251 pub slot: u64,
1253 #[serde(default)]
1259 pub batch_index: Option<u32>,
1260}
1261
1262#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1264#[serde(rename_all = "lowercase")]
1265pub enum BinaryEncoding {
1266 Base64,
1267}
1268
1269impl BinaryEncoding {
1270 pub fn encode(self, bytes: &[u8]) -> String {
1271 match self {
1272 Self::Base64 => BASE64.encode(bytes),
1273 }
1274 }
1275
1276 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
1277 match self {
1278 Self::Base64 => BASE64.decode(data),
1279 }
1280 }
1281}
1282
1283#[derive(Debug, Clone, Serialize, Deserialize)]
1285#[serde(rename_all = "camelCase")]
1286pub struct EncodedBinary {
1287 pub data: String,
1289 pub encoding: BinaryEncoding,
1291}
1292
1293impl EncodedBinary {
1294 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
1295 Self { data, encoding }
1296 }
1297
1298 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
1299 Self {
1300 data: encoding.encode(bytes),
1301 encoding,
1302 }
1303 }
1304
1305 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
1306 self.encoding.decode(&self.data)
1307 }
1308}
1309
1310#[serde_with::serde_as]
1312#[derive(Debug, Clone, Serialize, Deserialize)]
1313#[serde(rename_all = "camelCase")]
1314pub struct AccountData {
1315 pub data: EncodedBinary,
1317 pub executable: bool,
1319 pub lamports: u64,
1321 #[serde_as(as = "serde_with::DisplayFromStr")]
1322 pub owner: Address,
1324 pub space: u64,
1326}
1327
1328impl AccountData {
1329 pub fn to_account(&self) -> Result<solana_account::Account, Base64DecodeError> {
1330 Ok(solana_account::Account {
1331 data: self.data.decode()?,
1332 lamports: self.lamports,
1333 owner: self.owner,
1334 executable: self.executable,
1335 rent_epoch: 0,
1336 })
1337 }
1338}
1339
1340#[derive(Debug, Clone, Serialize, Deserialize)]
1342#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1343pub enum BacktestResponse {
1344 SessionCreated {
1345 session_id: SessionId,
1346 rpc_endpoint: String,
1347 #[serde(default, skip_serializing_if = "Option::is_none")]
1348 task_id: Option<String>,
1349 },
1350 SessionAttached {
1351 session_id: SessionId,
1352 rpc_endpoint: String,
1353 #[serde(default, skip_serializing_if = "Option::is_none")]
1354 task_id: Option<String>,
1355 },
1356 SessionsCreatedV2 {
1357 control_session_id: ControlSessionId,
1358 session_ids: Vec<SessionId>,
1359 #[serde(default)]
1360 task_ids: Vec<Option<String>>,
1361 #[serde(default)]
1366 start_slots: Vec<u64>,
1367 #[serde(default)]
1368 end_slots: Vec<u64>,
1369 },
1370 ParallelSessionAttachedV2 {
1371 control_session_id: ControlSessionId,
1372 session_ids: Vec<SessionId>,
1373 #[serde(default)]
1374 task_ids: Vec<Option<String>>,
1375 },
1376 ReadyForContinue,
1377 SlotNotification(u64),
1378 Paused(PausedEvent),
1379 DiscoveryBatch(DiscoveryBatchEvent),
1380 Error(BacktestError),
1381 Success,
1382 Completed {
1383 #[serde(skip_serializing_if = "Option::is_none")]
1387 summary: Option<SessionSummary>,
1388 },
1389 Status {
1390 status: BacktestStatus,
1391 },
1392 SessionEventV2 {
1393 session_id: SessionId,
1394 seq_id: u64,
1395 event: SessionEventKind,
1396 },
1397}
1398
1399impl BacktestResponse {
1400 pub fn is_completed(&self) -> bool {
1401 matches!(self, BacktestResponse::Completed { .. })
1402 }
1403
1404 pub fn is_terminal(&self) -> bool {
1405 match self {
1406 BacktestResponse::Completed { .. } => true,
1407 BacktestResponse::Error(e) => e.is_terminal(),
1408 _ => false,
1409 }
1410 }
1411}
1412
1413impl From<BacktestStatus> for BacktestResponse {
1414 fn from(status: BacktestStatus) -> Self {
1415 Self::Status { status }
1416 }
1417}
1418
1419impl From<String> for BacktestResponse {
1420 fn from(message: String) -> Self {
1421 BacktestError::Internal { error: message }.into()
1422 }
1423}
1424
1425impl From<&str> for BacktestResponse {
1426 fn from(message: &str) -> Self {
1427 BacktestError::Internal {
1428 error: message.to_string(),
1429 }
1430 .into()
1431 }
1432}
1433
1434#[derive(Debug, Clone, Serialize, Deserialize)]
1435#[serde(tag = "method", content = "params", rename_all = "camelCase")]
1436pub enum SessionEventKind {
1437 ReadyForContinue,
1438 SlotNotification(u64),
1439 Paused(PausedEvent),
1440 DiscoveryBatch(DiscoveryBatchEvent),
1441 Error(BacktestError),
1442 Success,
1443 Completed {
1444 #[serde(skip_serializing_if = "Option::is_none")]
1445 summary: Option<SessionSummary>,
1446 },
1447 Status {
1448 status: BacktestStatus,
1449 },
1450}
1451
1452impl SessionEventKind {
1453 pub fn is_terminal(&self) -> bool {
1454 match self {
1455 Self::Completed { .. } => true,
1456 Self::Error(e) => e.is_terminal(),
1457 _ => false,
1458 }
1459 }
1460}
1461
1462#[derive(Debug, Clone, Serialize, Deserialize)]
1465#[serde(rename_all = "camelCase")]
1466pub struct SequencedResponse {
1467 pub seq_id: u64,
1468 #[serde(flatten)]
1469 pub response: BacktestResponse,
1470}
1471
1472#[derive(Debug, Clone, Serialize, Deserialize)]
1474#[serde(rename_all = "camelCase")]
1475pub enum BacktestStatus {
1476 StartingRuntime,
1478 DecodedTransactions,
1479 AppliedAccountModifications,
1480 ReadyToExecuteUserTransactions,
1481 ExecutedUserTransactions,
1482 ExecutingBlockTransactions,
1483 ExecutedBlockTransactions,
1484 ProgramAccountsLoaded,
1485}
1486
1487impl std::fmt::Display for BacktestStatus {
1488 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1489 let s = match self {
1490 Self::StartingRuntime => "starting runtime",
1491 Self::DecodedTransactions => "decoded transactions",
1492 Self::AppliedAccountModifications => "applied account modifications",
1493 Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
1494 Self::ExecutedUserTransactions => "executed user transactions",
1495 Self::ExecutingBlockTransactions => "executing block transactions",
1496 Self::ExecutedBlockTransactions => "executed block transactions",
1497 Self::ProgramAccountsLoaded => "program accounts loaded",
1498 };
1499 f.write_str(s)
1500 }
1501}
1502
1503#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1508#[serde(rename_all = "camelCase")]
1509pub struct RerouteStatsReport {
1510 #[serde(default)]
1512 pub swaps_detected: u64,
1513 #[serde(default)]
1517 pub swaps_filtered: u64,
1518 #[serde(default)]
1520 pub swaps_rerouted: u64,
1521 #[serde(default)]
1523 pub swaps_simulated: u64,
1524 #[serde(default)]
1526 pub swaps_succeeded: u64,
1527 #[serde(default)]
1529 pub requote_failures: u64,
1530 #[serde(default)]
1532 pub override_setup_failures: u64,
1533 #[serde(default)]
1535 pub metis_unavailable_blocks: u64,
1536 #[serde(default)]
1540 pub quote_attempts_unavailable: u64,
1541 #[serde(default)]
1543 pub direct_fill_matched: u64,
1544 #[serde(default)]
1546 pub direct_fill_built: u64,
1547 #[serde(default)]
1551 pub direct_fill_rejections: BTreeMap<String, u64>,
1552 #[serde(default)]
1554 pub direct_fill_outcomes: BTreeMap<String, u64>,
1555 #[serde(default)]
1557 pub direct_fill_scored: u64,
1558 #[serde(default)]
1562 pub direct_fill_bps_total: i64,
1563}
1564
1565#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1567#[serde(rename_all = "camelCase")]
1568pub struct SessionSummary {
1569 pub correct_simulation: usize,
1572 pub incorrect_simulation: usize,
1575 pub execution_errors: usize,
1577 pub balance_diff: usize,
1579 pub log_diff: usize,
1581 #[serde(default)]
1584 pub unsanitizable: usize,
1585 #[serde(default, skip_serializing_if = "Option::is_none")]
1588 pub reroute_stats: Option<Box<RerouteStatsReport>>,
1589}
1590
1591impl SessionSummary {
1592 pub fn has_deviations(&self) -> bool {
1594 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
1595 }
1596
1597 pub fn total_transactions(&self) -> usize {
1599 self.correct_simulation
1600 + self.incorrect_simulation
1601 + self.execution_errors
1602 + self.balance_diff
1603 + self.log_diff
1604 }
1605
1606 pub fn deviation_details(&self, balance_label: &str, log_label: &str) -> String {
1611 format!(
1612 "{} incorrect simulations, {} execution errors, {} {balance_label}, {} {log_label}",
1613 self.incorrect_simulation, self.execution_errors, self.balance_diff, self.log_diff,
1614 )
1615 }
1616}
1617
1618impl std::fmt::Display for SessionSummary {
1619 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1620 let total = self.total_transactions();
1621 write!(
1622 f,
1623 "Session summary: {total} transactions\n\
1624 \x20 - {} correct simulation\n\
1625 \x20 - {} incorrect simulation\n\
1626 \x20 - {} execution errors\n\
1627 \x20 - {} unsanitizable\n\
1628 \x20 - {} balance diffs\n\
1629 \x20 - {} log diffs",
1630 self.correct_simulation,
1631 self.incorrect_simulation,
1632 self.execution_errors,
1633 self.unsanitizable,
1634 self.balance_diff,
1635 self.log_diff,
1636 )
1637 }
1638}
1639
1640#[derive(Debug, Clone, Serialize, Deserialize)]
1642#[serde(rename_all = "camelCase")]
1643pub enum BacktestError {
1644 InvalidTransactionEncoding {
1645 index: usize,
1646 error: String,
1647 },
1648 InvalidTransactionFormat {
1649 index: usize,
1650 error: String,
1651 },
1652 InvalidAccountEncoding {
1653 address: String,
1654 encoding: BinaryEncoding,
1655 error: String,
1656 },
1657 InvalidAccountOwner {
1658 address: String,
1659 error: String,
1660 },
1661 InvalidAccountPubkey {
1662 address: String,
1663 error: String,
1664 },
1665 NoMoreBlocks,
1666 BundleNotFound {
1667 start_slot: u64,
1668 end_slot: u64,
1669 },
1670 AdvanceSlotFailed {
1671 slot: u64,
1672 error: String,
1673 },
1674 FinalizeSlotFailed {
1675 slot: u64,
1676 error: String,
1677 },
1678 InvalidRequest {
1679 error: String,
1680 },
1681 Internal {
1682 error: String,
1683 },
1684 InvalidBlockhashFormat {
1685 slot: u64,
1686 error: String,
1687 },
1688 InitializingSysvarsFailed {
1689 slot: u64,
1690 error: String,
1691 },
1692 ClerkError {
1693 error: String,
1694 },
1695 SimulationError {
1696 error: String,
1697 },
1698 SessionNotFound {
1699 session_id: String,
1700 },
1701 SessionOwnerMismatch,
1702 SessionOwnershipBusy {
1707 reason: String,
1708 },
1709 NoProducedBlocksInRange {
1715 start_slot: u64,
1716 end_slot: u64,
1717 },
1718}
1719
1720impl BacktestError {
1721 pub fn is_terminal(&self) -> bool {
1725 matches!(
1726 self,
1727 BacktestError::NoMoreBlocks
1728 | BacktestError::BundleNotFound { .. }
1729 | BacktestError::AdvanceSlotFailed { .. }
1730 | BacktestError::FinalizeSlotFailed { .. }
1731 | BacktestError::Internal { .. }
1732 | BacktestError::NoProducedBlocksInRange { .. }
1733 )
1734 }
1735}
1736
1737#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1739pub struct AvailableRange {
1740 pub bundle_start_slot: u64,
1741 pub bundle_start_slot_utc: Option<String>,
1742 pub max_bundle_end_slot: Option<u64>,
1743 pub max_bundle_end_slot_utc: Option<String>,
1744 pub max_bundle_size: Option<u64>,
1745 #[serde(default)]
1748 pub kind: Option<BundleKind>,
1749 #[serde(default)]
1752 pub scope: Option<RangeScope>,
1753}
1754
1755#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1757#[serde(rename_all = "snake_case")]
1758pub enum RangeScope {
1759 Global,
1761 User,
1763}
1764
1765impl RangeScope {
1766 pub fn as_str(self) -> &'static str {
1768 match self {
1769 Self::Global => "global",
1770 Self::User => "user",
1771 }
1772 }
1773}
1774
1775#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1777#[serde(rename_all = "snake_case")]
1778pub enum BundleKind {
1779 #[default]
1781 Blocks,
1782 AccountState,
1786}
1787
1788pub const REPLAY_RANGE_ADVICE: &str = "replayed state is resolved by the recording that begins \
1792 where the request does, so request a range that begins where one was recorded and ends \
1793 within it";
1794
1795impl BundleKind {
1796 fn coverage_noun(self) -> &'static str {
1798 match self {
1799 Self::Blocks => "bundle range",
1800 Self::AccountState => "account-state bundle you own",
1801 }
1802 }
1803
1804 fn coverage_nouns(self) -> &'static str {
1806 match self {
1807 Self::Blocks => "bundle ranges",
1808 Self::AccountState => "account-state bundles you own",
1809 }
1810 }
1811
1812 pub fn as_str(self) -> &'static str {
1814 match self {
1815 Self::Blocks => "blocks",
1816 Self::AccountState => "account_state",
1817 }
1818 }
1819
1820 pub fn from_str_opt(value: &str) -> Option<Self> {
1822 match value {
1823 "blocks" => Some(Self::Blocks),
1824 "account_state" => Some(Self::AccountState),
1825 _ => None,
1826 }
1827 }
1828}
1829
1830impl fmt::Display for BundleKind {
1833 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1834 let label = match self {
1835 Self::Blocks => "transactions",
1836 Self::AccountState => "account_state",
1837 };
1838 f.pad(label)
1839 }
1840}
1841
1842#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1846pub struct BundleBuildRequest {
1847 pub start_slot: u64,
1848 pub end_slot: u64,
1849 #[serde(default)]
1850 pub bundle_size: Option<u64>,
1851 #[serde(default)]
1854 pub idempotency_key: Option<String>,
1855 #[serde(default)]
1857 pub kind: BundleKind,
1858}
1859
1860#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1861#[serde(rename_all = "snake_case")]
1862pub enum BundleBuildStatus {
1863 Completed,
1865 Failed,
1867 InProgress,
1869 NeedsInvestigation,
1872}
1873
1874impl BundleBuildStatus {
1875 pub fn as_str(self) -> &'static str {
1877 match self {
1878 Self::Completed => "completed",
1879 Self::Failed => "failed",
1880 Self::InProgress => "in_progress",
1881 Self::NeedsInvestigation => "needs_investigation",
1882 }
1883 }
1884}
1885
1886#[derive(Debug, Clone, Serialize, Deserialize)]
1888pub struct BundleBuildStatusResponse {
1889 pub request_id: String,
1890 pub start_slot: u64,
1891 pub end_slot: u64,
1892 pub bundle_size: Option<u64>,
1893 pub status: BundleBuildStatus,
1894 pub created_at_unix_ms: u64,
1897 #[serde(default)]
1899 pub kind: BundleKind,
1900}
1901
1902#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1904pub struct CompletedBundle {
1905 pub start_slot: u64,
1906 pub end_slot: u64,
1907 pub kind: BundleKind,
1908 #[serde(default)]
1910 pub completed_at_unix_ms: Option<i64>,
1911 #[serde(default)]
1912 pub size_bytes: Option<i64>,
1913}
1914
1915#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1917pub struct BundleDeleteRequest {
1918 pub start_slot: u64,
1919 pub end_slot: u64,
1920 #[serde(default)]
1922 pub kind: BundleKind,
1923}
1924
1925#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1927pub struct BundleDeleteResponse {
1928 pub start_slot: u64,
1929 pub end_slot: u64,
1930 pub kind: BundleKind,
1931}
1932
1933#[derive(Debug, Clone, Serialize, Deserialize)]
1935pub struct BundleBuildListResponse {
1936 pub builds: Vec<BundleBuildStatusResponse>,
1939 #[serde(default)]
1944 pub bundles: Option<Vec<CompletedBundle>>,
1945}
1946
1947#[derive(Debug, Clone, Serialize, Deserialize)]
1950pub struct SnapshotSlotResponse {
1951 pub snapshot_slot: u64,
1952}
1953
1954pub fn split_range(
1977 ranges: &[AvailableRange],
1978 requested: SlotRange,
1979 kind: BundleKind,
1980) -> Result<Vec<(u64, u64)>, String> {
1981 let (requested_start, requested_end) = (requested.start(), requested.end());
1982
1983 let ends_by_start = ranges
1990 .iter()
1991 .filter(|r| r.kind.unwrap_or_default() == kind)
1992 .filter_map(|r| {
1993 r.max_bundle_end_slot
1994 .filter(|&end| end >= r.bundle_start_slot)
1995 .map(|end| (r.bundle_start_slot, end))
1996 })
1997 .fold(
1998 BTreeMap::<u64, BTreeSet<u64>>::new(),
1999 |mut ends_by_start, (start, end)| {
2000 ends_by_start.entry(start).or_default().insert(end);
2001 ends_by_start
2002 },
2003 );
2004
2005 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
2013 ends.iter()
2014 .next_back()
2015 .is_some_and(|&end| end >= requested_start)
2016 }) else {
2017 return Err(format!(
2018 "start_slot {requested_start} is not covered by any available {}",
2019 kind.coverage_noun()
2020 ));
2021 };
2022
2023 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
2029 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
2030 let mut best: Option<Vec<(u64, u64)>> = None;
2031 for &end in ends {
2032 let candidate = if end >= requested_end {
2033 Some(vec![(start, requested_end)])
2034 } else {
2035 best_from.get(&(end + 1)).map(|rest| {
2036 std::iter::once((start, end))
2037 .chain(rest.iter().copied())
2038 .collect()
2039 })
2040 };
2041 if let Some(candidate) = candidate
2042 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
2043 {
2044 best = Some(candidate);
2045 }
2046 }
2047 if let Some(best) = best {
2048 best_from.insert(start, best);
2049 }
2050 }
2051
2052 best_from.remove(&anchor_start).ok_or_else(|| {
2053 let mut covered_to = anchor_start.saturating_sub(1);
2057 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
2058 if start > covered_to.saturating_add(1) {
2059 break;
2060 }
2061 if let Some(&end) = ends.iter().next_back() {
2062 covered_to = covered_to.max(end);
2063 }
2064 }
2065 if covered_to < requested_end {
2066 format!("gap in coverage at slot {}", covered_to + 1)
2067 } else {
2068 format!(
2069 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available {}",
2070 kind.coverage_nouns()
2071 )
2072 }
2073 })
2074}
2075
2076impl From<BacktestError> for BacktestResponse {
2077 fn from(error: BacktestError) -> Self {
2078 Self::Error(error)
2079 }
2080}
2081
2082impl std::error::Error for BacktestError {}
2083
2084impl fmt::Display for BacktestError {
2085 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2086 match self {
2087 BacktestError::InvalidTransactionEncoding { index, error } => {
2088 write!(f, "invalid transaction encoding at index {index}: {error}")
2089 }
2090 BacktestError::InvalidTransactionFormat { index, error } => {
2091 write!(f, "invalid transaction format at index {index}: {error}")
2092 }
2093 BacktestError::InvalidAccountEncoding {
2094 address,
2095 encoding,
2096 error,
2097 } => write!(
2098 f,
2099 "invalid encoding for account {address} ({encoding:?}): {error}"
2100 ),
2101 BacktestError::InvalidAccountOwner { address, error } => {
2102 write!(f, "invalid owner for account {address}: {error}")
2103 }
2104 BacktestError::InvalidAccountPubkey { address, error } => {
2105 write!(f, "invalid account pubkey {address}: {error}")
2106 }
2107 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
2108 BacktestError::BundleNotFound {
2109 start_slot,
2110 end_slot,
2111 } => write!(
2112 f,
2113 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
2114 ),
2115 BacktestError::AdvanceSlotFailed { slot, error } => {
2116 write!(f, "failed to advance to slot {slot}: {error}")
2117 }
2118 BacktestError::FinalizeSlotFailed { slot, error } => {
2119 write!(f, "failed to finalize slot {slot}: {error}")
2120 }
2121 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
2122 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
2123 BacktestError::InvalidBlockhashFormat { slot, error } => {
2124 write!(f, "invalid blockhash at slot {slot}: {error}")
2125 }
2126 BacktestError::InitializingSysvarsFailed { slot, error } => {
2127 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
2128 }
2129 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
2130 BacktestError::SimulationError { error } => {
2131 write!(f, "simulation error: {error}")
2132 }
2133 BacktestError::SessionNotFound { session_id } => {
2134 write!(f, "session not found: {session_id}")
2135 }
2136 BacktestError::SessionOwnerMismatch => {
2137 write!(f, "session owner mismatch")
2138 }
2139 BacktestError::SessionOwnershipBusy { reason } => {
2140 write!(f, "session ownership busy: {reason}")
2141 }
2142 BacktestError::NoProducedBlocksInRange {
2143 start_slot,
2144 end_slot,
2145 } => write!(
2146 f,
2147 "no produced blocks in requested range {start_slot}..={end_slot}; \
2148 slot(s) may be skipped on-chain"
2149 ),
2150 }
2151 }
2152}
2153
2154#[cfg(test)]
2155mod tests {
2156 use rstest::rstest;
2157 use solana_account::ReadableAccount;
2158
2159 use super::*;
2160
2161 fn address(byte: u8) -> Address {
2162 Address::from([byte; 32])
2163 }
2164
2165 #[rstest]
2166 #[case::one("jupiter", Some(&[SwapAggregator::Jupiter][..]))]
2167 #[case::several("jupiter,okx", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2168 #[case::spaced_and_uppercased(" Jupiter , OKX ", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2169 #[case::repeats_collapse("okx,okx", Some(&[SwapAggregator::Okx][..]))]
2170 #[case::trailing_separator("okx,", Some(&[SwapAggregator::Okx][..]))]
2171 #[case::unknown("jupiter,bogus", None)]
2172 #[case::empty("", None)]
2173 #[case::separators_only(",,", None)]
2174 fn reroute_aggregators_parse_the_shapes_a_flag_carries(
2175 #[case] input: &str,
2176 #[case] expected: Option<&[SwapAggregator]>,
2177 ) {
2178 assert_eq!(
2179 input.parse::<RerouteAggregators>().ok(),
2180 expected.map(|aggregators| RerouteAggregators::new(aggregators.iter().copied()))
2181 );
2182 }
2183
2184 #[test]
2186 fn reroute_aggregators_round_trip_through_their_rendering() {
2187 let aggregators = RerouteAggregators::new([SwapAggregator::Dflow, SwapAggregator::Jupiter]);
2188
2189 assert_eq!(aggregators.to_string(), "jupiter,dflow");
2190 assert_eq!(
2191 aggregators
2192 .to_string()
2193 .parse::<RerouteAggregators>()
2194 .unwrap(),
2195 aggregators
2196 );
2197 assert_eq!(RerouteAggregators::default().to_string(), "jupiter");
2198 }
2199
2200 #[test]
2203 fn aggregators_deserialize_through_the_same_parser_the_flag_uses() {
2204 let params: CreateSessionParams = serde_json::from_value(
2205 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {"aggregators": ["Jupiter", "OKX"]}}),
2206 )
2207 .expect("decode");
2208
2209 assert_eq!(
2210 params.reroute.expect("rerouting requested").aggregators,
2211 RerouteAggregators::new([SwapAggregator::Jupiter, SwapAggregator::Okx])
2212 );
2213 }
2214
2215 #[test]
2218 fn params_without_the_new_reroute_fields_decode_to_the_narrow_defaults() {
2219 let params: CreateSessionParams = serde_json::from_value(
2220 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {}}),
2221 )
2222 .expect("decode");
2223
2224 let reroute = params.reroute.expect("rerouting requested");
2225 assert_eq!(reroute.aggregators, RerouteAggregators::default());
2226 assert!(reroute.aggregators.contains(SwapAggregator::Jupiter));
2227 assert!(!reroute.aggregators.contains(SwapAggregator::Okx));
2228 assert!(!reroute.circular_arbs);
2229 assert!(!reroute.detect_failed_l1_swaps);
2230 }
2231
2232 #[test]
2234 fn a_request_without_reroute_reserves_no_sidecar() {
2235 let params: CreateSessionParams =
2236 serde_json::from_value(serde_json::json!({"startSlot": 1, "endSlot": 2}))
2237 .expect("decode");
2238
2239 assert!(!params.requires_sidecar());
2240 }
2241
2242 #[test]
2247 fn a_per_slot_action_schedule_compresses_across_actions() {
2248 let slots = 2_000u64;
2249 let actions = (0..slots)
2250 .map(|slot| ScheduledAction {
2251 anchor: ActionAnchor::AfterSlot { slots: vec![slot] },
2252 kind: ActionKind::Simulate,
2253 feeds_reroute: false,
2254 transactions: Vec::new(),
2255 account_overrides: AccountModifications(BTreeMap::from([(
2256 address(7),
2257 AccountData {
2258 data: EncodedBinary::new(
2260 BASE64.encode(
2261 [&slot.to_le_bytes()[..], &[0xAB; 2040][..]]
2262 .concat()
2263 .as_slice(),
2264 ),
2265 BinaryEncoding::Base64,
2266 ),
2267 executable: false,
2268 lamports: 15_144_966,
2269 owner: address(9),
2270 space: 2048,
2271 },
2272 )])),
2273 return_accounts: Vec::new(),
2274 label: None,
2275 })
2276 .collect::<Vec<_>>();
2277 let params = CreateSessionParams::builder()
2278 .start_slot(1)
2279 .end_slot(slots)
2280 .actions(actions.clone())
2281 .build();
2282
2283 let encoded = serde_json::to_string(¶ms).expect("serialize");
2284 let raw = serde_json::to_string(&actions).expect("serialize raw actions");
2285 assert!(
2286 encoded.len() * 8 < raw.len(),
2287 "expected the schedule to compress well; got {} bytes from {} raw",
2288 encoded.len(),
2289 raw.len()
2290 );
2291
2292 let decoded: CreateSessionParams = serde_json::from_str(&encoded).expect("round trip");
2293 assert_eq!(
2294 serde_json::to_string(&decoded.actions).expect("serialize decoded"),
2295 raw
2296 );
2297 }
2298
2299 #[test]
2302 fn an_empty_action_list_is_omitted_entirely() {
2303 let params = CreateSessionParams::builder()
2304 .start_slot(1)
2305 .end_slot(2)
2306 .build();
2307 let json = serde_json::to_value(¶ms).expect("serialize");
2308 assert!(json.get("actions").is_none(), "{json}");
2309 let decoded: CreateSessionParams = serde_json::from_value(json).expect("round trip");
2310 assert!(decoded.actions.is_empty());
2311 }
2312
2313 #[test]
2316 fn send_actions_conflict_with_replay_only_together() {
2317 let params = |replay, kind| {
2318 CreateSessionParams::builder()
2319 .start_slot(1)
2320 .end_slot(2)
2321 .replay_account_state(replay)
2322 .actions(vec![ScheduledAction {
2323 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2324 kind,
2325 feeds_reroute: false,
2326 transactions: vec!["dGVzdA==".to_string()],
2327 account_overrides: Default::default(),
2328 return_accounts: Default::default(),
2329 label: None,
2330 }])
2331 .build()
2332 };
2333 assert!(replay_support::send_action_conflict(¶ms(false, ActionKind::Send)).is_none());
2334 assert!(
2335 replay_support::send_action_conflict(¶ms(true, ActionKind::Simulate)).is_none()
2336 );
2337 let mut publishing = params(true, ActionKind::Simulate);
2338 publishing.actions[0].feeds_reroute = true;
2339 assert!(replay_support::send_action_conflict(&publishing).is_none());
2340
2341 let conflict = replay_support::send_action_conflict(¶ms(true, ActionKind::Send))
2342 .expect("send under replay conflicts");
2343 assert!(conflict.contains("kind: send"), "{conflict}");
2344 assert!(conflict.contains(replay_support::SEND_ACTION), "{conflict}");
2345 }
2346
2347 fn override_entry(owner: Address, executable: bool) -> AccountData {
2348 AccountData {
2349 data: EncodedBinary::from_bytes(&[1, 2, 3], BinaryEncoding::Base64),
2350 executable,
2351 lamports: 1,
2352 owner,
2353 space: 3,
2354 }
2355 }
2356
2357 fn before() -> ActionAnchor {
2358 ActionAnchor::BeforeSlot { slots: vec![1] }
2359 }
2360
2361 fn valid() -> ScheduledAction {
2362 override_action(before(), address(3), override_entry(address(4), false))
2363 }
2364
2365 fn override_action(
2366 anchor: ActionAnchor,
2367 account: Address,
2368 data: AccountData,
2369 ) -> ScheduledAction {
2370 ScheduledAction {
2371 anchor,
2372 kind: ActionKind::default(),
2373 transactions: Vec::new(),
2374 account_overrides: AccountModifications(BTreeMap::from([(account, data)])),
2375 return_accounts: Vec::new(),
2376 label: None,
2377 feeds_reroute: true,
2378 }
2379 }
2380
2381 #[test]
2382 fn override_action_round_trips() {
2383 let key = address(1);
2384 let entry = override_entry(address(2), false);
2385 let action = override_action(
2386 ActionAnchor::BeforeSlot { slots: vec![5, 9] },
2387 key,
2388 entry.clone(),
2389 );
2390 let json = serde_json::to_value(&action).unwrap();
2391 assert!(
2392 json["anchor"]["slots"].is_string(),
2393 "slots travel as a compressed blob: {json}"
2394 );
2395 let back: ScheduledAction = serde_json::from_value(json).unwrap();
2396 assert_eq!(back.anchor, ActionAnchor::BeforeSlot { slots: vec![5, 9] });
2397 assert!(back.feeds_reroute);
2398 assert!(!back.is_computed_override(), "no transactions to simulate");
2399 let survived = &back.account_overrides.0[&key];
2400 assert_eq!(survived.lamports, entry.lamports);
2401 assert_eq!(survived.data.data, entry.data.data);
2402 }
2403
2404 #[test]
2409 fn a_token_account_override_is_accepted() {
2410 let action = override_action(
2411 before(),
2412 address(3),
2413 override_entry(spl_token_interface::ID, false),
2414 );
2415 let schedule = decode_override_actions(&[action]).expect("token overrides decode");
2416 assert!(!schedule.active_at(1).is_empty());
2417 }
2418
2419 #[rstest]
2420 #[case::executable(override_action(before(), address(3), override_entry(address(10), true)))]
2421 #[case::program_owned(override_action(
2422 before(),
2423 address(3),
2424 override_entry(solana_account::PROGRAM_OWNERS[0], false),
2425 ))]
2426 #[case::after_slot_anchor(override_action(
2427 ActionAnchor::AfterSlot { slots: vec![1] },
2428 address(3),
2429 override_entry(address(4), false),
2430 ))]
2431 #[case::nothing_to_post(ScheduledAction {
2432 account_overrides: AccountModifications::default(),
2433 ..valid()
2434 })]
2435 #[case::send(ScheduledAction {
2437 kind: ActionKind::Send,
2438 ..valid()
2439 })]
2440 #[allow(deprecated, reason = "exercise the loader-owner rejection path")]
2441 fn override_actions_reject_invalid_accounts_and_shapes(#[case] action: ScheduledAction) {
2442 assert!(decode_override_actions(&[action]).is_err());
2443 }
2444
2445 #[test]
2447 fn override_actions_accept_a_static_and_a_computed_shape() {
2448 assert!(decode_override_actions(&[valid()]).is_ok());
2449 assert!(
2452 decode_override_actions(&[ScheduledAction {
2453 transactions: vec!["dGVzdA==".to_string()],
2454 ..valid()
2455 }])
2456 .is_ok()
2457 );
2458 }
2459
2460 #[test]
2461 fn override_actions_merge_account_wise_later_wins() {
2462 let entry = |lamports| AccountData {
2463 lamports,
2464 ..override_entry(address(9), false)
2465 };
2466 let slot_action = |slots, account, lamports| {
2467 override_action(ActionAnchor::BeforeSlot { slots }, account, entry(lamports))
2468 };
2469 let actions = [
2470 slot_action(vec![5, 7], address(1), 1),
2471 slot_action(vec![5], address(2), 2),
2472 slot_action(vec![5], address(1), 3),
2473 ScheduledAction {
2474 anchor: ActionAnchor::AfterSlot { slots: vec![5] },
2475 kind: ActionKind::Simulate,
2476 feeds_reroute: false,
2477 transactions: vec!["dGVzdA==".to_string()],
2478 account_overrides: AccountModifications::default(),
2479 return_accounts: Vec::new(),
2480 label: None,
2481 },
2482 ];
2483 let schedule = decode_override_actions(&actions).unwrap();
2484 assert_eq!(schedule.slots().collect::<Vec<_>>(), vec![5, 7]);
2485 let lamports_at = |key, slot| {
2486 schedule
2487 .active_for(&key, slot)
2488 .map(|account| account.lamports())
2489 };
2490 assert_eq!(lamports_at(address(1), 5), Some(3));
2491 assert_eq!(lamports_at(address(2), 5), Some(2));
2492 assert_eq!(lamports_at(address(1), 7), Some(1));
2493 }
2494
2495 #[test]
2496 fn computed_override_resolution_preserves_action_order_and_failure() {
2497 let key = address(1);
2498 let static_action = |lamports| {
2499 override_action(
2500 ActionAnchor::BeforeSlot { slots: vec![5] },
2501 key,
2502 AccountData {
2503 lamports,
2504 ..override_entry(address(9), false)
2505 },
2506 )
2507 };
2508 let computed_action = || ScheduledAction {
2509 transactions: vec!["dGVzdA==".to_string()],
2510 ..static_action(2)
2511 };
2512
2513 let computed_last =
2514 decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2515 computed_last
2516 .resolve(5, 1, Some(HashMap::from([(key, shared_account(2))])))
2517 .unwrap();
2518 assert_eq!(
2519 computed_last.active_for(&key, 5).map(|a| a.lamports()),
2520 Some(2)
2521 );
2522
2523 let static_last = decode_override_actions(&[computed_action(), static_action(3)]).unwrap();
2524 static_last
2525 .resolve(5, 0, Some(HashMap::from([(key, shared_account(2))])))
2526 .unwrap();
2527 assert_eq!(
2528 static_last.active_for(&key, 5).map(|a| a.lamports()),
2529 Some(3)
2530 );
2531
2532 let failed = decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2533 failed.resolve(5, 1, None).unwrap();
2534 assert_eq!(failed.active_for(&key, 5).map(|a| a.lamports()), Some(1));
2535 assert!(failed.resolve(5, 1, None).is_err(), "resolving twice");
2536 }
2537
2538 fn shared_account(lamports: u64) -> AccountSharedData {
2539 AccountSharedData::from(solana_account::Account {
2540 lamports,
2541 data: vec![1],
2542 owner: address(9),
2543 executable: false,
2544 rent_epoch: 0,
2545 })
2546 }
2547
2548 fn carry_forward_schedule() -> (Address, Address, OverrideSchedule) {
2549 let (a, b) = (address(1), address(2));
2550 let schedule = OverrideSchedule::from(BTreeMap::from([
2551 (10, HashMap::from([(a, shared_account(1))])),
2552 (
2553 20,
2554 HashMap::from([(a, shared_account(2)), (b, shared_account(9))]),
2555 ),
2556 ]));
2557 (a, b, schedule)
2558 }
2559
2560 #[test]
2561 fn override_schedule_resolves_carry_forward_per_account() {
2562 let (a, b, schedule) = carry_forward_schedule();
2563
2564 assert!(schedule.active_for(&a, 9).is_none());
2565 let cases = [
2566 (9, None, None),
2567 (10, Some(1), None),
2568 (15, Some(1), None),
2569 (20, Some(2), Some(9)),
2570 (u64::MAX, Some(2), Some(9)),
2571 ];
2572 for (slot, want_a, want_b) in cases {
2573 assert_eq!(
2574 schedule
2575 .active_for(&a, slot)
2576 .map(|account| account.lamports()),
2577 want_a,
2578 "active_for a at {slot}"
2579 );
2580 let active = schedule.active_at(slot);
2581 assert_eq!(
2582 active.get(&a).map(ReadableAccount::lamports),
2583 want_a,
2584 "active_at a at {slot}"
2585 );
2586 assert_eq!(
2587 active.get(&b).map(ReadableAccount::lamports),
2588 want_b,
2589 "active_at b at {slot}"
2590 );
2591 }
2592 }
2593
2594 #[test]
2595 fn newly_active_yields_final_values_in_window() {
2596 let (a, b, schedule) = carry_forward_schedule();
2597
2598 let newly: HashMap<_, _> = schedule.newly_active(Some(10), 20).into_iter().collect();
2599 assert_eq!(newly.len(), 2);
2600 assert_eq!(newly[&a].lamports(), 2);
2601 assert_eq!(newly[&b].lamports(), 9);
2602 let spanning: HashMap<_, _> = schedule.newly_active(None, 20).into_iter().collect();
2603 assert_eq!(spanning[&a].lamports(), 2, "later slot wins");
2604 assert_eq!(schedule.newly_active(None, 15).len(), 1);
2605 }
2606
2607 #[test]
2608 fn reroute_filter_round_trips_camel_case() {
2609 let filter = RerouteFilter {
2610 pairs: BTreeSet::from([MintPair::new(address(1), address(2))]),
2611 };
2612 let json = serde_json::to_value(&filter).unwrap();
2613 assert!(
2614 json["pairs"][0][0].is_string() && json["pairs"][0][1].is_string(),
2615 "a pair serializes as two base58 strings: {json}"
2616 );
2617 let back: RerouteFilter = serde_json::from_value(json).unwrap();
2618 assert_eq!(back, filter);
2619 }
2620
2621 #[test]
2624 fn mint_pairs_are_normalized_however_they_are_built() {
2625 let (one, other) = (address(1), address(2));
2626 assert_eq!(MintPair::new(one, other), MintPair::new(other, one));
2627
2628 let reversed: MintPair =
2629 serde_json::from_value(serde_json::json!([other.to_string(), one.to_string()]))
2630 .unwrap();
2631 assert_eq!(reversed, MintPair::new(one, other));
2632 }
2633
2634 #[test]
2635 fn fail_fast_divergence_kind_str_round_trips() {
2636 for kind in [
2637 FailFastDivergenceKind::AnyNonBenign,
2638 FailFastDivergenceKind::Tracked,
2639 ] {
2640 assert_eq!(
2641 FailFastDivergenceKind::from_str_opt(kind.as_str()),
2642 Some(kind)
2643 );
2644 }
2645 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
2646 assert_eq!(
2647 FailFastDivergenceKind::default(),
2648 FailFastDivergenceKind::AnyNonBenign
2649 );
2650 }
2651
2652 #[test]
2653 fn bundle_not_found_is_terminal_and_descriptive() {
2654 let error = BacktestError::BundleNotFound {
2655 start_slot: 100,
2656 end_slot: 600,
2657 };
2658
2659 assert!(error.is_terminal());
2660 assert_eq!(
2661 error.to_string(),
2662 "block bundle not found for start_slot=100, end_slot=600"
2663 );
2664 }
2665
2666 #[test]
2667 fn no_produced_blocks_is_terminal_and_descriptive() {
2668 let error = BacktestError::NoProducedBlocksInRange {
2669 start_slot: 100,
2670 end_slot: 100,
2671 };
2672
2673 assert!(error.is_terminal());
2674 assert_eq!(
2677 error.to_string(),
2678 "no produced blocks in requested range 100..=100; slot(s) may be skipped on-chain"
2679 );
2680 }
2681
2682 #[test]
2683 fn bundle_build_request_optional_fields_default_to_none() {
2684 let req: BundleBuildRequest =
2685 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
2686 assert_eq!((req.start_slot, req.end_slot), (1, 2));
2687 assert_eq!(req.bundle_size, None);
2688 assert_eq!(req.idempotency_key, None);
2689 }
2690
2691 #[test]
2692 fn bundle_build_request_parses_optional_fields() {
2693 let req: BundleBuildRequest = serde_json::from_str(
2694 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
2695 )
2696 .expect("parse");
2697 assert_eq!(req.bundle_size, Some(500));
2698 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
2699 }
2700
2701 #[test]
2702 fn bundle_build_status_serde_round_trips_with_snake_case() {
2703 let cases = [
2704 (BundleBuildStatus::Completed, "\"completed\""),
2705 (BundleBuildStatus::Failed, "\"failed\""),
2706 (BundleBuildStatus::InProgress, "\"in_progress\""),
2707 (
2708 BundleBuildStatus::NeedsInvestigation,
2709 "\"needs_investigation\"",
2710 ),
2711 ];
2712 for (status, expected) in cases {
2713 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
2714 assert_eq!(
2715 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
2716 status
2717 );
2718 assert_eq!(status.as_str(), expected.trim_matches('"'));
2719 }
2720 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
2721 }
2722
2723 #[test]
2724 fn bundle_build_request_without_kind_deserializes_as_blocks() {
2725 let request: BundleBuildRequest =
2727 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
2728 assert_eq!(request.kind, BundleKind::Blocks);
2729 }
2730
2731 #[test]
2732 fn bundle_kind_round_trips_over_serde_and_as_str() {
2733 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
2734 let json = serde_json::to_string(&kind).unwrap();
2735 assert_eq!(json, format!("\"{}\"", kind.as_str()));
2736 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
2737 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
2740 }
2741 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
2742 assert_eq!(BundleKind::from_str_opt("accountState"), None);
2743 }
2744
2745 #[test]
2746 fn bundle_build_request_carries_kind_through_serde() {
2747 let request = BundleBuildRequest {
2748 start_slot: 100,
2749 end_slot: 200,
2750 kind: BundleKind::AccountState,
2751 ..Default::default()
2752 };
2753 let json = serde_json::to_string(&request).unwrap();
2754 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2755 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
2756 assert_eq!(parsed.kind, BundleKind::AccountState);
2757 }
2758
2759 #[test]
2760 fn bundle_build_status_response_serializes_request_and_status() {
2761 let response = BundleBuildStatusResponse {
2762 request_id: "r".to_string(),
2763 start_slot: 100,
2764 end_slot: 200,
2765 bundle_size: Some(50),
2766 status: BundleBuildStatus::InProgress,
2767 created_at_unix_ms: 0,
2768 kind: BundleKind::Blocks,
2769 };
2770 let json = serde_json::to_value(&response).unwrap();
2771 assert_eq!(json["request_id"].as_str(), Some("r"));
2772 assert_eq!(json["start_slot"].as_u64(), Some(100));
2773 assert_eq!(json["bundle_size"].as_u64(), Some(50));
2774 assert_eq!(json["status"].as_str(), Some("in_progress"));
2775 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
2776 }
2777
2778 #[test]
2779 fn an_empty_bundles_list_is_distinct_from_an_absent_one() {
2780 let absent: BundleBuildListResponse = serde_json::from_str(r#"{"builds":[]}"#).unwrap();
2782 let empty: BundleBuildListResponse =
2783 serde_json::from_str(r#"{"builds":[],"bundles":[]}"#).unwrap();
2784 assert_eq!(absent.bundles, None);
2785 assert_eq!(empty.bundles, Some(Vec::new()));
2786 }
2787
2788 #[test]
2789 fn completed_bundle_round_trips() {
2790 let bundle = CompletedBundle {
2791 start_slot: 100,
2792 end_slot: 200,
2793 kind: BundleKind::AccountState,
2794 completed_at_unix_ms: Some(1_700_000_000_000),
2795 size_bytes: Some(4_096),
2796 };
2797 let json = serde_json::to_string(&bundle).unwrap();
2798 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2799 assert_eq!(
2800 serde_json::from_str::<CompletedBundle>(&json).unwrap(),
2801 bundle
2802 );
2803
2804 let sparse = r#"{"start_slot":1,"end_slot":2,"kind":"blocks"}"#;
2805 let parsed: CompletedBundle = serde_json::from_str(sparse).unwrap();
2806 assert_eq!(parsed.completed_at_unix_ms, None);
2807 assert_eq!(parsed.size_bytes, None);
2808 }
2809
2810 #[test]
2811 fn available_range_reads_a_server_that_predates_kind_and_scope() {
2812 let json = r#"{
2814 "bundle_start_slot": 100,
2815 "bundle_start_slot_utc": null,
2816 "max_bundle_end_slot": 200,
2817 "max_bundle_end_slot_utc": null,
2818 "max_bundle_size": 101
2819 }"#;
2820 let parsed: AvailableRange = serde_json::from_str(json).unwrap();
2821 assert_eq!(parsed.bundle_start_slot, 100);
2822 assert_eq!(parsed.kind, None);
2823 assert_eq!(parsed.scope, None);
2824 }
2825
2826 #[test]
2827 fn available_range_ignores_fields_it_does_not_know() {
2828 let json = r#"{"bundle_start_slot": 1, "some_future_field": "x"}"#;
2830 assert_eq!(
2831 serde_json::from_str::<AvailableRange>(json)
2832 .unwrap()
2833 .bundle_start_slot,
2834 1
2835 );
2836 }
2837
2838 #[test]
2839 fn available_range_labels_round_trip() {
2840 let range = AvailableRange {
2841 bundle_start_slot: 100,
2842 max_bundle_end_slot: Some(200),
2843 kind: Some(BundleKind::AccountState),
2844 scope: Some(RangeScope::User),
2845 ..Default::default()
2846 };
2847 let json = serde_json::to_string(&range).unwrap();
2848 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2849 assert!(json.contains(r#""scope":"user""#), "{json}");
2850 let parsed: AvailableRange = serde_json::from_str(&json).unwrap();
2851 assert_eq!(parsed.kind, Some(BundleKind::AccountState));
2852 assert_eq!(parsed.scope, Some(RangeScope::User));
2853 }
2854
2855 fn range(start: u64, end: u64) -> AvailableRange {
2856 AvailableRange {
2857 bundle_start_slot: start,
2858 max_bundle_end_slot: Some(end),
2859 ..Default::default()
2860 }
2861 }
2862
2863 #[rstest::rstest]
2867 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
2868 #[case::multi(
2869 vec![range(100, 200), range(201, 300), range(301, 400)],
2870 100, 300, Some(vec![(100, 200), (201, 300)])
2871 )]
2872 #[case::nested(
2875 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
2876 100, 900, Some(vec![(100, 500), (501, 900)])
2877 )]
2878 #[case::prefers_finer_grid(
2882 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
2883 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
2884 )]
2885 #[case::shared_start_prefers_finer(
2889 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
2890 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
2891 )]
2892 #[case::coarse_overlap_prefers_finer_pair(
2896 vec![range(100, 200), range(100, 150), range(151, 160)],
2897 100, 160, Some(vec![(100, 150), (151, 160)])
2898 )]
2899 #[case::falls_back_to_coarse(
2903 vec![range(100, 160), range(100, 120), range(121, 140)],
2904 100, 160, Some(vec![(100, 160)])
2905 )]
2906 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
2908 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
2911 #[case::anchors_then_continues(
2912 vec![range(150, 350), range(351, 600)],
2913 200, 600, Some(vec![(150, 350), (351, 600)])
2914 )]
2915 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
2916 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
2918 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
2920 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
2921 #[case::user_and_global_ranges_not_collapsed(
2926 vec![range(100, 200), range(100, 150), range(151, 200)],
2927 100, 200, Some(vec![(100, 150), (151, 200)])
2928 )]
2929 fn split_range_cases(
2930 #[case] ranges: Vec<AvailableRange>,
2931 #[case] start: u64,
2932 #[case] end: u64,
2933 #[case] expected: Option<Vec<(u64, u64)>>,
2934 ) {
2935 match expected {
2936 Some(expected) => assert_eq!(
2937 split_range(
2938 &ranges,
2939 SlotRange::new_unchecked(start, end),
2940 BundleKind::Blocks
2941 )
2942 .unwrap(),
2943 expected
2944 ),
2945 None => assert!(
2946 split_range(
2947 &ranges,
2948 SlotRange::new_unchecked(start, end),
2949 BundleKind::Blocks
2950 )
2951 .is_err()
2952 ),
2953 }
2954 }
2955
2956 fn account_state_range(start: u64, end: u64) -> AvailableRange {
2957 AvailableRange {
2958 kind: Some(BundleKind::AccountState),
2959 ..range(start, end)
2960 }
2961 }
2962
2963 #[test]
2964 fn a_replay_split_rides_the_recordings_not_a_finer_block_grid() {
2965 let ranges = vec![
2968 range(100, 149),
2969 range(150, 199),
2970 account_state_range(100, 199),
2971 ];
2972 let requested = SlotRange::new_unchecked(100, 199);
2973
2974 assert_eq!(
2975 split_range(&ranges, requested, BundleKind::Blocks).unwrap(),
2976 [(100, 149), (150, 199)]
2977 );
2978 assert_eq!(
2979 split_range(&ranges, requested, BundleKind::AccountState).unwrap(),
2980 [(100, 199)]
2981 );
2982 }
2983
2984 #[test]
2985 fn a_replay_split_tiles_consecutive_recordings() {
2986 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
2987 assert_eq!(
2988 split_range(
2989 &ranges,
2990 SlotRange::new_unchecked(100, 299),
2991 BundleKind::AccountState
2992 )
2993 .unwrap(),
2994 [(100, 199), (200, 299)]
2995 );
2996 }
2997
2998 #[test]
2999 fn a_replay_split_ends_short_of_a_recording() {
3000 let requested = SlotRange::new_unchecked(100, 150);
3003 assert_eq!(
3004 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3005 [(100, 150)]
3006 );
3007 assert_eq!(
3008 split_range(
3009 &[account_state_range(100, 199)],
3010 requested,
3011 BundleKind::AccountState
3012 )
3013 .unwrap(),
3014 [(100, 150)]
3015 );
3016 }
3017
3018 #[test]
3019 fn a_replay_split_ends_short_of_its_last_recording() {
3020 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
3023 assert_eq!(
3024 split_range(
3025 &ranges,
3026 SlotRange::new_unchecked(100, 250),
3027 BundleKind::AccountState
3028 )
3029 .unwrap(),
3030 [(100, 199), (200, 250)]
3031 );
3032 }
3033
3034 #[test]
3035 fn a_replay_split_tiles_a_one_slot_recording() {
3036 let ranges = vec![account_state_range(100, 249), account_state_range(250, 250)];
3039 assert_eq!(
3040 split_range(
3041 &ranges,
3042 SlotRange::new_unchecked(100, 250),
3043 BundleKind::AccountState
3044 )
3045 .unwrap(),
3046 [(100, 249), (250, 250)]
3047 );
3048 }
3049
3050 #[test]
3051 fn a_replay_split_anchors_a_mid_recording_start() {
3052 let requested = SlotRange::new_unchecked(150, 199);
3056 assert_eq!(
3057 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3058 [(100, 199)]
3059 );
3060 assert_eq!(
3061 split_range(
3062 &[account_state_range(100, 199)],
3063 requested,
3064 BundleKind::AccountState
3065 )
3066 .unwrap(),
3067 [(100, 199)]
3068 );
3069 }
3070
3071 #[test]
3072 fn a_replay_split_refuses_a_start_no_recording_covers() {
3073 let error = split_range(
3074 &[account_state_range(200, 299)],
3075 SlotRange::new_unchecked(100, 299),
3076 BundleKind::AccountState,
3077 )
3078 .expect_err("no recording covers 100");
3079 assert!(
3080 error.contains("is not covered by any available account-state bundle"),
3081 "{error}"
3082 );
3083 }
3084
3085 #[test]
3086 fn a_replay_split_ignores_block_ranges() {
3087 let error = split_range(
3088 &[range(100, 199)],
3089 SlotRange::new_unchecked(100, 199),
3090 BundleKind::AccountState,
3091 )
3092 .expect_err("owns no recordings");
3093 assert!(error.contains("account-state bundle"), "{error}");
3094 }
3095
3096 #[test]
3097 fn blocks_mode_admits_ranges_from_a_server_that_predates_kind() {
3098 let unlabelled = range(100, 199);
3101 assert_eq!(unlabelled.kind, None);
3102 assert_eq!(
3103 split_range(
3104 &[unlabelled],
3105 SlotRange::new_unchecked(100, 150),
3106 BundleKind::Blocks
3107 )
3108 .unwrap(),
3109 [(100, 150)]
3110 );
3111 }
3112
3113 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
3116 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
3117 for r in ranges {
3118 if let Some(end) = r.max_bundle_end_slot
3119 && end >= r.bundle_start_slot
3120 {
3121 ends.entry(r.bundle_start_slot).or_default().insert(end);
3122 }
3123 }
3124 ends
3125 }
3126
3127 fn reference_max_split(
3131 ends: &BTreeMap<u64, BTreeSet<u64>>,
3132 cursor: u64,
3133 end: u64,
3134 ) -> Option<Vec<(u64, u64)>> {
3135 ends.get(&cursor)?
3136 .iter()
3137 .filter_map(|&bundle_end| {
3138 if bundle_end >= end {
3139 Some(vec![(cursor, end)])
3140 } else {
3141 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
3142 rest.insert(0, (cursor, bundle_end));
3143 rest
3144 })
3145 }
3146 })
3147 .max_by_key(Vec::len)
3148 }
3149
3150 fn is_valid_split(
3154 split: &[(u64, u64)],
3155 ends: &BTreeMap<u64, BTreeSet<u64>>,
3156 start: u64,
3157 end: u64,
3158 ) -> bool {
3159 split.first().is_some_and(|&(s, _)| s == start)
3160 && split.last().is_some_and(|&(_, e)| e == end)
3161 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
3162 && split.iter().all(|&(s, e)| {
3163 e >= s
3164 && ends
3165 .get(&s)
3166 .and_then(|bundle_ends| bundle_ends.iter().next_back())
3167 .is_some_and(|&max_end| e <= max_end)
3168 })
3169 }
3170
3171 #[test]
3176 fn split_range_matches_reference() {
3177 let mut seed: u64 = 0x9E3779B97F4A7C15;
3178 let mut next = || {
3179 seed = seed
3180 .wrapping_mul(6364136223846793005)
3181 .wrapping_add(1442695040888963407);
3182 seed >> 33
3183 };
3184
3185 for _ in 0..50_000 {
3186 let ranges: Vec<AvailableRange> = (0..next() % 6)
3189 .map(|_| {
3190 let start = next() % 12;
3191 range(start, start + next() % 6) })
3193 .collect();
3194 let start = next() % 12;
3195 let end = start + next() % 6; let got = split_range(
3198 &ranges,
3199 SlotRange::new_unchecked(start, end),
3200 BundleKind::Blocks,
3201 );
3202 let ends = ends_by_start(&ranges);
3203 let anchor = ends
3206 .range(..=start)
3207 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
3208 .map(|(&s, _)| s);
3209 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
3210
3211 let layout: Vec<_> = ranges
3212 .iter()
3213 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
3214 .collect();
3215 match (&got, &reference) {
3216 (Ok(split), Some(best)) => {
3217 assert!(
3218 is_valid_split(split, &ends, anchor.unwrap(), end),
3219 "invalid split {split:?} for {layout:?} [{start},{end}]"
3220 );
3221 assert_eq!(
3222 split.len(),
3223 best.len(),
3224 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
3225 );
3226 }
3227 (Err(_), None) => {}
3228 _ => panic!(
3229 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
3230 ),
3231 }
3232 }
3233 }
3234}