diff --git a/plans/feed-upgrade.md b/plans/feed-upgrade.md new file mode 100644 index 0000000..7a49c06 --- /dev/null +++ b/plans/feed-upgrade.md @@ -0,0 +1,272 @@ +# Feed Upgrade — Implementation Plan + +## Overview + +A series of improvements to the feed page and post interaction bar, centered on: +1. **Simplified feed** — show posts without inline replies, just counts +2. **Post thread page** — click comment button to open post with full comment thread in a new tab +3. **Zap capability indicators** — show what the recipient can receive (nutzap, Lightning) +4. **Zap rail selector** — let users choose between nutzap and Lightning melt +5. **Balance check and error handling** — pre-flight checks and better error messages + +The project uses **Cashu as the only payment rail** — nutzaps (NIP-61) for ecash transfers, and Cashu melt for Lightning zaps (NIP-57). No NWC, CLINK, or onchain needed. + +### Current state + +- [`www/feed.html`](www/feed.html) — shows posts with inline replies (toggled by "Comments" button). Replies appear under the post as they occur. This is unreliable. +- [`www/feed2.html`](www/feed2.html) — copy of feed.html, to be modified per this plan +- [`www/post.html`](www/post.html) — posting/composer page (NOT a thread viewer). Accepts `?event=`, `?npub=`, `?profile=` params. +- [`www/note.html`](www/note.html) — long-form note editor (kind 30023/30024), NOT for kind 1 threads +- [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) — renders the interaction bar (like, comment, quote, zap buttons) and handles zap routing +- [`www/js/zaps.mjs`](www/js/zaps.mjs) — NIP-57 Lightning zap protocol + +### URL schema (existing convention) + +The project uses `URLSearchParams` with query parameters: +- `?npub=` — Nostr public key (bech32) +- `?pubkey=` — Nostr public key (hex) +- `?event=` — Event identifier +- `?auth=required|optional|none` — Auth mode + +The new `post-feed.html` page will follow this schema: `post-feed.html?event=` + +### CSS theme + +The project uses CSS variables (never hardcode colors): +- `--primary-color` (black in light mode, white in dark mode) — main text/icons +- `--secondary-color` (white in light mode, black in dark mode) — background +- `--accent-color` (#ff0000 red, both modes) — the **only** highlight color +- `--muted-color` (#dddddd light / #777777 dark) — dimmed/disabled + +All indicators must use these variables. "Colored" = `var(--accent-color)`, "dimmed" = `var(--muted-color)`. + +--- + +## Phase 1: Simplified Feed (feed2.html) + +Remove inline replies from the feed. Show only the original post with interaction counts (likes, comments, zaps, nutzaps). Clicking the comment button opens the post thread page in a **new tab**. + +### Tasks + +- [ ] **1.1** In `feed2.html`, remove the inline comment rendering and the "Comments" toggle button. The feed should only show top-level posts. + +- [ ] **1.2** Change the comment button behavior — instead of calling `onCommentIntentFn` to show inline comments, open `post-feed.html?event=` in a **new tab** using `window.open('./post-feed.html?event=' + encodeURIComponent(postId), '_blank')`. + +- [ ] **1.3** Keep the interaction bar functional in the feed: + - ✅ Like button — works inline (no navigation) + - ✅ Zap button — works inline (opens zap dialog, sends nutzap or Lightning melt) + - ✅ Nutzap count display — shows total nutzaps received + - 🔄 Comment button — opens `post-feed.html?event=` in a new tab + - 🔄 Quote button — opens quote composer (no navigation) + +- [ ] **1.4** Keep real-time updates for counts (likes, zaps, comments, nutzaps) — the feed should still update these counts as events arrive, just not render the reply content inline. + +### Files to modify + +| File | Changes | +|------|---------| +| [`www/feed2.html`](www/feed2.html) | Remove inline comment rendering, comment button opens post-feed.html in new tab | + +--- + +## Phase 2: Post Thread Page (post-feed.html) + +Create a new `www/post-feed.html` page that shows a post with its full comment thread — all replies, with the ability to reply inline. + +### URL schema + +``` +post-feed.html?event= +``` + +Accepts the same event identifier formats as the rest of the project: +- Hex event ID: `post-feed.html?event=abc123...` +- nevent: `post-feed.html?event=nevent1q...` +- note: `post-feed.html?event=note1...` + +### Tasks + +- [ ] **2.1** Create `www/post-feed.html` based on the [`www/template.html`](www/template.html) pattern — standard header, hamburger, sidenav, footer. + +- [ ] **2.2** Parse the `event` URL parameter using `URLSearchParams` (same pattern as `post.html` line 248). Decode nevent/note to hex if needed. + +- [ ] **2.3** Fetch the original post event via the worker's `ndkFetchEvents` or `queryCache` function. Display it using `renderPostItem` from `post-interactions.mjs`. + +- [ ] **2.4** Fetch all replies — kind 1 events with an `e` tag pointing to the post ID. Use `ndkFetchEvents` with a filter like `{ kinds: [1], '#e': [postId] }`. + +- [ ] **2.5** Render the comment thread below the original post: + - Flat thread (all replies in chronological order) — simplest, most reliable + - Each reply rendered using `renderPostItem` from `post-interactions.mjs` + - Start with flat; can add nesting/threading later + +- [ ] **2.6** Add an inline composer at the bottom for posting replies. The composer should: + - Tag the original post: `['e', postId, '', 'reply']` + - Tag the original author: `['p', postPubkey]` + - Use the existing post composer component (`post-composer.mjs`) + +- [ ] **2.7** Real-time updates — subscribe to new replies via the worker's subscription system. Append new replies to the thread as they arrive. + +- [ ] **2.8** Each reply should have its own interaction bar (like, zap, quote) via `post-interactions.mjs`. The comment button on a reply opens `post-feed.html?event=` in a new tab for that reply's sub-thread. + +### Files to create + +| File | Purpose | +|------|---------| +| `www/post-feed.html` | New post thread page — shows post + all replies + inline composer | + +--- + +## Phase 3: Zap Capability Indicators in the Feed + +Show zap capability badges on each post's interaction bar so users can see at a glance whether the recipient can receive nutzaps, Lightning zaps, or both. + +### Design + +Compact indicators next to the existing zap button, using the project's CSS variables: + +| Indicator | CSS | Meaning | +|-----------|-----|---------| +| 🥜 (accent color) | `color: var(--accent-color)` | Recipient can receive nutzaps — has kind 10019 with a shared mint | +| 🥜 (muted) | `color: var(--muted-color)` | Recipient has kind 10019 but no shared mint with your wallet | +| ⚡ (accent color) | `color: var(--accent-color)` | Recipient can receive Lightning zaps — has lud16 | +| ⚡ (muted) | `color: var(--muted-color)` | Recipient has no lud16 — cannot receive Lightning zaps | + +Tooltip on hover shows details: "Can nutzap via mint.minibits.cash/Bitcoin" or "No shared mints — Lightning only" or "No zap capability". + +### When and where to fetch the data + +Two-tier strategy based on whether the author is a followed user or not: + +**Tier 1: Followed users (proactive fetch on startup)** +- The worker already fetches the user's kind 3 (contact list) on startup (line 1799 of `ndk-worker.js`) +- After the kind 3 is loaded, proactively fetch kind 10019 for all followed pubkeys in a batch +- This happens once on startup, results cached in the worker's Dexie/IndexedDB +- Followed users are the most likely zap recipients, and their 10019 rarely changes +- Add a subscription for kind 10019 updates for followed pubkeys (so changes are picked up) + +**Tier 2: Non-followed users (lazy fetch on first encounter)** +- When a post by a non-followed user appears in the feed, lazily fetch their kind 10019 +- Use the existing `walletFetchMintList` worker function +- Cache the result per pubkey in an in-memory Map with TTL (5 minutes) +- Only fetch once per pubkey per session (unless cache expires) + +**What's already available (no fetch needed):** +- lud16 (Lightning address) — already cached in `profile-cache.mjs` when post headers are rendered. The profile fetch already happens for every post in the feed. +- The sender's own wallet mints — already in the worker's `directProofStore`, accessible via `walletGetMints` + +### Tasks + +- [ ] **3.1** Create a `resolveZapCapabilities(pubkey)` function in [`www/js/zaps.mjs`](www/js/zaps.mjs) that checks: + - Does the recipient have a lud16 (Lightning address)? → check `profile-cache.mjs` cache (already fetched for post rendering) → `canLightning: true` + - Does the recipient have a kind 10019 (nutzap mint list)? → check local cache first, then fetch via `walletFetchMintList` if not cached + - If yes to 10019, which mints? Do any overlap with the sender's wallet mints (from `walletGetMints`)? → `canNutzap: true`, `sharedMints: [...]` + - Return `{ canLightning, canNutzap, sharedMints, nutzapMints, lud16 }` + +- [ ] **3.2** Add a proactive kind 10019 batch fetch for followed users in the worker: + - After the kind 3 (contact list) is loaded on startup, collect all followed pubkeys + - Fetch kind 10019 for all followed pubkeys: `ndk.fetchEvents({ kinds: [10019], authors: [...followedPubkeys] })` + - Cache the results in the worker's IndexedDB so they persist across sessions + - Add a subscription for kind 10019 updates for followed pubkeys + +- [ ] **3.3** For non-followed users, lazily fetch kind 10019 via `walletFetchMintList`: + - Cache results in an in-memory Map with TTL (5 minutes) in `zaps.mjs` + - Only fetch if not already in the worker's IndexedDB cache or the in-memory cache + - Non-blocking: render the interaction bar without badges, then update when the fetch completes + +- [ ] **3.4** Update the zap button rendering in `post-interactions.mjs` to show capability indicators: + - Add a small badge element next to the zap button + - Show 🥜 in `var(--accent-color)` if `canNutzap` is true + - Show 🥜 in `var(--muted-color)` if recipient has 10019 but no shared mint + - Show ⚡ in `var(--accent-color)` if `canLightning` is true + - Add tooltip with shared mint details + +- [ ] **3.5** Make the capability check async and non-blocking — render the interaction bar immediately, then update the badges when the capability check completes. The lud16 check is instant (from profile cache); the 10019 check may take a moment for non-followed users. + +### Files to modify + +| File | Changes | +|------|---------| +| [`www/js/zaps.mjs`](www/js/zaps.mjs) | Add `resolveZapCapabilities()` with caching, lazy fetch for non-followed users | +| [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) | Add capability badges to zap button rendering | +| [`www/ndk-worker.js`](www/ndk-worker.js) | Add proactive kind 10019 batch fetch for followed users on startup | + +--- + +## Phase 4: Zap Rail Selector in the Zap Dialog + +When the user taps the zap button, show which rails are available and let them choose. + +```mermaid +flowchart TD + A[User taps zap button] --> B[Show zap dialog with amount input] + B --> C{What rails are available?} + C -->|Nutzap + Lightning| D[Show rail toggle: 🥜 Nutzap | ⚡ Lightning] + C -->|Lightning only| E[Show Lightning only] + C -->|Nutzap only| F[Show Nutzap only] + C -->|Neither| G[Show error: recipient cannot receive zaps] + D --> H[User selects rail + amount] + H --> I{Rail selected} + I -->|Nutzap| J[Send NIP-61 nutzap via walletSendNutzap] + I -->|Lightning| K[Create LN invoice, melt Cashu via walletPayInvoice] + E --> K + F --> J +``` + +### Tasks + +- [ ] **4.1** Update `promptZapDetails` in [`www/js/zaps.mjs`](www/js/zaps.mjs) to accept and display rail options: + - If nutzap is available → show "🥜 Nutzap" option + - If Lightning is available → show "⚡ Lightning" option + - Let the user pick which rail to use via a toggle/selector + - Selected rail uses `var(--accent-color)`, unselected uses `var(--muted-color)` + +- [ ] **4.2** Show balance info in the dialog: "Cashu balance: 261 sats". If amount > balance → show warning in `var(--accent-color)`. + +- [ ] **4.3** Default rail selection: + - If nutzap is available and amount < 1000 sats → default to nutzap (lower fees) + - If nutzap is available and amount ≥ 1000 sats → default to Lightning + - If nutzap is not available → default to Lightning + +- [ ] **4.4** Pass the selected rail back to `handleZapClick` in `post-interactions.mjs` so it uses the right send function. + +- [ ] **4.5** Show shared mint info when nutzap is selected: "Will send via mint.minibits.cash/Bitcoin" + +### Files to modify + +| File | Changes | +|------|---------| +| [`www/js/zaps.mjs`](www/js/zaps.mjs) | Update `promptZapDetails` with rail selector UI | +| [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) | Update `handleZapClick` to respect user's rail choice | + +--- + +## Phase 5: Balance Check and Error Handling + +- [ ] **5.1** Before sending a zap, check the user's Cashu balance (via `walletGetBalance`): + - If balance < zap amount → show "Insufficient balance. Current: X sats, needed: Y sats" in `var(--accent-color)` + - If balance is sufficient but barely → show confirmation "This will use most of your balance. Continue?" + +- [ ] **5.2** Improve error messages for melt failures: + - "Mint couldn't route Lightning payment" (mint liquidity issue) + - "Mint returned an error: [details]" (mint API error) + - "Proofs were spent but payment failed — try again or contact the mint" + +- [ ] **5.3** After a successful zap, refresh the balance display. + +### Files to modify + +| File | Changes | +|------|---------| +| [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) | Add balance check before zap, improve error handling | + +--- + +## Implementation Order + +1. **Phase 1** (simplified feed) — remove inline replies from feed2.html, comment button opens post-feed.html in new tab +2. **Phase 2** (post thread page) — create post-feed.html to show full post + comment thread +3. **Phase 3** (zap capability indicators) — show what the recipient can receive +4. **Phase 4** (rail selector) — let users choose nutzap vs Lightning melt +5. **Phase 5** (balance + errors) — pre-flight checks and better error messages + +Phases 1-2 are the feed restructuring. Phases 3-5 are zap improvements. diff --git a/plans/wallet-page.md b/plans/wallet-page.md index 8823ddc..caebb09 100644 --- a/plans/wallet-page.md +++ b/plans/wallet-page.md @@ -1,406 +1,178 @@ -# Unified Wallet Page (`wallet.html`) — Implementation Plan +# Cashu Wallet Enhancements — Implementation Plan ## Overview -Create a new unified wallet page (`www/wallet.html`) that mirrors Amethyst's multi-rail wallet architecture, bringing together four payment rails into a single page: +Enhance the existing `www/cashu.html` page and zap flow, centered on **Cashu as the primary (and only) payment rail**. The core functionality is already implemented — this plan adds UI improvements to show zap capabilities in the feed and let users choose their zap rail. -1. **Bitcoin (Onchain / Taproot)** — NIP-BC onchain zaps, Taproot address display, onchain transaction history -2. **Cashu (NIP-60 ecash)** — Fix the existing kind 17375 content format bug, then migrate cashu.html functionality into the new page -3. **NWC (Nostr Wallet Connect / NIP-47)** — Lightning wallet connections via `nostr+walletconnect://` URIs -4. **CLINK Debit** — Spend-only debit pointers (`ndebit1...`) for CLINK protocol +### Design philosophy: Cashu-only -This plan also fixes the critical NIP-60 compliance bug discovered during investigation: the current `ndk-worker.js` writes a proprietary JSON object as the kind 17375 encrypted content instead of the NIP-60 standard tag-array format, which makes Amethyst (and any standard NIP-60 client) unable to read the wallet's mints or privkey. +The project uses **Cashu for everything** — no NWC, CLINK, onchain, or Lightning channels to maintain: + +| Payment type | How it works | Already implemented? | +|-------------|-------------|---------------------| +| **Nutzap (NIP-61)** | Send ecash proofs locked to recipient's p2pk via kind 9321 | ✅ Yes — `walletSendNutzap` | +| **Lightning zap (NIP-57)** | Melt Cashu ecash at a mint to pay a BOLT11 invoice | ✅ Yes — `walletPayInvoice` | +| **P2P ecash transfer** | Send cashuB token directly to another user | ✅ Yes — `walletSendToken` | +| **Lightning deposit** | Pay a BOLT11 invoice, mint issues ecash proofs | ✅ Yes — `walletCreateDeposit` | +| **Lightning withdrawal** | Melt ecash to pay an external BOLT11 invoice | ✅ Yes — `walletPayInvoice` | --- -## Architecture +## Phase 1: NIP-60 Compliance Fix (DONE ✅) + +Already completed and deployed (v0.7.19–v0.7.24): +- Fixed kind 17375 content format from proprietary `{mints, nutzap}` object to NIP-60 tag-array `[["mint",url],["privkey",hex]]` +- Removed non-standard `pubkey` tag from 17375 public tags +- Added `parseWalletContent()` backwards-compat shim for reading both formats +- Added auto-migration for legacy 17375 events on startup +- Added "Republish Wallet (kind 17375)" button with lightweight 17375-only republish +- Added `ensureDirectNutzapP2pk()` call in `publishDirectProofs` so privkey is always included + +--- + +## Phase 2: Zap Capability Indicators in the Feed + +Show zap capability badges on each post's interaction bar so users can see at a glance whether the recipient can receive nutzaps, Lightning zaps, or both. + +### Current state + +The interaction bar in [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) already renders: +- ⚡ Zap button with count +- 🥜 Nutzap count display (separate element showing total nutzaps received) + +### What to add + +Small capability icons/badges next to the zap button showing what the recipient can receive: + +``` +┌──────────────────────────────────────────────────┐ +│ ⚡ 5 🥜 2 🥜✅ ⚡✅ 💬 3 🔄 1 2h ago │ +│ └─ zap ─┘ └─ nutzap ─┘ └─ capability badges ─┘ │ +└──────────────────────────────────────────────────┘ +``` + +Or a more compact design — color the zap icon differently based on capabilities: + +| Visual | Meaning | +|--------|---------| +| ⚡ (default color) | Recipient can receive Lightning zaps (has lud16) | +| ⚡ + 🥜 (nut badge) | Recipient can also receive nutzaps (has kind 10019 with shared mint) | +| ⚡ (dimmed) | Recipient cannot receive any zaps (no lud16, no 10019) | + +### Implementation + +- [ ] **2.1** Create a `resolveZapCapabilities(pubkey)` function that checks: + - Does the recipient have a lud16 (Lightning address)? → can receive Lightning zaps + - Does the recipient have a kind 10019 (nutzap mint list)? → can receive nutzaps + - If yes to 10019, which mints? Do any overlap with the sender's wallet mints? + +- [ ] **2.2** Cache the results per pubkey (the kind 10019 and lud16 don't change often). Use a simple in-memory Map with TTL. + +- [ ] **2.3** Update `createInteractionItem` for the zap type to show capability indicators: + - Add a small 🥜 badge on the zap button if the recipient can receive nutzaps + - Add a tooltip showing the shared mints + - Dim the zap button if the recipient can't receive any zaps + +- [ ] **2.4** Fetch kind 10019 events for post authors as the feed loads. The worker already fetches kind 10019 for nutzap sending — extend this to cache the results for feed display. Use the existing `walletFetchMintList` worker function. + +- [ ] **2.5** Show shared mint info on hover/tooltip: "Can nutzap via mint.minibits.cash/Bitcoin" or "No shared mints — Lightning only" or "No zap capability" + +### Where the code lives + +| File | What to change | +|------|---------------| +| [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) | Add capability badges to zap button rendering | +| [`www/js/zaps.mjs`](www/js/zaps.mjs) | Add `resolveZapCapabilities()` function (or extend existing `resolveZapSpecForPubkey`) | +| [`www/ndk-worker.js`](www/ndk-worker.js) | May need a lightweight "fetch 10019 for display" message (or reuse `walletFetchMintList`) | + +--- + +## Phase 3: Zap Rail Selector in the Zap Dialog + +When the user taps the zap button, show which rails are available and let them choose. ```mermaid flowchart TD - subgraph Page["wallet.html (UI)"] - A[Header + Sidenav] - B[Bitcoin Onchain Section] - C[Cashu Wallet Section] - D[NWC Wallet List] - E[CLINK Debit List] - F[Add Wallet Modal] - end - - subgraph Modules["JS Modules"] - G["wallet-ui.mjs - new orchestrator"] - H["cashu-wallet.mjs - existing, updated"] - I["nwc-wallet.mjs - new"] - J["onchain-wallet.mjs - new"] - K["clink-wallet.mjs - new"] - end - - subgraph Worker["ndk-worker.js"] - L[NDK Core + Relays] - M[Direct Cashu proof store] - N["NWC request/response - new"] - O["Onchain zap events - new"] - end - - subgraph NDK["ndk-core.bundle.js"] - P[NDKCashuWallet] - Q[NDKNWCWallet] - R[NDKEvent / NDKKind] - S[cashu-ts CashuWallet] - end - - A --> G - B --> J - C --> H - D --> I + A[User taps zap button] --> B[Show zap dialog with amount input] + B --> C{What rails are available?} + C -->|Nutzap + Lightning| D[Show rail toggle: 🥜 Nutzap | ⚡ Lightning] + C -->|Lightning only| E[Show Lightning only] + C -->|Nutzap only| F[Show Nutzap only] + C -->|Neither| G[Show error: recipient cannot receive zaps] + D --> H[User selects rail + amount] + H --> I{Rail selected} + I -->|Nutzap| J[Send NIP-61 nutzap via walletSendNutzap] + I -->|Lightning| K[Create LN invoice, melt Cashu via walletPayInvoice] E --> K - F --> G - G --> H - G --> I - G --> J - G --> K - H --> M - I --> N - J --> O - K --> N - M --> L - N --> L - O --> L - H --> S - I --> Q - H --> P - J --> R + F --> J ``` -### File Structure +- [ ] **3.1** Update `promptZapDetails` in `zaps.mjs` to accept and display rail options: + - If nutzap is available (recipient has 10019 with shared mint) → show "🥜 Nutzap" option + - If Lightning is available (recipient has lud16) → show "⚡ Lightning" option + - Let the user pick which rail to use via a toggle/selector -| File | Type | Purpose | -|------|------|---------| -| `www/wallet.html` | New | Page HTML + page-specific styles + inline module script | -| `www/js/wallet-ui.mjs` | New | Top-level orchestrator that renders all four rail sections | -| `www/js/cashu-wallet.mjs` | Existing, updated | Cashu controller — update to use NIP-60 tag-array format | -| `www/js/nwc-wallet.mjs` | New | NWC wallet list management, balance fetch, pay invoice | -| `www/js/onchain-wallet.mjs` | New | Bitcoin onchain section — Taproot address, onchain zap send/history | -| `www/js/clink-wallet.mjs` | New | CLINK debit pointer management, budget requests | +- [ ] **3.2** Show balance info in the dialog: + - "Cashu balance: 261 sats" + - If amount > balance → show warning "Insufficient balance" -The module approach follows the existing pattern used by `cashu-wallet.mjs` and `post-interactions.mjs`. +- [ ] **3.3** Default rail selection: + - If nutzap is available and amount < 1000 sats → default to nutzap (lower fees) + - If nutzap is available and amount ≥ 1000 sats → default to Lightning (more reliable for larger amounts) + - If nutzap is not available → default to Lightning + +- [ ] **3.4** Pass the selected rail back to `handleZapClick` in `post-interactions.mjs` so it uses the right send function. --- -## Phase 1: Fix NIP-60 Compliance Bug (Critical, do first) +## Phase 4: cashu.html UI Polish -### Problem +Minor UI improvements to the existing cashu.html page: -In [`www/ndk-worker.js`](www/ndk-worker.js:2758) lines 2758–2769, the worker hand-rolls the kind 17375 wallet event with a **proprietary JSON object** as the encrypted content: +- [ ] **4.1** Consider renaming the page title from "CASHU" to "WALLET" since it's the project's unified wallet. -```js -// CURRENT (wrong — proprietary) -const walletPayloadObj = { - mints: getDirectWalletMints(), - ...(directNutzapPrivateKeyHex - ? { nutzap: { privkey: directNutzapPrivateKeyHex, p2pk: directNutzapP2pk } } - : {}) -}; -``` +- [ ] **4.2** Update the sidenav entry in `index.html` from "CASHU" to "WALLET" (keeping the same `cashu.html` URL). -NIP-60 requires the decrypted content to be a **JSON array of tag arrays** (the same format NDK's `payloadForEvent` and Amethyst's `CashuWalletEvent.build()` produce): - -```json -[["mint","https://mint.example.com"],["privkey",""]] -``` - -### Fix - -- [ ] **1.1** Change the wallet payload construction in `ndk-worker.js` to the NIP-60 tag-array format: - -```js -// FIXED (NIP-60 standard) -const walletPayloadObj = [ - ...getDirectWalletMints().map((url) => ["mint", url]), - ...(directNutzapPrivateKeyHex ? [["privkey", directNutzapPrivateKeyHex]] : []) -]; -const walletPayload = JSON.stringify(walletPayloadObj); -``` - -- [ ] **1.2** Remove the non-standard `pubkey` tag from the public tags of the 17375 event (line 2755). The p2pk pubkey belongs on the kind 10019 `NutzapInfoEvent`, not on kind 17375. Amethyst reads the p2pk from the decrypted `privkey` tag, not from a public `pubkey` tag. - -- [ ] **1.3** Update the wallet event **reading** path in `ndk-worker.js` (`hydrateNutzapKeyFromWalletEvents` and any other 17375 decryption) to parse the tag-array format instead of the object format. Add a backwards-compatibility shim: if the decrypted content parses as an object (old format), extract `mints` and `nutzap.privkey` from it; if it parses as an array, extract `mint` and `privkey` tags. - -- [ ] **1.4** Add a one-time migration: when the worker detects an old-format 17375 event on startup, automatically republish it in the new tag-array format and NIP-09 delete the old one. This ensures existing users get upgraded transparently. - -- [ ] **1.5** Test with Amethyst: after republishing, verify that Amethyst shows the wallet's mints and the "Could not read the existing wallet key" error no longer appears. - ---- - -## Phase 2: Create `wallet.html` Page Shell - -- [ ] **2.1** Create `www/wallet.html` based on the [`www/template.html`](www/template.html) pattern — standard header, hamburger, sidenav with AI section, footer with relay status. - -- [ ] **2.2** Add a sidenav nav entry to [`www/index.html`](www/index.html:543) — replace or augment the existing `cashu` entry with a `wallet` entry pointing to `wallet.html`. - -- [ ] **2.3** Page layout — single-column centered (like `cashu.html` and `post.html`), with four stacked sections: - -``` -┌─────────────────────────────────────┐ -│ HEADER (hamburger + "WALLET") │ -├─────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────────┐ │ -│ │ BITCOIN (Onchain) │ │ -│ │ ₿ Taproot address │ │ -│ │ Balance: 0 sats │ │ -│ │ [Send Onchain Zap] [History]│ │ -│ └─────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────┐ │ -│ │ CASHU (NIP-60 ecash) │ │ -│ │ Balance: 1,234 sats │ │ -│ │ [Receive] [Send] [Deposit] │ │ -│ │ [Withdraw] [Mints] [History]│ │ -│ └─────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────┐ │ -│ │ LIGHTNING (NWC) │ │ -│ │ ┌─ Alby ──────── 500 sats ┐│ │ -│ │ └──────────────────────────┘│ │ -│ │ ┌─ Mutiny ────── 1,200 sats┐│ │ -│ │ └──────────────────────────┘│ │ -│ │ [+ Add NWC Connection] │ │ -│ └─────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────┐ │ -│ │ CLINK DEBIT │ │ -│ │ ┌─ My Debit ─── spend-only ┐│ │ -│ │ └──────────────────────────┘│ │ -│ │ [+ Add CLINK Pointer] │ │ -│ └─────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────┐ │ -│ │ [+ ADD WALLET] │ │ -│ │ Choose: Cashu | NWC | CLINK│ │ -│ └─────────────────────────────┘ │ -│ │ -├─────────────────────────────────────┤ -│ FOOTER (relay status + balance) │ -└─────────────────────────────────────┘ -``` - -- [ ] **2.4** Sidenav sections: keep the existing AI section, relay section. Move the Cashu mint-discovery and zap-settings sections from `cashu.html` into the `wallet.html` sidenav. - ---- - -## Phase 3: Cashu Section (Migrate from cashu.html) - -- [ ] **3.1** Port the Cashu UI from [`www/cashu.html`](www/cashu.html) into the Cashu section of `wallet.html` — balance card, action buttons (Receive/Send/Deposit/Withdraw), action panels, transaction history, mint management. - -- [ ] **3.2** Reuse the existing [`www/js/cashu-wallet.mjs`](www/js/cashu-wallet.mjs) controller — it already wraps the worker wallet API. No changes needed to the controller itself (the fix is in the worker, Phase 1). - -- [ ] **3.3** Port the mint-discovery sidebar and zap-settings sidebar from `cashu.html` into `wallet.html`'s sidenav. - -- [ ] **3.4** Port the nutzap mint-list (kind 10019) publishing UI from `cashu.html`. - -- [ ] **3.5** After `wallet.html` is complete, redirect `cashu.html` to `wallet.html` (or keep `cashu.html` as a thin redirect) and update the sidenav link in `index.html`. - ---- - -## Phase 4: NWC (Nostr Wallet Connect) Section - -NIP-47 (NWC) allows connecting to external Lightning wallets via `nostr+walletconnect://` URIs. The NDK bundle already includes [`NDKNWCWallet`](www/ndk-core.bundle.js:30422) which handles the NIP-47 protocol. - -### Storage - -NWC connection URIs are stored as a **kind 37550** application event (or in localStorage as a simpler approach initially). Each entry contains: -- Wallet name (user-defined label) -- NWC URI (`nostr+walletconnect://?relay=&secret=`) -- Whether it's the default wallet - -### Tasks - -- [ ] **4.1** Create `www/js/nwc-wallet.mjs` with: - - `loadNwcWallets()` — read stored NWC connections from localStorage (key: `nwcWallets`) - - `addNwcWallet(name, uri)` — parse and validate the URI, store it - - `removeNwcWallet(id)` — remove a connection - - `setDefaultNwcWallet(id)` — set the default wallet for zaps - - `fetchNwcBalance(walletId)` — use `NDKNWCWallet` to call `get_balance` via NIP-47 - - `payInvoiceViaNwc(walletId, bolt11)` — use `NDKNWCWallet` to call `pay_invoice` - - `getNwcInfo(walletId)` — call `get_info` for wallet capabilities - -- [ ] **4.2** Add NWC worker support in `ndk-worker.js`: - - New message handlers: `nwcAddWallet`, `nwcRemoveWallet`, `nwcGetBalance`, `nwcPayInvoice`, `nwcGetInfo` - - Use `NDKNWCWallet` from the bundle to make NIP-47 requests - - The NWC wallet sends kind 23194 events to the wallet service's relay and listens for responses - -- [ ] **4.3** NWC UI in `wallet.html`: - - Wallet list — each row shows name, balance, default badge, delete button - - "Add NWC Connection" form — name input + URI input + paste/scan - - Wallet detail view — balance, transactions, send Lightning payment - - URI validation: must start with `nostr+walletconnect://` or `nostrwalletconnect://` - -- [ ] **4.4** Integrate NWC as a zap payment source in [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs:1585) — when sending a zap, if the user has an NWC wallet configured, use it to pay the LN invoice automatically (replacing the current "open external wallet" fallback). - ---- - -## Phase 5: Bitcoin Onchain Section - -NIP-BC (Onchain Zaps) uses Taproot addresses derived from the user's Nostr pubkey to send on-chain Bitcoin zaps. Amethyst's [`OnchainSection.kt`](../amethyst/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainSection.kt:91) shows the user's Taproot address, balance, and a send dialog. - -### Tasks - -- [ ] **5.1** Create `www/js/onchain-wallet.mjs` with: - - `getTaprootAddress(pubkey)` — derive the Taproot address from the user's Nostr pubkey (NIP-BC spec: tweak the pubkey with a standard script tree) - - `fetchOnchainBalance(address)` — query a block explorer API (mempool.space) for the address UTXO balance - - `fetchOnchainTransactions(address)` — query mempool.space for address transactions - - `sendOnchainZap(recipientPubkey, amountSats)` — build and broadcast an onchain zap (NIP-BC kind 8333 receipt event + Bitcoin tx) - - Subscribe to kind 8333 (`OnchainZapEvent`) for incoming/outgoing zap history - -- [ ] **5.2** Onchain UI in `wallet.html`: - - Taproot address display with copy button and QR code - - Balance display (fetched from mempool.space) - - "Send Onchain Zap" button — opens a dialog to select recipient (by npub or address) and amount - - Transaction history list — incoming (green) and outgoing (orange) onchain zaps with counterparty info - - Public address warning (same as Amethyst: "This address is public — anyone can see your balance") - -- [ ] **5.3** Add onchain zap event subscription in `ndk-worker.js`: - - Subscribe to kind 8333 events where `p` tag = user pubkey (incoming) and where author = user pubkey (outgoing) - - Broadcast onchain zap events to the page via `onchainZapReceived` / `onchainZapSent` messages - -- [ ] **5.4** Integrate onchain zaps as a payment option in `post-interactions.mjs` — when zapping, offer the choice between Lightning (NWC), Cashu (nutzap), and Onchain (NIP-BC), similar to Amethyst's unified zap chip. - ---- - -## Phase 6: CLINK Debit Section - -CLINK (NIP-CLINK) is a spend-only debit protocol where the user adds a debit pointer (`ndebit1...`) that authorizes payments against a CLINK service. Amethyst's [`AddClinkDebitWalletScreen.kt`](../amethyst/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddClinkDebitWalletScreen.kt:73) shows the add flow. - -### Tasks - -- [ ] **6.1** Create `www/js/clink-wallet.mjs` with: - - `loadClinkDebits()` — read stored CLINK debit pointers from localStorage (key: `clinkDebits`) - - `addClinkDebit(name, ndebitUri)` — parse the `ndebit1...` pointer, validate, store - - `removeClinkDebit(id)` — remove a debit pointer - - `requestClinkBudget(debitId, amountSats, frequency)` — request a spending budget from the CLINK service - - `payViaClinkDebit(debitId, bolt11)` — pay a Lightning invoice via the CLINK debit - -- [ ] **6.2** CLINK UI in `wallet.html`: - - Debit list — each row shows name, "spend-only" badge, budget info, delete button - - "Add CLINK Debit" form — name input + `ndebit1...` pointer input - - Budget dialog — set spending limit (amount + frequency: one-time / daily / weekly / monthly) - - No balance display (CLINK debits are spend-only, no balance to show — same as Amethyst) - -- [ ] **6.3** Integrate CLINK as a zap payment source in `post-interactions.mjs` — when the user has a CLINK debit configured, offer it as a payment option for Lightning zaps. - ---- - -## Phase 7: Unified "Add Wallet" Flow - -- [ ] **7.1** Add an "Add Wallet" button/card at the bottom of the wallet list that opens a modal with three choices (matching Amethyst's [`AddWalletScreen.kt`](../amethyst/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/AddWalletScreen.kt:64)): - - | Option | Icon | Description | - |--------|------|-------------| - | Cashu Wallet | Cashew nut | Create a NIP-60 ecash wallet with mints | - | NWC Connection | Lightning bolt | Connect a Lightning wallet via NIP-47 | - | CLINK Debit | Debit card | Add a spend-only CLINK debit pointer | - - (Bitcoin onchain is always available — no "add" needed, the Taproot address is derived from the pubkey automatically.) - -- [ ] **7.2** Each choice opens the corresponding add-form (reuse the forms built in Phases 3–6). - -- [ ] **7.3** Default wallet selection — let the user set which wallet is the default for zaps (a star/radio button on each wallet card, matching Amethyst's `setDefaultWallet`). - ---- - -## Phase 8: Unified Zap Routing - -- [ ] **8.1** Update [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs:1585) zap handler to use a unified payment router that tries rails in priority order: - -```mermaid -flowchart TD - A[User clicks Zap] --> B{Recipient has nutzap mint list?} - B -->|Yes| C{Sender has Cashu wallet with matching mint?} - C -->|Yes| D[Send Cashu Nutzap] - C -->|No| E{Sender has NWC or CLINK?} - B -->|No| E - E -->|NWC| F[Pay LN invoice via NWC] - E -->|CLINK| G[Pay LN invoice via CLINK] - E -->|None| H[Show invoice for external wallet] - F --> I{Recipient supports onchain zaps?} - G --> I - H --> I - I -->|Yes| J[Offer onchain zap alternative] - I -->|No| K[Done] - D --> K - J --> K -``` - -- [ ] **8.2** Add a zap method selector UI (like Amethyst's zap chip) that shows available rails for the current recipient and lets the user choose. - ---- - -## Phase 9: Worker Updates - -- [ ] **9.1** Add NWC message handlers to `ndk-worker.js` (Phase 4.2) -- [ ] **9.2** Add onchain zap subscription to `ndk-worker.js` (Phase 5.3) -- [ ] **9.3** Add CLINK payment request handler to `ndk-worker.js` (Phase 6.3) -- [ ] **9.4** Add `walletInit` expansion — the existing `walletInit` should also load NWC wallets and onchain state, not just Cashu -- [ ] **9.5** Add new worker-to-page events: `nwcBalanceUpdated`, `nwcPaymentResult`, `onchainZapReceived`, `onchainZapSent`, `clinkPaymentResult` -- [ ] **9.6** Update [`www/js/init-ndk.mjs`](www/js/init-ndk.mjs:454) to dispatch the new worker events to the page - ---- - -## Phase 10: Migration & Cleanup - -- [ ] **10.1** Redirect `cashu.html` → `wallet.html` (replace content with a `` redirect or a JS redirect) -- [ ] **10.2** Update sidenav in `index.html` — change `cashu` entry to `wallet` -- [ ] **10.3** Update any internal links that point to `cashu.html` (search all HTML files) -- [ ] **10.4** Update the footer balance display (currently in `relay-ui.mjs`) to show the unified wallet balance (Cashu + NWC + Onchain) -- [ ] **10.5** Keep `cashu-wallet.mjs` as-is (it's the Cashu controller, reused by `wallet.html`) +- [ ] **4.3** Add a "Zap via Lightning" info note in the Withdraw panel explaining that this melts ecash to pay a Lightning invoice — same mechanism used for Lightning zaps. --- ## NIP Compliance Reference -| Rail | NIP | Kind(s) | Status in this project | -|------|-----|---------|----------------------| -| Cashu Wallet | NIP-60 | 17375 (wallet), 7375 (token), 7376 (tx), 375 (backup) | **Bug: content format non-standard** — fixed in Phase 1 | -| Cashu Nutzap | NIP-61 | 10019 (mint list), 9321 (nutzap) | Working, but depends on 17375 fix for privkey | -| NWC | NIP-47 | 23194 (request/response) | **Not implemented** — Phase 4 | -| Onchain Zap | NIP-BC | 8333 (onchain zap receipt) | **Not implemented** — Phase 5 | -| CLINK Debit | NIP-CLINK | ndebit1 pointer | **Not implemented** — Phase 6 | +| Rail | NIP | Kind(s) | Status | +|------|-----|---------|--------| +| Cashu Wallet | NIP-60 | 17375, 7375, 7376, 375 | ✅ Fixed and working | +| Cashu Nutzap | NIP-61 | 10019, 9321 | ✅ Working | +| Lightning Zap | NIP-57 | 9734, 9735 | ✅ Working (via Cashu melt) | --- -## Key Files to Modify +## Key Files + +### Files to modify | File | Changes | |------|---------| -| [`www/ndk-worker.js`](www/ndk-worker.js) | Fix 17375 content format (Phase 1), add NWC/onchain/CLINK handlers (Phases 4-6) | -| [`www/js/init-ndk.mjs`](www/js/init-ndk.mjs) | Add new worker event dispatchers (Phase 9.6) | -| [`www/js/cashu-wallet.mjs`](www/js/cashu-wallet.mjs) | No changes (reused as-is) | -| [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) | Unified zap routing (Phase 8) | -| [`www/index.html`](www/index.html) | Sidenav entry update (Phase 10.2) | +| [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) | Add zap capability badges to interaction bar, pass selected rail to zap handler | +| [`www/js/zaps.mjs`](www/js/zaps.mjs) | Add `resolveZapCapabilities()`, update `promptZapDetails` with rail selector | +| [`www/cashu.html`](www/cashu.html) | Minor UI polish (title, info notes) | +| [`www/index.html`](www/index.html) | Sidenav label: "CASHU" → "WALLET" | -## Key Files to Create +### Files reused as-is -| File | Purpose | -|------|---------| -| `www/wallet.html` | New unified wallet page | -| `www/js/wallet-ui.mjs` | Page orchestrator | -| `www/js/nwc-wallet.mjs` | NWC wallet management | -| `www/js/onchain-wallet.mjs` | Bitcoin onchain section | -| `www/js/clink-wallet.mjs` | CLINK debit management | +| File | Why no changes needed | +|------|----------------------| +| [`www/js/cashu-wallet.mjs`](www/js/cashu-wallet.mjs) | Controller already wraps all worker functions | +| [`www/ndk-worker.js`](www/ndk-worker.js) | All wallet functions already exist (payInvoice, sendNutzap, fetchMintList) | +| [`www/js/init-ndk.mjs`](www/js/init-ndk.mjs) | All wallet exports already exist | --- ## Implementation Order -The phases are ordered by dependency and impact: +1. **Phase 1** (DONE) — NIP-60 compliance fix +2. **Phase 2** (zap capability indicators) — show what the recipient can receive in the feed +3. **Phase 3** (rail selector) — let users choose nutzap vs Lightning melt in the zap dialog +4. **Phase 4** (UI polish) — minor label/info changes -1. **Phase 1** (NIP-60 fix) — do this first; it's a bug fix that benefits the existing `cashu.html` immediately -2. **Phase 2** (page shell) — creates the container for everything else -3. **Phase 3** (Cashu migration) — moves existing working functionality into the new page -4. **Phase 4** (NWC) — adds the first new rail (Lightning) -5. **Phase 5** (Onchain) — adds Bitcoin onchain zaps -6. **Phase 6** (CLINK) — adds CLINK debit support -7. **Phase 7** (Add wallet flow) — ties the rails together with a unified add modal -8. **Phase 8** (Unified zap routing) — integrates all rails into the zap flow -9. **Phase 9** (Worker updates) — can be done incrementally alongside Phases 4-6 -10. **Phase 10** (Migration & cleanup) — final step after everything works +No new files needed. No new infrastructure. Just UI improvements on top of the existing working zap flow. diff --git a/www/feed2.html b/www/feed2.html new file mode 100644 index 0000000..aa99144 --- /dev/null +++ b/www/feed2.html @@ -0,0 +1,714 @@ + + + + + + + + FEED + + + + + + + + + + + + + + +
+ +
+
+
+
FEED
+
+
+
+ +
+
Loading follows…
+
+ +
+ +
+
+
+
+
0 sats
+
+ +
+
+
+
+
+
Feed
+ +
+
+ +
+
AI
+
+
No saved providers yet.
+
+
+ + +
+
リレー
+
Loading relays...
+
+ +
+
ブロッサム
+
Loading blossom servers...
+
+ +
+ loading... +
+ + +
+
+
+ + + + + + + + diff --git a/www/js/version.json b/www/js/version.json index 4ada06f..528aec7 100644 --- a/www/js/version.json +++ b/www/js/version.json @@ -1,5 +1,5 @@ { - "VERSION": "v0.7.24", - "VERSION_NUMBER": "0.7.24", - "BUILD_DATE": "2026-06-25T18:05:33.223Z" + "VERSION": "v0.7.25", + "VERSION_NUMBER": "0.7.25", + "BUILD_DATE": "2026-06-25T23:45:36.529Z" } diff --git a/www/post-feed.html b/www/post-feed.html new file mode 100644 index 0000000..29718e1 --- /dev/null +++ b/www/post-feed.html @@ -0,0 +1,908 @@ + + + + + + + + THREAD + + + + + + + + + + + + + + + + +
+ +
+
+
+
THREAD
+
+
+
+ +
+
+ ← Feed +
+
Loading thread…
+
+
+
Replies
+
+
+
+
+ +
+
+
+
+
0 sats
+
+ +
+
+
+
+
+ +
+
AI
+
+
No saved providers yet.
+
+
+ +
+
リレー
+
Loading relays...
+
+ +
+
ブロッサム
+
Loading blossom servers...
+
+ +
+ loading... +
+ + +
+
+
+ + + + + + + +