|
| 1 | +use std::{str::FromStr, sync::Arc, time::Duration}; |
| 2 | + |
| 3 | +use anyhow::Context; |
| 4 | +use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv}; |
| 5 | +use bdk_wallet::{KeychainKind, SignOptions, Wallet}; |
| 6 | +use bitcoin::{Amount, Network, Txid}; |
| 7 | + |
| 8 | +const DESCRIPTOR: &str = bdk_testenv::utils::DESCRIPTORS[3]; |
| 9 | + |
| 10 | +fn main() -> anyhow::Result<()> { |
| 11 | + let env = TestEnv::new().context("failed to start testenv")?; |
| 12 | + env.mine_blocks(101, None) |
| 13 | + .context("failed to mine blocks")?; |
| 14 | + |
| 15 | + let mut wallet = Wallet::create_single(DESCRIPTOR) |
| 16 | + .network(Network::Regtest) |
| 17 | + .create_wallet_no_persist() |
| 18 | + .context("failed to construct wallet")?; |
| 19 | + |
| 20 | + let mut emitter = bdk_bitcoind_rpc::Emitter::new( |
| 21 | + env.rpc_client(), |
| 22 | + wallet.latest_checkpoint(), |
| 23 | + 0, |
| 24 | + wallet |
| 25 | + .transactions() |
| 26 | + .filter(|tx| tx.chain_position.is_unconfirmed()) |
| 27 | + .map(|tx| tx.tx_node.txid), |
| 28 | + ); |
| 29 | + while let Some(block_event) = emitter.next_block()? { |
| 30 | + wallet.apply_block(&block_event.block, block_event.block_height())?; |
| 31 | + } |
| 32 | + |
| 33 | + // Receive an unconfirmed tx, spend from it, and the unconfirmed tx get's RBF'ed. |
| 34 | + // Our API should be able to recognise that the outgoing tx became evicted and allow the caller |
| 35 | + // to respond accordingly. |
| 36 | + let wallet_addr = wallet.next_unused_address(KeychainKind::External).address; |
| 37 | + let remote_addr = env |
| 38 | + .rpc_client() |
| 39 | + .get_new_address(None, None)? |
| 40 | + .assume_checked(); |
| 41 | + let incoming_txid = env.send(&wallet_addr, Amount::ONE_BTC)?; |
| 42 | + |
| 43 | + let mempool_event = emitter.mempool()?; |
| 44 | + wallet.apply_evicted_txs(mempool_event.evicted_ats()); |
| 45 | + wallet.apply_unconfirmed_txs(mempool_event.new_txs); |
| 46 | + assert_eq!(wallet.balance().total(), Amount::ONE_BTC); |
| 47 | + |
| 48 | + // Create & broadcast outgoing tx. |
| 49 | + let mut tx_builder = wallet.build_tx(); |
| 50 | + tx_builder.add_recipient(remote_addr, Amount::ONE_BTC / 2); |
| 51 | + let mut psbt = tx_builder.finish()?; |
| 52 | + assert!(wallet.sign(&mut psbt, SignOptions::default())?); |
| 53 | + let outgoing_tx = psbt.extract_tx()?; |
| 54 | + wallet.track_tx(outgoing_tx.clone()); |
| 55 | + assert_eq!(wallet.uncanonical_txs().count(), 1); |
| 56 | + |
| 57 | + // Sync. |
| 58 | + let outgoing_txid = env.rpc_client().send_raw_transaction(&outgoing_tx)?; |
| 59 | + env.wait_until_electrum_sees_txid(outgoing_txid, Duration::from_secs(5))?; |
| 60 | + let mempool_event = emitter.mempool()?; |
| 61 | + // TODO: Why is `outgoing_txid` not emitted? |
| 62 | + println!("mempool_event: {mempool_event:#?}"); |
| 63 | + wallet.apply_evicted_txs(mempool_event.evicted_ats()); |
| 64 | + wallet.apply_unconfirmed_txs(mempool_event.new_txs); |
| 65 | + let tx = wallet |
| 66 | + .canonical_txs() |
| 67 | + .find(|tx| tx.tx_node.txid == outgoing_txid) |
| 68 | + .expect("must find outgoing tx"); |
| 69 | + assert_eq!(wallet.uncanonical_txs().count(), 0); |
| 70 | + |
| 71 | + // RBF incoming tx. |
| 72 | + let res = env |
| 73 | + .rpc_client() |
| 74 | + .call::<serde_json::Value>("bumpfee", &[incoming_txid.to_string().into()])?; |
| 75 | + let incoming_replacement_txid = Txid::from_str(res.get("txid").unwrap().as_str().unwrap())?; |
| 76 | + |
| 77 | + let mempool_event = emitter.mempool()?; |
| 78 | + wallet.apply_evicted_txs(mempool_event.evicted_ats()); |
| 79 | + wallet.apply_unconfirmed_txs(mempool_event.new_txs); |
| 80 | + |
| 81 | + for uncanonical_tx in wallet.uncanonical_txs() {} |
| 82 | + |
| 83 | + Ok(()) |
| 84 | +} |
0 commit comments