# Lightweight Cashu Balance at Startup

## Problem

The current wallet balance display requires the full `NDKCashuWallet` machinery:
1. `relay-ui.mjs` calls `bootstrapWalletForFooter()` → `walletInit()`
2. Worker runs `loadCashuWalletFromRelays()` → fetches kind 17375, decrypts, instantiates `NDKCashuWallet`
3. Worker runs `ensureWalletStarted()` → calls `wallet.start()` which fetches kind 7375 + 7376 + deletions, decrypts all proofs, sets up subscriptions
4. Only then is a balance available

This is **heavy, slow, and fragile**. The footer shows "0 sats" for many seconds (or forever if something fails).

## Solution

Fetch kind 17375 + 7375 + 5 events **at startup in the worker** (alongside profile and relay list), keep persistent subscriptions for live updates, decrypt the kind 7375 token events, sum proof amounts, and broadcast the balance to all tabs via the existing `walletBalanceUpdated` message. The footer balance updates instantly without ever touching `NDKCashuWallet`.

### Key Insight: NDKCashuWallet Benefits from Pre-Caching

`NDKCashuWallet.start()` in `ndk/wallet/src/wallets/cashu/wallet/index.ts:359` explicitly reads from the Dexie cache first:

```javascript
if (this.ndk.cacheAdapter) {
    const events = await this.ndk.fetchEvents(
        [{ kinds: [NDKKind.CashuToken], authors: [pubkey] }],
        { cacheUsage: NDKSubscriptionCacheUsage.ONLY_CACHE }
    );
    // Process cached events → emit balance_updated
}
```

Since our startup fetch populates the same Dexie cache, when `NDKCashuWallet.start()` later runs on cashu.html it will find all token events already cached → instant optimistic balance, then use `NDKSync` to only fetch the diff.

## What's Already Implemented (Phase 1) ✅

### Startup Event Downloads
**File: `www/ndk-worker.js`** — function `fetchStartupEventDownloads(pubkey)` (line ~1084)

At startup, `handleInit()` now fetches these events sequentially with status broadcasting:
- Kind `3` (Contact List / follows) — limit 1
- Kind `17375` (Cashu Wallet definition) — limit 5
- Kind `7375` (Cashu Token events) — limit 500
- Kind `5` with `#k:["7375"]` (Token deletions) — limit 500

All fetched events are automatically cached in Dexie by NDK.

### Persistent Wallet Subscriptions
**File: `www/ndk-worker.js`** — function `ensureStartupWalletSubscriptions(pubkey)` (line ~1140)

After the initial fetch, a persistent subscription (`closeOnEose: false`) is opened for:
- `{ kinds: [17375], authors: [pubkey] }` — wallet metadata changes
- `{ kinds: [7375], authors: [pubkey] }` — new/updated token events
- `{ kinds: [5], authors: [pubkey], '#k': ['7375'] }` — token deletions

Events received via subscription are logged to the startup status stream.

### Startup Status Broadcasting
**File: `www/ndk-worker.js`** — function `broadcastStartupStatus(message, meta)` (line ~1230)

Every step of `handleInit()` broadcasts a `startupStatus` message with human-readable text and metadata (stage, state, count, durationMs). Messages include:
- `Initializing NDK worker...`
- `Connecting to relays...`
- `Fetching user settings (kind 30078)...`
- `Fetching user profile info (kind 0)...`
- `Fetching user relay list (kind 10002)...`
- `Subscribing to relay updates (kind 10002)...`
- `Fetching user follows (kind 3)...` → `done (N)`
- `Fetching user wallet information (kind 17375)...` → `done (N)`
- `Fetching user wallet tokens (kind 7375)...` → `done (N)`
- `Fetching user token deletions (kind 5 for 7375)...` → `done (N)`
- `Subscribing to wallet updates (kinds 17375/7375/5)...`
- `✓ Startup complete`

### Status Event Plumbing
**File: `www/js/init-ndk.mjs`** — `handleWorkerMessage()` dispatches `ndkStartupStatus` window events.

### Footer Status Display (index.html only)
**File: `www/index.html`** — Listens for `ndkStartupStatus` events, displays in `divFooterCenter`, logs to console. Messages persist for 12 seconds before the periodic `UpdateFooter` clears them.

## What Remains (Phase 2) — Lightweight Balance Computation

### Overview

The startup fetch and subscription infrastructure is in place. What's missing is:
1. **Decrypting** the kind 7375 token events (NIP-44 encrypted)
2. **Summing** the proof amounts to compute a balance
3. **Broadcasting** the balance via the existing `walletBalanceUpdated` message
4. **Updating** the balance when new events arrive via the persistent subscription
5. **Simplifying** `relay-ui.mjs` to stop calling `walletInit()` for the footer balance

### Step 1: Add lightweight balance state variables

**File: `www/ndk-worker.js`**

Add alongside existing worker state (near line 99):
```javascript
// Lightweight wallet balance state (independent of NDKCashuWallet)
let lightwalletBalance = 0;
let lightwalletBalanceByMint = {};  // mintUrl -> amount
let lightwalletTokenEvents = new Map();  // eventId -> { mint, unit, proofTotal }
let lightwalletDeletedIds = new Set();   // event IDs that have been NIP-09 deleted
let lightwalletHasWallet = false;
```

### Step 2: Add helper functions

**`decryptTokenEvent(event, pubkey)`** — Decrypts kind 7375 content via NIP-44:
```javascript
async function decryptTokenEvent(event, pubkey) {
    const decrypted = await messageSigner.requestFromPage('nip44Decrypt', {
        senderPubkey: pubkey,
        ciphertext: event.content
    });
    const plaintext = typeof decrypted === 'string' ? decrypted : decrypted?.plaintext;
    return JSON.parse(plaintext);
}
```

**`recomputeLightwalletBalance()`** — Sums proofs, skips deleted tokens:
```javascript
function recomputeLightwalletBalance() {
    let total = 0;
    const byMint = {};
    for (const [eventId, token] of lightwalletTokenEvents) {
        if (lightwalletDeletedIds.has(eventId)) continue;
        total += token.proofTotal;
        byMint[token.mint] = (byMint[token.mint] || 0) + token.proofTotal;
    }
    lightwalletBalance = total;
    lightwalletBalanceByMint = byMint;
}
```

### Step 3: Process fetched events in `fetchStartupEventDownloads()`

After the existing fetch loop completes, add a processing step that:
1. Iterates the fetched kind 5 events → populates `lightwalletDeletedIds`
2. Iterates the fetched kind 7375 events → decrypts each via NIP-44, extracts proof amounts, populates `lightwalletTokenEvents`, handles `del` field
3. Calls `recomputeLightwalletBalance()`
4. Broadcasts `walletBalanceUpdated` with the computed balance

**Important:** The fetched events are already in the NDK result sets from `fetchStartupEventDownloads()`. We need to capture those result sets (currently discarded) and pass them to the processing step.

### Step 4: Process subscription events in `ensureStartupWalletSubscriptions()`

Update the existing subscription event handler to:
- On kind 7375: decrypt, add to `lightwalletTokenEvents`, handle `del` field, recompute, broadcast
- On kind 5: add referenced event IDs to `lightwalletDeletedIds`, recompute, broadcast

### Step 5: Modify `getWalletBalancePayload()` fallback

When `NDKCashuWallet` is not active, fall back to lightweight balance:
```javascript
function getWalletBalancePayload() {
    if (cashuWallet && cashuWalletStatus === 'ready') {
        // existing full wallet logic
        ...
    }
    // Fallback to lightweight balance
    return {
        balance: lightwalletBalance,
        balancesByMint: { ...lightwalletBalanceByMint }
    };
}
```

### Step 6: Simplify `relay-ui.mjs` footer bootstrap

Remove the `walletInit()` call from `bootstrapWalletForFooter()`. The balance arrives automatically via `ndkWalletBalance` events from the worker's lightweight computation. Keep the existing event listeners as-is.

## NIP-44 Decryption Consideration

The kind 7375 token events are NIP-44 encrypted. The worker already has `messageSigner.requestFromPage()` for NIP-44 decryption (used for user settings kind 30078). The same mechanism will be used here.

**Important:** Decryption requires a connected page with signing capability. Since `handleInit()` is called from a page that just authenticated, the signing port will be available. For the persistent subscription, new events will be decrypted as they arrive (the signing port remains active as long as any tab is open).

## Risk Mitigation

- **Decryption failure:** If NIP-44 decryption fails for any token event, skip it and log a warning. The balance will be a lower bound.
- **No wallet:** If no kind 17375 event exists, skip balance computation. `lightwalletHasWallet` stays false.
- **Race with NDKCashuWallet:** When the full wallet starts on cashu.html, it takes over balance reporting via `getWalletBalancePayload()`. No conflict.
- **Footer center conflicts:** Startup status messages auto-clear. Page `UpdateFooter` functions naturally take over.

## Files Modified

| File | Phase 1 (Done) | Phase 2 (Remaining) |
|------|----------------|---------------------|
| `www/ndk-worker.js` | `broadcastStartupStatus()`, `fetchStartupEventDownloads()`, `ensureStartupWalletSubscriptions()`, status calls in `handleInit()` | Lightwallet state vars, `decryptTokenEvent()`, `recomputeLightwalletBalance()`, process events in fetch + subscription, modify `getWalletBalancePayload()` |
| `www/js/init-ndk.mjs` | `startupStatus` message handler | No changes needed |
| `www/index.html` | `ndkStartupStatus` listener + console logging | No changes needed |
| `www/js/relay-ui.mjs` | No changes yet | Remove `walletInit()` from `bootstrapWalletForFooter()` |
