1use alloc::string::{String, ToString};
2use alloc::sync::Arc;
3use std::boxed::Box;
4
5use miden_objects::crypto::rand::{FeltRng, RpoRandomCoin};
6use miden_objects::{Felt, MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES};
7use miden_tx::ExecutionOptions;
8use miden_tx::auth::TransactionAuthenticator;
9use rand::Rng;
10
11use crate::keystore::FilesystemKeyStore;
12use crate::rpc::NodeRpcClient;
13#[cfg(feature = "tonic")]
14use crate::rpc::{Endpoint, TonicRpcClient};
15use crate::store::Store;
16#[cfg(feature = "sqlite")]
17use crate::store::sqlite_store::SqliteStore;
18use crate::{Client, ClientError, DebugMode};
19
20const TX_GRACEFUL_BLOCKS: u32 = 20;
25
26enum AuthenticatorConfig<AUTH> {
37 Path(String),
38 Instance(Arc<AUTH>),
39}
40
41pub struct ClientBuilder<AUTH> {
50 rpc_api: Option<Arc<dyn NodeRpcClient + Send>>,
52 store: Option<Arc<dyn Store>>,
54 rng: Option<Box<dyn FeltRng>>,
56 #[cfg(feature = "sqlite")]
58 store_path: String,
59 keystore: Option<AuthenticatorConfig<AUTH>>,
61 in_debug_mode: DebugMode,
63 tx_graceful_blocks: Option<u32>,
66 max_block_number_delta: Option<u32>,
69}
70
71impl<AUTH> Default for ClientBuilder<AUTH> {
72 fn default() -> Self {
73 Self {
74 rpc_api: None,
75 store: None,
76 rng: None,
77 #[cfg(feature = "sqlite")]
78 store_path: "store.sqlite3".to_string(),
79 keystore: None,
80 in_debug_mode: DebugMode::Disabled,
81 tx_graceful_blocks: Some(TX_GRACEFUL_BLOCKS),
82 max_block_number_delta: None,
83 }
84 }
85}
86
87impl<AUTH> ClientBuilder<AUTH>
88where
89 AUTH: TransactionAuthenticator + From<FilesystemKeyStore<rand::rngs::StdRng>> + 'static,
90{
91 #[must_use]
93 pub fn new() -> Self {
94 Self::default()
95 }
96
97 #[must_use]
99 pub fn in_debug_mode(mut self, debug: DebugMode) -> Self {
100 self.in_debug_mode = debug;
101 self
102 }
103
104 #[must_use]
106 pub fn rpc(mut self, client: Arc<dyn NodeRpcClient + Send>) -> Self {
107 self.rpc_api = Some(client);
108 self
109 }
110
111 #[cfg(feature = "tonic")]
113 #[must_use]
114 pub fn tonic_rpc_client(mut self, endpoint: &Endpoint, timeout_ms: Option<u64>) -> Self {
115 self.rpc_api = Some(Arc::new(TonicRpcClient::new(endpoint, timeout_ms.unwrap_or(10_000))));
116 self
117 }
118
119 #[cfg(feature = "sqlite")]
121 #[must_use]
122 pub fn sqlite_store(mut self, path: &str) -> Self {
123 self.store_path = path.to_string();
124 self
125 }
126
127 #[must_use]
129 pub fn store(mut self, store: Arc<dyn Store>) -> Self {
130 self.store = Some(store);
131 self
132 }
133
134 #[must_use]
136 pub fn rng(mut self, rng: Box<dyn FeltRng>) -> Self {
137 self.rng = Some(rng);
138 self
139 }
140
141 #[must_use]
143 pub fn authenticator(mut self, authenticator: Arc<AUTH>) -> Self {
144 self.keystore = Some(AuthenticatorConfig::Instance(authenticator));
145 self
146 }
147
148 #[must_use]
151 pub fn max_block_number_delta(mut self, delta: u32) -> Self {
152 self.max_block_number_delta = Some(delta);
153 self
154 }
155
156 #[must_use]
160 pub fn tx_graceful_blocks(mut self, delta: Option<u32>) -> Self {
161 self.tx_graceful_blocks = delta;
162 self
163 }
164
165 #[must_use]
170 pub fn filesystem_keystore(mut self, keystore_path: &str) -> Self {
171 self.keystore = Some(AuthenticatorConfig::Path(keystore_path.to_string()));
172 self
173 }
174
175 #[allow(clippy::unused_async, unused_mut)]
183 pub async fn build(mut self) -> Result<Client<AUTH>, ClientError> {
184 let rpc_api: Arc<dyn NodeRpcClient + Send> = if let Some(client) = self.rpc_api {
186 client
187 } else {
188 return Err(ClientError::ClientInitializationError(
189 "RPC client or endpoint is required. Call `.rpc(...)` or `.tonic_rpc_client(...)` if `tonic` is enabled."
190 .into(),
191 ));
192 };
193
194 #[cfg(feature = "sqlite")]
195 if self.store.is_none() {
196 let store = SqliteStore::new(self.store_path.into())
197 .await
198 .map_err(ClientError::StoreError)?;
199 self.store = Some(Arc::new(store));
200 }
201
202 let arc_store: Arc<dyn Store> = if let Some(store) = self.store {
204 store
205 } else {
206 return Err(ClientError::ClientInitializationError(
207 "Store must be specified. Call `.store(...)` or `.sqlite_store(...)` with a store path if `sqlite` is enabled."
208 .into(),
209 ));
210 };
211
212 let rng = if let Some(user_rng) = self.rng {
214 user_rng
215 } else {
216 let mut seed_rng = rand::rng();
217 let coin_seed: [u64; 4] = seed_rng.random();
218 Box::new(RpoRandomCoin::new(coin_seed.map(Felt::new).into()))
219 };
220
221 let authenticator = match self.keystore {
223 Some(AuthenticatorConfig::Instance(authenticator)) => Some(authenticator),
224 Some(AuthenticatorConfig::Path(ref path)) => {
225 let keystore = FilesystemKeyStore::new(path.into())
226 .map_err(|err| ClientError::ClientInitializationError(err.to_string()))?;
227 Some(Arc::new(AUTH::from(keystore)))
228 },
229 None => None,
230 };
231
232 Client::new(
233 rpc_api,
234 rng,
235 arc_store,
236 authenticator,
237 ExecutionOptions::new(
238 Some(MAX_TX_EXECUTION_CYCLES),
239 MIN_TX_EXECUTION_CYCLES,
240 false,
241 self.in_debug_mode.into(),
242 )
243 .expect("Default executor's options should always be valid"),
244 self.tx_graceful_blocks,
245 self.max_block_number_delta,
246 )
247 .await
248 }
249}