[wallet] Add a TxBuilder struct to simplify create_tx()'s interface

This commit is contained in:
Alekos Filini 2020-08-06 13:09:39 +02:00
parent 927c2f37b9
commit 499e579824
No known key found for this signature in database
GPG Key ID: 5E8AFC3034FDFA4F
5 changed files with 125 additions and 48 deletions

View File

@ -13,7 +13,7 @@ use bitcoin::{Address, OutPoint};
use crate::error::Error; use crate::error::Error;
use crate::types::ScriptType; use crate::types::ScriptType;
use crate::Wallet; use crate::{TxBuilder, Wallet};
fn parse_addressee(s: &str) -> Result<(Address, u64), String> { fn parse_addressee(s: &str) -> Result<(Address, u64), String> {
let parts: Vec<_> = s.split(":").collect(); let parts: Vec<_> = s.split(":").collect();
@ -326,29 +326,37 @@ where
.map(|s| parse_addressee(s)) .map(|s| parse_addressee(s))
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map_err(|s| Error::Generic(s))?; .map_err(|s| Error::Generic(s))?;
let send_all = sub_matches.is_present("send_all"); let mut tx_builder = TxBuilder::from_addressees(addressees);
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( if sub_matches.is_present("send_all") {
addressees, tx_builder.send_all();
send_all, }
fee_rate * 1e-5, if let Some(fee_rate) = sub_matches.value_of("fee_rate") {
policy, let fee_rate = f32::from_str(fee_rate).map_err(|s| Error::Generic(s.to_string()))?;
utxos, tx_builder.fee_rate(fee_rate);
unspendable, }
)?; 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!( Ok(Some(format!(
"{:#?}\nPSBT: {}", "{:#?}\nPSBT: {}",
result.1, result.1,

View File

@ -1,4 +1,4 @@
use bitcoin::{OutPoint, Script, Txid}; use bitcoin::{Address, OutPoint, Script, Txid};
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
@ -10,6 +10,7 @@ pub enum Error {
SendAllMultipleOutputs, SendAllMultipleOutputs,
OutputBelowDustLimit(usize), OutputBelowDustLimit(usize),
InsufficientFunds, InsufficientFunds,
InvalidAddressNetork(Address),
UnknownUTXO, UnknownUTXO,
DifferentTransactions, DifferentTransactions,

View File

@ -42,4 +42,4 @@ pub mod types;
pub mod wallet; pub mod wallet;
pub use descriptor::ExtendedDescriptor; pub use descriptor::ExtendedDescriptor;
pub use wallet::{OfflineWallet, Wallet}; pub use wallet::{OfflineWallet, TxBuilder, Wallet};

View File

@ -17,8 +17,11 @@ use miniscript::BitcoinSig;
use log::{debug, error, info, trace}; use log::{debug, error, info, trace};
pub mod time; pub mod time;
pub mod tx_builder;
pub mod utils; pub mod utils;
pub use tx_builder::TxBuilder;
use self::utils::IsDust; use self::utils::IsDust;
use crate::blockchain::{noop_progress, Blockchain, OfflineBlockchain, OnlineBlockchain}; use crate::blockchain::{noop_progress, Blockchain, OfflineBlockchain, OnlineBlockchain};
@ -121,20 +124,13 @@ where
} }
// TODO: add a flag to ignore change in coin selection // TODO: add a flag to ignore change in coin selection
pub fn create_tx( pub fn create_tx(&self, builder: &TxBuilder) -> Result<(PSBT, TransactionDetails), Error> {
&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> {
let policy = self.descriptor.extract_policy()?.unwrap(); 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); 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); debug!("requirements: {:?}", requirements);
let mut tx = Transaction { let mut tx = Transaction {
@ -144,8 +140,8 @@ where
output: vec![], output: vec![],
}; };
let fee_rate = fee_perkb * 100_000.0; let fee_rate = builder.fee_perkb.unwrap_or(1e3) * 100_000.0;
if send_all && addressees.len() != 1 { if builder.send_all && builder.addressees.len() != 1 {
return Err(Error::SendAllMultipleOutputs); return Err(Error::SendAllMultipleOutputs);
} }
@ -157,15 +153,16 @@ where
let calc_fee_bytes = |wu| (wu as f32) * fee_rate / 4.0; let calc_fee_bytes = |wu| (wu as f32) * fee_rate / 4.0;
fee_val += calc_fee_bytes(tx.get_weight()); fee_val += calc_fee_bytes(tx.get_weight());
for (index, (address, satoshi)) in addressees.iter().enumerate() { for (index, (address, satoshi)) in builder.addressees.iter().enumerate() {
let value = match send_all { let value = match builder.send_all {
true => 0, true => 0,
false if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)), false if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)),
false => *satoshi, false => *satoshi,
}; };
// TODO: check address network if address.network != self.network {
if self.is_mine(&address.script_pubkey())? { return Err(Error::InvalidAddressNetork(address.clone()));
} else if self.is_mine(&address.script_pubkey())? {
received += value; received += value;
} }
@ -184,7 +181,7 @@ where
let input_witness_weight = self.descriptor.max_satisfaction_weight(); let input_witness_weight = self.descriptor.max_satisfaction_weight();
let (available_utxos, use_all_utxos) = 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( let (mut inputs, paths, selected_amount, mut fee_val) = self.coin_select(
available_utxos, available_utxos,
use_all_utxos, use_all_utxos,
@ -204,7 +201,7 @@ where
tx.input.append(&mut inputs); tx.input.append(&mut inputs);
// prepare the change output // prepare the change output
let change_output = match send_all { let change_output = match builder.send_all {
true => None, true => None,
false => { false => {
let change_script = self.get_change_address()?; let change_script = self.get_change_address()?;
@ -220,13 +217,13 @@ where
}; };
let change_val = selected_amount - outgoing - (fee_val.ceil() as u64); 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(); let mut change_output = change_output.unwrap();
change_output.value = change_val; change_output.value = change_val;
received += change_val; received += change_val;
tx.output.push(change_output); 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 // set the outgoing value to whatever we've put in
outgoing = selected_amount; outgoing = selected_amount;
// there's only one output, send everything to it // there's only one output, send everything to it
@ -236,7 +233,7 @@ where
if self.is_mine(&tx.output[0].script_pubkey)? { if self.is_mine(&tx.output[0].script_pubkey)? {
received = change_val; received = change_val;
} }
} else if send_all { } else if builder.send_all {
// send_all but the only output would be below dust limit // send_all but the only output would be below dust limit
return Err(Error::InsufficientFunds); // TODO: or OutputBelowDustLimit? return Err(Error::InsufficientFunds); // TODO: or OutputBelowDustLimit?
} }
@ -295,7 +292,7 @@ where
let transaction_details = TransactionDetails { let transaction_details = TransactionDetails {
transaction: None, transaction: None,
txid: txid, txid,
timestamp: time::get_timestamp(), timestamp: time::get_timestamp(),
received, received,
sent: outgoing, sent: outgoing,

71
src/wallet/tx_builder.rs Normal file
View File

@ -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
}
}