miden_client/
builder.rs

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
20// CONSTANTS
21// ================================================================================================
22
23/// The number of blocks that are considered old enough to discard pending transactions.
24const TX_GRACEFUL_BLOCKS: u32 = 20;
25
26// AUTHENTICATOR CONFIGURATION
27// ================================================================================================
28
29/// Represents the configuration for an authenticator.
30///
31/// This enum defers authenticator instantiation until the build phase. The builder can accept
32/// either:
33///
34/// - A direct instance of an authenticator, or
35/// - A keystore path as a string which is then used as an authenticator.
36enum AuthenticatorConfig<AUTH> {
37    Path(String),
38    Instance(Arc<AUTH>),
39}
40
41// CLIENT BUILDER
42// ================================================================================================
43
44/// A builder for constructing a Miden client.
45///
46/// This builder allows you to configure the various components required by the client, such as the
47/// RPC endpoint, store, RNG, and keystore. It is generic over the keystore type. By default, it
48/// uses `FilesystemKeyStore<rand::rngs::StdRng>`.
49pub struct ClientBuilder<AUTH> {
50    /// An optional custom RPC client. If provided, this takes precedence over `rpc_endpoint`.
51    rpc_api: Option<Arc<dyn NodeRpcClient + Send>>,
52    /// An optional store provided by the user.
53    store: Option<Arc<dyn Store>>,
54    /// An optional RNG provided by the user.
55    rng: Option<Box<dyn FeltRng>>,
56    /// The store path to use when no store is directly provided via `store()`.
57    #[cfg(feature = "sqlite")]
58    store_path: String,
59    /// The keystore configuration provided by the user.
60    keystore: Option<AuthenticatorConfig<AUTH>>,
61    /// A flag to enable debug mode.
62    in_debug_mode: DebugMode,
63    /// The number of blocks that are considered old enough to discard pending transactions. If
64    /// `None`, there is no limit and transactions will be kept indefinitely.
65    tx_graceful_blocks: Option<u32>,
66    /// Maximum number of blocks the client can be behind the network for transactions and account
67    /// proofs to be considered valid.
68    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    /// Create a new `ClientBuilder` with default settings.
92    #[must_use]
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Enable or disable debug mode.
98    #[must_use]
99    pub fn in_debug_mode(mut self, debug: DebugMode) -> Self {
100        self.in_debug_mode = debug;
101        self
102    }
103
104    /// Sets a custom RPC client directly.
105    #[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    /// Sets the a tonic RPC client from the endpoint and optional timeout.
112    #[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    /// Optionally set a custom store path.
120    #[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    /// Optionally provide a store directly.
128    #[must_use]
129    pub fn store(mut self, store: Arc<dyn Store>) -> Self {
130        self.store = Some(store);
131        self
132    }
133
134    /// Optionally provide a custom RNG.
135    #[must_use]
136    pub fn rng(mut self, rng: Box<dyn FeltRng>) -> Self {
137        self.rng = Some(rng);
138        self
139    }
140
141    /// Optionally provide a custom authenticator instance.
142    #[must_use]
143    pub fn authenticator(mut self, authenticator: Arc<AUTH>) -> Self {
144        self.keystore = Some(AuthenticatorConfig::Instance(authenticator));
145        self
146    }
147
148    /// Optionally set a maximum number of blocks that the client can be behind the network.
149    /// By default, there's no maximum.
150    #[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    /// Optionally set a maximum number of blocks to wait for a transaction to be confirmed. If
157    /// `None`, there is no limit and transactions will be kept indefinitely.
158    /// By default, the maximum is set to `TX_GRACEFUL_BLOCKS`.
159    #[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    /// **Required:** Provide the keystore path as a string.
166    ///
167    /// This stores the keystore path as a configuration option so that actual keystore
168    /// initialization is deferred until `build()`. This avoids panicking during builder chaining.
169    #[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    /// Build and return the `Client`.
176    ///
177    /// # Errors
178    ///
179    /// - Returns an error if no RPC client or endpoint was provided.
180    /// - Returns an error if the store cannot be instantiated.
181    /// - Returns an error if the keystore is not specified or fails to initialize.
182    #[allow(clippy::unused_async, unused_mut)]
183    pub async fn build(mut self) -> Result<Client<AUTH>, ClientError> {
184        // Determine the RPC client to use.
185        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        // If no store was provided, create a SQLite store from the given path.
203        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        // Use the provided RNG, or create a default one.
213        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        // Initialize the authenticator.
222        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}