import NDK, { getRelayListForUser, Hexpubkey, NDKEvent, NDKFilter, NDKRelayList, NDKRelaySet, NDKSubscriptionCacheUsage, NDKSubscriptionOptions, NDKUser, NDKUserProfile } from '@nostr-dev-kit/ndk' import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie' import { Dexie } from 'dexie' import { createContext, ReactNode, useEffect, useMemo } from 'react' import { toast } from 'react-toastify' import { UserRelaysType } from '../types' import { DEFAULT_LOOK_UP_RELAY_LIST, hexToNpub, orderEventsChronologically, SIGIT_RELAY, timeout } from '../utils' export interface NDKContextType { ndk: NDK fetchEvents: ( filter: NDKFilter, opts?: NDKSubscriptionOptions ) => Promise fetchEvent: ( filter: NDKFilter, opts?: NDKSubscriptionOptions ) => Promise fetchEventsFromUserRelays: ( filter: NDKFilter | NDKFilter[], hexKey: string, userRelaysType: UserRelaysType, opts?: NDKSubscriptionOptions ) => Promise fetchEventFromUserRelays: ( filter: NDKFilter | NDKFilter[], hexKey: string, userRelaysType: UserRelaysType, opts?: NDKSubscriptionOptions ) => Promise findMetadata: ( pubkey: string, opts?: NDKSubscriptionOptions ) => Promise getNDKRelayList: (pubkey: Hexpubkey) => Promise publish: (event: NDKEvent, explicitRelayUrls?: string[]) => Promise } // Create the context with an initial value of `null` export const NDKContext = createContext(null) // Create a provider component to wrap around parts of your app export const NDKContextProvider = ({ children }: { children: ReactNode }) => { useEffect(() => { window.onunhandledrejection = async (event: PromiseRejectionEvent) => { event.preventDefault() if (event.reason?.name === Dexie.errnames.DatabaseClosed) { console.log( 'Could not open Dexie DB, probably version change. Deleting old DB and reloading...' ) await Dexie.delete('degmod-db') // Must reload to open a brand new DB window.location.reload() } } }, []) const ndk = useMemo(() => { if (import.meta.env.MODE === 'development') { localStorage.setItem('debug', '*') } const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'sigit-db' }) dexieAdapter.locking = true const ndk = new NDK({ enableOutboxModel: true, autoConnectUserRelays: true, autoFetchUserMutelist: true, explicitRelayUrls: [...DEFAULT_LOOK_UP_RELAY_LIST], cacheAdapter: dexieAdapter }) ndk.connect() return ndk }, []) /** * Asynchronously retrieves multiple event based on a provided filter. * * @param filter - The filter criteria to find the event. * @returns Returns a promise that resolves to the found event or null if not found. */ const fetchEvents = async ( filter: NDKFilter, opts?: NDKSubscriptionOptions ): Promise => { return ndk .fetchEvents(filter, { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL, ...opts }) .then((ndkEventSet) => { const ndkEvents = Array.from(ndkEventSet) return orderEventsChronologically(ndkEvents) }) .catch((err) => { // Log the error and show a notification if fetching fails console.error('An error occurred in fetching events', err) toast.error('An error occurred in fetching events') // Show error notification return [] // Return an empty array in case of an error }) } /** * Asynchronously retrieves an event based on a provided filter. * * @param filter - The filter criteria to find the event. * @returns Returns a promise that resolves to the found event or null if not found. */ const fetchEvent = async ( filter: NDKFilter, opts?: NDKSubscriptionOptions ) => { const events = await fetchEvents(filter, opts) if (events.length === 0) return null return events[0] } /** * Asynchronously retrieves multiple events from the user's relays based on a specified filter. * The function first retrieves the user's relays, and then fetches the events using the provided filter. * * @param filter - The event filter to use when fetching the event (e.g., kinds, authors). * @param hexKey - The hexadecimal representation of the user's public key. * @param userRelaysType - The type of relays to search (e.g., write, read). * @returns A promise that resolves with an array of events. */ const fetchEventsFromUserRelays = async ( filter: NDKFilter | NDKFilter[], hexKey: string, userRelaysType: UserRelaysType, opts?: NDKSubscriptionOptions ): Promise => { // Find the user's relays (10s timeout). const relayUrls = await Promise.race([ getRelayListForUser(hexKey, ndk), timeout(3000) ]) .then((ndkRelayList) => { if (ndkRelayList) return ndkRelayList[userRelaysType] return [] // Return an empty array if ndkRelayList is undefined }) .catch((err) => { console.error( `An error occurred in fetching user's (${hexKey}) ${userRelaysType}`, err ) return [] as string[] }) if (!relayUrls.includes(SIGIT_RELAY)) { relayUrls.push(SIGIT_RELAY) } return ndk .fetchEvents( filter, { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL, ...opts }, relayUrls.length ? NDKRelaySet.fromRelayUrls(relayUrls, ndk, true) : undefined ) .then((ndkEventSet) => { const ndkEvents = Array.from(ndkEventSet) return orderEventsChronologically(ndkEvents) }) .catch((err) => { // Log the error and show a notification if fetching fails console.error('An error occurred in fetching events', err) toast.error('An error occurred in fetching events') // Show error notification return [] // Return an empty array in case of an error }) } /** * Fetches an event from the user's relays based on a specified filter. * The function first retrieves the user's relays, and then fetches the event using the provided filter. * * @param filter - The event filter to use when fetching the event (e.g., kinds, authors). * @param hexKey - The hexadecimal representation of the user's public key. * @param userRelaysType - The type of relays to search (e.g., write, read). * @returns A promise that resolves to the fetched event or null if the operation fails. */ const fetchEventFromUserRelays = async ( filter: NDKFilter | NDKFilter[], hexKey: string, userRelaysType: UserRelaysType, opts?: NDKSubscriptionOptions ) => { const events = await fetchEventsFromUserRelays( filter, hexKey, userRelaysType, opts ) if (events.length === 0) return null return events[0] } /** * Finds metadata for a given pubkey. * * @param hexKey - The pubkey to search for metadata. * @returns A promise that resolves to the metadata event. */ const findMetadata = async ( pubkey: string, opts?: NDKSubscriptionOptions ): Promise => { const npub = hexToNpub(pubkey) const user = new NDKUser({ npub }) user.ndk = ndk return await user.fetchProfile({ cacheUsage: NDKSubscriptionCacheUsage.PARALLEL, ...(opts || {}) }) } const getNDKRelayList = async (pubkey: Hexpubkey) => { const ndkRelayList = await Promise.race([ getRelayListForUser(pubkey, ndk), timeout(10000) ]).catch(() => { const relayList = new NDKRelayList(ndk) relayList.bothRelayUrls = [SIGIT_RELAY] return relayList }) return ndkRelayList } const publish = async ( event: NDKEvent, explicitRelayUrls?: string[] ): Promise => { if (!event.sig) throw new Error('Before publishing first sign the event!') let ndkRelaySet: NDKRelaySet | undefined if (explicitRelayUrls && explicitRelayUrls.length > 0) { if (!explicitRelayUrls.includes(SIGIT_RELAY)) { explicitRelayUrls = [...explicitRelayUrls, SIGIT_RELAY] } ndkRelaySet = NDKRelaySet.fromRelayUrls(explicitRelayUrls, ndk) } return await Promise.race([event.publish(ndkRelaySet), timeout(3000)]) .then((res) => { const relaysPublishedOn = Array.from(res) return relaysPublishedOn.map((relay) => relay.url) }) .catch((err) => { console.error(`An error occurred in publishing event`, err) return [] }) } return ( {children} ) }