import React, { useEffect, useMemo, useState } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faClock, faSpinner } from '@fortawesome/free-solid-svg-icons' import { Hooks } from 'applesauce-react' import { Accounts } from 'applesauce-accounts' import { NostrConnectSigner } from 'applesauce-signers' import { RelayPool } from 'applesauce-relay' import { getDefaultBunkerPermissions } from '../services/nostrConnect' import { DebugBus, type DebugLogEntry } from '../utils/debugBus' import VersionFooter from './VersionFooter' import { queryEvents } from '../services/dataFetch' import { KINDS } from '../config/kinds' import { collectBookmarksFromEvents } from '../services/bookmarkProcessing' import type { NostrEvent } from '../services/bookmarkHelpers' const defaultPayload = 'The quick brown fox jumps over the lazy dog.' interface DebugProps { relayPool?: RelayPool | null } const Debug: React.FC = ({ relayPool }) => { const activeAccount = Hooks.useActiveAccount() const accountManager = Hooks.useAccountManager() const [payload, setPayload] = useState(defaultPayload) const [cipher44, setCipher44] = useState('') const [cipher04, setCipher04] = useState('') const [plain44, setPlain44] = useState('') const [plain04, setPlain04] = useState('') const [tEncrypt44, setTEncrypt44] = useState(null) const [tEncrypt04, setTEncrypt04] = useState(null) const [tDecrypt44, setTDecrypt44] = useState(null) const [tDecrypt04, setTDecrypt04] = useState(null) const [logs, setLogs] = useState(DebugBus.snapshot()) const [debugEnabled, setDebugEnabled] = useState(() => localStorage.getItem('debug') === '*') // Bunker login state const [bunkerUri, setBunkerUri] = useState('') const [isBunkerLoading, setIsBunkerLoading] = useState(false) const [bunkerError, setBunkerError] = useState(null) // Bookmark loading state const [bookmarkEvents, setBookmarkEvents] = useState([]) const [isLoadingBookmarks, setIsLoadingBookmarks] = useState(false) const [isDecryptingBookmarks, setIsDecryptingBookmarks] = useState(false) const [bookmarkStats, setBookmarkStats] = useState<{ public: number; private: number } | null>(null) const [tLoadBookmarks, setTLoadBookmarks] = useState(null) const [tDecryptBookmarks, setTDecryptBookmarks] = useState(null) // Live timing state const [liveTiming, setLiveTiming] = useState<{ nip44?: { type: 'encrypt' | 'decrypt'; startTime: number } nip04?: { type: 'encrypt' | 'decrypt'; startTime: number } loadBookmarks?: { startTime: number } decryptBookmarks?: { startTime: number } }>({}) useEffect(() => { return DebugBus.subscribe((e) => setLogs(prev => [...prev, e].slice(-300))) }, []) // Live timer effect - triggers re-renders for live timing updates useEffect(() => { const interval = setInterval(() => { // Force re-render to update live timing display setLiveTiming(prev => prev) }, 16) // ~60fps for smooth updates return () => clearInterval(interval) }, []) const signer = useMemo(() => (activeAccount as unknown as { signer?: unknown })?.signer, [activeAccount]) const pubkey = (activeAccount as unknown as { pubkey?: string })?.pubkey const hasNip04 = typeof (signer as { nip04?: { encrypt?: unknown; decrypt?: unknown } } | undefined)?.nip04?.encrypt === 'function' const hasNip44 = typeof (signer as { nip44?: { encrypt?: unknown; decrypt?: unknown } } | undefined)?.nip44?.encrypt === 'function' const getKindName = (kind: number): string => { switch (kind) { case KINDS.ListSimple: return 'Simple List (10003)' case KINDS.ListReplaceable: return 'Replaceable List (30003)' case KINDS.List: return 'List (30001)' case KINDS.WebBookmark: return 'Web Bookmark (39701)' default: return `Kind ${kind}` } } const getEventSize = (evt: NostrEvent): number => { const content = evt.content || '' const tags = JSON.stringify(evt.tags || []) return content.length + tags.length } const getBookmarkCount = (evt: NostrEvent): { public: number; private: number } => { const publicTags = (evt.tags || []).filter((t: string[]) => t[0] === 'e' || t[0] === 'a') const hasPrivate = evt.content && evt.content.length > 0 return { public: publicTags.length, private: hasPrivate ? 1 : 0 // Can't know exact count until decrypted } } const formatBytes = (bytes: number): string => { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(2)} MB` } const getEventKey = (evt: NostrEvent): string => { if (evt.kind === 30003 || evt.kind === 30001) { // Replaceable: kind:pubkey:dtag const dTag = evt.tags?.find((t: string[]) => t[0] === 'd')?.[1] || '' return `${evt.kind}:${evt.pubkey}:${dTag}` } else if (evt.kind === 10003) { // Simple list: kind:pubkey return `${evt.kind}:${evt.pubkey}` } // Web bookmarks: use event id (no deduplication) return evt.id } const doEncrypt = async (mode: 'nip44' | 'nip04') => { if (!signer || !pubkey) return try { const api = (signer as { [key: string]: { encrypt: (pubkey: string, message: string) => Promise } })[mode] DebugBus.info('debug', `encrypt start ${mode}`, { pubkey, len: payload.length }) // Start live timing const start = performance.now() setLiveTiming(prev => ({ ...prev, [mode]: { type: 'encrypt', startTime: start } })) const cipher = await api.encrypt(pubkey, payload) const ms = Math.round(performance.now() - start) // Stop live timing setLiveTiming(prev => ({ ...prev, [mode]: undefined })) DebugBus.info('debug', `encrypt done ${mode}`, { len: typeof cipher === 'string' ? cipher.length : -1, ms }) if (mode === 'nip44') setCipher44(cipher) else setCipher04(cipher) if (mode === 'nip44') setTEncrypt44(ms) else setTEncrypt04(ms) } catch (e) { // Stop live timing on error setLiveTiming(prev => ({ ...prev, [mode]: undefined })) DebugBus.error('debug', `encrypt error ${mode}`, e instanceof Error ? e.message : String(e)) } } const doDecrypt = async (mode: 'nip44' | 'nip04') => { if (!signer || !pubkey) return try { const api = (signer as { [key: string]: { decrypt: (pubkey: string, ciphertext: string) => Promise } })[mode] const cipher = mode === 'nip44' ? cipher44 : cipher04 if (!cipher) { DebugBus.warn('debug', `no cipher to decrypt for ${mode}`) return } DebugBus.info('debug', `decrypt start ${mode}`, { len: cipher.length }) // Start live timing const start = performance.now() setLiveTiming(prev => ({ ...prev, [mode]: { type: 'decrypt', startTime: start } })) const plain = await api.decrypt(pubkey, cipher) const ms = Math.round(performance.now() - start) // Stop live timing setLiveTiming(prev => ({ ...prev, [mode]: undefined })) DebugBus.info('debug', `decrypt done ${mode}`, { len: typeof plain === 'string' ? plain.length : -1, ms }) if (mode === 'nip44') setPlain44(String(plain)) else setPlain04(String(plain)) if (mode === 'nip44') setTDecrypt44(ms) else setTDecrypt04(ms) } catch (e) { // Stop live timing on error setLiveTiming(prev => ({ ...prev, [mode]: undefined })) DebugBus.error('debug', `decrypt error ${mode}`, e instanceof Error ? e.message : String(e)) } } const toggleDebug = () => { const next = !debugEnabled setDebugEnabled(next) if (next) localStorage.setItem('debug', '*') else localStorage.removeItem('debug') } const handleLoadBookmarks = async () => { if (!relayPool || !activeAccount) { DebugBus.warn('debug', 'Cannot load bookmarks: missing relayPool or activeAccount') return } try { setIsLoadingBookmarks(true) setBookmarkStats(null) setBookmarkEvents([]) // Clear existing events DebugBus.info('debug', 'Loading bookmark events...') // Start timing const start = performance.now() setLiveTiming(prev => ({ ...prev, loadBookmarks: { startTime: start } })) // Use onEvent callback to stream events as they arrive // Trust EOSE - completes when relays finish, no artificial timeouts const rawEvents = await queryEvents( relayPool, { kinds: [KINDS.ListSimple, KINDS.ListReplaceable, KINDS.List, KINDS.WebBookmark], authors: [activeAccount.pubkey] }, { onEvent: (evt) => { // Add event immediately with live deduplication setBookmarkEvents(prev => { // Create unique key for deduplication const key = getEventKey(evt) // Find existing event with same key const existingIdx = prev.findIndex(e => getEventKey(e) === key) if (existingIdx >= 0) { // Replace if newer const existing = prev[existingIdx] if ((evt.created_at || 0) > (existing.created_at || 0)) { const newEvents = [...prev] newEvents[existingIdx] = evt return newEvents } return prev // Keep existing (it's newer) } // Add new event return [...prev, evt] }) } } ) const ms = Math.round(performance.now() - start) setLiveTiming(prev => ({ ...prev, loadBookmarks: undefined })) setTLoadBookmarks(ms) DebugBus.info('debug', `Loaded ${rawEvents.length} bookmark events`, { kinds: rawEvents.map(e => e.kind).join(', '), ms }) } catch (error) { setLiveTiming(prev => ({ ...prev, loadBookmarks: undefined })) DebugBus.error('debug', 'Failed to load bookmarks', error instanceof Error ? error.message : String(error)) } finally { setIsLoadingBookmarks(false) } } const handleDecryptBookmarks = async () => { if (!activeAccount || bookmarkEvents.length === 0) { DebugBus.warn('debug', 'Cannot decrypt: missing activeAccount or no bookmark events loaded') return } try { setIsDecryptingBookmarks(true) DebugBus.info('debug', 'Decrypting bookmark events...') // Start timing const start = performance.now() setLiveTiming(prev => ({ ...prev, decryptBookmarks: { startTime: start } })) const fullAccount = accountManager.getActive() const signerCandidate = fullAccount || activeAccount const { publicItemsAll, privateItemsAll } = await collectBookmarksFromEvents( bookmarkEvents, activeAccount, signerCandidate ) const ms = Math.round(performance.now() - start) setLiveTiming(prev => ({ ...prev, decryptBookmarks: undefined })) setTDecryptBookmarks(ms) setBookmarkStats({ public: publicItemsAll.length, private: privateItemsAll.length }) DebugBus.info('debug', `Decryption complete`, { public: publicItemsAll.length, private: privateItemsAll.length, total: publicItemsAll.length + privateItemsAll.length, ms }) } catch (error) { setLiveTiming(prev => ({ ...prev, decryptBookmarks: undefined })) DebugBus.error('debug', 'Failed to decrypt bookmarks', error instanceof Error ? error.message : String(error)) } finally { setIsDecryptingBookmarks(false) } } const handleClearBookmarks = () => { setBookmarkEvents([]) setBookmarkStats(null) setTLoadBookmarks(null) setTDecryptBookmarks(null) DebugBus.info('debug', 'Cleared bookmark data') } const handleBunkerLogin = async () => { if (!bunkerUri.trim()) { setBunkerError('Please enter a bunker URI') return } if (!bunkerUri.startsWith('bunker://')) { setBunkerError('Invalid bunker URI. Must start with bunker://') return } try { setIsBunkerLoading(true) setBunkerError(null) // Create signer from bunker URI with default permissions const permissions = getDefaultBunkerPermissions() const signer = await NostrConnectSigner.fromBunkerURI(bunkerUri, { permissions }) // Get pubkey from signer const pubkey = await signer.getPublicKey() // Create account from signer const account = new Accounts.NostrConnectAccount(pubkey, signer) // Add to account manager and set active accountManager.addAccount(account) accountManager.setActive(account) // Clear input on success setBunkerUri('') } catch (err) { console.error('[bunker] Login failed:', err) const errorMessage = err instanceof Error ? err.message : 'Failed to connect to bunker' // Check for permission-related errors if (errorMessage.toLowerCase().includes('permission') || errorMessage.toLowerCase().includes('unauthorized')) { setBunkerError('Your bunker connection is missing signing permissions. Reconnect and approve signing.') } else { setBunkerError(errorMessage) } } finally { setIsBunkerLoading(false) } } const CodeBox = ({ value }: { value: string }) => (
{value || '—'}
) const getLiveTiming = (mode: 'nip44' | 'nip04', type: 'encrypt' | 'decrypt') => { const timing = liveTiming[mode] if (timing && timing.type === type) { const elapsed = Math.round(performance.now() - timing.startTime) return elapsed } return null } const getBookmarkLiveTiming = (operation: 'loadBookmarks' | 'decryptBookmarks') => { const timing = liveTiming[operation] if (timing) { const elapsed = Math.round(performance.now() - timing.startTime) return elapsed } return null } const Stat = ({ label, value, mode, type, bookmarkOp }: { label: string; value?: string | number | null; mode?: 'nip44' | 'nip04'; type?: 'encrypt' | 'decrypt'; bookmarkOp?: 'loadBookmarks' | 'decryptBookmarks'; }) => { const liveValue = bookmarkOp ? getBookmarkLiveTiming(bookmarkOp) : (mode && type ? getLiveTiming(mode, type) : null) const isLive = !!liveValue let displayValue: string if (isLive) { displayValue = '' } else if (value !== null && value !== undefined) { displayValue = `${value}ms` } else { displayValue = '—' } return ( {label}: {isLive ? ( ) : ( displayValue )} ) } return (

Debug

Active pubkey: {pubkey || 'none'}
{/* Bunker Login Section */}

Bunker Connection

{!activeAccount ? (
Connect to your bunker (Nostr Connect signer) to enable encryption/decryption testing
setBunkerUri(e.target.value)} disabled={isBunkerLoading} />
{bunkerError && (
{bunkerError}
)}
) : (
Connected to bunker
{pubkey}
)}
{/* Encryption Tools Section */}

Encryption Tools