From 2b7ff6cdc0e8d429ccef711641e8f7bcad4d330a Mon Sep 17 00:00:00 2001 From: Dario Anongba Varela Date: Mon, 13 Apr 2026 16:08:06 +0000 Subject: [PATCH 1/2] docs: add browser, node, and nextjs examples --- CHANGELOG.md | 3 + README.md | 15 +++ examples/README.md | 35 ++++++ examples/browser-vite/README.md | 41 +++++++ examples/browser-vite/index.html | 12 ++ examples/browser-vite/src/main.js | 60 ++++++++++ examples/nextjs/README.md | 38 +++++++ .../nextjs/app/components/wallet-demo.tsx | 106 ++++++++++++++++++ examples/nextjs/app/page.tsx | 22 ++++ examples/node-wallet/README.md | 45 ++++++++ examples/node-wallet/index.mjs | 73 ++++++++++++ 11 files changed, 450 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/browser-vite/README.md create mode 100644 examples/browser-vite/index.html create mode 100644 examples/browser-vite/src/main.js create mode 100644 examples/nextjs/README.md create mode 100644 examples/nextjs/app/components/wallet-demo.tsx create mode 100644 examples/nextjs/app/page.tsx create mode 100644 examples/node-wallet/README.md create mode 100644 examples/node-wallet/index.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6061a93..ecce8c0 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; +}); From 730217b5e8c011070cef8115cd54631907f5a8d4 Mon Sep 17 00:00:00 2001 From: Dario Anongba Varela Date: Mon, 13 Apr 2026 16:14:16 +0000 Subject: [PATCH 2/2] ci: retry wasm-pack downloads in workflows --- .github/workflows/build-lint-test.yml | 68 +++++++++++++++++++++++++-- .github/workflows/publish-release.yml | 17 ++++++- 2 files changed, 80 insertions(+), 5 deletions(-) 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