miden_client/builder.rs
1use alloc::boxed::Box;
2use alloc::sync::Arc;
3
4use miden_protocol::assembly::{DefaultSourceManager, SourceManagerSync};
5use miden_protocol::block::BlockNumber;
6use miden_protocol::crypto::rand::RandomCoin;
7use miden_protocol::{Felt, MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES};
8use miden_tx::{ExecutionOptions, LocalTransactionProver};
9use rand::Rng;
10
11#[cfg(any(feature = "tonic", feature = "std"))]
12use crate::alloc::string::ToString;
13#[cfg(feature = "std")]
14use crate::keystore::FilesystemKeyStore;
15use crate::keystore::Keystore;
16use crate::note_transport::NoteTransportClient;
17use crate::rpc::{Endpoint, NodeRpcClient};
18use crate::store::{Store, StoreError};
19use crate::transaction::TransactionProver;
20use crate::{Client, ClientError, ClientRng, ClientRngBox, DebugMode, grpc_support};
21
22// CONSTANTS
23// ================================================================================================
24
25/// The default number of blocks after which pending transactions are considered stale and
26/// discarded.
27const TX_DISCARD_DELTA: u32 = 20;
28
29pub use grpc_support::*;
30
31// STORE BUILDER
32// ================================================================================================
33
34/// Allows the [`ClientBuilder`] to accept either an already built store instance or a factory for
35/// deferring the store instantiation.
36pub enum StoreBuilder {
37 Store(Arc<dyn Store>),
38 Factory(Box<dyn StoreFactory>),
39}
40
41/// Trait for building a store instance.
42#[async_trait::async_trait]
43pub trait StoreFactory {
44 /// Returns a new store instance.
45 async fn build(&self) -> Result<Arc<dyn Store>, StoreError>;
46}
47
48// CLIENT BUILDER
49// ================================================================================================
50
51/// A builder for constructing a Miden client.
52///
53/// This builder allows you to configure the various components required by the client, such as the
54/// RPC endpoint, store, RNG, and authenticator. It is generic over the authenticator type.
55///
56/// ## Network-Aware Constructors
57///
58/// Use one of the network-specific constructors to get sensible defaults for a specific network:
59/// - [`for_testnet()`](Self::for_testnet) - Pre-configured for Miden testnet
60/// - [`for_devnet()`](Self::for_devnet) - Pre-configured for Miden devnet
61/// - [`for_localhost()`](Self::for_localhost) - Pre-configured for local development
62///
63/// The builder provides defaults for:
64/// - **RPC endpoint**: Automatically configured based on the network
65/// - **Transaction prover**: Remote for testnet/devnet, local for localhost
66/// - **RNG**: Random seed-based prover randomness
67///
68/// ## Components
69///
70/// The client requires several components to function:
71///
72/// - **RPC client** ([`NodeRpcClient`]): Provides connectivity to the Miden node for submitting
73/// transactions, syncing state, and fetching account/note data. Configure via
74/// [`rpc()`](Self::rpc) or [`grpc_client()`](Self::grpc_client).
75///
76/// - **Store** ([`Store`]): Provides persistence for accounts, notes, and transaction history.
77/// Configure via [`store()`](Self::store).
78///
79/// - **RNG** ([`FeltRng`](miden_protocol::crypto::rand::FeltRng)): Provides randomness for
80/// generating keys, serial numbers, and other cryptographic operations. If not provided, a random
81/// seed-based RNG is created automatically. Configure via [`rng()`](Self::rng).
82///
83/// - **Authenticator** ([`TransactionAuthenticator`](miden_tx::auth::TransactionAuthenticator)):
84/// Handles transaction signing when signatures are requested from within the VM. Configure via
85/// [`authenticator()`](Self::authenticator).
86///
87/// - **Transaction prover** ([`TransactionProver`]): Generates proofs for transactions. Defaults to
88/// a local prover if not specified. Configure via [`prover()`](Self::prover).
89///
90/// - **Note transport** ([`NoteTransportClient`]): Optional component for exchanging private notes
91/// through the Miden note transport network. Configure via
92/// [`note_transport()`](Self::note_transport).
93///
94/// - **Debug mode**: Enables debug mode for transaction execution. Configure via
95/// [`in_debug_mode()`](Self::in_debug_mode).
96///
97/// - **Transaction discard delta**: Number of blocks after which pending transactions are
98/// considered stale and discarded. Configure via [`tx_discard_delta()`](Self::tx_discard_delta).
99///
100/// - **Max block number delta**: Maximum number of blocks the client can be behind the network for
101/// transactions and account proofs to be considered valid. Configure via
102/// [`max_block_number_delta()`](Self::max_block_number_delta).
103pub struct ClientBuilder<AUTH> {
104 /// An optional custom RPC client. If provided, this takes precedence over `rpc_endpoint`.
105 rpc_api: Option<Arc<dyn NodeRpcClient>>,
106 /// An optional store provided by the user.
107 pub store: Option<StoreBuilder>,
108 /// An optional RNG provided by the user.
109 rng: Option<ClientRngBox>,
110 /// The authenticator provided by the user.
111 authenticator: Option<Arc<AUTH>>,
112 /// A flag to enable debug mode.
113 in_debug_mode: DebugMode,
114 /// Number of blocks after which pending transactions are considered stale and discarded.
115 /// If `None`, there is no limit and transactions will be kept indefinitely.
116 tx_discard_delta: Option<u32>,
117 /// Maximum number of blocks the client can be behind the network for transactions and account
118 /// proofs to be considered valid.
119 max_block_number_delta: Option<u32>,
120 /// An optional custom note transport client.
121 note_transport_api: Option<Arc<dyn NoteTransportClient>>,
122 /// Configuration for lazy note transport initialization (used by network constructors).
123 #[allow(unused)]
124 note_transport_config: Option<NoteTransportConfig>,
125 /// An optional custom transaction prover.
126 tx_prover: Option<Arc<dyn TransactionProver + Send + Sync>>,
127 /// The endpoint used by the builder for network configuration.
128 endpoint: Option<Endpoint>,
129}
130
131impl<AUTH> Default for ClientBuilder<AUTH> {
132 fn default() -> Self {
133 Self {
134 rpc_api: None,
135 store: None,
136 rng: None,
137 authenticator: None,
138 in_debug_mode: DebugMode::Disabled,
139 tx_discard_delta: Some(TX_DISCARD_DELTA),
140 max_block_number_delta: None,
141 note_transport_api: None,
142 note_transport_config: None,
143 tx_prover: None,
144 endpoint: None,
145 }
146 }
147}
148
149/// Network-specific constructors for [`ClientBuilder`].
150///
151/// These constructors automatically configure the builder for a specific network,
152/// including RPC endpoint, transaction prover, and note transport (where applicable).
153#[cfg(feature = "tonic")]
154impl<AUTH> ClientBuilder<AUTH>
155where
156 AUTH: BuilderAuthenticator,
157{
158 /// Creates a `ClientBuilder` pre-configured for Miden testnet.
159 ///
160 /// This automatically configures:
161 /// - **RPC**: [`Endpoint::testnet()`]
162 /// - **Prover**: Remote prover at [`TESTNET_PROVER_ENDPOINT`]
163 /// - **Note transport**:
164 /// [`NOTE_TRANSPORT_TESTNET_ENDPOINT`](crate::note_transport::NOTE_TRANSPORT_TESTNET_ENDPOINT)
165 ///
166 /// You still need to provide:
167 /// - A store (via `.store()`)
168 /// - An authenticator (via `.authenticator()`)
169 ///
170 /// All defaults can be overridden by calling the corresponding builder methods
171 /// after `for_testnet()`.
172 ///
173 /// # Example
174 ///
175 /// ```ignore
176 /// let client = ClientBuilder::for_testnet()
177 /// .store(store)
178 /// .authenticator(Arc::new(keystore))
179 /// .build()
180 /// .await?;
181 /// ```
182 #[must_use]
183 pub fn for_testnet() -> Self {
184 let endpoint = Endpoint::testnet();
185 Self {
186 rpc_api: Some(Arc::new(crate::rpc::GrpcClient::new(
187 &endpoint,
188 DEFAULT_GRPC_TIMEOUT_MS,
189 ))),
190 tx_prover: Some(Arc::new(RemoteTransactionProver::new(
191 TESTNET_PROVER_ENDPOINT.to_string(),
192 ))),
193 note_transport_config: Some(NoteTransportConfig {
194 endpoint: crate::note_transport::NOTE_TRANSPORT_TESTNET_ENDPOINT.to_string(),
195 timeout_ms: DEFAULT_GRPC_TIMEOUT_MS,
196 }),
197 endpoint: Some(endpoint),
198 ..Self::default()
199 }
200 }
201
202 /// Creates a `ClientBuilder` pre-configured for Miden devnet.
203 ///
204 /// This automatically configures:
205 /// - **RPC**: [`Endpoint::devnet()`]
206 /// - **Prover**: Remote prover at [`DEVNET_PROVER_ENDPOINT`]
207 /// - **Note transport**:
208 /// [`NOTE_TRANSPORT_DEVNET_ENDPOINT`](crate::note_transport::NOTE_TRANSPORT_DEVNET_ENDPOINT)
209 ///
210 /// You still need to provide:
211 /// - A store (via `.store()`)
212 /// - An authenticator (via `.authenticator()`)
213 ///
214 /// All defaults can be overridden by calling the corresponding builder methods
215 /// after `for_devnet()`.
216 ///
217 /// # Example
218 ///
219 /// ```ignore
220 /// let client = ClientBuilder::for_devnet()
221 /// .store(store)
222 /// .authenticator(Arc::new(keystore))
223 /// .build()
224 /// .await?;
225 /// ```
226 #[must_use]
227 pub fn for_devnet() -> Self {
228 let endpoint = Endpoint::devnet();
229 Self {
230 rpc_api: Some(Arc::new(crate::rpc::GrpcClient::new(
231 &endpoint,
232 DEFAULT_GRPC_TIMEOUT_MS,
233 ))),
234 tx_prover: Some(Arc::new(RemoteTransactionProver::new(
235 DEVNET_PROVER_ENDPOINT.to_string(),
236 ))),
237 note_transport_config: Some(NoteTransportConfig {
238 endpoint: crate::note_transport::NOTE_TRANSPORT_DEVNET_ENDPOINT.to_string(),
239 timeout_ms: DEFAULT_GRPC_TIMEOUT_MS,
240 }),
241 endpoint: Some(endpoint),
242 ..Self::default()
243 }
244 }
245
246 /// Creates a `ClientBuilder` pre-configured for localhost.
247 ///
248 /// This automatically configures:
249 /// - **RPC**: `http://localhost:57291`
250 /// - **Prover**: Local (default)
251 ///
252 /// Note transport is not configured by default for localhost.
253 ///
254 /// You still need to provide:
255 /// - A store (via `.store()`)
256 /// - An authenticator (via `.authenticator()`)
257 ///
258 /// All defaults can be overridden by calling the corresponding builder methods
259 /// after `for_localhost()`.
260 ///
261 /// # Example
262 ///
263 /// ```ignore
264 /// let client = ClientBuilder::for_localhost()
265 /// .store(store)
266 /// .authenticator(Arc::new(keystore))
267 /// .build()
268 /// .await?;
269 /// ```
270 #[must_use]
271 pub fn for_localhost() -> Self {
272 let endpoint = Endpoint::localhost();
273 Self {
274 rpc_api: Some(Arc::new(crate::rpc::GrpcClient::new(
275 &endpoint,
276 DEFAULT_GRPC_TIMEOUT_MS,
277 ))),
278 endpoint: Some(endpoint),
279 ..Self::default()
280 }
281 }
282}
283
284impl<AUTH> ClientBuilder<AUTH>
285where
286 AUTH: BuilderAuthenticator,
287{
288 /// Create a new `ClientBuilder` with default settings.
289 #[must_use]
290 pub fn new() -> Self {
291 Self::default()
292 }
293
294 /// Enable or disable debug mode.
295 #[must_use]
296 pub fn in_debug_mode(mut self, debug: DebugMode) -> Self {
297 self.in_debug_mode = debug;
298 self
299 }
300
301 /// Sets a custom RPC client directly.
302 #[must_use]
303 pub fn rpc(mut self, client: Arc<dyn NodeRpcClient>) -> Self {
304 self.rpc_api = Some(client);
305 self
306 }
307
308 /// Sets a gRPC client from the endpoint and optional timeout.
309 #[must_use]
310 #[cfg(feature = "tonic")]
311 pub fn grpc_client(mut self, endpoint: &crate::rpc::Endpoint, timeout_ms: Option<u64>) -> Self {
312 self.rpc_api = Some(Arc::new(crate::rpc::GrpcClient::new(
313 endpoint,
314 timeout_ms.unwrap_or(DEFAULT_GRPC_TIMEOUT_MS),
315 )));
316 self
317 }
318
319 /// Provide a store to be used by the client.
320 #[must_use]
321 pub fn store(mut self, store: Arc<dyn Store>) -> Self {
322 self.store = Some(StoreBuilder::Store(store));
323 self
324 }
325
326 /// Optionally provide a custom RNG.
327 #[must_use]
328 pub fn rng(mut self, rng: ClientRngBox) -> Self {
329 self.rng = Some(rng);
330 self
331 }
332
333 /// Optionally provide a custom authenticator instance.
334 #[must_use]
335 pub fn authenticator(mut self, authenticator: Arc<AUTH>) -> Self {
336 self.authenticator = Some(authenticator);
337 self
338 }
339
340 /// Optionally set a maximum number of blocks that the client can be behind the network.
341 /// By default, there's no maximum.
342 #[must_use]
343 pub fn max_block_number_delta(mut self, delta: u32) -> Self {
344 self.max_block_number_delta = Some(delta);
345 self
346 }
347
348 /// Sets the number of blocks after which pending transactions are considered stale and
349 /// discarded.
350 ///
351 /// If a transaction has not been included in a block within this many blocks after submission,
352 /// it will be discarded. If `None`, transactions will be kept indefinitely.
353 ///
354 /// By default, the delta is set to `TX_DISCARD_DELTA` (20 blocks).
355 #[must_use]
356 pub fn tx_discard_delta(mut self, delta: Option<u32>) -> Self {
357 self.tx_discard_delta = delta;
358 self
359 }
360
361 /// Sets the number of blocks after which pending transactions are considered stale and
362 /// discarded.
363 ///
364 /// This is an alias for [`tx_discard_delta`](Self::tx_discard_delta).
365 #[deprecated(since = "0.10.0", note = "Use `tx_discard_delta` instead")]
366 #[must_use]
367 pub fn tx_graceful_blocks(mut self, delta: Option<u32>) -> Self {
368 self.tx_discard_delta = delta;
369 self
370 }
371
372 /// Sets a custom note transport client directly.
373 #[must_use]
374 pub fn note_transport(mut self, client: Arc<dyn NoteTransportClient>) -> Self {
375 self.note_transport_api = Some(client);
376 self
377 }
378
379 /// Sets a custom transaction prover.
380 #[must_use]
381 pub fn prover(mut self, prover: Arc<dyn TransactionProver + Send + Sync>) -> Self {
382 self.tx_prover = Some(prover);
383 self
384 }
385
386 /// Returns the endpoint configured for this builder, if any.
387 ///
388 /// This is set automatically when using network-specific constructors like
389 /// [`for_testnet()`](Self::for_testnet), [`for_devnet()`](Self::for_devnet),
390 /// or [`for_localhost()`](Self::for_localhost).
391 #[must_use]
392 pub fn endpoint(&self) -> Option<&Endpoint> {
393 self.endpoint.as_ref()
394 }
395
396 /// Build and return the `Client`.
397 ///
398 /// # Errors
399 ///
400 /// - Returns an error if no RPC client was provided.
401 /// - Returns an error if the store cannot be instantiated.
402 #[allow(clippy::unused_async, unused_mut)]
403 pub async fn build(mut self) -> Result<Client<AUTH>, ClientError> {
404 // Determine the RPC client to use.
405 let rpc_api: Arc<dyn NodeRpcClient> = if let Some(client) = self.rpc_api {
406 client
407 } else {
408 return Err(ClientError::ClientInitializationError(
409 "RPC client is required. Call `.rpc(...)` or `.grpc_client(...)`.".into(),
410 ));
411 };
412
413 // Ensure a store was provided.
414 let store = if let Some(store_builder) = self.store {
415 match store_builder {
416 StoreBuilder::Store(store) => store,
417 StoreBuilder::Factory(factory) => factory.build().await?,
418 }
419 } else {
420 return Err(ClientError::ClientInitializationError(
421 "Store must be specified. Call `.store(...)`.".into(),
422 ));
423 };
424
425 // Use the provided RNG, or create a default one.
426 let rng = if let Some(user_rng) = self.rng {
427 user_rng
428 } else {
429 let mut seed_rng = rand::rng();
430 let coin_seed: [u64; 4] = seed_rng.random();
431 Box::new(RandomCoin::new(coin_seed.map(Felt::new).into()))
432 };
433
434 // Set default prover if not provided
435 let tx_prover: Arc<dyn TransactionProver + Send + Sync> =
436 self.tx_prover.unwrap_or_else(|| Arc::new(LocalTransactionProver::default()));
437
438 // Initialize genesis commitment in RPC client
439 if let Some((genesis, _)) = store.get_block_header_by_num(BlockNumber::GENESIS).await? {
440 rpc_api.set_genesis_commitment(genesis.commitment()).await?;
441 }
442
443 // Set the RPC client with persisted limits if available.
444 // If not present, they will be fetched from the node during sync_state.
445 if let Some(limits) = store.get_rpc_limits().await? {
446 rpc_api.set_rpc_limits(limits).await;
447 }
448
449 // Initialize note transport: prefer explicit client, fall back to config (tonic only)
450 #[cfg(feature = "tonic")]
451 if self.note_transport_api.is_none()
452 && let Some(config) = self.note_transport_config
453 {
454 let transport = crate::note_transport::grpc::GrpcNoteTransportClient::new(
455 config.endpoint,
456 config.timeout_ms,
457 );
458
459 self.note_transport_api = Some(Arc::new(transport) as Arc<dyn NoteTransportClient>);
460 }
461
462 // Create source manager for MASM source information
463 let source_manager: Arc<dyn SourceManagerSync> = Arc::new(DefaultSourceManager::default());
464
465 // Construct and return the Client
466 Ok(Client {
467 store,
468 rng: ClientRng::new(rng),
469 rpc_api,
470 tx_prover,
471 authenticator: self.authenticator,
472 source_manager,
473 exec_options: ExecutionOptions::new(
474 Some(MAX_TX_EXECUTION_CYCLES),
475 MIN_TX_EXECUTION_CYCLES,
476 ExecutionOptions::DEFAULT_CORE_TRACE_FRAGMENT_SIZE,
477 false,
478 self.in_debug_mode.into(),
479 )
480 .expect("Default executor's options should always be valid"),
481 tx_discard_delta: self.tx_discard_delta,
482 max_block_number_delta: self.max_block_number_delta,
483 note_transport_api: self.note_transport_api.clone(),
484 })
485 }
486}
487
488// FILESYSTEM KEYSTORE CONVENIENCE METHOD
489// ================================================================================================
490
491/// Marker trait to capture the bounds the builder requires for the authenticator type
492/// parameter.
493#[cfg(feature = "std")]
494pub trait BuilderAuthenticator: Keystore + From<FilesystemKeyStore> + 'static {}
495#[cfg(feature = "std")]
496impl<T> BuilderAuthenticator for T where T: Keystore + From<FilesystemKeyStore> + 'static {}
497
498#[cfg(not(feature = "std"))]
499pub trait BuilderAuthenticator: Keystore + 'static {}
500#[cfg(not(feature = "std"))]
501impl<T> BuilderAuthenticator for T where T: Keystore + 'static {}
502
503/// Convenience method for [`ClientBuilder`] when using [`FilesystemKeyStore`] as the authenticator.
504#[cfg(feature = "std")]
505impl ClientBuilder<FilesystemKeyStore> {
506 /// Creates a [`FilesystemKeyStore`] from the given path and sets it as the authenticator.
507 ///
508 /// This is a convenience method that creates the keystore and configures it as the
509 /// authenticator in a single call. The keystore provides transaction signing capabilities
510 /// using keys stored on the filesystem.
511 ///
512 /// # Errors
513 ///
514 /// Returns an error if the keystore cannot be created from the given path.
515 ///
516 /// # Example
517 ///
518 /// ```ignore
519 /// let client = ClientBuilder::new()
520 /// .rpc(rpc_client)
521 /// .store(store)
522 /// .filesystem_keystore("path/to/keys")?
523 /// .build()
524 /// .await?;
525 /// ```
526 pub fn filesystem_keystore(
527 self,
528 keystore_path: impl Into<std::path::PathBuf>,
529 ) -> Result<Self, ClientError> {
530 let keystore = FilesystemKeyStore::new(keystore_path.into())
531 .map_err(|e| ClientError::ClientInitializationError(e.to_string()))?;
532 Ok(self.authenticator(Arc::new(keystore)))
533 }
534}