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