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