# Nostr Entity Decoding & Zaps Implementation Plan

## Overview

Two features for `post.html` and the post-interactions module:

1. **Nostr Entity Decoding** — Detect and render `nostr:` prefixed bech32 entities (nprofile, nevent, naddr, note, npub) embedded in post content as clickable cards/links
2. **Zaps** — Full NIP-57 zap flow: resolve LNURL from recipient profile, create zap request, fetch bolt11 invoice, pay via Cashu wallet

## Key NDK Source References

The NDK codebase already has robust implementations we can leverage:

- **Entity regex**: [`content-tagger.ts`](ndk/core/src/events/content-tagger.ts:121) — `/@|nostr:)(npub|nprofile|note|nevent|naddr)[a-zA-Z0-9]+/g`
- **Entity decoding**: [`entity.ts`](ndk/core/src/ndk/entity.ts:11) — `getEntity()` decodes nip19 and returns NDK objects
- **LNURL resolution**: [`ln.ts`](ndk/core/src/zapper/ln.ts:50) — `getNip57ZapSpecFromLud()` handles lud16→endpoint and lud06→endpoint
- **Zap request creation**: [`nip57.ts`](ndk/core/src/zapper/nip57.ts:22) — `generateZapRequest()` builds and signs kind 9734
- **Invoice fetching**: [`index.ts`](ndk/core/src/zapper/index.ts:563) — `getLnInvoice()` calls LNURL callback with zap request
- **Zap invoice parsing**: [`invoice.ts`](ndk/core/src/zap/invoice.ts:46) — `zapInvoiceFromEvent()` parses kind 9735 with bolt11 decoder
- **NDK bundle exports**: All of these are exported from `ndk-core.bundle.js`: `getNip57ZapSpecFromLud`, `generateZapRequest`, `zapInvoiceFromEvent`, `NDKZapper`, `getBolt11Amount`

---

## Feature 1: Nostr Entity Decoding

### Current State

- [`htmlFormatText()`](www/js/utilities.mjs:515) only handles HTTP URLs and image extensions
- [`renderPostItem()`](www/js/post-interactions.mjs:285) calls `htmlFormatText()` to render post content
- `window.NostrTools.nip19.decode()` is available globally via `nostr.bundle.js` and supports all bech32 entity types: npub, note, nprofile, nevent, naddr, nrelay
- No current handling of `nostr:` prefixed strings in post content

### Example Input

```
Check out nostr:nprofile1qy2hwumn8ghj7un9d3shjtnyd968gmewwp6kyqpqjqhfpx394z4l3zr4zs77zgztsjpy4z6q48q6npxdakv5elyrhaws5cavn4
```

### Entity Types to Handle

| Prefix | NIP-19 Type | Decoded Data | Render As |
|--------|-------------|--------------|-----------|
| `nostr:npub1...` | npub | `{ type: 'npub', data: hexPubkey }` | Profile link with name/avatar |
| `nostr:nprofile1...` | nprofile | `{ type: 'nprofile', data: { pubkey, relays } }` | Profile link with name/avatar |
| `nostr:note1...` | note | `{ type: 'note', data: hexEventId }` | Embedded post card |
| `nostr:nevent1...` | nevent | `{ type: 'nevent', data: { id, relays, author, kind } }` | Embedded post card |
| `nostr:naddr1...` | naddr | `{ type: 'naddr', data: { identifier, pubkey, kind, relays } }` | Link to addressable event |

### Implementation Plan

```mermaid
flowchart TD
    A[Post content string] --> B[htmlFormatText]
    B --> C{Regex: nostr:n...1...}
    C -->|Match| D[nip19.decode]
    D --> E{Entity type?}
    E -->|npub/nprofile| F[Render profile mention link]
    E -->|note/nevent| G[Render embedded post placeholder]
    E -->|naddr| H[Render addressable event link]
    C -->|No match| I[Existing URL/image handling]
    F --> J[Async: fetch profile, update DOM]
    G --> K[Async: fetch event, render inline card]
```

#### Step 1: Enhance `htmlFormatText()` in `utilities.mjs`

Add a regex pass for `nostr:` entities **before** the URL regex pass:

```
/nostr:(npub1|nprofile1|note1|nevent1|naddr1)[a-z0-9]+/gi
```

For each match:
- Call `window.NostrTools.nip19.decode(entityWithoutNostrPrefix)`
- Replace with an HTML placeholder element containing a `data-nostr-entity` attribute and the bech32 string
- Profile mentions (npub/nprofile): render as `<a>` link to `post.html?npub=...&profile=true&post=false`
- Event references (note/nevent): render as a `<div class="nostr-embed-placeholder">` with `data-event-id`
- Addressable events (naddr): render as `<a>` link

#### Step 2: Post-render async hydration in `post-interactions.mjs`

After `contentEl.innerHTML = htmlFormatText(...)`:
- Query all `.nostr-embed-placeholder` elements
- For profile mentions: fetch profile via `fetchProfile()`, update display name and avatar
- For event embeds: fetch the referenced event via `fetchEventsFromAllRelays()`, render as an inline mini-post card

#### Step 3: CSS for embedded entities

Add styles for:
- `.nostr-mention` — inline profile mention with avatar thumbnail
- `.nostr-embed` — embedded post card with border, content preview, author

---

## Feature 2: Zaps (NIP-57)

### Current State

- [`handleZapClick()`](www/js/post-interactions.mjs:870) shows "Coming soon" placeholder
- [`extractZapAmount()`](www/js/post-interactions.mjs:1298) returns 0 — needs BOLT11 parsing
- [`extractZapComment()`](www/js/post-interactions.mjs:1314) works correctly
- Zap receipt processing (kind 9735) already works in the interaction state tracker
- `walletPayInvoice()` exists in [`init-ndk.mjs`](www/js/init-ndk.mjs:919) — pays bolt11 via Cashu wallet
- `light-bolt11-decoder` is bundled inside `ndk-core.bundle.js` but not directly exported
- NDK bundle includes `NDKZapper` class with full NIP-57 flow, but it operates on NDK instances (worker-side)
- Profile `lud16` field is available from kind 0 metadata

### Zap Flow (NIP-57)

```mermaid
sequenceDiagram
    participant U as User
    participant PI as post-interactions.mjs
    participant P as Profile Cache
    participant LN as LNURL Server
    participant W as Cashu Wallet
    participant R as Relays

    U->>PI: Click Z button
    PI->>PI: Show amount selector popup
    U->>PI: Select amount
    PI->>P: Get recipient lud16/lud06
    P-->>PI: lud16 = user@domain.com
    PI->>LN: GET https://domain.com/.well-known/lnurlp/user
    LN-->>PI: LNURL-pay metadata with callback, allowsNostr, nostrPubkey
    PI->>PI: Create kind 9734 zap request event
    PI->>PI: Sign zap request via window.nostr.signEvent
    PI->>LN: GET callback?amount=X&nostr=zapRequestJSON&lnurl=...
    LN-->>PI: bolt11 invoice
    PI->>W: walletPayInvoice with bolt11
    W-->>PI: Payment result
    PI->>PI: Update Z icon to active state
    Note over R: Zap receipt kind 9735 arrives via subscription
    R-->>PI: Zap receipt event
    PI->>PI: Update zap count display
```

### Implementation Steps

#### Step 1: LNURL Resolution

Port the pattern from [`getNip57ZapSpecFromLud()`](ndk/core/src/zapper/ln.ts:50) to a main-thread helper:
- Parse `lud16` format: `user@domain` → `https://domain/.well-known/lnurlp/user`
- Also handle `lud06` (bech32 encoded LNURL) — NDK uses `bech32.decode()` from `@scure/base`
- Fetch the LNURL-pay endpoint metadata
- Validate response: check `allowsNostr: true`, `nostrPubkey` field
- Return `{ callback, minSendable, maxSendable, nostrPubkey, allowsNostr }`

**Note**: The NDK bundle exports `getNip57ZapSpecFromLud` but it requires an NDK instance. Since our NDK runs in a worker, we'll implement a lightweight version on the main thread that just does the HTTP fetch.

#### Step 2: Zap Request Creation (kind 9734)

Port the pattern from [`generateZapRequest()`](ndk/core/src/zapper/nip57.ts:22):
- Build kind 9734 event with tags:
  - `['relays', ...relayUrls]` (up to 4 relays)
  - `['amount', amountInMillisats.toString()]`
  - `['lnurl', callback]`
  - `['e', postId]` (the event being zapped)
  - `['p', postPubkey]` (the recipient)
- Set `content` to optional comment
- Sign via `window.nostr.signEvent()`

#### Step 3: Fetch Bolt11 Invoice

Port the pattern from [`getLnInvoice()`](ndk/core/src/zapper/index.ts:563):
- Build URL: `callback?amount=X&nostr=JSON.stringify(signedZapRequest)`
- Fetch and parse response for `{ pr: bolt11Invoice }`

#### Step 4: Payment via Cashu Wallet

Update `handleZapClick()`:
- Show amount selector UI (preset amounts: 21, 100, 500, 1000, 5000 sats)
- Resolve LNURL from recipient profile
- Create and sign zap request
- Fetch bolt11 invoice
- Call `walletPayInvoice(bolt11)` from init-ndk.mjs
- Handle success/failure states in UI

#### Step 5: Fix `extractZapAmount()`

Port the pattern from [`zapInvoiceFromEvent()`](ndk/core/src/zap/invoice.ts:46) which uses `light-bolt11-decoder`:
- The NDK bundle includes `getSatoshisAmountFromBolt11()` which parses the HRP
- For our main-thread code, implement a simple HRP parser:
  - Parse the prefix after `lnbc` to extract amount and multiplier
  - Multipliers: `m` = milli (0.001), `u` = micro (0.000001), `n` = nano, `p` = pico

#### Step 6: Zap Amount Selector UI

Create a small popup/modal:
- Preset buttons: ⚡21, ⚡100, ⚡500, ⚡1K, ⚡5K
- Custom amount input field
- Optional comment field
- Send button
- Shows wallet balance from Cashu
- Styled consistently with existing `.divPostItem` design

---

## Files to Modify

| File | Changes |
|------|---------|
| [`www/js/utilities.mjs`](www/js/utilities.mjs) | Enhance `htmlFormatText()` with nostr: entity regex and rendering |
| [`www/js/post-interactions.mjs`](www/js/post-interactions.mjs) | Add async entity hydration after render; implement full zap flow in `handleZapClick()`; fix `extractZapAmount()`; add zap amount selector UI |
| [`www/css/client.css`](www/css/client.css) | Add styles for `.nostr-mention`, `.nostr-embed`, `.zap-selector` |
| [`www/post.html`](www/post.html) | Import `walletPayInvoice` from init-ndk.mjs; pass to interactions init |

## Dependencies

- `window.NostrTools.nip19.decode()` — already available
- `walletPayInvoice()` — already available in init-ndk.mjs
- `fetchEventsFromAllRelays()` — already available
- `fetchCachedProfile()` / `fetchProfile()` — already available
- `window.nostr.signEvent()` — available via NIP-07 signer
- LNURL resolution requires CORS-friendly lightning address servers (standard for most)

## Risks & Considerations

- **CORS on LNURL servers**: Most lightning address providers support CORS, but some may not. Consider a fallback message.
- **Cashu wallet balance**: User needs sufficient balance. Show balance in zap selector and handle insufficient funds gracefully.
- **Recursive embeds**: A `nostr:nevent` could reference a post that itself contains `nostr:` entities. Limit embed depth to 1 level.
- **Performance**: Fetching referenced events for embeds should be non-blocking. Show placeholder first, hydrate async.
- **BOLT11 parsing**: The simple HRP-based parser handles most invoices but edge cases exist. The `light-bolt11-decoder` in the NDK bundle is more robust but not directly accessible from the main thread.
