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