]> Untitled Git - bdk/commitdiff
[wallet] Add a `TxBuilder` struct to simplify `create_tx()`'s interface
authorAlekos Filini <alekos.filini@gmail.com>
Thu, 6 Aug 2020 11:09:39 +0000 (13:09 +0200)
committerAlekos Filini <alekos.filini@gmail.com>
Thu, 6 Aug 2020 12:28:22 +0000 (14:28 +0200)
src/cli.rs
src/error.rs
src/lib.rs
src/wallet/mod.rs
src/wallet/tx_builder.rs [new file with mode: 0644]

index 788aa69d911eb36d33b3bc90cfe844ff3816b8ac..c946f22e50e0ddc7ce905ab8a14ed82b48e9480c 100644 (file)
@@ -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::<Result<Vec<_>, _>>()
             .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::<BTreeMap<String, Vec<usize>>>(&s).unwrap());
-
-        let result = wallet.create_tx(
-            addressees,
-            send_all,
-            fee_rate * 1e-5,
-            policy,
-            utxos,
-            unspendable,
-        )?;
+        let mut tx_builder = TxBuilder::from_addressees(addressees);
+
+        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::<Result<Vec<_>, _>>()
+                .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::<Result<Vec<_>, _>>()
+                .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::<BTreeMap<String, Vec<usize>>>(&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,
index 307c2b4572bd17f2e56d2863a0fc8f3633dfdea5..72dfe944b98314247c34a9cef6d6dbae35a33e78 100644 (file)
@@ -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,
 
index 10e659b4a5ceb590e333c55ee418d19271386e48..0d5ce881cf7a996b3188ebf4e269a269b51d2c13 100644 (file)
@@ -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};
index 8ad197f5ba99f210e7486c6e0ada041500313168..55aa9fbf6772db84d7108dd921885da5a644cf4b 100644 (file)
@@ -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<BTreeMap<String, Vec<usize>>>,
-        utxos: Option<Vec<OutPoint>>,
-        unspendable: Option<Vec<OutPoint>>,
-    ) -> 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 (file)
index 0000000..964b69c
--- /dev/null
@@ -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<f32>,
+    pub(crate) policy_path: Option<BTreeMap<String, Vec<usize>>>,
+    pub(crate) utxos: Option<Vec<OutPoint>>,
+    pub(crate) unspendable: Option<Vec<OutPoint>>,
+}
+
+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<String, Vec<usize>>) -> &mut TxBuilder {
+        self.policy_path = Some(policy_path);
+        self
+    }
+
+    pub fn utxos(&mut self, utxos: Vec<OutPoint>) -> &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<OutPoint>) -> &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
+    }
+}