From 1a4e1bd96cc7ba4a46a4222ac3e0d542277f8ceb Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Fri, 7 Feb 2020 23:22:28 +0100 Subject: [PATCH] Wallet logic --- .travis.yml | 5 +- Cargo.toml | 11 +- examples/parse_descriptor.rs | 9 +- examples/repl.rs | 358 ++++++++++++ src/database/keyvalue.rs | 16 +- src/database/mod.rs | 8 +- src/descriptor/mod.rs | 19 +- src/descriptor/policy.rs | 399 +++++++++++++ src/error.rs | 35 ++ src/lib.rs | 10 +- src/psbt.rs | 78 ++- src/types.rs | 2 +- src/wallet/mod.rs | 1039 ++++++++++++++++++++++++++++++++++ src/wallet/offline_stream.rs | 52 ++ src/wallet/utils.rs | 48 ++ 15 files changed, 2057 insertions(+), 32 deletions(-) create mode 100644 examples/repl.rs create mode 100644 src/descriptor/policy.rs create mode 100644 src/wallet/mod.rs create mode 100644 src/wallet/offline_stream.rs create mode 100644 src/wallet/utils.rs diff --git a/.travis.yml b/.travis.yml index 28ec6fc8..d5f5e3cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,11 @@ before_script: - rustup component add rustfmt script: - cargo fmt -- --check --verbose - - cargo build --verbose --all - cargo test --verbose --all + - cargo build --verbose --all + - cargo build --verbose --no-default-features --features=minimal + - cargo build --verbose --no-default-features --features=key-value-db + - cargo build --verbose --no-default-features --features=electrum notifications: email: false diff --git a/Cargo.toml b/Cargo.toml index 81e268f9..f65ba76f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,17 @@ base64 = "^0.11" # Optional dependencies sled = { version = "0.31.0", optional = true } +electrum-client = { version = "0.1.0-beta.1", optional = true } [features] -default = ["sled"] +minimal = [] +default = ["sled", "electrum-client"] +electrum = ["electrum-client"] key-value-db = ["sled"] [dev-dependencies] -lazy_static = "1.4.0" +lazy_static = "1.4" +rustyline = "5.0" # newer version requires 2018 edition +clap = "2.33" +dirs = "2.0" +env_logger = "0.7" diff --git a/examples/parse_descriptor.rs b/examples/parse_descriptor.rs index 2af42b3a..63c16b4c 100644 --- a/examples/parse_descriptor.rs +++ b/examples/parse_descriptor.rs @@ -1,4 +1,5 @@ extern crate magical_bitcoin_wallet; +extern crate serde_json; use std::str::FromStr; @@ -6,12 +7,12 @@ use magical_bitcoin_wallet::bitcoin::*; use magical_bitcoin_wallet::descriptor::*; fn main() { - let desc = "sh(wsh(or_d(\ + let desc = "wsh(or_d(\ thresh_m(\ 2,[d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*,tprv8ZgxMBicQKsPduL5QnGihpprdHyypMGi4DhimjtzYemu7se5YQNcZfAPLqXRuGHb5ZX2eTQj62oNqMnyxJ7B7wz54Uzswqw8fFqMVdcmVF7/1/*\ ),\ and_v(vc:pk_h(cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy),older(1000))\ - )))"; + ))"; let extended_desc = ExtendedDescriptor::from_str(desc).unwrap(); println!("{:?}", extended_desc); @@ -19,6 +20,10 @@ fn main() { let derived_desc = extended_desc.derive(42).unwrap(); println!("{:?}", derived_desc); + if let Descriptor::Wsh(x) = &derived_desc { + println!("{}", serde_json::to_string(&x.extract_policy()).unwrap()); + } + let addr = derived_desc.address(Network::Testnet).unwrap(); println!("{}", addr); diff --git a/examples/repl.rs b/examples/repl.rs new file mode 100644 index 00000000..7ee163b2 --- /dev/null +++ b/examples/repl.rs @@ -0,0 +1,358 @@ +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 clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; + +use rustyline::error::ReadlineError; +use rustyline::Editor; + +#[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 magical_bitcoin_wallet::bitcoin; +use magical_bitcoin_wallet::sled; +use magical_bitcoin_wallet::types::ScriptType; +use magical_bitcoin_wallet::{Client, ExtendedDescriptor, Wallet}; + +fn prepare_home_dir() -> PathBuf { + let mut dir = PathBuf::new(); + dir.push(&dirs::home_dir().unwrap()); + dir.push(".magical-bitcoin"); + + if !dir.exists() { + info!("Creating home directory {}", dir.as_path().display()); + fs::create_dir(&dir).unwrap(); + } + + dir.push("database.sled"); + 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(|_| ()) +} + +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), + )); + + 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 matches = app.get_matches(); + + // TODO + // let level = match matches.occurrences_of("v") { + // 0 => LevelFilter::Info, + // 1 => LevelFilter::Debug, + // _ => LevelFilter::Trace, + // }; + + let network = match matches.value_of("network") { + Some("regtest") => Network::Regtest, + Some("testnet") | _ => Network::Testnet, + }; + + let descriptor = matches + .value_of("descriptor") + .map(|x| ExtendedDescriptor::from_str(x).unwrap()) + .unwrap(); + let change_descriptor = matches + .value_of("change_descriptor") + .map(|x| ExtendedDescriptor::from_str(x).unwrap()); + debug!("descriptors: {:?} {:?}", descriptor, change_descriptor); + + let database = sled::open(prepare_home_dir().to_str().unwrap()).unwrap(); + let tree = database + .open_tree(matches.value_of("wallet").unwrap()) + .unwrap(); + debug!("database opened successfully"); + + let client = Client::new(matches.value_of("server").unwrap()).unwrap(); + let wallet = Wallet::new(descriptor, change_descriptor, network, tree, client); + + // TODO: print errors in a nice way + let handle_matches = |matches: ArgMatches<'_>| { + 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).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 (psbt, finalized) = wallet.sign(psbt).unwrap(); + + println!("Finalized: {}", finalized); + if finalized { + println!("Extracted: {}", serialize_hex(&psbt.extract_tx())); + } else { + println!("PSBT: {}", base64::encode(&serialize(&psbt))); + } + } + }; + + if let Some(_sub_matches) = matches.subcommand_matches("repl") { + let mut rl = Editor::<()>::new(); + + // if rl.load_history("history.txt").is_err() { + // println!("No previous history."); + // } + + loop { + let readline = rl.readline(">> "); + match readline { + Ok(line) => { + if line.trim() == "" { + continue; + } + + rl.add_history_entry(line.as_str()); + let matches = repl_app.get_matches_from_safe_borrow(line.split(" ")); + if let Err(err) = matches { + println!("{}", err.message); + continue; + } + + handle_matches(matches.unwrap()); + } + Err(ReadlineError::Interrupted) => continue, + Err(ReadlineError::Eof) => break, + Err(err) => { + println!("{:?}", err); + break; + } + } + } + + // rl.save_history("history.txt").unwrap(); + } else { + handle_matches(matches); + } +} diff --git a/src/database/keyvalue.rs b/src/database/keyvalue.rs index d34ac9f7..a96577dd 100644 --- a/src/database/keyvalue.rs +++ b/src/database/keyvalue.rs @@ -235,7 +235,7 @@ impl BatchOperations for Batch { } impl Database for Tree { - fn iter_script_pubkeys(&self, script_type: Option) -> Vec> { + fn iter_script_pubkeys(&self, script_type: Option) -> Result, Error> { let key = SledKey::Path((script_type, None)).as_sled_key(); self.scan_prefix(key) .map(|x| -> Result<_, Error> { @@ -245,7 +245,7 @@ impl Database for Tree { .collect() } - fn iter_utxos(&self) -> Vec> { + fn iter_utxos(&self) -> Result, Error> { let key = SledKey::UTXO(None).as_sled_key(); self.scan_prefix(key) .map(|x| -> Result<_, Error> { @@ -257,7 +257,7 @@ impl Database for Tree { .collect() } - fn iter_raw_txs(&self) -> Vec> { + fn iter_raw_txs(&self) -> Result, Error> { let key = SledKey::RawTx(None).as_sled_key(); self.scan_prefix(key) .map(|x| -> Result<_, Error> { @@ -267,8 +267,8 @@ impl Database for Tree { .collect() } - fn iter_txs(&self, include_raw: bool) -> Vec> { - let key = SledKey::RawTx(None).as_sled_key(); + fn iter_txs(&self, include_raw: bool) -> Result, Error> { + let key = SledKey::Transaction(None).as_sled_key(); self.scan_prefix(key) .map(|x| -> Result<_, Error> { let (k, v) = x?; @@ -516,7 +516,7 @@ mod test { tree.set_script_pubkey(&script, script_type, &path).unwrap(); - assert_eq!(tree.iter_script_pubkeys(None).len(), 1); + assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1); } #[test] @@ -530,11 +530,11 @@ mod test { let script_type = ScriptType::External; tree.set_script_pubkey(&script, script_type, &path).unwrap(); - assert_eq!(tree.iter_script_pubkeys(None).len(), 1); + assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 1); tree.del_script_pubkey_from_path(script_type, &path) .unwrap(); - assert_eq!(tree.iter_script_pubkeys(None).len(), 0); + assert_eq!(tree.iter_script_pubkeys(None).unwrap().len(), 0); } #[test] diff --git a/src/database/mod.rs b/src/database/mod.rs index 0e28b78b..52dc83b8 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -40,10 +40,10 @@ pub trait BatchOperations { } pub trait Database: BatchOperations { - fn iter_script_pubkeys(&self, script_type: Option) -> Vec>; - fn iter_utxos(&self) -> Vec>; - fn iter_raw_txs(&self) -> Vec>; - fn iter_txs(&self, include_raw: bool) -> Vec>; + fn iter_script_pubkeys(&self, script_type: Option) -> Result, Error>; + fn iter_utxos(&self) -> Result, Error>; + fn iter_raw_txs(&self) -> Result, Error>; + fn iter_txs(&self, include_raw: bool) -> Result, Error>; fn get_script_pubkey_from_path>( &self, diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 01c6a0a8..72fe5982 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -16,9 +16,15 @@ use serde::{Deserialize, Serialize}; pub mod error; pub mod extended_key; +pub mod policy; pub use self::error::Error; pub use self::extended_key::{DerivationIndex, DescriptorExtendedKey}; +pub use self::policy::{ExtractPolicy, Policy}; + +trait MiniscriptExtractPolicy { + fn extract_policy(&self, lookup_map: &BTreeMap>) -> Option; +} #[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Default)] struct DummyKey(); @@ -86,6 +92,7 @@ where fn psbt_witness_script(&self) -> Option