Skip to content
Merged
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
140 changes: 140 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# CLAUDE.md - Agent Instructions for bdk-wasm

## Overview

WASM bindings for [BDK](https://github.com/bitcoindevkit/bdk_wallet) (Bitcoin Dev Kit).
Wraps `bdk_wallet` for use in browsers and Node.js via `wasm-bindgen`.

**Used in production by MetaMask Bitcoin Snap (~30M+ AUM). Treat all changes with extreme care.**

## Architecture

```
src/
├── lib.rs # Crate root, re-exports
├── bitcoin/ # Core wallet functionality wrappers
│ ├── wallet.rs # Wallet (create, load, sign, sync, addresses, UTXOs)
│ ├── tx_builder.rs # Transaction builder
│ ├── esplora_client.rs # Esplora blockchain client (behind `esplora` feature)
│ ├── descriptor.rs # Descriptor utilities
│ └── wallet_tx.rs # Wallet transaction wrapper
├── types/ # WASM-compatible type wrappers (From/Into pattern)
│ ├── address.rs, amount.rs, balance.rs, block.rs, chain.rs,
│ │ changeset.rs, checkpoint.rs, error.rs, fee.rs, input.rs,
│ │ keychain.rs, network.rs, output.rs, psbt.rs, script.rs,
│ │ slip10.rs, transaction.rs
│ └── mod.rs
└── utils/ # Helpers (descriptor utils, panic hook, result type)
```

### Pattern

Every BDK type is wrapped with a WASM-compatible struct that:
1. Holds the inner BDK type
2. Implements `From<BdkType>` and `Into<BdkType>` conversions
3. Exposes methods via `#[wasm_bindgen]`

`Wallet` uses `Rc<RefCell<BdkWallet>>` because `wasm_bindgen` doesn't support Rust lifetimes.
`TxBuilder` shares the wallet reference via `Rc<RefCell<>>` and builds its own parameter set,
then calls the real BDK builder in `finish()`.

## Building

Requires: Rust stable, `wasm-pack`, `wasm32-unknown-unknown` target.

```bash
# Browser target (default)
wasm-pack build --all-features

# Node.js target
wasm-pack build --target nodejs --all-features

# Specific features
wasm-pack build --features esplora
wasm-pack build --features debug,esplora
```

## Testing

### Browser tests (Rust)
```bash
wasm-pack test --chrome --firefox --headless --features debug,default
wasm-pack test --chrome --firefox --headless --features debug,esplora
```

### Node.js tests (TypeScript/Jest)
```bash
cd tests/node
yarn install --immutable
yarn build # runs wasm-pack build --target nodejs --all-features
yarn test # runs jest
yarn lint # runs eslint
```

Node tests are in `tests/node/integration/`:
- `wallet.test.ts` — Wallet creation, addresses, descriptors
- `esplora.test.ts` — Esplora sync, full scan, transaction sending (uses **Mutinynet signet**)
- `utilities.test.ts` — Amount, Script, Address utilities
- `errors.test.ts` — Error handling and error codes

**Note:** `esplora.test.ts` depends on Mutinynet signet (`https://mutinynet.com/api`) with a
pre-funded test wallet. This test can be flaky if the faucet/signet is down.

### CI

GitHub Actions runs on every PR:
- **Lint:** `cargo fmt --check` + `cargo clippy --all-features --all-targets -- -D warnings`
- **Browser build:** Three matrix configs (all features, debug+default, debug+esplora)
- **Node build + test:** Full wasm-pack build + Jest test suite

CI must be green before merging. Clippy treats warnings as errors (`-D warnings`).

## Features

- `default` — Core wallet functionality only
- `esplora` — Adds `EsploraClient` for blockchain sync (enables `bdk_esplora` + `wasm-bindgen-futures`)
- `debug` — Enables `console_error_panic_hook` for better WASM error messages

## Dependencies

Key dependencies (keep these in sync):
- `bdk_wallet` — Core wallet library
- `bdk_esplora` — Esplora client (must match `bdk_wallet` version series)
- `bitcoin` — Bitcoin primitives
- `wasm-bindgen` — Rust/JS interop

Check https://crates.io/crates/bdk_wallet/versions for latest releases.
BDK uses a monorepo-ish approach: `bdk_wallet` and `bdk_esplora` versions must be compatible.

## Conventions

- **Conventional commits** (required for all commits and PR titles):
- `feat:` — New feature or API wrapper
- `fix:` — Bug fix
- `refactor:` — Code restructuring without behavior change
- `docs:` — Documentation only
- `test:` — Adding or updating tests
- `chore:` — Maintenance (deps, config, tooling)
- `ci:` — CI/CD pipeline changes
- `build:` — Build system changes
- Scope is optional but encouraged: `feat(wallet):`, `fix(tx_builder):`, `chore(deps):`
- Breaking changes: add `!` after type, e.g. `feat!:` or `feat(wallet)!:`
- These prefixes feed into CHANGELOG.md generation
- **Formatting:** `cargo fmt` with default settings
- **All public items must be documented**
- **Safe Rust only** — no `unsafe` without exceptional justification
- **New features require tests**

## Known Issues

- `SignOptions` is deprecated in BDK 2.2.0+ (signer module moved to `bitcoin::psbt`).
We use `#[allow(deprecated)]` until BDK provides a migration path, since `Wallet::sign`
still requires it internally.
- Esplora integration tests use Mutinynet signet which can be flaky.

## Maintenance Notes

- This repo is maintained by an AI agent (Toshi) with human review by @darioAnongba
- All changes go through PRs — never push to main directly
- One PR at a time to keep review manageable
- Check BDK releases periodically for new APIs to wrap
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ web-sys = { version = "0.3.77", default-features = false, features = [
getrandom = { version = "0.2.16", features = ["js"] }

# Bitcoin dependencies
bdk_wallet = { version = "2.0.0" }
bdk_esplora = { version = "0.22.0", default-features = false, features = [
bdk_wallet = { version = "2.3.0" }
bdk_esplora = { version = "0.22.1", default-features = false, features = [
"async-https",
], optional = true }
bitcoin = { version = "0.32.6", default-features = false, features = [
Expand Down
25 changes: 25 additions & 0 deletions src/bitcoin/tx_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct TxBuilder {
drain_to: Option<ScriptBuf>,
allow_dust: bool,
ordering: TxOrdering,
min_confirmations: Option<u32>,
}

#[wasm_bindgen]
Expand All @@ -38,6 +39,7 @@ impl TxBuilder {
allow_dust: false,
drain_to: None,
ordering: BdkTxOrdering::default().into(),
min_confirmations: None,
}
}

Expand Down Expand Up @@ -102,6 +104,25 @@ impl TxBuilder {
self
}

/// Exclude outpoints whose enclosing transaction has fewer than `min_confirms`
/// confirmations.
///
/// - Passing `0` will include all transactions (no filtering).
/// - Passing `1` will exclude all unconfirmed transactions (equivalent to
/// [`exclude_unconfirmed`]).
/// - Passing `6` will only allow outpoints from transactions with at least 6 confirmations.
pub fn exclude_below_confirmations(mut self, min_confirms: u32) -> Self {
self.min_confirmations = Some(min_confirms);
self
}

/// Exclude outpoints whose enclosing transaction is unconfirmed.
///
/// This is a shorthand for [`exclude_below_confirmations(1)`](Self::exclude_below_confirmations).
pub fn exclude_unconfirmed(self) -> Self {
self.exclude_below_confirmations(1)
}

/// Set whether or not the dust limit is checked.
///
/// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard.
Expand Down Expand Up @@ -130,6 +151,10 @@ impl TxBuilder {
.fee_rate(self.fee_rate.into())
.allow_dust(self.allow_dust);

if let Some(min_confirms) = self.min_confirmations {
builder.exclude_below_confirmations(min_confirms);
}

if self.drain_wallet {
builder.drain_wallet();
}
Expand Down
29 changes: 28 additions & 1 deletion src/bitcoin/wallet.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::{cell::RefCell, rc::Rc};

use bdk_wallet::{SignOptions as BdkSignOptions, Wallet as BdkWallet};
#[allow(deprecated)]
use bdk_wallet::SignOptions as BdkSignOptions;
use bdk_wallet::Wallet as BdkWallet;
use wasm_bindgen::{prelude::wasm_bindgen, JsError};
use web_sys::js_sys::Date;

Expand Down Expand Up @@ -33,6 +35,22 @@ impl Wallet {
Ok(Wallet(Rc::new(RefCell::new(wallet))))
}

/// Create a new [`Wallet`] from a BIP-389 two-path multipath descriptor.
///
/// The descriptor must contain exactly two derivation paths (receive and change),
/// separated by a semicolon in angle brackets, e.g.:
/// `wpkh([fingerprint/path]xpub.../<0;1>/*)`
///
/// The first path is used for the external (receive) keychain and the second
/// for the internal (change) keychain.
pub fn create_from_two_path_descriptor(network: Network, descriptor: String) -> JsResult<Wallet> {
let wallet = BdkWallet::create_from_two_path_descriptor(descriptor)
.network(network.into())
.create_wallet_no_persist()?;

Ok(Wallet(Rc::new(RefCell::new(wallet))))
}

pub fn load(
changeset: ChangeSet,
external_descriptor: Option<String>,
Expand Down Expand Up @@ -201,9 +219,16 @@ impl Wallet {
}
}

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

#[allow(deprecated)]
#[wasm_bindgen]
impl SignOptions {
#[wasm_bindgen(constructor)]
Expand Down Expand Up @@ -272,12 +297,14 @@ impl SignOptions {
}
}

#[allow(deprecated)]
impl From<SignOptions> for BdkSignOptions {
fn from(options: SignOptions) -> Self {
options.0
}
}

#[allow(deprecated)]
impl Default for SignOptions {
fn default() -> Self {
Self::new()
Expand Down