From 34b096b121c6b254ace44ac155ce51539e373518 Mon Sep 17 00:00:00 2001 From: daniyal Date: Wed, 11 Sep 2024 22:27:37 +0500 Subject: [PATCH] chore(refactor): improve subscription process --- src/controllers/relay.ts | 114 ++++++++++++++++++++++++--------------- src/controllers/zap.ts | 57 ++++++++++++-------- src/utils/nostr.ts | 28 ++-------- 3 files changed, 110 insertions(+), 89 deletions(-) diff --git a/src/controllers/relay.ts b/src/controllers/relay.ts index deae1b8..0b1413f 100644 --- a/src/controllers/relay.ts +++ b/src/controllers/relay.ts @@ -1,4 +1,4 @@ -import { Event, Filter, kinds, Relay } from 'nostr-tools' +import { Event, Filter, kinds, nip57, Relay } from 'nostr-tools' import { extractZapAmount, log, @@ -600,29 +600,20 @@ export class RelayController { const processedEvents: string[] = [] // To keep track of processed events // Create a promise for each relay subscription - const subPromises = relays.map((relay) => { - return new Promise((resolve) => { - // Subscribe to the relay with the specified filter - const sub = relay.subscribe([filter], { - // Handle incoming events - onevent: (e) => { - // Process event only if it hasn't been processed before - if (!processedEvents.includes(e.id)) { - processedEvents.push(e.id) - eventHandler(e) // Call the event handler with the event - } - }, - // Handle the End-Of-Stream (EOSE) message - oneose: () => { - sub.close() // Close the subscription - resolve() // Resolve the promise when EOSE is received + const subscriptions = relays.map((relay) => + relay.subscribe([filter], { + // Handle incoming events + onevent: (e) => { + // Process event only if it hasn't been processed before + if (!processedEvents.includes(e.id)) { + processedEvents.push(e.id) + eventHandler(e) // Call the event handler with the event } - }) + } }) - }) + ) - // Wait for all subscriptions to complete - await Promise.allSettled(subPromises) + return subscriptions } getTotalZapAmount = async ( @@ -638,53 +629,90 @@ export class RelayController { UserRelaysType.Read ) - // add app relay to relays array - relayUrls.push(import.meta.env.VITE_APP_RELAY) - - // add admin relays to relays array - metadataController.adminRelays.forEach((url) => { - relayUrls.push(url) - }) + const appRelay = import.meta.env.VITE_APP_RELAY + if (!relayUrls.includes(appRelay)) { + relayUrls.push(appRelay) + } // Connect to all specified relays const relayPromises = relayUrls.map((relayUrl) => this.connectRelay(relayUrl) ) - await Promise.allSettled(relayPromises) + + // Use Promise.allSettled to wait for all promises to settle + const results = await Promise.allSettled(relayPromises) + + // Extract non-null values from fulfilled promises in a single pass + const relays = results.reduce((acc, result) => { + if (result.status === 'fulfilled') { + const value = result.value + if (value) { + acc.push(value) + } + } + return acc + }, []) let accumulatedZapAmount = 0 let hasZapped = false const eventIds = new Set() // To keep track of event IDs and avoid duplicates - const filter: Filter = { - kinds: [kinds.Zap] - } + const filters: Filter[] = [ + { + kinds: [kinds.Zap], + '#e': [eTag] + } + ] if (aTag) { - filter['#a'] = [aTag] - } else { - filter['#e'] = [eTag] + filters.push({ + kinds: [kinds.Zap], + '#a': [aTag] + }) } // Create a promise for each relay subscription - const subPromises = this.connectedRelays.map((relay) => { + const subPromises = relays.map((relay) => { return new Promise((resolve) => { // Subscribe to the relay with the specified filter - const sub = relay.subscribe([filter], { + const sub = relay.subscribe(filters, { // Handle incoming events onevent: (e) => { // Add the event to the array if it's not a duplicate if (!eventIds.has(e.id)) { eventIds.add(e.id) // Record the event ID - const amount = extractZapAmount(e) + + const zapRequestStr = e.tags.find( + (t) => t[0] === 'description' + )?.[1] + if (!zapRequestStr) return + + const error = nip57.validateZapRequest(zapRequestStr) + if (error) return + + let zapRequest: Event | null = null + + try { + zapRequest = JSON.parse(zapRequestStr) + } catch (error) { + log( + true, + LogType.Error, + 'Error occurred in parsing zap request', + error + ) + } + + if (!zapRequest) return + + const amount = extractZapAmount(zapRequest) accumulatedZapAmount += amount - if (!hasZapped) { - hasZapped = - e.tags.findIndex( - (tag) => tag[0] === 'P' && tag[1] === currentLoggedInUser - ) > -1 + if (amount > 0) { + if (!hasZapped) { + hasZapped = zapRequest.pubkey === currentLoggedInUser + } } } }, diff --git a/src/controllers/zap.ts b/src/controllers/zap.ts index 608fee1..5bbaf51 100644 --- a/src/controllers/zap.ts +++ b/src/controllers/zap.ts @@ -12,6 +12,7 @@ import { } from '../types' import { log, LogType, npubToHex } from '../utils' import { RelayController } from './relay' +import { MetadataController, UserRelaysType } from './metadata' /** * Singleton class to manage zap related operations. @@ -147,7 +148,7 @@ export class ZapController { const cleanup = () => { clearTimeout(timeout) - sub.close() + subscriptions.forEach((subscription) => subscription.close()) } // Polling timeout @@ -160,13 +161,11 @@ export class ZapController { pollingTimeout || 6 * 60 * 1000 // 6 minutes ) - const relay = await RelayController.getInstance().connectRelay( - this.appRelay - ) + const relaysTag = zapRequest.tags.find((t) => t[0] === 'relays') + if (!relaysTag) + throw new Error('Zap request does not contain relays tag.') - if (!relay) { - return reject('Polling Zap Receipt: Could not connect to app relay!') - } + const relayUrls = relaysTag.slice(1) // filter relay for event of kind 9735 const filter: Filter = { @@ -174,25 +173,27 @@ export class ZapController { since: created_at } - const sub = relay.subscribe([filter], { - // Handle incoming events - onevent: async (event) => { - // get description tag of the event - const description = event.tags.filter( - (tag) => tag[0] === 'description' - )[0] + const subscriptions = + await RelayController.getInstance().subscribeForEvents( + filter, + relayUrls, + async (event) => { + // get description tag of the event + const description = event.tags.filter( + (tag) => tag[0] === 'description' + )[0] - // compare description tag of the event with stringified zap request - if (description[1] === zapRequestStringified) { - // validate zap receipt - if (await this.validateZapReceipt(pr, event as ZapReceipt)) { - cleanup() + // compare description tag of the event with stringified zap request + if (description[1] === zapRequestStringified) { + // validate zap receipt + if (await this.validateZapReceipt(pr, event as ZapReceipt)) { + cleanup() - resolve(event as ZapReceipt) + resolve(event as ZapReceipt) + } } } - } - }) + ) }) } @@ -280,11 +281,21 @@ export class ZapController { if (!recipientHexKey) throw 'Invalid recipient pubKey.' + const metadataController = await MetadataController.getInstance() + const receiverReadRelays = await metadataController.findUserRelays( + recipientHexKey, + UserRelaysType.Read + ) + + if (!receiverReadRelays.includes(this.appRelay)) { + receiverReadRelays.push(this.appRelay) + } + const zapRequest: ZapRequest = { kind: kinds.ZapRequest, content, tags: [ - ['relays', `${this.appRelay}`], + ['relays', ...receiverReadRelays], ['amount', `${amount}`], ['p', recipientHexKey] ], diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index ef03678..234c69d 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -104,31 +104,13 @@ export const npubToHex = (pubKey: string): string | null => { * @returns The zap amount in the form of a number, converted from the extracted data, or 0 if the amount cannot be determined. */ export const extractZapAmount = (event: Event): number => { - // Find the 'description' tag within the event's tags - const description = event.tags.find( - (tag) => tag[0] === 'description' && typeof tag[1] === 'string' + // Find the 'amount' tag within the parsed description's tags + const amountTag = event.tags.find( + (tag) => tag[0] === 'amount' && typeof tag[1] === 'string' ) - // If the 'description' tag is found and it has a valid value - if (description && description[1]) { - try { - // Parse the description as JSON to get additional details - const parsedDescription: Event = JSON.parse(description[1]) - - // Find the 'amount' tag within the parsed description's tags - const amountTag = parsedDescription.tags.find( - (tag) => tag[0] === 'amount' && typeof tag[1] === 'string' - ) - - // If the 'amount' tag is found and it has a valid value, convert it to an integer and return - if (amountTag && amountTag[1]) return parseInt(amountTag[1]) / 1000 - } catch (error) { - // Log an error message if JSON parsing fails - console.log( - `An error occurred while parsing description of zap event: ${error}` - ) - } - } + // If the 'amount' tag is found and it has a valid value, convert it to an integer and return + if (amountTag && amountTag[1]) return parseInt(amountTag[1]) / 1000 // Return 0 if the zap amount cannot be determined return 0