]> Untitled Git - bdk/commitdiff
[tests] Add tests for `Wallet::create_tx()`
authorAlekos Filini <alekos.filini@gmail.com>
Mon, 10 Aug 2020 15:16:47 +0000 (17:16 +0200)
committerAlekos Filini <alekos.filini@gmail.com>
Tue, 11 Aug 2020 09:31:11 +0000 (11:31 +0200)
src/database/memory.rs
src/error.rs
src/wallet/mod.rs
src/wallet/tx_builder.rs
testutils/src/lib.rs

index 987aa7b68421fd9283510139e5e9f3ff19ba8e25..7dd7acceed78d43d8e5090b9589deb49d6bcf3a9 100644 (file)
@@ -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<u32>,
+    ) -> 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;
index 16f67f1d4843e701ed78a79a37137a7f8370e6df..22cbb09c3ea85416e38f822a598cc877e0ece373 100644 (file)
@@ -8,6 +8,7 @@ pub enum Error {
     Generic(String),
     ScriptDoesntHaveAddressForm,
     SendAllMultipleOutputs,
+    NoAddressees,
     OutputBelowDustLimit(usize),
     InsufficientFunds,
     InvalidAddressNetwork(Address),
index 4d3636a9addb2edaa7cbf1d845cf0a263f363479..fd868890e8ed84903b71d0b2e8097e6f5a2d99d0 100644 (file)
@@ -127,6 +127,10 @@ where
         &self,
         builder: TxBuilder<Cs>,
     ) -> 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<u32>) -> 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<MemoryDatabase>,
+        (ExtendedDescriptor, Option<ExtendedDescriptor>),
+        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::<u8>::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::<u8>::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::<u8>::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());
+    }
 }
index ea34fbcffc254ffe69c56baf819532c78f8e51d1..cddc9d9f148c2dabc7948a3c3a8b0addfdd06509 100644 (file)
@@ -19,7 +19,7 @@ pub struct TxBuilder<Cs: CoinSelectionAlgorithm> {
     pub(crate) ordering: TxOrdering,
     pub(crate) locktime: Option<u32>,
     pub(crate) rbf: Option<u32>,
-    pub(crate) version: Version,
+    pub(crate) version: Option<Version>,
     pub(crate) change_policy: ChangeSpendPolicy,
     pub(crate) force_non_witness_utxo: bool,
     pub(crate) coin_selection: Cs,
@@ -108,7 +108,7 @@ impl<Cs: CoinSelectionAlgorithm> TxBuilder<Cs> {
     }
 
     pub fn version(mut self, version: u32) -> Self {
-        self.version = Version(version);
+        self.version = Some(Version(version));
         self
     }
 
@@ -152,7 +152,7 @@ impl<Cs: CoinSelectionAlgorithm> TxBuilder<Cs> {
     }
 }
 
-#[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<UTXO> {
+        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);
+    }
 }
index 093711cf20e9db22ff63e260707b6b1595b06432..a8477788deea92ac7892f3cc2a8d57962ddb933f 100644 (file)
@@ -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)