diff --git a/.github/workflows/build-lint-test.yml b/.github/workflows/build-lint-test.yml
index b86d6d4..9f44ce6 100644
--- a/.github/workflows/build-lint-test.yml
+++ b/.github/workflows/build-lint-test.yml
@@ -23,7 +23,22 @@ jobs:
with:
toolchain: ${{ matrix.rust }}
- name: Install wasm-pack
- run: curl -sSfL https://github.com/rustwasm/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz | tar xz -C /usr/local/bin --strip-components=1 wasm-pack-v0.14.0-x86_64-unknown-linux-musl/wasm-pack
+ run: |
+ wasm_pack_url="https://github.com/rustwasm/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz"
+ archive="/tmp/wasm-pack.tar.gz"
+
+ for attempt in 1 2 3 4 5; do
+ if curl --fail --location --retry 5 --retry-all-errors --retry-delay 2 --retry-connrefused --output "$archive" "$wasm_pack_url"; then
+ tar xzf "$archive" -C /usr/local/bin --strip-components=1 wasm-pack-v0.14.0-x86_64-unknown-linux-musl/wasm-pack
+ exit 0
+ fi
+
+ echo "wasm-pack download failed on attempt $attempt/5"
+ sleep 5
+ done
+
+ echo "Failed to download wasm-pack after multiple attempts"
+ exit 1
- name: Rust Cache
uses: Swatinem/rust-cache@401aff9a7a08acb9d27b64936a90db81024cff97 # v2.8.2
- name: Build
@@ -45,7 +60,22 @@ jobs:
- name: Enable Corepack
run: corepack enable
- name: Install wasm-pack
- run: curl -sSfL https://github.com/rustwasm/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz | tar xz -C /usr/local/bin --strip-components=1 wasm-pack-v0.14.0-x86_64-unknown-linux-musl/wasm-pack
+ run: |
+ wasm_pack_url="https://github.com/rustwasm/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz"
+ archive="/tmp/wasm-pack.tar.gz"
+
+ for attempt in 1 2 3 4 5; do
+ if curl --fail --location --retry 5 --retry-all-errors --retry-delay 2 --retry-connrefused --output "$archive" "$wasm_pack_url"; then
+ tar xzf "$archive" -C /usr/local/bin --strip-components=1 wasm-pack-v0.14.0-x86_64-unknown-linux-musl/wasm-pack
+ exit 0
+ fi
+
+ echo "wasm-pack download failed on attempt $attempt/5"
+ sleep 5
+ done
+
+ echo "Failed to download wasm-pack after multiple attempts"
+ exit 1
- name: Setup Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
@@ -70,7 +100,22 @@ jobs:
- name: Enable Corepack
run: corepack enable
- name: Install wasm-pack
- run: curl -sSfL https://github.com/rustwasm/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz | tar xz -C /usr/local/bin --strip-components=1 wasm-pack-v0.14.0-x86_64-unknown-linux-musl/wasm-pack
+ run: |
+ wasm_pack_url="https://github.com/rustwasm/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz"
+ archive="/tmp/wasm-pack.tar.gz"
+
+ for attempt in 1 2 3 4 5; do
+ if curl --fail --location --retry 5 --retry-all-errors --retry-delay 2 --retry-connrefused --output "$archive" "$wasm_pack_url"; then
+ tar xzf "$archive" -C /usr/local/bin --strip-components=1 wasm-pack-v0.14.0-x86_64-unknown-linux-musl/wasm-pack
+ exit 0
+ fi
+
+ echo "wasm-pack download failed on attempt $attempt/5"
+ sleep 5
+ done
+
+ echo "Failed to download wasm-pack after multiple attempts"
+ exit 1
- name: Rust Cache
uses: Swatinem/rust-cache@401aff9a7a08acb9d27b64936a90db81024cff97 # v2.8.2
- name: Setup Node
@@ -120,7 +165,22 @@ jobs:
- name: Enable Corepack
run: corepack enable
- name: Install wasm-pack
- run: curl -sSfL https://github.com/rustwasm/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz | tar xz -C /usr/local/bin --strip-components=1 wasm-pack-v0.14.0-x86_64-unknown-linux-musl/wasm-pack
+ run: |
+ wasm_pack_url="https://github.com/rustwasm/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz"
+ archive="/tmp/wasm-pack.tar.gz"
+
+ for attempt in 1 2 3 4 5; do
+ if curl --fail --location --retry 5 --retry-all-errors --retry-delay 2 --retry-connrefused --output "$archive" "$wasm_pack_url"; then
+ tar xzf "$archive" -C /usr/local/bin --strip-components=1 wasm-pack-v0.14.0-x86_64-unknown-linux-musl/wasm-pack
+ exit 0
+ fi
+
+ echo "wasm-pack download failed on attempt $attempt/5"
+ sleep 5
+ done
+
+ echo "Failed to download wasm-pack after multiple attempts"
+ exit 1
- name: Rust Cache
uses: Swatinem/rust-cache@401aff9a7a08acb9d27b64936a90db81024cff97 # v2.8.2
- name: Setup Node
diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
index 4eec7b5..7dd7536 100644
--- a/.github/workflows/publish-release.yml
+++ b/.github/workflows/publish-release.yml
@@ -17,7 +17,22 @@ jobs:
with:
toolchain: stable
- name: Install wasm-pack
- run: curl -sSfL https://github.com/rustwasm/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz | tar xz -C /usr/local/bin --strip-components=1 wasm-pack-v0.14.0-x86_64-unknown-linux-musl/wasm-pack
+ run: |
+ wasm_pack_url="https://github.com/rustwasm/wasm-pack/releases/download/v0.14.0/wasm-pack-v0.14.0-x86_64-unknown-linux-musl.tar.gz"
+ archive="/tmp/wasm-pack.tar.gz"
+
+ for attempt in 1 2 3 4 5; do
+ if curl --fail --location --retry 5 --retry-all-errors --retry-delay 2 --retry-connrefused --output "$archive" "$wasm_pack_url"; then
+ tar xzf "$archive" -C /usr/local/bin --strip-components=1 wasm-pack-v0.14.0-x86_64-unknown-linux-musl/wasm-pack
+ exit 0
+ fi
+
+ echo "wasm-pack download failed on attempt $attempt/5"
+ sleep 5
+ done
+
+ echo "Failed to download wasm-pack after multiple attempts"
+ exit 1
- name: Install jq
run: sudo apt-get update -y && sudo apt-get install -y jq
- name: Rust Cache
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69964dd..80200ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- Add an `examples/` directory with browser (Vite), Node.js, and Next.js
+ tutorials, plus README links for quicker onboarding
+ ([#23](https://github.com/bitcoindevkit/bdk-wasm/issues/23))
- Expand Wallet API surface ([#21](https://github.com/bitcoindevkit/bdk-wasm/issues/21)):
- `Wallet::finalize_psbt` for finalizing PSBTs (adding finalized script/witness to inputs)
- `Wallet::cancel_tx` for releasing reserved change addresses when a transaction won't be broadcast
diff --git a/README.md b/README.md
index 00407bf..86d547d 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,21 @@ yarn add @bitcoindevkit/bdk-wallet-web
yarn add @bitcoindevkit/bdk-wallet-node
```
+## Examples and tutorials
+
+The repository now includes ready-to-copy example projects under
+[`examples/`](./examples/):
+
+- [`examples/browser-vite`](./examples/browser-vite) — vanilla JavaScript +
+ Vite browser example using `@bitcoindevkit/bdk-wallet-web`
+- [`examples/node-wallet`](./examples/node-wallet) — Node.js example that
+ creates a wallet, syncs with Esplora, signs a PSBT, and broadcasts a
+ self-send transaction
+- [`examples/nextjs`](./examples/nextjs) — Next.js client-side integration
+ example for `@bitcoindevkit/bdk-wallet-web`
+- [`examples/README.md`](./examples/README.md) — overview, safety notes, and
+ when to use each example
+
## Notes on WASM Specific Considerations
> [!WARNING]
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..f6157fa
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,35 @@
+# Examples
+
+This directory contains small, focused examples for the published
+`bdk-wasm` JavaScript packages.
+
+## Included examples
+
+- [`browser-vite`](./browser-vite) — vanilla JavaScript + Vite in the browser
+- [`node-wallet`](./node-wallet) — Node.js + Esplora full scan, PSBT signing,
+ and transaction broadcast
+- [`nextjs`](./nextjs) — client-side loading pattern for Next.js / React apps
+
+## Safety note
+
+The browser and Next.js examples embed throwaway demo descriptors so the code
+works out of the box. Those descriptors are for documentation only.
+
+Do **not** ship private descriptors, seeds, or xprvs inside browser bundles or
+React apps in production. For production:
+
+- use public descriptors client-side when possible
+- keep signing in a secure backend or hardware signer flow
+- persist wallet state outside the WASM module using the exported `ChangeSet`
+
+## Picking the right example
+
+- Start with [`browser-vite`](./browser-vite) if you want the smallest browser
+ setup and only need local wallet operations.
+- Start with [`node-wallet`](./node-wallet) if you want a scriptable backend,
+ job worker, or service that talks to Esplora.
+- Start with [`nextjs`](./nextjs) if you are integrating `bdk-wasm` into a
+ React application with server-side rendering in the stack.
+
+Each example README lists the exact commands needed to bootstrap a fresh app
+and copy the sample files over.
diff --git a/examples/browser-vite/README.md b/examples/browser-vite/README.md
new file mode 100644
index 0000000..e4ebc33
--- /dev/null
+++ b/examples/browser-vite/README.md
@@ -0,0 +1,41 @@
+# Browser example with Vite
+
+This example shows the smallest browser setup for
+`@bitcoindevkit/bdk-wallet-web` using vanilla JavaScript and Vite.
+
+It creates a demo signet wallet in the browser and renders:
+
+- the network
+- the first derived address
+- the next revealed address
+- the public external descriptor
+
+## 1. Create a fresh Vite app
+
+```sh
+npm create vite@latest browser-vite -- --template vanilla
+cd browser-vite
+npm install
+npm install @bitcoindevkit/bdk-wallet-web
+```
+
+## 2. Replace the generated files
+
+Copy the sample files from this directory into the fresh Vite app:
+
+- `index.html`
+- `src/main.js`
+
+## 3. Start the dev server
+
+```sh
+npm run dev
+```
+
+## Notes
+
+- The descriptors in `src/main.js` are throwaway demo descriptors copied from
+ the repository test fixtures.
+- This example intentionally avoids syncing against Esplora so it stays focused
+ on local wallet initialization in a browser context.
+- In production, never embed real xprvs or seed material in client-side code.
diff --git a/examples/browser-vite/index.html b/examples/browser-vite/index.html
new file mode 100644
index 0000000..18ade10
--- /dev/null
+++ b/examples/browser-vite/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ bdk-wasm Vite example
+
+
+
+
+
+
diff --git a/examples/browser-vite/src/main.js b/examples/browser-vite/src/main.js
new file mode 100644
index 0000000..936e347
--- /dev/null
+++ b/examples/browser-vite/src/main.js
@@ -0,0 +1,60 @@
+import init, { Wallet } from "@bitcoindevkit/bdk-wallet-web";
+
+const network = "signet";
+
+const demoDescriptors = {
+ external:
+ "wpkh(tprv8ZgxMBicQKsPd5puBG1xsJ5V53vVPfCy2gnZfsqzmDSDjaQx8LEW4REFvrj6PQMuer7NqZeBiy9iP9ucqJZiveeEGqQ5CvcfV6SPcy8LQR7/84'/1'/0'/0/*)#jjcsy5wd",
+ internal:
+ "wpkh(tprv8ZgxMBicQKsPd5puBG1xsJ5V53vVPfCy2gnZfsqzmDSDjaQx8LEW4REFvrj6PQMuer7NqZeBiy9iP9ucqJZiveeEGqQ5CvcfV6SPcy8LQR7/84'/1'/0'/1/*)#rxa3ep74",
+};
+
+document.querySelector("#app").innerHTML = `
+
+ bdk-wasm browser example
+
+ This page loads @bitcoindevkit/bdk-wallet-web, creates a
+ demo signet wallet, and derives a couple of addresses in the browser.
+
+
+ - Network
+ - Loading...
+
+ - First external address
+ - Loading...
+
+ - Next revealed external address
+ - Loading...
+
+ - Public external descriptor
+ Loading...
+
+
+
+
+`;
+
+async function main() {
+ await init();
+
+ const wallet = Wallet.create(
+ network,
+ demoDescriptors.external,
+ demoDescriptors.internal
+ );
+
+ const firstAddress = wallet.peek_address("external", 0).address.toString();
+ const nextAddress = wallet.reveal_next_address("external").address.toString();
+
+ document.querySelector("#network").textContent = wallet.network;
+ document.querySelector("#first-address").textContent = firstAddress;
+ document.querySelector("#next-address").textContent = nextAddress;
+ document.querySelector("#descriptor").textContent =
+ wallet.public_descriptor("external");
+}
+
+main().catch((error) => {
+ console.error(error);
+ document.querySelector("#error").textContent =
+ error instanceof Error ? error.message : String(error);
+});
diff --git a/examples/nextjs/README.md b/examples/nextjs/README.md
new file mode 100644
index 0000000..b7a6200
--- /dev/null
+++ b/examples/nextjs/README.md
@@ -0,0 +1,38 @@
+# Next.js example
+
+This example shows the recommended loading pattern for
+`@bitcoindevkit/bdk-wallet-web` inside a Next.js app:
+
+- keep the page itself server-rendered
+- load the WASM package only inside a client component
+- initialize the module inside `useEffect`
+
+## 1. Create a new app
+
+```sh
+npx create-next-app@latest nextjs-bdk-demo --ts --app
+cd nextjs-bdk-demo
+npm install @bitcoindevkit/bdk-wallet-web
+```
+
+## 2. Copy the sample files
+
+Copy these files into the generated project:
+
+- `app/page.tsx`
+- `app/components/wallet-demo.tsx`
+
+## 3. Start the app
+
+```sh
+npm run dev
+```
+
+## Why this pattern matters
+
+`@bitcoindevkit/bdk-wallet-web` is a browser-side WASM package. Importing it at
+module scope in a server component can break SSR builds. Dynamic importing it
+inside a `"use client"` component keeps the boundary explicit and reliable.
+
+The sample uses demo signet descriptors for illustration only. Replace them
+with your own safe integration strategy before shipping anything real.
diff --git a/examples/nextjs/app/components/wallet-demo.tsx b/examples/nextjs/app/components/wallet-demo.tsx
new file mode 100644
index 0000000..ec519ca
--- /dev/null
+++ b/examples/nextjs/app/components/wallet-demo.tsx
@@ -0,0 +1,106 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+const demoDescriptors = {
+ external:
+ "wpkh(tprv8ZgxMBicQKsPd5puBG1xsJ5V53vVPfCy2gnZfsqzmDSDjaQx8LEW4REFvrj6PQMuer7NqZeBiy9iP9ucqJZiveeEGqQ5CvcfV6SPcy8LQR7/84'/1'/0'/0/*)#jjcsy5wd",
+ internal:
+ "wpkh(tprv8ZgxMBicQKsPd5puBG1xsJ5V53vVPfCy2gnZfsqzmDSDjaQx8LEW4REFvrj6PQMuer7NqZeBiy9iP9ucqJZiveeEGqQ5CvcfV6SPcy8LQR7/84'/1'/0'/1/*)#rxa3ep74",
+};
+
+type WalletState = {
+ network: string;
+ firstAddress: string;
+ publicDescriptor: string;
+};
+
+export function WalletDemo() {
+ const [walletState, setWalletState] = useState(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ async function loadWallet() {
+ try {
+ const { default: init, Wallet } = await import(
+ "@bitcoindevkit/bdk-wallet-web"
+ );
+
+ await init();
+
+ const wallet = Wallet.create(
+ "signet",
+ demoDescriptors.external,
+ demoDescriptors.internal
+ );
+
+ if (cancelled) {
+ return;
+ }
+
+ setWalletState({
+ network: wallet.network,
+ firstAddress: wallet.peek_address("external", 0).address.toString(),
+ publicDescriptor: wallet.public_descriptor("external"),
+ });
+ } catch (err) {
+ if (!cancelled) {
+ setError(err instanceof Error ? err.message : String(err));
+ }
+ }
+ }
+
+ loadWallet();
+
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ if (error) {
+ return (
+
+ Failed to load bdk-wasm: {error}
+
+ );
+ }
+
+ if (!walletState) {
+ return Loading wallet module...
;
+ }
+
+ return (
+
+ Wallet details
+
+ Network: {walletState.network}
+
+
+ First external address: {walletState.firstAddress}
+
+
+ Public descriptor:
+
+
+ {walletState.publicDescriptor}
+
+
+ );
+}
diff --git a/examples/nextjs/app/page.tsx b/examples/nextjs/app/page.tsx
new file mode 100644
index 0000000..350287d
--- /dev/null
+++ b/examples/nextjs/app/page.tsx
@@ -0,0 +1,22 @@
+import { WalletDemo } from "./components/wallet-demo";
+
+export default function Home() {
+ return (
+
+ bdk-wasm Next.js example
+
+ This page stays server-rendered while the wallet module loads only in a
+ client component.
+
+
+
+ );
+}
diff --git a/examples/node-wallet/README.md b/examples/node-wallet/README.md
new file mode 100644
index 0000000..d27b6ff
--- /dev/null
+++ b/examples/node-wallet/README.md
@@ -0,0 +1,45 @@
+# Node.js wallet example
+
+This example uses `@bitcoindevkit/bdk-wallet-node` to:
+
+1. create a wallet from descriptors
+2. sync it with Esplora
+3. build and sign a PSBT
+4. broadcast a self-send transaction
+
+## 1. Create a new directory
+
+```sh
+mkdir node-wallet
+cd node-wallet
+npm init -y
+npm install @bitcoindevkit/bdk-wallet-node
+```
+
+## 2. Copy the sample script
+
+Copy `index.mjs` from this directory into your new project.
+
+## 3. Fund the wallet
+
+The script defaults to the same demo signet descriptors used in the repository
+tests. Before it can broadcast a transaction, fund the first derived address on
+signet or point it at your own descriptors via environment variables.
+
+Useful environment variables:
+
+- `NETWORK` — defaults to `signet`
+- `ESPLORA_URL` — defaults to `https://mutinynet.com/api`
+- `EXTERNAL_DESCRIPTOR`
+- `INTERNAL_DESCRIPTOR`
+- `SEND_SATS` — defaults to `1000`
+- `FEE_RATE_SAT_VB` — defaults to `1`
+
+## 4. Run it
+
+```sh
+node index.mjs
+```
+
+The example self-sends back into the same wallet, so you do not need a second
+recipient address.
diff --git a/examples/node-wallet/index.mjs b/examples/node-wallet/index.mjs
new file mode 100644
index 0000000..8181552
--- /dev/null
+++ b/examples/node-wallet/index.mjs
@@ -0,0 +1,73 @@
+import {
+ Amount,
+ EsploraClient,
+ FeeRate,
+ Recipient,
+ SignOptions,
+ Wallet,
+} from "@bitcoindevkit/bdk-wallet-node";
+
+const network = process.env.NETWORK ?? "signet";
+const esploraUrl = process.env.ESPLORA_URL ?? "https://mutinynet.com/api";
+const sendSats = BigInt(process.env.SEND_SATS ?? "1000");
+const feeRateSatVb = BigInt(process.env.FEE_RATE_SAT_VB ?? "1");
+const stopGap = Number(process.env.STOP_GAP ?? "20");
+const parallelRequests = Number(process.env.PARALLEL_REQUESTS ?? "5");
+
+const externalDescriptor =
+ process.env.EXTERNAL_DESCRIPTOR ??
+ "wpkh(tprv8ZgxMBicQKsPd5puBG1xsJ5V53vVPfCy2gnZfsqzmDSDjaQx8LEW4REFvrj6PQMuer7NqZeBiy9iP9ucqJZiveeEGqQ5CvcfV6SPcy8LQR7/84'/1'/0'/0/*)#jjcsy5wd";
+const internalDescriptor =
+ process.env.INTERNAL_DESCRIPTOR ??
+ "wpkh(tprv8ZgxMBicQKsPd5puBG1xsJ5V53vVPfCy2gnZfsqzmDSDjaQx8LEW4REFvrj6PQMuer7NqZeBiy9iP9ucqJZiveeEGqQ5CvcfV6SPcy8LQR7/84'/1'/0'/1/*)#rxa3ep74";
+
+async function main() {
+ const wallet = Wallet.create(network, externalDescriptor, internalDescriptor);
+ const client = new EsploraClient(esploraUrl, 0);
+
+ const fundingAddress = wallet.peek_address("external", 0).address.toString();
+ console.log(`Network: ${wallet.network}`);
+ console.log(`Fund this address first: ${fundingAddress}`);
+
+ const update = await client.full_scan(
+ wallet.start_full_scan(),
+ stopGap,
+ parallelRequests
+ );
+ wallet.apply_update(update);
+
+ const spendable = wallet.balance.trusted_spendable.to_sat();
+ console.log(`Spendable balance: ${spendable.toString()} sats`);
+
+ if (spendable <= sendSats) {
+ throw new Error(
+ `Wallet needs more than ${sendSats.toString()} sats before it can self-send.`
+ );
+ }
+
+ const recipient = wallet.peek_address("external", 5);
+ const psbt = wallet
+ .build_tx()
+ .fee_rate(new FeeRate(feeRateSatVb))
+ .add_recipient(
+ new Recipient(recipient.address.script_pubkey, Amount.from_sat(sendSats))
+ )
+ .finish();
+
+ const signOptions = new SignOptions();
+ const finalized = wallet.sign(psbt, signOptions);
+
+ if (!finalized) {
+ throw new Error("wallet.sign() did not finalize the PSBT");
+ }
+
+ const tx = psbt.extract_tx();
+ await client.broadcast(tx);
+
+ console.log(`Broadcast txid: ${tx.compute_txid().toString()}`);
+}
+
+main().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});