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}
141
142#[serde_with::serde_as]
144#[derive(Debug, Serialize, Deserialize, Default)]
145#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
146pub struct AccountModifications(
147 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
148 #[serde(default)]
149 #[cfg_attr(feature = "ts-rs", ts(as = "BTreeMap<String, AccountData>"))]
150 pub BTreeMap<Address, AccountData>,
151);
152
153#[serde_with::serde_as]
155#[derive(Debug, Serialize, Deserialize)]
156#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
157#[serde(rename_all = "camelCase")]
158pub struct ContinueParams {
159 #[serde(default = "ContinueParams::default_advance_count")]
160 pub advance_count: u64,
162 #[serde(default)]
163 pub transactions: Vec<String>,
165 #[serde(default)]
166 pub modify_account_states: AccountModifications,
168}
169
170impl Default for ContinueParams {
171 fn default() -> Self {
172 Self {
173 advance_count: Self::default_advance_count(),
174 transactions: Vec::new(),
175 modify_account_states: AccountModifications(BTreeMap::new()),
176 }
177 }
178}
179
180impl ContinueParams {
181 pub fn default_advance_count() -> u64 {
182 1
183 }
184}
185
186#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
188#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
189#[serde(rename_all = "lowercase")]
190pub enum BinaryEncoding {
191 Base64,
192}
193
194impl BinaryEncoding {
195 pub fn encode(self, bytes: &[u8]) -> String {
196 match self {
197 Self::Base64 => BASE64.encode(bytes),
198 }
199 }
200
201 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
202 match self {
203 Self::Base64 => BASE64.decode(data),
204 }
205 }
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
211#[serde(rename_all = "camelCase")]
212pub struct EncodedBinary {
213 pub data: String,
215 pub encoding: BinaryEncoding,
217}
218
219impl EncodedBinary {
220 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
221 Self { data, encoding }
222 }
223
224 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
225 Self {
226 data: encoding.encode(bytes),
227 encoding,
228 }
229 }
230
231 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
232 self.encoding.decode(&self.data)
233 }
234}
235
236#[serde_with::serde_as]
238#[derive(Debug, Serialize, Deserialize)]
239#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
240#[serde(rename_all = "camelCase")]
241pub struct AccountData {
242 pub data: EncodedBinary,
244 pub executable: bool,
246 pub lamports: u64,
248 #[serde_as(as = "serde_with::DisplayFromStr")]
249 #[cfg_attr(feature = "ts-rs", ts(as = "String"))]
250 pub owner: Address,
252 pub space: u64,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
259#[serde(tag = "method", content = "params", rename_all = "camelCase")]
260#[cfg_attr(feature = "ts-rs", ts(export))]
261pub enum BacktestResponse {
262 SessionCreated {
263 session_id: String,
264 rpc_endpoint: String,
265 },
266 SessionAttached {
267 session_id: String,
268 rpc_endpoint: String,
269 },
270 SessionsCreated {
271 session_ids: Vec<String>,
272 },
273 ReadyForContinue,
274 SlotNotification(u64),
275 Error(BacktestError),
276 Success,
277 Completed {
278 #[serde(skip_serializing_if = "Option::is_none")]
282 summary: Option<SessionSummary>,
283 },
284 Status {
285 status: BacktestStatus,
286 },
287 SessionEventV1 {
288 session_id: String,
289 event: SessionEventV1,
290 },
291}
292
293impl BacktestResponse {
294 pub fn is_completed(&self) -> bool {
295 matches!(self, BacktestResponse::Completed { .. })
296 }
297
298 pub fn is_terminal(&self) -> bool {
299 match self {
300 BacktestResponse::Completed { .. } => true,
301 BacktestResponse::Error(e) => matches!(
302 e,
303 BacktestError::NoMoreBlocks
304 | BacktestError::AdvanceSlotFailed { .. }
305 | BacktestError::Internal { .. }
306 ),
307 _ => false,
308 }
309 }
310}
311
312impl From<BacktestStatus> for BacktestResponse {
313 fn from(status: BacktestStatus) -> Self {
314 Self::Status { status }
315 }
316}
317
318impl From<String> for BacktestResponse {
319 fn from(message: String) -> Self {
320 BacktestError::Internal { error: message }.into()
321 }
322}
323
324impl From<&str> for BacktestResponse {
325 fn from(message: &str) -> Self {
326 BacktestError::Internal {
327 error: message.to_string(),
328 }
329 .into()
330 }
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
334#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
335#[serde(tag = "method", content = "params", rename_all = "camelCase")]
336#[cfg_attr(feature = "ts-rs", ts(export))]
337pub enum SessionEventV1 {
338 ReadyForContinue,
339 SlotNotification(u64),
340 Error(BacktestError),
341 Success,
342 Completed {
343 #[serde(skip_serializing_if = "Option::is_none")]
344 summary: Option<SessionSummary>,
345 },
346 Status {
347 status: BacktestStatus,
348 },
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
354#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
355#[serde(rename_all = "camelCase")]
356#[cfg_attr(feature = "ts-rs", ts(export))]
357pub struct SequencedResponse {
358 pub seq_id: u64,
359 #[serde(flatten)]
360 pub response: BacktestResponse,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
365#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
366#[serde(rename_all = "camelCase")]
367pub enum BacktestStatus {
368 PreparingBundle,
370 BundleReady,
372 StartingRuntime,
374 DecodedTransactions,
375 AppliedAccountModifications,
376 ReadyToExecuteUserTransactions,
377 ExecutedUserTransactions,
378 ExecutingBlockTransactions,
379 ExecutedBlockTransactions,
380 ProgramAccountsLoaded,
381}
382
383#[derive(Debug, Clone, Default, Serialize, Deserialize)]
385#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
386#[serde(rename_all = "camelCase")]
387pub struct SessionSummary {
388 pub correct_simulation: usize,
391 pub incorrect_simulation: usize,
394 pub execution_errors: usize,
396 pub balance_diff: usize,
398 pub log_diff: usize,
400}
401
402impl SessionSummary {
403 pub fn has_deviations(&self) -> bool {
405 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
406 }
407
408 pub fn total_transactions(&self) -> usize {
410 self.correct_simulation
411 + self.incorrect_simulation
412 + self.execution_errors
413 + self.balance_diff
414 + self.log_diff
415 }
416}
417
418impl std::fmt::Display for SessionSummary {
419 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 let total = self.total_transactions();
421 write!(
422 f,
423 "Session summary: {total} transactions\n\
424 \x20 - {} correct simulation\n\
425 \x20 - {} incorrect simulation\n\
426 \x20 - {} execution errors\n\
427 \x20 - {} balance diffs\n\
428 \x20 - {} log diffs",
429 self.correct_simulation,
430 self.incorrect_simulation,
431 self.execution_errors,
432 self.balance_diff,
433 self.log_diff,
434 )
435 }
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
440#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
441#[serde(rename_all = "camelCase")]
442pub enum BacktestError {
443 InvalidTransactionEncoding {
444 index: usize,
445 error: String,
446 },
447 InvalidTransactionFormat {
448 index: usize,
449 error: String,
450 },
451 InvalidAccountEncoding {
452 address: String,
453 encoding: BinaryEncoding,
454 error: String,
455 },
456 InvalidAccountOwner {
457 address: String,
458 error: String,
459 },
460 InvalidAccountPubkey {
461 address: String,
462 error: String,
463 },
464 NoMoreBlocks,
465 AdvanceSlotFailed {
466 slot: u64,
467 error: String,
468 },
469 InvalidRequest {
470 error: String,
471 },
472 Internal {
473 error: String,
474 },
475 InvalidBlockhashFormat {
476 slot: u64,
477 error: String,
478 },
479 InitializingSysvarsFailed {
480 slot: u64,
481 error: String,
482 },
483 ClerkError {
484 error: String,
485 },
486 SimulationError {
487 error: String,
488 },
489 SessionNotFound {
490 session_id: String,
491 },
492 SessionOwnerMismatch,
493}
494
495impl From<BacktestError> for BacktestResponse {
496 fn from(error: BacktestError) -> Self {
497 Self::Error(error)
498 }
499}
500
501impl std::error::Error for BacktestError {}
502
503impl fmt::Display for BacktestError {
504 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
505 match self {
506 BacktestError::InvalidTransactionEncoding { index, error } => {
507 write!(f, "invalid transaction encoding at index {index}: {error}")
508 }
509 BacktestError::InvalidTransactionFormat { index, error } => {
510 write!(f, "invalid transaction format at index {index}: {error}")
511 }
512 BacktestError::InvalidAccountEncoding {
513 address,
514 encoding,
515 error,
516 } => write!(
517 f,
518 "invalid encoding for account {address} ({encoding:?}): {error}"
519 ),
520 BacktestError::InvalidAccountOwner { address, error } => {
521 write!(f, "invalid owner for account {address}: {error}")
522 }
523 BacktestError::InvalidAccountPubkey { address, error } => {
524 write!(f, "invalid account pubkey {address}: {error}")
525 }
526 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
527 BacktestError::AdvanceSlotFailed { slot, error } => {
528 write!(f, "failed to advance to slot {slot}: {error}")
529 }
530 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
531 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
532 BacktestError::InvalidBlockhashFormat { slot, error } => {
533 write!(f, "invalid blockhash at slot {slot}: {error}")
534 }
535 BacktestError::InitializingSysvarsFailed { slot, error } => {
536 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
537 }
538 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
539 BacktestError::SimulationError { error } => {
540 write!(f, "simulation error: {error}")
541 }
542 BacktestError::SessionNotFound { session_id } => {
543 write!(f, "session not found: {session_id}")
544 }
545 BacktestError::SessionOwnerMismatch => {
546 write!(f, "session owner mismatch")
547 }
548 }
549 }
550}