[wallet] Add a TxBuilder
struct to simplify create_tx()
's interface
This commit is contained in:
parent
927c2f37b9
commit
499e579824
54
src/cli.rs
54
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::<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,
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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};
|
||||
|
@ -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
71
src/wallet/tx_builder.rs
Normal 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
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user