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 withcreate(),deposit(),send(),receive(),lnPay(), balance tracking, state managementNDKCashuToken(kind 7375) — Token event class with proof decryptionNDKCashuWalletTx(kind 7376) — Transaction history event classNDKCashuMintList(kind 10019) — Mint list for nutzap receptionNDKCashuDeposit— Lightning deposit flow with quote managementNDKCashuWalletBackup(kind 375) — Wallet backup/restoreNDKWalletStatus— 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
.btnKindinevent.html - Card/pane styling pattern from
msg.html(border + border-radius + background) #divBodyoverride pattern:flex-direction: column !importantetc..clsHidden/.clsVisiblefor 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
-
Update
cashu.htmlpage structure — Change title, add<style>block with#divBodycolumn layout override, add HTML skeleton for all UI sections inside#divBody -
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
- Imports NDK cashu classes from
-
Wire up initialization in
cashu.html— AfterinitNDKPage()completes, callinitWallet()and render initial state
Phase 2: Balance Display
-
Render balance card — Show total balance in sats, with per-mint breakdown. Update on wallet state changes. Show loading spinner during sync.
-
Handle wallet states — Show appropriate UI for: no wallet found (offer to create), wallet loading, wallet ready, wallet error
Phase 3: Receive Tokens
- Receive panel — Textarea to paste a
cashuA...orcashuB...token string, "Receive" button. On submit, callwallet.receive(token), show success/error, update balance.
Phase 4: Send Tokens
- 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
- Deposit panel — Amount input, mint selector, "Create Invoice" button. Uses
NDKCashuDepositto 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
- 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
- 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
- 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/Bitcoinhttps://mint.coinos.iohttps://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.