Skip to main content

simulator_client/
builders.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
4use bon::Builder;
5use simulator_api::{
6    AccountData, AccountModifications, AgentParams, BinaryEncoding, ContinueParams,
7    CreateBacktestSessionRequest, CreateBacktestSessionRequestV1, CreateSessionParams,
8};
9use solana_address::Address;
10use solana_client::rpc_client::SerializableTransaction;
11use thiserror::Error;
12
13use crate::BacktestClientError;
14
15/// Error serializing a transaction for injection.
16#[derive(Debug, Error)]
17pub enum SerializeEncodeError {
18    #[error("bincode serialization error: {0}")]
19    Bincode(#[from] bincode::error::EncodeError),
20}
21
22fn serialize_to_base64(value: &impl serde::Serialize) -> Result<String, SerializeEncodeError> {
23    let bytes = bincode::serde::encode_to_vec(
24        value,
25        bincode::config::standard()
26            .with_fixed_int_encoding()
27            .with_little_endian(),
28    )?;
29    Ok(BASE64.encode(&bytes))
30}
31
32/// Builder for `CreateBacktestSession`.
33///
34/// Set either `end_slot` or `slot_count` (not both). Use the helper methods to
35/// add account filters or preload programs.
36#[derive(Debug, Clone, Builder)]
37pub struct CreateSession {
38    pub start_slot: u64,
39
40    pub end_slot: Option<u64>,
41
42    pub slot_count: Option<u64>,
43
44    #[builder(default)]
45    pub signer_filter: BTreeSet<Address>,
46
47    #[builder(default)]
48    pub preload_programs: BTreeSet<Address>,
49
50    #[builder(default)]
51    pub preload_account_bundles: Vec<String>,
52
53    /// When true, include a session summary with transaction statistics in the Completed response.
54    #[builder(default)]
55    pub send_summary: bool,
56
57    /// When true, ask manager to create all available sessions in the target epoch.
58    #[builder(default)]
59    pub parallel: bool,
60
61    pub capacity_wait_timeout_secs: Option<u16>,
62
63    pub disconnect_timeout_secs: Option<u16>,
64
65    /// Extra compute units to add to each transaction's SetComputeUnitLimit budget.
66    pub extra_compute_units: Option<u32>,
67
68    #[builder(default)]
69    pub agents: Vec<AgentParams>,
70}
71
72impl CreateSession {
73    /// Add an account to the signer filter.
74    pub fn add_signer_filter(mut self, address: Address) -> Self {
75        self.signer_filter.insert(address);
76        self
77    }
78
79    /// Add a program to preload before the first continue.
80    pub fn add_preload_program(mut self, address: Address) -> Self {
81        self.preload_programs.insert(address);
82        self
83    }
84
85    /// Convert the builder into API parameters, validating slot options.
86    pub fn into_params(self) -> Result<CreateSessionParams, BacktestClientError> {
87        let end_slot = match (self.end_slot, self.slot_count) {
88            (Some(_), Some(_)) => {
89                return Err(BacktestClientError::InvalidParams {
90                    message: "CreateSession: set only one of end_slot or slot_count".to_string(),
91                });
92            }
93            (Some(end_slot), None) => end_slot,
94            (None, Some(slot_count)) => {
95                self.start_slot.checked_add(slot_count).ok_or_else(|| {
96                    BacktestClientError::InvalidParams {
97                        message: "CreateSession: start_slot + slot_count overflow".to_string(),
98                    }
99                })?
100            }
101            (None, None) => {
102                return Err(BacktestClientError::InvalidParams {
103                    message: "CreateSession: must set end_slot or slot_count".to_string(),
104                });
105            }
106        };
107
108        if end_slot < self.start_slot {
109            return Err(BacktestClientError::InvalidParams {
110                message: format!(
111                    "CreateSession: end_slot ({end_slot}) must be >= start_slot ({})",
112                    self.start_slot
113                ),
114            });
115        }
116
117        Ok(CreateSessionParams {
118            start_slot: self.start_slot,
119            end_slot,
120            signer_filter: self.signer_filter,
121            preload_programs: self.preload_programs,
122            preload_account_bundles: self.preload_account_bundles,
123            send_summary: self.send_summary,
124            capacity_wait_timeout_secs: self.capacity_wait_timeout_secs,
125            disconnect_timeout_secs: self.disconnect_timeout_secs,
126            extra_compute_units: self.extra_compute_units,
127            agents: self.agents,
128        })
129    }
130
131    /// Convert the builder into versioned create request payload.
132    pub fn into_request(self) -> Result<CreateBacktestSessionRequest, BacktestClientError> {
133        let parallel = self.parallel;
134        let request = self.into_params()?;
135        if parallel {
136            Ok(CreateBacktestSessionRequestV1 { request, parallel }.into())
137        } else {
138            Ok(request.into())
139        }
140    }
141}
142
143/// Builder for `Continue` requests.
144///
145/// Use this to advance the simulation, inject transactions, or patch accounts.
146#[derive(Debug, Builder)]
147pub struct Continue {
148    #[builder(default = ContinueParams::default_advance_count())]
149    pub advance_count: u64,
150
151    #[builder(default)]
152    pub transactions: Vec<String>,
153
154    #[builder(default)]
155    pub modify_accounts: BTreeMap<Address, AccountData>,
156}
157
158impl Continue {
159    /// Append a base64-encoded transaction payload.
160    pub fn push_transaction_base64(mut self, data: impl Into<String>) -> Self {
161        self.transactions.push(data.into());
162        self
163    }
164
165    /// Append a raw transaction payload encoded as base64.
166    pub fn push_transaction_bytes(mut self, bytes: &[u8]) -> Self {
167        self.transactions.push(BinaryEncoding::Base64.encode(bytes));
168        self
169    }
170
171    /// Append a serializable transaction encoded as base64.
172    pub fn push_transaction(
173        mut self,
174        transaction: &impl SerializableTransaction,
175    ) -> Result<Self, SerializeEncodeError> {
176        self.transactions.push(serialize_to_base64(&transaction)?);
177        Ok(self)
178    }
179
180    /// Modify an account state prior to execution.
181    pub fn modify_account(mut self, address: Address, account: AccountData) -> Self {
182        self.modify_accounts.insert(address, account);
183        self
184    }
185
186    /// Convert the builder into API parameters.
187    pub fn into_params(self) -> ContinueParams {
188        ContinueParams {
189            advance_count: self.advance_count,
190            transactions: self.transactions,
191            modify_account_states: AccountModifications(self.modify_accounts),
192        }
193    }
194}