From 1ff9852cff671b0ddb42fe595306b1b5c4f1ee3d Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Fri, 8 May 2020 23:30:45 +0200 Subject: [PATCH] [wasm] Fix SystemTime for wasm and refactor the cli part --- Cargo.toml | 7 +- examples/repl.rs | 297 ++------------------------------------------- src/cli.rs | 300 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + src/wallet/mod.rs | 9 ++ 5 files changed, 325 insertions(+), 291 deletions(-) create mode 100644 src/cli.rs diff --git a/Cargo.toml b/Cargo.toml index 7bb030e5..5137c7f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ sled = { version = "0.31.0", optional = true } electrum-client = { git = "https://github.com/MagicalBitcoin/rust-electrum-client.git", optional = true } reqwest = { version = "0.10", optional = true, features = ["json"] } futures = { version = "0.3", optional = true } +clap = { version = "2.33", optional = true } [features] minimal = [] @@ -26,18 +27,19 @@ default = ["key-value-db", "electrum"] electrum = ["electrum-client"] esplora = ["reqwest", "futures"] key-value-db = ["sled"] +cli-utils = ["clap"] [dev-dependencies] tokio = { version = "0.2", features = ["macros"] } lazy_static = "1.4" -rustyline = "5.0" # newer version requires 2018 edition -clap = "2.33" +rustyline = "6.0" dirs = "2.0" env_logger = "0.7" rand = "0.7" [[example]] name = "repl" +required-features = ["cli-utils"] [[example]] name = "psbt" [[example]] @@ -52,4 +54,5 @@ required-features = ["compiler"] [[example]] name = "magic" path = "examples/repl.rs" +required-features = ["cli-utils"] diff --git a/examples/repl.rs b/examples/repl.rs index 74d366eb..5a78bed3 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -1,32 +1,21 @@ -extern crate base64; -extern crate clap; -extern crate dirs; -extern crate env_logger; -extern crate log; -extern crate magical_bitcoin_wallet; -extern crate rustyline; - use std::fs; use std::path::PathBuf; -use std::str::FromStr; use std::sync::Arc; -use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; - use rustyline::error::ReadlineError; use rustyline::Editor; +use clap::AppSettings; + #[allow(unused_imports)] use log::{debug, error, info, trace, LevelFilter}; -use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex}; -use bitcoin::util::psbt::PartiallySignedTransaction; -use bitcoin::{Address, Network, OutPoint}; +use bitcoin::Network; use magical_bitcoin_wallet::bitcoin; use magical_bitcoin_wallet::blockchain::ElectrumBlockchain; +use magical_bitcoin_wallet::cli; use magical_bitcoin_wallet::sled; -use magical_bitcoin_wallet::types::ScriptType; use magical_bitcoin_wallet::{Client, Wallet}; fn prepare_home_dir() -> PathBuf { @@ -43,204 +32,14 @@ fn prepare_home_dir() -> PathBuf { dir } -fn parse_addressee(s: &str) -> Result<(Address, u64), String> { - let parts: Vec<_> = s.split(":").collect(); - if parts.len() != 2 { - return Err("Invalid format".to_string()); - } - - let addr = Address::from_str(&parts[0]); - if let Err(e) = addr { - return Err(format!("{:?}", e)); - } - let val = u64::from_str(&parts[1]); - if let Err(e) = val { - return Err(format!("{:?}", e)); - } - - Ok((addr.unwrap(), val.unwrap())) -} - -fn parse_outpoint(s: &str) -> Result { - OutPoint::from_str(s).map_err(|e| format!("{:?}", e)) -} - -fn addressee_validator(s: String) -> Result<(), String> { - parse_addressee(&s).map(|_| ()) -} - -fn outpoint_validator(s: String) -> Result<(), String> { - parse_outpoint(&s).map(|_| ()) -} - #[tokio::main] async fn main() { env_logger::init(); - let app = App::new("Magical Bitcoin Wallet") - .version(option_env!("CARGO_PKG_VERSION").unwrap_or("unknown")) - .author(option_env!("CARGO_PKG_AUTHORS").unwrap_or("")) - .about("A modern, lightweight, descriptor-based wallet") - .subcommand( - SubCommand::with_name("get_new_address").about("Generates a new external address"), - ) - .subcommand(SubCommand::with_name("sync").about("Syncs with the chosen Electrum server")) - .subcommand( - SubCommand::with_name("list_unspent").about("Lists the available spendable UTXOs"), - ) - .subcommand( - SubCommand::with_name("get_balance").about("Returns the current wallet balance"), - ) - .subcommand( - SubCommand::with_name("create_tx") - .about("Creates a new unsigned tranasaction") - .arg( - Arg::with_name("to") - .long("to") - .value_name("ADDRESS:SAT") - .help("Adds an addressee to the transaction") - .takes_value(true) - .number_of_values(1) - .required(true) - .multiple(true) - .validator(addressee_validator), - ) - .arg( - Arg::with_name("send_all") - .short("all") - .long("send_all") - .help("Sends all the funds (or all the selected utxos). Requires only one addressees of value 0"), - ) - .arg( - Arg::with_name("utxos") - .long("utxos") - .value_name("TXID:VOUT") - .help("Selects which utxos *must* be spent") - .takes_value(true) - .number_of_values(1) - .multiple(true) - .validator(outpoint_validator), - ) - .arg( - Arg::with_name("unspendable") - .long("unspendable") - .value_name("TXID:VOUT") - .help("Marks an utxo as unspendable") - .takes_value(true) - .number_of_values(1) - .multiple(true) - .validator(outpoint_validator), - ) - .arg( - Arg::with_name("fee_rate") - .short("fee") - .long("fee_rate") - .value_name("SATS_VBYTE") - .help("Fee rate to use in sat/vbyte") - .takes_value(true), - ) - .arg( - Arg::with_name("policy") - .long("policy") - .value_name("POLICY") - .help("Selects which policy will be used to satisfy the descriptor") - .takes_value(true) - .number_of_values(1), - ), - ) - .subcommand( - SubCommand::with_name("policies") - .about("Returns the available spending policies for the descriptor") - ) - .subcommand( - SubCommand::with_name("sign") - .about("Signs and tries to finalize a PSBT") - .arg( - Arg::with_name("psbt") - .long("psbt") - .value_name("BASE64_PSBT") - .help("Sets the PSBT to sign") - .takes_value(true) - .number_of_values(1) - .required(true), - ) - .arg( - Arg::with_name("assume_height") - .long("assume_height") - .value_name("HEIGHT") - .help("Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor") - .takes_value(true) - .number_of_values(1) - .required(false), - )) - .subcommand( - SubCommand::with_name("broadcast") - .about("Extracts the finalized transaction from a PSBT and broadcasts it to the network") - .arg( - Arg::with_name("psbt") - .long("psbt") - .value_name("BASE64_PSBT") - .help("Sets the PSBT to broadcast") - .takes_value(true) - .number_of_values(1) - .required(true), - )); - + let app = cli::make_cli_subcommands(); let mut repl_app = app.clone().setting(AppSettings::NoBinaryName); - let app = app - .arg( - Arg::with_name("network") - .short("n") - .long("network") - .value_name("NETWORK") - .help("Sets the network") - .takes_value(true) - .default_value("testnet") - .possible_values(&["testnet", "regtest"]), - ) - .arg( - Arg::with_name("wallet") - .short("w") - .long("wallet") - .value_name("WALLET_NAME") - .help("Selects the wallet to use") - .takes_value(true) - .default_value("main"), - ) - .arg( - Arg::with_name("server") - .short("s") - .long("server") - .value_name("SERVER:PORT") - .help("Sets the Electrum server to use") - .takes_value(true) - .default_value("tn.not.fyi:55001"), - ) - .arg( - Arg::with_name("descriptor") - .short("d") - .long("descriptor") - .value_name("DESCRIPTOR") - .help("Sets the descriptor to use for the external addresses") - .required(true) - .takes_value(true), - ) - .arg( - Arg::with_name("change_descriptor") - .short("c") - .long("change_descriptor") - .value_name("DESCRIPTOR") - .help("Sets the descriptor to use for internal addresses") - .takes_value(true), - ) - .arg( - Arg::with_name("v") - .short("v") - .multiple(true) - .help("Sets the level of verbosity"), - ) - .subcommand(SubCommand::with_name("repl").about("Opens an interactive shell")); + let app = cli::add_global_flags(app); let matches = app.get_matches(); @@ -280,86 +79,6 @@ async fn main() { .unwrap(); let wallet = Arc::new(wallet); - // TODO: print errors in a nice way - async fn handle_matches(wallet: Arc>, matches: ArgMatches<'_>) - where - C: magical_bitcoin_wallet::blockchain::OnlineBlockchain, - D: magical_bitcoin_wallet::database::BatchDatabase, - { - if let Some(_sub_matches) = matches.subcommand_matches("get_new_address") { - println!("{}", wallet.get_new_address().unwrap().to_string()); - } else if let Some(_sub_matches) = matches.subcommand_matches("sync") { - wallet.sync(None, None).await.unwrap(); - } else if let Some(_sub_matches) = matches.subcommand_matches("list_unspent") { - for utxo in wallet.list_unspent().unwrap() { - println!("{} value {} SAT", utxo.outpoint, utxo.txout.value); - } - } else if let Some(_sub_matches) = matches.subcommand_matches("get_balance") { - println!("{} SAT", wallet.get_balance().unwrap()); - } else if let Some(sub_matches) = matches.subcommand_matches("create_tx") { - let addressees = sub_matches - .values_of("to") - .unwrap() - .map(|s| parse_addressee(s).unwrap()) - .collect(); - let send_all = sub_matches.is_present("send_all"); - let fee_rate = sub_matches - .value_of("fee_rate") - .map(|s| f32::from_str(s).unwrap()) - .unwrap_or(1.0); - let utxos = sub_matches - .values_of("utxos") - .map(|s| s.map(|i| parse_outpoint(i).unwrap()).collect()); - let unspendable = sub_matches - .values_of("unspendable") - .map(|s| s.map(|i| parse_outpoint(i).unwrap()).collect()); - let policy: Option> = sub_matches - .value_of("policy") - .map(|s| serde_json::from_str::>>(&s).unwrap()); - - let result = wallet - .create_tx( - addressees, - send_all, - fee_rate * 1e-5, - policy, - utxos, - unspendable, - ) - .unwrap(); - println!("{:#?}", result.1); - println!("PSBT: {}", base64::encode(&serialize(&result.0))); - } else if let Some(_sub_matches) = matches.subcommand_matches("policies") { - println!( - "External: {}", - serde_json::to_string(&wallet.policies(ScriptType::External).unwrap()).unwrap() - ); - println!( - "Internal: {}", - serde_json::to_string(&wallet.policies(ScriptType::Internal).unwrap()).unwrap() - ); - } else if let Some(sub_matches) = matches.subcommand_matches("sign") { - let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap(); - let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); - let assume_height = sub_matches - .value_of("assume_height") - .and_then(|s| Some(s.parse().unwrap())); - let (psbt, finalized) = wallet.sign(psbt, assume_height).unwrap(); - - println!("PSBT: {}", base64::encode(&serialize(&psbt))); - println!("Finalized: {}", finalized); - if finalized { - println!("Extracted: {}", serialize_hex(&psbt.extract_tx())); - } - } else if let Some(sub_matches) = matches.subcommand_matches("broadcast") { - let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap(); - let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); - let (txid, _) = wallet.broadcast(psbt).await.unwrap(); - - println!("TXID: {}", txid); - } - }; - if let Some(_sub_matches) = matches.subcommand_matches("repl") { let mut rl = Editor::<()>::new(); @@ -382,7 +101,7 @@ async fn main() { continue; } - handle_matches(Arc::clone(&wallet), matches.unwrap()).await; + cli::handle_matches(&Arc::clone(&wallet), matches.unwrap()).await; } Err(ReadlineError::Interrupted) => continue, Err(ReadlineError::Eof) => break, @@ -395,6 +114,6 @@ async fn main() { // rl.save_history("history.txt").unwrap(); } else { - handle_matches(wallet, matches).await; + cli::handle_matches(&wallet, matches).await; } } diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000..5b24544c --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,300 @@ +use std::str::FromStr; + +use clap::{App, Arg, ArgMatches, SubCommand}; + +#[allow(unused_imports)] +use log::{debug, error, info, trace, LevelFilter}; + +use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex}; +use bitcoin::util::psbt::PartiallySignedTransaction; +use bitcoin::{Address, OutPoint}; + +use crate::error::Error; +use crate::types::ScriptType; +use crate::Wallet; + +fn parse_addressee(s: &str) -> Result<(Address, u64), String> { + let parts: Vec<_> = s.split(":").collect(); + if parts.len() != 2 { + return Err("Invalid format".to_string()); + } + + let addr = Address::from_str(&parts[0]); + if let Err(e) = addr { + return Err(format!("{:?}", e)); + } + let val = u64::from_str(&parts[1]); + if let Err(e) = val { + return Err(format!("{:?}", e)); + } + + Ok((addr.unwrap(), val.unwrap())) +} + +fn parse_outpoint(s: &str) -> Result { + OutPoint::from_str(s).map_err(|e| format!("{:?}", e)) +} + +fn addressee_validator(s: String) -> Result<(), String> { + parse_addressee(&s).map(|_| ()) +} + +fn outpoint_validator(s: String) -> Result<(), String> { + parse_outpoint(&s).map(|_| ()) +} + +pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> { + App::new("Magical Bitcoin Wallet") + .version(option_env!("CARGO_PKG_VERSION").unwrap_or("unknown")) + .author(option_env!("CARGO_PKG_AUTHORS").unwrap_or("")) + .about("A modern, lightweight, descriptor-based wallet") + .subcommand( + SubCommand::with_name("get_new_address").about("Generates a new external address"), + ) + .subcommand(SubCommand::with_name("sync").about("Syncs with the chosen Electrum server")) + .subcommand( + SubCommand::with_name("list_unspent").about("Lists the available spendable UTXOs"), + ) + .subcommand( + SubCommand::with_name("get_balance").about("Returns the current wallet balance"), + ) + .subcommand( + SubCommand::with_name("create_tx") + .about("Creates a new unsigned tranasaction") + .arg( + Arg::with_name("to") + .long("to") + .value_name("ADDRESS:SAT") + .help("Adds an addressee to the transaction") + .takes_value(true) + .number_of_values(1) + .required(true) + .multiple(true) + .validator(addressee_validator), + ) + .arg( + Arg::with_name("send_all") + .short("all") + .long("send_all") + .help("Sends all the funds (or all the selected utxos). Requires only one addressees of value 0"), + ) + .arg( + Arg::with_name("utxos") + .long("utxos") + .value_name("TXID:VOUT") + .help("Selects which utxos *must* be spent") + .takes_value(true) + .number_of_values(1) + .multiple(true) + .validator(outpoint_validator), + ) + .arg( + Arg::with_name("unspendable") + .long("unspendable") + .value_name("TXID:VOUT") + .help("Marks an utxo as unspendable") + .takes_value(true) + .number_of_values(1) + .multiple(true) + .validator(outpoint_validator), + ) + .arg( + Arg::with_name("fee_rate") + .short("fee") + .long("fee_rate") + .value_name("SATS_VBYTE") + .help("Fee rate to use in sat/vbyte") + .takes_value(true), + ) + .arg( + Arg::with_name("policy") + .long("policy") + .value_name("POLICY") + .help("Selects which policy will be used to satisfy the descriptor") + .takes_value(true) + .number_of_values(1), + ), + ) + .subcommand( + SubCommand::with_name("policies") + .about("Returns the available spending policies for the descriptor") + ) + .subcommand( + SubCommand::with_name("sign") + .about("Signs and tries to finalize a PSBT") + .arg( + Arg::with_name("psbt") + .long("psbt") + .value_name("BASE64_PSBT") + .help("Sets the PSBT to sign") + .takes_value(true) + .number_of_values(1) + .required(true), + ) + .arg( + Arg::with_name("assume_height") + .long("assume_height") + .value_name("HEIGHT") + .help("Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor") + .takes_value(true) + .number_of_values(1) + .required(false), + )) + .subcommand( + SubCommand::with_name("broadcast") + .about("Extracts the finalized transaction from a PSBT and broadcasts it to the network") + .arg( + Arg::with_name("psbt") + .long("psbt") + .value_name("BASE64_PSBT") + .help("Sets the PSBT to broadcast") + .takes_value(true) + .number_of_values(1) + .required(true), + )) +} + +pub fn add_global_flags<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("network") + .short("n") + .long("network") + .value_name("NETWORK") + .help("Sets the network") + .takes_value(true) + .default_value("testnet") + .possible_values(&["testnet", "regtest"]), + ) + .arg( + Arg::with_name("wallet") + .short("w") + .long("wallet") + .value_name("WALLET_NAME") + .help("Selects the wallet to use") + .takes_value(true) + .default_value("main"), + ) + .arg( + Arg::with_name("server") + .short("s") + .long("server") + .value_name("SERVER:PORT") + .help("Sets the Electrum server to use") + .takes_value(true) + .default_value("tn.not.fyi:55001"), + ) + .arg( + Arg::with_name("descriptor") + .short("d") + .long("descriptor") + .value_name("DESCRIPTOR") + .help("Sets the descriptor to use for the external addresses") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("change_descriptor") + .short("c") + .long("change_descriptor") + .value_name("DESCRIPTOR") + .help("Sets the descriptor to use for internal addresses") + .takes_value(true), + ) + .arg( + Arg::with_name("v") + .short("v") + .multiple(true) + .help("Sets the level of verbosity"), + ) + .subcommand(SubCommand::with_name("repl").about("Opens an interactive shell")) +} + +pub async fn handle_matches( + wallet: &Wallet, + matches: ArgMatches<'_>, +) -> Result, Error> +where + C: crate::blockchain::OnlineBlockchain, + D: crate::database::BatchDatabase, +{ + if let Some(_sub_matches) = matches.subcommand_matches("get_new_address") { + Ok(Some(format!("{}", wallet.get_new_address()?))) + } else if let Some(_sub_matches) = matches.subcommand_matches("sync") { + wallet.sync(None, None).await?; + Ok(None) + } else if let Some(_sub_matches) = matches.subcommand_matches("list_unspent") { + let mut res = String::new(); + for utxo in wallet.list_unspent()? { + res += &format!("{} value {} SAT\n", utxo.outpoint, utxo.txout.value); + } + + Ok(Some(res)) + } else if let Some(_sub_matches) = matches.subcommand_matches("get_balance") { + Ok(Some(format!("{} SAT", wallet.get_balance()?))) + } else if let Some(sub_matches) = matches.subcommand_matches("create_tx") { + let addressees = sub_matches + .values_of("to") + .unwrap() + .map(|s| parse_addressee(s).unwrap()) + .collect(); + let send_all = sub_matches.is_present("send_all"); + let fee_rate = sub_matches + .value_of("fee_rate") + .map(|s| f32::from_str(s).unwrap()) + .unwrap_or(1.0); + let utxos = sub_matches + .values_of("utxos") + .map(|s| s.map(|i| parse_outpoint(i).unwrap()).collect()); + let unspendable = sub_matches + .values_of("unspendable") + .map(|s| s.map(|i| parse_outpoint(i).unwrap()).collect()); + let policy: Option> = sub_matches + .value_of("policy") + .map(|s| serde_json::from_str::>>(&s).unwrap()); + + let result = wallet.create_tx( + addressees, + send_all, + fee_rate * 1e-5, + policy, + utxos, + unspendable, + )?; + Ok(Some(format!( + "{:#?}\nPSBT: {}", + result.1, + base64::encode(&serialize(&result.0)) + ))) + } else if let Some(_sub_matches) = matches.subcommand_matches("policies") { + Ok(Some(format!( + "External: {}\nInternal:{}", + serde_json::to_string(&wallet.policies(ScriptType::External)?).unwrap(), + serde_json::to_string(&wallet.policies(ScriptType::Internal)?).unwrap(), + ))) + } else if let Some(sub_matches) = matches.subcommand_matches("sign") { + let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap(); + let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); + let assume_height = sub_matches + .value_of("assume_height") + .and_then(|s| Some(s.parse().unwrap())); + let (psbt, finalized) = wallet.sign(psbt, assume_height)?; + + let mut res = String::new(); + + res += &format!("PSBT: {}\n", base64::encode(&serialize(&psbt))); + res += &format!("Finalized: {}", finalized); + if finalized { + res += &format!("\nExtracted: {}", serialize_hex(&psbt.extract_tx())); + } + + Ok(Some(res)) + } else if let Some(sub_matches) = matches.subcommand_matches("broadcast") { + let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap(); + let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); + let (txid, _) = wallet.broadcast(psbt).await?; + + Ok(Some(format!("TXID: {}", txid))) + } else { + Ok(None) + } +} diff --git a/src/lib.rs b/src/lib.rs index 2b7ceece..8cd97373 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,9 @@ pub use blockchain::esplora::EsploraBlockchain; #[cfg(feature = "key-value-db")] pub extern crate sled; +#[cfg(feature = "cli-utils")] +pub mod cli; + #[macro_use] pub mod error; pub mod blockchain; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index f1501786..dac90aee 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -498,6 +498,7 @@ where // Internals + #[cfg(not(target_arch = "wasm32"))] fn get_timestamp() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -505,6 +506,11 @@ where .as_secs() } + #[cfg(target_arch = "wasm32")] + fn get_timestamp() -> u64 { + 0 + } + fn get_descriptor_for(&self, script_type: ScriptType) -> &ExtendedDescriptor { let desc = match script_type { ScriptType::External => &self.descriptor, @@ -646,6 +652,7 @@ where // safe to run only on the descriptor because we assume the change descriptor also has // the same structure let desc = self.descriptor.derive_from_psbt_input(psbt, n); + debug!("{:?}", psbt.inputs[n].hd_keypaths); debug!("reconstructed descriptor is {:?}", desc); let desc = match desc { @@ -765,6 +772,7 @@ where // cache a few of our addresses if last_addr.is_none() { let mut address_batch = self.database.borrow().begin_batch(); + #[cfg(not(target_arch = "wasm32"))] let start = Instant::now(); for i in 0..=max_address { @@ -790,6 +798,7 @@ where } } + #[cfg(not(target_arch = "wasm32"))] info!( "derivation of {} addresses, took {} ms", max_address,