Skip to main content

miden_client/
builder.rs

1use alloc::string::{String, ToString};
2use alloc::sync::Arc;
3use std::boxed::Box;
4
5use miden_protocol::crypto::rand::RpoRandomCoin;
6use miden_protocol::{Felt, MAX_TX_EXECUTION_CYCLES, MIN_TX_EXECUTION_CYCLES};
7use miden_tx::ExecutionOptions;
8use miden_tx::auth::TransactionAuthenticator;
9use rand::Rng;
10
11use crate::keystore::FilesystemKeyStore;
12use crate::note_transport::NoteTransportClient;
13use crate::rpc::NodeRpcClient;
14use crate::store::{Store, StoreError};
15use crate::transaction::TransactionProver;
16use crate::{Client, ClientError, ClientRngBox, DebugMode};
17
18// CONSTANTS
19// ================================================================================================
20
21/// The number of blocks that are considered old enough to discard pending transactions.
22const TX_GRACEFUL_BLOCKS: u32 = 20;
23
24// AUTHENTICATOR CONFIGURATION
25// ================================================================================================
26
27/// Represents the configuration for an authenticator.
28///
29/// This enum defers authenticator instantiation until the build phase. The builder can accept
30/// either:
31///
32/// - A direct instance of an authenticator, or
33/// - A keystore path as a string which is then used as an authenticator.
34enum AuthenticatorConfig<AUTH> {
35    Path(String),
36    Instance(Arc<AUTH>),
37}
38
39// STORE BUILDER
40// ================================================================================================
41
42/// Allows the [`ClientBuilder`] to accept either an already built store instance or a factory for
43/// deferring the store instantiation.
44pub enum StoreBuilder {
45    Store(Arc<dyn Store>),
46    Factory(Box<dyn StoreFactory>),
47}
48
49/// Trait for building a store instance.
50#[async_trait::async_trait]
51pub trait StoreFactory {
52    /// Returns a new store instance.
53    async fn build(&self) -> Result<Arc<dyn Store>, StoreError>;
54}
55
56// CLIENT BUILDER
57// ================================================================================================
58
59/// A builder for constructing a Miden client.
60///
61/// This builder allows you to configure the various components required by the client, such as the
62/// RPC endpoint, store, RNG, and keystore. It is generic over the keystore type. By default, it
63/// uses [`FilesystemKeyStore`].
64pub struct ClientBuilder<AUTH> {
65    /// An optional custom RPC client. If provided, this takes precedence over `rpc_endpoint`.
66    rpc_api: Option<Arc<dyn NodeRpcClient>>,
67    /// An optional store provided by the user.
68    pub store: Option<StoreBuilder>,
69    /// An optional RNG provided by the user.
70    rng: Option<ClientRngBox>,
71    /// The keystore configuration provided by the user.
72    keystore: Option<AuthenticatorConfig<AUTH>>,
73    /// A flag to enable debug mode.
74    in_debug_mode: DebugMode,
75    /// The number of blocks that are considered old enough to discard pending transactions. If
76    /// `None`, there is no limit and transactions will be kept indefinitely.
77    tx_graceful_blocks: Option<u32>,
78    /// Maximum number of blocks the client can be behind the network for transactions and account
79    /// proofs to be considered valid.
80    max_block_number_delta: Option<u32>,
81    /// An optional custom note transport client.
82    note_transport_api: Option<Arc<dyn NoteTransportClient>>,
83    /// An optional custom transaction prover.
84    tx_prover: Option<Arc<dyn TransactionProver + Send + Sync>>,
85}
86
87impl<AUTH> Default for ClientBuilder<AUTH> {
88    fn default() -> Self {
89        Self {
90            rpc_api: None,
91            store: None,
92            rng: None,
93            keystore: None,
94            in_debug_mode: DebugMode::Disabled,
95            tx_graceful_blocks: Some(TX_GRACEFUL_BLOCKS),
96            max_block_number_delta: None,
97            note_transport_api: None,
98            tx_prover: None,
99        }
100    }
101}
102
103impl<AUTH> ClientBuilder<AUTH>
104where
105    AUTH: BuilderAuthenticator,
106{
107    /// Create a new `ClientBuilder` with default settings.
108    #[must_use]
109    pub fn new() -> Self {
110        Self::default()
111    }
112
113    /// Enable or disable debug mode.
114    #[must_use]
115    pub fn in_debug_mode(mut self, debug: DebugMode) -> Self {
116        self.in_debug_mode = debug;
117        self
118    }
119
120    /// Sets a custom RPC client directly.
121    #[must_use]
122    pub fn rpc(mut self, client: Arc<dyn NodeRpcClient>) -> Self {
123        self.rpc_api = Some(client);
124        self
125    }
126
127    /// Sets a gRPC client from the endpoint and optional timeout.
128    #[must_use]
129    #[cfg(feature = "tonic")]
130    pub fn grpc_client(mut self, endpoint: &crate::rpc::Endpoint, timeout_ms: Option<u64>) -> Self {
131        self.rpc_api =
132            Some(Arc::new(crate::rpc::GrpcClient::new(endpoint, timeout_ms.unwrap_or(10_000))));
133        self
134    }
135
136    /// Provide a store to be used by the client.
137    #[must_use]
138    pub fn store(mut self, store: Arc<dyn Store>) -> Self {
139        self.store = Some(StoreBuilder::Store(store));
140        self
141    }
142
143    /// Optionally provide a custom RNG.
144    #[must_use]
145    pub fn rng(mut self, rng: ClientRngBox) -> Self {
146        self.rng = Some(rng);
147        self
148    }
149
150    /// Optionally provide a custom authenticator instance.
151    #[must_use]
152    pub fn authenticator(mut self, authenticator: Arc<AUTH>) -> Self {
153        self.keystore = Some(AuthenticatorConfig::Instance(authenticator));
154        self
155    }
156
157    /// Optionally set a maximum number of blocks that the client can be behind the network.
158    /// By default, there's no maximum.
159    #[must_use]
160    pub fn max_block_number_delta(mut self, delta: u32) -> Self {
161        self.max_block_number_delta = Some(delta);
162        self
163    }
164
165    /// Optionally set a maximum number of blocks to wait for a transaction to be confirmed. If
166    /// `None`, there is no limit and transactions will be kept indefinitely.
167    /// By default, the maximum is set to `TX_GRACEFUL_BLOCKS`.
168    #[must_use]
169    pub fn tx_graceful_blocks(mut self, delta: Option<u32>) -> Self {
170        self.tx_graceful_blocks = delta;
171        self
172    }
173
174    /// **Required:** Provide the keystore path as a string.
175    ///
176    /// This stores the keystore path as a configuration option so that actual keystore
177    /// initialization is deferred until `build()`. This avoids panicking during builder chaining.
178    #[must_use]
179    pub fn filesystem_keystore(mut self, keystore_path: &str) -> Self {
180        self.keystore = Some(AuthenticatorConfig::Path(keystore_path.to_string()));
181        self
182    }
183
184    /// Sets a custom note transport client directly.
185    #[must_use]
186    pub fn note_transport(mut self, client: Arc<dyn NoteTransportClient>) -> Self {
187        self.note_transport_api = Some(client);
188        self
189    }
190
191    /// Sets a custom transaction prover.
192    #[must_use]
193    pub fn prover(mut self, prover: Arc<dyn TransactionProver + Send + Sync>) -> Self {
194        self.tx_prover = Some(prover);
195        self
196    }
197
198    /// Build and return the `Client`.
199    ///
200    /// # Errors
201    ///
202    /// - Returns an error if no RPC client or endpoint was provided.
203    /// - Returns an error if the store cannot be instantiated.
204    /// - Returns an error if the keystore is not specified or fails to initialize.
205    #[allow(clippy::unused_async, unused_mut)]
206    pub async fn build(mut self) -> Result<Client<AUTH>, ClientError> {
207        // Determine the RPC client to use.
208        let rpc_api: Arc<dyn NodeRpcClient> = if let Some(client) = self.rpc_api {
209            client
210        } else {
211            return Err(ClientError::ClientInitializationError(
212                "RPC client or endpoint is required. Call `.rpc(...)` or `.tonic_rpc_client(...)`."
213                    .into(),
214            ));
215        };
216
217        // Ensure a store was provided.
218        let store = if let Some(store_builder) = self.store {
219            match store_builder {
220                StoreBuilder::Store(store) => store,
221                StoreBuilder::Factory(factory) => factory.build().await?,
222            }
223        } else {
224            return Err(ClientError::ClientInitializationError(
225                "Store must be specified. Call `.store(...)`.".into(),
226            ));
227        };
228
229        // Use the provided RNG, or create a default one.
230        let rng = if let Some(user_rng) = self.rng {
231            user_rng
232        } else {
233            let mut seed_rng = rand::rng();
234            let coin_seed: [u64; 4] = seed_rng.random();
235            Box::new(RpoRandomCoin::new(coin_seed.map(Felt::new).into()))
236        };
237
238        // Initialize the authenticator.
239        let authenticator = match self.keystore {
240            Some(AuthenticatorConfig::Instance(authenticator)) => Some(authenticator),
241            Some(AuthenticatorConfig::Path(ref path)) => {
242                let keystore = FilesystemKeyStore::new(path.into())
243                    .map_err(|err| ClientError::ClientInitializationError(err.to_string()))?;
244                Some(Arc::new(AUTH::from(keystore)))
245            },
246            None => None,
247        };
248
249        Client::new(
250            rpc_api,
251            rng,
252            store,
253            authenticator,
254            ExecutionOptions::new(
255                Some(MAX_TX_EXECUTION_CYCLES),
256                MIN_TX_EXECUTION_CYCLES,
257                false,
258                self.in_debug_mode.into(),
259            )
260            .expect("Default executor's options should always be valid"),
261            self.tx_graceful_blocks,
262            self.max_block_number_delta,
263            self.note_transport_api,
264            self.tx_prover,
265        )
266        .await
267    }
268}
269
270// AUTH TRAIT MARKER
271// ================================================================================================
272
273/// Marker trait to capture the bounds the builder requires for the authenticator type
274/// parameter
275pub trait BuilderAuthenticator:
276    TransactionAuthenticator + From<FilesystemKeyStore> + 'static
277{
278}
279impl<T> BuilderAuthenticator for T where
280    T: TransactionAuthenticator + From<FilesystemKeyStore> + 'static
281{
282}