@@ -274,25 +274,30 @@ 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 utxos = outpoints
278
+ . iter ( )
279
+ . map ( |outpoint| {
280
+ self . wallet
281
+ . get_utxo ( * outpoint)
282
+ . or_else ( || {
283
+ // allow selecting a spent output if we're bumping fee
284
+ self . params
285
+ . bumping_fee
286
+ . and_then ( |_| self . wallet . get_output ( * outpoint) )
287
+ } )
288
+ . ok_or ( AddUtxoError :: UnknownUtxo ( * outpoint) )
289
+ } )
290
+ . collect :: < Result < Vec < _ > , _ > > ( ) ?;
291
+
292
+ for utxo in utxos {
293
+ let descriptor = self . wallet . public_descriptor ( utxo. keychain ) ;
294
+ let satisfaction_weight = descriptor
295
+ . max_weight_to_satisfy ( )
296
+ . expect ( "descriptor should be satisfiable" ) ;
297
+ self . params . utxos . push ( WeightedUtxo {
298
+ satisfaction_weight,
299
+ utxo : Utxo :: Local ( utxo) ,
300
+ } ) ;
296
301
}
297
302
298
303
Ok ( self )
@@ -306,6 +311,120 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
306
311
self . add_utxos ( & [ outpoint] )
307
312
}
308
313
314
+ /// Replace an unconfirmed transaction.
315
+ ///
316
+ /// This method attempts to create a replacement for the transaction with `txid` by
317
+ /// looking for the largest input that is owned by this wallet and adding it to the
318
+ /// list of UTXOs to spend.
319
+ ///
320
+ /// # Note
321
+ ///
322
+ /// Aside from reusing one of the inputs, the method makes no assumptions about the
323
+ /// structure of the replacement, so if you need to reuse the original recipient(s)
324
+ /// and/or change address, you should add them manually before [`finish`] is called.
325
+ ///
326
+ /// # Example
327
+ ///
328
+ /// Create a replacement for an unconfirmed wallet transaction
329
+ ///
330
+ /// ```rust,no_run
331
+ /// # let mut wallet = bdk_wallet::doctest_wallet!();
332
+ /// let wallet_txs = wallet.transactions().collect::<Vec<_>>();
333
+ /// let tx = wallet_txs.first().expect("must have wallet tx");
334
+ ///
335
+ /// if !tx.chain_position.is_confirmed() {
336
+ /// let txid = tx.tx_node.txid;
337
+ /// let mut builder = wallet.build_tx();
338
+ /// builder.replace_tx(txid).expect("should replace");
339
+ ///
340
+ /// // Continue building tx...
341
+ ///
342
+ /// let psbt = builder.finish()?;
343
+ /// }
344
+ /// # Ok::<_, anyhow::Error>(())
345
+ /// ```
346
+ ///
347
+ /// # Errors
348
+ ///
349
+ /// - If the original transaction is not found in the tx graph
350
+ /// - If the orginal transaction is confirmed
351
+ /// - If none of the inputs are owned by this wallet
352
+ ///
353
+ /// [`finish`]: TxBuilder::finish
354
+ pub fn replace_tx ( & mut self , txid : Txid ) -> Result < & mut Self , ReplaceTxError > {
355
+ let tx = self
356
+ . wallet
357
+ . indexed_graph
358
+ . graph ( )
359
+ . get_tx ( txid)
360
+ . ok_or ( ReplaceTxError :: MissingTransaction ) ?;
361
+ if self
362
+ . wallet
363
+ . transactions ( )
364
+ . find ( |c| c. tx_node . txid == txid)
365
+ . map ( |c| c. chain_position . is_confirmed ( ) )
366
+ . unwrap_or ( false )
367
+ {
368
+ return Err ( ReplaceTxError :: TransactionConfirmed ) ;
369
+ }
370
+ let outpoint = tx
371
+ . input
372
+ . iter ( )
373
+ . filter_map ( |txin| {
374
+ let prev_tx = self
375
+ . wallet
376
+ . indexed_graph
377
+ . graph ( )
378
+ . get_tx ( txin. previous_output . txid ) ?;
379
+ let txout = & prev_tx. output [ txin. previous_output . vout as usize ] ;
380
+ if self . wallet . is_mine ( txout. script_pubkey . clone ( ) ) {
381
+ Some ( ( txin. previous_output , txout. value ) )
382
+ } else {
383
+ None
384
+ }
385
+ } )
386
+ . max_by_key ( |( _, value) | * value)
387
+ . map ( |( op, _) | op)
388
+ . ok_or ( ReplaceTxError :: NonReplaceable ) ?;
389
+
390
+ // add previous fee
391
+ let absolute = self . wallet . calculate_fee ( & tx) . unwrap_or_default ( ) ;
392
+ let rate = absolute / tx. weight ( ) ;
393
+ self . params . bumping_fee = Some ( PreviousFee { absolute, rate } ) ;
394
+
395
+ self . add_utxo ( outpoint) . expect ( "we must have the utxo" ) ;
396
+
397
+ // do not try to spend the outputs of the replaced tx including descendants
398
+ core:: iter:: once ( ( txid, tx) )
399
+ . chain (
400
+ self . wallet
401
+ . tx_graph ( )
402
+ . walk_descendants ( txid, |_, descendant_txid| {
403
+ let tx = self . wallet . tx_graph ( ) . get_tx ( txid) ?;
404
+ Some ( ( descendant_txid, tx) )
405
+ } ) ,
406
+ )
407
+ . for_each ( |( txid, tx) | {
408
+ self . params
409
+ . unspendable
410
+ . extend ( ( 0 ..tx. output . len ( ) ) . map ( |vout| OutPoint :: new ( txid, vout as u32 ) ) ) ;
411
+ } ) ;
412
+
413
+ Ok ( self )
414
+ }
415
+
416
+ /// Get the previous fee and feerate, i.e. the fee of the tx being fee-bumped, if any.
417
+ ///
418
+ /// This method may be used in combination with either [`build_fee_bump`] or [`replace_tx`]
419
+ /// and is useful for deciding what fee to attach to a transaction for the purpose of
420
+ /// "replace-by-fee" (RBF).
421
+ ///
422
+ /// [`build_fee_bump`]: Wallet::build_fee_bump
423
+ /// [`replace_tx`]: Self::replace_tx
424
+ pub fn previous_fee ( & self ) -> Option < ( Amount , FeeRate ) > {
425
+ self . params . bumping_fee . map ( |p| ( p. absolute , p. rate ) )
426
+ }
427
+
309
428
/// Add a foreign UTXO i.e. a UTXO not owned by this wallet.
310
429
///
311
430
/// At a minimum to add a foreign UTXO we need:
@@ -697,6 +816,30 @@ impl fmt::Display for AddUtxoError {
697
816
#[ cfg( feature = "std" ) ]
698
817
impl std:: error:: Error for AddUtxoError { }
699
818
819
+ /// Error returned by [`TxBuilder::replace_tx`].
820
+ #[ derive( Debug ) ]
821
+ pub enum ReplaceTxError {
822
+ /// Transaction was not found in tx graph
823
+ MissingTransaction ,
824
+ /// Transaction can't be replaced by this wallet
825
+ NonReplaceable ,
826
+ /// Transaction is already confirmed
827
+ TransactionConfirmed ,
828
+ }
829
+
830
+ impl fmt:: Display for ReplaceTxError {
831
+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
832
+ match self {
833
+ Self :: MissingTransaction => write ! ( f, "transaction not found in tx graph" ) ,
834
+ Self :: NonReplaceable => write ! ( f, "no replaceable input found" ) ,
835
+ Self :: TransactionConfirmed => write ! ( f, "cannot replace a confirmed tx" ) ,
836
+ }
837
+ }
838
+ }
839
+
840
+ #[ cfg( feature = "std" ) ]
841
+ impl std:: error:: Error for ReplaceTxError { }
842
+
700
843
#[ derive( Debug ) ]
701
844
/// Error returned from [`TxBuilder::add_foreign_utxo`].
702
845
pub enum AddForeignUtxoError {
@@ -833,6 +976,7 @@ mod test {
833
976
} ;
834
977
}
835
978
979
+ use crate :: test_utils:: * ;
836
980
use bitcoin:: consensus:: deserialize;
837
981
use bitcoin:: hex:: FromHex ;
838
982
use bitcoin:: TxOut ;
@@ -1052,4 +1196,135 @@ mod test {
1052
1196
assert_eq ! ( filtered. len( ) , 1 ) ;
1053
1197
assert_eq ! ( filtered[ 0 ] . keychain, KeychainKind :: Internal ) ;
1054
1198
}
1199
+
1200
+ #[ test]
1201
+ fn replace_tx_allows_selecting_spent_outputs ( ) {
1202
+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1203
+ let outpoint_1 = OutPoint :: new ( txid_0, 0 ) ;
1204
+
1205
+ // receive output 2
1206
+ let outpoint_2 = receive_output_in_latest_block ( & mut wallet, 49_000 ) ;
1207
+ assert_eq ! ( wallet. list_unspent( ) . count( ) , 2 ) ;
1208
+ assert_eq ! ( wallet. balance( ) . total( ) . to_sat( ) , 99_000 ) ;
1209
+
1210
+ // create tx1: 2-in/1-out sending all to `recip`
1211
+ let recip = ScriptBuf :: from_hex ( "0014446906a6560d8ad760db3156706e72e171f3a2aa" ) . unwrap ( ) ;
1212
+ let mut builder = wallet. build_tx ( ) ;
1213
+ builder. add_recipient ( recip. clone ( ) , Amount :: from_sat ( 98_800 ) ) ;
1214
+ let psbt = builder. finish ( ) . unwrap ( ) ;
1215
+ let tx1 = psbt. unsigned_tx ;
1216
+ let txid1 = tx1. compute_txid ( ) ;
1217
+ insert_tx ( & mut wallet, tx1) ;
1218
+ assert ! ( wallet. list_unspent( ) . next( ) . is_none( ) ) ;
1219
+
1220
+ // now replace tx1 with a new transaction
1221
+ let mut builder = wallet. build_tx ( ) ;
1222
+ builder. replace_tx ( txid1) . expect ( "should replace input" ) ;
1223
+ let prev_feerate = builder. previous_fee ( ) . unwrap ( ) . 1 ;
1224
+ builder. add_recipient ( recip, Amount :: from_sat ( 98_500 ) ) ;
1225
+ builder. fee_rate ( FeeRate :: from_sat_per_kwu (
1226
+ prev_feerate. to_sat_per_kwu ( ) + 250 ,
1227
+ ) ) ;
1228
+
1229
+ // Because outpoint 2 was spent in tx1, by default it won't be available for selection,
1230
+ // but we can add it manually, with the caveat that the builder is in a bump-fee
1231
+ // context.
1232
+ builder. add_utxo ( outpoint_2) . expect ( "should add output" ) ;
1233
+ let psbt = builder. finish ( ) . unwrap ( ) ;
1234
+
1235
+ assert ! ( psbt
1236
+ . unsigned_tx
1237
+ . input
1238
+ . iter( )
1239
+ . any( |txin| txin. previous_output == outpoint_1) ) ;
1240
+ assert ! ( psbt
1241
+ . unsigned_tx
1242
+ . input
1243
+ . iter( )
1244
+ . any( |txin| txin. previous_output == outpoint_2) ) ;
1245
+ }
1246
+
1247
+ #[ test]
1248
+ fn test_replace_tx_unspendable_with_descendants ( ) {
1249
+ use crate :: KeychainKind :: External ;
1250
+
1251
+ // Replacing a tx should mark the original txouts unspendable
1252
+
1253
+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1254
+ let outpoint_0 = OutPoint :: new ( txid_0, 0 ) ;
1255
+ let balance = wallet. balance ( ) . total ( ) ;
1256
+ let fee = Amount :: from_sat ( 256 ) ;
1257
+
1258
+ let mut previous_output = outpoint_0;
1259
+
1260
+ // apply 3 unconfirmed txs to wallet
1261
+ for i in 1 ..=3 {
1262
+ let tx = Transaction {
1263
+ input : vec ! [ TxIn {
1264
+ previous_output,
1265
+ ..Default :: default ( )
1266
+ } ] ,
1267
+ output : vec ! [ TxOut {
1268
+ script_pubkey: wallet. reveal_next_address( External ) . script_pubkey( ) ,
1269
+ value: balance - fee * i as u64 ,
1270
+ } ] ,
1271
+ ..new_tx ( i)
1272
+ } ;
1273
+
1274
+ let txid = tx. compute_txid ( ) ;
1275
+ insert_tx ( & mut wallet, tx) ;
1276
+ previous_output = OutPoint :: new ( txid, 0 ) ;
1277
+ }
1278
+
1279
+ let unconfirmed_txs: Vec < _ > = wallet
1280
+ . transactions ( )
1281
+ . filter ( |c| !c. chain_position . is_confirmed ( ) )
1282
+ . collect ( ) ;
1283
+ let txid_1 = unconfirmed_txs
1284
+ . iter ( )
1285
+ . find ( |c| c. tx_node . input [ 0 ] . previous_output == outpoint_0)
1286
+ . map ( |c| c. tx_node . txid )
1287
+ . unwrap ( ) ;
1288
+ let unconfirmed_txids: Vec < _ > = unconfirmed_txs. iter ( ) . map ( |c| c. tx_node . txid ) . collect ( ) ;
1289
+ assert_eq ! ( unconfirmed_txids. len( ) , 3 ) ;
1290
+
1291
+ // replace tx1
1292
+ let mut builder = wallet. build_tx ( ) ;
1293
+ builder. replace_tx ( txid_1) . unwrap ( ) ;
1294
+ assert_eq ! (
1295
+ builder. params. utxos. first( ) . unwrap( ) . utxo. outpoint( ) ,
1296
+ outpoint_0
1297
+ ) ;
1298
+ for txid in unconfirmed_txids {
1299
+ assert ! ( builder. params. unspendable. contains( & OutPoint :: new( txid, 0 ) ) ) ;
1300
+ }
1301
+ }
1302
+
1303
+ #[ test]
1304
+ fn test_replace_tx_error ( ) {
1305
+ use bitcoin:: hashes:: Hash ;
1306
+ let ( mut wallet, txid_0) = get_funded_wallet_wpkh ( ) ;
1307
+
1308
+ // tx does not exist
1309
+ let mut builder = wallet. build_tx ( ) ;
1310
+ let res = builder. replace_tx ( Txid :: all_zeros ( ) ) ;
1311
+ assert ! ( matches!( res, Err ( ReplaceTxError :: MissingTransaction ) ) ) ;
1312
+
1313
+ // tx confirmed
1314
+ let mut builder = wallet. build_tx ( ) ;
1315
+ let res = builder. replace_tx ( txid_0) ;
1316
+ assert ! ( matches!( res, Err ( ReplaceTxError :: TransactionConfirmed ) ) ) ;
1317
+
1318
+ // can't replace a foreign tx
1319
+ let tx = Transaction {
1320
+ input : vec ! [ TxIn :: default ( ) ] ,
1321
+ output : vec ! [ TxOut :: NULL ] ,
1322
+ ..new_tx ( 0 )
1323
+ } ;
1324
+ let txid = tx. compute_txid ( ) ;
1325
+ insert_tx ( & mut wallet, tx) ;
1326
+ let mut builder = wallet. build_tx ( ) ;
1327
+ let res = builder. replace_tx ( txid) ;
1328
+ assert ! ( matches!( res, Err ( ReplaceTxError :: NonReplaceable ) ) ) ;
1329
+ }
1055
1330
}
0 commit comments