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 #[serde(default)]
1903 pub eta_secs: Option<u64>,
1904}
1905
1906#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1908pub struct CompletedBundle {
1909 pub start_slot: u64,
1910 pub end_slot: u64,
1911 pub kind: BundleKind,
1912 #[serde(default)]
1914 pub completed_at_unix_ms: Option<i64>,
1915 #[serde(default)]
1916 pub size_bytes: Option<i64>,
1917}
1918
1919#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1921pub struct BundleDeleteRequest {
1922 pub start_slot: u64,
1923 pub end_slot: u64,
1924 #[serde(default)]
1926 pub kind: BundleKind,
1927}
1928
1929#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1931pub struct BundleDeleteResponse {
1932 pub start_slot: u64,
1933 pub end_slot: u64,
1934 pub kind: BundleKind,
1935}
1936
1937#[derive(Debug, Clone, Serialize, Deserialize)]
1939pub struct BundleBuildListResponse {
1940 pub builds: Vec<BundleBuildStatusResponse>,
1943 #[serde(default)]
1948 pub bundles: Option<Vec<CompletedBundle>>,
1949}
1950
1951#[derive(Debug, Clone, Serialize, Deserialize)]
1954pub struct SnapshotSlotResponse {
1955 pub snapshot_slot: u64,
1956}
1957
1958pub fn split_range(
1981 ranges: &[AvailableRange],
1982 requested: SlotRange,
1983 kind: BundleKind,
1984) -> Result<Vec<(u64, u64)>, String> {
1985 let (requested_start, requested_end) = (requested.start(), requested.end());
1986
1987 let ends_by_start = ranges
1994 .iter()
1995 .filter(|r| r.kind.unwrap_or_default() == kind)
1996 .filter_map(|r| {
1997 r.max_bundle_end_slot
1998 .filter(|&end| end >= r.bundle_start_slot)
1999 .map(|end| (r.bundle_start_slot, end))
2000 })
2001 .fold(
2002 BTreeMap::<u64, BTreeSet<u64>>::new(),
2003 |mut ends_by_start, (start, end)| {
2004 ends_by_start.entry(start).or_default().insert(end);
2005 ends_by_start
2006 },
2007 );
2008
2009 let Some((&anchor_start, _)) = ends_by_start.range(..=requested_start).rfind(|(_, ends)| {
2017 ends.iter()
2018 .next_back()
2019 .is_some_and(|&end| end >= requested_start)
2020 }) else {
2021 return Err(format!(
2022 "start_slot {requested_start} is not covered by any available {}",
2023 kind.coverage_noun()
2024 ));
2025 };
2026
2027 let mut best_from: BTreeMap<u64, Vec<(u64, u64)>> = BTreeMap::new();
2033 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end).rev() {
2034 let mut best: Option<Vec<(u64, u64)>> = None;
2035 for &end in ends {
2036 let candidate = if end >= requested_end {
2037 Some(vec![(start, requested_end)])
2038 } else {
2039 best_from.get(&(end + 1)).map(|rest| {
2040 std::iter::once((start, end))
2041 .chain(rest.iter().copied())
2042 .collect()
2043 })
2044 };
2045 if let Some(candidate) = candidate
2046 && best.as_ref().is_none_or(|b| candidate.len() > b.len())
2047 {
2048 best = Some(candidate);
2049 }
2050 }
2051 if let Some(best) = best {
2052 best_from.insert(start, best);
2053 }
2054 }
2055
2056 best_from.remove(&anchor_start).ok_or_else(|| {
2057 let mut covered_to = anchor_start.saturating_sub(1);
2061 for (&start, ends) in ends_by_start.range(anchor_start..=requested_end) {
2062 if start > covered_to.saturating_add(1) {
2063 break;
2064 }
2065 if let Some(&end) = ends.iter().next_back() {
2066 covered_to = covered_to.max(end);
2067 }
2068 }
2069 if covered_to < requested_end {
2070 format!("gap in coverage at slot {}", covered_to + 1)
2071 } else {
2072 format!(
2073 "no gap-free split of [{requested_start}, {requested_end}] aligns with the available {}",
2074 kind.coverage_nouns()
2075 )
2076 }
2077 })
2078}
2079
2080impl From<BacktestError> for BacktestResponse {
2081 fn from(error: BacktestError) -> Self {
2082 Self::Error(error)
2083 }
2084}
2085
2086impl std::error::Error for BacktestError {}
2087
2088impl fmt::Display for BacktestError {
2089 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2090 match self {
2091 BacktestError::InvalidTransactionEncoding { index, error } => {
2092 write!(f, "invalid transaction encoding at index {index}: {error}")
2093 }
2094 BacktestError::InvalidTransactionFormat { index, error } => {
2095 write!(f, "invalid transaction format at index {index}: {error}")
2096 }
2097 BacktestError::InvalidAccountEncoding {
2098 address,
2099 encoding,
2100 error,
2101 } => write!(
2102 f,
2103 "invalid encoding for account {address} ({encoding:?}): {error}"
2104 ),
2105 BacktestError::InvalidAccountOwner { address, error } => {
2106 write!(f, "invalid owner for account {address}: {error}")
2107 }
2108 BacktestError::InvalidAccountPubkey { address, error } => {
2109 write!(f, "invalid account pubkey {address}: {error}")
2110 }
2111 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
2112 BacktestError::BundleNotFound {
2113 start_slot,
2114 end_slot,
2115 } => write!(
2116 f,
2117 "block bundle not found for start_slot={start_slot}, end_slot={end_slot}"
2118 ),
2119 BacktestError::AdvanceSlotFailed { slot, error } => {
2120 write!(f, "failed to advance to slot {slot}: {error}")
2121 }
2122 BacktestError::FinalizeSlotFailed { slot, error } => {
2123 write!(f, "failed to finalize slot {slot}: {error}")
2124 }
2125 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
2126 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
2127 BacktestError::InvalidBlockhashFormat { slot, error } => {
2128 write!(f, "invalid blockhash at slot {slot}: {error}")
2129 }
2130 BacktestError::InitializingSysvarsFailed { slot, error } => {
2131 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
2132 }
2133 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
2134 BacktestError::SimulationError { error } => {
2135 write!(f, "simulation error: {error}")
2136 }
2137 BacktestError::SessionNotFound { session_id } => {
2138 write!(f, "session not found: {session_id}")
2139 }
2140 BacktestError::SessionOwnerMismatch => {
2141 write!(f, "session owner mismatch")
2142 }
2143 BacktestError::SessionOwnershipBusy { reason } => {
2144 write!(f, "session ownership busy: {reason}")
2145 }
2146 BacktestError::NoProducedBlocksInRange {
2147 start_slot,
2148 end_slot,
2149 } => write!(
2150 f,
2151 "no produced blocks in requested range {start_slot}..={end_slot}; \
2152 slot(s) may be skipped on-chain"
2153 ),
2154 }
2155 }
2156}
2157
2158#[cfg(test)]
2159mod tests {
2160 use rstest::rstest;
2161 use solana_account::ReadableAccount;
2162
2163 use super::*;
2164
2165 fn address(byte: u8) -> Address {
2166 Address::from([byte; 32])
2167 }
2168
2169 #[rstest]
2170 #[case::one("jupiter", Some(&[SwapAggregator::Jupiter][..]))]
2171 #[case::several("jupiter,okx", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2172 #[case::spaced_and_uppercased(" Jupiter , OKX ", Some(&[SwapAggregator::Jupiter, SwapAggregator::Okx][..]))]
2173 #[case::repeats_collapse("okx,okx", Some(&[SwapAggregator::Okx][..]))]
2174 #[case::trailing_separator("okx,", Some(&[SwapAggregator::Okx][..]))]
2175 #[case::unknown("jupiter,bogus", None)]
2176 #[case::empty("", None)]
2177 #[case::separators_only(",,", None)]
2178 fn reroute_aggregators_parse_the_shapes_a_flag_carries(
2179 #[case] input: &str,
2180 #[case] expected: Option<&[SwapAggregator]>,
2181 ) {
2182 assert_eq!(
2183 input.parse::<RerouteAggregators>().ok(),
2184 expected.map(|aggregators| RerouteAggregators::new(aggregators.iter().copied()))
2185 );
2186 }
2187
2188 #[test]
2190 fn reroute_aggregators_round_trip_through_their_rendering() {
2191 let aggregators = RerouteAggregators::new([SwapAggregator::Dflow, SwapAggregator::Jupiter]);
2192
2193 assert_eq!(aggregators.to_string(), "jupiter,dflow");
2194 assert_eq!(
2195 aggregators
2196 .to_string()
2197 .parse::<RerouteAggregators>()
2198 .unwrap(),
2199 aggregators
2200 );
2201 assert_eq!(RerouteAggregators::default().to_string(), "jupiter");
2202 }
2203
2204 #[test]
2207 fn aggregators_deserialize_through_the_same_parser_the_flag_uses() {
2208 let params: CreateSessionParams = serde_json::from_value(
2209 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {"aggregators": ["Jupiter", "OKX"]}}),
2210 )
2211 .expect("decode");
2212
2213 assert_eq!(
2214 params.reroute.expect("rerouting requested").aggregators,
2215 RerouteAggregators::new([SwapAggregator::Jupiter, SwapAggregator::Okx])
2216 );
2217 }
2218
2219 #[test]
2222 fn params_without_the_new_reroute_fields_decode_to_the_narrow_defaults() {
2223 let params: CreateSessionParams = serde_json::from_value(
2224 serde_json::json!({"startSlot": 1, "endSlot": 2, "reroute": {}}),
2225 )
2226 .expect("decode");
2227
2228 let reroute = params.reroute.expect("rerouting requested");
2229 assert_eq!(reroute.aggregators, RerouteAggregators::default());
2230 assert!(reroute.aggregators.contains(SwapAggregator::Jupiter));
2231 assert!(!reroute.aggregators.contains(SwapAggregator::Okx));
2232 assert!(!reroute.circular_arbs);
2233 assert!(!reroute.detect_failed_l1_swaps);
2234 }
2235
2236 #[test]
2238 fn a_request_without_reroute_reserves_no_sidecar() {
2239 let params: CreateSessionParams =
2240 serde_json::from_value(serde_json::json!({"startSlot": 1, "endSlot": 2}))
2241 .expect("decode");
2242
2243 assert!(!params.requires_sidecar());
2244 }
2245
2246 #[test]
2251 fn a_per_slot_action_schedule_compresses_across_actions() {
2252 let slots = 2_000u64;
2253 let actions = (0..slots)
2254 .map(|slot| ScheduledAction {
2255 anchor: ActionAnchor::AfterSlot { slots: vec![slot] },
2256 kind: ActionKind::Simulate,
2257 feeds_reroute: false,
2258 transactions: Vec::new(),
2259 account_overrides: AccountModifications(BTreeMap::from([(
2260 address(7),
2261 AccountData {
2262 data: EncodedBinary::new(
2264 BASE64.encode(
2265 [&slot.to_le_bytes()[..], &[0xAB; 2040][..]]
2266 .concat()
2267 .as_slice(),
2268 ),
2269 BinaryEncoding::Base64,
2270 ),
2271 executable: false,
2272 lamports: 15_144_966,
2273 owner: address(9),
2274 space: 2048,
2275 },
2276 )])),
2277 return_accounts: Vec::new(),
2278 label: None,
2279 })
2280 .collect::<Vec<_>>();
2281 let params = CreateSessionParams::builder()
2282 .start_slot(1)
2283 .end_slot(slots)
2284 .actions(actions.clone())
2285 .build();
2286
2287 let encoded = serde_json::to_string(¶ms).expect("serialize");
2288 let raw = serde_json::to_string(&actions).expect("serialize raw actions");
2289 assert!(
2290 encoded.len() * 8 < raw.len(),
2291 "expected the schedule to compress well; got {} bytes from {} raw",
2292 encoded.len(),
2293 raw.len()
2294 );
2295
2296 let decoded: CreateSessionParams = serde_json::from_str(&encoded).expect("round trip");
2297 assert_eq!(
2298 serde_json::to_string(&decoded.actions).expect("serialize decoded"),
2299 raw
2300 );
2301 }
2302
2303 #[test]
2306 fn an_empty_action_list_is_omitted_entirely() {
2307 let params = CreateSessionParams::builder()
2308 .start_slot(1)
2309 .end_slot(2)
2310 .build();
2311 let json = serde_json::to_value(¶ms).expect("serialize");
2312 assert!(json.get("actions").is_none(), "{json}");
2313 let decoded: CreateSessionParams = serde_json::from_value(json).expect("round trip");
2314 assert!(decoded.actions.is_empty());
2315 }
2316
2317 #[test]
2320 fn send_actions_conflict_with_replay_only_together() {
2321 let params = |replay, kind| {
2322 CreateSessionParams::builder()
2323 .start_slot(1)
2324 .end_slot(2)
2325 .replay_account_state(replay)
2326 .actions(vec![ScheduledAction {
2327 anchor: ActionAnchor::BeforeSlot { slots: vec![1] },
2328 kind,
2329 feeds_reroute: false,
2330 transactions: vec!["dGVzdA==".to_string()],
2331 account_overrides: Default::default(),
2332 return_accounts: Default::default(),
2333 label: None,
2334 }])
2335 .build()
2336 };
2337 assert!(replay_support::send_action_conflict(¶ms(false, ActionKind::Send)).is_none());
2338 assert!(
2339 replay_support::send_action_conflict(¶ms(true, ActionKind::Simulate)).is_none()
2340 );
2341 let mut publishing = params(true, ActionKind::Simulate);
2342 publishing.actions[0].feeds_reroute = true;
2343 assert!(replay_support::send_action_conflict(&publishing).is_none());
2344
2345 let conflict = replay_support::send_action_conflict(¶ms(true, ActionKind::Send))
2346 .expect("send under replay conflicts");
2347 assert!(conflict.contains("kind: send"), "{conflict}");
2348 assert!(conflict.contains(replay_support::SEND_ACTION), "{conflict}");
2349 }
2350
2351 fn override_entry(owner: Address, executable: bool) -> AccountData {
2352 AccountData {
2353 data: EncodedBinary::from_bytes(&[1, 2, 3], BinaryEncoding::Base64),
2354 executable,
2355 lamports: 1,
2356 owner,
2357 space: 3,
2358 }
2359 }
2360
2361 fn before() -> ActionAnchor {
2362 ActionAnchor::BeforeSlot { slots: vec![1] }
2363 }
2364
2365 fn valid() -> ScheduledAction {
2366 override_action(before(), address(3), override_entry(address(4), false))
2367 }
2368
2369 fn override_action(
2370 anchor: ActionAnchor,
2371 account: Address,
2372 data: AccountData,
2373 ) -> ScheduledAction {
2374 ScheduledAction {
2375 anchor,
2376 kind: ActionKind::default(),
2377 transactions: Vec::new(),
2378 account_overrides: AccountModifications(BTreeMap::from([(account, data)])),
2379 return_accounts: Vec::new(),
2380 label: None,
2381 feeds_reroute: true,
2382 }
2383 }
2384
2385 #[test]
2386 fn override_action_round_trips() {
2387 let key = address(1);
2388 let entry = override_entry(address(2), false);
2389 let action = override_action(
2390 ActionAnchor::BeforeSlot { slots: vec![5, 9] },
2391 key,
2392 entry.clone(),
2393 );
2394 let json = serde_json::to_value(&action).unwrap();
2395 assert!(
2396 json["anchor"]["slots"].is_string(),
2397 "slots travel as a compressed blob: {json}"
2398 );
2399 let back: ScheduledAction = serde_json::from_value(json).unwrap();
2400 assert_eq!(back.anchor, ActionAnchor::BeforeSlot { slots: vec![5, 9] });
2401 assert!(back.feeds_reroute);
2402 assert!(!back.is_computed_override(), "no transactions to simulate");
2403 let survived = &back.account_overrides.0[&key];
2404 assert_eq!(survived.lamports, entry.lamports);
2405 assert_eq!(survived.data.data, entry.data.data);
2406 }
2407
2408 #[test]
2413 fn a_token_account_override_is_accepted() {
2414 let action = override_action(
2415 before(),
2416 address(3),
2417 override_entry(spl_token_interface::ID, false),
2418 );
2419 let schedule = decode_override_actions(&[action]).expect("token overrides decode");
2420 assert!(!schedule.active_at(1).is_empty());
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::after_slot_anchor(override_action(
2431 ActionAnchor::AfterSlot { slots: vec![1] },
2432 address(3),
2433 override_entry(address(4), false),
2434 ))]
2435 #[case::nothing_to_post(ScheduledAction {
2436 account_overrides: AccountModifications::default(),
2437 ..valid()
2438 })]
2439 #[case::send(ScheduledAction {
2441 kind: ActionKind::Send,
2442 ..valid()
2443 })]
2444 #[allow(deprecated, reason = "exercise the loader-owner rejection path")]
2445 fn override_actions_reject_invalid_accounts_and_shapes(#[case] action: ScheduledAction) {
2446 assert!(decode_override_actions(&[action]).is_err());
2447 }
2448
2449 #[test]
2451 fn override_actions_accept_a_static_and_a_computed_shape() {
2452 assert!(decode_override_actions(&[valid()]).is_ok());
2453 assert!(
2456 decode_override_actions(&[ScheduledAction {
2457 transactions: vec!["dGVzdA==".to_string()],
2458 ..valid()
2459 }])
2460 .is_ok()
2461 );
2462 }
2463
2464 #[test]
2465 fn override_actions_merge_account_wise_later_wins() {
2466 let entry = |lamports| AccountData {
2467 lamports,
2468 ..override_entry(address(9), false)
2469 };
2470 let slot_action = |slots, account, lamports| {
2471 override_action(ActionAnchor::BeforeSlot { slots }, account, entry(lamports))
2472 };
2473 let actions = [
2474 slot_action(vec![5, 7], address(1), 1),
2475 slot_action(vec![5], address(2), 2),
2476 slot_action(vec![5], address(1), 3),
2477 ScheduledAction {
2478 anchor: ActionAnchor::AfterSlot { slots: vec![5] },
2479 kind: ActionKind::Simulate,
2480 feeds_reroute: false,
2481 transactions: vec!["dGVzdA==".to_string()],
2482 account_overrides: AccountModifications::default(),
2483 return_accounts: Vec::new(),
2484 label: None,
2485 },
2486 ];
2487 let schedule = decode_override_actions(&actions).unwrap();
2488 assert_eq!(schedule.slots().collect::<Vec<_>>(), vec![5, 7]);
2489 let lamports_at = |key, slot| {
2490 schedule
2491 .active_for(&key, slot)
2492 .map(|account| account.lamports())
2493 };
2494 assert_eq!(lamports_at(address(1), 5), Some(3));
2495 assert_eq!(lamports_at(address(2), 5), Some(2));
2496 assert_eq!(lamports_at(address(1), 7), Some(1));
2497 }
2498
2499 #[test]
2500 fn computed_override_resolution_preserves_action_order_and_failure() {
2501 let key = address(1);
2502 let static_action = |lamports| {
2503 override_action(
2504 ActionAnchor::BeforeSlot { slots: vec![5] },
2505 key,
2506 AccountData {
2507 lamports,
2508 ..override_entry(address(9), false)
2509 },
2510 )
2511 };
2512 let computed_action = || ScheduledAction {
2513 transactions: vec!["dGVzdA==".to_string()],
2514 ..static_action(2)
2515 };
2516
2517 let computed_last =
2518 decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2519 computed_last
2520 .resolve(5, 1, Some(HashMap::from([(key, shared_account(2))])))
2521 .unwrap();
2522 assert_eq!(
2523 computed_last.active_for(&key, 5).map(|a| a.lamports()),
2524 Some(2)
2525 );
2526
2527 let static_last = decode_override_actions(&[computed_action(), static_action(3)]).unwrap();
2528 static_last
2529 .resolve(5, 0, Some(HashMap::from([(key, shared_account(2))])))
2530 .unwrap();
2531 assert_eq!(
2532 static_last.active_for(&key, 5).map(|a| a.lamports()),
2533 Some(3)
2534 );
2535
2536 let failed = decode_override_actions(&[static_action(1), computed_action()]).unwrap();
2537 failed.resolve(5, 1, None).unwrap();
2538 assert_eq!(failed.active_for(&key, 5).map(|a| a.lamports()), Some(1));
2539 assert!(failed.resolve(5, 1, None).is_err(), "resolving twice");
2540 }
2541
2542 fn shared_account(lamports: u64) -> AccountSharedData {
2543 AccountSharedData::from(solana_account::Account {
2544 lamports,
2545 data: vec![1],
2546 owner: address(9),
2547 executable: false,
2548 rent_epoch: 0,
2549 })
2550 }
2551
2552 fn carry_forward_schedule() -> (Address, Address, OverrideSchedule) {
2553 let (a, b) = (address(1), address(2));
2554 let schedule = OverrideSchedule::from(BTreeMap::from([
2555 (10, HashMap::from([(a, shared_account(1))])),
2556 (
2557 20,
2558 HashMap::from([(a, shared_account(2)), (b, shared_account(9))]),
2559 ),
2560 ]));
2561 (a, b, schedule)
2562 }
2563
2564 #[test]
2565 fn override_schedule_resolves_carry_forward_per_account() {
2566 let (a, b, schedule) = carry_forward_schedule();
2567
2568 assert!(schedule.active_for(&a, 9).is_none());
2569 let cases = [
2570 (9, None, None),
2571 (10, Some(1), None),
2572 (15, Some(1), None),
2573 (20, Some(2), Some(9)),
2574 (u64::MAX, Some(2), Some(9)),
2575 ];
2576 for (slot, want_a, want_b) in cases {
2577 assert_eq!(
2578 schedule
2579 .active_for(&a, slot)
2580 .map(|account| account.lamports()),
2581 want_a,
2582 "active_for a at {slot}"
2583 );
2584 let active = schedule.active_at(slot);
2585 assert_eq!(
2586 active.get(&a).map(ReadableAccount::lamports),
2587 want_a,
2588 "active_at a at {slot}"
2589 );
2590 assert_eq!(
2591 active.get(&b).map(ReadableAccount::lamports),
2592 want_b,
2593 "active_at b at {slot}"
2594 );
2595 }
2596 }
2597
2598 #[test]
2599 fn newly_active_yields_final_values_in_window() {
2600 let (a, b, schedule) = carry_forward_schedule();
2601
2602 let newly: HashMap<_, _> = schedule.newly_active(Some(10), 20).into_iter().collect();
2603 assert_eq!(newly.len(), 2);
2604 assert_eq!(newly[&a].lamports(), 2);
2605 assert_eq!(newly[&b].lamports(), 9);
2606 let spanning: HashMap<_, _> = schedule.newly_active(None, 20).into_iter().collect();
2607 assert_eq!(spanning[&a].lamports(), 2, "later slot wins");
2608 assert_eq!(schedule.newly_active(None, 15).len(), 1);
2609 }
2610
2611 #[test]
2612 fn reroute_filter_round_trips_camel_case() {
2613 let filter = RerouteFilter {
2614 pairs: BTreeSet::from([MintPair::new(address(1), address(2))]),
2615 };
2616 let json = serde_json::to_value(&filter).unwrap();
2617 assert!(
2618 json["pairs"][0][0].is_string() && json["pairs"][0][1].is_string(),
2619 "a pair serializes as two base58 strings: {json}"
2620 );
2621 let back: RerouteFilter = serde_json::from_value(json).unwrap();
2622 assert_eq!(back, filter);
2623 }
2624
2625 #[test]
2628 fn mint_pairs_are_normalized_however_they_are_built() {
2629 let (one, other) = (address(1), address(2));
2630 assert_eq!(MintPair::new(one, other), MintPair::new(other, one));
2631
2632 let reversed: MintPair =
2633 serde_json::from_value(serde_json::json!([other.to_string(), one.to_string()]))
2634 .unwrap();
2635 assert_eq!(reversed, MintPair::new(one, other));
2636 }
2637
2638 #[test]
2639 fn fail_fast_divergence_kind_str_round_trips() {
2640 for kind in [
2641 FailFastDivergenceKind::AnyNonBenign,
2642 FailFastDivergenceKind::Tracked,
2643 ] {
2644 assert_eq!(
2645 FailFastDivergenceKind::from_str_opt(kind.as_str()),
2646 Some(kind)
2647 );
2648 }
2649 assert_eq!(FailFastDivergenceKind::from_str_opt("nonsense"), None);
2650 assert_eq!(
2651 FailFastDivergenceKind::default(),
2652 FailFastDivergenceKind::AnyNonBenign
2653 );
2654 }
2655
2656 #[test]
2657 fn bundle_not_found_is_terminal_and_descriptive() {
2658 let error = BacktestError::BundleNotFound {
2659 start_slot: 100,
2660 end_slot: 600,
2661 };
2662
2663 assert!(error.is_terminal());
2664 assert_eq!(
2665 error.to_string(),
2666 "block bundle not found for start_slot=100, end_slot=600"
2667 );
2668 }
2669
2670 #[test]
2671 fn no_produced_blocks_is_terminal_and_descriptive() {
2672 let error = BacktestError::NoProducedBlocksInRange {
2673 start_slot: 100,
2674 end_slot: 100,
2675 };
2676
2677 assert!(error.is_terminal());
2678 assert_eq!(
2681 error.to_string(),
2682 "no produced blocks in requested range 100..=100; slot(s) may be skipped on-chain"
2683 );
2684 }
2685
2686 #[test]
2687 fn bundle_build_request_optional_fields_default_to_none() {
2688 let req: BundleBuildRequest =
2689 serde_json::from_str(r#"{"start_slot":1,"end_slot":2}"#).expect("parse");
2690 assert_eq!((req.start_slot, req.end_slot), (1, 2));
2691 assert_eq!(req.bundle_size, None);
2692 assert_eq!(req.idempotency_key, None);
2693 }
2694
2695 #[test]
2696 fn bundle_build_request_parses_optional_fields() {
2697 let req: BundleBuildRequest = serde_json::from_str(
2698 r#"{"start_slot":1,"end_slot":2,"bundle_size":500,"idempotency_key":"abc"}"#,
2699 )
2700 .expect("parse");
2701 assert_eq!(req.bundle_size, Some(500));
2702 assert_eq!(req.idempotency_key.as_deref(), Some("abc"));
2703 }
2704
2705 #[test]
2706 fn bundle_build_status_serde_round_trips_with_snake_case() {
2707 let cases = [
2708 (BundleBuildStatus::Completed, "\"completed\""),
2709 (BundleBuildStatus::Failed, "\"failed\""),
2710 (BundleBuildStatus::InProgress, "\"in_progress\""),
2711 (
2712 BundleBuildStatus::NeedsInvestigation,
2713 "\"needs_investigation\"",
2714 ),
2715 ];
2716 for (status, expected) in cases {
2717 assert_eq!(serde_json::to_string(&status).unwrap(), expected);
2718 assert_eq!(
2719 serde_json::from_str::<BundleBuildStatus>(expected).unwrap(),
2720 status
2721 );
2722 assert_eq!(status.as_str(), expected.trim_matches('"'));
2723 }
2724 assert!(serde_json::from_str::<BundleBuildStatus>("\"queued\"").is_err());
2725 }
2726
2727 #[test]
2728 fn bundle_build_request_without_kind_deserializes_as_blocks() {
2729 let request: BundleBuildRequest =
2731 serde_json::from_str(r#"{"start_slot":100,"end_slot":200}"#).unwrap();
2732 assert_eq!(request.kind, BundleKind::Blocks);
2733 }
2734
2735 #[test]
2736 fn bundle_kind_round_trips_over_serde_and_as_str() {
2737 for kind in [BundleKind::Blocks, BundleKind::AccountState] {
2738 let json = serde_json::to_string(&kind).unwrap();
2739 assert_eq!(json, format!("\"{}\"", kind.as_str()));
2740 assert_eq!(serde_json::from_str::<BundleKind>(&json).unwrap(), kind);
2741 assert_eq!(BundleKind::from_str_opt(kind.as_str()), Some(kind));
2744 }
2745 assert!(serde_json::from_str::<BundleKind>("\"accountState\"").is_err());
2746 assert_eq!(BundleKind::from_str_opt("accountState"), None);
2747 }
2748
2749 #[test]
2750 fn bundle_build_request_carries_kind_through_serde() {
2751 let request = BundleBuildRequest {
2752 start_slot: 100,
2753 end_slot: 200,
2754 kind: BundleKind::AccountState,
2755 ..Default::default()
2756 };
2757 let json = serde_json::to_string(&request).unwrap();
2758 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2759 let parsed: BundleBuildRequest = serde_json::from_str(&json).unwrap();
2760 assert_eq!(parsed.kind, BundleKind::AccountState);
2761 }
2762
2763 #[test]
2764 fn bundle_build_status_response_serializes_request_and_status() {
2765 let response = BundleBuildStatusResponse {
2766 request_id: "r".to_string(),
2767 start_slot: 100,
2768 end_slot: 200,
2769 bundle_size: Some(50),
2770 status: BundleBuildStatus::InProgress,
2771 created_at_unix_ms: 0,
2772 kind: BundleKind::Blocks,
2773 eta_secs: Some(600),
2774 };
2775 let json = serde_json::to_value(&response).unwrap();
2776 assert_eq!(json["request_id"].as_str(), Some("r"));
2777 assert_eq!(json["start_slot"].as_u64(), Some(100));
2778 assert_eq!(json["bundle_size"].as_u64(), Some(50));
2779 assert_eq!(json["status"].as_str(), Some("in_progress"));
2780 assert_eq!(json["created_at_unix_ms"].as_u64(), Some(0));
2781 assert_eq!(json["eta_secs"].as_u64(), Some(600));
2782 }
2783
2784 #[test]
2785 fn an_empty_bundles_list_is_distinct_from_an_absent_one() {
2786 let absent: BundleBuildListResponse = serde_json::from_str(r#"{"builds":[]}"#).unwrap();
2788 let empty: BundleBuildListResponse =
2789 serde_json::from_str(r#"{"builds":[],"bundles":[]}"#).unwrap();
2790 assert_eq!(absent.bundles, None);
2791 assert_eq!(empty.bundles, Some(Vec::new()));
2792 }
2793
2794 #[test]
2795 fn completed_bundle_round_trips() {
2796 let bundle = CompletedBundle {
2797 start_slot: 100,
2798 end_slot: 200,
2799 kind: BundleKind::AccountState,
2800 completed_at_unix_ms: Some(1_700_000_000_000),
2801 size_bytes: Some(4_096),
2802 };
2803 let json = serde_json::to_string(&bundle).unwrap();
2804 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2805 assert_eq!(
2806 serde_json::from_str::<CompletedBundle>(&json).unwrap(),
2807 bundle
2808 );
2809
2810 let sparse = r#"{"start_slot":1,"end_slot":2,"kind":"blocks"}"#;
2811 let parsed: CompletedBundle = serde_json::from_str(sparse).unwrap();
2812 assert_eq!(parsed.completed_at_unix_ms, None);
2813 assert_eq!(parsed.size_bytes, None);
2814 }
2815
2816 #[test]
2817 fn available_range_reads_a_server_that_predates_kind_and_scope() {
2818 let json = r#"{
2820 "bundle_start_slot": 100,
2821 "bundle_start_slot_utc": null,
2822 "max_bundle_end_slot": 200,
2823 "max_bundle_end_slot_utc": null,
2824 "max_bundle_size": 101
2825 }"#;
2826 let parsed: AvailableRange = serde_json::from_str(json).unwrap();
2827 assert_eq!(parsed.bundle_start_slot, 100);
2828 assert_eq!(parsed.kind, None);
2829 assert_eq!(parsed.scope, None);
2830 }
2831
2832 #[test]
2833 fn available_range_ignores_fields_it_does_not_know() {
2834 let json = r#"{"bundle_start_slot": 1, "some_future_field": "x"}"#;
2836 assert_eq!(
2837 serde_json::from_str::<AvailableRange>(json)
2838 .unwrap()
2839 .bundle_start_slot,
2840 1
2841 );
2842 }
2843
2844 #[test]
2845 fn available_range_labels_round_trip() {
2846 let range = AvailableRange {
2847 bundle_start_slot: 100,
2848 max_bundle_end_slot: Some(200),
2849 kind: Some(BundleKind::AccountState),
2850 scope: Some(RangeScope::User),
2851 ..Default::default()
2852 };
2853 let json = serde_json::to_string(&range).unwrap();
2854 assert!(json.contains(r#""kind":"account_state""#), "{json}");
2855 assert!(json.contains(r#""scope":"user""#), "{json}");
2856 let parsed: AvailableRange = serde_json::from_str(&json).unwrap();
2857 assert_eq!(parsed.kind, Some(BundleKind::AccountState));
2858 assert_eq!(parsed.scope, Some(RangeScope::User));
2859 }
2860
2861 fn range(start: u64, end: u64) -> AvailableRange {
2862 AvailableRange {
2863 bundle_start_slot: start,
2864 max_bundle_end_slot: Some(end),
2865 ..Default::default()
2866 }
2867 }
2868
2869 #[rstest::rstest]
2873 #[case::single(vec![range(100, 300)], 100, 300, Some(vec![(100, 300)]))]
2874 #[case::multi(
2875 vec![range(100, 200), range(201, 300), range(301, 400)],
2876 100, 300, Some(vec![(100, 200), (201, 300)])
2877 )]
2878 #[case::nested(
2881 vec![range(100, 500), range(110, 150), range(150, 190), range(501, 900)],
2882 100, 900, Some(vec![(100, 500), (501, 900)])
2883 )]
2884 #[case::prefers_finer_grid(
2888 vec![range(1_000, 1_999), range(1_500, 3_400), range(2_000, 2_999), range(3_000, 3_999)],
2889 1_000, 3_999, Some(vec![(1_000, 1_999), (2_000, 2_999), (3_000, 3_999)])
2890 )]
2891 #[case::shared_start_prefers_finer(
2895 vec![range(100, 150), range(100, 120), range(121, 140), range(141, 160)],
2896 100, 160, Some(vec![(100, 120), (121, 140), (141, 160)])
2897 )]
2898 #[case::coarse_overlap_prefers_finer_pair(
2902 vec![range(100, 200), range(100, 150), range(151, 160)],
2903 100, 160, Some(vec![(100, 150), (151, 160)])
2904 )]
2905 #[case::falls_back_to_coarse(
2909 vec![range(100, 160), range(100, 120), range(121, 140)],
2910 100, 160, Some(vec![(100, 160)])
2911 )]
2912 #[case::clamps_final_bundle(vec![range(100, 199), range(200, 999)], 100, 450, Some(vec![(100, 199), (200, 450)]))]
2914 #[case::anchors_mid_bundle(vec![range(150, 350)], 200, 300, Some(vec![(150, 300)]))]
2917 #[case::anchors_then_continues(
2918 vec![range(150, 350), range(351, 600)],
2919 200, 600, Some(vec![(150, 350), (351, 600)])
2920 )]
2921 #[case::start_inside_bundle_anchors(vec![range(200, 400)], 300, 400, Some(vec![(200, 400)]))]
2922 #[case::start_before_first_bundle(vec![range(200, 400)], 100, 400, None)]
2924 #[case::end_not_covered(vec![range(100, 200)], 100, 300, None)]
2926 #[case::gap_in_coverage(vec![range(100, 200), range(210, 300)], 100, 300, None)]
2927 #[case::user_and_global_ranges_not_collapsed(
2932 vec![range(100, 200), range(100, 150), range(151, 200)],
2933 100, 200, Some(vec![(100, 150), (151, 200)])
2934 )]
2935 fn split_range_cases(
2936 #[case] ranges: Vec<AvailableRange>,
2937 #[case] start: u64,
2938 #[case] end: u64,
2939 #[case] expected: Option<Vec<(u64, u64)>>,
2940 ) {
2941 match expected {
2942 Some(expected) => assert_eq!(
2943 split_range(
2944 &ranges,
2945 SlotRange::new_unchecked(start, end),
2946 BundleKind::Blocks
2947 )
2948 .unwrap(),
2949 expected
2950 ),
2951 None => assert!(
2952 split_range(
2953 &ranges,
2954 SlotRange::new_unchecked(start, end),
2955 BundleKind::Blocks
2956 )
2957 .is_err()
2958 ),
2959 }
2960 }
2961
2962 fn account_state_range(start: u64, end: u64) -> AvailableRange {
2963 AvailableRange {
2964 kind: Some(BundleKind::AccountState),
2965 ..range(start, end)
2966 }
2967 }
2968
2969 #[test]
2970 fn a_replay_split_rides_the_recordings_not_a_finer_block_grid() {
2971 let ranges = vec![
2974 range(100, 149),
2975 range(150, 199),
2976 account_state_range(100, 199),
2977 ];
2978 let requested = SlotRange::new_unchecked(100, 199);
2979
2980 assert_eq!(
2981 split_range(&ranges, requested, BundleKind::Blocks).unwrap(),
2982 [(100, 149), (150, 199)]
2983 );
2984 assert_eq!(
2985 split_range(&ranges, requested, BundleKind::AccountState).unwrap(),
2986 [(100, 199)]
2987 );
2988 }
2989
2990 #[test]
2991 fn a_replay_split_tiles_consecutive_recordings() {
2992 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
2993 assert_eq!(
2994 split_range(
2995 &ranges,
2996 SlotRange::new_unchecked(100, 299),
2997 BundleKind::AccountState
2998 )
2999 .unwrap(),
3000 [(100, 199), (200, 299)]
3001 );
3002 }
3003
3004 #[test]
3005 fn a_replay_split_ends_short_of_a_recording() {
3006 let requested = SlotRange::new_unchecked(100, 150);
3009 assert_eq!(
3010 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3011 [(100, 150)]
3012 );
3013 assert_eq!(
3014 split_range(
3015 &[account_state_range(100, 199)],
3016 requested,
3017 BundleKind::AccountState
3018 )
3019 .unwrap(),
3020 [(100, 150)]
3021 );
3022 }
3023
3024 #[test]
3025 fn a_replay_split_ends_short_of_its_last_recording() {
3026 let ranges = vec![account_state_range(100, 199), account_state_range(200, 299)];
3029 assert_eq!(
3030 split_range(
3031 &ranges,
3032 SlotRange::new_unchecked(100, 250),
3033 BundleKind::AccountState
3034 )
3035 .unwrap(),
3036 [(100, 199), (200, 250)]
3037 );
3038 }
3039
3040 #[test]
3041 fn a_replay_split_tiles_a_one_slot_recording() {
3042 let ranges = vec![account_state_range(100, 249), account_state_range(250, 250)];
3045 assert_eq!(
3046 split_range(
3047 &ranges,
3048 SlotRange::new_unchecked(100, 250),
3049 BundleKind::AccountState
3050 )
3051 .unwrap(),
3052 [(100, 249), (250, 250)]
3053 );
3054 }
3055
3056 #[test]
3057 fn a_replay_split_anchors_a_mid_recording_start() {
3058 let requested = SlotRange::new_unchecked(150, 199);
3062 assert_eq!(
3063 split_range(&[range(100, 199)], requested, BundleKind::Blocks).unwrap(),
3064 [(100, 199)]
3065 );
3066 assert_eq!(
3067 split_range(
3068 &[account_state_range(100, 199)],
3069 requested,
3070 BundleKind::AccountState
3071 )
3072 .unwrap(),
3073 [(100, 199)]
3074 );
3075 }
3076
3077 #[test]
3078 fn a_replay_split_refuses_a_start_no_recording_covers() {
3079 let error = split_range(
3080 &[account_state_range(200, 299)],
3081 SlotRange::new_unchecked(100, 299),
3082 BundleKind::AccountState,
3083 )
3084 .expect_err("no recording covers 100");
3085 assert!(
3086 error.contains("is not covered by any available account-state bundle"),
3087 "{error}"
3088 );
3089 }
3090
3091 #[test]
3092 fn a_replay_split_ignores_block_ranges() {
3093 let error = split_range(
3094 &[range(100, 199)],
3095 SlotRange::new_unchecked(100, 199),
3096 BundleKind::AccountState,
3097 )
3098 .expect_err("owns no recordings");
3099 assert!(error.contains("account-state bundle"), "{error}");
3100 }
3101
3102 #[test]
3103 fn blocks_mode_admits_ranges_from_a_server_that_predates_kind() {
3104 let unlabelled = range(100, 199);
3107 assert_eq!(unlabelled.kind, None);
3108 assert_eq!(
3109 split_range(
3110 &[unlabelled],
3111 SlotRange::new_unchecked(100, 150),
3112 BundleKind::Blocks
3113 )
3114 .unwrap(),
3115 [(100, 150)]
3116 );
3117 }
3118
3119 fn ends_by_start(ranges: &[AvailableRange]) -> BTreeMap<u64, BTreeSet<u64>> {
3122 let mut ends: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
3123 for r in ranges {
3124 if let Some(end) = r.max_bundle_end_slot
3125 && end >= r.bundle_start_slot
3126 {
3127 ends.entry(r.bundle_start_slot).or_default().insert(end);
3128 }
3129 }
3130 ends
3131 }
3132
3133 fn reference_max_split(
3137 ends: &BTreeMap<u64, BTreeSet<u64>>,
3138 cursor: u64,
3139 end: u64,
3140 ) -> Option<Vec<(u64, u64)>> {
3141 ends.get(&cursor)?
3142 .iter()
3143 .filter_map(|&bundle_end| {
3144 if bundle_end >= end {
3145 Some(vec![(cursor, end)])
3146 } else {
3147 reference_max_split(ends, bundle_end + 1, end).map(|mut rest| {
3148 rest.insert(0, (cursor, bundle_end));
3149 rest
3150 })
3151 }
3152 })
3153 .max_by_key(Vec::len)
3154 }
3155
3156 fn is_valid_split(
3160 split: &[(u64, u64)],
3161 ends: &BTreeMap<u64, BTreeSet<u64>>,
3162 start: u64,
3163 end: u64,
3164 ) -> bool {
3165 split.first().is_some_and(|&(s, _)| s == start)
3166 && split.last().is_some_and(|&(_, e)| e == end)
3167 && split.windows(2).all(|w| w[1].0 == w[0].1 + 1)
3168 && split.iter().all(|&(s, e)| {
3169 e >= s
3170 && ends
3171 .get(&s)
3172 .and_then(|bundle_ends| bundle_ends.iter().next_back())
3173 .is_some_and(|&max_end| e <= max_end)
3174 })
3175 }
3176
3177 #[test]
3182 fn split_range_matches_reference() {
3183 let mut seed: u64 = 0x9E3779B97F4A7C15;
3184 let mut next = || {
3185 seed = seed
3186 .wrapping_mul(6364136223846793005)
3187 .wrapping_add(1442695040888963407);
3188 seed >> 33
3189 };
3190
3191 for _ in 0..50_000 {
3192 let ranges: Vec<AvailableRange> = (0..next() % 6)
3195 .map(|_| {
3196 let start = next() % 12;
3197 range(start, start + next() % 6) })
3199 .collect();
3200 let start = next() % 12;
3201 let end = start + next() % 6; let got = split_range(
3204 &ranges,
3205 SlotRange::new_unchecked(start, end),
3206 BundleKind::Blocks,
3207 );
3208 let ends = ends_by_start(&ranges);
3209 let anchor = ends
3212 .range(..=start)
3213 .rfind(|(_, e)| e.iter().next_back().is_some_and(|&x| x >= start))
3214 .map(|(&s, _)| s);
3215 let reference = anchor.and_then(|a| reference_max_split(&ends, a, end));
3216
3217 let layout: Vec<_> = ranges
3218 .iter()
3219 .map(|r| (r.bundle_start_slot, r.max_bundle_end_slot))
3220 .collect();
3221 match (&got, &reference) {
3222 (Ok(split), Some(best)) => {
3223 assert!(
3224 is_valid_split(split, &ends, anchor.unwrap(), end),
3225 "invalid split {split:?} for {layout:?} [{start},{end}]"
3226 );
3227 assert_eq!(
3228 split.len(),
3229 best.len(),
3230 "suboptimal split {split:?} vs {best:?} for {layout:?} [{start},{end}]"
3231 );
3232 }
3233 (Err(_), None) => {}
3234 _ => panic!(
3235 "disagreement: split_range={got:?}, reference={reference:?} for {layout:?} [{start},{end}]"
3236 ),
3237 }
3238 }
3239 }
3240}