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)]
139 pub disconnect_timeout_secs: Option<u16>,
140 #[serde(default)]
145 pub extra_compute_units: Option<u32>,
146}
147
148#[serde_with::serde_as]
150#[derive(Debug, Serialize, Deserialize, Default)]
151#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
152pub struct AccountModifications(
153 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
154 #[serde(default)]
155 #[cfg_attr(feature = "ts-rs", ts(as = "BTreeMap<String, AccountData>"))]
156 pub BTreeMap<Address, AccountData>,
157);
158
159#[serde_with::serde_as]
161#[derive(Debug, Serialize, Deserialize)]
162#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
163#[serde(rename_all = "camelCase")]
164pub struct ContinueParams {
165 #[serde(default = "ContinueParams::default_advance_count")]
166 pub advance_count: u64,
168 #[serde(default)]
169 pub transactions: Vec<String>,
171 #[serde(default)]
172 pub modify_account_states: AccountModifications,
174}
175
176impl Default for ContinueParams {
177 fn default() -> Self {
178 Self {
179 advance_count: Self::default_advance_count(),
180 transactions: Vec::new(),
181 modify_account_states: AccountModifications(BTreeMap::new()),
182 }
183 }
184}
185
186impl ContinueParams {
187 pub fn default_advance_count() -> u64 {
188 1
189 }
190}
191
192#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
194#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
195#[serde(rename_all = "lowercase")]
196pub enum BinaryEncoding {
197 Base64,
198}
199
200impl BinaryEncoding {
201 pub fn encode(self, bytes: &[u8]) -> String {
202 match self {
203 Self::Base64 => BASE64.encode(bytes),
204 }
205 }
206
207 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
208 match self {
209 Self::Base64 => BASE64.decode(data),
210 }
211 }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
217#[serde(rename_all = "camelCase")]
218pub struct EncodedBinary {
219 pub data: String,
221 pub encoding: BinaryEncoding,
223}
224
225impl EncodedBinary {
226 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
227 Self { data, encoding }
228 }
229
230 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
231 Self {
232 data: encoding.encode(bytes),
233 encoding,
234 }
235 }
236
237 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
238 self.encoding.decode(&self.data)
239 }
240}
241
242#[serde_with::serde_as]
244#[derive(Debug, Serialize, Deserialize)]
245#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
246#[serde(rename_all = "camelCase")]
247pub struct AccountData {
248 pub data: EncodedBinary,
250 pub executable: bool,
252 pub lamports: u64,
254 #[serde_as(as = "serde_with::DisplayFromStr")]
255 #[cfg_attr(feature = "ts-rs", ts(as = "String"))]
256 pub owner: Address,
258 pub space: u64,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
265#[serde(tag = "method", content = "params", rename_all = "camelCase")]
266#[cfg_attr(feature = "ts-rs", ts(export))]
267pub enum BacktestResponse {
268 SessionCreated {
269 session_id: String,
270 rpc_endpoint: String,
271 },
272 SessionAttached {
273 session_id: String,
274 rpc_endpoint: String,
275 },
276 SessionsCreated {
277 session_ids: Vec<String>,
278 },
279 ReadyForContinue,
280 SlotNotification(u64),
281 Error(BacktestError),
282 Success,
283 Completed {
284 #[serde(skip_serializing_if = "Option::is_none")]
288 summary: Option<SessionSummary>,
289 },
290 Status {
291 status: BacktestStatus,
292 },
293 SessionEventV1 {
294 session_id: String,
295 event: SessionEventV1,
296 },
297}
298
299impl BacktestResponse {
300 pub fn is_completed(&self) -> bool {
301 matches!(self, BacktestResponse::Completed { .. })
302 }
303
304 pub fn is_terminal(&self) -> bool {
305 match self {
306 BacktestResponse::Completed { .. } => true,
307 BacktestResponse::Error(e) => matches!(
308 e,
309 BacktestError::NoMoreBlocks
310 | BacktestError::AdvanceSlotFailed { .. }
311 | BacktestError::Internal { .. }
312 ),
313 _ => false,
314 }
315 }
316}
317
318impl From<BacktestStatus> for BacktestResponse {
319 fn from(status: BacktestStatus) -> Self {
320 Self::Status { status }
321 }
322}
323
324impl From<String> for BacktestResponse {
325 fn from(message: String) -> Self {
326 BacktestError::Internal { error: message }.into()
327 }
328}
329
330impl From<&str> for BacktestResponse {
331 fn from(message: &str) -> Self {
332 BacktestError::Internal {
333 error: message.to_string(),
334 }
335 .into()
336 }
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
340#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
341#[serde(tag = "method", content = "params", rename_all = "camelCase")]
342#[cfg_attr(feature = "ts-rs", ts(export))]
343pub enum SessionEventV1 {
344 ReadyForContinue,
345 SlotNotification(u64),
346 Error(BacktestError),
347 Success,
348 Completed {
349 #[serde(skip_serializing_if = "Option::is_none")]
350 summary: Option<SessionSummary>,
351 },
352 Status {
353 status: BacktestStatus,
354 },
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
360#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
361#[serde(rename_all = "camelCase")]
362#[cfg_attr(feature = "ts-rs", ts(export))]
363pub struct SequencedResponse {
364 pub seq_id: u64,
365 #[serde(flatten)]
366 pub response: BacktestResponse,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
372#[serde(rename_all = "camelCase")]
373pub enum BacktestStatus {
374 PreparingBundle,
376 BundleReady,
378 StartingRuntime,
380 DecodedTransactions,
381 AppliedAccountModifications,
382 ReadyToExecuteUserTransactions,
383 ExecutedUserTransactions,
384 ExecutingBlockTransactions,
385 ExecutedBlockTransactions,
386 ProgramAccountsLoaded,
387}
388
389#[derive(Debug, Clone, Default, Serialize, Deserialize)]
391#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
392#[serde(rename_all = "camelCase")]
393pub struct SessionSummary {
394 pub correct_simulation: usize,
397 pub incorrect_simulation: usize,
400 pub execution_errors: usize,
402 pub balance_diff: usize,
404 pub log_diff: usize,
406}
407
408impl SessionSummary {
409 pub fn has_deviations(&self) -> bool {
411 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
412 }
413
414 pub fn total_transactions(&self) -> usize {
416 self.correct_simulation
417 + self.incorrect_simulation
418 + self.execution_errors
419 + self.balance_diff
420 + self.log_diff
421 }
422}
423
424impl std::fmt::Display for SessionSummary {
425 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426 let total = self.total_transactions();
427 write!(
428 f,
429 "Session summary: {total} transactions\n\
430 \x20 - {} correct simulation\n\
431 \x20 - {} incorrect simulation\n\
432 \x20 - {} execution errors\n\
433 \x20 - {} balance diffs\n\
434 \x20 - {} log diffs",
435 self.correct_simulation,
436 self.incorrect_simulation,
437 self.execution_errors,
438 self.balance_diff,
439 self.log_diff,
440 )
441 }
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize)]
446#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
447#[serde(rename_all = "camelCase")]
448pub enum BacktestError {
449 InvalidTransactionEncoding {
450 index: usize,
451 error: String,
452 },
453 InvalidTransactionFormat {
454 index: usize,
455 error: String,
456 },
457 InvalidAccountEncoding {
458 address: String,
459 encoding: BinaryEncoding,
460 error: String,
461 },
462 InvalidAccountOwner {
463 address: String,
464 error: String,
465 },
466 InvalidAccountPubkey {
467 address: String,
468 error: String,
469 },
470 NoMoreBlocks,
471 AdvanceSlotFailed {
472 slot: u64,
473 error: String,
474 },
475 InvalidRequest {
476 error: String,
477 },
478 Internal {
479 error: String,
480 },
481 InvalidBlockhashFormat {
482 slot: u64,
483 error: String,
484 },
485 InitializingSysvarsFailed {
486 slot: u64,
487 error: String,
488 },
489 ClerkError {
490 error: String,
491 },
492 SimulationError {
493 error: String,
494 },
495 SessionNotFound {
496 session_id: String,
497 },
498 SessionOwnerMismatch,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct AvailableRange {
504 pub bundle_start_slot: u64,
505 pub max_bundle_end_slot: Option<u64>,
506}
507
508pub fn split_range(
514 ranges: &[AvailableRange],
515 requested_start: u64,
516 requested_end: u64,
517) -> Result<Vec<(u64, u64)>, String> {
518 if requested_end < requested_start {
519 return Err(format!(
520 "invalid range: start_slot {requested_start} > end_slot {requested_end}"
521 ));
522 }
523
524 let mut candidates: Vec<(u64, u64)> = ranges
525 .iter()
526 .filter_map(|r| Some((r.bundle_start_slot, r.max_bundle_end_slot?)))
527 .filter(|(start, end)| end >= start)
528 .collect();
529
530 candidates.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
531 candidates.dedup_by_key(|(start, _)| *start);
532
533 if candidates.is_empty() {
534 return Err("no available bundle ranges found on server".to_string());
535 }
536
537 let mut non_overlapping: Vec<(u64, u64)> = Vec::with_capacity(candidates.len());
538 for (i, (start, mut end)) in candidates.iter().copied().enumerate() {
539 if let Some((next_start, _)) = candidates.get(i + 1).copied()
540 && next_start <= end
541 {
542 end = next_start.saturating_sub(1);
543 }
544 if end >= start {
545 non_overlapping.push((start, end));
546 }
547 }
548
549 let anchor = non_overlapping
550 .iter()
551 .enumerate()
552 .rev()
553 .find(|(_, (s, e))| *s <= requested_start && *e >= requested_start)
554 .map(|(i, _)| i)
555 .ok_or_else(|| {
556 format!("start_slot {requested_start} is not covered by any available bundle range")
557 })?;
558
559 let mut result = Vec::new();
560 for (start, range_end) in non_overlapping.into_iter().skip(anchor) {
561 if start > requested_end {
562 break;
563 }
564 let end = range_end.min(requested_end);
565 if end >= start {
566 result.push((start, end));
567 }
568 if end == requested_end {
569 break;
570 }
571 }
572
573 if result.is_empty() {
574 return Err(format!(
575 "no available bundle ranges intersect requested range [{requested_start}-{requested_end}]"
576 ));
577 }
578
579 Ok(result)
580}
581
582impl From<BacktestError> for BacktestResponse {
583 fn from(error: BacktestError) -> Self {
584 Self::Error(error)
585 }
586}
587
588impl std::error::Error for BacktestError {}
589
590impl fmt::Display for BacktestError {
591 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
592 match self {
593 BacktestError::InvalidTransactionEncoding { index, error } => {
594 write!(f, "invalid transaction encoding at index {index}: {error}")
595 }
596 BacktestError::InvalidTransactionFormat { index, error } => {
597 write!(f, "invalid transaction format at index {index}: {error}")
598 }
599 BacktestError::InvalidAccountEncoding {
600 address,
601 encoding,
602 error,
603 } => write!(
604 f,
605 "invalid encoding for account {address} ({encoding:?}): {error}"
606 ),
607 BacktestError::InvalidAccountOwner { address, error } => {
608 write!(f, "invalid owner for account {address}: {error}")
609 }
610 BacktestError::InvalidAccountPubkey { address, error } => {
611 write!(f, "invalid account pubkey {address}: {error}")
612 }
613 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
614 BacktestError::AdvanceSlotFailed { slot, error } => {
615 write!(f, "failed to advance to slot {slot}: {error}")
616 }
617 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
618 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
619 BacktestError::InvalidBlockhashFormat { slot, error } => {
620 write!(f, "invalid blockhash at slot {slot}: {error}")
621 }
622 BacktestError::InitializingSysvarsFailed { slot, error } => {
623 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
624 }
625 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
626 BacktestError::SimulationError { error } => {
627 write!(f, "simulation error: {error}")
628 }
629 BacktestError::SessionNotFound { session_id } => {
630 write!(f, "session not found: {session_id}")
631 }
632 BacktestError::SessionOwnerMismatch => {
633 write!(f, "session owner mismatch")
634 }
635 }
636 }
637}