diff --git a/.claude/skills/cashu-ts/README.md b/.claude/skills/cashu-ts/README.md new file mode 100644 index 0000000..2e7fa23 --- /dev/null +++ b/.claude/skills/cashu-ts/README.md @@ -0,0 +1,96 @@ +# cashu-ts Library Skill + +Expert knowledge of cashu-ts, the official TypeScript/JavaScript library for building Cashu wallets and applications. + +## What is cashu-ts? + +cashu-ts is a JavaScript library for Cashu wallets written in TypeScript. It provides a complete implementation of the Cashu protocol with a clean, intuitive API for minting, sending, receiving, and melting ecash tokens. + +## When to Use This Skill + +Use this skill when you need help with: + +- Building Cashu wallets (web, mobile, desktop) +- Implementing ecash operations in TypeScript/JavaScript +- Using the CashuWallet and CashuMint classes +- Encoding and decoding tokens +- Managing proofs and wallet state +- Integrating Lightning payments +- Implementing wallet backup and recovery +- Working with P2PK and spending conditions + +## What This Skill Covers + +### Core Classes + +- **CashuWallet**: Main wallet interface for all operations +- **CashuMint**: HTTP client for mint API communication +- **WalletOps**: Builder pattern for complex transactions + +### Wallet Operations + +- **Minting**: Create quotes, check payment status, mint proofs +- **Sending**: Coin selection, amount splitting, token encoding +- **Receiving**: Token decoding, proof swapping for security +- **Melting**: Lightning invoice payment, fee handling, change management +- **State Checking**: Verify proof validity and spent status + +### Token Management + +- **Encoding**: V4 (CBOR) and V3 (JSON) token formats +- **Decoding**: Parse and validate token strings +- **Proof utilities**: Sum amounts, split by keyset, validate structure +- **Denomination handling**: Binary decomposition for efficiency + +### Advanced Features + +- **Deterministic Secrets**: BIP39-based wallet backup +- **Counter Management**: For wallet recovery (NUT-09, NUT-13) +- **P2PK Locking**: Lock tokens to specific public keys (NUT-11) +- **BOLT12 Support**: Lightning offers for reusable payments +- **Multi-mint**: Handle tokens from multiple mints + +### TypeScript Types + +Complete type definitions for all protocol data structures: +- `Proof`, `Token`, `MintKeys`, `BlindedMessage` +- `MintQuoteResponse`, `MeltQuoteResponse`, `SendResponse` +- `ProofState`, `MintInfo`, and more + +## Installation + +```bash +npm install @cashu/cashu-ts +``` + +## Quick Example + +```typescript +import { CashuWallet, CashuMint } from '@cashu/cashu-ts'; + +// Create wallet +const wallet = new CashuWallet(new CashuMint('https://mint.example.com')); +await wallet.loadMint(); + +// Mint tokens +const quote = await wallet.createMintQuote(1000); +// ... user pays invoice ... +const { proofs } = await wallet.mintProofs(1000, quote.quote); + +// Send tokens +const { send } = await wallet.send(100, proofs); +const token = getEncodedTokenV4({ token: [{ mint: mintUrl, proofs: send }] }); +``` + +## Related Skills + +- **cashu**: Core Cashu protocol and NUT specifications +- **react**: Building wallet UIs with React +- **nostr**: Nostr integration for wallet backups + +## Resources + +- **GitHub**: https://github.com/cashubtc/cashu-ts +- **Documentation**: https://cashubtc.github.io/cashu-ts/docs/ +- **NPM Package**: https://www.npmjs.com/package/@cashu/cashu-ts +- **Examples**: Integration tests in GitHub repo diff --git a/.claude/skills/cashu-ts/SKILL.md b/.claude/skills/cashu-ts/SKILL.md new file mode 100644 index 0000000..d22d8f4 --- /dev/null +++ b/.claude/skills/cashu-ts/SKILL.md @@ -0,0 +1,1068 @@ +--- +name: cashu-ts +description: This skill should be used when working with cashu-ts library for building Cashu wallets and handling ecash operations in TypeScript. Provides comprehensive knowledge of the cashu-ts API, wallet operations, mint interactions, and token management. +--- + +# cashu-ts Library Expert + +## Purpose + +This skill provides expert-level assistance with cashu-ts, the official TypeScript/JavaScript library for building Cashu wallets. It enables developers to integrate Chaumian ecash functionality into web, mobile, and Node.js applications. + +## When to Use + +Activate this skill when: +- Building Cashu wallets (web, mobile, desktop) +- Implementing ecash operations in TypeScript/JavaScript +- Working with CashuWallet or CashuMint classes +- Handling token encoding/decoding +- Managing proofs and ecash state +- Integrating Lightning payments with ecash +- Implementing wallet backup and recovery +- Using WalletOps builder pattern + +## Core Concepts + +### cashu-ts Overview + +cashu-ts provides: +- **Wallet operations** - Mint, send, receive, melt ecash +- **Mint communication** - HTTP client for Cashu mint API +- **Token management** - Encode, decode, validate tokens +- **Cryptography** - BDHKE, blind signatures, DLEQ proofs +- **Type definitions** - Complete TypeScript types for Cashu protocol +- **NUT implementations** - Support for NUT-00 through NUT-13+ + +### Installation + +```bash +npm install @cashu/cashu-ts +``` + +**Peer Dependencies:** +```bash +npm install @noble/curves @noble/hashes +``` + +### Library Structure + +``` +@cashu/cashu-ts +├── CashuWallet - Main wallet interface +├── CashuMint - Mint API client +├── getEncodedTokenV4 - Token encoding +├── getDecodedToken - Token decoding +├── deriveKeysetId - Keyset utilities +├── generateSeed - Key generation +└── types - TypeScript definitions +``` + +## Core Classes + +### CashuWallet + +Primary interface for wallet operations. + +#### Constructor + +```typescript +import { CashuWallet, CashuMint } from '@cashu/cashu-ts'; + +// Simple constructor (fetches mint info) +const wallet = new CashuWallet(new CashuMint(mintUrl)); +await wallet.loadMint(); + +// Advanced constructor (with cached data) +const wallet = new CashuWallet( + new CashuMint(mintUrl), + { + unit: 'sat', // Currency unit + keys: cachedKeys, // Cached mint keys + keysets: cachedKeysets, // Cached keysets + mintInfo: cachedInfo // Cached mint info + } +); +await wallet.loadMint(); +``` + +**Important**: Always call `loadMint()` after instantiation to fetch mint keys. + +#### Wallet Properties + +```typescript +wallet.mint // CashuMint instance +wallet.keys // Current keyset keys (MintKeys) +wallet.keysetId // Active keyset ID +wallet.unit // Currency unit (default: 'sat') +``` + +### CashuMint + +HTTP client for mint API communication. + +```typescript +import { CashuMint } from '@cashu/cashu-ts'; + +const mint = new CashuMint(mintUrl); + +// Fetch mint information +const info = await mint.getInfo(); +console.log(info.name, info.version, info.nuts); + +// Get active keys +const keys = await mint.getKeys(); + +// Get all keysets +const keysets = await mint.getKeysets(); +``` + +## Wallet Operations + +### 1. Minting (Deposit Bitcoin → Receive Ecash) + +#### Create Mint Quote + +```typescript +// Create quote for 1000 sats +const mintQuote = await wallet.createMintQuote(1000); + +console.log('Pay invoice:', mintQuote.request); // BOLT11 invoice +console.log('Quote ID:', mintQuote.quote); +console.log('Paid:', mintQuote.paid); // false initially +``` + +**MintQuoteResponse:** +```typescript +{ + quote: string; // Quote identifier + request: string; // BOLT11 invoice to pay + paid: boolean; // Payment status + expiry: number; // Unix timestamp +} +``` + +#### Check Quote Status + +```typescript +const status = await wallet.checkMintQuote(mintQuote.quote); +console.log('Paid:', status.paid); +``` + +#### Mint Tokens + +```typescript +// After paying the invoice +const { proofs } = await wallet.mintProofs(1000, mintQuote.quote); + +console.log('Minted proofs:', proofs); +// proofs = [ +// { amount: 512, secret: '...', C: '...', id: '...' }, +// { amount: 256, secret: '...', C: '...', id: '...' }, +// { amount: 128, secret: '...', C: '...', id: '...' }, +// { amount: 64, secret: '...', C: '...', id: '...' }, +// { amount: 32, secret: '...', C: '...', id: '...' }, +// { amount: 8, secret: '...', C: '...', id: '...' } +// ] +``` + +**Complete Flow:** + +```typescript +async function depositBitcoin(wallet: CashuWallet, amount: number) { + // Step 1: Create quote + const quote = await wallet.createMintQuote(amount); + + // Step 2: Display invoice to user + console.log('Pay this invoice:', quote.request); + displayQRCode(quote.request); + + // Step 3: Poll for payment + while (true) { + const status = await wallet.checkMintQuote(quote.quote); + if (status.paid) break; + await sleep(2000); + } + + // Step 4: Mint tokens + const { proofs } = await wallet.mintProofs(amount, quote.quote); + + // Step 5: Store proofs + await saveProofs(proofs); + + return proofs; +} +``` + +### 2. Sending (Transfer Ecash) + +#### Basic Send + +```typescript +// Send 100 sats from existing proofs +const { keep, send } = await wallet.send(100, proofsToSpend); + +// keep: proofs to keep in wallet (change) +// send: proofs to send to recipient + +// Encode as shareable token +const token = getEncodedTokenV4({ + token: [{ + mint: wallet.mint.mintUrl, + proofs: send + }] +}); + +console.log('Send this token:', token); +// cashuBo2F0gaNhbK... +``` + +#### Send with Options + +```typescript +const { keep, send } = await wallet.send( + 100, // Amount to send + proofsToSpend, // Proofs to use + { + includeFees: true, // Include swap fees + keysetId: keysetId, // Specific keyset + counter: 42, // Deterministic secret counter + pubkey: recipientPk, // P2PK lock to recipient + privkey: senderSk // For signing P2PK + } +); +``` + +**Send Response:** +```typescript +{ + keep: Proof[]; // Proofs to keep (change) + send: Proof[]; // Proofs to send + totalKeep: number; // Total amount kept + totalSend: number; // Total amount sent +} +``` + +#### With Memo + +```typescript +const token = getEncodedTokenV4({ + token: [{ + mint: wallet.mint.mintUrl, + proofs: send, + unit: 'sat' + }], + memo: 'Coffee payment ☕' +}); +``` + +### 3. Receiving (Accept Ecash) + +#### Basic Receive + +```typescript +// Decode token from string +const decodedToken = getDecodedToken(tokenString); + +// Receive proofs (swaps to new secrets) +const receivedProofs = await wallet.receive(decodedToken); + +console.log('Received amount:', sumProofs(receivedProofs)); +``` + +**Why Receive Swaps:** +- Sender knows the secrets and could double-spend +- Swapping creates new secrets only recipient knows +- Makes tokens truly yours + +#### Receive with P2PK + +```typescript +// Receive P2PK locked token +const receivedProofs = await wallet.receive( + decodedToken, + { + privkey: recipientPrivkey // Required to unlock P2PK + } +); +``` + +#### Multi-Mint Receive + +```typescript +const decodedToken = getDecodedToken(tokenString); + +// Token may contain proofs from multiple mints +for (const tokenEntry of decodedToken.token) { + const wallet = getOrCreateWallet(tokenEntry.mint); + const received = await wallet.receive({ + token: [tokenEntry] + }); + console.log(`Received ${sumProofs(received)} from ${tokenEntry.mint}`); +} +``` + +### 4. Melting (Withdraw Ecash → Pay Bitcoin) + +#### Create Melt Quote + +```typescript +const invoice = 'lnbc10000...'; // BOLT11 invoice + +// Get melt quote +const meltQuote = await wallet.createMeltQuote(invoice); + +console.log('Amount:', meltQuote.amount); // Invoice amount +console.log('Fee reserve:', meltQuote.fee_reserve); // Estimated fee +console.log('Total needed:', meltQuote.amount + meltQuote.fee_reserve); +``` + +**MeltQuoteResponse:** +```typescript +{ + quote: string; // Quote identifier + amount: number; // Invoice amount + fee_reserve: number; // Fee estimate + paid: boolean; // false initially + expiry: number; // Unix timestamp +} +``` + +#### Melt Tokens + +```typescript +// Select proofs for amount + fee +const proofsToSend = selectProofs( + proofs, + meltQuote.amount + meltQuote.fee_reserve +); + +// Melt tokens (pays invoice) +const meltResponse = await wallet.meltProofs(meltQuote, proofsToSend); + +console.log('Paid:', meltResponse.paid); +console.log('Preimage:', meltResponse.payment_preimage); + +// Get change if fee was overestimated +if (meltResponse.change) { + const changeProofs = meltResponse.change; + console.log('Change:', sumProofs(changeProofs)); +} +``` + +**MeltProofsResponse:** +```typescript +{ + paid: boolean; // Payment successful + payment_preimage: string; // Proof of payment + change: Proof[]; // Change proofs (if overpaid) +} +``` + +**Complete Flow:** + +```typescript +async function payInvoice(wallet: CashuWallet, invoice: string, proofs: Proof[]) { + // Step 1: Get quote + const quote = await wallet.createMeltQuote(invoice); + const totalNeeded = quote.amount + quote.fee_reserve; + + // Step 2: Check balance + const balance = sumProofs(proofs); + if (balance < totalNeeded) { + throw new Error('Insufficient balance'); + } + + // Step 3: Select proofs + const proofsToSend = selectProofs(proofs, totalNeeded); + + // Step 4: Melt (pay invoice) + const result = await wallet.meltProofs(quote, proofsToSend); + + // Step 5: Handle change + const spent = sumProofs(proofsToSend); + const changeAmount = result.change ? sumProofs(result.change) : 0; + const actualFee = spent - quote.amount - changeAmount; + + console.log(`Paid ${quote.amount}, fee: ${actualFee}, change: ${changeAmount}`); + + return { + paid: result.paid, + preimage: result.payment_preimage, + change: result.change || [], + fee: actualFee + }; +} +``` + +### 5. Swapping Tokens + +Swap proofs for new proofs (same total value). + +**Use Cases:** +- Coin selection (split/combine denominations) +- Key rotation (swap to new keyset) +- Change secrets (privacy) + +```typescript +// Swap proofs for specific amounts +const { keep, send } = await wallet.send( + desiredAmount, + allProofs, + { includeFees: true } +); + +// Or use swap directly +const newProofs = await wallet.swap(desiredAmount, oldProofs); +``` + +### 6. Checking Token State + +Check if proofs are spent or unspent. + +```typescript +const states = await wallet.checkProofsSpent(proofs); + +for (const state of states) { + console.log(`Secret: ${state.secret}`); + console.log(`State: ${state.state}`); // UNSPENT, SPENT, PENDING + console.log(`Witness: ${state.witness}`); +} +``` + +**ProofState:** +```typescript +{ + secret: string; + state: 'UNSPENT' | 'SPENT' | 'PENDING'; + witness?: string; +} +``` + +## WalletOps Builder Pattern + +Flexible transaction builder for complex operations. + +```typescript +import { WalletOps } from '@cashu/cashu-ts'; + +const ops = new WalletOps(wallet); + +// Chain operations +const result = await ops + .send(100, proofs) // Send 100 sats + .send(50, proofs) // Send 50 more sats + .melt(invoice, proofs) // Pay invoice + .execute(); // Execute all operations + +console.log('Results:', result); +``` + +## Token Encoding/Decoding + +### Encode Token V4 + +```typescript +import { getEncodedTokenV4 } from '@cashu/cashu-ts'; + +const token = getEncodedTokenV4({ + token: [ + { + mint: 'https://mint.example.com', + proofs: proofs, + unit: 'sat' + } + ], + memo: 'Optional memo' +}); + +// token = 'cashuBo2F0gaNhbK...' +``` + +### Decode Token + +```typescript +import { getDecodedToken } from '@cashu/cashu-ts'; + +const decoded = getDecodedToken(tokenString); + +console.log(decoded); +// { +// token: [ +// { +// mint: 'https://mint.example.com', +// proofs: [...], +// unit: 'sat' +// } +// ], +// memo: 'Optional memo' +// } +``` + +### Decode Token V3 (Legacy) + +```typescript +import { getDecodedToken } from '@cashu/cashu-ts'; + +// Automatically detects V3 (cashuA) or V4 (cashuB) +const decoded = getDecodedToken(tokenString); +``` + +## Deterministic Secrets (Wallet Backup) + +### Generate Deterministic Seed + +```typescript +import { generateSeed, deriveSeedFromMnemonic } from '@cashu/cashu-ts'; + +// Generate new seed +const seed = generateSeed(); // Uint8Array(32) + +// Or derive from mnemonic (BIP39) +const mnemonic = 'abandon abandon abandon...'; // 12 or 24 words +const seed = deriveSeedFromMnemonic(mnemonic); +``` + +### Use with Wallet + +```typescript +import { CashuWallet } from '@cashu/cashu-ts'; + +const wallet = new CashuWallet( + new CashuMint(mintUrl), + { + unit: 'sat', + mnemonicOrSeed: seed // Enable deterministic secrets + } +); + +// Wallet now uses deterministic secrets +// Can restore from seed later +``` + +### Counter Management + +```typescript +// Get current counter value +const counter = wallet.getCounter(); + +// Set counter (for recovery) +wallet.setCounter(42); + +// Wallet generates secrets as: +// secret = HMAC-SHA256(seed, counter || keyset_id || amount) +``` + +### Restore from Seed + +```typescript +async function restoreWallet(mintUrl: string, seed: Uint8Array) { + const wallet = new CashuWallet( + new CashuMint(mintUrl), + { mnemonicOrSeed: seed } + ); + await wallet.loadMint(); + + // Restore proofs by checking state + const restoredProofs = await wallet.restore(0, 100); // Check counters 0-100 + + console.log('Restored proofs:', restoredProofs); + return restoredProofs; +} +``` + +## P2PK (Pay-to-Public-Key) + +Lock proofs to a specific public key. + +### Send P2PK Locked Token + +```typescript +import { getPublicKey } from '@cashu/cashu-ts'; + +const recipientPubkey = 'recipient-public-key-hex'; + +// Send with P2PK lock +const { keep, send } = await wallet.send( + 100, + proofs, + { + pubkey: recipientPubkey // Lock to recipient's key + } +); + +// Recipient MUST have private key to spend +``` + +### Receive P2PK Token + +```typescript +// Recipient needs their private key +const recipientPrivkey = 'recipient-private-key-hex'; + +const receivedProofs = await wallet.receive( + token, + { + privkey: recipientPrivkey // Unlock with private key + } +); +``` + +### Check P2PK Requirements + +```typescript +function isP2PKLocked(proof: Proof): boolean { + try { + const secret = JSON.parse(proof.secret); + return Array.isArray(secret) && secret[0] === 'P2PK'; + } catch { + return false; + } +} +``` + +## BOLT12 (Lightning Offers) + +Support for reusable Lightning offers. + +```typescript +// Create melt quote for BOLT12 offer +const offer = 'lno1...'; // BOLT12 offer string + +const meltQuote = await wallet.createMeltQuoteBolt12(offer, { + amount: 1000 // Optional amount for flexible offers +}); + +// Rest is same as BOLT11 +const result = await wallet.meltProofs(meltQuote, proofs); +``` + +## Utility Functions + +### Proof Utilities + +```typescript +import { sumProofs, splitProofs } from '@cashu/cashu-ts'; + +// Sum proof amounts +const total = sumProofs(proofs); + +// Split proofs by keyset +const byKeyset = splitProofs(proofs); +// { 'keyset-id-1': [...], 'keyset-id-2': [...] } +``` + +### Keyset Utilities + +```typescript +import { deriveKeysetId } from '@cashu/cashu-ts'; + +// Derive keyset ID from keys +const keysetId = deriveKeysetId(keys); +``` + +### Amount to Denominations + +```typescript +import { splitAmount } from '@cashu/cashu-ts'; + +const amounts = splitAmount(1000); +// [8, 32, 64, 128, 256, 512] (powers of 2) +``` + +## TypeScript Types + +### Core Types + +```typescript +import type { + Proof, + Token, + MintKeys, + MintKeyset, + BlindedMessage, + BlindSignature, + MintQuoteResponse, + MeltQuoteResponse, + MintInfo, + SendResponse, + ProofState +} from '@cashu/cashu-ts'; + +// Proof +type Proof = { + amount: number; + secret: string; + C: string; // Signature (hex-encoded point) + id: string; // Keyset ID + witness?: string; // P2PK signature or other witness +}; + +// Token +type Token = { + token: Array<{ + mint: string; + proofs: Proof[]; + unit?: string; + }>; + memo?: string; +}; + +// Mint Keys +type MintKeys = { + id: string; // Keyset ID + unit: string; // Currency unit + keys: Record; // amount → pubkey +}; + +// Mint Info +type MintInfo = { + name?: string; + pubkey?: string; + version?: string; + description?: string; + description_long?: string; + contact?: Array>; + motd?: string; + nuts: Record; // Supported NUTs +}; +``` + +## Best Practices + +### Wallet Management + +1. **Always load mint first** + ```typescript + const wallet = new CashuWallet(new CashuMint(url)); + await wallet.loadMint(); // REQUIRED + ``` + +2. **Cache mint data** + ```typescript + // Save after loading + const keys = wallet.keys; + const keysetId = wallet.keysetId; + localStorage.setItem('mint-keys', JSON.stringify(keys)); + + // Load from cache + const cached = JSON.parse(localStorage.getItem('mint-keys')); + const wallet = new CashuWallet(new CashuMint(url), { + keys: cached, + keysetId: cached.id + }); + ``` + +3. **Handle errors gracefully** + ```typescript + try { + const proofs = await wallet.mintProofs(amount, quote); + } catch (error) { + if (error.message.includes('Quote not paid')) { + // Wait for payment + } else if (error.message.includes('Quote already issued')) { + // Already minted + } else { + throw error; + } + } + ``` + +### Proof Management + +1. **Track proofs per mint** + ```typescript + const proofsByMint = new Map(); + proofsByMint.set(mintUrl, proofs); + ``` + +2. **Check spent status before sending** + ```typescript + const states = await wallet.checkProofsSpent(proofs); + const unspent = proofs.filter((p, i) => + states[i].state === 'UNSPENT' + ); + ``` + +3. **Always swap received tokens** + ```typescript + // WRONG: Just add to wallet + proofs.push(...receivedProofs); + + // RIGHT: Swap first + const newProofs = await wallet.receive(token); + proofs.push(...newProofs); + ``` + +### Security + +1. **Store seed securely** + ```typescript + // Encrypt before storing + const encrypted = await encrypt(seed, userPassword); + localStorage.setItem('wallet-seed', encrypted); + ``` + +2. **Don't expose proofs** + ```typescript + // WRONG: Logs secrets + console.log('Proofs:', proofs); + + // RIGHT: Log summary only + console.log('Balance:', sumProofs(proofs)); + ``` + +3. **Use P2PK for sensitive transfers** + ```typescript + const { send } = await wallet.send(amount, proofs, { + pubkey: recipientPubkey + }); + // Recipient must have private key + ``` + +### Performance + +1. **Batch operations** + ```typescript + // Use WalletOps for multiple operations + const result = await new WalletOps(wallet) + .send(100, proofs) + .send(200, proofs) + .execute(); + ``` + +2. **Minimize API calls** + ```typescript + // Cache mint info + const info = await mint.getInfo(); + // Don't call getInfo() repeatedly + ``` + +3. **Use appropriate denominations** + ```typescript + // Let library handle denomination selection + const { send } = await wallet.send(amount, proofs); + // More efficient than manual selection + ``` + +## Common Patterns + +### Multi-Mint Wallet + +```typescript +class MultiMintWallet { + private wallets = new Map(); + private proofs = new Map(); + + async addMint(mintUrl: string) { + const wallet = new CashuWallet(new CashuMint(mintUrl)); + await wallet.loadMint(); + this.wallets.set(mintUrl, wallet); + this.proofs.set(mintUrl, []); + } + + async deposit(mintUrl: string, amount: number) { + const wallet = this.wallets.get(mintUrl); + if (!wallet) throw new Error('Mint not found'); + + const quote = await wallet.createMintQuote(amount); + // ... handle payment ... + const { proofs } = await wallet.mintProofs(amount, quote.quote); + + this.proofs.get(mintUrl)!.push(...proofs); + return proofs; + } + + async send(mintUrl: string, amount: number) { + const wallet = this.wallets.get(mintUrl); + const proofs = this.proofs.get(mintUrl); + if (!wallet || !proofs) throw new Error('Mint not found'); + + const { keep, send } = await wallet.send(amount, proofs); + + this.proofs.set(mintUrl, keep); + return getEncodedTokenV4({ + token: [{ mint: mintUrl, proofs: send }] + }); + } + + async receive(tokenString: string) { + const decoded = getDecodedToken(tokenString); + + const results = []; + for (const entry of decoded.token) { + let wallet = this.wallets.get(entry.mint); + if (!wallet) { + await this.addMint(entry.mint); + wallet = this.wallets.get(entry.mint)!; + } + + const received = await wallet.receive({ token: [entry] }); + this.proofs.get(entry.mint)!.push(...received); + results.push({ mint: entry.mint, amount: sumProofs(received) }); + } + + return results; + } + + getTotalBalance(): number { + let total = 0; + for (const proofs of this.proofs.values()) { + total += sumProofs(proofs); + } + return total; + } +} +``` + +### Proof Selection Algorithm + +```typescript +function selectProofs(proofs: Proof[], target: number): Proof[] { + // Sort by amount descending + const sorted = [...proofs].sort((a, b) => b.amount - a.amount); + + const selected: Proof[] = []; + let sum = 0; + + for (const proof of sorted) { + if (sum >= target) break; + selected.push(proof); + sum += proof.amount; + } + + if (sum < target) { + throw new Error(`Insufficient balance: have ${sum}, need ${target}`); + } + + return selected; +} +``` + +### Wallet Persistence + +```typescript +interface WalletState { + mintUrl: string; + proofs: Proof[]; + seed: string; + counter: number; +} + +async function saveWallet(wallet: CashuWallet, proofs: Proof[]) { + const state: WalletState = { + mintUrl: wallet.mint.mintUrl, + proofs: proofs, + seed: bytesToHex(wallet.seed), + counter: wallet.getCounter() + }; + + const encrypted = await encrypt(JSON.stringify(state), password); + localStorage.setItem('wallet-state', encrypted); +} + +async function loadWallet(): Promise<{ wallet: CashuWallet; proofs: Proof[] }> { + const encrypted = localStorage.getItem('wallet-state'); + if (!encrypted) throw new Error('No wallet found'); + + const decrypted = await decrypt(encrypted, password); + const state: WalletState = JSON.parse(decrypted); + + const seed = hexToBytes(state.seed); + const wallet = new CashuWallet( + new CashuMint(state.mintUrl), + { mnemonicOrSeed: seed } + ); + await wallet.loadMint(); + wallet.setCounter(state.counter); + + return { wallet, proofs: state.proofs }; +} +``` + +## Troubleshooting + +### Common Issues + +**"Quote not paid" error:** +```typescript +// Wait for payment before minting +const quote = await wallet.createMintQuote(amount); +// ... user pays invoice ... +await waitForPayment(quote.quote); // Poll checkMintQuote +const proofs = await wallet.mintProofs(amount, quote.quote); +``` + +**"Insufficient balance" error:** +```typescript +// Check balance first +const balance = sumProofs(proofs); +if (balance < amount) { + throw new Error(`Need ${amount}, have ${balance}`); +} +``` + +**"Proofs already spent" error:** +```typescript +// Check state before sending +const states = await wallet.checkProofsSpent(proofs); +const unspent = proofs.filter((p, i) => states[i].state === 'UNSPENT'); +``` + +**"Invalid signature" error:** +```typescript +// Verify proof structure +function isValidProof(proof: Proof): boolean { + return ( + typeof proof.amount === 'number' && + typeof proof.secret === 'string' && + typeof proof.C === 'string' && + typeof proof.id === 'string' && + proof.C.length === 66 // 33-byte hex + ); +} +``` + +## Development Resources + +### Testing + +```typescript +import { CashuWallet, CashuMint } from '@cashu/cashu-ts'; + +// Use test mint (local nutshell instance) +const TEST_MINT_URL = 'http://localhost:3338'; + +describe('Cashu Wallet', () => { + let wallet: CashuWallet; + + beforeEach(async () => { + wallet = new CashuWallet(new CashuMint(TEST_MINT_URL)); + await wallet.loadMint(); + }); + + it('should mint tokens', async () => { + const quote = await wallet.createMintQuote(100); + // ... pay invoice ... + const { proofs } = await wallet.mintProofs(100, quote.quote); + expect(sumProofs(proofs)).toBe(100); + }); +}); +``` + +### Example Projects + +- **Cashu.me**: Web wallet (https://cashu.me) +- **Nutstash**: PWA wallet with multi-mint support +- **eNuts**: Mobile wallet (React Native) + +### Key Repositories + +- **cashu-ts**: https://github.com/cashubtc/cashu-ts +- **cashu-ts docs**: https://cashubtc.github.io/cashu-ts/docs/ +- **NPM package**: https://www.npmjs.com/package/@cashu/cashu-ts + +## Related Skills + +- **cashu** - Cashu protocol fundamentals and NUT specifications +- **nostr** - Nostr integration (NUT-25 wallet backup) +- **react** - Building Cashu wallet UIs with React diff --git a/.claude/skills/cashu/README.md b/.claude/skills/cashu/README.md new file mode 100644 index 0000000..bb962ce --- /dev/null +++ b/.claude/skills/cashu/README.md @@ -0,0 +1,67 @@ +# Cashu Protocol Skill + +Expert knowledge of the Cashu ecash protocol, a free and open-source Chaumian ecash system built for Bitcoin. + +## What is Cashu? + +Cashu is an ecash protocol based on Blind Diffie-Hellman Key Exchange (BDHKE), enabling private, instant, and nearly free transactions over Bitcoin's Lightning Network. It uses blind signatures to preserve user privacy while maintaining Bitcoin backing through custodial mints. + +## When to Use This Skill + +Use this skill when you need help with: + +- Understanding Cashu protocol fundamentals +- Implementing Cashu wallets or mints +- Working with blind signatures and BDHKE +- Learning about NUT (Notation, Usage, and Terminology) specifications +- Building privacy-preserving payment applications +- Integrating Lightning Network with ecash +- Token serialization and encoding + +## What This Skill Covers + +### Core Concepts + +- **Blind Signatures**: How BDHKE enables privacy-preserving token issuance +- **Token Format**: Proofs, BlindedMessages, and BlindSignatures +- **Serialization**: V3 (JSON) and V4 (CBOR) token formats +- **Keysets**: How mints manage keys for different denominations + +### Operations + +- **Minting**: Deposit Bitcoin via Lightning, receive ecash +- **Sending**: Transfer ecash tokens between users +- **Receiving**: Accept ecash and swap for new secrets +- **Melting**: Withdraw Bitcoin by melting ecash + +### NUT Specifications + +- **Mandatory NUTs** (NUT-00 through NUT-06): Core protocol +- **Optional NUTs**: Extended features (P2PK, HTLC, DLEQ, etc.) +- Complete reference in `references/nuts-overview.md` + +### Best Practices + +- Wallet implementation patterns +- Security considerations +- Error handling +- Performance optimization +- Multi-mint support + +## Reference Files + +- **SKILL.md**: Complete protocol documentation +- **references/nuts-overview.md**: Detailed NUT specifications +- **references/common-patterns.md**: Implementation patterns and code examples + +## Related Skills + +- **cashu-ts**: TypeScript/JavaScript library for building Cashu applications +- **nostr**: Nostr protocol (for NUT-25 wallet backups) + +## Resources + +- **Official Website**: https://cashu.space +- **Documentation**: https://docs.cashu.space +- **NUTs Repository**: https://github.com/cashubtc/nuts +- **Protocol Spec**: https://docs.cashu.space/protocol diff --git a/.claude/skills/cashu/SKILL.md b/.claude/skills/cashu/SKILL.md new file mode 100644 index 0000000..95e8881 --- /dev/null +++ b/.claude/skills/cashu/SKILL.md @@ -0,0 +1,840 @@ +--- +name: cashu +description: This skill should be used when working with the Cashu ecash protocol, implementing Cashu wallets or mints, handling ecash tokens, or discussing Cashu NUT specifications. Provides comprehensive knowledge of Chaumian blind signatures, Bitcoin Lightning integration, and privacy-preserving digital cash. +--- + +# Cashu Protocol Expert + +## Purpose + +This skill provides expert-level assistance with the Cashu protocol, a free and open-source Chaumian ecash system built for Bitcoin. The protocol enables private, instant, and nearly free transactions using blind signatures over the Lightning Network. + +## When to Use + +Activate this skill when: +- Implementing Cashu wallets or mints +- Working with ecash tokens and blind signatures +- Handling minting and melting operations +- Implementing any Cashu NUT specification +- Building privacy-preserving payment applications +- Integrating Lightning Network with ecash +- Discussing cryptographic operations (BDHKE, blind signatures) +- Working with token serialization and encoding + +## Core Concepts + +### Protocol Foundation + +Cashu is built on **Blind Diffie-Hellman Key Exchange (BDHKE)**, a variant of David Wagner's Chaumian blinding scheme. The protocol involves three parties: + +1. **Alice (User)** - Holds secrets and creates blinded messages +2. **Bob (Mint)** - Issues blind signatures and manages private keys +3. **Carol (Recipient)** - Receives and verifies tokens + +Key principles: +- Privacy through blind signatures (mint can't link tokens to users) +- Instant and final transactions (like physical cash) +- Bitcoin-backed via Lightning Network +- Custodial model (mint holds Bitcoin) +- Open protocol (anyone can run a mint) + +### Ecash System Architecture + +**Mint (Server):** +- Holds Bitcoin in custody +- Issues blind signatures for denominations +- Manages private keys per denomination +- Provides Lightning endpoints for deposits/withdrawals +- Cannot track token ownership or transaction history + +**Wallet (Client):** +- Generates secrets and blinding factors +- Creates blinded messages for minting +- Unblinds signatures to create valid tokens +- Stores proofs (ecash tokens) +- Handles sending and receiving +- Melts tokens back to Lightning + +### Cryptographic Operations (BDHKE) + +#### Key Generation +``` +Mint generates: + k = private key (scalar) + K = k·G = public key (point on secp256k1) + G = generator point +``` + +#### Blinding Protocol (5 Steps) + +**1. Blinding (Alice → Bob)** +``` +Alice generates: + x = secret (random 32 bytes) + r = blinding factor (random scalar) + Y = hash_to_curve(x) + B_ = Y + r·G (blinded message) +Alice sends B_ to Bob +``` + +**2. Signing (Bob → Alice)** +``` +Bob computes: + C_ = k·B_ (blind signature) +Bob sends C_ to Alice +``` + +**3. Unblinding (Alice)** +``` +Alice computes: + C = C_ - r·K = k·Y (unblinded signature) +Result: Proof = (x, C) +``` + +**4. Transfer (Alice → Carol)** +``` +Alice sends proof (x, C) to Carol +Carol sends to Bob for verification +``` + +**5. Verification (Bob)** +``` +Bob checks: + k·hash_to_curve(x) == C + x not in spent secrets database +If valid, Bob marks x as spent +``` + +#### Hash-to-Curve Function + +Deterministic mapping from secret to curve point: + +``` +msg_hash = SHA256(DOMAIN_SEPARATOR || x) +DOMAIN_SEPARATOR = b"Secp256k1_HashToCurve_Cashu_" + +counter = 0 +loop: + candidate = SHA256(msg_hash || counter) + try: + Y = PublicKey('02' || candidate) // Compressed point + if valid: return Y + counter += 1 +``` + +### Token Format + +#### Proof Structure + +A **Proof** is an unblinded signature representing value: + +```json +{ + "amount": 8, // Token denomination + "secret": "9a3b2c1d...", // 64-char hex (or UTF-8) + "C": "02ab3c4d5e...", // Unblinded signature (33-byte compressed point) + "id": "00ffd48b78a7b2f4" // Keyset ID (identifies mint's key) +} +``` + +**Proof Fields:** +- `amount`: Denomination in base unit (sats) +- `secret`: Random hex string (must be unique) +- `C`: secp256k1 point (hex-encoded compressed format) +- `id`: Keyset identifier (8-byte hex) + +#### BlindedMessage Structure + +Sent by wallet to request blind signature: + +```json +{ + "amount": 8, + "id": "00ffd48b78a7b2f4", // Keyset ID + "B_": "02c1a3b5d7..." // Blinded secret (33-byte point) +} +``` + +#### BlindSignature (Promise) + +Returned by mint after blinding: + +```json +{ + "amount": 8, + "id": "00ffd48b78a7b2f4", + "C_": "03ab5c7d9e..." // Blinded signature +} +``` + +### Token Serialization + +#### V4 Tokens (Current Standard) + +Format: `cashuB[base64_urlsafe_cbor]` + +**Structure (CBOR-encoded):** +```json +{ + "t": [ // Token array + { + "m": "https://mint.host", // Mint URL + "u": "sat", // Unit (sat, usd, etc.) + "d": "Thanks!", // Optional memo + "t": [ // Token entries + { + "i": "00ffd48b", // Keyset ID (short form) + "p": [ // Proofs array + { + "a": 8, // Amount + "s": "9a3b2c...", // Secret + "c": "02ab3c..." // Signature + } + ] + } + ] + } + ] +} +``` + +**Binary Encoding:** +``` +"cashu" (UTF-8) + "B" + CBOR(token_object) +``` + +**Advantages:** +- Space-efficient (CBOR vs JSON) +- Abbreviated keys (single letters) +- Short keyset IDs (8 bytes instead of 16) +- Multi-mint support in single token + +#### V3 Tokens (Deprecated) + +Format: `cashuA[base64_urlsafe_json]` + +JSON-based with full field names. Still readable but less efficient. + +### Keysets and Denominations + +**Keyset**: Set of public keys for different amounts + +```json +{ + "id": "00ffd48b78a7b2f4", // Keyset identifier + "unit": "sat", // Currency unit + "keys": { + "1": "02ab3c4d...", // Public key for 1 sat + "2": "03bc5d6e...", // Public key for 2 sats + "4": "02cd7e8f...", // Public key for 4 sats + "8": "03de9f0a...", // Public key for 8 sats + // ... powers of 2 + } +} +``` + +**Why Powers of 2?** +- Any amount can be represented as sum of powers of 2 +- Efficient coin selection +- Example: 13 sats = 8 + 4 + 1 (3 proofs) + +**Keyset ID Derivation:** +``` +id = SHA256(sorted_pubkeys_concatenated)[:16] // First 16 bytes (hex) +``` + +## Cashu NUTs (Specifications) + +### Mandatory NUTs (Must Implement) + +All wallets and mints **MUST** implement these: + +#### NUT-00: Cryptography and Models +- BDHKE blind signature scheme +- Token data structures (Proof, BlindedMessage, BlindSignature) +- Token serialization (V3 and V4 formats) +- Hash-to-curve function +- Error codes and responses + +#### NUT-01: Mint Public Keys +- **Endpoint**: `GET /v1/keys` +- **Response**: Keyset with public keys for each amount +- Allows wallet to unblind signatures +- Supports key rotation + +#### NUT-02: Keysets and Fees +- **Endpoint**: `GET /v1/keysets` +- Lists all active and inactive keysets +- Fee structure per keyset +- Unit specification (sat, msat, usd, etc.) + +#### NUT-03: Swapping Tokens +- **Endpoint**: `POST /v1/swap` +- Exchange proofs for new proofs (same total value) +- Used for: sending specific amounts, combining/splitting, key rotation +- Atomic operation (all or nothing) + +#### NUT-04: Minting Tokens +- **Endpoint**: `POST /v1/mint/quote/bolt11` (create quote) +- **Endpoint**: `POST /v1/mint/bolt11` (mint tokens) +- Deposit Bitcoin via Lightning +- Receive blinded signatures (promises) +- Unblind to get valid proofs + +#### NUT-05: Melting Tokens +- **Endpoint**: `POST /v1/melt/quote/bolt11` (create quote) +- **Endpoint**: `POST /v1/melt/bolt11` (melt tokens) +- Withdraw Bitcoin via Lightning +- Provide proofs to cover amount + fee +- Receive change if overpaid + +#### NUT-06: Mint Information +- **Endpoint**: `GET /v1/info` +- Mint metadata (name, description, version) +- Supported NUTs +- Contact information +- Message of the day (MOTD) + +### Optional NUTs (Recommended) + +#### NUT-07: Token State Check +- **Endpoint**: `POST /v1/checkstate` +- Check if secrets are spent or pending +- Returns state: `UNSPENT`, `SPENT`, `PENDING` +- Useful for wallet recovery + +#### NUT-08: Overpaid Lightning Fees +- **Endpoint**: `POST /v1/melt/quote/bolt11` returns fee_reserve +- Return change when actual fee < fee_reserve +- Prevents fee overpayment + +#### NUT-09: Restore Signatures +- **Endpoint**: `POST /v1/restore` +- Recover lost proofs using deterministic secrets +- Requires counter-based secret generation +- Useful for wallet backup/recovery + +#### NUT-10: Spending Conditions +- P2PK (Pay-to-Public-Key) locking +- HTLC (Hashed Timelock Contracts) +- Custom spending conditions on proofs +- Enhanced security and programmability + +#### NUT-11: Pay-to-Public-Key (P2PK) +- Lock proofs to specific pubkey +- Requires signature to spend +- Format: `["P2PK", {"nonce": "...", "data": "pubkey", "tags": [...]}]` +- Prevents theft if proofs leaked + +#### NUT-12: DLEQ Proofs +- Discrete Log Equality Proofs +- Proves mint signed correctly without revealing key +- Prevents mint from creating unbacked tokens +- Optional but increases trust + +#### NUT-13: Deterministic Secrets +- Generate secrets from counter + seed +- Enables deterministic wallet recovery +- Works with NUT-09 for backup +- Format: `secret = hash(seed || counter)` + +#### NUT-14: Hashed Timelock Contracts (HTLC) +- Time-locked proofs +- Preimage reveals spending ability +- Useful for atomic swaps, escrow +- Format: `["HTLC", {"nonce": "...", "data": "hash", "tags": [["locktime", "timestamp"]]}]` + +#### NUT-15: Multi-Path Payments (MPP) +- Split payment across multiple routes +- Improves Lightning payment success rate +- Coordination between wallet and mint + +### Extended NUTs + +- **NUT-16**: Animated QR codes for large tokens +- **NUT-17**: WebSocket subscriptions for real-time updates +- **NUT-18**: Payment requests (invoicing) +- **NUT-19**: Cached responses for performance +- **NUT-20**: Multiple signature methods +- **NUT-21**: BOLT11 Lightning invoices (mandatory for Lightning) +- **NUT-22**: BOLT12 Lightning offers +- **NUT-23**: HTTP 402 Payment Required +- **NUT-24**: Bech32m encoding for tokens +- **NUT-25**: Nostr-based wallet backup + +## Core Operations + +### 1. Minting (Deposit Bitcoin → Get Ecash) + +**Flow:** +``` +1. Wallet creates mint quote + POST /v1/mint/quote/bolt11 {amount: 1000} + → {quote: "quote_id", request: "lnbc...", paid: false} + +2. User pays Lightning invoice (external) + +3. Wallet generates secrets and blinds them + secrets = [random_32_bytes() for _ in amounts] + blinded_messages = [blind(secret, amount) for secret, amount in zip(secrets, amounts)] + +4. Wallet requests signatures + POST /v1/mint/bolt11 {quote: "quote_id", outputs: blinded_messages} + → {signatures: [blind_signatures]} + +5. Wallet unblinds signatures + proofs = [unblind(signature, secret) for signature, secret in zip(signatures, secrets)] + +6. Wallet stores proofs +``` + +**Example (1000 sats → proofs for 512, 256, 128, 64, 32, 8):** +```typescript +// Step 1: Create quote +const quote = await mint.createMintQuote(1000); +console.log("Pay invoice:", quote.request); + +// Step 2: Wait for payment +await waitForPayment(quote.quote); + +// Step 3-4: Generate blinded messages +const outputs = generateBlindedMessages([512, 256, 128, 64, 32, 8]); + +// Step 4: Request signatures +const { signatures } = await mint.mintTokens(quote.quote, outputs); + +// Step 5: Unblind +const proofs = unblindSignatures(signatures, secrets); +``` + +### 2. Sending (Transfer Ecash) + +**Flow:** +``` +1. Select proofs totaling send_amount (+ optional fee) + selected = coin_select(proofs, send_amount) + +2. If exact amount: send proofs directly + If over: swap to get exact amount + change + +3. Create swap request + POST /v1/swap { + inputs: selected_proofs, + outputs: blinded_messages_for(send_amount + change) + } + → {signatures: [...]} + +4. Unblind signatures + send_proofs = unblind(signatures[:send_count]) + keep_proofs = unblind(signatures[send_count:]) + +5. Encode send_proofs as token + token = encode_token_v4(send_proofs, mint_url) + +6. Share token with recipient (QR, text, NFC) +``` + +**Example (send 100 sats, have proofs for 128):** +```typescript +// Swap 128 proof into 100 (send) + 28 (change) +const { keep, send } = await wallet.send(100, proofs); +const token = getEncodedTokenV4({ mint: mintUrl, proofs: send }); +console.log("Send this:", token); // cashuB... +``` + +### 3. Receiving (Accept Ecash) + +**Flow:** +``` +1. Decode token + decoded = decode_token_v4(token_string) + → {mint: "url", proofs: [...]} + +2. Verify mint URL (trust check) + +3. Swap received proofs for new ones (prevents double-spend) + POST /v1/swap { + inputs: decoded.proofs, + outputs: blinded_messages_for_same_total + } + → {signatures: [...]} + +4. Unblind signatures + new_proofs = unblind(signatures) + +5. Store new_proofs in wallet +``` + +**Why swap on receive?** +- Sender knows the secrets, could double-spend +- Swapping creates new secrets only recipient knows +- Makes tokens truly bearer assets after swap + +**Example:** +```typescript +const token = getDecodedToken(tokenString); +const newProofs = await wallet.receive(token); +// newProofs are now safe to spend +``` + +### 4. Melting (Withdraw Ecash → Get Bitcoin) + +**Flow:** +``` +1. Create melt quote with Lightning invoice + POST /v1/melt/quote/bolt11 {request: "lnbc..."} + → {quote: "quote_id", amount: 1000, fee_reserve: 10} + +2. Select proofs for amount + fee_reserve + proofs = coin_select(wallet.proofs, 1010) + +3. Melt tokens + POST /v1/melt/bolt11 { + quote: "quote_id", + inputs: proofs + } + → {paid: true, payment_preimage: "...", change: [signatures]} + +4. If overpaid, unblind change + if change: + change_proofs = unblind(change) + store(change_proofs) + +5. Lightning payment is now complete +``` + +**Example (pay 1000 sat invoice with 1024 sats of proofs):** +```typescript +const invoice = "lnbc1000..."; +const meltQuote = await mint.createMeltQuote(invoice); +// meltQuote.amount = 1000, meltQuote.fee_reserve = 24 + +const proofs = selectProofs(1024); // Cover amount + fee +const meltResult = await mint.meltTokens(meltQuote.quote, proofs); + +if (meltResult.change) { + const changeProofs = unblind(meltResult.change); + // Actual fee was less, got change back +} +``` + +### 5. Checking Token State + +**Flow:** +``` +POST /v1/checkstate { + Ys: [hash_to_curve(secret1), hash_to_curve(secret2), ...] +} +→ { + states: [ + {Y: "02ab...", state: "UNSPENT", witness: null}, + {Y: "03cd...", state: "SPENT", witness: null}, + {Y: "02ef...", state: "PENDING", witness: null} + ] +} +``` + +**States:** +- `UNSPENT`: Token valid and unspent +- `SPENT`: Token already redeemed +- `PENDING`: Token in pending transaction + +## Implementation Best Practices + +### For Wallets + +1. **Key Management** + - Generate cryptographically secure random secrets + - Never reuse secrets (prevents linking) + - Store proofs encrypted at rest + +2. **Coin Selection** + - Use powers of 2 for efficient representation + - Minimize number of proofs sent (combines proofs) + - Consider fees in selection logic + +3. **Always Swap on Receive** + - Sender knows secrets, could double-spend + - Creates new secrets only you know + - Critical security practice + +4. **Multiple Mint Support** + - Allow users to trust multiple mints + - Separate balance per mint + - V4 tokens support multi-mint payments + +5. **Backup and Recovery** + - Implement NUT-13 (deterministic secrets) + - Use NUT-09 for signature restoration + - Store seed securely (12/24 word mnemonic) + +6. **Error Handling** + - Handle double-spend errors gracefully + - Retry failed melt operations + - Check token state before sending + +7. **Privacy Considerations** + - Don't correlate amounts across sessions + - Use Tor/VPN for mint connections + - Regularly swap tokens for new secrets + +### For Mints + +1. **Signature Verification** + - Always verify proof signatures + - Check secrets not in spent database + - Validate keyset IDs + +2. **Database Management** + - Index spent secrets for fast lookup + - Store pending operations atomically + - Archive old keysets (never delete) + +3. **Lightning Integration** + - Robust Lightning node management + - Handle payment failures gracefully + - Return change for overpaid fees (NUT-08) + +4. **Key Rotation** + - Rotate keysets periodically + - Keep old keysets active for swapping + - Announce deprecation before deactivation + +5. **Rate Limiting** + - Prevent spam (checkstate, mint quote spam) + - Implement proof-of-work (optional) + - Monitor for abuse patterns + +6. **DLEQ Proofs** (Recommended) + - Implement NUT-12 for transparency + - Proves signatures are valid + - Builds user trust + +7. **Monitoring and Logging** + - Track Lightning balance vs issued proofs + - Alert on discrepancies + - Log all operations for auditing + +### Security Considerations + +1. **Mint Custody Risk** + - Mints are custodial (hold Bitcoin) + - Users must trust mint operator + - Diversify across multiple mints + +2. **Double-Spend Prevention** + - Mint tracks spent secrets + - Atomic swap operations + - Network race conditions possible (accept only once) + +3. **Key Compromise** + - If mint's private key leaks, tokens can be forged + - Key rotation limits damage + - Monitor for anomalies + +4. **Secret Reuse** + - NEVER reuse secrets (breaks privacy) + - NEVER share secrets before receiving payment + - Always generate fresh secrets + +5. **Token Lifetime** + - Tokens don't expire (unless mint goes offline) + - Old keysets can become invalid + - Swap to active keysets regularly + +6. **Network Privacy** + - Use Tor for mint connections + - Don't reveal IP to mint + - Avoid timing correlation + +## Common Patterns + +### Coin Selection Algorithm + +```typescript +function selectProofs(proofs: Proof[], targetAmount: number): Proof[] { + // Sort by amount descending + const sorted = proofs.sort((a, b) => b.amount - a.amount); + + const selected: Proof[] = []; + let sum = 0; + + for (const proof of sorted) { + if (sum >= targetAmount) break; + selected.push(proof); + sum += proof.amount; + } + + if (sum < targetAmount) { + throw new Error("Insufficient balance"); + } + + return selected; +} +``` + +### Amount to Denominations + +```typescript +function amountToDenominations(amount: number): number[] { + const denominations: number[] = []; + let remaining = amount; + let power = 0; + + while (remaining > 0) { + if (remaining & 1) { + denominations.push(1 << power); // 2^power + } + remaining >>= 1; + power++; + } + + return denominations; +} + +// Example: 1000 = 512 + 256 + 128 + 64 + 32 + 8 +console.log(amountToDenominations(1000)); +// [8, 32, 64, 128, 256, 512] +``` + +### Deterministic Secret Generation + +```typescript +import { sha256 } from '@noble/hashes/sha256'; +import { bytesToHex } from '@noble/hashes/utils'; + +function generateSecret(seed: Uint8Array, counter: number): string { + const counterBytes = new Uint8Array(8); + new DataView(counterBytes.buffer).setBigUint64(0, BigInt(counter), false); + + const combined = new Uint8Array(seed.length + counterBytes.length); + combined.set(seed); + combined.set(counterBytes, seed.length); + + return bytesToHex(sha256(combined)); +} +``` + +### Token Encoding Helper + +```typescript +import { encodeBase64Url } from './utils'; +import { encode as cborEncode } from 'cbor-x'; + +function encodeTokenV4(proofs: Proof[], mintUrl: string, memo?: string): string { + // Group proofs by keyset ID + const grouped = new Map(); + for (const proof of proofs) { + if (!grouped.has(proof.id)) { + grouped.set(proof.id, []); + } + grouped.get(proof.id)!.push(proof); + } + + // Build token object + const token = { + t: [{ + m: mintUrl, + u: "sat", + ...(memo && { d: memo }), + t: Array.from(grouped.entries()).map(([id, proofs]) => ({ + i: id.slice(0, 16), // Short form (8 bytes) + p: proofs.map(p => ({ + a: p.amount, + s: p.secret, + c: p.C + })) + })) + }] + }; + + const cbor = cborEncode(token); + return 'cashuB' + encodeBase64Url(cbor); +} +``` + +## Troubleshooting + +### Common Issues + +**Token Already Spent:** +- Cause: Secret already in spent database +- Solution: Don't reuse tokens, always swap received tokens +- Check state before sending: `POST /v1/checkstate` + +**Signature Verification Failed:** +- Cause: Invalid proof, wrong keyset, or tampered data +- Solution: Verify proof structure, check keyset ID, re-request from sender + +**Lightning Payment Failed:** +- Cause: Invoice expired, insufficient liquidity, routing failure +- Solution: Retry with new quote, try different route, check balance + +**Mint Not Responding:** +- Cause: Mint offline, network issues, rate limiting +- Solution: Use backup mint, check connection, wait and retry + +**Amount Mismatch:** +- Cause: Fee estimation wrong, coin selection error +- Solution: Request melt quote first (shows fee), add buffer for fees + +**Cannot Decode Token:** +- Cause: Invalid encoding, wrong version, corrupted data +- Solution: Check token prefix (cashuA/cashuB), validate base64, try different parser + +## Development Resources + +### Essential NUTs for Beginners + +Start with these specifications in order: +1. **NUT-00** - Cryptography and models (MUST read) +2. **NUT-01** - Mint public keys +3. **NUT-04** - Minting tokens +4. **NUT-05** - Melting tokens +5. **NUT-03** - Swapping tokens +6. **NUT-07** - Token state check + +### Testing and Development + +- **Reference Mint**: nutshell (Python implementation) +- **Test Mint**: Use local nutshell instance for development +- **Libraries**: cashu-ts (TypeScript), cashu-crab (Rust), cashu-feni (Dart) +- **Wallets**: Nutstash (web), eNuts (mobile), Cashu.me (web) +- **Tools**: Cashu Explorer, token decoder utilities + +### Key Repositories + +- **NUTs Repository**: https://github.com/cashubtc/nuts +- **Nutshell (Python mint)**: https://github.com/cashubtc/nutshell +- **cashu-ts (TypeScript)**: https://github.com/cashubtc/cashu-ts +- **Cashu Website**: https://cashu.space +- **Documentation**: https://docs.cashu.space + +## Reference Files + +For comprehensive technical details, see: +- **references/nuts-overview.md** - Detailed descriptions of all NUT specifications +- **references/common-patterns.md** - Code patterns and best practices + +## Quick Checklist + +When implementing Cashu: +- [ ] Proofs have all required fields (amount, secret, C, id) +- [ ] Secrets are cryptographically random (32 bytes) +- [ ] Never reuse secrets across operations +- [ ] Always swap tokens on receive (critical for security) +- [ ] Verify mint signatures using public keys from NUT-01 +- [ ] Use powers of 2 denominations for efficiency +- [ ] Handle keyset rotation gracefully +- [ ] Implement backup/recovery (NUT-09, NUT-13) +- [ ] Check token state before sending (NUT-07) +- [ ] Connected to multiple mints for redundancy +- [ ] Following relevant NUTs for features implemented + +## Official Resources + +- **Cashu Website**: https://cashu.space +- **Cashu Documentation**: https://docs.cashu.space +- **NUTs Repository**: https://github.com/cashubtc/nuts +- **Cashu Organization**: https://github.com/cashubtc +- **Cashu Community**: Telegram, Discord (links at cashu.space) diff --git a/.claude/skills/cashu/references/common-patterns.md b/.claude/skills/cashu/references/common-patterns.md new file mode 100644 index 0000000..c38f60b --- /dev/null +++ b/.claude/skills/cashu/references/common-patterns.md @@ -0,0 +1,1111 @@ +# Cashu Common Patterns and Best Practices + +This document provides practical implementation patterns, code examples, and best practices for building Cashu wallets and mints. + +## Table of Contents + +1. [Wallet Patterns](#wallet-patterns) +2. [Token Management](#token-management) +3. [Security Patterns](#security-patterns) +4. [Error Handling](#error-handling) +5. [Performance Optimization](#performance-optimization) +6. [Multi-Mint Wallets](#multi-mint-wallets) +7. [Backup and Recovery](#backup-and-recovery) +8. [Testing Patterns](#testing-patterns) + +--- + +## Wallet Patterns + +### Basic Wallet Structure + +```typescript +interface WalletState { + mintUrl: string; + proofs: Proof[]; + pendingProofs: Proof[]; + seed?: Uint8Array; + counter: number; +} + +class CashuWalletManager { + private state: WalletState; + private wallet: CashuWallet; + + async initialize(mintUrl: string, seed?: Uint8Array) { + this.state = { + mintUrl, + proofs: [], + pendingProofs: [], + seed, + counter: 0 + }; + + this.wallet = new CashuWallet(new CashuMint(mintUrl), { + mnemonicOrSeed: seed + }); + await this.wallet.loadMint(); + + // Load persisted state + await this.loadState(); + } + + async loadState() { + const stored = localStorage.getItem('wallet-state'); + if (stored) { + const parsed = JSON.parse(stored); + this.state.proofs = parsed.proofs || []; + this.state.counter = parsed.counter || 0; + } + } + + async saveState() { + localStorage.setItem('wallet-state', JSON.stringify({ + mintUrl: this.state.mintUrl, + proofs: this.state.proofs, + counter: this.state.counter + })); + } + + getBalance(): number { + return sumProofs(this.state.proofs); + } + + async sync() { + // Check which proofs are still valid + const states = await this.wallet.checkProofsSpent(this.state.proofs); + this.state.proofs = this.state.proofs.filter((p, i) => + states[i].state === 'UNSPENT' + ); + await this.saveState(); + } +} +``` + +### Proof State Management + +```typescript +enum ProofStatus { + AVAILABLE = 'available', + PENDING = 'pending', + SPENT = 'spent' +} + +class ProofManager { + private proofs = new Map(); + + addProof(proof: Proof) { + const key = this.proofKey(proof); + this.proofs.set(key, { proof, status: ProofStatus.AVAILABLE }); + } + + markPending(proof: Proof) { + const key = this.proofKey(proof); + const entry = this.proofs.get(key); + if (entry) { + entry.status = ProofStatus.PENDING; + } + } + + markSpent(proof: Proof) { + const key = this.proofKey(proof); + this.proofs.delete(key); // Remove spent proofs + } + + getAvailable(): Proof[] { + return Array.from(this.proofs.values()) + .filter(e => e.status === ProofStatus.AVAILABLE) + .map(e => e.proof); + } + + private proofKey(proof: Proof): string { + return `${proof.amount}:${proof.secret}`; + } + + // Rollback pending to available on error + rollbackPending() { + for (const entry of this.proofs.values()) { + if (entry.status === ProofStatus.PENDING) { + entry.status = ProofStatus.AVAILABLE; + } + } + } +} +``` + +--- + +## Token Management + +### Coin Selection Algorithms + +#### Greedy Selection (Minimal Proofs) + +```typescript +function greedySelect(proofs: Proof[], target: number): Proof[] { + // Sort descending to minimize number of proofs + const sorted = [...proofs].sort((a, b) => b.amount - a.amount); + + const selected: Proof[] = []; + let sum = 0; + + for (const proof of sorted) { + if (sum >= target) break; + selected.push(proof); + sum += proof.amount; + } + + if (sum < target) { + throw new Error(`Insufficient balance: need ${target}, have ${sum}`); + } + + return selected; +} +``` + +#### Exact Match Selection (Minimize Change) + +```typescript +function exactMatchSelect(proofs: Proof[], target: number): Proof[] { + // Try to find exact match first + const exactMatch = findSubsetSum(proofs, target); + if (exactMatch) return exactMatch; + + // Fall back to greedy with minimal overpayment + return greedySelect(proofs, target); +} + +function findSubsetSum(proofs: Proof[], target: number): Proof[] | null { + // Dynamic programming subset sum + const n = proofs.length; + const dp: boolean[][] = Array(n + 1).fill(null).map(() => + Array(target + 1).fill(false) + ); + + // Base case: sum of 0 is always possible + for (let i = 0; i <= n; i++) { + dp[i][0] = true; + } + + // Fill DP table + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= target; j++) { + dp[i][j] = dp[i - 1][j]; // Don't include proof i + + if (j >= proofs[i - 1].amount) { + dp[i][j] = dp[i][j] || dp[i - 1][j - proofs[i - 1].amount]; + } + } + } + + if (!dp[n][target]) return null; + + // Backtrack to find subset + const selected: Proof[] = []; + let i = n, j = target; + + while (i > 0 && j > 0) { + if (!dp[i - 1][j]) { + selected.push(proofs[i - 1]); + j -= proofs[i - 1].amount; + } + i--; + } + + return selected; +} +``` + +### Amount Decomposition + +```typescript +function amountToDenominations(amount: number): number[] { + const denominations: number[] = []; + let remaining = amount; + let power = 0; + + // Binary decomposition (powers of 2) + while (remaining > 0) { + if (remaining & 1) { + denominations.push(1 << power); + } + remaining >>= 1; + power++; + } + + return denominations; +} + +// Example: 1337 = 1024 + 256 + 32 + 16 + 8 + 1 +console.log(amountToDenominations(1337)); +// [1, 8, 16, 32, 256, 1024] +``` + +### Token Validation + +```typescript +function validateProof(proof: Proof): boolean { + return ( + typeof proof.amount === 'number' && + proof.amount > 0 && + typeof proof.secret === 'string' && + proof.secret.length > 0 && + typeof proof.C === 'string' && + proof.C.length === 66 && // Compressed secp256k1 point + typeof proof.id === 'string' && + proof.id.length === 16 // 8-byte hex + ); +} + +function validateToken(token: Token): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!token.token || !Array.isArray(token.token)) { + errors.push('Token must have token array'); + return { valid: false, errors }; + } + + for (const entry of token.token) { + if (!entry.mint || typeof entry.mint !== 'string') { + errors.push('Missing or invalid mint URL'); + } + + if (!entry.proofs || !Array.isArray(entry.proofs)) { + errors.push('Missing or invalid proofs array'); + } + + for (const proof of entry.proofs) { + if (!validateProof(proof)) { + errors.push(`Invalid proof: ${JSON.stringify(proof)}`); + } + } + } + + return { valid: errors.length === 0, errors }; +} +``` + +--- + +## Security Patterns + +### Secure Seed Storage + +```typescript +import { encrypt, decrypt } from './crypto'; + +class SecureSeedStorage { + private static STORAGE_KEY = 'wallet-seed-encrypted'; + + static async storeSeed(seed: Uint8Array, password: string) { + // Derive encryption key from password + const salt = crypto.getRandomValues(new Uint8Array(16)); + const key = await this.deriveKey(password, salt); + + // Encrypt seed + const encrypted = await encrypt(seed, key); + + // Store with salt + const stored = { + encrypted: Array.from(encrypted), + salt: Array.from(salt) + }; + + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(stored)); + } + + static async retrieveSeed(password: string): Promise { + const storedStr = localStorage.getItem(this.STORAGE_KEY); + if (!storedStr) return null; + + const stored = JSON.parse(storedStr); + const encrypted = new Uint8Array(stored.encrypted); + const salt = new Uint8Array(stored.salt); + + // Derive key from password + const key = await this.deriveKey(password, salt); + + // Decrypt seed + try { + return await decrypt(encrypted, key); + } catch { + return null; // Wrong password + } + } + + private static async deriveKey(password: string, salt: Uint8Array): Promise { + const encoder = new TextEncoder(); + const passwordKey = await crypto.subtle.importKey( + 'raw', + encoder.encode(password), + 'PBKDF2', + false, + ['deriveBits', 'deriveKey'] + ); + + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations: 100000, + hash: 'SHA-256' + }, + passwordKey, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] + ); + } +} +``` + +### Secret Generation Best Practices + +```typescript +class SecretGenerator { + // Cryptographically secure random secret + static generateRandom(): string { + const bytes = crypto.getRandomValues(new Uint8Array(32)); + return Array.from(bytes) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + } + + // Deterministic secret (for backup) + static generateDeterministic( + seed: Uint8Array, + counter: number, + keysetId: string, + amount: number + ): string { + const counterBytes = new Uint8Array(8); + new DataView(counterBytes.buffer).setBigUint64(0, BigInt(counter), false); + + const amountBytes = new Uint8Array(8); + new DataView(amountBytes.buffer).setBigUint64(0, BigInt(amount), false); + + const keysetBytes = new Uint8Array( + keysetId.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)) + ); + + // HMAC-SHA256(seed, counter || keyset_id || amount) + const message = new Uint8Array([ + ...counterBytes, + ...keysetBytes, + ...amountBytes + ]); + + return this.hmacSHA256(seed, message); + } + + private static hmacSHA256(key: Uint8Array, message: Uint8Array): string { + // Use @noble/hashes or similar + return bytesToHex(hmac(sha256, key, message)); + } +} +``` + +### Spending Condition Verification + +```typescript +function verifyP2PKWitness( + proof: Proof, + publicKey: string +): boolean { + try { + const secret = JSON.parse(proof.secret); + if (!Array.isArray(secret) || secret[0] !== 'P2PK') { + return true; // Not P2PK locked + } + + const condition = secret[1]; + if (condition.data !== publicKey) { + return false; // Wrong pubkey + } + + if (!proof.witness) { + return false; // Missing witness + } + + const witness = JSON.parse(proof.witness); + const signatures = witness.signatures; + + if (!signatures || signatures.length === 0) { + return false; // No signatures + } + + // Verify signature (implementation depends on sigflag) + return verifySchnorrSignature( + signatures[0], + publicKey, + proof + ); + } catch { + return false; + } +} +``` + +--- + +## Error Handling + +### Robust Minting Flow + +```typescript +async function mintWithRetry( + wallet: CashuWallet, + amount: number, + maxRetries = 3 +): Promise { + let quote: MintQuoteResponse | null = null; + + // Step 1: Create quote + for (let i = 0; i < maxRetries; i++) { + try { + quote = await wallet.createMintQuote(amount); + break; + } catch (error) { + if (i === maxRetries - 1) throw error; + await sleep(2000 * (i + 1)); // Exponential backoff + } + } + + if (!quote) throw new Error('Failed to create quote'); + + // Step 2: Wait for payment + console.log('Pay invoice:', quote.request); + + let paid = false; + for (let i = 0; i < 60; i++) { // Wait up to 2 minutes + try { + const status = await wallet.checkMintQuote(quote.quote); + if (status.paid) { + paid = true; + break; + } + } catch (error) { + console.error('Error checking quote:', error); + } + await sleep(2000); + } + + if (!paid) throw new Error('Payment timeout'); + + // Step 3: Mint tokens with retry + for (let i = 0; i < maxRetries; i++) { + try { + const { proofs } = await wallet.mintProofs(amount, quote.quote); + return proofs; + } catch (error) { + if (error.message.includes('already issued')) { + // Quote already minted, try to restore + // (requires NUT-09 and deterministic secrets) + throw new Error('Quote already minted, use restore'); + } + + if (i === maxRetries - 1) throw error; + await sleep(2000 * (i + 1)); + } + } + + throw new Error('Failed to mint tokens'); +} +``` + +### Transaction Rollback + +```typescript +class Transaction { + private originalProofs: Proof[]; + private proofManager: ProofManager; + private committed = false; + + constructor(proofs: Proof[], manager: ProofManager) { + this.originalProofs = [...proofs]; + this.proofManager = manager; + } + + async execute(operation: () => Promise): Promise { + // Mark proofs as pending + for (const proof of this.originalProofs) { + this.proofManager.markPending(proof); + } + + try { + const newProofs = await operation(); + this.committed = true; + + // Mark old proofs as spent + for (const proof of this.originalProofs) { + this.proofManager.markSpent(proof); + } + + // Add new proofs + for (const proof of newProofs) { + this.proofManager.addProof(proof); + } + + return newProofs; + } catch (error) { + // Rollback on error + if (!this.committed) { + this.proofManager.rollbackPending(); + } + throw error; + } + } +} + +// Usage +const tx = new Transaction(proofsToSend, proofManager); +const newProofs = await tx.execute(async () => { + return await wallet.send(amount, proofsToSend); +}); +``` + +--- + +## Performance Optimization + +### Proof Caching and Indexing + +```typescript +class ProofCache { + private byAmount = new Map(); + private byKeyset = new Map(); + private bySecret = new Map(); + + addProofs(proofs: Proof[]) { + for (const proof of proofs) { + // Index by amount + if (!this.byAmount.has(proof.amount)) { + this.byAmount.set(proof.amount, []); + } + this.byAmount.get(proof.amount)!.push(proof); + + // Index by keyset + if (!this.byKeyset.has(proof.id)) { + this.byKeyset.set(proof.id, []); + } + this.byKeyset.get(proof.id)!.push(proof); + + // Index by secret + this.bySecret.set(proof.secret, proof); + } + } + + getByAmount(amount: number): Proof[] { + return this.byAmount.get(amount) || []; + } + + getByKeyset(keysetId: string): Proof[] { + return this.byKeyset.get(keysetId) || []; + } + + findBySecret(secret: string): Proof | undefined { + return this.bySecret.get(secret); + } + + // Fast coin selection using indexed amounts + selectFast(target: number): Proof[] { + const denominations = amountToDenominations(target); + const selected: Proof[] = []; + + for (const amount of denominations) { + const candidates = this.getByAmount(amount); + if (candidates.length === 0) { + // Need to split larger proofs + return this.selectWithSplitting(target); + } + selected.push(candidates[0]); + } + + return selected; + } + + private selectWithSplitting(target: number): Proof[] { + // Fall back to greedy selection + const allProofs = Array.from(this.bySecret.values()); + return greedySelect(allProofs, target); + } +} +``` + +### Batch Operations + +```typescript +async function batchCheckState( + wallet: CashuWallet, + proofs: Proof[], + batchSize = 100 +): Promise { + const results: ProofState[] = []; + + // Process in batches to avoid overwhelming mint + for (let i = 0; i < proofs.length; i += batchSize) { + const batch = proofs.slice(i, i + batchSize); + const states = await wallet.checkProofsSpent(batch); + results.push(...states); + + // Small delay between batches + if (i + batchSize < proofs.length) { + await sleep(100); + } + } + + return results; +} +``` + +--- + +## Multi-Mint Wallets + +### Complete Multi-Mint Implementation + +```typescript +interface MintConfig { + url: string; + name: string; + trusted: boolean; + maxBalance?: number; +} + +class MultiMintWallet { + private wallets = new Map(); + private proofs = new Map(); + private configs = new Map(); + + async addMint(config: MintConfig) { + const wallet = new CashuWallet(new CashuMint(config.url)); + await wallet.loadMint(); + + this.wallets.set(config.url, wallet); + this.proofs.set(config.url, []); + this.configs.set(config.url, config); + } + + async removeMint(mintUrl: string) { + const balance = this.getBalance(mintUrl); + if (balance > 0) { + throw new Error(`Cannot remove mint with balance: ${balance} sats`); + } + + this.wallets.delete(mintUrl); + this.proofs.delete(mintUrl); + this.configs.delete(mintUrl); + } + + getBalance(mintUrl?: string): number { + if (mintUrl) { + const proofs = this.proofs.get(mintUrl) || []; + return sumProofs(proofs); + } + + // Total balance across all mints + let total = 0; + for (const proofs of this.proofs.values()) { + total += sumProofs(proofs); + } + return total; + } + + async send(mintUrl: string, amount: number): Promise { + const wallet = this.wallets.get(mintUrl); + const proofs = this.proofs.get(mintUrl); + + if (!wallet || !proofs) { + throw new Error('Mint not found'); + } + + const balance = sumProofs(proofs); + if (balance < amount) { + throw new Error(`Insufficient balance: have ${balance}, need ${amount}`); + } + + const { keep, send } = await wallet.send(amount, proofs); + + // Update state + this.proofs.set(mintUrl, keep); + + // Encode token + return getEncodedTokenV4({ + token: [{ mint: mintUrl, proofs: send }] + }); + } + + async receive(tokenString: string): Promise<{ mint: string; amount: number }[]> { + const decoded = getDecodedToken(tokenString); + const results: { mint: string; amount: number }[] = []; + + for (const entry of decoded.token) { + // Check if mint is trusted + const config = this.configs.get(entry.mint); + if (!config?.trusted) { + console.warn(`Untrusted mint: ${entry.mint}`); + // Could prompt user for confirmation + } + + // Get or create wallet for this mint + let wallet = this.wallets.get(entry.mint); + if (!wallet) { + await this.addMint({ + url: entry.mint, + name: entry.mint, + trusted: false + }); + wallet = this.wallets.get(entry.mint)!; + } + + // Receive tokens + const received = await wallet.receive({ token: [entry] }); + const amount = sumProofs(received); + + // Store proofs + const existing = this.proofs.get(entry.mint) || []; + this.proofs.set(entry.mint, [...existing, ...received]); + + results.push({ mint: entry.mint, amount }); + } + + return results; + } + + // Smart send: select mint with sufficient balance + async sendAuto(amount: number): Promise { + // Try trusted mints first + for (const [mintUrl, config] of this.configs) { + if (!config.trusted) continue; + + const balance = this.getBalance(mintUrl); + if (balance >= amount) { + return this.send(mintUrl, amount); + } + } + + // Fall back to any mint + for (const mintUrl of this.wallets.keys()) { + const balance = this.getBalance(mintUrl); + if (balance >= amount) { + return this.send(mintUrl, amount); + } + } + + throw new Error(`Insufficient balance across all mints`); + } + + // Rebalance proofs across mints + async rebalance() { + // Move small balances to trusted mints + // (requires Lightning payments: melt from one mint, mint to another) + throw new Error('Not implemented: requires Lightning integration'); + } +} +``` + +--- + +## Backup and Recovery + +### Complete Backup Solution + +```typescript +interface WalletBackup { + version: number; + timestamp: number; + mints: Array<{ + url: string; + name: string; + proofs: Proof[]; + }>; + seed?: string; // Encrypted + counter?: number; +} + +class BackupManager { + async createBackup( + wallet: MultiMintWallet, + password: string + ): Promise { + const backup: WalletBackup = { + version: 1, + timestamp: Date.now(), + mints: [] + }; + + // Backup proofs from each mint + for (const [url, proofs] of wallet['proofs']) { + backup.mints.push({ + url, + name: wallet['configs'].get(url)?.name || url, + proofs + }); + } + + // Encrypt backup + const json = JSON.stringify(backup); + const encrypted = await this.encrypt(json, password); + + return btoa(String.fromCharCode(...new Uint8Array(encrypted))); + } + + async restoreBackup( + backupString: string, + password: string + ): Promise { + // Decrypt backup + const encrypted = Uint8Array.from(atob(backupString), c => c.charCodeAt(0)); + const json = await this.decrypt(encrypted, password); + const backup: WalletBackup = JSON.parse(json); + + // Recreate wallet + const wallet = new MultiMintWallet(); + + for (const mintBackup of backup.mints) { + await wallet.addMint({ + url: mintBackup.url, + name: mintBackup.name, + trusted: true + }); + + // Check which proofs are still valid + const cashuWallet = wallet['wallets'].get(mintBackup.url)!; + const states = await cashuWallet.checkProofsSpent(mintBackup.proofs); + + const validProofs = mintBackup.proofs.filter((p, i) => + states[i].state === 'UNSPENT' + ); + + wallet['proofs'].set(mintBackup.url, validProofs); + } + + return wallet; + } + + private async encrypt(data: string, password: string): Promise { + // Implementation using Web Crypto API + const encoder = new TextEncoder(); + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const key = await this.deriveKey(password, salt); + const encrypted = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + encoder.encode(data) + ); + + // Combine salt + iv + encrypted + const result = new Uint8Array(salt.length + iv.length + encrypted.byteLength); + result.set(salt, 0); + result.set(iv, salt.length); + result.set(new Uint8Array(encrypted), salt.length + iv.length); + + return result.buffer; + } + + private async decrypt(encrypted: Uint8Array, password: string): Promise { + const salt = encrypted.slice(0, 16); + const iv = encrypted.slice(16, 28); + const data = encrypted.slice(28); + + const key = await this.deriveKey(password, salt); + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv }, + key, + data + ); + + const decoder = new TextDecoder(); + return decoder.decode(decrypted); + } + + private async deriveKey(password: string, salt: Uint8Array): Promise { + const encoder = new TextEncoder(); + const passwordKey = await crypto.subtle.importKey( + 'raw', + encoder.encode(password), + 'PBKDF2', + false, + ['deriveKey'] + ); + + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations: 100000, + hash: 'SHA-256' + }, + passwordKey, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] + ); + } +} +``` + +--- + +## Testing Patterns + +### Mock Mint for Testing + +```typescript +class MockMint implements MintAPI { + private keysets = new Map(); + private spentSecrets = new Set(); + private quotes = new Map(); + + constructor() { + // Generate test keyset + const keys = this.generateTestKeys(); + this.keysets.set(keys.id, keys); + } + + async getKeys(): Promise { + return Array.from(this.keysets.values())[0]; + } + + async createMintQuote(amount: number): Promise { + const quote = `test-quote-${Math.random()}`; + this.quotes.set(quote, { amount, paid: false }); + + return { + quote, + request: `lnbc${amount}n...`, // Mock invoice + paid: false, + expiry: Date.now() + 3600000 + }; + } + + async checkMintQuote(quoteId: string): Promise { + const quote = this.quotes.get(quoteId); + if (!quote) throw new Error('Quote not found'); + + return { + quote: quoteId, + request: `lnbc${quote.amount}n...`, + paid: quote.paid, + expiry: Date.now() + 3600000 + }; + } + + // Mock: Instantly mark quote as paid + mockPayQuote(quoteId: string) { + const quote = this.quotes.get(quoteId); + if (quote) { + quote.paid = true; + } + } + + async mintTokens(quoteId: string, outputs: BlindedMessage[]): Promise<{ signatures: BlindSignature[] }> { + const quote = this.quotes.get(quoteId); + if (!quote) throw new Error('Quote not found'); + if (!quote.paid) throw new Error('Quote not paid'); + + // Mock: Return blind signatures + const signatures = outputs.map(output => ({ + amount: output.amount, + id: output.id, + C_: this.mockBlindSign(output.B_) + })); + + return { signatures }; + } + + async swap(inputs: Proof[], outputs: BlindedMessage[]): Promise<{ signatures: BlindSignature[] }> { + // Verify inputs not spent + for (const input of inputs) { + if (this.spentSecrets.has(input.secret)) { + throw new Error('Token already spent'); + } + } + + // Mark as spent + for (const input of inputs) { + this.spentSecrets.add(input.secret); + } + + // Return blind signatures + const signatures = outputs.map(output => ({ + amount: output.amount, + id: output.id, + C_: this.mockBlindSign(output.B_) + })); + + return { signatures }; + } + + private generateTestKeys(): MintKeys { + // Generate mock keys (not cryptographically valid) + const keys: Record = {}; + for (const amount of [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]) { + keys[amount] = '02' + '00'.repeat(32); // Mock pubkey + } + + return { + id: '00ffd48b78a7b2f4', + unit: 'sat', + keys + }; + } + + private mockBlindSign(B_: string): string { + // Mock blind signature (not cryptographically valid) + return '03' + '00'.repeat(32); + } +} +``` + +### Integration Test Example + +```typescript +describe('Cashu Wallet Integration', () => { + let wallet: CashuWallet; + let mockMint: MockMint; + + beforeEach(async () => { + mockMint = new MockMint(); + wallet = new CashuWallet(mockMint); + await wallet.loadMint(); + }); + + it('should mint tokens', async () => { + // Create quote + const quote = await wallet.createMintQuote(1000); + expect(quote.paid).toBe(false); + + // Mock payment + mockMint.mockPayQuote(quote.quote); + + // Mint tokens + const { proofs } = await wallet.mintProofs(1000, quote.quote); + expect(sumProofs(proofs)).toBe(1000); + expect(proofs.length).toBeLessThanOrEqual(10); // Binary decomposition + }); + + it('should send tokens', async () => { + // Mint first + const quote = await wallet.createMintQuote(1000); + mockMint.mockPayQuote(quote.quote); + const { proofs } = await wallet.mintProofs(1000, quote.quote); + + // Send + const { keep, send } = await wallet.send(100, proofs); + + expect(sumProofs(send)).toBe(100); + expect(sumProofs(keep)).toBe(900); + }); +}); +``` + +--- + +## Resources + +- **Cashu Docs**: https://docs.cashu.space +- **cashu-ts Examples**: https://github.com/cashubtc/cashu-ts/tree/main/test +- **Nutshell (Python)**: https://github.com/cashubtc/nutshell diff --git a/.claude/skills/cashu/references/nuts-overview.md b/.claude/skills/cashu/references/nuts-overview.md new file mode 100644 index 0000000..6729b02 --- /dev/null +++ b/.claude/skills/cashu/references/nuts-overview.md @@ -0,0 +1,871 @@ +# Cashu NUTs (Notation, Usage, and Terminology) - Complete Reference + +This document provides detailed descriptions of all Cashu NUT specifications. NUTs define the Cashu protocol and enable interoperability between different implementations. + +## NUT Status + +- **Mandatory**: All wallets and mints MUST implement +- **Optional**: Implementations CAN implement for enhanced features +- **Draft**: Under development, subject to change + +## Mandatory NUTs + +### NUT-00: Cryptography and Models + +**Status**: Mandatory +**Purpose**: Foundational cryptographic protocol and data structures + +#### Overview + +Defines the Blind Diffie-Hellman Key Exchange (BDHKE) scheme used for blind signatures, core data models, token serialization formats, and error handling. + +#### Key Concepts + +**BDHKE Scheme:** +``` +1. Alice generates secret x, computes Y = hash_to_curve(x) +2. Alice blinds: B_ = Y + r·G +3. Bob signs: C_ = k·B_ +4. Alice unblinds: C = C_ - r·K +5. Verification: k·hash_to_curve(x) == C +``` + +**Hash-to-Curve:** +``` +DOMAIN_SEPARATOR = b"Secp256k1_HashToCurve_Cashu_" +msg_hash = SHA256(DOMAIN_SEPARATOR || x) + +counter = 0 +loop: + candidate = SHA256(msg_hash || counter) + try parse as secp256k1 point + if valid: return point + counter += 1 +``` + +#### Data Structures + +**BlindedMessage** (wallet → mint): +```json +{ + "amount": 64, + "id": "00ffd48b78a7b2f4", + "B_": "02ab3c4d5e..." +} +``` + +**BlindSignature** (mint → wallet): +```json +{ + "amount": 64, + "id": "00ffd48b78a7b2f4", + "C_": "03bc5d6e7f..." +} +``` + +**Proof** (unblinded token): +```json +{ + "amount": 64, + "secret": "9a3b2c1d4e5f6a7b8c9d0e1f2a3b4c5d", + "C": "02cd7e8f9a...", + "id": "00ffd48b78a7b2f4", + "witness": "..." // Optional (P2PK, HTLC) +} +``` + +#### Token Serialization + +**V4 Format (Current)**: `cashuB[base64_urlsafe_cbor]` +- CBOR encoding for efficiency +- Abbreviated keys (t, m, u, d, i, p, a, s, c) +- Short keyset IDs (8 bytes) +- Multi-mint support + +**V3 Format (Deprecated)**: `cashuA[base64_urlsafe_json]` +- JSON encoding +- Full key names +- Legacy support only + +#### Error Codes + +Standard HTTP 400 response: +```json +{ + "detail": "human-readable message", + "code": 1000X +} +``` + +Common codes: +- `10001`: Token already spent +- `10002`: Invalid proof +- `10003`: Quote not paid +- `10004`: Insufficient funds + +--- + +### NUT-01: Mint Public Keys + +**Status**: Mandatory +**Endpoint**: `GET /v1/keys` or `GET /v1/keys/{keyset_id}` + +#### Purpose + +Exchange mint's public keys with wallets. Keys are needed to unblind signatures and verify proofs. + +#### Response Format + +```json +{ + "keysets": [ + { + "id": "00ffd48b78a7b2f4", + "unit": "sat", + "keys": { + "1": "02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104", + "2": "03b0f36d6d47ce14df8a7be9137712c42bcdd960b19dd02f1d4a9703b1f31d7513", + "4": "0366be6e026e42852498efb82014ca91e89da2e7a5bd3761bdad699fa2aec9f96c", + "8": "0253de5237f189606f29d8a690ea719f74d65f617bb1cb6fbea34f2bc4f930016d" + } + } + ] +} +``` + +#### Key Fields + +- `id`: Keyset identifier (16-byte hex, SHA256 of concatenated pubkeys) +- `unit`: Currency unit (sat, msat, usd, eur, etc.) +- `keys`: Map of amount → public key (hex-encoded compressed secp256k1 points) + +#### Key Rotation + +Mints can rotate keys by creating new keysets. Old keysets should remain available for swapping until all proofs are migrated. + +--- + +### NUT-02: Keysets and Fees + +**Status**: Mandatory +**Endpoint**: `GET /v1/keysets` + +#### Purpose + +List all keysets (active and inactive) with fee information. + +#### Response Format + +```json +{ + "keysets": [ + { + "id": "00ffd48b78a7b2f4", + "unit": "sat", + "active": true, + "input_fee_ppk": 0, + "output_fee_ppk": 0 + }, + { + "id": "00ab12cd34ef5678", + "unit": "sat", + "active": false, + "input_fee_ppk": 100, + "output_fee_ppk": 100 + } + ] +} +``` + +#### Fee Structure + +- `input_fee_ppk`: Fee per 1000 input proofs (parts per thousand) +- `output_fee_ppk`: Fee per 1000 output proofs + +**Example**: 100 ppk = 0.1 sat per proof = 100 sats per 1000 proofs + +#### Keyset Status + +- `active: true`: Mint will sign new proofs with this keyset +- `active: false`: Keyset deprecated, can still swap but not mint + +--- + +### NUT-03: Swapping Tokens + +**Status**: Mandatory +**Endpoint**: `POST /v1/swap` + +#### Purpose + +Exchange proofs for new proofs of the same total value. Used for coin selection, sending exact amounts, and key rotation. + +#### Request Format + +```json +{ + "inputs": [ + { + "amount": 128, + "secret": "secret1", + "C": "02ab3c...", + "id": "00ffd48b" + } + ], + "outputs": [ + { + "amount": 64, + "id": "00ffd48b", + "B_": "03cd7e..." + }, + { + "amount": 64, + "id": "00ffd48b", + "B_": "02ef9a..." + } + ] +} +``` + +#### Response Format + +```json +{ + "signatures": [ + { + "amount": 64, + "id": "00ffd48b", + "C_": "02ab3c..." + }, + { + "amount": 64, + "id": "00ffd48b", + "C_": "03de9f..." + } + ] +} +``` + +#### Validation + +- Sum of inputs MUST equal sum of outputs (minus fees) +- All input proofs MUST be valid and unspent +- Atomic operation: all or nothing +- Mint marks input secrets as spent + +--- + +### NUT-04: Minting Tokens + +**Status**: Mandatory +**Endpoints**: +- `POST /v1/mint/quote/bolt11` - Create quote +- `GET /v1/mint/quote/bolt11/{quote_id}` - Check quote +- `POST /v1/mint/bolt11` - Mint tokens + +#### Purpose + +Deposit Bitcoin via Lightning and receive ecash. + +#### Flow + +**1. Create Quote** + +Request: +```json +{ + "amount": 1000, + "unit": "sat" +} +``` + +Response: +```json +{ + "quote": "quote_12345", + "request": "lnbc1000n...", + "paid": false, + "expiry": 1704153600 +} +``` + +**2. Check Quote Status** + +Request: `GET /v1/mint/quote/bolt11/quote_12345` + +Response: +```json +{ + "quote": "quote_12345", + "request": "lnbc1000n...", + "paid": true, + "expiry": 1704153600 +} +``` + +**3. Mint Tokens** + +Request: +```json +{ + "quote": "quote_12345", + "outputs": [ + {"amount": 512, "id": "00ffd48b", "B_": "02ab..."}, + {"amount": 256, "id": "00ffd48b", "B_": "03cd..."}, + {"amount": 128, "id": "00ffd48b", "B_": "02ef..."}, + {"amount": 64, "id": "00ffd48b", "B_": "03gh..."}, + {"amount": 32, "id": "00ffd48b", "B_": "02ij..."}, + {"amount": 8, "id": "00ffd48b", "B_": "03kl..."} + ] +} +``` + +Response: +```json +{ + "signatures": [ + {"amount": 512, "id": "00ffd48b", "C_": "02mn..."}, + {"amount": 256, "id": "00ffd48b", "C_": "03op..."}, + {"amount": 128, "id": "00ffd48b", "C_": "02qr..."}, + {"amount": 64, "id": "00ffd48b", "C_": "03st..."}, + {"amount": 32, "id": "00ffd48b", "C_": "02uv..."}, + {"amount": 8, "id": "00ffd48b", "C_": "03wx..."} + ] +} +``` + +#### Error Codes + +- `20001`: Quote not paid +- `20002`: Quote expired +- `20003`: Quote already issued + +--- + +### NUT-05: Melting Tokens + +**Status**: Mandatory +**Endpoints**: +- `POST /v1/melt/quote/bolt11` - Create quote +- `GET /v1/melt/quote/bolt11/{quote_id}` - Check quote +- `POST /v1/melt/bolt11` - Melt tokens + +#### Purpose + +Withdraw Bitcoin via Lightning by melting ecash. + +#### Flow + +**1. Create Quote** + +Request: +```json +{ + "request": "lnbc1000n...", + "unit": "sat" +} +``` + +Response: +```json +{ + "quote": "melt_12345", + "amount": 1000, + "fee_reserve": 10, + "paid": false, + "expiry": 1704153600 +} +``` + +**2. Melt Tokens** + +Request: +```json +{ + "quote": "melt_12345", + "inputs": [ + {"amount": 512, "secret": "...", "C": "...", "id": "..."}, + {"amount": 256, "secret": "...", "C": "...", "id": "..."}, + {"amount": 128, "secret": "...", "C": "...", "id": "..."}, + {"amount": 64, "secret": "...", "C": "...", "id": "..."}, + {"amount": 32, "secret": "...", "C": "...", "id": "..."}, + {"amount": 16, "secret": "...", "C": "...", "id": "..."}, + {"amount": 2, "secret": "...", "C": "...", "id": "..."} + ], + "outputs": [ + {"amount": 8, "id": "00ffd48b", "B_": "02ab..."}, + {"amount": 2, "id": "00ffd48b", "B_": "03cd..."} + ] +} +``` + +Response: +```json +{ + "paid": true, + "payment_preimage": "0e5b2f1a...", + "change": [ + {"amount": 8, "id": "00ffd48b", "C_": "02ef..."}, + {"amount": 2, "id": "00ffd48b", "C_": "03gh..."} + ] +} +``` + +#### Fee Handling + +- `fee_reserve`: Estimated maximum fee +- Inputs MUST cover: amount + fee_reserve +- Actual fee may be less → change returned +- See NUT-08 for overpaid fee handling + +--- + +### NUT-06: Mint Information + +**Status**: Mandatory +**Endpoint**: `GET /v1/info` + +#### Purpose + +Provide mint metadata and capabilities. + +#### Response Format + +```json +{ + "name": "Example Mint", + "pubkey": "02ab3c4d...", + "version": "nutshell/0.15.0", + "description": "Community ecash mint", + "description_long": "A Cashu mint operated for the Bitcoin community...", + "contact": [ + ["email", "admin@example.com"], + ["nostr", "npub1..."], + ["twitter", "@example"] + ], + "motd": "Welcome! Please backup your tokens.", + "nuts": { + "4": { + "methods": [ + {"method": "bolt11", "unit": "sat", "min_amount": 1, "max_amount": 1000000} + ], + "disabled": false + }, + "5": { + "methods": [ + {"method": "bolt11", "unit": "sat", "min_amount": 1, "max_amount": 1000000} + ], + "disabled": false + }, + "7": {"supported": true}, + "8": {"supported": true}, + "9": {"supported": true}, + "10": {"supported": true}, + "11": {"supported": true}, + "12": {"supported": true} + } +} +``` + +#### Key Fields + +- `name`: Human-readable mint name +- `version`: Mint software version +- `nuts`: Supported NUT specifications with parameters +- `contact`: Contact methods (array of [type, value]) +- `motd`: Message of the day (displayed to users) + +--- + +## Optional NUTs + +### NUT-07: Token State Check + +**Status**: Optional (Highly Recommended) +**Endpoint**: `POST /v1/checkstate` + +#### Purpose + +Check if secrets are spent, unspent, or pending without revealing the secret itself. + +#### Request Format + +```json +{ + "Ys": [ + "02ab3c4d5e...", // Y = hash_to_curve(secret) + "03bc5d6e7f...", + "02cd7e8f9a..." + ] +} +``` + +#### Response Format + +```json +{ + "states": [ + { + "Y": "02ab3c4d5e...", + "state": "UNSPENT", + "witness": null + }, + { + "Y": "03bc5d6e7f...", + "state": "SPENT", + "witness": null + }, + { + "Y": "02cd7e8f9a...", + "state": "PENDING", + "witness": null + } + ] +} +``` + +#### States + +- `UNSPENT`: Secret has not been used +- `SPENT`: Secret has been redeemed +- `PENDING`: Secret is in pending transaction + +#### Use Cases + +- Wallet recovery (check which proofs are still valid) +- Verify token validity before accepting +- Detect double-spend attempts + +--- + +### NUT-08: Overpaid Lightning Fees + +**Status**: Optional (Recommended) +**Integrated**: NUT-05 melt response + +#### Purpose + +Return change when actual Lightning fee is less than fee_reserve. + +#### Implementation + +When melting (NUT-05): +1. Wallet provides inputs for amount + fee_reserve +2. Mint pays invoice (actual_fee ≤ fee_reserve) +3. If actual_fee < fee_reserve, mint returns change +4. Change = fee_reserve - actual_fee + +#### Example + +``` +Invoice amount: 1000 sats +Fee reserve: 10 sats +Inputs provided: 1010 sats +Actual fee: 3 sats +Change returned: 7 sats +``` + +--- + +### NUT-09: Restore Signatures + +**Status**: Optional (Recommended for backup) +**Endpoint**: `POST /v1/restore` + +#### Purpose + +Recover lost proofs using deterministic secrets (NUT-13). + +#### Request Format + +```json +{ + "outputs": [ + {"amount": 1, "id": "00ffd48b", "B_": "02ab..."}, + {"amount": 2, "id": "00ffd48b", "B_": "03cd..."}, + {"amount": 4, "id": "00ffd48b", "B_": "02ef..."} + ] +} +``` + +#### Response Format + +```json +{ + "outputs": [ + {"amount": 1, "id": "00ffd48b", "C_": "02gh..."}, + null, // Not issued + {"amount": 4, "id": "00ffd48b", "C_": "02ij..."} + ], + "signatures": [ + {"amount": 1, "id": "00ffd48b", "C_": "02gh..."}, + {"amount": 4, "id": "00ffd48b", "C_": "02ij..."} + ] +} +``` + +#### Usage Pattern + +1. Wallet generates deterministic secrets (counter-based) +2. After mint loss, wallet regenerates secrets with same counters +3. Wallet creates blinded messages from regenerated secrets +4. Mint returns signatures for secrets that were previously issued +5. Wallet unblinds to recover proofs + +--- + +### NUT-10: Spending Conditions + +**Status**: Optional +**Purpose**: Programmable spending conditions on proofs + +#### Supported Conditions + +- P2PK (NUT-11): Pay-to-Public-Key +- HTLC (NUT-14): Hash Time Locked Contracts +- Custom: Extensible format + +#### Secret Format + +Spending condition secrets are JSON arrays: +```json +[ + "condition_type", + { + "nonce": "random_hex", + "data": "condition_data", + "tags": [["key", "value"]] + } +] +``` + +#### Witness Format + +When spending condition-locked proofs, include witness: +```json +{ + "amount": 64, + "secret": "[\"P2PK\",{\"nonce\":\"abc\",\"data\":\"pubkey\"}]", + "C": "02ab3c...", + "id": "00ffd48b", + "witness": "{\"signatures\":[\"sig1\"]}" +} +``` + +--- + +### NUT-11: Pay-to-Public-Key (P2PK) + +**Status**: Optional +**Purpose**: Lock proofs to specific public key (requires signature to spend) + +#### Secret Format + +```json +[ + "P2PK", + { + "nonce": "da62796403af76c80cd6ce9153ed3746", + "data": "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e", + "tags": [ + ["sigflag", "SIG_INPUTS"], // Optional: what to sign + ["n_sigs", "2"], // Optional: multisig (N-of-M) + ["pubkeys", "pk1,pk2,pk3"] // Optional: additional pubkeys + ] + } +] +``` + +#### Witness Format + +```json +{ + "signatures": [ + "fd5e0e3e6e5c6fa14059c56a66e36c44e1e06d820b0e164f56f14051a85c1cda6a9d1d7ddbb15e98e3b4eb5a34f97e49141c64bb3cbc70b1bde85bc5b53e16bb" + ] +} +``` + +#### Signature Flags + +- `SIG_INPUTS`: Sign all input proofs +- `SIG_OUTPUTS`: Sign all outputs (prevents mint alteration) + +#### Use Cases + +- Secure transfers (recipient must have private key) +- Non-custodial storage (wallet can't spend without key) +- Escrow services + +--- + +### NUT-12: DLEQ Proofs + +**Status**: Optional (Recommended for trust) +**Purpose**: Discrete Log Equality proofs to verify mint signatures + +#### Concept + +Proves that `C = k·Y` without revealing `k` (mint's private key). + +#### DLEQ Structure + +```json +{ + "e": "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9", + "s": "9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9" +} +``` + +#### Verification + +``` +C = s·G + e·K +Y' = s·B_ + e·C_ + +Verify: e == SHA256(K || C || Y') +``` + +#### Benefits + +- Proves mint signed correctly +- Detects mint misbehavior (creating unbacked tokens) +- Builds user trust in mint + +--- + +### NUT-13: Deterministic Secrets + +**Status**: Optional (Recommended for backup) +**Purpose**: Generate secrets deterministically for wallet recovery + +#### Secret Generation + +``` +secret = HMAC-SHA256( + key = seed, + msg = counter || keyset_id || amount +) +``` + +#### Counter Format + +``` +counter = 8-byte big-endian integer +``` + +#### Usage Pattern + +1. Wallet generates seed (BIP39 mnemonic) +2. For each proof, increment counter +3. Generate secret from: seed + counter + keyset_id + amount +4. Store only seed and counter (not individual secrets) +5. For recovery: regenerate secrets and use NUT-09 restore + +#### Benefits + +- Backup entire wallet with 12/24 words +- Restore all proofs from seed +- No need to backup individual proofs + +--- + +### NUT-14: Hashed Timelock Contracts (HTLC) + +**Status**: Optional +**Purpose**: Time-locked proofs with hash preimage reveal + +#### Secret Format + +```json +[ + "HTLC", + { + "nonce": "da62796403af76c80cd6ce9153ed3746", + "data": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "tags": [ + ["locktime", "1704153600"], // Unix timestamp + ["refund", "pubkey"] // Refund pubkey after locktime + ] + } +] +``` + +#### Witness Format + +**Before locktime (with preimage):** +```json +{ + "preimage": "preimage_hex" +} +``` + +**After locktime (refund):** +```json +{ + "signatures": ["refund_signature"] +} +``` + +#### Use Cases + +- Atomic swaps +- Escrow with time limits +- Payment channels + +--- + +### Extended NUTs (NUT-15 to NUT-27) + +Brief descriptions of additional optional specifications: + +- **NUT-15**: MPP (Multi-Path Payments) - Split Lightning payments across routes +- **NUT-16**: Animated QR codes - Encode large tokens across multiple QR frames +- **NUT-17**: WebSocket subscriptions - Real-time updates for quotes and proofs +- **NUT-18**: Payment requests - Invoice-like payment requests +- **NUT-19**: Cached responses - Performance optimization via caching +- **NUT-20**: Multiple signature methods - Support various signature schemes +- **NUT-21**: BOLT11 - Lightning invoice support (standard for minting/melting) +- **NUT-22**: BOLT12 - Lightning offers support (reusable payment requests) +- **NUT-23**: HTTP 402 - Payment Required status code integration +- **NUT-24**: Bech32m - Alternative token encoding format +- **NUT-25**: Nostr backup - Backup wallet to Nostr relays (encrypted kind 10000+ events) +- **NUT-26**: Subscription payments - Recurring payments +- **NUT-27**: Multi-mint atomic swaps - Atomic swaps across different mints + +--- + +## Implementation Priority + +### For Basic Wallet + +1. NUT-00 (Cryptography) +2. NUT-01 (Mint Keys) +3. NUT-04 (Minting) +4. NUT-05 (Melting) +5. NUT-03 (Swapping) +6. NUT-07 (State Check) + +### For Production Wallet + +Add these to basic wallet: +7. NUT-08 (Overpaid Fees) +8. NUT-13 (Deterministic Secrets) +9. NUT-09 (Restore) +10. NUT-11 (P2PK) + +### For Advanced Features + +Add based on use case: +- NUT-12 (DLEQ) - Verify mint honesty +- NUT-14 (HTLC) - Atomic swaps, escrow +- NUT-17 (WebSocket) - Real-time updates +- NUT-25 (Nostr Backup) - Cloud backup + +--- + +## Resources + +- **Official NUTs**: https://github.com/cashubtc/nuts +- **NUT Status**: https://cashubtc.github.io/nuts/ +- **Cashu Docs**: https://docs.cashu.space