From 9efdee7379298c6cba765bd684f19105caeedee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 12 Aug 2022 21:56:19 +0800 Subject: [PATCH] Separate CLI argument handling --- src/args.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 58 ++++++++++++++---------------------------- 2 files changed, 92 insertions(+), 39 deletions(-) create mode 100644 src/args.rs diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..5e9a8ae --- /dev/null +++ b/src/args.rs @@ -0,0 +1,73 @@ +use std::{collections::VecDeque, ffi::OsString, fmt, num::ParseIntError}; + +use crate::{ScheduledChannel, ScheduledPayJoin}; + +/// CLI argument errors. +#[derive(Debug)] +pub(crate) enum ArgError { + /// Argument not UTF-8 + NotUTF8(OsString), + /// Parse feerate error + FeeRateError(ParseIntError), + /// Parse node address error + InvalidNodeAddress(ln_types::p2p_address::ParseError), + /// Parse bitcoin amount error + InvalidBitcoinAmount(bitcoin::util::amount::ParseAmountError), + /// Wallet amount error + InvalidWalletAmount(bitcoin::util::amount::ParseAmountError), +} + +impl fmt::Display for ArgError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // TODO: Do this properly. + write!(f, "invalid arguments: {:?}", self) + } +} + +impl std::error::Error for ArgError {} + +/// Parses arguments in `[fee_rate] [(, )...] [wallet_amount]` +pub(crate) fn parse_args(args: A) -> Result, ArgError> +where + A: Iterator, +{ + // ensure all args are utf8 + let args = args + .map(|arg| arg.into_string()) + .collect::, _>>() + .map_err(ArgError::NotUTF8)?; + + // first argument is fee rate + let fee_rate = match args.get(0) { + Some(fee_rate_str) => fee_rate_str.parse::().map_err(ArgError::FeeRateError)?, + None => return Ok(None), + }; + + // return None if no remaining args. + let mut args = match args.get(1..) { + Some(args) => args.iter(), + None => return Ok(None), + }; + + // parse scheduled channel arguments: pairs of (addr, amount) + let mut channels = VecDeque::with_capacity(args.len() / 2); + + // the remaining single argument is the wallet amount in satoshis (if any) + let wallet_amount = loop { + match (args.next(), args.next()) { + // we have a pair of arguments, interpret it as a scheduled channel (p2p addr, amount) + (Some(addr), Some(amount)) => { + channels.push_back(ScheduledChannel::from_args(addr, amount)?); + } + // if there is a remaining single argument, it is the wallet amount + (Some(amount), None) => { + break bitcoin::Amount::from_str_in(amount, bitcoin::Denomination::Satoshi) + .map_err(ArgError::InvalidWalletAmount)? + } + // if there is no remaining single argument, the wallet amount is 0 + _ => break bitcoin::Amount::ZERO, + } + }; + + Ok(Some(ScheduledPayJoin { wallet_amount, channels, fee_rate })) +} diff --git a/src/main.rs b/src/main.rs index 328904e..a1080bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,19 @@ +mod args; + +use args::ArgError; use bitcoin::util::address::Address; use bitcoin::util::psbt::PartiallySignedTransaction; use bitcoin::{Script, TxOut}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Request, Response, Server, StatusCode}; use ln_types::P2PAddress; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::convert::TryInto; use std::fmt; use std::sync::{Arc, Mutex}; +use crate::args::parse_args; + #[macro_use] extern crate serde_derive; extern crate configure_me; @@ -29,15 +34,13 @@ struct ScheduledChannel { } impl ScheduledChannel { - fn from_args(addr: std::ffi::OsString, amount: std::ffi::OsString) -> Self { - use configure_me::parse_arg::Arg; + fn from_args(addr: &str, amount: &str) -> Result { + let node = addr.parse::().map_err(ArgError::InvalidNodeAddress)?; - let node = addr.parse().expect("invalid node address"); - let amount = amount.to_str().expect("invalid channel amount"); - let amount = bitcoin::Amount::from_str_in(&amount, bitcoin::Denomination::Satoshi) - .expect("invalid channel amount"); + let amount = bitcoin::Amount::from_str_in(amount, bitcoin::Denomination::Satoshi) + .map_err(ArgError::InvalidBitcoinAmount)?; - ScheduledChannel { node, amount } + Ok(Self { node, amount }) } } @@ -45,7 +48,7 @@ impl ScheduledChannel { struct ScheduledPayJoin { #[serde(with = "bitcoin::util::amount::serde::as_sat")] wallet_amount: bitcoin::Amount, - channels: Vec, + channels: VecDeque, fee_rate: u64, } @@ -218,9 +221,11 @@ async fn get_new_bech32_address(client: &mut tonic_lnd::Client) -> Address { #[tokio::main] async fn main() -> Result<(), Box> { - let (config, mut args) = + let (config, args) = Config::including_optional_config_files(std::iter::empty::<&str>()).unwrap_or_exit(); + let scheduled_pj = parse_args(args).expect("failed to parse remaining arguments"); + let client = tonic_lnd::connect(config.lnd_address, &config.lnd_cert_path, &config.lnd_macaroon_path) .await @@ -228,42 +233,17 @@ async fn main() -> Result<(), Box> { let mut handler = Handler::new(client).await?; - if let Some(fee_rate) = args.next() { - let fee_rate = fee_rate.into_string().expect("fee rate is not UTF-8").parse::()?; + if let Some(payjoin) = scheduled_pj { + payjoin.test_connections(&mut handler.client).await; let address = get_new_bech32_address(&mut handler.client).await; - let mut args = args.fuse(); - let mut scheduled_channels = Vec::with_capacity(args.size_hint().0 / 2); - let mut wallet_amount = bitcoin::Amount::ZERO; - while let Some(arg) = args.next() { - match args.next() { - Some(channel_amount) => { - scheduled_channels.push(ScheduledChannel::from_args(arg, channel_amount)) - } - None => { - wallet_amount = bitcoin::Amount::from_str_in( - arg.to_str().expect("wallet amount not UTF-8"), - bitcoin::Denomination::Satoshi, - )? - } - } - } - - let scheduled_payjoin = - ScheduledPayJoin { wallet_amount, channels: scheduled_channels, fee_rate }; - - scheduled_payjoin.test_connections(&mut handler.client).await; - println!( "bitcoin:{}?amount={}&pj=https://example.com/pj", address, - scheduled_payjoin.total_amount().to_string_in(bitcoin::Denomination::Bitcoin) + payjoin.total_amount().to_string_in(bitcoin::Denomination::Bitcoin) ); - handler - .payjoins - .insert(&address, scheduled_payjoin) - .expect("New Handler is supposed to be empty"); + handler.payjoins.insert(&address, payjoin).expect("new handler should be empty"); } let addr = ([127, 0, 0, 1], config.bind_port).into();