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