cli.rs (9523B)
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 apply_cli_p2p_config_overrides( 40 opts: &CliOptions, 41 ui_config: &mut config_store::UiConfig, 42 ) -> bool { 43 let mut dirty = false; 44 if opts.p2p_addr_configured { 45 let bind_port = opts.p2p_addr.port(); 46 if ui_config.p2p_bind_port != bind_port { 47 ui_config.p2p_bind_port = bind_port; 48 dirty = true; 49 } 50 } 51 if let Some(addr) = opts.p2p_announce_addr { 52 let announce_addr = addr.to_string(); 53 if !ui_config.p2p_accept_inbound 54 || ui_config.p2p_announce_addr.as_deref() != Some(&announce_addr) 55 { 56 ui_config.p2p_accept_inbound = true; 57 ui_config.p2p_announce_addr = Some(announce_addr); 58 dirty = true; 59 } 60 } 61 dirty 62 } 63 64 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 65 pub(crate) enum ChainMode { 66 Setup, 67 Genesis, 68 Join, 69 } 70 71 #[derive(Debug)] 72 pub(crate) struct CliOptions { 73 pub(crate) wallet_path: Option<PathBuf>, 74 pub(crate) chain_db_path: Option<PathBuf>, 75 pub(crate) http_addr: SocketAddr, 76 pub(crate) p2p_addr: SocketAddr, 77 pub(crate) p2p_addr_configured: bool, 78 pub(crate) p2p_announce_addr: Option<SocketAddr>, 79 pub(crate) stratum_addr: Option<SocketAddr>, 80 pub(crate) peers: Vec<String>, 81 pub(crate) join_peers: Vec<String>, 82 pub(crate) chain_mode: ChainMode, 83 pub(crate) data_dir: PathBuf, 84 pub(crate) debug: bool, 85 } 86 87 impl CliOptions { 88 pub(crate) fn parse() -> Result<Option<Self>> { 89 Self::parse_from(std::env::args().skip(1)) 90 } 91 92 pub(crate) fn parse_from(args: impl IntoIterator<Item = String>) -> Result<Option<Self>> { 93 let mut opts = Self { 94 wallet_path: None, 95 chain_db_path: None, 96 http_addr: SocketAddr::from_str("127.0.0.1:18661")?, 97 p2p_addr: SocketAddr::from_str("127.0.0.1:9444")?, 98 p2p_addr_configured: false, 99 p2p_announce_addr: None, 100 stratum_addr: None, 101 peers: Vec::new(), 102 join_peers: Vec::new(), 103 chain_mode: ChainMode::Setup, 104 data_dir: default_data_dir(), 105 debug: false, 106 }; 107 108 let raw_args = args.into_iter().collect::<Vec<_>>(); 109 let mut args = raw_args.into_iter(); 110 while let Some(arg) = args.next() { 111 match arg.as_str() { 112 "--genesis" => { 113 if opts.chain_mode == ChainMode::Join { 114 bail!("choose either --genesis or --join, not both"); 115 } 116 opts.chain_mode = ChainMode::Genesis; 117 } 118 "--wallet" => { 119 opts.wallet_path = Some(PathBuf::from(next_value(&mut args, "--wallet")?)) 120 } 121 "--chain-db" => { 122 opts.chain_db_path = Some(PathBuf::from(next_value(&mut args, "--chain-db")?)) 123 } 124 "--wallet-seed" => { 125 bail!( 126 "--wallet-seed was removed; wallets are stored in --wallet <path> or ~/.iuna/wallet.json" 127 ) 128 } 129 "--http" => { 130 opts.http_addr = next_value(&mut args, "--http")? 131 .parse() 132 .context("invalid --http address")?; 133 } 134 "--p2p" => { 135 opts.p2p_addr = next_value(&mut args, "--p2p")? 136 .parse() 137 .context("invalid --p2p address")?; 138 opts.p2p_addr_configured = true; 139 } 140 "--p2p-announce" => { 141 opts.p2p_announce_addr = Some( 142 next_value(&mut args, "--p2p-announce")? 143 .parse() 144 .context("invalid --p2p-announce address")?, 145 ); 146 } 147 "--stratum" => { 148 opts.stratum_addr = Some( 149 next_value(&mut args, "--stratum")? 150 .parse() 151 .context("invalid --stratum address")?, 152 ); 153 } 154 "--join" => { 155 if opts.chain_mode == ChainMode::Genesis { 156 bail!("choose either --genesis or --join, not both"); 157 } 158 let peer = next_value(&mut args, "--join")?; 159 opts.chain_mode = ChainMode::Join; 160 opts.peers.push(peer.clone()); 161 opts.join_peers.push(peer); 162 } 163 "--data-dir" => opts.data_dir = PathBuf::from(next_value(&mut args, "--data-dir")?), 164 "--debug" => opts.debug = true, 165 "--help" | "-h" => { 166 print_help(); 167 std::process::exit(0); 168 } 169 other => bail!("unknown argument {other}; pass --help for usage"), 170 } 171 } 172 173 if opts.chain_mode == ChainMode::Genesis && !opts.join_peers.is_empty() { 174 bail!("choose either --genesis or --join, not both"); 175 } 176 177 Ok(Some(opts)) 178 } 179 180 pub(crate) fn wallet_path(&self) -> PathBuf { 181 self.wallet_path 182 .clone() 183 .unwrap_or_else(|| self.data_dir.join("wallet.json")) 184 } 185 186 pub(crate) fn chain_db_path(&self) -> PathBuf { 187 self.chain_db_path 188 .clone() 189 .unwrap_or_else(|| self.data_dir.join("chain.sqlite3")) 190 } 191 192 pub(crate) fn config_path(&self) -> PathBuf { 193 self.data_dir.join("config.json") 194 } 195 196 pub(crate) fn has_chain(&self) -> bool { 197 self.chain_mode != ChainMode::Setup 198 } 199 } 200 201 fn next_value(args: &mut impl Iterator<Item = String>, flag: &str) -> Result<String> { 202 args.next() 203 .with_context(|| format!("missing value after {flag}")) 204 } 205 206 pub(crate) fn validate_wallet_for_mode( 207 opts: &CliOptions, 208 wallet_path: &Path, 209 wallet_file_exists: bool, 210 ) -> Result<()> { 211 if opts.chain_mode == ChainMode::Genesis && wallet_file_exists { 212 bail!( 213 "--genesis requires a fresh wallet path, but {} already exists; start without --genesis to reuse it or choose an empty --data-dir/--wallet", 214 wallet_path.display() 215 ); 216 } 217 Ok(()) 218 } 219 220 pub(crate) fn initial_burn_per_block( 221 opts: &CliOptions, 222 ui_config: &config_store::UiConfig, 223 ) -> Amount { 224 match opts.chain_mode { 225 ChainMode::Genesis => GENESIS_INITIAL_BURN_PER_BLOCK, 226 ChainMode::Setup | ChainMode::Join => ui_config.burn_per_block, 227 } 228 } 229 230 pub(crate) fn initial_burn_fee(opts: &CliOptions, ui_config: &config_store::UiConfig) -> Amount { 231 match opts.chain_mode { 232 ChainMode::Genesis => GENESIS_INITIAL_BURN_FEE, 233 ChainMode::Setup | ChainMode::Join => ui_config.burn_fee, 234 } 235 } 236 237 fn print_help() { 238 println!("{}", help_text()); 239 } 240 241 pub(crate) fn help_text() -> &'static str { 242 "iuna\n\n\ 243 Usage:\n\ 244 iuna [options]\n\ 245 iuna --genesis [options]\n\ 246 iuna --join <addr:port> [options]\n\n\ 247 Options:\n\ 248 --genesis Create a new chain with a fresh setup wallet\n\ 249 --wallet <path> Wallet file (default <data-dir>/wallet.json)\n\ 250 --chain-db <path> Chain SQLite database (default <data-dir>/chain.sqlite3)\n\ 251 --http <addr:port> HTTP management UI address (default 127.0.0.1:18661)\n\ 252 --p2p <addr:port> Inbound P2P listener address when public node is enabled\n\ 253 --p2p-announce <addr:port> Public P2P address to gossip; enables inbound P2P\n\ 254 --stratum <addr:port> Stratum V1 listener for SHA-256 ASIC miners\n\ 255 --join <addr:port> Fetch chain snapshot from this peer before finalization\n\ 256 --data-dir <path> Local wallet directory (default ~/.iuna)\n\ 257 --debug Print verbose runtime logs\n\n\ 258 Environment:\n\ 259 IUNA_DEV_SKIP_SEED_VERIFY=1 Show a setup button to skip seed verification\n" 260 } 261 262 pub(crate) fn default_data_dir() -> PathBuf { 263 std::env::var_os("HOME") 264 .filter(|home| !home.is_empty()) 265 .or_else(|| std::env::var_os("USERPROFILE").filter(|home| !home.is_empty())) 266 .map(PathBuf::from) 267 .map(|home| home.join(".iuna")) 268 .unwrap_or_else(|| PathBuf::from(".iuna")) 269 }