import { SimplePool } from 'nostr-tools';
export interface RelayStats {
url: string;
latency?: number;
reportedLatency?: number;
status: 'pending' | 'measuring' | 'done' | 'error';
lastError?: string;
}
const DISCOVERY_RELAYS = [
'wss://relay.nostr.band',
'wss://nos.lol',
'wss://relay.damus.io',
'wss://purplepag.es',
'wss://relay.snort.social'
];
export async function discoverRelays(): Promise<RelayStats[]> {
const pool = new SimplePool();
try {
// NIP-66 uses kind 30166 for relay discovery events
const events = await pool.querySync(DISCOVERY_RELAYS, {
kinds: [30166],
limit: 100
}, { maxWait: 8000 });
const relayUrls = new Set<string>();
const stats: RelayStats[] = [];
for (const event of events) {
// NIP-66: 'd' tag contains the relay URL
const dTag = event.tags.find(t => t[0] === 'd')?.[1];
// Only accept valid websocket URLs
if (dTag && (dTag.startsWith('wss://') || dTag.startsWith('ws://'))) {
// Filter out local/private addresses
if (dTag.includes('127.0.0.1') || dTag.includes('localhost') || dTag.includes('192.168.') || dTag.includes('10.') || dTag.includes('172.')) {
continue;
}
if (!relayUrls.has(dTag)) {
relayUrls.add(dTag);
// NIP-66 uses various tags for RTT - check all common variants
const rttOpenTag = event.tags.find(t => t[0] === 'rtt-open');
const rttReadTag = event.tags.find(t => t[0] === 'rtt-read');
const rttWriteTag = event.tags.find(t => t[0] === 'rtt-write');
const rttTag = event.tags.find(t => t[0] === 'rtt');
const latencyTag = event.tags.find(t => t[0] === 'latency');
const reportedLatency =
rttOpenTag ? parseInt(rttOpenTag[1]) :
rttReadTag ? parseInt(rttReadTag[1]) :
rttWriteTag ? parseInt(rttWriteTag[1]) :
rttTag ? parseInt(rttTag[1]) :
latencyTag ? parseInt(latencyTag[1]) : undefined;
if (stats.length < 21) {
stats.push({
url: dTag,
reportedLatency,
status: 'pending'
});
}
}
}
}
return stats;
} catch (error) {
console.error('Error during relay discovery:', error);
throw error;
} finally {
pool.close(DISCOVERY_RELAYS);
}
}
export async function measureLatency(url: string): Promise<number> {
return new Promise((resolve, reject) => {
const start = performance.now();
let ws: WebSocket;
try {
ws = new WebSocket(url);
} catch (e) {
return reject(new Error('Invalid URL'));
}
const timeout = setTimeout(() => {
ws.close();
reject(new Error('Timeout'));
}, 5000);
ws.onopen = () => {
const reqId = Math.random().toString(36).substring(7);
ws.send(JSON.stringify(['REQ', reqId, { kinds: [0], limit: 1 }]));
};
ws.onmessage = (msg) => {
try {
const data = JSON.parse(msg.data);
if (data[0] === 'EOSE' || data[0] === 'EVENT') {
const end = performance.now();
ws.close();
clearTimeout(timeout);
resolve(Math.round(end - start));
}
} catch {
// Ignore malformed messages
}
};
ws.onerror = () => {
ws.close();
clearTimeout(timeout);
reject(new Error('Connection failed'));
};
});
}