/** * Relay Discovery Tool * * Discovers all known relays on the Nostr network and displays them * sorted by frequency of occurrence in NIP-65 relay lists. */ import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' import { ScrollArea } from '@/components/ui/scroll-area' import { Slider } from '@/components/ui/slider' import relayDiscoveryService, { DiscoveryProgress, DiscoveryResult, RelayFrequency } from '@/services/relay-discovery.service' import storage from '@/services/local-storage.service' import { Copy, Download, Loader2, Play, RefreshCw, Square } from 'lucide-react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' export default function RelayDiscovery() { const { t } = useTranslation() const [isRunning, setIsRunning] = useState(false) const [progress, setProgress] = useState(null) const [result, setResult] = useState(null) const [copied, setCopied] = useState(false) const [fallbackCount, setFallbackCount] = useState(storage.getFallbackRelayCount()) // Load cached result on mount useEffect(() => { const cached = relayDiscoveryService.getCachedResult() if (cached) { setResult(cached) } }, []) const handleStart = useCallback(async () => { setIsRunning(true) setProgress({ phase: 'phase1', relaysQueried: 0, totalRelays: 0, eventsFound: 0, uniqueRelaysFound: 0 }) try { const discoveryResult = await relayDiscoveryService.discover((prog) => { setProgress(prog) }) setResult(discoveryResult) toast.success(t('Discovery complete'), { description: `${discoveryResult.relays.length} relays found` }) } catch (err) { console.error('[RelayDiscovery] Error:', err) toast.error(t('Discovery failed')) } finally { setIsRunning(false) setProgress(null) } }, [t]) const handleStop = useCallback(() => { relayDiscoveryService.abort() setIsRunning(false) setProgress(null) }, []) const handleRefresh = useCallback(() => { relayDiscoveryService.clearCache() setResult(null) handleStart() }, [handleStart]) const handleCopy = useCallback(() => { if (!result) return const text = relayDiscoveryService.exportAsPlaintext(result.relays) navigator.clipboard.writeText(text) setCopied(true) setTimeout(() => setCopied(false), 2000) toast.success(t('Copied to clipboard')) }, [result, t]) const handleDownload = useCallback(() => { if (!result) return relayDiscoveryService.downloadAsFile(result.relays) toast.success(t('Downloaded')) }, [result, t]) const getPhaseLabel = (phase: string): string => { switch (phase) { case 'phase1': return t('Phase 1: Querying bootstrap relays') case 'phase2': return t('Phase 2: Querying discovered relays') case 'complete': return t('Complete') default: return '' } } const getProgressPercent = (): number => { if (!progress) return 0 if (progress.totalRelays === 0) return 0 const basePercent = progress.phase === 'phase1' ? 0 : 50 const phasePercent = (progress.relaysQueried / progress.totalRelays) * 50 return Math.round(basePercent + phasePercent) } return (
{t('Discover all known relays on the Nostr network by querying NIP-65 relay lists.')}
{/* Fallback Relay Configuration */}
{t('Number of top discovered relays to search when notes aren\'t found.')}
{ setFallbackCount(value) storage.setFallbackRelayCount(value) }} min={3} max={50} step={1} disabled={!result} />
3 50
{/* Controls */}
{!isRunning ? ( <> {!result ? ( ) : ( )} ) : ( )} {result && !isRunning && ( <> )}
{/* Progress */} {isRunning && progress && (
{getPhaseLabel(progress.phase)}
{t('Relays queried')}: {progress.relaysQueried}/{progress.totalRelays} |{' '} {t('Events found')}: {progress.eventsFound} |{' '} {t('Unique relays')}: {progress.uniqueRelaysFound}
)} {/* Results */} {result && !isRunning && (
{t('Found {{count}} relays from {{events}} relay list events', { count: result.relays.length, events: result.totalEvents })}
{t('Last updated')}: {new Date(result.timestamp).toLocaleString()}
{result.relays.map((relay, index) => ( ))}
# {t('Relay URL')} {t('Count')} %
)}
) } function RelayRow({ relay, index }: { relay: RelayFrequency; index: number }) { return ( {index} {relay.url} {relay.count} {relay.percentage}% ) }