diff --git a/SKILL.md b/SKILL.md index 8702191..c0a98a7 100644 --- a/SKILL.md +++ b/SKILL.md @@ -17,6 +17,78 @@ Use this skill to understand how to build apps that require bitcoin lightning wa - [Lightning Tools: Request invoices from a lightning address, parse BOLT-11 invoices, verify a preimage for a BOLT-11 invoice, LNURL-Verify, do bitcoin <-> fiat conversions](./references/lightning-tools/lightning-tools.md) - [Bitcoin Connect: Browser-only UI components for connecting wallets and accepting payments in React, Vue, or pure HTML web apps](./references/bitcoin-connect/bitcoin-connect.md) +## Which library to use + +| Scenario | Library | Runtime | +|---|---|---| +| Backend / server-side / console app wallet operations (send, receive, balance, invoices, notifications) | NWC Client (`@getalby/sdk`) | Node.js, Deno, Bun, Browser | +| Browser / frontend wallet connection UI and payment modals | Bitcoin Connect (`@getalby/bitcoin-connect`) | Browser only | +| Utility: parse invoices, lightning address lookups, fiat conversion, LNURL | Lightning Tools (`@getalby/lightning-tools`) | Node.js, Deno, Bun, Browser | +| Backend + Frontend in the same app | NWC Client (backend) + Bitcoin Connect (frontend) | Both | + +- **Do NOT use Bitcoin Connect in Node.js / server-side environments** — it requires a browser DOM. +- **Do NOT use NWC Client in the frontend if the goal is wallet connection UI** — use Bitcoin Connect instead, which provides the UI and manages the NWC connection for you. +- NWC Client and Lightning Tools can be freely combined in any environment. + +## ⚠️ Unit Warning + +NWC Client operates in **millisats** (1 sat = 1,000 millisats). +Lightning Tools and Bitcoin Connect/WebLN operate in **sats**. + +When combining libraries, always convert: +- NWC millisats → sats: `Math.floor(millisats / 1000)` +- sats → NWC millisats: `sats * 1000` + +## Node.js Project Setup + +All packages in this skill are **ESM-only**. When creating a new Node.js project: + +1. Set `"type": "module"` in `package.json` +2. For TypeScript, use the following minimal `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "esModuleInterop": true, + "outDir": "dist", + "strict": true + } +} +``` + +3. Install dependencies based on what you need: + +```bash +# NWC Client (wallet operations) +npm install @getalby/sdk + +# Lightning Tools (invoices, lightning addresses, fiat conversion) +npm install @getalby/lightning-tools + +# Both (common for backend apps) +npm install @getalby/sdk @getalby/lightning-tools + +# Bitcoin Connect (browser only — do NOT install for Node.js-only projects) +npm install @getalby/bitcoin-connect +# or for React specifically: +npm install @getalby/bitcoin-connect-react +``` + +4. If using TypeScript with Bitcoin Connect, also install WebLN types: + +```bash +npm install -D @webbtc/webln-types +``` + +Then create a `webln-types.d.ts` file: + +```ts +/// +``` + ## Prefer Typescript When the user says to use "JS" or "Javascript" or "NodeJS" or something similar, use typescript unless the user explicitly says to not use typescript or the project does not support it. @@ -44,6 +116,46 @@ Testing wallets should be used for [automated testing](./references/automated-te It is recommended to write tests so that the agent can test its own work and fix bugs itself without requiring human input. +## Cross-Library Recipe + +When combining NWC Client and Lightning Tools (e.g. fiat conversion + invoicing + forwarding payments), see the [cross-library recipe](./references/cross-library-recipe.md) for a full end-to-end example with proper unit conversion. + ## Production Wallet -If they do not have a wallet yet [here are some options](./references/production-wallets.md) \ No newline at end of file +If they do not have a wallet yet [here are some options](./references/production-wallets.md) + +## Quickstart Decision Guide + +- **Backend wallet ops (send/receive/balance/notifications)** → Use `NWCClient` (`@getalby/sdk`). Combine with Lightning Tools for fiat conversion and invoice parsing. Units: msats. +- **Browser wallet connection + payment UI** → Use Bitcoin Connect (`@getalby/bitcoin-connect` or `-react`). Units: sats. SSR frameworks must gate imports to the client. +- **Full-stack app** → Backend: `NWCClient` for wallet ops. Frontend: Bitcoin Connect for connect/pay UI. Shared utils: Lightning Tools for fiat, LNURL, invoice parsing. +- **Lightning address pay/receive utilities** → Use Lightning Tools `lnurl` APIs (sats). For payment, either WebLN (browser) or `NWCClient.payInvoice` (backend). +- **Fiat pricing** → Lightning Tools `fiat` APIs to convert fiat↔sats; multiply/divide by 1000 when handing amounts to/from `NWCClient`. +- **Zaps (Nostr-tied payments)** → Lightning Tools zap helpers + WebLN provider (browser) or `NWCClient.payInvoice` (backend). +- **L402 client** → Browser: `fetchWithL402` + Bitcoin Connect provider. Node: `fetchWithL402` + `NostrWebLNProvider` from `@getalby/sdk/webln`. +- **L402 server** → `NWCClient.makeInvoice` + macaroon verification (see `lightning-tools/l402.md`). +- **Testing** → Always prefer [testing wallets](./references/testing-wallets.md) and wire them into automated tests (Jest/Vitest/Playwright) per [automated testing](./references/automated-testing.md). + +## NWC Secret Handling (Security) + +- Treat `nostrWalletConnectUrl` as a secret API key. Never log, print, or expose it. +- Backend: keep in environment variables (e.g., `NWC_URL`), never commit to source control, and redact in error messages. +- Browser: request from user input; keep only in memory unless the user explicitly opts into persistence. Do not bake into bundles or HTML. +- When wrapping errors, strip or mask the connection URL before surfacing to logs/telemetry. + +## Invoice Safety & Common Pitfalls + +- Always decode and check expiry before paying a BOLT-11 invoice; warn if expiry is under ~60 seconds. +- Units: `NWCClient` = **msats**; Lightning Tools/Bitcoin Connect/WebLN = **sats**. Convert carefully when mixing. +- Do not import Lightning Tools from the package root; always use subpath imports (e.g., `@getalby/lightning-tools/fiat`). +- Bitcoin Connect requires a browser DOM; never import it in SSR server code. Call `init()` exactly once on the client. +- Close resources: `unsub()` notifications and `client.close()` when shutting down long-lived processes. +- Handle permission/budget errors: on `QUOTA_EXCEEDED` or `RESTRICTED`, prompt for a new or expanded connection; on `RATE_LIMITED`, back off and retry later. + +## Recipe Pointers + +- End-to-end msats↔sats with invoicing and forwarding: [cross-library recipe](./references/cross-library-recipe.md). +- L402 client/server patterns: [L402 guide](./references/lightning-tools/l402.md). +- LNURL-pay, comments, payer data, and verify: [lnurl guide](./references/lightning-tools/lnurl.md). +- Invoice parsing/expiry/preimage verification: [invoice guide](./references/lightning-tools/invoice.md). +- Automated wallet creation for tests: [testing wallets](./references/testing-wallets.md) and [automated testing](./references/automated-testing.md). \ No newline at end of file diff --git a/references/automated-testing.md b/references/automated-testing.md index 52a9df7..d0191fb 100644 --- a/references/automated-testing.md +++ b/references/automated-testing.md @@ -10,31 +10,49 @@ Each test can create brand new wallet(s) as required to ensure reproducable resu ### Code Example -The below example allows for temporary networking errors. +The below example allows for temporary networking errors and is reusable as a fixture helper. ```ts -async function createTestWallet(retries = 3): Promise<{ nwcUrl: string; lightningAddress: string }> { +type TestWallet = { nwcUrl: string; lightningAddress: string }; + +async function createTestWallet({ + retries = 4, + delayMs = 500, + balance = 10000, +}: { + retries?: number; + delayMs?: number; + balance?: number; +} = {}): Promise { + let lastError: Error | undefined; for (let i = 0; i < retries; i++) { - const response = await fetch("https://faucet.nwc.dev?balance=10000", { method: "POST" }); - if (!response.ok) { + try { + const response = await fetch(`https://faucet.nwc.dev?balance=${balance}`, { method: "POST" }); + if (!response.ok) { + throw new Error(`Faucet request failed: ${response.status} ${await response.text()}`); + } + const nwcUrl = (await response.text()).trim(); + const lud16Match = nwcUrl.match(/lud16=([^&\s]+)/); + if (!lud16Match) { + throw new Error(`No lud16 found in NWC URL: ${nwcUrl}`); + } + const lightningAddress = decodeURIComponent(lud16Match[1]); + return { nwcUrl, lightningAddress }; + } catch (error) { + lastError = error as Error; if (i < retries - 1) { - await new Promise((r) => setTimeout(r, 1000)); - continue; + await new Promise((r) => setTimeout(r, delayMs * (i + 1))); } - throw new Error(`Faucet request failed: ${response.status} ${await response.text()}`); - } - const nwcUrl = (await response.text()).trim(); - const lud16Match = nwcUrl.match(/lud16=([^&\s]+)/); - if (!lud16Match) { - throw new Error(`No lud16 found in NWC URL: ${nwcUrl}`); } - const lightningAddress = decodeURIComponent(lud16Match[1]); - return { nwcUrl, lightningAddress }; } - throw new Error("Failed to create test wallet after retries"); + throw lastError ?? new Error("Failed to create test wallet after retries"); } ``` -## What to test +#### Fixture patterns (Jest/Vitest/Playwright) -Test both happy path and failure cases (payment failed with insufficient balance etc.) +- Create a fresh wallet per test (or per suite) to avoid state bleed and flakiness. +- Expose `nwcUrl` via env or in-memory fixtures; never log or print the secret. +- In Playwright E2E, pass `nwcUrl` to the app via query param or `page.evaluate` to set it in localStorage/sessionStorage as a setup step. +- Add a top-up helper for balance-sensitive flows: `POST https://faucet.nwc.dev/wallets//topup?amount=...`. +- Cover failure cases with real flows (insufficient balance, expired invoice, rate limit) and not just mocks. diff --git a/references/bitcoin-connect/bitcoin-connect.md b/references/bitcoin-connect/bitcoin-connect.md index 5da3580..3146f04 100644 --- a/references/bitcoin-connect/bitcoin-connect.md +++ b/references/bitcoin-connect/bitcoin-connect.md @@ -35,6 +35,76 @@ or ``` +## ⚠️ SSR / SSG Warning (Next.js, Nuxt, SvelteKit, Remix, Astro) + +Bitcoin Connect requires a browser DOM and **will crash if imported on the server**. In frameworks that do server-side rendering or static site generation, you MUST use dynamic imports or client-only wrappers. Never import `@getalby/bitcoin-connect` or `@getalby/bitcoin-connect-react` in server code or shared modules that execute during SSR — gate imports to the client and dynamically load components. + +### Next.js (App Router) + +Mark the component as client-only and use dynamic import: + +```tsx +"use client"; + +import dynamic from "next/dynamic"; +import { useEffect } from "react"; + +// Dynamic import — prevents server-side import of bitcoin-connect +const BitcoinConnectButton = dynamic( + () => import("@getalby/bitcoin-connect-react").then((mod) => { + // init() must be called once before using components + mod.init({ appName: "My App" }); + return { default: mod.Button }; + }), + { ssr: false } +); + +export default function WalletButton() { + return ; +} +``` + +### Next.js (Pages Router) + +```tsx +import dynamic from "next/dynamic"; + +const WalletButton = dynamic( + () => import("../components/WalletButton"), + { ssr: false } +); +``` + +### Nuxt 3 + +```vue + +``` + +### SvelteKit + +```svelte + +``` + +### General Rule + +If using any SSR framework, **never import `@getalby/bitcoin-connect` or `@getalby/bitcoin-connect-react` at the top level of a server-rendered file**. Always gate the import behind a browser/client check or dynamic import. + ## Key concepts - Web components for connecting Lightning wallets and enabling WebLN @@ -49,6 +119,15 @@ Unlike NWC, WebLN operates on sats, not millisats. (1000 millisats = 1 satoshi) ## Initialization +Call `init()` **once** when your app starts. Do NOT call it multiple times or conditionally inside render loops. If you have multiple entry points/components, centralize `init()` to a single client-only location to avoid duplicate initialization. + +**Where to call `init()`:** + +- **React (Vite / CRA):** in `main.tsx` or `App.tsx`, outside any component, at the top level +- **React (Next.js):** inside the dynamically imported client component (see SSR warning above) +- **Vue:** in `main.ts` before `createApp()` +- **Plain HTML:** in a ` +``` + +### Browser: Using NWC Client with Bitcoin Connect + +If the user connects their wallet via [Bitcoin Connect](../bitcoin-connect/bitcoin-connect.md), you can access the underlying NWC Client for advanced operations (e.g. notifications, hold invoices) that aren't available through WebLN alone: + +```ts +import { WebLNProviders, requestProvider } from "@getalby/bitcoin-connect"; +import { NWCClient } from "@getalby/sdk/nwc"; + +const provider = await requestProvider(); + +if (provider instanceof WebLNProviders.NostrWebLNProvider) { + // Get the NWC connection URL from the connected provider + const nwcUrl = provider.client.nostrWalletConnectUrl; + + // Create a dedicated NWCClient for advanced operations + const client = new NWCClient({ nostrWalletConnectUrl: nwcUrl }); + + // Now you can use notifications, hold invoices, etc. + const unsub = await client.subscribeNotifications((notification) => { + console.log("Payment notification:", notification); + }); +} +``` + ## Referenced files Make sure to read the [NWC Client typings](./nwc.d.ts) when using any of the below referenced files. +- [Common operations: getBalance, makeInvoice, getInfo, listTransactions, lookupInvoice, getBudget, signMessage, multiPayInvoice](./common-operations.md) - [subscribe to notifications of sent or received payments](./notifications.md) - [How to pay a BOLT-11 lightning invoice](pay-invoice.md) - [How to create, settle and cancel HOLD invoices for conditional payments](hold-invoices.md) +- [Error handling: error types, wallet error codes, and retry patterns](./error-handling.md) + +## Cleanup + +### Node.js + +Always close the client when your application exits to avoid leaked WebSocket connections: + +```ts +process.on("SIGINT", () => { + client.close(); + process.exit(); +}); +``` + +If you are using `subscribeNotifications`, unsubscribe before closing: + +```ts +const unsub = await client.subscribeNotifications(onNotification); + +// later, when shutting down: +unsub(); +client.close(); +``` + +### Browser + +In the browser, clean up on page unload or when the component unmounts: + +```ts +// On page unload +window.addEventListener("beforeunload", () => { + unsub?.(); + client.close(); +}); +``` + +In React, clean up in a `useEffect` return: + +```tsx +useEffect(() => { + const client = new NWCClient({ nostrWalletConnectUrl: nwcUrl }); + let unsub: (() => void) | undefined; + + client.subscribeNotifications((notification) => { + console.log("Notification:", notification); + }).then((unsubFn) => { + unsub = unsubFn; + }); + + return () => { + unsub?.(); + client.close(); + }; +}, [nwcUrl]); +``` + +### Long-Lived Node.js Processes + +When subscribing to notifications in a Node.js script, the process must stay alive for the subscription to work. The WebSocket connection kept open by `subscribeNotifications` will keep the Node.js event loop running automatically — no extra keep-alive code is needed. The process will stay alive as long as the subscription is active. Call `unsub()` and `client.close()` when you want the process to exit. + +## Advanced: Creating new connections and NWA/NWCWalletService + +To mint new app connections programmatically, use the authorization helpers: + +- `NWCClient.getAuthorizationUrl(basePath, options, pubkey)` to build a deeplink/QR for a wallet UI that will provision a new connection with requested methods, budget, expiry, isolated flag, etc. +- `NWCClient.fromAuthorizationUrl(basePath, options?, secret?)` to generate and return a ready `NWCClient` plus the full `nostrWalletConnectUrl` you should persist (store securely, never log). Show the resulting URL to the user (deeplink or QR) so they can approve it. +- `client.createConnection({ pubkey, name, request_methods, ... })` to request a scoped connection from within an existing session. + +When you receive the resulting `nostrWalletConnectUrl`, persist it securely (env var on backend; user-controlled persistence on frontend) and never print or log it. + +The typings also export `NWAClient` (Nostr Wallet Auth — wallet-initiated connections) and `NWCWalletService` (build a wallet provider). These are **advanced** and should only be used when you intend to: +- Build a wallet service/provider (use `NWCWalletService`). +- Implement wallet-initiated auth flows (use `NWAClient`). + +For typical application development (sending/receiving payments, checking balances, notifications, budgets), use `NWCClient` as documented above. \ No newline at end of file