miden-client-cli 0.14.4

The official command line client for interacting with the Miden network
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
use core::fmt::Debug;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::Duration;

use figment::providers::{Format, Toml};
use figment::value::{Dict, Map};
use figment::{Figment, Metadata, Profile, Provider};
use miden_client::note_transport::{
    NOTE_TRANSPORT_DEVNET_ENDPOINT,
    NOTE_TRANSPORT_TESTNET_ENDPOINT,
};
use miden_client::rpc::Endpoint;
use serde::{Deserialize, Serialize};

use crate::errors::CliError;

pub const MIDEN_DIR: &str = ".miden";
pub const CLIENT_CONFIG_FILE_NAME: &str = "miden-client.toml";
pub const TOKEN_SYMBOL_MAP_FILENAME: &str = "token_symbol_map.toml";
pub const DEFAULT_PACKAGES_DIR: &str = "packages";
pub const STORE_FILENAME: &str = "store.sqlite3";
pub const KEYSTORE_DIRECTORY: &str = "keystore";
pub const DEFAULT_REMOTE_PROVER_TIMEOUT: Duration = Duration::from_secs(20);

/// Returns the global miden directory path.
///
/// If the `MIDEN_CLIENT_HOME` environment variable is set, returns that path directly.
/// Otherwise, returns the `.miden` directory in the user's home directory.
pub fn get_global_miden_dir() -> Result<PathBuf, std::io::Error> {
    if let Ok(miden_home) = std::env::var("MIDEN_CLIENT_HOME") {
        return Ok(PathBuf::from(miden_home));
    }
    dirs::home_dir()
        .ok_or_else(|| {
            std::io::Error::new(std::io::ErrorKind::NotFound, "Could not determine home directory")
        })
        .map(|home| home.join(MIDEN_DIR))
}

/// Returns the local miden directory path relative to the current working directory
pub fn get_local_miden_dir() -> Result<PathBuf, std::io::Error> {
    std::env::current_dir().map(|cwd| cwd.join(MIDEN_DIR))
}

// CLI CONFIG
// ================================================================================================

/// Whether the configuration was loaded from the local or global `.miden` directory.
#[derive(Debug, Clone)]
pub enum ConfigKind {
    Local,
    Global,
}

/// The `.miden` directory from which the configuration was loaded.
#[derive(Debug, Clone)]
pub struct ConfigDir {
    pub path: PathBuf,
    pub kind: ConfigKind,
}

impl std::fmt::Display for ConfigDir {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} ({:?})", self.path.display(), self.kind)
    }
}

#[derive(Debug, Deserialize, Serialize)]
pub struct CliConfig {
    /// The directory this configuration was loaded from. Not part of the TOML file.
    #[serde(skip)]
    pub config_dir: Option<ConfigDir>,
    /// Describes settings related to the RPC endpoint.
    pub rpc: RpcConfig,
    /// Path to the `SQLite` store file.
    pub store_filepath: PathBuf,
    /// Path to the directory that contains the secret key files.
    pub secret_keys_directory: PathBuf,
    /// Path to the file containing the token symbol map.
    pub token_symbol_map_filepath: PathBuf,
    /// RPC endpoint for the remote prover. If this isn't present, a local prover will be used.
    pub remote_prover_endpoint: Option<CliEndpoint>,
    /// Path to the directory from where packages will be loaded.
    pub package_directory: PathBuf,
    /// Maximum number of blocks the client can be behind the network for transactions and account
    /// proofs to be considered valid.
    pub max_block_number_delta: Option<u32>,
    /// Describes settings related to the note transport endpoint.
    pub note_transport: Option<NoteTransportConfig>,
    /// Timeout for the remote prover requests.
    pub remote_prover_timeout: Duration,
}

// Make `ClientConfig` a provider itself for composability.
impl Provider for CliConfig {
    fn metadata(&self) -> Metadata {
        Metadata::named("CLI Config")
    }

    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
        figment::providers::Serialized::defaults(CliConfig::default()).data()
    }

    fn profile(&self) -> Option<Profile> {
        // Optionally, a profile that's selected by default.
        None
    }
}

/// Default implementation for `CliConfig`.
///
/// **Note**: This implementation is primarily used by the [`figment`] `Provider` trait
/// (see [`CliConfig::data()`]) to provide default values during configuration merging.
/// The paths returned are relative and intended to be resolved against a `.miden` directory.
///
/// For loading configuration from the filesystem, use [`CliConfig::load()`] instead.
impl Default for CliConfig {
    fn default() -> Self {
        // Create paths relative to the config file location (which is in .miden directory)
        // These will be resolved relative to the .miden directory when the config is loaded
        Self {
            config_dir: None,
            rpc: RpcConfig::default(),
            store_filepath: PathBuf::from(STORE_FILENAME),
            secret_keys_directory: PathBuf::from(KEYSTORE_DIRECTORY),
            token_symbol_map_filepath: PathBuf::from(TOKEN_SYMBOL_MAP_FILENAME),
            remote_prover_endpoint: None,
            package_directory: PathBuf::from(DEFAULT_PACKAGES_DIR),
            max_block_number_delta: None,
            note_transport: None,
            remote_prover_timeout: DEFAULT_REMOTE_PROVER_TIMEOUT,
        }
    }
}

impl CliConfig {
    /// Returns `true` when this config was loaded from the local `.miden` directory.
    ///
    /// This is typically set when loading via [`CliConfig::from_local_dir`] or
    /// [`CliConfig::load`] (when local takes precedence).
    pub fn is_local(&self) -> bool {
        matches!(&self.config_dir, Some(ConfigDir { kind: ConfigKind::Local, .. }))
    }

    /// Returns `true` when this config was loaded from the global `.miden` directory.
    ///
    /// This is typically set when loading via [`CliConfig::from_global_dir`] or
    /// [`CliConfig::load`] (when local config is not available).
    pub fn is_global(&self) -> bool {
        matches!(&self.config_dir, Some(ConfigDir { kind: ConfigKind::Global, .. }))
    }

    /// Loads configuration from a specific `.miden` directory.
    ///
    /// # ⚠️ WARNING: Advanced Use Only
    ///
    /// **This method bypasses the standard CLI configuration discovery logic.**
    ///
    /// This method loads config from an explicitly specified directory, which means:
    /// - It does NOT check for local `.miden` directory first
    /// - It does NOT fall back to global `~/.miden` directory
    /// - It does NOT follow CLI priority logic
    ///
    /// ## Recommended Alternative
    ///
    /// For standard CLI-like configuration loading, use:
    /// ```ignore
    /// CliConfig::load()  // Respects local → global priority
    /// ```
    ///
    /// Or for client initialization:
    /// ```ignore
    /// CliClient::new(debug_mode).await?
    /// ```
    ///
    /// ## When to use this method
    ///
    /// - **Testing**: When you need to test with config from a specific directory
    /// - **Explicit Control**: When you must load from a non-standard location
    ///
    /// # Arguments
    ///
    /// * `miden_dir` - Path to the `.miden` directory containing `miden-client.toml`
    ///
    /// # Returns
    ///
    /// A configured [`CliConfig`] instance with resolved paths.
    ///
    /// # Errors
    ///
    /// Returns a [`CliError`](crate::errors::CliError):
    /// - [`CliError::ConfigNotFound`](crate::errors::CliError::ConfigNotFound) if the config file
    ///   doesn't exist in the specified directory
    /// - [`CliError::Config`](crate::errors::CliError::Config) if configuration file parsing fails
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use std::path::PathBuf;
    ///
    /// use miden_client_cli::config::CliConfig;
    ///
    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// // ⚠️ This bypasses standard config discovery!
    /// let config = CliConfig::from_dir(&PathBuf::from("/path/to/.miden"))?;
    ///
    /// // ✅ Prefer this for CLI-like behavior:
    /// let config = CliConfig::load()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn from_dir(miden_dir: &Path) -> Result<Self, CliError> {
        let config_path = miden_dir.join(CLIENT_CONFIG_FILE_NAME);

        if !config_path.exists() {
            return Err(CliError::ConfigNotFound(format!(
                "Config file does not exist at {}",
                config_path.display()
            )));
        }

        let mut cli_config = Self::load_from_file(&config_path)?;

        // Resolve all relative paths relative to the .miden directory
        Self::resolve_relative_path(&mut cli_config.store_filepath, miden_dir);
        Self::resolve_relative_path(&mut cli_config.secret_keys_directory, miden_dir);
        Self::resolve_relative_path(&mut cli_config.token_symbol_map_filepath, miden_dir);
        Self::resolve_relative_path(&mut cli_config.package_directory, miden_dir);

        Ok(cli_config)
    }

    /// Loads configuration from the local `.miden` directory (current working directory).
    ///
    /// # ⚠️ WARNING: Advanced Use Only
    ///
    /// **This method bypasses the standard CLI configuration discovery logic.**
    ///
    /// This method ONLY checks the local directory and does NOT fall back to the global
    /// configuration if the local config doesn't exist. This differs from CLI behavior.
    ///
    /// ## Recommended Alternative
    ///
    /// For standard CLI-like behavior:
    /// ```ignore
    /// CliConfig::load()  // Respects local → global fallback
    /// CliClient::new(debug_mode).await?
    /// ```
    ///
    /// ## When to use this method
    ///
    /// - **Testing**: When you need to ensure only local config is used
    /// - **Explicit Control**: When you must avoid global config
    ///
    /// # Returns
    ///
    /// A configured [`CliConfig`] instance.
    ///
    /// # Errors
    ///
    /// Returns a [`CliError`](crate::errors::CliError) if:
    /// - Cannot determine current working directory
    /// - The config file doesn't exist locally
    /// - Configuration file parsing fails
    pub fn from_local_dir() -> Result<Self, CliError> {
        let local_miden_dir = get_local_miden_dir()?;
        let mut config = Self::from_dir(&local_miden_dir)?;
        config.config_dir = Some(ConfigDir {
            path: local_miden_dir,
            kind: ConfigKind::Local,
        });
        Ok(config)
    }

    /// Loads configuration from the global `.miden` directory (user's home directory).
    ///
    /// # ⚠️ WARNING: Advanced Use Only
    ///
    /// **This method bypasses the standard CLI configuration discovery logic.**
    ///
    /// This method ONLY checks the global directory and does NOT check for local config first.
    /// This differs from CLI behavior which prioritizes local config over global.
    ///
    /// ## Recommended Alternative
    ///
    /// For standard CLI-like behavior:
    /// ```ignore
    /// CliConfig::load()  // Respects local → global priority
    /// CliClient::new(debug_mode).await?
    /// ```
    ///
    /// ## When to use this method
    ///
    /// - **Testing**: When you need to ensure only global config is used
    /// - **Explicit Control**: When you must bypass local config
    ///
    /// # Returns
    ///
    /// A configured [`CliConfig`] instance.
    ///
    /// # Errors
    ///
    /// Returns a [`CliError`](crate::errors::CliError) if:
    /// - Cannot determine home directory
    /// - The config file doesn't exist globally
    /// - Configuration file parsing fails
    pub fn from_global_dir() -> Result<Self, CliError> {
        let global_miden_dir = get_global_miden_dir().map_err(|e| {
            CliError::Config(Box::new(e), "Failed to determine global config directory".to_string())
        })?;
        let mut config = Self::from_dir(&global_miden_dir)?;
        config.config_dir = Some(ConfigDir {
            path: global_miden_dir,
            kind: ConfigKind::Global,
        });
        Ok(config)
    }

    /// Loads configuration from system directories with priority: local first, then global
    /// fallback.
    ///
    /// # ✅ Recommended Method
    ///
    /// **This is the recommended method for loading CLI configuration as it follows the same
    /// discovery logic as the CLI tool itself.**
    ///
    /// This method searches for configuration files in the following order:
    /// 1. Local `.miden/miden-client.toml` in the current working directory
    /// 2. Global `.miden/miden-client.toml` in the home directory (fallback)
    ///
    /// This matches the CLI's configuration priority logic. For most use cases, you should
    /// use [`CliClient::new()`](crate::CliClient::new) instead, which uses this method
    /// internally.
    ///
    /// # Returns
    ///
    /// A configured [`CliConfig`] instance.
    ///
    /// # Errors
    ///
    /// Returns a [`CliError`](crate::errors::CliError):
    /// - [`CliError::ConfigNotFound`](crate::errors::CliError::ConfigNotFound) if neither local nor
    ///   global config file exists
    /// - [`CliError::Config`](crate::errors::CliError::Config) if configuration file parsing fails
    ///
    /// Note: If a local config file exists but has parse errors, the error is returned
    /// immediately without falling back to global config.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use miden_client_cli::config::CliConfig;
    ///
    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// // ✅ Recommended: Loads from local .miden dir if it exists, otherwise from global
    /// let config = CliConfig::load()?;
    ///
    /// // Or even better, use CliClient directly:
    /// // let client = CliClient::new(DebugMode::Disabled).await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn load() -> Result<Self, CliError> {
        // Try local first
        match Self::from_local_dir() {
            Ok(config) => Ok(config),
            // Only fall back to global if the local config file was not found
            // (not for parse errors or other issues)
            Err(CliError::ConfigNotFound(_)) => {
                // Fall back to global
                Self::from_global_dir().map_err(|e| match e {
                    CliError::ConfigNotFound(_) => CliError::ConfigNotFound(
                        "Neither local nor global config file exists".to_string(),
                    ),
                    other => other,
                })
            },
            // For other errors (like parse errors), propagate them immediately
            Err(e) => Err(e),
        }
    }

    /// Loads the client configuration from a TOML file.
    fn load_from_file(config_file: &Path) -> Result<Self, CliError> {
        Figment::from(Toml::file(config_file)).extract().map_err(|err| {
            CliError::Config("failed to load config file".to_string().into(), err.to_string())
        })
    }

    /// Resolves a relative path against a base directory.
    /// If the path is already absolute, it remains unchanged.
    fn resolve_relative_path(path: &mut PathBuf, base_dir: &Path) {
        if path.is_relative() {
            *path = base_dir.join(&*path);
        }
    }
}

// RPC CONFIG
// ================================================================================================

/// Settings for the RPC client.
#[derive(Debug, Deserialize, Serialize)]
pub struct RpcConfig {
    /// Address of the Miden node to connect to.
    pub endpoint: CliEndpoint,
    /// Timeout for the RPC api requests, in milliseconds.
    pub timeout_ms: u64,
}

impl Default for RpcConfig {
    fn default() -> Self {
        Self {
            endpoint: Endpoint::testnet().into(),
            timeout_ms: 10000,
        }
    }
}

// NOTE TRANSPORT CONFIG
// ================================================================================================

/// Settings for the note transport client.
#[derive(Debug, Deserialize, Serialize)]
pub struct NoteTransportConfig {
    /// Address of the Miden Note Transport node to connect to.
    pub endpoint: String,
    /// Timeout for the Note Transport RPC api requests, in milliseconds.
    pub timeout_ms: u64,
}

impl Default for NoteTransportConfig {
    fn default() -> Self {
        Self {
            endpoint: NOTE_TRANSPORT_TESTNET_ENDPOINT.to_string(),
            timeout_ms: 10000,
        }
    }
}

impl NoteTransportConfig {
    /// Returns a `NoteTransportConfig` for the devnet network.
    pub fn devnet() -> Self {
        Self {
            endpoint: NOTE_TRANSPORT_DEVNET_ENDPOINT.to_string(),
            timeout_ms: 10000,
        }
    }
}

// CLI ENDPOINT
// ================================================================================================

#[derive(Clone, Debug)]
pub struct CliEndpoint(pub Endpoint);

impl Display for CliEndpoint {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl TryFrom<&str> for CliEndpoint {
    type Error = String;

    fn try_from(endpoint: &str) -> Result<Self, Self::Error> {
        let endpoint = Endpoint::try_from(endpoint).map_err(|err| err.clone())?;
        Ok(Self(endpoint))
    }
}

impl From<Endpoint> for CliEndpoint {
    fn from(endpoint: Endpoint) -> Self {
        Self(endpoint)
    }
}

impl TryFrom<Network> for CliEndpoint {
    type Error = CliError;

    fn try_from(value: Network) -> Result<Self, Self::Error> {
        Ok(Self(Endpoint::try_from(value.to_rpc_endpoint().as_str()).map_err(|err| {
            CliError::Parse(err.into(), "Failed to parse RPC endpoint".to_string())
        })?))
    }
}

impl From<CliEndpoint> for Endpoint {
    fn from(endpoint: CliEndpoint) -> Self {
        endpoint.0
    }
}

impl From<&CliEndpoint> for Endpoint {
    fn from(endpoint: &CliEndpoint) -> Self {
        endpoint.0.clone()
    }
}

impl Serialize for CliEndpoint {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for CliEndpoint {
    fn deserialize<D>(deserializer: D) -> Result<CliEndpoint, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let endpoint = String::deserialize(deserializer)?;
        CliEndpoint::try_from(endpoint.as_str()).map_err(serde::de::Error::custom)
    }
}

// NETWORK
// ================================================================================================

/// Represents the network to which the client connects. It is used to determine the RPC endpoint
/// and network ID for the CLI.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum Network {
    Custom(String),
    Devnet,
    Localhost,
    Testnet,
}

impl FromStr for Network {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "devnet" => Ok(Network::Devnet),
            "localhost" => Ok(Network::Localhost),
            "testnet" => Ok(Network::Testnet),
            custom => Ok(Network::Custom(custom.to_string())),
        }
    }
}

impl Network {
    /// Converts the Network variant to its corresponding RPC endpoint string
    #[allow(dead_code)]
    pub fn to_rpc_endpoint(&self) -> String {
        match self {
            Network::Custom(custom) => custom.clone(),
            Network::Devnet => Endpoint::devnet().to_string(),
            Network::Localhost => Endpoint::default().to_string(),
            Network::Testnet => Endpoint::testnet().to_string(),
        }
    }
}