From eee75219e0e9a66b9639e258f001f3ef5f1c739f Mon Sep 17 00:00:00 2001 From: Alekos Filini Date: Fri, 4 Sep 2020 15:45:11 +0200 Subject: [PATCH] Write more docs, make `TxBuilder::with_recipients` take Scripts --- examples/address_validator.rs | 2 +- examples/compiler.rs | 3 +- examples/repl.rs | 28 +++--- src/cli.rs | 6 +- src/descriptor/policy.rs | 12 ++- src/wallet/address_validator.rs | 5 +- src/wallet/coin_selection.rs | 2 +- src/wallet/export.rs | 63 ++++++++++++ src/wallet/mod.rs | 165 +++++++++++++++++++++----------- src/wallet/signer.rs | 88 ++++++++++++++++- src/wallet/time.rs | 10 ++ src/wallet/tx_builder.rs | 121 +++++++++++++++++++++-- testutils-macros/src/lib.rs | 14 +-- 13 files changed, 419 insertions(+), 100 deletions(-) diff --git a/examples/address_validator.rs b/examples/address_validator.rs index 639bbdfd..3e576c08 100644 --- a/examples/address_validator.rs +++ b/examples/address_validator.rs @@ -57,7 +57,7 @@ impl AddressValidator for DummyValidator { } } -fn main() -> Result<(), magical_bitcoin_wallet::error::Error> { +fn main() -> Result<(), magical_bitcoin_wallet::Error> { let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))"; let mut wallet: OfflineWallet<_> = Wallet::new_offline(descriptor, None, Network::Regtest, MemoryDatabase::new())?; diff --git a/examples/compiler.rs b/examples/compiler.rs index f8724cd8..2d50830c 100644 --- a/examples/compiler.rs +++ b/examples/compiler.rs @@ -40,8 +40,7 @@ use miniscript::policy::Concrete; use miniscript::Descriptor; use magical_bitcoin_wallet::database::memory::MemoryDatabase; -use magical_bitcoin_wallet::types::ScriptType; -use magical_bitcoin_wallet::{OfflineWallet, Wallet}; +use magical_bitcoin_wallet::{OfflineWallet, Wallet, ScriptType}; fn main() { env_logger::init_from_env( diff --git a/examples/repl.rs b/examples/repl.rs index 8e8b38e0..2756d2d4 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -37,10 +37,10 @@ use log::{debug, error, info, trace, LevelFilter}; use bitcoin::Network; use magical_bitcoin_wallet::bitcoin; -use magical_bitcoin_wallet::blockchain::ElectrumBlockchain; +use magical_bitcoin_wallet::blockchain::compact_filters::*; use magical_bitcoin_wallet::cli; use magical_bitcoin_wallet::sled; -use magical_bitcoin_wallet::{Client, Wallet}; +use magical_bitcoin_wallet::Wallet; fn prepare_home_dir() -> PathBuf { let mut dir = PathBuf::new(); @@ -88,19 +88,17 @@ fn main() { .unwrap(); debug!("database opened successfully"); - let client = Client::new( - matches.value_of("server").unwrap(), - matches.value_of("proxy"), - ) - .unwrap(); - let wallet = Wallet::new( - descriptor, - change_descriptor, - network, - tree, - ElectrumBlockchain::from(client), - ) - .unwrap(); + let num_threads = 1; + + let mempool = Arc::new(Mempool::default()); + let peers = (0..num_threads) + .map(|_| Peer::connect("192.168.1.136:8333", Arc::clone(&mempool), Network::Bitcoin)) + .collect::>() + .unwrap(); + let blockchain = + CompactFiltersBlockchain::new(peers, "./wallet-filters", Some(500_000)).unwrap(); + + let wallet = Wallet::new(descriptor, change_descriptor, network, tree, blockchain).unwrap(); let wallet = Arc::new(wallet); if let Some(_sub_matches) = matches.subcommand_matches("repl") { diff --git a/src/cli.rs b/src/cli.rs index 51c56ef9..e09fedd4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -33,14 +33,14 @@ use log::{debug, error, info, trace, LevelFilter}; use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex}; use bitcoin::hashes::hex::FromHex; use bitcoin::util::psbt::PartiallySignedTransaction; -use bitcoin::{Address, OutPoint, Txid}; +use bitcoin::{Address, OutPoint, Script, Txid}; use crate::blockchain::log_progress; use crate::error::Error; use crate::types::ScriptType; use crate::{FeeRate, TxBuilder, Wallet}; -fn parse_recipient(s: &str) -> Result<(Address, u64), String> { +fn parse_recipient(s: &str) -> Result<(Script, u64), String> { let parts: Vec<_> = s.split(":").collect(); if parts.len() != 2 { return Err("Invalid format".to_string()); @@ -55,7 +55,7 @@ fn parse_recipient(s: &str) -> Result<(Address, u64), String> { return Err(format!("{:?}", e)); } - Ok((addr.unwrap(), val.unwrap())) + Ok((addr.unwrap().script_pubkey(), val.unwrap())) } fn parse_outpoint(s: &str) -> Result { diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index 7f54e6eb..7158701d 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -403,12 +403,16 @@ impl From for Satisfaction { /// Descriptor spending policy #[derive(Debug, Clone, Serialize)] pub struct Policy { - id: String, + /// Identifier for this policy node + pub id: String, + /// Type of this policy node #[serde(flatten)] - item: SatisfiableItem, - satisfaction: Satisfaction, - contribution: Satisfaction, + pub item: SatisfiableItem, + /// How a much given PSBT already satisfies this polcy node **(currently unused)** + pub satisfaction: Satisfaction, + /// How the wallet's descriptor can satisfy this policy node + pub contribution: Satisfaction, } /// An extra condition that must be satisfied but that is out of control of the user diff --git a/src/wallet/address_validator.rs b/src/wallet/address_validator.rs index 2b0d3ab3..d40343bc 100644 --- a/src/wallet/address_validator.rs +++ b/src/wallet/address_validator.rs @@ -153,7 +153,10 @@ mod test { let addr = testutils!(@external descriptors, 10); wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)])) + .create_tx(TxBuilder::with_recipients(vec![( + addr.script_pubkey(), + 25_000, + )])) .unwrap(); } } diff --git a/src/wallet/coin_selection.rs b/src/wallet/coin_selection.rs index 9505b222..ab294d6c 100644 --- a/src/wallet/coin_selection.rs +++ b/src/wallet/coin_selection.rs @@ -93,7 +93,7 @@ //! //! let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); //! let (psbt, details) = wallet.create_tx( -//! TxBuilder::with_recipients(vec![(to_address, 50_000)]) +//! TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)]) //! .coin_selection(AlwaysSpendEverything), //! )?; //! diff --git a/src/wallet/export.rs b/src/wallet/export.rs index c72f769d..7d3d48fa 100644 --- a/src/wallet/export.rs +++ b/src/wallet/export.rs @@ -22,6 +22,51 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +//! Wallet export +//! +//! This modules implements the wallet export format used by [FullyNoded](https://github.com/Fonta1n3/FullyNoded/blob/10b7808c8b929b171cca537fb50522d015168ac9/Docs/Wallets/Wallet-Export-Spec.md). +//! +//! ## Examples +//! +//! ### Import from JSON +//! +//! ``` +//! # use std::str::FromStr; +//! # use bitcoin::*; +//! # use magical_bitcoin_wallet::database::*; +//! # use magical_bitcoin_wallet::wallet::export::*; +//! # use magical_bitcoin_wallet::*; +//! let import = r#"{ +//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)", +//! "blockheight":1782088, +//! "label":"testnet" +//! }"#; +//! +//! let import = WalletExport::from_str(import)?; +//! let wallet: OfflineWallet<_> = Wallet::new_offline(&import.descriptor(), import.change_descriptor().as_deref(), Network::Testnet, MemoryDatabase::default())?; +//! # Ok::<_, magical_bitcoin_wallet::Error>(()) +//! ``` +//! +//! ### Export a `Wallet` +//! ``` +//! # use bitcoin::*; +//! # use magical_bitcoin_wallet::database::*; +//! # use magical_bitcoin_wallet::wallet::export::*; +//! # use magical_bitcoin_wallet::*; +//! let wallet: OfflineWallet<_> = Wallet::new_offline( +//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)", +//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"), +//! Network::Testnet, +//! MemoryDatabase::default() +//! )?; +//! let export = WalletExport::export_wallet(&wallet, "exported wallet", true) +//! .map_err(ToString::to_string) +//! .map_err(magical_bitcoin_wallet::Error::Generic)?; +//! +//! println!("Exported: {}", export.to_string()); +//! # Ok::<_, magical_bitcoin_wallet::Error>(()) +//! ``` + use std::str::FromStr; use serde::{Deserialize, Serialize}; @@ -32,10 +77,15 @@ use crate::blockchain::Blockchain; use crate::database::BatchDatabase; use crate::wallet::Wallet; +/// Structure that contains the export of a wallet +/// +/// For a usage example see [this module](crate::wallet::export)'s documentation. #[derive(Debug, Serialize, Deserialize)] pub struct WalletExport { descriptor: String, + /// Earliest block to rescan when looking for the wallet's transactions pub blockheight: u32, + /// Arbitrary label for the wallet pub label: String, } @@ -54,6 +104,17 @@ impl FromStr for WalletExport { } impl WalletExport { + /// Export a wallet + /// + /// This function returns an error if it determines that the `wallet`'s descriptor(s) are not + /// supported by Bitcoin Core or don't follow the standard derivation paths defined by BIP44 + /// and others. + /// + /// If `include_blockheight` is `true`, this function will look into the `wallet`'s database + /// for the oldest transaction it knows and use that as the earliest block to rescan. + /// + /// If the database is empty or `include_blockheight` is false, the `blockheight` field + /// returned will be `0`. pub fn export_wallet( wallet: &Wallet, label: &str, @@ -118,10 +179,12 @@ impl WalletExport { } } + /// Return the external descriptor pub fn descriptor(&self) -> String { self.descriptor.clone() } + /// Return the internal descriptor, if present pub fn change_descriptor(&self) -> Option { let replaced = self.descriptor.replace("/0/*", "/1/*"); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 683533a7..21d3a161 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -229,7 +229,7 @@ where (None, Some(csv)) => csv, (Some(rbf), Some(csv)) if rbf < csv => return Err(Error::Generic(format!("Cannot enable RBF with nSequence `{}`, since at least `{}` is required to spend with OP_CSV", rbf, csv))), (None, _) if requirements.timelock.is_some() => 0xFFFFFFFE, - (Some(rbf), _) if rbf >= 0xFFFFFFFE => return Err(Error::Generic("Cannot enable RBF with anumber >= 0xFFFFFFFE".into())), + (Some(rbf), _) if rbf >= 0xFFFFFFFE => return Err(Error::Generic("Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into())), (Some(rbf), _) => rbf, (None, _) => 0xFFFFFFFF, }; @@ -254,22 +254,19 @@ where let calc_fee_bytes = |wu| (wu as f32) * fee_rate.as_sat_vb() / 4.0; fee_amount += calc_fee_bytes(tx.get_weight()); - for (index, (address, satoshi)) in builder.recipients.iter().enumerate() { + for (index, (script_pubkey, satoshi)) in builder.recipients.iter().enumerate() { let value = match builder.send_all { true => 0, false if satoshi.is_dust() => return Err(Error::OutputBelowDustLimit(index)), false => *satoshi, }; - // TODO: proper checks for testnet/regtest p2sh/p2pkh - if address.network != self.network && self.network != Network::Regtest { - return Err(Error::InvalidAddressNetwork(address.clone())); - } else if self.is_mine(&address.script_pubkey())? { + if self.is_mine(script_pubkey)? { received += value; } let new_out = TxOut { - script_pubkey: address.script_pubkey(), + script_pubkey: script_pubkey.clone(), value, }; fee_amount += calc_fee_bytes(serialize(&new_out).len() * 4); @@ -1251,7 +1248,7 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).version(0)) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).version(0)) .unwrap(); } @@ -1263,7 +1260,7 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.get_new_address().unwrap(); wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).version(1)) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).version(1)) .unwrap(); } @@ -1272,7 +1269,7 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).version(42)) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).version(42)) .unwrap(); assert_eq!(psbt.global.unsigned_tx.version, 42); @@ -1283,7 +1280,10 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)])) + .create_tx(TxBuilder::with_recipients(vec![( + addr.script_pubkey(), + 25_000, + )])) .unwrap(); assert_eq!(psbt.global.unsigned_tx.lock_time, 0); @@ -1294,7 +1294,10 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)])) + .create_tx(TxBuilder::with_recipients(vec![( + addr.script_pubkey(), + 25_000, + )])) .unwrap(); assert_eq!(psbt.global.unsigned_tx.lock_time, 100_000); @@ -1305,7 +1308,9 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).nlocktime(630_000)) + .create_tx( + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).nlocktime(630_000), + ) .unwrap(); assert_eq!(psbt.global.unsigned_tx.lock_time, 630_000); @@ -1316,7 +1321,9 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).nlocktime(630_000)) + .create_tx( + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).nlocktime(630_000), + ) .unwrap(); assert_eq!(psbt.global.unsigned_tx.lock_time, 630_000); @@ -1330,7 +1337,9 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); let addr = wallet.get_new_address().unwrap(); wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).nlocktime(50000)) + .create_tx( + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).nlocktime(50000), + ) .unwrap(); } @@ -1339,7 +1348,10 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)])) + .create_tx(TxBuilder::with_recipients(vec![( + addr.script_pubkey(), + 25_000, + )])) .unwrap(); assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 6); @@ -1350,7 +1362,9 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).enable_rbf()) + .create_tx( + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(), + ) .unwrap(); assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFD); @@ -1364,7 +1378,10 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.get_new_address().unwrap(); wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).enable_rbf_with_sequence(3)) + .create_tx( + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) + .enable_rbf_with_sequence(3), + ) .unwrap(); } @@ -1373,20 +1390,23 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)])) + .create_tx(TxBuilder::with_recipients(vec![( + addr.script_pubkey(), + 25_000, + )])) .unwrap(); assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFE); } #[test] - #[should_panic(expected = "Cannot enable RBF with anumber >= 0xFFFFFFFE")] + #[should_panic(expected = "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")] fn test_create_tx_invalid_rbf_sequence() { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); wallet .create_tx( - TxBuilder::with_recipients(vec![(addr, 25_000)]) + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) .enable_rbf_with_sequence(0xFFFFFFFE), ) .unwrap(); @@ -1398,7 +1418,7 @@ mod test { let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet .create_tx( - TxBuilder::with_recipients(vec![(addr, 25_000)]) + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) .enable_rbf_with_sequence(0xDEADBEEF), ) .unwrap(); @@ -1411,7 +1431,10 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)])) + .create_tx(TxBuilder::with_recipients(vec![( + addr.script_pubkey(), + 25_000, + )])) .unwrap(); assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFF); @@ -1426,7 +1449,8 @@ mod test { let addr = wallet.get_new_address().unwrap(); wallet .create_tx( - TxBuilder::with_recipients(vec![(addr.clone(), 25_000)]).do_not_spend_change(), + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) + .do_not_spend_change(), ) .unwrap(); } @@ -1438,7 +1462,11 @@ mod test { let addr = wallet.get_new_address().unwrap(); wallet .create_tx( - TxBuilder::with_recipients(vec![(addr.clone(), 25_000), (addr, 10_000)]).send_all(), + TxBuilder::with_recipients(vec![ + (addr.script_pubkey(), 25_000), + (addr.script_pubkey(), 10_000), + ]) + .send_all(), ) .unwrap(); } @@ -1448,7 +1476,7 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); let (psbt, details) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); assert_eq!(psbt.global.unsigned_tx.output.len(), 1); @@ -1463,7 +1491,7 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); let (psbt, details) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::default(), @add_signature); @@ -1475,7 +1503,7 @@ mod test { let addr = wallet.get_new_address().unwrap(); let (psbt, details) = wallet .create_tx( - TxBuilder::with_recipients(vec![(addr.clone(), 0)]) + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) .fee_rate(FeeRate::from_sat_per_vb(5.0)) .send_all(), ) @@ -1492,7 +1520,7 @@ mod test { let addr = wallet.get_new_address().unwrap(); let (psbt, details) = wallet .create_tx( - TxBuilder::with_recipients(vec![(addr.clone(), 25_000)]) + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]) .ordering(TxOrdering::Untouched), ) .unwrap(); @@ -1510,7 +1538,10 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 49_800)])) + .create_tx(TxBuilder::with_recipients(vec![( + addr.script_pubkey(), + 49_800, + )])) .unwrap(); assert_eq!(psbt.global.unsigned_tx.output.len(), 1); @@ -1525,7 +1556,7 @@ mod test { // very high fee rate, so that the only output would be below dust wallet .create_tx( - TxBuilder::with_recipients(vec![(addr.clone(), 0)]) + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) .send_all() .fee_rate(crate::FeeRate::from_sat_per_vb(453.0)), ) @@ -1538,8 +1569,11 @@ mod test { let addr = wallet.get_new_address().unwrap(); let (psbt, details) = wallet .create_tx( - TxBuilder::with_recipients(vec![(addr.clone(), 30_000), (addr.clone(), 10_000)]) - .ordering(super::tx_builder::TxOrdering::BIP69Lexicographic), + TxBuilder::with_recipients(vec![ + (addr.script_pubkey(), 30_000), + (addr.script_pubkey(), 10_000), + ]) + .ordering(super::tx_builder::TxOrdering::BIP69Lexicographic), ) .unwrap(); @@ -1557,7 +1591,10 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 30_000)])) + .create_tx(TxBuilder::with_recipients(vec![( + addr.script_pubkey(), + 30_000, + )])) .unwrap(); assert_eq!(psbt.inputs[0].sighash_type, Some(bitcoin::SigHashType::All)); @@ -1569,7 +1606,7 @@ mod test { let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet .create_tx( - TxBuilder::with_recipients(vec![(addr.clone(), 30_000)]) + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 30_000)]) .sighash(bitcoin::SigHashType::Single), ) .unwrap(); @@ -1588,7 +1625,7 @@ mod test { let (wallet, _, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)"); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); assert_eq!(psbt.inputs[0].hd_keypaths.len(), 1); @@ -1612,7 +1649,7 @@ mod test { let addr = testutils!(@external descriptors, 5); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); assert_eq!(psbt.outputs[0].hd_keypaths.len(), 1); @@ -1633,7 +1670,7 @@ mod test { get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); assert_eq!( @@ -1656,7 +1693,7 @@ mod test { get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); assert_eq!(psbt.inputs[0].redeem_script, None); @@ -1679,7 +1716,7 @@ mod test { get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))"); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); let script = Script::from( @@ -1699,7 +1736,7 @@ mod test { get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); assert!(psbt.inputs[0].non_witness_utxo.is_some()); @@ -1712,7 +1749,7 @@ mod test { get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); assert!(psbt.inputs[0].non_witness_utxo.is_none()); @@ -1726,7 +1763,7 @@ mod test { let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet .create_tx( - TxBuilder::with_recipients(vec![(addr.clone(), 0)]) + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) .force_non_witness_utxo() .send_all(), ) @@ -1742,7 +1779,10 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); let (psbt, mut details) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)])) + .create_tx(TxBuilder::with_recipients(vec![( + addr.script_pubkey(), + 25_000, + )])) .unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1759,7 +1799,10 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); let (psbt, mut details) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)])) + .create_tx(TxBuilder::with_recipients(vec![( + addr.script_pubkey(), + 25_000, + )])) .unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1777,7 +1820,9 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_new_address().unwrap(); let (psbt, mut details) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr, 25_000)]).enable_rbf()) + .create_tx( + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(), + ) .unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1798,7 +1843,9 @@ mod test { let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let (psbt, mut original_details) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 25_000)]).enable_rbf()) + .create_tx( + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 25_000)]).enable_rbf(), + ) .unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); @@ -1860,7 +1907,7 @@ mod test { let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let (psbt, mut original_details) = wallet .create_tx( - TxBuilder::with_recipients(vec![(addr.clone(), 0)]) + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) .send_all() .enable_rbf(), ) @@ -1914,7 +1961,7 @@ mod test { let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let (psbt, mut original_details) = wallet .create_tx( - TxBuilder::with_recipients(vec![(addr.clone(), 0)]) + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) .utxos(vec![OutPoint { txid: incoming_txid, vout: 0, @@ -1961,7 +2008,9 @@ mod test { let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let (psbt, mut original_details) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 45_000)]).enable_rbf()) + .create_tx( + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(), + ) .unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); @@ -2025,7 +2074,7 @@ mod test { let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let (psbt, mut original_details) = wallet .create_tx( - TxBuilder::with_recipients(vec![(addr.clone(), 0)]) + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]) .send_all() .add_utxo(OutPoint { txid: incoming_txid, @@ -2101,7 +2150,9 @@ mod test { let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let (psbt, mut original_details) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 45_000)]).enable_rbf()) + .create_tx( + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(), + ) .unwrap(); let mut tx = psbt.extract_tx(); assert_eq!(tx.input.len(), 1); @@ -2161,7 +2212,9 @@ mod test { let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let (psbt, mut original_details) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 45_000)]).enable_rbf()) + .create_tx( + TxBuilder::with_recipients(vec![(addr.script_pubkey(), 45_000)]).enable_rbf(), + ) .unwrap(); let mut tx = psbt.extract_tx(); let txid = tx.txid(); @@ -2226,7 +2279,7 @@ mod test { let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); @@ -2242,7 +2295,7 @@ mod test { get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"); let addr = wallet.get_new_address().unwrap(); let (psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); let (signed_psbt, finalized) = wallet.sign(psbt, None).unwrap(); @@ -2257,7 +2310,7 @@ mod test { let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); let addr = wallet.get_new_address().unwrap(); let (mut psbt, _) = wallet - .create_tx(TxBuilder::with_recipients(vec![(addr.clone(), 0)]).send_all()) + .create_tx(TxBuilder::with_recipients(vec![(addr.script_pubkey(), 0)]).send_all()) .unwrap(); psbt.inputs[0].hd_keypaths.clear(); diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index 766f4bd2..c90afafb 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -22,6 +22,72 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +//! Generalized signers +//! +//! This module provides the ability to add customized signers to a [`Wallet`](super::Wallet) +//! through the [`Wallet::add_signer`](super::Wallet::add_signer) function. +//! +//! ``` +//! # use std::sync::Arc; +//! # use std::str::FromStr; +//! # use bitcoin::*; +//! # use bitcoin::util::psbt; +//! # use bitcoin::util::bip32::Fingerprint; +//! # use magical_bitcoin_wallet::signer::*; +//! # use magical_bitcoin_wallet::database::*; +//! # use magical_bitcoin_wallet::*; +//! # #[derive(Debug)] +//! # struct CustomHSM; +//! # impl CustomHSM { +//! # fn sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> { +//! # Ok(()) +//! # } +//! # fn connect() -> Self { +//! # CustomHSM +//! # } +//! # } +//! #[derive(Debug)] +//! struct CustomSigner { +//! device: CustomHSM, +//! } +//! +//! impl CustomSigner { +//! fn connect() -> Self { +//! CustomSigner { device: CustomHSM::connect() } +//! } +//! } +//! +//! impl Signer for CustomSigner { +//! fn sign( +//! &self, +//! psbt: &mut psbt::PartiallySignedTransaction, +//! input_index: Option, +//! ) -> Result<(), SignerError> { +//! let input_index = input_index.ok_or(SignerError::InputIndexOutOfRange)?; +//! self.device.sign_input(psbt, input_index)?; +//! +//! Ok(()) +//! } +//! +//! fn sign_whole_tx(&self) -> bool { +//! false +//! } +//! } +//! +//! let custom_signer = CustomSigner::connect(); +//! +//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; +//! let mut wallet: OfflineWallet<_> = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?; +//! wallet.add_signer( +//! ScriptType::External, +//! Fingerprint::from_str("e30f11b8").unwrap().into(), +//! SignerOrdering(200), +//! Arc::new(Box::new(custom_signer)) +//! ); +//! +//! # Ok::<_, magical_bitcoin_wallet::Error>(()) +//! ``` + use std::cmp::Ordering; use std::collections::BTreeMap; use std::fmt; @@ -42,7 +108,7 @@ use miniscript::{Legacy, MiniscriptKey, Segwitv0}; use crate::descriptor::XKeyUtils; /// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among -/// many of them +/// multiple of them #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SignerId { PkHash(::Hash), @@ -93,15 +159,30 @@ impl fmt::Display for SignerError { impl std::error::Error for SignerError {} /// Trait for signers +/// +/// This trait can be implemented to provide customized signers to the wallet. For an example see +/// [`this module`](crate::wallet::signer)'s documentation. pub trait Signer: fmt::Debug { + /// Sign a PSBT + /// + /// The `input_index` argument is only provided if the wallet doesn't declare to sign the whole + /// transaction in one go (see [`Signer::sign_whole_tx`]). Otherwise its value is `None` and + /// can be ignored. fn sign( &self, psbt: &mut psbt::PartiallySignedTransaction, input_index: Option, ) -> Result<(), SignerError>; + /// Return whether or not the signer signs the whole transaction in one go instead of every + /// input individually fn sign_whole_tx(&self) -> bool; + /// Return the secret key for the signer + /// + /// This is used internally to reconstruct the original descriptor that may contain secrets. + /// External signers that are meant to keep key isolated should just return `None` here (which + /// is the default for this method, if not overridden). fn descriptor_secret_key(&self) -> Option { None } @@ -195,6 +276,11 @@ impl Signer for PrivateKey { } } +/// Defines the order in which signers are called +/// +/// The default value is `100`. Signers with an ordering above that will be called later, +/// and they will thus see the partial signatures added to the transaction once they get to sign +/// themselves. #[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq)] pub struct SignerOrdering(pub usize); diff --git a/src/wallet/time.rs b/src/wallet/time.rs index 0d0a77c7..6b6c436e 100644 --- a/src/wallet/time.rs +++ b/src/wallet/time.rs @@ -22,6 +22,14 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +//! Cross-platform time +//! +//! This module provides a function to get the current timestamp that works on all the platforms +//! supported by the library. +//! +//! It can be useful to compare it with the timestamps found in +//! [`TransactionDetails`](crate::types::TransactionDetails). + use std::time::Duration; #[cfg(target_arch = "wasm32")] @@ -29,6 +37,7 @@ use js_sys::Date; #[cfg(not(target_arch = "wasm32"))] use std::time::{Instant as SystemInstant, SystemTime, UNIX_EPOCH}; +/// Return the current timestamp in seconds #[cfg(not(target_arch = "wasm32"))] pub fn get_timestamp() -> u64 { SystemTime::now() @@ -36,6 +45,7 @@ pub fn get_timestamp() -> u64 { .unwrap() .as_secs() } +/// Return the current timestamp in seconds #[cfg(target_arch = "wasm32")] pub fn get_timestamp() -> u64 { let millis = Date::now(); diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index aab9951f..1f3d421f 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -22,17 +22,40 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +//! Transaction builder +//! +//! ## Example +//! +//! ``` +//! # use std::str::FromStr; +//! # use bitcoin::*; +//! # use magical_bitcoin_wallet::*; +//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); +//! // Create a transaction with one output to `to_address` of 50_000 satoshi, with a custom fee rate +//! // of 5.0 satoshi/vbyte, only spending non-change outputs and with RBF signaling +//! // enabled +//! let builder = TxBuilder::with_recipients(vec![(to_address.script_pubkey(), 50_000)]) +//! .fee_rate(FeeRate::from_sat_per_vb(5.0)) +//! .do_not_spend_change() +//! .enable_rbf(); +//! ``` + use std::collections::BTreeMap; use std::default::Default; -use bitcoin::{Address, OutPoint, SigHashType, Transaction}; +use bitcoin::{OutPoint, Script, SigHashType, Transaction}; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; use crate::types::{FeeRate, UTXO}; +/// A transaction builder +/// +/// This structure contains the configuration that the wallet must follow to build a transaction. +/// +/// For an example see [this module](super::tx_builder)'s documentation; #[derive(Debug, Default)] pub struct TxBuilder { - pub(crate) recipients: Vec<(Address, u64)>, + pub(crate) recipients: Vec<(Script, u64)>, pub(crate) send_all: bool, pub(crate) fee_rate: Option, pub(crate) policy_path: Option>>, @@ -49,112 +72,182 @@ pub struct TxBuilder { } impl TxBuilder { + /// Create an empty builder pub fn new() -> Self { Self::default() } - pub fn with_recipients(recipients: Vec<(Address, u64)>) -> Self { + /// Create a builder starting from a list of recipients + pub fn with_recipients(recipients: Vec<(Script, u64)>) -> Self { Self::default().set_recipients(recipients) } } impl TxBuilder { - pub fn set_recipients(mut self, recipients: Vec<(Address, u64)>) -> Self { + /// Replace the recipients already added with a new list + pub fn set_recipients(mut self, recipients: Vec<(Script, u64)>) -> Self { self.recipients = recipients; self } - pub fn add_recipient(mut self, address: Address, amount: u64) -> Self { - self.recipients.push((address, amount)); + /// Add a recipient to the internal list + pub fn add_recipient(mut self, script_pubkey: Script, amount: u64) -> Self { + self.recipients.push((script_pubkey, amount)); self } + /// Send all the selected utxos to a single output + /// + /// Adding more than one recipients with this option enabled will result in an error. + /// + /// The value associated with the only recipient is irrelevant and will be replaced by the wallet. pub fn send_all(mut self) -> Self { self.send_all = true; self } + /// Set a custom fee rate pub fn fee_rate(mut self, fee_rate: FeeRate) -> Self { self.fee_rate = Some(fee_rate); self } + /// Set the policy path to use while creating the transaction + /// + /// This method accepts a map where the key is the policy node id (see + /// [`Policy::id`](crate::descriptor::Policy::id)) and the value is the list of the indexes of + /// the items that are intended to be satisfied from the policy node (see + /// [`SatisfiableItem::Thresh::items`](crate::descriptor::policy::SatisfiableItem::Thresh::items)). pub fn policy_path(mut self, policy_path: BTreeMap>) -> Self { self.policy_path = Some(policy_path); self } - /// These have priority over the "unspendable" utxos + /// Replace the internal list of utxos that **must** be spent with a new list + /// + /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in + /// the "utxos" and the "unspendable" list, it will be spent. pub fn utxos(mut self, utxos: Vec) -> Self { self.utxos = Some(utxos); self } - /// This has priority over the "unspendable" utxos + /// Add a utxo to the internal list of utxos that **must** be spent + /// + /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in + /// the "utxos" and the "unspendable" list, it will be spent. pub fn add_utxo(mut self, utxo: OutPoint) -> Self { self.utxos.get_or_insert(vec![]).push(utxo); self } + /// Replace the internal list of unspendable utxos with a new list + /// + /// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::utxos`] and + /// [`TxBuilder::add_utxo`] have priority over these. See the docs of the two linked methods + /// for more details. pub fn unspendable(mut self, unspendable: Vec) -> Self { self.unspendable = Some(unspendable); self } + /// Add a utxo to the internal list of unspendable utxos + /// + /// It's important to note that the "must-be-spent" utxos added with [`TxBuilder::utxos`] and + /// [`TxBuilder::add_utxo`] have priority over this. See the docs of the two linked methods + /// for more details. pub fn add_unspendable(mut self, unspendable: OutPoint) -> Self { self.unspendable.get_or_insert(vec![]).push(unspendable); self } + /// Sign with a specific sig hash + /// + /// **Use this option very carefully** pub fn sighash(mut self, sighash: SigHashType) -> Self { self.sighash = Some(sighash); self } + /// Choose the ordering for inputs and outputs of the transaction pub fn ordering(mut self, ordering: TxOrdering) -> Self { self.ordering = ordering; self } + /// Use a specific nLockTime while creating the transaction + /// + /// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator. pub fn nlocktime(mut self, locktime: u32) -> Self { self.locktime = Some(locktime); self } + /// Enable signaling RBF + /// + /// This will use the default nSequence value of `0xFFFFFFFD`. pub fn enable_rbf(self) -> Self { self.enable_rbf_with_sequence(0xFFFFFFFD) } + /// Enable signaling RBF with a specific nSequence value + /// + /// This can cause conflicts if the wallet's descriptors contain an "older" (OP_CSV) operator + /// and the given `nsequence` is lower than the CSV value. + /// + /// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not + /// be a valid nSequence to signal RBF. pub fn enable_rbf_with_sequence(mut self, nsequence: u32) -> Self { self.rbf = Some(nsequence); self } + /// Build a transaction with a specific version + /// + /// The `version` should always be greater than `0` and greater than `1` if the wallet's + /// descriptors contain an "older" (OP_CSV) operator. pub fn version(mut self, version: u32) -> Self { self.version = Some(Version(version)); self } + /// Do not spend change outputs + /// + /// This effectively adds all the change outputs to the "unspendable" list. See + /// [`TxBuilder::unspendable`]. pub fn do_not_spend_change(mut self) -> Self { self.change_policy = ChangeSpendPolicy::ChangeForbidden; self } + /// Only spend change outputs + /// + /// This effectively adds all the non-change outputs to the "unspendable" list. See + /// [`TxBuilder::unspendable`]. pub fn only_spend_change(mut self) -> Self { self.change_policy = ChangeSpendPolicy::OnlyChange; self } + /// Set a specific [`ChangeSpendPolicy`]. See [`TxBuilder::do_not_spend_change`] and + /// [`TxBuilder::only_spend_change`] for some shortcuts. pub fn change_policy(mut self, change_policy: ChangeSpendPolicy) -> Self { self.change_policy = change_policy; self } + /// Fill-in the [`psbt::Input::non_witness_utxo`](bitcoin::util::psbt::Input::non_witness_utxo) field even if the wallet only has SegWit + /// descriptors. + /// + /// This is useful for signers which always require it, like Trezor hardware wallets. pub fn force_non_witness_utxo(mut self) -> Self { self.force_non_witness_utxo = true; self } + /// Choose the coin selection algorithm + /// + /// Overrides the [`DefaultCoinSelectionAlgorithm`](super::coin_selection::DefaultCoinSelectionAlgorithm). pub fn coin_selection(self, coin_selection: P) -> TxBuilder

{ TxBuilder { recipients: self.recipients, @@ -175,10 +268,14 @@ impl TxBuilder { } } +/// Ordering of the transaction's inputs and outputs #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] pub enum TxOrdering { + /// Randomized (default) Shuffle, + /// Unchanged Untouched, + /// BIP69 / Lexicographic BIP69Lexicographic, } @@ -215,7 +312,9 @@ impl TxOrdering { } } -// Helper type that wraps u32 and has a default value of 1 +/// Transaction version +/// +/// Has a default value of `1` #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] pub(crate) struct Version(pub(crate) u32); @@ -225,10 +324,14 @@ impl Default for Version { } } +/// Policy regarding the use of change outputs when creating a transaction #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] pub enum ChangeSpendPolicy { + /// Use both change and non-change outputs (default) ChangeAllowed, + /// Only use change outputs (see [`TxBuilder::only_spend_change`]) OnlyChange, + /// Only use non-change outputs (see [`TxBuilder::do_not_spend_change`]) ChangeForbidden, } diff --git a/testutils-macros/src/lib.rs b/testutils-macros/src/lib.rs index 67847352..6242f760 100644 --- a/testutils-macros/src/lib.rs +++ b/testutils-macros/src/lib.rs @@ -307,7 +307,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt wallet.sync(noop_progress(), None).unwrap(); assert_eq!(wallet.get_balance().unwrap(), 50_000); - let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr, 25_000)])).unwrap(); + let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey(), 25_000)])).unwrap(); let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); assert!(finalized, "Cannot finalize transaction"); let tx = psbt.extract_tx(); @@ -334,7 +334,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt wallet.sync(noop_progress(), None).unwrap(); assert_eq!(wallet.get_balance().unwrap(), 50_000); - let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr, 25_000)])).unwrap(); + let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey(), 25_000)])).unwrap(); let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); assert!(finalized, "Cannot finalize transaction"); let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap(); @@ -373,7 +373,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt let mut total_sent = 0; for _ in 0..5 { - let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.clone(), 5_000)])).unwrap(); + let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 5_000)])).unwrap(); let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); assert!(finalized, "Cannot finalize transaction"); wallet.broadcast(psbt.extract_tx()).unwrap(); @@ -405,7 +405,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt wallet.sync(noop_progress(), None).unwrap(); assert_eq!(wallet.get_balance().unwrap(), 50_000); - let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.clone(), 5_000)]).enable_rbf()).unwrap(); + let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 5_000)]).enable_rbf()).unwrap(); let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); assert!(finalized, "Cannot finalize transaction"); wallet.broadcast(psbt.extract_tx()).unwrap(); @@ -437,7 +437,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt wallet.sync(noop_progress(), None).unwrap(); assert_eq!(wallet.get_balance().unwrap(), 50_000); - let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap(); + let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 49_000)]).enable_rbf()).unwrap(); let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); assert!(finalized, "Cannot finalize transaction"); wallet.broadcast(psbt.extract_tx()).unwrap(); @@ -470,7 +470,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt wallet.sync(noop_progress(), None).unwrap(); assert_eq!(wallet.get_balance().unwrap(), 75_000); - let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap(); + let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 49_000)]).enable_rbf()).unwrap(); let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); assert!(finalized, "Cannot finalize transaction"); wallet.broadcast(psbt.extract_tx()).unwrap(); @@ -501,7 +501,7 @@ pub fn magical_blockchain_tests(attr: TokenStream, item: TokenStream) -> TokenSt wallet.sync(noop_progress(), None).unwrap(); assert_eq!(wallet.get_balance().unwrap(), 75_000); - let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.clone(), 49_000)]).enable_rbf()).unwrap(); + let (psbt, details) = wallet.create_tx(TxBuilder::with_recipients(vec![(node_addr.script_pubkey().clone(), 49_000)]).enable_rbf()).unwrap(); let (psbt, finalized) = wallet.sign(psbt, None).unwrap(); assert!(finalized, "Cannot finalize transaction"); wallet.broadcast(psbt.extract_tx()).unwrap();