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, ask manager to create all available sessions in the target epoch.
53    #[builder(default)]
54    pub parallel: bool,
55
56    pub capacity_wait_timeout_secs: Option<u16>,
57
58    pub disconnect_timeout_secs: Option<u16>,
59
60    /// Extra compute units to add to each transaction's SetComputeUnitLimit budget.
61    pub extra_compute_units: Option<u32>,
62
63    #[builder(default)]
64    pub agents: Vec<AgentParams>,
65
66    /// Batch discovery filters registered at session creation. For each
67    /// filter, the server emits [`BacktestResponse::DiscoveryBatch`] ahead
68    /// of every matching batch so the caller can pause via
69    /// [`BacktestSession::advance_to_discovery`] before it executes.
70    #[builder(default)]
71    pub discoveries: Vec<DiscoveryFilter>,
72
73    /// Transactions the server runs automatically during the session, with
74    /// results streamed over `actionSubscribe`.
75    #[builder(default)]
76    pub actions: Vec<ScheduledAction>,
77}
78
79impl CreateSession {
80    /// Add an account to the signer filter.
81    pub fn add_signer_filter(mut self, address: Address) -> Self {
82        self.signer_filter.insert(address);
83        self
84    }
85
86    /// Convert the builder into API parameters, validating slot options.
87    pub fn into_params(self) -> Result<CreateSessionParams, BacktestClientError> {
88        let end_slot = match (self.end_slot, self.slot_count) {
89            (Some(_), Some(_)) => {
90                return Err(BacktestClientError::InvalidParams {
91                    message: "CreateSession: set only one of end_slot or slot_count".to_string(),
92                });
93            }
94            (Some(end_slot), None) => end_slot,
95            (None, Some(slot_count)) => {
96                self.start_slot.checked_add(slot_count).ok_or_else(|| {
97                    BacktestClientError::InvalidParams {
98                        message: "CreateSession: start_slot + slot_count overflow".to_string(),
99                    }
100                })?
101            }
102            (None, None) => {
103                return Err(BacktestClientError::InvalidParams {
104                    message: "CreateSession: must set end_slot or slot_count".to_string(),
105                });
106            }
107        };
108
109        if end_slot < self.start_slot {
110            return Err(BacktestClientError::InvalidParams {
111                message: format!(
112                    "CreateSession: end_slot ({end_slot}) must be >= start_slot ({})",
113                    self.start_slot
114                ),
115            });
116        }
117
118        Ok(CreateSessionParams::builder()
119            .start_slot(self.start_slot)
120            .end_slot(end_slot)
121            .signer_filter(self.signer_filter)
122            .send_summary(self.send_summary)
123            .maybe_capacity_wait_timeout_secs(self.capacity_wait_timeout_secs)
124            .maybe_disconnect_timeout_secs(self.disconnect_timeout_secs)
125            .maybe_extra_compute_units(self.extra_compute_units)
126            .agents(self.agents)
127            .discoveries(self.discoveries)
128            .actions(self.actions)
129            .build())
130    }
131
132    /// Convert the builder into versioned create request payload.
133    pub fn into_request(self) -> Result<CreateBacktestSessionRequest, BacktestClientError> {
134        let parallel = self.parallel;
135        let request = self.into_params()?;
136        if parallel {
137            Ok(CreateBacktestSessionRequestV1 { request, parallel }.into())
138        } else {
139            Ok(request.into())
140        }
141    }
142}
143
144/// Builder for `Continue` requests.
145///
146/// Use this to advance the simulation, inject transactions, or patch accounts.
147#[derive(Debug, Builder)]
148pub struct Continue {
149    #[builder(default = ContinueParams::default_advance_count())]
150    pub advance_count: u64,
151
152    #[builder(default)]
153    pub transactions: Vec<String>,
154
155    #[builder(default)]
156    pub modify_accounts: BTreeMap<Address, AccountData>,
157}
158
159impl Continue {
160    /// Append a base64-encoded transaction payload.
161    pub fn push_transaction_base64(mut self, data: impl Into<String>) -> Self {
162        self.transactions.push(data.into());
163        self
164    }
165
166    /// Append a raw transaction payload encoded as base64.
167    pub fn push_transaction_bytes(mut self, bytes: &[u8]) -> Self {
168        self.transactions.push(BinaryEncoding::Base64.encode(bytes));
169        self
170    }
171
172    /// Append a serializable transaction encoded as base64.
173    pub fn push_transaction(
174        mut self,
175        transaction: &impl SerializableTransaction,
176    ) -> Result<Self, SerializeEncodeError> {
177        self.transactions.push(serialize_to_base64(&transaction)?);
178        Ok(self)
179    }
180
181    /// Modify an account state prior to execution.
182    pub fn modify_account(mut self, address: Address, account: AccountData) -> Self {
183        self.modify_accounts.insert(address, account);
184        self
185    }
186
187    /// Convert the builder into API parameters.
188    pub fn into_params(self) -> ContinueParams {
189        ContinueParams {
190            advance_count: self.advance_count,
191            transactions: self.transactions,
192            modify_account_states: AccountModifications(self.modify_accounts),
193        }
194    }
195}