Skip to main content

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}