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();
.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,
-use bitcoin::{OutPoint, Script, Txid};
+use bitcoin::{Address, OutPoint, Script, Txid};
#[derive(Debug)]
pub enum Error {
SendAllMultipleOutputs,
OutputBelowDustLimit(usize),
InsufficientFunds,
+ InvalidAddressNetork(Address),
UnknownUTXO,
DifferentTransactions,
pub mod wallet;
pub use descriptor::ExtendedDescriptor;
-pub use wallet::{OfflineWallet, Wallet};
+pub use wallet::{OfflineWallet, TxBuilder, Wallet};
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};
}
// 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 {
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);
}
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;
}
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,
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()?;
};
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
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?
}
let transaction_details = TransactionDetails {
transaction: None,
- txid: txid,
+ txid,
timestamp: time::get_timestamp(),
received,
sent: outgoing,
--- /dev/null
+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
+ }
+}