@@ -52,7 +52,7 @@ use rand_core::RngCore;
52
52
use super :: coin_selection:: CoinSelectionAlgorithm ;
53
53
use super :: utils:: shuffle_slice;
54
54
use super :: { CreateTxError , Wallet } ;
55
- use crate :: collections:: { BTreeMap , HashSet } ;
55
+ use crate :: collections:: { BTreeMap , HashMap , HashSet } ;
56
56
use crate :: { KeychainKind , LocalOutput , Utxo , WeightedUtxo } ;
57
57
58
58
/// A transaction builder
@@ -274,25 +274,35 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
274
274
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
275
275
/// the "utxos" and the "unspendable" list, it will be spent.
276
276
pub fn add_utxos ( & mut self , outpoints : & [ OutPoint ] ) -> Result < & mut Self , AddUtxoError > {
277
- {
278
- let wallet = & mut self . wallet ;
279
- let utxos = outpoints
280
- . iter ( )
281
- . map ( |outpoint| {
282
- wallet
283
- . get_utxo ( * outpoint)
284
- . ok_or ( AddUtxoError :: UnknownUtxo ( * outpoint) )
285
- } )
286
- . collect :: < Result < Vec < _ > , _ > > ( ) ?;
287
-
288
- for utxo in utxos {
289
- let descriptor = wallet. public_descriptor ( utxo. keychain ) ;
290
- let satisfaction_weight = descriptor. max_weight_to_satisfy ( ) . unwrap ( ) ;
291
- self . params . utxos . push ( WeightedUtxo {
292
- satisfaction_weight,
293
- utxo : Utxo :: Local ( utxo) ,
294
- } ) ;
295
- }
277
+ let outputs = self
278
+ . wallet
279
+ . list_output ( )
280
+ . map ( |out| ( out. outpoint , out) )
281
+ . collect :: < HashMap < _ , _ > > ( ) ;
282
+ let utxos = outpoints
283
+ . iter ( )
284
+ . map ( |op| {
285
+ let output = outputs
286
+ . get ( op)
287
+ . cloned ( )
288
+ . ok_or ( AddUtxoError :: UnknownUtxo ( * op) ) ?;
289
+ // the output should be unspent unless we're doing a RBF
290
+ if self . params . bumping_fee . is_none ( ) && output. is_spent {
291
+ return Err ( AddUtxoError :: UnknownUtxo ( * op) ) ;
292
+ }
293
+ Ok ( output)
294
+ } )
295
+ . collect :: < Result < Vec < _ > , _ > > ( ) ?;
296
+
297
+ for utxo in utxos {
298
+ let descriptor = self . wallet . public_descriptor ( utxo. keychain ) ;
299
+ let satisfaction_weight = descriptor
300
+ . max_weight_to_satisfy ( )
301
+ . expect ( "descriptor should be satisfiable" ) ;
302
+ self . params . utxos . push ( WeightedUtxo {
303
+ satisfaction_weight,
304
+ utxo : Utxo :: Local ( utxo) ,
305
+ } ) ;
296
306
}
297
307
298
308
Ok ( self )
@@ -306,6 +316,122 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
306
316
self . add_utxos ( & [ outpoint] )
307
317
}
308
318
319
+ /// Replace an unconfirmed transaction.
320
+ ///
321
+ /// This method attempts to create a replacement for the transaction with `txid` by
322
+ /// looking for the largest input that is owned by this wallet and adding it to the
323
+ /// list of UTXOs to spend.
324
+ ///
325
+ /// # Note
326
+ ///
327
+ /// Aside from reusing one of the inputs, the method makes no assumptions about the
328
+ /// structure of the replacement, so if you need to reuse the original recipient(s)
329
+ /// and/or change address, you should add them manually before [`finish`] is called.
330
+ ///
331
+ /// # Example
332
+ ///
333
+ /// Create a replacement for an unconfirmed wallet transaction
334
+ ///
335
+ /// ```rust,no_run
336
+ /// # let mut wallet = bdk_wallet::doctest_wallet!();
337
+ /// let wallet_txs = wallet.transactions().collect::<Vec<_>>();
338
+ /// let tx = wallet_txs.first().expect("must have wallet tx");
339
+ ///
340
+ /// if !tx.chain_position.is_confirmed() {
341
+ /// let txid = tx.tx_node.txid;
342
+ /// let mut builder = wallet.build_tx();
343
+ /// builder.replace_tx(txid).expect("should replace");
344
+ ///
345
+ /// // Continue building tx...
346
+ ///
347
+ /// let psbt = builder.finish()?;
348
+ /// }
349
+ /// # Ok::<_, anyhow::Error>(())
350
+ /// ```
351
+ ///
352
+ /// # Errors
353
+ ///
354
+ /// - If the original transaction is not found in the tx graph
355
+ /// - If the orginal transaction is confirmed
356
+ /// - If none of the inputs are owned by this wallet
357
+ ///
358
+ /// [`finish`]: TxBuilder::finish
359
+ pub fn replace_tx ( & mut self , txid : Txid ) -> Result < & mut Self , ReplaceTxError > {
360
+ let tx = self
361
+ . wallet
362
+ . indexed_graph
363
+ . graph ( )
364
+ . get_tx ( txid)
365
+ . ok_or ( ReplaceTxError :: MissingTransaction ) ?;
366
+ if self
367
+ . wallet
368
+ . transactions ( )
369
+ . find ( |c| c. tx_node . txid == txid)
370
+ . map ( |c| c. chain_position . is_confirmed ( ) )
371
+ . unwrap_or ( false )
372
+ {
373
+ return Err ( ReplaceTxError :: TransactionConfirmed ) ;
374
+ }
375
+ let outpoint = tx
376
+ . input
377
+ . iter ( )
378
+ . filter_map ( |txin| {
379
+ let prev_tx = self
380
+ . wallet
381
+ . indexed_graph
382
+ . graph ( )
383
+ . get_tx ( txin. previous_output . txid ) ?;
384
+ let txout = & prev_tx. output [ txin. previous_output . vout as usize ] ;
385
+ if self . wallet . is_mine ( txout. script_pubkey . clone ( ) ) {
386
+ Some ( ( txin. previous_output , txout. value ) )
387
+ } else {
388
+ None
389
+ }
390
+ } )
391
+ . max_by_key ( |( _, value) | * value)
392
+ . map ( |( op, _) | op)
393
+ . ok_or ( ReplaceTxError :: NonReplaceable ) ?;
394
+
395
+ // add previous fee
396
+ let absolute = self . wallet . calculate_fee ( & tx) . unwrap_or_default ( ) ;
397
+ let rate = absolute / tx. weight ( ) ;
398
+ self . params . bumping_fee = Some ( PreviousFee { absolute, rate } ) ;
399
+
400
+ self . add_utxo ( outpoint) . expect ( "we must have the utxo" ) ;
401
+
402
+ // do not allow spending outputs of the replaced tx including descendants
403
+ core:: iter:: once ( ( txid, tx) )
404
+ . chain (
405
+ self . wallet
406
+ . tx_graph ( )
407
+ . walk_descendants ( txid, |_, descendant_txid| {
408
+ Some ( (
409
+ descendant_txid,
410
+ self . wallet . tx_graph ( ) . get_tx ( descendant_txid) ?,
411
+ ) )
412
+ } ) ,
413
+ )
414
+ . for_each ( |( txid, tx) | {
415
+ self . params
416
+ . unspendable
417
+ . extend ( ( 0 ..tx. output . len ( ) ) . map ( |vout| OutPoint :: new ( txid, vout as u32 ) ) ) ;
418
+ } ) ;
419
+
420
+ Ok ( self )
421
+ }
422
+
423
+ /// Get the previous fee and feerate, i.e. the fee of the tx being fee-bumped, if any.
424
+ ///
425
+ /// This method may be used in combination with either [`build_fee_bump`] or [`replace_tx`]
426
+ /// and is useful for deciding what fee to attach to a transaction for the purpose of
427
+ /// "replace-by-fee" (RBF).
428
+ ///
429
+ /// [`build_fee_bump`]: Wallet::build_fee_bump
430
+ /// [`replace_tx`]: Self::replace_tx
431
+ pub fn previous_fee ( & self ) -> Option < ( Amount , FeeRate ) > {
432
+ self . params . bumping_fee . map ( |p| ( p. absolute , p. rate ) )
433
+ }
434
+
309
435
/// Add a foreign UTXO i.e. a UTXO not owned by this wallet.
310
436
///
311
437
/// At a minimum to add a foreign UTXO we need:
@@ -697,6 +823,30 @@ impl fmt::Display for AddUtxoError {
697
823
#[ cfg( feature = "std" ) ]
698
824
impl std:: error:: Error for AddUtxoError { }
699
825
826
+ /// Error returned by [`TxBuilder::replace_tx`].
827
+ #[ derive( Debug ) ]
828
+ pub enum ReplaceTxError {
829
+ /// Transaction was not found in tx graph
830
+ MissingTransaction ,
831
+ /// Transaction can't be replaced by this wallet
832
+ NonReplaceable ,
833
+ /// Transaction is already confirmed
834
+ TransactionConfirmed ,
835
+ }
836
+
837
+ impl fmt:: Display for ReplaceTxError {
838
+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
839
+ match self {
840
+ Self :: MissingTransaction => write ! ( f, "transaction not found in tx graph" ) ,
841
+ Self :: NonReplaceable => write ! ( f, "no replaceable input found" ) ,
842
+ Self :: TransactionConfirmed => write ! ( f, "cannot replace a confirmed tx" ) ,
843
+ }
844
+ }
845
+ }
846
+
847
+ #[ cfg( feature = "std" ) ]
848
+ impl std:: error:: Error for ReplaceTxError { }
849
+
700
850
#[ derive( Debug ) ]
701
851
/// Error returned from [`TxBuilder::add_foreign_utxo`].
702
852
pub enum AddForeignUtxoError {
@@ -833,6 +983,7 @@ mod test {
833
983
} ;
834
984
}
835
985
986
+ use crate :: test_utils:: * ;
836
987
use bitcoin:: consensus:: deserialize;
837
988
use bitcoin:: hex:: FromHex ;
838
989
use bitcoin:: TxOut ;
@@ -1098,4 +1249,137 @@ mod test {
1098
1249
builder. fee_rate ( FeeRate :: from_sat_per_kwu ( feerate + 250 ) ) ;
1099
1250
let _ = builder. finish ( ) . unwrap ( ) ;
1100
1251
}
1252
+ #[ test]
1253
+ fn replace_tx_allows_selecting_spent_outputs ( ) {
1254
+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1255
+ let outpoint_1 = OutPoint :: new ( txid_0, 0 ) ;
1256
+
1257
+ // receive output 2
1258
+ let outpoint_2 = receive_output_in_latest_block ( & mut wallet, 49_000 ) ;
1259
+ assert_eq ! ( wallet. list_unspent( ) . count( ) , 2 ) ;
1260
+ assert_eq ! ( wallet. balance( ) . total( ) . to_sat( ) , 99_000 ) ;
1261
+
1262
+ // create tx1: 2-in/1-out sending all to `recip`
1263
+ let recip = ScriptBuf :: from_hex ( "0014446906a6560d8ad760db3156706e72e171f3a2aa" ) . unwrap ( ) ;
1264
+ let mut builder = wallet. build_tx ( ) ;
1265
+ builder. add_recipient ( recip. clone ( ) , Amount :: from_sat ( 98_800 ) ) ;
1266
+ let psbt = builder. finish ( ) . unwrap ( ) ;
1267
+ let tx1 = psbt. unsigned_tx ;
1268
+ let txid1 = tx1. compute_txid ( ) ;
1269
+ insert_tx ( & mut wallet, tx1) ;
1270
+ assert ! ( wallet. list_unspent( ) . next( ) . is_none( ) ) ;
1271
+
1272
+ // now replace tx1 with a new transaction
1273
+ let mut builder = wallet. build_tx ( ) ;
1274
+ builder. replace_tx ( txid1) . expect ( "should replace input" ) ;
1275
+ let prev_feerate = builder. previous_fee ( ) . unwrap ( ) . 1 ;
1276
+ builder. add_recipient ( recip, Amount :: from_sat ( 98_500 ) ) ;
1277
+ builder. fee_rate ( FeeRate :: from_sat_per_kwu (
1278
+ prev_feerate. to_sat_per_kwu ( ) + 250 ,
1279
+ ) ) ;
1280
+
1281
+ // Because outpoint 2 was spent in tx1, by default it won't be available for selection,
1282
+ // but we can add it manually, with the caveat that the builder is in a bump-fee
1283
+ // context.
1284
+ builder. add_utxo ( outpoint_2) . expect ( "should add output" ) ;
1285
+ let psbt = builder. finish ( ) . unwrap ( ) ;
1286
+
1287
+ assert ! ( psbt
1288
+ . unsigned_tx
1289
+ . input
1290
+ . iter( )
1291
+ . any( |txin| txin. previous_output == outpoint_1) ) ;
1292
+ assert ! ( psbt
1293
+ . unsigned_tx
1294
+ . input
1295
+ . iter( )
1296
+ . any( |txin| txin. previous_output == outpoint_2) ) ;
1297
+ }
1298
+
1299
+ #[ test]
1300
+ fn test_replace_tx_unspendable_with_descendants ( ) {
1301
+ use crate :: KeychainKind :: External ;
1302
+
1303
+ // Replacing a tx should mark the original txouts unspendable
1304
+
1305
+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1306
+ let outpoint_0 = OutPoint :: new ( txid_0, 0 ) ;
1307
+ let balance = wallet. balance ( ) . total ( ) ;
1308
+ let fee = Amount :: from_sat ( 256 ) ;
1309
+
1310
+ let mut previous_output = outpoint_0;
1311
+
1312
+ // apply 3 unconfirmed txs to wallet
1313
+ for i in 1 ..=3 {
1314
+ let tx = Transaction {
1315
+ input : vec ! [ TxIn {
1316
+ previous_output,
1317
+ ..Default :: default ( )
1318
+ } ] ,
1319
+ output : vec ! [ TxOut {
1320
+ script_pubkey: wallet. reveal_next_address( External ) . script_pubkey( ) ,
1321
+ value: balance - fee * i as u64 ,
1322
+ } ] ,
1323
+ ..new_tx ( i)
1324
+ } ;
1325
+
1326
+ let txid = tx. compute_txid ( ) ;
1327
+ insert_tx ( & mut wallet, tx) ;
1328
+ previous_output = OutPoint :: new ( txid, 0 ) ;
1329
+ }
1330
+
1331
+ let unconfirmed_txs: Vec < _ > = wallet
1332
+ . transactions ( )
1333
+ . filter ( |c| !c. chain_position . is_confirmed ( ) )
1334
+ . collect ( ) ;
1335
+ let txid_1 = unconfirmed_txs
1336
+ . iter ( )
1337
+ . find ( |c| c. tx_node . input [ 0 ] . previous_output == outpoint_0)
1338
+ . map ( |c| c. tx_node . txid )
1339
+ . unwrap ( ) ;
1340
+ let unconfirmed_txids = unconfirmed_txs
1341
+ . iter ( )
1342
+ . map ( |c| c. tx_node . txid )
1343
+ . collect :: < Vec < _ > > ( ) ;
1344
+ assert_eq ! ( unconfirmed_txids. len( ) , 3 ) ;
1345
+
1346
+ // replace tx1
1347
+ let mut builder = wallet. build_tx ( ) ;
1348
+ builder. replace_tx ( txid_1) . unwrap ( ) ;
1349
+ assert_eq ! (
1350
+ builder. params. utxos. first( ) . unwrap( ) . utxo. outpoint( ) ,
1351
+ outpoint_0
1352
+ ) ;
1353
+ for txid in unconfirmed_txids {
1354
+ assert ! ( builder. params. unspendable. contains( & OutPoint :: new( txid, 0 ) ) ) ;
1355
+ }
1356
+ }
1357
+
1358
+ #[ test]
1359
+ fn test_replace_tx_error ( ) {
1360
+ use bitcoin:: hashes:: Hash ;
1361
+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1362
+
1363
+ // tx does not exist
1364
+ let mut builder = wallet. build_tx ( ) ;
1365
+ let res = builder. replace_tx ( Txid :: all_zeros ( ) ) ;
1366
+ assert ! ( matches!( res, Err ( ReplaceTxError :: MissingTransaction ) ) ) ;
1367
+
1368
+ // tx confirmed
1369
+ let mut builder = wallet. build_tx ( ) ;
1370
+ let res = builder. replace_tx ( txid_0) ;
1371
+ assert ! ( matches!( res, Err ( ReplaceTxError :: TransactionConfirmed ) ) ) ;
1372
+
1373
+ // can't replace a foreign tx
1374
+ let tx = Transaction {
1375
+ input : vec ! [ TxIn :: default ( ) ] ,
1376
+ output : vec ! [ TxOut :: NULL ] ,
1377
+ ..new_tx ( 0 )
1378
+ } ;
1379
+ let txid = tx. compute_txid ( ) ;
1380
+ insert_tx ( & mut wallet, tx) ;
1381
+ let mut builder = wallet. build_tx ( ) ;
1382
+ let res = builder. replace_tx ( txid) ;
1383
+ assert ! ( matches!( res, Err ( ReplaceTxError :: NonReplaceable ) ) ) ;
1384
+ }
1101
1385
}
0 commit comments