From 499e579824ce24818949875e222e30bb08b59cae Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Thu, 6 Aug 2020 13:09:39 +0200 Subject: [PATCH] [wallet] Add a `TxBuilder` struct to simplify `create_tx()`'s interface --- src/cli.rs | 54 +++++++++++++++++------------- src/error.rs | 3 +- src/lib.rs | 2 +- src/wallet/mod.rs | 43 +++++++++++------------- src/wallet/tx_builder.rs | 71 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 48 deletions(-) create mode 100644 src/wallet/tx_builder.rs diff --git a/src/cli.rs b/src/cli.rs index 788aa69d..c946f22e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -13,7 +13,7 @@ use bitcoin::{Address, OutPoint}; use crate::error::Error; use crate::types::ScriptType; -use crate::Wallet; +use crate::{TxBuilder, Wallet}; fn parse_addressee(s: &str) -> Result<(Address, u64), String> { let parts: Vec<_> = s.split(":").collect(); @@ -326,29 +326,37 @@ where .map(|s| parse_addressee(s)) .collect::, _>>() .map_err(|s| Error::Generic(s))?; - 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 mut tx_builder = TxBuilder::from_addressees(addressees); - let result = wallet.create_tx( - addressees, - send_all, - fee_rate * 1e-5, - policy, - utxos, - unspendable, - )?; + if sub_matches.is_present("send_all") { + tx_builder.send_all(); + } + 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.fee_rate(fee_rate); + } + if let Some(utxos) = sub_matches.values_of("utxos") { + let utxos = utxos + .map(|i| parse_outpoint(i)) + .collect::, _>>() + .map_err(|s| Error::Generic(s.to_string()))?; + tx_builder.utxos(utxos); + } + + if let Some(unspendable) = sub_matches.values_of("unspendable") { + let unspendable = unspendable + .map(|i| parse_outpoint(i)) + .collect::, _>>() + .map_err(|s| Error::Generic(s.to_string()))?; + 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.policy_path(policy); + } + + let result = wallet.create_tx(&tx_builder)?; Ok(Some(format!( "{:#?}\nPSBT: {}", result.1, diff --git a/src/error.rs b/src/error.rs index 307c2b45..72dfe944 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use bitcoin::{OutPoint, Script, Txid}; +use bitcoin::{Address, OutPoint, Script, Txid}; #[derive(Debug)] pub enum Error { @@ -10,6 +10,7 @@ pub enum Error { SendAllMultipleOutputs, OutputBelowDustLimit(usize), InsufficientFunds, + InvalidAddressNetork(Address), UnknownUTXO, DifferentTransactions, diff --git a/src/lib.rs b/src/lib.rs index 10e659b4..0d5ce881 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,4 +42,4 @@ pub mod types; pub mod wallet; pub use descriptor::ExtendedDescriptor; -pub use wallet::{OfflineWallet, Wallet}; +pub use wallet::{OfflineWallet, TxBuilder, Wallet}; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 8ad197f5..55aa9fbf 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -17,8 +17,11 @@ use miniscript::BitcoinSig; use log::{debug, error, info, trace}; pub mod time; +pub mod tx_builder; pub mod utils; +pub use tx_builder::TxBuilder; + use self::utils::IsDust; use crate::blockchain::{noop_progress, Blockchain, OfflineBlockchain, OnlineBlockchain}; @@ -121,20 +124,13 @@ where } // TODO: add a flag to ignore change in coin selection - pub fn create_tx( - &self, - addressees: Vec<(Address, u64)>, - send_all: bool, - fee_perkb: f32, - policy_path: Option>>, - utxos: Option>, - unspendable: Option>, - ) -> Result<(PSBT, TransactionDetails), Error> { + pub fn create_tx(&self, builder: &TxBuilder) -> Result<(PSBT, TransactionDetails), Error> { let policy = self.descriptor.extract_policy()?.unwrap(); - if policy.requires_path() && policy_path.is_none() { + if policy.requires_path() && builder.policy_path.is_none() { return Err(Error::SpendingPolicyRequired); } - let requirements = policy.get_requirements(&policy_path.unwrap_or(BTreeMap::new()))?; + let requirements = + policy.get_requirements(builder.policy_path.as_ref().unwrap_or(&BTreeMap::new()))?; debug!("requirements: {:?}", requirements); let mut tx = Transaction { @@ -144,8 +140,8 @@ where output: vec![], }; - let fee_rate = fee_perkb * 100_000.0; - if send_all && addressees.len() != 1 { + let fee_rate = builder.fee_perkb.unwrap_or(1e3) * 100_000.0; + if builder.send_all && builder.addressees.len() != 1 { return Err(Error::SendAllMultipleOutputs); } @@ -157,15 +153,16 @@ where let calc_fee_bytes = |wu| (wu as f32) * fee_rate / 4.0; fee_val += calc_fee_bytes(tx.get_weight()); - for (index, (address, satoshi)) in addressees.iter().enumerate() { - let value = match send_all { + for (index, (address, satoshi)) in builder.addressees.iter().enumerate() { + let value = match builder.send_all { true => 0, false if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)), false => *satoshi, }; - // TODO: check address network - if self.is_mine(&address.script_pubkey())? { + if address.network != self.network { + return Err(Error::InvalidAddressNetork(address.clone())); + } else if self.is_mine(&address.script_pubkey())? { received += value; } @@ -184,7 +181,7 @@ where let input_witness_weight = self.descriptor.max_satisfaction_weight(); let (available_utxos, use_all_utxos) = - self.get_available_utxos(&utxos, &unspendable, send_all)?; + self.get_available_utxos(&builder.utxos, &builder.unspendable, builder.send_all)?; let (mut inputs, paths, selected_amount, mut fee_val) = self.coin_select( available_utxos, use_all_utxos, @@ -204,7 +201,7 @@ where tx.input.append(&mut inputs); // prepare the change output - let change_output = match send_all { + let change_output = match builder.send_all { true => None, false => { let change_script = self.get_change_address()?; @@ -220,13 +217,13 @@ where }; let change_val = selected_amount - outgoing - (fee_val.ceil() as u64); - if !send_all && !change_val.is_dust() { + if !builder.send_all && !change_val.is_dust() { let mut change_output = change_output.unwrap(); change_output.value = change_val; received += change_val; tx.output.push(change_output); - } else if send_all && !change_val.is_dust() { + } else if builder.send_all && !change_val.is_dust() { // set the outgoing value to whatever we've put in outgoing = selected_amount; // there's only one output, send everything to it @@ -236,7 +233,7 @@ where if self.is_mine(&tx.output[0].script_pubkey)? { received = change_val; } - } else if send_all { + } else if builder.send_all { // send_all but the only output would be below dust limit return Err(Error::InsufficientFunds); // TODO: or OutputBelowDustLimit? } @@ -295,7 +292,7 @@ where let transaction_details = TransactionDetails { transaction: None, - txid: txid, + txid, timestamp: time::get_timestamp(), received, sent: outgoing, diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs new file mode 100644 index 00000000..964b69ca --- /dev/null +++ b/src/wallet/tx_builder.rs @@ -0,0 +1,71 @@ +use std::collections::BTreeMap; + +use bitcoin::{Address, OutPoint}; + +#[derive(Debug, Default)] +pub struct TxBuilder { + pub(crate) addressees: Vec<(Address, u64)>, + pub(crate) send_all: bool, + pub(crate) fee_perkb: Option, + pub(crate) policy_path: Option>>, + pub(crate) utxos: Option>, + pub(crate) unspendable: Option>, +} + +impl TxBuilder { + pub fn new() -> TxBuilder { + TxBuilder::default() + } + + pub fn from_addressees(addressees: Vec<(Address, u64)>) -> TxBuilder { + let mut tx_builder = TxBuilder::default(); + tx_builder.addressees = addressees; + + tx_builder + } + + pub fn add_addressee(&mut self, address: Address, amount: u64) -> &mut TxBuilder { + self.addressees.push((address, amount)); + self + } + + pub fn send_all(&mut self) -> &mut TxBuilder { + self.send_all = true; + self + } + + pub fn fee_rate(&mut self, satoshi_per_vbyte: f32) -> &mut TxBuilder { + self.fee_perkb = Some(satoshi_per_vbyte * 1e3); + self + } + + pub fn fee_rate_perkb(&mut self, satoshi_per_kb: f32) -> &mut TxBuilder { + self.fee_perkb = Some(satoshi_per_kb); + self + } + + pub fn policy_path(&mut self, policy_path: BTreeMap>) -> &mut TxBuilder { + self.policy_path = Some(policy_path); + self + } + + pub fn utxos(&mut self, utxos: Vec) -> &mut TxBuilder { + self.utxos = Some(utxos); + self + } + + pub fn add_utxo(&mut self, utxo: OutPoint) -> &mut TxBuilder { + self.utxos.get_or_insert(vec![]).push(utxo); + self + } + + pub fn unspendable(&mut self, unspendable: Vec) -> &mut TxBuilder { + self.unspendable = Some(unspendable); + self + } + + pub fn add_unspendable(&mut self, unspendable: OutPoint) -> &mut TxBuilder { + self.unspendable.get_or_insert(vec![]).push(unspendable); + self + } +}