# Cashu Direct Wallet — Full NDK Wallet Removal

## Goal

Strip out all `NDKCashuWallet` usage from `ndk-worker.js` and replace with direct `@cashu/cashu-ts` operations. The NDK wallet abstraction is unreliable — ephemeral state, slow startup, black-box proof management. We replace it with the same proven pattern from `NostrTerminal_5/nt.mjs`: own the proofs, call cashu-ts directly.

## What gets removed from ndk-worker.js

All of these functions/variables are NDK wallet code that will be deleted:

- `NDKCashuWallet` import and reference
- `cashuWallet` variable and `cashuWalletStatus`
- `walletStartInFlight`
- `ensureCashuWalletClass()`
- `loadCashuWalletFromRelays()`
- `ensureCashuWallet()`
- `logWalletKindDiagnostics()`
- `bindCashuWalletListeners()` / `clearWalletListeners()` / `walletTxUnsubscribe` / `walletBalanceListenerUnsub`
- `ensureWalletStarted()`
- `syncLightwalletFromFullWallet()`
- `getWalletDebugSnapshot()`
- `getProofBalanceSnapshot()` (replaced by simpler direct proof sum)
- All `[Worker][diag]` diagnostic logging added for NDK wallet debugging

## What stays in ndk-worker.js

- NDK core: `initNDK()`, relay management, subscriptions, event publishing
- `MessageBasedSigner` for NIP-44 encryption
- Lightweight balance system: `lightwalletTokenEvents`, `lightwalletBalance`, `recomputeLightwalletBalance()`, `broadcastLightwalletBalance()`, `processLightwalletTokenEvent()`, `hydrateLightwalletFromFetchedEvents()`
- Startup event downloads: `fetchStartupEventDownloads()`, `ensureStartupWalletSubscriptions()`
- User settings system
- All relay/subscription/publish infrastructure
- `broadcast()`, `broadcastWalletStatus()`, `broadcastStartupStatus()`

## Architecture

```
cashu.html ──► cashu-wallet.mjs ──► init-ndk.mjs ──► ndk-worker.js
                                                         │
                                                    DirectCashu
                                                    ┌──────────┐
                                                    │proofStore │ { mint: Proof[] }
                                                    │mintList   │ string[]
                                                    │send()     │ cashu-ts wallet.send
                                                    │receive()  │ cashu-ts wallet.receive
                                                    │deposit()  │ cashu-ts mint quote
                                                    │publish()  │ NDK kind 7375/17375
                                                    └──────────┘
```

No changes to cashu.html, cashu-wallet.mjs, or init-ndk.mjs — same worker message API.

## Implementation Steps

### Step 1: Expose cashu-ts from bundle

**File**: `build/ndk-entry.js`

Add re-exports so the worker can access cashu-ts primitives:
```js
export { CashuMint, CashuWallet } from '@cashu/cashu-ts';
export { getEncodedTokenV4, getDecodedToken } from '@cashu/cashu-ts';
```

Rebuild bundle: `node build-ndk-bundle.js`

Verify in worker:
```js
const CashuMint = NDKExports?.CashuMint;
const CashuWallet = NDKExports?.CashuWallet;
```

### Step 2: Direct proof store and balance

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

New state:
```js
const directProofStore = {};   // { mintUrl: Proof[] }
let directMintList = [];       // from kind 17375
let directWalletLoaded = false;
```

Hydrate from existing lightweight token events — we already download and decrypt kind 7375 events during startup. Instead of just counting sats, parse the full proof objects:
```js
function hydrateDirectProofStore() {
    directProofStore = {};
    for (const [id, entry] of lightwalletTokenEvents) {
        // entry.decrypted contains { mint, proofs }
        const data = JSON.parse(entry.decrypted);
        if (!directProofStore[data.mint]) directProofStore[data.mint] = [];
        directProofStore[data.mint].push(...data.proofs);
    }
    // Deduplicate proofs by secret
    for (const mint of Object.keys(directProofStore)) {
        const seen = new Set();
        directProofStore[mint] = directProofStore[mint].filter(p => {
            if (seen.has(p.secret)) return false;
            seen.add(p.secret);
            return true;
        });
    }
    directWalletLoaded = true;
}
```

Balance is just a sum:
```js
function getDirectBalance() {
    let total = 0;
    const byMint = {};
    for (const [mint, proofs] of Object.entries(directProofStore)) {
        const mintTotal = proofs.reduce((s, p) => s + p.amount, 0);
        byMint[mint] = mintTotal;
        total += mintTotal;
    }
    return { balance: total, balancesByMint: byMint };
}
```

### Step 3: Direct send

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

```js
async function directSend(amount, mintUrl) {
    const mint = new CashuMint(mintUrl);
    const wallet = new CashuWallet(mint);
    await wallet.loadMint();  // loads keysets

    const proofs = directProofStore[mintUrl] || [];
    const { keep, send } = await wallet.send(amount, proofs);

    directProofStore[mintUrl] = keep;
    const token = getEncodedTokenV4({ mint: mintUrl, proofs: send });

    await publishDirectProofs();
    return { token, amount, mint: mintUrl, ...getDirectBalance() };
}
```

### Step 4: Direct receive

```js
async function directReceive(encodedToken) {
    const decoded = getDecodedToken(encodedToken);
    const mint = new CashuMint(decoded.mint);
    const wallet = new CashuWallet(mint);

    const receiveProofs = await wallet.receive(encodedToken);

    if (!directProofStore[decoded.mint]) directProofStore[decoded.mint] = [];
    directProofStore[decoded.mint].push(...receiveProofs);

    await publishDirectProofs();
    return { success: true, ...getDirectBalance() };
}
```

### Step 5: Direct deposit

```js
async function directDeposit(amount, mintUrl) {
    const mint = new CashuMint(mintUrl);
    const wallet = new CashuWallet(mint);
    const quote = await wallet.createMintQuote(amount);

    return {
        invoice: quote.request,
        quoteId: quote.quote,
        mint: mintUrl,
        amount,
        // Caller polls with directCheckDeposit()
    };
}

async function directCheckDeposit(mintUrl, quoteId, amount) {
    const mint = new CashuMint(mintUrl);
    const wallet = new CashuWallet(mint);
    const checked = await wallet.checkMintQuote(quoteId);

    if (checked.state === 'PAID') {
        const proofs = await wallet.mintProofs(amount, quoteId);
        if (!directProofStore[mintUrl]) directProofStore[mintUrl] = [];
        directProofStore[mintUrl].push(...proofs);
        directProofStore[mintUrl].sort((a, b) => a.amount - b.amount);
        await publishDirectProofs();
        return { paid: true, ...getDirectBalance() };
    }
    return { paid: false };
}
```

### Step 6: Direct Lightning payment

```js
async function directPayInvoice(invoice, mintUrl) {
    const mint = new CashuMint(mintUrl);
    const wallet = new CashuWallet(mint);
    await wallet.loadMint();

    const proofs = directProofStore[mintUrl] || [];
    // meltQuote + meltProofs flow
    const meltQuote = await wallet.createMeltQuote(invoice);
    const totalNeeded = meltQuote.amount + meltQuote.fee_reserve;

    const { keep, send } = await wallet.send(totalNeeded, proofs);
    const result = await wallet.meltProofs(meltQuote, send);

    directProofStore[mintUrl] = keep;
    if (result.change && result.change.length > 0) {
        directProofStore[mintUrl].push(...result.change);
    }

    await publishDirectProofs();
    return { success: true, preimage: result.preimage, ...getDirectBalance() };
}
```

### Step 7: Nostr persistence — publish proofs

```js
async function publishDirectProofs() {
    // 1. Fetch existing kind 7375 events
    const existing7375 = await ndk.fetchEvents({
        kinds: [7375],
        authors: [currentPubkey]
    });

    // 2. Publish new 7375 for each mint
    for (const [mintUrl, proofs] of Object.entries(directProofStore)) {
        const content = JSON.stringify({ mint: mintUrl, proofs });
        const encrypted = await messageSigner.requestFromPage('nip44Encrypt', {
            recipientPubkey: currentPubkey,
            plaintext: content
        });
        const event = new NDKEvent(ndk, {
            kind: 7375,
            content: typeof encrypted === 'string' ? encrypted : encrypted?.ciphertext
        });
        await event.publish();
    }

    // 3. Delete old 7375 events
    for (const oldEvent of existing7375) {
        const deleteEvent = new NDKEvent(ndk, {
            kind: 5,
            tags: [['e', oldEvent.id]]
        });
        await deleteEvent.publish();
    }

    // 4. Update kind 17375 wallet metadata
    const mintTags = Object.keys(directProofStore).map(m => ['mint', m]);
    const walletContent = JSON.stringify(mintTags);
    const encryptedWallet = await messageSigner.requestFromPage('nip44Encrypt', {
        recipientPubkey: currentPubkey,
        plaintext: walletContent
    });
    const walletEvent = new NDKEvent(ndk, {
        kind: 17375,
        content: typeof encryptedWallet === 'string' ? encryptedWallet : encryptedWallet?.ciphertext
    });
    await walletEvent.publish();
}
```

### Step 8: Proof checking

```js
async function directCheckProofs() {
    let totalChecked = 0;
    let totalRemoved = 0;

    for (const [mintUrl, proofs] of Object.entries(directProofStore)) {
        if (proofs.length === 0) continue;

        const mint = new CashuMint(mintUrl);
        const wallet = new CashuWallet(mint);

        const secrets = proofs.map(p => p.secret);
        const states = await wallet.checkProofsStates(proofs);

        totalChecked += proofs.length;
        const validProofs = proofs.filter((p, i) =>
            states[i]?.state === 'UNSPENT'
        );
        totalRemoved += proofs.length - validProofs.length;
        directProofStore[mintUrl] = validProofs;
    }

    if (totalRemoved > 0) {
        await publishDirectProofs();
    }

    return {
        proofsChecked: totalChecked,
        proofsRemoved: totalRemoved,
        ...getDirectBalance()
    };
}
```

### Step 9: Replace all handler functions

Replace these handlers to use direct functions instead of NDKCashuWallet:

| Handler | Before | After |
|---------|--------|-------|
| `handleWalletInit` | `loadCashuWalletFromRelays()` | `hydrateDirectProofStore()` |
| `handleWalletStart` | `ensureWalletStarted()` | no-op (already hydrated) |
| `handleWalletCreate` | `NDKCashuWallet.create()` | `publishDirectProofs()` with initial mints |
| `handleWalletGetBalance` | `ensureWalletStarted()` + stale fields | `getDirectBalance()` |
| `handleWalletGetMints` | `cashuWallet.mints` | `Object.keys(directProofStore)` |
| `handleWalletSendToken` | `ensureWalletStarted()` + `active.send()` | `directSend()` |
| `handleWalletReceiveToken` | `active.receiveToken()` | `directReceive()` |
| `handleWalletCreateDeposit` | `active.deposit()` | `directDeposit()` |
| `handleWalletPayInvoice` | `active.lnPay()` | `directPayInvoice()` |
| `handleWalletCheckProofs` | `active.consolidateTokens()` | `directCheckProofs()` |
| `handleWalletAddMint` | `active.addMint()` | add to directProofStore + publish |
| `handleWalletRemoveMint` | `active.removeMint()` | remove from directProofStore + publish |
| `handleWalletGetTransactions` | `active.fetchTransactions()` | fetch kind 7376 events directly |
| `handleWalletShutdown` | `cashuWallet.stop()` | clear directProofStore |

### Step 10: Delete all NDK wallet code

Remove from `ndk-worker.js`:
- `NDKCashuWallet` import line
- `cashuWallet`, `cashuWalletStatus`, `walletStartInFlight` variables
- `ensureCashuWalletClass()`
- `loadCashuWalletFromRelays()`
- `ensureCashuWallet()`
- `logWalletKindDiagnostics()`
- `bindCashuWalletListeners()`, `clearWalletListeners()`
- `walletTxUnsubscribe`, `walletBalanceListenerUnsub`
- `ensureWalletStarted()`
- `syncLightwalletFromFullWallet()`
- `getWalletDebugSnapshot()`
- `getProofBalanceSnapshot()`
- All `[Worker][diag]` diagnostic logging
- `pendingDeposits` Map (replace with simpler polling)

## Deployment Strategy

This is a single deployment — not incremental. All handler replacements happen together because the worker message API stays the same. The UI never knows the difference.

1. Modify `build/ndk-entry.js` to export cashu-ts
2. Rebuild bundle
3. Rewrite handler section of `ndk-worker.js`
4. Test locally
5. Deploy

## Risk Mitigation

- **Git tag before changes** — can revert to v0.3.45 instantly
- **Same Nostr event format** — kind 7375/17375 compatible with other wallets
- **Same worker message API** — cashu.html and cashu-wallet.mjs unchanged
- **Proofs already downloaded** — lightweight system already has the data we need
