Skip to content

Commit 46db8a2

Browse files
committed
feat: Add create_psbt{_with_aux_rand} for Wallet
Add module psbt/params.rs and introduce PsbtParams. test: Add `test_create_psbt` Introduce `ReplaceParams` Add `Wallet::replace_by_fee_and_recipients` Add `Wallet::replace_by_fee_with_aux_rand` test: `test_sanitize_rbf_set` test: Add `test_replace_by_fee` test: Add `test_spend_non_canonical_txout`
1 parent 3af563e commit 46db8a2

File tree

10 files changed

+1501
-16
lines changed

10 files changed

+1501
-16
lines changed

wallet/Cargo.toml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ bitcoin = { version = "0.32.6", features = [ "serde", "base64" ], default-featur
2222
serde = { version = "^1.0", features = ["derive"] }
2323
serde_json = { version = "^1.0" }
2424
bdk_chain = { version = "0.23.1", features = [ "miniscript", "serde" ], default-features = false }
25-
anyhow = { version = "1.0.98", optional = true }
26-
tempfile = { version = "3.20.0", optional = true }
25+
bdk_tx = { version = "0.1.0" }
26+
bdk_coin_select = { version = "0.4.0" }
2727

2828
# Optional dependencies
2929
bip39 = { version = "2.0", optional = true }
3030
bdk_file_store = { version = "0.21.1", optional = true }
31+
anyhow = { version = "1.0.98", optional = true }
32+
tempfile = { version = "3.20.0", optional = true }
3133

3234
[features]
3335
default = ["std"]
@@ -60,3 +62,11 @@ required-features = ["all-keys"]
6062
name = "miniscriptc"
6163
path = "examples/compiler.rs"
6264
required-features = ["compiler"]
65+
66+
[[example]]
67+
name = "psbt"
68+
required-features = ["test-utils"]
69+
70+
[[example]]
71+
name = "rbf"
72+
required-features = ["test-utils"]

wallet/examples/psbt.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#![allow(clippy::print_stdout)]
2+
3+
use std::collections::HashMap;
4+
use std::str::FromStr;
5+
6+
use bdk_chain::BlockId;
7+
use bdk_chain::ConfirmationBlockTime;
8+
use bdk_wallet::psbt::{PsbtParams, SelectionStrategy::*};
9+
use bdk_wallet::test_utils::*;
10+
use bdk_wallet::{KeychainKind::External, Wallet};
11+
use bitcoin::{
12+
bip32, consensus,
13+
secp256k1::{self, rand},
14+
Address, Amount, TxIn, TxOut,
15+
};
16+
use rand::Rng;
17+
18+
// This example shows how to create a PSBT using BDK Wallet.
19+
20+
const NETWORK: bitcoin::Network = bitcoin::Network::Signet;
21+
const SEND_TO: &str = "tb1pw3g5qvnkryghme7pyal228ekj6vq48zc5k983lqtlr2a96n4xw0q5ejknw";
22+
const AMOUNT: Amount = Amount::from_sat(42_000);
23+
const FEERATE: f64 = 2.0; // sat/vb
24+
25+
fn main() -> anyhow::Result<()> {
26+
let (desc, change_desc) = get_test_wpkh_and_change_desc();
27+
let secp = secp256k1::Secp256k1::new();
28+
29+
// Xpriv to be used for signing the PSBT
30+
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L")?;
31+
32+
// Create wallet and fund it.
33+
let mut wallet = Wallet::create(desc, change_desc)
34+
.network(NETWORK)
35+
.create_wallet_no_persist()?;
36+
37+
fund_wallet(&mut wallet)?;
38+
39+
let utxos = wallet
40+
.list_unspent()
41+
.map(|output| (output.outpoint, output))
42+
.collect::<HashMap<_, _>>();
43+
44+
// Build params.
45+
let mut params = PsbtParams::default();
46+
let addr = Address::from_str(SEND_TO)?.require_network(NETWORK)?;
47+
let feerate = feerate_unchecked(FEERATE);
48+
params
49+
.add_recipients([(addr, AMOUNT)])
50+
.feerate(feerate)
51+
.coin_selection(SingleRandomDraw);
52+
53+
// Create PSBT (which also returns the Finalizer).
54+
let (mut psbt, finalizer) = wallet.create_psbt(params)?;
55+
56+
dbg!(&psbt);
57+
58+
let tx = &psbt.unsigned_tx;
59+
for txin in &tx.input {
60+
let op = txin.previous_output;
61+
let output = utxos.get(&op).unwrap();
62+
println!("TxIn: {}", output.txout.value);
63+
}
64+
for txout in &tx.output {
65+
println!("TxOut: {}", txout.value);
66+
}
67+
68+
let _ = psbt.sign(&xprv, &secp);
69+
println!("Signed: {}", !psbt.inputs[0].partial_sigs.is_empty());
70+
let finalize_res = finalizer.finalize(&mut psbt);
71+
println!("Finalized: {}", finalize_res.is_finalized());
72+
73+
let tx = psbt.extract_tx()?;
74+
let feerate = wallet.calculate_fee_rate(&tx)?;
75+
println!("Fee rate: {} sat/vb", bdk_wallet::floating_rate!(feerate));
76+
77+
println!("{}", consensus::encode::serialize_hex(&tx));
78+
79+
Ok(())
80+
}
81+
82+
fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<()> {
83+
let anchor = ConfirmationBlockTime {
84+
block_id: BlockId {
85+
height: 260071,
86+
hash: "000000099f67ae6469d1ad0525d756e24d4b02fbf27d65b3f413d5feb367ec48".parse()?,
87+
},
88+
confirmation_time: 1752184658,
89+
};
90+
insert_checkpoint(wallet, anchor.block_id);
91+
92+
let mut rng = rand::thread_rng();
93+
94+
// Fund wallet with several random utxos
95+
for i in 0..21 {
96+
let addr = wallet.reveal_next_address(External).address;
97+
let value = 10_000 * (i + 1) + (100 * rng.gen_range(0..10));
98+
let tx = bitcoin::Transaction {
99+
lock_time: bitcoin::absolute::LockTime::ZERO,
100+
version: bitcoin::transaction::Version::TWO,
101+
input: vec![TxIn::default()],
102+
output: vec![TxOut {
103+
script_pubkey: addr.script_pubkey(),
104+
value: Amount::from_sat(value),
105+
}],
106+
};
107+
insert_tx_anchor(wallet, tx, anchor.block_id);
108+
}
109+
110+
let tip = BlockId {
111+
height: 260171,
112+
hash: "0000000b9efb77450e753ae9fd7be9f69219511c27b6e95c28f4126f3e1591c3".parse()?,
113+
};
114+
insert_checkpoint(wallet, tip);
115+
116+
Ok(())
117+
}

wallet/examples/rbf.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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_wallet::test_utils::*;
8+
use bdk_wallet::Wallet;
9+
use bitcoin::{bip32, consensus, secp256k1, Address, FeeRate, Transaction};
10+
11+
// This example shows how to create a Replace-By-Fee (RBF) transaction using BDK Wallet.
12+
13+
const NETWORK: bitcoin::Network = bitcoin::Network::Regtest;
14+
const SEND_TO: &str = "bcrt1q3yfqg2v9d605r45y5ddt5unz5n8v7jl5yk4a4f";
15+
16+
fn main() -> anyhow::Result<()> {
17+
let desc = "wpkh(tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU/84h/1h/0h/0/*)";
18+
let change_desc = "wpkh(tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU/84h/1h/0h/1/*)";
19+
let secp = secp256k1::Secp256k1::new();
20+
21+
// Xpriv to be used for signing the PSBT
22+
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU")?;
23+
24+
// Create wallet and "fund" it.
25+
let mut wallet = Wallet::create(desc, change_desc)
26+
.network(NETWORK)
27+
.create_wallet_no_persist()?;
28+
29+
// `tx_1` is the unconfirmed wallet tx that we want to replace.
30+
let tx_1 = fund_wallet(&mut wallet)?;
31+
wallet.apply_unconfirmed_txs([(tx_1.clone(), 1234567000)]);
32+
33+
// We'll need to fill in the original recipient details.
34+
let addr = Address::from_str(SEND_TO)?.require_network(NETWORK)?;
35+
let txo = tx_1
36+
.output
37+
.iter()
38+
.find(|txo| txo.script_pubkey == addr.script_pubkey())
39+
.expect("failed to find orginal recipient")
40+
.clone();
41+
42+
// Now build fee bump.
43+
let (mut psbt, finalizer) = wallet.replace_by_fee_and_recipients(
44+
&[Arc::clone(&tx_1)],
45+
FeeRate::from_sat_per_vb_unchecked(5),
46+
vec![(txo.script_pubkey, txo.value)],
47+
)?;
48+
49+
let _ = psbt.sign(&xprv, &secp);
50+
println!("Signed: {}", !psbt.inputs[0].partial_sigs.is_empty());
51+
let finalize_res = finalizer.finalize(&mut psbt);
52+
println!("Finalized: {}", finalize_res.is_finalized());
53+
54+
let tx = psbt.extract_tx()?;
55+
let feerate = wallet.calculate_fee_rate(&tx)?;
56+
println!("Fee rate: {} sat/vb", bdk_wallet::floating_rate!(feerate));
57+
58+
println!("{}", consensus::encode::serialize_hex(&tx));
59+
60+
wallet.apply_unconfirmed_txs([(tx.clone(), 1234567001)]);
61+
62+
let txid_2 = tx.compute_txid();
63+
64+
assert!(
65+
wallet
66+
.tx_graph()
67+
.direct_conflicts(&tx_1)
68+
.any(|(_, txid)| txid == txid_2),
69+
"ERROR: RBF tx does not replace `tx_1`",
70+
);
71+
72+
Ok(())
73+
}
74+
75+
fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<Arc<Transaction>> {
76+
// The parent of `tx`. This is needed to compute the original fee.
77+
let tx0: Transaction = consensus::encode::deserialize_hex(
78+
"020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0200f2052a010000001600144d34238b9c4c59b9e2781e2426a142a75b8901ab0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000",
79+
)?;
80+
81+
let anchor_block = BlockId {
82+
height: 101,
83+
hash: "3bcc1c447c6b3886f43e416b5c21cf5c139dc4829a71dc78609bc8f6235611c5".parse()?,
84+
};
85+
insert_tx_anchor(wallet, tx0, anchor_block);
86+
87+
let tx: Transaction = consensus::encode::deserialize_hex(
88+
"020000000001014cb96536e94ba3f840cb5c2c965c8f9a306209de63fcd02060219aaf14f1d7b30000000000fdffffff0280de80020000000016001489120429856e9f41d684a35aba7262a4cecf4bf4f312852701000000160014757a57b3009c0e9b2b9aa548434dc295e21aeb05024730440220400c0a767ce42e0ea02b72faabb7f3433e607b475111285e0975bba1e6fd2e13022059453d83cbacb6652ba075f59ca0437036f3f94cae1959c7c5c0f96a8954707a012102c0851c2d2bddc1dd0b05caeac307703ec0c4b96ecad5a85af47f6420e2ef6c661b000000",
89+
)?;
90+
91+
Ok(Arc::new(tx))
92+
}

wallet/src/psbt/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ use bitcoin::FeeRate;
1717
use bitcoin::Psbt;
1818
use bitcoin::TxOut;
1919

20+
mod params;
21+
22+
pub use params::*;
23+
2024
// TODO upstream the functions here to `rust-bitcoin`?
2125

2226
/// Trait to add functions to extract utxos and calculate fees.

0 commit comments

Comments
 (0)