/home/darosior/projects/bdk/crates/wallet/tests/wallet.rs
Line | Count | Source (jump to first uncovered line) |
1 | | extern crate alloc; |
2 | | |
3 | | use std::path::Path; |
4 | | use std::str::FromStr; |
5 | | |
6 | | use anyhow::Context; |
7 | | use assert_matches::assert_matches; |
8 | | use bdk_chain::{tx_graph, COINBASE_MATURITY}; |
9 | | use bdk_chain::{BlockId, ConfirmationTime}; |
10 | | use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection}; |
11 | | use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; |
12 | | use bdk_wallet::error::CreateTxError; |
13 | | use bdk_wallet::psbt::PsbtUtils; |
14 | | use bdk_wallet::signer::{SignOptions, SignerError}; |
15 | | use bdk_wallet::tx_builder::AddForeignUtxoError; |
16 | | use bdk_wallet::{AddressInfo, Balance, ChangeSet, Wallet, WalletPersister}; |
17 | | use bdk_wallet::{KeychainKind, LoadError, LoadMismatch, LoadWithPersistError}; |
18 | | use bitcoin::constants::ChainHash; |
19 | | use bitcoin::hashes::Hash; |
20 | | use bitcoin::key::Secp256k1; |
21 | | use bitcoin::psbt; |
22 | | use bitcoin::script::PushBytesBuf; |
23 | | use bitcoin::sighash::{EcdsaSighashType, TapSighashType}; |
24 | | use bitcoin::taproot::TapNodeHash; |
25 | | use bitcoin::{ |
26 | | absolute, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, ScriptBuf, |
27 | | Sequence, Transaction, TxIn, TxOut, Txid, Weight, |
28 | | }; |
29 | | use rand::rngs::StdRng; |
30 | | use rand::SeedableRng; |
31 | | |
32 | | mod common; |
33 | | use common::*; |
34 | | |
35 | 10 | fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) -> OutPoint { |
36 | 10 | let addr = wallet.next_unused_address(KeychainKind::External).address; |
37 | 10 | receive_output_to_address(wallet, addr, value, height) |
38 | 10 | } |
39 | | |
40 | 10 | fn receive_output_to_address( |
41 | 10 | wallet: &mut Wallet, |
42 | 10 | addr: Address, |
43 | 10 | value: u64, |
44 | 10 | height: ConfirmationTime, |
45 | 10 | ) -> OutPoint { |
46 | 10 | let tx = Transaction { |
47 | 10 | version: transaction::Version::ONE, |
48 | 10 | lock_time: absolute::LockTime::ZERO, |
49 | 10 | input: vec![], |
50 | 10 | output: vec![TxOut { |
51 | 10 | script_pubkey: addr.script_pubkey(), |
52 | 10 | value: Amount::from_sat(value), |
53 | 10 | }], |
54 | 10 | }; |
55 | 10 | |
56 | 10 | let txid = tx.compute_txid(); |
57 | 10 | wallet.insert_tx(tx); |
58 | 10 | |
59 | 10 | match height { |
60 | 6 | ConfirmationTime::Confirmed { .. } => { |
61 | 6 | insert_anchor_from_conf(wallet, txid, height); |
62 | 6 | } |
63 | 4 | ConfirmationTime::Unconfirmed { last_seen } => { |
64 | 4 | insert_seen_at(wallet, txid, last_seen); |
65 | 4 | } |
66 | | } |
67 | | |
68 | 10 | OutPoint { txid, vout: 0 } |
69 | 10 | } |
70 | | |
71 | 7 | fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { |
72 | 7 | let latest_cp = wallet.latest_checkpoint(); |
73 | 7 | let height = latest_cp.height(); |
74 | 7 | let anchor = if height == 0 { |
75 | 1 | ConfirmationTime::Unconfirmed { last_seen: 0 } |
76 | | } else { |
77 | 6 | ConfirmationTime::Confirmed { height, time: 0 } |
78 | | }; |
79 | 7 | receive_output(wallet, value, anchor) |
80 | 7 | } |
81 | | |
82 | 24 | fn insert_seen_at(wallet: &mut Wallet, txid: Txid, seen_at: u64) { |
83 | 24 | use bdk_wallet::Update; |
84 | 24 | wallet |
85 | 24 | .apply_update(Update { |
86 | 24 | tx_update: tx_graph::TxUpdate { |
87 | 24 | seen_ats: [(txid, seen_at)].into_iter().collect(), |
88 | 24 | ..Default::default() |
89 | 24 | }, |
90 | 24 | ..Default::default() |
91 | 24 | }) |
92 | 24 | .unwrap(); |
93 | 24 | } |
94 | | |
95 | | // The satisfaction size of a P2WPKH is 112 WU = |
96 | | // 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len) |
97 | | // On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for |
98 | | // a total of 105 WU. |
99 | | // Here, we push just once for simplicity, so we have to add an extra byte for the missing |
100 | | // OP_PUSH. |
101 | | const P2WPKH_FAKE_WITNESS_SIZE: usize = 106; |
102 | | |
103 | | const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48]; |
104 | | |
105 | | #[test] |
106 | 1 | fn wallet_is_persisted() -> anyhow::Result<()> { |
107 | 2 | fn run<Db, CreateDb, OpenDb>( |
108 | 2 | filename: &str, |
109 | 2 | create_db: CreateDb, |
110 | 2 | open_db: OpenDb, |
111 | 2 | ) -> anyhow::Result<()> |
112 | 2 | where |
113 | 2 | CreateDb: Fn(&Path) -> anyhow::Result<Db>, |
114 | 2 | OpenDb: Fn(&Path) -> anyhow::Result<Db>, |
115 | 2 | Db: WalletPersister, |
116 | 2 | Db::Error: std::error::Error + Send + Sync + 'static, |
117 | 2 | { |
118 | 2 | let temp_dir = tempfile::tempdir().expect("must create tempdir"); |
119 | 2 | let file_path = temp_dir.path().join(filename); |
120 | 2 | let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc(); |
121 | 1 | |
122 | 1 | // create new wallet |
123 | 2 | let wallet_spk_index = { |
124 | 2 | let mut db = create_db(&file_path)?0 ; |
125 | 2 | let mut wallet = Wallet::create(external_desc, internal_desc) |
126 | 2 | .network(Network::Testnet) |
127 | 2 | .create_wallet(&mut db)?0 ; |
128 | 2 | wallet.reveal_next_address(KeychainKind::External); |
129 | 2 | |
130 | 2 | // persist new wallet changes |
131 | 2 | assert!(wallet.persist(&mut db)?0 , "must write"0 ); |
132 | 2 | wallet.spk_index().clone() |
133 | 1 | }; |
134 | 1 | |
135 | 1 | // recover wallet |
136 | 1 | { |
137 | 2 | let mut db = open_db(&file_path).context("failed to recover db")?0 ; |
138 | 2 | let _ = Wallet::load() |
139 | 2 | .check_network(Network::Testnet) |
140 | 2 | .load_wallet(&mut db)?0 |
141 | 2 | .expect("wallet must exist"); |
142 | 1 | } |
143 | 1 | { |
144 | 2 | let mut db = open_db(&file_path).context("failed to recover db")?0 ; |
145 | 2 | let wallet = Wallet::load() |
146 | 2 | .descriptor(KeychainKind::External, Some(external_desc)) |
147 | 2 | .descriptor(KeychainKind::Internal, Some(internal_desc)) |
148 | 2 | .check_network(Network::Testnet) |
149 | 2 | .load_wallet(&mut db)?0 |
150 | 2 | .expect("wallet must exist"); |
151 | 2 | |
152 | 2 | assert_eq!(wallet.network(), Network::Testnet); |
153 | 2 | assert_eq!( |
154 | 2 | wallet.spk_index().keychains().collect::<Vec<_>>(), |
155 | 2 | wallet_spk_index.keychains().collect::<Vec<_>>() |
156 | 2 | ); |
157 | 2 | assert_eq!( |
158 | 2 | wallet.spk_index().last_revealed_indices(), |
159 | 2 | wallet_spk_index.last_revealed_indices() |
160 | 2 | ); |
161 | 2 | let secp = Secp256k1::new(); |
162 | 2 | assert_eq!( |
163 | 2 | *wallet.public_descriptor(KeychainKind::External), |
164 | 2 | external_desc |
165 | 2 | .into_wallet_descriptor(&secp, wallet.network()) |
166 | 2 | .unwrap() |
167 | 2 | .0 |
168 | 2 | ); |
169 | 1 | } |
170 | 1 | |
171 | 2 | Ok(()) |
172 | 2 | } wallet::wallet_is_persisted::run::<bdk_file_store::store::Store<bdk_wallet::wallet::changeset::ChangeSet>, wallet::wallet_is_persisted::{closure#0}, wallet::wallet_is_persisted::{closure#1}> Line | Count | Source | 107 | 1 | fn run<Db, CreateDb, OpenDb>( | 108 | 1 | filename: &str, | 109 | 1 | create_db: CreateDb, | 110 | 1 | open_db: OpenDb, | 111 | 1 | ) -> anyhow::Result<()> | 112 | 1 | where | 113 | 1 | CreateDb: Fn(&Path) -> anyhow::Result<Db>, | 114 | 1 | OpenDb: Fn(&Path) -> anyhow::Result<Db>, | 115 | 1 | Db: WalletPersister, | 116 | 1 | Db::Error: std::error::Error + Send + Sync + 'static, | 117 | 1 | { | 118 | 1 | let temp_dir = tempfile::tempdir().expect("must create tempdir"); | 119 | 1 | let file_path = temp_dir.path().join(filename); | 120 | 1 | let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc(); | 121 | | | 122 | | // create new wallet | 123 | 1 | let wallet_spk_index = { | 124 | 1 | let mut db = create_db(&file_path)?0 ; | 125 | 1 | let mut wallet = Wallet::create(external_desc, internal_desc) | 126 | 1 | .network(Network::Testnet) | 127 | 1 | .create_wallet(&mut db)?0 ; | 128 | 1 | wallet.reveal_next_address(KeychainKind::External); | 129 | 1 | | 130 | 1 | // persist new wallet changes | 131 | 1 | assert!(wallet.persist(&mut db)?0 , "must write"0 ); | 132 | 1 | wallet.spk_index().clone() | 133 | | }; | 134 | | | 135 | | // recover wallet | 136 | | { | 137 | 1 | let mut db = open_db(&file_path).context("failed to recover db")?0 ; | 138 | 1 | let _ = Wallet::load() | 139 | 1 | .check_network(Network::Testnet) | 140 | 1 | .load_wallet(&mut db)?0 | 141 | 1 | .expect("wallet must exist"); | 142 | | } | 143 | | { | 144 | 1 | let mut db = open_db(&file_path).context("failed to recover db")?0 ; | 145 | 1 | let wallet = Wallet::load() | 146 | 1 | .descriptor(KeychainKind::External, Some(external_desc)) | 147 | 1 | .descriptor(KeychainKind::Internal, Some(internal_desc)) | 148 | 1 | .check_network(Network::Testnet) | 149 | 1 | .load_wallet(&mut db)?0 | 150 | 1 | .expect("wallet must exist"); | 151 | 1 | | 152 | 1 | assert_eq!(wallet.network(), Network::Testnet); | 153 | 1 | assert_eq!( | 154 | 1 | wallet.spk_index().keychains().collect::<Vec<_>>(), | 155 | 1 | wallet_spk_index.keychains().collect::<Vec<_>>() | 156 | 1 | ); | 157 | 1 | assert_eq!( | 158 | 1 | wallet.spk_index().last_revealed_indices(), | 159 | 1 | wallet_spk_index.last_revealed_indices() | 160 | 1 | ); | 161 | 1 | let secp = Secp256k1::new(); | 162 | 1 | assert_eq!( | 163 | 1 | *wallet.public_descriptor(KeychainKind::External), | 164 | 1 | external_desc | 165 | 1 | .into_wallet_descriptor(&secp, wallet.network()) | 166 | 1 | .unwrap() | 167 | 1 | .0 | 168 | 1 | ); | 169 | | } | 170 | | | 171 | 1 | Ok(()) | 172 | 1 | } |
wallet::wallet_is_persisted::run::<rusqlite::Connection, wallet::wallet_is_persisted::{closure#2}, wallet::wallet_is_persisted::{closure#3}> Line | Count | Source | 107 | 1 | fn run<Db, CreateDb, OpenDb>( | 108 | 1 | filename: &str, | 109 | 1 | create_db: CreateDb, | 110 | 1 | open_db: OpenDb, | 111 | 1 | ) -> anyhow::Result<()> | 112 | 1 | where | 113 | 1 | CreateDb: Fn(&Path) -> anyhow::Result<Db>, | 114 | 1 | OpenDb: Fn(&Path) -> anyhow::Result<Db>, | 115 | 1 | Db: WalletPersister, | 116 | 1 | Db::Error: std::error::Error + Send + Sync + 'static, | 117 | 1 | { | 118 | 1 | let temp_dir = tempfile::tempdir().expect("must create tempdir"); | 119 | 1 | let file_path = temp_dir.path().join(filename); | 120 | 1 | let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc(); | 121 | | | 122 | | // create new wallet | 123 | 1 | let wallet_spk_index = { | 124 | 1 | let mut db = create_db(&file_path)?0 ; | 125 | 1 | let mut wallet = Wallet::create(external_desc, internal_desc) | 126 | 1 | .network(Network::Testnet) | 127 | 1 | .create_wallet(&mut db)?0 ; | 128 | 1 | wallet.reveal_next_address(KeychainKind::External); | 129 | 1 | | 130 | 1 | // persist new wallet changes | 131 | 1 | assert!(wallet.persist(&mut db)?0 , "must write"0 ); | 132 | 1 | wallet.spk_index().clone() | 133 | | }; | 134 | | | 135 | | // recover wallet | 136 | | { | 137 | 1 | let mut db = open_db(&file_path).context("failed to recover db")?0 ; | 138 | 1 | let _ = Wallet::load() | 139 | 1 | .check_network(Network::Testnet) | 140 | 1 | .load_wallet(&mut db)?0 | 141 | 1 | .expect("wallet must exist"); | 142 | | } | 143 | | { | 144 | 1 | let mut db = open_db(&file_path).context("failed to recover db")?0 ; | 145 | 1 | let wallet = Wallet::load() | 146 | 1 | .descriptor(KeychainKind::External, Some(external_desc)) | 147 | 1 | .descriptor(KeychainKind::Internal, Some(internal_desc)) | 148 | 1 | .check_network(Network::Testnet) | 149 | 1 | .load_wallet(&mut db)?0 | 150 | 1 | .expect("wallet must exist"); | 151 | 1 | | 152 | 1 | assert_eq!(wallet.network(), Network::Testnet); | 153 | 1 | assert_eq!( | 154 | 1 | wallet.spk_index().keychains().collect::<Vec<_>>(), | 155 | 1 | wallet_spk_index.keychains().collect::<Vec<_>>() | 156 | 1 | ); | 157 | 1 | assert_eq!( | 158 | 1 | wallet.spk_index().last_revealed_indices(), | 159 | 1 | wallet_spk_index.last_revealed_indices() | 160 | 1 | ); | 161 | 1 | let secp = Secp256k1::new(); | 162 | 1 | assert_eq!( | 163 | 1 | *wallet.public_descriptor(KeychainKind::External), | 164 | 1 | external_desc | 165 | 1 | .into_wallet_descriptor(&secp, wallet.network()) | 166 | 1 | .unwrap() | 167 | 1 | .0 | 168 | 1 | ); | 169 | | } | 170 | | | 171 | 1 | Ok(()) | 172 | 1 | } |
|
173 | 1 | |
174 | 1 | run( |
175 | 1 | "store.db", |
176 | 1 | |path| Ok(bdk_file_store::Store::create_new(DB_MAGIC, path)?0 ), |
177 | 2 | |path| Ok(bdk_file_store::Store::open(DB_MAGIC, path)?0 ), |
178 | 1 | )?0 ; |
179 | 1 | run::<bdk_chain::rusqlite::Connection, _, _>( |
180 | 1 | "store.sqlite", |
181 | 1 | |path| Ok(bdk_chain::rusqlite::Connection::open(path)?0 ), |
182 | 2 | |path| Ok(bdk_chain::rusqlite::Connection::open(path)?0 ), |
183 | 1 | )?0 ; |
184 | | |
185 | 1 | Ok(()) |
186 | 1 | } |
187 | | |
188 | | #[test] |
189 | 1 | fn wallet_load_checks() -> anyhow::Result<()> { |
190 | 2 | fn run<Db, CreateDb, OpenDb>( |
191 | 2 | filename: &str, |
192 | 2 | create_db: CreateDb, |
193 | 2 | open_db: OpenDb, |
194 | 2 | ) -> anyhow::Result<()> |
195 | 2 | where |
196 | 2 | CreateDb: Fn(&Path) -> anyhow::Result<Db>, |
197 | 2 | OpenDb: Fn(&Path) -> anyhow::Result<Db>, |
198 | 2 | Db: WalletPersister + std::fmt::Debug, |
199 | 2 | Db::Error: std::error::Error + Send + Sync + 'static, |
200 | 2 | { |
201 | 2 | let temp_dir = tempfile::tempdir().expect("must create tempdir"); |
202 | 2 | let file_path = temp_dir.path().join(filename); |
203 | 2 | let network = Network::Testnet; |
204 | 2 | let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc(); |
205 | 2 | |
206 | 2 | // create new wallet |
207 | 2 | let _ = Wallet::create(external_desc, internal_desc) |
208 | 2 | .network(network) |
209 | 2 | .create_wallet(&mut create_db(&file_path)?0 )?0 ; |
210 | 1 | |
211 | 1 | assert_matches!0 ( |
212 | 2 | Wallet::load() |
213 | 2 | .check_network(Network::Regtest) |
214 | 2 | .load_wallet(&mut open_db(&file_path)?0 ), |
215 | 1 | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( |
216 | 1 | LoadMismatch::Network { |
217 | 1 | loaded: Network::Testnet, |
218 | 1 | expected: Network::Regtest, |
219 | 1 | } |
220 | 1 | ))), |
221 | 1 | "unexpected network check result: Regtest (check) is not Testnet (loaded)", |
222 | 1 | ); |
223 | 2 | let mainnet_hash = BlockHash::from_byte_array(ChainHash::BITCOIN.to_bytes()); |
224 | 1 | assert_matches!0 ( |
225 | 2 | Wallet::load().check_genesis_hash(mainnet_hash).load_wallet(&mut open_db(&file_path)?0 ), |
226 | 1 | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Genesis { .. }))), |
227 | 1 | "unexpected genesis hash check result: mainnet hash (check) is not testnet hash (loaded)", |
228 | 1 | ); |
229 | 1 | assert_matches!0 ( |
230 | 2 | Wallet::load() |
231 | 2 | .descriptor(KeychainKind::External, Some(internal_desc)) |
232 | 2 | .load_wallet(&mut open_db(&file_path)?0 ), |
233 | 1 | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( |
234 | 1 | LoadMismatch::Descriptor { .. } |
235 | 1 | ))), |
236 | 1 | "unexpected descriptors check result", |
237 | 1 | ); |
238 | 1 | assert_matches!0 ( |
239 | 2 | Wallet::load() |
240 | 2 | .descriptor(KeychainKind::External, Option::<&str>::None) |
241 | 2 | .load_wallet(&mut open_db(&file_path)?0 ), |
242 | 1 | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( |
243 | 1 | LoadMismatch::Descriptor { .. } |
244 | 1 | ))), |
245 | 1 | "unexpected descriptors check result", |
246 | 1 | ); |
247 | 1 | |
248 | 2 | Ok(()) |
249 | 2 | } wallet::wallet_load_checks::run::<bdk_file_store::store::Store<bdk_wallet::wallet::changeset::ChangeSet>, wallet::wallet_load_checks::{closure#0}, wallet::wallet_load_checks::{closure#1}> Line | Count | Source | 190 | 1 | fn run<Db, CreateDb, OpenDb>( | 191 | 1 | filename: &str, | 192 | 1 | create_db: CreateDb, | 193 | 1 | open_db: OpenDb, | 194 | 1 | ) -> anyhow::Result<()> | 195 | 1 | where | 196 | 1 | CreateDb: Fn(&Path) -> anyhow::Result<Db>, | 197 | 1 | OpenDb: Fn(&Path) -> anyhow::Result<Db>, | 198 | 1 | Db: WalletPersister + std::fmt::Debug, | 199 | 1 | Db::Error: std::error::Error + Send + Sync + 'static, | 200 | 1 | { | 201 | 1 | let temp_dir = tempfile::tempdir().expect("must create tempdir"); | 202 | 1 | let file_path = temp_dir.path().join(filename); | 203 | 1 | let network = Network::Testnet; | 204 | 1 | let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc(); | 205 | 1 | | 206 | 1 | // create new wallet | 207 | 1 | let _ = Wallet::create(external_desc, internal_desc) | 208 | 1 | .network(network) | 209 | 1 | .create_wallet(&mut create_db(&file_path)?0 )?0 ; | 210 | | | 211 | 0 | assert_matches!( | 212 | 1 | Wallet::load() | 213 | 1 | .check_network(Network::Regtest) | 214 | 1 | .load_wallet(&mut open_db(&file_path)?0 ), | 215 | | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( | 216 | | LoadMismatch::Network { | 217 | | loaded: Network::Testnet, | 218 | | expected: Network::Regtest, | 219 | | } | 220 | | ))), | 221 | | "unexpected network check result: Regtest (check) is not Testnet (loaded)", | 222 | | ); | 223 | 1 | let mainnet_hash = BlockHash::from_byte_array(ChainHash::BITCOIN.to_bytes()); | 224 | 0 | assert_matches!( | 225 | 1 | Wallet::load().check_genesis_hash(mainnet_hash).load_wallet(&mut open_db(&file_path)?0 ), | 226 | | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Genesis { .. }))), | 227 | | "unexpected genesis hash check result: mainnet hash (check) is not testnet hash (loaded)", | 228 | | ); | 229 | 0 | assert_matches!( | 230 | 1 | Wallet::load() | 231 | 1 | .descriptor(KeychainKind::External, Some(internal_desc)) | 232 | 1 | .load_wallet(&mut open_db(&file_path)?0 ), | 233 | | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( | 234 | | LoadMismatch::Descriptor { .. } | 235 | | ))), | 236 | | "unexpected descriptors check result", | 237 | | ); | 238 | 0 | assert_matches!( | 239 | 1 | Wallet::load() | 240 | 1 | .descriptor(KeychainKind::External, Option::<&str>::None) | 241 | 1 | .load_wallet(&mut open_db(&file_path)?0 ), | 242 | | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( | 243 | | LoadMismatch::Descriptor { .. } | 244 | | ))), | 245 | | "unexpected descriptors check result", | 246 | | ); | 247 | | | 248 | 1 | Ok(()) | 249 | 1 | } |
wallet::wallet_load_checks::run::<rusqlite::Connection, wallet::wallet_load_checks::{closure#2}, wallet::wallet_load_checks::{closure#3}> Line | Count | Source | 190 | 1 | fn run<Db, CreateDb, OpenDb>( | 191 | 1 | filename: &str, | 192 | 1 | create_db: CreateDb, | 193 | 1 | open_db: OpenDb, | 194 | 1 | ) -> anyhow::Result<()> | 195 | 1 | where | 196 | 1 | CreateDb: Fn(&Path) -> anyhow::Result<Db>, | 197 | 1 | OpenDb: Fn(&Path) -> anyhow::Result<Db>, | 198 | 1 | Db: WalletPersister + std::fmt::Debug, | 199 | 1 | Db::Error: std::error::Error + Send + Sync + 'static, | 200 | 1 | { | 201 | 1 | let temp_dir = tempfile::tempdir().expect("must create tempdir"); | 202 | 1 | let file_path = temp_dir.path().join(filename); | 203 | 1 | let network = Network::Testnet; | 204 | 1 | let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc(); | 205 | 1 | | 206 | 1 | // create new wallet | 207 | 1 | let _ = Wallet::create(external_desc, internal_desc) | 208 | 1 | .network(network) | 209 | 1 | .create_wallet(&mut create_db(&file_path)?0 )?0 ; | 210 | | | 211 | 0 | assert_matches!( | 212 | 1 | Wallet::load() | 213 | 1 | .check_network(Network::Regtest) | 214 | 1 | .load_wallet(&mut open_db(&file_path)?0 ), | 215 | | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( | 216 | | LoadMismatch::Network { | 217 | | loaded: Network::Testnet, | 218 | | expected: Network::Regtest, | 219 | | } | 220 | | ))), | 221 | | "unexpected network check result: Regtest (check) is not Testnet (loaded)", | 222 | | ); | 223 | 1 | let mainnet_hash = BlockHash::from_byte_array(ChainHash::BITCOIN.to_bytes()); | 224 | 0 | assert_matches!( | 225 | 1 | Wallet::load().check_genesis_hash(mainnet_hash).load_wallet(&mut open_db(&file_path)?0 ), | 226 | | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Genesis { .. }))), | 227 | | "unexpected genesis hash check result: mainnet hash (check) is not testnet hash (loaded)", | 228 | | ); | 229 | 0 | assert_matches!( | 230 | 1 | Wallet::load() | 231 | 1 | .descriptor(KeychainKind::External, Some(internal_desc)) | 232 | 1 | .load_wallet(&mut open_db(&file_path)?0 ), | 233 | | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( | 234 | | LoadMismatch::Descriptor { .. } | 235 | | ))), | 236 | | "unexpected descriptors check result", | 237 | | ); | 238 | 0 | assert_matches!( | 239 | 1 | Wallet::load() | 240 | 1 | .descriptor(KeychainKind::External, Option::<&str>::None) | 241 | 1 | .load_wallet(&mut open_db(&file_path)?0 ), | 242 | | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch( | 243 | | LoadMismatch::Descriptor { .. } | 244 | | ))), | 245 | | "unexpected descriptors check result", | 246 | | ); | 247 | | | 248 | 1 | Ok(()) | 249 | 1 | } |
|
250 | 1 | |
251 | 1 | run( |
252 | 1 | "store.db", |
253 | 1 | |path| { |
254 | 1 | Ok(bdk_file_store::Store::<ChangeSet>::create_new( |
255 | 1 | DB_MAGIC, path, |
256 | 1 | )?0 ) |
257 | 1 | }, |
258 | 4 | |path| Ok(bdk_file_store::Store::<ChangeSet>::open(DB_MAGIC, path)?0 ), |
259 | 1 | )?0 ; |
260 | 1 | run( |
261 | 1 | "store.sqlite", |
262 | 1 | |path| Ok(bdk_chain::rusqlite::Connection::open(path)?0 ), |
263 | 4 | |path| Ok(bdk_chain::rusqlite::Connection::open(path)?0 ), |
264 | 1 | )?0 ; |
265 | | |
266 | 1 | Ok(()) |
267 | 1 | } |
268 | | |
269 | | #[test] |
270 | 1 | fn single_descriptor_wallet_persist_and_recover() { |
271 | 1 | use bdk_chain::miniscript::Descriptor; |
272 | 1 | use bdk_chain::miniscript::DescriptorPublicKey; |
273 | 1 | use bdk_chain::rusqlite; |
274 | 1 | let temp_dir = tempfile::tempdir().unwrap(); |
275 | 1 | let db_path = temp_dir.path().join("wallet.db"); |
276 | 1 | let mut db = rusqlite::Connection::open(db_path).unwrap(); |
277 | 1 | |
278 | 1 | let desc = get_test_tr_single_sig_xprv(); |
279 | 1 | let mut wallet = Wallet::create_single(desc) |
280 | 1 | .network(Network::Testnet) |
281 | 1 | .create_wallet(&mut db) |
282 | 1 | .unwrap(); |
283 | 1 | let _ = wallet.reveal_addresses_to(KeychainKind::External, 2); |
284 | 1 | assert!(wallet.persist(&mut db).unwrap()); |
285 | | |
286 | | // should recover persisted wallet |
287 | 1 | let secp = wallet.secp_ctx(); |
288 | 1 | let (_, keymap) = <Descriptor<DescriptorPublicKey>>::parse_descriptor(secp, desc).unwrap(); |
289 | 1 | assert!(!keymap.is_empty()); |
290 | 1 | let wallet = Wallet::load() |
291 | 1 | .descriptor(KeychainKind::External, Some(desc)) |
292 | 1 | .extract_keys() |
293 | 1 | .load_wallet(&mut db) |
294 | 1 | .unwrap() |
295 | 1 | .expect("must have loaded changeset"); |
296 | 1 | assert_eq!(wallet.derivation_index(KeychainKind::External), Some(2)); |
297 | | // should have private key |
298 | 1 | assert_eq!( |
299 | 1 | wallet.get_signers(KeychainKind::External).as_key_map(secp), |
300 | 1 | keymap, |
301 | 1 | ); |
302 | | |
303 | | // should error on wrong internal params |
304 | 1 | let desc = get_test_wpkh(); |
305 | 1 | let (exp_desc, _) = <Descriptor<DescriptorPublicKey>>::parse_descriptor(secp, desc).unwrap(); |
306 | 1 | let err = Wallet::load() |
307 | 1 | .descriptor(KeychainKind::Internal, Some(desc)) |
308 | 1 | .extract_keys() |
309 | 1 | .load_wallet(&mut db); |
310 | 1 | assert_matches!( |
311 | 1 | err, |
312 | 1 | Err(LoadWithPersistError::InvalidChangeSet(LoadError::Mismatch(LoadMismatch::Descriptor { keychain, loaded, expected }))) |
313 | 1 | if keychain == KeychainKind::Internal && loaded.is_none() && expected == Some(exp_desc), |
314 | 1 | "single descriptor wallet should refuse change descriptor param" |
315 | 1 | ); |
316 | 1 | } |
317 | | |
318 | | #[test] |
319 | 1 | fn test_error_external_and_internal_are_the_same() { |
320 | 1 | // identical descriptors should fail to create wallet |
321 | 1 | let desc = get_test_wpkh(); |
322 | 1 | let err = Wallet::create(desc, desc) |
323 | 1 | .network(Network::Testnet) |
324 | 1 | .create_wallet_no_persist(); |
325 | 1 | assert!( |
326 | 1 | matches!0 (&err, Err(DescriptorError::ExternalAndInternalAreTheSame)), |
327 | 0 | "expected same descriptors error, got {:?}", |
328 | | err, |
329 | | ); |
330 | | |
331 | | // public + private of same descriptor should fail to create wallet |
332 | 1 | let desc = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)"; |
333 | 1 | let change_desc = "wpkh([3c31d632/84'/1'/0']tpubDCYwFkks2cg78N7eoYbBatsFEGje8vW8arSKW4rLwD1AU1s9KJMDRHE32JkvYERuiFjArrsH7qpWSpJATed5ShZbG9KsskA5Rmi6NSYgYN2/0/*)"; |
334 | 1 | let err = Wallet::create(desc, change_desc) |
335 | 1 | .network(Network::Testnet) |
336 | 1 | .create_wallet_no_persist(); |
337 | 1 | assert!( |
338 | 1 | matches!0 (err, Err(DescriptorError::ExternalAndInternalAreTheSame)), |
339 | 0 | "expected same descriptors error, got {:?}", |
340 | | err, |
341 | | ); |
342 | 1 | } |
343 | | |
344 | | #[test] |
345 | 1 | fn test_descriptor_checksum() { |
346 | 1 | let (wallet, _) = get_funded_wallet_wpkh(); |
347 | 1 | let checksum = wallet.descriptor_checksum(KeychainKind::External); |
348 | 1 | assert_eq!(checksum.len(), 8); |
349 | | |
350 | 1 | let raw_descriptor = wallet |
351 | 1 | .keychains() |
352 | 1 | .next() |
353 | 1 | .unwrap() |
354 | 1 | .1 |
355 | 1 | .to_string() |
356 | 1 | .split_once('#') |
357 | 1 | .unwrap() |
358 | 1 | .0 |
359 | 1 | .to_string(); |
360 | 1 | assert_eq!(calc_checksum(&raw_descriptor).unwrap(), checksum); |
361 | 1 | } |
362 | | |
363 | | #[test] |
364 | 1 | fn test_get_funded_wallet_balance() { |
365 | 1 | let (wallet, _) = get_funded_wallet_wpkh(); |
366 | 1 | |
367 | 1 | // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 |
368 | 1 | // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 |
369 | 1 | // sats are the transaction fee. |
370 | 1 | assert_eq!(wallet.balance().confirmed, Amount::from_sat(50_000)); |
371 | 1 | } |
372 | | |
373 | | #[test] |
374 | 1 | fn test_get_funded_wallet_sent_and_received() { |
375 | 1 | let (wallet, txid) = get_funded_wallet_wpkh(); |
376 | 1 | |
377 | 1 | let mut tx_amounts: Vec<(Txid, (Amount, Amount))> = wallet |
378 | 1 | .transactions() |
379 | 2 | .map(|ct| (ct.tx_node.txid, wallet.sent_and_received(&ct.tx_node))) |
380 | 1 | .collect(); |
381 | 1 | tx_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0)); |
382 | 1 | |
383 | 1 | let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; |
384 | 1 | let (sent, received) = wallet.sent_and_received(&tx); |
385 | 1 | |
386 | 1 | // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 |
387 | 1 | // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 |
388 | 1 | // sats are the transaction fee. |
389 | 1 | assert_eq!(sent.to_sat(), 76_000); |
390 | 1 | assert_eq!(received.to_sat(), 50_000); |
391 | 1 | } |
392 | | |
393 | | #[test] |
394 | 1 | fn test_get_funded_wallet_tx_fees() { |
395 | 1 | let (wallet, txid) = get_funded_wallet_wpkh(); |
396 | 1 | |
397 | 1 | let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; |
398 | 1 | let tx_fee = wallet.calculate_fee(&tx).expect("transaction fee"); |
399 | 1 | |
400 | 1 | // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 |
401 | 1 | // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 |
402 | 1 | // sats are the transaction fee. |
403 | 1 | assert_eq!(tx_fee, Amount::from_sat(1000)) |
404 | 1 | } |
405 | | |
406 | | #[test] |
407 | 1 | fn test_get_funded_wallet_tx_fee_rate() { |
408 | 1 | let (wallet, txid) = get_funded_wallet_wpkh(); |
409 | 1 | |
410 | 1 | let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; |
411 | 1 | let tx_fee_rate = wallet |
412 | 1 | .calculate_fee_rate(&tx) |
413 | 1 | .expect("transaction fee rate"); |
414 | 1 | |
415 | 1 | // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 |
416 | 1 | // to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000 |
417 | 1 | // sats are the transaction fee. |
418 | 1 | |
419 | 1 | // tx weight = 452 wu, as vbytes = (452 + 3) / 4 = 113 |
420 | 1 | // fee_rate (sats per kwu) = fee / weight = 1000sat / 0.452kwu = 2212 |
421 | 1 | // fee_rate (sats per vbyte ceil) = fee / vsize = 1000sat / 113vb = 9 |
422 | 1 | assert_eq!(tx_fee_rate.to_sat_per_kwu(), 2212); |
423 | 1 | assert_eq!(tx_fee_rate.to_sat_per_vb_ceil(), 9); |
424 | 1 | } |
425 | | |
426 | | #[test] |
427 | 1 | fn test_list_output() { |
428 | 1 | let (wallet, txid) = get_funded_wallet_wpkh(); |
429 | 1 | let txos = wallet |
430 | 1 | .list_output() |
431 | 2 | .map(|op| (op.outpoint, op)) |
432 | 1 | .collect::<std::collections::BTreeMap<_, _>>(); |
433 | 1 | assert_eq!(txos.len(), 2); |
434 | 3 | for (op, txo2 ) in txos { |
435 | 2 | if op.txid == txid { |
436 | 1 | assert_eq!(txo.txout.value.to_sat(), 50_000); |
437 | 1 | assert!(!txo.is_spent); |
438 | | } else { |
439 | 1 | assert_eq!(txo.txout.value.to_sat(), 76_000); |
440 | 1 | assert!(txo.is_spent); |
441 | | } |
442 | | } |
443 | 1 | } |
444 | | |
445 | | macro_rules! assert_fee_rate { |
446 | | ($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({ |
447 | | let psbt = $psbt.clone(); |
448 | | #[allow(unused_mut)] |
449 | | let mut tx = $psbt.clone().extract_tx().expect("failed to extract tx"); |
450 | | $( |
451 | | $( $add_signature )* |
452 | | for txin in &mut tx.input { |
453 | | txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature |
454 | | } |
455 | | )* |
456 | | |
457 | | #[allow(unused_mut)] |
458 | | #[allow(unused_assignments)] |
459 | | let mut dust_change = false; |
460 | | $( |
461 | | $( $dust_change )* |
462 | | dust_change = true; |
463 | | )* |
464 | | |
465 | | let fee_amount = psbt |
466 | | .inputs |
467 | | .iter() |
468 | 15 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) wallet::test_create_tx_default_fee_rate::{closure#0} Line | Count | Source | 468 | 1 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) |
wallet::test_create_tx_custom_fee_rate::{closure#0} Line | Count | Source | 468 | 1 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) |
wallet::test_bump_fee_reduce_change::{closure#2} Line | Count | Source | 468 | 1 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) |
wallet::test_bump_fee_reduce_single_recipient::{closure#0} Line | Count | Source | 468 | 1 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) |
wallet::test_bump_fee_add_input::{closure#2} Line | Count | Source | 468 | 2 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) |
wallet::test_bump_fee_no_change_add_input_and_change::{closure#2} Line | Count | Source | 468 | 2 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) |
wallet::test_bump_fee_add_input_change_dust::{closure#1} Line | Count | Source | 468 | 2 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) |
wallet::test_bump_fee_force_add_input::{closure#2} Line | Count | Source | 468 | 2 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) |
wallet::test_fee_amount_negative_drain_val::{closure#0} Line | Count | Source | 468 | 1 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) |
wallet::test_fee_rate_sign_no_grinding_high_r::{closure#1} Line | Count | Source | 468 | 1 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) |
wallet::test_fee_rate_sign_grinding_low_r::{closure#0} Line | Count | Source | 468 | 1 | .fold(Amount::ZERO, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value) |
|
469 | | - psbt |
470 | | .unsigned_tx |
471 | | .output |
472 | | .iter() |
473 | 18 | .fold(Amount::ZERO, |acc, o| acc + o.value); wallet::test_create_tx_default_fee_rate::{closure#1} Line | Count | Source | 473 | 2 | .fold(Amount::ZERO, |acc, o| acc + o.value); |
wallet::test_create_tx_custom_fee_rate::{closure#1} Line | Count | Source | 473 | 2 | .fold(Amount::ZERO, |acc, o| acc + o.value); |
wallet::test_bump_fee_reduce_change::{closure#3} Line | Count | Source | 473 | 2 | .fold(Amount::ZERO, |acc, o| acc + o.value); |
wallet::test_bump_fee_reduce_single_recipient::{closure#1} Line | Count | Source | 473 | 1 | .fold(Amount::ZERO, |acc, o| acc + o.value); |
wallet::test_bump_fee_add_input::{closure#3} Line | Count | Source | 473 | 2 | .fold(Amount::ZERO, |acc, o| acc + o.value); |
wallet::test_bump_fee_no_change_add_input_and_change::{closure#3} Line | Count | Source | 473 | 2 | .fold(Amount::ZERO, |acc, o| acc + o.value); |
wallet::test_bump_fee_add_input_change_dust::{closure#2} Line | Count | Source | 473 | 1 | .fold(Amount::ZERO, |acc, o| acc + o.value); |
wallet::test_bump_fee_force_add_input::{closure#3} Line | Count | Source | 473 | 2 | .fold(Amount::ZERO, |acc, o| acc + o.value); |
wallet::test_fee_amount_negative_drain_val::{closure#1} Line | Count | Source | 473 | 1 | .fold(Amount::ZERO, |acc, o| acc + o.value); |
wallet::test_fee_rate_sign_no_grinding_high_r::{closure#2} Line | Count | Source | 473 | 2 | .fold(Amount::ZERO, |acc, o| acc + o.value); |
wallet::test_fee_rate_sign_grinding_low_r::{closure#1} Line | Count | Source | 473 | 1 | .fold(Amount::ZERO, |acc, o| acc + o.value); |
|
474 | | |
475 | | assert_eq!(fee_amount, $fees); |
476 | | |
477 | | let tx_fee_rate = (fee_amount / tx.weight()) |
478 | | .to_sat_per_kwu(); |
479 | | let fee_rate = $fee_rate.to_sat_per_kwu(); |
480 | | let half_default = FeeRate::BROADCAST_MIN.checked_div(2) |
481 | | .unwrap() |
482 | | .to_sat_per_kwu(); |
483 | | |
484 | | if !dust_change { |
485 | | assert!(tx_fee_rate >= fee_rate && tx_fee_rate - fee_rate < half_default, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate); |
486 | | } else { |
487 | | assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate); |
488 | | } |
489 | | }); |
490 | | } |
491 | | |
492 | | macro_rules! from_str { |
493 | | ($e:expr, $t:ty) => {{ |
494 | | use core::str::FromStr; |
495 | | <$t>::from_str($e).unwrap() |
496 | | }}; |
497 | | |
498 | | ($e:expr) => { |
499 | | from_str!($e, _) |
500 | | }; |
501 | | } |
502 | | |
503 | | #[test] |
504 | | #[should_panic(expected = "NoRecipients")] |
505 | 1 | fn test_create_tx_empty_recipients() { |
506 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
507 | 1 | wallet.build_tx().finish().unwrap(); |
508 | 1 | } |
509 | | |
510 | | #[test] |
511 | | #[should_panic(expected = "NoUtxosSelected")] |
512 | 1 | fn test_create_tx_manually_selected_empty_utxos() { |
513 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
514 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
515 | 1 | let mut builder = wallet.build_tx(); |
516 | 1 | builder |
517 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
518 | 1 | .manually_selected_only(); |
519 | 1 | builder.finish().unwrap(); |
520 | 1 | } |
521 | | |
522 | | #[test] |
523 | 1 | fn test_create_tx_version_0() { |
524 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
525 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
526 | 1 | let mut builder = wallet.build_tx(); |
527 | 1 | builder |
528 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
529 | 1 | .version(0); |
530 | 1 | assert!(matches!0 (builder.finish(), Err(CreateTxError::Version0))); |
531 | 1 | } |
532 | | |
533 | | #[test] |
534 | 1 | fn test_create_tx_version_1_csv() { |
535 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); |
536 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
537 | 1 | let mut builder = wallet.build_tx(); |
538 | 1 | builder |
539 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
540 | 1 | .version(1); |
541 | 1 | assert!(matches!0 (builder.finish(), Err(CreateTxError::Version1Csv))); |
542 | 1 | } |
543 | | |
544 | | #[test] |
545 | 1 | fn test_create_tx_custom_version() { |
546 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
547 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
548 | 1 | let mut builder = wallet.build_tx(); |
549 | 1 | builder |
550 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
551 | 1 | .version(42); |
552 | 1 | let psbt = builder.finish().unwrap(); |
553 | 1 | |
554 | 1 | assert_eq!(psbt.unsigned_tx.version.0, 42); |
555 | 1 | } |
556 | | |
557 | | #[test] |
558 | 1 | fn test_create_tx_default_locktime_is_last_sync_height() { |
559 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
560 | 1 | |
561 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
562 | 1 | let mut builder = wallet.build_tx(); |
563 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
564 | 1 | let psbt = builder.finish().unwrap(); |
565 | 1 | |
566 | 1 | // Since we never synced the wallet we don't have a last_sync_height |
567 | 1 | // we could use to try to prevent fee sniping. We default to 0. |
568 | 1 | assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 2_000); |
569 | 1 | } |
570 | | |
571 | | #[test] |
572 | 1 | fn test_create_tx_fee_sniping_locktime_last_sync() { |
573 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
574 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
575 | 1 | let mut builder = wallet.build_tx(); |
576 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
577 | 1 | |
578 | 1 | let psbt = builder.finish().unwrap(); |
579 | 1 | |
580 | 1 | // If there's no current_height we're left with using the last sync height |
581 | 1 | assert_eq!( |
582 | 1 | psbt.unsigned_tx.lock_time.to_consensus_u32(), |
583 | 1 | wallet.latest_checkpoint().height() |
584 | 1 | ); |
585 | 1 | } |
586 | | |
587 | | #[test] |
588 | 1 | fn test_create_tx_default_locktime_cltv() { |
589 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); |
590 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
591 | 1 | let mut builder = wallet.build_tx(); |
592 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
593 | 1 | let psbt = builder.finish().unwrap(); |
594 | 1 | |
595 | 1 | assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 100_000); |
596 | 1 | } |
597 | | |
598 | | #[test] |
599 | 1 | fn test_create_tx_custom_locktime() { |
600 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
601 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
602 | 1 | let mut builder = wallet.build_tx(); |
603 | 1 | builder |
604 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
605 | 1 | .current_height(630_001) |
606 | 1 | .nlocktime(absolute::LockTime::from_height(630_000).unwrap()); |
607 | 1 | let psbt = builder.finish().unwrap(); |
608 | 1 | |
609 | 1 | // When we explicitly specify a nlocktime |
610 | 1 | // we don't try any fee sniping prevention trick |
611 | 1 | // (we ignore the current_height) |
612 | 1 | assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 630_000); |
613 | 1 | } |
614 | | |
615 | | #[test] |
616 | 1 | fn test_create_tx_custom_locktime_compatible_with_cltv() { |
617 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); |
618 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
619 | 1 | let mut builder = wallet.build_tx(); |
620 | 1 | builder |
621 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
622 | 1 | .nlocktime(absolute::LockTime::from_height(630_000).unwrap()); |
623 | 1 | let psbt = builder.finish().unwrap(); |
624 | 1 | |
625 | 1 | assert_eq!(psbt.unsigned_tx.lock_time.to_consensus_u32(), 630_000); |
626 | 1 | } |
627 | | |
628 | | #[test] |
629 | 1 | fn test_create_tx_custom_locktime_incompatible_with_cltv() { |
630 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); |
631 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
632 | 1 | let mut builder = wallet.build_tx(); |
633 | 1 | builder |
634 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
635 | 1 | .nlocktime(absolute::LockTime::from_height(50000).unwrap()); |
636 | 1 | assert!(matches!(builder.finish(), |
637 | 1 | Err(CreateTxError::LockTime { requested, required }) |
638 | 1 | if requested.to_consensus_u32() == 50_000 && required.to_consensus_u32() == 100_000)); |
639 | 1 | } |
640 | | |
641 | | #[test] |
642 | 1 | fn test_create_tx_no_rbf_csv() { |
643 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); |
644 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
645 | 1 | let mut builder = wallet.build_tx(); |
646 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
647 | 1 | let psbt = builder.finish().unwrap(); |
648 | 1 | |
649 | 1 | assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); |
650 | 1 | } |
651 | | |
652 | | #[test] |
653 | 1 | fn test_create_tx_with_default_rbf_csv() { |
654 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); |
655 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
656 | 1 | let mut builder = wallet.build_tx(); |
657 | 1 | builder |
658 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
659 | 1 | .enable_rbf(); |
660 | 1 | let psbt = builder.finish().unwrap(); |
661 | 1 | // When CSV is enabled it takes precedence over the rbf value (unless forced by the user). |
662 | 1 | // It will be set to the OP_CSV value, in this case 6 |
663 | 1 | assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); |
664 | 1 | } |
665 | | |
666 | | #[test] |
667 | 1 | fn test_create_tx_with_custom_rbf_csv() { |
668 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); |
669 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
670 | 1 | let mut builder = wallet.build_tx(); |
671 | 1 | builder |
672 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
673 | 1 | .enable_rbf_with_sequence(Sequence(3)); |
674 | 1 | assert!(matches!(builder.finish(), |
675 | 1 | Err(CreateTxError::RbfSequenceCsv { rbf, csv }) |
676 | 1 | if rbf.to_consensus_u32() == 3 && csv.to_consensus_u32() == 6)); |
677 | 1 | } |
678 | | |
679 | | #[test] |
680 | 1 | fn test_create_tx_no_rbf_cltv() { |
681 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); |
682 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
683 | 1 | let mut builder = wallet.build_tx(); |
684 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
685 | 1 | let psbt = builder.finish().unwrap(); |
686 | 1 | |
687 | 1 | assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); |
688 | 1 | } |
689 | | |
690 | | #[test] |
691 | 1 | fn test_create_tx_invalid_rbf_sequence() { |
692 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
693 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
694 | 1 | let mut builder = wallet.build_tx(); |
695 | 1 | builder |
696 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
697 | 1 | .enable_rbf_with_sequence(Sequence(0xFFFFFFFE)); |
698 | 1 | assert!(matches!0 (builder.finish(), Err(CreateTxError::RbfSequence))); |
699 | 1 | } |
700 | | |
701 | | #[test] |
702 | 1 | fn test_create_tx_custom_rbf_sequence() { |
703 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
704 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
705 | 1 | let mut builder = wallet.build_tx(); |
706 | 1 | builder |
707 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
708 | 1 | .enable_rbf_with_sequence(Sequence(0xDEADBEEF)); |
709 | 1 | let psbt = builder.finish().unwrap(); |
710 | 1 | |
711 | 1 | assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF)); |
712 | 1 | } |
713 | | |
714 | | #[test] |
715 | 1 | fn test_create_tx_change_policy() { |
716 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
717 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
718 | 1 | let mut builder = wallet.build_tx(); |
719 | 1 | builder |
720 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
721 | 1 | .do_not_spend_change(); |
722 | 1 | assert!(builder.finish().is_ok()); |
723 | | |
724 | | // wallet has no change, so setting `only_spend_change` |
725 | | // should cause tx building to fail |
726 | 1 | let mut builder = wallet.build_tx(); |
727 | 1 | builder |
728 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
729 | 1 | .only_spend_change(); |
730 | 1 | assert!(matches!0 ( |
731 | 1 | builder.finish(), |
732 | | Err(CreateTxError::CoinSelection( |
733 | | coin_selection::Error::InsufficientFunds { .. } |
734 | | )), |
735 | | )); |
736 | 1 | } |
737 | | |
738 | | #[test] |
739 | 1 | fn test_create_tx_default_sequence() { |
740 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
741 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
742 | 1 | let mut builder = wallet.build_tx(); |
743 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
744 | 1 | let psbt = builder.finish().unwrap(); |
745 | 1 | |
746 | 1 | assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); |
747 | 1 | } |
748 | | |
749 | | macro_rules! check_fee { |
750 | | ($wallet:expr, $psbt: expr) => {{ |
751 | | let tx = $psbt.clone().extract_tx().expect("failed to extract tx"); |
752 | | let tx_fee = $wallet.calculate_fee(&tx).ok(); |
753 | | assert_eq!(tx_fee, $psbt.fee_amount()); |
754 | | tx_fee |
755 | | }}; |
756 | | } |
757 | | |
758 | | #[test] |
759 | 1 | fn test_create_tx_drain_wallet_and_drain_to() { |
760 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
761 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
762 | 1 | let mut builder = wallet.build_tx(); |
763 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
764 | 1 | let psbt = builder.finish().unwrap(); |
765 | 1 | let fee = check_fee!(wallet, psbt); |
766 | | |
767 | 1 | assert_eq!(psbt.unsigned_tx.output.len(), 1); |
768 | 1 | assert_eq!( |
769 | 1 | psbt.unsigned_tx.output[0].value, |
770 | 1 | Amount::from_sat(50_000) - fee.unwrap_or(Amount::ZERO) |
771 | 1 | ); |
772 | 1 | } |
773 | | |
774 | | #[test] |
775 | 1 | fn test_create_tx_drain_wallet_and_drain_to_and_with_recipient() { |
776 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
777 | 1 | let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") |
778 | 1 | .unwrap() |
779 | 1 | .assume_checked(); |
780 | 1 | let drain_addr = wallet.next_unused_address(KeychainKind::External); |
781 | 1 | let mut builder = wallet.build_tx(); |
782 | 1 | builder |
783 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(20_000)) |
784 | 1 | .drain_to(drain_addr.script_pubkey()) |
785 | 1 | .drain_wallet(); |
786 | 1 | let psbt = builder.finish().unwrap(); |
787 | 1 | let fee = check_fee!(wallet, psbt); |
788 | 1 | let outputs = psbt.unsigned_tx.output; |
789 | 1 | |
790 | 1 | assert_eq!(outputs.len(), 2); |
791 | 1 | let main_output = outputs |
792 | 1 | .iter() |
793 | 2 | .find(|x| x.script_pubkey == addr.script_pubkey()) |
794 | 1 | .unwrap(); |
795 | 1 | let drain_output = outputs |
796 | 1 | .iter() |
797 | 1 | .find(|x| x.script_pubkey == drain_addr.script_pubkey()) |
798 | 1 | .unwrap(); |
799 | 1 | assert_eq!(main_output.value, Amount::from_sat(20_000)); |
800 | 1 | assert_eq!( |
801 | 1 | drain_output.value, |
802 | 1 | Amount::from_sat(30_000) - fee.unwrap_or(Amount::ZERO) |
803 | 1 | ); |
804 | 1 | } |
805 | | |
806 | | #[test] |
807 | 1 | fn test_create_tx_drain_to_and_utxos() { |
808 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
809 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
810 | 1 | let utxos: Vec<_> = wallet.list_unspent().map(|u| u.outpoint).collect(); |
811 | 1 | let mut builder = wallet.build_tx(); |
812 | 1 | builder |
813 | 1 | .drain_to(addr.script_pubkey()) |
814 | 1 | .add_utxos(&utxos) |
815 | 1 | .unwrap(); |
816 | 1 | let psbt = builder.finish().unwrap(); |
817 | 1 | let fee = check_fee!(wallet, psbt); |
818 | | |
819 | 1 | assert_eq!(psbt.unsigned_tx.output.len(), 1); |
820 | 1 | assert_eq!( |
821 | 1 | psbt.unsigned_tx.output[0].value, |
822 | 1 | Amount::from_sat(50_000) - fee.unwrap_or(Amount::ZERO) |
823 | 1 | ); |
824 | 1 | } |
825 | | |
826 | | #[test] |
827 | | #[should_panic(expected = "NoRecipients")] |
828 | 1 | fn test_create_tx_drain_to_no_drain_wallet_no_utxos() { |
829 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
830 | 1 | let drain_addr = wallet.next_unused_address(KeychainKind::External); |
831 | 1 | let mut builder = wallet.build_tx(); |
832 | 1 | builder.drain_to(drain_addr.script_pubkey()); |
833 | 1 | builder.finish().unwrap(); |
834 | 1 | } |
835 | | |
836 | | #[test] |
837 | 1 | fn test_create_tx_default_fee_rate() { |
838 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
839 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
840 | 1 | let mut builder = wallet.build_tx(); |
841 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
842 | 1 | let psbt = builder.finish().unwrap(); |
843 | 1 | let fee = check_fee!(wallet, psbt); |
844 | | |
845 | | assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::BROADCAST_MIN, @add_signature); |
846 | 1 | } |
847 | | |
848 | | #[test] |
849 | 1 | fn test_create_tx_custom_fee_rate() { |
850 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
851 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
852 | 1 | let mut builder = wallet.build_tx(); |
853 | 1 | builder |
854 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
855 | 1 | .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); |
856 | 1 | let psbt = builder.finish().unwrap(); |
857 | 1 | let fee = check_fee!(wallet, psbt); |
858 | | |
859 | | assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(5), @add_signature); |
860 | 1 | } |
861 | | |
862 | | #[test] |
863 | 1 | fn test_create_tx_absolute_fee() { |
864 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
865 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
866 | 1 | let mut builder = wallet.build_tx(); |
867 | 1 | builder |
868 | 1 | .drain_to(addr.script_pubkey()) |
869 | 1 | .drain_wallet() |
870 | 1 | .fee_absolute(Amount::from_sat(100)); |
871 | 1 | let psbt = builder.finish().unwrap(); |
872 | 1 | let fee = check_fee!(wallet, psbt); |
873 | | |
874 | 1 | assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(100)); |
875 | 1 | assert_eq!(psbt.unsigned_tx.output.len(), 1); |
876 | 1 | assert_eq!( |
877 | 1 | psbt.unsigned_tx.output[0].value, |
878 | 1 | Amount::from_sat(50_000) - fee.unwrap_or(Amount::ZERO) |
879 | 1 | ); |
880 | 1 | } |
881 | | |
882 | | #[test] |
883 | 1 | fn test_create_tx_absolute_zero_fee() { |
884 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
885 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
886 | 1 | let mut builder = wallet.build_tx(); |
887 | 1 | builder |
888 | 1 | .drain_to(addr.script_pubkey()) |
889 | 1 | .drain_wallet() |
890 | 1 | .fee_absolute(Amount::ZERO); |
891 | 1 | let psbt = builder.finish().unwrap(); |
892 | 1 | let fee = check_fee!(wallet, psbt); |
893 | | |
894 | 1 | assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::ZERO); |
895 | 1 | assert_eq!(psbt.unsigned_tx.output.len(), 1); |
896 | 1 | assert_eq!( |
897 | 1 | psbt.unsigned_tx.output[0].value, |
898 | 1 | Amount::from_sat(50_000) - fee.unwrap_or(Amount::ZERO) |
899 | 1 | ); |
900 | 1 | } |
901 | | |
902 | | #[test] |
903 | | #[should_panic(expected = "InsufficientFunds")] |
904 | 1 | fn test_create_tx_absolute_high_fee() { |
905 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
906 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
907 | 1 | let mut builder = wallet.build_tx(); |
908 | 1 | builder |
909 | 1 | .drain_to(addr.script_pubkey()) |
910 | 1 | .drain_wallet() |
911 | 1 | .fee_absolute(Amount::from_sat(60_000)); |
912 | 1 | let _ = builder.finish().unwrap(); |
913 | 1 | } |
914 | | |
915 | | #[test] |
916 | 1 | fn test_create_tx_add_change() { |
917 | 1 | use bdk_wallet::tx_builder::TxOrdering; |
918 | 1 | let seed = [0; 32]; |
919 | 1 | let mut rng: StdRng = SeedableRng::from_seed(seed); |
920 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
921 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
922 | 1 | let mut builder = wallet.build_tx(); |
923 | 1 | builder |
924 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
925 | 1 | .ordering(TxOrdering::Shuffle); |
926 | 1 | let psbt = builder.finish_with_aux_rand(&mut rng).unwrap(); |
927 | 1 | let fee = check_fee!(wallet, psbt); |
928 | | |
929 | 1 | assert_eq!(psbt.unsigned_tx.output.len(), 2); |
930 | 1 | assert_eq!(psbt.unsigned_tx.output[0].value, Amount::from_sat(25_000)); |
931 | 1 | assert_eq!( |
932 | 1 | psbt.unsigned_tx.output[1].value, |
933 | 1 | Amount::from_sat(25_000) - fee.unwrap_or(Amount::ZERO) |
934 | 1 | ); |
935 | 1 | } |
936 | | |
937 | | #[test] |
938 | 1 | fn test_create_tx_skip_change_dust() { |
939 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
940 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
941 | 1 | let mut builder = wallet.build_tx(); |
942 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(49_800)); |
943 | 1 | let psbt = builder.finish().unwrap(); |
944 | 1 | let fee = check_fee!(wallet, psbt); |
945 | | |
946 | 1 | assert_eq!(psbt.unsigned_tx.output.len(), 1); |
947 | 1 | assert_eq!(psbt.unsigned_tx.output[0].value.to_sat(), 49_800); |
948 | 1 | assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(200)); |
949 | 1 | } |
950 | | |
951 | | #[test] |
952 | | #[should_panic(expected = "InsufficientFunds")] |
953 | 1 | fn test_create_tx_drain_to_dust_amount() { |
954 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
955 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
956 | 1 | // very high fee rate, so that the only output would be below dust |
957 | 1 | let mut builder = wallet.build_tx(); |
958 | 1 | builder |
959 | 1 | .drain_to(addr.script_pubkey()) |
960 | 1 | .drain_wallet() |
961 | 1 | .fee_rate(FeeRate::from_sat_per_vb_unchecked(454)); |
962 | 1 | builder.finish().unwrap(); |
963 | 1 | } |
964 | | |
965 | | #[test] |
966 | 1 | fn test_create_tx_ordering_respected() { |
967 | 1 | use alloc::sync::Arc; |
968 | 1 | |
969 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
970 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
971 | 1 | |
972 | 1 | let bip69_txin_cmp = |tx_a: &TxIn, tx_b: &TxIn| { |
973 | 0 | let project_outpoint = |t: &TxIn| (t.previous_output.txid, t.previous_output.vout); |
974 | 0 | project_outpoint(tx_a).cmp(&project_outpoint(tx_b)) |
975 | 0 | }; |
976 | | |
977 | 3 | let bip69_txout_cmp1 = |tx_a: &TxOut, tx_b: &TxOut| { |
978 | 6 | let project_utxo = |t: &TxOut| (t.value, t.script_pubkey.clone()); |
979 | 3 | project_utxo(tx_a).cmp(&project_utxo(tx_b)) |
980 | 3 | }; |
981 | | |
982 | 1 | let custom_bip69_ordering = bdk_wallet::tx_builder::TxOrdering::Custom { |
983 | 1 | input_sort: Arc::new(bip69_txin_cmp), |
984 | 1 | output_sort: Arc::new(bip69_txout_cmp), |
985 | 1 | }; |
986 | 1 | |
987 | 1 | let mut builder = wallet.build_tx(); |
988 | 1 | builder |
989 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) |
990 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)) |
991 | 1 | .ordering(custom_bip69_ordering); |
992 | 1 | |
993 | 1 | let psbt = builder.finish().unwrap(); |
994 | 1 | let fee = check_fee!(wallet, psbt); |
995 | | |
996 | 1 | assert_eq!(psbt.unsigned_tx.output.len(), 3); |
997 | 1 | assert_eq!( |
998 | 1 | psbt.unsigned_tx.output[0].value, |
999 | 1 | Amount::from_sat(10_000) - fee.unwrap_or(Amount::ZERO) |
1000 | 1 | ); |
1001 | 1 | assert_eq!(psbt.unsigned_tx.output[1].value, Amount::from_sat(10_000)); |
1002 | 1 | assert_eq!(psbt.unsigned_tx.output[2].value, Amount::from_sat(30_000)); |
1003 | 1 | } |
1004 | | |
1005 | | #[test] |
1006 | 1 | fn test_create_tx_default_sighash() { |
1007 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1008 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1009 | 1 | let mut builder = wallet.build_tx(); |
1010 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)); |
1011 | 1 | let psbt = builder.finish().unwrap(); |
1012 | 1 | |
1013 | 1 | assert_eq!(psbt.inputs[0].sighash_type, None); |
1014 | 1 | } |
1015 | | |
1016 | | #[test] |
1017 | 1 | fn test_create_tx_custom_sighash() { |
1018 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1019 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1020 | 1 | let mut builder = wallet.build_tx(); |
1021 | 1 | builder |
1022 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) |
1023 | 1 | .sighash(EcdsaSighashType::Single.into()); |
1024 | 1 | let psbt = builder.finish().unwrap(); |
1025 | 1 | |
1026 | 1 | assert_eq!( |
1027 | 1 | psbt.inputs[0].sighash_type, |
1028 | 1 | Some(EcdsaSighashType::Single.into()) |
1029 | 1 | ); |
1030 | 1 | } |
1031 | | |
1032 | | #[test] |
1033 | 1 | fn test_create_tx_input_hd_keypaths() { |
1034 | 1 | use bitcoin::bip32::{DerivationPath, Fingerprint}; |
1035 | 1 | use core::str::FromStr; |
1036 | 1 | |
1037 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); |
1038 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1039 | 1 | let mut builder = wallet.build_tx(); |
1040 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
1041 | 1 | let psbt = builder.finish().unwrap(); |
1042 | 1 | |
1043 | 1 | assert_eq!(psbt.inputs[0].bip32_derivation.len(), 1); |
1044 | 1 | assert_eq!( |
1045 | 1 | psbt.inputs[0].bip32_derivation.values().next().unwrap(), |
1046 | 1 | &( |
1047 | 1 | Fingerprint::from_str("d34db33f").unwrap(), |
1048 | 1 | DerivationPath::from_str("m/44'/0'/0'/0/0").unwrap() |
1049 | 1 | ) |
1050 | 1 | ); |
1051 | 1 | } |
1052 | | |
1053 | | #[test] |
1054 | 1 | fn test_create_tx_output_hd_keypaths() { |
1055 | 1 | use bitcoin::bip32::{DerivationPath, Fingerprint}; |
1056 | 1 | use core::str::FromStr; |
1057 | 1 | |
1058 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/44'/0'/0']tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)"); |
1059 | 1 | |
1060 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1061 | 1 | let mut builder = wallet.build_tx(); |
1062 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
1063 | 1 | let psbt = builder.finish().unwrap(); |
1064 | 1 | |
1065 | 1 | assert_eq!(psbt.outputs[0].bip32_derivation.len(), 1); |
1066 | 1 | let expected_derivation_path = format!("m/44'/0'/0'/0/{}", addr.index); |
1067 | 1 | assert_eq!( |
1068 | 1 | psbt.outputs[0].bip32_derivation.values().next().unwrap(), |
1069 | 1 | &( |
1070 | 1 | Fingerprint::from_str("d34db33f").unwrap(), |
1071 | 1 | DerivationPath::from_str(&expected_derivation_path).unwrap() |
1072 | 1 | ) |
1073 | 1 | ); |
1074 | 1 | } |
1075 | | |
1076 | | #[test] |
1077 | 1 | fn test_create_tx_set_redeem_script_p2sh() { |
1078 | 1 | use bitcoin::hex::FromHex; |
1079 | 1 | |
1080 | 1 | let (mut wallet, _) = |
1081 | 1 | get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); |
1082 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1083 | 1 | let mut builder = wallet.build_tx(); |
1084 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
1085 | 1 | let psbt = builder.finish().unwrap(); |
1086 | 1 | |
1087 | 1 | assert_eq!( |
1088 | 1 | psbt.inputs[0].redeem_script, |
1089 | 1 | Some(ScriptBuf::from( |
1090 | 1 | Vec::<u8>::from_hex( |
1091 | 1 | "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" |
1092 | 1 | ) |
1093 | 1 | .unwrap() |
1094 | 1 | )) |
1095 | 1 | ); |
1096 | 1 | assert_eq!(psbt.inputs[0].witness_script, None); |
1097 | 1 | } |
1098 | | |
1099 | | #[test] |
1100 | 1 | fn test_create_tx_set_witness_script_p2wsh() { |
1101 | 1 | use bitcoin::hex::FromHex; |
1102 | 1 | |
1103 | 1 | let (mut wallet, _) = |
1104 | 1 | get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); |
1105 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1106 | 1 | let mut builder = wallet.build_tx(); |
1107 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
1108 | 1 | let psbt = builder.finish().unwrap(); |
1109 | 1 | |
1110 | 1 | assert_eq!(psbt.inputs[0].redeem_script, None); |
1111 | 1 | assert_eq!( |
1112 | 1 | psbt.inputs[0].witness_script, |
1113 | 1 | Some(ScriptBuf::from( |
1114 | 1 | Vec::<u8>::from_hex( |
1115 | 1 | "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac" |
1116 | 1 | ) |
1117 | 1 | .unwrap() |
1118 | 1 | )) |
1119 | 1 | ); |
1120 | 1 | } |
1121 | | |
1122 | | #[test] |
1123 | 1 | fn test_create_tx_set_redeem_witness_script_p2wsh_p2sh() { |
1124 | 1 | let (mut wallet, _) = |
1125 | 1 | get_funded_wallet("sh(wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)))"); |
1126 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1127 | 1 | let mut builder = wallet.build_tx(); |
1128 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
1129 | 1 | let psbt = builder.finish().unwrap(); |
1130 | 1 | |
1131 | 1 | let script = ScriptBuf::from_hex( |
1132 | 1 | "21032b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3ac", |
1133 | 1 | ) |
1134 | 1 | .unwrap(); |
1135 | 1 | |
1136 | 1 | assert_eq!(psbt.inputs[0].redeem_script, Some(script.to_p2wsh())); |
1137 | 1 | assert_eq!(psbt.inputs[0].witness_script, Some(script)); |
1138 | 1 | } |
1139 | | |
1140 | | #[test] |
1141 | 1 | fn test_create_tx_non_witness_utxo() { |
1142 | 1 | let (mut wallet, _) = |
1143 | 1 | get_funded_wallet("sh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); |
1144 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1145 | 1 | let mut builder = wallet.build_tx(); |
1146 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
1147 | 1 | let psbt = builder.finish().unwrap(); |
1148 | 1 | |
1149 | 1 | assert!(psbt.inputs[0].non_witness_utxo.is_some()); |
1150 | 1 | assert!(psbt.inputs[0].witness_utxo.is_none()); |
1151 | 1 | } |
1152 | | |
1153 | | #[test] |
1154 | 1 | fn test_create_tx_only_witness_utxo() { |
1155 | 1 | let (mut wallet, _) = |
1156 | 1 | get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); |
1157 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1158 | 1 | let mut builder = wallet.build_tx(); |
1159 | 1 | builder |
1160 | 1 | .drain_to(addr.script_pubkey()) |
1161 | 1 | .only_witness_utxo() |
1162 | 1 | .drain_wallet(); |
1163 | 1 | let psbt = builder.finish().unwrap(); |
1164 | 1 | |
1165 | 1 | assert!(psbt.inputs[0].non_witness_utxo.is_none()); |
1166 | 1 | assert!(psbt.inputs[0].witness_utxo.is_some()); |
1167 | 1 | } |
1168 | | |
1169 | | #[test] |
1170 | 1 | fn test_create_tx_shwpkh_has_witness_utxo() { |
1171 | 1 | let (mut wallet, _) = |
1172 | 1 | get_funded_wallet("sh(wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); |
1173 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1174 | 1 | let mut builder = wallet.build_tx(); |
1175 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
1176 | 1 | let psbt = builder.finish().unwrap(); |
1177 | 1 | |
1178 | 1 | assert!(psbt.inputs[0].witness_utxo.is_some()); |
1179 | 1 | } |
1180 | | |
1181 | | #[test] |
1182 | 1 | fn test_create_tx_both_non_witness_utxo_and_witness_utxo_default() { |
1183 | 1 | let (mut wallet, _) = |
1184 | 1 | get_funded_wallet("wsh(pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW))"); |
1185 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1186 | 1 | let mut builder = wallet.build_tx(); |
1187 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
1188 | 1 | let psbt = builder.finish().unwrap(); |
1189 | 1 | |
1190 | 1 | assert!(psbt.inputs[0].non_witness_utxo.is_some()); |
1191 | 1 | assert!(psbt.inputs[0].witness_utxo.is_some()); |
1192 | 1 | } |
1193 | | |
1194 | | #[test] |
1195 | 1 | fn test_create_tx_add_utxo() { |
1196 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1197 | 1 | let small_output_tx = Transaction { |
1198 | 1 | input: vec![], |
1199 | 1 | output: vec![TxOut { |
1200 | 1 | script_pubkey: wallet |
1201 | 1 | .next_unused_address(KeychainKind::External) |
1202 | 1 | .script_pubkey(), |
1203 | 1 | value: Amount::from_sat(25_000), |
1204 | 1 | }], |
1205 | 1 | version: transaction::Version::non_standard(0), |
1206 | 1 | lock_time: absolute::LockTime::ZERO, |
1207 | 1 | }; |
1208 | 1 | let txid = small_output_tx.compute_txid(); |
1209 | 1 | wallet.insert_tx(small_output_tx); |
1210 | 1 | insert_anchor_from_conf( |
1211 | 1 | &mut wallet, |
1212 | 1 | txid, |
1213 | 1 | ConfirmationTime::Confirmed { |
1214 | 1 | height: 2000, |
1215 | 1 | time: 200, |
1216 | 1 | }, |
1217 | 1 | ); |
1218 | 1 | |
1219 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1220 | 1 | .unwrap() |
1221 | 1 | .assume_checked(); |
1222 | 1 | let mut builder = wallet.build_tx(); |
1223 | 1 | builder |
1224 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) |
1225 | 1 | .add_utxo(OutPoint { txid, vout: 0 }) |
1226 | 1 | .unwrap(); |
1227 | 1 | let psbt = builder.finish().unwrap(); |
1228 | 1 | let sent_received = |
1229 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
1230 | 1 | |
1231 | 1 | assert_eq!( |
1232 | 1 | psbt.unsigned_tx.input.len(), |
1233 | | 2, |
1234 | 0 | "should add an additional input since 25_000 < 30_000" |
1235 | | ); |
1236 | 1 | assert_eq!( |
1237 | 1 | sent_received.0, |
1238 | 1 | Amount::from_sat(75_000), |
1239 | 0 | "total should be sum of both inputs" |
1240 | | ); |
1241 | 1 | } |
1242 | | |
1243 | | #[test] |
1244 | | #[should_panic(expected = "InsufficientFunds")] |
1245 | 1 | fn test_create_tx_manually_selected_insufficient() { |
1246 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1247 | 1 | let small_output_tx = Transaction { |
1248 | 1 | input: vec![], |
1249 | 1 | output: vec![TxOut { |
1250 | 1 | script_pubkey: wallet |
1251 | 1 | .next_unused_address(KeychainKind::External) |
1252 | 1 | .script_pubkey(), |
1253 | 1 | value: Amount::from_sat(25_000), |
1254 | 1 | }], |
1255 | 1 | version: transaction::Version::non_standard(0), |
1256 | 1 | lock_time: absolute::LockTime::ZERO, |
1257 | 1 | }; |
1258 | 1 | let txid = small_output_tx.compute_txid(); |
1259 | 1 | wallet.insert_tx(small_output_tx.clone()); |
1260 | 1 | insert_anchor_from_conf( |
1261 | 1 | &mut wallet, |
1262 | 1 | txid, |
1263 | 1 | ConfirmationTime::Confirmed { |
1264 | 1 | height: 2000, |
1265 | 1 | time: 200, |
1266 | 1 | }, |
1267 | 1 | ); |
1268 | 1 | |
1269 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1270 | 1 | .unwrap() |
1271 | 1 | .assume_checked(); |
1272 | 1 | let mut builder = wallet.build_tx(); |
1273 | 1 | builder |
1274 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) |
1275 | 1 | .add_utxo(OutPoint { txid, vout: 0 }) |
1276 | 1 | .unwrap() |
1277 | 1 | .manually_selected_only(); |
1278 | 1 | builder.finish().unwrap(); |
1279 | 1 | } |
1280 | | |
1281 | | #[test] |
1282 | | #[should_panic(expected = "SpendingPolicyRequired(External)")] |
1283 | 1 | fn test_create_tx_policy_path_required() { |
1284 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); |
1285 | 1 | |
1286 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1287 | 1 | .unwrap() |
1288 | 1 | .assume_checked(); |
1289 | 1 | let mut builder = wallet.build_tx(); |
1290 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)); |
1291 | 1 | builder.finish().unwrap(); |
1292 | 1 | } |
1293 | | |
1294 | | #[test] |
1295 | 1 | fn test_create_tx_policy_path_no_csv() { |
1296 | 1 | let (descriptor, change_descriptor) = get_test_wpkh_with_change_desc(); |
1297 | 1 | let mut wallet = Wallet::create(descriptor, change_descriptor) |
1298 | 1 | .network(Network::Regtest) |
1299 | 1 | .create_wallet_no_persist() |
1300 | 1 | .expect("wallet"); |
1301 | 1 | |
1302 | 1 | let tx = Transaction { |
1303 | 1 | version: transaction::Version::non_standard(0), |
1304 | 1 | lock_time: absolute::LockTime::ZERO, |
1305 | 1 | input: vec![], |
1306 | 1 | output: vec![TxOut { |
1307 | 1 | script_pubkey: wallet |
1308 | 1 | .next_unused_address(KeychainKind::External) |
1309 | 1 | .script_pubkey(), |
1310 | 1 | value: Amount::from_sat(50_000), |
1311 | 1 | }], |
1312 | 1 | }; |
1313 | 1 | let txid = tx.compute_txid(); |
1314 | 1 | wallet.insert_tx(tx); |
1315 | 1 | insert_seen_at(&mut wallet, txid, 0); |
1316 | 1 | |
1317 | 1 | let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); |
1318 | 1 | let root_id = external_policy.id; |
1319 | 1 | // child #0 is just the key "A" |
1320 | 1 | let path = vec![(root_id, vec![0])].into_iter().collect(); |
1321 | 1 | |
1322 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1323 | 1 | .unwrap() |
1324 | 1 | .assume_checked(); |
1325 | 1 | let mut builder = wallet.build_tx(); |
1326 | 1 | builder |
1327 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) |
1328 | 1 | .policy_path(path, KeychainKind::External); |
1329 | 1 | let psbt = builder.finish().unwrap(); |
1330 | 1 | |
1331 | 1 | assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF)); |
1332 | 1 | } |
1333 | | |
1334 | | #[test] |
1335 | 1 | fn test_create_tx_policy_path_use_csv() { |
1336 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_a_or_b_plus_csv()); |
1337 | 1 | |
1338 | 1 | let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); |
1339 | 1 | let root_id = external_policy.id; |
1340 | 1 | // child #1 is or(pk(B),older(144)) |
1341 | 1 | let path = vec![(root_id, vec![1])].into_iter().collect(); |
1342 | 1 | |
1343 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1344 | 1 | .unwrap() |
1345 | 1 | .assume_checked(); |
1346 | 1 | let mut builder = wallet.build_tx(); |
1347 | 1 | builder |
1348 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) |
1349 | 1 | .policy_path(path, KeychainKind::External); |
1350 | 1 | let psbt = builder.finish().unwrap(); |
1351 | 1 | |
1352 | 1 | assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(144)); |
1353 | 1 | } |
1354 | | |
1355 | | #[test] |
1356 | 1 | fn test_create_tx_policy_path_ignored_subtree_with_csv() { |
1357 | 1 | let (mut wallet, _) = get_funded_wallet("wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),or_i(and_v(v:pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(30)),and_v(v:pkh(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(90)))))"); |
1358 | 1 | |
1359 | 1 | let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); |
1360 | 1 | let root_id = external_policy.id; |
1361 | 1 | // child #0 is pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu) |
1362 | 1 | let path = vec![(root_id, vec![0])].into_iter().collect(); |
1363 | 1 | |
1364 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1365 | 1 | .unwrap() |
1366 | 1 | .assume_checked(); |
1367 | 1 | let mut builder = wallet.build_tx(); |
1368 | 1 | builder |
1369 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(30_000)) |
1370 | 1 | .policy_path(path, KeychainKind::External); |
1371 | 1 | let psbt = builder.finish().unwrap(); |
1372 | 1 | |
1373 | 1 | assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); |
1374 | 1 | } |
1375 | | |
1376 | | #[test] |
1377 | 1 | fn test_create_tx_global_xpubs_with_origin() { |
1378 | 1 | use bitcoin::bip32; |
1379 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh([73756c7f/48'/0'/0'/2']tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); |
1380 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1381 | 1 | let mut builder = wallet.build_tx(); |
1382 | 1 | builder |
1383 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
1384 | 1 | .add_global_xpubs(); |
1385 | 1 | let psbt = builder.finish().unwrap(); |
1386 | 1 | |
1387 | 1 | let key = bip32::Xpub::from_str("tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3").unwrap(); |
1388 | 1 | let fingerprint = bip32::Fingerprint::from_hex("73756c7f").unwrap(); |
1389 | 1 | let path = bip32::DerivationPath::from_str("m/48'/0'/0'/2'").unwrap(); |
1390 | 1 | |
1391 | 1 | assert_eq!(psbt.xpub.len(), 2); |
1392 | 1 | assert_eq!(psbt.xpub.get(&key), Some(&(fingerprint, path))); |
1393 | 1 | } |
1394 | | |
1395 | | #[test] |
1396 | 1 | fn test_add_foreign_utxo() { |
1397 | 1 | let (mut wallet1, _) = get_funded_wallet_wpkh(); |
1398 | 1 | let (wallet2, _) = |
1399 | 1 | get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); |
1400 | 1 | |
1401 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1402 | 1 | .unwrap() |
1403 | 1 | .assume_checked(); |
1404 | 1 | let utxo = wallet2.list_unspent().next().expect("must take!"); |
1405 | 1 | let foreign_utxo_satisfaction = wallet2 |
1406 | 1 | .public_descriptor(KeychainKind::External) |
1407 | 1 | .max_weight_to_satisfy() |
1408 | 1 | .unwrap(); |
1409 | 1 | |
1410 | 1 | let psbt_input = psbt::Input { |
1411 | 1 | witness_utxo: Some(utxo.txout.clone()), |
1412 | 1 | ..Default::default() |
1413 | 1 | }; |
1414 | 1 | |
1415 | 1 | let mut builder = wallet1.build_tx(); |
1416 | 1 | builder |
1417 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)) |
1418 | 1 | .only_witness_utxo() |
1419 | 1 | .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) |
1420 | 1 | .unwrap(); |
1421 | 1 | let mut psbt = builder.finish().unwrap(); |
1422 | 1 | wallet1.insert_txout(utxo.outpoint, utxo.txout); |
1423 | 1 | let fee = check_fee!(wallet1, psbt); |
1424 | 1 | let sent_received = |
1425 | 1 | wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
1426 | 1 | |
1427 | 1 | assert_eq!( |
1428 | 1 | (sent_received.0 - sent_received.1), |
1429 | 1 | Amount::from_sat(10_000) + fee.unwrap_or(Amount::ZERO), |
1430 | 0 | "we should have only net spent ~10_000" |
1431 | | ); |
1432 | | |
1433 | 1 | assert!( |
1434 | 1 | psbt.unsigned_tx |
1435 | 1 | .input |
1436 | 1 | .iter() |
1437 | 2 | .any(|input| input.previous_output == utxo.outpoint)1 , |
1438 | 0 | "foreign_utxo should be in there" |
1439 | | ); |
1440 | | |
1441 | 1 | let finished = wallet1 |
1442 | 1 | .sign( |
1443 | 1 | &mut psbt, |
1444 | 1 | SignOptions { |
1445 | 1 | trust_witness_utxo: true, |
1446 | 1 | ..Default::default() |
1447 | 1 | }, |
1448 | 1 | ) |
1449 | 1 | .unwrap(); |
1450 | 1 | |
1451 | 1 | assert!( |
1452 | 1 | !finished, |
1453 | 0 | "only one of the inputs should have been signed so far" |
1454 | | ); |
1455 | | |
1456 | 1 | let finished = wallet2 |
1457 | 1 | .sign( |
1458 | 1 | &mut psbt, |
1459 | 1 | SignOptions { |
1460 | 1 | trust_witness_utxo: true, |
1461 | 1 | ..Default::default() |
1462 | 1 | }, |
1463 | 1 | ) |
1464 | 1 | .unwrap(); |
1465 | 1 | assert!(finished, "all the inputs should have been signed now"0 ); |
1466 | 1 | } |
1467 | | |
1468 | | #[test] |
1469 | | #[should_panic( |
1470 | | expected = "MissingTxOut([OutPoint { txid: 21d7fb1bceda00ab4069fc52d06baa13470803e9050edd16f5736e5d8c4925fd, vout: 0 }])" |
1471 | | )] |
1472 | 1 | fn test_calculate_fee_with_missing_foreign_utxo() { |
1473 | 1 | let (mut wallet1, _) = get_funded_wallet_wpkh(); |
1474 | 1 | let (wallet2, _) = |
1475 | 1 | get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); |
1476 | 1 | |
1477 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1478 | 1 | .unwrap() |
1479 | 1 | .assume_checked(); |
1480 | 1 | let utxo = wallet2.list_unspent().next().expect("must take!"); |
1481 | 1 | let foreign_utxo_satisfaction = wallet2 |
1482 | 1 | .public_descriptor(KeychainKind::External) |
1483 | 1 | .max_weight_to_satisfy() |
1484 | 1 | .unwrap(); |
1485 | 1 | |
1486 | 1 | let psbt_input = psbt::Input { |
1487 | 1 | witness_utxo: Some(utxo.txout.clone()), |
1488 | 1 | ..Default::default() |
1489 | 1 | }; |
1490 | 1 | |
1491 | 1 | let mut builder = wallet1.build_tx(); |
1492 | 1 | builder |
1493 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)) |
1494 | 1 | .only_witness_utxo() |
1495 | 1 | .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) |
1496 | 1 | .unwrap(); |
1497 | 1 | let psbt = builder.finish().unwrap(); |
1498 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
1499 | 1 | wallet1.calculate_fee(&tx).unwrap(); |
1500 | 1 | } |
1501 | | |
1502 | | #[test] |
1503 | 1 | fn test_add_foreign_utxo_invalid_psbt_input() { |
1504 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1505 | 1 | let outpoint = wallet.list_unspent().next().expect("must exist").outpoint; |
1506 | 1 | let foreign_utxo_satisfaction = wallet |
1507 | 1 | .public_descriptor(KeychainKind::External) |
1508 | 1 | .max_weight_to_satisfy() |
1509 | 1 | .unwrap(); |
1510 | 1 | |
1511 | 1 | let mut builder = wallet.build_tx(); |
1512 | 1 | let result = |
1513 | 1 | builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction); |
1514 | 1 | assert!(matches!0 (result, Err(AddForeignUtxoError::MissingUtxo))); |
1515 | 1 | } |
1516 | | |
1517 | | #[test] |
1518 | 1 | fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { |
1519 | 1 | let (mut wallet1, txid1) = get_funded_wallet_wpkh(); |
1520 | 1 | let (wallet2, txid2) = |
1521 | 1 | get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); |
1522 | 1 | |
1523 | 1 | let utxo2 = wallet2.list_unspent().next().unwrap(); |
1524 | 1 | let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone(); |
1525 | 1 | let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx.clone(); |
1526 | 1 | |
1527 | 1 | let satisfaction_weight = wallet2 |
1528 | 1 | .public_descriptor(KeychainKind::External) |
1529 | 1 | .max_weight_to_satisfy() |
1530 | 1 | .unwrap(); |
1531 | 1 | |
1532 | 1 | let mut builder = wallet1.build_tx(); |
1533 | 1 | assert!( |
1534 | 1 | builder |
1535 | 1 | .add_foreign_utxo( |
1536 | 1 | utxo2.outpoint, |
1537 | 1 | psbt::Input { |
1538 | 1 | non_witness_utxo: Some(tx1.as_ref().clone()), |
1539 | 1 | ..Default::default() |
1540 | 1 | }, |
1541 | 1 | satisfaction_weight |
1542 | 1 | ) |
1543 | 1 | .is_err(), |
1544 | 0 | "should fail when outpoint doesn't match psbt_input" |
1545 | | ); |
1546 | 1 | assert!( |
1547 | 1 | builder |
1548 | 1 | .add_foreign_utxo( |
1549 | 1 | utxo2.outpoint, |
1550 | 1 | psbt::Input { |
1551 | 1 | non_witness_utxo: Some(tx2.as_ref().clone()), |
1552 | 1 | ..Default::default() |
1553 | 1 | }, |
1554 | 1 | satisfaction_weight |
1555 | 1 | ) |
1556 | 1 | .is_ok(), |
1557 | 0 | "should be ok when outpoint does match psbt_input" |
1558 | | ); |
1559 | 1 | } |
1560 | | |
1561 | | #[test] |
1562 | 1 | fn test_add_foreign_utxo_only_witness_utxo() { |
1563 | 1 | let (mut wallet1, _) = get_funded_wallet_wpkh(); |
1564 | 1 | let (wallet2, txid2) = |
1565 | 1 | get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); |
1566 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1567 | 1 | .unwrap() |
1568 | 1 | .assume_checked(); |
1569 | 1 | let utxo2 = wallet2.list_unspent().next().unwrap(); |
1570 | 1 | |
1571 | 1 | let satisfaction_weight = wallet2 |
1572 | 1 | .public_descriptor(KeychainKind::External) |
1573 | 1 | .max_weight_to_satisfy() |
1574 | 1 | .unwrap(); |
1575 | 1 | |
1576 | 1 | let mut builder = wallet1.build_tx(); |
1577 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)); |
1578 | 1 | |
1579 | 1 | { |
1580 | 1 | let mut builder = builder.clone(); |
1581 | 1 | let psbt_input = psbt::Input { |
1582 | 1 | witness_utxo: Some(utxo2.txout.clone()), |
1583 | 1 | ..Default::default() |
1584 | 1 | }; |
1585 | 1 | builder |
1586 | 1 | .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) |
1587 | 1 | .unwrap(); |
1588 | 1 | assert!( |
1589 | 1 | builder.finish().is_err(), |
1590 | 0 | "psbt_input with witness_utxo should fail with only witness_utxo" |
1591 | | ); |
1592 | | } |
1593 | | |
1594 | | { |
1595 | 1 | let mut builder = builder.clone(); |
1596 | 1 | let psbt_input = psbt::Input { |
1597 | 1 | witness_utxo: Some(utxo2.txout.clone()), |
1598 | 1 | ..Default::default() |
1599 | 1 | }; |
1600 | 1 | builder |
1601 | 1 | .only_witness_utxo() |
1602 | 1 | .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) |
1603 | 1 | .unwrap(); |
1604 | 1 | assert!( |
1605 | 1 | builder.finish().is_ok(), |
1606 | 0 | "psbt_input with just witness_utxo should succeed when `only_witness_utxo` is enabled" |
1607 | | ); |
1608 | | } |
1609 | | |
1610 | | { |
1611 | 1 | let mut builder = builder.clone(); |
1612 | 1 | let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx; |
1613 | 1 | let psbt_input = psbt::Input { |
1614 | 1 | non_witness_utxo: Some(tx2.as_ref().clone()), |
1615 | 1 | ..Default::default() |
1616 | 1 | }; |
1617 | 1 | builder |
1618 | 1 | .add_foreign_utxo(utxo2.outpoint, psbt_input, satisfaction_weight) |
1619 | 1 | .unwrap(); |
1620 | 1 | assert!( |
1621 | 1 | builder.finish().is_ok(), |
1622 | 0 | "psbt_input with non_witness_utxo should succeed by default" |
1623 | | ); |
1624 | | } |
1625 | 1 | } |
1626 | | |
1627 | | #[test] |
1628 | 1 | fn test_get_psbt_input() { |
1629 | 1 | // this should grab a known good utxo and set the input |
1630 | 1 | let (wallet, _) = get_funded_wallet_wpkh(); |
1631 | 1 | for utxo in wallet.list_unspent() { |
1632 | 1 | let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap(); |
1633 | 1 | assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some()0 ); |
1634 | | } |
1635 | 1 | } |
1636 | | |
1637 | | #[test] |
1638 | | #[should_panic( |
1639 | | expected = "MissingKeyOrigin(\"tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3\")" |
1640 | | )] |
1641 | 1 | fn test_create_tx_global_xpubs_origin_missing() { |
1642 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh(tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*)"); |
1643 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1644 | 1 | let mut builder = wallet.build_tx(); |
1645 | 1 | builder |
1646 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
1647 | 1 | .add_global_xpubs(); |
1648 | 1 | builder.finish().unwrap(); |
1649 | 1 | } |
1650 | | |
1651 | | #[test] |
1652 | 1 | fn test_create_tx_global_xpubs_master_without_origin() { |
1653 | 1 | use bitcoin::bip32; |
1654 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh(tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL/0/*)"); |
1655 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1656 | 1 | let mut builder = wallet.build_tx(); |
1657 | 1 | builder |
1658 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
1659 | 1 | .add_global_xpubs(); |
1660 | 1 | let psbt = builder.finish().unwrap(); |
1661 | 1 | |
1662 | 1 | let key = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4Y55A58Gv9RSNF5hy84b5AJqYy7sCcjFrkcLpPre8kmgfit6kY1Zs3BLgeypTDBZJM222guPpdz7Cup5yzaMu62u7mYGbwFL").unwrap(); |
1663 | 1 | let fingerprint = bip32::Fingerprint::from_hex("997a323b").unwrap(); |
1664 | 1 | |
1665 | 1 | assert_eq!(psbt.xpub.len(), 2); |
1666 | 1 | assert_eq!( |
1667 | 1 | psbt.xpub.get(&key), |
1668 | 1 | Some(&(fingerprint, bip32::DerivationPath::default())) |
1669 | 1 | ); |
1670 | 1 | } |
1671 | | |
1672 | | #[test] |
1673 | | #[should_panic(expected = "IrreplaceableTransaction")] |
1674 | 1 | fn test_bump_fee_irreplaceable_tx() { |
1675 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1676 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1677 | 1 | let mut builder = wallet.build_tx(); |
1678 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
1679 | 1 | let psbt = builder.finish().unwrap(); |
1680 | 1 | |
1681 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
1682 | 1 | let txid = tx.compute_txid(); |
1683 | 1 | wallet.insert_tx(tx); |
1684 | 1 | insert_seen_at(&mut wallet, txid, 0); |
1685 | 1 | wallet.build_fee_bump(txid).unwrap().finish().unwrap(); |
1686 | 1 | } |
1687 | | |
1688 | | #[test] |
1689 | | #[should_panic(expected = "TransactionConfirmed")] |
1690 | 1 | fn test_bump_fee_confirmed_tx() { |
1691 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1692 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1693 | 1 | let mut builder = wallet.build_tx(); |
1694 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
1695 | 1 | let psbt = builder.finish().unwrap(); |
1696 | 1 | |
1697 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
1698 | 1 | let txid = tx.compute_txid(); |
1699 | 1 | |
1700 | 1 | wallet.insert_tx(tx); |
1701 | 1 | insert_anchor_from_conf( |
1702 | 1 | &mut wallet, |
1703 | 1 | txid, |
1704 | 1 | ConfirmationTime::Confirmed { |
1705 | 1 | height: 42, |
1706 | 1 | time: 42_000, |
1707 | 1 | }, |
1708 | 1 | ); |
1709 | 1 | |
1710 | 1 | wallet.build_fee_bump(txid).unwrap().finish().unwrap(); |
1711 | 1 | } |
1712 | | |
1713 | | #[test] |
1714 | 1 | fn test_bump_fee_low_fee_rate() { |
1715 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1716 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1717 | 1 | let mut builder = wallet.build_tx(); |
1718 | 1 | builder |
1719 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
1720 | 1 | .enable_rbf(); |
1721 | 1 | let psbt = builder.finish().unwrap(); |
1722 | 1 | let feerate = psbt.fee_rate().unwrap(); |
1723 | 1 | |
1724 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
1725 | 1 | let txid = tx.compute_txid(); |
1726 | 1 | |
1727 | 1 | wallet.insert_tx(tx); |
1728 | 1 | insert_seen_at(&mut wallet, txid, 0); |
1729 | 1 | |
1730 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
1731 | 1 | builder.fee_rate(FeeRate::BROADCAST_MIN); |
1732 | 1 | let res = builder.finish(); |
1733 | 0 | assert_matches!( |
1734 | 1 | res, |
1735 | | Err(CreateTxError::FeeRateTooLow { .. }), |
1736 | | "expected FeeRateTooLow error" |
1737 | | ); |
1738 | | |
1739 | 1 | let required = feerate.to_sat_per_kwu() + 250; // +1 sat/vb |
1740 | 1 | let sat_vb = required as f64 / 250.0; |
1741 | 1 | let expect = format!("Fee rate too low: required {} sat/vb", sat_vb); |
1742 | 1 | assert_eq!(res.unwrap_err().to_string(), expect); |
1743 | 1 | } |
1744 | | |
1745 | | #[test] |
1746 | | #[should_panic(expected = "FeeTooLow")] |
1747 | 1 | fn test_bump_fee_low_abs() { |
1748 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1749 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1750 | 1 | let mut builder = wallet.build_tx(); |
1751 | 1 | builder |
1752 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
1753 | 1 | .enable_rbf(); |
1754 | 1 | let psbt = builder.finish().unwrap(); |
1755 | 1 | |
1756 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
1757 | 1 | let txid = tx.compute_txid(); |
1758 | 1 | |
1759 | 1 | wallet.insert_tx(tx); |
1760 | 1 | insert_seen_at(&mut wallet, txid, 0); |
1761 | 1 | |
1762 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
1763 | 1 | builder.fee_absolute(Amount::from_sat(10)); |
1764 | 1 | builder.finish().unwrap(); |
1765 | 1 | } |
1766 | | |
1767 | | #[test] |
1768 | | #[should_panic(expected = "FeeTooLow")] |
1769 | 1 | fn test_bump_fee_zero_abs() { |
1770 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1771 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
1772 | 1 | let mut builder = wallet.build_tx(); |
1773 | 1 | builder |
1774 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
1775 | 1 | .enable_rbf(); |
1776 | 1 | let psbt = builder.finish().unwrap(); |
1777 | 1 | |
1778 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
1779 | 1 | let txid = tx.compute_txid(); |
1780 | 1 | wallet.insert_tx(tx); |
1781 | 1 | insert_seen_at(&mut wallet, txid, 0); |
1782 | 1 | |
1783 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
1784 | 1 | builder.fee_absolute(Amount::ZERO); |
1785 | 1 | builder.finish().unwrap(); |
1786 | 1 | } |
1787 | | |
1788 | | #[test] |
1789 | 1 | fn test_bump_fee_reduce_change() { |
1790 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1791 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1792 | 1 | .unwrap() |
1793 | 1 | .assume_checked(); |
1794 | 1 | let mut builder = wallet.build_tx(); |
1795 | 1 | builder |
1796 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) |
1797 | 1 | .enable_rbf(); |
1798 | 1 | let psbt = builder.finish().unwrap(); |
1799 | 1 | let original_sent_received = |
1800 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
1801 | 1 | let original_fee = check_fee!(wallet, psbt); |
1802 | | |
1803 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
1804 | 1 | let txid = tx.compute_txid(); |
1805 | 1 | wallet.insert_tx(tx); |
1806 | 1 | insert_seen_at(&mut wallet, txid, 0); |
1807 | 1 | |
1808 | 1 | let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb |
1809 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
1810 | 1 | builder.fee_rate(feerate).enable_rbf(); |
1811 | 1 | let psbt = builder.finish().unwrap(); |
1812 | 1 | let sent_received = |
1813 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
1814 | 1 | let fee = check_fee!(wallet, psbt); |
1815 | | |
1816 | 1 | assert_eq!(sent_received.0, original_sent_received.0); |
1817 | 1 | assert_eq!( |
1818 | 1 | sent_received.1 + fee.unwrap_or(Amount::ZERO), |
1819 | 1 | original_sent_received.1 + original_fee.unwrap_or(Amount::ZERO) |
1820 | 1 | ); |
1821 | 1 | assert!(fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO)); |
1822 | | |
1823 | 1 | let tx = &psbt.unsigned_tx; |
1824 | 1 | assert_eq!(tx.output.len(), 2); |
1825 | 1 | assert_eq!( |
1826 | 1 | tx.output |
1827 | 1 | .iter() |
1828 | 2 | .find(|txout| txout.script_pubkey == addr.script_pubkey()) |
1829 | 1 | .unwrap() |
1830 | 1 | .value, |
1831 | 1 | Amount::from_sat(25_000) |
1832 | 1 | ); |
1833 | 1 | assert_eq!( |
1834 | 1 | tx.output |
1835 | 1 | .iter() |
1836 | 1 | .find(|txout| txout.script_pubkey != addr.script_pubkey()) |
1837 | 1 | .unwrap() |
1838 | 1 | .value, |
1839 | 1 | sent_received.1 |
1840 | 1 | ); |
1841 | | |
1842 | | assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), feerate, @add_signature); |
1843 | | |
1844 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
1845 | 1 | builder.fee_absolute(Amount::from_sat(200)); |
1846 | 1 | builder.enable_rbf(); |
1847 | 1 | let psbt = builder.finish().unwrap(); |
1848 | 1 | let sent_received = |
1849 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
1850 | 1 | let fee = check_fee!(wallet, psbt); |
1851 | | |
1852 | 1 | assert_eq!(sent_received.0, original_sent_received.0); |
1853 | 1 | assert_eq!( |
1854 | 1 | sent_received.1 + fee.unwrap_or(Amount::ZERO), |
1855 | 1 | original_sent_received.1 + original_fee.unwrap_or(Amount::ZERO) |
1856 | 1 | ); |
1857 | 1 | assert!( |
1858 | 1 | fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO), |
1859 | 0 | "{} > {}", |
1860 | 0 | fee.unwrap_or(Amount::ZERO), |
1861 | 0 | original_fee.unwrap_or(Amount::ZERO) |
1862 | | ); |
1863 | | |
1864 | 1 | let tx = &psbt.unsigned_tx; |
1865 | 1 | assert_eq!(tx.output.len(), 2); |
1866 | 1 | assert_eq!( |
1867 | 1 | tx.output |
1868 | 1 | .iter() |
1869 | 1 | .find(|txout| txout.script_pubkey == addr.script_pubkey()) |
1870 | 1 | .unwrap() |
1871 | 1 | .value, |
1872 | 1 | Amount::from_sat(25_000) |
1873 | 1 | ); |
1874 | 1 | assert_eq!( |
1875 | 1 | tx.output |
1876 | 1 | .iter() |
1877 | 2 | .find(|txout| txout.script_pubkey != addr.script_pubkey()) |
1878 | 1 | .unwrap() |
1879 | 1 | .value, |
1880 | 1 | sent_received.1 |
1881 | 1 | ); |
1882 | | |
1883 | 1 | assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(200)); |
1884 | 1 | } |
1885 | | |
1886 | | #[test] |
1887 | 1 | fn test_bump_fee_reduce_single_recipient() { |
1888 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1889 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1890 | 1 | .unwrap() |
1891 | 1 | .assume_checked(); |
1892 | 1 | let mut builder = wallet.build_tx(); |
1893 | 1 | builder |
1894 | 1 | .drain_to(addr.script_pubkey()) |
1895 | 1 | .drain_wallet() |
1896 | 1 | .enable_rbf(); |
1897 | 1 | let psbt = builder.finish().unwrap(); |
1898 | 1 | let tx = psbt.clone().extract_tx().expect("failed to extract tx"); |
1899 | 1 | let original_sent_received = wallet.sent_and_received(&tx); |
1900 | 1 | let original_fee = check_fee!(wallet, psbt); |
1901 | 1 | let txid = tx.compute_txid(); |
1902 | 1 | wallet.insert_tx(tx); |
1903 | 1 | insert_seen_at(&mut wallet, txid, 0); |
1904 | 1 | |
1905 | 1 | let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb |
1906 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
1907 | 1 | builder |
1908 | 1 | .fee_rate(feerate) |
1909 | 1 | // remove original tx drain_to address and amount |
1910 | 1 | .set_recipients(Vec::new()) |
1911 | 1 | // set back original drain_to address |
1912 | 1 | .drain_to(addr.script_pubkey()) |
1913 | 1 | // drain wallet output amount will be re-calculated with new fee rate |
1914 | 1 | .drain_wallet(); |
1915 | 1 | let psbt = builder.finish().unwrap(); |
1916 | 1 | let sent_received = |
1917 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
1918 | 1 | let fee = check_fee!(wallet, psbt); |
1919 | | |
1920 | 1 | assert_eq!(sent_received.0, original_sent_received.0); |
1921 | 1 | assert!(fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO)); |
1922 | | |
1923 | 1 | let tx = &psbt.unsigned_tx; |
1924 | 1 | assert_eq!(tx.output.len(), 1); |
1925 | 1 | assert_eq!( |
1926 | 1 | tx.output[0].value + fee.unwrap_or(Amount::ZERO), |
1927 | 1 | sent_received.0 |
1928 | 1 | ); |
1929 | | |
1930 | | assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), feerate, @add_signature); |
1931 | 1 | } |
1932 | | |
1933 | | #[test] |
1934 | 1 | fn test_bump_fee_absolute_reduce_single_recipient() { |
1935 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1936 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
1937 | 1 | .unwrap() |
1938 | 1 | .assume_checked(); |
1939 | 1 | let mut builder = wallet.build_tx(); |
1940 | 1 | builder |
1941 | 1 | .drain_to(addr.script_pubkey()) |
1942 | 1 | .drain_wallet() |
1943 | 1 | .enable_rbf(); |
1944 | 1 | let psbt = builder.finish().unwrap(); |
1945 | 1 | let original_fee = check_fee!(wallet, psbt); |
1946 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
1947 | 1 | let original_sent_received = wallet.sent_and_received(&tx); |
1948 | 1 | let txid = tx.compute_txid(); |
1949 | 1 | wallet.insert_tx(tx); |
1950 | 1 | insert_seen_at(&mut wallet, txid, 0); |
1951 | 1 | |
1952 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
1953 | 1 | builder |
1954 | 1 | .fee_absolute(Amount::from_sat(300)) |
1955 | 1 | // remove original tx drain_to address and amount |
1956 | 1 | .set_recipients(Vec::new()) |
1957 | 1 | // set back original drain_to address |
1958 | 1 | .drain_to(addr.script_pubkey()) |
1959 | 1 | // drain wallet output amount will be re-calculated with new fee rate |
1960 | 1 | .drain_wallet(); |
1961 | 1 | let psbt = builder.finish().unwrap(); |
1962 | 1 | let tx = &psbt.unsigned_tx; |
1963 | 1 | let sent_received = wallet.sent_and_received(tx); |
1964 | 1 | let fee = check_fee!(wallet, psbt); |
1965 | | |
1966 | 1 | assert_eq!(sent_received.0, original_sent_received.0); |
1967 | 1 | assert!(fee.unwrap_or(Amount::ZERO) > original_fee.unwrap_or(Amount::ZERO)); |
1968 | | |
1969 | 1 | assert_eq!(tx.output.len(), 1); |
1970 | 1 | assert_eq!( |
1971 | 1 | tx.output[0].value + fee.unwrap_or(Amount::ZERO), |
1972 | 1 | sent_received.0 |
1973 | 1 | ); |
1974 | | |
1975 | 1 | assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(300)); |
1976 | 1 | } |
1977 | | |
1978 | | #[test] |
1979 | 1 | fn test_bump_fee_drain_wallet() { |
1980 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
1981 | 1 | // receive an extra tx so that our wallet has two utxos. |
1982 | 1 | let tx = Transaction { |
1983 | 1 | version: transaction::Version::ONE, |
1984 | 1 | lock_time: absolute::LockTime::ZERO, |
1985 | 1 | input: vec![], |
1986 | 1 | output: vec![TxOut { |
1987 | 1 | script_pubkey: wallet |
1988 | 1 | .next_unused_address(KeychainKind::External) |
1989 | 1 | .script_pubkey(), |
1990 | 1 | value: Amount::from_sat(25_000), |
1991 | 1 | }], |
1992 | 1 | }; |
1993 | 1 | let txid = tx.compute_txid(); |
1994 | 1 | let tip = wallet.latest_checkpoint().height(); |
1995 | 1 | wallet.insert_tx(tx.clone()); |
1996 | 1 | insert_anchor_from_conf( |
1997 | 1 | &mut wallet, |
1998 | 1 | txid, |
1999 | 1 | ConfirmationTime::Confirmed { |
2000 | 1 | height: tip, |
2001 | 1 | time: 42_000, |
2002 | 1 | }, |
2003 | 1 | ); |
2004 | 1 | |
2005 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2006 | 1 | .unwrap() |
2007 | 1 | .assume_checked(); |
2008 | 1 | |
2009 | 1 | let mut builder = wallet.build_tx(); |
2010 | 1 | builder |
2011 | 1 | .drain_to(addr.script_pubkey()) |
2012 | 1 | .add_utxo(OutPoint { |
2013 | 1 | txid: tx.compute_txid(), |
2014 | 1 | vout: 0, |
2015 | 1 | }) |
2016 | 1 | .unwrap() |
2017 | 1 | .manually_selected_only() |
2018 | 1 | .enable_rbf(); |
2019 | 1 | let psbt = builder.finish().unwrap(); |
2020 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
2021 | 1 | let original_sent_received = wallet.sent_and_received(&tx); |
2022 | 1 | |
2023 | 1 | let txid = tx.compute_txid(); |
2024 | 1 | wallet.insert_tx(tx); |
2025 | 1 | insert_seen_at(&mut wallet, txid, 0); |
2026 | 1 | assert_eq!(original_sent_received.0, Amount::from_sat(25_000)); |
2027 | | |
2028 | | // for the new feerate, it should be enough to reduce the output, but since we specify |
2029 | | // `drain_wallet` we expect to spend everything |
2030 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
2031 | 1 | builder |
2032 | 1 | .drain_wallet() |
2033 | 1 | .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); |
2034 | 1 | let psbt = builder.finish().unwrap(); |
2035 | 1 | let sent_received = wallet.sent_and_received(&psbt.extract_tx().expect("failed to extract tx")); |
2036 | 1 | |
2037 | 1 | assert_eq!(sent_received.0, Amount::from_sat(75_000)); |
2038 | 1 | } |
2039 | | |
2040 | | #[test] |
2041 | | #[should_panic(expected = "InsufficientFunds")] |
2042 | 1 | fn test_bump_fee_remove_output_manually_selected_only() { |
2043 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
2044 | 1 | // receive an extra tx so that our wallet has two utxos. then we manually pick only one of |
2045 | 1 | // them, and make sure that `bump_fee` doesn't try to add more. This fails because we've |
2046 | 1 | // told the wallet it's not allowed to add more inputs AND it can't reduce the value of the |
2047 | 1 | // existing output. In other words, bump_fee + manually_selected_only is always an error |
2048 | 1 | // unless there is a change output. |
2049 | 1 | let init_tx = Transaction { |
2050 | 1 | version: transaction::Version::ONE, |
2051 | 1 | lock_time: absolute::LockTime::ZERO, |
2052 | 1 | input: vec![], |
2053 | 1 | output: vec![TxOut { |
2054 | 1 | script_pubkey: wallet |
2055 | 1 | .next_unused_address(KeychainKind::External) |
2056 | 1 | .script_pubkey(), |
2057 | 1 | value: Amount::from_sat(25_000), |
2058 | 1 | }], |
2059 | 1 | }; |
2060 | 1 | let position: ConfirmationTime = wallet |
2061 | 1 | .transactions() |
2062 | 1 | .last() |
2063 | 1 | .unwrap() |
2064 | 1 | .chain_position |
2065 | 1 | .cloned() |
2066 | 1 | .into(); |
2067 | 1 | |
2068 | 1 | wallet.insert_tx(init_tx.clone()); |
2069 | 1 | insert_anchor_from_conf(&mut wallet, init_tx.compute_txid(), position); |
2070 | 1 | |
2071 | 1 | let outpoint = OutPoint { |
2072 | 1 | txid: init_tx.compute_txid(), |
2073 | 1 | vout: 0, |
2074 | 1 | }; |
2075 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2076 | 1 | .unwrap() |
2077 | 1 | .assume_checked(); |
2078 | 1 | let mut builder = wallet.build_tx(); |
2079 | 1 | builder |
2080 | 1 | .drain_to(addr.script_pubkey()) |
2081 | 1 | .add_utxo(outpoint) |
2082 | 1 | .unwrap() |
2083 | 1 | .manually_selected_only() |
2084 | 1 | .enable_rbf(); |
2085 | 1 | let psbt = builder.finish().unwrap(); |
2086 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
2087 | 1 | let original_sent_received = wallet.sent_and_received(&tx); |
2088 | 1 | let txid = tx.compute_txid(); |
2089 | 1 | wallet.insert_tx(tx); |
2090 | 1 | insert_seen_at(&mut wallet, txid, 0); |
2091 | 1 | assert_eq!(original_sent_received.0, Amount::from_sat(25_000)); |
2092 | | |
2093 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
2094 | 1 | builder |
2095 | 1 | .manually_selected_only() |
2096 | 1 | .fee_rate(FeeRate::from_sat_per_vb_unchecked(255)); |
2097 | 1 | builder.finish().unwrap(); |
2098 | 1 | } |
2099 | | |
2100 | | #[test] |
2101 | 1 | fn test_bump_fee_add_input() { |
2102 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
2103 | 1 | let init_tx = Transaction { |
2104 | 1 | version: transaction::Version::ONE, |
2105 | 1 | lock_time: absolute::LockTime::ZERO, |
2106 | 1 | input: vec![], |
2107 | 1 | output: vec![TxOut { |
2108 | 1 | script_pubkey: wallet |
2109 | 1 | .next_unused_address(KeychainKind::External) |
2110 | 1 | .script_pubkey(), |
2111 | 1 | value: Amount::from_sat(25_000), |
2112 | 1 | }], |
2113 | 1 | }; |
2114 | 1 | let txid = init_tx.compute_txid(); |
2115 | 1 | let pos: ConfirmationTime = wallet |
2116 | 1 | .transactions() |
2117 | 1 | .last() |
2118 | 1 | .unwrap() |
2119 | 1 | .chain_position |
2120 | 1 | .cloned() |
2121 | 1 | .into(); |
2122 | 1 | wallet.insert_tx(init_tx); |
2123 | 1 | insert_anchor_from_conf(&mut wallet, txid, pos); |
2124 | 1 | |
2125 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2126 | 1 | .unwrap() |
2127 | 1 | .assume_checked(); |
2128 | 1 | let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); |
2129 | 1 | builder |
2130 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) |
2131 | 1 | .enable_rbf(); |
2132 | 1 | let psbt = builder.finish().unwrap(); |
2133 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
2134 | 1 | let original_details = wallet.sent_and_received(&tx); |
2135 | 1 | let txid = tx.compute_txid(); |
2136 | 1 | wallet.insert_tx(tx); |
2137 | 1 | insert_seen_at(&mut wallet, txid, 0); |
2138 | 1 | |
2139 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
2140 | 1 | builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50)); |
2141 | 1 | let psbt = builder.finish().unwrap(); |
2142 | 1 | let sent_received = |
2143 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
2144 | 1 | let fee = check_fee!(wallet, psbt); |
2145 | 1 | assert_eq!( |
2146 | 1 | sent_received.0, |
2147 | 1 | original_details.0 + Amount::from_sat(25_000) |
2148 | 1 | ); |
2149 | 1 | assert_eq!( |
2150 | 1 | fee.unwrap_or(Amount::ZERO) + sent_received.1, |
2151 | 1 | Amount::from_sat(30_000) |
2152 | 1 | ); |
2153 | | |
2154 | 1 | let tx = &psbt.unsigned_tx; |
2155 | 1 | assert_eq!(tx.input.len(), 2); |
2156 | 1 | assert_eq!(tx.output.len(), 2); |
2157 | 1 | assert_eq!( |
2158 | 1 | tx.output |
2159 | 1 | .iter() |
2160 | 2 | .find(|txout| txout.script_pubkey == addr.script_pubkey()) |
2161 | 1 | .unwrap() |
2162 | 1 | .value, |
2163 | 1 | Amount::from_sat(45_000) |
2164 | 1 | ); |
2165 | 1 | assert_eq!( |
2166 | 1 | tx.output |
2167 | 1 | .iter() |
2168 | 1 | .find(|txout| txout.script_pubkey != addr.script_pubkey()) |
2169 | 1 | .unwrap() |
2170 | 1 | .value, |
2171 | 1 | sent_received.1 |
2172 | 1 | ); |
2173 | | |
2174 | | assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); |
2175 | 1 | } |
2176 | | |
2177 | | #[test] |
2178 | 1 | fn test_bump_fee_absolute_add_input() { |
2179 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
2180 | 1 | receive_output_in_latest_block(&mut wallet, 25_000); |
2181 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2182 | 1 | .unwrap() |
2183 | 1 | .assume_checked(); |
2184 | 1 | let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); |
2185 | 1 | builder |
2186 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) |
2187 | 1 | .enable_rbf(); |
2188 | 1 | let psbt = builder.finish().unwrap(); |
2189 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
2190 | 1 | let original_sent_received = wallet.sent_and_received(&tx); |
2191 | 1 | let txid = tx.compute_txid(); |
2192 | 1 | wallet.insert_tx(tx); |
2193 | 1 | insert_seen_at(&mut wallet, txid, 0); |
2194 | 1 | |
2195 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
2196 | 1 | builder.fee_absolute(Amount::from_sat(6_000)); |
2197 | 1 | let psbt = builder.finish().unwrap(); |
2198 | 1 | let sent_received = |
2199 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
2200 | 1 | let fee = check_fee!(wallet, psbt); |
2201 | | |
2202 | 1 | assert_eq!( |
2203 | 1 | sent_received.0, |
2204 | 1 | original_sent_received.0 + Amount::from_sat(25_000) |
2205 | 1 | ); |
2206 | 1 | assert_eq!( |
2207 | 1 | fee.unwrap_or(Amount::ZERO) + sent_received.1, |
2208 | 1 | Amount::from_sat(30_000) |
2209 | 1 | ); |
2210 | | |
2211 | 1 | let tx = &psbt.unsigned_tx; |
2212 | 1 | assert_eq!(tx.input.len(), 2); |
2213 | 1 | assert_eq!(tx.output.len(), 2); |
2214 | 1 | assert_eq!( |
2215 | 1 | tx.output |
2216 | 1 | .iter() |
2217 | 1 | .find(|txout| txout.script_pubkey == addr.script_pubkey()) |
2218 | 1 | .unwrap() |
2219 | 1 | .value, |
2220 | 1 | Amount::from_sat(45_000) |
2221 | 1 | ); |
2222 | 1 | assert_eq!( |
2223 | 1 | tx.output |
2224 | 1 | .iter() |
2225 | 2 | .find(|txout| txout.script_pubkey != addr.script_pubkey()) |
2226 | 1 | .unwrap() |
2227 | 1 | .value, |
2228 | 1 | sent_received.1 |
2229 | 1 | ); |
2230 | | |
2231 | 1 | assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(6_000)); |
2232 | 1 | } |
2233 | | |
2234 | | #[test] |
2235 | 1 | fn test_bump_fee_no_change_add_input_and_change() { |
2236 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
2237 | 1 | let op = receive_output_in_latest_block(&mut wallet, 25_000); |
2238 | 1 | |
2239 | 1 | // initially make a tx without change by using `drain_to` |
2240 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2241 | 1 | .unwrap() |
2242 | 1 | .assume_checked(); |
2243 | 1 | let mut builder = wallet.build_tx(); |
2244 | 1 | builder |
2245 | 1 | .drain_to(addr.script_pubkey()) |
2246 | 1 | .add_utxo(op) |
2247 | 1 | .unwrap() |
2248 | 1 | .manually_selected_only() |
2249 | 1 | .enable_rbf(); |
2250 | 1 | let psbt = builder.finish().unwrap(); |
2251 | 1 | let original_sent_received = |
2252 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
2253 | 1 | let original_fee = check_fee!(wallet, psbt); |
2254 | | |
2255 | 1 | let tx = psbt.extract_tx().expect("failed to extract tx"); |
2256 | 1 | let txid = tx.compute_txid(); |
2257 | 1 | wallet.insert_tx(tx); |
2258 | 1 | insert_seen_at(&mut wallet, txid, 0); |
2259 | 1 | |
2260 | 1 | // Now bump the fees, the wallet should add an extra input and a change output, and leave |
2261 | 1 | // the original output untouched. |
2262 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
2263 | 1 | builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50)); |
2264 | 1 | let psbt = builder.finish().unwrap(); |
2265 | 1 | let sent_received = |
2266 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
2267 | 1 | let fee = check_fee!(wallet, psbt); |
2268 | | |
2269 | 1 | let original_send_all_amount = original_sent_received.0 - original_fee.unwrap_or(Amount::ZERO); |
2270 | 1 | assert_eq!( |
2271 | 1 | sent_received.0, |
2272 | 1 | original_sent_received.0 + Amount::from_sat(50_000) |
2273 | 1 | ); |
2274 | 1 | assert_eq!( |
2275 | 1 | sent_received.1, |
2276 | 1 | Amount::from_sat(75_000) - original_send_all_amount - fee.unwrap_or(Amount::ZERO) |
2277 | 1 | ); |
2278 | | |
2279 | 1 | let tx = &psbt.unsigned_tx; |
2280 | 1 | assert_eq!(tx.input.len(), 2); |
2281 | 1 | assert_eq!(tx.output.len(), 2); |
2282 | 1 | assert_eq!( |
2283 | 1 | tx.output |
2284 | 1 | .iter() |
2285 | 2 | .find(|txout| txout.script_pubkey == addr.script_pubkey()) |
2286 | 1 | .unwrap() |
2287 | 1 | .value, |
2288 | 1 | original_send_all_amount |
2289 | 1 | ); |
2290 | 1 | assert_eq!( |
2291 | 1 | tx.output |
2292 | 1 | .iter() |
2293 | 1 | .find(|txout| txout.script_pubkey != addr.script_pubkey()) |
2294 | 1 | .unwrap() |
2295 | 1 | .value, |
2296 | 1 | Amount::from_sat(75_000) - original_send_all_amount - fee.unwrap_or(Amount::ZERO) |
2297 | 1 | ); |
2298 | | |
2299 | | assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(50), @add_signature); |
2300 | 1 | } |
2301 | | |
2302 | | #[test] |
2303 | 1 | fn test_bump_fee_add_input_change_dust() { |
2304 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
2305 | 1 | receive_output_in_latest_block(&mut wallet, 25_000); |
2306 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2307 | 1 | .unwrap() |
2308 | 1 | .assume_checked(); |
2309 | 1 | let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); |
2310 | 1 | builder |
2311 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) |
2312 | 1 | .enable_rbf(); |
2313 | 1 | let psbt = builder.finish().unwrap(); |
2314 | 1 | let original_sent_received = |
2315 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
2316 | 1 | let original_fee = check_fee!(wallet, psbt); |
2317 | | |
2318 | 1 | let mut tx = psbt.extract_tx().expect("failed to extract tx"); |
2319 | 2 | for txin1 in &mut tx.input { |
2320 | 1 | txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // to get realistic weight |
2321 | 1 | } |
2322 | 1 | let original_tx_weight = tx.weight(); |
2323 | 1 | assert_eq!(tx.input.len(), 1); |
2324 | 1 | assert_eq!(tx.output.len(), 2); |
2325 | 1 | let txid = tx.compute_txid(); |
2326 | 1 | wallet.insert_tx(tx); |
2327 | 1 | insert_seen_at(&mut wallet, txid, 0); |
2328 | 1 | |
2329 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
2330 | 1 | // We set a fee high enough that during rbf we are forced to add |
2331 | 1 | // a new input and also that we have to remove the change |
2332 | 1 | // that we had previously |
2333 | 1 | |
2334 | 1 | // We calculate the new weight as: |
2335 | 1 | // original weight |
2336 | 1 | // + extra input weight: 160 WU = (32 (prevout) + 4 (vout) + 4 (nsequence)) * 4 |
2337 | 1 | // + input satisfaction weight: 112 WU = 106 (witness) + 2 (witness len) + (1 (script len)) * 4 |
2338 | 1 | // - change output weight: 124 WU = (8 (value) + 1 (script len) + 22 (script)) * 4 |
2339 | 1 | let new_tx_weight = |
2340 | 1 | original_tx_weight + Weight::from_wu(160) + Weight::from_wu(112) - Weight::from_wu(124); |
2341 | 1 | // two inputs (50k, 25k) and one output (45k) - epsilon |
2342 | 1 | // We use epsilon here to avoid asking for a slightly too high feerate |
2343 | 1 | let fee_abs = 50_000 + 25_000 - 45_000 - 10; |
2344 | 1 | builder.fee_rate(Amount::from_sat(fee_abs) / new_tx_weight); |
2345 | 1 | let psbt = builder.finish().unwrap(); |
2346 | 1 | let sent_received = |
2347 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
2348 | 1 | let fee = check_fee!(wallet, psbt); |
2349 | | |
2350 | 1 | assert_eq!( |
2351 | 1 | original_sent_received.1, |
2352 | 1 | Amount::from_sat(5_000) - original_fee.unwrap_or(Amount::ZERO) |
2353 | 1 | ); |
2354 | | |
2355 | 1 | assert_eq!( |
2356 | 1 | sent_received.0, |
2357 | 1 | original_sent_received.0 + Amount::from_sat(25_000) |
2358 | 1 | ); |
2359 | 1 | assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(30_000)); |
2360 | 1 | assert_eq!(sent_received.1, Amount::ZERO); |
2361 | | |
2362 | 1 | let tx = &psbt.unsigned_tx; |
2363 | 1 | assert_eq!(tx.input.len(), 2); |
2364 | 1 | assert_eq!(tx.output.len(), 1); |
2365 | 1 | assert_eq!( |
2366 | 1 | tx.output |
2367 | 1 | .iter() |
2368 | 1 | .find(|txout| txout.script_pubkey == addr.script_pubkey()) |
2369 | 1 | .unwrap() |
2370 | 1 | .value, |
2371 | 1 | Amount::from_sat(45_000) |
2372 | 1 | ); |
2373 | | |
2374 | | assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(140), @dust_change, @add_signature); |
2375 | 1 | } |
2376 | | |
2377 | | #[test] |
2378 | 1 | fn test_bump_fee_force_add_input() { |
2379 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
2380 | 1 | let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); |
2381 | 1 | |
2382 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2383 | 1 | .unwrap() |
2384 | 1 | .assume_checked(); |
2385 | 1 | let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); |
2386 | 1 | builder |
2387 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) |
2388 | 1 | .enable_rbf(); |
2389 | 1 | let psbt = builder.finish().unwrap(); |
2390 | 1 | let mut tx = psbt.extract_tx().expect("failed to extract tx"); |
2391 | 1 | let original_sent_received = wallet.sent_and_received(&tx); |
2392 | 1 | let txid = tx.compute_txid(); |
2393 | 2 | for txin1 in &mut tx.input { |
2394 | 1 | txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature |
2395 | 1 | } |
2396 | 1 | wallet.insert_tx(tx.clone()); |
2397 | 1 | insert_seen_at(&mut wallet, txid, 0); |
2398 | 1 | // the new fee_rate is low enough that just reducing the change would be fine, but we force |
2399 | 1 | // the addition of an extra input with `add_utxo()` |
2400 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
2401 | 1 | builder |
2402 | 1 | .add_utxo(incoming_op) |
2403 | 1 | .unwrap() |
2404 | 1 | .fee_rate(FeeRate::from_sat_per_vb_unchecked(5)); |
2405 | 1 | let psbt = builder.finish().unwrap(); |
2406 | 1 | let sent_received = |
2407 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
2408 | 1 | let fee = check_fee!(wallet, psbt); |
2409 | | |
2410 | 1 | assert_eq!( |
2411 | 1 | sent_received.0, |
2412 | 1 | original_sent_received.0 + Amount::from_sat(25_000) |
2413 | 1 | ); |
2414 | 1 | assert_eq!( |
2415 | 1 | fee.unwrap_or(Amount::ZERO) + sent_received.1, |
2416 | 1 | Amount::from_sat(30_000) |
2417 | 1 | ); |
2418 | | |
2419 | 1 | let tx = &psbt.unsigned_tx; |
2420 | 1 | assert_eq!(tx.input.len(), 2); |
2421 | 1 | assert_eq!(tx.output.len(), 2); |
2422 | 1 | assert_eq!( |
2423 | 1 | tx.output |
2424 | 1 | .iter() |
2425 | 2 | .find(|txout| txout.script_pubkey == addr.script_pubkey()) |
2426 | 1 | .unwrap() |
2427 | 1 | .value, |
2428 | 1 | Amount::from_sat(45_000) |
2429 | 1 | ); |
2430 | 1 | assert_eq!( |
2431 | 1 | tx.output |
2432 | 1 | .iter() |
2433 | 1 | .find(|txout| txout.script_pubkey != addr.script_pubkey()) |
2434 | 1 | .unwrap() |
2435 | 1 | .value, |
2436 | 1 | sent_received.1 |
2437 | 1 | ); |
2438 | | |
2439 | | assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), FeeRate::from_sat_per_vb_unchecked(5), @add_signature); |
2440 | 1 | } |
2441 | | |
2442 | | #[test] |
2443 | 1 | fn test_bump_fee_absolute_force_add_input() { |
2444 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
2445 | 1 | let incoming_op = receive_output_in_latest_block(&mut wallet, 25_000); |
2446 | 1 | |
2447 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2448 | 1 | .unwrap() |
2449 | 1 | .assume_checked(); |
2450 | 1 | let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); |
2451 | 1 | builder |
2452 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) |
2453 | 1 | .enable_rbf(); |
2454 | 1 | let psbt = builder.finish().unwrap(); |
2455 | 1 | let mut tx = psbt.extract_tx().expect("failed to extract tx"); |
2456 | 1 | let original_sent_received = wallet.sent_and_received(&tx); |
2457 | 1 | let txid = tx.compute_txid(); |
2458 | | // skip saving the new utxos, we know they can't be used anyways |
2459 | 2 | for txin1 in &mut tx.input { |
2460 | 1 | txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature |
2461 | 1 | } |
2462 | 1 | wallet.insert_tx(tx.clone()); |
2463 | 1 | insert_seen_at(&mut wallet, txid, 0); |
2464 | 1 | |
2465 | 1 | // the new fee_rate is low enough that just reducing the change would be fine, but we force |
2466 | 1 | // the addition of an extra input with `add_utxo()` |
2467 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
2468 | 1 | builder |
2469 | 1 | .add_utxo(incoming_op) |
2470 | 1 | .unwrap() |
2471 | 1 | .fee_absolute(Amount::from_sat(250)); |
2472 | 1 | let psbt = builder.finish().unwrap(); |
2473 | 1 | let sent_received = |
2474 | 1 | wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
2475 | 1 | let fee = check_fee!(wallet, psbt); |
2476 | | |
2477 | 1 | assert_eq!( |
2478 | 1 | sent_received.0, |
2479 | 1 | original_sent_received.0 + Amount::from_sat(25_000) |
2480 | 1 | ); |
2481 | 1 | assert_eq!( |
2482 | 1 | fee.unwrap_or(Amount::ZERO) + sent_received.1, |
2483 | 1 | Amount::from_sat(30_000) |
2484 | 1 | ); |
2485 | | |
2486 | 1 | let tx = &psbt.unsigned_tx; |
2487 | 1 | assert_eq!(tx.input.len(), 2); |
2488 | 1 | assert_eq!(tx.output.len(), 2); |
2489 | 1 | assert_eq!( |
2490 | 1 | tx.output |
2491 | 1 | .iter() |
2492 | 2 | .find(|txout| txout.script_pubkey == addr.script_pubkey()) |
2493 | 1 | .unwrap() |
2494 | 1 | .value, |
2495 | 1 | Amount::from_sat(45_000) |
2496 | 1 | ); |
2497 | 1 | assert_eq!( |
2498 | 1 | tx.output |
2499 | 1 | .iter() |
2500 | 1 | .find(|txout| txout.script_pubkey != addr.script_pubkey()) |
2501 | 1 | .unwrap() |
2502 | 1 | .value, |
2503 | 1 | sent_received.1 |
2504 | 1 | ); |
2505 | | |
2506 | 1 | assert_eq!(fee.unwrap_or(Amount::ZERO), Amount::from_sat(250)); |
2507 | 1 | } |
2508 | | |
2509 | | #[test] |
2510 | | #[should_panic(expected = "InsufficientFunds")] |
2511 | 1 | fn test_bump_fee_unconfirmed_inputs_only() { |
2512 | 1 | // We try to bump the fee, but: |
2513 | 1 | // - We can't reduce the change, as we have no change |
2514 | 1 | // - All our UTXOs are unconfirmed |
2515 | 1 | // So, we fail with "InsufficientFunds", as per RBF rule 2: |
2516 | 1 | // The replacement transaction may only include an unconfirmed input |
2517 | 1 | // if that input was included in one of the original transactions. |
2518 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
2519 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2520 | 1 | .unwrap() |
2521 | 1 | .assume_checked(); |
2522 | 1 | let mut builder = wallet.build_tx(); |
2523 | 1 | builder |
2524 | 1 | .drain_wallet() |
2525 | 1 | .drain_to(addr.script_pubkey()) |
2526 | 1 | .enable_rbf(); |
2527 | 1 | let psbt = builder.finish().unwrap(); |
2528 | 1 | // Now we receive one transaction with 0 confirmations. We won't be able to use that for |
2529 | 1 | // fee bumping, as it's still unconfirmed! |
2530 | 1 | receive_output( |
2531 | 1 | &mut wallet, |
2532 | 1 | 25_000, |
2533 | 1 | ConfirmationTime::Unconfirmed { last_seen: 0 }, |
2534 | 1 | ); |
2535 | 1 | let mut tx = psbt.extract_tx().expect("failed to extract tx"); |
2536 | 1 | let txid = tx.compute_txid(); |
2537 | 2 | for txin1 in &mut tx.input { |
2538 | 1 | txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature |
2539 | 1 | } |
2540 | 1 | wallet.insert_tx(tx); |
2541 | 1 | insert_seen_at(&mut wallet, txid, 0); |
2542 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
2543 | 1 | builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25)); |
2544 | 1 | builder.finish().unwrap(); |
2545 | 1 | } |
2546 | | |
2547 | | #[test] |
2548 | 1 | fn test_bump_fee_unconfirmed_input() { |
2549 | 1 | // We create a tx draining the wallet and spending one confirmed |
2550 | 1 | // and one unconfirmed UTXO. We check that we can fee bump normally |
2551 | 1 | // (BIP125 rule 2 only apply to newly added unconfirmed input, you can |
2552 | 1 | // always fee bump with an unconfirmed input if it was included in the |
2553 | 1 | // original transaction) |
2554 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
2555 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2556 | 1 | .unwrap() |
2557 | 1 | .assume_checked(); |
2558 | 1 | // We receive a tx with 0 confirmations, which will be used as an input |
2559 | 1 | // in the drain tx. |
2560 | 1 | receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0)); |
2561 | 1 | let mut builder = wallet.build_tx(); |
2562 | 1 | builder |
2563 | 1 | .drain_wallet() |
2564 | 1 | .drain_to(addr.script_pubkey()) |
2565 | 1 | .enable_rbf(); |
2566 | 1 | let psbt = builder.finish().unwrap(); |
2567 | 1 | let mut tx = psbt.extract_tx().expect("failed to extract tx"); |
2568 | 1 | let txid = tx.compute_txid(); |
2569 | 3 | for txin2 in &mut tx.input { |
2570 | 2 | txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature |
2571 | 2 | } |
2572 | 1 | wallet.insert_tx(tx); |
2573 | 1 | insert_seen_at(&mut wallet, txid, 0); |
2574 | 1 | |
2575 | 1 | let mut builder = wallet.build_fee_bump(txid).unwrap(); |
2576 | 1 | builder |
2577 | 1 | .fee_rate(FeeRate::from_sat_per_vb_unchecked(15)) |
2578 | 1 | // remove original tx drain_to address and amount |
2579 | 1 | .set_recipients(Vec::new()) |
2580 | 1 | // set back original drain_to address |
2581 | 1 | .drain_to(addr.script_pubkey()) |
2582 | 1 | // drain wallet output amount will be re-calculated with new fee rate |
2583 | 1 | .drain_wallet(); |
2584 | 1 | builder.finish().unwrap(); |
2585 | 1 | } |
2586 | | |
2587 | | #[test] |
2588 | 1 | fn test_fee_amount_negative_drain_val() { |
2589 | 1 | // While building the transaction, bdk would calculate the drain_value |
2590 | 1 | // as |
2591 | 1 | // current_delta - fee_amount - drain_fee |
2592 | 1 | // using saturating_sub, meaning that if the result would end up negative, |
2593 | 1 | // it'll remain to zero instead. |
2594 | 1 | // This caused a bug in master where we would calculate the wrong fee |
2595 | 1 | // for a transaction. |
2596 | 1 | // See https://github.com/bitcoindevkit/bdk/issues/660 |
2597 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
2598 | 1 | let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt") |
2599 | 1 | .unwrap() |
2600 | 1 | .assume_checked(); |
2601 | 1 | let fee_rate = FeeRate::from_sat_per_kwu(500); |
2602 | 1 | let incoming_op = receive_output_in_latest_block(&mut wallet, 8859); |
2603 | 1 | |
2604 | 1 | let mut builder = wallet.build_tx(); |
2605 | 1 | builder |
2606 | 1 | .add_recipient(send_to.script_pubkey(), Amount::from_sat(8630)) |
2607 | 1 | .add_utxo(incoming_op) |
2608 | 1 | .unwrap() |
2609 | 1 | .enable_rbf() |
2610 | 1 | .fee_rate(fee_rate); |
2611 | 1 | let psbt = builder.finish().unwrap(); |
2612 | 1 | let fee = check_fee!(wallet, psbt); |
2613 | | |
2614 | 1 | assert_eq!(psbt.inputs.len(), 1); |
2615 | | assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), fee_rate, @add_signature); |
2616 | 1 | } |
2617 | | |
2618 | | #[test] |
2619 | 1 | fn test_sign_single_xprv() { |
2620 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); |
2621 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
2622 | 1 | let mut builder = wallet.build_tx(); |
2623 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
2624 | 1 | let mut psbt = builder.finish().unwrap(); |
2625 | 1 | |
2626 | 1 | let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); |
2627 | 1 | assert!(finalized); |
2628 | | |
2629 | 1 | let extracted = psbt.extract_tx().expect("failed to extract tx"); |
2630 | 1 | assert_eq!(extracted.input[0].witness.len(), 2); |
2631 | 1 | } |
2632 | | |
2633 | | #[test] |
2634 | 1 | fn test_sign_single_xprv_with_master_fingerprint_and_path() { |
2635 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh([d34db33f/84h/1h/0h]tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); |
2636 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
2637 | 1 | let mut builder = wallet.build_tx(); |
2638 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
2639 | 1 | let mut psbt = builder.finish().unwrap(); |
2640 | 1 | |
2641 | 1 | let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); |
2642 | 1 | assert!(finalized); |
2643 | | |
2644 | 1 | let extracted = psbt.extract_tx().expect("failed to extract tx"); |
2645 | 1 | assert_eq!(extracted.input[0].witness.len(), 2); |
2646 | 1 | } |
2647 | | |
2648 | | #[test] |
2649 | 1 | fn test_sign_single_xprv_bip44_path() { |
2650 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/44'/0'/0'/0/*)"); |
2651 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
2652 | 1 | let mut builder = wallet.build_tx(); |
2653 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
2654 | 1 | let mut psbt = builder.finish().unwrap(); |
2655 | 1 | |
2656 | 1 | let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); |
2657 | 1 | assert!(finalized); |
2658 | | |
2659 | 1 | let extracted = psbt.extract_tx().expect("failed to extract tx"); |
2660 | 1 | assert_eq!(extracted.input[0].witness.len(), 2); |
2661 | 1 | } |
2662 | | |
2663 | | #[test] |
2664 | 1 | fn test_sign_single_xprv_sh_wpkh() { |
2665 | 1 | let (mut wallet, _) = get_funded_wallet("sh(wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*))"); |
2666 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
2667 | 1 | let mut builder = wallet.build_tx(); |
2668 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
2669 | 1 | let mut psbt = builder.finish().unwrap(); |
2670 | 1 | |
2671 | 1 | let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); |
2672 | 1 | assert!(finalized); |
2673 | | |
2674 | 1 | let extracted = psbt.extract_tx().expect("failed to extract tx"); |
2675 | 1 | assert_eq!(extracted.input[0].witness.len(), 2); |
2676 | 1 | } |
2677 | | |
2678 | | #[test] |
2679 | 1 | fn test_sign_single_wif() { |
2680 | 1 | let (mut wallet, _) = |
2681 | 1 | get_funded_wallet("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"); |
2682 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
2683 | 1 | let mut builder = wallet.build_tx(); |
2684 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
2685 | 1 | let mut psbt = builder.finish().unwrap(); |
2686 | 1 | |
2687 | 1 | let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); |
2688 | 1 | assert!(finalized); |
2689 | | |
2690 | 1 | let extracted = psbt.extract_tx().expect("failed to extract tx"); |
2691 | 1 | assert_eq!(extracted.input[0].witness.len(), 2); |
2692 | 1 | } |
2693 | | |
2694 | | #[test] |
2695 | 1 | fn test_sign_single_xprv_no_hd_keypaths() { |
2696 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); |
2697 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
2698 | 1 | let mut builder = wallet.build_tx(); |
2699 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
2700 | 1 | let mut psbt = builder.finish().unwrap(); |
2701 | 1 | |
2702 | 1 | psbt.inputs[0].bip32_derivation.clear(); |
2703 | 1 | assert_eq!(psbt.inputs[0].bip32_derivation.len(), 0); |
2704 | | |
2705 | 1 | let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); |
2706 | 1 | assert!(finalized); |
2707 | | |
2708 | 1 | let extracted = psbt.extract_tx().expect("failed to extract tx"); |
2709 | 1 | assert_eq!(extracted.input[0].witness.len(), 2); |
2710 | 1 | } |
2711 | | |
2712 | | #[test] |
2713 | 1 | fn test_include_output_redeem_witness_script() { |
2714 | 1 | let desc = get_test_wpkh(); |
2715 | 1 | let change_desc = "sh(wsh(multi(1,cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW,cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu)))"; |
2716 | 1 | let (mut wallet, _) = get_funded_wallet_with_change(desc, change_desc); |
2717 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2718 | 1 | .unwrap() |
2719 | 1 | .assume_checked(); |
2720 | 1 | let mut builder = wallet.build_tx(); |
2721 | 1 | builder |
2722 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) |
2723 | 1 | .include_output_redeem_witness_script(); |
2724 | 1 | let psbt = builder.finish().unwrap(); |
2725 | 1 | |
2726 | 1 | // p2sh-p2wsh transaction should contain both witness and redeem scripts |
2727 | 1 | assert!(psbt |
2728 | 1 | .outputs |
2729 | 1 | .iter() |
2730 | 1 | .any(|output| output.redeem_script.is_some() && output.witness_script.is_some())); |
2731 | 1 | } |
2732 | | |
2733 | | #[test] |
2734 | 1 | fn test_signing_only_one_of_multiple_inputs() { |
2735 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
2736 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
2737 | 1 | .unwrap() |
2738 | 1 | .assume_checked(); |
2739 | 1 | let mut builder = wallet.build_tx(); |
2740 | 1 | builder |
2741 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) |
2742 | 1 | .include_output_redeem_witness_script(); |
2743 | 1 | let mut psbt = builder.finish().unwrap(); |
2744 | 1 | |
2745 | 1 | // add another input to the psbt that is at least passable. |
2746 | 1 | let dud_input = bitcoin::psbt::Input { |
2747 | 1 | witness_utxo: Some(TxOut { |
2748 | 1 | value: Amount::from_sat(100_000), |
2749 | 1 | script_pubkey: miniscript::Descriptor::<bitcoin::PublicKey>::from_str( |
2750 | 1 | "wpkh(025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357)", |
2751 | 1 | ) |
2752 | 1 | .unwrap() |
2753 | 1 | .script_pubkey(), |
2754 | 1 | }), |
2755 | 1 | ..Default::default() |
2756 | 1 | }; |
2757 | 1 | |
2758 | 1 | psbt.inputs.push(dud_input); |
2759 | 1 | psbt.unsigned_tx.input.push(bitcoin::TxIn::default()); |
2760 | 1 | let is_final = wallet |
2761 | 1 | .sign( |
2762 | 1 | &mut psbt, |
2763 | 1 | SignOptions { |
2764 | 1 | trust_witness_utxo: true, |
2765 | 1 | ..Default::default() |
2766 | 1 | }, |
2767 | 1 | ) |
2768 | 1 | .unwrap(); |
2769 | 1 | assert!( |
2770 | 1 | !is_final, |
2771 | 0 | "shouldn't be final since we can't sign one of the inputs" |
2772 | | ); |
2773 | 1 | assert!( |
2774 | 1 | psbt.inputs[0].final_script_witness.is_some(), |
2775 | 0 | "should finalized input it signed" |
2776 | | ) |
2777 | 1 | } |
2778 | | |
2779 | | #[test] |
2780 | 1 | fn test_try_finalize_sign_option() { |
2781 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); |
2782 | | |
2783 | 3 | for try_finalize2 in &[true, false] { |
2784 | 2 | let addr = wallet.next_unused_address(KeychainKind::External); |
2785 | 2 | let mut builder = wallet.build_tx(); |
2786 | 2 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
2787 | 2 | let mut psbt = builder.finish().unwrap(); |
2788 | 2 | |
2789 | 2 | let finalized = wallet |
2790 | 2 | .sign( |
2791 | 2 | &mut psbt, |
2792 | 2 | SignOptions { |
2793 | 2 | try_finalize: *try_finalize, |
2794 | 2 | ..Default::default() |
2795 | 2 | }, |
2796 | 2 | ) |
2797 | 2 | .unwrap(); |
2798 | 2 | |
2799 | 2 | psbt.inputs.iter().for_each(|input| { |
2800 | 2 | if *try_finalize { |
2801 | 1 | assert!(finalized); |
2802 | 1 | assert!(input.final_script_sig.is_none()); |
2803 | 1 | assert!(input.final_script_witness.is_some()); |
2804 | | } else { |
2805 | 1 | assert!(!finalized); |
2806 | 1 | assert!(input.final_script_sig.is_none()); |
2807 | 1 | assert!(input.final_script_witness.is_none()); |
2808 | | } |
2809 | 2 | }); |
2810 | 2 | } |
2811 | 1 | } |
2812 | | |
2813 | | #[test] |
2814 | 1 | fn test_taproot_try_finalize_sign_option() { |
2815 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); |
2816 | | |
2817 | 3 | for try_finalize2 in &[true, false] { |
2818 | 2 | let addr = wallet.next_unused_address(KeychainKind::External); |
2819 | 2 | let mut builder = wallet.build_tx(); |
2820 | 2 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
2821 | 2 | let mut psbt = builder.finish().unwrap(); |
2822 | 2 | |
2823 | 2 | let finalized = wallet |
2824 | 2 | .sign( |
2825 | 2 | &mut psbt, |
2826 | 2 | SignOptions { |
2827 | 2 | try_finalize: *try_finalize, |
2828 | 2 | ..Default::default() |
2829 | 2 | }, |
2830 | 2 | ) |
2831 | 2 | .unwrap(); |
2832 | 2 | |
2833 | 2 | psbt.inputs.iter().for_each(|input| { |
2834 | 2 | if *try_finalize { |
2835 | 1 | assert!(finalized); |
2836 | 1 | assert!(input.final_script_sig.is_none()); |
2837 | 1 | assert!(input.final_script_witness.is_some()); |
2838 | 1 | assert!(input.tap_key_sig.is_none()); |
2839 | 1 | assert!(input.tap_script_sigs.is_empty()); |
2840 | 1 | assert!(input.tap_scripts.is_empty()); |
2841 | 1 | assert!(input.tap_key_origins.is_empty()); |
2842 | 1 | assert!(input.tap_internal_key.is_none()); |
2843 | 1 | assert!(input.tap_merkle_root.is_none()); |
2844 | | } else { |
2845 | 1 | assert!(!finalized); |
2846 | 1 | assert!(input.final_script_sig.is_none()); |
2847 | 1 | assert!(input.final_script_witness.is_none()); |
2848 | | } |
2849 | 2 | }); |
2850 | 2 | psbt.outputs.iter().for_each(|output| { |
2851 | 2 | if *try_finalize { |
2852 | 1 | assert!(finalized); |
2853 | 1 | assert!(output.tap_key_origins.is_empty()); |
2854 | | } else { |
2855 | 1 | assert!(!finalized); |
2856 | 1 | assert!(!output.tap_key_origins.is_empty()); |
2857 | | } |
2858 | 2 | }); |
2859 | 2 | } |
2860 | 1 | } |
2861 | | |
2862 | | #[test] |
2863 | 1 | fn test_sign_nonstandard_sighash() { |
2864 | 1 | let sighash = EcdsaSighashType::NonePlusAnyoneCanPay; |
2865 | 1 | |
2866 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); |
2867 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
2868 | 1 | let mut builder = wallet.build_tx(); |
2869 | 1 | builder |
2870 | 1 | .drain_to(addr.script_pubkey()) |
2871 | 1 | .sighash(sighash.into()) |
2872 | 1 | .drain_wallet(); |
2873 | 1 | let mut psbt = builder.finish().unwrap(); |
2874 | 1 | |
2875 | 1 | let result = wallet.sign(&mut psbt, Default::default()); |
2876 | 1 | assert!( |
2877 | 1 | result.is_err(), |
2878 | 0 | "Signing should have failed because the TX uses non-standard sighashes" |
2879 | | ); |
2880 | 0 | assert_matches!( |
2881 | 1 | result, |
2882 | | Err(SignerError::NonStandardSighash), |
2883 | | "Signing failed with the wrong error type" |
2884 | | ); |
2885 | | |
2886 | | // try again after opting-in |
2887 | 1 | let result = wallet.sign( |
2888 | 1 | &mut psbt, |
2889 | 1 | SignOptions { |
2890 | 1 | allow_all_sighashes: true, |
2891 | 1 | ..Default::default() |
2892 | 1 | }, |
2893 | 1 | ); |
2894 | 1 | assert!(result.is_ok(), "Signing should have worked"0 ); |
2895 | 1 | assert!( |
2896 | 1 | result.unwrap(), |
2897 | 0 | "Should finalize the input since we can produce signatures" |
2898 | | ); |
2899 | | |
2900 | 1 | let extracted = psbt.extract_tx().expect("failed to extract tx"); |
2901 | 1 | assert_eq!( |
2902 | 1 | *extracted.input[0].witness.to_vec()[0].last().unwrap(), |
2903 | 1 | sighash.to_u32() as u8, |
2904 | 0 | "The signature should have been made with the right sighash" |
2905 | | ); |
2906 | 1 | } |
2907 | | |
2908 | | #[test] |
2909 | 1 | fn test_unused_address() { |
2910 | 1 | let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; |
2911 | 1 | let change_descriptor = get_test_wpkh(); |
2912 | 1 | let mut wallet = Wallet::create(descriptor, change_descriptor) |
2913 | 1 | .network(Network::Testnet) |
2914 | 1 | .create_wallet_no_persist() |
2915 | 1 | .expect("wallet"); |
2916 | 1 | |
2917 | 1 | // `list_unused_addresses` should be empty if we haven't revealed any |
2918 | 1 | assert!(wallet |
2919 | 1 | .list_unused_addresses(KeychainKind::External) |
2920 | 1 | .next() |
2921 | 1 | .is_none()); |
2922 | | |
2923 | 1 | assert_eq!( |
2924 | 1 | wallet |
2925 | 1 | .next_unused_address(KeychainKind::External) |
2926 | 1 | .to_string(), |
2927 | 1 | "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" |
2928 | 1 | ); |
2929 | 1 | assert_eq!( |
2930 | 1 | wallet |
2931 | 1 | .list_unused_addresses(KeychainKind::External) |
2932 | 1 | .next() |
2933 | 1 | .unwrap() |
2934 | 1 | .to_string(), |
2935 | 1 | "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" |
2936 | 1 | ); |
2937 | 1 | } |
2938 | | |
2939 | | #[test] |
2940 | 1 | fn test_next_unused_address() { |
2941 | 1 | let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; |
2942 | 1 | let change_descriptor = get_test_wpkh(); |
2943 | 1 | let mut wallet = Wallet::create(descriptor, change_descriptor) |
2944 | 1 | .network(Network::Testnet) |
2945 | 1 | .create_wallet_no_persist() |
2946 | 1 | .expect("wallet"); |
2947 | 1 | assert_eq!(wallet.derivation_index(KeychainKind::External), None); |
2948 | | |
2949 | 1 | assert_eq!( |
2950 | 1 | wallet |
2951 | 1 | .next_unused_address(KeychainKind::External) |
2952 | 1 | .to_string(), |
2953 | 1 | "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" |
2954 | 1 | ); |
2955 | 1 | assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); |
2956 | | // calling next_unused again gives same address |
2957 | 1 | assert_eq!( |
2958 | 1 | wallet |
2959 | 1 | .next_unused_address(KeychainKind::External) |
2960 | 1 | .to_string(), |
2961 | 1 | "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" |
2962 | 1 | ); |
2963 | 1 | assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); |
2964 | | |
2965 | | // test mark used / unused |
2966 | 1 | assert!(wallet.mark_used(KeychainKind::External, 0)); |
2967 | 1 | let next_unused_addr = wallet.next_unused_address(KeychainKind::External); |
2968 | 1 | assert_eq!(next_unused_addr.index, 1); |
2969 | | |
2970 | 1 | assert!(wallet.unmark_used(KeychainKind::External, 0)); |
2971 | 1 | let next_unused_addr = wallet.next_unused_address(KeychainKind::External); |
2972 | 1 | assert_eq!(next_unused_addr.index, 0); |
2973 | | |
2974 | | // use the above address |
2975 | 1 | receive_output_in_latest_block(&mut wallet, 25_000); |
2976 | 1 | |
2977 | 1 | assert_eq!( |
2978 | 1 | wallet |
2979 | 1 | .next_unused_address(KeychainKind::External) |
2980 | 1 | .to_string(), |
2981 | 1 | "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" |
2982 | 1 | ); |
2983 | 1 | assert_eq!(wallet.derivation_index(KeychainKind::External), Some(1)); |
2984 | | |
2985 | | // trying to mark index 0 unused should return false |
2986 | 1 | assert!(!wallet.unmark_used(KeychainKind::External, 0)); |
2987 | 1 | } |
2988 | | |
2989 | | #[test] |
2990 | 1 | fn test_peek_address_at_index() { |
2991 | 1 | let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; |
2992 | 1 | let change_descriptor = get_test_wpkh(); |
2993 | 1 | let mut wallet = Wallet::create(descriptor, change_descriptor) |
2994 | 1 | .network(Network::Testnet) |
2995 | 1 | .create_wallet_no_persist() |
2996 | 1 | .expect("wallet"); |
2997 | 1 | |
2998 | 1 | assert_eq!( |
2999 | 1 | wallet.peek_address(KeychainKind::External, 1).to_string(), |
3000 | 1 | "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" |
3001 | 1 | ); |
3002 | | |
3003 | 1 | assert_eq!( |
3004 | 1 | wallet.peek_address(KeychainKind::External, 0).to_string(), |
3005 | 1 | "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" |
3006 | 1 | ); |
3007 | | |
3008 | 1 | assert_eq!( |
3009 | 1 | wallet.peek_address(KeychainKind::External, 2).to_string(), |
3010 | 1 | "tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2" |
3011 | 1 | ); |
3012 | | |
3013 | | // current new address is not affected |
3014 | 1 | assert_eq!( |
3015 | 1 | wallet |
3016 | 1 | .reveal_next_address(KeychainKind::External) |
3017 | 1 | .to_string(), |
3018 | 1 | "tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a" |
3019 | 1 | ); |
3020 | | |
3021 | 1 | assert_eq!( |
3022 | 1 | wallet |
3023 | 1 | .reveal_next_address(KeychainKind::External) |
3024 | 1 | .to_string(), |
3025 | 1 | "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" |
3026 | 1 | ); |
3027 | 1 | } |
3028 | | |
3029 | | #[test] |
3030 | 1 | fn test_peek_address_at_index_not_derivable() { |
3031 | 1 | let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)"; |
3032 | 1 | let wallet = Wallet::create(descriptor, get_test_wpkh()) |
3033 | 1 | .network(Network::Testnet) |
3034 | 1 | .create_wallet_no_persist() |
3035 | 1 | .unwrap(); |
3036 | 1 | |
3037 | 1 | assert_eq!( |
3038 | 1 | wallet.peek_address(KeychainKind::External, 1).to_string(), |
3039 | 1 | "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" |
3040 | 1 | ); |
3041 | | |
3042 | 1 | assert_eq!( |
3043 | 1 | wallet.peek_address(KeychainKind::External, 0).to_string(), |
3044 | 1 | "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" |
3045 | 1 | ); |
3046 | | |
3047 | 1 | assert_eq!( |
3048 | 1 | wallet.peek_address(KeychainKind::External, 2).to_string(), |
3049 | 1 | "tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7" |
3050 | 1 | ); |
3051 | 1 | } |
3052 | | |
3053 | | #[test] |
3054 | 1 | fn test_returns_index_and_address() { |
3055 | 1 | let descriptor = |
3056 | 1 | "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; |
3057 | 1 | let mut wallet = Wallet::create(descriptor, get_test_wpkh()) |
3058 | 1 | .network(Network::Testnet) |
3059 | 1 | .create_wallet_no_persist() |
3060 | 1 | .unwrap(); |
3061 | 1 | |
3062 | 1 | // new index 0 |
3063 | 1 | assert_eq!( |
3064 | 1 | wallet.reveal_next_address(KeychainKind::External), |
3065 | 1 | AddressInfo { |
3066 | 1 | index: 0, |
3067 | 1 | address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a") |
3068 | 1 | .unwrap() |
3069 | 1 | .assume_checked(), |
3070 | 1 | keychain: KeychainKind::External, |
3071 | 1 | } |
3072 | 1 | ); |
3073 | | |
3074 | | // new index 1 |
3075 | 1 | assert_eq!( |
3076 | 1 | wallet.reveal_next_address(KeychainKind::External), |
3077 | 1 | AddressInfo { |
3078 | 1 | index: 1, |
3079 | 1 | address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7") |
3080 | 1 | .unwrap() |
3081 | 1 | .assume_checked(), |
3082 | 1 | keychain: KeychainKind::External, |
3083 | 1 | } |
3084 | 1 | ); |
3085 | | |
3086 | | // peek index 25 |
3087 | 1 | assert_eq!( |
3088 | 1 | wallet.peek_address(KeychainKind::External, 25), |
3089 | 1 | AddressInfo { |
3090 | 1 | index: 25, |
3091 | 1 | address: Address::from_str("tb1qsp7qu0knx3sl6536dzs0703u2w2ag6ppl9d0c2") |
3092 | 1 | .unwrap() |
3093 | 1 | .assume_checked(), |
3094 | 1 | keychain: KeychainKind::External, |
3095 | 1 | } |
3096 | 1 | ); |
3097 | | |
3098 | | // new index 2 |
3099 | 1 | assert_eq!( |
3100 | 1 | wallet.reveal_next_address(KeychainKind::External), |
3101 | 1 | AddressInfo { |
3102 | 1 | index: 2, |
3103 | 1 | address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2") |
3104 | 1 | .unwrap() |
3105 | 1 | .assume_checked(), |
3106 | 1 | keychain: KeychainKind::External, |
3107 | 1 | } |
3108 | 1 | ); |
3109 | 1 | } |
3110 | | |
3111 | | #[test] |
3112 | 1 | fn test_sending_to_bip350_bech32m_address() { |
3113 | 1 | let (mut wallet, _) = get_funded_wallet_wpkh(); |
3114 | 1 | let addr = Address::from_str("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c") |
3115 | 1 | .unwrap() |
3116 | 1 | .assume_checked(); |
3117 | 1 | let mut builder = wallet.build_tx(); |
3118 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); |
3119 | 1 | builder.finish().unwrap(); |
3120 | 1 | } |
3121 | | |
3122 | | #[test] |
3123 | 1 | fn test_get_address() { |
3124 | 1 | use bdk_wallet::descriptor::template::Bip84; |
3125 | 1 | let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); |
3126 | 1 | let wallet = Wallet::create( |
3127 | 1 | Bip84(key, KeychainKind::External), |
3128 | 1 | Bip84(key, KeychainKind::Internal), |
3129 | 1 | ) |
3130 | 1 | .network(Network::Regtest) |
3131 | 1 | .create_wallet_no_persist() |
3132 | 1 | .unwrap(); |
3133 | 1 | |
3134 | 1 | assert_eq!( |
3135 | 1 | wallet.peek_address(KeychainKind::External, 0), |
3136 | 1 | AddressInfo { |
3137 | 1 | index: 0, |
3138 | 1 | address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w") |
3139 | 1 | .unwrap() |
3140 | 1 | .assume_checked(), |
3141 | 1 | keychain: KeychainKind::External, |
3142 | 1 | } |
3143 | 1 | ); |
3144 | | |
3145 | 1 | assert_eq!( |
3146 | 1 | wallet.peek_address(KeychainKind::Internal, 0), |
3147 | 1 | AddressInfo { |
3148 | 1 | index: 0, |
3149 | 1 | address: Address::from_str("bcrt1q0ue3s5y935tw7v3gmnh36c5zzsaw4n9c9smq79") |
3150 | 1 | .unwrap() |
3151 | 1 | .assume_checked(), |
3152 | 1 | keychain: KeychainKind::Internal, |
3153 | 1 | } |
3154 | 1 | ); |
3155 | 1 | } |
3156 | | |
3157 | | #[test] |
3158 | 1 | fn test_reveal_addresses() { |
3159 | 1 | let (desc, change_desc) = get_test_tr_single_sig_xprv_with_change_desc(); |
3160 | 1 | let mut wallet = Wallet::create(desc, change_desc) |
3161 | 1 | .network(Network::Signet) |
3162 | 1 | .create_wallet_no_persist() |
3163 | 1 | .unwrap(); |
3164 | 1 | let keychain = KeychainKind::External; |
3165 | 1 | |
3166 | 1 | let last_revealed_addr = wallet.reveal_addresses_to(keychain, 9).last().unwrap(); |
3167 | 1 | assert_eq!(wallet.derivation_index(keychain), Some(9)); |
3168 | | |
3169 | 1 | let unused_addrs = wallet.list_unused_addresses(keychain).collect::<Vec<_>>(); |
3170 | 1 | assert_eq!(unused_addrs.len(), 10); |
3171 | 1 | assert_eq!(unused_addrs.last().unwrap(), &last_revealed_addr); |
3172 | | |
3173 | | // revealing to an already revealed index returns nothing |
3174 | 1 | let mut already_revealed = wallet.reveal_addresses_to(keychain, 9); |
3175 | 1 | assert!(already_revealed.next().is_none()); |
3176 | 1 | } |
3177 | | |
3178 | | #[test] |
3179 | 1 | fn test_get_address_no_reuse() { |
3180 | 1 | use bdk_wallet::descriptor::template::Bip84; |
3181 | 1 | use std::collections::HashSet; |
3182 | 1 | |
3183 | 1 | let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); |
3184 | 1 | let mut wallet = Wallet::create( |
3185 | 1 | Bip84(key, KeychainKind::External), |
3186 | 1 | Bip84(key, KeychainKind::Internal), |
3187 | 1 | ) |
3188 | 1 | .network(Network::Regtest) |
3189 | 1 | .create_wallet_no_persist() |
3190 | 1 | .unwrap(); |
3191 | 1 | |
3192 | 1 | let mut used_set = HashSet::new(); |
3193 | 1 | |
3194 | 3 | (0..3).for_each(|_| { |
3195 | 3 | let external_addr = wallet.reveal_next_address(KeychainKind::External).address; |
3196 | 3 | assert!(used_set.insert(external_addr)); |
3197 | | |
3198 | 3 | let internal_addr = wallet.reveal_next_address(KeychainKind::Internal).address; |
3199 | 3 | assert!(used_set.insert(internal_addr)); |
3200 | 3 | }); |
3201 | 1 | } |
3202 | | |
3203 | | #[test] |
3204 | 1 | fn test_taproot_psbt_populate_tap_key_origins() { |
3205 | 1 | let (desc, change_desc) = get_test_tr_single_sig_xprv_with_change_desc(); |
3206 | 1 | let (mut wallet, _) = get_funded_wallet_with_change(desc, change_desc); |
3207 | 1 | let addr = wallet.reveal_next_address(KeychainKind::External); |
3208 | 1 | |
3209 | 1 | let mut builder = wallet.build_tx(); |
3210 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
3211 | 1 | let psbt = builder.finish().unwrap(); |
3212 | 1 | |
3213 | 1 | assert_eq!( |
3214 | 1 | psbt.inputs[0] |
3215 | 1 | .tap_key_origins |
3216 | 1 | .clone() |
3217 | 1 | .into_iter() |
3218 | 1 | .collect::<Vec<_>>(), |
3219 | 1 | vec![( |
3220 | 1 | from_str!("0841db1dbaf949dbbda893e01a18f2cca9179cf8ea2d8e667857690502b06483"), |
3221 | 1 | (vec![], (from_str!("f6a5cb8b"), from_str!("m/0/0"))) |
3222 | 1 | )], |
3223 | 0 | "Wrong input tap_key_origins" |
3224 | | ); |
3225 | 1 | assert_eq!( |
3226 | 1 | psbt.outputs[0] |
3227 | 1 | .tap_key_origins |
3228 | 1 | .clone() |
3229 | 1 | .into_iter() |
3230 | 1 | .collect::<Vec<_>>(), |
3231 | 1 | vec![( |
3232 | 1 | from_str!("9187c1e80002d19ddde9c5c7f5394e9a063cee8695867b58815af0562695ca21"), |
3233 | 1 | (vec![], (from_str!("f6a5cb8b"), from_str!("m/0/1"))) |
3234 | 1 | )], |
3235 | 0 | "Wrong output tap_key_origins" |
3236 | | ); |
3237 | 1 | } |
3238 | | |
3239 | | #[test] |
3240 | 1 | fn test_taproot_psbt_populate_tap_key_origins_repeated_key() { |
3241 | 1 | let (mut wallet, _) = |
3242 | 1 | get_funded_wallet_with_change(get_test_tr_repeated_key(), get_test_tr_single_sig()); |
3243 | 1 | let addr = wallet.reveal_next_address(KeychainKind::External); |
3244 | 1 | |
3245 | 1 | let path = vec![("rn4nre9c".to_string(), vec![0])] |
3246 | 1 | .into_iter() |
3247 | 1 | .collect(); |
3248 | 1 | |
3249 | 1 | let mut builder = wallet.build_tx(); |
3250 | 1 | builder |
3251 | 1 | .drain_to(addr.script_pubkey()) |
3252 | 1 | .drain_wallet() |
3253 | 1 | .policy_path(path, KeychainKind::External); |
3254 | 1 | let psbt = builder.finish().unwrap(); |
3255 | 1 | |
3256 | 1 | let mut input_key_origins = psbt.inputs[0] |
3257 | 1 | .tap_key_origins |
3258 | 1 | .clone() |
3259 | 1 | .into_iter() |
3260 | 1 | .collect::<Vec<_>>(); |
3261 | 1 | input_key_origins.sort(); |
3262 | 1 | |
3263 | 1 | assert_eq!( |
3264 | 1 | input_key_origins, |
3265 | 1 | vec![ |
3266 | 1 | ( |
3267 | 1 | from_str!("2b0558078bec38694a84933d659303e2575dae7e91685911454115bfd64487e3"), |
3268 | 1 | ( |
3269 | 1 | vec![ |
3270 | 1 | from_str!( |
3271 | 1 | "858ad7a7d7f270e2c490c4d6ba00c499e46b18fdd59ea3c2c47d20347110271e" |
3272 | 1 | ), |
3273 | 1 | from_str!( |
3274 | 1 | "f6e927ad4492c051fe325894a4f5f14538333b55a35f099876be42009ec8f903" |
3275 | 1 | ), |
3276 | 1 | ], |
3277 | 1 | (FromStr::from_str("ece52657").unwrap(), vec![].into()) |
3278 | 1 | ) |
3279 | 1 | ), |
3280 | 1 | ( |
3281 | 1 | from_str!("b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55"), |
3282 | 1 | ( |
3283 | 1 | vec![], |
3284 | 1 | (FromStr::from_str("871fd295").unwrap(), vec![].into()) |
3285 | 1 | ) |
3286 | 1 | ) |
3287 | 1 | ], |
3288 | 0 | "Wrong input tap_key_origins" |
3289 | | ); |
3290 | | |
3291 | 1 | let mut output_key_origins = psbt.outputs[0] |
3292 | 1 | .tap_key_origins |
3293 | 1 | .clone() |
3294 | 1 | .into_iter() |
3295 | 1 | .collect::<Vec<_>>(); |
3296 | 1 | output_key_origins.sort(); |
3297 | 1 | |
3298 | 1 | assert_eq!( |
3299 | | input_key_origins, output_key_origins, |
3300 | 0 | "Wrong output tap_key_origins" |
3301 | | ); |
3302 | 1 | } |
3303 | | |
3304 | | #[test] |
3305 | 1 | fn test_taproot_psbt_input_tap_tree() { |
3306 | 1 | use bitcoin::hex::FromHex; |
3307 | 1 | use bitcoin::taproot; |
3308 | 1 | |
3309 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); |
3310 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3311 | 1 | |
3312 | 1 | let mut builder = wallet.build_tx(); |
3313 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
3314 | 1 | let psbt = builder.finish().unwrap(); |
3315 | 1 | |
3316 | 1 | assert_eq!( |
3317 | 1 | psbt.inputs[0].tap_merkle_root, |
3318 | 1 | Some( |
3319 | 1 | TapNodeHash::from_str( |
3320 | 1 | "61f81509635053e52d9d1217545916167394490da2287aca4693606e43851986" |
3321 | 1 | ) |
3322 | 1 | .unwrap() |
3323 | 1 | ), |
3324 | 1 | ); |
3325 | 1 | assert_eq!( |
3326 | 1 | psbt.inputs[0].tap_scripts.clone().into_iter().collect::<Vec<_>>(), |
3327 | 1 | vec![ |
3328 | 1 | (taproot::ControlBlock::decode(&Vec::<u8>::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b7ef769a745e625ed4b9a4982a4dc08274c59187e73e6f07171108f455081cb2").unwrap()).unwrap(), (ScriptBuf::from_hex("208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac").unwrap(), taproot::LeafVersion::TapScript)), |
3329 | 1 | (taproot::ControlBlock::decode(&Vec::<u8>::from_hex("c0b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55b9a515f7be31a70186e3c5937ee4a70cc4b4e1efe876c1d38e408222ffc64834").unwrap()).unwrap(), (ScriptBuf::from_hex("2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac").unwrap(), taproot::LeafVersion::TapScript)), |
3330 | 1 | ], |
3331 | 1 | ); |
3332 | 1 | assert_eq!( |
3333 | 1 | psbt.inputs[0].tap_internal_key, |
3334 | 1 | Some(from_str!( |
3335 | 1 | "b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55" |
3336 | 1 | )) |
3337 | 1 | ); |
3338 | | |
3339 | | // Since we are creating an output to the same address as the input, assert that the |
3340 | | // internal_key is the same |
3341 | 1 | assert_eq!( |
3342 | 1 | psbt.inputs[0].tap_internal_key, |
3343 | 1 | psbt.outputs[0].tap_internal_key |
3344 | 1 | ); |
3345 | | |
3346 | 1 | let tap_tree: bitcoin::taproot::TapTree = serde_json::from_str(r#"[1,{"Script":["2051494dc22e24a32fe9dcfbd7e85faf345fa1df296fb49d156e859ef345201295ac",192]},1,{"Script":["208aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642ac",192]}]"#).unwrap(); |
3347 | 1 | assert_eq!(psbt.outputs[0].tap_tree, Some(tap_tree)); |
3348 | 1 | } |
3349 | | |
3350 | | #[test] |
3351 | 1 | fn test_taproot_sign_missing_witness_utxo() { |
3352 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); |
3353 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3354 | 1 | let mut builder = wallet.build_tx(); |
3355 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
3356 | 1 | let mut psbt = builder.finish().unwrap(); |
3357 | 1 | let witness_utxo = psbt.inputs[0].witness_utxo.take(); |
3358 | 1 | |
3359 | 1 | let result = wallet.sign( |
3360 | 1 | &mut psbt, |
3361 | 1 | SignOptions { |
3362 | 1 | allow_all_sighashes: true, |
3363 | 1 | ..Default::default() |
3364 | 1 | }, |
3365 | 1 | ); |
3366 | 0 | assert_matches!( |
3367 | 1 | result, |
3368 | | Err(SignerError::MissingWitnessUtxo), |
3369 | | "Signing should have failed with the correct error because the witness_utxo is missing" |
3370 | | ); |
3371 | | |
3372 | | // restore the witness_utxo |
3373 | 1 | psbt.inputs[0].witness_utxo = witness_utxo; |
3374 | 1 | |
3375 | 1 | let result = wallet.sign( |
3376 | 1 | &mut psbt, |
3377 | 1 | SignOptions { |
3378 | 1 | allow_all_sighashes: true, |
3379 | 1 | ..Default::default() |
3380 | 1 | }, |
3381 | 1 | ); |
3382 | | |
3383 | 1 | assert_matches!( |
3384 | 1 | result, |
3385 | | Ok(true), |
3386 | | "Should finalize the input since we can produce signatures" |
3387 | | ); |
3388 | 1 | } |
3389 | | |
3390 | | #[test] |
3391 | 1 | fn test_taproot_sign_using_non_witness_utxo() { |
3392 | 1 | let (mut wallet, prev_txid) = get_funded_wallet(get_test_tr_single_sig()); |
3393 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3394 | 1 | let mut builder = wallet.build_tx(); |
3395 | 1 | builder.drain_to(addr.script_pubkey()).drain_wallet(); |
3396 | 1 | let mut psbt = builder.finish().unwrap(); |
3397 | 1 | |
3398 | 1 | psbt.inputs[0].witness_utxo = None; |
3399 | 1 | psbt.inputs[0].non_witness_utxo = |
3400 | 1 | Some(wallet.get_tx(prev_txid).unwrap().tx_node.as_ref().clone()); |
3401 | 1 | assert!( |
3402 | 1 | psbt.inputs[0].non_witness_utxo.is_some(), |
3403 | 0 | "Previous tx should be present in the database" |
3404 | | ); |
3405 | | |
3406 | 1 | let result = wallet.sign(&mut psbt, Default::default()); |
3407 | 1 | assert!(result.is_ok(), "Signing should have worked"0 ); |
3408 | 1 | assert!( |
3409 | 1 | result.unwrap(), |
3410 | 0 | "Should finalize the input since we can produce signatures" |
3411 | | ); |
3412 | 1 | } |
3413 | | |
3414 | | #[test] |
3415 | 1 | fn test_taproot_foreign_utxo() { |
3416 | 1 | let (mut wallet1, _) = get_funded_wallet_wpkh(); |
3417 | 1 | let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig()); |
3418 | 1 | |
3419 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
3420 | 1 | .unwrap() |
3421 | 1 | .assume_checked(); |
3422 | 1 | let utxo = wallet2.list_unspent().next().unwrap(); |
3423 | 1 | let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); |
3424 | 1 | let foreign_utxo_satisfaction = wallet2 |
3425 | 1 | .public_descriptor(KeychainKind::External) |
3426 | 1 | .max_weight_to_satisfy() |
3427 | 1 | .unwrap(); |
3428 | 1 | |
3429 | 1 | assert!( |
3430 | 1 | psbt_input.non_witness_utxo.is_none(), |
3431 | 0 | "`non_witness_utxo` should never be populated for taproot" |
3432 | | ); |
3433 | | |
3434 | 1 | let mut builder = wallet1.build_tx(); |
3435 | 1 | builder |
3436 | 1 | .add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)) |
3437 | 1 | .add_foreign_utxo(utxo.outpoint, psbt_input, foreign_utxo_satisfaction) |
3438 | 1 | .unwrap(); |
3439 | 1 | let psbt = builder.finish().unwrap(); |
3440 | 1 | let sent_received = |
3441 | 1 | wallet1.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); |
3442 | 1 | wallet1.insert_txout(utxo.outpoint, utxo.txout); |
3443 | 1 | let fee = check_fee!(wallet1, psbt); |
3444 | | |
3445 | 1 | assert_eq!( |
3446 | 1 | sent_received.0 - sent_received.1, |
3447 | 1 | Amount::from_sat(10_000) + fee.unwrap_or(Amount::ZERO), |
3448 | 0 | "we should have only net spent ~10_000" |
3449 | | ); |
3450 | | |
3451 | 1 | assert!( |
3452 | 1 | psbt.unsigned_tx |
3453 | 1 | .input |
3454 | 1 | .iter() |
3455 | 1 | .any(|input| input.previous_output == utxo.outpoint), |
3456 | 0 | "foreign_utxo should be in there" |
3457 | | ); |
3458 | 1 | } |
3459 | | |
3460 | 2 | fn test_spend_from_wallet(mut wallet: Wallet) { |
3461 | 2 | let addr = wallet.next_unused_address(KeychainKind::External); |
3462 | 2 | |
3463 | 2 | let mut builder = wallet.build_tx(); |
3464 | 2 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
3465 | 2 | let mut psbt = builder.finish().unwrap(); |
3466 | 2 | |
3467 | 2 | assert!( |
3468 | 2 | wallet.sign(&mut psbt, Default::default()).unwrap(), |
3469 | 0 | "Unable to finalize tx" |
3470 | | ); |
3471 | 2 | } |
3472 | | |
3473 | | // #[test] |
3474 | | // fn test_taproot_key_spend() { |
3475 | | // let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); |
3476 | | // test_spend_from_wallet(wallet); |
3477 | | |
3478 | | // let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); |
3479 | | // test_spend_from_wallet(wallet); |
3480 | | // } |
3481 | | |
3482 | | #[test] |
3483 | 1 | fn test_taproot_no_key_spend() { |
3484 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); |
3485 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3486 | 1 | |
3487 | 1 | let mut builder = wallet.build_tx(); |
3488 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
3489 | 1 | let mut psbt = builder.finish().unwrap(); |
3490 | 1 | |
3491 | 1 | assert!( |
3492 | 1 | wallet |
3493 | 1 | .sign( |
3494 | 1 | &mut psbt, |
3495 | 1 | SignOptions { |
3496 | 1 | sign_with_tap_internal_key: false, |
3497 | 1 | ..Default::default() |
3498 | 1 | }, |
3499 | 1 | ) |
3500 | 1 | .unwrap(), |
3501 | 0 | "Unable to finalize tx" |
3502 | | ); |
3503 | | |
3504 | 1 | assert!(psbt.inputs.iter().all(|i| i.tap_key_sig.is_none())); |
3505 | 1 | } |
3506 | | |
3507 | | #[test] |
3508 | 1 | fn test_taproot_script_spend() { |
3509 | 1 | let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree()); |
3510 | 1 | test_spend_from_wallet(wallet); |
3511 | 1 | |
3512 | 1 | let (wallet, _) = get_funded_wallet(get_test_tr_with_taptree_xprv()); |
3513 | 1 | test_spend_from_wallet(wallet); |
3514 | 1 | } |
3515 | | |
3516 | | #[test] |
3517 | 1 | fn test_taproot_script_spend_sign_all_leaves() { |
3518 | 1 | use bdk_wallet::signer::TapLeavesOptions; |
3519 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); |
3520 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3521 | 1 | |
3522 | 1 | let mut builder = wallet.build_tx(); |
3523 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
3524 | 1 | let mut psbt = builder.finish().unwrap(); |
3525 | 1 | |
3526 | 1 | assert!( |
3527 | 1 | wallet |
3528 | 1 | .sign( |
3529 | 1 | &mut psbt, |
3530 | 1 | SignOptions { |
3531 | 1 | tap_leaves_options: TapLeavesOptions::All, |
3532 | 1 | ..Default::default() |
3533 | 1 | }, |
3534 | 1 | ) |
3535 | 1 | .unwrap(), |
3536 | 0 | "Unable to finalize tx" |
3537 | | ); |
3538 | | |
3539 | 1 | assert!(psbt |
3540 | 1 | .inputs |
3541 | 1 | .iter() |
3542 | 1 | .all(|i| i.tap_script_sigs.len() == i.tap_scripts.len())); |
3543 | 1 | } |
3544 | | |
3545 | | #[test] |
3546 | 1 | fn test_taproot_script_spend_sign_include_some_leaves() { |
3547 | 1 | use bdk_wallet::signer::TapLeavesOptions; |
3548 | 1 | use bitcoin::taproot::TapLeafHash; |
3549 | 1 | |
3550 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); |
3551 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3552 | 1 | |
3553 | 1 | let mut builder = wallet.build_tx(); |
3554 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
3555 | 1 | let mut psbt = builder.finish().unwrap(); |
3556 | 1 | let mut script_leaves: Vec<_> = psbt.inputs[0] |
3557 | 1 | .tap_scripts |
3558 | 1 | .clone() |
3559 | 1 | .values() |
3560 | 2 | .map(|(script, version)| TapLeafHash::from_script(script, *version)) |
3561 | 1 | .collect(); |
3562 | 1 | let included_script_leaves = vec![script_leaves.pop().unwrap()]; |
3563 | 1 | let excluded_script_leaves = script_leaves; |
3564 | 1 | |
3565 | 1 | assert!( |
3566 | 1 | wallet |
3567 | 1 | .sign( |
3568 | 1 | &mut psbt, |
3569 | 1 | SignOptions { |
3570 | 1 | tap_leaves_options: TapLeavesOptions::Include(included_script_leaves.clone()), |
3571 | 1 | ..Default::default() |
3572 | 1 | }, |
3573 | 1 | ) |
3574 | 1 | .unwrap(), |
3575 | 0 | "Unable to finalize tx" |
3576 | | ); |
3577 | | |
3578 | 1 | assert!(psbt.inputs[0] |
3579 | 1 | .tap_script_sigs |
3580 | 1 | .iter() |
3581 | 1 | .all(|s| included_script_leaves.contains(&s.0 .1)0 |
3582 | 1 | && !excluded_script_leaves.contains(&s.0 .10 )0 )); |
3583 | 1 | } |
3584 | | |
3585 | | #[test] |
3586 | 1 | fn test_taproot_script_spend_sign_exclude_some_leaves() { |
3587 | 1 | use bdk_wallet::signer::TapLeavesOptions; |
3588 | 1 | use bitcoin::taproot::TapLeafHash; |
3589 | 1 | |
3590 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); |
3591 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3592 | 1 | |
3593 | 1 | let mut builder = wallet.build_tx(); |
3594 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
3595 | 1 | let mut psbt = builder.finish().unwrap(); |
3596 | 1 | let mut script_leaves: Vec<_> = psbt.inputs[0] |
3597 | 1 | .tap_scripts |
3598 | 1 | .clone() |
3599 | 1 | .values() |
3600 | 2 | .map(|(script, version)| TapLeafHash::from_script(script, *version)) |
3601 | 1 | .collect(); |
3602 | 1 | let included_script_leaves = [script_leaves.pop().unwrap()]; |
3603 | 1 | let excluded_script_leaves = script_leaves; |
3604 | 1 | |
3605 | 1 | assert!( |
3606 | 1 | wallet |
3607 | 1 | .sign( |
3608 | 1 | &mut psbt, |
3609 | 1 | SignOptions { |
3610 | 1 | tap_leaves_options: TapLeavesOptions::Exclude(excluded_script_leaves.clone()), |
3611 | 1 | ..Default::default() |
3612 | 1 | }, |
3613 | 1 | ) |
3614 | 1 | .unwrap(), |
3615 | 0 | "Unable to finalize tx" |
3616 | | ); |
3617 | | |
3618 | 1 | assert!(psbt.inputs[0] |
3619 | 1 | .tap_script_sigs |
3620 | 1 | .iter() |
3621 | 1 | .all(|s| included_script_leaves.contains(&s.0 .1)0 |
3622 | 1 | && !excluded_script_leaves.contains(&s.0 .10 )0 )); |
3623 | 1 | } |
3624 | | |
3625 | | #[test] |
3626 | 1 | fn test_taproot_script_spend_sign_no_leaves() { |
3627 | 1 | use bdk_wallet::signer::TapLeavesOptions; |
3628 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv()); |
3629 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3630 | 1 | |
3631 | 1 | let mut builder = wallet.build_tx(); |
3632 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
3633 | 1 | let mut psbt = builder.finish().unwrap(); |
3634 | 1 | |
3635 | 1 | wallet |
3636 | 1 | .sign( |
3637 | 1 | &mut psbt, |
3638 | 1 | SignOptions { |
3639 | 1 | tap_leaves_options: TapLeavesOptions::None, |
3640 | 1 | ..Default::default() |
3641 | 1 | }, |
3642 | 1 | ) |
3643 | 1 | .unwrap(); |
3644 | 1 | |
3645 | 1 | assert!(psbt.inputs.iter().all(|i| i.tap_script_sigs.is_empty())); |
3646 | 1 | } |
3647 | | |
3648 | | #[test] |
3649 | 1 | fn test_taproot_sign_derive_index_from_psbt() { |
3650 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); |
3651 | 1 | |
3652 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3653 | 1 | |
3654 | 1 | let mut builder = wallet.build_tx(); |
3655 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); |
3656 | 1 | let mut psbt = builder.finish().unwrap(); |
3657 | 1 | |
3658 | 1 | // re-create the wallet with an empty db |
3659 | 1 | let wallet_empty = Wallet::create(get_test_tr_single_sig_xprv(), get_test_tr_single_sig()) |
3660 | 1 | .network(Network::Regtest) |
3661 | 1 | .create_wallet_no_persist() |
3662 | 1 | .unwrap(); |
3663 | 1 | |
3664 | 1 | // signing with an empty db means that we will only look at the psbt to infer the |
3665 | 1 | // derivation index |
3666 | 1 | assert!( |
3667 | 1 | wallet_empty.sign(&mut psbt, Default::default()).unwrap(), |
3668 | 0 | "Unable to finalize tx" |
3669 | | ); |
3670 | 1 | } |
3671 | | |
3672 | | #[test] |
3673 | 1 | fn test_taproot_sign_explicit_sighash_all() { |
3674 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); |
3675 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3676 | 1 | let mut builder = wallet.build_tx(); |
3677 | 1 | builder |
3678 | 1 | .drain_to(addr.script_pubkey()) |
3679 | 1 | .sighash(TapSighashType::All.into()) |
3680 | 1 | .drain_wallet(); |
3681 | 1 | let mut psbt = builder.finish().unwrap(); |
3682 | 1 | |
3683 | 1 | let result = wallet.sign(&mut psbt, Default::default()); |
3684 | 1 | assert!( |
3685 | 1 | result.is_ok(), |
3686 | 0 | "Signing should work because SIGHASH_ALL is safe" |
3687 | | ) |
3688 | 1 | } |
3689 | | |
3690 | | #[test] |
3691 | 1 | fn test_taproot_sign_non_default_sighash() { |
3692 | 1 | let sighash = TapSighashType::NonePlusAnyoneCanPay; |
3693 | 1 | |
3694 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig()); |
3695 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3696 | 1 | let mut builder = wallet.build_tx(); |
3697 | 1 | builder |
3698 | 1 | .drain_to(addr.script_pubkey()) |
3699 | 1 | .sighash(sighash.into()) |
3700 | 1 | .drain_wallet(); |
3701 | 1 | let mut psbt = builder.finish().unwrap(); |
3702 | 1 | |
3703 | 1 | let witness_utxo = psbt.inputs[0].witness_utxo.take(); |
3704 | 1 | |
3705 | 1 | let result = wallet.sign(&mut psbt, Default::default()); |
3706 | 1 | assert!( |
3707 | 1 | result.is_err(), |
3708 | 0 | "Signing should have failed because the TX uses non-standard sighashes" |
3709 | | ); |
3710 | 0 | assert_matches!( |
3711 | 1 | result, |
3712 | | Err(SignerError::NonStandardSighash), |
3713 | | "Signing failed with the wrong error type" |
3714 | | ); |
3715 | | |
3716 | | // try again after opting-in |
3717 | 1 | let result = wallet.sign( |
3718 | 1 | &mut psbt, |
3719 | 1 | SignOptions { |
3720 | 1 | allow_all_sighashes: true, |
3721 | 1 | ..Default::default() |
3722 | 1 | }, |
3723 | 1 | ); |
3724 | 1 | assert!( |
3725 | 1 | result.is_err(), |
3726 | 0 | "Signing should have failed because the witness_utxo is missing" |
3727 | | ); |
3728 | 0 | assert_matches!( |
3729 | 1 | result, |
3730 | | Err(SignerError::MissingWitnessUtxo), |
3731 | | "Signing failed with the wrong error type" |
3732 | | ); |
3733 | | |
3734 | | // restore the witness_utxo |
3735 | 1 | psbt.inputs[0].witness_utxo = witness_utxo; |
3736 | 1 | |
3737 | 1 | let result = wallet.sign( |
3738 | 1 | &mut psbt, |
3739 | 1 | SignOptions { |
3740 | 1 | allow_all_sighashes: true, |
3741 | 1 | ..Default::default() |
3742 | 1 | }, |
3743 | 1 | ); |
3744 | 1 | |
3745 | 1 | assert!(result.is_ok(), "Signing should have worked"0 ); |
3746 | 1 | assert!( |
3747 | 1 | result.unwrap(), |
3748 | 0 | "Should finalize the input since we can produce signatures" |
3749 | | ); |
3750 | | |
3751 | 1 | let extracted = psbt.extract_tx().expect("failed to extract tx"); |
3752 | 1 | assert_eq!( |
3753 | 1 | *extracted.input[0].witness.to_vec()[0].last().unwrap(), |
3754 | 1 | sighash as u8, |
3755 | 0 | "The signature should have been made with the right sighash" |
3756 | | ); |
3757 | 1 | } |
3758 | | |
3759 | | #[test] |
3760 | 1 | fn test_spend_coinbase() { |
3761 | 1 | let (desc, change_desc) = get_test_wpkh_with_change_desc(); |
3762 | 1 | let mut wallet = Wallet::create(desc, change_desc) |
3763 | 1 | .network(Network::Regtest) |
3764 | 1 | .create_wallet_no_persist() |
3765 | 1 | .unwrap(); |
3766 | 1 | |
3767 | 1 | let confirmation_height = 5; |
3768 | 1 | wallet |
3769 | 1 | .insert_checkpoint(BlockId { |
3770 | 1 | height: confirmation_height, |
3771 | 1 | hash: BlockHash::all_zeros(), |
3772 | 1 | }) |
3773 | 1 | .unwrap(); |
3774 | 1 | let coinbase_tx = Transaction { |
3775 | 1 | version: transaction::Version::ONE, |
3776 | 1 | lock_time: absolute::LockTime::ZERO, |
3777 | 1 | input: vec![TxIn { |
3778 | 1 | previous_output: OutPoint::null(), |
3779 | 1 | ..Default::default() |
3780 | 1 | }], |
3781 | 1 | output: vec![TxOut { |
3782 | 1 | script_pubkey: wallet |
3783 | 1 | .next_unused_address(KeychainKind::External) |
3784 | 1 | .script_pubkey(), |
3785 | 1 | value: Amount::from_sat(25_000), |
3786 | 1 | }], |
3787 | 1 | }; |
3788 | 1 | let txid = coinbase_tx.compute_txid(); |
3789 | 1 | wallet.insert_tx(coinbase_tx); |
3790 | 1 | insert_anchor_from_conf( |
3791 | 1 | &mut wallet, |
3792 | 1 | txid, |
3793 | 1 | ConfirmationTime::Confirmed { |
3794 | 1 | height: confirmation_height, |
3795 | 1 | time: 30_000, |
3796 | 1 | }, |
3797 | 1 | ); |
3798 | 1 | |
3799 | 1 | let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1; |
3800 | 1 | let maturity_time = confirmation_height + COINBASE_MATURITY; |
3801 | 1 | |
3802 | 1 | let balance = wallet.balance(); |
3803 | 1 | assert_eq!( |
3804 | 1 | balance, |
3805 | 1 | Balance { |
3806 | 1 | immature: Amount::from_sat(25_000), |
3807 | 1 | trusted_pending: Amount::ZERO, |
3808 | 1 | untrusted_pending: Amount::ZERO, |
3809 | 1 | confirmed: Amount::ZERO |
3810 | 1 | } |
3811 | 1 | ); |
3812 | | |
3813 | | // We try to create a transaction, only to notice that all |
3814 | | // our funds are unspendable |
3815 | 1 | let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX") |
3816 | 1 | .unwrap() |
3817 | 1 | .assume_checked(); |
3818 | 1 | let mut builder = wallet.build_tx(); |
3819 | 1 | builder |
3820 | 1 | .add_recipient(addr.script_pubkey(), balance.immature / 2) |
3821 | 1 | .current_height(confirmation_height); |
3822 | 1 | assert!(matches!0 ( |
3823 | 1 | builder.finish(), |
3824 | | Err(CreateTxError::CoinSelection( |
3825 | | coin_selection::Error::InsufficientFunds { |
3826 | | needed: _, |
3827 | | available: 0 |
3828 | | } |
3829 | | )) |
3830 | | )); |
3831 | | |
3832 | | // Still unspendable... |
3833 | 1 | let mut builder = wallet.build_tx(); |
3834 | 1 | builder |
3835 | 1 | .add_recipient(addr.script_pubkey(), balance.immature / 2) |
3836 | 1 | .current_height(not_yet_mature_time); |
3837 | 0 | assert_matches!( |
3838 | 1 | builder.finish(), |
3839 | | Err(CreateTxError::CoinSelection( |
3840 | | coin_selection::Error::InsufficientFunds { |
3841 | | needed: _, |
3842 | | available: 0 |
3843 | | } |
3844 | | )) |
3845 | | ); |
3846 | | |
3847 | 1 | wallet |
3848 | 1 | .insert_checkpoint(BlockId { |
3849 | 1 | height: maturity_time, |
3850 | 1 | hash: BlockHash::all_zeros(), |
3851 | 1 | }) |
3852 | 1 | .unwrap(); |
3853 | 1 | let balance = wallet.balance(); |
3854 | 1 | assert_eq!( |
3855 | 1 | balance, |
3856 | 1 | Balance { |
3857 | 1 | immature: Amount::ZERO, |
3858 | 1 | trusted_pending: Amount::ZERO, |
3859 | 1 | untrusted_pending: Amount::ZERO, |
3860 | 1 | confirmed: Amount::from_sat(25_000) |
3861 | 1 | } |
3862 | 1 | ); |
3863 | 1 | let mut builder = wallet.build_tx(); |
3864 | 1 | builder |
3865 | 1 | .add_recipient(addr.script_pubkey(), balance.confirmed / 2) |
3866 | 1 | .current_height(maturity_time); |
3867 | 1 | builder.finish().unwrap(); |
3868 | 1 | } |
3869 | | |
3870 | | #[test] |
3871 | 1 | fn test_allow_dust_limit() { |
3872 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); |
3873 | 1 | |
3874 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3875 | 1 | |
3876 | 1 | let mut builder = wallet.build_tx(); |
3877 | 1 | |
3878 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::ZERO); |
3879 | | |
3880 | 0 | assert_matches!( |
3881 | 1 | builder.finish(), |
3882 | | Err(CreateTxError::OutputBelowDustLimit(0)) |
3883 | | ); |
3884 | | |
3885 | 1 | let mut builder = wallet.build_tx(); |
3886 | 1 | |
3887 | 1 | builder |
3888 | 1 | .allow_dust(true) |
3889 | 1 | .add_recipient(addr.script_pubkey(), Amount::ZERO); |
3890 | 1 | |
3891 | 1 | assert!(builder.finish().is_ok()); |
3892 | 1 | } |
3893 | | |
3894 | | #[test] |
3895 | 1 | fn test_fee_rate_sign_no_grinding_high_r() { |
3896 | 1 | // Our goal is to obtain a transaction with a signature with high-R (71 bytes |
3897 | 1 | // instead of 70). We then check that our fee rate and fee calculation is |
3898 | 1 | // alright. |
3899 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); |
3900 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3901 | 1 | let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); |
3902 | 1 | let mut builder = wallet.build_tx(); |
3903 | 1 | let mut data = PushBytesBuf::try_from(vec![0]).unwrap(); |
3904 | 1 | builder |
3905 | 1 | .drain_to(addr.script_pubkey()) |
3906 | 1 | .drain_wallet() |
3907 | 1 | .fee_rate(fee_rate) |
3908 | 1 | .add_data(&data); |
3909 | 1 | let mut psbt = builder.finish().unwrap(); |
3910 | 1 | let fee = check_fee!(wallet, psbt); |
3911 | 1 | let (op_return_vout, _) = psbt |
3912 | 1 | .unsigned_tx |
3913 | 1 | .output |
3914 | 1 | .iter() |
3915 | 1 | .enumerate() |
3916 | 1 | .find(|(_n, i)| i.script_pubkey.is_op_return()) |
3917 | 1 | .unwrap(); |
3918 | 1 | |
3919 | 1 | let mut sig_len: usize = 0; |
3920 | | // We try to sign many different times until we find a longer signature (71 bytes) |
3921 | 3 | while sig_len < 71 { |
3922 | 2 | // Changing the OP_RETURN data will make the signature change (but not the fee, until |
3923 | 2 | // data[0] is small enough) |
3924 | 2 | data.as_mut_bytes()[0] += 1; |
3925 | 2 | psbt.unsigned_tx.output[op_return_vout].script_pubkey = ScriptBuf::new_op_return(&data); |
3926 | 2 | // Clearing the previous signature |
3927 | 2 | psbt.inputs[0].partial_sigs.clear(); |
3928 | 2 | // Signing |
3929 | 2 | wallet |
3930 | 2 | .sign( |
3931 | 2 | &mut psbt, |
3932 | 2 | SignOptions { |
3933 | 2 | try_finalize: false, |
3934 | 2 | allow_grinding: false, |
3935 | 2 | ..Default::default() |
3936 | 2 | }, |
3937 | 2 | ) |
3938 | 2 | .unwrap(); |
3939 | 2 | // We only have one key in the partial_sigs map, this is a trick to retrieve it |
3940 | 2 | let key = psbt.inputs[0].partial_sigs.keys().next().unwrap(); |
3941 | 2 | sig_len = psbt.inputs[0].partial_sigs[key] |
3942 | 2 | .signature |
3943 | 2 | .serialize_der() |
3944 | 2 | .len(); |
3945 | 2 | } |
3946 | | // Actually finalizing the transaction... |
3947 | 1 | wallet |
3948 | 1 | .sign( |
3949 | 1 | &mut psbt, |
3950 | 1 | SignOptions { |
3951 | 1 | allow_grinding: false, |
3952 | 1 | ..Default::default() |
3953 | 1 | }, |
3954 | 1 | ) |
3955 | 1 | .unwrap(); |
3956 | | // ...and checking that everything is fine |
3957 | | assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), fee_rate); |
3958 | 1 | } |
3959 | | |
3960 | | #[test] |
3961 | 1 | fn test_fee_rate_sign_grinding_low_r() { |
3962 | 1 | // Our goal is to obtain a transaction with a signature with low-R (70 bytes) |
3963 | 1 | // by setting the `allow_grinding` signing option as true. |
3964 | 1 | // We then check that our fee rate and fee calculation is alright and that our |
3965 | 1 | // signature is 70 bytes. |
3966 | 1 | let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)"); |
3967 | 1 | let addr = wallet.next_unused_address(KeychainKind::External); |
3968 | 1 | let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); |
3969 | 1 | let mut builder = wallet.build_tx(); |
3970 | 1 | builder |
3971 | 1 | .drain_to(addr.script_pubkey()) |
3972 | 1 | .drain_wallet() |
3973 | 1 | .fee_rate(fee_rate); |
3974 | 1 | let mut psbt = builder.finish().unwrap(); |
3975 | 1 | let fee = check_fee!(wallet, psbt); |
3976 | | |
3977 | 1 | wallet |
3978 | 1 | .sign( |
3979 | 1 | &mut psbt, |
3980 | 1 | SignOptions { |
3981 | 1 | try_finalize: false, |
3982 | 1 | allow_grinding: true, |
3983 | 1 | ..Default::default() |
3984 | 1 | }, |
3985 | 1 | ) |
3986 | 1 | .unwrap(); |
3987 | 1 | |
3988 | 1 | let key = psbt.inputs[0].partial_sigs.keys().next().unwrap(); |
3989 | 1 | let sig_len = psbt.inputs[0].partial_sigs[key] |
3990 | 1 | .signature |
3991 | 1 | .serialize_der() |
3992 | 1 | .len(); |
3993 | 1 | assert_eq!(sig_len, 70); |
3994 | | assert_fee_rate!(psbt, fee.unwrap_or(Amount::ZERO), fee_rate); |
3995 | 1 | } |
3996 | | |
3997 | | #[test] |
3998 | 1 | fn test_taproot_load_descriptor_duplicated_keys() { |
3999 | 1 | // Added after issue https://github.com/bitcoindevkit/bdk/issues/760 |
4000 | 1 | // |
4001 | 1 | // Having the same key in multiple taproot leaves is safe and should be accepted by BDK |
4002 | 1 | |
4003 | 1 | let (wallet, _) = get_funded_wallet(get_test_tr_dup_keys()); |
4004 | 1 | let addr = wallet.peek_address(KeychainKind::External, 0); |
4005 | 1 | |
4006 | 1 | assert_eq!( |
4007 | 1 | addr.to_string(), |
4008 | 1 | "bcrt1pvysh4nmh85ysrkpwtrr8q8gdadhgdejpy6f9v424a8v9htjxjhyqw9c5s5" |
4009 | 1 | ); |
4010 | 1 | } |
4011 | | |
4012 | | /// In dev mode this test panics, but in release mode, or if the `debug_panic` in `TxOutIndex::replenish_inner_index` |
4013 | | /// is commented out, there is no panic and the balance is calculated correctly. See issue [#1483] |
4014 | | /// and PR [#1486] for discussion on mixing non-wildcard and wildcard descriptors. |
4015 | | /// |
4016 | | /// [#1483]: https://github.com/bitcoindevkit/bdk/issues/1483 |
4017 | | /// [#1486]: https://github.com/bitcoindevkit/bdk/pull/1486 |
4018 | | #[test] |
4019 | | #[cfg(debug_assertions)] |
4020 | | #[should_panic( |
4021 | | expected = "replenish lookahead: must not have existing spk: keychain=Internal, lookahead=25, next_store_index=0, next_reveal_index=0" |
4022 | | )] |
4023 | 1 | fn test_keychains_with_overlapping_spks() { |
4024 | 1 | // this can happen if a non-wildcard descriptor keychain derives an spk that a |
4025 | 1 | // wildcard descriptor keychain in the same wallet also derives. |
4026 | 1 | |
4027 | 1 | // index 1 spk overlaps with non-wildcard change descriptor |
4028 | 1 | let wildcard_keychain = "wpkh(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)"; |
4029 | 1 | let non_wildcard_keychain = "wpkh(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/1)"; |
4030 | 1 | |
4031 | 1 | let (mut wallet, _) = get_funded_wallet_with_change(wildcard_keychain, non_wildcard_keychain); |
4032 | 1 | assert_eq!(wallet.balance().confirmed, Amount::from_sat(50000)); |
4033 | | |
4034 | 0 | let addr = wallet |
4035 | 0 | .reveal_addresses_to(KeychainKind::External, 1) |
4036 | 0 | .last() |
4037 | 0 | .unwrap() |
4038 | 0 | .address; |
4039 | 0 | let _outpoint = receive_output_to_address( |
4040 | 0 | &mut wallet, |
4041 | 0 | addr, |
4042 | 0 | 8000, |
4043 | 0 | ConfirmationTime::Confirmed { |
4044 | 0 | height: 2000, |
4045 | 0 | time: 0, |
4046 | 0 | }, |
4047 | 0 | ); |
4048 | 0 | assert_eq!(wallet.balance().confirmed, Amount::from_sat(58000)); |
4049 | 0 | } |
4050 | | |
4051 | | #[test] |
4052 | | /// The wallet should re-use previously allocated change addresses when the tx using them is cancelled |
4053 | 1 | fn test_tx_cancellation() { |
4054 | 1 | macro_rules! new_tx { |
4055 | 1 | ($wallet:expr) => {{ |
4056 | 1 | let addr = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") |
4057 | 1 | .unwrap() |
4058 | 1 | .assume_checked(); |
4059 | 1 | let mut builder = $wallet.build_tx(); |
4060 | 1 | builder.add_recipient(addr.script_pubkey(), Amount::from_sat(10_000)); |
4061 | 1 | |
4062 | 1 | let psbt = builder.finish().unwrap(); |
4063 | 1 | |
4064 | 1 | psbt |
4065 | 1 | }}; |
4066 | 1 | } |
4067 | 1 | |
4068 | 1 | let (mut wallet, _) = |
4069 | 1 | get_funded_wallet_with_change(get_test_wpkh(), get_test_tr_single_sig_xprv()); |
4070 | 1 | |
4071 | 1 | let psbt1 = new_tx!(wallet); |
4072 | 1 | let change_derivation_1 = psbt1 |
4073 | 1 | .unsigned_tx |
4074 | 1 | .output |
4075 | 1 | .iter() |
4076 | 1 | .find_map(|txout| wallet.derivation_of_spk(txout.script_pubkey.clone())) |
4077 | 1 | .unwrap(); |
4078 | 1 | assert_eq!(change_derivation_1, (KeychainKind::Internal, 0)); |
4079 | | |
4080 | 1 | let psbt2 = new_tx!(wallet); |
4081 | 1 | |
4082 | 1 | let change_derivation_2 = psbt2 |
4083 | 1 | .unsigned_tx |
4084 | 1 | .output |
4085 | 1 | .iter() |
4086 | 1 | .find_map(|txout| wallet.derivation_of_spk(txout.script_pubkey.clone())) |
4087 | 1 | .unwrap(); |
4088 | 1 | assert_eq!(change_derivation_2, (KeychainKind::Internal, 1)); |
4089 | | |
4090 | 1 | wallet.cancel_tx(&psbt1.extract_tx().expect("failed to extract tx")); |
4091 | 1 | |
4092 | 1 | let psbt3 = new_tx!(wallet); |
4093 | 1 | let change_derivation_3 = psbt3 |
4094 | 1 | .unsigned_tx |
4095 | 1 | .output |
4096 | 1 | .iter() |
4097 | 2 | .find_map(|txout| wallet.derivation_of_spk(txout.script_pubkey.clone())) |
4098 | 1 | .unwrap(); |
4099 | 1 | assert_eq!(change_derivation_3, (KeychainKind::Internal, 0)); |
4100 | | |
4101 | 1 | let psbt3 = new_tx!(wallet); |
4102 | 1 | let change_derivation_3 = psbt3 |
4103 | 1 | .unsigned_tx |
4104 | 1 | .output |
4105 | 1 | .iter() |
4106 | 2 | .find_map(|txout| wallet.derivation_of_spk(txout.script_pubkey.clone())) |
4107 | 1 | .unwrap(); |
4108 | 1 | assert_eq!(change_derivation_3, (KeychainKind::Internal, 2)); |
4109 | | |
4110 | 1 | wallet.cancel_tx(&psbt3.extract_tx().expect("failed to extract tx")); |
4111 | 1 | |
4112 | 1 | let psbt3 = new_tx!(wallet); |
4113 | 1 | let change_derivation_4 = psbt3 |
4114 | 1 | .unsigned_tx |
4115 | 1 | .output |
4116 | 1 | .iter() |
4117 | 1 | .find_map(|txout| wallet.derivation_of_spk(txout.script_pubkey.clone())) |
4118 | 1 | .unwrap(); |
4119 | 1 | assert_eq!(change_derivation_4, (KeychainKind::Internal, 2)); |
4120 | 1 | } |
4121 | | |
4122 | | #[test] |
4123 | 1 | fn test_thread_safety() { |
4124 | 1 | fn thread_safe<T: Send + Sync>() {} |
4125 | 1 | thread_safe::<Wallet>(); // compiles only if true |
4126 | 1 | } |
4127 | | |
4128 | | #[test] |
4129 | 1 | fn test_insert_tx_balance_and_utxos() { |
4130 | 1 | // creating many txs has no effect on the wallet's available utxos |
4131 | 1 | let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv()); |
4132 | 1 | let addr = Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm") |
4133 | 1 | .unwrap() |
4134 | 1 | .assume_checked(); |
4135 | 1 | |
4136 | 1 | let unspent: Vec<_> = wallet.list_unspent().collect(); |
4137 | 1 | assert!(!unspent.is_empty()); |
4138 | | |
4139 | 1 | let balance = wallet.balance().total(); |
4140 | 1 | let fee = Amount::from_sat(143); |
4141 | 1 | let amt = balance - fee; |
4142 | | |
4143 | 4 | for _ in 0..3 { |
4144 | 3 | let mut builder = wallet.build_tx(); |
4145 | 3 | builder.add_recipient(addr.script_pubkey(), amt); |
4146 | 3 | let mut psbt = builder.finish().unwrap(); |
4147 | 3 | assert!(wallet.sign(&mut psbt, SignOptions::default()).unwrap()); |
4148 | 3 | let tx = psbt.extract_tx().unwrap(); |
4149 | 3 | let _ = wallet.insert_tx(tx); |
4150 | | } |
4151 | 1 | assert_eq!(wallet.list_unspent().collect::<Vec<_>>(), unspent); |
4152 | 1 | assert_eq!(wallet.balance().confirmed, balance); |
4153 | | |
4154 | | // manually setting a tx last_seen will consume the wallet's available utxos |
4155 | 1 | let addr = Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr") |
4156 | 1 | .unwrap() |
4157 | 1 | .assume_checked(); |
4158 | 1 | let mut builder = wallet.build_tx(); |
4159 | 1 | builder.add_recipient(addr.script_pubkey(), amt); |
4160 | 1 | let mut psbt = builder.finish().unwrap(); |
4161 | 1 | assert!(wallet.sign(&mut psbt, SignOptions::default()).unwrap()); |
4162 | 1 | let tx = psbt.extract_tx().unwrap(); |
4163 | 1 | let txid = tx.compute_txid(); |
4164 | 1 | let _ = wallet.insert_tx(tx); |
4165 | 1 | insert_seen_at(&mut wallet, txid, 2); |
4166 | 1 | assert!(wallet.list_unspent().next().is_none()); |
4167 | 1 | assert_eq!(wallet.balance().total().to_sat(), 0); |
4168 | 1 | } |
4169 | | |
4170 | | #[test] |
4171 | 1 | fn single_descriptor_wallet_can_create_tx_and_receive_change() { |
4172 | 1 | // create single descriptor wallet and fund it |
4173 | 1 | let mut wallet = Wallet::create_single(get_test_tr_single_sig_xprv()) |
4174 | 1 | .network(Network::Testnet) |
4175 | 1 | .create_wallet_no_persist() |
4176 | 1 | .unwrap(); |
4177 | 1 | assert_eq!(wallet.keychains().count(), 1); |
4178 | 1 | let amt = Amount::from_sat(5_000); |
4179 | 1 | receive_output( |
4180 | 1 | &mut wallet, |
4181 | 1 | 2 * amt.to_sat(), |
4182 | 1 | ConfirmationTime::Unconfirmed { last_seen: 2 }, |
4183 | 1 | ); |
4184 | 1 | // create spend tx that produces a change output |
4185 | 1 | let addr = Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm") |
4186 | 1 | .unwrap() |
4187 | 1 | .assume_checked(); |
4188 | 1 | let mut builder = wallet.build_tx(); |
4189 | 1 | builder.add_recipient(addr.script_pubkey(), amt); |
4190 | 1 | let mut psbt = builder.finish().unwrap(); |
4191 | 1 | assert!(wallet.sign(&mut psbt, SignOptions::default()).unwrap()); |
4192 | 1 | let tx = psbt.extract_tx().unwrap(); |
4193 | 1 | let txid = tx.compute_txid(); |
4194 | 1 | wallet.insert_tx(tx); |
4195 | 1 | insert_seen_at(&mut wallet, txid, 4); |
4196 | 1 | let unspent: Vec<_> = wallet.list_unspent().collect(); |
4197 | 1 | assert_eq!(unspent.len(), 1); |
4198 | 1 | let utxo = unspent.first().unwrap(); |
4199 | 1 | assert!(utxo.txout.value < amt); |
4200 | 1 | assert_eq!( |
4201 | | utxo.keychain, |
4202 | | KeychainKind::External, |
4203 | 0 | "tx change should go to external keychain" |
4204 | | ); |
4205 | 1 | } |