Files
client/plans/cashu-wallet-plan.md
2026-04-17 16:52:51 -04:00

9.5 KiB

Cashu Wallet (NIP-60) — Implementation Plan

Overview

Implement a Cashu wallet page (www/cashu.html) following the NIP-60 specification. The page will allow users to manage their ecash wallet stored on Nostr relays — view balance, receive tokens, send tokens, deposit via Lightning, and view transaction history.

Key Discovery: NDK Bundle Already Includes Everything

The ndk-core.bundle.js already bundles:

  • @cashu/cashu-ts — Full Cashu protocol library (mint interactions, token encoding/decoding, proof management)
  • NDKCashuWallet — High-level NIP-60 wallet class with create(), deposit(), send(), receive(), lnPay(), balance tracking, state management
  • NDKCashuToken (kind 7375) — Token event class with proof decryption
  • NDKCashuWalletTx (kind 7376) — Transaction history event class
  • NDKCashuMintList (kind 10019) — Mint list for nutzap reception
  • NDKCashuDeposit — Lightning deposit flow with quote management
  • NDKCashuWalletBackup (kind 375) — Wallet backup/restore
  • NDKWalletStatus — Status enum (INITIAL, LOADING, READY, FAILED)

The nostr.bundle.js includes NIP-44 encryption support, and init-ndk.mjs already handles NIP-44 encrypt/decrypt sign requests from the worker.

Architecture

graph TD
    A[cashu.html] --> B[init-ndk.mjs]
    B --> C[NDK SharedWorker]
    C --> D[Nostr Relays]
    A --> E[cashu-wallet.mjs - new module]
    E --> F[NDK.NDKCashuWallet]
    F --> G[cashu-ts CashuWallet]
    G --> H[Cashu Mint HTTP API]
    F --> D
    E --> I[window.nostr.nip44 - encryption]

File Structure

  • www/cashu.html — Page HTML + page-specific <style> + inline <script type=module>
  • www/js/cashu-wallet.mjs — Cashu wallet logic module (keeps cashu.html clean)

The module approach follows the pattern used by post-interactions.mjs and strudel-nostr.mjs.

UI Layout

The page uses a single-column centered layout (like post.html) with these sections:

┌─────────────────────────────────────┐
│  HEADER (hamburger + title)         │
├─────────────────────────────────────┤
│                                     │
│  ┌─────────────────────────────┐    │
│  │  BALANCE CARD               │    │
│  │  ₿ 1,234 sats              │    │
│  │  [mint1] 800  [mint2] 434   │    │
│  └─────────────────────────────┘    │
│                                     │
│  ┌──────┐ ┌──────┐ ┌──────┐        │
│  │RECEIVE│ │ SEND │ │DEPOSIT│       │
│  └──────┘ └──────┘ └──────┘        │
│                                     │
│  ┌─────────────────────────────┐    │
│  │  ACTION PANEL               │    │
│  │  (changes based on button)  │    │
│  │                             │    │
│  │  Receive: paste token input │    │
│  │  Send: amount + generate    │    │
│  │  Deposit: amount + LN inv   │    │
│  └─────────────────────────────┘    │
│                                     │
│  ┌─────────────────────────────┐    │
│  │  TRANSACTION HISTORY        │    │
│  │  ↓ 100 sats  2h ago        │    │
│  │  ↑  50 sats  5h ago        │    │
│  │  ...                        │    │
│  └─────────────────────────────┘    │
│                                     │
│  ┌─────────────────────────────┐    │
│  │  SETTINGS (collapsible)     │    │
│  │  Mints: [list + add/remove] │    │
│  └─────────────────────────────┘    │
│                                     │
├─────────────────────────────────────┤
│  FOOTER (relay status)              │
└─────────────────────────────────────┘

CSS Strategy

Use existing CSS variables and patterns from client.css:

  • var(--primary-color), var(--secondary-color), var(--accent-color), var(--muted-color)
  • var(--border-width), var(--border-radius), var(--border-color)
  • var(--font-family), var(--font-mono)
  • var(--button-color), var(--button-background-color), var(--button-hover-color)
  • Button styling pattern from .btnKind in event.html
  • Card/pane styling pattern from msg.html (border + border-radius + background)
  • #divBody override pattern: flex-direction: column !important etc.
  • .clsHidden / .clsVisible for show/hide

All page-specific styles go in <style> block inside cashu.html, never in client.css.

Implementation Steps

Phase 1: Page Setup & Wallet Initialization

  1. Update cashu.html page structure — Change title, add <style> block with #divBody column layout override, add HTML skeleton for all UI sections inside #divBody

  2. Create www/js/cashu-wallet.mjs — Module that:

    • Imports NDK cashu classes from window.NDK (the bundle exposes them globally)
    • Provides initWallet(ndk, pubkey) — loads or creates NIP-60 wallet
    • Provides getBalance(), getBalanceByMint() — balance queries
    • Provides receiveToken(encodedToken) — receive cashu token
    • Provides sendToken(amount, mint) — create sendable cashu token
    • Provides createDeposit(amount, mint) — Lightning deposit flow
    • Provides getTransactionHistory() — fetch kind 7376 events
    • Provides getMints(), addMint(url), removeMint(url) — mint management
  3. Wire up initialization in cashu.html — After initNDKPage() completes, call initWallet() and render initial state

Phase 2: Balance Display

  1. Render balance card — Show total balance in sats, with per-mint breakdown. Update on wallet state changes. Show loading spinner during sync.

  2. Handle wallet states — Show appropriate UI for: no wallet found (offer to create), wallet loading, wallet ready, wallet error

Phase 3: Receive Tokens

  1. Receive panel — Textarea to paste a cashuA... or cashuB... token string, "Receive" button. On submit, call wallet.receive(token), show success/error, update balance.

Phase 4: Send Tokens

  1. Send panel — Amount input, mint selector dropdown, "Generate Token" button. On submit, call wallet.send(amount), display the generated cashu token string with a copy button.

Phase 5: Lightning Deposit

  1. Deposit panel — Amount input, mint selector, "Create Invoice" button. Uses NDKCashuDeposit to create a Lightning invoice. Display the bolt11 invoice as text with copy button. Poll for payment confirmation, update balance on success.

Phase 6: Transaction History

  1. Transaction list — Fetch kind 7376 events, decrypt content, display as a scrollable list with direction arrows, amounts, and relative timestamps.

Phase 7: Settings / Mint Management

  1. Settings section — Collapsible section showing configured mints. Add mint input + button. Remove mint button per entry. "Create New Wallet" button if no wallet exists.

Phase 8: Wallet Creation Flow

  1. First-run experience — If no kind 17375 wallet event found, show a setup panel: enter mint URL(s), click "Create Wallet". Uses NDKCashuWallet.create().

Technical Notes

Accessing NDK Cashu Classes

The NDK bundle exposes everything via window.NDK:

const { NDKCashuWallet, NDKCashuToken, NDKCashuWalletTx, 
        NDKCashuDeposit, NDKCashuMintList, NDKWalletStatus } = window.NDK;

NIP-44 Encryption

NIP-60 wallet content is NIP-44 encrypted. The existing init-ndk.mjs sign request handler already supports nip44Encrypt and nip44Decrypt methods, so NDK's built-in encryption will work automatically through the message-based signer.

Event Deletion (NIP-09)

When spending tokens, old kind 7375 events must be deleted via kind 5 events. The publishEvent() function from init-ndk.mjs can publish deletion events:

const deleteEvent = {
  kind: 5,
  tags: [["e", tokenEventId], ["k", "7375"]],
  content: ""
};
await publishEvent(deleteEvent);

Default Mints

Suggest these well-known mints as defaults in the UI:

  • https://mint.minibits.cash/Bitcoin
  • https://mint.coinos.io
  • https://stablenut.umint.cash

Worker vs Direct NDK

The current architecture uses a SharedWorker for NDK. However, NDKCashuWallet needs direct access to the NDK instance (not through the worker message protocol). Two approaches:

Option A (Recommended): Direct NDK instance on the page Create a lightweight NDK instance directly in the page for cashu operations. This NDK instance uses the same signer (window.nostr) and connects to the same relays. The worker continues handling subscriptions and relay status.

Option B: Extend worker protocol Add cashu-specific message types to the worker. This is more complex and less flexible.

Option A is simpler and aligns with how NDKCashuWallet expects to be used.