From 2a91dd818de44fcbc5c1dda9056c8015b5bb4fad Mon Sep 17 00:00:00 2001 From: Malte Herrmann Date: Thu, 16 Oct 2025 23:00:17 +0200 Subject: [PATCH 1/8] commit wip introducing network config --- src/block.rs | 26 +++++-------- src/cli.rs | 2 + src/config.rs | 62 +++++++++++++++++++++++++++++ src/errors.rs | 2 +- src/helper.rs | 33 +++++++++------- src/inputs.rs | 103 ++++++++++++++++++++++++------------------------- src/main.rs | 1 + src/version.rs | 39 +++++++++---------- 8 files changed, 163 insertions(+), 105 deletions(-) create mode 100644 src/config.rs diff --git a/src/block.rs b/src/block.rs index 25deb26..25e9b3a 100644 --- a/src/block.rs +++ b/src/block.rs @@ -1,5 +1,4 @@ -use crate::errors::BlockError; -use crate::{http::get_body, network::Network}; +use crate::{config::NetworkConfig, errors::BlockError, http::get_body}; use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -64,11 +63,7 @@ pub fn round_to_nearest_500(height: u64) -> u64 { /// Gets the latest block from the Evmos network. async fn get_latest_block(base_url: &Url) -> Result { - process_block_body( - get_body( - base_url.join(LATEST_BLOCK_ENDPOINT)? - ).await? - ) + process_block_body(get_body(base_url.join(LATEST_BLOCK_ENDPOINT)?).await?) } /// Gets the block at the given height from the Evmos network. @@ -77,20 +72,17 @@ async fn get_block(base_url: &Url, height: u64) -> Result { get_body( base_url .join(BLOCKS_ENDPOINT)? - .join(height.to_string().as_str())? - ).await? + .join(height.to_string().as_str())?, + ) + .await?, ) } /// Returns the appropriate REST provider for the given network. -pub fn get_rest_provider(network: Network) -> Url { - let base_url = match network { - Network::LocalNode => "http://localhost:1317", - Network::Mainnet => "https://rest.evmos.lava.build", - Network::Testnet => "https://rest.evmos-testnet.lava.build", - }; - - Url::parse(base_url).unwrap() +/// +/// TODO: this can probably be removed +pub fn get_rest_provider(cfg: &NetworkConfig) -> Url { + Url::parse(&cfg.rest).unwrap() } /// Processes the block body. diff --git a/src/cli.rs b/src/cli.rs index 891e91b..5899465 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -35,6 +35,8 @@ pub enum SubCommand { #[derive(Debug, Clone, Args)] pub struct GenerateProposalArgs { /// The LLM model to use for summarizing the release notes. + /// + /// TODO: enable using e.g. claude or cursor-agent in headless mode #[clap(short, long, default_value_t = OpenAIModel::Gpt4o)] model: OpenAIModel, } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e14187b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,62 @@ +use chrono::Duration; +use std::{path::PathBuf, str::FromStr}; + +/// Contains the configuration of the proposer tool for a given +/// profile. +pub struct Config { + pub networks: Vec, +} + +/// Contains the network information for a given network. +#[derive(Clone, Default)] +pub struct NetworkConfig { + pub name: String, + pub path: PathBuf, + pub allow_rc: bool, + pub rest: String, + // TODO: potentially remove going forward + pub voting_period: Option, + pub target_time_utc: String, +} + +pub fn get_evmos_config() -> Config { + Config { + networks: vec![ + NetworkConfig { + name: "Local Node".to_string(), + path: PathBuf::from_str("/Users/malte/.tmp-evmosd").expect("node home path"), + allow_rc: true, + rest: "http://localhost:1317".into(), + voting_period: Some(Duration::hours(1)), + target_time_utc: "16:00".into(), + }, + NetworkConfig { + name: "Testnet".to_string(), + path: PathBuf::from_str("/Users/malte/.evmosd").expect("node home path"), + allow_rc: true, + rest: "https://rest.evmos-testnet.lava.build".into(), + voting_period: Some(Duration::hours(12)), + target_time_utc: "16:00".into(), + }, + NetworkConfig { + name: "Mainnet".to_string(), + path: PathBuf::from_str("/Users/malte/.evmosd").expect("node home path"), + allow_rc: false, + rest: "https://rest.evmos.lava.build".into(), + voting_period: Some(Duration::hours(120)), + target_time_utc: "16:00".into(), + }, + ], + } +} + +#[derive(Default)] +pub struct UpgradeConfig { + pub upgrade_name: String, +} + +pub fn get_example_upgrade() -> UpgradeConfig { + UpgradeConfig { + upgrade_name: "Evmos v10.0.0 Testnet Upgrade".into(), + } +} diff --git a/src/errors.rs b/src/errors.rs index 50af06d..906ad8c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -181,7 +181,7 @@ pub enum ValidationError { #[error("Invalid previous version: {0}")] PreviousVersion(String), #[error("Invalid target version for {0}: {1}")] - TargetVersion(Network, String), + TargetVersion(String, String), #[error("Invalid upgrade time: {0}")] UpgradeTime(DateTime), } diff --git a/src/helper.rs b/src/helper.rs index 8268efc..2f3f87f 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,8 +1,13 @@ -use crate::block::{get_estimated_height, get_rest_provider, round_to_nearest_500}; -use crate::errors::{HelperError, InputError, ValidationError}; -use crate::llm::{create_summary, OpenAIModel}; -use crate::release::{get_instance, get_release}; -use crate::{evmosd, inputs, network::Network, version}; +use crate::{ + block::{get_estimated_height, get_rest_provider, round_to_nearest_500}, + config, + errors::{HelperError, InputError, ValidationError}, + evmosd, inputs, + llm::{create_summary, OpenAIModel}, + network::Network, + release::{get_instance, get_release}, + version, +}; use chrono::{DateTime, Duration, Utc}; use std::path::{Path, PathBuf}; use std::{fs, io}; @@ -42,14 +47,13 @@ impl UpgradeHelper { /// Creates a new instance of the upgrade helper. pub fn new( evmosd_home: PathBuf, - network: Network, + network_config: &config::NetworkConfig, previous_version: &str, target_version: &str, upgrade_time: DateTime, upgrade_height: u64, summary: &str, ) -> UpgradeHelper { - let chain_id = get_chain_id(network); let proposal_name = format!("Evmos {} {} Upgrade", network, target_version); let voting_period = get_voting_period(network); let proposal_file_name = format!("proposal-{}-{}.md", network, target_version); @@ -133,7 +137,7 @@ pub fn get_helper_from_json(path: &Path) -> Result { /// Creates a new instance of the upgrade helper based on querying the user for the necessary input. pub async fn get_helper_from_inputs(model: OpenAIModel) -> Result { // Query and check the network to use - let used_network = inputs::get_used_network()?; + let network_config = inputs::get_network_config(&config::get_evmos_config())?; // Query and check the version to upgrade from let previous_version = inputs::get_text("Previous version to upgrade from:")?; @@ -146,17 +150,16 @@ pub async fn get_helper_from_inputs(model: OpenAIModel) -> Result Result Result { let current_dir = std::env::current_dir()?; - // Get all files in the current directory + // Get all files in the current directory. let paths = fs::read_dir(¤t_dir)?; - // Filter for JSON files + // Filter for JSON files. let json_files = paths.filter(|path| { path.as_ref() .unwrap() @@ -42,7 +43,7 @@ pub fn choose_config() -> Result { .ends_with(".json") }); - // Collect the file names + // Collect the file names. let config_files: Vec = json_files .map(|file| file.unwrap().path().to_string_lossy().to_string()) .collect(); @@ -51,9 +52,7 @@ pub fn choose_config() -> Result { return Err(InputError::NoConfigFiles(current_dir)); } - // Prompt the user to select the configuration file - // - // FIXME: Why does the question mark operator not work here? It doesn't register the #[from] attribute in the error enum somehow? + // Prompt the user to select the configuration file. match Select::new("Select configuration file", config_files).prompt() { Ok(file) => Ok(current_dir.join(file)), Err(e) => Err(InputError::UserInput(e)), @@ -77,36 +76,24 @@ pub async fn choose_commonwealth_link() -> Result { } /// Prompts the user to select the network type used. -pub fn get_used_network() -> Result { - let network_options = vec!["Local Node", "Testnet", "Mainnet"]; +pub fn get_network_config(cfg: &config::Config) -> Result { + let network_options = cfg.networks.iter().map(|n| n.name.clone()).collect(); let chosen_network = Select::new("Select network", network_options).prompt()?; - // TODO: improve handling here! Should be more elegant to reverse the print stuff from the Network - // type. - let used_network = match chosen_network { - "Local Node" => Network::LocalNode, - "Testnet" => Network::Testnet, - "Mainnet" => Network::Mainnet, - &_ => { - return Err(InputError::InvalidNetwork(chosen_network.to_string())); - } - }; + let used_config = cfg + .networks + .iter() + .find(|&n| n.name == chosen_network) + .expect("expected to find chosen network name"); - Ok(used_network) + Ok(used_config.clone()) } /// Prompts the user to input the duration of the voting period. /// The duration is given in hours. -pub fn get_evmosd_home(network: &Network) -> Result { - let mut default_path = dirs::home_dir().expect("failed to get home directory"); - - match network { - Network::LocalNode => &default_path.push(".tmp-evmosd"), - _ => &default_path.push(".evmosd"), - }; - - let selected_option = inquire::Text::new("Enter the home path to your Evmos keyring") - .with_default(default_path.as_os_str().to_str().unwrap()) +pub fn get_node_home(cfg: &NetworkConfig) -> Result { + let selected_option = inquire::Text::new("Enter the home path to your node keyring") + .with_default(cfg.path.as_os_str().to_str().unwrap()) .prompt()?; Ok(PathBuf::from(selected_option)) @@ -120,10 +107,10 @@ pub fn get_text(prompt: &str) -> Result { /// Prompts the user to input the date for the planned upgrade. /// The date is calculated based on the current time and the voting period duration. pub fn get_upgrade_time( - voting_period: Duration, + cfg: &NetworkConfig, utc_time: DateTime, ) -> Result, InputError> { - let default_date = calculate_planned_date(voting_period, utc_time); + let default_date = calculate_planned_date(cfg, utc_time); // Prompt the user to input the desired upgrade date let date = DateSelect::new("Select date for the planned upgrade") @@ -141,10 +128,15 @@ pub fn get_upgrade_time( /// Calculates the date for the planned upgrade given the current time and the voting period duration. /// Per default, 4 pm UTC is used as a reference time. /// If the passed UTC time is after 2 pm UTC, the planned date will be shifted to the next day. -fn calculate_planned_date(voting_period: Duration, utc_time: DateTime) -> DateTime { - let mut end_of_voting = utc_time.add(voting_period); +fn calculate_planned_date(cfg: &NetworkConfig, utc_time: DateTime) -> DateTime { + let mut end_of_voting = match cfg.voting_period { + Some(vp) => utc_time.add(vp), + None => utc_time.add(Duration::days(1)), + }; // NOTE: if using the tool after 2pm UTC or the end of voting would be at or after 2 PM, the upgrade should happen on the next day + // + // TODO: rather we should get the input for the target update time. if utc_time.hour() > 14 || end_of_voting.hour() >= 16 { end_of_voting = end_of_voting.add(Duration::days(1)); } @@ -156,13 +148,16 @@ fn calculate_planned_date(voting_period: Duration, utc_time: DateTime) -> D _ => {} } + let target_time = chrono::NaiveTime::parse_from_str(&cfg.target_time_utc, "%H:%M") + .expect("failed to parse target time"); + Utc.with_ymd_and_hms( end_of_voting.year(), end_of_voting.month(), end_of_voting.day(), - 16, - 0, - 0, + target_time.hour(), + target_time.minute(), + target_time.second(), ) .unwrap() } @@ -215,22 +210,26 @@ mod tests { } #[fixture] - fn testnet_voting_period() -> Duration { - Duration::hours(12) + fn testnet_voting_period() -> NetworkConfig { + let mut cfg = NetworkConfig::default(); + cfg.voting_period = Some(Duration::hours(12)); + cfg } #[fixture] - fn mainnet_voting_period() -> Duration { - Duration::hours(120) + fn mainnet_voting_period() -> NetworkConfig { + let mut cfg = NetworkConfig::default(); + cfg.voting_period = Some(Duration::hours(120)); + cfg } #[rstest] fn test_calculate_planned_date_monday_morning_testnet( monday_morning: DateTime, - testnet_voting_period: Duration, + testnet_voting_period: NetworkConfig, ) { assert_eq!( - calculate_planned_date(testnet_voting_period, monday_morning), + calculate_planned_date(&testnet_voting_period, monday_morning), Utc.with_ymd_and_hms(2023, 10, 24, 16, 0, 0).unwrap(), "expected different date for testnet upgrade when calling on monday morning", ); @@ -239,10 +238,10 @@ mod tests { #[rstest] fn test_calculate_planned_date_monday_morning_mainnet( monday_morning: DateTime, - mainnet_voting_period: Duration, + mainnet_voting_period: NetworkConfig, ) { assert_eq!( - calculate_planned_date(mainnet_voting_period, monday_morning), + calculate_planned_date(&mainnet_voting_period, monday_morning), // NOTE: the upgrade should happen on the next monday 4PM, not on saturday which would be t+120h Utc.with_ymd_and_hms(2023, 10, 30, 16, 0, 0).unwrap(), "expected different date for mainnet upgrade when calling on monday morning", @@ -252,10 +251,10 @@ mod tests { #[rstest] fn test_calculate_planned_date_monday_evening_testnet( monday_evening: DateTime, - testnet_voting_period: Duration, + testnet_voting_period: NetworkConfig, ) { assert_eq!( - calculate_planned_date(testnet_voting_period, monday_evening), + calculate_planned_date(&testnet_voting_period, monday_evening), Utc.with_ymd_and_hms(2023, 10, 25, 16, 0, 0).unwrap(), "expected different date for testnet upgrade when calling on monday evening", ); @@ -264,10 +263,10 @@ mod tests { #[rstest] fn test_calculate_planned_date_monday_evening_mainnet( monday_evening: DateTime, - mainnet_voting_period: Duration, + mainnet_voting_period: NetworkConfig, ) { assert_eq!( - calculate_planned_date(mainnet_voting_period, monday_evening), + calculate_planned_date(&mainnet_voting_period, monday_evening), // NOTE: the upgrade should happen on the next monday 4PM, not on saturday which would be t+120h Utc.with_ymd_and_hms(2023, 10, 30, 16, 0, 0).unwrap(), "expected different date for mainnet upgrade when calling on monday evening", @@ -277,10 +276,10 @@ mod tests { #[rstest] fn test_calculate_planned_date_friday_morning_testnet( friday_morning: DateTime, - testnet_voting_period: Duration, + testnet_voting_period: NetworkConfig, ) { assert_eq!( - calculate_planned_date(testnet_voting_period, friday_morning), + calculate_planned_date(&testnet_voting_period, friday_morning), // NOTE: the upgrade should happen on the next monday 4PM, not on saturday which would be t+12h Utc.with_ymd_and_hms(2023, 10, 30, 16, 0, 0).unwrap(), "expected different date for testnet upgrade when calling on thursday morning", @@ -290,10 +289,10 @@ mod tests { #[rstest] fn test_calculate_planned_date_friday_morning_mainnet( friday_morning: DateTime, - mainnet_voting_period: Duration, + mainnet_voting_period: NetworkConfig, ) { assert_eq!( - calculate_planned_date(mainnet_voting_period, friday_morning), + calculate_planned_date(&mainnet_voting_period, friday_morning), // NOTE: the upgrade should happen on the next wednesday 4PM Utc.with_ymd_and_hms(2023, 11, 1, 16, 0, 0).unwrap(), "expected different date for mainnet upgrade when calling on thursday morning", diff --git a/src/main.rs b/src/main.rs index e77c9ae..75b201a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod block; mod cli; mod command; mod commonwealth; +mod config; mod errors; mod evmosd; mod helper; diff --git a/src/version.rs b/src/version.rs index 7733980..8fd8d74 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,4 +1,4 @@ -use crate::network::Network; +use crate::config::NetworkConfig; use regex::Regex; /// Returns a boolean value if the defined version fulfills the semantic @@ -13,14 +13,16 @@ pub fn is_valid_version(version: &str) -> bool { /// the requirements for the selected network type. /// The target version must be in the format `vX.Y.Z`. /// Testnet upgrades must use a release candidate with the suffix `-rcX`. -pub fn is_valid_version_for_network(network: Network, target_version: &str) -> bool { - let re = match network { - Network::LocalNode => Regex::new(r"^v\d+\.\d{1}\.\d+(-rc\d+)*$").unwrap(), - Network::Testnet => Regex::new(r"^v\d+\.\d{1}\.\d+(-rc\d+)*$").unwrap(), - Network::Mainnet => Regex::new(r"^v\d+\.\d{1}\.\d+$").unwrap(), - }; +pub fn is_valid_version_for_network(cfg: &NetworkConfig, target_version: &str) -> bool { + let mut pattern = r"^v\d+\.\d{1}\.\d+".to_string(); + if cfg.allow_rc { + pattern.push_str(r"(-rc\d+)*"); + } + pattern.push('$'); - re.is_match(target_version) + Regex::new(&pattern) + .expect("invalid regex") + .is_match(target_version) } #[cfg(test)] @@ -42,32 +44,29 @@ mod tests { #[test] fn test_is_valid_target_version_local_node_pass() { - assert_eq!( - is_valid_version_for_network(network::Network::LocalNode, "v14.0.0",), - true - ); + let cfg = NetworkConfig::default(); + assert_eq!(is_valid_version_for_network(&cfg, "v14.0.0",), true); } #[test] fn test_is_valid_target_version_local_node_fail() { assert_eq!( - is_valid_version_for_network(network::Network::LocalNode, "v14.0",), + is_valid_version_for_network(&NetworkConfig::default(), "v14.0",), false ); } #[test] fn test_is_valid_target_version_testnet_pass() { - assert_eq!( - is_valid_version_for_network(network::Network::Testnet, "v14.0.0-rc1",), - true - ); + let mut cfg = NetworkConfig::default(); + cfg.allow_rc = true; + assert_eq!(is_valid_version_for_network(&cfg, "v14.0.0-rc1",), true); } #[test] fn test_is_valid_target_version_testnet_fail() { assert_eq!( - is_valid_version_for_network(network::Network::Testnet, "v14.00",), + is_valid_version_for_network(&NetworkConfig::default(), "v14.00",), false ); } @@ -75,7 +74,7 @@ mod tests { #[test] fn test_is_valid_target_version_mainnet_pass() { assert_eq!( - is_valid_version_for_network(network::Network::Mainnet, "v14.0.0",), + is_valid_version_for_network(&NetworkConfig::default(), "v14.0.0",), true ); } @@ -83,7 +82,7 @@ mod tests { #[test] fn test_is_valid_target_version_mainnet_fail() { assert_eq!( - is_valid_version_for_network(network::Network::Mainnet, "v14.0.0-rc1",), + is_valid_version_for_network(&NetworkConfig::default(), "v14.0.0-rc1",), false ); } From 7ca67e3187197b1d5c1b2a6ebdf0b52404b53584 Mon Sep 17 00:00:00 2001 From: Malte Herrmann Date: Mon, 20 Oct 2025 23:36:17 +0200 Subject: [PATCH 2/8] move to configs --- src/balance.rs | 30 +++---- src/block.rs | 9 +-- src/cli.rs | 17 ++-- src/command.rs | 68 +++++----------- src/commonwealth.rs | 75 ----------------- src/config.rs | 80 +++++++++++++----- src/errors.rs | 3 +- src/evmosd.rs | 6 +- src/helper.rs | 193 +++++++++++++++++++++++--------------------- src/inputs.rs | 15 ++-- src/keys.rs | 49 ++++++----- src/main.rs | 1 - src/network.rs | 4 +- src/proposal.rs | 66 ++++++--------- src/release.rs | 16 ++-- src/version.rs | 1 - 16 files changed, 273 insertions(+), 360 deletions(-) delete mode 100644 src/commonwealth.rs diff --git a/src/balance.rs b/src/balance.rs index cb40e17..d050c3b 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -1,8 +1,5 @@ -use crate::errors::KeysError; -use crate::http::get_body; -use crate::network::{get_denom, Network}; +use crate::{config::NetworkConfig, errors::KeysError, http::get_body}; use serde::{Deserialize, Serialize}; -use url::Url; const BALANCES_ENDPOINT: &str = "cosmos/bank/v1beta1/balances/"; @@ -20,15 +17,11 @@ struct Balance { } /// Checks if a given address has a non-zero balance on the given network. -pub async fn has_balance( - address: &str, - network: &Network, - base_url: &Url, -) -> Result { - let native_denom = get_denom(*network); - let balances_endpoint = base_url +pub async fn has_balance(address: &str, network_config: &NetworkConfig) -> Result { + let balances_endpoint = network_config + .rest .join(BALANCES_ENDPOINT)? - .join(format!("{}/by_denom?denom={}", address, native_denom).as_str())?; + .join(format!("{}/by_denom?denom={}", address, network_config.fee_denom).as_str())?; let balance: BalanceResponse = serde_json::from_str(get_body(balances_endpoint).await?.as_str())?; @@ -39,9 +32,8 @@ pub async fn has_balance( #[cfg(test)] mod tests { use super::*; - use crate::network::Network; use serde_json::Value; - use std::str::FromStr; + use url::Url; use wiremock::matchers::{method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -72,15 +64,13 @@ mod tests { #[tokio::test] async fn test_has_balance() { - let network = Network::LocalNode; let mock_server = setup_mock_api().await; - let mock_path = - Url::from_str(mock_server.uri().as_str()).expect("failed to parse mock server uri"); + + let mut network_config = NetworkConfig::default(); + network_config.rest = Url::parse(mock_server.uri().as_str()).unwrap(); assert!( - has_balance(TEST_ADDRESS, &network, &mock_path) - .await - .unwrap(), + has_balance(TEST_ADDRESS, &network_config).await.unwrap(), "expected a non-zero balance" ); } diff --git a/src/block.rs b/src/block.rs index 25e9b3a..1c2e886 100644 --- a/src/block.rs +++ b/src/block.rs @@ -1,4 +1,4 @@ -use crate::{config::NetworkConfig, errors::BlockError, http::get_body}; +use crate::{errors::BlockError, http::get_body}; use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -78,13 +78,6 @@ async fn get_block(base_url: &Url, height: u64) -> Result { ) } -/// Returns the appropriate REST provider for the given network. -/// -/// TODO: this can probably be removed -pub fn get_rest_provider(cfg: &NetworkConfig) -> Url { - Url::parse(&cfg.rest).unwrap() -} - /// Processes the block body. fn process_block_body(body: String) -> Result { let body: BlockResponse = serde_json::from_str(&body)?; diff --git a/src/cli.rs b/src/cli.rs index 5899465..b0fbb3c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,12 +1,10 @@ use crate::evmosd::get_client_config; use crate::{ command, - commonwealth::check_commonwealth_link, errors::{CommandError, ProposalError}, helper::{get_helper_from_inputs, get_helper_from_json}, inputs, keys, llm::OpenAIModel, - network::Network, proposal, utils, }; use clap::{Args, Parser, Subcommand}; @@ -61,23 +59,20 @@ pub async fn generate_command(args: GenerateCommandArgs) -> Result<(), CommandEr let mut upgrade_helper = get_helper_from_json(&helper_config_path)?; let client_config = get_client_config( upgrade_helper - .evmosd_home + .network_config + .path .join("config/client.toml") .as_path(), )?; - if upgrade_helper.network == Network::Mainnet { + // TODO: remove commonwealth logic. + if upgrade_helper.network_config.name == "Mainnet" { let commonwealth_link = inputs::choose_commonwealth_link().await?; - check_commonwealth_link(&commonwealth_link, &upgrade_helper).await?; upgrade_helper.commonwealth_link = Some(commonwealth_link.clone()); } - let keys_with_balances = keys::get_keys_with_balances(keys::FilterKeysConfig { - config: client_config.clone(), - home: upgrade_helper.evmosd_home.clone(), - network: upgrade_helper.network, - }) - .await?; + let keys_with_balances = + keys::get_keys_with_balances(&client_config, &upgrade_helper.network_config).await?; let key = inputs::get_key(keys_with_balances)?; // Prepare command to submit proposal diff --git a/src/command.rs b/src/command.rs index b055cd9..e180f7e 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,7 +1,6 @@ use crate::errors::PrepareError; use crate::evmosd::ClientConfig; use crate::helper::UpgradeHelper; -use crate::network::{get_denom, Network}; use crate::release::{get_asset_string, get_instance, get_release}; use handlebars::{no_escape, Handlebars}; use serde_json::json; @@ -14,13 +13,15 @@ pub async fn prepare_command( key: &str, ) -> Result { let mut description = get_description_from_md(&helper.proposal_file_name)?; - let release = get_release(&get_instance(), helper.target_version.as_str()).await?; + let release = get_release( + &get_instance(), + helper.upgrade_config.target_version.as_str(), + ) + .await?; let assets = get_asset_string(&release).await?; - let denom = get_denom(helper.network); // TODO: get fees from network conditions? - let fees = format!("10000000000{}", denom); - let tm_rpc = get_rpc_url(helper.network); + let fees = format!("10000000000{}", helper.network_config.fee_denom); let mut handlebars = Handlebars::new(); handlebars.set_strict_mode(true); @@ -47,17 +48,17 @@ pub async fn prepare_command( let data = json!({ "assets": assets, - "chain_id": helper.chain_id, + "chain_id": helper.network_config.chain_id, "commonwealth": helper.commonwealth_link, "description": description.replace('\n', "\\n"), // NOTE: this is necessary to not print the actual new lines when rendering the template. "fees": fees, - "height": helper.upgrade_height, - "home": helper.evmosd_home, + "height": helper.upgrade_config.upgrade_height, + "home": helper.network_config.path, "key": key, "keyring": client_config.keyring_backend, - "title": helper.proposal_name, - "tm_rpc": tm_rpc, - "version": helper.target_version, + "title": helper.upgrade_config.upgrade_name, + "tm_rpc": helper.network_config.cosmos_rpc, + "version": helper.upgrade_config.target_version, }); let command = handlebars.render("command", &data)?; @@ -70,33 +71,23 @@ fn get_description_from_md(filename: &str) -> io::Result { std::fs::read_to_string(filename) } -/// Returns the RPC URL based on the network. -fn get_rpc_url(network: Network) -> String { - match network { - Network::Mainnet => "https://tm.evmos.lava.build:443".to_string(), - Network::Testnet => "https://tm.evmos-testnet.lava.build:443".to_string(), - Network::LocalNode => "http://localhost:26657".to_string(), - } -} - #[cfg(test)] mod tests { use super::*; - use crate::network::Network; + use crate::config::{NetworkConfig, UpgradeConfig}; use chrono::Utc; - use std::path::PathBuf; #[tokio::test] async fn test_prepare_command() { - let helper = UpgradeHelper::new( - PathBuf::from("./.evmosd"), - Network::Testnet, - "v13.0.0", - "v14.0.0", - Utc::now(), - 60, - "", - ); + let nc = NetworkConfig::default(); + let mut uc = UpgradeConfig::default(); + uc.previous_version = "v13.0.0".to_string(); + uc.target_version = "v14.0.0".to_string(); + uc.upgrade_time = Utc::now(); + uc.upgrade_height = 60; + uc.upgrade_name = "".to_string(); + + let helper = UpgradeHelper::new(&nc, &uc); let client_config = ClientConfig { chain_id: "evmos_9000-4".to_string(), @@ -141,19 +132,4 @@ mod tests { "description should be err, but is not" ); } - - #[test] - fn test_get_rpc_url() { - let rpc = get_rpc_url(Network::Mainnet); - assert_eq!(rpc, "https://tm.evmos.lava.build:443", "rpc does not match"); - - let rpc = get_rpc_url(Network::Testnet); - assert_eq!( - rpc, "https://tm.evmos-testnet.lava.build:443", - "rpc does not match" - ); - - let rpc = get_rpc_url(Network::LocalNode); - assert_eq!(rpc, "http://localhost:26657", "rpc does not match"); - } } diff --git a/src/commonwealth.rs b/src/commonwealth.rs deleted file mode 100644 index e7246ce..0000000 --- a/src/commonwealth.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{errors::CommonwealthError, helper::UpgradeHelper, http::get_body}; -use url::Url; - -/// Check if the page body can be retrieved (i.e. the link is valid) and do some basic -/// validation on the expected contents. -pub async fn check_commonwealth_link( - commonwealth_link: &str, - _: &UpgradeHelper, // TODO: use when accessing the Commonwealth API -) -> Result<(), CommonwealthError> { - // NOTE: for now we just check that the contents at the given URL can be retrieved - let _ = get_body(Url::parse(commonwealth_link)?).await?; - Ok(()) - - // TODO: This is not working as expected because the returned contents in the body are not reliably containing the - // desired information. This could probably be better with access to the Commonwealth API. - // if !body.contains(upgrade_helper.target_version.as_str()) - // || !body.contains(upgrade_helper.network.to_string().as_str()) - // || !body.contains(upgrade_helper.previous_version.to_string().as_str()) - // || !body.contains(upgrade_helper.upgrade_height.to_string().as_str()) - // { - // return Err(CommonwealthError::InvalidCommonwealthLink); - // } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::network::Network; - use chrono::Utc; - use std::path::PathBuf; - use std::str::FromStr; - use wiremock::matchers::{method, path}; - use wiremock::{Mock, MockServer, ResponseTemplate}; - - /// Sets up a mock server to return the given response template - /// mocking the Commonwealth page contents. - async fn setup_mock_api(called_endpoint: &str) -> MockServer { - let template = ResponseTemplate::new(200) - .set_body_string(include_str!("testdata/commonwealth_response.html")); - - let mock_server = MockServer::start().await; - - Mock::given(method("GET")) - .and(path(called_endpoint)) - .respond_with(template) - .mount(&mock_server) - .await; - - mock_server - } - - #[tokio::test] - async fn test_check_commonwealth_link_pass() { - let helper = UpgradeHelper::new( - PathBuf::from("./.evmosd"), - Network::Mainnet, - "v15.0.0", - "v16.0.0", - Utc::now(), - 60, - "", - ); - - let endpoint = "/evmos/discussion/14754-evmos-mainnet-v1600-upgrade"; - let mock_server = setup_mock_api(endpoint).await; - let mock_path = Url::from_str(mock_server.uri().as_str()) - .expect("failed to parse mock server uri") - .join(endpoint) - .expect("failed to join url"); - - assert!(check_commonwealth_link(mock_path.as_str(), &helper) - .await - .is_ok()); - } -} diff --git a/src/config.rs b/src/config.rs index e14187b..813ed3e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ -use chrono::Duration; +use chrono::{DateTime, Duration, Utc}; use std::{path::PathBuf, str::FromStr}; +use url::Url; /// Contains the configuration of the proposer tool for a given /// profile. @@ -8,55 +9,90 @@ pub struct Config { } /// Contains the network information for a given network. -#[derive(Clone, Default)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct NetworkConfig { + pub allow_rc: bool, + pub chain_id: String, + pub cosmos_rpc: Url, + pub fee_denom: String, pub name: String, pub path: PathBuf, - pub allow_rc: bool, - pub rest: String, - // TODO: potentially remove going forward - pub voting_period: Option, + pub rest: Url, + /// The desired time of the upgrade in UTC. pub target_time_utc: String, + // TODO: potentially remove going forward + pub voting_period: Option, +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { + allow_rc: false, + chain_id: "".into(), + cosmos_rpc: Url::parse("http://localhost:26657").expect("cosmos rpc url"), + fee_denom: "".into(), + name: "".into(), + path: PathBuf::new(), + rest: Url::parse("http://localhost:1317").expect("rest url"), + target_time_utc: "".into(), + voting_period: None, + } + } } pub fn get_evmos_config() -> Config { Config { networks: vec![ NetworkConfig { - name: "Local Node".to_string(), + chain_id: "evmos_9002-1".into(), + cosmos_rpc: Url::parse("http://localhost:26657").expect("cosmos rpc url"), + fee_denom: "aevmos".into(), + name: "Local Node".into(), path: PathBuf::from_str("/Users/malte/.tmp-evmosd").expect("node home path"), allow_rc: true, - rest: "http://localhost:1317".into(), - voting_period: Some(Duration::hours(1)), + rest: Url::parse("http://localhost:1317".into()).expect("rest url"), + voting_period: Some(Duration::hours(1).num_hours()), target_time_utc: "16:00".into(), }, NetworkConfig { - name: "Testnet".to_string(), + chain_id: "evmos_9001-1".into(), + cosmos_rpc: Url::parse("https://rpc.evmos-testnet.lava.build:443") + .expect("cosmos rpc url"), + fee_denom: "atevmos".into(), + name: "Testnet".into(), path: PathBuf::from_str("/Users/malte/.evmosd").expect("node home path"), allow_rc: true, - rest: "https://rest.evmos-testnet.lava.build".into(), - voting_period: Some(Duration::hours(12)), + rest: Url::parse("https://rest.evmos-testnet.lava.build").expect("rest url"), + voting_period: Some(Duration::hours(12).num_hours()), target_time_utc: "16:00".into(), }, NetworkConfig { - name: "Mainnet".to_string(), + chain_id: "evmos_9000-4".into(), + cosmos_rpc: Url::parse("https://rpc.evmos.lava.build:443").expect("cosmos rpc url"), + fee_denom: "aevmos".into(), + name: "Mainnet".into(), path: PathBuf::from_str("/Users/malte/.evmosd").expect("node home path"), allow_rc: false, - rest: "https://rest.evmos.lava.build".into(), - voting_period: Some(Duration::hours(120)), + rest: Url::parse("https://rest.evmos.lava.build").expect("rest url"), + voting_period: Some(Duration::hours(120).num_hours()), target_time_utc: "16:00".into(), }, ], } } -#[derive(Default)] +#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] pub struct UpgradeConfig { + /// The previous version to upgrade from. + pub previous_version: String, + /// The summary of the changes in the release. + pub summary: String, + /// The target version to upgrade to. + pub target_version: String, + /// The name of the upgrade. pub upgrade_name: String, -} - -pub fn get_example_upgrade() -> UpgradeConfig { - UpgradeConfig { - upgrade_name: "Evmos v10.0.0 Testnet Upgrade".into(), - } + /// The projected time of the upgrade. + pub upgrade_time: DateTime, + /// The block height where the upgrade is applied. + pub upgrade_height: u64, } diff --git a/src/errors.rs b/src/errors.rs index 906ad8c..e7e0483 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,4 +1,3 @@ -use crate::network::Network; use async_openai::error::OpenAIError; use chrono::{DateTime, Utc}; use inquire::InquireError; @@ -86,6 +85,8 @@ pub enum InputError { GitHub(#[from] octocrab::Error), #[error("Invalid network: {0}")] InvalidNetwork(String), + #[error("Home directory does not exist: {0}")] + HomeDir(String), #[error("Got IO error: {0}")] IO(#[from] std::io::Error), #[error("No configuration files found in current directory: {0}")] diff --git a/src/evmosd.rs b/src/evmosd.rs index 28ff20d..dfb73ef 100644 --- a/src/evmosd.rs +++ b/src/evmosd.rs @@ -3,7 +3,9 @@ use serde::Deserialize; use std::path::Path; /// The client configuration for the `evmosd` node. -#[derive(Clone, Deserialize)] +/// +/// TODO: check if this can be removed +#[derive(Clone, Default, Deserialize)] pub struct ClientConfig { #[serde(rename = "chain-id")] pub chain_id: String, @@ -16,6 +18,8 @@ pub struct ClientConfig { } /// This method returns the client configuration for the `evmosd` node. +/// +/// TODO: this lib should be renamed to not be tied to `evmosd`. pub fn get_client_config(path: &Path) -> Result { Ok(toml::from_str::( std::fs::read_to_string(path)?.as_str(), diff --git a/src/helper.rs b/src/helper.rs index 2f3f87f..c613643 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,5 +1,5 @@ use crate::{ - block::{get_estimated_height, get_rest_provider, round_to_nearest_500}, + block::{get_estimated_height, round_to_nearest_500}, config, errors::{HelperError, InputError, ValidationError}, evmosd, inputs, @@ -8,103 +8,83 @@ use crate::{ release::{get_instance, get_release}, version, }; -use chrono::{DateTime, Duration, Utc}; -use std::path::{Path, PathBuf}; +use chrono::{Duration, Utc}; +use std::path::Path; use std::{fs, io}; /// Contains all relevant information for the scheduled upgrade. -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] pub struct UpgradeHelper { - /// The chain ID of the node. - pub chain_id: String, /// The link to the Commonwealth proposal (optional). pub commonwealth_link: Option, /// The name of the config file. pub config_file_name: String, - /// The home directory of the Evmos binary. - pub evmosd_home: PathBuf, - /// The network to create the commands and proposal description for. - pub network: Network, - /// The previous version to upgrade from. - pub previous_version: String, - /// The name of the proposal. - pub proposal_name: String, + /// The configuration of the used node binary. + pub network_config: config::NetworkConfig, /// The name of the proposal file. pub proposal_file_name: String, - /// The summary of the changes in the release. - pub summary: String, - /// The target version to upgrade to. - pub target_version: String, - /// The scheduled height of the upgrade. - pub upgrade_height: u64, - /// The scheduled time of the upgrade. - pub upgrade_time: DateTime, - /// The number of hours for the voting period. - pub voting_period: i64, + /// The configuration of the generated proposal contents. + pub upgrade_config: config::UpgradeConfig, } impl UpgradeHelper { /// Creates a new instance of the upgrade helper. pub fn new( - evmosd_home: PathBuf, network_config: &config::NetworkConfig, - previous_version: &str, - target_version: &str, - upgrade_time: DateTime, - upgrade_height: u64, - summary: &str, + upgrade_config: &config::UpgradeConfig, ) -> UpgradeHelper { - let proposal_name = format!("Evmos {} {} Upgrade", network, target_version); - let voting_period = get_voting_period(network); - let proposal_file_name = format!("proposal-{}-{}.md", network, target_version); - let config_file_name = format!("proposal-{}-{}.json", network, target_version); + let proposal_file_name = format!( + "proposal-{}-{}.md", + network_config.name, upgrade_config.target_version + ); + let config_file_name = format!( + "proposal-{}-{}.json", + network_config.name, upgrade_config.target_version + ); UpgradeHelper { - chain_id, commonwealth_link: None, config_file_name, - evmosd_home, - network, - previous_version: previous_version.to_string(), - proposal_name, + network_config: network_config.clone(), proposal_file_name, - summary: summary.to_string(), - target_version: target_version.to_string(), - upgrade_height, - upgrade_time, - voting_period: voting_period.num_hours(), + upgrade_config: upgrade_config.clone(), } } /// Validates the upgrade helper. pub fn validate(&self) -> Result<(), ValidationError> { // Check if the target version is valid - if !version::is_valid_version_for_network(self.network, self.target_version.as_str()) { + if !version::is_valid_version_for_network( + &self.network_config, + self.upgrade_config.target_version.as_str(), + ) { return Err(ValidationError::TargetVersion( - self.network, - self.target_version.clone(), + self.network_config.name.clone(), + self.upgrade_config.target_version.clone(), )); } // Check if the previous version is valid - if !version::is_valid_version(self.previous_version.as_str()) { + if !version::is_valid_version(self.upgrade_config.previous_version.as_str()) { return Err(ValidationError::PreviousVersion( - self.previous_version.clone(), + self.upgrade_config.previous_version.clone(), )); } // Check if the upgrade time is valid - if !inputs::is_valid_upgrade_time(self.upgrade_time) { - return Err(ValidationError::UpgradeTime(self.upgrade_time)); + if !inputs::is_valid_upgrade_time(self.upgrade_config.upgrade_time) { + return Err(ValidationError::UpgradeTime( + self.upgrade_config.upgrade_time, + )); } // Check if home folder exists - if !path_exists(&self.evmosd_home) { - return Err(ValidationError::HomeDir(self.evmosd_home.clone())); + if !path_exists(&self.network_config.path) { + return Err(ValidationError::HomeDir(self.network_config.path.clone())); } // Check if the home folder contains the client configuration - evmosd::get_client_config(&self.evmosd_home.join("config/client.toml"))?; + evmosd::get_client_config(&self.network_config.path.join("config/client.toml"))?; Ok(()) } @@ -137,7 +117,7 @@ pub fn get_helper_from_json(path: &Path) -> Result { /// Creates a new instance of the upgrade helper based on querying the user for the necessary input. pub async fn get_helper_from_inputs(model: OpenAIModel) -> Result { // Query and check the network to use - let network_config = inputs::get_network_config(&config::get_evmos_config())?; + let mut network_config = inputs::get_network_config(&config::get_evmos_config())?; // Query and check the version to upgrade from let previous_version = inputs::get_text("Previous version to upgrade from:")?; @@ -159,8 +139,8 @@ pub async fn get_helper_from_inputs(model: OpenAIModel) -> Result Result Result Result { let selected_option = inquire::Text::new("Enter the home path to your node keyring") .with_default(cfg.path.as_os_str().to_str().unwrap()) .prompt()?; + if !PathBuf::from(&selected_option).exists() { + return Err(InputError::HomeDir(selected_option.to_string())); + } + Ok(PathBuf::from(selected_option)) } @@ -130,8 +135,8 @@ pub fn get_upgrade_time( /// If the passed UTC time is after 2 pm UTC, the planned date will be shifted to the next day. fn calculate_planned_date(cfg: &NetworkConfig, utc_time: DateTime) -> DateTime { let mut end_of_voting = match cfg.voting_period { - Some(vp) => utc_time.add(vp), - None => utc_time.add(Duration::days(1)), + Some(vp) => utc_time.add(Duration::hours(vp)), + None => utc_time, }; // NOTE: if using the tool after 2pm UTC or the end of voting would be at or after 2 PM, the upgrade should happen on the next day @@ -212,14 +217,14 @@ mod tests { #[fixture] fn testnet_voting_period() -> NetworkConfig { let mut cfg = NetworkConfig::default(); - cfg.voting_period = Some(Duration::hours(12)); + cfg.voting_period = Some(Duration::hours(12).num_hours()); cfg } #[fixture] fn mainnet_voting_period() -> NetworkConfig { let mut cfg = NetworkConfig::default(); - cfg.voting_period = Some(Duration::hours(120)); + cfg.voting_period = Some(Duration::hours(120).num_hours()); cfg } diff --git a/src/keys.rs b/src/keys.rs index 5794370..265c94a 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1,7 +1,6 @@ use crate::evmosd::ClientConfig; -use crate::{balance, block::get_rest_provider, errors::KeysError, network::Network}; +use crate::{balance, config::NetworkConfig, errors::KeysError}; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; use std::process; /// Represents a key with its name and address. @@ -14,19 +13,24 @@ pub struct Key { /// Contains all necessary configuration to get the keys from the keyring and filter for ones with a balance. pub struct FilterKeysConfig { pub config: ClientConfig, - pub home: PathBuf, - pub network: Network, + pub network_config: NetworkConfig, } /// Returns a list of keys that have a non-zero balance on the configured network. -pub async fn get_keys_with_balances(config: FilterKeysConfig) -> Result, KeysError> { - let keys = get_keys_from_keyring(&config)?; - filter_keys_with_balance(config, keys).await +pub async fn get_keys_with_balances( + client_config: &ClientConfig, + network_config: &NetworkConfig, +) -> Result, KeysError> { + let keys = get_keys_from_keyring(client_config, network_config)?; + filter_keys_with_balance(network_config, keys).await } /// Returns a list of keys from the configured keyring. /// The keyring is configured in the user's home directory. -fn get_keys_from_keyring(config: &FilterKeysConfig) -> Result, KeysError> { +fn get_keys_from_keyring( + client_config: &ClientConfig, + network_config: &NetworkConfig, +) -> Result, KeysError> { let output = process::Command::new("evmosd") .args([ "keys", @@ -34,9 +38,12 @@ fn get_keys_from_keyring(config: &FilterKeysConfig) -> Result, KeysErro "--output", "json", "--keyring-backend", - &config.config.keyring_backend, + &client_config.keyring_backend, "--home", - config.home.to_str().expect("failed to unwrap home path"), + network_config + .path + .to_str() + .expect("failed to unwrap home path"), ]) .output()?; @@ -53,14 +60,13 @@ fn parse_keys_output(output: &str) -> Result, serde_json::Error> { /// Filters the keys for ones that have a non-zero balance on the configured network. async fn filter_keys_with_balance( - config: FilterKeysConfig, + network_config: &NetworkConfig, keys: Vec, ) -> Result, KeysError> { let mut keys_with_balance = Vec::new(); - let base_url = get_rest_provider(config.network); for key in keys { - if balance::has_balance(&key.address, &config.network, &base_url).await? { + if balance::has_balance(&key.address, network_config).await? { keys_with_balance.push(key.name); } } @@ -79,19 +85,10 @@ mod tests { #[cfg(not(feature = "remote"))] #[tokio::test] async fn test_get_keys_with_balances() { - let filter_config = FilterKeysConfig { - config: ClientConfig { - chain_id: "evmos_9000-1".to_string(), - keyring_backend: "os".to_string(), - output: "text".to_string(), - node: "https://evmos-testnet.lava.build".to_string(), - broadcast_mode: "sync".to_string(), - }, - home: dirs::home_dir().unwrap().join(".evmosd"), - network: Network::Testnet, - }; - - let res = get_keys_with_balances(filter_config).await; + let nc = NetworkConfig::default(); + let cc = ClientConfig::default(); + + let res = get_keys_with_balances(&cc, &nc).await; assert!(res.is_ok()); let keys = res.unwrap(); diff --git a/src/main.rs b/src/main.rs index 75b201a..60e711c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,6 @@ mod balance; mod block; mod cli; mod command; -mod commonwealth; mod config; mod errors; mod evmosd; diff --git a/src/network.rs b/src/network.rs index 6748229..ea67362 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,6 +1,8 @@ use std::fmt; -// Enum to represent different network options +/// Enum to represent different network options +/// +/// TODO: should be removed #[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum Network { LocalNode, diff --git a/src/proposal.rs b/src/proposal.rs index a2168e5..bf91d9c 100644 --- a/src/proposal.rs +++ b/src/proposal.rs @@ -1,12 +1,13 @@ use crate::{ block::N_BLOCKS, errors::ProposalError, helper::UpgradeHelper, inputs::get_time_string, - network::Network, }; use handlebars::Handlebars; use num_format::ToFormattedString; use serde_json::json; /// Renders the proposal template, filling in the necessary information. +/// +/// TODO: remember for now pub fn render_proposal(helper: &UpgradeHelper) -> Result { let mut handlebars = Handlebars::new(); handlebars.set_strict_mode(true); @@ -15,49 +16,33 @@ pub fn render_proposal(helper: &UpgradeHelper) -> Result .register_template_file("proposal", "src/templates/proposal.hbs") .unwrap(); - let height_link = get_height_with_link(helper.network, helper.upgrade_height); + let height_link = format!( + "[{}](https://mintscan.io/evmos/blocks/{})", + helper.upgrade_config.upgrade_height, helper.upgrade_config.upgrade_height + ); let n_blocks = N_BLOCKS.to_formatted_string(&num_format::Locale::en); - let upgrade_time = get_time_string(helper.upgrade_time); + let upgrade_time = get_time_string(helper.upgrade_config.upgrade_time); let data = json!({ "author": "Malte Herrmann, Evmos Core Team", "diff_link": format!("https://github.com/evmos/evmos/compare/{}..{}", - helper.previous_version, - helper.target_version, + helper.upgrade_config.previous_version, + helper.upgrade_config.target_version, ), "estimated_time": upgrade_time, - "features": helper.summary, + "features": helper.upgrade_config.summary, "height": height_link, - "name": helper.proposal_name, + "name": helper.upgrade_config.upgrade_name, "n_blocks": n_blocks, - "network": helper.network, - "previous_version": get_release_md_link(helper.previous_version.as_str()), - "version": get_release_md_link(helper.target_version.as_str()), - "voting_time": helper.voting_period, + "network": helper.network_config.name, + "previous_version": get_release_md_link(helper.upgrade_config.previous_version.as_str()), + "version": get_release_md_link(helper.upgrade_config.target_version.as_str()), + "voting_time": helper.network_config.voting_period, }); Ok(handlebars.render("proposal", &data)?) } -/// Returns the appropriate Markdown link to the block on Mintscan for the given network and height. -fn get_height_with_link(network: Network, height: u64) -> String { - let height_with_commas = height.to_formatted_string(&num_format::Locale::en); - match network { - Network::LocalNode => format!( - "[{}](https://mintscan.io/evmos/blocks/{})", - height_with_commas, height - ), - Network::Mainnet => format!( - "[{}](https://mintscan.io/evmos/blocks/{})", - height_with_commas, height - ), - Network::Testnet => format!( - "[{}](https://mintscan.io/evmos-testnet/blocks/{})", - height_with_commas, height - ), - } -} - /// Returns the appropriate Markdown link to the release on GitHub for the given version. fn get_release_md_link(version: &str) -> String { format!( @@ -70,19 +55,20 @@ fn get_release_md_link(version: &str) -> String { mod tests { use super::*; use chrono::Utc; - use std::path::PathBuf; + + use crate::config::{NetworkConfig, UpgradeConfig}; #[test] fn test_render_proposal_pass() { - let helper = UpgradeHelper::new( - PathBuf::from("./.evmosd"), - Network::Mainnet, - "v0.0.1", - "v0.1.0", - Utc::now(), - 60, - "", - ); + let nc = NetworkConfig::default(); + let mut uc = UpgradeConfig::default(); + uc.previous_version = "v0.0.1".to_string(); + uc.target_version = "v0.1.0".to_string(); + uc.upgrade_time = Utc::now(); + uc.upgrade_height = 60; + uc.upgrade_name = "".to_string(); + + let helper = UpgradeHelper::new(&nc, &uc); let result = render_proposal(&helper); assert!( diff --git a/src/release.rs b/src/release.rs index 84e6d4b..c562a21 100644 --- a/src/release.rs +++ b/src/release.rs @@ -133,12 +133,14 @@ fn build_assets_json(release: &Release, checksums: HashMap) -> V "binaries": {} }); - release.assets.iter() + release + .assets + .iter() .filter_map(|asset| { let os_key = get_os_key_from_asset_name(&asset.name)?; let checksum = checksums.get(&asset.name)?; let url = format!("{}?checksum={}", asset.browser_download_url, checksum); - + Some((os_key, url)) }) .for_each(|(os_key, url)| { @@ -180,9 +182,8 @@ fn get_os_key_from_asset_name(name: &str) -> Option { /// Downloads the checksum file from the release assets and returns the built checksum string. async fn get_checksum_map(assets: &[Asset]) -> Result, PrepareError> { - let checksum = get_checksum_from_assets(assets) - .ok_or(PrepareError::GetChecksumAsset)?; - + let checksum = get_checksum_from_assets(assets).ok_or(PrepareError::GetChecksumAsset)?; + let body = get_body(checksum.browser_download_url.clone()).await?; let checksums = body @@ -199,9 +200,8 @@ fn parse_checksum_line(line: &str) -> Option<(String, String)> { let mut parts = line.split_whitespace(); let checksum = parts.next()?.to_string(); let asset_name = parts.next()?.to_string(); - - (!parts.next().is_some() && !asset_name.contains("Windows")) - .then_some((asset_name, checksum)) + + (!parts.next().is_some() && !asset_name.contains("Windows")).then_some((asset_name, checksum)) } /// Returns an Octocrab instance. diff --git a/src/version.rs b/src/version.rs index 8fd8d74..507521a 100644 --- a/src/version.rs +++ b/src/version.rs @@ -28,7 +28,6 @@ pub fn is_valid_version_for_network(cfg: &NetworkConfig, target_version: &str) - #[cfg(test)] mod tests { use super::*; - use crate::network; #[test] fn test_is_valid_version_pass() { From 971bf1701f9d4c418f1bc87fbc41e03e97ec9afa Mon Sep 17 00:00:00 2001 From: Malte Herrmann Date: Mon, 20 Oct 2025 23:44:27 +0200 Subject: [PATCH 3/8] address some linters --- src/config.rs | 2 +- src/errors.rs | 2 -- src/helper.rs | 33 ++++---------------------------- src/http.rs | 4 ++-- src/inputs.rs | 51 ++++--------------------------------------------- src/keys.rs | 8 +------- src/network.rs | 9 --------- src/proposal.rs | 13 +++++++------ src/release.rs | 8 ++++---- src/version.rs | 38 +++++++++++++++++------------------- 10 files changed, 41 insertions(+), 127 deletions(-) diff --git a/src/config.rs b/src/config.rs index 813ed3e..9b7a12e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,7 +50,7 @@ pub fn get_evmos_config() -> Config { name: "Local Node".into(), path: PathBuf::from_str("/Users/malte/.tmp-evmosd").expect("node home path"), allow_rc: true, - rest: Url::parse("http://localhost:1317".into()).expect("rest url"), + rest: Url::parse("http://localhost:1317").expect("rest url"), voting_period: Some(Duration::hours(1).num_hours()), target_time_utc: "16:00".into(), }, diff --git a/src/errors.rs b/src/errors.rs index e7e0483..2cb2cf3 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -83,8 +83,6 @@ pub enum InputError { Block(#[from] BlockError), #[error("Failed to get GitHub data: {0}")] GitHub(#[from] octocrab::Error), - #[error("Invalid network: {0}")] - InvalidNetwork(String), #[error("Home directory does not exist: {0}")] HomeDir(String), #[error("Got IO error: {0}")] diff --git a/src/helper.rs b/src/helper.rs index c613643..a0d0800 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -4,11 +4,10 @@ use crate::{ errors::{HelperError, InputError, ValidationError}, evmosd, inputs, llm::{create_summary, OpenAIModel}, - network::Network, release::{get_instance, get_release}, version, }; -use chrono::{Duration, Utc}; +use chrono::{Utc}; use std::path::Path; use std::{fs, io}; @@ -248,13 +247,7 @@ mod helper_tests { ); // remove the config file - match fs::remove_file(&path) { - Ok(_) => {} - Err(e) => { - println!("Failed to remove file '{}': {}", path.to_str().unwrap(), e); - assert!(false, "expected success removing config file"); - } - } + assert!(fs::remove_file(path).is_ok(), "expected success removing config file"); } } @@ -274,30 +267,12 @@ mod path_tests { #[test] fn test_path_exists() { let path = Path::new("/tmp"); - assert_eq!(path_exists(path), true); + assert!(path_exists(path)); } #[test] fn test_path_does_not_exist() { let path = Path::new("/tmp/does-not-exist"); - assert_eq!(path_exists(path), false); - } -} - -/// Returns the voting period duration based on the network. -pub fn get_voting_period(network: Network) -> Duration { - match network { - Network::LocalNode => Duration::hours(1), - Network::Testnet => Duration::hours(12), - Network::Mainnet => Duration::hours(120), - } -} - -/// Returns the chain ID based on the network. -fn get_chain_id(network: Network) -> String { - match network { - Network::LocalNode => "evmos_9000-4".to_string(), - Network::Testnet => "evmos_9000-4".to_string(), - Network::Mainnet => "evmos_9001-2".to_string(), + assert!(!path_exists(path)); } } diff --git a/src/http.rs b/src/http.rs index a2caed8..9202ce3 100644 --- a/src/http.rs +++ b/src/http.rs @@ -14,13 +14,13 @@ mod tests { async fn test_get_body_pass() { let url = Url::parse("https://httpbin.org/get").unwrap(); let res = get_body(url).await; - assert_eq!(res.is_ok(), true, "the request should be successful"); + assert!(res.is_ok(), "the request should be successful"); } #[tokio::test] async fn test_get_body_fail() { let url = Url::parse("https://invalidurl.org/get").unwrap(); let res = get_body(url).await; - assert_eq!(res.is_err(), true); + assert!(res.is_err()); } } diff --git a/src/inputs.rs b/src/inputs.rs index 34d8d66..3c1b19e 100644 --- a/src/inputs.rs +++ b/src/inputs.rs @@ -214,30 +214,12 @@ mod tests { Utc.with_ymd_and_hms(2023, 10, 27, 11, 0, 0).unwrap() } - #[fixture] - fn testnet_voting_period() -> NetworkConfig { - let mut cfg = NetworkConfig::default(); - cfg.voting_period = Some(Duration::hours(12).num_hours()); - cfg - } - #[fixture] fn mainnet_voting_period() -> NetworkConfig { - let mut cfg = NetworkConfig::default(); - cfg.voting_period = Some(Duration::hours(120).num_hours()); - cfg - } - - #[rstest] - fn test_calculate_planned_date_monday_morning_testnet( - monday_morning: DateTime, - testnet_voting_period: NetworkConfig, - ) { - assert_eq!( - calculate_planned_date(&testnet_voting_period, monday_morning), - Utc.with_ymd_and_hms(2023, 10, 24, 16, 0, 0).unwrap(), - "expected different date for testnet upgrade when calling on monday morning", - ); + NetworkConfig{ + voting_period: Some(Duration::hours(120).num_hours()), + ..NetworkConfig::default() + } } #[rstest] @@ -253,18 +235,6 @@ mod tests { ); } - #[rstest] - fn test_calculate_planned_date_monday_evening_testnet( - monday_evening: DateTime, - testnet_voting_period: NetworkConfig, - ) { - assert_eq!( - calculate_planned_date(&testnet_voting_period, monday_evening), - Utc.with_ymd_and_hms(2023, 10, 25, 16, 0, 0).unwrap(), - "expected different date for testnet upgrade when calling on monday evening", - ); - } - #[rstest] fn test_calculate_planned_date_monday_evening_mainnet( monday_evening: DateTime, @@ -278,19 +248,6 @@ mod tests { ); } - #[rstest] - fn test_calculate_planned_date_friday_morning_testnet( - friday_morning: DateTime, - testnet_voting_period: NetworkConfig, - ) { - assert_eq!( - calculate_planned_date(&testnet_voting_period, friday_morning), - // NOTE: the upgrade should happen on the next monday 4PM, not on saturday which would be t+12h - Utc.with_ymd_and_hms(2023, 10, 30, 16, 0, 0).unwrap(), - "expected different date for testnet upgrade when calling on thursday morning", - ); - } - #[rstest] fn test_calculate_planned_date_friday_morning_mainnet( friday_morning: DateTime, diff --git a/src/keys.rs b/src/keys.rs index 265c94a..8387149 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -10,12 +10,6 @@ pub struct Key { pub address: String, } -/// Contains all necessary configuration to get the keys from the keyring and filter for ones with a balance. -pub struct FilterKeysConfig { - pub config: ClientConfig, - pub network_config: NetworkConfig, -} - /// Returns a list of keys that have a non-zero balance on the configured network. pub async fn get_keys_with_balances( client_config: &ClientConfig, @@ -92,7 +86,7 @@ mod tests { assert!(res.is_ok()); let keys = res.unwrap(); - assert!(keys.len() >= 1); + assert!(!keys.is_empty()); } #[test] diff --git a/src/network.rs b/src/network.rs index ea67362..e315392 100644 --- a/src/network.rs +++ b/src/network.rs @@ -19,12 +19,3 @@ impl fmt::Display for Network { } } } - -/// Returns the native denomination for the given network. -pub fn get_denom(network: Network) -> String { - match network { - Network::LocalNode => "aevmos".to_string(), - Network::Testnet => "atevmos".to_string(), - Network::Mainnet => "aevmos".to_string(), - } -} diff --git a/src/proposal.rs b/src/proposal.rs index bf91d9c..66156f4 100644 --- a/src/proposal.rs +++ b/src/proposal.rs @@ -61,12 +61,13 @@ mod tests { #[test] fn test_render_proposal_pass() { let nc = NetworkConfig::default(); - let mut uc = UpgradeConfig::default(); - uc.previous_version = "v0.0.1".to_string(); - uc.target_version = "v0.1.0".to_string(); - uc.upgrade_time = Utc::now(); - uc.upgrade_height = 60; - uc.upgrade_name = "".to_string(); + let uc = UpgradeConfig{ + previous_version: "v0.0.1".to_string(), + target_version: "v0.1.0".to_string(), + upgrade_time: Utc::now(), + upgrade_height: 60, + ..UpgradeConfig::default() + }; let helper = UpgradeHelper::new(&nc, &uc); diff --git a/src/release.rs b/src/release.rs index c562a21..cd206dc 100644 --- a/src/release.rs +++ b/src/release.rs @@ -87,7 +87,7 @@ mod release_tests { let client = setup_octocrab(&mock_server.uri()); let res = get_release(&client, "invalidj.xjaf/ie").await; - assert_eq!(res.is_err(), true); + assert!(res.is_err()); } } @@ -165,7 +165,7 @@ fn get_checksum_from_assets(assets: &[Asset]) -> Option<&Asset> { /// Returns the OS key from the asset name. fn get_os_key_from_asset_name(name: &str) -> Option { // Check for regex (Linux|Darwin)_(amd64|arm64).tar.gz and store os and arch in variables - return match regex::Regex::new(r"(Linux|Darwin)_(amd64|arm64)") { + match regex::Regex::new(r"(Linux|Darwin)_(amd64|arm64)") { Ok(re) => { let captures = re.captures(name)?; let os = captures.get(1)?.as_str().to_ascii_lowercase(); @@ -177,7 +177,7 @@ fn get_os_key_from_asset_name(name: &str) -> Option { println!("no key found for asset: {}", name); None } - }; + } } /// Downloads the checksum file from the release assets and returns the built checksum string. @@ -201,7 +201,7 @@ fn parse_checksum_line(line: &str) -> Option<(String, String)> { let checksum = parts.next()?.to_string(); let asset_name = parts.next()?.to_string(); - (!parts.next().is_some() && !asset_name.contains("Windows")).then_some((asset_name, checksum)) + (parts.next().is_none() && !asset_name.contains("Windows")).then_some((asset_name, checksum)) } /// Returns an Octocrab instance. diff --git a/src/version.rs b/src/version.rs index 507521a..bdbaa3f 100644 --- a/src/version.rs +++ b/src/version.rs @@ -31,58 +31,56 @@ mod tests { #[test] fn test_is_valid_version_pass() { - assert_eq!(is_valid_version("v14.0.0"), true); - assert_eq!(is_valid_version("v14.0.0-rc1"), true); + assert!(is_valid_version("v14.0.0")); + assert!(is_valid_version("v14.0.0-rc1")); } #[test] fn test_is_valid_version_fail() { - assert_eq!(is_valid_version("v14.0."), false); - assert_eq!(is_valid_version("v.0.1"), false); + assert!(!is_valid_version("v14.0.")); + assert!(!is_valid_version("v.0.1")); } #[test] fn test_is_valid_target_version_local_node_pass() { let cfg = NetworkConfig::default(); - assert_eq!(is_valid_version_for_network(&cfg, "v14.0.0",), true); + assert!(is_valid_version_for_network(&cfg, "v14.0.0",)); } #[test] fn test_is_valid_target_version_local_node_fail() { - assert_eq!( - is_valid_version_for_network(&NetworkConfig::default(), "v14.0",), - false + assert!( + !is_valid_version_for_network(&NetworkConfig::default(), "v14.0",) ); } #[test] fn test_is_valid_target_version_testnet_pass() { - let mut cfg = NetworkConfig::default(); - cfg.allow_rc = true; - assert_eq!(is_valid_version_for_network(&cfg, "v14.0.0-rc1",), true); + let cfg = NetworkConfig{ + allow_rc: true, + ..NetworkConfig::default() + }; + assert!(is_valid_version_for_network(&cfg, "v14.0.0-rc1",)); } #[test] fn test_is_valid_target_version_testnet_fail() { - assert_eq!( - is_valid_version_for_network(&NetworkConfig::default(), "v14.00",), - false + assert!( + !is_valid_version_for_network(&NetworkConfig::default(), "v14.00",) ); } #[test] fn test_is_valid_target_version_mainnet_pass() { - assert_eq!( - is_valid_version_for_network(&NetworkConfig::default(), "v14.0.0",), - true + assert!( + is_valid_version_for_network(&NetworkConfig::default(), "v14.0.0",) ); } #[test] fn test_is_valid_target_version_mainnet_fail() { - assert_eq!( - is_valid_version_for_network(&NetworkConfig::default(), "v14.0.0-rc1",), - false + assert!( + !is_valid_version_for_network(&NetworkConfig::default(), "v14.0.0-rc1",) ); } } From 2c536db1c42e1e4cf058b73b4cc5f07bd10111c7 Mon Sep 17 00:00:00 2001 From: Malte Herrmann Date: Mon, 20 Oct 2025 23:44:41 +0200 Subject: [PATCH 4/8] format --- src/helper.rs | 7 +++++-- src/inputs.rs | 2 +- src/proposal.rs | 2 +- src/version.rs | 30 +++++++++++++++++------------- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/helper.rs b/src/helper.rs index a0d0800..6575619 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -7,7 +7,7 @@ use crate::{ release::{get_instance, get_release}, version, }; -use chrono::{Utc}; +use chrono::Utc; use std::path::Path; use std::{fs, io}; @@ -247,7 +247,10 @@ mod helper_tests { ); // remove the config file - assert!(fs::remove_file(path).is_ok(), "expected success removing config file"); + assert!( + fs::remove_file(path).is_ok(), + "expected success removing config file" + ); } } diff --git a/src/inputs.rs b/src/inputs.rs index 3c1b19e..3dbd0b3 100644 --- a/src/inputs.rs +++ b/src/inputs.rs @@ -216,7 +216,7 @@ mod tests { #[fixture] fn mainnet_voting_period() -> NetworkConfig { - NetworkConfig{ + NetworkConfig { voting_period: Some(Duration::hours(120).num_hours()), ..NetworkConfig::default() } diff --git a/src/proposal.rs b/src/proposal.rs index 66156f4..844da39 100644 --- a/src/proposal.rs +++ b/src/proposal.rs @@ -61,7 +61,7 @@ mod tests { #[test] fn test_render_proposal_pass() { let nc = NetworkConfig::default(); - let uc = UpgradeConfig{ + let uc = UpgradeConfig { previous_version: "v0.0.1".to_string(), target_version: "v0.1.0".to_string(), upgrade_time: Utc::now(), diff --git a/src/version.rs b/src/version.rs index bdbaa3f..b9283f2 100644 --- a/src/version.rs +++ b/src/version.rs @@ -49,14 +49,15 @@ mod tests { #[test] fn test_is_valid_target_version_local_node_fail() { - assert!( - !is_valid_version_for_network(&NetworkConfig::default(), "v14.0",) - ); + assert!(!is_valid_version_for_network( + &NetworkConfig::default(), + "v14.0", + )); } #[test] fn test_is_valid_target_version_testnet_pass() { - let cfg = NetworkConfig{ + let cfg = NetworkConfig { allow_rc: true, ..NetworkConfig::default() }; @@ -65,22 +66,25 @@ mod tests { #[test] fn test_is_valid_target_version_testnet_fail() { - assert!( - !is_valid_version_for_network(&NetworkConfig::default(), "v14.00",) - ); + assert!(!is_valid_version_for_network( + &NetworkConfig::default(), + "v14.00", + )); } #[test] fn test_is_valid_target_version_mainnet_pass() { - assert!( - is_valid_version_for_network(&NetworkConfig::default(), "v14.0.0",) - ); + assert!(is_valid_version_for_network( + &NetworkConfig::default(), + "v14.0.0", + )); } #[test] fn test_is_valid_target_version_mainnet_fail() { - assert!( - !is_valid_version_for_network(&NetworkConfig::default(), "v14.0.0-rc1",) - ); + assert!(!is_valid_version_for_network( + &NetworkConfig::default(), + "v14.0.0-rc1", + )); } } From 749b8443c7b736b1101a3551f85e5bb0e4187458 Mon Sep 17 00:00:00 2001 From: Malte Herrmann Date: Mon, 20 Oct 2025 23:47:36 +0200 Subject: [PATCH 5/8] more linters --- src/balance.rs | 6 ++++-- src/block.rs | 4 ++-- src/command.rs | 13 +++++++------ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/balance.rs b/src/balance.rs index d050c3b..d089997 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -66,8 +66,10 @@ mod tests { async fn test_has_balance() { let mock_server = setup_mock_api().await; - let mut network_config = NetworkConfig::default(); - network_config.rest = Url::parse(mock_server.uri().as_str()).unwrap(); + let network_config = NetworkConfig{ + rest: Url::parse(mock_server.uri().as_str()).unwrap(), + ..NetworkConfig::default() + }; assert!( has_balance(TEST_ADDRESS, &network_config).await.unwrap(), diff --git a/src/block.rs b/src/block.rs index 1c2e886..3469383 100644 --- a/src/block.rs +++ b/src/block.rs @@ -185,7 +185,7 @@ mod tests { assert_eq!(block.height, 18748834, "expected a different block height"); assert_eq!( block.time, - Utc.with_ymd_and_hms(2024, 01, 05, 04, 39, 20).unwrap(), + Utc.with_ymd_and_hms(2024, 1, 5, 4, 39, 20).unwrap(), "expected a different block time", ); } @@ -201,7 +201,7 @@ mod tests { assert_eq!(block.height, 18500000, "expected a different block height"); assert_eq!( block.time, - Utc.with_ymd_and_hms(2023, 11, 07, 02, 41, 36).unwrap(), + Utc.with_ymd_and_hms(2023, 11, 7, 2, 41, 36).unwrap(), "expected a different block time", ); } diff --git a/src/command.rs b/src/command.rs index e180f7e..96f3e66 100644 --- a/src/command.rs +++ b/src/command.rs @@ -80,12 +80,13 @@ mod tests { #[tokio::test] async fn test_prepare_command() { let nc = NetworkConfig::default(); - let mut uc = UpgradeConfig::default(); - uc.previous_version = "v13.0.0".to_string(); - uc.target_version = "v14.0.0".to_string(); - uc.upgrade_time = Utc::now(); - uc.upgrade_height = 60; - uc.upgrade_name = "".to_string(); + let uc = UpgradeConfig{ + previous_version: "v13.0.0".to_string(), + target_version: "v14.0.0".to_string(), + upgrade_time: Utc::now(), + upgrade_height: 60, + ..UpgradeConfig::default() + }; let helper = UpgradeHelper::new(&nc, &uc); From bc33ed78d8bb606673bd48629f79278759161815 Mon Sep 17 00:00:00 2001 From: Malte Herrmann Date: Tue, 21 Oct 2025 00:34:37 +0200 Subject: [PATCH 6/8] fix some tests --- src/helper.rs | 6 ++---- src/inputs.rs | 15 ++++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/helper.rs b/src/helper.rs index 6575619..d0d2997 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -176,23 +176,21 @@ mod helper_tests { let helper = UpgradeHelper::new( &NetworkConfig::default(), &UpgradeConfig { - upgrade_name: "".to_string(), previous_version: "v14.0.0".to_string(), target_version: "v14.0.0-rc1".to_string(), upgrade_time, upgrade_height: 60, - summary: "".to_string(), + ..UpgradeConfig::default() }, ); - assert_eq!(helper.upgrade_config.previous_version, "".to_string()); + assert_eq!(helper.upgrade_config.previous_version, "v14.0.0".to_string()); assert_eq!( helper.upgrade_config.target_version, "v14.0.0-rc1".to_string() ); assert_eq!(helper.upgrade_config.upgrade_time, upgrade_time); assert_eq!(helper.upgrade_config.upgrade_height, 60); - assert_eq!(helper.upgrade_config.summary, "".to_string()); } #[test] diff --git a/src/inputs.rs b/src/inputs.rs index 3dbd0b3..cd1bd13 100644 --- a/src/inputs.rs +++ b/src/inputs.rs @@ -215,8 +215,9 @@ mod tests { } #[fixture] - fn mainnet_voting_period() -> NetworkConfig { + fn network_config() -> NetworkConfig { NetworkConfig { + target_time_utc: "16:00".into(), voting_period: Some(Duration::hours(120).num_hours()), ..NetworkConfig::default() } @@ -225,10 +226,10 @@ mod tests { #[rstest] fn test_calculate_planned_date_monday_morning_mainnet( monday_morning: DateTime, - mainnet_voting_period: NetworkConfig, + network_config: NetworkConfig, ) { assert_eq!( - calculate_planned_date(&mainnet_voting_period, monday_morning), + calculate_planned_date(&network_config, monday_morning), // NOTE: the upgrade should happen on the next monday 4PM, not on saturday which would be t+120h Utc.with_ymd_and_hms(2023, 10, 30, 16, 0, 0).unwrap(), "expected different date for mainnet upgrade when calling on monday morning", @@ -238,10 +239,10 @@ mod tests { #[rstest] fn test_calculate_planned_date_monday_evening_mainnet( monday_evening: DateTime, - mainnet_voting_period: NetworkConfig, + network_config: NetworkConfig, ) { assert_eq!( - calculate_planned_date(&mainnet_voting_period, monday_evening), + calculate_planned_date(&network_config, monday_evening), // NOTE: the upgrade should happen on the next monday 4PM, not on saturday which would be t+120h Utc.with_ymd_and_hms(2023, 10, 30, 16, 0, 0).unwrap(), "expected different date for mainnet upgrade when calling on monday evening", @@ -251,10 +252,10 @@ mod tests { #[rstest] fn test_calculate_planned_date_friday_morning_mainnet( friday_morning: DateTime, - mainnet_voting_period: NetworkConfig, + network_config: NetworkConfig, ) { assert_eq!( - calculate_planned_date(&mainnet_voting_period, friday_morning), + calculate_planned_date(&network_config, friday_morning), // NOTE: the upgrade should happen on the next wednesday 4PM Utc.with_ymd_and_hms(2023, 11, 1, 16, 0, 0).unwrap(), "expected different date for mainnet upgrade when calling on thursday morning", From 131bac5626194a440b6623b4d0f3fe502b5d1af6 Mon Sep 17 00:00:00 2001 From: Malte Herrmann Date: Tue, 21 Oct 2025 11:41:18 +0200 Subject: [PATCH 7/8] some more alignments and removal of outdated items --- src/balance.rs | 2 +- src/cli.rs | 3 ++ src/command.rs | 2 +- src/config.rs | 109 ++++++++++++++++++++++++++++-------------------- src/helper.rs | 11 +++-- src/inputs.rs | 11 +++-- src/proposal.rs | 2 +- src/version.rs | 16 +++---- 8 files changed, 92 insertions(+), 64 deletions(-) diff --git a/src/balance.rs b/src/balance.rs index d089997..c05ed80 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -66,7 +66,7 @@ mod tests { async fn test_has_balance() { let mock_server = setup_mock_api().await; - let network_config = NetworkConfig{ + let network_config = NetworkConfig { rest: Url::parse(mock_server.uri().as_str()).unwrap(), ..NetworkConfig::default() }; diff --git a/src/cli.rs b/src/cli.rs index b0fbb3c..e74aa11 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -50,6 +50,9 @@ pub struct GenerateCommandArgs { } /// Runs the logic for the `generate-command` sub-command. +/// +/// TODO: this should be updated to use the new Cosmos SDK v50 based approach of the +/// `MsgSoftwareUpgrade` from the `x/upgrade` module. pub async fn generate_command(args: GenerateCommandArgs) -> Result<(), CommandError> { let helper_config_path = match args.config { Some(config_file_name) => config_file_name, diff --git a/src/command.rs b/src/command.rs index 96f3e66..513c5d4 100644 --- a/src/command.rs +++ b/src/command.rs @@ -80,7 +80,7 @@ mod tests { #[tokio::test] async fn test_prepare_command() { let nc = NetworkConfig::default(); - let uc = UpgradeConfig{ + let uc = UpgradeConfig { previous_version: "v13.0.0".to_string(), target_version: "v14.0.0".to_string(), upgrade_time: Utc::now(), diff --git a/src/config.rs b/src/config.rs index 9b7a12e..70fa6f2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,12 +2,6 @@ use chrono::{DateTime, Duration, Utc}; use std::{path::PathBuf, str::FromStr}; use url::Url; -/// Contains the configuration of the proposer tool for a given -/// profile. -pub struct Config { - pub networks: Vec, -} - /// Contains the network information for a given network. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct NetworkConfig { @@ -40,45 +34,70 @@ impl Default for NetworkConfig { } } -pub fn get_evmos_config() -> Config { - Config { - networks: vec![ - NetworkConfig { - chain_id: "evmos_9002-1".into(), - cosmos_rpc: Url::parse("http://localhost:26657").expect("cosmos rpc url"), - fee_denom: "aevmos".into(), - name: "Local Node".into(), - path: PathBuf::from_str("/Users/malte/.tmp-evmosd").expect("node home path"), - allow_rc: true, - rest: Url::parse("http://localhost:1317").expect("rest url"), - voting_period: Some(Duration::hours(1).num_hours()), - target_time_utc: "16:00".into(), - }, - NetworkConfig { - chain_id: "evmos_9001-1".into(), - cosmos_rpc: Url::parse("https://rpc.evmos-testnet.lava.build:443") - .expect("cosmos rpc url"), - fee_denom: "atevmos".into(), - name: "Testnet".into(), - path: PathBuf::from_str("/Users/malte/.evmosd").expect("node home path"), - allow_rc: true, - rest: Url::parse("https://rest.evmos-testnet.lava.build").expect("rest url"), - voting_period: Some(Duration::hours(12).num_hours()), - target_time_utc: "16:00".into(), - }, - NetworkConfig { - chain_id: "evmos_9000-4".into(), - cosmos_rpc: Url::parse("https://rpc.evmos.lava.build:443").expect("cosmos rpc url"), - fee_denom: "aevmos".into(), - name: "Mainnet".into(), - path: PathBuf::from_str("/Users/malte/.evmosd").expect("node home path"), - allow_rc: false, - rest: Url::parse("https://rest.evmos.lava.build").expect("rest url"), - voting_period: Some(Duration::hours(120).num_hours()), - target_time_utc: "16:00".into(), - }, - ], - } +/// Returns the available network configurations. +/// +/// TODO: This should eventually read from YAML. +pub fn get_available_configs() -> Vec { + vec![ + NetworkConfig { + chain_id: "evmos_9002-1".into(), + cosmos_rpc: Url::parse("http://localhost:26657").expect("cosmos rpc url"), + fee_denom: "aevmos".into(), + name: "Local Node".into(), + path: PathBuf::from_str("/Users/malteherrmann/.tmp-evmosd").expect("node home path"), + allow_rc: true, + rest: Url::parse("http://localhost:1317").expect("rest url"), + voting_period: Some(Duration::hours(1).num_hours()), + target_time_utc: "16:00".into(), + }, + NetworkConfig { + chain_id: "evmos_9001-1".into(), + cosmos_rpc: Url::parse("https://rpc.evmos-testnet.lava.build:443") + .expect("cosmos rpc url"), + fee_denom: "atevmos".into(), + name: "Testnet".into(), + path: PathBuf::from_str("/Users/malteherrmann/.evmosd").expect("node home path"), + allow_rc: true, + rest: Url::parse("https://rest.evmos-testnet.lava.build").expect("rest url"), + voting_period: Some(Duration::hours(12).num_hours()), + target_time_utc: "16:00".into(), + }, + NetworkConfig { + chain_id: "evmos_9000-4".into(), + cosmos_rpc: Url::parse("https://rpc.evmos.lava.build:443").expect("cosmos rpc url"), + fee_denom: "aevmos".into(), + name: "Mainnet".into(), + path: PathBuf::from_str("/Users/malteherrmann/.evmosd").expect("node home path"), + allow_rc: false, + rest: Url::parse("https://rest.evmos.lava.build").expect("rest url"), + voting_period: Some(Duration::hours(120).num_hours()), + target_time_utc: "16:00".into(), + }, + NetworkConfig { + chain_id: "noble-1".into(), + cosmos_rpc: Url::parse("https://rpc.noble.xyz:443").expect("cosmos rpc url"), + fee_denom: "uusdc".into(), + name: "Noble Mainnet".into(), + // TODO: use $HOME instead and expand, but doesn't work like that out of the box but + // requires manual parsing. + path: PathBuf::from_str("/Users/malteherrmann/.noble").expect("node home path"), + allow_rc: false, + rest: Url::parse("https://api.noble.xyz").expect("rest url"), + voting_period: None, + target_time_utc: "16:00".into(), + }, + NetworkConfig { + chain_id: "grand-1".into(), + cosmos_rpc: Url::parse("https://rpc.testnet.noble.xyz:443").expect("cosmos rpc url"), + fee_denom: "uusdc".into(), + name: "Noble Testnet".into(), + path: PathBuf::from_str("/Users/malteherrmann/.noble").expect("node home path"), + allow_rc: false, + rest: Url::parse("https://api.testnet.noble.xyz").expect("rest url"), + voting_period: None, + target_time_utc: "16:00".into(), + }, + ] } #[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] diff --git a/src/helper.rs b/src/helper.rs index d0d2997..07b71f3 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -114,9 +114,11 @@ pub fn get_helper_from_json(path: &Path) -> Result { } /// Creates a new instance of the upgrade helper based on querying the user for the necessary input. +/// +/// TODO: this is currently doing too much, the summary etc. should be optional and not hardwired. pub async fn get_helper_from_inputs(model: OpenAIModel) -> Result { // Query and check the network to use - let mut network_config = inputs::get_network_config(&config::get_evmos_config())?; + let mut network_config = inputs::get_network_config(config::get_available_configs())?; // Query and check the version to upgrade from let previous_version = inputs::get_text("Previous version to upgrade from:")?; @@ -145,7 +147,7 @@ pub async fn get_helper_from_inputs(model: OpenAIModel) -> Result Result { } /// Prompts the user to select the network type used. -pub fn get_network_config(cfg: &config::Config) -> Result { - let network_options = cfg.networks.iter().map(|n| n.name.clone()).collect(); +pub fn get_network_config( + available_configs: Vec, +) -> Result { + let network_options = available_configs.iter().map(|n| n.name.clone()).collect(); let chosen_network = Select::new("Select network", network_options).prompt()?; - let used_config = cfg - .networks + let used_config = available_configs .iter() .find(|&n| n.name == chosen_network) .expect("expected to find chosen network name"); @@ -90,8 +91,6 @@ pub fn get_network_config(cfg: &config::Config) -> Result Result { let selected_option = inquire::Text::new("Enter the home path to your node keyring") .with_default(cfg.path.as_os_str().to_str().unwrap()) diff --git a/src/proposal.rs b/src/proposal.rs index 844da39..a38b5ee 100644 --- a/src/proposal.rs +++ b/src/proposal.rs @@ -7,7 +7,7 @@ use serde_json::json; /// Renders the proposal template, filling in the necessary information. /// -/// TODO: remember for now +/// TODO: this can be removed or moved to another tool / maybe a plugin type thing pub fn render_proposal(helper: &UpgradeHelper) -> Result { let mut handlebars = Handlebars::new(); handlebars.set_strict_mode(true); diff --git a/src/version.rs b/src/version.rs index b9283f2..8d438d6 100644 --- a/src/version.rs +++ b/src/version.rs @@ -4,7 +4,7 @@ use regex::Regex; /// Returns a boolean value if the defined version fulfills the semantic /// versioning requirements. pub fn is_valid_version(version: &str) -> bool { - Regex::new(r"^v\d+\.\d+\.\d+(-rc\d+)*$") + Regex::new(r"^v\d+\.\d+\.\d+(-rc\.\d+)*$") .unwrap() .is_match(version) } @@ -12,11 +12,13 @@ pub fn is_valid_version(version: &str) -> bool { /// Returns a boolean value if the defined target version fits /// the requirements for the selected network type. /// The target version must be in the format `vX.Y.Z`. -/// Testnet upgrades must use a release candidate with the suffix `-rcX`. +/// +/// Depending on the used network configuration, a release candidate suffix (e.g. -rc.0) +/// either fine or invalid. pub fn is_valid_version_for_network(cfg: &NetworkConfig, target_version: &str) -> bool { - let mut pattern = r"^v\d+\.\d{1}\.\d+".to_string(); + let mut pattern = r"^v\d+\.\d+\.\d+".to_string(); if cfg.allow_rc { - pattern.push_str(r"(-rc\d+)*"); + pattern.push_str(r"(-rc\.\d+)*"); } pattern.push('$'); @@ -32,7 +34,7 @@ mod tests { #[test] fn test_is_valid_version_pass() { assert!(is_valid_version("v14.0.0")); - assert!(is_valid_version("v14.0.0-rc1")); + assert!(is_valid_version("v14.0.0-rc.1")); } #[test] @@ -61,7 +63,7 @@ mod tests { allow_rc: true, ..NetworkConfig::default() }; - assert!(is_valid_version_for_network(&cfg, "v14.0.0-rc1",)); + assert!(is_valid_version_for_network(&cfg, "v14.0.0-rc.1",)); } #[test] @@ -84,7 +86,7 @@ mod tests { fn test_is_valid_target_version_mainnet_fail() { assert!(!is_valid_version_for_network( &NetworkConfig::default(), - "v14.0.0-rc1", + "v14.0.0-rc.1", )); } } From 5de2fad25fcb18ff5285927ad823ecb35d370c32 Mon Sep 17 00:00:00 2001 From: Malte Herrmann Date: Tue, 21 Oct 2025 12:27:36 +0200 Subject: [PATCH 8/8] remove more evmosd references --- README.md | 12 ++++++++---- src/{evmosd.rs => appd.rs} | 6 ++---- src/cli.rs | 2 +- src/command.rs | 3 ++- src/config.rs | 27 +++++++++++++++++---------- src/helper.rs | 9 +++++---- src/keys.rs | 4 ++-- src/main.rs | 2 +- src/templates/command.hbs | 4 ++-- 9 files changed, 40 insertions(+), 29 deletions(-) rename src/{evmosd.rs => appd.rs} (89%) diff --git a/README.md b/README.md index a672857..6eada80 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ This repository contains a helper to aid in generating the contents of Software ## Usage + + **NOTE:** Because the Commonwealth integration is not yet implemented (an API key is already requested) it is not possible to run all of this in one go. @@ -53,15 +55,17 @@ Options: The tool is using OpenAI's LLMs to generate a summary of the changes in the release(s). To use this feature, ensure that you run the binary in an environment where `OPENAI_API_KEY` is set. -- **Configured `.evmosd` Home** +- **Configured `.appd` Home** To generate a shell command that can be instantly used, - the tool is checking `$HOME/.evmosd` for the configured keyring. + the tool is checking the given home directory for the node + to extract the configured keyring. This keyring is then used to get the list of available keys. It is checked, which of those keys hold a balance on the selected network (mainnet/testnet) and lets the user select the one to execute the command with if there are multiple. - To use this feature, ensure that you have your `$HOME/.evmosd` configuration set - so that the configured keyring holds your mainnet or testnet keys. + + To use this feature, ensure that you have your `$HOME/.appd` configuration set + so that the configured keyring holds your desired keys. ## Installation diff --git a/src/evmosd.rs b/src/appd.rs similarity index 89% rename from src/evmosd.rs rename to src/appd.rs index dfb73ef..f4cd04b 100644 --- a/src/evmosd.rs +++ b/src/appd.rs @@ -2,7 +2,7 @@ use crate::errors::ConfigError; use serde::Deserialize; use std::path::Path; -/// The client configuration for the `evmosd` node. +/// The client configuration for the used node binary. /// /// TODO: check if this can be removed #[derive(Clone, Default, Deserialize)] @@ -17,9 +17,7 @@ pub struct ClientConfig { pub broadcast_mode: String, } -/// This method returns the client configuration for the `evmosd` node. -/// -/// TODO: this lib should be renamed to not be tied to `evmosd`. +/// This method returns the client configuration for the used node binary. pub fn get_client_config(path: &Path) -> Result { Ok(toml::from_str::( std::fs::read_to_string(path)?.as_str(), diff --git a/src/cli.rs b/src/cli.rs index e74aa11..88b8f77 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -use crate::evmosd::get_client_config; +use crate::appd::get_client_config; use crate::{ command, errors::{CommandError, ProposalError}, diff --git a/src/command.rs b/src/command.rs index 513c5d4..4b61e3f 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,5 +1,5 @@ +use crate::appd::ClientConfig; use crate::errors::PrepareError; -use crate::evmosd::ClientConfig; use crate::helper::UpgradeHelper; use crate::release::{get_asset_string, get_instance, get_release}; use handlebars::{no_escape, Handlebars}; @@ -48,6 +48,7 @@ pub async fn prepare_command( let data = json!({ "assets": assets, + "bin": helper.network_config.binary, "chain_id": helper.network_config.chain_id, "commonwealth": helper.commonwealth_link, "description": description.replace('\n', "\\n"), // NOTE: this is necessary to not print the actual new lines when rendering the template. diff --git a/src/config.rs b/src/config.rs index 70fa6f2..9c647d4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ use url::Url; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct NetworkConfig { pub allow_rc: bool, + pub binary: PathBuf, pub chain_id: String, pub cosmos_rpc: Url, pub fee_denom: String, @@ -22,6 +23,7 @@ impl Default for NetworkConfig { fn default() -> Self { Self { allow_rc: false, + binary: PathBuf::new(), chain_id: "".into(), cosmos_rpc: Url::parse("http://localhost:26657").expect("cosmos rpc url"), fee_denom: "".into(), @@ -40,40 +42,45 @@ impl Default for NetworkConfig { pub fn get_available_configs() -> Vec { vec![ NetworkConfig { + allow_rc: true, + binary: PathBuf::from_str("evmosd").expect("binary"), chain_id: "evmos_9002-1".into(), cosmos_rpc: Url::parse("http://localhost:26657").expect("cosmos rpc url"), fee_denom: "aevmos".into(), name: "Local Node".into(), path: PathBuf::from_str("/Users/malteherrmann/.tmp-evmosd").expect("node home path"), - allow_rc: true, rest: Url::parse("http://localhost:1317").expect("rest url"), - voting_period: Some(Duration::hours(1).num_hours()), target_time_utc: "16:00".into(), + voting_period: Some(Duration::hours(1).num_hours()), }, NetworkConfig { + allow_rc: true, + binary: PathBuf::from_str("evmosd").expect("binary"), chain_id: "evmos_9001-1".into(), cosmos_rpc: Url::parse("https://rpc.evmos-testnet.lava.build:443") .expect("cosmos rpc url"), fee_denom: "atevmos".into(), name: "Testnet".into(), path: PathBuf::from_str("/Users/malteherrmann/.evmosd").expect("node home path"), - allow_rc: true, rest: Url::parse("https://rest.evmos-testnet.lava.build").expect("rest url"), - voting_period: Some(Duration::hours(12).num_hours()), target_time_utc: "16:00".into(), + voting_period: Some(Duration::hours(12).num_hours()), }, NetworkConfig { + allow_rc: false, + binary: PathBuf::from_str("evmosd").expect("binary"), chain_id: "evmos_9000-4".into(), cosmos_rpc: Url::parse("https://rpc.evmos.lava.build:443").expect("cosmos rpc url"), fee_denom: "aevmos".into(), name: "Mainnet".into(), path: PathBuf::from_str("/Users/malteherrmann/.evmosd").expect("node home path"), - allow_rc: false, rest: Url::parse("https://rest.evmos.lava.build").expect("rest url"), - voting_period: Some(Duration::hours(120).num_hours()), target_time_utc: "16:00".into(), + voting_period: Some(Duration::hours(120).num_hours()), }, NetworkConfig { + allow_rc: false, + binary: PathBuf::from_str("nobled").expect("binary"), chain_id: "noble-1".into(), cosmos_rpc: Url::parse("https://rpc.noble.xyz:443").expect("cosmos rpc url"), fee_denom: "uusdc".into(), @@ -81,21 +88,21 @@ pub fn get_available_configs() -> Vec { // TODO: use $HOME instead and expand, but doesn't work like that out of the box but // requires manual parsing. path: PathBuf::from_str("/Users/malteherrmann/.noble").expect("node home path"), - allow_rc: false, rest: Url::parse("https://api.noble.xyz").expect("rest url"), - voting_period: None, target_time_utc: "16:00".into(), + voting_period: None, }, NetworkConfig { + allow_rc: false, + binary: PathBuf::from_str("nobled").expect("binary"), chain_id: "grand-1".into(), cosmos_rpc: Url::parse("https://rpc.testnet.noble.xyz:443").expect("cosmos rpc url"), fee_denom: "uusdc".into(), name: "Noble Testnet".into(), path: PathBuf::from_str("/Users/malteherrmann/.noble").expect("node home path"), - allow_rc: false, rest: Url::parse("https://api.testnet.noble.xyz").expect("rest url"), - voting_period: None, target_time_utc: "16:00".into(), + voting_period: None, }, ] } diff --git a/src/helper.rs b/src/helper.rs index 07b71f3..fd3d6dc 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,8 +1,9 @@ use crate::{ + appd, block::{get_estimated_height, round_to_nearest_500}, config, errors::{HelperError, InputError, ValidationError}, - evmosd, inputs, + inputs, llm::{create_summary, OpenAIModel}, release::{get_instance, get_release}, version, @@ -83,7 +84,7 @@ impl UpgradeHelper { } // Check if the home folder contains the client configuration - evmosd::get_client_config(&self.network_config.path.join("config/client.toml"))?; + appd::get_client_config(&self.network_config.path.join("config/client.toml"))?; Ok(()) } @@ -148,8 +149,8 @@ pub async fn get_helper_from_inputs(model: OpenAIModel) -> Result Result, KeysError> { - let output = process::Command::new("evmosd") + let output = process::Command::new(&network_config.binary) .args([ "keys", "list", diff --git a/src/main.rs b/src/main.rs index 60e711c..57aabce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ +mod appd; mod balance; mod block; mod cli; mod command; mod config; mod errors; -mod evmosd; mod helper; mod http; mod inputs; diff --git a/src/templates/command.hbs b/src/templates/command.hbs index 19f91f4..e85f9c3 100644 --- a/src/templates/command.hbs +++ b/src/templates/command.hbs @@ -1,4 +1,4 @@ -evmosd tx gov submit-legacy-proposal software-upgrade {{version}} \ +{{bin}} tx gov submit-legacy-proposal software-upgrade {{version}} \ --title "{{title}}" \ --upgrade-height {{height}} \ --description "{{description}}" \ @@ -10,4 +10,4 @@ evmosd tx gov submit-legacy-proposal software-upgrade {{version}} \ --home {{home}} \ --node {{tm_rpc}} \ --upgrade-info '{{assets}}' \ --b sync \ No newline at end of file +-b sync