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