diff --git a/src/contexts/NDKContext.tsx b/src/contexts/NDKContext.tsx index 5db3ba5..55100e8 100644 --- a/src/contexts/NDKContext.tsx +++ b/src/contexts/NDKContext.tsx @@ -27,17 +27,25 @@ import { export interface NDKContextType { ndk: NDK - fetchEvents: (filter: NDKFilter) => Promise - fetchEvent: (filter: NDKFilter) => Promise + fetchEvents: ( + filter: NDKFilter, + opts?: NDKSubscriptionOptions + ) => Promise + fetchEvent: ( + filter: NDKFilter, + opts?: NDKSubscriptionOptions + ) => Promise fetchEventsFromUserRelays: ( filter: NDKFilter | NDKFilter[], hexKey: string, - userRelaysType: UserRelaysType + userRelaysType: UserRelaysType, + opts?: NDKSubscriptionOptions ) => Promise fetchEventFromUserRelays: ( filter: NDKFilter | NDKFilter[], hexKey: string, - userRelaysType: UserRelaysType + userRelaysType: UserRelaysType, + opts?: NDKSubscriptionOptions ) => Promise findMetadata: ( pubkey: string, @@ -67,8 +75,8 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { }, []) const ndk = useMemo(() => { - localStorage.setItem('debug', '*') - const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'degmod-db' }) + // localStorage.setItem('debug', '*') + const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'sigit-db' }) dexieAdapter.locking = true const ndk = new NDK({ enableOutboxModel: true, @@ -88,11 +96,15 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { * @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): Promise => { + const fetchEvents = async ( + filter: NDKFilter, + opts?: NDKSubscriptionOptions + ): Promise => { return ndk .fetchEvents(filter, { closeOnEose: true, - cacheUsage: NDKSubscriptionCacheUsage.PARALLEL + cacheUsage: NDKSubscriptionCacheUsage.PARALLEL, + ...opts }) .then((ndkEventSet) => { const ndkEvents = Array.from(ndkEventSet) @@ -112,8 +124,11 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { * @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) => { - const events = await fetchEvents(filter) + const fetchEvent = async ( + filter: NDKFilter, + opts?: NDKSubscriptionOptions + ) => { + const events = await fetchEvents(filter, opts) if (events.length === 0) return null return events[0] } @@ -130,7 +145,8 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { const fetchEventsFromUserRelays = async ( filter: NDKFilter | NDKFilter[], hexKey: string, - userRelaysType: UserRelaysType + userRelaysType: UserRelaysType, + opts?: NDKSubscriptionOptions ): Promise => { // Find the user's relays (10s timeout). const relayUrls = await Promise.race([ @@ -156,7 +172,11 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { return ndk .fetchEvents( filter, - { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL }, + { + closeOnEose: true, + cacheUsage: NDKSubscriptionCacheUsage.PARALLEL, + ...opts + }, relayUrls.length ? NDKRelaySet.fromRelayUrls(relayUrls, ndk, true) : undefined @@ -185,12 +205,14 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { const fetchEventFromUserRelays = async ( filter: NDKFilter | NDKFilter[], hexKey: string, - userRelaysType: UserRelaysType + userRelaysType: UserRelaysType, + opts?: NDKSubscriptionOptions ) => { const events = await fetchEventsFromUserRelays( filter, hexKey, - userRelaysType + userRelaysType, + opts ) if (events.length === 0) return null return events[0] diff --git a/src/hooks/useNDK.ts b/src/hooks/useNDK.ts index 42fa053..ea1ab7c 100644 --- a/src/hooks/useNDK.ts +++ b/src/hooks/useNDK.ts @@ -2,7 +2,13 @@ import { useCallback } from 'react' import { toast } from 'react-toastify' import { bytesToHex } from '@noble/hashes/utils' -import { NDKEvent, NDKFilter, NDKKind, NDKRelaySet } from '@nostr-dev-kit/ndk' +import { + NDKEvent, + NDKFilter, + NDKKind, + NDKRelaySet, + NDKSubscriptionCacheUsage +} from '@nostr-dev-kit/ndk' import _ from 'lodash' import { Event, @@ -74,7 +80,9 @@ export const useNDK = () => { '#d': [dTag] } - const encryptedContent = await fetchEvent(filter) + const encryptedContent = await fetchEvent(filter, { + cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY + }) .then((event) => { if (event) return event.content @@ -171,38 +179,40 @@ export const useNDK = () => { }, [usersPubkey, fetchEvent]) const updateUsersAppData = useCallback( - async (meta: Meta) => { + async (metaArray: Meta[]) => { if (!appData || !appData.keyPair || !usersPubkey) return null const sigits = _.cloneDeep(appData.sigits) - - const createSignatureEvent = await parseJson( - meta.createSignature - ).catch((err) => { - console.log('err in parsing the createSignature event:>> ', err) - toast.error( - err.message || 'error occurred in parsing the create signature event' - ) - return null - }) - - if (!createSignatureEvent) return null - - const id = createSignatureEvent.id let isUpdated = false - // check if sigit already exists - if (id in sigits) { - // update meta only if incoming meta is more recent - // than already existing one - const existingMeta = sigits[id] - if (existingMeta.modifiedAt < meta.modifiedAt) { + for (const meta of metaArray) { + const createSignatureEvent = await parseJson( + meta.createSignature + ).catch((err) => { + console.log('Error in parsing the createSignature event:', err) + toast.error( + err.message || + 'Error occurred in parsing the create signature event' + ) + return null + }) + + if (!createSignatureEvent) continue + + const id = createSignatureEvent.id + + // Check if sigit already exists + if (id in sigits) { + // Update meta only if incoming meta is more recent + const existingMeta = sigits[id] + if (existingMeta.modifiedAt < meta.modifiedAt) { + sigits[id] = meta + isUpdated = true + } + } else { sigits[id] = meta isUpdated = true } - } else { - sigits[id] = meta - isUpdated = true } if (!isUpdated) return null @@ -215,34 +225,31 @@ export const useNDK = () => { appData.keyPair.private ).catch((err) => { console.log( - 'An error occurred in uploading user app data file to blossom server', + 'Error uploading user app data file to Blossom server:', err ) toast.error( - 'An error occurred in uploading user app data file to blossom server' + 'Error occurred in uploading user app data file to Blossom server' ) return null }) if (!newBlossomUrl) return null - // insert new blossom url at the start of the array + // Insert new blossom URL at the start of the array blossomUrls.unshift(newBlossomUrl) - // only keep last 10 blossom urls, delete older ones + // Keep only the last 10 Blossom URLs, delete older ones if (blossomUrls.length > 10) { const filesToDelete = blossomUrls.splice(10) filesToDelete.forEach((url) => { deleteBlossomFile(url, appData.keyPair!.private).catch((err) => { - console.log( - 'An error occurred in removing old file of user app data from blossom server', - err - ) + console.log('Error removing old file from Blossom server:', err) }) }) } - // encrypt content for storing in kind 30078 event + // Encrypt content for storing in kind 30078 event const nostrController = NostrController.getInstance() const encryptedContent = await nostrController .nip04Encrypt( @@ -253,20 +260,14 @@ export const useNDK = () => { }) ) .catch((err) => { - console.log( - 'An error occurred in encryption of content for app data', - err - ) - toast.error( - err.message || - 'An error occurred in encryption of content for app data' - ) + console.log('Error encrypting content for app data:', err) + toast.error(err.message || 'Error encrypting content for app data') return null }) if (!encryptedContent) return null - // generate the identifier for user's appData event + // Generate the identifier for user's appData event const dTag = await getDTagForUserAppData() if (!dTag) return null @@ -281,8 +282,8 @@ export const useNDK = () => { const signedEvent = await nostrController .signEvent(updatedEvent) .catch((err) => { - console.log('An error occurred in signing event', err) - toast.error(err.message || 'An error occurred in signing event') + console.log('Error signing event:', err) + toast.error(err.message || 'Error signing event') return null }) @@ -291,16 +292,14 @@ export const useNDK = () => { const ndkEvent = new NDKEvent(ndk, signedEvent) const publishResult = await publish(ndkEvent) - if (publishResult.length === 0) { - toast.error( - 'An unexpected error occurred in publishing updated app data ' - ) + if (publishResult.length === 0 || !publishResult) { + toast.error('Unexpected error occurred in publishing updated app data') return null } - if (!publishResult) return null + console.count('updateUserAppData useNDK') - // update redux store + // Update Redux store dispatch( updateUserAppDataAction({ sigits, @@ -317,94 +316,103 @@ export const useNDK = () => { [appData, dispatch, ndk, publish, usersPubkey] ) - const processReceivedEvent = useCallback( - async (event: NDKEvent, difficulty: number = 5) => { - // Abort processing if userAppData is undefined + const processReceivedEvents = useCallback( + async (events: NDKEvent[], difficulty: number = 5) => { if (!processedEvents) return - if (processedEvents.includes(event.id)) return + const validMetaArray: Meta[] = [] // Array to store valid Meta objects + const updatedProcessedEvents = [...processedEvents] // Keep track of processed event IDs - dispatch(updateProcessedGiftWraps([...processedEvents, event.id])) + for (const event of events) { + // Skip already processed events + if (processedEvents.includes(event.id)) continue - // validate PoW - // Count the number of leading zero bits in the hash - const leadingZeroes = countLeadingZeroes(event.id) - if (leadingZeroes < difficulty) return + // Validate PoW + const leadingZeroes = countLeadingZeroes(event.id) + if (leadingZeroes < difficulty) continue - // decrypt the content of gift wrap event - const nostrController = NostrController.getInstance() - const decrypted = await nostrController.nip44Decrypt( - event.pubkey, - event.content - ) + // Decrypt the content of the gift wrap event + const nostrController = NostrController.getInstance() + const decrypted = await nostrController + .nip44Decrypt(event.pubkey, event.content) + .catch((err) => { + console.log('An error occurred in decrypting event content', err) + return null + }) - const internalUnsignedEvent = await parseJson( - decrypted - ).catch((err) => { - console.log( - 'An error occurred in parsing the internal unsigned event', - err - ) - return null - }) + if (!decrypted) continue - if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938) return - - const parsedContent = await parseJson( - internalUnsignedEvent.content - ).catch((err) => { - console.log( - 'An error occurred in parsing the internal unsigned event', - err - ) - return null - }) - - if (!parsedContent) return - - let meta: Meta - - if (isSigitNotification(parsedContent)) { - const notification = parsedContent - if (!notification.keys || !usersPubkey) return - - let encryptionKey: string | undefined - - const { sender, keys } = notification.keys - - // Retrieve the user's public key from the state - const usersNpub = hexToNpub(usersPubkey) - - // Check if the user's public key is in the keys object - if (usersNpub in keys) { - // Instantiate the NostrController to decrypt the encryption key - const nostrController = NostrController.getInstance() - const decrypted = await nostrController - .nip04Decrypt(sender, keys[usersNpub]) - .catch((err) => { - console.log('An error occurred in decrypting encryption key', err) - return undefined - }) - - encryptionKey = decrypted - } - try { - meta = await fetchMetaFromFileStorage( - notification.metaUrl, - encryptionKey + const internalUnsignedEvent = await parseJson( + decrypted + ).catch((err) => { + console.log( + 'An error occurred in parsing the internal unsigned event', + err ) - } catch (error) { - console.error( - `An error occured fetching meta file from storage`, - error - ) - return + return null + }) + + if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938) + continue + + const parsedContent = await parseJson( + internalUnsignedEvent.content + ).catch((err) => { + console.log('An error occurred in parsing event content', err) + return null + }) + + if (!parsedContent) continue + + let meta: Meta + + if (isSigitNotification(parsedContent)) { + const notification = parsedContent + if (!notification.keys || !usersPubkey) continue + + let encryptionKey: string | undefined + const { sender, keys } = notification.keys + const usersNpub = hexToNpub(usersPubkey) + + if (usersNpub in keys) { + encryptionKey = await nostrController + .nip04Decrypt(sender, keys[usersNpub]) + .catch((err) => { + console.log( + 'An error occurred in decrypting encryption key', + err + ) + return undefined + }) + } + + try { + meta = await fetchMetaFromFileStorage( + notification.metaUrl, + encryptionKey + ) + } catch (error) { + console.error( + 'An error occurred fetching meta file from storage', + error + ) + continue + } + } else { + meta = parsedContent } - } else { - meta = parsedContent + + validMetaArray.push(meta) // Add valid Meta to the array + updatedProcessedEvents.push(event.id) // Mark event as processed } - await updateUsersAppData(meta) + // Update processed events in the Redux store + dispatch(updateProcessedGiftWraps(updatedProcessedEvents)) + + // Pass the array of Meta objects to updateUsersAppData + if (validMetaArray.length > 0) { + await updateUsersAppData(validMetaArray) + } }, [dispatch, processedEvents, updateUsersAppData, usersPubkey] ) @@ -421,14 +429,15 @@ export const useNDK = () => { const events = await fetchEventsFromUserRelays( filter, pubkey, - UserRelaysType.Read + UserRelaysType.Read, + { + cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY + } ) - for (const e of events) { - await processReceivedEvent(e) - } + await processReceivedEvents(events) }, - [fetchEventsFromUserRelays, processReceivedEvent] + [fetchEventsFromUserRelays, processReceivedEvents] ) /** diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index 8ca946a..de9c3d3 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -904,7 +904,7 @@ export const CreatePage = () => { setLoadingSpinnerDesc('Updating user app data') - const event = await updateUsersAppData(meta) + const event = await updateUsersAppData([meta]) if (!event) return const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey) diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index f7166e2..d81dd1b 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -58,7 +58,7 @@ export const HomePage = () => { const usersAppData = useAppSelector((state) => state.userAppData) useEffect(() => { - if (usersAppData) { + if (usersAppData?.sigits) { const getSigitInfo = async () => { const parsedSigits: { [key: string]: SigitCardDisplayInfo } = {} for (const key in usersAppData.sigits) { @@ -80,7 +80,7 @@ export const HomePage = () => { setSigits(usersAppData.sigits) getSigitInfo() } - }, [usersAppData]) + }, [usersAppData?.sigits]) const onDrop = useCallback( async (acceptedFiles: File[]) => { diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index d4071e0..8eb782e 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -646,7 +646,7 @@ export const SignPage = () => { encryptionKey: string | undefined ) => { setLoadingSpinnerDesc('Updating users app data') - const updatedEvent = await updateUsersAppData(meta) + const updatedEvent = await updateUsersAppData([meta]) if (!updatedEvent) { setIsLoading(false) return diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 43dcc4b..e39ca44 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -353,7 +353,7 @@ export const VerifyPage = () => { updatedMeta.timestamps = [...finalTimestamps] updatedMeta.modifiedAt = unixNow() - const updatedEvent = await updateUsersAppData(updatedMeta) + const updatedEvent = await updateUsersAppData([updatedMeta]) if (!updatedEvent) return const metaUrl = await uploadMetaToFileStorage(