// Magical Bitcoin Library // Written in 2020 by // Alekos Filini // // Copyright (c) 2020 Magical Bitcoin // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::collections::BTreeMap; 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::hashes::hex::FromHex; use bitcoin::util::psbt::PartiallySignedTransaction; use bitcoin::{Address, OutPoint, Script, Txid}; use crate::blockchain::log_progress; use crate::error::Error; use crate::types::ScriptType; use crate::{FeeRate, TxBuilder, Wallet}; fn parse_recipient(s: &str) -> Result<(Script, 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().script_pubkey(), val.unwrap())) } fn parse_outpoint(s: &str) -> Result { OutPoint::from_str(s).map_err(|e| format!("{:?}", e)) } fn recipient_validator(s: String) -> Result<(), String> { parse_recipient(&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").arg( Arg::with_name("max_addresses") .required(false) .takes_value(true) .long("max_addresses") .help("max addresses to consider"), )) .subcommand( SubCommand::with_name("list_unspent").about("Lists the available spendable UTXOs"), ) .subcommand( SubCommand::with_name("list_transactions").about("Lists all the incoming and outgoing transactions of the wallet"), ) .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 a recipient to the transaction") .takes_value(true) .number_of_values(1) .required(true) .multiple(true) .validator(recipient_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 recipients of value 0"), ) .arg( Arg::with_name("enable_rbf") .short("rbf") .long("enable_rbf") .help("Enables Replace-By-Fee (BIP125)"), ) .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 should be used to satisfy the descriptor") .takes_value(true) .number_of_values(1), ), ) .subcommand( SubCommand::with_name("bump_fee") .about("Bumps the fees of an RBF transaction") .arg( Arg::with_name("txid") .required(true) .takes_value(true) .short("txid") .long("txid") .help("TXID of the transaction to update"), ) .arg( Arg::with_name("send_all") .short("all") .long("send_all") .help("Allows the wallet to reduce the amount of the only output in order to increase fees. This is generally the expected behavior for transactions originally created with `send_all`"), ) .arg( Arg::with_name("utxos") .long("utxos") .value_name("TXID:VOUT") .help("Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used") .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, in case more inputs are needed to cover the extra fees") .takes_value(true) .number_of_values(1) .multiple(true) .validator(outpoint_validator), ) .arg( Arg::with_name("fee_rate") .required(true) .short("fee") .long("fee_rate") .value_name("SATS_VBYTE") .help("The new targeted fee rate in sat/vbyte") .takes_value(true), ), ) .subcommand( SubCommand::with_name("policies") .about("Returns the available spending policies for the descriptor") ) .subcommand( SubCommand::with_name("public_descriptor") .about("Returns the public version of the wallet's descriptor(s)") ) .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("Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract") .arg( Arg::with_name("psbt") .long("psbt") .value_name("BASE64_PSBT") .help("Sets the PSBT to extract and broadcast") .takes_value(true) .required_unless("tx") .number_of_values(1)) .arg( Arg::with_name("tx") .long("tx") .value_name("RAWTX") .help("Sets the raw transaction to broadcast") .takes_value(true) .required_unless("psbt") .number_of_values(1)) ) .subcommand( SubCommand::with_name("extract_psbt") .about("Extracts a raw transaction from a PSBT") .arg( Arg::with_name("psbt") .long("psbt") .value_name("BASE64_PSBT") .help("Sets the PSBT to extract") .takes_value(true) .required(true) .number_of_values(1)) ) .subcommand( SubCommand::with_name("finalize_psbt") .about("Finalizes a psbt") .arg( Arg::with_name("psbt") .long("psbt") .value_name("BASE64_PSBT") .help("Sets the PSBT to finalize") .takes_value(true) .required(true) .number_of_values(1)) .arg( Arg::with_name("assume_height") .long("assume_height") .value_name("HEIGHT") .help("Assume the blockchain has reached a specific height") .takes_value(true) .number_of_values(1) .required(false)) ) .subcommand( SubCommand::with_name("combine_psbt") .about("Combines multiple PSBTs into one") .arg( Arg::with_name("psbt") .long("psbt") .value_name("BASE64_PSBT") .help("Add one PSBT to comine. This option can be repeated multiple times, one for each PSBT") .takes_value(true) .number_of_values(1) .required(true) .multiple(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("ssl://electrum.blockstream.info:60002"), ) .arg( Arg::with_name("proxy") .short("p") .long("proxy") .value_name("SERVER:PORT") .help("Sets the SOCKS5 proxy for the Electrum client") .takes_value(true), ) .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")) } #[maybe_async] pub fn handle_matches( wallet: &Wallet, matches: ArgMatches<'_>, ) -> Result where C: crate::blockchain::Blockchain, D: crate::database::BatchDatabase, { if let Some(_sub_matches) = matches.subcommand_matches("get_new_address") { Ok(json!({ "address": wallet.get_new_address()? })) } else if let Some(sub_matches) = matches.subcommand_matches("sync") { let max_addresses: Option = sub_matches .value_of("max_addresses") .and_then(|m| m.parse().ok()); maybe_await!(wallet.sync(log_progress(), max_addresses))?; Ok(json!({})) } else if let Some(_sub_matches) = matches.subcommand_matches("list_unspent") { Ok(serde_json::to_value(&wallet.list_unspent()?)?) } else if let Some(_sub_matches) = matches.subcommand_matches("list_transactions") { Ok(serde_json::to_value(&wallet.list_transactions(false)?)?) } else if let Some(_sub_matches) = matches.subcommand_matches("get_balance") { Ok(json!({ "satoshi": wallet.get_balance()? })) } else if let Some(sub_matches) = matches.subcommand_matches("create_tx") { let recipients = sub_matches .values_of("to") .unwrap() .map(|s| parse_recipient(s)) .collect::, _>>() .map_err(Error::Generic)?; let mut tx_builder = TxBuilder::with_recipients(recipients); if sub_matches.is_present("send_all") { tx_builder = tx_builder.send_all(); } if sub_matches.is_present("enable_rbf") { tx_builder = tx_builder.enable_rbf(); } if let Some(fee_rate) = sub_matches.value_of("fee_rate") { let fee_rate = f32::from_str(fee_rate).map_err(|s| Error::Generic(s.to_string()))?; tx_builder = tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate)); } if let Some(utxos) = sub_matches.values_of("utxos") { let utxos = utxos .map(|i| parse_outpoint(i)) .collect::, _>>() .map_err(Error::Generic)?; tx_builder = tx_builder.utxos(utxos).manually_selected_only(); } if let Some(unspendable) = sub_matches.values_of("unspendable") { let unspendable = unspendable .map(|i| parse_outpoint(i)) .collect::, _>>() .map_err(Error::Generic)?; tx_builder = tx_builder.unspendable(unspendable); } if let Some(policy) = sub_matches.value_of("policy") { let policy = serde_json::from_str::>>(&policy) .map_err(|s| Error::Generic(s.to_string()))?; tx_builder = tx_builder.policy_path(policy); } let (psbt, details) = wallet.create_tx(tx_builder)?; Ok(json!({ "psbt": base64::encode(&serialize(&psbt)), "details": details, })) } else if let Some(sub_matches) = matches.subcommand_matches("bump_fee") { let txid = Txid::from_str(sub_matches.value_of("txid").unwrap()) .map_err(|s| Error::Generic(s.to_string()))?; let fee_rate = f32::from_str(sub_matches.value_of("fee_rate").unwrap()) .map_err(|s| Error::Generic(s.to_string()))?; let mut tx_builder = TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(fee_rate)); if sub_matches.is_present("send_all") { tx_builder = tx_builder.send_all(); } if let Some(utxos) = sub_matches.values_of("utxos") { let utxos = utxos .map(|i| parse_outpoint(i)) .collect::, _>>() .map_err(Error::Generic)?; tx_builder = tx_builder.utxos(utxos); } if let Some(unspendable) = sub_matches.values_of("unspendable") { let unspendable = unspendable .map(|i| parse_outpoint(i)) .collect::, _>>() .map_err(Error::Generic)?; tx_builder = tx_builder.unspendable(unspendable); } let (psbt, details) = wallet.bump_fee(&txid, tx_builder)?; Ok(json!({ "psbt": base64::encode(&serialize(&psbt)), "details": details, })) } else if let Some(_sub_matches) = matches.subcommand_matches("policies") { Ok(json!({ "external": wallet.policies(ScriptType::External)?, "internal": wallet.policies(ScriptType::Internal)?, })) } else if let Some(_sub_matches) = matches.subcommand_matches("public_descriptor") { Ok(json!({ "external": wallet.public_descriptor(ScriptType::External)?.map(|d| d.to_string()), "internal": wallet.public_descriptor(ScriptType::Internal)?.map(|d| d.to_string()), })) } 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") .map(|s| s.parse().unwrap()); let (psbt, finalized) = wallet.sign(psbt, assume_height)?; Ok(json!({ "psbt": base64::encode(&serialize(&psbt)), "is_finalized": finalized, })) } else if let Some(sub_matches) = matches.subcommand_matches("broadcast") { let tx = if sub_matches.value_of("psbt").is_some() { let psbt = base64::decode(&sub_matches.value_of("psbt").unwrap()).unwrap(); let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); psbt.extract_tx() } else if sub_matches.value_of("tx").is_some() { deserialize(&Vec::::from_hex(&sub_matches.value_of("tx").unwrap()).unwrap()) .unwrap() } else { panic!("Missing `psbt` and `tx` option"); }; let txid = maybe_await!(wallet.broadcast(tx))?; Ok(json!({ "txid": txid })) } else if let Some(sub_matches) = matches.subcommand_matches("extract_psbt") { let psbt = base64::decode(&sub_matches.value_of("psbt").unwrap()).unwrap(); let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); Ok(json!({ "raw_tx": serialize_hex(&psbt.extract_tx()), })) } else if let Some(sub_matches) = matches.subcommand_matches("finalize_psbt") { 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") .map(|s| s.parse().unwrap()); let (psbt, finalized) = wallet.finalize_psbt(psbt, assume_height)?; Ok(json!({ "psbt": base64::encode(&serialize(&psbt)), "is_finalized": finalized, })) } else if let Some(sub_matches) = matches.subcommand_matches("combine_psbt") { let mut psbts = sub_matches .values_of("psbt") .unwrap() .map(|s| { let psbt = base64::decode(&s).unwrap(); let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); psbt }) .collect::>(); let init_psbt = psbts.pop().unwrap(); let final_psbt = psbts .into_iter() .try_fold::<_, _, Result>( init_psbt, |mut acc, x| { acc.merge(x)?; Ok(acc) }, )?; Ok(json!({ "psbt": base64::encode(&serialize(&final_psbt)) })) } else { Ok(serde_json::Value::Null) } }