import axios from 'axios' import { Event, Filter } from 'nostr-tools' import { RelayList } from 'nostr-tools/kinds' import { relayController } from '../controllers/RelayController.ts' import { localCache } from '../services' import { setMostPopularRelaysAction } from '../store/actions' import store from '../store/store' import { RelayMap, RelayReadStats, RelaySet, RelayStats } from '../types' import { ONE_WEEK_IN_MS, SIGIT_RELAY } from './const.ts' const READ_MARKER = 'read' const WRITE_MARKER = 'write' /** * Attempts to find a relay list from the provided lookUpRelays. * If the relay list is found, it will be added to the user relay list metadata. * @param lookUpRelays * @param hexKey * @return found relay list or null */ const findRelayListAndUpdateCache = async ( lookUpRelays: string[], hexKey: string ): Promise => { try { const eventFilter: Filter = { kinds: [RelayList], authors: [hexKey] } const event = await relayController.fetchEvent(eventFilter, lookUpRelays) if (event) { await localCache.addUserRelayListMetadata(event) } return event } catch (error) { console.error(error) return null } } /** * Attempts to find a relay list in cache. If it is present, it will check that the cached event is not * older than one week. * @param hexKey * @return RelayList event if it's not older than a week; otherwise null */ const findRelayListInCache = async (hexKey: string): Promise => { try { // Attempt to retrieve the metadata event from the local cache const cachedRelayListMetadataEvent = await localCache.getUserRelayListMetadata(hexKey) // Check if the cached event is not older than one week if ( cachedRelayListMetadataEvent && isOlderThanOneWeek(cachedRelayListMetadataEvent.cachedAt) ) { return cachedRelayListMetadataEvent.event } return null } catch (error) { console.error(error) return null } } /** * Transforms a list of relay tags from a Nostr Event to a RelaySet. * @param tags */ const getUserRelaySet = (tags: string[][]): RelaySet => { return tags .filter(isRelayTag) .reduce(toRelaySet, getDefaultRelaySet()) } const getDefaultRelaySet = (): RelaySet => ({ read: [SIGIT_RELAY], write: [SIGIT_RELAY] }) const getDefaultRelayMap = (): RelayMap => ({ [SIGIT_RELAY]: { write: true, read: true } }) const isOlderThanOneWeek = (cachedAt: number) => { return Date.now() - cachedAt < ONE_WEEK_IN_MS } const isRelayTag = (tag: string[]): boolean => tag[0] === 'r' const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => { if (tag.length >= 3) { const marker = tag[2] if (marker === READ_MARKER) { obj.read.push(tag[1]) } else if (marker === WRITE_MARKER) { obj.write.push(tag[1]) } } if (tag.length === 2) { obj.read.push(tag[1]) obj.write.push(tag[1]) } return obj } /** * Provides most popular relays. * @param numberOfTopRelays - number representing how many most popular relays to provide * @returns - promise that resolves into an array of most popular relays */ const getMostPopularRelays = async ( numberOfTopRelays: number = 30 ): Promise => { const mostPopularRelaysState = store.getState().relays?.mostPopular // return most popular relays from app state if present if (mostPopularRelaysState) return mostPopularRelaysState // relays in env const { VITE_MOST_POPULAR_RELAYS } = import.meta.env const hardcodedPopularRelays = (VITE_MOST_POPULAR_RELAYS || '').split(' ') const url = `https://stats.nostr.band/stats_api?method=stats` const response = await axios.get(url).catch(() => undefined) if (!response) { return hardcodedPopularRelays //return hardcoded relay list } const data = response.data if (!data) { return hardcodedPopularRelays //return hardcoded relay list } const apiTopRelays = data.relay_stats.user_picks.read_relays .slice(0, numberOfTopRelays) .map((relay: RelayReadStats) => relay.d) if (!apiTopRelays.length) { return Promise.reject(`Couldn't fetch popular relays.`) } if (store.getState().auth?.loggedIn) { store.dispatch(setMostPopularRelaysAction(apiTopRelays)) } return apiTopRelays } export { findRelayListAndUpdateCache, findRelayListInCache, getDefaultRelayMap, getDefaultRelaySet, getMostPopularRelays, getUserRelaySet, isOlderThanOneWeek }