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