@@ -274,27 +274,36 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
274274 /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
275275 /// the "utxos" and the "unspendable" list, it will be spent.
276276 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 < _ , _ > > ( ) ;
277282 let utxo_batch = outpoints
278283 . iter ( )
279284 . 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+ ) )
296304 } )
297305 . collect :: < Result < HashMap < OutPoint , WeightedUtxo > , AddUtxoError > > ( ) ?;
306+
298307 self . params . utxos . extend ( utxo_batch) ;
299308
300309 Ok ( self )
@@ -308,6 +317,122 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
308317 self . add_utxos ( & [ outpoint] )
309318 }
310319
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+
311436 /// Add a foreign UTXO i.e. a UTXO not known by this wallet.
312437 ///
313438 /// There might be cases where the UTxO belongs to the wallet but it doesn't have knowledge of
@@ -721,6 +846,30 @@ impl fmt::Display for AddUtxoError {
721846#[ cfg( feature = "std" ) ]
722847impl std:: error:: Error for AddUtxoError { }
723848
849+ /// Error returned by [`TxBuilder::replace_tx`].
850+ #[ derive( Debug ) ]
851+ pub enum ReplaceTxError {
852+ /// Transaction was not found in tx graph
853+ MissingTransaction ,
854+ /// Transaction can't be replaced by this wallet
855+ NonReplaceable ,
856+ /// Transaction is already confirmed
857+ TransactionConfirmed ,
858+ }
859+
860+ impl fmt:: Display for ReplaceTxError {
861+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
862+ match self {
863+ Self :: MissingTransaction => write ! ( f, "transaction not found in tx graph" ) ,
864+ Self :: NonReplaceable => write ! ( f, "no replaceable input found" ) ,
865+ Self :: TransactionConfirmed => write ! ( f, "cannot replace a confirmed tx" ) ,
866+ }
867+ }
868+ }
869+
870+ #[ cfg( feature = "std" ) ]
871+ impl std:: error:: Error for ReplaceTxError { }
872+
724873#[ derive( Debug ) ]
725874/// Error returned from [`TxBuilder::add_foreign_utxo`].
726875pub enum AddForeignUtxoError {
@@ -857,6 +1006,7 @@ mod test {
8571006 } ;
8581007 }
8591008
1009+ use crate :: test_utils:: * ;
8601010 use bitcoin:: consensus:: deserialize;
8611011 use bitcoin:: hex:: FromHex ;
8621012 use bitcoin:: TxOut ;
@@ -1346,4 +1496,135 @@ mod test {
13461496 }
13471497 ) ) ;
13481498 }
1499+
1500+ #[ test]
1501+ fn replace_tx_allows_selecting_spent_outputs ( ) {
1502+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1503+ let outpoint_1 = OutPoint :: new ( txid_0, 0 ) ;
1504+
1505+ // receive output 2
1506+ let outpoint_2 = receive_output_in_latest_block ( & mut wallet, 49_000 ) ;
1507+ assert_eq ! ( wallet. list_unspent( ) . count( ) , 2 ) ;
1508+ assert_eq ! ( wallet. balance( ) . total( ) . to_sat( ) , 99_000 ) ;
1509+
1510+ // create tx1: 2-in/1-out sending all to `recip`
1511+ let recip = ScriptBuf :: from_hex ( "0014446906a6560d8ad760db3156706e72e171f3a2aa" ) . unwrap ( ) ;
1512+ let mut builder = wallet. build_tx ( ) ;
1513+ builder. add_recipient ( recip. clone ( ) , Amount :: from_sat ( 98_800 ) ) ;
1514+ let psbt = builder. finish ( ) . unwrap ( ) ;
1515+ let tx1 = psbt. unsigned_tx ;
1516+ let txid1 = tx1. compute_txid ( ) ;
1517+ insert_tx ( & mut wallet, tx1) ;
1518+ assert ! ( wallet. list_unspent( ) . next( ) . is_none( ) ) ;
1519+
1520+ // now replace tx1 with a new transaction
1521+ let mut builder = wallet. build_tx ( ) ;
1522+ builder. replace_tx ( txid1) . expect ( "should replace input" ) ;
1523+ let prev_feerate = builder. previous_fee ( ) . unwrap ( ) . 1 ;
1524+ builder. add_recipient ( recip, Amount :: from_sat ( 98_500 ) ) ;
1525+ builder. fee_rate ( FeeRate :: from_sat_per_kwu (
1526+ prev_feerate. to_sat_per_kwu ( ) + 250 ,
1527+ ) ) ;
1528+
1529+ // Because outpoint 2 was spent in tx1, by default it won't be available for selection,
1530+ // but we can add it manually, with the caveat that the builder is in a bump-fee
1531+ // context.
1532+ builder. add_utxo ( outpoint_2) . expect ( "should add output" ) ;
1533+ let psbt = builder. finish ( ) . unwrap ( ) ;
1534+
1535+ assert ! ( psbt
1536+ . unsigned_tx
1537+ . input
1538+ . iter( )
1539+ . any( |txin| txin. previous_output == outpoint_1) ) ;
1540+ assert ! ( psbt
1541+ . unsigned_tx
1542+ . input
1543+ . iter( )
1544+ . any( |txin| txin. previous_output == outpoint_2) ) ;
1545+ }
1546+
1547+ // Replacing a tx should mark the original txouts unspendable
1548+ #[ test]
1549+ fn test_replace_tx_unspendable ( ) {
1550+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1551+ let outpoint_0 = OutPoint :: new ( txid_0, 0 ) ;
1552+ let balance = wallet. balance ( ) . total ( ) ;
1553+ let fee = Amount :: from_sat ( 256 ) ;
1554+
1555+ let mut previous_output = outpoint_0;
1556+
1557+ // apply 3 unconfirmed txs to wallet
1558+ for i in 1 ..=3 {
1559+ let tx = Transaction {
1560+ input : vec ! [ TxIn {
1561+ previous_output,
1562+ ..Default :: default ( )
1563+ } ] ,
1564+ output : vec ! [ TxOut {
1565+ script_pubkey: wallet
1566+ . reveal_next_address( KeychainKind :: External )
1567+ . script_pubkey( ) ,
1568+ value: balance - fee * i as u64 ,
1569+ } ] ,
1570+ ..new_tx ( i)
1571+ } ;
1572+
1573+ let txid = tx. compute_txid ( ) ;
1574+ insert_tx ( & mut wallet, tx) ;
1575+ previous_output = OutPoint :: new ( txid, 0 ) ;
1576+ }
1577+
1578+ let unconfirmed_txs: Vec < _ > = wallet
1579+ . transactions ( )
1580+ . filter ( |c| !c. chain_position . is_confirmed ( ) )
1581+ . collect ( ) ;
1582+ let txid_1 = unconfirmed_txs
1583+ . iter ( )
1584+ . find ( |c| c. tx_node . input [ 0 ] . previous_output == outpoint_0)
1585+ . map ( |c| c. tx_node . txid )
1586+ . unwrap ( ) ;
1587+ let unconfirmed_txids = unconfirmed_txs
1588+ . iter ( )
1589+ . map ( |c| c. tx_node . txid )
1590+ . collect :: < Vec < _ > > ( ) ;
1591+ assert_eq ! ( unconfirmed_txids. len( ) , 3 ) ;
1592+
1593+ // replace tx1
1594+ let mut builder = wallet. build_tx ( ) ;
1595+ builder. replace_tx ( txid_1) . unwrap ( ) ;
1596+ assert_eq ! ( builder. params. utxos. len( ) , 1 ) ;
1597+ assert ! ( builder. params. utxos. contains_key( & outpoint_0) ) ;
1598+ for txid in unconfirmed_txids {
1599+ assert ! ( builder. params. unspendable. contains( & OutPoint :: new( txid, 0 ) ) ) ;
1600+ }
1601+ }
1602+
1603+ #[ test]
1604+ fn test_replace_tx_error ( ) {
1605+ use bitcoin:: hashes:: Hash ;
1606+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1607+
1608+ // tx does not exist
1609+ let mut builder = wallet. build_tx ( ) ;
1610+ let res = builder. replace_tx ( Txid :: all_zeros ( ) ) ;
1611+ assert ! ( matches!( res, Err ( ReplaceTxError :: MissingTransaction ) ) ) ;
1612+
1613+ // tx confirmed
1614+ let mut builder = wallet. build_tx ( ) ;
1615+ let res = builder. replace_tx ( txid_0) ;
1616+ assert ! ( matches!( res, Err ( ReplaceTxError :: TransactionConfirmed ) ) ) ;
1617+
1618+ // can't replace a foreign tx
1619+ let tx = Transaction {
1620+ input : vec ! [ TxIn :: default ( ) ] ,
1621+ output : vec ! [ TxOut :: NULL ] ,
1622+ ..new_tx ( 0 )
1623+ } ;
1624+ let txid = tx. compute_txid ( ) ;
1625+ insert_tx ( & mut wallet, tx) ;
1626+ let mut builder = wallet. build_tx ( ) ;
1627+ let res = builder. replace_tx ( txid) ;
1628+ assert ! ( matches!( res, Err ( ReplaceTxError :: NonReplaceable ) ) ) ;
1629+ }
13491630}
0 commit comments