diff --git a/src/database/memory.rs b/src/database/memory.rs index 987aa7b6..7dd7acce 100644 --- a/src/database/memory.rs +++ b/src/database/memory.rs @@ -404,6 +404,64 @@ impl BatchDatabase for MemoryDatabase { } } +#[cfg(test)] +impl MemoryDatabase { + // Artificially insert a tx in the database, as if we had found it with a `sync` + pub fn received_tx( + &mut self, + tx_meta: testutils::TestIncomingTx, + current_height: Option, + ) -> bitcoin::Txid { + use std::str::FromStr; + + let tx = Transaction { + version: 1, + lock_time: 0, + input: vec![], + output: tx_meta + .output + .iter() + .map(|out_meta| bitcoin::TxOut { + value: out_meta.value, + script_pubkey: bitcoin::Address::from_str(&out_meta.to_address) + .unwrap() + .script_pubkey(), + }) + .collect(), + }; + + let txid = tx.txid(); + let height = tx_meta + .min_confirmations + .map(|conf| current_height.unwrap().checked_sub(conf as u32).unwrap()); + + let tx_details = TransactionDetails { + transaction: Some(tx.clone()), + txid, + timestamp: 0, + height, + received: 0, + sent: 0, + fees: 0, + }; + + self.set_tx(&tx_details).unwrap(); + for (vout, out) in tx.output.iter().enumerate() { + self.set_utxo(&UTXO { + txout: out.clone(), + outpoint: OutPoint { + txid, + vout: vout as u32, + }, + is_internal: false, + }) + .unwrap(); + } + + txid + } +} + #[cfg(test)] mod test { use super::MemoryDatabase; diff --git a/src/error.rs b/src/error.rs index 16f67f1d..22cbb09c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,6 +8,7 @@ pub enum Error { Generic(String), ScriptDoesntHaveAddressForm, SendAllMultipleOutputs, + NoAddressees, OutputBelowDustLimit(usize), InsufficientFunds, InvalidAddressNetwork(Address), diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 4d3636a9..fd868890 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -127,6 +127,10 @@ where &self, builder: TxBuilder, ) -> Result<(PSBT, TransactionDetails), Error> { + if builder.addressees.is_empty() { + return Err(Error::NoAddressees); + } + // TODO: fetch both internal and external policies let policy = self.descriptor.extract_policy()?.unwrap(); if policy.requires_path() && builder.policy_path.is_none() { @@ -137,14 +141,18 @@ where debug!("requirements: {:?}", requirements); let version = match builder.version { - tx_builder::Version(0) => return Err(Error::Generic("Invalid version `0`".into())), - tx_builder::Version(1) if requirements.csv.is_some() => { + Some(tx_builder::Version(0)) => { + return Err(Error::Generic("Invalid version `0`".into())) + } + Some(tx_builder::Version(1)) if requirements.csv.is_some() => { return Err(Error::Generic( "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV" .into(), )) } - tx_builder::Version(x) => x, + Some(tx_builder::Version(x)) => x, + None if requirements.csv.is_some() => 2, + _ => 1, }; let lock_time = match builder.locktime { @@ -219,6 +227,14 @@ where .max_satisfaction_weight(), ); + if builder.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed + && self.change_descriptor.is_none() + { + return Err(Error::Generic( + "The `change_policy` can be set only if the wallet has a change_descriptor".into(), + )); + } + let (available_utxos, use_all_utxos) = self.get_available_utxos( builder.change_policy, &builder.utxos, @@ -335,7 +351,24 @@ where } } - self.add_hd_keypaths(&mut psbt)?; + // probably redundant but it doesn't hurt... + self.add_input_hd_keypaths(&mut psbt)?; + + // add metadata for the outputs + for (psbt_output, tx_output) in psbt + .outputs + .iter_mut() + .zip(psbt.global.unsigned_tx.output.iter()) + { + if let Some((script_type, child)) = self + .database + .borrow() + .get_path_from_script_pubkey(&tx_output.script_pubkey)? + { + let (desc, _) = self.get_descriptor_for(script_type); + psbt_output.hd_keypaths = desc.get_hd_keypaths(child)?; + } + } let transaction_details = TransactionDetails { transaction: None, @@ -353,7 +386,7 @@ where // TODO: define an enum for signing errors pub fn sign(&self, mut psbt: PSBT, assume_height: Option) -> Result<(PSBT, bool), Error> { // this helps us doing our job later - self.add_hd_keypaths(&mut psbt)?; + self.add_input_hd_keypaths(&mut psbt)?; let tx = &psbt.global.unsigned_tx; @@ -701,7 +734,7 @@ where } } - fn add_hd_keypaths(&self, psbt: &mut PSBT) -> Result<(), Error> { + fn add_input_hd_keypaths(&self, psbt: &mut PSBT) -> Result<(), Error> { let mut input_utxos = Vec::with_capacity(psbt.inputs.len()); for n in 0..psbt.inputs.len() { input_utxos.push(psbt.get_utxo_for(n).clone()); @@ -787,6 +820,8 @@ where } } + // TODO: what if i generate an address first and cache some addresses? + // TODO: we should sync if generating an address triggers a new batch to be stored if run_setup { maybe_await!(self.client.setup( None, @@ -818,8 +853,11 @@ where mod test { use bitcoin::Network; + use miniscript::Descriptor; + use crate::database::memory::MemoryDatabase; use crate::database::Database; + use crate::descriptor::ExtendedDescriptor; use crate::types::ScriptType; use super::*; @@ -913,4 +951,518 @@ mod test { .unwrap() .is_some()); } + + fn get_test_wpkh() -> &'static str { + "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)" + } + + fn get_test_single_sig_csv() -> &'static str { + // and(pk(Alice),older(6)) + "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))" + } + + fn get_test_single_sig_cltv() -> &'static str { + // and(pk(Alice),after(100000)) + "wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))" + } + + fn get_funded_wallet( + descriptor: &str, + ) -> ( + OfflineWallet, + (ExtendedDescriptor, Option), + bitcoin::Txid, + ) { + let descriptors = testutils!(@descriptors (descriptor)); + let wallet: OfflineWallet<_> = Wallet::new_offline( + &descriptors.0.to_string(), + None, + Network::Regtest, + MemoryDatabase::new(), + ) + .unwrap(); + + let txid = wallet.database.borrow_mut().received_tx( + testutils! { + @tx ( (@external descriptors, 0) => 50_000 ) + }, + None, + ); + + (wallet, descriptors, txid) + } + + #[test] + #[should_panic(expected = "NoAddressees")] + fn test_create_tx_empty_addressees() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + wallet + .create_tx(TxBuilder::from_addressees(vec![]).version(0)) + .unwrap(); + } + + #[test] + #[should_panic(expected = "Invalid version `0`")] + fn test_create_tx_version_0() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]).version(0)) + .unwrap(); + } + + #[test] + #[should_panic( + expected = "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV" + )] + fn test_create_tx_version_1_csv() { + let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.get_new_address().unwrap(); + wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]).version(1)) + .unwrap(); + } + + #[test] + fn test_create_tx_custom_version() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]).version(42)) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.version, 42); + } + + #[test] + fn test_create_tx_default_locktime() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)])) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.lock_time, 0); + } + + #[test] + fn test_create_tx_default_locktime_cltv() { + let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)])) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.lock_time, 100_000); + } + + #[test] + fn test_create_tx_custom_locktime() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]).nlocktime(630_000)) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.lock_time, 630_000); + } + + #[test] + fn test_create_tx_custom_locktime_compatible_with_cltv() { + let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]).nlocktime(630_000)) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.lock_time, 630_000); + } + + #[test] + #[should_panic( + expected = "TxBuilder requested timelock of `50000`, but at least `100000` is required to spend from this script" + )] + fn test_create_tx_custom_locktime_incompatible_with_cltv() { + let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.get_new_address().unwrap(); + wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]).nlocktime(50000)) + .unwrap(); + } + + #[test] + fn test_create_tx_no_rbf_csv() { + let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)])) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 6); + } + + #[test] + fn test_create_tx_with_default_rbf_csv() { + let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]).enable_rbf()) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFD); + } + + #[test] + #[should_panic( + expected = "Cannot enable RBF with nSequence `3`, since at least `6` is required to spend with OP_CSV" + )] + fn test_create_tx_with_custom_rbf_csv() { + let (wallet, _, _) = get_funded_wallet(get_test_single_sig_csv()); + let addr = wallet.get_new_address().unwrap(); + wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)]).enable_rbf_with_sequence(3)) + .unwrap(); + } + + #[test] + fn test_create_tx_no_rbf_cltv() { + let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)])) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFE); + } + + #[test] + #[should_panic(expected = "Cannot enable RBF with anumber >= 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::from_addressees(vec![(addr, 25_000)]) + .enable_rbf_with_sequence(0xFFFFFFFE), + ) + .unwrap(); + } + + #[test] + fn test_create_tx_custom_rbf_sequence() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr, 25_000)]) + .enable_rbf_with_sequence(0xDEADBEEF), + ) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xDEADBEEF); + } + + #[test] + fn test_create_tx_default_sequence() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr, 25_000)])) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.input[0].sequence, 0xFFFFFFFF); + } + + #[test] + #[should_panic( + expected = "The `change_policy` can be set only if the wallet has a change_descriptor" + )] + fn test_create_tx_change_policy_no_internal() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr.clone(), 25_000)]).do_not_spend_change(), + ) + .unwrap(); + } + + #[test] + #[should_panic(expected = "SendAllMultipleOutputs")] + fn test_create_tx_send_all_multiple_outputs() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr.clone(), 25_000), (addr, 10_000)]).send_all(), + ) + .unwrap(); + } + + #[test] + fn test_create_tx_send_all() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, details) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 0)]).send_all()) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.output.len(), 1); + assert_eq!( + psbt.global.unsigned_tx.output[0].value, + 50_000 - details.fees + ); + } + + #[test] + fn test_create_tx_add_change() { + use super::tx_builder::TxOrdering; + + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, details) = wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr.clone(), 25_000)]) + .ordering(TxOrdering::Untouched), + ) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.output.len(), 2); + assert_eq!(psbt.global.unsigned_tx.output[0].value, 25_000); + assert_eq!( + psbt.global.unsigned_tx.output[1].value, + 25_000 - details.fees + ); + } + + #[test] + fn test_create_tx_skip_change_dust() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 49_800)])) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.output.len(), 1); + assert_eq!(psbt.global.unsigned_tx.output[0].value, 49_800); + } + + #[test] + #[should_panic(expected = "InsufficientFunds")] + fn test_create_tx_send_all_dust_amount() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + // very high fee rate, so that the only output would be below dust + wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr.clone(), 0)]) + .send_all() + .fee_rate(super::utils::FeeRate::from_sat_per_vb(453.0)), + ) + .unwrap(); + } + + #[test] + fn test_create_tx_ordering_respected() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, details) = wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr.clone(), 30_000), (addr.clone(), 10_000)]) + .ordering(super::tx_builder::TxOrdering::BIP69Lexicographic), + ) + .unwrap(); + + assert_eq!(psbt.global.unsigned_tx.output.len(), 3); + assert_eq!( + psbt.global.unsigned_tx.output[0].value, + 10_000 - details.fees + ); + assert_eq!(psbt.global.unsigned_tx.output[1].value, 10_000); + assert_eq!(psbt.global.unsigned_tx.output[2].value, 30_000); + } + + #[test] + fn test_create_tx_default_sighash() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 30_000)])) + .unwrap(); + + assert_eq!(psbt.inputs[0].sighash_type, Some(bitcoin::SigHashType::All)); + } + + #[test] + fn test_create_tx_custom_sighash() { + let (wallet, _, _) = get_funded_wallet(get_test_wpkh()); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr.clone(), 30_000)]) + .sighash(bitcoin::SigHashType::Single), + ) + .unwrap(); + + assert_eq!( + psbt.inputs[0].sighash_type, + Some(bitcoin::SigHashType::Single) + ); + } + + #[test] + fn test_create_tx_input_hd_keypaths() { + use bitcoin::util::bip32::{DerivationPath, Fingerprint}; + use std::str::FromStr; + + 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::from_addressees(vec![(addr.clone(), 0)]).send_all()) + .unwrap(); + + assert_eq!(psbt.inputs[0].hd_keypaths.len(), 1); + assert_eq!( + psbt.inputs[0].hd_keypaths.values().nth(0).unwrap(), + &( + Fingerprint::from_str("d34db33f").unwrap(), + DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap() + ) + ); + } + + #[test] + fn test_create_tx_output_hd_keypaths() { + use bitcoin::util::bip32::{DerivationPath, Fingerprint}; + use std::str::FromStr; + + let (wallet, descriptors, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)"); + // cache some addresses + wallet.get_new_address().unwrap(); + + let addr = testutils!(@external descriptors, 5); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 0)]).send_all()) + .unwrap(); + + assert_eq!(psbt.outputs[0].hd_keypaths.len(), 1); + assert_eq!( + psbt.outputs[0].hd_keypaths.values().nth(0).unwrap(), + &( + Fingerprint::from_str("d34db33f").unwrap(), + DerivationPath::from_str("m/44'/0'/0'/0/5").unwrap() + ) + ); + } + + #[test] + fn test_create_tx_set_redeem_script_p2sh() { + use bitcoin::hashes::hex::FromHex; + + let (wallet, _, _) = + get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 0)]).send_all()) + .unwrap(); + + assert_eq!( + psbt.inputs[0].redeem_script, + Some(Script::from( + Vec::::from_hex( + "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" + ) + .unwrap() + )) + ); + assert_eq!(psbt.inputs[0].witness_script, None); + } + + #[test] + fn test_create_tx_set_witness_script_p2wsh() { + use bitcoin::hashes::hex::FromHex; + + let (wallet, _, _) = + get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 0)]).send_all()) + .unwrap(); + + assert_eq!(psbt.inputs[0].redeem_script, None); + assert_eq!( + psbt.inputs[0].witness_script, + Some(Script::from( + Vec::::from_hex( + "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" + ) + .unwrap() + )) + ); + } + + #[test] + fn test_create_tx_set_redeem_witness_script_p2wsh_p2sh() { + use bitcoin::hashes::hex::FromHex; + + let (wallet, _, _) = + get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))"); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 0)]).send_all()) + .unwrap(); + + let script = Script::from( + Vec::::from_hex( + "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac", + ) + .unwrap(), + ); + + assert_eq!(psbt.inputs[0].redeem_script, Some(script.to_v0_p2wsh())); + assert_eq!(psbt.inputs[0].witness_script, Some(script)); + } + + #[test] + fn test_create_tx_non_witness_utxo() { + let (wallet, _, _) = + get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 0)]).send_all()) + .unwrap(); + + assert!(psbt.inputs[0].non_witness_utxo.is_some()); + assert!(psbt.inputs[0].witness_utxo.is_none()); + } + + #[test] + fn test_create_tx_only_witness_utxo() { + let (wallet, _, _) = + get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx(TxBuilder::from_addressees(vec![(addr.clone(), 0)]).send_all()) + .unwrap(); + + assert!(psbt.inputs[0].non_witness_utxo.is_none()); + assert!(psbt.inputs[0].witness_utxo.is_some()); + } + + #[test] + fn test_create_tx_both_non_witness_utxo_and_witness_utxo() { + let (wallet, _, _) = + get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); + let addr = wallet.get_new_address().unwrap(); + let (psbt, _) = wallet + .create_tx( + TxBuilder::from_addressees(vec![(addr.clone(), 0)]) + .force_non_witness_utxo() + .send_all(), + ) + .unwrap(); + + assert!(psbt.inputs[0].non_witness_utxo.is_some()); + assert!(psbt.inputs[0].witness_utxo.is_some()); + } } diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index ea34fbcf..cddc9d9f 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -19,7 +19,7 @@ pub struct TxBuilder { pub(crate) ordering: TxOrdering, pub(crate) locktime: Option, pub(crate) rbf: Option, - pub(crate) version: Version, + pub(crate) version: Option, pub(crate) change_policy: ChangeSpendPolicy, pub(crate) force_non_witness_utxo: bool, pub(crate) coin_selection: Cs, @@ -108,7 +108,7 @@ impl TxBuilder { } pub fn version(mut self, version: u32) -> Self { - self.version = Version(version); + self.version = Some(Version(version)); self } @@ -152,7 +152,7 @@ impl TxBuilder { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] pub enum TxOrdering { Shuffle, Untouched, @@ -193,7 +193,7 @@ impl TxOrdering { } // Helper type that wraps u32 and has a default value of 1 -#[derive(Debug)] +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] pub(crate) struct Version(pub(crate) u32); impl Default for Version { @@ -202,7 +202,7 @@ impl Default for Version { } } -#[derive(Debug)] +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] pub enum ChangeSpendPolicy { ChangeAllowed, OnlyChange, @@ -245,6 +245,11 @@ mod test { use super::*; + #[test] + fn test_output_ordering_default_shuffle() { + assert_eq!(TxOrdering::default(), TxOrdering::Shuffle); + } + #[test] fn test_output_ordering_untouched() { let original_tx = ordering_test_tx!(); @@ -301,4 +306,57 @@ mod test { assert_eq!(tx.output[1].script_pubkey, From::from(vec![0xAA])); assert_eq!(tx.output[2].script_pubkey, From::from(vec![0xAA, 0xEE])); } + + fn get_test_utxos() -> Vec { + vec![ + UTXO { + outpoint: OutPoint { + txid: Default::default(), + vout: 0, + }, + txout: Default::default(), + is_internal: false, + }, + UTXO { + outpoint: OutPoint { + txid: Default::default(), + vout: 1, + }, + txout: Default::default(), + is_internal: true, + }, + ] + } + + #[test] + fn test_change_spend_policy_default() { + let change_spend_policy = ChangeSpendPolicy::default(); + let filtered = change_spend_policy.filter_utxos(get_test_utxos().into_iter()); + + assert_eq!(filtered.len(), 2); + } + + #[test] + fn test_change_spend_policy_no_internal() { + let change_spend_policy = ChangeSpendPolicy::ChangeForbidden; + let filtered = change_spend_policy.filter_utxos(get_test_utxos().into_iter()); + + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].is_internal, false); + } + + #[test] + fn test_change_spend_policy_only_internal() { + let change_spend_policy = ChangeSpendPolicy::OnlyChange; + let filtered = change_spend_policy.filter_utxos(get_test_utxos().into_iter()); + + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].is_internal, true); + } + + #[test] + fn test_default_tx_version_1() { + let version = Version::default(); + assert_eq!(version.0, 1); + } } diff --git a/testutils/src/lib.rs b/testutils/src/lib.rs index 093711cf..a8477788 100644 --- a/testutils/src/lib.rs +++ b/testutils/src/lib.rs @@ -10,7 +10,6 @@ use std::env; use std::ops::Deref; use std::path::PathBuf; use std::str::FromStr; -use std::sync::Mutex; use std::time::Duration; #[allow(unused_imports)] @@ -26,12 +25,6 @@ pub use bitcoincore_rpc::{Auth, Client as RpcClient, RpcApi}; pub use electrum_client::{Client as ElectrumClient, ElectrumApi}; -lazy_static! { - static ref SYNC_TESTS_MUTEX: Mutex<()> = Mutex::new(()); -} - -pub fn test_init() {} - // TODO: we currently only support env vars, we could also parse a toml file fn get_auth() -> Auth { match env::var("MAGICAL_RPC_AUTH").as_ref().map(String::as_ref) { @@ -217,7 +210,6 @@ macro_rules! testutils { }).unwrap(); internal = Some(string_internal.try_into().unwrap()); - )* (external, internal)