Skip to main content

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}