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    DiscoveryFilter, ScheduledAction,
9};
10use solana_address::Address;
11use solana_client::rpc_client::SerializableTransaction;
12use thiserror::Error;
13
14use crate::BacktestClientError;
15
16/// Error serializing a transaction for injection.
17#[derive(Debug, Error)]
18pub enum SerializeEncodeError {
19    #[error("bincode serialization error: {0}")]
20    Bincode(#[from] bincode::error::EncodeError),
21}
22
23fn serialize_to_base64(value: &impl serde::Serialize) -> Result<String, SerializeEncodeError> {
24    let bytes = bincode::serde::encode_to_vec(
25        value,
26        bincode::config::standard()
27            .with_fixed_int_encoding()
28            .with_little_endian(),
29    )?;
30    Ok(BASE64.encode(&bytes))
31}
32
33/// Builder for `CreateBacktestSession`.
34///
35/// Set either `end_slot` or `slot_count` (not both). Use the helper methods to
36/// add account filters.
37#[derive(Debug, Clone, Builder)]
38pub struct CreateSession {
39    pub start_slot: u64,
40
41    pub end_slot: Option<u64>,
42
43    pub slot_count: Option<u64>,
44
45    #[builder(default)]
46    pub signer_filter: BTreeSet<Address>,
47
48    /// When true, include a session summary with transaction statistics in the Completed response.
49    #[builder(default)]
50    pub send_summary: bool,
51
52    /// When true, replace historical swaps with transactions re-quoted through the
53    /// metis router against the current simulation state.
54    #[builder(default)]
55    pub reroute_order_flow: 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    /// Batch discovery filters registered at session creation. For each
72    /// filter, the server emits [`BacktestResponse::DiscoveryBatch`] ahead
73    /// of every matching batch so the caller can pause via
74    /// [`BacktestSession::advance_to_discovery`] before it executes.
75    #[builder(default)]
76    pub discoveries: Vec<DiscoveryFilter>,
77
78    /// Transactions the server runs automatically during the session, with
79    /// results streamed over `actionSubscribe`.
80    #[builder(default)]
81    pub actions: Vec<ScheduledAction>,
82}
83
84impl CreateSession {
85    /// Add an account to the signer filter.
86    pub fn add_signer_filter(mut self, address: Address) -> Self {
87        self.signer_filter.insert(address);
88        self
89    }
90
91    /// Convert the builder into API parameters, validating slot options.
92    pub fn into_params(self) -> Result<CreateSessionParams, BacktestClientError> {
93        let end_slot = match (self.end_slot, self.slot_count) {
94            (Some(_), Some(_)) => {
95                return Err(BacktestClientError::InvalidParams {
96                    message: "CreateSession: set only one of end_slot or slot_count".to_string(),
97                });
98            }
99            (Some(end_slot), None) => end_slot,
100            (None, Some(slot_count)) => {
101                self.start_slot.checked_add(slot_count).ok_or_else(|| {
102                    BacktestClientError::InvalidParams {
103                        message: "CreateSession: start_slot + slot_count overflow".to_string(),
104                    }
105                })?
106            }
107            (None, None) => {
108                return Err(BacktestClientError::InvalidParams {
109                    message: "CreateSession: must set end_slot or slot_count".to_string(),
110                });
111            }
112        };
113
114        if end_slot < self.start_slot {
115            return Err(BacktestClientError::InvalidParams {
116                message: format!(
117                    "CreateSession: end_slot ({end_slot}) must be >= start_slot ({})",
118                    self.start_slot
119                ),
120            });
121        }
122
123        Ok(CreateSessionParams::builder()
124            .start_slot(self.start_slot)
125            .end_slot(end_slot)
126            .signer_filter(self.signer_filter)
127            .send_summary(self.send_summary)
128            .reroute_order_flow(self.reroute_order_flow)
129            .maybe_capacity_wait_timeout_secs(self.capacity_wait_timeout_secs)
130            .maybe_disconnect_timeout_secs(self.disconnect_timeout_secs)
131            .maybe_extra_compute_units(self.extra_compute_units)
132            .agents(self.agents)
133            .discoveries(self.discoveries)
134            .actions(self.actions)
135            .build())
136    }
137
138    /// Convert the builder into versioned create request payload.
139    pub fn into_request(self) -> Result<CreateBacktestSessionRequest, BacktestClientError> {
140        let parallel = self.parallel;
141        let request = self.into_params()?;
142        if parallel {
143            Ok(CreateBacktestSessionRequestV1 { request, parallel }.into())
144        } else {
145            Ok(request.into())
146        }
147    }
148}
149
150/// Builder for `Continue` requests.
151///
152/// Use this to advance the simulation, inject transactions, or patch accounts.
153#[derive(Debug, Builder)]
154pub struct Continue {
155    #[builder(default = ContinueParams::default_advance_count())]
156    pub advance_count: u64,
157
158    #[builder(default)]
159    pub transactions: Vec<String>,
160
161    #[builder(default)]
162    pub modify_accounts: BTreeMap<Address, AccountData>,
163}
164
165impl Continue {
166    /// Append a base64-encoded transaction payload.
167    pub fn push_transaction_base64(mut self, data: impl Into<String>) -> Self {
168        self.transactions.push(data.into());
169        self
170    }
171
172    /// Append a raw transaction payload encoded as base64.
173    pub fn push_transaction_bytes(mut self, bytes: &[u8]) -> Self {
174        self.transactions.push(BinaryEncoding::Base64.encode(bytes));
175        self
176    }
177
178    /// Append a serializable transaction encoded as base64.
179    pub fn push_transaction(
180        mut self,
181        transaction: &impl SerializableTransaction,
182    ) -> Result<Self, SerializeEncodeError> {
183        self.transactions.push(serialize_to_base64(&transaction)?);
184        Ok(self)
185    }
186
187    /// Modify an account state prior to execution.
188    pub fn modify_account(mut self, address: Address, account: AccountData) -> Self {
189        self.modify_accounts.insert(address, account);
190        self
191    }
192
193    /// Convert the builder into API parameters.
194    pub fn into_params(self) -> ContinueParams {
195        ContinueParams {
196            advance_count: self.advance_count,
197            transactions: self.transactions,
198            modify_account_states: AccountModifications(self.modify_accounts),
199        }
200    }
201}