// Shared signer manager for Nostr signers (remote and extension) import { SimplePool } from 'nostr-tools'; import { BunkerSigner } from 'nostr-tools/nip46'; const REMOTE_SIGNER_KEY = 'amber_remote_signer'; const MAX_RETRIES = 3; const RETRY_DELAY_MS = 2000; let remoteSigner = null; let remoteSignerPromise = null; let remoteSignerPool = null; export async function getSigner(retryCount = 0) { // If remote signer session is active, use it const session = getRemoteSignerSession(); console.log('[signer_manager] getSigner called, session exists:', !!session, 'retry:', retryCount); if (session) { if (remoteSigner) { console.log('[signer_manager] Returning cached remote signer'); return remoteSigner; } if (remoteSignerPromise) { console.log('[signer_manager] Returning existing connection promise'); return remoteSignerPromise; } console.log('[signer_manager] Recreating BunkerSigner from stored session...'); remoteSignerPromise = createRemoteSignerFromSession(session) .then(signer => { remoteSigner = signer; console.log('[signer_manager] Remote signer successfully recreated and cached'); return signer; }) .catch(async (error) => { console.error('[signer_manager] Remote signer creation failed:', error); remoteSignerPromise = null; // Retry connection instead of clearing session if (retryCount < MAX_RETRIES) { console.log(`[signer_manager] Retrying connection (${retryCount + 1}/${MAX_RETRIES}) in ${RETRY_DELAY_MS}ms...`); await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); return getSigner(retryCount + 1); } // After all retries failed, throw error but DON'T clear session // User can manually retry or reconnect console.error('[signer_manager] All connection attempts failed. Remote signer may be offline.'); throw new Error('Remote signer connection failed after ' + MAX_RETRIES + ' attempts. Please ensure Amber is running and try again.'); }); return remoteSignerPromise; } // Fallback to browser extension ONLY if no remote session console.log('[signer_manager] No remote session, checking for browser extension'); if (window.nostr && typeof window.nostr.signEvent === 'function') { console.log('[signer_manager] Using browser extension'); return window.nostr; } throw new Error('No signer available'); } export function setRemoteSignerSession(session) { localStorage.setItem(REMOTE_SIGNER_KEY, JSON.stringify(session)); } /** * Clear the remote signer session from localStorage and close connections * WARNING: Only call this on explicit logout - NOT on page navigation/disconnect * The whole point of session persistence is to survive page reloads */ export function clearRemoteSignerSession() { console.log('[signer_manager] Clearing remote signer session'); localStorage.removeItem(REMOTE_SIGNER_KEY); remoteSigner = null; remoteSignerPromise = null; if (remoteSignerPool) { try { remoteSignerPool.close?.([]); } catch (_) {} remoteSignerPool = null; } } export function getRemoteSignerSession() { const raw = localStorage.getItem(REMOTE_SIGNER_KEY); if (!raw) return null; try { return JSON.parse(raw); } catch { return null; } } // Create BunkerSigner from stored session // Uses fromBunker() for reconnection with stored BunkerPointer // Falls back to fromURI() for legacy sessions async function createRemoteSignerFromSession(session) { console.log('[signer_manager] ===== Recreating BunkerSigner from session ====='); // Reuse existing pool if available, otherwise create new one if (!remoteSignerPool) { console.log('[signer_manager] Creating new SimplePool'); remoteSignerPool = new SimplePool(); } else { console.log('[signer_manager] Reusing existing SimplePool'); } try { let signer; // NEW PATTERN: Use fromBunker() with stored BunkerPointer (preferred) if (session.bunkerPointer) { console.log('[signer_manager] Using fromBunker() with stored BunkerPointer'); console.log('[signer_manager] BunkerPointer pubkey:', session.bunkerPointer.pubkey); console.log('[signer_manager] BunkerPointer relays:', session.bunkerPointer.relays); // fromBunker() is for reconnecting to an already-authorized bunker // It doesn't wait for a new connect message like fromURI() does signer = BunkerSigner.fromBunker( session.privkey, session.bunkerPointer, { pool: remoteSignerPool } ); console.log('[signer_manager] ✅ BunkerSigner created from pointer!'); } // LEGACY PATTERN: Fallback to fromURI() for old sessions (backward compatibility) else if (session.uri) { console.log('[signer_manager] ⚠️ Using legacy fromURI() pattern (session has no bunkerPointer)'); console.log('[signer_manager] Session URI:', session.uri); console.log('[signer_manager] Session relays:', session.relays); // fromURI returns a Promise - await it to get the signer signer = await BunkerSigner.fromURI(session.privkey, session.uri, { pool: remoteSignerPool }); console.log('[signer_manager] ✅ BunkerSigner created from URI!'); // With fromURI, we need to call connect() console.log('[signer_manager] Calling connect() to establish relay connection...'); await signer.connect(); console.log('[signer_manager] ✅ Connected to remote signer!'); } else { throw new Error('Session missing both bunkerPointer and uri'); } // Test the signer to make sure it works try { console.log('[signer_manager] Testing signer with getPublicKey...'); const pubkey = await signer.getPublicKey(); console.log('[signer_manager] ✅ Signer verified! Pubkey:', pubkey); return signer; } catch (testError) { console.error('[signer_manager] ❌ Signer test failed:', testError); throw new Error('Signer created but failed verification: ' + testError.message); } } catch (error) { console.error('[signer_manager] ❌ Failed to create signer:', error); // Clean up on error if (remoteSignerPool) { try { console.log('[signer_manager] Closing pool after error'); remoteSignerPool.close?.([]); } catch (_) {} remoteSignerPool = null; } remoteSigner = null; remoteSignerPromise = null; throw error; } }