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