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(CreateSessionParams),
19 Continue(ContinueParams),
20 CloseBacktestSession,
21 AttachBacktestSession {
22 session_id: String,
23 last_sequence: Option<u64>,
26 },
27}
28
29#[serde_with::serde_as]
31#[derive(Debug, Serialize, Deserialize)]
32#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
33#[serde(rename_all = "camelCase")]
34pub struct CreateSessionParams {
35 pub start_slot: u64,
37 pub end_slot: u64,
39 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
40 #[serde(default)]
41 #[cfg_attr(feature = "ts-rs", ts(as = "Vec<String>"))]
42 pub signer_filter: BTreeSet<Address>,
44 #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
45 #[serde(default)]
46 #[cfg_attr(feature = "ts-rs", ts(as = "Vec<String>"))]
47 pub preload_programs: BTreeSet<Address>,
49 #[serde(default)]
51 pub preload_account_bundles: Vec<String>,
52 #[serde(default)]
55 pub send_summary: bool,
56 #[serde(default)]
60 pub disconnect_timeout_secs: Option<u16>,
61}
62
63#[serde_with::serde_as]
65#[derive(Debug, Serialize, Deserialize, Default)]
66#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
67pub struct AccountModifications(
68 #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
69 #[serde(default)]
70 #[cfg_attr(feature = "ts-rs", ts(as = "BTreeMap<String, AccountData>"))]
71 pub BTreeMap<Address, AccountData>,
72);
73
74#[serde_with::serde_as]
76#[derive(Debug, Serialize, Deserialize)]
77#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
78#[serde(rename_all = "camelCase")]
79pub struct ContinueParams {
80 #[serde(default = "ContinueParams::default_advance_count")]
81 pub advance_count: u64,
83 #[serde(default)]
84 pub transactions: Vec<String>,
86 #[serde(default)]
87 pub modify_account_states: AccountModifications,
89}
90
91impl Default for ContinueParams {
92 fn default() -> Self {
93 Self {
94 advance_count: Self::default_advance_count(),
95 transactions: Vec::new(),
96 modify_account_states: AccountModifications(BTreeMap::new()),
97 }
98 }
99}
100
101impl ContinueParams {
102 pub fn default_advance_count() -> u64 {
103 1
104 }
105}
106
107#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
109#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
110#[serde(rename_all = "lowercase")]
111pub enum BinaryEncoding {
112 Base64,
113}
114
115impl BinaryEncoding {
116 pub fn encode(self, bytes: &[u8]) -> String {
117 match self {
118 Self::Base64 => BASE64.encode(bytes),
119 }
120 }
121
122 pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
123 match self {
124 Self::Base64 => BASE64.decode(data),
125 }
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
132#[serde(rename_all = "camelCase")]
133pub struct EncodedBinary {
134 pub data: String,
136 pub encoding: BinaryEncoding,
138}
139
140impl EncodedBinary {
141 pub fn new(data: String, encoding: BinaryEncoding) -> Self {
142 Self { data, encoding }
143 }
144
145 pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
146 Self {
147 data: encoding.encode(bytes),
148 encoding,
149 }
150 }
151
152 pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
153 self.encoding.decode(&self.data)
154 }
155}
156
157#[serde_with::serde_as]
159#[derive(Debug, Serialize, Deserialize)]
160#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
161#[serde(rename_all = "camelCase")]
162pub struct AccountData {
163 pub data: EncodedBinary,
165 pub executable: bool,
167 pub lamports: u64,
169 #[serde_as(as = "serde_with::DisplayFromStr")]
170 #[cfg_attr(feature = "ts-rs", ts(as = "String"))]
171 pub owner: Address,
173 pub space: u64,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
180#[serde(tag = "method", content = "params", rename_all = "camelCase")]
181#[cfg_attr(feature = "ts-rs", ts(export))]
182pub enum BacktestResponse {
183 SessionCreated {
184 session_id: String,
185 rpc_endpoint: String,
186 },
187 SessionAttached {
188 session_id: String,
189 rpc_endpoint: String,
190 },
191 ReadyForContinue,
192 SlotNotification(u64),
193 Error(BacktestError),
194 Success,
195 Completed {
196 #[serde(skip_serializing_if = "Option::is_none")]
200 summary: Option<SessionSummary>,
201 },
202 Status {
203 status: BacktestStatus,
204 },
205}
206
207impl BacktestResponse {
208 pub fn is_completed(&self) -> bool {
209 matches!(self, BacktestResponse::Completed { .. })
210 }
211}
212
213impl From<BacktestStatus> for BacktestResponse {
214 fn from(status: BacktestStatus) -> Self {
215 Self::Status { status }
216 }
217}
218
219impl From<String> for BacktestResponse {
220 fn from(message: String) -> Self {
221 BacktestError::Internal { error: message }.into()
222 }
223}
224
225impl From<&str> for BacktestResponse {
226 fn from(message: &str) -> Self {
227 BacktestError::Internal {
228 error: message.to_string(),
229 }
230 .into()
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
237#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
238#[serde(rename_all = "camelCase")]
239#[cfg_attr(feature = "ts-rs", ts(export))]
240pub struct SequencedResponse {
241 pub seq_id: u64,
242 #[serde(flatten)]
243 pub response: BacktestResponse,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
249#[serde(rename_all = "camelCase")]
250pub enum BacktestStatus {
251 PreparingBundle,
253 BundleReady,
255 StartingRuntime,
257 DecodedTransactions,
258 AppliedAccountModifications,
259 ReadyToExecuteUserTransactions,
260 ExecutedUserTransactions,
261 ExecutingBlockTransactions,
262 ExecutedBlockTransactions,
263 ProgramAccountsLoaded,
264}
265
266#[derive(Debug, Clone, Default, Serialize, Deserialize)]
268#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
269#[serde(rename_all = "camelCase")]
270pub struct SessionSummary {
271 pub correct_simulation: usize,
274 pub incorrect_simulation: usize,
277 pub execution_errors: usize,
279 pub balance_diff: usize,
281 pub log_diff: usize,
283}
284
285impl SessionSummary {
286 pub fn has_deviations(&self) -> bool {
288 self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
289 }
290
291 pub fn total_transactions(&self) -> usize {
293 self.correct_simulation
294 + self.incorrect_simulation
295 + self.execution_errors
296 + self.balance_diff
297 + self.log_diff
298 }
299}
300
301impl std::fmt::Display for SessionSummary {
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 let total = self.total_transactions();
304 write!(
305 f,
306 "Session summary: {total} transactions\n\
307 \x20 - {} correct simulation\n\
308 \x20 - {} incorrect simulation\n\
309 \x20 - {} execution errors\n\
310 \x20 - {} balance diffs\n\
311 \x20 - {} log diffs",
312 self.correct_simulation,
313 self.incorrect_simulation,
314 self.execution_errors,
315 self.balance_diff,
316 self.log_diff,
317 )
318 }
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
324#[serde(rename_all = "camelCase")]
325pub enum BacktestError {
326 InvalidTransactionEncoding {
327 index: usize,
328 error: String,
329 },
330 InvalidTransactionFormat {
331 index: usize,
332 error: String,
333 },
334 InvalidAccountEncoding {
335 address: String,
336 encoding: BinaryEncoding,
337 error: String,
338 },
339 InvalidAccountOwner {
340 address: String,
341 error: String,
342 },
343 InvalidAccountPubkey {
344 address: String,
345 error: String,
346 },
347 NoMoreBlocks,
348 AdvanceSlotFailed {
349 slot: u64,
350 error: String,
351 },
352 InvalidRequest {
353 error: String,
354 },
355 Internal {
356 error: String,
357 },
358 InvalidBlockhashFormat {
359 slot: u64,
360 error: String,
361 },
362 InitializingSysvarsFailed {
363 slot: u64,
364 error: String,
365 },
366 ClerkError {
367 error: String,
368 },
369 SimulationError {
370 error: String,
371 },
372 SessionNotFound {
373 session_id: String,
374 },
375 SessionOwnerMismatch,
376}
377
378impl From<BacktestError> for BacktestResponse {
379 fn from(error: BacktestError) -> Self {
380 Self::Error(error)
381 }
382}
383
384impl std::error::Error for BacktestError {}
385
386impl fmt::Display for BacktestError {
387 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388 match self {
389 BacktestError::InvalidTransactionEncoding { index, error } => {
390 write!(f, "invalid transaction encoding at index {index}: {error}")
391 }
392 BacktestError::InvalidTransactionFormat { index, error } => {
393 write!(f, "invalid transaction format at index {index}: {error}")
394 }
395 BacktestError::InvalidAccountEncoding {
396 address,
397 encoding,
398 error,
399 } => write!(
400 f,
401 "invalid encoding for account {address} ({encoding:?}): {error}"
402 ),
403 BacktestError::InvalidAccountOwner { address, error } => {
404 write!(f, "invalid owner for account {address}: {error}")
405 }
406 BacktestError::InvalidAccountPubkey { address, error } => {
407 write!(f, "invalid account pubkey {address}: {error}")
408 }
409 BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
410 BacktestError::AdvanceSlotFailed { slot, error } => {
411 write!(f, "failed to advance to slot {slot}: {error}")
412 }
413 BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
414 BacktestError::Internal { error } => write!(f, "internal error: {error}"),
415 BacktestError::InvalidBlockhashFormat { slot, error } => {
416 write!(f, "invalid blockhash at slot {slot}: {error}")
417 }
418 BacktestError::InitializingSysvarsFailed { slot, error } => {
419 write!(f, "failed to initialize sysvars at slot {slot}: {error}")
420 }
421 BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
422 BacktestError::SimulationError { error } => {
423 write!(f, "simulation error: {error}")
424 }
425 BacktestError::SessionNotFound { session_id } => {
426 write!(f, "session not found: {session_id}")
427 }
428 BacktestError::SessionOwnerMismatch => {
429 write!(f, "session owner mismatch")
430 }
431 }
432 }
433}