Skip to content

Commit 4bcf525

Browse files
authored
feat: upgrade BDK to 2.3.0 and wrap new APIs (#14)
1 parent f53c3ba commit 4bcf525

4 files changed

Lines changed: 195 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# CLAUDE.md - Agent Instructions for bdk-wasm
2+
3+
## Overview
4+
5+
WASM bindings for [BDK](https://github.com/bitcoindevkit/bdk_wallet) (Bitcoin Dev Kit).
6+
Wraps `bdk_wallet` for use in browsers and Node.js via `wasm-bindgen`.
7+
8+
**Used in production by MetaMask Bitcoin Snap (~30M+ AUM). Treat all changes with extreme care.**
9+
10+
## Architecture
11+
12+
```
13+
src/
14+
├── lib.rs # Crate root, re-exports
15+
├── bitcoin/ # Core wallet functionality wrappers
16+
│ ├── wallet.rs # Wallet (create, load, sign, sync, addresses, UTXOs)
17+
│ ├── tx_builder.rs # Transaction builder
18+
│ ├── esplora_client.rs # Esplora blockchain client (behind `esplora` feature)
19+
│ ├── descriptor.rs # Descriptor utilities
20+
│ └── wallet_tx.rs # Wallet transaction wrapper
21+
├── types/ # WASM-compatible type wrappers (From/Into pattern)
22+
│ ├── address.rs, amount.rs, balance.rs, block.rs, chain.rs,
23+
│ │ changeset.rs, checkpoint.rs, error.rs, fee.rs, input.rs,
24+
│ │ keychain.rs, network.rs, output.rs, psbt.rs, script.rs,
25+
│ │ slip10.rs, transaction.rs
26+
│ └── mod.rs
27+
└── utils/ # Helpers (descriptor utils, panic hook, result type)
28+
```
29+
30+
### Pattern
31+
32+
Every BDK type is wrapped with a WASM-compatible struct that:
33+
1. Holds the inner BDK type
34+
2. Implements `From<BdkType>` and `Into<BdkType>` conversions
35+
3. Exposes methods via `#[wasm_bindgen]`
36+
37+
`Wallet` uses `Rc<RefCell<BdkWallet>>` because `wasm_bindgen` doesn't support Rust lifetimes.
38+
`TxBuilder` shares the wallet reference via `Rc<RefCell<>>` and builds its own parameter set,
39+
then calls the real BDK builder in `finish()`.
40+
41+
## Building
42+
43+
Requires: Rust stable, `wasm-pack`, `wasm32-unknown-unknown` target.
44+
45+
```bash
46+
# Browser target (default)
47+
wasm-pack build --all-features
48+
49+
# Node.js target
50+
wasm-pack build --target nodejs --all-features
51+
52+
# Specific features
53+
wasm-pack build --features esplora
54+
wasm-pack build --features debug,esplora
55+
```
56+
57+
## Testing
58+
59+
### Browser tests (Rust)
60+
```bash
61+
wasm-pack test --chrome --firefox --headless --features debug,default
62+
wasm-pack test --chrome --firefox --headless --features debug,esplora
63+
```
64+
65+
### Node.js tests (TypeScript/Jest)
66+
```bash
67+
cd tests/node
68+
yarn install --immutable
69+
yarn build # runs wasm-pack build --target nodejs --all-features
70+
yarn test # runs jest
71+
yarn lint # runs eslint
72+
```
73+
74+
Node tests are in `tests/node/integration/`:
75+
- `wallet.test.ts` — Wallet creation, addresses, descriptors
76+
- `esplora.test.ts` — Esplora sync, full scan, transaction sending (uses **Mutinynet signet**)
77+
- `utilities.test.ts` — Amount, Script, Address utilities
78+
- `errors.test.ts` — Error handling and error codes
79+
80+
**Note:** `esplora.test.ts` depends on Mutinynet signet (`https://mutinynet.com/api`) with a
81+
pre-funded test wallet. This test can be flaky if the faucet/signet is down.
82+
83+
### CI
84+
85+
GitHub Actions runs on every PR:
86+
- **Lint:** `cargo fmt --check` + `cargo clippy --all-features --all-targets -- -D warnings`
87+
- **Browser build:** Three matrix configs (all features, debug+default, debug+esplora)
88+
- **Node build + test:** Full wasm-pack build + Jest test suite
89+
90+
CI must be green before merging. Clippy treats warnings as errors (`-D warnings`).
91+
92+
## Features
93+
94+
- `default` — Core wallet functionality only
95+
- `esplora` — Adds `EsploraClient` for blockchain sync (enables `bdk_esplora` + `wasm-bindgen-futures`)
96+
- `debug` — Enables `console_error_panic_hook` for better WASM error messages
97+
98+
## Dependencies
99+
100+
Key dependencies (keep these in sync):
101+
- `bdk_wallet` — Core wallet library
102+
- `bdk_esplora` — Esplora client (must match `bdk_wallet` version series)
103+
- `bitcoin` — Bitcoin primitives
104+
- `wasm-bindgen` — Rust/JS interop
105+
106+
Check https://crates.io/crates/bdk_wallet/versions for latest releases.
107+
BDK uses a monorepo-ish approach: `bdk_wallet` and `bdk_esplora` versions must be compatible.
108+
109+
## Conventions
110+
111+
- **Conventional commits** (required for all commits and PR titles):
112+
- `feat:` — New feature or API wrapper
113+
- `fix:` — Bug fix
114+
- `refactor:` — Code restructuring without behavior change
115+
- `docs:` — Documentation only
116+
- `test:` — Adding or updating tests
117+
- `chore:` — Maintenance (deps, config, tooling)
118+
- `ci:` — CI/CD pipeline changes
119+
- `build:` — Build system changes
120+
- Scope is optional but encouraged: `feat(wallet):`, `fix(tx_builder):`, `chore(deps):`
121+
- Breaking changes: add `!` after type, e.g. `feat!:` or `feat(wallet)!:`
122+
- These prefixes feed into CHANGELOG.md generation
123+
- **Formatting:** `cargo fmt` with default settings
124+
- **All public items must be documented**
125+
- **Safe Rust only** — no `unsafe` without exceptional justification
126+
- **New features require tests**
127+
128+
## Known Issues
129+
130+
- `SignOptions` is deprecated in BDK 2.2.0+ (signer module moved to `bitcoin::psbt`).
131+
We use `#[allow(deprecated)]` until BDK provides a migration path, since `Wallet::sign`
132+
still requires it internally.
133+
- Esplora integration tests use Mutinynet signet which can be flaky.
134+
135+
## Maintenance Notes
136+
137+
- This repo is maintained by an AI agent (Toshi) with human review by @darioAnongba
138+
- All changes go through PRs — never push to main directly
139+
- One PR at a time to keep review manageable
140+
- Check BDK releases periodically for new APIs to wrap

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ web-sys = { version = "0.3.77", default-features = false, features = [
3232
getrandom = { version = "0.2.16", features = ["js"] }
3333

3434
# Bitcoin dependencies
35-
bdk_wallet = { version = "2.0.0" }
36-
bdk_esplora = { version = "0.22.0", default-features = false, features = [
35+
bdk_wallet = { version = "2.3.0" }
36+
bdk_esplora = { version = "0.22.1", default-features = false, features = [
3737
"async-https",
3838
], optional = true }
3939
bitcoin = { version = "0.32.6", default-features = false, features = [

src/bitcoin/tx_builder.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub struct TxBuilder {
2323
drain_to: Option<ScriptBuf>,
2424
allow_dust: bool,
2525
ordering: TxOrdering,
26+
min_confirmations: Option<u32>,
2627
}
2728

2829
#[wasm_bindgen]
@@ -38,6 +39,7 @@ impl TxBuilder {
3839
allow_dust: false,
3940
drain_to: None,
4041
ordering: BdkTxOrdering::default().into(),
42+
min_confirmations: None,
4143
}
4244
}
4345

@@ -102,6 +104,25 @@ impl TxBuilder {
102104
self
103105
}
104106

107+
/// Exclude outpoints whose enclosing transaction has fewer than `min_confirms`
108+
/// confirmations.
109+
///
110+
/// - Passing `0` will include all transactions (no filtering).
111+
/// - Passing `1` will exclude all unconfirmed transactions (equivalent to
112+
/// [`exclude_unconfirmed`]).
113+
/// - Passing `6` will only allow outpoints from transactions with at least 6 confirmations.
114+
pub fn exclude_below_confirmations(mut self, min_confirms: u32) -> Self {
115+
self.min_confirmations = Some(min_confirms);
116+
self
117+
}
118+
119+
/// Exclude outpoints whose enclosing transaction is unconfirmed.
120+
///
121+
/// This is a shorthand for [`exclude_below_confirmations(1)`](Self::exclude_below_confirmations).
122+
pub fn exclude_unconfirmed(self) -> Self {
123+
self.exclude_below_confirmations(1)
124+
}
125+
105126
/// Set whether or not the dust limit is checked.
106127
///
107128
/// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard.
@@ -130,6 +151,10 @@ impl TxBuilder {
130151
.fee_rate(self.fee_rate.into())
131152
.allow_dust(self.allow_dust);
132153

154+
if let Some(min_confirms) = self.min_confirmations {
155+
builder.exclude_below_confirmations(min_confirms);
156+
}
157+
133158
if self.drain_wallet {
134159
builder.drain_wallet();
135160
}

src/bitcoin/wallet.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::{cell::RefCell, rc::Rc};
22

3-
use bdk_wallet::{SignOptions as BdkSignOptions, Wallet as BdkWallet};
3+
#[allow(deprecated)]
4+
use bdk_wallet::SignOptions as BdkSignOptions;
5+
use bdk_wallet::Wallet as BdkWallet;
46
use wasm_bindgen::{prelude::wasm_bindgen, JsError};
57
use web_sys::js_sys::Date;
68

@@ -33,6 +35,22 @@ impl Wallet {
3335
Ok(Wallet(Rc::new(RefCell::new(wallet))))
3436
}
3537

38+
/// Create a new [`Wallet`] from a BIP-389 two-path multipath descriptor.
39+
///
40+
/// The descriptor must contain exactly two derivation paths (receive and change),
41+
/// separated by a semicolon in angle brackets, e.g.:
42+
/// `wpkh([fingerprint/path]xpub.../<0;1>/*)`
43+
///
44+
/// The first path is used for the external (receive) keychain and the second
45+
/// for the internal (change) keychain.
46+
pub fn create_from_two_path_descriptor(network: Network, descriptor: String) -> JsResult<Wallet> {
47+
let wallet = BdkWallet::create_from_two_path_descriptor(descriptor)
48+
.network(network.into())
49+
.create_wallet_no_persist()?;
50+
51+
Ok(Wallet(Rc::new(RefCell::new(wallet))))
52+
}
53+
3654
pub fn load(
3755
changeset: ChangeSet,
3856
external_descriptor: Option<String>,
@@ -201,9 +219,16 @@ impl Wallet {
201219
}
202220
}
203221

222+
/// Options for signing a PSBT.
223+
///
224+
/// Note: `bdk_wallet::SignOptions` is deprecated upstream (BDK 2.2.0) in favor of
225+
/// `bitcoin::psbt::Psbt::sign()`. However, `Wallet::sign` still requires `SignOptions`
226+
/// internally, so we continue wrapping it until BDK provides a migration path.
227+
#[allow(deprecated)]
204228
#[wasm_bindgen]
205229
pub struct SignOptions(BdkSignOptions);
206230

231+
#[allow(deprecated)]
207232
#[wasm_bindgen]
208233
impl SignOptions {
209234
#[wasm_bindgen(constructor)]
@@ -272,12 +297,14 @@ impl SignOptions {
272297
}
273298
}
274299

300+
#[allow(deprecated)]
275301
impl From<SignOptions> for BdkSignOptions {
276302
fn from(options: SignOptions) -> Self {
277303
options.0
278304
}
279305
}
280306

307+
#[allow(deprecated)]
281308
impl Default for SignOptions {
282309
fn default() -> Self {
283310
Self::new()

0 commit comments

Comments
 (0)