1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fmt,
4};
5
6use base64::{
7 DecodeError as Base64DecodeError, Engine as _, engine::general_purpose::STANDARD as BASE64,
8};
9use serde::{Deserialize, Serialize};
10use solana_address::Address;
11
12#[derive(Debug, Serialize, Deserialize)]
14#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
15#[serde(tag = "method", content = "params", rename_all = "camelCase")]
16#[cfg_attr(feature = "ts-rs", ts(export))]
17pub enum BacktestRequest {
18 CreateBacktestSession(CreateBacktestSessionRequest),
19 Continue(ContinueParams),
20 ContinueSessionV1(ContinueSessionRequestV1),
21 CloseBacktestSession,
22 CloseSessionV1(CloseSessionRequestV1),
23 AttachBacktestSession {
24 session_id: String,
25 last_sequence: Option<u64>,
28 },
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
36#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
37#[serde(untagged)]
38#[cfg_attr(feature = "ts-rs", ts(export))]
39pub enum CreateBacktestSessionRequest {
40 V1(CreateBacktestSessionRequestV1),
41 V0(CreateSessionParams),
42}
43
44impl CreateBacktestSessionRequest {
45 pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
46 match self {
47 Self::V0(request) => CreateBacktestSessionRequestOptions {
48 request,
49 parallel: false,
50 },
51 Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
52 CreateBacktestSessionRequestOptions { request, parallel }
53 }
54 }
55 }
56
57 pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
58 let options = self.into_request_options();
59 (options.request, options.parallel)
60 }
61}
62
63impl From<CreateSessionParams> for CreateBacktestSessionRequest {
64 fn from(value: CreateSessionParams) -> Self {
65 Self::V0(value)
66 }
67}
68
69impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
70 fn from(value: CreateBacktestSessionRequestV1) -> Self {
71 Self::V1(value)
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
77#[serde(rename_all = "camelCase")]
78#[cfg_attr(feature = "ts-rs", ts(export))]
79pub struct CreateBacktestSessionRequestV1 {
80 #[serde(flatten)]
81 pub request: CreateSessionParams,
82 pub parallel: bool,
83}
84
85#[derive(Debug, Clone)]
86pub struct CreateBacktestSessionRequestOptions {
87 pub request: CreateSessionParams,
88 pub parallel: bool,
89}
90
91#[derive(Debug, Serialize, Deserialize)]
92#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
93#[serde(rename_all = "camelCase")]
94#[cfg_attr(feature = "ts-rs", ts(export))]
95pub struct ContinueSessionRequestV1 {
96 pub session_id: String,
97 pub request: ContinueParams,
98}
99
100#[derive(Debug, Serialize, Deserialize)]
101#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
102#[serde(rename_all = "camelCase")]
103#[cfg_attr(feature = "ts-rs", ts(export))]
104pub struct CloseSessionRequestV1 {
105 pub session_id: String,
106}
107
108#[serde_with::serde_as]
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
112#[serde(rename_all = "camelCase")]
113pub struct CreateSessionParams {
114 pub start_slot: u64,
116 pub end_slot: u64,
118 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
119 #[serde(default)]
120 #[cfg_attr(feature = "ts-rs", ts(as = "Vec<String>"))]
121 pub signer_filter: BTreeSet<Address>,
123 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
124 #[serde(default)]
125 #[cfg_attr(feature = "ts-rs", ts(as = "Vec<String>"))]
126 pub preload_programs: BTreeSet<Address>,
128 #[serde(default)]
130 pub preload_account_bundles: Vec<String>,
131 #[serde(default)]
134 pub send_summary: bool,
135 #[serde(default)]
138 #[cfg_attr(feature = "ts-rs", ts(optional))]
139 pub capacity_wait_timeout_secs: Option<u16>,
140 #[serde(default)]
144 pub disconnect_timeout_secs: Option<u16>,
145 #[serde(default)]
150 pub extra_compute_units: Option<u32>,
151 #[serde(default)]
153 pub agents: Vec<AgentParams>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
159#[serde(rename_all = "camelCase")]
160#[cfg_attr(feature = "ts-rs", ts(export))]
161pub enum AgentType {
162 Arb,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
168#[serde(rename_all = "camelCase")]
169#[cfg_attr(feature = "ts-rs", ts(export))]
170pub struct ArbRouteParams {
171 pub base_mint: String,
172 pub temp_mint: String,
173 #[serde(default)]
174 pub buy_dexes: Vec<String>,
175 #[serde(default)]
176 pub sell_dexes: Vec<String>,
177 pub min_input: u64,
178 pub max_input: u64,
179 #[serde(default)]
180 pub min_profit: u64,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
186#[serde(rename_all = "camelCase")]
187#[cfg_attr(feature = "ts-rs", ts(export))]
188pub struct AgentParams {
189 pub agent_type: AgentType,
190 pub wallet: Option<String>,
191 pub seed_sol_lamports: Option<u64>,
192 #[serde(default)]
193 pub arb_routes: Vec<ArbRouteParams>,
194}
195
196#[serde_with::serde_as]
198#[derive(Debug, Serialize, Deserialize, Default)]
199#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
200pub struct AccountModifications(
201 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
202 #[serde(default)]
203 #[cfg_attr(feature = "ts-rs", ts(as = "BTreeMap<String, AccountData>"))]
204 pub BTreeMap<Address, AccountData>,
205);
206
207#[serde_with::serde_as]
209#[derive(Debug, Serialize, Deserialize)]
210#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
211#[serde(rename_all = "camelCase")]
212pub struct ContinueParams {
213 #[serde(default = "ContinueParams::default_advance_count")]
214 pub advance_count: u64,
216 #[serde(default)]
217 pub transactions: Vec<String>,
219 #[serde(default)]
220 pub modify_account_states: AccountModifications,
222}
223
224impl Default for ContinueParams {
225 fn default() -> Self {
226 Self {
227 advance_count: Self::default_advance_count(),
228 transactions: Vec::new(),
229 modify_account_states: AccountModifications(BTreeMap::new()),
230 }
231 }
232}
233
234impl ContinueParams {
235 pub fn default_advance_count() -> u64 {
236 1
237 }
238}
239
240#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
242#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
243#[serde(rename_all = "lowercase")]
244pub enum BinaryEncoding {
245 Base64,
246}
247
248impl BinaryEncoding {
249 pub fn encode(self, bytes: &[u8]) -> String {
250 match self {
251 Self::Base64 => BASE64.encode(bytes),
252 }
253 }
254
255 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
256 match self {
257 Self::Base64 => BASE64.decode(data),
258 }
259 }
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
265#[serde(rename_all = "camelCase")]
266pub struct EncodedBinary {
267 pub data: String,
269 pub encoding: BinaryEncoding,
271}
272
273impl EncodedBinary {
274 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
275 Self { data, encoding }
276 }
277
278 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
279 Self {
280 data: encoding.encode(bytes),
281 encoding,
282 }
283 }
284
285 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
286 self.encoding.decode(&self.data)
287 }
288}
289
290#[serde_with::serde_as]
292#[derive(Debug, Serialize, Deserialize)]
293#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
294#[serde(rename_all = "camelCase")]
295pub struct AccountData {
296 pub data: EncodedBinary,
298 pub executable: bool,
300 pub lamports: u64,
302 #[serde_as(as = "serde_with::DisplayFromStr")]
303 #[cfg_attr(feature = "ts-rs", ts(as = "String"))]
304 pub owner: Address,
306 pub space: u64,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
313#[serde(tag = "method", content = "params", rename_all = "camelCase")]
314#[cfg_attr(feature = "ts-rs", ts(export))]
315pub enum BacktestResponse {
316 SessionCreated {
317 session_id: String,
318 rpc_endpoint: String,
319 },
320 SessionAttached {
321 session_id: String,
322 rpc_endpoint: String,
323 },
324 SessionsCreated {
325 session_ids: Vec<String>,
326 },
327 ReadyForContinue,
328 SlotNotification(u64),
329 Error(BacktestError),
330 Success,
331 Completed {
332 #[serde(skip_serializing_if = "Option::is_none")]
336 summary: Option<SessionSummary>,
337 },
338 Status {
339 status: BacktestStatus,
340 },
341 SessionEventV1 {
342 session_id: String,
343 event: SessionEventV1,
344 },
345}
346
347impl BacktestResponse {
348 pub fn is_completed(&self) -> bool {
349 matches!(self, BacktestResponse::Completed { .. })
350 }
351
352 pub fn is_terminal(&self) -> bool {
353 match self {
354 BacktestResponse::Completed { .. } => true,
355 BacktestResponse::Error(e) => matches!(
356 e,
357 BacktestError::NoMoreBlocks
358 | BacktestError::AdvanceSlotFailed { .. }
359 | BacktestError::Internal { .. }
360 ),
361 _ => false,
362 }
363 }
364}
365
366impl From<BacktestStatus> for BacktestResponse {
367 fn from(status: BacktestStatus) -> Self {
368 Self::Status { status }
369 }
370}
371
372impl From<String> for BacktestResponse {
373 fn from(message: String) -> Self {
374 BacktestError::Internal { error: message }.into()
375 }
376}
377
378impl From<&str> for BacktestResponse {
379 fn from(message: &str) -> Self {
380 BacktestError::Internal {
381 error: message.to_string(),
382 }
383 .into()
384 }
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
388#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
389#[serde(tag = "method", content = "params", rename_all = "camelCase")]
390#[cfg_attr(feature = "ts-rs", ts(export))]
391pub enum SessionEventV1 {
392 ReadyForContinue,
393 SlotNotification(u64),
394 Error(BacktestError),
395 Success,
396 Completed {
397 #[serde(skip_serializing_if = "Option::is_none")]
398 summary: Option<SessionSummary>,
399 },
400 Status {
401 status: BacktestStatus,
402 },
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
408#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
409#[serde(rename_all = "camelCase")]
410#[cfg_attr(feature = "ts-rs", ts(export))]
411pub struct SequencedResponse {
412 pub seq_id: u64,
413 #[serde(flatten)]
414 pub response: BacktestResponse,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
420#[serde(rename_all = "camelCase")]
421pub enum BacktestStatus {
422 StartingRuntime,
424 DecodedTransactions,
425 AppliedAccountModifications,
426 ReadyToExecuteUserTransactions,
427 ExecutedUserTransactions,
428 ExecutingBlockTransactions,
429 ExecutedBlockTransactions,
430 ProgramAccountsLoaded,
431}
432
433#[derive(Debug, Clone, Default, Serialize, Deserialize)]
435#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
436#[serde(rename_all = "camelCase")]
437pub struct SessionSummary {
438 pub correct_simulation: usize,
441 pub incorrect_simulation: usize,
444 pub execution_errors: usize,
446 pub balance_diff: usize,
448 pub log_diff: usize,
450}
451
452impl SessionSummary {
453 pub fn has_deviations(&self) -> bool {
455 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
456 }
457
458 pub fn total_transactions(&self) -> usize {
460 self.correct_simulation
461 + self.incorrect_simulation
462 + self.execution_errors
463 + self.balance_diff
464 + self.log_diff
465 }
466}
467
468impl std::fmt::Display for SessionSummary {
469 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470 let total = self.total_transactions();
471 write!(
472 f,
473 "Session summary: {total} transactions\n\
474 \x20 - {} correct simulation\n\
475 \x20 - {} incorrect simulation\n\
476 \x20 - {} execution errors\n\
477 \x20 - {} balance diffs\n\
478 \x20 - {} log diffs",
479 self.correct_simulation,
480 self.incorrect_simulation,
481 self.execution_errors,
482 self.balance_diff,
483 self.log_diff,
484 )
485 }
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
490#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
491#[serde(rename_all = "camelCase")]
492pub enum BacktestError {
493 InvalidTransactionEncoding {
494 index: usize,
495 error: String,
496 },
497 InvalidTransactionFormat {
498 index: usize,
499 error: String,
500 },
501 InvalidAccountEncoding {
502 address: String,
503 encoding: BinaryEncoding,
504 error: String,
505 },
506 InvalidAccountOwner {
507 address: String,
508 error: String,
509 },
510 InvalidAccountPubkey {
511 address: String,
512 error: String,
513 },
514 NoMoreBlocks,
515 AdvanceSlotFailed {
516 slot: u64,
517 error: String,
518 },
519 InvalidRequest {
520 error: String,
521 },
522 Internal {
523 error: String,
524 },
525 InvalidBlockhashFormat {
526 slot: u64,
527 error: String,
528 },
529 InitializingSysvarsFailed {
530 slot: u64,
531 error: String,
532 },
533 ClerkError {
534 error: String,
535 },
536 SimulationError {
537 error: String,
538 },
539 SessionNotFound {
540 session_id: String,
541 },
542 SessionOwnerMismatch,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
547pub struct AvailableRange {
548 pub bundle_start_slot: u64,
549 pub bundle_start_slot_utc: Option<String>,
550 pub max_bundle_end_slot: Option<u64>,
551 pub max_bundle_end_slot_utc: Option<String>,
552}
553
554pub fn split_range(
560 ranges: &[AvailableRange],
561 requested_start: u64,
562 requested_end: u64,
563) -> Result<Vec<(u64, u64)>, String> {
564 if requested_end < requested_start {
565 return Err(format!(
566 "invalid range: start_slot {requested_start} > end_slot {requested_end}"
567 ));
568 }
569
570 let mut candidates: Vec<(u64, u64)> = ranges
571 .iter()
572 .filter_map(|r| Some((r.bundle_start_slot, r.max_bundle_end_slot?)))
573 .filter(|(start, end)| end >= start)
574 .collect();
575
576 candidates.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
577 candidates.dedup_by_key(|(start, _)| *start);
578
579 if candidates.is_empty() {
580 return Err("no available bundle ranges found on server".to_string());
581 }
582
583 let mut non_overlapping: Vec<(u64, u64)> = Vec::with_capacity(candidates.len());
584 for (i, (start, mut end)) in candidates.iter().copied().enumerate() {
585 if let Some((next_start, _)) = candidates.get(i + 1).copied()
586 && next_start <= end
587 {
588 end = next_start.saturating_sub(1);
589 }
590 if end >= start {
591 non_overlapping.push((start, end));
592 }
593 }
594
595 let anchor = non_overlapping
596 .iter()
597 .enumerate()
598 .rev()
599 .find(|(_, (s, e))| *s <= requested_start && *e >= requested_start)
600 .map(|(i, _)| i)
601 .ok_or_else(|| {
602 format!("start_slot {requested_start} is not covered by any available bundle range")
603 })?;
604
605 let mut result = Vec::new();
606 for (start, range_end) in non_overlapping.into_iter().skip(anchor) {
607 if start > requested_end {
608 break;
609 }
610 let end = range_end.min(requested_end);
611 if end >= start {
612 result.push((start, end));
613 }
614 if end == requested_end {
615 break;
616 }
617 }
618
619 if result.is_empty() {
620 return Err(format!(
621 "no available bundle ranges intersect requested range [{requested_start}-{requested_end}]"
622 ));
623 }
624
625 Ok(result)
626}
627
628impl From<BacktestError> for BacktestResponse {
629 fn from(error: BacktestError) -> Self {
630 Self::Error(error)
631 }
632}
633
634impl std::error::Error for BacktestError {}
635
636impl fmt::Display for BacktestError {
637 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
638 match self {
639 BacktestError::InvalidTransactionEncoding { index, error } => {
640 write!(f, "invalid transaction encoding at index {index}: {error}")
641 }
642 BacktestError::InvalidTransactionFormat { index, error } => {
643 write!(f, "invalid transaction format at index {index}: {error}")
644 }
645 BacktestError::InvalidAccountEncoding {
646 address,
647 encoding,
648 error,
649 } => write!(
650 f,
651 "invalid encoding for account {address} ({encoding:?}): {error}"
652 ),
653 BacktestError::InvalidAccountOwner { address, error } => {
654 write!(f, "invalid owner for account {address}: {error}")
655 }
656 BacktestError::InvalidAccountPubkey { address, error } => {
657 write!(f, "invalid account pubkey {address}: {error}")
658 }
659 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
660 BacktestError::AdvanceSlotFailed { slot, error } => {
661 write!(f, "failed to advance to slot {slot}: {error}")
662 }
663 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
664 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
665 BacktestError::InvalidBlockhashFormat { slot, error } => {
666 write!(f, "invalid blockhash at slot {slot}: {error}")
667 }
668 BacktestError::InitializingSysvarsFailed { slot, error } => {
669 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
670 }
671 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
672 BacktestError::SimulationError { error } => {
673 write!(f, "simulation error: {error}")
674 }
675 BacktestError::SessionNotFound { session_id } => {
676 write!(f, "session not found: {session_id}")
677 }
678 BacktestError::SessionOwnerMismatch => {
679 write!(f, "session owner mismatch")
680 }
681 }
682 }
683}