Skip to content
Draft
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
10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }

[dependencies]
bdk_chain = { version = "0.23.3", features = ["miniscript", "serde"], default-features = false }
bdk_tx = { git = "https://github.com/evanlinjin/bdk-tx", branch = "feature/tx-template", default-features = false }
bitcoin = { version = "0.32.8", features = ["serde", "base64"], default-features = false }
miniscript = { version = "12.3.5", features = ["serde"], default-features = false }
rand_core = { version = "0.6.4" }
Expand All @@ -33,9 +34,10 @@ bdk_file_store = { version = "0.22.0", optional = true }
bip39 = { version = "2.2.2", optional = true }
tempfile = { version = "3.26.0", optional = true }


[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 +79,9 @@ name = "esplora_blocking"

[[example]]
name = "bitcoind_rpc"

[[example]]
name = "psbt"

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

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

use bdk_chain::BlockId;
use bdk_chain::ConfirmationBlockTime;
use bdk_wallet::psbt::{FinishParams, SelectParams, SelectionStrategy::*};
use bdk_wallet::test_utils::*;
use bdk_wallet::{KeychainKind::External, Wallet};
use bitcoin::{consensus, secp256k1::rand, transaction::Version, 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 address.
let addr = Address::from_str(SEND_TO)?.require_network(NETWORK)?;

// Stage 1: resolve the spendable candidates.
let coins = wallet.candidates()?;

// Stage 2: run coin selection, yielding a `TxTemplate`.
let template = wallet.select(
coins,
SelectParams {
recipients: vec![(addr.script_pubkey(), AMOUNT)],
coin_selection: LowestFee {
longterm_feerate: feerate_unchecked(3.0),
max_rounds: 500_000,
},
fee_rate: feerate_unchecked(FEERATE),
..Default::default()
},
)?;

// Shape the template before emitting. We pin the tx version, and — importantly — shuffle the
// outputs so the change output is not left in its default, trivially-identifiable position
// (`select` returns an *unshuffled* template). Other shaping (locktime, anti-fee-sniping,
// input ordering) is likewise done on the template.
let mut rng = rand::thread_rng();
let template = template.set_version(Version(3))?.shuffle_outputs(&mut rng);

// Stage 3: emit the PSBT (which also returns the Finalizer).
let (mut psbt, finalizer) = wallet.finish(template, FinishParams::default())?;

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(())
}
189 changes: 189 additions & 0 deletions examples/replace_by_fee.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#![allow(clippy::print_stdout)]

use std::sync::Arc;

use bdk_chain::BlockId;
use bdk_wallet::psbt::{FinishParams, SelectParams};
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 transaction with `SelectParams` 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 transaction (tx1)...");

// Create tx1: pay an amount to our own derived address at a low feerate.
let coins = wallet.candidates()?;
let template1 = wallet.select(
coins,
SelectParams {
recipients: vec![(derived_descriptor.script_pubkey(), Amount::from_sat(10_000))],
fee_rate: FeeRate::from_sat_per_vb(2).expect("valid feerate"),
..Default::default()
},
)?;
let (mut psbt1, finalizer1) = wallet.finish(template1, FinishParams::default())?;

// 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, paying the same recipient. Seed a candidate set
// with the tx to replace, then shape the output with bumped fee rate.
let coins = wallet.rbf_candidates(&[tx1.compute_txid()])?;
let template2 = wallet.select(
coins,
SelectParams {
recipients: vec![(derived_descriptor.script_pubkey(), Amount::from_sat(10_000))],
fee_rate: FeeRate::from_sat_per_vb(5).expect("valid feerate"),
..Default::default()
},
)?;
let (mut psbt2, finalizer2) = wallet.finish(template2, FinishParams::default())?;

// 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