feat(Relays): added logic to manage relays #63
@ -74,24 +74,15 @@ export class AuthController {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const relaysState = store.getState().relays
|
|
||||||
|
|
||||||
if (relaysState) {
|
|
||||||
// Relays state is defined and there is no need to await for the latest relay map
|
|
||||||
this.nostrController.getRelayMap(pubkey).then((relayMap) => {
|
|
||||||
if (!compareObjects(relaysState?.map, relayMap)) {
|
|
||||||
store.dispatch(setRelayMapAction(relayMap.map))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Relays state is not defined, await for the latest relay map
|
|
||||||
const relayMap = await this.nostrController.getRelayMap(pubkey)
|
const relayMap = await this.nostrController.getRelayMap(pubkey)
|
||||||
|
|
||||||
if (Object.keys(relayMap).length < 1) {
|
if (Object.keys(relayMap).length < 1) {
|
||||||
// Navigate user to relays page
|
// Navigate user to relays page if relay map is empty
|
||||||
return Promise.resolve(appPrivateRoutes.relays)
|
return Promise.resolve(appPrivateRoutes.relays)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (store.getState().auth?.loggedIn) {
|
||||||
|
if (!compareObjects(store.getState().relays?.map, relayMap.map))
|
||||||
store.dispatch(setRelayMapAction(relayMap.map))
|
store.dispatch(setRelayMapAction(relayMap.map))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@ import NDK, {
|
|||||||
NDKNip46Signer,
|
NDKNip46Signer,
|
||||||
NDKPrivateKeySigner,
|
NDKPrivateKeySigner,
|
||||||
NDKUser,
|
NDKUser,
|
||||||
NostrEvent
|
NostrEvent,
|
||||||
|
NDKSubscription
|
||||||
} from '@nostr-dev-kit/ndk'
|
} from '@nostr-dev-kit/ndk'
|
||||||
import {
|
import {
|
||||||
Event,
|
Event,
|
||||||
@ -11,25 +12,45 @@ import {
|
|||||||
SimplePool,
|
SimplePool,
|
||||||
UnsignedEvent,
|
UnsignedEvent,
|
||||||
Filter,
|
Filter,
|
||||||
|
Relay,
|
||||||
finalizeEvent,
|
finalizeEvent,
|
||||||
nip04,
|
nip04,
|
||||||
nip19,
|
nip19,
|
||||||
kinds
|
kinds
|
||||||
} from 'nostr-tools'
|
} from 'nostr-tools'
|
||||||
import { EventEmitter } from 'tseep'
|
import { EventEmitter } from 'tseep'
|
||||||
import { updateNsecbunkerPubkey } from '../store/actions'
|
import {
|
||||||
|
updateNsecbunkerPubkey,
|
||||||
|
setMostPopularRelaysAction,
|
||||||
|
setRelayInfoAction,
|
||||||
|
setRelayConnectionStatusAction
|
||||||
|
} from '../store/actions'
|
||||||
import { AuthState, LoginMethods } from '../store/auth/types'
|
import { AuthState, LoginMethods } from '../store/auth/types'
|
||||||
import store from '../store/store'
|
import store from '../store/store'
|
||||||
import { SignedEvent, RelayMap } from '../types'
|
import {
|
||||||
import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils'
|
SignedEvent,
|
||||||
|
RelayMap,
|
||||||
|
RelayStats,
|
||||||
|
ReadRelay,
|
||||||
|
RelayInfoObject,
|
||||||
|
RelayConnectionStatus,
|
||||||
|
RelayConnectionState
|
||||||
|
} from '../types'
|
||||||
|
import {
|
||||||
|
compareObjects,
|
||||||
|
getNsecBunkerDelegatedKey,
|
||||||
|
verifySignedEvent
|
||||||
|
} from '../utils'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
export class NostrController extends EventEmitter {
|
export class NostrController extends EventEmitter {
|
||||||
private static instance: NostrController
|
private static instance: NostrController
|
||||||
private specialMetadataRelay = 'wss://purplepag.es'
|
|
||||||
|
|
||||||
private bunkerNDK: NDK | undefined
|
private bunkerNDK: NDK | undefined
|
||||||
private remoteSigner: NDKNip46Signer | undefined
|
private remoteSigner: NDKNip46Signer | undefined
|
||||||
|
|
||||||
|
private connectedRelays: Relay[] | undefined
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@ -390,12 +411,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 = import.meta.env.VITE_MOST_POPULAR_RELAYS
|
const mostPopularRelays = await this.getMostPopularRelays()
|
||||||
const hardcodedPopularRelays = (mostPopularRelays || '').split(' ')
|
|
||||||
const popularRelayURIs = [
|
|
||||||
this.specialMetadataRelay,
|
|
||||||
...hardcodedPopularRelays
|
|
||||||
]
|
|
||||||
|
|
||||||
const pool = new SimplePool()
|
const pool = new SimplePool()
|
||||||
|
|
||||||
@ -405,7 +421,9 @@ export class NostrController extends EventEmitter {
|
|||||||
authors: [npub]
|
authors: [npub]
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = await pool.get(popularRelayURIs, eventFilter).catch((err) => {
|
const event = await pool
|
||||||
|
.get(mostPopularRelays, eventFilter)
|
||||||
|
.catch((err) => {
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -427,6 +445,10 @@ export class NostrController extends EventEmitter {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.getRelayInfo(Object.keys(relaysMap))
|
||||||
|
|
||||||
|
this.connectToRelays(Object.keys(relaysMap))
|
||||||
|
|
||||||
return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at })
|
return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at })
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject('User relays were not found.')
|
return Promise.reject('User relays were not found.')
|
||||||
@ -437,11 +459,13 @@ export class NostrController extends EventEmitter {
|
|||||||
* Publishes relay map.
|
* Publishes relay map.
|
||||||
* @param relayMap - relay map.
|
* @param relayMap - relay map.
|
||||||
* @param npub - user's npub.
|
* @param npub - user's npub.
|
||||||
|
* @param extraRelaysToPublish - optional relays to publish relay map.
|
||||||
* @returns - promise that resolves into a string representing publishing result.
|
* @returns - promise that resolves into a string representing publishing result.
|
||||||
*/
|
*/
|
||||||
publishRelayMap = async (
|
publishRelayMap = async (
|
||||||
relayMap: RelayMap,
|
relayMap: RelayMap,
|
||||||
npub: string
|
npub: string,
|
||||||
|
extraRelaysToPublish?: string[]
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const timestamp = Math.floor(Date.now() / 1000)
|
const timestamp = Math.floor(Date.now() / 1000)
|
||||||
const relayURIs = Object.keys(relayMap)
|
const relayURIs = Object.keys(relayMap)
|
||||||
@ -471,18 +495,266 @@ export class NostrController extends EventEmitter {
|
|||||||
|
|
||||||
let relaysToPublish = relayURIs
|
let relaysToPublish = relayURIs
|
||||||
|
|
||||||
|
// Add extra relays if provided
|
||||||
|
if (extraRelaysToPublish) {
|
||||||
|
relaysToPublish = [...relaysToPublish, ...extraRelaysToPublish]
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
const mostPopularRelays = import.meta.env.VITE_MOST_POPULAR_RELAYS
|
relaysToPublish = await this.getMostPopularRelays()
|
||||||
const hardcodedPopularRelays = (mostPopularRelays || '').split(' ')
|
|
||||||
|
|
||||||
relaysToPublish = [this.specialMetadataRelay, ...hardcodedPopularRelays]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.publishEvent(signedEvent, relaysToPublish)
|
const publishResult = await this.publishEvent(signedEvent, relaysToPublish)
|
||||||
|
|
||||||
|
if (publishResult && publishResult.length) {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
`Relay Map published on: ${relaysToPublish.join('\n')}`
|
`Relay Map published on: ${publishResult.join('\n')}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: ReadRelay) => 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.
|
||||||
|
* @param relayURIs - relay URIs to get information about
|
||||||
|
*/
|
||||||
|
getRelayInfo = async (relayURIs: string[]) => {
|
||||||
|
// initialize job request
|
||||||
|
const jobEventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: Math.round(Date.now() / 1000),
|
||||||
|
kind: 68001,
|
||||||
|
tags: [
|
||||||
|
['i', `${JSON.stringify(relayURIs)}`],
|
||||||
|
['j', 'relay-info']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign job request event
|
||||||
|
const jobSignedEvent = await this.signEvent(jobEventTemplate)
|
||||||
|
|
||||||
|
const relays = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://relay.primal.net',
|
||||||
|
'wss://relayable.org'
|
||||||
|
]
|
||||||
|
|
||||||
|
// publish job request
|
||||||
|
await this.publishEvent(jobSignedEvent, relays)
|
||||||
|
|
||||||
|
console.log('jobSignedEvent :>> ', jobSignedEvent)
|
||||||
|
|
||||||
|
const subscribeWithTimeout = (
|
||||||
|
subscription: NDKSubscription,
|
||||||
|
timeoutMs: number
|
||||||
|
): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const eventHandler = (event: NDKEvent) => {
|
||||||
|
subscription.stop()
|
||||||
|
resolve(event.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.on('event', eventHandler)
|
||||||
|
|
||||||
|
// Set up a timeout to stop the subscription after a specified time
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
subscription.stop() // Stop the subscription
|
||||||
|
reject(new Error('Subscription timed out')) // Reject the promise with a timeout error
|
||||||
|
}, timeoutMs)
|
||||||
|
|
||||||
|
// Handle subscription close event
|
||||||
|
subscription.on('close', () => clearTimeout(timeout))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const dvmNDK = new NDK({
|
||||||
|
explicitRelayUrls: relays
|
||||||
|
})
|
||||||
|
|
||||||
|
await dvmNDK.connect(2000)
|
||||||
|
|
||||||
|
// filter for getting DVM job's result
|
||||||
|
const sub = dvmNDK.subscribe({
|
||||||
|
kinds: [68002 as number],
|
||||||
|
'#e': [jobSignedEvent.id],
|
||||||
|
'#p': [jobSignedEvent.pubkey]
|
||||||
|
})
|
||||||
|
|
||||||
|
// asynchronously get block number from dvm job with 20 seconds timeout
|
||||||
|
const dvmJobResult = await subscribeWithTimeout(sub, 20000)
|
||||||
|
|
||||||
|
if (!dvmJobResult) {
|
||||||
|
return Promise.reject(`Relay(s) information wasn't received`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let relaysInfo: RelayInfoObject
|
||||||
|
|
||||||
|
try {
|
||||||
|
relaysInfo = JSON.parse(dvmJobResult)
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(`Invalid relay(s) information.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
relaysInfo &&
|
||||||
|
!compareObjects(store.getState().relays?.info, relaysInfo)
|
||||||
|
) {
|
||||||
|
store.dispatch(setRelayInfoAction(relaysInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes connection to relays.
|
||||||
|
* @param relayURIs - an array of relay URIs
|
||||||
|
* @returns - promise that resolves into an array of connections
|
||||||
|
*/
|
||||||
|
connectToRelays = async (relayURIs: string[]) => {
|
||||||
|
// Copy of relay connection status
|
||||||
|
const relayConnectionsStatus: RelayConnectionStatus = JSON.parse(
|
||||||
|
JSON.stringify(store.getState().relays?.connectionStatus || {})
|
||||||
|
)
|
||||||
|
|
||||||
|
const connectedRelayURLs = this.connectedRelays
|
||||||
|
? this.connectedRelays.map((relay) => relay.url)
|
||||||
|
: []
|
||||||
|
|
||||||
|
// Check if connections already established
|
||||||
|
if (compareObjects(connectedRelayURLs, relayURIs)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const connections = relayURIs
|
||||||
|
.filter((relayURI) => !connectedRelayURLs.includes(relayURI))
|
||||||
|
.map((relayURI) =>
|
||||||
|
Relay.connect(relayURI)
|
||||||
|
.then((relay) => {
|
||||||
|
// put connection status into relayConnectionsStatus object
|
||||||
|
relayConnectionsStatus[relayURI] = relay.connected
|
||||||
|
? RelayConnectionState.Connected
|
||||||
|
: RelayConnectionState.NotConnected
|
||||||
|
|
||||||
|
return relay
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
relayConnectionsStatus[relayURI] = RelayConnectionState.NotConnected
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const connected = await Promise.all(connections)
|
||||||
|
|
||||||
|
// put connected relays into connectedRelays private property, so it can be closed later
|
||||||
|
this.connectedRelays = connected.filter(
|
||||||
|
(relay) => relay instanceof Relay && relay.connected
|
||||||
|
) as Relay[]
|
||||||
|
|
||||||
|
if (Object.keys(relayConnectionsStatus)) {
|
||||||
|
if (
|
||||||
|
!compareObjects(
|
||||||
|
store.getState().relays?.connectionStatus,
|
||||||
|
relayConnectionsStatus
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
store.dispatch(setRelayConnectionStatusAction(relayConnectionsStatus))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(relayConnectionsStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnects from relays.
|
||||||
|
* @param relayURIs - array of relay URIs to disconnect from
|
||||||
|
*/
|
||||||
|
disconnectFromRelays = async (relayURIs: string[]) => {
|
||||||
|
const connectedRelayURLs = this.connectedRelays
|
||||||
|
? this.connectedRelays.map((relay) => relay.url)
|
||||||
|
: []
|
||||||
|
|
||||||
|
relayURIs
|
||||||
|
.filter((relayURI) => connectedRelayURLs.includes(relayURI))
|
||||||
|
.forEach((relayURI) => {
|
||||||
|
if (this.connectedRelays) {
|
||||||
|
const relay = this.connectedRelays.find(
|
||||||
|
(relay) => relay.url === relayURI
|
||||||
|
)
|
||||||
|
|
||||||
|
if (relay) {
|
||||||
|
// close relay connection
|
||||||
|
relay.close()
|
||||||
|
|
||||||
|
// remove relay from connectedRelays property
|
||||||
|
this.connectedRelays = this.connectedRelays.filter(
|
||||||
|
(relay) => relay.url !== relayURI
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (store.getState().relays?.connectionStatus) {
|
||||||
|
const connectionStatus = JSON.parse(
|
||||||
|
JSON.stringify(store.getState().relays?.connectionStatus)
|
||||||
|
)
|
||||||
|
|
||||||
|
relayURIs.forEach((relay) => {
|
||||||
|
delete connectionStatus[relay]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
!compareObjects(
|
||||||
|
store.getState().relays?.connectionStatus,
|
||||||
|
connectionStatus
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Update app state
|
||||||
|
store.dispatch(setRelayConnectionStatusAction(connectionStatus))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user