From 65aff7cc8740448c521de81c45334aa07b7ad969 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Jan 2026 22:42:54 +0000 Subject: [PATCH] feat: add NIP-60/61 and applesauce-wallet skills Add comprehensive skill documentation for: - NIP-60: Cashu wallets on Nostr (kind:17375, 7375, 7376, 7374) - NIP-61: Nutzaps - P2PK-locked Cashu payments (kind:9321, 10019) - applesauce-wallet: hzrd149's package for NIP-60 implementation including actions, casts, helpers, and integration patterns --- .claude/skills/applesauce-wallet/SKILL.md | 586 ++++++++++++++++++++++ .claude/skills/nip-60-61/SKILL.md | 369 ++++++++++++++ 2 files changed, 955 insertions(+) create mode 100644 .claude/skills/applesauce-wallet/SKILL.md create mode 100644 .claude/skills/nip-60-61/SKILL.md diff --git a/.claude/skills/applesauce-wallet/SKILL.md b/.claude/skills/applesauce-wallet/SKILL.md new file mode 100644 index 0000000..86f8416 --- /dev/null +++ b/.claude/skills/applesauce-wallet/SKILL.md @@ -0,0 +1,586 @@ +--- +name: applesauce-wallet +description: This skill should be used when building NIP-60 Cashu wallets using the applesauce-wallet package. Covers wallet actions (CreateWallet, UnlockWallet, ReceiveNutzaps, TokensOperation), casts (Wallet, WalletToken, Nutzap), helpers (IndexedDBCouch), and integration with the applesauce ecosystem. +--- + +# applesauce-wallet Package + +## Purpose + +This skill provides expert-level assistance with `applesauce-wallet`, a TypeScript package for building NIP-60 Cashu wallets and NIP-61 nutzaps on Nostr. Part of the applesauce ecosystem by hzrd149. + +## When to Use + +Activate this skill when: +- Building a NIP-60 wallet with applesauce +- Implementing nutzap sending/receiving +- Managing Cashu tokens on Nostr +- Working with wallet actions and casts +- Integrating wallet functionality into a Nostr client + +## Installation + +```bash +npm install applesauce-wallet +# Also need related packages: +npm install applesauce-core applesauce-actions applesauce-react @cashu/cashu-ts +``` + +## Package Structure + +### Imports + +```typescript +// Actions - wallet operations +import { + CreateWallet, + UnlockWallet, + ReceiveNutzaps, + ReceiveToken, + ConsolidateTokens, + RecoverFromCouch, + SetWalletMints, + SetWalletRelays, + AddNutzapInfoMint, + RemoveNutzapInfoMint, + TokensOperation, +} from "applesauce-wallet/actions"; + +// Casts - reactive data wrappers +import { + Wallet, + WalletToken, + WalletHistory, + Nutzap, +} from "applesauce-wallet/casts"; + +// Helpers - utilities and constants +import { + getWalletRelays, + IndexedDBCouch, + WALLET_KIND, + WALLET_TOKEN_KIND, + WALLET_HISTORY_KIND, + NUTZAP_KIND, +} from "applesauce-wallet/helpers"; + +// IMPORTANT: Import casts to enable user.wallet$ property +import "applesauce-wallet/casts"; +``` + +## Event Kind Constants + +```typescript +const WALLET_KIND = 17375; // Wallet configuration +const WALLET_TOKEN_KIND = 7375; // Token storage +const WALLET_HISTORY_KIND = 7376; // Spending history +const NUTZAP_KIND = 9321; // Nutzap events +const NUTZAP_INFO_KIND = 10019; // Nutzap recipient info +``` + +## Core Setup + +### Prerequisites + +```typescript +import { EventStore, EventFactory } from "applesauce-core"; +import { ActionRunner } from "applesauce-actions"; +import { RelayPool } from "applesauce-relay"; +import { ProxySigner } from "applesauce-accounts"; +import { persistEncryptedContent } from "applesauce-common/helpers"; +import { IndexedDBCouch } from "applesauce-wallet/helpers"; + +// Create singletons +const eventStore = new EventStore(); +const pool = new RelayPool(); +const couch = new IndexedDBCouch(); + +// Setup encrypted content persistence +const storage$ = new BehaviorSubject(null); +persistEncryptedContent(eventStore, storage$.pipe(defined())); + +// Create action runner +const factory = new EventFactory({ + signer: new ProxySigner(signer$.pipe(defined())) +}); + +const actions = new ActionRunner(eventStore, factory, async (event) => { + // Publish handler - determine relays and publish + const relays = getPublishRelays(event); + await pool.publish(relays, event); +}); +``` + +## Wallet Casts + +### Wallet + +The main wallet cast provides reactive observables for wallet state. + +```typescript +import { castUser } from "applesauce-common/casts"; +import "applesauce-wallet/casts"; // Enable wallet$ property + +const user = castUser(pubkey, eventStore); + +// Access wallet +const wallet = use$(user.wallet$); + +// Wallet properties (all are observables) +wallet.unlocked // boolean - whether wallet is decrypted +wallet.mints$ // string[] - configured mints +wallet.relays$ // string[] - wallet relays +wallet.balance$ // { [mint: string]: number } - balance by mint +wallet.tokens$ // WalletToken[] - all token events +wallet.received$ // string[] - received nutzap IDs +``` + +### WalletToken + +Represents a kind:7375 token event. + +```typescript +interface WalletToken { + id: string; // Event ID + event: NostrEvent; // Raw event + unlocked: boolean; // Is content decrypted + mint?: string; // Mint URL (when unlocked) + proofs?: Proof[]; // Cashu proofs (when unlocked) + seen?: string[]; // Relays where seen + + // Observables + meta$: Observable; // Parsed metadata + amount$: Observable; // Total amount +} +``` + +### WalletHistory + +Represents a kind:7376 history event. + +```typescript +interface WalletHistory { + id: string; + event: NostrEvent; + unlocked: boolean; + + // Observable + meta$: Observable<{ + direction: "in" | "out"; + amount: number; + mint?: string; + unit?: string; + }>; +} +``` + +### Nutzap + +Represents a kind:9321 nutzap event. + +```typescript +interface Nutzap { + id: string; + event: NostrEvent; + amount: number; // Total nutzap amount + mint?: string; // Mint URL + comment?: string; // Message content + createdAt: Date; // Event timestamp + + // Cast references + sender: User; // Sender user cast + + // Observables + zapped$: Observable; // Referenced event +} +``` + +## User Extensions + +When you import `"applesauce-wallet/casts"`, the User cast is extended with: + +```typescript +// Available on User cast +user.wallet$ // Observable +user.nutzap$ // Observable + +// NutzapInfo structure +interface NutzapInfo { + mints: Array<{ mint: string; units: string[] }>; + relays?: string[]; + pubkey?: string; // P2PK pubkey for receiving +} +``` + +## Wallet Actions + +### CreateWallet + +Creates a new NIP-60 wallet. + +```typescript +import { CreateWallet } from "applesauce-wallet/actions"; +import { generateSecretKey } from "nostr-tools"; + +await actions.run(CreateWallet, { + mints: ["https://mint1.example.com", "https://mint2.example.com"], + privateKey: generateSecretKey(), // For nutzap reception (optional) + relays: ["wss://relay1.example.com", "wss://relay2.example.com"] +}); +``` + +### UnlockWallet + +Decrypts wallet content using NIP-44. + +```typescript +import { UnlockWallet } from "applesauce-wallet/actions"; + +await actions.run(UnlockWallet, { + history: true, // Also unlock history events + tokens: true // Also unlock token events +}); +``` + +### ReceiveToken + +Receives a Cashu token and adds it to the wallet. + +```typescript +import { ReceiveToken } from "applesauce-wallet/actions"; +import { getDecodedToken } from "@cashu/cashu-ts"; + +const token = getDecodedToken(tokenString); +await actions.run(ReceiveToken, token, { couch }); +``` + +### ReceiveNutzaps + +Claims one or more nutzap events. + +```typescript +import { ReceiveNutzaps } from "applesauce-wallet/actions"; + +// Single nutzap +await actions.run(ReceiveNutzaps, nutzapEvent, couch); + +// Multiple nutzaps +const nutzapEvents = nutzaps.map(n => n.event); +await actions.run(ReceiveNutzaps, nutzapEvents, couch); +``` + +### TokensOperation + +Generic operation on wallet tokens. Used for sending, swapping, etc. + +```typescript +import { TokensOperation } from "applesauce-wallet/actions"; + +await actions.run( + TokensOperation, + amount, // Amount to operate on + async ({ selectedProofs, mint, cashuWallet }) => { + // cashuWallet is a @cashu/cashu-ts Wallet instance + const { keep, send } = await cashuWallet.ops + .send(amount, selectedProofs) + .run(); + + return { + change: keep.length > 0 ? keep : undefined + }; + }, + { mint: selectedMint, couch } // Options +); +``` + +### SetWalletMints + +Updates the wallet's configured mints. + +```typescript +import { SetWalletMints } from "applesauce-wallet/actions"; + +const newMints = ["https://mint1.com", "https://mint2.com"]; +await actions.run(SetWalletMints, newMints); +``` + +### SetWalletRelays + +Updates the wallet's relays. + +```typescript +import { SetWalletRelays } from "applesauce-wallet/actions"; + +const newRelays = ["wss://relay1.com", "wss://relay2.com"]; +await actions.run(SetWalletRelays, newRelays); +``` + +### AddNutzapInfoMint / RemoveNutzapInfoMint + +Manages mints in the user's kind:10019 nutzap info. + +```typescript +import { AddNutzapInfoMint, RemoveNutzapInfoMint } from "applesauce-wallet/actions"; + +// Add mint to nutzap config +await actions.run(AddNutzapInfoMint, { + url: "https://mint.example.com", + units: ["sat"] +}); + +// Remove mint from nutzap config +await actions.run(RemoveNutzapInfoMint, "https://mint.example.com"); +``` + +### ConsolidateTokens + +Merges multiple small tokens into fewer larger ones. + +```typescript +import { ConsolidateTokens } from "applesauce-wallet/actions"; + +await actions.run(ConsolidateTokens, { + unlockTokens: true, + couch +}); +``` + +### RecoverFromCouch + +Recovers tokens stored in the couch during failed operations. + +```typescript +import { RecoverFromCouch } from "applesauce-wallet/actions"; + +await actions.run(RecoverFromCouch, couch); +``` + +## IndexedDBCouch + +The "couch" is temporary storage for proofs during operations that could fail. + +```typescript +import { IndexedDBCouch } from "applesauce-wallet/helpers"; + +const couch = new IndexedDBCouch(); + +// Used in operations +await actions.run(ReceiveToken, token, { couch }); +await actions.run(TokensOperation, amount, callback, { couch }); +``` + +**Why use a couch?** +- Prevents losing proofs if app crashes mid-operation +- Enables recovery of stuck transactions +- Provides atomic operation semantics + +## Subscribing to Wallet Events + +```typescript +import { use$ } from "applesauce-react/hooks"; +import { relaySet } from "applesauce-core/helpers"; + +// Subscribe to wallet-related events +use$(() => { + const relays = relaySet(walletRelays, userOutboxes); + if (relays.length === 0) return undefined; + + return pool.subscription( + relays, + [ + // Wallet events + { + kinds: [WALLET_KIND, WALLET_TOKEN_KIND, WALLET_HISTORY_KIND], + authors: [user.pubkey] + }, + // Token deletions + { + kinds: [kinds.EventDeletion], + "#k": [String(WALLET_TOKEN_KIND)] + } + ], + { eventStore } + ); +}, [walletRelays, userOutboxes, user.pubkey]); + +// Subscribe to incoming nutzaps +use$(() => { + const relays = relaySet(nutzapRelays, userInboxes); + if (relays.length === 0) return undefined; + + return pool.subscription( + relays, + { kinds: [NUTZAP_KIND], "#p": [user.pubkey] }, + { eventStore } + ); +}, [nutzapRelays, userInboxes, user.pubkey]); +``` + +## Complete Send Example + +```typescript +import { TokensOperation } from "applesauce-wallet/actions"; +import { getEncodedToken } from "@cashu/cashu-ts"; + +async function sendTokens(amount: number, selectedMint?: string) { + let createdToken: string | null = null; + + await actions.run( + TokensOperation, + amount, + async ({ selectedProofs, mint, cashuWallet }) => { + const { keep, send } = await cashuWallet.ops + .send(amount, selectedProofs) + .run(); + + // Encode token for sharing + createdToken = getEncodedToken({ + mint, + proofs: send, + unit: "sat" + }); + + return { + change: keep.length > 0 ? keep : undefined + }; + }, + { mint: selectedMint, couch } + ); + + return createdToken; +} +``` + +## Complete Receive Example + +```typescript +import { ReceiveToken } from "applesauce-wallet/actions"; +import { getDecodedToken } from "@cashu/cashu-ts"; + +async function receiveToken(tokenString: string) { + const token = getDecodedToken(tokenString.trim()); + + if (!token) { + throw new Error("Failed to decode token"); + } + + await actions.run(ReceiveToken, token, { couch }); +} +``` + +## Auto-Unlock Pattern + +```typescript +const unlocking = useRef(false); + +useEffect(() => { + if (unlocking.current || !autoUnlock) return; + + let needsUnlock = false; + + if (wallet && !wallet.unlocked) needsUnlock = true; + if (tokens?.some(t => !t.unlocked)) needsUnlock = true; + if (history?.some(h => !h.unlocked)) needsUnlock = true; + + if (needsUnlock) { + unlocking.current = true; + actions + .run(UnlockWallet, { history: true, tokens: true }) + .catch(console.error) + .finally(() => { + unlocking.current = false; + }); + } +}, [wallet?.unlocked, tokens?.length, history?.length, autoUnlock]); +``` + +## Nutzap Timeline + +```typescript +import { castTimelineStream } from "applesauce-common/observable"; +import { Nutzap } from "applesauce-wallet/casts"; + +const nutzaps = use$( + () => eventStore + .timeline({ kinds: [NUTZAP_KIND], "#p": [user.pubkey] }) + .pipe(castTimelineStream(Nutzap, eventStore)), + [user.pubkey] +); + +// Filter unclaimed nutzaps +const unclaimed = useMemo(() => { + if (!nutzaps || !received) return nutzaps || []; + return nutzaps.filter(n => !received.includes(n.id)); +}, [nutzaps, received]); +``` + +## Helper Functions + +### getWalletRelays + +Extract relay URLs from a wallet event. + +```typescript +import { getWalletRelays } from "applesauce-wallet/helpers"; + +const wallet = await firstValueFrom( + eventStore.replaceable(WALLET_KIND, pubkey) +); +const relays = wallet ? getWalletRelays(wallet) : []; +``` + +## Integration with cashu-ts + +The wallet actions use `@cashu/cashu-ts` internally. In `TokensOperation`: + +```typescript +async ({ selectedProofs, mint, cashuWallet }) => { + // cashuWallet is a @cashu/cashu-ts Wallet instance + // Already initialized and connected to the mint + + // Use cashu-ts WalletOps API + const { keep, send } = await cashuWallet.ops + .send(amount, selectedProofs) + .run(); + + // Return change proofs to be stored + return { change: keep }; +} +``` + +## Error Handling + +```typescript +try { + await actions.run(ReceiveNutzaps, nutzapEvents, couch); +} catch (err) { + if (err instanceof Error) { + console.error("Failed to receive nutzaps:", err.message); + } + // Tokens are safely stored in couch + // Can recover with RecoverFromCouch action +} +``` + +## Best Practices + +1. **Always use couch**: Pass `couch` to operations that modify tokens +2. **Unlock before operations**: Check `wallet.unlocked` before actions +3. **Handle recovery**: Periodically run `RecoverFromCouch` on app start +4. **Subscribe to events**: Keep wallet data synced via subscriptions +5. **Check mint support**: Verify mint is in wallet config before operations + +## Related Packages + +- `applesauce-core` - Core event store and utilities +- `applesauce-actions` - ActionRunner for executing actions +- `applesauce-react` - React hooks like `use$` +- `applesauce-common` - Common helpers and casts +- `@cashu/cashu-ts` - Cashu protocol implementation + +## Official Resources + +- [Applesauce Documentation](https://hzrd149.github.io/applesauce/) +- [GitHub Repository](https://github.com/hzrd149/applesauce) +- [NIP-60 Specification](https://github.com/nostr-protocol/nips/blob/master/60.md) +- [NIP-61 Specification](https://github.com/nostr-protocol/nips/blob/master/61.md) diff --git a/.claude/skills/nip-60-61/SKILL.md b/.claude/skills/nip-60-61/SKILL.md new file mode 100644 index 0000000..f9089d7 --- /dev/null +++ b/.claude/skills/nip-60-61/SKILL.md @@ -0,0 +1,369 @@ +--- +name: nip-60-61 +description: This skill should be used when implementing NIP-60 Cashu wallets or NIP-61 nutzaps on Nostr. Covers the wallet event structure (kind:17375), token events (kind:7375), spending history (kind:7376), nutzap events (kind:9321), nutzap info (kind:10019), P2PK locking, and wallet workflows. +--- + +# NIP-60 & NIP-61: Cashu Wallets and Nutzaps + +## Purpose + +This skill provides expert-level assistance with NIP-60 (Cashu Wallets) and NIP-61 (Nutzaps) - the Nostr protocols for storing ecash wallets on relays and sending P2PK-locked Cashu payments. + +## When to Use + +Activate this skill when: +- Implementing a NIP-60 compatible Cashu wallet +- Sending or receiving nutzaps (NIP-61) +- Working with encrypted wallet events +- Managing ecash proofs on Nostr relays +- Implementing P2PK-locked Cashu transfers +- Building nutzap-enabled Nostr clients + +## NIP-60: Cashu Wallets + +### Overview + +NIP-60 enables Cashu-based wallets with information stored on Nostr relays, providing: +- **Ease of use**: Users can receive funds without external accounts +- **Interoperability**: Wallet follows users across applications +- **Privacy**: Content encrypted with NIP-44 + +### Event Kinds + +| Kind | Name | Purpose | +|------|------|---------| +| **17375** | Wallet | Replaceable wallet configuration | +| **7375** | Token | Unspent proof storage | +| **7376** | History | Spending/receiving history | +| **7374** | Quote | Pending mint quotes | +| **10019** | Nutzap Info | Recipient preferences (NIP-61) | + +### Wallet Event (kind:17375) + +Replaceable event storing wallet configuration. Content is NIP-44 encrypted. + +```json +{ + "kind": 17375, + "content": "", + "tags": [ + ["mint", "https://mint1.example.com", "sat"], + ["mint", "https://mint2.example.com", "sat"], + ["relay", "wss://relay1.example.com"], + ["relay", "wss://relay2.example.com"] + ] +} +``` + +**Encrypted content structure:** +```json +{ + "privkey": "" +} +``` + +**Important**: The `privkey` is a **separate key** exclusively for the wallet, NOT the user's Nostr private key. Used for: +- Receiving NIP-61 nutzaps (P2PK-locked tokens) +- Unlocking tokens locked to the wallet's pubkey + +### Token Event (kind:7375) + +Records unspent Cashu proofs. Multiple events can exist per mint. + +```json +{ + "kind": 7375, + "content": "", + "tags": [] +} +``` + +**Encrypted content structure:** +```json +{ + "mint": "https://mint.example.com", + "unit": "sat", + "proofs": [ + { + "id": "009a1f293253e41e", + "amount": 8, + "secret": "secret_string", + "C": "02abc..." + } + ], + "del": ["", ""] +} +``` + +| Field | Description | +|-------|-------------| +| `mint` | Mint URL | +| `unit` | Currency unit (default: "sat") | +| `proofs` | Array of unspent Cashu proofs | +| `del` | IDs of deleted/spent token events | + +### Spending History Event (kind:7376) + +Optional events documenting transactions. + +```json +{ + "kind": 7376, + "content": "", + "tags": [ + ["e", "", "", "created"], + ["e", "", "", "destroyed"], + ["e", "", "", "redeemed"] + ] +} +``` + +**Encrypted content structure:** +```json +{ + "direction": "in", + "amount": 100, + "unit": "sat" +} +``` + +**Direction values:** +- `"in"` - Received tokens +- `"out"` - Sent tokens + +**Tag markers:** +- `created` - New token event created +- `destroyed` - Token event consumed/deleted +- `redeemed` - Nutzap event redeemed (leave unencrypted) + +### Quote Event (kind:7374) + +Tracks pending mint quotes for deposits. + +```json +{ + "kind": 7374, + "content": "", + "tags": [ + ["expiration", ""], + ["mint", "https://mint.example.com"] + ] +} +``` + +Expiration typically ~2 weeks. Delete after quote is used or expired. + +### Relay Discovery + +Clients discover wallet relays from: +1. Kind 10019 event (preferred) +2. Kind 10002 NIP-65 relay list (fallback) + +## NIP-61: Nutzaps + +### Overview + +Nutzaps are P2PK-locked Cashu tokens sent via Nostr. The payment itself serves as the receipt - no Lightning invoices needed. + +Key principle: **"A Nutzap is a P2PK Cashu token in which the payment itself is the receipt."** + +### Nutzap Info Event (kind:10019) + +Recipients publish this to configure nutzap reception. + +```json +{ + "kind": 10019, + "content": "", + "tags": [ + ["relay", "wss://relay1.example.com"], + ["relay", "wss://relay2.example.com"], + ["mint", "https://mint1.example.com", "sat"], + ["mint", "https://mint2.example.com", "sat", "usd"], + ["pubkey", "02"] + ] +} +``` + +| Tag | Description | +|-----|-------------| +| `relay` | Where senders should publish nutzaps | +| `mint` | Trusted mints with supported units | +| `pubkey` | P2PK pubkey for locking tokens (NOT Nostr key) | + +**Critical**: The pubkey MUST be prefixed with `02` for Nostr-Cashu compatibility. + +### Nutzap Event (kind:9321) + +The actual payment event sent by the payer. + +```json +{ + "kind": 9321, + "content": "Optional message/comment", + "pubkey": "", + "tags": [ + ["proof", "{\"id\":\"...\",\"amount\":8,\"secret\":\"...\",\"C\":\"...\",\"dleq\":{...}}"], + ["proof", "{\"id\":\"...\",\"amount\":4,\"secret\":\"...\",\"C\":\"...\",\"dleq\":{...}}"], + ["u", "https://mint.example.com"], + ["p", ""], + ["e", "", ""] + ] +} +``` + +| Tag | Description | +|-----|-------------| +| `proof` | JSON-stringified Cashu proof with DLEQ | +| `u` | Mint URL (must match recipient's config exactly) | +| `p` | Recipient's Nostr pubkey | +| `e` | Event being nutzapped (optional) | + +**Proof structure (in proof tag):** +```json +{ + "id": "009a1f293253e41e", + "amount": 8, + "secret": "[\"P2PK\",{\"nonce\":\"...\",\"data\":\"02pubkey\"}]", + "C": "02abc...", + "dleq": { + "e": "...", + "s": "...", + "r": "..." + } +} +``` + +### P2PK Locking Requirements + +1. **Use recipient's wallet pubkey** from kind:10019, NOT their Nostr pubkey +2. **Prefix with '02'** for compatibility: `02<32-byte-hex>` +3. **Include DLEQ proofs** for offline verification (NUT-12) +4. Lock using NUT-11 P2PK spending condition + +### Validation Rules + +Observers verifying nutzaps must confirm: +1. Recipient has kind:10019 listing the mint +2. Token is locked to recipient's specified pubkey +3. Mint URL matches exactly (case-sensitive) +4. DLEQ proof validates offline + +### Sending Workflow + +``` +1. Fetch recipient's kind:10019 + ↓ +2. Select a mint from their trusted list + ↓ +3. Mint or swap tokens at that mint + ↓ +4. P2PK-lock proofs to recipient's pubkey + ↓ +5. Publish kind:9321 to recipient's relays +``` + +### Receiving Workflow + +``` +1. Subscribe to kind:9321 events tagged with your pubkey + ↓ +2. Validate nutzap (mint in config, correct pubkey lock) + ↓ +3. Swap proofs at the mint (prevents double-claim) + ↓ +4. Create kind:7375 token event with new proofs + ↓ +5. Create kind:7376 history event with "redeemed" marker +``` + +## Wallet Workflows + +### Token Spending + +When spending tokens: + +1. **Select proofs** for the amount to spend +2. **Create new token event** with remaining (unspent) proofs +3. **Add spent token IDs** to the `del` field +4. **Delete original token event** via NIP-09 +5. **Create history event** (optional) documenting the transaction + +```typescript +// Pseudocode for spending +const { spend, keep } = await selectProofs(wallet.proofs, amount); + +// Create new token event with change +if (keep.length > 0) { + await createTokenEvent({ + proofs: keep, + del: [originalTokenEventId] + }); +} + +// Delete original token event +await deleteEvent(originalTokenEventId); + +// Record history +await createHistoryEvent({ + direction: 'out', + amount: spend.reduce((a, p) => a + p.amount, 0), + tags: [['e', originalTokenEventId, '', 'destroyed']] +}); +``` + +### Token Receiving + +When receiving tokens (from nutzap or direct transfer): + +1. **Validate token** (correct mint, valid proofs) +2. **Swap proofs** at mint (prevents sender reuse) +3. **Create token event** with new proofs +4. **Create history event** with "redeemed" marker + +### Couch Pattern (Safe Operations) + +For atomic operations that could fail mid-way: + +1. **Store proofs in "couch"** (temporary storage like IndexedDB) +2. **Perform mint operation** (swap, melt, etc.) +3. **On success**: Delete from couch, create new token event +4. **On failure**: Recover from couch + +This prevents losing proofs if the app crashes during an operation. + +## Security Considerations + +1. **Separate wallet key**: Never use your Nostr private key for the wallet +2. **Encrypt everything**: All token data must be NIP-44 encrypted +3. **Verify DLEQ**: Always verify DLEQ proofs when receiving nutzaps +4. **Swap immediately**: Swap received tokens to prevent sender double-spending +5. **Trusted mints only**: Only accept nutzaps from mints you trust +6. **Relay redundancy**: Store wallet data on multiple relays + +## Event Kind Reference + +| Kind | Name | Type | Encryption | +|------|------|------|------------| +| 7374 | Quote | Regular | NIP-44 | +| 7375 | Token | Regular | NIP-44 | +| 7376 | History | Regular | NIP-44 (partial) | +| 9321 | Nutzap | Regular | None | +| 10019 | Nutzap Info | Replaceable | None | +| 17375 | Wallet | Replaceable | NIP-44 | + +## Best Practices + +1. **Multiple token events**: Don't put all proofs in one event +2. **Consolidate periodically**: Merge small tokens to reduce event count +3. **Track history**: Create kind:7376 events for audit trail +4. **Handle offline**: Use DLEQ for offline nutzap verification +5. **Clean up quotes**: Delete expired kind:7374 events +6. **Normalize URLs**: Use consistent mint URL formatting + +## Official Resources + +- [NIP-60 Specification](https://github.com/nostr-protocol/nips/blob/master/60.md) +- [NIP-61 Specification](https://github.com/nostr-protocol/nips/blob/master/61.md) +- [NIP-44 Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md) +- [Cashu NUTs](https://cashubtc.github.io/nuts/)