@@ -274,27 +274,36 @@ 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
+ let outputs = self
278
+ . wallet
279
+ . list_output ( )
280
+ . map ( |out| ( out. outpoint , out) )
281
+ . collect :: < HashMap < _ , _ > > ( ) ;
277
282
let utxo_batch = outpoints
278
283
. iter ( )
279
284
. map ( |outpoint| {
280
- self . wallet
281
- . get_utxo ( * outpoint)
282
- . ok_or ( AddUtxoError :: UnknownUtxo ( * outpoint) )
283
- . map ( |output| {
284
- (
285
- * outpoint,
286
- WeightedUtxo {
287
- satisfaction_weight : self
288
- . wallet
289
- . public_descriptor ( output. keychain )
290
- . max_weight_to_satisfy ( )
291
- . unwrap ( ) ,
292
- utxo : Utxo :: Local ( output) ,
293
- } ,
294
- )
295
- } )
285
+ let output = outputs
286
+ . get ( outpoint)
287
+ . cloned ( )
288
+ . ok_or ( AddUtxoError :: UnknownUtxo ( * outpoint) ) ?;
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 ( * outpoint) ) ;
292
+ }
293
+ Ok ( (
294
+ * outpoint,
295
+ WeightedUtxo {
296
+ satisfaction_weight : self
297
+ . wallet
298
+ . public_descriptor ( output. keychain )
299
+ . max_weight_to_satisfy ( )
300
+ . unwrap ( ) ,
301
+ utxo : Utxo :: Local ( output) ,
302
+ } ,
303
+ ) )
296
304
} )
297
305
. collect :: < Result < HashMap < OutPoint , WeightedUtxo > , AddUtxoError > > ( ) ?;
306
+
298
307
self . params . utxos . extend ( utxo_batch) ;
299
308
300
309
Ok ( self )
@@ -308,6 +317,122 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
308
317
self . add_utxos ( & [ outpoint] )
309
318
}
310
319
320
+ /// Replace an unconfirmed transaction.
321
+ ///
322
+ /// This method attempts to create a replacement for the transaction with `txid` by
323
+ /// looking for the largest input that is owned by this wallet and adding it to the
324
+ /// list of UTXOs to spend.
325
+ ///
326
+ /// # Note
327
+ ///
328
+ /// Aside from reusing one of the inputs, the method makes no assumptions about the
329
+ /// structure of the replacement, so if you need to reuse the original recipient(s)
330
+ /// and/or change address, you should add them manually before [`finish`] is called.
331
+ ///
332
+ /// # Example
333
+ ///
334
+ /// Create a replacement for an unconfirmed wallet transaction
335
+ ///
336
+ /// ```rust,no_run
337
+ /// # let mut wallet = bdk_wallet::doctest_wallet!();
338
+ /// let wallet_txs = wallet.transactions().collect::<Vec<_>>();
339
+ /// let tx = wallet_txs.first().expect("must have wallet tx");
340
+ ///
341
+ /// if !tx.chain_position.is_confirmed() {
342
+ /// let txid = tx.tx_node.txid;
343
+ /// let mut builder = wallet.build_tx();
344
+ /// builder.replace_tx(txid).expect("should replace");
345
+ ///
346
+ /// // Continue building tx...
347
+ ///
348
+ /// let psbt = builder.finish()?;
349
+ /// }
350
+ /// # Ok::<_, anyhow::Error>(())
351
+ /// ```
352
+ ///
353
+ /// # Errors
354
+ ///
355
+ /// - If the original transaction is not found in the tx graph
356
+ /// - If the original transaction is confirmed
357
+ /// - If none of the inputs are owned by this wallet
358
+ ///
359
+ /// [`finish`]: TxBuilder::finish
360
+ pub fn replace_tx ( & mut self , txid : Txid ) -> Result < & mut Self , ReplaceTxError > {
361
+ let tx = self
362
+ . wallet
363
+ . indexed_graph
364
+ . graph ( )
365
+ . get_tx ( txid)
366
+ . ok_or ( ReplaceTxError :: MissingTransaction ) ?;
367
+ if self
368
+ . wallet
369
+ . transactions ( )
370
+ . find ( |c| c. tx_node . txid == txid)
371
+ . map ( |c| c. chain_position . is_confirmed ( ) )
372
+ . unwrap_or ( false )
373
+ {
374
+ return Err ( ReplaceTxError :: TransactionConfirmed ) ;
375
+ }
376
+ let outpoint = tx
377
+ . input
378
+ . iter ( )
379
+ . filter_map ( |txin| {
380
+ let prev_tx = self
381
+ . wallet
382
+ . indexed_graph
383
+ . graph ( )
384
+ . get_tx ( txin. previous_output . txid ) ?;
385
+ let txout = & prev_tx. output [ txin. previous_output . vout as usize ] ;
386
+ if self . wallet . is_mine ( txout. script_pubkey . clone ( ) ) {
387
+ Some ( ( txin. previous_output , txout. value ) )
388
+ } else {
389
+ None
390
+ }
391
+ } )
392
+ . max_by_key ( |( _, value) | * value)
393
+ . map ( |( op, _) | op)
394
+ . ok_or ( ReplaceTxError :: NonReplaceable ) ?;
395
+
396
+ // add previous fee
397
+ let absolute = self . wallet . calculate_fee ( & tx) . unwrap_or_default ( ) ;
398
+ let rate = absolute / tx. weight ( ) ;
399
+ self . params . bumping_fee = Some ( PreviousFee { absolute, rate } ) ;
400
+
401
+ self . add_utxo ( outpoint) . expect ( "we must have the utxo" ) ;
402
+
403
+ // do not allow spending outputs descending from the replaced tx
404
+ core:: iter:: once ( ( txid, tx) )
405
+ . chain (
406
+ self . wallet
407
+ . tx_graph ( )
408
+ . walk_descendants ( txid, |_, descendant_txid| {
409
+ Some ( (
410
+ descendant_txid,
411
+ self . wallet . tx_graph ( ) . get_tx ( descendant_txid) ?,
412
+ ) )
413
+ } ) ,
414
+ )
415
+ . for_each ( |( txid, tx) | {
416
+ self . params
417
+ . unspendable
418
+ . extend ( ( 0 ..tx. output . len ( ) ) . map ( |vout| OutPoint :: new ( txid, vout as u32 ) ) ) ;
419
+ } ) ;
420
+
421
+ Ok ( self )
422
+ }
423
+
424
+ /// Get the previous fee and feerate, i.e. the fee of the tx being fee-bumped, if any.
425
+ ///
426
+ /// This method may be used in combination with either [`build_fee_bump`] or [`replace_tx`]
427
+ /// and is useful for deciding what fee to attach to a transaction for the purpose of
428
+ /// "replace-by-fee" (RBF).
429
+ ///
430
+ /// [`build_fee_bump`]: Wallet::build_fee_bump
431
+ /// [`replace_tx`]: Self::replace_tx
432
+ pub fn previous_fee ( & self ) -> Option < ( Amount , FeeRate ) > {
433
+ self . params . bumping_fee . map ( |p| ( p. absolute , p. rate ) )
434
+ }
435
+
311
436
/// Add a foreign UTXO i.e. a UTXO not known by this wallet.
312
437
///
313
438
/// There might be cases where the UTxO belongs to the wallet but it doesn't have knowledge of
@@ -717,6 +842,30 @@ impl fmt::Display for AddUtxoError {
717
842
#[ cfg( feature = "std" ) ]
718
843
impl std:: error:: Error for AddUtxoError { }
719
844
845
+ /// Error returned by [`TxBuilder::replace_tx`].
846
+ #[ derive( Debug ) ]
847
+ pub enum ReplaceTxError {
848
+ /// Transaction was not found in tx graph
849
+ MissingTransaction ,
850
+ /// Transaction can't be replaced by this wallet
851
+ NonReplaceable ,
852
+ /// Transaction is already confirmed
853
+ TransactionConfirmed ,
854
+ }
855
+
856
+ impl fmt:: Display for ReplaceTxError {
857
+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
858
+ match self {
859
+ Self :: MissingTransaction => write ! ( f, "transaction not found in tx graph" ) ,
860
+ Self :: NonReplaceable => write ! ( f, "no replaceable input found" ) ,
861
+ Self :: TransactionConfirmed => write ! ( f, "cannot replace a confirmed tx" ) ,
862
+ }
863
+ }
864
+ }
865
+
866
+ #[ cfg( feature = "std" ) ]
867
+ impl std:: error:: Error for ReplaceTxError { }
868
+
720
869
#[ derive( Debug ) ]
721
870
/// Error returned from [`TxBuilder::add_foreign_utxo`].
722
871
pub enum AddForeignUtxoError {
@@ -853,6 +1002,7 @@ mod test {
853
1002
} ;
854
1003
}
855
1004
1005
+ use crate :: test_utils:: * ;
856
1006
use bitcoin:: consensus:: deserialize;
857
1007
use bitcoin:: hex:: FromHex ;
858
1008
use bitcoin:: TxOut ;
@@ -1342,4 +1492,135 @@ mod test {
1342
1492
}
1343
1493
) ) ;
1344
1494
}
1495
+
1496
+ #[ test]
1497
+ fn replace_tx_allows_selecting_spent_outputs ( ) {
1498
+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1499
+ let outpoint_1 = OutPoint :: new ( txid_0, 0 ) ;
1500
+
1501
+ // receive output 2
1502
+ let outpoint_2 = receive_output_in_latest_block ( & mut wallet, 49_000 ) ;
1503
+ assert_eq ! ( wallet. list_unspent( ) . count( ) , 2 ) ;
1504
+ assert_eq ! ( wallet. balance( ) . total( ) . to_sat( ) , 99_000 ) ;
1505
+
1506
+ // create tx1: 2-in/1-out sending all to `recip`
1507
+ let recip = ScriptBuf :: from_hex ( "0014446906a6560d8ad760db3156706e72e171f3a2aa" ) . unwrap ( ) ;
1508
+ let mut builder = wallet. build_tx ( ) ;
1509
+ builder. add_recipient ( recip. clone ( ) , Amount :: from_sat ( 98_800 ) ) ;
1510
+ let psbt = builder. finish ( ) . unwrap ( ) ;
1511
+ let tx1 = psbt. unsigned_tx ;
1512
+ let txid1 = tx1. compute_txid ( ) ;
1513
+ insert_tx ( & mut wallet, tx1) ;
1514
+ assert ! ( wallet. list_unspent( ) . next( ) . is_none( ) ) ;
1515
+
1516
+ // now replace tx1 with a new transaction
1517
+ let mut builder = wallet. build_tx ( ) ;
1518
+ builder. replace_tx ( txid1) . expect ( "should replace input" ) ;
1519
+ let prev_feerate = builder. previous_fee ( ) . unwrap ( ) . 1 ;
1520
+ builder. add_recipient ( recip, Amount :: from_sat ( 98_500 ) ) ;
1521
+ builder. fee_rate ( FeeRate :: from_sat_per_kwu (
1522
+ prev_feerate. to_sat_per_kwu ( ) + 250 ,
1523
+ ) ) ;
1524
+
1525
+ // Because outpoint 2 was spent in tx1, by default it won't be available for selection,
1526
+ // but we can add it manually, with the caveat that the builder is in a bump-fee
1527
+ // context.
1528
+ builder. add_utxo ( outpoint_2) . expect ( "should add output" ) ;
1529
+ let psbt = builder. finish ( ) . unwrap ( ) ;
1530
+
1531
+ assert ! ( psbt
1532
+ . unsigned_tx
1533
+ . input
1534
+ . iter( )
1535
+ . any( |txin| txin. previous_output == outpoint_1) ) ;
1536
+ assert ! ( psbt
1537
+ . unsigned_tx
1538
+ . input
1539
+ . iter( )
1540
+ . any( |txin| txin. previous_output == outpoint_2) ) ;
1541
+ }
1542
+
1543
+ // Replacing a tx should mark the original txouts unspendable
1544
+ #[ test]
1545
+ fn test_replace_tx_unspendable ( ) {
1546
+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1547
+ let outpoint_0 = OutPoint :: new ( txid_0, 0 ) ;
1548
+ let balance = wallet. balance ( ) . total ( ) ;
1549
+ let fee = Amount :: from_sat ( 256 ) ;
1550
+
1551
+ let mut previous_output = outpoint_0;
1552
+
1553
+ // apply 3 unconfirmed txs to wallet
1554
+ for i in 1 ..=3 {
1555
+ let tx = Transaction {
1556
+ input : vec ! [ TxIn {
1557
+ previous_output,
1558
+ ..Default :: default ( )
1559
+ } ] ,
1560
+ output : vec ! [ TxOut {
1561
+ script_pubkey: wallet
1562
+ . reveal_next_address( KeychainKind :: External )
1563
+ . script_pubkey( ) ,
1564
+ value: balance - fee * i as u64 ,
1565
+ } ] ,
1566
+ ..new_tx ( i)
1567
+ } ;
1568
+
1569
+ let txid = tx. compute_txid ( ) ;
1570
+ insert_tx ( & mut wallet, tx) ;
1571
+ previous_output = OutPoint :: new ( txid, 0 ) ;
1572
+ }
1573
+
1574
+ let unconfirmed_txs: Vec < _ > = wallet
1575
+ . transactions ( )
1576
+ . filter ( |c| !c. chain_position . is_confirmed ( ) )
1577
+ . collect ( ) ;
1578
+ let txid_1 = unconfirmed_txs
1579
+ . iter ( )
1580
+ . find ( |c| c. tx_node . input [ 0 ] . previous_output == outpoint_0)
1581
+ . map ( |c| c. tx_node . txid )
1582
+ . unwrap ( ) ;
1583
+ let unconfirmed_txids = unconfirmed_txs
1584
+ . iter ( )
1585
+ . map ( |c| c. tx_node . txid )
1586
+ . collect :: < Vec < _ > > ( ) ;
1587
+ assert_eq ! ( unconfirmed_txids. len( ) , 3 ) ;
1588
+
1589
+ // replace tx1
1590
+ let mut builder = wallet. build_tx ( ) ;
1591
+ builder. replace_tx ( txid_1) . unwrap ( ) ;
1592
+ assert_eq ! ( builder. params. utxos. len( ) , 1 ) ;
1593
+ assert ! ( builder. params. utxos. contains_key( & outpoint_0) ) ;
1594
+ for txid in unconfirmed_txids {
1595
+ assert ! ( builder. params. unspendable. contains( & OutPoint :: new( txid, 0 ) ) ) ;
1596
+ }
1597
+ }
1598
+
1599
+ #[ test]
1600
+ fn test_replace_tx_error ( ) {
1601
+ use bitcoin:: hashes:: Hash ;
1602
+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1603
+
1604
+ // tx does not exist
1605
+ let mut builder = wallet. build_tx ( ) ;
1606
+ let res = builder. replace_tx ( Txid :: all_zeros ( ) ) ;
1607
+ assert ! ( matches!( res, Err ( ReplaceTxError :: MissingTransaction ) ) ) ;
1608
+
1609
+ // tx confirmed
1610
+ let mut builder = wallet. build_tx ( ) ;
1611
+ let res = builder. replace_tx ( txid_0) ;
1612
+ assert ! ( matches!( res, Err ( ReplaceTxError :: TransactionConfirmed ) ) ) ;
1613
+
1614
+ // can't replace a foreign tx
1615
+ let tx = Transaction {
1616
+ input : vec ! [ TxIn :: default ( ) ] ,
1617
+ output : vec ! [ TxOut :: NULL ] ,
1618
+ ..new_tx ( 0 )
1619
+ } ;
1620
+ let txid = tx. compute_txid ( ) ;
1621
+ insert_tx ( & mut wallet, tx) ;
1622
+ let mut builder = wallet. build_tx ( ) ;
1623
+ let res = builder. replace_tx ( txid) ;
1624
+ assert ! ( matches!( res, Err ( ReplaceTxError :: NonReplaceable ) ) ) ;
1625
+ }
1345
1626
}
0 commit comments