iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }