[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::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 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::<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,

View 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,

View 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};

View 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,

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