Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,15 @@ bdk_file_store = { version = "0.22.0", optional = true }
bip39 = { version = "2.2.2", optional = true }
tempfile = { version = "3.26.0", optional = true }

[dependencies.bdk_tx]
version = "0.2.0"
default-features = false
git = "https://github.com/valuedmammal/bdk-tx"
branch = "release/0_2_0"

[features]
default = ["std"]
std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std"]
std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std", "bdk_tx/std"]
compiler = ["miniscript/compiler"]
all-keys = ["keys-bip39"]
keys-bip39 = ["bip39"]
Expand Down Expand Up @@ -77,3 +83,9 @@ name = "esplora_blocking"

[[example]]
name = "bitcoind_rpc"

[[example]]
name = "psbt"

[[example]]
name = "replace_by_fee"
120 changes: 120 additions & 0 deletions examples/psbt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#![allow(clippy::print_stdout)]

use std::collections::HashMap;
use std::str::FromStr;

use bdk_chain::BlockId;
use bdk_chain::ConfirmationBlockTime;
use bdk_wallet::psbt::{PsbtParams, SelectionStrategy::*};
use bdk_wallet::test_utils::*;
use bdk_wallet::{KeychainKind::External, Wallet};
use bitcoin::{consensus, secp256k1::rand, Address, Amount, TxIn, TxOut};
use rand::Rng;

// This example shows how to create a PSBT using BDK Wallet.

const NETWORK: bitcoin::Network = bitcoin::Network::Signet;
const SEND_TO: &str = "tb1pw3g5qvnkryghme7pyal228ekj6vq48zc5k983lqtlr2a96n4xw0q5ejknw";
const AMOUNT: Amount = Amount::from_sat(42_000);
const FEERATE: f64 = 2.0; // sat/vb

fn main() -> anyhow::Result<()> {
let (desc, change_desc) = get_test_wpkh_and_change_desc();

// Create wallet and fund it.
let mut wallet = Wallet::create(desc, change_desc)
.network(NETWORK)
.create_wallet_no_persist()?;

fund_wallet(&mut wallet)?;

// Create PSBT Signer, external to the wallet
let signer = {
let secp = wallet.secp_ctx();
let (_, external_keymap) = miniscript::Descriptor::parse_descriptor(secp, desc)?;
let (_, internal_keymap) = miniscript::Descriptor::parse_descriptor(secp, change_desc)?;
bdk_tx::Signer(external_keymap.into_iter().chain(internal_keymap).collect())
};

let utxos = wallet
.list_unspent()
.map(|output| (output.outpoint, output))
.collect::<HashMap<_, _>>();

// Build params.
let mut params = PsbtParams::default();
let addr = Address::from_str(SEND_TO)?.require_network(NETWORK)?;
let feerate = feerate_unchecked(FEERATE);
params
.add_recipients([(addr, AMOUNT)])
.fee_rate(feerate)
.coin_selection(SingleRandomDraw);

// Create PSBT (which also returns the Finalizer).
let (mut psbt, finalizer) = wallet.create_psbt(params)?;

dbg!(&psbt);

let tx = &psbt.unsigned_tx;
for txin in &tx.input {
let op = txin.previous_output;
let output = utxos.get(&op).unwrap();
println!("TxIn: {}", output.txout.value);
}
for txout in &tx.output {
println!("TxOut: {}", txout.value);
}

let _ = psbt
.sign(&signer, wallet.secp_ctx())
.map_err(|(_, errors)| anyhow::anyhow!("failed to sign PSBT: {errors:?}"))?;

println!("Signed: {}", !psbt.inputs[0].partial_sigs.is_empty());
let finalize_res = finalizer.finalize(&mut psbt);
println!("Finalized: {}", finalize_res.is_finalized());

let tx = psbt.extract_tx()?;
let feerate = wallet.calculate_fee_rate(&tx)?;
println!("Fee rate: {} sat/vb", bdk_wallet::floating_rate!(feerate));

println!("{}", consensus::encode::serialize_hex(&tx));

Ok(())
}

fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<()> {
let anchor = ConfirmationBlockTime {
block_id: BlockId {
height: 260071,
hash: "000000099f67ae6469d1ad0525d756e24d4b02fbf27d65b3f413d5feb367ec48".parse()?,
},
confirmation_time: 1752184658,
};
insert_checkpoint(wallet, anchor.block_id);

let mut rng = rand::thread_rng();

// Fund wallet with several random utxos
for i in 0..21 {
let addr = wallet.reveal_next_address(External).address;
let value = 10_000 * (i + 1) + (100 * rng.gen_range(0..10));
let tx = bitcoin::Transaction {
lock_time: bitcoin::absolute::LockTime::ZERO,
version: bitcoin::transaction::Version::TWO,
input: vec![TxIn::default()],
output: vec![TxOut {
script_pubkey: addr.script_pubkey(),
value: Amount::from_sat(value),
}],
};
insert_tx_anchor(wallet, tx, anchor.block_id);
}

let tip = BlockId {
height: 260171,
hash: "0000000b9efb77450e753ae9fd7be9f69219511c27b6e95c28f4126f3e1591c3".parse()?,
};
insert_checkpoint(wallet, tip);

Ok(())
}
185 changes: 185 additions & 0 deletions examples/replace_by_fee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#![allow(clippy::print_stdout)]

use std::sync::Arc;

use bdk_chain::BlockId;
use bdk_tx::ChangeScript;
use bdk_wallet::psbt::PsbtParams;
use bdk_wallet::test_utils::*;
use bdk_wallet::{KeychainKind, Wallet};
use bitcoin::{Amount, FeeRate, TxIn, TxOut};
use miniscript::{DefiniteDescriptorKey, Descriptor};

// This example demonstrates creating a sweep transaction using PsbtParams and replacing it with a
// higher feerate.

const NETWORK: bitcoin::Network = bitcoin::Network::Regtest;

fn main() -> anyhow::Result<()> {
let (desc, change_desc) = get_test_wpkh_and_change_desc();

// Create wallet and "fund" it with a single UTXO.
let mut wallet = Wallet::create(desc, change_desc)
.network(NETWORK)
.create_wallet_no_persist()?;

fund_wallet(&mut wallet)?;

// Create PSBT Signer, external to the wallet
let signer = {
let secp = wallet.secp_ctx();
let (_, external_keymap) = miniscript::Descriptor::parse_descriptor(secp, desc)?;
let (_, internal_keymap) = miniscript::Descriptor::parse_descriptor(secp, change_desc)?;
bdk_tx::Signer(external_keymap.into_iter().chain(internal_keymap).collect())
};

// Get a derived descriptor from the wallet to sweep funds to
let derived_descriptor: Descriptor<DefiniteDescriptorKey> = wallet
.public_descriptor(KeychainKind::External)
.at_derivation_index(1)?;

println!(
"Wallet funded with {}\n",
wallet.balance().total().display_dynamic()
);
println!("Creating first sweep transaction (tx1)...");

// Create tx1: sweep all funds to our own address at a low feerate
let mut params = PsbtParams::new();
params
.drain_wallet()
.change_script(ChangeScript::from_descriptor(derived_descriptor.clone()))
.fee_rate(FeeRate::from_sat_per_vb(2).expect("valid feerate"));

let (mut psbt1, finalizer1) = wallet.create_psbt(params)?;

// Sign and finalize tx1
let _ = psbt1
.sign(&signer, wallet.secp_ctx())
.map_err(|(_, errors)| anyhow::anyhow!("failed to sign PSBT: {errors:?}"))?;
println!("tx1 signed: {}", !psbt1.inputs[0].partial_sigs.is_empty());

let finalize_res = finalizer1.finalize(&mut psbt1);
println!("tx1 finalized: {}", finalize_res.is_finalized());

let tx1 = Arc::new(psbt1.extract_tx()?);
let feerate1 = wallet.calculate_fee_rate(&tx1)?;
let fee1 = wallet.calculate_fee(&tx1)?;

println!(" txid: {}", tx1.compute_txid());
println!(
" fee rate: {} sat/vb",
bdk_wallet::floating_rate!(feerate1)
);
println!(" absolute fee: {} sats", fee1.to_sat());

// Apply tx1 to wallet as unconfirmed
wallet.apply_unconfirmed_txs([(tx1.clone(), 1234567000)]);

println!("\nCreating RBF replacement transaction (tx2)...");

// Create tx2: Replace tx1 at a higher feerate using PsbtParams
let mut rbf_params = PsbtParams::new().replace_txs(&[Arc::clone(&tx1)]);

// Set higher feerate for the replacement
rbf_params.fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate"));

// Retain the original sweep destination
rbf_params.change_script(ChangeScript::from_descriptor(derived_descriptor));

let (mut psbt2, finalizer2) = wallet.replace_by_fee(rbf_params)?;

// Sign and finalize tx2
let _ = psbt2
.sign(&signer, wallet.secp_ctx())
.map_err(|(_, errors)| anyhow::anyhow!("failed to sign PSBT: {errors:?}"))?;
println!("tx2 signed: {}", !psbt2.inputs[0].partial_sigs.is_empty());

let finalize_res = finalizer2.finalize(&mut psbt2);
println!("tx2 finalized: {}", finalize_res.is_finalized());

let tx2 = psbt2.extract_tx()?;
let feerate2 = wallet.calculate_fee_rate(&tx2)?;
let fee2 = wallet.calculate_fee(&tx2)?;

println!(" txid: {}", tx2.compute_txid());
println!(
" fee rate: {} sat/vb",
bdk_wallet::floating_rate!(feerate2)
);
println!(" absolute fee: {} sats", fee2.to_sat());

println!("\nVerifying RBF properties...");

// Verify that tx1 and tx2 conflict (spend the same input)
let tx1_input = tx1.input[0].previous_output;
let tx2_input = tx2.input[0].previous_output;

assert_eq!(
tx1_input, tx2_input,
"ERROR: tx1 and tx2 must spend the same input"
);
println!("✓ Both transactions spend the same input: {}", tx1_input);

// Verify that tx2 has a higher feerate than tx1
assert!(
feerate2 > feerate1,
"ERROR: tx2 must have a higher feerate than tx1"
);
println!(
"✓ Replacement has higher fee rate ({} vs {} sat/vb)",
bdk_wallet::floating_rate!(feerate2),
bdk_wallet::floating_rate!(feerate1)
);

// Verify absolute fee increase
assert!(fee2 > fee1, "ERROR: tx2 must have a higher fee than tx1");
let fee_increase = fee2.to_sat() as i64 - fee1.to_sat() as i64;
println!("✓ Absolute fee increased by {} sats", fee_increase);

// Apply tx2 to wallet so it recognizes the conflict
wallet.apply_unconfirmed_txs([(tx2.clone(), 1234567001)]);

// Verify that the wallet recognizes the conflict
let txid2 = tx2.compute_txid();
assert!(
wallet
.tx_graph()
.direct_conflicts(&tx1)
.any(|(_, txid)| txid == txid2),
"ERROR: Wallet does not recognize tx2 as replacing tx1",
);
println!("✓ Wallet recognizes the transaction conflict");

println!("\n✓✓✓ RBF sweep complete! ✓✓✓");

Ok(())
}

fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<()> {
let anchor_block = BlockId {
height: 1,
hash: "3bcc1c447c6b3886f43e416b5c21cf5c139dc4829a71dc78609bc8f6235611c5".parse()?,
};
let chain_tip = BlockId {
height: 101,
hash: "7f96292d115d19450e4bf7d4c4e15c9f3ad21e3a3cf616c498110b988963470b".parse()?,
};

insert_checkpoint(wallet, anchor_block);

let addr = wallet.reveal_next_address(KeychainKind::External).address;
let tx = bitcoin::Transaction {
lock_time: bitcoin::absolute::LockTime::ZERO,
version: bitcoin::transaction::Version::TWO,
input: vec![TxIn::default()],
output: vec![TxOut {
script_pubkey: addr.script_pubkey(),
value: Amount::from_sat(50_000_000),
}],
};
insert_tx_anchor(wallet, tx, anchor_block);
insert_checkpoint(wallet, chain_tip);

Ok(())
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub use bdk_chain::rusqlite;
pub use bdk_chain::rusqlite_impl;
pub use descriptor::template;
pub use descriptor::HdKeyPaths;
pub use psbt::*;
pub use signer;
pub use signer::SignOptions;
pub use tx_builder::*;
Expand Down
4 changes: 4 additions & 0 deletions src/psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ use bitcoin::FeeRate;
use bitcoin::Psbt;
use bitcoin::TxOut;

mod params;

pub use params::*;

// TODO upstream the functions here to `rust-bitcoin`?

/// Trait to add functions to extract utxos and calculate fees.
Expand Down
Loading