diff --git a/src/commands/config/add_connection/mod.rs b/src/commands/config/add_connection/mod.rs index 82add09bb..5c0cc9fda 100644 --- a/src/commands/config/add_connection/mod.rs +++ b/src/commands/config/add_connection/mod.rs @@ -38,7 +38,8 @@ pub struct AddNetworkConnection { #[interactive_clap(skip_default_input_arg)] fastnear_url: Option, #[interactive_clap(long)] - staking_pools_factory_account_id: crate::types::account_id::AccountId, + #[interactive_clap(skip_default_input_arg)] + staking_pools_factory_account_id: Option, } #[derive(Debug, Clone)] @@ -77,11 +78,13 @@ impl AddNetworkConnectionContext { staking_pools_factory_account_id: scope .staking_pools_factory_account_id .clone() - .into(), + .map(|staking_pools_factory_account_id| { + staking_pools_factory_account_id.into() + }), }, ); eprintln!(); - crate::common::write_config_toml(config)?; + config.write_config_toml()?; eprintln!( "Network connection \"{}\" was successfully added to config.toml", &scope.connection_name @@ -244,4 +247,29 @@ impl AddNetworkConnection { Ok(None) } } + + fn input_staking_pools_factory_account_id( + _context: &crate::GlobalContext, + ) -> color_eyre::eyre::Result> { + eprintln!(); + #[derive(strum_macros::Display)] + enum ConfirmOptions { + #[strum(to_string = "Yes, I want to enter the staking pools factory account ID")] + Yes, + #[strum(to_string = "No, I don't want to enter the staking pools factory account ID")] + No, + } + let select_choose_input = Select::new( + "Do you want to enter the staking pools factory account ID?", + vec![ConfirmOptions::Yes, ConfirmOptions::No], + ) + .prompt()?; + if let ConfirmOptions::Yes = select_choose_input { + let account_id: crate::types::account_id::AccountId = + CustomType::new("What is the staking pools factory account ID?").prompt()?; + Ok(Some(account_id)) + } else { + Ok(None) + } + } } diff --git a/src/commands/config/delete_connection/mod.rs b/src/commands/config/delete_connection/mod.rs index d01238e1e..c03bba59b 100644 --- a/src/commands/config/delete_connection/mod.rs +++ b/src/commands/config/delete_connection/mod.rs @@ -18,7 +18,7 @@ impl DeleteNetworkConnectionContext { let mut config = previous_context.config; config.network_connection.remove(&scope.connection_name); eprintln!(); - crate::common::write_config_toml(config)?; + config.write_config_toml()?; eprintln!( "Network connection \"{}\" was successfully removed from config.toml", &scope.connection_name diff --git a/src/common.rs b/src/common.rs index d6f670a7b..b92dab4fc 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1253,39 +1253,6 @@ pub fn save_access_key_to_legacy_keychain( } } -pub fn get_config_toml() -> color_eyre::eyre::Result { - if let Some(mut path_config_toml) = dirs::config_dir() { - path_config_toml.extend(&["near-cli", "config.toml"]); - - if !path_config_toml.is_file() { - write_config_toml(crate::config::Config::default())?; - }; - let config_toml = std::fs::read_to_string(&path_config_toml)?; - toml::from_str(&config_toml).or_else(|err| { - eprintln!("Warning: `near` CLI configuration file stored at {path_config_toml:?} could not be parsed due to: {err}"); - eprintln!("Note: The default configuration printed below will be used instead:\n"); - let default_config = crate::config::Config::default(); - eprintln!("{}", toml::to_string(&default_config)?); - Ok(default_config) - }) - } else { - Ok(crate::config::Config::default()) - } -} -pub fn write_config_toml(config: crate::config::Config) -> CliResult { - let config_toml = toml::to_string(&config)?; - let mut path_config_toml = dirs::config_dir().wrap_err("Impossible to get your config dir!")?; - path_config_toml.push("near-cli"); - std::fs::create_dir_all(&path_config_toml)?; - path_config_toml.push("config.toml"); - std::fs::File::create(&path_config_toml) - .wrap_err_with(|| format!("Failed to create file: {path_config_toml:?}"))? - .write(config_toml.as_bytes()) - .wrap_err_with(|| format!("Failed to write to file: {path_config_toml:?}"))?; - eprintln!("Note: `near` CLI configuration is stored in {path_config_toml:?}"); - Ok(()) -} - pub fn try_external_subcommand_execution(error: clap::Error) -> CliResult { let (subcommand, args) = { let mut args = std::env::args().skip(1); @@ -1525,8 +1492,14 @@ pub fn fetch_validators_api( pub fn fetch_validators_rpc( json_rpc_client: &near_jsonrpc_client::JsonRpcClient, - staking_pools_factory_account_id: near_primitives::types::AccountId, + staking_pools_factory_account_id: Option, ) -> color_eyre::Result> { + let Some(staking_pools_factory_account_id) = staking_pools_factory_account_id else { + return Err(color_eyre::Report::msg( + "Staking pools factory account ID is not set for selected network", + )); + }; + let query_view_method_response = json_rpc_client .blocking_call(near_jsonrpc_client::methods::query::RpcQueryRequest { block_reference: near_primitives::types::Finality::Final.into(), diff --git a/src/config/migrations.rs b/src/config/migrations.rs new file mode 100644 index 000000000..e0e9aada4 --- /dev/null +++ b/src/config/migrations.rs @@ -0,0 +1,94 @@ +use crate::config::Config as ConfigV2; +use crate::config::NetworkConfig as NetworkConfigV2; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ConfigV1 { + pub credentials_home_dir: std::path::PathBuf, + pub network_connection: linked_hash_map::LinkedHashMap, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct NetworkConfigV1 { + pub network_name: String, + pub rpc_url: url::Url, + pub rpc_api_key: Option, + pub wallet_url: url::Url, + pub explorer_transaction_url: url::Url, + // https://github.com/near/near-cli-rs/issues/116 + pub linkdrop_account_id: Option, + // https://docs.near.org/social/contract + pub near_social_db_contract_account_id: Option, + pub faucet_url: Option, + pub meta_transaction_relayer_url: Option, +} + +impl From for ConfigV2 { + fn from(config: ConfigV1) -> Self { + ConfigV2 { + credentials_home_dir: config.credentials_home_dir, + network_connection: config + .network_connection + .into_iter() + .map(|(network_name, network_config)| (network_name, network_config.into())) + .collect(), + } + } +} + +impl From for NetworkConfigV2 { + fn from(network_config: NetworkConfigV1) -> Self { + match network_config.network_name.as_str() { + "mainnet" => NetworkConfigV2 { + network_name: network_config.network_name, + rpc_url: network_config.rpc_url, + wallet_url: network_config.wallet_url, + explorer_transaction_url: network_config.explorer_transaction_url, + rpc_api_key: network_config.rpc_api_key, + linkdrop_account_id: network_config.linkdrop_account_id, + near_social_db_contract_account_id: network_config + .near_social_db_contract_account_id, + faucet_url: network_config.faucet_url, + meta_transaction_relayer_url: network_config.meta_transaction_relayer_url, + fastnear_url: Some(String::from("https://api.fastnear.com")), + staking_pools_factory_account_id: Some("poolv1.near".parse().unwrap()), + }, + "testnet" => NetworkConfigV2 { + network_name: network_config.network_name, + rpc_url: network_config.rpc_url, + wallet_url: network_config.wallet_url, + explorer_transaction_url: network_config.explorer_transaction_url, + rpc_api_key: network_config.rpc_api_key, + linkdrop_account_id: network_config.linkdrop_account_id, + near_social_db_contract_account_id: network_config + .near_social_db_contract_account_id, + faucet_url: network_config.faucet_url, + meta_transaction_relayer_url: network_config.meta_transaction_relayer_url, + fastnear_url: None, + staking_pools_factory_account_id: Some("pool.f863973.m0".parse().unwrap()), + }, + _ => NetworkConfigV2 { + network_name: network_config.network_name, + rpc_url: network_config.rpc_url, + wallet_url: network_config.wallet_url, + explorer_transaction_url: network_config.explorer_transaction_url, + rpc_api_key: network_config.rpc_api_key, + linkdrop_account_id: network_config.linkdrop_account_id, + near_social_db_contract_account_id: network_config + .near_social_db_contract_account_id, + faucet_url: network_config.faucet_url, + meta_transaction_relayer_url: network_config.meta_transaction_relayer_url, + fastnear_url: None, + staking_pools_factory_account_id: None, + }, + } + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(tag = "version")] +pub enum ConfigVersion { + #[serde(rename = "1")] + V1(ConfigV1), + #[serde(rename = "2")] + V2(ConfigV2), +} diff --git a/src/config.rs b/src/config/mod.rs similarity index 59% rename from src/config.rs rename to src/config/mod.rs index 0be45b614..3e47e2cda 100644 --- a/src/config.rs +++ b/src/config/mod.rs @@ -1,5 +1,9 @@ -use color_eyre::eyre::WrapErr; -use std::str::FromStr; +mod migrations; + +pub type CliResult = color_eyre::eyre::Result<()>; + +use color_eyre::eyre::{ContextCompat, WrapErr}; +use std::{io::Write, str::FromStr}; use tracing_indicatif::span_ext::IndicatifSpanExt; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -30,7 +34,7 @@ impl Default for Config { faucet_url: None, meta_transaction_relayer_url: None, fastnear_url: Some(String::from("https://api.fastnear.com")), - staking_pools_factory_account_id: "poolv1.near".parse().unwrap(), + staking_pools_factory_account_id: Some("poolv1.near".parse().unwrap()), }, ); network_connection.insert( @@ -48,7 +52,7 @@ impl Default for Config { faucet_url: Some("https://helper.nearprotocol.com/account".parse().unwrap()), meta_transaction_relayer_url: None, fastnear_url: None, - staking_pools_factory_account_id: "pool.f863973.m0".parse().unwrap(), + staking_pools_factory_account_id: Some("pool.f863973.m0".parse().unwrap()), }, ); @@ -66,6 +70,57 @@ impl Config { .map(|(_, network_config)| network_config.network_name.clone()) .collect() } + + pub fn into_latest_version(self) -> migrations::ConfigVersion { + migrations::ConfigVersion::V2(self) + } + + pub fn get_config_toml() -> color_eyre::eyre::Result { + if let Some(mut path_config_toml) = dirs::config_dir() { + path_config_toml.extend(&["near-cli", "config.toml"]); + + if !path_config_toml.is_file() { + Self::write_config_toml(crate::config::Config::default())?; + }; + + let config_toml = std::fs::read_to_string(&path_config_toml)?; + + let config_version = toml::from_str::(&config_toml).or_else::(|err| { + if let Ok(config_v1) = toml::from_str::(&config_toml) { + Ok(migrations::ConfigVersion::V1(config_v1)) + } else { + eprintln!("Warning: `near` CLI configuration file stored at {path_config_toml:?} could not be parsed due to: {err}"); + eprintln!("Note: The default configuration printed below will be used instead:\n"); + let default_config = crate::config::Config::default(); + eprintln!("{}", toml::to_string(&default_config)?); + Ok(default_config.into_latest_version()) + } + })?; + + Ok(config_version.into()) + } else { + Ok(crate::config::Config::default()) + } + } + + pub fn write_config_toml(self) -> CliResult { + let config_toml = toml::to_string(&self.into_latest_version())?; + let mut path_config_toml = + dirs::config_dir().wrap_err("Impossible to get your config dir!")?; + + path_config_toml.push("near-cli"); + std::fs::create_dir_all(&path_config_toml)?; + path_config_toml.push("config.toml"); + + std::fs::File::create(&path_config_toml) + .wrap_err_with(|| format!("Failed to create file: {path_config_toml:?}"))? + .write(config_toml.as_bytes()) + .wrap_err_with(|| format!("Failed to write to file: {path_config_toml:?}"))?; + + eprintln!("Note: `near` CLI configuration is stored in {path_config_toml:?}"); + + Ok(()) + } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -82,7 +137,7 @@ pub struct NetworkConfig { pub faucet_url: Option, pub meta_transaction_relayer_url: Option, pub fastnear_url: Option, - pub staking_pools_factory_account_id: near_primitives::types::AccountId, + pub staking_pools_factory_account_id: Option, } impl NetworkConfig { @@ -115,3 +170,18 @@ impl NetworkConfig { } } } + +impl From for Config { + fn from(mut config_version: migrations::ConfigVersion) -> Self { + loop { + config_version = match config_version { + migrations::ConfigVersion::V1(config_v1) => { + migrations::ConfigVersion::V2(config_v1.into()) + } + migrations::ConfigVersion::V2(config_v2) => { + break config_v2; + } + }; + } + } +} diff --git a/src/js_command_match/generate_key.rs b/src/js_command_match/generate_key.rs index e0ac4679d..8740ddd4e 100644 --- a/src/js_command_match/generate_key.rs +++ b/src/js_command_match/generate_key.rs @@ -12,7 +12,7 @@ pub struct GenerateKeyArgs { impl GenerateKeyArgs { pub fn to_cli_args(&self, network_config: String) -> color_eyre::eyre::Result> { - let config = crate::common::get_config_toml()?; + let config = crate::config::Config::get_config_toml()?; let mut generation_method = "use-auto-generation".to_string(); if self.use_ledger_key.is_some() { generation_method = "use-ledger".to_string(); diff --git a/src/js_command_match/set_api_key.rs b/src/js_command_match/set_api_key.rs index 65069d14f..09c81349f 100644 --- a/src/js_command_match/set_api_key.rs +++ b/src/js_command_match/set_api_key.rs @@ -9,7 +9,7 @@ pub struct SetApiKeyArgs { impl SetApiKeyArgs { pub fn to_cli_args(&self, network_name: String) -> color_eyre::eyre::Result> { - let config = crate::common::get_config_toml()?; + let config = crate::config::Config::get_config_toml()?; let network_config = match config.network_connection.get(&network_name) { Some(network_config) => network_config, None => { diff --git a/src/main.rs b/src/main.rs index 99933ed37..504848730 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,7 +59,7 @@ impl From for crate::GlobalContext { } fn main() -> crate::common::CliResult { - let config = crate::common::get_config_toml()?; + let config = crate::config::Config::get_config_toml()?; if !crate::common::is_used_account_list_exist(&config.credentials_home_dir) { crate::common::create_used_account_list_from_keychain(&config.credentials_home_dir)?;