import { useState, useEffect, useCallback, useRef } from 'react'; import { useLocalStorage } from '../useLocalStorage'; import * as bip39 from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; 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>; // 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; } export function useCashu(): UseCashuReturn { const [isInitialized, setIsInitialized] = useState(false); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // Mnemonic management with localStorage const [mnemonic, setMnemonicState] = useLocalStorage('cashu-mnemonic', null); // Manager instance ref const cocoRef = useRef(null); const repositoriesRef = useRef(null); // React state for UI reactivity const [balances, setBalances] = useState<{ [mintUrl: string]: number }>({}); const [mints, setMints] = useState>([]); 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('[Cashu] 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(); // Create initialization promise globalInitializationPromise = (async () => { const coco = await initializeCoco({ repo, seedGetter: async () => seed, logger: new ConsoleLogger("coco", { level: "info" }) }); return { coco, repositories: repo }; })(); // Initialize coco manager const { coco, repositories } = await globalInitializationPromise; // Store as singleton console.log('[Cashu] 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 try { console.log('[Cashu] Enabling coco services...'); await coco.enableMintQuoteWatcher({ watchExistingPendingOnStart: false }); await coco.enableMintQuoteProcessor(); await coco.enableProofStateWatcher(); console.log('[Cashu] All coco services enabled successfully'); } catch (err) { console.warn('[Cashu] Failed to enable some coco services:', err); } // Add default mint if no mints exist. // NOTE: the mint MUST send permissive CORS headers, otherwise the browser // blocks every request with a NetworkError ("Failed to fetch mint"). // mint.minibits.cash does NOT set Access-Control-Allow-Origin, so it can't // be used directly from a browser — we default to a CORS-enabled mint. try { const DEFAULT_MINT_URL = 'https://mint.coinos.io'; 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('[Cashu] Adding default mint:', DEFAULT_MINT_URL); await coco.mint.addMint(DEFAULT_MINT_URL, { trusted: true }); console.log('[Cashu] Default mint added successfully'); } } catch (err) { console.warn('[Cashu] 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; } 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.error('[Cashu] 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 { coco: cocoRef.current, repositories: repositoriesRef.current, balances, mints, totalBalance, isInitialized, isLoading, error, mnemonic, setMnemonic, generateMnemonic, clearError, getEncodedToken, refreshMints, }; }