iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }