From e3670570e21a2719f982a16d8fa225ec0273f029 Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Thu, 15 Jan 2026 13:05:21 +0000 Subject: [PATCH 01/10] p2poolv2_config parser --- Cargo.lock | 92 ++++ Cargo.toml | 2 + src/lib.rs | 1 + src/p2poolv2_config_parser.rs | 952 ++++++++++++++++++++++++++++++++++ 4 files changed, 1047 insertions(+) create mode 100644 src/p2poolv2_config_parser.rs diff --git a/Cargo.lock b/Cargo.lock index bc839fb..07b8c07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,42 @@ dependencies = [ "syn", ] +[[package]] +name = "bech32" +version = "0.10.0-beta" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" + +[[package]] +name = "bitcoin" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae" +dependencies = [ + "bech32", + "bitcoin-internals", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -64,6 +100,16 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cc" +version = "1.2.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -361,6 +407,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" + [[package]] name = "fnv" version = "1.0.7" @@ -438,6 +490,18 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex-conservative" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "ident_case" version = "1.0.1" @@ -625,10 +689,12 @@ name = "pdm" version = "0.1.0" dependencies = [ "anyhow", + "bitcoin", "config", "crossterm 0.29.0", "insta", "ratatui", + "serde", "tempfile", ] @@ -806,6 +872,25 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + [[package]] name = "semver" version = "1.0.27" @@ -819,6 +904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] @@ -886,6 +972,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.18" diff --git a/Cargo.toml b/Cargo.toml index 0acfed8..3fd46f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ anyhow = "1.0.100" config = "0.15.19" crossterm = "0.29.0" ratatui = "0.29.0" +serde = { version = "1.0", features = ["derive"] } +bitcoin = "0.31" [dev-dependencies] insta = "1.44.3" diff --git a/src/lib.rs b/src/lib.rs index 9a0fca6..3b3b3d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,4 +5,5 @@ pub mod app; pub mod components; pub mod config; +pub mod p2poolv2_config_parser; pub mod ui; diff --git a/src/p2poolv2_config_parser.rs b/src/p2poolv2_config_parser.rs new file mode 100644 index 0000000..05a4470 --- /dev/null +++ b/src/p2poolv2_config_parser.rs @@ -0,0 +1,952 @@ +// SPDX-FileCopyrightText: 2024 PDM Authors +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use anyhow::{Result, anyhow}; +use bitcoin::secp256k1::PublicKey as CompressedPublicKey; +use bitcoin::{Address, Network, address::NetworkChecked}; +use config::{Config as ConfigLoader, Environment, File, FileFormat}; +use serde::Deserialize; +use std::marker::PhantomData; +use std::path::Path; +use std::str::FromStr; + +// UI MODEL +#[derive(Debug, Clone, PartialEq)] +pub struct ConfigEntry { + pub section: String, + pub key: String, + pub value: String, + pub is_default: bool, +} + +// P2POOL SCHEMA +const MAX_POOL_SIGNATURE_LENGTH: usize = 16; + +#[derive(Debug, Clone, Default)] +pub struct Raw; +#[derive(Debug, Clone)] +pub struct Parsed; + +fn default_hostname() -> String { + "0.0.0.0".to_string() +} +fn default_stratum_port() -> u16 { + 3333 +} +fn default_start_difficulty() -> u64 { + 10000 +} +fn default_minimum_difficulty() -> u64 { + 100 +} +fn default_zmqpubhashblock() -> String { + "tcp://127.0.0.1:28332".to_string() +} +fn default_network() -> Network { + Network::Signet +} +fn default_version_mask() -> i32 { + 0x1fffe000 +} +fn default_difficulty_multiplier() -> f64 { + 1.0 +} +fn default_listen_address() -> String { + "/ip4/0.0.0.0/tcp/6884".to_string() +} + +#[derive(Debug, Deserialize, Clone)] +pub struct StratumConfig { + #[serde(default = "default_hostname")] + pub hostname: String, + #[serde(default = "default_stratum_port")] + pub port: u16, + #[serde(default = "default_start_difficulty")] + pub start_difficulty: u64, + #[serde(default = "default_minimum_difficulty")] + pub minimum_difficulty: u64, + pub maximum_difficulty: Option, + pub solo_address: Option, + #[serde(default = "default_zmqpubhashblock")] + pub zmqpubhashblock: String, + pub bootstrap_address: Option, + pub donation_address: Option, + pub donation: Option, + pub fee_address: Option, + pub fee: Option, + #[serde(default = "default_network", deserialize_with = "deserialize_network")] + pub network: Network, + #[serde( + default = "default_version_mask", + deserialize_with = "deserialize_version_mask" + )] + pub version_mask: i32, + #[serde(default = "default_difficulty_multiplier")] + pub difficulty_multiplier: f64, + pub ignore_difficulty: Option, + pub pool_signature: Option, + #[serde(skip)] + pub(crate) bootstrap_address_parsed: Option>, + #[serde(skip)] + pub(crate) donation_address_parsed: Option>, + #[serde(skip)] + pub(crate) fee_address_parsed: Option>, + #[serde(skip, default)] + _state: PhantomData, +} + +impl StratumConfig { + pub fn parse(self) -> Result> { + if let Some(sig) = &self.pool_signature { + if sig.len() > MAX_POOL_SIGNATURE_LENGTH { + return Err(anyhow!("Pool signature exceeds max length")); + } + } + + let bootstrap = if let Some(addr_str) = &self.bootstrap_address { + let addr = + Address::from_str(addr_str).map_err(|_| anyhow!("Invalid bootstrap_address"))?; + let addr = addr + .require_network(self.network) + .map_err(|_| anyhow!("Invalid bootstrap_address"))?; + Some(addr) + } else { + None + }; + + let donation = if let Some(addr) = &self.donation_address { + Some( + Address::from_str(addr) + .map_err(|_| anyhow!("Invalid donation_address"))? + .require_network(self.network) + .map_err(|_| anyhow!("Invalid donation_address"))?, + ) + } else { + None + }; + + if self.donation.is_some() && donation.is_none() { + return Err(anyhow!("donation_address is required when donation is set")); + } + let fee = if let Some(addr) = &self.fee_address { + Some( + Address::from_str(addr) + .map_err(|_| anyhow!("Invalid fee_address"))? + .require_network(self.network) + .map_err(|_| anyhow!("Invalid fee_address"))?, + ) + } else { + None + }; + + if self.fee.is_some() && fee.is_none() { + return Err(anyhow!("fee_address is required when fee is set")); + } + + Ok(StratumConfig { + hostname: self.hostname, + port: self.port, + start_difficulty: self.start_difficulty, + minimum_difficulty: self.minimum_difficulty, + maximum_difficulty: self.maximum_difficulty, + solo_address: self.solo_address, + zmqpubhashblock: self.zmqpubhashblock, + bootstrap_address: self.bootstrap_address, + donation_address: self.donation_address, + donation: self.donation, + fee_address: self.fee_address, + fee: self.fee, + network: self.network, + version_mask: self.version_mask, + difficulty_multiplier: self.difficulty_multiplier, + ignore_difficulty: self.ignore_difficulty, + pool_signature: self.pool_signature, + bootstrap_address_parsed: bootstrap, + donation_address_parsed: donation, + fee_address_parsed: fee, + _state: PhantomData, + }) + } +} + +fn deserialize_network<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s: String = serde::Deserialize::deserialize(deserializer)?; + Network::from_core_arg(&s).map_err(serde::de::Error::custom) +} + +fn deserialize_version_mask<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s: String = serde::Deserialize::deserialize(deserializer)?; + i32::from_str_radix(&s, 16) + .map_err(|_| serde::de::Error::custom("version_mask must be hex (e.g. 1fffe000)")) +} + +// NETWORK CONFIG + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct NetworkConfig { + #[serde(default = "default_listen_address")] + pub listen_address: String, + #[serde(default)] + pub dial_peers: Vec, + #[serde(default = "d10")] + pub max_pending_incoming: u32, + #[serde(default = "d10")] + pub max_pending_outgoing: u32, + #[serde(default = "d50")] + pub max_established_incoming: u32, + #[serde(default = "d50")] + pub max_established_outgoing: u32, + #[serde(default = "d1_u32")] + pub max_established_per_peer: u32, + #[serde(default = "d10")] + pub max_workbase_per_second: u32, + #[serde(default = "d10")] + pub max_userworkbase_per_second: u32, + #[serde(default = "d100")] + pub max_miningshare_per_second: u32, + #[serde(default = "d100")] + pub max_inventory_per_second: u32, + #[serde(default = "d100")] + pub max_transaction_per_second: u32, + #[serde(default = "d1_u64")] + pub rate_limit_window_secs: u64, + #[serde(default = "d1_u64")] + pub max_requests_per_second: u64, + #[serde(default = "d60")] + pub peer_inactivity_timeout_secs: u64, + #[serde(default = "d30")] + pub dial_timeout_secs: u64, +} + +fn d1_u32() -> u32 { + 1 +} +fn d10() -> u32 { + 10 +} +fn d50() -> u32 { + 50 +} +fn d100() -> u32 { + 100 +} +fn d1_u64() -> u64 { + 1 +} +fn d60() -> u64 { + 60 +} +fn d30() -> u64 { + 30 +} + +#[derive(Debug, Deserialize, Clone)] +pub struct StoreConfig { + pub path: String, + #[serde(default = "d1_u64")] + pub background_task_frequency_hours: u64, + #[serde(default = "d7")] + pub pplns_ttl_days: u64, +} +fn d7() -> u64 { + 7 +} + +#[derive(Debug, Deserialize, Clone)] +pub struct MinerConfig { + pub pubkey: String, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct BitcoinRpcConfig { + pub url: String, + pub username: String, + pub password: String, +} + +#[derive(Debug, Deserialize, Default, Clone)] +pub struct LoggingConfig { + pub file: Option, + #[serde(default = "log_level")] + pub level: String, + #[serde(default = "stats_dir")] + pub stats_dir: String, +} +fn log_level() -> String { + "info".into() +} +fn stats_dir() -> String { + "./logs/stats".into() +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ApiConfig { + pub hostname: String, + pub port: u16, + pub auth_user: Option, + pub auth_token: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct P2PoolConfig { + #[serde(default)] + pub network: NetworkConfig, + pub store: Option, + pub stratum: Option>, + pub miner: Option, + pub bitcoinrpc: Option, + #[serde(default)] + pub logging: LoggingConfig, + pub api: Option, +} + +// PARSER + +pub fn parse_config(path: &Path) -> Result> { + let raw_text = if path.exists() { + std::fs::read_to_string(path).unwrap_or_default() + } else { + String::new() + }; + + let env_override_present = std::env::vars().any(|(k, _)| k.starts_with("P2POOL_")); + + // Accept configs with any known section or env var + let looks_like_p2pool = env_override_present + || raw_text.contains("[stratum]") + || raw_text.contains("[store]") + || raw_text.contains("[network]") + || raw_text.contains("[logging]") + || raw_text.contains("[api]") + || raw_text.contains("[miner]") + || raw_text.contains("[bitcoinrpc]"); + + if !looks_like_p2pool { + return Err(anyhow!("Invalid P2Pool config: not a p2pool configuration")); + } + + let mut cfg = ConfigLoader::builder(); + if path.exists() { + cfg = cfg.add_source(File::from(path).format(FileFormat::Toml)); + } + cfg = cfg.add_source(Environment::with_prefix("P2POOL").separator("_")); + let raw = cfg.build()?; + + let p: P2PoolConfig = raw.clone().try_deserialize().map_err(|e| { + anyhow!( + "Failed to deserialize config: {}\nFile: {}", + e, + path.display() + ) + })?; + + if let Some(stratum_raw) = &p.stratum { + stratum_raw.clone().parse()?; + } + + let network_section_present = raw_text.contains("[network]"); + let stratum_section_present = raw_text.contains("[stratum]"); + let logging_section_present = raw_text.contains("[logging]"); + + Ok(flatten( + &p, + network_section_present, + stratum_section_present, + logging_section_present, + )) +} + +// FLATTENER + +fn flatten( + p: &P2PoolConfig, + network_section_present: bool, + stratum_section_present: bool, + logging_section_present: bool, +) -> Vec { + let mut e = Vec::new(); + + // NETWORK + let n = &p.network; + macro_rules! n { + ($k:expr, $v:expr, $default_val:expr) => { + push( + &mut e, + "network", + $k, + $v, + !network_section_present && $default_val, + ) + }; + } + n!( + "listen_address", + n.listen_address.clone(), + n.listen_address.is_empty() + ); + n!( + "dial_peers", + n.dial_peers.join(", "), + n.dial_peers.is_empty() + ); + n!( + "max_pending_incoming", + n.max_pending_incoming.to_string(), + n.max_pending_incoming == 10 + ); + n!( + "max_pending_outgoing", + n.max_pending_outgoing.to_string(), + n.max_pending_outgoing == 10 + ); + n!( + "max_established_incoming", + n.max_established_incoming.to_string(), + n.max_established_incoming == 50 + ); + n!( + "max_established_outgoing", + n.max_established_outgoing.to_string(), + n.max_established_outgoing == 50 + ); + n!( + "max_established_per_peer", + n.max_established_per_peer.to_string(), + !network_section_present && n.max_established_per_peer == 1 + ); + + n!( + "max_workbase_per_second", + n.max_workbase_per_second.to_string(), + n.max_workbase_per_second == 10 + ); + n!( + "max_userworkbase_per_second", + n.max_userworkbase_per_second.to_string(), + n.max_userworkbase_per_second == 10 + ); + n!( + "max_miningshare_per_second", + n.max_miningshare_per_second.to_string(), + n.max_miningshare_per_second == 100 + ); + n!( + "max_inventory_per_second", + n.max_inventory_per_second.to_string(), + n.max_inventory_per_second == 100 + ); + n!( + "max_transaction_per_second", + n.max_transaction_per_second.to_string(), + n.max_transaction_per_second == 100 + ); + n!( + "rate_limit_window_secs", + n.rate_limit_window_secs.to_string(), + n.rate_limit_window_secs == 1 + ); + n!( + "max_requests_per_second", + n.max_requests_per_second.to_string(), + n.max_requests_per_second == 1 + ); + n!( + "peer_inactivity_timeout_secs", + n.peer_inactivity_timeout_secs.to_string(), + n.peer_inactivity_timeout_secs == 60 + ); + n!( + "dial_timeout_secs", + n.dial_timeout_secs.to_string(), + n.dial_timeout_secs == 30 + ); + + // STORE + if let Some(s) = &p.store { + macro_rules! s_store { + ($k:expr, $v:expr, $d:expr) => { + push(&mut e, "store", $k, $v, $d) + }; + } + s_store!("path", s.path.clone(), s.path == "./store.db"); + s_store!( + "background_task_frequency_hours", + s.background_task_frequency_hours.to_string(), + s.background_task_frequency_hours == 1 + ); + s_store!( + "pplns_ttl_days", + s.pplns_ttl_days.to_string(), + s.pplns_ttl_days == 7 + ); + } + + // STRATUM + if let Some(stratum) = &p.stratum { + macro_rules! stratum_m { + ($k:expr, $v:expr, $d:expr) => { + push(&mut e, "stratum", $k, $v, !stratum_section_present && $d) + }; + } + stratum_m!( + "hostname", + stratum.hostname.clone(), + stratum.hostname == "0.0.0.0" + ); + stratum_m!("port", stratum.port.to_string(), stratum.port == 3333); + stratum_m!( + "start_difficulty", + stratum.start_difficulty.to_string(), + stratum.start_difficulty == 10000 + ); + stratum_m!( + "minimum_difficulty", + stratum.minimum_difficulty.to_string(), + stratum.minimum_difficulty == 100 + ); + opt( + &mut e, + "stratum", + "maximum_difficulty", + stratum.maximum_difficulty.map(|v| v.to_string()), + false, + ); + opt( + &mut e, + "stratum", + "solo_address", + stratum.solo_address.clone(), + false, + ); + stratum_m!( + "zmqpubhashblock", + stratum.zmqpubhashblock.clone(), + stratum.zmqpubhashblock == "tcp://127.0.0.1:28332" + ); + opt( + &mut e, + "stratum", + "bootstrap_address", + stratum.bootstrap_address.clone(), + false, + ); + opt( + &mut e, + "stratum", + "donation_address", + stratum.donation_address.clone(), + false, + ); + opt( + &mut e, + "stratum", + "donation", + stratum.donation.map(|v| { + let pct = v as f64 / 100.0; + if pct.fract() == 0.0 { + format!("{} bp ({:.0}%)", v, pct) + } else { + format!("{} bp ({:.2}%)", v, pct) + } + }), + false, + ); + opt( + &mut e, + "stratum", + "fee_address", + stratum.fee_address.clone(), + false, + ); + opt( + &mut e, + "stratum", + "fee", + stratum.fee.map(|v| { + let pct = v as f64 / 100.0; + if pct.fract() == 0.0 { + format!("{} bp ({:.0}%)", v, pct) + } else { + format!("{} bp ({:.2}%)", v, pct) + } + }), + false, + ); + stratum_m!( + "network", + format!("{:?}", stratum.network).to_lowercase(), + stratum.network == Network::Signet + ); + stratum_m!( + "version_mask", + format!("{:08x}", stratum.version_mask), + stratum.version_mask == 0x1fffe000 + ); + stratum_m!( + "difficulty_multiplier", + format!("{:.1}", stratum.difficulty_multiplier), + stratum.difficulty_multiplier == 1.0 + ); + opt( + &mut e, + "stratum", + "ignore_difficulty", + stratum.ignore_difficulty.map(|v| v.to_string()), + false, + ); + opt( + &mut e, + "stratum", + "pool_signature", + stratum.pool_signature.clone(), + false, + ); + } + + // MINER + if let Some(m) = &p.miner { + if CompressedPublicKey::from_str(&m.pubkey).is_err() { + push(&mut e, "miner", "pubkey", "".into(), false); + } else { + push(&mut e, "miner", "pubkey", m.pubkey.clone(), false); + } + } + + // BITCOIN RPC + if let Some(b) = &p.bitcoinrpc { + macro_rules! b_m { + ($k:expr, $v:expr, $d:expr) => { + push(&mut e, "bitcoinrpc", $k, $v, $d) + }; + } + b_m!("url", b.url.clone(), b.url == "http://127.0.0.1:38332"); + b_m!("username", b.username.clone(), b.username == "p2pool"); + b_m!( + "password", + if b.password.is_empty() { + "".into() + } else { + "*****".into() + }, + false + ); + } + + // LOGGING + let l = &p.logging; + macro_rules! l_m { + ($k:expr, $v:expr, $d:expr) => { + push(&mut e, "logging", $k, $v, !logging_section_present && $d) + }; + } + opt(&mut e, "logging", "file", l.file.clone(), false); + l_m!("level", l.level.clone(), l.level == "info"); + l_m!( + "stats_dir", + l.stats_dir.clone(), + l.stats_dir == "./logs/stats" + ); + + // API + if let Some(a) = &p.api { + macro_rules! a_m { + ($k:expr, $v:expr, $d:expr) => { + push(&mut e, "api", $k, $v, $d) + }; + } + a_m!("hostname", a.hostname.clone(), a.hostname == "127.0.0.1"); + a_m!("port", a.port.to_string(), false); + opt(&mut e, "api", "auth_user", a.auth_user.clone(), false); + opt( + &mut e, + "api", + "auth_token", + a.auth_token.clone().map(|_| String::from("*****")), + false, + ); + } + + e +} + +// HELPERS +fn push(e: &mut Vec, s: &str, k: &str, v: String, is_default: bool) { + e.push(ConfigEntry { + section: s.into(), + key: k.into(), + value: v, + is_default, + }); +} + +fn opt(e: &mut Vec, s: &str, k: &str, v: Option, is_default: bool) { + if let Some(v) = v { + push(e, s, k, v.to_string(), is_default); + } +} + +impl From> for StratumConfig { + fn from(parsed: StratumConfig) -> Self { + StratumConfig { + hostname: parsed.hostname, + port: parsed.port, + start_difficulty: parsed.start_difficulty, + minimum_difficulty: parsed.minimum_difficulty, + maximum_difficulty: parsed.maximum_difficulty, + solo_address: parsed.solo_address, + zmqpubhashblock: parsed.zmqpubhashblock, + bootstrap_address: parsed.bootstrap_address, + donation_address: parsed.donation_address, + donation: parsed.donation, + fee_address: parsed.fee_address, + fee: parsed.fee, + network: parsed.network, + version_mask: parsed.version_mask, + difficulty_multiplier: parsed.difficulty_multiplier, + ignore_difficulty: parsed.ignore_difficulty, + pool_signature: parsed.pool_signature, + bootstrap_address_parsed: None, + donation_address_parsed: None, + fee_address_parsed: None, + _state: PhantomData, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::Write; + use tempfile::tempdir; + + fn write_cfg(txt: &str) -> (std::path::PathBuf, tempfile::TempDir) { + let dir = tempdir().unwrap(); + let path = dir.path().join("p2pool.toml"); + let mut f = File::create(&path).unwrap(); + f.write_all(txt.as_bytes()).unwrap(); + (path, dir) + } + + #[test] + fn loads_and_flattens_full_config() { + let (path, _dir) = write_cfg( + r#" +[network] +listen_address = "/ip4/127.0.0.1/tcp/6884" +dial_peers = ["p1", "p2"] + +[store] +path = "./store.db" +background_task_frequency_hours = 24 +pplns_ttl_days = 7 + +[stratum] +hostname = "0.0.0.0" +port = 3333 +start_difficulty = 10000 +minimum_difficulty = 100 +maximum_difficulty = 1000000 +solo_address = "tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk" +bootstrap_address = "tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk" +donation_address = "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx" +donation = 100 +fee_address = "tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk" +fee = 50 +zmqpubhashblock = "tcp://127.0.0.1:28332" +network = "signet" +version_mask = "1fffe000" +difficulty_multiplier = 1.0 +ignore_difficulty = true +pool_signature = "TestPool" + +[miner] +pubkey = "020202020202020202020202020202020202020202020202020202020202020202" + +[bitcoinrpc] +url = "http://127.0.0.1:38332" +username = "user" +password = "pass" + +[logging] +file = "./logs/p2pool.log" +level = "debug" +stats_dir = "./logs/stats" + +[api] +hostname = "127.0.0.1" +port = 46884 +auth_user = "admin" +auth_token = "token" +"#, + ); + + let entries = parse_config(&path).unwrap(); + + assert!(entries.iter().any(|x| x.section == "network" + && x.key == "listen_address" + && x.value == "/ip4/127.0.0.1/tcp/6884" + && !x.is_default)); + assert!( + entries + .iter() + .any(|x| x.section == "stratum" && x.key == "hostname" && !x.is_default) + ); + assert!( + entries + .iter() + .any(|x| x.section == "stratum" && x.key == "donation" && x.value == "100 bp (1%)") + ); + assert!( + entries + .iter() + .any(|x| x.section == "bitcoinrpc" && x.key == "password" && x.value == "*****") + ); + assert!( + entries + .iter() + .any(|x| x.section == "api" && x.key == "auth_token" && x.value == "*****") + ); + assert!( + entries + .iter() + .any(|x| x.section == "stratum" && x.key == "network" && x.value == "signet") + ); + assert!( + entries.iter().any(|x| x.section == "stratum" + && x.key == "version_mask" + && x.value == "1fffe000") + ); + } + + #[test] + fn invalid_address_fails() { + let (path, _dir) = write_cfg( + r#" +[stratum] +hostname = "0.0.0.0" +port = 3333 +start_difficulty = 10000 +minimum_difficulty = 100 +bootstrap_address = "invalid" +zmqpubhashblock = "tcp://127.0.0.1:28332" +network = "signet" +version_mask = "1fffe000" +difficulty_multiplier = 1.0 + +[store] +path = "./store.db" + +[bitcoinrpc] +url = "http://127.0.0.1:38332" +username = "p2pool" +password = "p2pool" + +[api] +hostname = "127.0.0.1" +port = 46884 +"#, + ); + + let err = parse_config(&path).unwrap_err(); + assert!(err.to_string().contains("Invalid bootstrap_address")); + } + + #[test] + fn pool_signature_too_long_fails() { + let (path, _dir) = write_cfg( + r#" +[stratum] +hostname = "0.0.0.0" +port = 3333 +start_difficulty = 10000 +minimum_difficulty = 100 +bootstrap_address = "tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk" +zmqpubhashblock = "tcp://127.0.0.1:28332" +network = "signet" +version_mask = "1fffe000" +difficulty_multiplier = 1.0 +pool_signature = "ThisIsWayTooLongForASignature" + +[store] +path = "./store.db" + +[bitcoinrpc] +url = "http://127.0.0.1:38332" +username = "p2pool" +password = "p2pool" + +[api] +hostname = "127.0.0.1" +port = 46884 +"#, + ); + + let err = parse_config(&path).unwrap_err(); + assert!( + err.to_string() + .contains("Pool signature exceeds max length") + ); + } + + #[test] + fn env_var_override_works() { + unsafe { std::env::set_var("P2POOL_STRATUM_PORT", "9999") }; + + let (path, _dir) = write_cfg( + r#" +[stratum] +hostname = "0.0.0.0" +port = 3333 +start_difficulty = 10000 +minimum_difficulty = 100 +bootstrap_address = "tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk" +zmqpubhashblock = "tcp://127.0.0.1:28332" +network = "signet" +version_mask = "1fffe000" +difficulty_multiplier = 1.0 + +[store] +path = "./store.db" + +[bitcoinrpc] +url = "http://127.0.0.1:38332" +username = "p2pool" +password = "p2pool" + +[api] +hostname = "127.0.0.1" +port = 46884 +"#, + ); + + let entries = parse_config(&path).unwrap(); + assert!( + entries + .iter() + .any(|x| x.section == "stratum" && x.key == "port" && x.value == "9999") + ); + + unsafe { std::env::remove_var("P2POOL_STRATUM_PORT") }; + } + + #[test] + fn non_p2pool_file_fails() { + let (path, _dir) = write_cfg( + r#" +foo = "bar" +answer = 42 +"#, + ); + + let err = parse_config(&path).unwrap_err(); + assert!(err.to_string().contains("Invalid P2Pool config")); + } +} From 866d1cb3214e977f6933564567594be6fa4ca3dc Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Thu, 22 Jan 2026 13:55:05 +0000 Subject: [PATCH 02/10] added test cases for wrong network address , minimal config , address --- src/p2poolv2_config_parser.rs | 87 +++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/p2poolv2_config_parser.rs b/src/p2poolv2_config_parser.rs index 05a4470..dddb406 100644 --- a/src/p2poolv2_config_parser.rs +++ b/src/p2poolv2_config_parser.rs @@ -949,4 +949,91 @@ answer = 42 let err = parse_config(&path).unwrap_err(); assert!(err.to_string().contains("Invalid P2Pool config")); } + + #[test] + fn wrong_network_address_is_rejected() { + // bc1... is a MAINNET address, but network is set to signet + let (path, _dir) = write_cfg( + r#" +[stratum] +network = "signet" +bootstrap_address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080" +version_mask = "1fffe000" +zmqpubhashblock = "tcp://127.0.0.1:28332" + +[store] +path = "./store.db" + +[bitcoinrpc] +url = "http://127.0.0.1:38332" +username = "p2pool" +password = "p2pool" + +[api] +hostname = "127.0.0.1" +port = 46884 +"#, + ); + + let err = parse_config(&path).unwrap_err(); + + assert!( + err.to_string().contains("Invalid bootstrap_address"), + "expected wrong-network address to be rejected, got: {err}" + ); + } + + #[test] + fn minimal_config_uses_defaults() { + // ensures Serde defaults + flattening actually work together + let (path, _dir) = write_cfg( + r#" +[stratum] +network = "signet" +version_mask = "1fffe000" +zmqpubhashblock = "tcp://127.0.0.1:28332" +"#, + ); + + let entries = parse_config(&path).unwrap(); + + assert!(entries.iter().any(|e| e.key == "port" && e.value == "3333")); + assert!( + entries + .iter() + .any(|e| e.key == "minimum_difficulty" && e.value == "100") + ); + } + + #[test] + fn donation_without_address_fails() { + let (path, _dir) = write_cfg( + r#" +[stratum] +donation = 100 +network = "signet" +version_mask = "1fffe000" +zmqpubhashblock = "tcp://127.0.0.1:28332" +"#, + ); + + let err = parse_config(&path).unwrap_err(); + assert!(err.to_string().contains("donation_address is required")); + } + + #[test] + fn fee_without_address_fails() { + let (path, _dir) = write_cfg( + r#" +[stratum] +fee = 50 +network = "signet" +version_mask = "1fffe000" +zmqpubhashblock = "tcp://127.0.0.1:28332" +"#, + ); + + let err = parse_config(&path).unwrap_err(); + assert!(err.to_string().contains("fee_address is required")); + } } From 26bee7428ab158697d694733ec96de16f1274f3a Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Thu, 22 Jan 2026 14:07:29 +0000 Subject: [PATCH 03/10] add test parsed_to_raw_preserves_all_fields --- src/p2poolv2_config_parser.rs | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/p2poolv2_config_parser.rs b/src/p2poolv2_config_parser.rs index dddb406..c2371e7 100644 --- a/src/p2poolv2_config_parser.rs +++ b/src/p2poolv2_config_parser.rs @@ -1036,4 +1036,52 @@ zmqpubhashblock = "tcp://127.0.0.1:28332" let err = parse_config(&path).unwrap_err(); assert!(err.to_string().contains("fee_address is required")); } + + #[test] + fn parsed_to_raw_preserves_all_fields() { + let parsed = StratumConfig:: { + hostname: "0.0.0.0".into(), + port: 3333, + start_difficulty: 10000, + minimum_difficulty: 100, + maximum_difficulty: Some(1_000_000), + solo_address: Some("tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk".into()), + zmqpubhashblock: "tcp://127.0.0.1:28332".into(), + bootstrap_address: Some("tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk".into()), + donation_address: Some("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx".into()), + donation: Some(100), + fee_address: Some("tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk".into()), + fee: Some(50), + network: Network::Signet, + version_mask: 0x1fffe000, + difficulty_multiplier: 1.0, + ignore_difficulty: Some(true), + pool_signature: Some("TestPool".into()), + bootstrap_address_parsed: None, + donation_address_parsed: None, + fee_address_parsed: None, + _state: PhantomData, + }; + + let raw: StratumConfig = parsed.clone().into(); + + // Explicit field-by-field assertions + assert_eq!(raw.hostname, parsed.hostname); + assert_eq!(raw.port, parsed.port); + assert_eq!(raw.start_difficulty, parsed.start_difficulty); + assert_eq!(raw.minimum_difficulty, parsed.minimum_difficulty); + assert_eq!(raw.maximum_difficulty, parsed.maximum_difficulty); + assert_eq!(raw.solo_address, parsed.solo_address); + assert_eq!(raw.zmqpubhashblock, parsed.zmqpubhashblock); + assert_eq!(raw.bootstrap_address, parsed.bootstrap_address); + assert_eq!(raw.donation_address, parsed.donation_address); + assert_eq!(raw.donation, parsed.donation); + assert_eq!(raw.fee_address, parsed.fee_address); + assert_eq!(raw.fee, parsed.fee); + assert_eq!(raw.network, parsed.network); + assert_eq!(raw.version_mask, parsed.version_mask); + assert_eq!(raw.difficulty_multiplier, parsed.difficulty_multiplier); + assert_eq!(raw.ignore_difficulty, parsed.ignore_difficulty); + assert_eq!(raw.pool_signature, parsed.pool_signature); + } } From 369dbf0432f13dd8d1e56cd15004c9e9a8da8a41 Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Thu, 22 Jan 2026 14:17:23 +0000 Subject: [PATCH 04/10] chore: trigger CI From 225755a4ed675986417cfead91159f11639d371e Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Fri, 23 Jan 2026 16:35:11 +0000 Subject: [PATCH 05/10] Tui for p2poolv2 config --- src/app.rs | 30 ++- src/components/file_explorer.rs | 23 +++ src/main.rs | 325 ++++++++++---------------------- src/ui.rs | 121 ++++++++++-- 4 files changed, 258 insertions(+), 241 deletions(-) diff --git a/src/app.rs b/src/app.rs index 62bf6fd..1ba4e35 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,21 +3,44 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use crate::components::file_explorer::FileExplorer; +use crate::config::ConfigEntry as BitcoinEntry; +use crate::p2poolv2_config_parser::ConfigEntry as P2PoolEntry; use std::path::PathBuf; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum CurrentScreen { Home, BitcoinConfig, + P2PoolConfig, FileExplorer, Exiting, } +/// Actions that components (Explorer, Editors) can trigger. +/// This decouples input handling from business logic. +#[derive(Debug, Clone)] +pub enum AppAction { + None, + Quit, + ToggleMenu, + Navigate(CurrentScreen), + // Triggers the file explorer for a specific screen + OpenExplorer(CurrentScreen), + // Returned by the Explorer when user picks a file + FileSelected(PathBuf), + // Closes the explorer without selection + CloseModal, +} + pub struct App { pub current_screen: CurrentScreen, pub sidebar_index: usize, + pub explorer_trigger: Option, pub bitcoin_conf_path: Option, + pub p2pool_conf_path: Option, pub explorer: FileExplorer, + pub p2pool_data: Vec, + pub bitcoin_data: Vec, } impl App { @@ -25,8 +48,12 @@ impl App { App { current_screen: CurrentScreen::Home, sidebar_index: 0, + explorer_trigger: None, bitcoin_conf_path: None, + p2pool_conf_path: None, explorer: FileExplorer::new(), + p2pool_data: Vec::new(), + bitcoin_data: Vec::new(), } } @@ -35,6 +62,7 @@ impl App { match self.sidebar_index { 0 => self.current_screen = CurrentScreen::Home, 1 => self.current_screen = CurrentScreen::BitcoinConfig, + 2 => self.current_screen = CurrentScreen::P2PoolConfig, _ => {} } } diff --git a/src/components/file_explorer.rs b/src/components/file_explorer.rs index f5c476e..fee01b0 100644 --- a/src/components/file_explorer.rs +++ b/src/components/file_explorer.rs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use crate::app::AppAction; +use crossterm::event::{KeyCode, KeyEvent}; use std::fs; use std::path::PathBuf; @@ -115,6 +117,27 @@ impl FileExplorer { None } + + pub fn handle_input(&mut self, key: KeyEvent) -> AppAction { + match key.code { + KeyCode::Up => { + self.previous(); + AppAction::None + } + KeyCode::Down => { + self.next(); + AppAction::None + } + KeyCode::Enter => { + if let Some(path) = self.select() { + return AppAction::FileSelected(path); + } + AppAction::None + } + KeyCode::Esc => AppAction::CloseModal, + _ => AppAction::None, + } + } } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index da04a67..bf27efb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,15 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +use pdm::app::AppAction; use pdm::app::{App, CurrentScreen}; +use pdm::config::parse_config as parse_bitcoin_config; +use pdm::p2poolv2_config_parser::parse_config as parse_p2pool_config; use pdm::ui; use anyhow::Result; use crossterm::{ - event::{self, Event, KeyCode, KeyEventKind}, + event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, }; @@ -15,266 +18,142 @@ use ratatui::{Terminal, backend::Backend, backend::CrosstermBackend}; use std::io; fn main() -> Result<()> { - // Setup Terminal + // Setup Terminal enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - // Run App + // Run App let mut app = App::new(); - let res = run_app(&mut terminal, &mut app, |_app: &mut App| event::read()); + let res = run_app(&mut terminal, &mut app); - // Restore Terminal + // Restore Terminal disable_raw_mode()?; execute!(terminal.backend_mut(), LeaveAlternateScreen)?; terminal.show_cursor()?; if let Err(err) = res { - println!("{err:?}"); + println!("Error: {:?}", err); } - Ok(()) } -// Accept any Backend and an Event Provider Closure -fn run_app( - terminal: &mut Terminal, - app: &mut App, - mut event_provider: F, -) -> io::Result<()> -where - F: FnMut(&mut App) -> io::Result, -{ +fn run_app(terminal: &mut Terminal>, app: &mut App) -> Result<()> { loop { terminal.draw(|f| ui::ui(f, app))?; - // We check the event from our provider - if let Event::Key(key) = event_provider(app)? - && key.kind == KeyEventKind::Press - { - if key.code == KeyCode::Char('q') { - return Ok(()); - } - match app.current_screen { - // File Explorer Modal - CurrentScreen::FileExplorer => match key.code { - KeyCode::Up => app.explorer.previous(), - KeyCode::Down => app.explorer.next(), - KeyCode::Esc => app.toggle_menu(), // Cancel - KeyCode::Enter => { - if let Some(path) = app.explorer.select() { - // File Selected! - app.bitcoin_conf_path = Some(path); - app.toggle_menu(); // Go back to main screen - } - } - _ => {} - }, + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + if key.code == KeyCode::Char('q') + || (key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('c')) + { + return Ok(()); + } - // Standard Navigation - _ => match key.code { - KeyCode::Up => { - if app.sidebar_index > 0 { - app.sidebar_index -= 1; - app.toggle_menu(); + if app.current_screen == CurrentScreen::FileExplorer { + let action = app.explorer.handle_input(key); + handle_action(action, app)?; + continue; + } + + let action = if app.current_screen == CurrentScreen::FileExplorer { + app.explorer.handle_input(key) + } else { + // Global Navigation (Sidebar & Opens Explorer) + match key.code { + // Enter: Opens Explorer if we are on a config screen + KeyCode::Enter => { + if app.current_screen == CurrentScreen::BitcoinConfig + || app.current_screen == CurrentScreen::P2PoolConfig + { + AppAction::OpenExplorer(app.current_screen.clone()) + } else { + AppAction::None + } } - } - KeyCode::Down => { - if app.sidebar_index < 1 { - app.sidebar_index += 1; - app.toggle_menu(); + + // Sidebar Navigation + KeyCode::Down => { + if app.sidebar_index < 2 { + app.sidebar_index += 1; + AppAction::ToggleMenu + } else { + AppAction::None + } } - } - KeyCode::Enter => { - // If we are on "Bitcoin Config", open the explorer - if app.current_screen == CurrentScreen::BitcoinConfig { - app.current_screen = CurrentScreen::FileExplorer; + KeyCode::Up => { + if app.sidebar_index > 0 { + app.sidebar_index -= 1; + AppAction::ToggleMenu + } else { + AppAction::None + } } + _ => AppAction::None, } - _ => {} - }, + }; + + if handle_action(action, app)? { + return Ok(()); + } } } } } -#[cfg(test)] -mod tests { - use super::*; - use crossterm::event::{KeyEvent, KeyEventState, KeyModifiers}; - use ratatui::backend::TestBackend; +// Logic Handler +fn handle_action(action: AppAction, app: &mut App) -> Result { + match action { + AppAction::Quit => return Ok(true), - #[test] - fn test_app_integration_smoke_test() { - let backend = TestBackend::new(80, 25); - let mut terminal = Terminal::new(backend).unwrap(); - let mut app = App::new(); + AppAction::ToggleMenu => app.toggle_menu(), - let mut step = 0; + AppAction::OpenExplorer(trigger) => { + app.explorer_trigger = Some(trigger); + app.current_screen = CurrentScreen::FileExplorer; + } - let event_provider = |_app: &mut App| { - step += 1; - match step { - 1 => Ok(Event::Key(KeyEvent { - code: KeyCode::Down, - modifiers: KeyModifiers::empty(), - kind: KeyEventKind::Press, - state: KeyEventState::empty(), - })), - 2 => Ok(Event::Key(KeyEvent { - code: KeyCode::Char('q'), - modifiers: KeyModifiers::empty(), - kind: KeyEventKind::Press, - state: KeyEventState::empty(), - })), - _ => panic!("Should have exited"), + AppAction::CloseModal => { + if let Some(trigger) = &app.explorer_trigger { + app.current_screen = trigger.clone(); + } else { + app.toggle_menu(); } - }; - - // First frame - terminal.draw(|f| ui::ui(f, &mut app)).unwrap(); - insta::assert_debug_snapshot!("home_screen", terminal.backend()); - - // Run app (process events + redraws) - let res = run_app(&mut terminal, &mut app, event_provider); - assert!(res.is_ok()); - - // Final frame after DOWN - insta::assert_debug_snapshot!("menu_toggled", terminal.backend()); - - assert_eq!(app.sidebar_index, 1); - } - - #[test] - fn test_file_explorer_flow() { - // Setup - let backend = TestBackend::new(80, 25); - let mut terminal = Terminal::new(backend).unwrap(); - let mut app = App::new(); - - // Define Steps - let mut step = 0; - let event_provider = |app: &mut App| { - step += 1; - match step { - 1 => { - // Start at Home. - // Action: Move DOWN to "Bitcoin Config" - Ok(Event::Key(KeyEvent::new( - KeyCode::Down, - KeyModifiers::empty(), - ))) - } - 2 => { - // Action: Press ENTER to open File Explorer - Ok(Event::Key(KeyEvent::new( - KeyCode::Enter, - KeyModifiers::empty(), - ))) - } - 3 => { - // WE ARE NOW IN FILE EXPLORER - // Assertion: Check internal state (safer than snapshotting dynamic file lists) - assert_eq!( - app.current_screen, - CurrentScreen::FileExplorer, - "Should have switched to File Explorer" - ); - - // Action: Move DOWN (Navigate file list) - Ok(Event::Key(KeyEvent::new( - KeyCode::Down, - KeyModifiers::empty(), - ))) - } - 4 => { - // Assertion: Check that selection moved - assert_eq!( - app.explorer.selected_index, 1, - "Should have selected the second file" - ); - - // Action: Press ESC to Cancel/Close - Ok(Event::Key(KeyEvent::new( - KeyCode::Esc, - KeyModifiers::empty(), - ))) - } - 5 => { - // BACK TO SIDEBAR - assert_eq!( - app.current_screen, - CurrentScreen::BitcoinConfig, - "Should have returned to Sidebar" - ); + app.explorer_trigger = None; + } - // Action: Quit - Ok(Event::Key(KeyEvent::new( - KeyCode::Char('q'), - KeyModifiers::empty(), - ))) + AppAction::FileSelected(path) => { + if let Some(trigger) = &app.explorer_trigger { + match trigger { + CurrentScreen::P2PoolConfig => { + app.p2pool_conf_path = Some(path.clone()); + // DIRECT LOAD: Save to vector + if let Ok(entries) = parse_p2pool_config(&path) { + app.p2pool_data = entries; + } + app.current_screen = CurrentScreen::P2PoolConfig; + } + CurrentScreen::BitcoinConfig => { + app.bitcoin_conf_path = Some(path.clone()); + // DIRECT LOAD: Save to vector + if let Ok(entries) = parse_bitcoin_config(&path) { + app.bitcoin_data = entries; + } + app.current_screen = CurrentScreen::BitcoinConfig; + } + _ => {} } - _ => panic!("Step {} not handled", step), } - }; - - // Run - let res = run_app(&mut terminal, &mut app, event_provider); - assert!(res.is_ok()); - } - - #[test] - fn test_file_explorer_wrap_and_select_sets_config() { - use std::env::temp_dir; - use std::fs::{File, create_dir_all}; - - // Setup a temporary filesystem sandbox - let base = temp_dir().join("pdm_select_test"); - let _ = std::fs::remove_dir_all(&base); - create_dir_all(&base).unwrap(); - let file_path = base.join("bitcoin.conf"); - File::create(&file_path).unwrap(); - - let backend = TestBackend::new(80, 25); - let mut terminal = Terminal::new(backend).unwrap(); - let mut app = App::new(); - app.explorer.current_dir = base.clone(); - app.explorer.load_directory(); - - let mut step = 0; - - let event_provider = |app: &mut App| { - step += 1; - match step { - 1 => Ok(Event::Key(KeyEvent::new( - KeyCode::Down, - KeyModifiers::empty(), - ))), // move to bitcoin config - 2 => Ok(Event::Key(KeyEvent::new( - KeyCode::Enter, - KeyModifiers::empty(), - ))), // open explorer - 3 => Ok(Event::Key(KeyEvent::new( - KeyCode::Up, - KeyModifiers::empty(), - ))), // force wrap-around - 4 => Ok(Event::Key(KeyEvent::new( - KeyCode::Enter, - KeyModifiers::empty(), - ))), // select file - 5 => Ok(Event::Key(KeyEvent::new( - KeyCode::Char('q'), - KeyModifiers::empty(), - ))), - _ => panic!("unexpected"), - } - }; + app.explorer_trigger = None; + } - let res = run_app(&mut terminal, &mut app, event_provider); - assert!(res.is_ok()); + AppAction::Navigate(screen) => { + app.current_screen = screen; + } - assert_eq!(app.bitcoin_conf_path, Some(file_path)); + AppAction::None => {} } + Ok(false) } diff --git a/src/ui.rs b/src/ui.rs index 0ea370d..cf29be1 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -18,7 +18,11 @@ pub fn ui(f: &mut Frame, app: &mut App) { .split(f.area()); // Sidebar - let items = vec![ListItem::new("Home"), ListItem::new("Bitcoin Config")]; + let items = vec![ + ListItem::new("Home"), + ListItem::new("Bitcoin Config"), + ListItem::new("P2Pool Config"), + ]; // Highlight the active one let mut state = ListState::default(); @@ -35,27 +39,35 @@ pub fn ui(f: &mut Frame, app: &mut App) { match app.current_screen { CurrentScreen::Home => { - let config_status = match &app.bitcoin_conf_path { - Some(p) => format!("Loaded: {:?}", p), - None => "No config loaded".to_string(), - }; - - let text = format!( - "Welcome to PDM.\n\n{}\n\n(Navigate to 'Bitcoin Config' to load)", - config_status - ); - let p = Paragraph::new(text) + let p = Paragraph::new("Welcome to PDM.\n\nSelect a config from the sidebar to edit.") .block(Block::default().borders(Borders::ALL).title(" Home ")) .wrap(Wrap { trim: true }); f.render_widget(p, main_area); } CurrentScreen::BitcoinConfig => { - let p = Paragraph::new("Press [Enter] to select a bitcoin.conf file").block( - Block::default() - .borders(Borders::ALL) - .title(" Bitcoin Config "), - ); - f.render_widget(p, main_area); + if app.bitcoin_conf_path.is_some() { + render_bitcoin_view(f, app, main_area); + } else { + let p = Paragraph::new("Press [Enter] to select a bitcoin.conf file").block( + Block::default() + .borders(Borders::ALL) + .title(" Bitcoin Config "), + ); + f.render_widget(p, main_area); + } + } + + CurrentScreen::P2PoolConfig => { + if app.p2pool_conf_path.is_some() { + render_p2pool_view(f, app, main_area); + } else { + let p = Paragraph::new("Press [Enter] to select a p2poolv2 config file").block( + Block::default() + .borders(Borders::ALL) + .title(" P2Pool Config "), + ); + f.render_widget(p, main_area); + } } CurrentScreen::FileExplorer => { render_file_explorer(f, app, main_area); @@ -92,3 +104,78 @@ fn render_file_explorer(f: &mut Frame, app: &mut App, area: Rect) { f.render_stateful_widget(list, area, &mut state); } + +fn render_p2pool_view(f: &mut Frame, app: &mut App, area: Rect) { + let items: Vec = app + .p2pool_data + .iter() + .map(|entry| { + let style = if !entry.is_default { + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::DarkGray) + }; + + let content = Line::from(vec![ + Span::styled( + format!("[{}] ", entry.section), + Style::default().fg(Color::Blue), + ), + Span::styled(format!("{} = ", entry.key), style), + Span::styled(&entry.value, style), + ]); + + ListItem::new(content) + }) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .title(" P2Pool Configuration "), + ) + .highlight_style(Style::default().bg(Color::Blue)); + + f.render_widget(list, area); +} + +fn render_bitcoin_view(f: &mut Frame, app: &mut App, area: Rect) { + let items: Vec = app + .bitcoin_data + .iter() + .map(|entry| { + let style = if entry.enabled { + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::DarkGray) + }; + + let content = Line::from(vec![ + Span::styled(format!("{} = ", entry.key), style), + Span::styled(&entry.value, style), + if !entry.enabled { + Span::styled(" (disabled)", style) + } else { + Span::raw("") + }, + ]); + + ListItem::new(content) + }) + .collect(); + + let list = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Bitcoin Configuration "), + ) + .highlight_style(Style::default().bg(Color::Yellow)); + + f.render_widget(list, area); +} From 4da5429d036ca6fa1fa316075b4bc2ac2220ee13 Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Sat, 24 Jan 2026 03:58:45 +0000 Subject: [PATCH 06/10] added test cases --- src/main.rs | 226 ++++++++++++++---- src/snapshots/pdm__tests__home_screen.snap | 4 +- src/snapshots/pdm__tests__menu_toggled.snap | 2 +- .../ui_snapshots__config_screen_render.snap | 2 +- .../ui_snapshots__home_screen_render.snap | 4 +- 5 files changed, 184 insertions(+), 54 deletions(-) diff --git a/src/main.rs b/src/main.rs index bf27efb..d8dfa64 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,67 +37,67 @@ fn main() -> Result<()> { if let Err(err) = res { println!("Error: {:?}", err); } + Ok(()) } -fn run_app(terminal: &mut Terminal>, app: &mut App) -> Result<()> { +fn run_app(terminal: &mut Terminal, app: &mut App) -> Result<()> { loop { terminal.draw(|f| ui::ui(f, app))?; if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press { - if key.code == KeyCode::Char('q') - || (key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('c')) - { - return Ok(()); - } + if key.kind != KeyEventKind::Press { + continue; + } - if app.current_screen == CurrentScreen::FileExplorer { - let action = app.explorer.handle_input(key); - handle_action(action, app)?; - continue; - } + // Hard exit (always allowed) + if key.code == KeyCode::Char('q') + || (key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('c')) + { + return Ok(()); + } + + let action = match app.current_screen { + CurrentScreen::FileExplorer => app.explorer.handle_input(key), + + _ => match key.code { + KeyCode::Char('q') => AppAction::Quit, - let action = if app.current_screen == CurrentScreen::FileExplorer { - app.explorer.handle_input(key) - } else { - // Global Navigation (Sidebar & Opens Explorer) - match key.code { - // Enter: Opens Explorer if we are on a config screen - KeyCode::Enter => { - if app.current_screen == CurrentScreen::BitcoinConfig - || app.current_screen == CurrentScreen::P2PoolConfig - { - AppAction::OpenExplorer(app.current_screen.clone()) - } else { - AppAction::None - } + KeyCode::Enter => { + if matches!( + app.current_screen, + CurrentScreen::BitcoinConfig | CurrentScreen::P2PoolConfig + ) { + AppAction::OpenExplorer(app.current_screen.clone()) + } else { + AppAction::None } + } - // Sidebar Navigation - KeyCode::Down => { - if app.sidebar_index < 2 { - app.sidebar_index += 1; - AppAction::ToggleMenu - } else { - AppAction::None - } + KeyCode::Down => { + if app.sidebar_index < 2 { + app.sidebar_index += 1; + AppAction::ToggleMenu + } else { + AppAction::None } - KeyCode::Up => { - if app.sidebar_index > 0 { - app.sidebar_index -= 1; - AppAction::ToggleMenu - } else { - AppAction::None - } + } + + KeyCode::Up => { + if app.sidebar_index > 0 { + app.sidebar_index -= 1; + AppAction::ToggleMenu + } else { + AppAction::None } - _ => AppAction::None, } - }; - if handle_action(action, app)? { - return Ok(()); - } + _ => AppAction::None, + }, + }; + + if handle_action(action, app)? { + return Ok(()); } } } @@ -129,7 +129,6 @@ fn handle_action(action: AppAction, app: &mut App) -> Result { match trigger { CurrentScreen::P2PoolConfig => { app.p2pool_conf_path = Some(path.clone()); - // DIRECT LOAD: Save to vector if let Ok(entries) = parse_p2pool_config(&path) { app.p2pool_data = entries; } @@ -137,7 +136,6 @@ fn handle_action(action: AppAction, app: &mut App) -> Result { } CurrentScreen::BitcoinConfig => { app.bitcoin_conf_path = Some(path.clone()); - // DIRECT LOAD: Save to vector if let Ok(entries) = parse_bitcoin_config(&path) { app.bitcoin_data = entries; } @@ -155,5 +153,137 @@ fn handle_action(action: AppAction, app: &mut App) -> Result { AppAction::None => {} } + Ok(false) } + +#[cfg(test)] +mod tests { + use super::*; + use ratatui::backend::TestBackend; + + #[test] + fn test_app_integration_smoke_test() { + let backend = TestBackend::new(80, 25); + let mut terminal = Terminal::new(backend).unwrap(); + let mut app = App::new(); + + // Initial render + terminal.draw(|f| ui::ui(f, &mut app)).unwrap(); + insta::assert_debug_snapshot!("home_screen", terminal.backend()); + + // Simulate sidebar move + app.sidebar_index = 1; + app.toggle_menu(); + + terminal.draw(|f| ui::ui(f, &mut app)).unwrap(); + insta::assert_debug_snapshot!("menu_toggled", terminal.backend()); + + assert_eq!(app.current_screen, CurrentScreen::BitcoinConfig); + } + + #[test] + fn test_file_explorer_flow_state_only() { + let backend = TestBackend::new(80, 25); + let mut terminal = Terminal::new(backend).unwrap(); + let mut app = App::new(); + + // Navigate to Bitcoin config + app.sidebar_index = 1; + app.toggle_menu(); + assert_eq!(app.current_screen, CurrentScreen::BitcoinConfig); + + // Open explorer + handle_action( + AppAction::OpenExplorer(CurrentScreen::BitcoinConfig), + &mut app, + ) + .unwrap(); + + assert_eq!(app.current_screen, CurrentScreen::FileExplorer); + + // Close explorer + handle_action(AppAction::CloseModal, &mut app).unwrap(); + assert_eq!(app.current_screen, CurrentScreen::BitcoinConfig); + + terminal.draw(|f| ui::ui(f, &mut app)).unwrap(); + } + + #[test] + fn test_file_explorer_wrap_and_select_sets_config() { + use crossterm::event::KeyEvent; + use std::env::temp_dir; + use std::fs::{File, create_dir_all}; + let base = temp_dir().join("pdm_select_test"); + let _ = std::fs::remove_dir_all(&base); + create_dir_all(&base).unwrap(); + + let file_path = base.join("bitcoin.conf"); + File::create(&file_path).unwrap(); + + let backend = TestBackend::new(80, 25); + let mut terminal = Terminal::new(backend).unwrap(); + let mut app = App::new(); + + app.explorer.current_dir = base.clone(); + app.explorer.load_directory(); + + handle_action( + AppAction::OpenExplorer(CurrentScreen::BitcoinConfig), + &mut app, + ) + .unwrap(); + + // Move selection DOWN to the actual file (skip "..") + app.explorer + .handle_input(KeyEvent::new(KeyCode::Down, KeyModifiers::empty())); + let action = app.explorer.handle_input(crossterm::event::KeyEvent::new( + KeyCode::Enter, + KeyModifiers::empty(), + )); + + handle_action(action, &mut app).unwrap(); + + assert_eq!(app.bitcoin_conf_path, Some(file_path)); + + terminal.draw(|f| ui::ui(f, &mut app)).unwrap(); + } + + #[test] + fn app_action_open_explorer_sets_state() { + let mut app = App::new(); + + let exited = handle_action( + AppAction::OpenExplorer(CurrentScreen::BitcoinConfig), + &mut app, + ) + .unwrap(); + + assert!(!exited); + assert_eq!(app.current_screen, CurrentScreen::FileExplorer); + assert_eq!(app.explorer_trigger, Some(CurrentScreen::BitcoinConfig)); + } + + #[test] + fn app_action_close_modal_returns_to_trigger_screen() { + let mut app = App::new(); + + app.explorer_trigger = Some(CurrentScreen::BitcoinConfig); + app.current_screen = CurrentScreen::FileExplorer; + + let exited = handle_action(AppAction::CloseModal, &mut app).unwrap(); + + assert!(!exited); + assert_eq!(app.current_screen, CurrentScreen::BitcoinConfig); + assert!(app.explorer_trigger.is_none()); + } + + #[test] + fn app_action_quit_requests_exit() { + let mut app = App::new(); + + let exited = handle_action(AppAction::Quit, &mut app).unwrap(); + + assert!(exited); + } +} diff --git a/src/snapshots/pdm__tests__home_screen.snap b/src/snapshots/pdm__tests__home_screen.snap index ba3a6fa..bf9b5ea 100644 --- a/src/snapshots/pdm__tests__home_screen.snap +++ b/src/snapshots/pdm__tests__home_screen.snap @@ -9,9 +9,9 @@ TestBackend { "┌ PDM ──────────────────┐┌ Home ───────────────────────────────────────────────┐", "│Home ││Welcome to PDM. │", "│Bitcoin Config ││ │", - "│ ││No config loaded │", + "│P2Pool Config ││Select a config from the sidebar to edit. │", + "│ ││ │", "│ ││ │", - "│ ││(Navigate to 'Bitcoin Config' to load) │", "│ ││ │", "│ ││ │", "│ ││ │", diff --git a/src/snapshots/pdm__tests__menu_toggled.snap b/src/snapshots/pdm__tests__menu_toggled.snap index 36aabde..bc9e283 100644 --- a/src/snapshots/pdm__tests__menu_toggled.snap +++ b/src/snapshots/pdm__tests__menu_toggled.snap @@ -9,7 +9,7 @@ TestBackend { "┌ PDM ──────────────────┐┌ Bitcoin Config ─────────────────────────────────────┐", "│Home ││Press [Enter] to select a bitcoin.conf file │", "│Bitcoin Config ││ │", - "│ ││ │", + "│P2Pool Config ││ │", "│ ││ │", "│ ││ │", "│ ││ │", diff --git a/tests/snapshots/ui_snapshots__config_screen_render.snap b/tests/snapshots/ui_snapshots__config_screen_render.snap index 177ff52..32f7690 100644 --- a/tests/snapshots/ui_snapshots__config_screen_render.snap +++ b/tests/snapshots/ui_snapshots__config_screen_render.snap @@ -9,7 +9,7 @@ TestBackend { "┌ PDM ──────────────────┐┌ Bitcoin Config ─────────────────────────────────────┐", "│Home ││Press [Enter] to select a bitcoin.conf file │", "│Bitcoin Config ││ │", - "│ ││ │", + "│P2Pool Config ││ │", "│ ││ │", "│ ││ │", "│ ││ │", diff --git a/tests/snapshots/ui_snapshots__home_screen_render.snap b/tests/snapshots/ui_snapshots__home_screen_render.snap index d16626d..df0f123 100644 --- a/tests/snapshots/ui_snapshots__home_screen_render.snap +++ b/tests/snapshots/ui_snapshots__home_screen_render.snap @@ -9,9 +9,9 @@ TestBackend { "┌ PDM ──────────────────┐┌ Home ───────────────────────────────────────────────┐", "│Home ││Welcome to PDM. │", "│Bitcoin Config ││ │", - "│ ││No config loaded │", + "│P2Pool Config ││Select a config from the sidebar to edit. │", + "│ ││ │", "│ ││ │", - "│ ││(Navigate to 'Bitcoin Config' to load) │", "│ ││ │", "│ ││ │", "│ ││ │", From b7210acc0be1ade61c23e2ac0f4f97f10af46b37 Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Mon, 16 Feb 2026 06:23:56 +0000 Subject: [PATCH 07/10] initial commit of status view --- Cargo.lock | 1536 ++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + src/app.rs | 9 +- src/components/metrics.rs | 73 ++ src/components/mod.rs | 1 + src/main.rs | 96 ++- src/ui.rs | 34 + 7 files changed, 1722 insertions(+), 29 deletions(-) create mode 100644 src/components/metrics.rs diff --git a/Cargo.lock b/Cargo.lock index 07b8c07..e4ca070 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,40 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bech32" version = "0.10.0-beta" @@ -85,6 +119,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cassowary" version = "0.3.0" @@ -107,15 +153,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compact_str" version = "0.8.1" @@ -200,6 +279,32 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -335,6 +440,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlv-list" version = "0.5.2" @@ -353,6 +469,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -425,6 +547,70 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -442,8 +628,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -453,9 +641,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] @@ -475,6 +684,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" @@ -502,12 +717,226 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + [[package]] name = "indoc" version = "2.0.7" @@ -541,6 +970,22 @@ dependencies = [ "syn", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.13.0" @@ -556,6 +1001,48 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "json5" version = "0.4.1" @@ -585,6 +1072,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + [[package]] name = "litrs" version = "1.0.0" @@ -615,12 +1108,24 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.1.1" @@ -639,6 +1144,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -694,10 +1205,18 @@ dependencies = [ "crossterm 0.29.0", "insta", "ratatui", + "reqwest", "serde", "tempfile", + "tokio", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pest" version = "2.8.5" @@ -742,14 +1261,100 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.103" +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.42" @@ -765,6 +1370,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "ratatui" version = "0.29.0" @@ -795,6 +1429,60 @@ dependencies = [ "bitflags", ] +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "ron" version = "0.12.0" @@ -819,6 +1507,12 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -854,6 +1548,81 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -866,6 +1635,24 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -891,6 +1678,29 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -1014,12 +1824,34 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -1054,6 +1886,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.111" @@ -1065,6 +1903,47 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.23.0" @@ -1078,6 +1957,46 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -1087,6 +2006,82 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.9.10+spec-1.1.0" @@ -1119,26 +2114,96 @@ dependencies = [ ] [[package]] -name = "typeid" -version = "1.0.3" +name = "tower" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] [[package]] -name = "typenum" -version = "1.19.0" +name = "tower-http" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] [[package]] -name = "ucd-trie" -version = "0.1.7" +name = "tower-layer" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] -name = "unicode-ident" -version = "1.0.22" +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" @@ -1171,12 +2236,55 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1192,6 +2300,94 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1208,6 +2404,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1220,13 +2425,69 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -1238,70 +2499,192 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.14" @@ -1317,6 +2700,12 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "yaml-rust2" version = "0.10.4" @@ -1328,6 +2717,109 @@ dependencies = [ "hashlink", ] +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index 3fd46f0..15e5dd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,8 @@ crossterm = "0.29.0" ratatui = "0.29.0" serde = { version = "1.0", features = ["derive"] } bitcoin = "0.31" +tokio = { version = "1.49.0", features = ["full"] } +reqwest = { version = "0.13.2", features = ["blocking"] } [dev-dependencies] insta = "1.44.3" diff --git a/src/app.rs b/src/app.rs index 1ba4e35..2cd27c4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use crate::components::file_explorer::FileExplorer; +use crate::components::metrics::P2PoolMetrics; use crate::config::ConfigEntry as BitcoinEntry; use crate::p2poolv2_config_parser::ConfigEntry as P2PoolEntry; use std::path::PathBuf; @@ -10,6 +11,7 @@ use std::path::PathBuf; #[derive(Debug, PartialEq, Eq, Clone)] pub enum CurrentScreen { Home, + P2PoolStatus, BitcoinConfig, P2PoolConfig, FileExplorer, @@ -38,6 +40,7 @@ pub struct App { pub explorer_trigger: Option, pub bitcoin_conf_path: Option, pub p2pool_conf_path: Option, + pub node_metrics: Option, pub explorer: FileExplorer, pub p2pool_data: Vec, pub bitcoin_data: Vec, @@ -51,6 +54,7 @@ impl App { explorer_trigger: None, bitcoin_conf_path: None, p2pool_conf_path: None, + node_metrics: None, explorer: FileExplorer::new(), p2pool_data: Vec::new(), bitcoin_data: Vec::new(), @@ -61,8 +65,9 @@ impl App { // Logic to switch between sidebar items match self.sidebar_index { 0 => self.current_screen = CurrentScreen::Home, - 1 => self.current_screen = CurrentScreen::BitcoinConfig, - 2 => self.current_screen = CurrentScreen::P2PoolConfig, + 1 => self.current_screen = CurrentScreen::P2PoolStatus, + 2 => self.current_screen = CurrentScreen::BitcoinConfig, + 3 => self.current_screen = CurrentScreen::P2PoolConfig, _ => {} } } diff --git a/src/components/metrics.rs b/src/components/metrics.rs new file mode 100644 index 0000000..fe7e4b6 --- /dev/null +++ b/src/components/metrics.rs @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2024 PDM Authors +// SPDX-License-Identifier: AGPL-3.0-or-later + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct P2PoolMetrics { + pub shares_accepted: u64, + pub shares_rejected: u64, + pub pool_difficulty: u64, + pub start_time: u64, + pub coinbase_total: u64, +} + +impl P2PoolMetrics { + /// Parses a raw Prometheus text response into our P2PoolMetrics struct + pub fn parse_prometheus(raw_text: &str) -> Self { + let mut metrics = P2PoolMetrics::default(); + + for line in raw_text.lines() { + let trimmed = line.trim(); + + // Skip comments and empty lines + if trimmed.starts_with('#') || trimmed.is_empty() { + continue; + } + + // Split by whitespace + let mut parts = trimmed.split_whitespace(); + if let (Some(key), Some(value_str)) = (parts.next(), parts.next()) { + // Try to parse the value as a u64 + let value = value_str.parse::().unwrap_or(0); + + // Map the Prometheus keys to our struct fields + match key { + "shares_accepted_total" => metrics.shares_accepted = value, + "shares_rejected_total" => metrics.shares_rejected = value, + "pool_difficulty" => metrics.pool_difficulty = value, + "start_time_seconds" => metrics.start_time = value, + "coinbase_total" => metrics.coinbase_total = value, + _ => {} // Ignore metrics we don't care about + } + } + } + + metrics + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_prometheus_output() { + let raw_data = "\ +# HELP shares_accepted_total Total number of accepted shares +# TYPE shares_accepted_total counter +shares_accepted_total 0 + +# HELP start_time_seconds Pool start time in Unix timestamp +# TYPE start_time_seconds gauge +start_time_seconds 1771149691 + +coinbase_output{index=\"0\",address=\"tb1qyazxde6558qj6z3d9np5e6msmrspwpf6k0qggk\"} 2500000000 +coinbase_total 2500000000 +"; + + let metrics = P2PoolMetrics::parse_prometheus(raw_data); + + assert_eq!(metrics.shares_accepted, 0); + assert_eq!(metrics.start_time, 1771149691); + assert_eq!(metrics.coinbase_total, 2500000000); + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 6d3b562..c6266a8 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -3,3 +3,4 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub mod file_explorer; +pub mod metrics; diff --git a/src/main.rs b/src/main.rs index d8dfa64..8fc993d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use pdm::app::AppAction; use pdm::app::{App, CurrentScreen}; +use pdm::components::metrics::P2PoolMetrics; use pdm::config::parse_config as parse_bitcoin_config; use pdm::p2poolv2_config_parser::parse_config as parse_p2pool_config; use pdm::ui; @@ -16,8 +17,13 @@ use crossterm::{ }; use ratatui::{Terminal, backend::Backend, backend::CrosstermBackend}; use std::io; +use std::sync::Arc; +use tokio::sync::Mutex; +use tokio::sync::mpsc; +use tokio::time::Duration; -fn main() -> Result<()> { +#[tokio::main] +async fn main() -> Result<()> { // Setup Terminal enable_raw_mode()?; let mut stdout = io::stdout(); @@ -27,7 +33,47 @@ fn main() -> Result<()> { // Run App let mut app = App::new(); - let res = run_app(&mut terminal, &mut app); + // A shared thread-safe String for the API URL + let api_url = Arc::new(Mutex::new(Some( + "http://127.0.0.1:46884/metrics".to_string(), + ))); + let api_url_clone = Arc::clone(&api_url); + + // A channel to send metrics from the background task to the UI + let (tx, mut rx) = mpsc::unbounded_channel::(); + + // Spawn the background fetcher task + tokio::spawn(async move { + let client = reqwest::Client::new(); + loop { + // Check if we have a URL yet + let url = { api_url_clone.lock().await.clone() }; + + if let Some(u) = url { + // Fetch the metrics! + if let Ok(resp) = client.get(&u).send().await { + if let Ok(text) = resp.text().await { + let metrics = P2PoolMetrics::parse_prometheus(&text); + // Send them to the UI thread + let _ = tx.send(metrics); + } + } + } + // Wait 2 seconds before polling again + tokio::time::sleep(Duration::from_secs(2)).await; + } + }); + + // We use poll() with a 250ms timeout. If no key is pressed, we return None + // so the loop can continue and check for new metrics. + let res = run_app(&mut terminal, &mut app, api_url, &mut rx, |_| { + if event::poll(Duration::from_millis(250))? { + Ok(Some(event::read()?)) + } else { + Ok(None) + } + }) + .await; // Restore Terminal disable_raw_mode()?; @@ -41,8 +87,21 @@ fn main() -> Result<()> { Ok(()) } -fn run_app(terminal: &mut Terminal, app: &mut App) -> Result<()> { +async fn run_app( + terminal: &mut Terminal, + app: &mut App, + api_url: Arc>>, + rx: &mut mpsc::UnboundedReceiver, + mut event_handler: F, +) -> Result<()> +where + F: FnMut(&mut App) -> Result>, +{ loop { + // try_recv() reads from the channel instantly without blocking + while let Ok(metrics) = rx.try_recv() { + app.node_metrics = Some(metrics); + } terminal.draw(|f| ui::ui(f, app))?; if let Event::Key(key) = event::read()? { @@ -75,7 +134,7 @@ fn run_app(terminal: &mut Terminal, app: &mut App) -> Result<()> } KeyCode::Down => { - if app.sidebar_index < 2 { + if app.sidebar_index < 3 { app.sidebar_index += 1; AppAction::ToggleMenu } else { @@ -96,7 +155,7 @@ fn run_app(terminal: &mut Terminal, app: &mut App) -> Result<()> }, }; - if handle_action(action, app)? { + if handle_action_with_url(action, app, &api_url).await? { return Ok(()); } } @@ -157,6 +216,33 @@ fn handle_action(action: AppAction, app: &mut App) -> Result { Ok(false) } +async fn handle_action_with_url( + action: AppAction, + app: &mut App, + api_url: &Arc>>, +) -> anyhow::Result { + // Run the normal action FIRST so the file is parsed + let should_quit = handle_action(action.clone(), app)?; + + // If the user manually loaded a config file, override the sensible default + if let AppAction::FileSelected(_) = action { + if app.current_screen == CurrentScreen::P2PoolConfig { + if let Some(config) = &app.p2pool_conf_path { + // dynamic override: + // let host = &config.api_host; + // let port = config.api_port; + + let host = "127.0.0.1"; + let port = 46884; + let dynamic_url = format!("http://{}:{}/metrics", host, port); + *api_url.lock().await = Some(dynamic_url); + } + } + } + + Ok(should_quit) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ui.rs b/src/ui.rs index cf29be1..682df78 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -20,6 +20,7 @@ pub fn ui(f: &mut Frame, app: &mut App) { // Sidebar let items = vec![ ListItem::new("Home"), + ListItem::new("P2Pool Status"), ListItem::new("Bitcoin Config"), ListItem::new("P2Pool Config"), ]; @@ -44,6 +45,39 @@ pub fn ui(f: &mut Frame, app: &mut App) { .wrap(Wrap { trim: true }); f.render_widget(p, main_area); } + CurrentScreen::P2PoolStatus => { + if let Some(metrics) = &app.node_metrics { + // We have live data! + let text = format!( + "Node is ONLINE \n\n\ + Accepted Shares: {}\n\ + Rejected Shares: {}\n\ + Pool Difficulty: {}\n\ + Coinbase Total: {}", + metrics.shares_accepted, + metrics.shares_rejected, + metrics.pool_difficulty, + metrics.coinbase_total + ); + + let p = Paragraph::new(text) + .block( + Block::default() + .borders(Borders::ALL) + .title(" P2Pool Live Status "), + ) + .style(Style::default().fg(Color::Green)) + .wrap(Wrap { trim: true }); + f.render_widget(p, main_area); + } else { + // Waiting for data / config + let p = Paragraph::new("Waiting for node data...\n\nEnsure your p2pool-v2 node is running and the config is loaded.") + .block(Block::default().borders(Borders::ALL).title(" P2Pool Live Status ")) + .style(Style::default().fg(Color::DarkGray)) + .wrap(Wrap { trim: true }); + f.render_widget(p, main_area); + } + } CurrentScreen::BitcoinConfig => { if app.bitcoin_conf_path.is_some() { render_bitcoin_view(f, app, main_area); From 194765f6c87b3aea37ed5a41adbcb825186843a2 Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Tue, 17 Feb 2026 11:30:31 +0000 Subject: [PATCH 08/10] changed order of list item --- src/app.rs | 8 +-- src/snapshots/pdm__tests__home_screen.snap | 2 +- src/snapshots/pdm__tests__menu_toggled.snap | 2 +- src/ui.rs | 52 +++++++++---------- .../ui_snapshots__config_screen_render.snap | 2 +- .../ui_snapshots__home_screen_render.snap | 2 +- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2cd27c4..e76307c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,9 +11,9 @@ use std::path::PathBuf; #[derive(Debug, PartialEq, Eq, Clone)] pub enum CurrentScreen { Home, - P2PoolStatus, BitcoinConfig, P2PoolConfig, + P2PoolStatus, FileExplorer, Exiting, } @@ -65,9 +65,9 @@ impl App { // Logic to switch between sidebar items match self.sidebar_index { 0 => self.current_screen = CurrentScreen::Home, - 1 => self.current_screen = CurrentScreen::P2PoolStatus, - 2 => self.current_screen = CurrentScreen::BitcoinConfig, - 3 => self.current_screen = CurrentScreen::P2PoolConfig, + 1 => self.current_screen = CurrentScreen::BitcoinConfig, + 2 => self.current_screen = CurrentScreen::P2PoolConfig, + 3 => self.current_screen = CurrentScreen::P2PoolStatus, _ => {} } } diff --git a/src/snapshots/pdm__tests__home_screen.snap b/src/snapshots/pdm__tests__home_screen.snap index bf9b5ea..c42bd9b 100644 --- a/src/snapshots/pdm__tests__home_screen.snap +++ b/src/snapshots/pdm__tests__home_screen.snap @@ -10,7 +10,7 @@ TestBackend { "│Home ││Welcome to PDM. │", "│Bitcoin Config ││ │", "│P2Pool Config ││Select a config from the sidebar to edit. │", - "│ ││ │", + "│P2Pool Status ││ │", "│ ││ │", "│ ││ │", "│ ││ │", diff --git a/src/snapshots/pdm__tests__menu_toggled.snap b/src/snapshots/pdm__tests__menu_toggled.snap index bc9e283..9742c8b 100644 --- a/src/snapshots/pdm__tests__menu_toggled.snap +++ b/src/snapshots/pdm__tests__menu_toggled.snap @@ -10,7 +10,7 @@ TestBackend { "│Home ││Press [Enter] to select a bitcoin.conf file │", "│Bitcoin Config ││ │", "│P2Pool Config ││ │", - "│ ││ │", + "│P2Pool Status ││ │", "│ ││ │", "│ ││ │", "│ ││ │", diff --git a/src/ui.rs b/src/ui.rs index 682df78..a394246 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -20,9 +20,9 @@ pub fn ui(f: &mut Frame, app: &mut App) { // Sidebar let items = vec![ ListItem::new("Home"), - ListItem::new("P2Pool Status"), ListItem::new("Bitcoin Config"), ListItem::new("P2Pool Config"), + ListItem::new("P2Pool Status"), ]; // Highlight the active one @@ -45,6 +45,31 @@ pub fn ui(f: &mut Frame, app: &mut App) { .wrap(Wrap { trim: true }); f.render_widget(p, main_area); } + CurrentScreen::BitcoinConfig => { + if app.bitcoin_conf_path.is_some() { + render_bitcoin_view(f, app, main_area); + } else { + let p = Paragraph::new("Press [Enter] to select a bitcoin.conf file").block( + Block::default() + .borders(Borders::ALL) + .title(" Bitcoin Config "), + ); + f.render_widget(p, main_area); + } + } + + CurrentScreen::P2PoolConfig => { + if app.p2pool_conf_path.is_some() { + render_p2pool_view(f, app, main_area); + } else { + let p = Paragraph::new("Press [Enter] to select a p2poolv2 config file").block( + Block::default() + .borders(Borders::ALL) + .title(" P2Pool Config "), + ); + f.render_widget(p, main_area); + } + } CurrentScreen::P2PoolStatus => { if let Some(metrics) = &app.node_metrics { // We have live data! @@ -78,31 +103,6 @@ pub fn ui(f: &mut Frame, app: &mut App) { f.render_widget(p, main_area); } } - CurrentScreen::BitcoinConfig => { - if app.bitcoin_conf_path.is_some() { - render_bitcoin_view(f, app, main_area); - } else { - let p = Paragraph::new("Press [Enter] to select a bitcoin.conf file").block( - Block::default() - .borders(Borders::ALL) - .title(" Bitcoin Config "), - ); - f.render_widget(p, main_area); - } - } - - CurrentScreen::P2PoolConfig => { - if app.p2pool_conf_path.is_some() { - render_p2pool_view(f, app, main_area); - } else { - let p = Paragraph::new("Press [Enter] to select a p2poolv2 config file").block( - Block::default() - .borders(Borders::ALL) - .title(" P2Pool Config "), - ); - f.render_widget(p, main_area); - } - } CurrentScreen::FileExplorer => { render_file_explorer(f, app, main_area); } diff --git a/tests/snapshots/ui_snapshots__config_screen_render.snap b/tests/snapshots/ui_snapshots__config_screen_render.snap index 32f7690..98edfe5 100644 --- a/tests/snapshots/ui_snapshots__config_screen_render.snap +++ b/tests/snapshots/ui_snapshots__config_screen_render.snap @@ -10,7 +10,7 @@ TestBackend { "│Home ││Press [Enter] to select a bitcoin.conf file │", "│Bitcoin Config ││ │", "│P2Pool Config ││ │", - "│ ││ │", + "│P2Pool Status ││ │", "│ ││ │", "│ ││ │", "│ ││ │", diff --git a/tests/snapshots/ui_snapshots__home_screen_render.snap b/tests/snapshots/ui_snapshots__home_screen_render.snap index df0f123..accc4e5 100644 --- a/tests/snapshots/ui_snapshots__home_screen_render.snap +++ b/tests/snapshots/ui_snapshots__home_screen_render.snap @@ -10,7 +10,7 @@ TestBackend { "│Home ││Welcome to PDM. │", "│Bitcoin Config ││ │", "│P2Pool Config ││Select a config from the sidebar to edit. │", - "│ ││ │", + "│P2Pool Status ││ │", "│ ││ │", "│ ││ │", "│ ││ │", From a10628e39e199681658e2cfd39c24996616659fc Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Tue, 17 Feb 2026 11:58:12 +0000 Subject: [PATCH 09/10] add test cases --- src/main.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/main.rs b/src/main.rs index 8fc993d..9a9e34a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -247,6 +247,10 @@ async fn handle_action_with_url( mod tests { use super::*; use ratatui::backend::TestBackend; + use pdm::components::metrics::P2PoolMetrics; + use std::sync::Arc; + use tokio::sync::Mutex; + use tokio::sync::mpsc; #[test] fn test_app_integration_smoke_test() { @@ -372,4 +376,60 @@ mod tests { assert!(exited); } + + #[tokio::test] + async fn test_handle_action_with_url_updates_mutex() { + + let mut app = App::new(); + // Start with a test default URL + let api_url = Arc::new(Mutex::new(Some("http://default-host:9999/metrics".to_string()))); + + // Setup App state: Simulate the user opening the Explorer from P2PoolConfig + app.explorer_trigger = Some(CurrentScreen::P2PoolConfig); + app.current_screen = CurrentScreen::FileExplorer; + + // Simulate the user selecting a file + let fake_path = std::path::PathBuf::from("test_p2pool.toml"); + let action = AppAction::FileSelected(fake_path); + + // Execute the wrapper function + let _ = handle_action_with_url(action, &mut app, &api_url).await.unwrap(); + + // Check that the App navigated back to the config screen + assert_eq!(app.current_screen, CurrentScreen::P2PoolConfig); + + // Check that the Mutex was updated with our new dynamic URL + // (Currently hardcoded to 127.0.0.1:46884 in our logic until you map the struct fields) + let updated_url = api_url.lock().await.clone(); + assert_eq!(updated_url, Some("http://127.0.0.1:46884/metrics".to_string())); + } + + #[test] + fn test_app_receives_metrics_from_channel() { + + let mut app = App::new(); + let (tx, mut rx) = mpsc::unbounded_channel::(); + + // Verify app starts with no metrics + assert!(app.node_metrics.is_none()); + + // Create fake metrics simulating the background task + let mut fake_metrics = P2PoolMetrics::default(); + fake_metrics.shares_accepted = 999; + fake_metrics.pool_difficulty = 5000; + + // Send down the channel + tx.send(fake_metrics).unwrap(); + + // Simulate the top of the run_app() loop + if let Ok(metrics) = rx.try_recv() { + app.node_metrics = Some(metrics); + } + + // Verify the App absorbed the data correctly + assert!(app.node_metrics.is_some()); + let saved_metrics = app.node_metrics.unwrap(); + assert_eq!(saved_metrics.shares_accepted, 999); + assert_eq!(saved_metrics.pool_difficulty, 5000); + } } From e573c6135a84d05e239cc090b0499c9751394610 Mon Sep 17 00:00:00 2001 From: Raunak Kumar Date: Tue, 17 Feb 2026 11:58:52 +0000 Subject: [PATCH 10/10] fix linting --- src/main.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9a9e34a..c2c0a8a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -246,8 +246,8 @@ async fn handle_action_with_url( #[cfg(test)] mod tests { use super::*; - use ratatui::backend::TestBackend; use pdm::components::metrics::P2PoolMetrics; + use ratatui::backend::TestBackend; use std::sync::Arc; use tokio::sync::Mutex; use tokio::sync::mpsc; @@ -379,10 +379,11 @@ mod tests { #[tokio::test] async fn test_handle_action_with_url_updates_mutex() { - let mut app = App::new(); // Start with a test default URL - let api_url = Arc::new(Mutex::new(Some("http://default-host:9999/metrics".to_string()))); + let api_url = Arc::new(Mutex::new(Some( + "http://default-host:9999/metrics".to_string(), + ))); // Setup App state: Simulate the user opening the Explorer from P2PoolConfig app.explorer_trigger = Some(CurrentScreen::P2PoolConfig); @@ -393,7 +394,9 @@ mod tests { let action = AppAction::FileSelected(fake_path); // Execute the wrapper function - let _ = handle_action_with_url(action, &mut app, &api_url).await.unwrap(); + let _ = handle_action_with_url(action, &mut app, &api_url) + .await + .unwrap(); // Check that the App navigated back to the config screen assert_eq!(app.current_screen, CurrentScreen::P2PoolConfig); @@ -401,12 +404,14 @@ mod tests { // Check that the Mutex was updated with our new dynamic URL // (Currently hardcoded to 127.0.0.1:46884 in our logic until you map the struct fields) let updated_url = api_url.lock().await.clone(); - assert_eq!(updated_url, Some("http://127.0.0.1:46884/metrics".to_string())); + assert_eq!( + updated_url, + Some("http://127.0.0.1:46884/metrics".to_string()) + ); } #[test] fn test_app_receives_metrics_from_channel() { - let mut app = App::new(); let (tx, mut rx) = mpsc::unbounded_channel::();