# Lightning Wallet

> Use when implementing a standalone Lightning wallet using Cashu mints - provides complete patterns for minting (receiving Lightning payments), melting (sending Lightning payments), mint management, transaction history, and integrating with Lightning addresses and invoices

- Skill: `majiayu000/lightning-wallet` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add majiayu000/lightning-wallet`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/lightning-wallet/raw
- Safety review: pending (external: skill-scanner PASS, skillspector CAUTION)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/lightning-wallet

---


# Lightning Wallet Implementation

## Overview

Complete implementation guide for a standalone Lightning wallet using Cashu mints and the coco-cashu-core library. This wallet focuses on Lightning operations (minting and melting) rather than token management, providing a streamlined interface for sending and receiving Lightning payments through Cashu mints.

**Core Capabilities:**
- Receive Lightning payments via mint quotes (minting)
- Send Lightning payments via melt quotes (melting)
- Manage multiple Cashu mints with trust/remove operations
- Track transaction history (mint/melt operations)
- Integrate with Lightning addresses and BOLT11 invoices
- Automatic quote processing and state watching
- QR code generation for invoices
- Comprehensive error handling and user feedback

## Prerequisites

### Required Dependencies

**IMPORTANT:** Before adding dependencies, review your project's `package.json` to check if any of these packages already exist. If they do, verify the versions are compatible with the requirements below. Only add packages that are missing or need version updates.

Add these packages to `package.json`:

```json
{
  "dependencies": {
    "@cashu/cashu-ts": "2.8.1",
    "coco-cashu-core": "1.1.2-rc.30",
    "coco-cashu-indexeddb": "1.1.2-rc.30",
    "dexie": "^4.0.8",
    "@scure/bip39": "1.6.0",
    "@scure/bip32": "^2.0.1",
    "@noble/hashes": "^2.0.1",
    "@noble/curves": "^2.0.1"
  }
}
```

**Critical Notes:**
- `@cashu/cashu-ts@2.8.1` **MUST** be explicitly added (required by `coco-cashu-core@1.1.2-rc.30`; prevents build system from resolving to incompatible `3.0.2` which lacks required sub-paths)
- `dexie@^4.0.8` **MUST** be added (required by `coco-cashu-indexeddb@1.1.2-rc.30` for IndexedDB operations)
- `@scure/bip39@1.6.0` **MUST** be used (version 2.0.0+ requires `@noble/hashes@2.0.0` which is now compatible with the updated `coco-cashu-core@1.1.2-rc.30`, but `@scure/bip39@1.6.0` works with both `@noble/hashes@1.8.0` and `@noble/hashes@^2.0.1`)
- `@scure/bip32@^2.0.1` **MUST** be explicitly added if your project uses BIP32 (required by `coco-cashu-core@1.1.2-rc.30` which uses `@noble/hashes@^2.0.1`)
- `@noble/curves@^2.0.1` **MUST** be used (required by `coco-cashu-core@1.1.2-rc.30` which uses `@noble/hashes@^2.0.1`)
- `@noble/hashes@^2.0.1` **MUST** be used (required by `coco-cashu-core@1.1.2-rc.30` and compatible with `@scure/bip32@^2.0.1`)

### Build System Compatibility

**For browser-based builds (act2/Shakespeare):**
- The build system uses esm.sh CDN to fetch packages
- Ensure `package-lock.json` exists (generated via `npm install` if needed)
- All required nested dependencies are listed above - no additional packages needed

**For Node.js builds (Vite/Webpack):**
- Run `npm install` to install dependencies locally
- Verify `node_modules` contains all packages before building

**Required skills (must be referenced/implemented):**
- `qr-code-generator` - QR code generation for Lightning invoices (see `qr-code-generator` skill)
- `lightning-address` - Lightning address support for sending payments (see `lightning-address` skill)
- `lightning-invoice-decoder` - BOLT11 invoice decoding and validation (see `lightning-invoice-decoder` skill)
- `exchange-rates` - Exchange rate functionality for displaying BTC/fiat conversions (see `exchange-rates` skill)

**Optional skills:**
- `npub-cash-address` - Static Lightning address from npub.cash (`username@npubx.cash`) for receiving payments (see `npub-cash-address` skill)

## Implementation Checklist

- [ ] Add all required packages to `package.json`: `@cashu/cashu-ts@2.8.1`, `coco-cashu-core@1.1.2-rc.30`, `coco-cashu-indexeddb@1.1.2-rc.30`, `dexie@^4.0.8`, `@scure/bip39@1.6.0`, `@scure/bip32@^2.0.1` (if using BIP32), `@noble/hashes@^2.0.1`, `@noble/curves@^2.0.1`
- [ ] Initialize coco-cashu-core with IndexedDB backend
- [ ] Implement mnemonic generation and storage
- [ ] Add default mint (`https://mint.minibits.cash/Bitcoin`) automatically if no mints exist
- [ ] Create mint management (add, remove, trust) - **REQUIRED**: Users must be able to add/remove mints including the default
- [ ] Implement Lightning receive (mint quotes)
- [ ] Implement Lightning send (melt quotes)
- [ ] Add transaction history tracking
- [ ] **REQUIRED:** Implement `qr-code-generator` skill for invoice QR codes
- [ ] **REQUIRED:** Implement `lightning-address` skill for sending payments
- [ ] **REQUIRED:** Implement `lightning-invoice-decoder` skill for invoice validation
- [ ] **REQUIRED:** Implement `exchange-rates` skill for BTC/fiat conversions
- [ ] Implement automatic quote processing
- [ ] Add error handling and user feedback
- [ ] Create React hooks for wallet operations

## Part 1: Understanding the Lightning Wallet Architecture

### Wallet vs Token Wallet

This Lightning wallet is **standalone** and focuses exclusively on Lightning operations:

- **Minting**: Receive Lightning payments by creating mint quotes that generate Lightning invoices
- **Melting**: Send Lightning payments by creating melt quotes that pay Lightning invoices
- **No Token Operations**: This wallet does not handle Cashu token transfers, splitting, or token management

### Cashu Mint Architecture

Cashu mints act as Lightning payment processors:

1. **Mint Quote (Receive)**: Request a Lightning invoice from a mint
   - User requests invoice for X sats
   - Mint generates Lightning invoice
   - User pays invoice via Lightning
   - Mint issues Cashu tokens (automatically converted to balance)
   - Tokens are immediately available in wallet

2. **Melt Quote (Send)**: Pay a Lightning invoice using mint tokens
   - User provides Lightning invoice
   - Mint creates melt quote
   - Mint pays invoice via Lightning
   - Tokens are deducted from wallet balance

### Library Structure

The coco-cashu-core library provides:

- **Manager**: Main wallet interface (`initializeCoco`)
- **Repositories**: Direct data access (proofs, mints, history)
- **Events**: Real-time updates via event emitters
- **Services**: Built-in watchers and processors for automatic quote handling

## Part 2: Core Wallet Initialization

### Wallet Setup Hook

**Initialize the coco-cashu-core wallet with IndexedDB storage:**

```typescript
// hooks/wallet/useCashu.ts
import { useState, useEffect, useCallback, useRef } from 'react';
import { useLocalStorage } from '../useLocalStorage';
import * as bip39 from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english.js';
import { initializeCoco, ConsoleLogger, getEncodedToken, type Repositories } from 'coco-cashu-core';
import { IndexedDbRepositories } from 'coco-cashu-indexeddb';
import type { Mint } from 'coco-cashu-core';

// Types from coco-cashu-core
type CocoManager = Awaited<ReturnType<typeof initializeCoco>>;

// Singleton pattern to prevent multiple initializations
let globalCocoInstance: CocoManager | null = null;
let globalRepositories: Repositories | null = null;
let globalInitializationPromise: Promise<{ coco: CocoManager; repositories: Repositories }> | null = null;

export interface UseCashuReturn {
  // The typed coco manager (direct library access)
  coco: CocoManager | null;
  
  // Repositories for direct access to proof data
  repositories: Repositories | null;
  
  // React state (for reactivity)
  balances: { [mintUrl: string]: number };
  mints: Array<{ url: string; name?: string; info?: unknown }>;
  totalBalance: number;
  
  // Status
  isInitialized: boolean;
  isLoading: boolean;
  error: Error | null;
  
  // Mnemonic management
  mnemonic: string | null;
  setMnemonic: (mnemonic: string) => void;
  generateMnemonic: () => string;
  
  // Utility functions
  clearError: () => void;
  getEncodedToken: typeof getEncodedToken;
  refreshMints: () => Promise<void>;
}

export function useCashu(): UseCashuReturn {
  const [isInitialized, setIsInitialized] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  
  // Mnemonic management with localStorage
  const [mnemonic, setMnemonicState] = useLocalStorage<string | null>('cashu-mnemonic', null);
  
  // Manager instance ref
  const cocoRef = useRef<CocoManager | null>(null);
  const repositoriesRef = useRef<Repositories | null>(null);
  
  // React state for UI reactivity
  const [balances, setBalances] = useState<{ [mintUrl: string]: number }>({});
  const [mints, setMints] = useState<Array<{ url: string; name?: string; info?: unknown }>>([]);

  const clearError = useCallback(() => {
    setError(null);
  }, []);

  const generateMnemonic = useCallback(() => {
    return bip39.generateMnemonic(wordlist);
  }, []);

  const refreshMints = useCallback(async () => {
    const coco = cocoRef.current;
    if (!coco) return;
    
    try {
      // Only show trusted mints in the UI
      const mintList = await coco.mint.getAllTrustedMints();
      setMints(mintList.map((m: Mint) => ({ 
        url: m.mintUrl, 
        name: m.name,
        info: m.mintInfo
      })));
    } catch (err) {
      console.error('Failed to refresh mints:', err);
    }
  }, []);

  const setMnemonic = useCallback((newMnemonic: string) => {
    if (!bip39.validateMnemonic(newMnemonic, wordlist)) {
      throw new Error('Invalid mnemonic');
    }
    setMnemonicState(newMnemonic);
    // Reinitialize manager with new mnemonic
    setIsInitialized(false);
  }, [setMnemonicState]);

  // Initialize the coco manager
  const initializeCocoManager = useCallback(async () => {
    if (isInitialized || isLoading) return;
    
    setIsLoading(true);
    setError(null);
    
    try {
      // Use singleton pattern to prevent multiple initializations
      if (globalCocoInstance && globalRepositories) {
        cocoRef.current = globalCocoInstance;
        repositoriesRef.current = globalRepositories;
        setIsInitialized(true);
        setIsLoading(false);
        return;
      }
      
      // If there's already an initialization in progress, wait for it
      if (globalInitializationPromise) {
        console.log('Waiting for existing initialization...');
        const { coco, repositories } = await globalInitializationPromise;
        cocoRef.current = coco;
        repositoriesRef.current = repositories;
        setIsInitialized(true);
        setIsLoading(false);
        return;
      }
      
      // Generate or retrieve mnemonic
      let currentMnemonic = mnemonic;
      if (!currentMnemonic) {
        currentMnemonic = bip39.generateMnemonic(wordlist);
        setMnemonicState(currentMnemonic);
      }
      
      // Create seed from mnemonic
      const seed = bip39.mnemonicToSeedSync(currentMnemonic);
      
      // Initialize repositories with proper initialization
      const repo = new IndexedDbRepositories({ name: "cashu-wallet" });
      await repo.init(); // Ensure repository is properly initialized
      
      // Create initialization promise
      globalInitializationPromise = (async () => {
        const coco = await initializeCoco({
          repo,
          seedGetter: async () => seed,
          logger: new ConsoleLogger("coco", { level: "debug" }) // Use debug level for better logging
        });
        return { coco, repositories: repo };
      })();
      
      // Initialize coco manager using the new API
      const { coco, repositories } = await globalInitializationPromise;
      
      // Store as singleton
      console.log('Storing new coco instance as singleton');
      globalCocoInstance = coco;
      globalRepositories = repositories;
      globalInitializationPromise = null;
      cocoRef.current = coco;
      repositoriesRef.current = repositories;
      
      // Enable built-in services for automatic quote processing and state watching
      try {
        console.log('Enabling coco services...');
        await coco.enableMintQuoteWatcher({ watchExistingPendingOnStart: false });
        console.log('Mint quote watcher enabled');
        
        await coco.enableMintQuoteProcessor();
        console.log('Mint quote processor enabled');
        
        await coco.enableProofStateWatcher();
        console.log('Proof state watcher enabled');
        
        console.log('All coco services enabled successfully');
      } catch (err) {
        // Don't fail initialization if watchers can't be enabled
        console.warn('Failed to enable some coco services:', err);
      }
      
      // Add default mint if no mints exist (users can remove it later)
      try {
        const DEFAULT_MINT_URL = 'https://mint.minibits.cash/Bitcoin';
        const existingMints = await coco.mint.getAllTrustedMints();
        const hasDefaultMint = existingMints.some((m: Mint) => 
          m.mintUrl.toLowerCase() === DEFAULT_MINT_URL.toLowerCase()
        );
        
        if (!hasDefaultMint && existingMints.length === 0) {
          console.log('Adding default mint:', DEFAULT_MINT_URL);
          await coco.mint.addMint(DEFAULT_MINT_URL, { trusted: true });
          console.log('Default mint added successfully');
        }
      } catch (err) {
        // Don't fail initialization if default mint can't be added
        console.warn('Failed to add default mint:', err);
      }
      
      setIsInitialized(true);
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : 'Failed to initialize Cashu wallet';
      setError(new Error(errorMessage));
      globalInitializationPromise = null; // Reset on error
    } finally {
      setIsLoading(false);
    }
  }, [isInitialized, isLoading, mnemonic, setMnemonicState]);

  // Initialize on mount and when mnemonic changes
  useEffect(() => {
    initializeCocoManager();
  }, [initializeCocoManager]);

  // Auto-refresh balances and mints on manager events
  useEffect(() => {
    const coco = cocoRef.current;
    if (!coco) return;
    
    const refreshBalances = async () => {
      try {
        const newBalances = await coco.wallet.getBalances();
        setBalances(newBalances);
      } catch (err) {
        console.log('Failed to refresh balances:', err);
      }
    };
    
    // Initial load
    refreshBalances();
    refreshMints();
    
    // Listen to events
    const unsubs = [
      coco.on('proofs:saved', refreshBalances),
      coco.on('proofs:state-changed', refreshBalances),
      coco.on('send:created', refreshBalances),
      coco.on('receive:created', refreshBalances),
      coco.on('mint:added', refreshMints),
      coco.on('mint:updated', refreshMints),
    ];
    
    return () => unsubs.forEach(unsub => unsub());
  }, [isInitialized, refreshMints]);

  // Calculate total balance
  const totalBalance = Object.values(balances).reduce((sum, balance) => sum + balance, 0);

  return {
    // The typed coco manager (direct library access)
    coco: cocoRef.current,
    
    // Repositories for direct access to proof data
    repositories: repositoriesRef.current,
    
    // React state (for reactivity)
    balances,
    mints,
    totalBalance,
    
    // Status
    isInitialized,
    isLoading,
    error,
    
    // Mnemonic management
    mnemonic,
    setMnemonic,
    generateMnemonic,
    
    // Utility functions
    clearError,
    getEncodedToken,
    refreshMints,
  };
}
```

**Key Points:**
- **Singleton Pattern**: Prevents multiple wallet initializations
- **Mnemonic Storage**: Uses localStorage for persistence
- **Automatic Services**: Enables quote watchers and processors for automatic handling
- **Default Mint**: Automatically adds `https://mint.minibits.cash/Bitcoin` as the default mint if no mints exist (users can remove it later)
- **Event Listeners**: Auto-refreshes balances and mints on wallet events
- **Repository Access**: Provides direct access to proof/mint data when needed

## Part 3: Mint Management

### Mint Manager Hook

**Manage Cashu mints (add, remove, trust):**

```typescript
// hooks/wallet/useMintManager.ts
import { useState, useCallback } from 'react';
import type { Manager, Repositories } from 'coco-cashu-core';
import { useToast } from '@/hooks/useToast';

export interface CashuMint {
  url: string;
  name?: string;
}

// Mint utility functions
export const getCleanMintLabel = (mint: { name?: string; url: string }): string => {
  if (mint.name && mint.name.trim().length > 0) {
    return mint.name.replace(/^https?:\/\//, '');
  }
  try {
    return new URL(mint.url).host;
  } catch {
    return mint.url.replace(/^https?:\/\//, '');
  }
};

export const isUnknownMintError = (err: unknown): boolean => {
  const errorMessage = err instanceof Error ? err.message : String(err);
  const errorName = err instanceof Error ? err.constructor.name : '';
  return errorName === 'UnknownMintError' || 
         errorMessage.includes('UnknownMintError') || 
         (errorMessage.includes('Mint') && errorMessage.includes('is not known'));
};

export const extractMintUrlFromError = (err: unknown): string | null => {
  const errorMessage = err instanceof Error ? err.message : String(err);
  const mintUrlMatch = errorMessage.match(/Mint (https?:\/\/[^\s]+)/);
  return mintUrlMatch?.[1] || null;
};

interface UseMintManagerProps {
  coco: Manager | null;
  mints: CashuMint[];
  repositories: Repositories | null;
  refreshMints: () => Promise<void>;
}

export function useMintManager({
  coco,
  mints,
  repositories,
  refreshMints,
}: UseMintManagerProps) {
  const { toast } = useToast();
  const [activeMintUrl, setActiveMintUrl] = useState<string | null>(null);
  const [mintUrl, setMintUrl] = useState('');
  const [mintToRemove, setMintToRemove] = useState<CashuMint | null>(null);
  const [showRemoveMintModal, setShowRemoveMintModal] = useState(false);

  // Set active mint
  const handleSetActiveMint = useCallback(
    async (url: string | null) => {
      setActiveMintUrl(url);
    },
    []
  );

  // Add a new mint
  const handleAddMint = useCallback(async () => {
    if (!mintUrl.trim() || !coco) return;
    try {
      // Normalize URL - add https:// if not present
      let normalizedUrl = mintUrl.trim();
      if (
        !normalizedUrl.startsWith('http://') &&
        !normalizedUrl.startsWith('https://')
      ) {
        normalizedUrl = `https://${normalizedUrl}`;
      }

      // Check if mint with same name already exists (case insensitive)
      const existingMint = mints.find(mint => 
        mint.url.toLowerCase() === normalizedUrl.toLowerCase()
      );
      
      if (existingMint) {
        // Default: Toast notification
        toast({ variant: 'destructive', title: 'Mint Already Exists', description: 'A mint with this URL already exists' });
        // Alternative options (commented):
        // Option 1: Console logging
        // console.error('Mint Already Exists: A mint with this URL already exists');
        // Option 2: No notification (silent failure)
        return;
      }

      // Add mint and automatically trust it
      await coco.mint.addMint(normalizedUrl, { trusted: true });
      setMintUrl('');

      // Optional: User feedback - choose one:
      // Option 1: Console logging
      // console.log('Mint Added: Mint added and trusted successfully!');
      // Option 2: Toast notification (if toast is available)
      // toast({ title: 'Mint Added', description: 'Mint added and trusted successfully!' });
      // Option 3: No notification (silent success)
    } catch (err) {
      // Default: Toast notification
      toast({ variant: 'destructive', title: 'Add Mint Failed', description: err instanceof Error ? err.message : 'Unknown error' });
      // Alternative options (commented):
      // Option 1: Console logging
      // console.error('Add Mint Failed:', err instanceof Error ? err.message : 'Unknown error');
      // Option 2: No notification (silent failure)
    }
  }, [mintUrl, coco, mints, toast]);

  // Show remove confirmation modal
  const handleRemoveMintClick = useCallback((mint: CashuMint) => {
    setMintToRemove(mint);
    setShowRemoveMintModal(true);
  }, []);

  // Confirm and remove mint
  const handleConfirmRemoveMint = useCallback(async () => {
    if (!mintToRemove || !repositories) return;

    try {
      // Delete the mint from the backend using the mint repository directly
      await repositories.mintRepository.deleteMint(mintToRemove.url);

      // If the removed mint was active, switch to the first available mint
      if (activeMintUrl === mintToRemove.url) {
        const remainingMints = mints.filter(
          (mint) => mint.url !== mintToRemove.url
        );
        if (remainingMints.length > 0) {
          handleSetActiveMint(remainingMints[0].url);
        } else {
          setActiveMintUrl(null);
        }
      }

      // Optional: User feedback - choose one:
      // Option 1: Console logging
      // console.log('Mint Removed:', `${mintToRemove.name || mintToRemove.url} has been permanently removed`);
      // Option 2: Toast notification (if toast is available)
      // toast({ title: 'Mint Removed', description: `${mintToRemove.name || mintToRemove.url} has been permanently removed` });
      // Option 3: No notification (silent success)

      // Refresh the mints list to update the dropdown immediately
      await refreshMints();

      setShowRemoveMintModal(false);
      setMintToRemove(null);
    } catch (err) {
      // Default: Toast notification
      toast({ variant: 'destructive', title: 'Remove Mint Failed', description: err instanceof Error ? err.message : 'Unknown error' });
      // Alternative options (commented):
      // Option 1: Console logging
      // console.error('Remove Mint Failed:', err instanceof Error ? err.message : 'Unknown error');
      // Option 2: No notification (silent failure)
    }
  }, [
    mintToRemove,
    mints,
    activeMintUrl,
    handleSetActiveMint,
    repositories,
    refreshMints,
    toast,
  ]);

  return {
    activeMintUrl,
    setActiveMintUrl: handleSetActiveMint,
    mintUrl,
    setMintUrl,
    handleAddMint,
    handleRemoveMintClick,
    handleConfirmRemoveMint,
    mintToRemove,
    setMintToRemove,
    showRemoveMintModal,
    setShowRemoveMintModal,
    getCleanMintLabel,
  };
}
```

**Key Operations:**
- **Add Mint**: Normalize URL, check for duplicates, add with trusted flag
- **Remove Mint**: Delete from repository, handle active mint switching (including the default mint - users are not locked to any mint)
- **Active Mint**: Track which mint is currently selected for operations
- **Error Handling**: Detect unknown mint errors and extract mint URLs from errors

**CRITICAL: Mint Management Requirements:**
- **Users must be able to add/remove mints**: The wallet must provide full mint management capabilities. Users are NOT locked to the default mint (`https://mint.minibits.cash/Bitcoin`) and can remove it if desired.
- **Default mint is optional**: The default mint is only added automatically if no mints exist. Once users add their own mints, they can remove the default mint just like any other mint.
- **Mint Selector UI**: Display a mint selector component that allows users to:
  - View all available mints with their balances
  - Select an active mint for operations
  - Add new mints inline
  - Remove mints (with confirmation modal) - **including the default mint**
  - See which mint is currently active
- **Active Mint Display**: Always show the currently selected mint in the wallet UI, as all Lightning operations (send/receive) require an active mint to be selected

## Part 4: Lightning Operations

### Receive Lightning Payments (Minting)

**Create mint quotes to receive Lightning payments:**

```typescript
// hooks/wallet/useLightningOperations.ts (excerpt)
import { useState, useCallback, useEffect, useRef } from 'react';
import type { Manager } from 'coco-cashu-core';
import { useToast } from '@/hooks/useToast';

interface UseLightningOperationsProps {
  coco: Manager | null;
  activeMintUrl: string | null;
  generateQRCode: (text: string) => Promise<string>;
}

export function useLightningOperations({
  coco,
  activeMintUrl,
  generateQRCode,
}: UseLightningOperationsProps) {
  // Default: Toast notifications
  const { toast } = useToast();
  // Alternative options (commented):
  // Option 1: Console logging
  // const logMessage = (message: string) => console.log(message);
  // Option 2: No notification handler
  
  // Receive state
  const [quoteAmount, setQuoteAmount] = useState('');
  
  // Invoice modal state
  const [showInvoiceModal, setShowInvoiceModal] = useState(false);
  const [currentInvoice, setCurrentInvoice] = useState<{
    quoteId: string;
    mintUrl: string;
    amount: number;
    invoice: string;
    qrCodeUrl: string;
    expiry: number;
  } | null>(null);

  // Ref to store the mint quote redeemed event unsubscribe function
  const mintQuoteUnsubscribeRef = useRef<(() => void) | null>(null);

  // Handle receive (Lightning quote/invoice creation)
  const handleLightningReceive = useCallback(async () => {
    if (!coco || !quoteAmount || !activeMintUrl) {
      return;
    }

    try {
      const amount = parseInt(quoteAmount);
      if (isNaN(amount) || amount <= 0) {
        // Default: Toast notification
        toast({ variant: 'destructive', title: 'Invalid Amount', description: 'Please enter a valid amount' });
        // Alternative options (commented):
        // Option 1: Console logging
        // console.error('Invalid Amount: Please enter a valid amount');
        // Option 2: No notification (silent failure)
        return;
      }
      
      const quote = await coco.quotes.createMintQuote(activeMintUrl, amount);

      // Generate QR code for the Lightning invoice
      const qrCodeUrl = await generateQRCode(quote.request);

      // Set the invoice data and show modal
      setCurrentInvoice({
        quoteId: quote.quote,
        mintUrl: activeMintUrl,
        amount,
        invoice: quote.request,
        qrCodeUrl,
        expiry: quote.expiry,
      });
      
      setShowInvoiceModal(true);
      setQuoteAmount('');

      // Clean up any previous listener
      if (mintQuoteUnsubscribeRef.current) {
        mintQuoteUnsubscribeRef.current();
        mintQuoteUnsubscribeRef.current = null;
      }

      // Listen for payment notification via mint-quote:redeemed event
      const quoteId = quote.quote;
      const invoiceAmount = amount;

      const unsubscribe = coco.on('mint-quote:redeemed', (payload: unknown) => {
        const redeemedQuote = payload as { quoteId?: string };

        // Check if this is the quote we're waiting for
        if (redeemedQuote.quoteId === quoteId) {
          // Default: Toast notification
          toast({ title: 'Payment Received', description: `Successfully received ${invoiceAmount} sats! Tokens have been added to your wallet.` });
          // Alternative options (commented):
          // Option 1: Console logging
          // console.log(`Payment Received: Successfully received ${invoiceAmount} sats! Tokens have been added to your wallet.`);
          // Option 2: No notification (silent success)
          setShowInvoiceModal(false);
          setCurrentInvoice(null);

          // Clean up the event listener
          mintQuoteUnsubscribeRef.current = null;
          unsubscribe();
        }
      });

      // Store unsubscribe function for cleanup
      mintQuoteUnsubscribeRef.current = unsubscribe;
    } catch (err) {
      // Default: Toast notification
      toast({ variant: 'destructive', title: 'Quote Creation Failed', description: err instanceof Error ? err.message : 'Unknown error' });
      // Alternative options (commented):
      // Option 1: Console logging
      // console.error('Quote Creation Failed:', err instanceof Error ? err.message : 'Unknown error');
      // Option 2: No notification (silent failure)
    }
  }, [coco, quoteAmount, activeMintUrl, generateQRCode, toast]);

  return {
    quoteAmount,
    setQuoteAmount,
    showInvoiceModal,
    setShowInvoiceModal,
    currentInvoice,
    setCurrentInvoice,
    handleLightningReceive,
  };
}
```

**Key Points:**
- **Mint Quote Creation**: `coco.quotes.createMintQuote(mintUrl, amount)` returns invoice
- **QR Code Generation**: Generate QR for invoice display
- **Event Listening**: Listen for `mint-quote:redeemed` to detect payment
- **Automatic Processing**: Built-in quote processor handles invoice payment automatically

### Send Lightning Payments (Melting)

**Pay Lightning invoices using melt quotes:**

```typescript
// hooks/wallet/useLightningOperations.ts (excerpt)
import { decodeBolt11Amount, isValidBolt11Invoice } from '@/lib/bolt11Decoder';

export function useLightningOperations({
  coco,
  activeMintUrl,
  getLightningInvoice, // For Lightning addresses
}: UseLightningOperationsProps) {
  // Optional: User feedback notifications
  // Option 1: Console logging
  // const logMessage = (message: string) => console.log(message);
  // Option 2: Toast notifications (if useToast hook is available)
  // const { toast } = useToast();
  // Option 3: No notification handler
  
  // Send state
  const [lightningInvoice, setLightningInvoice] = useState('');
  const [lightningAddressInput, setLightningAddressInput] = useState('');
  const [lightningAddressAmount, setLightningAddressAmount] = useState('');
  const [pendingLightningAddress, setPendingLightningAddress] = useState<string | null>(null);
  const [decodedInvoiceAmount, setDecodedInvoiceAmount] = useState<number | null>(null);
  const [pendingInvoice, setPendingInvoice] = useState<string | null>(null);
  
  // Processing state to prevent concurrent executions
  const [isProcessingPayment, setIsProcessingPayment] = useState(false);

  // Detect input type and handle Lightning address/invoice parsing
  const detectAndHandleLightningInput = useCallback((input: string): 'lightning_address' | 'invoice' | null => {
    const trimmed = input.trim();
    if (!trimmed) return null;

    // Check if it's a Lightning address (contains @)
    if (trimmed.includes('@')) {
      setPendingLightningAddress(trimmed);
      setLightningAddressInput('');
      return 'lightning_address';
    }

    // Check if it's a Lightning invoice
    if (isValidBolt11Invoice(trimmed)) {
      const amount = decodeBolt11Amount(trimmed);
      if (amount) {
        // Show amount confirmation screen
        setPendingInvoice(trimmed);
        setDecodedInvoiceAmount(amount);
        setLightningInvoice('');
        return 'invoice';
      } else {
        // Failed to decode amount - show error
        // Default: Toast notification
        toast({ variant: 'destructive', title: 'Invalid Invoice', description: 'Unable to decode the lightning invoice amount. Please check the invoice format.' });
        // Alternative options (commented):
        // Option 1: Console logging
        // console.error('Invalid Invoice: Unable to decode the lightning invoice amount. Please check the invoice format.');
        // Option 2: No notification (silent failure)
        setLightningInvoice('');
        return null;
      }
    } else {
      // Not a valid BOLT11 invoice - show error
      // Default: Toast notification
      toast({ variant: 'destructive', title: 'Invalid Invoice', description: 'This does not appear to be a valid lightning invoice. Please check the format.' });
      // Alternative options (commented):
      // Option 1: Console logging
      // console.error('Invalid Invoice: This does not appear to be a valid lightning invoice. Please check the format.');
      // Option 2: No notification (silent failure)
      setLightningInvoice('');
      return null;
    }
  }, [toast]);

  // Handle Lightning address payment
  const handleLightningAddressPayment = useCallback(async () => {
    // Prevent concurrent executions
    if (isProcessingPayment) {
      return;
    }

    if (!coco || !pendingLightningAddress || !lightningAddressAmount || !activeMintUrl) {
      return;
    }

    setIsProcessingPayment(true);

    try {
      const amount = parseInt(lightningAddressAmount);
      if (isNaN(amount) || amount <= 0) {
        // Default: Toast notification
        toast({ variant: 'destructive', title: 'Invalid Amount', description: 'Please enter a valid amount' });
        // Alternative options (commented):
        // Option 1: Console logging
        // console.error('Invalid Amount: Please enter a valid amount');
        // Option 2: No notification (silent failure)
        setIsProcessingPayment(false);
        return;
      }

      // Get invoice from Lightning address (REQUIRED: use lightning-address skill)
      const invoice = await getLightningInvoice({
        lightningAddress: pendingLightningAddress,
        amountSats: amount,
      });

      // Create melt quote and pay it
      const meltQuote = await coco.quotes.createMeltQuote(
        activeMintUrl,
        invoice
      );
      await coco.quotes.payMeltQuote(activeMintUrl, meltQuote.quote);

      // Default: Toast notification
      toast({ title: 'Payment Sent', description: `Successfully sent ${amount} sats to ${pendingLightningAddress}!` });
      // Alternative options (commented):
      // Option 1: Console logging
      // console.log(`Payment Sent: Successfully sent ${amount} sats to ${pendingLightningAddress}!`);
      // Option 2: No notification (silent success)

      // Reset state
      setPendingLightningAddress(null);
      setLightningAddressInput('');
      setLightningAddressAmount('');
    } catch (err) {
      // Default: Toast notification
      toast({ variant: 'destructive', title: 'Payment Failed', description: err instanceof Error ? err.message : 'Unknown error' });
      // Alternative options (commented):
      // Option 1: Console logging
      // console.error('Payment Failed:', err instanceof Error ? err.message : 'Unknown error');
      // Option 2: No notification (silent failure)
    } finally {
      setIsProcessingPayment(false);
    }
  }, [coco, pendingLightningAddress, lightningAddressAmount, activeMintUrl, getLightningInvoice, isProcessingPayment, toast]);

  // Handle confirmed invoice payment
  const handleConfirmInvoicePayment = useCallback(async () => {
    // Prevent concurrent executions
    if (isProcessingPayment) {
      return;
    }

    if (!coco || !pendingInvoice || !activeMintUrl) return;

    setIsProcessingPayment(true);

    try {
      const meltQuote = await coco.quotes.createMeltQuote(
        activeMintUrl,
        pendingInvoice
      );
      await coco.quotes.payMeltQuote(activeMintUrl, meltQuote.quote);

      // Default: Toast notification
      toast({ title: 'Invoice Paid', description: `Lightning invoice paid successfully for ${decodedInvoiceAmount} sats!` });
      // Alternative options (commented):
      // Option 1: Console logging
      // console.log(`Invoice Paid: Lightning invoice paid successfully for ${decodedInvoiceAmount} sats!`);
      // Option 2: No notification (silent success)

      // Reset state
      setPendingInvoice(null);
      setDecodedInvoiceAmount(null);
    } catch (err) {
      // Default: Toast notification
      toast({ variant: 'destructive', title: 'Invoice Payment Failed', description: err instanceof Error ? err.message : 'Unknown error' });
      // Alternative options (commented):
      // Option 1: Console logging
      // console.error('Invoice Payment Failed:', err instanceof Error ? err.message : 'Unknown error');
      // Option 2: No notification (silent failure)
    } finally {
      setIsProcessingPayment(false);
    }
  }, [coco, pendingInvoice, activeMintUrl, decodedInvoiceAmount, isProcessingPayment, toast]);

  return {
    // Send state
    lightningInvoice,
    setLightningInvoice,
    lightningAddressInput,
    setLightningAddressInput,
    lightningAddressAmount,
    setLightningAddressAmount,
    pendingLightningAddress,
    setPendingLightningAddress,
    decodedInvoiceAmount,
    setDecodedInvoiceAmount,
    pendingInvoice,
    setPendingInvoice,
    // Processing state
    isProcessingPayment,
    // Handlers
    detectAndHandleLightningInput,
    handleLightningAddressPayment,
    handleConfirmInvoicePayment,
  };
}
```

**Key Operations:**
- **Melt Quote Creation**: `coco.quotes.createMeltQuote(mintUrl, invoice)` creates quote
- **Quote Payment**: `coco.quotes.payMeltQuote(mintUrl, quoteId)` pays the invoice
- **Input Detection**: Automatically detects Lightning addresses vs BOLT11 invoices
- **Invoice Decoding**: **REQUIRED** - Uses `lightning-invoice-decoder` skill for amount extraction
- **Lightning Address Support**: **REQUIRED** - Uses `lightning-address` skill for invoice generation
- **QR Code Generation**: **REQUIRED** - Uses `qr-code-generator` skill for invoice QR codes

## Part 5: Transaction History

### History Manager Hook

**Track and display transaction history:**

```typescript
// hooks/wallet/useHistoryManager.ts
import { useState, useCallback, useEffect } from 'react';
import type { HistoryEntry, Manager } from 'coco-cashu-core';

interface UseHistoryManagerProps {
  coco: Manager | null;
  isInitialized: boolean;
}

export function useHistoryManager({
  coco,
  isInitialized,
}: UseHistoryManagerProps) {
  const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([]);
  const [isLoadingHistory, setIsLoadingHistory] = useState(false);
  const [isHistoryExpanded, setIsHistoryExpanded] = useState(false);

  // Load history from wallet
  const loadHistory = useCallback(async () => {
    if (!coco) return;

    try {
      setIsLoadingHistory(true);
      const entries = await coco.history.getPaginatedHistory(0, 50);
      
      // Filter out unpaid quotes: only show completed transactions
      // - Mint quotes: only show 'ISSUED' (completed)
      // - Melt quotes: only show 'PAID' (completed)
      // - Send/Receive: show all (no state field)
      const filteredEntries = entries.filter((entry) => {
        if (entry.type === 'mint') {
          return entry.state === 'ISSUED';
        }
        if (entry.type === 'melt') {
          return entry.state === 'PAID';
        }
        // Send and receive entries don't have a state field, show all
        return true;
      });
      
      setHistoryEntries(filteredEntries);
    } catch (err) {
      console.error('Failed to load history:', err);
    } finally {
      setIsLoadingHistory(false);
    }
  }, [coco]);

  // Load history on wallet initialization
  useEffect(() => {
    if (isInitialized && historyEntries.length === 0) {
      loadHistory();
    }
  }, [isInitialized, historyEntries.length, loadHistory]);

  // Auto-refresh on wallet events
  useEffect(() => {
    if (!isInitialized || !coco) return;

    // Listen for history updates (fires on send, receive transactions)
    const unsubscribeHistoryUpdated = coco.on('history:updated', () => {
      loadHistory();
    });

    // Listen for melt quote creation (coco-cashu bug: doesn't emit history:updated)
    const unsubscribeMeltCreated = coco.on('melt-quote:created', () => {
      loadHistory();
    });

    // Listen for melt quote paid (backup, in case creation event missed)
    const unsubscribeMeltPaid = coco.on('melt-quote:paid', () => {
      loadHistory();
    });

    // Listen for mint quote REDEEMED (only after invoice is paid, not created)
    const unsubscribeMintRedeemed = coco.on('mint-quote:redeemed', () => {
      loadHistory();
    });

    return () => {
      unsubscribeHistoryUpdated();
      unsubscribeMeltCreated();
      unsubscribeMeltPaid();
      unsubscribeMintRedeemed();
    };
  }, [isInitialized, coco, loadHistory]);

  return {
    historyEntries,
    setHistoryEntries,
    isLoadingHistory,
    isHistoryExpanded,
    setIsHistoryExpanded,
    loadHistory,
  };
}
```

**Key Points:**
- **Filtering**: Only show completed transactions (ISSUED mint quotes, PAID melt quotes)
- **Event Listeners**: Auto-refresh on quote creation, payment, and redemption
- **Pagination**: Use `getPaginatedHistory(offset, limit)` for la

…(truncated)
