feat: implemented relay controller and use that for fetching and publishing events
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
This commit is contained in:
parent
b8ba8d0ab4
commit
a775d7b265
@ -1,28 +1,29 @@
|
|||||||
|
import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
|
||||||
import {
|
import {
|
||||||
Filter,
|
|
||||||
SimplePool,
|
|
||||||
VerifiedEvent,
|
|
||||||
kinds,
|
|
||||||
validateEvent,
|
|
||||||
verifyEvent,
|
|
||||||
Event,
|
Event,
|
||||||
EventTemplate,
|
EventTemplate,
|
||||||
nip19
|
Filter,
|
||||||
|
VerifiedEvent,
|
||||||
|
kinds,
|
||||||
|
nip19,
|
||||||
|
validateEvent,
|
||||||
|
verifyEvent
|
||||||
} from 'nostr-tools'
|
} from 'nostr-tools'
|
||||||
import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types'
|
|
||||||
import { NostrController } from '.'
|
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { queryNip05, unixNow } from '../utils'
|
|
||||||
import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
|
|
||||||
import { EventEmitter } from 'tseep'
|
import { EventEmitter } from 'tseep'
|
||||||
|
import { NostrController, relayController } from '.'
|
||||||
import { localCache } from '../services'
|
import { localCache } from '../services'
|
||||||
|
import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types'
|
||||||
import {
|
import {
|
||||||
findRelayListAndUpdateCache,
|
findRelayListAndUpdateCache,
|
||||||
findRelayListInCache,
|
findRelayListInCache,
|
||||||
getDefaultRelaySet,
|
getDefaultRelaySet,
|
||||||
|
getMostPopularRelays,
|
||||||
getUserRelaySet,
|
getUserRelaySet,
|
||||||
isOlderThanOneWeek
|
isOlderThanOneWeek,
|
||||||
} from '../utils/relays.ts'
|
queryNip05,
|
||||||
|
unixNow
|
||||||
|
} from '../utils'
|
||||||
|
|
||||||
export class MetadataController extends EventEmitter {
|
export class MetadataController extends EventEmitter {
|
||||||
private nostrController: NostrController
|
private nostrController: NostrController
|
||||||
@ -51,11 +52,9 @@ export class MetadataController extends EventEmitter {
|
|||||||
authors: [hexKey] // Authored by the specified key
|
authors: [hexKey] // Authored by the specified key
|
||||||
}
|
}
|
||||||
|
|
||||||
const pool = new SimplePool()
|
|
||||||
|
|
||||||
// Try to get the metadata event from a special relay (wss://purplepag.es)
|
// Try to get the metadata event from a special relay (wss://purplepag.es)
|
||||||
const metadataEvent = await pool
|
const metadataEvent = await relayController
|
||||||
.get([this.specialMetadataRelay], eventFilter)
|
.fetchEvent(eventFilter, [this.specialMetadataRelay])
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err) // Log any errors
|
console.error(err) // Log any errors
|
||||||
return null // Return null if an error occurs
|
return null // Return null if an error occurs
|
||||||
@ -80,11 +79,12 @@ export class MetadataController extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If no valid metadata event is found from the special relay, get the most popular relays
|
// If no valid metadata event is found from the special relay, get the most popular relays
|
||||||
const mostPopularRelays = await this.nostrController.getMostPopularRelays()
|
const mostPopularRelays = await getMostPopularRelays()
|
||||||
|
|
||||||
// Query the most popular relays for metadata events
|
// Query the most popular relays for metadata events
|
||||||
const events = await pool
|
|
||||||
.querySync(mostPopularRelays, eventFilter)
|
const events = await relayController
|
||||||
|
.fetchEvents(eventFilter, mostPopularRelays)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err) // Log any errors
|
console.error(err) // Log any errors
|
||||||
return null // Return null if an error occurs
|
return null // Return null if an error occurs
|
||||||
@ -169,10 +169,7 @@ export class MetadataController extends EventEmitter {
|
|||||||
[this.specialMetadataRelay],
|
[this.specialMetadataRelay],
|
||||||
hexKey
|
hexKey
|
||||||
)) ||
|
)) ||
|
||||||
(await findRelayListAndUpdateCache(
|
(await findRelayListAndUpdateCache(await getMostPopularRelays(), hexKey))
|
||||||
await this.nostrController.getMostPopularRelays(),
|
|
||||||
hexKey
|
|
||||||
))
|
|
||||||
|
|
||||||
return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet()
|
return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet()
|
||||||
}
|
}
|
||||||
@ -206,11 +203,15 @@ export class MetadataController extends EventEmitter {
|
|||||||
await this.nostrController.signEvent(newMetadataEvent)
|
await this.nostrController.signEvent(newMetadataEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.nostrController
|
await relayController
|
||||||
.publishEvent(signedMetadataEvent, [this.specialMetadataRelay])
|
.publish(signedMetadataEvent, [this.specialMetadataRelay])
|
||||||
.then((relays) => {
|
.then((relays) => {
|
||||||
toast.success(`Metadata event published on: ${relays.join('\n')}`)
|
if (relays.length) {
|
||||||
this.handleNewMetadataEvent(signedMetadataEvent as VerifiedEvent)
|
toast.success(`Metadata event published on: ${relays.join('\n')}`)
|
||||||
|
this.handleNewMetadataEvent(signedMetadataEvent as VerifiedEvent)
|
||||||
|
} else {
|
||||||
|
toast.error('Could not publish metadata event to any relay!')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
toast.error(err.message)
|
toast.error(err.message)
|
||||||
@ -250,16 +251,10 @@ export class MetadataController extends EventEmitter {
|
|||||||
authors: [hexKey]
|
authors: [hexKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
const pool = new SimplePool()
|
// find user's kind 0 event published on user's relays
|
||||||
|
const event = await relayController.fetchEvent(eventFilter, userRelays)
|
||||||
|
|
||||||
// find user's kind 0 events published on user's relays
|
if (event) {
|
||||||
const events = await pool.querySync(userRelays, eventFilter)
|
|
||||||
if (events && events.length) {
|
|
||||||
// sort events by created_at time in ascending order
|
|
||||||
events.sort((a, b) => a.created_at - b.created_at)
|
|
||||||
|
|
||||||
// get first ever event published on user's relays
|
|
||||||
const event = events[0]
|
|
||||||
const { created_at } = event
|
const { created_at } = event
|
||||||
|
|
||||||
// initialize job request
|
// initialize job request
|
||||||
@ -283,10 +278,12 @@ export class MetadataController extends EventEmitter {
|
|||||||
'wss://relayable.org'
|
'wss://relayable.org'
|
||||||
]
|
]
|
||||||
|
|
||||||
// publish job request
|
await relayController.publish(jobSignedEvent, relays).catch((err) => {
|
||||||
await this.nostrController.publishEvent(jobSignedEvent, relays)
|
console.error(
|
||||||
|
'Error occurred in publish blockChain-block-number DVM job',
|
||||||
console.log('jobSignedEvent :>> ', jobSignedEvent)
|
err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const subscribeWithTimeout = (
|
const subscribeWithTimeout = (
|
||||||
subscription: NDKSubscription,
|
subscription: NDKSubscription,
|
||||||
|
@ -6,7 +6,6 @@ import NDK, {
|
|||||||
NDKUser,
|
NDKUser,
|
||||||
NostrEvent
|
NostrEvent
|
||||||
} from '@nostr-dev-kit/ndk'
|
} from '@nostr-dev-kit/ndk'
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
import {
|
||||||
Event,
|
Event,
|
||||||
EventTemplate,
|
EventTemplate,
|
||||||
@ -20,10 +19,8 @@ import {
|
|||||||
nip19,
|
nip19,
|
||||||
nip44
|
nip44
|
||||||
} from 'nostr-tools'
|
} from 'nostr-tools'
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { EventEmitter } from 'tseep'
|
import { EventEmitter } from 'tseep'
|
||||||
import {
|
import {
|
||||||
setMostPopularRelaysAction,
|
|
||||||
setRelayConnectionStatusAction,
|
setRelayConnectionStatusAction,
|
||||||
setRelayInfoAction,
|
setRelayInfoAction,
|
||||||
updateNsecbunkerPubkey
|
updateNsecbunkerPubkey
|
||||||
@ -35,17 +32,17 @@ import {
|
|||||||
RelayConnectionStatus,
|
RelayConnectionStatus,
|
||||||
RelayInfoObject,
|
RelayInfoObject,
|
||||||
RelayMap,
|
RelayMap,
|
||||||
RelayReadStats,
|
|
||||||
RelayStats,
|
|
||||||
SignedEvent
|
SignedEvent
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import {
|
import {
|
||||||
compareObjects,
|
compareObjects,
|
||||||
|
getDefaultRelayMap,
|
||||||
|
getMostPopularRelays,
|
||||||
getNsecBunkerDelegatedKey,
|
getNsecBunkerDelegatedKey,
|
||||||
unixNow,
|
unixNow,
|
||||||
verifySignedEvent
|
verifySignedEvent
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { getDefaultRelayMap } from '../utils/relays.ts'
|
import { relayController } from './'
|
||||||
|
|
||||||
export class NostrController extends EventEmitter {
|
export class NostrController extends EventEmitter {
|
||||||
private static instance: NostrController
|
private static instance: NostrController
|
||||||
@ -223,98 +220,6 @@ export class NostrController extends EventEmitter {
|
|||||||
return NostrController.instance
|
return NostrController.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Function will publish provided event to the provided relays
|
|
||||||
*
|
|
||||||
* @param event - The event to publish.
|
|
||||||
* @param relays - An array of relay URLs to publish the event to.
|
|
||||||
* @returns A promise that resolves to an array of relays where the event was successfully published.
|
|
||||||
*/
|
|
||||||
publishEvent = async (event: Event, relays: string[]) => {
|
|
||||||
const simplePool = new SimplePool()
|
|
||||||
|
|
||||||
// Publish the event to all relays
|
|
||||||
const promises = simplePool.publish(relays, event)
|
|
||||||
|
|
||||||
// Use Promise.race to wait for the first successful publish
|
|
||||||
const firstSuccessfulPublish = await Promise.race(
|
|
||||||
promises.map((promise, index) =>
|
|
||||||
promise.then(() => relays[index]).catch(() => null)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!firstSuccessfulPublish) {
|
|
||||||
// If no publish was successful, collect the reasons for failures
|
|
||||||
const failedPublishes: unknown[] = []
|
|
||||||
const fallbackRejectionReason =
|
|
||||||
'Attempt to publish an event has been rejected with unknown reason.'
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(promises)
|
|
||||||
results.forEach((res, index) => {
|
|
||||||
if (res.status === 'rejected') {
|
|
||||||
failedPublishes.push({
|
|
||||||
relay: relays[index],
|
|
||||||
error: res.reason
|
|
||||||
? res.reason.message || fallbackRejectionReason
|
|
||||||
: fallbackRejectionReason
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
throw failedPublishes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue publishing to other relays in the background
|
|
||||||
promises.forEach((promise, index) => {
|
|
||||||
promise.catch((err) => {
|
|
||||||
console.log(`Failed to publish to ${relays[index]}`, err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return [firstSuccessfulPublish]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously retrieves an event from a set of relays based on a provided filter.
|
|
||||||
* If no relays are specified, it defaults to using connected relays.
|
|
||||||
*
|
|
||||||
* @param {Filter} filter - The filter criteria to find the event.
|
|
||||||
* @param {string[]} [relays] - An optional array of relay URLs to search for the event.
|
|
||||||
* @returns {Promise<Event | null>} - Returns a promise that resolves to the found event or null if not found.
|
|
||||||
*/
|
|
||||||
getEvent = async (
|
|
||||||
filter: Filter,
|
|
||||||
relays?: string[]
|
|
||||||
): Promise<Event | null> => {
|
|
||||||
// If no relays are provided or the provided array is empty, use connected relays if available.
|
|
||||||
if (!relays || relays.length === 0) {
|
|
||||||
relays = this.connectedRelays
|
|
||||||
? this.connectedRelays.map((relay) => relay.url)
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
|
|
||||||
// If still no relays are available, reject the promise with an error message.
|
|
||||||
if (relays.length === 0) {
|
|
||||||
return Promise.reject('Provide some relays to find the event')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new instance of SimplePool to handle the relay connections and event retrieval.
|
|
||||||
const pool = new SimplePool()
|
|
||||||
|
|
||||||
// Attempt to retrieve the event from the specified relays using the filter criteria.
|
|
||||||
const event = await pool.get(relays, filter).catch((err) => {
|
|
||||||
// Log any errors that occur during the event retrieval process.
|
|
||||||
console.log('An error occurred in finding the event', err)
|
|
||||||
// Show an error toast notification to the user.
|
|
||||||
toast.error('An error occurred in finding the event')
|
|
||||||
// Return null if an error occurs, indicating that no event was found.
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return the found event, or null if an error occurred.
|
|
||||||
return event
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts the given content for the specified receiver using NIP-44 encryption.
|
* Encrypts the given content for the specified receiver using NIP-44 encryption.
|
||||||
*
|
*
|
||||||
@ -659,7 +564,7 @@ export class NostrController extends EventEmitter {
|
|||||||
getRelayMap = async (
|
getRelayMap = async (
|
||||||
npub: string
|
npub: string
|
||||||
): Promise<{ map: RelayMap; mapUpdated?: number }> => {
|
): Promise<{ map: RelayMap; mapUpdated?: number }> => {
|
||||||
const mostPopularRelays = await this.getMostPopularRelays()
|
const mostPopularRelays = await getMostPopularRelays()
|
||||||
|
|
||||||
const pool = new SimplePool()
|
const pool = new SimplePool()
|
||||||
|
|
||||||
@ -750,10 +655,13 @@ export class NostrController extends EventEmitter {
|
|||||||
|
|
||||||
// If relay map is empty, use most popular relay URIs
|
// If relay map is empty, use most popular relay URIs
|
||||||
if (!relaysToPublish.length) {
|
if (!relaysToPublish.length) {
|
||||||
relaysToPublish = await this.getMostPopularRelays()
|
relaysToPublish = await getMostPopularRelays()
|
||||||
}
|
}
|
||||||
|
|
||||||
const publishResult = await this.publishEvent(signedEvent, relaysToPublish)
|
const publishResult = await relayController.publish(
|
||||||
|
signedEvent,
|
||||||
|
relaysToPublish
|
||||||
|
)
|
||||||
|
|
||||||
if (publishResult && publishResult.length) {
|
if (publishResult && publishResult.length) {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
@ -764,51 +672,6 @@ export class NostrController extends EventEmitter {
|
|||||||
return Promise.reject('Publishing updated relay map was unsuccessful.')
|
return Promise.reject('Publishing updated relay map was unsuccessful.')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
getMostPopularRelays = async (
|
|
||||||
numberOfTopRelays: number = 30
|
|
||||||
): Promise<string[]> => {
|
|
||||||
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<RelayStats>(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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets information about relays into relays.info app state.
|
* Sets information about relays into relays.info app state.
|
||||||
* @param relayURIs - relay URIs to get information about
|
* @param relayURIs - relay URIs to get information about
|
||||||
@ -835,7 +698,7 @@ export class NostrController extends EventEmitter {
|
|||||||
]
|
]
|
||||||
|
|
||||||
// publish job request
|
// publish job request
|
||||||
await this.publishEvent(jobSignedEvent, relays)
|
await relayController.publish(jobSignedEvent, relays)
|
||||||
|
|
||||||
console.log('jobSignedEvent :>> ', jobSignedEvent)
|
console.log('jobSignedEvent :>> ', jobSignedEvent)
|
||||||
|
|
||||||
|
293
src/controllers/RelayController.ts
Normal file
293
src/controllers/RelayController.ts
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import { Filter, Relay, Event } from 'nostr-tools'
|
||||||
|
import { normalizeWebSocketURL, timeout } from '../utils'
|
||||||
|
import { SIGIT_RELAY } from '../utils/const'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton class to manage relay operations.
|
||||||
|
*/
|
||||||
|
export class RelayController {
|
||||||
|
private static instance: RelayController
|
||||||
|
public connectedRelays: Relay[] = []
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the singleton instance of RelayController.
|
||||||
|
*
|
||||||
|
* @returns The singleton instance of RelayController.
|
||||||
|
*/
|
||||||
|
public static getInstance(): RelayController {
|
||||||
|
if (!RelayController.instance) {
|
||||||
|
RelayController.instance = new RelayController()
|
||||||
|
}
|
||||||
|
return RelayController.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects to a relay server if not already connected.
|
||||||
|
*
|
||||||
|
* This method checks if a relay with the given URL is already in the list of connected relays.
|
||||||
|
* If it is not connected, it attempts to establish a new connection.
|
||||||
|
* On successful connection, the relay is added to the list of connected relays and returned.
|
||||||
|
* If the connection fails, an error is logged and `null` is returned.
|
||||||
|
*
|
||||||
|
* @param relayUrl - The URL of the relay server to connect to.
|
||||||
|
* @returns A promise that resolves to the connected relay object if successful, or `null` if the connection fails.
|
||||||
|
*/
|
||||||
|
public connectRelay = async (relayUrl: string) => {
|
||||||
|
// Check if a relay with the same URL is already connected
|
||||||
|
const relay = this.connectedRelays.find(
|
||||||
|
(relay) =>
|
||||||
|
normalizeWebSocketURL(relay.url) === normalizeWebSocketURL(relayUrl)
|
||||||
|
)
|
||||||
|
|
||||||
|
// If a matching relay is found, return it (skip connection)
|
||||||
|
if (relay) {
|
||||||
|
return relay
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Attempt to connect to the relay using the provided URL
|
||||||
|
const newRelay = await Relay.connect(relayUrl)
|
||||||
|
|
||||||
|
// Add the newly connected relay to the list of connected relays
|
||||||
|
this.connectedRelays.push(newRelay)
|
||||||
|
|
||||||
|
// Return the newly connected relay
|
||||||
|
return newRelay
|
||||||
|
} catch (err) {
|
||||||
|
// Log an error message if the connection fails
|
||||||
|
console.error(`Relay connection failed: ${relayUrl}`, err)
|
||||||
|
|
||||||
|
// Return null to indicate connection failure
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously retrieves multiple event from a set of relays based on a provided filter.
|
||||||
|
* If no relays are specified, it defaults to using connected relays.
|
||||||
|
*
|
||||||
|
* @param {Filter} filter - The filter criteria to find the event.
|
||||||
|
* @param {string[]} [relays] - An optional array of relay URLs to search for the event.
|
||||||
|
* @returns {Promise<Event | null>} - Returns a promise that resolves to the found event or null if not found.
|
||||||
|
*/
|
||||||
|
fetchEvents = async (
|
||||||
|
filter: Filter,
|
||||||
|
relayUrls: string[] = []
|
||||||
|
): Promise<Event[]> => {
|
||||||
|
// add app relay to relays array
|
||||||
|
relayUrls.push(SIGIT_RELAY)
|
||||||
|
|
||||||
|
// Connect to all specified relays
|
||||||
|
const relayPromises = relayUrls.map((relayUrl) =>
|
||||||
|
this.connectRelay(relayUrl)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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<Relay[]>((acc, result) => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
const value = result.value
|
||||||
|
if (value) {
|
||||||
|
acc.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Check if any relays are connected
|
||||||
|
if (relays.length === 0) {
|
||||||
|
throw new Error('No relay is connected to fetch events!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const events: Event[] = []
|
||||||
|
const eventIds = new Set<string>() // To keep track of event IDs and avoid duplicates
|
||||||
|
|
||||||
|
// Create a promise for each relay subscription
|
||||||
|
const subPromises = relays.map((relay) => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
// Subscribe to the relay with the specified filter
|
||||||
|
const sub = relay.subscribe([filter], {
|
||||||
|
// 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
|
||||||
|
events.push(e) // Add the event to the array
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Handle the End-Of-Stream (EOSE) message
|
||||||
|
oneose: () => {
|
||||||
|
sub.close() // Close the subscription
|
||||||
|
resolve() // Resolve the promise when EOSE is received
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for all subscriptions to complete
|
||||||
|
await Promise.allSettled(subPromises)
|
||||||
|
|
||||||
|
// It is possible that different relays will send different events and events array may contain more events then specified limit in filter
|
||||||
|
// To fix this issue we'll first sort these events and then return only limited events
|
||||||
|
if (filter.limit) {
|
||||||
|
// Sort events by creation date in descending order
|
||||||
|
events.sort((a, b) => b.created_at - a.created_at)
|
||||||
|
|
||||||
|
return events.slice(0, filter.limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously retrieves an event from a set of relays based on a provided filter.
|
||||||
|
* If no relays are specified, it defaults to using connected relays.
|
||||||
|
*
|
||||||
|
* @param {Filter} filter - The filter criteria to find the event.
|
||||||
|
* @param {string[]} [relays] - An optional array of relay URLs to search for the event.
|
||||||
|
* @returns {Promise<Event | null>} - Returns a promise that resolves to the found event or null if not found.
|
||||||
|
*/
|
||||||
|
fetchEvent = async (
|
||||||
|
filter: Filter,
|
||||||
|
relays: string[] = []
|
||||||
|
): Promise<Event | null> => {
|
||||||
|
const events = await this.fetchEvents(filter, relays)
|
||||||
|
|
||||||
|
// Sort events by creation date in descending order
|
||||||
|
events.sort((a, b) => b.created_at - a.created_at)
|
||||||
|
|
||||||
|
// Return the most recent event, or null if no events were received
|
||||||
|
return events[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to events from multiple relays.
|
||||||
|
*
|
||||||
|
* This method connects to the specified relay URLs and subscribes to events
|
||||||
|
* using the provided filter. It handles incoming events through the given
|
||||||
|
* `eventHandler` callback and manages the subscription lifecycle.
|
||||||
|
*
|
||||||
|
* @param filter - The filter criteria to apply when subscribing to events.
|
||||||
|
* @param relayUrls - An optional array of relay URLs to connect to. The default relay URL (`SIGIT_RELAY`) is added automatically.
|
||||||
|
* @param eventHandler - A callback function to handle incoming events. It receives an `Event` object.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
subscribeForEvents = async (
|
||||||
|
filter: Filter,
|
||||||
|
relayUrls: string[] = [],
|
||||||
|
eventHandler: (event: Event) => void
|
||||||
|
) => {
|
||||||
|
// add app relay to relays array
|
||||||
|
relayUrls.push(SIGIT_RELAY)
|
||||||
|
|
||||||
|
// Connect to all specified relays
|
||||||
|
const relayPromises = relayUrls.map((relayUrl) =>
|
||||||
|
this.connectRelay(relayUrl)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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<Relay[]>((acc, result) => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
const value = result.value
|
||||||
|
if (value) {
|
||||||
|
acc.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Check if any relays are connected
|
||||||
|
if (relays.length === 0) {
|
||||||
|
throw new Error('No relay is connected to fetch events!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedEvents: string[] = [] // To keep track of processed events
|
||||||
|
|
||||||
|
// Create a promise for each relay subscription
|
||||||
|
const subPromises = relays.map((relay) => {
|
||||||
|
return new Promise<void>((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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for all subscriptions to complete
|
||||||
|
await Promise.allSettled(subPromises)
|
||||||
|
}
|
||||||
|
|
||||||
|
publish = async (
|
||||||
|
event: Event,
|
||||||
|
relayUrls: string[] = []
|
||||||
|
): Promise<string[]> => {
|
||||||
|
// add app relay to relays array
|
||||||
|
relayUrls.push(SIGIT_RELAY)
|
||||||
|
|
||||||
|
// Connect to all specified relays
|
||||||
|
const relayPromises = relayUrls.map((relayUrl) =>
|
||||||
|
this.connectRelay(relayUrl)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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<Relay[]>((acc, result) => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
const value = result.value
|
||||||
|
if (value) {
|
||||||
|
acc.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Check if any relays are connected
|
||||||
|
if (relays.length === 0) {
|
||||||
|
throw new Error('No relay is connected to publish event!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const publishedOnRelays: string[] = [] // List to track which relays successfully published the event
|
||||||
|
|
||||||
|
// Create a promise for publishing the event to each connected relay
|
||||||
|
const publishPromises = relays.map(async (relay) => {
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
relay.publish(event), // Publish the event to the relay
|
||||||
|
timeout(30000) // Set a timeout to handle cases where publishing takes too long
|
||||||
|
])
|
||||||
|
publishedOnRelays.push(relay.url) // Add the relay URL to the list of successfully published relays
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to publish event on relay: ${relay}`, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for all publish operations to complete (either fulfilled or rejected)
|
||||||
|
await Promise.allSettled(publishPromises)
|
||||||
|
|
||||||
|
// Return the list of relay URLs where the event was published
|
||||||
|
return publishedOnRelays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const relayController = RelayController.getInstance()
|
@ -1,3 +1,4 @@
|
|||||||
export * from './AuthController'
|
export * from './AuthController'
|
||||||
export * from './MetadataController'
|
export * from './MetadataController'
|
||||||
export * from './NostrController'
|
export * from './NostrController'
|
||||||
|
export * from './RelayController'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
import { AppBar } from '../components/AppBar/AppBar'
|
import { AppBar } from '../components/AppBar/AppBar'
|
||||||
@ -25,7 +25,6 @@ import {
|
|||||||
subscribeForSigits
|
subscribeForSigits
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { useAppSelector } from '../hooks'
|
import { useAppSelector } from '../hooks'
|
||||||
import { SubCloser } from 'nostr-tools/abstract-pool'
|
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { Footer } from '../components/Footer/Footer'
|
import { Footer } from '../components/Footer/Footer'
|
||||||
|
|
||||||
@ -36,6 +35,9 @@ export const MainLayout = () => {
|
|||||||
const authState = useSelector((state: State) => state.auth)
|
const authState = useSelector((state: State) => state.auth)
|
||||||
const usersAppData = useAppSelector((state) => state.userAppData)
|
const usersAppData = useAppSelector((state) => state.userAppData)
|
||||||
|
|
||||||
|
// Ref to track if `subscribeForSigits` has been called
|
||||||
|
const hasSubscribed = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const metadataController = new MetadataController()
|
const metadataController = new MetadataController()
|
||||||
|
|
||||||
@ -103,21 +105,15 @@ export const MainLayout = () => {
|
|||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let subCloser: SubCloser | null = null
|
|
||||||
|
|
||||||
if (authState.loggedIn && usersAppData) {
|
if (authState.loggedIn && usersAppData) {
|
||||||
const pubkey = authState.usersPubkey || authState.keyPair?.public
|
const pubkey = authState.usersPubkey || authState.keyPair?.public
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey && !hasSubscribed.current) {
|
||||||
subscribeForSigits(pubkey).then((res) => {
|
// Call `subscribeForSigits` only if it hasn't been called before
|
||||||
subCloser = res || null
|
subscribeForSigits(pubkey)
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
// Mark `subscribeForSigits` as called
|
||||||
if (subCloser) {
|
hasSubscribed.current = true
|
||||||
subCloser.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [authState, usersAppData])
|
}, [authState, usersAppData])
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
export * from './crypto'
|
export * from './crypto'
|
||||||
export * from './hash'
|
export * from './hash'
|
||||||
export * from './localStorage'
|
export * from './localStorage'
|
||||||
export * from './misc'
|
|
||||||
export * from './nostr'
|
|
||||||
export * from './string'
|
|
||||||
export * from './zip'
|
|
||||||
export * from './utils'
|
|
||||||
export * from './mark'
|
export * from './mark'
|
||||||
export * from './meta'
|
export * from './meta'
|
||||||
|
export * from './misc'
|
||||||
|
export * from './nostr'
|
||||||
|
export * from './relays'
|
||||||
|
export * from './string'
|
||||||
|
export * from './url'
|
||||||
|
export * from './utils'
|
||||||
|
export * from './zip'
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
Event,
|
Event,
|
||||||
EventTemplate,
|
EventTemplate,
|
||||||
Filter,
|
Filter,
|
||||||
SimplePool,
|
|
||||||
UnsignedEvent,
|
UnsignedEvent,
|
||||||
finalizeEvent,
|
finalizeEvent,
|
||||||
generateSecretKey,
|
generateSecretKey,
|
||||||
@ -18,7 +17,11 @@ import {
|
|||||||
} from 'nostr-tools'
|
} from 'nostr-tools'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { NIP05_REGEX } from '../constants'
|
import { NIP05_REGEX } from '../constants'
|
||||||
import { MetadataController, NostrController } from '../controllers'
|
import {
|
||||||
|
MetadataController,
|
||||||
|
NostrController,
|
||||||
|
relayController
|
||||||
|
} from '../controllers'
|
||||||
import {
|
import {
|
||||||
updateProcessedGiftWraps,
|
updateProcessedGiftWraps,
|
||||||
updateUserAppData as updateUserAppDataAction
|
updateUserAppData as updateUserAppDataAction
|
||||||
@ -328,20 +331,27 @@ export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches user application data based on user's public key and stored metadata.
|
||||||
|
*
|
||||||
|
* @returns The user application data or null if an error occurs or no data is found.
|
||||||
|
*/
|
||||||
export const getUsersAppData = async (): Promise<UserAppData | null> => {
|
export const getUsersAppData = async (): Promise<UserAppData | null> => {
|
||||||
|
// Initialize an array to hold relay URLs
|
||||||
const relays: string[] = []
|
const relays: string[] = []
|
||||||
|
|
||||||
|
// Retrieve the user's public key and relay map from the Redux store
|
||||||
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
|
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
|
||||||
const relayMap = store.getState().relays?.map
|
const relayMap = store.getState().relays?.map
|
||||||
|
|
||||||
const nostrController = NostrController.getInstance()
|
// Check if relayMap is undefined in the Redux store
|
||||||
|
|
||||||
// check if relaysMap in redux store is undefined
|
|
||||||
if (!relayMap) {
|
if (!relayMap) {
|
||||||
|
// If relayMap is not present, fetch relay list metadata
|
||||||
const metadataController = new MetadataController()
|
const metadataController = new MetadataController()
|
||||||
const relaySet = await metadataController
|
const relaySet = await metadataController
|
||||||
.findRelayListMetadata(usersPubkey)
|
.findRelayListMetadata(usersPubkey)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
// Log error and return null if fetching metadata fails
|
||||||
console.log(
|
console.log(
|
||||||
`An error occurred while finding relay list metadata for ${hexToNpub(usersPubkey)}`,
|
`An error occurred while finding relay list metadata for ${hexToNpub(usersPubkey)}`,
|
||||||
err
|
err
|
||||||
@ -349,41 +359,42 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
// Return if metadata retrieval failed
|
// Return null if metadata retrieval failed
|
||||||
if (!relaySet) return null
|
if (!relaySet) return null
|
||||||
|
|
||||||
// Ensure relay list is not empty
|
// Ensure that the relay list is not empty
|
||||||
if (relaySet.write.length === 0) return null
|
if (relaySet.write.length === 0) return null
|
||||||
|
|
||||||
|
// Add write relays to the relays array
|
||||||
relays.push(...relaySet.write)
|
relays.push(...relaySet.write)
|
||||||
} else {
|
} else {
|
||||||
// filter write relays from user's relayMap stored in redux store
|
// If relayMap exists, filter and add write relays from the stored map
|
||||||
const writeRelays = Object.keys(relayMap).filter(
|
const writeRelays = Object.keys(relayMap).filter(
|
||||||
(key) => relayMap[key].write
|
(key) => relayMap[key].write
|
||||||
)
|
)
|
||||||
|
|
||||||
relays.push(...writeRelays)
|
relays.push(...writeRelays)
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate an identifier for user's nip78
|
// Generate an identifier for the user's nip78
|
||||||
const hash = await getHash('938' + usersPubkey)
|
const hash = await getHash('938' + usersPubkey)
|
||||||
if (!hash) return null
|
if (!hash) return null
|
||||||
|
|
||||||
|
// Define a filter for fetching events
|
||||||
const filter: Filter = {
|
const filter: Filter = {
|
||||||
kinds: [kinds.Application],
|
kinds: [kinds.Application],
|
||||||
'#d': [hash]
|
'#d': [hash]
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptedContent = await nostrController
|
const encryptedContent = await relayController
|
||||||
.getEvent(filter, relays)
|
.fetchEvent(filter, relays)
|
||||||
.then((event) => {
|
.then((event) => {
|
||||||
if (event) return event.content
|
if (event) return event.content
|
||||||
|
|
||||||
// if person is using sigit for first time its possible that event is null
|
// If no event is found, return an empty stringified object
|
||||||
// so we'll return empty stringified object
|
|
||||||
return '{}'
|
return '{}'
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
// Log error and show a toast notification if fetching event fails
|
||||||
console.log(`An error occurred in finding kind 30078 event`, err)
|
console.log(`An error occurred in finding kind 30078 event`, err)
|
||||||
toast.error(
|
toast.error(
|
||||||
'An error occurred in finding kind 30078 event for data storage'
|
'An error occurred in finding kind 30078 event for data storage'
|
||||||
@ -391,8 +402,10 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Return null if encrypted content retrieval fails
|
||||||
if (!encryptedContent) return null
|
if (!encryptedContent) return null
|
||||||
|
|
||||||
|
// Handle case where the encrypted content is an empty object
|
||||||
if (encryptedContent === '{}') {
|
if (encryptedContent === '{}') {
|
||||||
const secret = generateSecretKey()
|
const secret = generateSecretKey()
|
||||||
const pubKey = getPublicKey(secret)
|
const pubKey = getPublicKey(secret)
|
||||||
@ -408,20 +421,28 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get an instance of the NostrController
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
|
// Decrypt the encrypted content
|
||||||
const decrypted = await nostrController
|
const decrypted = await nostrController
|
||||||
.nip04Decrypt(usersPubkey, encryptedContent)
|
.nip04Decrypt(usersPubkey, encryptedContent)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
// Log error and show a toast notification if decryption fails
|
||||||
console.log('An error occurred while decrypting app data', err)
|
console.log('An error occurred while decrypting app data', err)
|
||||||
toast.error('An error occurred while decrypting app data')
|
toast.error('An error occurred while decrypting app data')
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Return null if decryption fails
|
||||||
if (!decrypted) return null
|
if (!decrypted) return null
|
||||||
|
|
||||||
|
// Parse the decrypted content
|
||||||
const parsedContent = await parseJson<{
|
const parsedContent = await parseJson<{
|
||||||
blossomUrls: string[]
|
blossomUrls: string[]
|
||||||
keyPair: Keys
|
keyPair: Keys
|
||||||
}>(decrypted).catch((err) => {
|
}>(decrypted).catch((err) => {
|
||||||
|
// Log error and show a toast notification if parsing fails
|
||||||
console.log(
|
console.log(
|
||||||
'An error occurred in parsing the content of kind 30078 event',
|
'An error occurred in parsing the content of kind 30078 event',
|
||||||
err
|
err
|
||||||
@ -430,21 +451,26 @@ export const getUsersAppData = async (): Promise<UserAppData | null> => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Return null if parsing fails
|
||||||
if (!parsedContent) return null
|
if (!parsedContent) return null
|
||||||
|
|
||||||
const { blossomUrls, keyPair } = parsedContent
|
const { blossomUrls, keyPair } = parsedContent
|
||||||
|
|
||||||
|
// Return null if no blossom URLs are found
|
||||||
if (blossomUrls.length === 0) return null
|
if (blossomUrls.length === 0) return null
|
||||||
|
|
||||||
|
// Fetch additional user app data from the first blossom URL
|
||||||
const dataFromBlossom = await getUserAppDataFromBlossom(
|
const dataFromBlossom = await getUserAppDataFromBlossom(
|
||||||
blossomUrls[0],
|
blossomUrls[0],
|
||||||
keyPair.private
|
keyPair.private
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Return null if fetching data from blossom fails
|
||||||
if (!dataFromBlossom) return null
|
if (!dataFromBlossom) return null
|
||||||
|
|
||||||
const { sigits, processedGiftWraps } = dataFromBlossom
|
const { sigits, processedGiftWraps } = dataFromBlossom
|
||||||
|
|
||||||
|
// Return the final user application data
|
||||||
return {
|
return {
|
||||||
blossomUrls,
|
blossomUrls,
|
||||||
keyPair,
|
keyPair,
|
||||||
@ -575,9 +601,8 @@ export const updateUsersAppData = async (meta: Meta) => {
|
|||||||
const relayMap = (store.getState().relays as RelaysState).map!
|
const relayMap = (store.getState().relays as RelaysState).map!
|
||||||
const writeRelays = Object.keys(relayMap).filter((key) => relayMap[key].write)
|
const writeRelays = Object.keys(relayMap).filter((key) => relayMap[key].write)
|
||||||
|
|
||||||
console.log(`publishing event kind: ${kinds.Application}`)
|
|
||||||
const publishResult = await Promise.race([
|
const publishResult = await Promise.race([
|
||||||
nostrController.publishEvent(signedEvent, writeRelays),
|
relayController.publish(signedEvent, writeRelays),
|
||||||
timeout(1000 * 30)
|
timeout(1000 * 30)
|
||||||
]).catch((err) => {
|
]).catch((err) => {
|
||||||
console.log('err :>> ', err)
|
console.log('err :>> ', err)
|
||||||
@ -817,15 +842,8 @@ export const subscribeForSigits = async (pubkey: string) => {
|
|||||||
'#p': [pubkey]
|
'#p': [pubkey]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instantiate a new SimplePool for the subscription
|
relayController.subscribeForEvents(filter, relaySet.read, (event) => {
|
||||||
const pool = new SimplePool()
|
processReceivedEvent(event) // Process the received event
|
||||||
|
|
||||||
// Subscribe to the specified relays with the defined filter
|
|
||||||
return pool.subscribeMany(relaySet.read, [filter], {
|
|
||||||
// Define a callback function to handle received events
|
|
||||||
onevent: (event) => {
|
|
||||||
processReceivedEvent(event) // Process the received event
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -915,13 +933,9 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
|
|||||||
// Ensure relay list is not empty
|
// Ensure relay list is not empty
|
||||||
if (relaySet.read.length === 0) return
|
if (relaySet.read.length === 0) return
|
||||||
|
|
||||||
console.log('Publishing notifications')
|
|
||||||
// Publish the notification event to the recipient's read relays
|
// Publish the notification event to the recipient's read relays
|
||||||
const nostrController = NostrController.getInstance()
|
|
||||||
|
|
||||||
// Attempt to publish the event to the relays, with a timeout of 2 minutes
|
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
nostrController.publishEvent(wrappedEvent, relaySet.read),
|
relayController.publish(wrappedEvent, relaySet.read),
|
||||||
timeout(1000 * 30)
|
timeout(1000 * 30)
|
||||||
]).catch((err) => {
|
]).catch((err) => {
|
||||||
// Log an error if publishing the notification event fails
|
// Log an error if publishing the notification event fails
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { Filter, SimplePool } from 'nostr-tools'
|
import axios from 'axios'
|
||||||
|
import { Event, Filter } from 'nostr-tools'
|
||||||
import { RelayList } from 'nostr-tools/kinds'
|
import { RelayList } from 'nostr-tools/kinds'
|
||||||
import { Event } from 'nostr-tools'
|
import { relayController } from '../controllers/RelayController.ts'
|
||||||
import { localCache } from '../services'
|
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'
|
import { ONE_WEEK_IN_MS, SIGIT_RELAY } from './const.ts'
|
||||||
import { RelayMap, RelaySet } from '../types'
|
|
||||||
|
|
||||||
const READ_MARKER = 'read'
|
const READ_MARKER = 'read'
|
||||||
const WRITE_MARKER = 'write'
|
const WRITE_MARKER = 'write'
|
||||||
@ -24,8 +27,8 @@ const findRelayListAndUpdateCache = async (
|
|||||||
kinds: [RelayList],
|
kinds: [RelayList],
|
||||||
authors: [hexKey]
|
authors: [hexKey]
|
||||||
}
|
}
|
||||||
const pool = new SimplePool()
|
|
||||||
const event = await pool.get(lookUpRelays, eventFilter)
|
const event = await relayController.fetchEvent(eventFilter, lookUpRelays)
|
||||||
if (event) {
|
if (event) {
|
||||||
await localCache.addUserRelayListMetadata(event)
|
await localCache.addUserRelayListMetadata(event)
|
||||||
}
|
}
|
||||||
@ -106,11 +109,57 @@ const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => {
|
|||||||
return obj
|
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<string[]> => {
|
||||||
|
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<RelayStats>(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 {
|
export {
|
||||||
findRelayListAndUpdateCache,
|
findRelayListAndUpdateCache,
|
||||||
findRelayListInCache,
|
findRelayListInCache,
|
||||||
getUserRelaySet,
|
|
||||||
getDefaultRelaySet,
|
|
||||||
getDefaultRelayMap,
|
getDefaultRelayMap,
|
||||||
|
getDefaultRelaySet,
|
||||||
|
getMostPopularRelays,
|
||||||
|
getUserRelaySet,
|
||||||
isOlderThanOneWeek
|
isOlderThanOneWeek
|
||||||
}
|
}
|
||||||
|
47
src/utils/url.ts
Normal file
47
src/utils/url.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Normalizes a given URL by performing the following operations:
|
||||||
|
*
|
||||||
|
* 1. Ensures that the URL has a protocol by defaulting to 'wss://' if no protocol is provided.
|
||||||
|
* 2. Creates a `URL` object to easily manipulate and normalize the URL components.
|
||||||
|
* 3. Normalizes the pathname by:
|
||||||
|
* - Replacing multiple consecutive slashes with a single slash.
|
||||||
|
* - Removing the trailing slash if it exists.
|
||||||
|
* 4. Removes the port number if it is the default port for the protocol:
|
||||||
|
* - Port `80` for 'ws:' (WebSocket) protocol.
|
||||||
|
* - Port `443` for 'wss:' (WebSocket Secure) protocol.
|
||||||
|
* 5. Sorts the query parameters alphabetically.
|
||||||
|
* 6. Clears any fragment (hash) identifier from the URL.
|
||||||
|
*
|
||||||
|
* @param urlString - The URL string to be normalized.
|
||||||
|
* @returns A normalized URL string.
|
||||||
|
*/
|
||||||
|
export function normalizeWebSocketURL(urlString: string): string {
|
||||||
|
// If the URL string does not contain a protocol (e.g., "http://", "https://"),
|
||||||
|
// prepend "wss://" (WebSocket Secure) by default.
|
||||||
|
if (urlString.indexOf('://') === -1) urlString = 'wss://' + urlString
|
||||||
|
|
||||||
|
// Create a URL object from the provided URL string.
|
||||||
|
const url = new URL(urlString)
|
||||||
|
|
||||||
|
// Normalize the pathname by replacing multiple consecutive slashes with a single slash.
|
||||||
|
url.pathname = url.pathname.replace(/\/+/g, '/')
|
||||||
|
|
||||||
|
// Remove the trailing slash from the pathname if it exists.
|
||||||
|
if (url.pathname.endsWith('/')) url.pathname = url.pathname.slice(0, -1)
|
||||||
|
|
||||||
|
// Remove the port number if it is 80 for "ws:" protocol or 443 for "wss:" protocol, as these are default ports.
|
||||||
|
if (
|
||||||
|
(url.port === '80' && url.protocol === 'ws:') ||
|
||||||
|
(url.port === '443' && url.protocol === 'wss:')
|
||||||
|
)
|
||||||
|
url.port = ''
|
||||||
|
|
||||||
|
// Sort the search parameters alphabetically.
|
||||||
|
url.searchParams.sort()
|
||||||
|
|
||||||
|
// Clear any hash fragment from the URL.
|
||||||
|
url.hash = ''
|
||||||
|
|
||||||
|
// Return the normalized URL as a string.
|
||||||
|
return url.toString()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user