diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9874fe7 --- /dev/null +++ b/CLAUDE.md @@ -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` and `Into` conversions +3. Exposes methods via `#[wasm_bindgen]` + +`Wallet` uses `Rc>` because `wasm_bindgen` doesn't support Rust lifetimes. +`TxBuilder` shares the wallet reference via `Rc>` 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 diff --git a/Cargo.toml b/Cargo.toml index 609e648..2b552b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = [ diff --git a/src/bitcoin/tx_builder.rs b/src/bitcoin/tx_builder.rs index e60852c..c7d29ea 100644 --- a/src/bitcoin/tx_builder.rs +++ b/src/bitcoin/tx_builder.rs @@ -23,6 +23,7 @@ pub struct TxBuilder { drain_to: Option, allow_dust: bool, ordering: TxOrdering, + min_confirmations: Option, } #[wasm_bindgen] @@ -38,6 +39,7 @@ impl TxBuilder { allow_dust: false, drain_to: None, ordering: BdkTxOrdering::default().into(), + min_confirmations: None, } } @@ -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. @@ -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(); } diff --git a/src/bitcoin/wallet.rs b/src/bitcoin/wallet.rs index 6c585da..9e26ec9 100644 --- a/src/bitcoin/wallet.rs +++ b/src/bitcoin/wallet.rs @@ -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; @@ -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 { + 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, @@ -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)] @@ -272,12 +297,14 @@ impl SignOptions { } } +#[allow(deprecated)] impl From for BdkSignOptions { fn from(options: SignOptions) -> Self { options.0 } } +#[allow(deprecated)] impl Default for SignOptions { fn default() -> Self { Self::new()