cli.rs (12681B)
1 use std::{ 2 net::{Ipv4Addr, SocketAddr}, 3 path::{Path, PathBuf}, 4 str::FromStr, 5 }; 6 7 use anyhow::{Context, Result, bail}; 8 use iuna::{adapters::config_store, domain::Amount}; 9 10 use super::{GENESIS_INITIAL_BURN_FEE, GENESIS_INITIAL_BURN_PER_BLOCK}; 11 12 pub(crate) fn configured_p2p_announce_addr( 13 opts: &CliOptions, 14 ui_config: &config_store::UiConfig, 15 ) -> Result<Option<SocketAddr>> { 16 if let Some(addr) = opts.p2p_announce_addr { 17 return Ok(Some(addr)); 18 } 19 ui_config 20 .p2p_announce_addr 21 .as_deref() 22 .map(|addr| { 23 addr.parse() 24 .with_context(|| format!("invalid configured P2P announce address {addr}")) 25 }) 26 .transpose() 27 } 28 29 pub(crate) fn configured_p2p_bind_addr( 30 opts: &CliOptions, 31 ui_config: &config_store::UiConfig, 32 ) -> SocketAddr { 33 if opts.p2p_addr_configured || ui_config.p2p_accept_inbound { 34 return SocketAddr::from((Ipv4Addr::UNSPECIFIED, ui_config.p2p_bind_port)); 35 } 36 opts.p2p_addr 37 } 38 39 pub(crate) fn configured_stratum_addr( 40 opts: &CliOptions, 41 ui_config: &config_store::UiConfig, 42 ) -> Option<SocketAddr> { 43 opts.stratum_addr.or_else(|| { 44 ui_config 45 .stratum_enabled 46 .then(|| SocketAddr::from((Ipv4Addr::UNSPECIFIED, ui_config.stratum_bind_port))) 47 }) 48 } 49 50 pub(crate) fn configured_wallet_endpoint_addr( 51 opts: &CliOptions, 52 ui_config: &config_store::UiConfig, 53 ) -> Option<SocketAddr> { 54 opts.wallet_endpoint_addr.or_else(|| { 55 ui_config 56 .wallet_endpoint_enabled 57 .then(|| SocketAddr::from((Ipv4Addr::UNSPECIFIED, ui_config.wallet_endpoint_bind_port))) 58 }) 59 } 60 61 pub(crate) fn apply_cli_p2p_config_overrides( 62 opts: &CliOptions, 63 ui_config: &mut config_store::UiConfig, 64 ) -> bool { 65 let mut dirty = false; 66 if opts.p2p_addr_configured { 67 let bind_port = opts.p2p_addr.port(); 68 if ui_config.p2p_bind_port != bind_port { 69 ui_config.p2p_bind_port = bind_port; 70 dirty = true; 71 } 72 } 73 if let Some(addr) = opts.p2p_announce_addr { 74 let announce_addr = addr.to_string(); 75 if !ui_config.p2p_accept_inbound 76 || ui_config.p2p_announce_addr.as_deref() != Some(&announce_addr) 77 { 78 ui_config.p2p_accept_inbound = true; 79 ui_config.p2p_announce_addr = Some(announce_addr); 80 dirty = true; 81 } 82 } 83 dirty 84 } 85 86 pub(crate) fn apply_cli_stratum_config_overrides( 87 opts: &CliOptions, 88 ui_config: &mut config_store::UiConfig, 89 ) -> bool { 90 let Some(addr) = opts.stratum_addr else { 91 return false; 92 }; 93 let mut dirty = false; 94 if !ui_config.stratum_enabled { 95 ui_config.stratum_enabled = true; 96 dirty = true; 97 } 98 if ui_config.stratum_bind_port != addr.port() { 99 ui_config.stratum_bind_port = addr.port(); 100 dirty = true; 101 } 102 dirty 103 } 104 105 pub(crate) fn apply_cli_wallet_endpoint_config_overrides( 106 opts: &CliOptions, 107 ui_config: &mut config_store::UiConfig, 108 ) -> bool { 109 let Some(addr) = opts.wallet_endpoint_addr else { 110 return false; 111 }; 112 let dirty = 113 !ui_config.wallet_endpoint_enabled || ui_config.wallet_endpoint_bind_port != addr.port(); 114 ui_config.wallet_endpoint_enabled = true; 115 ui_config.wallet_endpoint_bind_port = addr.port(); 116 dirty 117 } 118 119 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 120 pub(crate) enum ChainMode { 121 Setup, 122 Genesis, 123 Join, 124 } 125 126 #[derive(Debug)] 127 pub(crate) struct CliOptions { 128 pub(crate) wallet_path: Option<PathBuf>, 129 pub(crate) chain_db_path: Option<PathBuf>, 130 pub(crate) http_addr: SocketAddr, 131 pub(crate) p2p_addr: SocketAddr, 132 pub(crate) p2p_addr_configured: bool, 133 pub(crate) p2p_announce_addr: Option<SocketAddr>, 134 pub(crate) stratum_addr: Option<SocketAddr>, 135 pub(crate) wallet_endpoint_addr: Option<SocketAddr>, 136 pub(crate) peers: Vec<String>, 137 pub(crate) join_peers: Vec<String>, 138 pub(crate) chain_mode: ChainMode, 139 pub(crate) data_dir: PathBuf, 140 pub(crate) debug: bool, 141 } 142 143 impl CliOptions { 144 pub(crate) fn parse() -> Result<Option<Self>> { 145 Self::parse_from(std::env::args().skip(1)) 146 } 147 148 pub(crate) fn parse_from(args: impl IntoIterator<Item = String>) -> Result<Option<Self>> { 149 let mut opts = Self { 150 wallet_path: None, 151 chain_db_path: None, 152 http_addr: SocketAddr::from_str("127.0.0.1:18661")?, 153 p2p_addr: SocketAddr::from_str("127.0.0.1:9444")?, 154 p2p_addr_configured: false, 155 p2p_announce_addr: None, 156 stratum_addr: None, 157 wallet_endpoint_addr: None, 158 peers: Vec::new(), 159 join_peers: Vec::new(), 160 chain_mode: ChainMode::Setup, 161 data_dir: default_data_dir(), 162 debug: false, 163 }; 164 165 let raw_args = args.into_iter().collect::<Vec<_>>(); 166 let mut args = raw_args.into_iter(); 167 while let Some(arg) = args.next() { 168 match arg.as_str() { 169 "--genesis" => { 170 if opts.chain_mode == ChainMode::Join { 171 bail!("choose either --genesis or --join, not both"); 172 } 173 opts.chain_mode = ChainMode::Genesis; 174 } 175 "--wallet" => { 176 opts.wallet_path = Some(PathBuf::from(next_value(&mut args, "--wallet")?)) 177 } 178 "--chain-db" => { 179 opts.chain_db_path = Some(PathBuf::from(next_value(&mut args, "--chain-db")?)) 180 } 181 "--wallet-seed" => { 182 bail!( 183 "--wallet-seed was removed; wallets are stored in --wallet <path> or ~/.iuna/wallet.json" 184 ) 185 } 186 "--http" => { 187 opts.http_addr = next_value(&mut args, "--http")? 188 .parse() 189 .context("invalid --http address")?; 190 } 191 "--p2p" => { 192 opts.p2p_addr = next_value(&mut args, "--p2p")? 193 .parse() 194 .context("invalid --p2p address")?; 195 opts.p2p_addr_configured = true; 196 } 197 "--p2p-announce" => { 198 opts.p2p_announce_addr = Some( 199 next_value(&mut args, "--p2p-announce")? 200 .parse() 201 .context("invalid --p2p-announce address")?, 202 ); 203 } 204 "--stratum" => { 205 opts.stratum_addr = Some( 206 next_value(&mut args, "--stratum")? 207 .parse() 208 .context("invalid --stratum address")?, 209 ); 210 } 211 "--wallet-endpoint" => { 212 opts.wallet_endpoint_addr = Some( 213 next_value(&mut args, "--wallet-endpoint")? 214 .parse() 215 .context("invalid --wallet-endpoint address")?, 216 ); 217 } 218 "--join" => { 219 if opts.chain_mode == ChainMode::Genesis { 220 bail!("choose either --genesis or --join, not both"); 221 } 222 let peer = next_value(&mut args, "--join")?; 223 opts.chain_mode = ChainMode::Join; 224 opts.peers.push(peer.clone()); 225 opts.join_peers.push(peer); 226 } 227 "--data-dir" => opts.data_dir = PathBuf::from(next_value(&mut args, "--data-dir")?), 228 "--debug" => opts.debug = true, 229 "--help" | "-h" => { 230 print_help(); 231 std::process::exit(0); 232 } 233 other => bail!("unknown argument {other}; pass --help for usage"), 234 } 235 } 236 237 if opts.chain_mode == ChainMode::Genesis && !opts.join_peers.is_empty() { 238 bail!("choose either --genesis or --join, not both"); 239 } 240 if opts 241 .wallet_endpoint_addr 242 .is_some_and(|address| address.port() == 0) 243 { 244 bail!("--wallet-endpoint port must be between 1 and 65535"); 245 } 246 247 Ok(Some(opts)) 248 } 249 250 pub(crate) fn wallet_path(&self) -> PathBuf { 251 self.wallet_path 252 .clone() 253 .unwrap_or_else(|| self.data_dir.join("wallet.json")) 254 } 255 256 pub(crate) fn chain_db_path(&self) -> PathBuf { 257 self.chain_db_path 258 .clone() 259 .unwrap_or_else(|| self.data_dir.join("chain.sqlite3")) 260 } 261 262 pub(crate) fn config_path(&self) -> PathBuf { 263 self.data_dir.join("config.json") 264 } 265 266 pub(crate) fn has_chain(&self) -> bool { 267 self.chain_mode != ChainMode::Setup 268 } 269 } 270 271 fn next_value(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<String> { 272 args.next() 273 .with_context(|| format!("missing value after {flag}")) 274 } 275 276 pub(crate) fn validate_wallet_for_mode( 277 opts: &CliOptions, 278 wallet_path: &Path, 279 wallet_file_exists: bool, 280 ) -> Result<()> { 281 if opts.chain_mode == ChainMode::Genesis && wallet_file_exists { 282 bail!( 283 "--genesis requires a fresh wallet path, but {} already exists; start without --genesis to reuse it or choose an empty --data-dir/--wallet", 284 wallet_path.display() 285 ); 286 } 287 Ok(()) 288 } 289 290 pub(crate) fn initial_burn_per_block( 291 opts: &CliOptions, 292 ui_config: &config_store::UiConfig, 293 ) -> Amount { 294 match opts.chain_mode { 295 ChainMode::Genesis => GENESIS_INITIAL_BURN_PER_BLOCK, 296 ChainMode::Setup | ChainMode::Join => ui_config.burn_per_block, 297 } 298 } 299 300 pub(crate) fn initial_burn_fee(opts: &CliOptions, ui_config: &config_store::UiConfig) -> Amount { 301 match opts.chain_mode { 302 ChainMode::Genesis => GENESIS_INITIAL_BURN_FEE, 303 ChainMode::Setup | ChainMode::Join => ui_config.burn_fee, 304 } 305 } 306 307 fn print_help() { 308 println!("{}", help_text()); 309 } 310 311 pub(crate) fn help_text() -> &'static str { 312 "iuna\n\n\ 313 Usage:\n\ 314 iuna [options]\n\ 315 iuna --genesis [options]\n\ 316 iuna --join <addr:port> [options]\n\ 317 iuna update [--check]\n\n\ 318 Options:\n\ 319 --version Print the installed iuna version\n\ 320 update [--check] Install or only check for a signed CLI update\n\ 321 --genesis Create a new chain with a fresh setup wallet\n\ 322 --wallet <path> Wallet file (default <data-dir>/wallet.json)\n\ 323 --chain-db <path> Chain SQLite database (default <data-dir>/chain.sqlite3)\n\ 324 --http <addr:port> HTTP management UI address (default 127.0.0.1:18661)\n\ 325 --p2p <addr:port> Inbound P2P listener address when public node is enabled\n\ 326 --p2p-announce <addr:port> Public P2P address to gossip; enables inbound P2P\n\ 327 --stratum <addr:port> Stratum V1 listener for SHA-256 ASIC miners\n\ 328 --wallet-endpoint <addr:port> Public lightweight-wallet API on a separate listener\n\ 329 --join <addr:port> Fetch chain snapshot from this peer before finalization\n\ 330 --data-dir <path> Local wallet directory (default ~/.iuna)\n\ 331 --debug Print verbose runtime logs\n\n\ 332 Environment:\n\ 333 IUNA_WALLET_PASSWORD=<password> Unlock/encrypt the startup wallet and configure UI auth\n\ 334 IUNA_SETUP_COMPLETE=true|false Persist initial setup completion at startup\n\ 335 IUNA_AUTOMATIC_BURN_ENABLED=true|false Persist automatic burn/finalization at startup\n\ 336 IUNA_POW_MINING_ENABLED=true|false Persist automatic PoW mining at startup\n\ 337 IUNA_POW_MINING_WORKERS=1..32 Set and persist the PoW worker count at startup\n\ 338 IUNA_WALLET_ENDPOINT_ENABLED=true|false Enable the separate public wallet API\n\ 339 IUNA_WALLET_ENDPOINT_PORT=1..65535 Set its public listener port (default 18662)\n\ 340 IUNA_DEV_SKIP_SEED_VERIFY=1 Show a setup button to skip seed verification\n" 341 } 342 343 pub(crate) fn default_data_dir() -> PathBuf { 344 std::env::var_os("HOME") 345 .filter(|home| !home.is_empty()) 346 .or_else(|| std::env::var_os("USERPROFILE").filter(|home| !home.is_empty())) 347 .map(PathBuf::from) 348 .map(|home| home.join(".iuna")) 349 .unwrap_or_else(|| PathBuf::from(".iuna")) 350 }