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