|
| 1 | +#![allow(clippy::print_stdout)] |
| 2 | + |
| 3 | +use std::str::FromStr; |
| 4 | +use std::sync::Arc; |
| 5 | + |
| 6 | +use bdk_chain::BlockId; |
| 7 | +use bdk_chain::ConfirmationBlockTime; |
| 8 | +use bdk_chain::TxUpdate; |
| 9 | +use bdk_wallet::test_utils::*; |
| 10 | +use bdk_wallet::{Update, Wallet}; |
| 11 | +use bitcoin::{bip32, consensus, secp256k1, Address, FeeRate, Transaction}; |
| 12 | + |
| 13 | +// This example shows how to create a Replace-By-Fee (RBF) transaction using BDK Wallet. |
| 14 | + |
| 15 | +const NETWORK: bitcoin::Network = bitcoin::Network::Regtest; |
| 16 | +const SEND_TO: &str = "bcrt1q3yfqg2v9d605r45y5ddt5unz5n8v7jl5yk4a4f"; |
| 17 | + |
| 18 | +fn main() -> anyhow::Result<()> { |
| 19 | + let desc = "wpkh(tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU/84h/1h/0h/0/*)"; |
| 20 | + let change_desc = "wpkh(tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU/84h/1h/0h/1/*)"; |
| 21 | + let secp = secp256k1::Secp256k1::new(); |
| 22 | + |
| 23 | + // Xpriv to be used for signing the PSBT |
| 24 | + let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU")?; |
| 25 | + |
| 26 | + // Create wallet and "fund" it. |
| 27 | + let mut wallet = Wallet::create(desc, change_desc) |
| 28 | + .network(NETWORK) |
| 29 | + .create_wallet_no_persist()?; |
| 30 | + |
| 31 | + // `tx_1` is the unconfirmed wallet tx that we want to replace. |
| 32 | + let tx_1 = fund_wallet(&mut wallet)?; |
| 33 | + wallet.apply_unconfirmed_txs([(tx_1.clone(), 1234567000)]); |
| 34 | + |
| 35 | + // We'll need to fill in the original recipient details. |
| 36 | + let addr = Address::from_str(SEND_TO)?.require_network(NETWORK)?; |
| 37 | + let txo = tx_1 |
| 38 | + .output |
| 39 | + .iter() |
| 40 | + .find(|txo| txo.script_pubkey == addr.script_pubkey()) |
| 41 | + .expect("failed to find orginal recipient") |
| 42 | + .clone(); |
| 43 | + |
| 44 | + // Now build fee bump. |
| 45 | + let (mut psbt, finalizer) = wallet.replace_by_fee_and_recipients( |
| 46 | + &[tx_1.clone()], |
| 47 | + FeeRate::from_sat_per_vb_unchecked(5), |
| 48 | + vec![(txo.script_pubkey, txo.value)], |
| 49 | + )?; |
| 50 | + |
| 51 | + let _ = psbt.sign(&xprv, &secp); |
| 52 | + println!("Signed: {}", !psbt.inputs[0].partial_sigs.is_empty()); |
| 53 | + let finalize_res = finalizer.finalize(&mut psbt); |
| 54 | + println!("Finalized: {}", finalize_res.is_finalized()); |
| 55 | + |
| 56 | + let tx = psbt.extract_tx()?; |
| 57 | + let feerate = wallet.calculate_fee_rate(&tx)?; |
| 58 | + println!("Fee rate: {} sat/vb", bdk_wallet::floating_rate!(feerate)); |
| 59 | + |
| 60 | + println!("{}", consensus::encode::serialize_hex(&tx)); |
| 61 | + |
| 62 | + wallet.apply_unconfirmed_txs([(tx.clone(), 1234567001)]); |
| 63 | + |
| 64 | + let txid_2 = tx.compute_txid(); |
| 65 | + |
| 66 | + assert!( |
| 67 | + wallet |
| 68 | + .tx_graph() |
| 69 | + .direct_conflicts(&tx_1) |
| 70 | + .any(|(_, txid)| txid == txid_2), |
| 71 | + "ERROR: RBF tx does not conflict with `tx_1`", |
| 72 | + ); |
| 73 | + |
| 74 | + Ok(()) |
| 75 | +} |
| 76 | + |
| 77 | +fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<Arc<Transaction>> { |
| 78 | + // The parent of `tx`. This is needed to compute the original fee. |
| 79 | + let tx0: Arc<Transaction> = consensus::encode::deserialize_hex( |
| 80 | + "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0200f2052a010000001600144d34238b9c4c59b9e2781e2426a142a75b8901ab0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", |
| 81 | + ) |
| 82 | + .map(Arc::new)?; |
| 83 | + |
| 84 | + let anchor_block = BlockId { |
| 85 | + height: 101, |
| 86 | + hash: "3bcc1c447c6b3886f43e416b5c21cf5c139dc4829a71dc78609bc8f6235611c5".parse()?, |
| 87 | + }; |
| 88 | + insert_tx_anchor(wallet, tx0, anchor_block); |
| 89 | + |
| 90 | + let tx: Arc<Transaction> = consensus::encode::deserialize_hex( |
| 91 | + "020000000001014cb96536e94ba3f840cb5c2c965c8f9a306209de63fcd02060219aaf14f1d7b30000000000fdffffff0280de80020000000016001489120429856e9f41d684a35aba7262a4cecf4bf4f312852701000000160014757a57b3009c0e9b2b9aa548434dc295e21aeb05024730440220400c0a767ce42e0ea02b72faabb7f3433e607b475111285e0975bba1e6fd2e13022059453d83cbacb6652ba075f59ca0437036f3f94cae1959c7c5c0f96a8954707a012102c0851c2d2bddc1dd0b05caeac307703ec0c4b96ecad5a85af47f6420e2ef6c661b000000", |
| 92 | + ) |
| 93 | + .map(Arc::new)?; |
| 94 | + |
| 95 | + Ok(tx) |
| 96 | +} |
| 97 | + |
| 98 | +fn insert_tx_anchor(wallet: &mut Wallet, tx: Arc<Transaction>, block_id: BlockId) { |
| 99 | + insert_checkpoint(wallet, block_id); |
| 100 | + let anchor = ConfirmationBlockTime { |
| 101 | + block_id, |
| 102 | + confirmation_time: 1234567000, |
| 103 | + }; |
| 104 | + let txid = tx.compute_txid(); |
| 105 | + |
| 106 | + let mut tx_update = TxUpdate::default(); |
| 107 | + tx_update.txs = vec![tx]; |
| 108 | + tx_update.anchors = [(anchor, txid)].into(); |
| 109 | + |
| 110 | + wallet |
| 111 | + .apply_update(Update { |
| 112 | + tx_update, |
| 113 | + ..Default::default() |
| 114 | + }) |
| 115 | + .expect("failed to apply update"); |
| 116 | +} |
0 commit comments