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);
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))
}
pub fn get_local_miden_dir() -> Result<PathBuf, std::io::Error> {
std::env::current_dir().map(|cwd| cwd.join(MIDEN_DIR))
}
#[derive(Debug, Clone)]
pub enum ConfigKind {
Local,
Global,
}
#[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 {
#[serde(skip)]
pub config_dir: Option<ConfigDir>,
pub rpc: RpcConfig,
pub store_filepath: PathBuf,
pub secret_keys_directory: PathBuf,
pub token_symbol_map_filepath: PathBuf,
pub remote_prover_endpoint: Option<CliEndpoint>,
pub package_directory: PathBuf,
pub max_block_number_delta: Option<u32>,
pub note_transport: Option<NoteTransportConfig>,
pub remote_prover_timeout: Duration,
}
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> {
None
}
}
impl Default for CliConfig {
fn default() -> Self {
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 {
pub fn is_local(&self) -> bool {
matches!(&self.config_dir, Some(ConfigDir { kind: ConfigKind::Local, .. }))
}
pub fn is_global(&self) -> bool {
matches!(&self.config_dir, Some(ConfigDir { kind: ConfigKind::Global, .. }))
}
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)?;
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)
}
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)
}
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)
}
pub fn load() -> Result<Self, CliError> {
match Self::from_local_dir() {
Ok(config) => Ok(config),
Err(CliError::ConfigNotFound(_)) => {
Self::from_global_dir().map_err(|e| match e {
CliError::ConfigNotFound(_) => CliError::ConfigNotFound(
"Neither local nor global config file exists".to_string(),
),
other => other,
})
},
Err(e) => Err(e),
}
}
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())
})
}
fn resolve_relative_path(path: &mut PathBuf, base_dir: &Path) {
if path.is_relative() {
*path = base_dir.join(&*path);
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct RpcConfig {
pub endpoint: CliEndpoint,
pub timeout_ms: u64,
}
impl Default for RpcConfig {
fn default() -> Self {
Self {
endpoint: Endpoint::testnet().into(),
timeout_ms: 10000,
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct NoteTransportConfig {
pub endpoint: String,
pub timeout_ms: u64,
}
impl Default for NoteTransportConfig {
fn default() -> Self {
Self {
endpoint: NOTE_TRANSPORT_TESTNET_ENDPOINT.to_string(),
timeout_ms: 10000,
}
}
}
impl NoteTransportConfig {
pub fn devnet() -> Self {
Self {
endpoint: NOTE_TRANSPORT_DEVNET_ENDPOINT.to_string(),
timeout_ms: 10000,
}
}
}
#[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)
}
}
#[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 {
#[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(),
}
}
}