feat: implemented relay controller and use that for fetching and publishing events #149
@ -1,22 +1,23 @@
|
|||||||
import { EventTemplate } from 'nostr-tools'
|
import { EventTemplate } from 'nostr-tools'
|
||||||
import { MetadataController, NostrController } from '.'
|
import { MetadataController, NostrController } from '.'
|
||||||
|
import { appPrivateRoutes } from '../routes'
|
||||||
import {
|
import {
|
||||||
setAuthState,
|
setAuthState,
|
||||||
setMetadataEvent,
|
setMetadataEvent,
|
||||||
setRelayMapAction
|
setRelayMapAction
|
||||||
} from '../store/actions'
|
} from '../store/actions'
|
||||||
import store from '../store/store'
|
import store from '../store/store'
|
||||||
|
import { SignedEvent } from '../types'
|
||||||
import {
|
import {
|
||||||
base64DecodeAuthToken,
|
base64DecodeAuthToken,
|
||||||
base64EncodeSignedEvent,
|
base64EncodeSignedEvent,
|
||||||
|
compareObjects,
|
||||||
getAuthToken,
|
getAuthToken,
|
||||||
|
getRelayMap,
|
||||||
getVisitedLink,
|
getVisitedLink,
|
||||||
saveAuthToken,
|
saveAuthToken,
|
||||||
compareObjects,
|
|
||||||
unixNow
|
unixNow
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { appPrivateRoutes } from '../routes'
|
|
||||||
import { SignedEvent } from '../types'
|
|
||||||
|
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
private nostrController: NostrController
|
private nostrController: NostrController
|
||||||
@ -75,7 +76,7 @@ export class AuthController {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const relayMap = await this.nostrController.getRelayMap(pubkey)
|
const relayMap = await getRelayMap(pubkey)
|
||||||
|
|
||||||
if (Object.keys(relayMap).length < 1) {
|
if (Object.keys(relayMap).length < 1) {
|
||||||
// Navigate user to relays page if relay map is empty
|
// Navigate user to relays page if relay map is empty
|
||||||
|
@ -2,47 +2,24 @@ import NDK, {
|
|||||||
NDKEvent,
|
NDKEvent,
|
||||||
NDKNip46Signer,
|
NDKNip46Signer,
|
||||||
NDKPrivateKeySigner,
|
NDKPrivateKeySigner,
|
||||||
NDKSubscription,
|
|
||||||
NDKUser,
|
NDKUser,
|
||||||
NostrEvent
|
NostrEvent
|
||||||
} from '@nostr-dev-kit/ndk'
|
} from '@nostr-dev-kit/ndk'
|
||||||
import {
|
import {
|
||||||
Event,
|
Event,
|
||||||
EventTemplate,
|
EventTemplate,
|
||||||
Filter,
|
|
||||||
Relay,
|
|
||||||
SimplePool,
|
|
||||||
UnsignedEvent,
|
UnsignedEvent,
|
||||||
finalizeEvent,
|
finalizeEvent,
|
||||||
kinds,
|
|
||||||
nip04,
|
nip04,
|
||||||
nip19,
|
nip19,
|
||||||
nip44
|
nip44
|
||||||
} from 'nostr-tools'
|
} from 'nostr-tools'
|
||||||
import { EventEmitter } from 'tseep'
|
import { EventEmitter } from 'tseep'
|
||||||
import {
|
import { updateNsecbunkerPubkey } from '../store/actions'
|
||||||
setRelayConnectionStatusAction,
|
|
||||||
setRelayInfoAction,
|
|
||||||
updateNsecbunkerPubkey
|
|
||||||
} 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 {
|
import { SignedEvent } from '../types'
|
||||||
RelayConnectionState,
|
import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils'
|
||||||
RelayConnectionStatus,
|
|
||||||
RelayInfoObject,
|
|
||||||
RelayMap,
|
|
||||||
SignedEvent
|
|
||||||
} from '../types'
|
|
||||||
import {
|
|
||||||
compareObjects,
|
|
||||||
getDefaultRelayMap,
|
|
||||||
getMostPopularRelays,
|
|
||||||
getNsecBunkerDelegatedKey,
|
|
||||||
unixNow,
|
|
||||||
verifySignedEvent
|
|
||||||
} from '../utils'
|
|
||||||
import { relayController } from './'
|
|
||||||
|
|
||||||
export class NostrController extends EventEmitter {
|
export class NostrController extends EventEmitter {
|
||||||
private static instance: NostrController
|
private static instance: NostrController
|
||||||
@ -50,14 +27,13 @@ export class NostrController extends EventEmitter {
|
|||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNostrObject = () => {
|
private getNostrObject = () => {
|
||||||
// fix: this is not picking up type declaration from src/system/index.d.ts
|
// fix: this is not picking up type declaration from src/system/index.d.ts
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
if (window.nostr) return window.nostr as any
|
if (window.nostr) return window.nostr as any
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -555,317 +531,4 @@ export class NostrController extends EventEmitter {
|
|||||||
generateDelegatedKey = (): string => {
|
generateDelegatedKey = (): string => {
|
||||||
return NDKPrivateKeySigner.generate().privateKey!
|
return NDKPrivateKeySigner.generate().privateKey!
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides relay map.
|
|
||||||
* @param npub - user's npub
|
|
||||||
* @returns - promise that resolves into relay map and a timestamp when it has been updated.
|
|
||||||
*/
|
|
||||||
getRelayMap = async (
|
|
||||||
npub: string
|
|
||||||
): Promise<{ map: RelayMap; mapUpdated?: number }> => {
|
|
||||||
const mostPopularRelays = await getMostPopularRelays()
|
|
||||||
|
|
||||||
const pool = new SimplePool()
|
|
||||||
|
|
||||||
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
|
|
||||||
const eventFilter: Filter = {
|
|
||||||
kinds: [kinds.RelayList],
|
|
||||||
authors: [npub]
|
|
||||||
}
|
|
||||||
|
|
||||||
const event = await pool
|
|
||||||
.get(mostPopularRelays, eventFilter)
|
|
||||||
.catch((err) => {
|
|
||||||
return Promise.reject(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
// Handle founded 10002 event
|
|
||||||
const relaysMap: RelayMap = {}
|
|
||||||
|
|
||||||
// 'r' stands for 'relay'
|
|
||||||
const relayTags = event.tags.filter((tag) => tag[0] === 'r')
|
|
||||||
|
|
||||||
relayTags.forEach((tag) => {
|
|
||||||
const uri = tag[1]
|
|
||||||
const relayType = tag[2]
|
|
||||||
|
|
||||||
// if 3rd element of relay tag is undefined, relay is WRITE and READ
|
|
||||||
relaysMap[uri] = {
|
|
||||||
write: relayType ? relayType === 'write' : true,
|
|
||||||
read: relayType ? relayType === 'read' : true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.getRelayInfo(Object.keys(relaysMap))
|
|
||||||
|
|
||||||
this.connectToRelays(Object.keys(relaysMap))
|
|
||||||
|
|
||||||
return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at })
|
|
||||||
} else {
|
|
||||||
return Promise.resolve({ map: getDefaultRelayMap() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publishes relay map.
|
|
||||||
* @param relayMap - relay map.
|
|
||||||
* @param npub - user's npub.
|
|
||||||
* @param extraRelaysToPublish - optional relays to publish relay map.
|
|
||||||
* @returns - promise that resolves into a string representing publishing result.
|
|
||||||
*/
|
|
||||||
publishRelayMap = async (
|
|
||||||
relayMap: RelayMap,
|
|
||||||
npub: string,
|
|
||||||
extraRelaysToPublish?: string[]
|
|
||||||
): Promise<string> => {
|
|
||||||
const timestamp = unixNow()
|
|
||||||
const relayURIs = Object.keys(relayMap)
|
|
||||||
|
|
||||||
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
|
|
||||||
const tags: string[][] = relayURIs.map((relayURI) =>
|
|
||||||
[
|
|
||||||
'r',
|
|
||||||
relayURI,
|
|
||||||
relayMap[relayURI].read && relayMap[relayURI].write
|
|
||||||
? ''
|
|
||||||
: relayMap[relayURI].write
|
|
||||||
? 'write'
|
|
||||||
: 'read'
|
|
||||||
].filter((value) => value !== '')
|
|
||||||
)
|
|
||||||
|
|
||||||
const newRelayMapEvent: UnsignedEvent = {
|
|
||||||
kind: kinds.RelayList,
|
|
||||||
tags,
|
|
||||||
content: '',
|
|
||||||
pubkey: npub,
|
|
||||||
created_at: timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
const signedEvent = await this.signEvent(newRelayMapEvent)
|
|
||||||
|
|
||||||
let relaysToPublish = relayURIs
|
|
||||||
|
|
||||||
// Add extra relays if provided
|
|
||||||
if (extraRelaysToPublish) {
|
|
||||||
relaysToPublish = [...relaysToPublish, ...extraRelaysToPublish]
|
|
||||||
}
|
|
||||||
|
|
||||||
// If relay map is empty, use most popular relay URIs
|
|
||||||
if (!relaysToPublish.length) {
|
|
||||||
relaysToPublish = await getMostPopularRelays()
|
|
||||||
}
|
|
||||||
|
|
||||||
const publishResult = await relayController.publish(
|
|
||||||
signedEvent,
|
|
||||||
relaysToPublish
|
|
||||||
)
|
|
||||||
|
|
||||||
if (publishResult && publishResult.length) {
|
|
||||||
return Promise.resolve(
|
|
||||||
`Relay Map published on: ${publishResult.join('\n')}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject('Publishing updated relay map was unsuccessful.')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: unixNow(),
|
|
||||||
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 relayController.publish(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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Filter, Relay, Event } from 'nostr-tools'
|
import { Event, Filter, Relay } from 'nostr-tools'
|
||||||
import { normalizeWebSocketURL, timeout } from '../utils'
|
import { normalizeWebSocketURL, timeout } from '../utils'
|
||||||
import { SIGIT_RELAY } from '../utils/const'
|
import { SIGIT_RELAY } from '../utils/const'
|
||||||
|
|
||||||
@ -7,7 +7,7 @@ import { SIGIT_RELAY } from '../utils/const'
|
|||||||
*/
|
*/
|
||||||
export class RelayController {
|
export class RelayController {
|
||||||
private static instance: RelayController
|
private static instance: RelayController
|
||||||
public connectedRelays: Relay[] = []
|
public connectedRelays = new Map<string, Relay>()
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
@ -34,34 +34,44 @@ export class RelayController {
|
|||||||
* @param relayUrl - The URL of the relay server to connect to.
|
* @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.
|
* @returns A promise that resolves to the connected relay object if successful, or `null` if the connection fails.
|
||||||
*/
|
*/
|
||||||
public connectRelay = async (relayUrl: string) => {
|
public connectRelay = async (relayUrl: string): Promise<Relay | null> => {
|
||||||
// Check if a relay with the same URL is already connected
|
// Check if a relay with the same URL is already connected
|
||||||
const relay = this.connectedRelays.find(
|
const normalizedWebSocketURL = normalizeWebSocketURL(relayUrl)
|
||||||
(relay) =>
|
const relay = this.connectedRelays.get(normalizedWebSocketURL)
|
||||||
normalizeWebSocketURL(relay.url) === normalizeWebSocketURL(relayUrl)
|
|
||||||
)
|
|
||||||
|
|
||||||
// If a matching relay is found, return it (skip connection)
|
|
||||||
if (relay) {
|
if (relay) {
|
||||||
return relay
|
// If a relay is found in connectedRelay map and is connected, just return it
|
||||||
|
if (relay.connected) return relay
|
||||||
|
|
||||||
|
// If relay is found in connectedRelay map but not connected,
|
||||||
|
// remove it from map and call connectRelay method again
|
||||||
|
this.connectedRelays.delete(relayUrl)
|
||||||
|
|
||||||
|
return this.connectRelay(relayUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Attempt to connect to the relay using the provided URL
|
||||||
// Attempt to connect to the relay using the provided URL
|
const newRelay = await Relay.connect(relayUrl)
|
||||||
const newRelay = await Relay.connect(relayUrl)
|
.then((relay) => {
|
||||||
|
if (relay.connected) {
|
||||||
|
// Add the newly connected relay to the connected relays map
|
||||||
|
this.connectedRelays.set(relayUrl, relay)
|
||||||
|
|
||||||
// Add the newly connected relay to the list of connected relays
|
// Return the newly connected relay
|
||||||
this.connectedRelays.push(newRelay)
|
return relay
|
||||||
|
}
|
||||||
|
|
||||||
// Return the newly connected relay
|
return null
|
||||||
return newRelay
|
})
|
||||||
} catch (err) {
|
.catch((err) => {
|
||||||
// Log an error message if the connection fails
|
// Log an error message if the connection fails
|
||||||
console.error(`Relay connection failed: ${relayUrl}`, err)
|
console.error(`Relay connection failed: ${relayUrl}`, err)
|
||||||
|
|
||||||
// Return null to indicate connection failure
|
// Return null to indicate connection failure
|
||||||
return null
|
return null
|
||||||
}
|
})
|
||||||
|
|
||||||
|
return newRelay
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,6 +119,11 @@ export class RelayController {
|
|||||||
// Create a promise for each relay subscription
|
// Create a promise for each relay subscription
|
||||||
const subPromises = relays.map((relay) => {
|
const subPromises = relays.map((relay) => {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
|
if (!relay.connected) {
|
||||||
|
console.log(`${relay.url} : Not connected!`, 'Skipping subscription')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Subscribe to the relay with the specified filter
|
// Subscribe to the relay with the specified filter
|
||||||
const sub = relay.subscribe([filter], {
|
const sub = relay.subscribe([filter], {
|
||||||
// Handle incoming events
|
// Handle incoming events
|
||||||
@ -274,11 +289,11 @@ export class RelayController {
|
|||||||
try {
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
relay.publish(event), // Publish the event to the relay
|
relay.publish(event), // Publish the event to the relay
|
||||||
timeout(30000) // Set a timeout to handle cases where publishing takes too long
|
timeout(20 * 1000) // 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
|
publishedOnRelays.push(relay.url) // Add the relay URL to the list of successfully published relays
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to publish event on relay: ${relay}`, err)
|
console.error(`Failed to publish event on relay: ${relay.url}`, err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from './store'
|
export * from './store'
|
||||||
|
export * from './useDidMount'
|
||||||
|
12
src/hooks/useDidMount.ts
Normal file
12
src/hooks/useDidMount.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useRef, useEffect } from 'react'
|
||||||
|
|
||||||
|
export const useDidMount = (callback: () => void) => {
|
||||||
|
const didMount = useRef<boolean>(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (callback && !didMount.current) {
|
||||||
|
didMount.current = true
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -12,138 +12,41 @@ import ListItemText from '@mui/material/ListItemText'
|
|||||||
import Switch from '@mui/material/Switch'
|
import Switch from '@mui/material/Switch'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { NostrController } from '../../../controllers'
|
import { Container } from '../../../components/Container'
|
||||||
import { useAppDispatch, useAppSelector } from '../../../hooks'
|
import { relayController } from '../../../controllers'
|
||||||
import {
|
import { useAppDispatch, useAppSelector, useDidMount } from '../../../hooks'
|
||||||
setRelayMapAction,
|
import { setRelayMapAction } from '../../../store/actions'
|
||||||
setRelayMapUpdatedAction
|
import { RelayConnectionState, RelayFee, RelayInfo } from '../../../types'
|
||||||
} from '../../../store/actions'
|
|
||||||
import {
|
|
||||||
RelayConnectionState,
|
|
||||||
RelayFee,
|
|
||||||
RelayInfoObject,
|
|
||||||
RelayMap
|
|
||||||
} from '../../../types'
|
|
||||||
import {
|
import {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
compareObjects,
|
getRelayInfo,
|
||||||
|
getRelayMap,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
|
publishRelayMap,
|
||||||
shorten
|
shorten
|
||||||
} from '../../../utils'
|
} from '../../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { Container } from '../../../components/Container'
|
|
||||||
|
|
||||||
export const RelaysPage = () => {
|
export const RelaysPage = () => {
|
||||||
const nostrController = NostrController.getInstance()
|
|
||||||
|
|
||||||
const relaysState = useAppSelector((state) => state.relays)
|
|
||||||
const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey)
|
const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey)
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const [newRelayURI, setNewRelayURI] = useState<string>()
|
const [newRelayURI, setNewRelayURI] = useState<string>()
|
||||||
const [newRelayURIerror, setNewRelayURIerror] = useState<string>()
|
const [newRelayURIerror, setNewRelayURIerror] = useState<string>()
|
||||||
const [relayMap, setRelayMap] = useState<RelayMap | undefined>(
|
|
||||||
relaysState?.map
|
const relayMap = useAppSelector((state) => state.relays?.map)
|
||||||
)
|
const relaysInfo = useAppSelector((state) => state.relays?.info)
|
||||||
const [relaysInfo, setRelaysInfo] = useState<RelayInfoObject | undefined>(
|
|
||||||
relaysState?.info
|
|
||||||
)
|
|
||||||
const [displayRelaysInfo, setDisplayRelaysInfo] = useState<string[]>([])
|
|
||||||
const [relaysConnectionStatus, setRelaysConnectionStatus] = useState(
|
|
||||||
relaysState?.connectionStatus
|
|
||||||
)
|
|
||||||
|
|
||||||
const webSocketPrefix = 'wss://'
|
const webSocketPrefix = 'wss://'
|
||||||
|
|
||||||
// Update relay connection status
|
useDidMount(() => {
|
||||||
useEffect(() => {
|
if (usersPubkey) {
|
||||||
if (
|
getRelayMap(usersPubkey).then((newRelayMap) => {
|
||||||
!compareObjects(relaysConnectionStatus, relaysState?.connectionStatus)
|
dispatch(setRelayMapAction(newRelayMap.map))
|
||||||
) {
|
})
|
||||||
setRelaysConnectionStatus(relaysState?.connectionStatus)
|
|
||||||
}
|
}
|
||||||
}, [relaysConnectionStatus, relaysState?.connectionStatus])
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!compareObjects(relaysInfo, relaysState?.info)) {
|
|
||||||
setRelaysInfo(relaysState?.info)
|
|
||||||
}
|
|
||||||
}, [relaysInfo, relaysState?.info])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!compareObjects(relayMap, relaysState?.map)) {
|
|
||||||
setRelayMap(relaysState?.map)
|
|
||||||
}
|
|
||||||
}, [relayMap, relaysState?.map])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isMounted = false
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
if (usersPubkey) {
|
|
||||||
isMounted = true
|
|
||||||
|
|
||||||
// call async func to fetch relay map
|
|
||||||
const newRelayMap = await nostrController.getRelayMap(usersPubkey)
|
|
||||||
|
|
||||||
// handle fetched relay map
|
|
||||||
if (isMounted) {
|
|
||||||
if (
|
|
||||||
!relaysState?.mapUpdated ||
|
|
||||||
(newRelayMap?.mapUpdated !== undefined &&
|
|
||||||
newRelayMap?.mapUpdated > relaysState?.mapUpdated)
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
!relaysState?.map ||
|
|
||||||
!compareObjects(relaysState.map, newRelayMap)
|
|
||||||
) {
|
|
||||||
setRelayMap(newRelayMap.map)
|
|
||||||
|
|
||||||
dispatch(setRelayMapAction(newRelayMap.map))
|
|
||||||
} else {
|
|
||||||
// Update relay map updated timestamp
|
|
||||||
dispatch(setRelayMapUpdatedAction())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publishing relay map can take some time.
|
|
||||||
// This is why data fetch should happen only if relay map was received more than 5 minutes ago.
|
|
||||||
if (
|
|
||||||
usersPubkey &&
|
|
||||||
(!relaysState?.mapUpdated ||
|
|
||||||
Date.now() - relaysState?.mapUpdated > 5 * 60 * 1000) // 5 minutes
|
|
||||||
) {
|
|
||||||
fetchData()
|
|
||||||
|
|
||||||
// Update relay connection status
|
|
||||||
if (relaysConnectionStatus) {
|
|
||||||
const notConnectedRelays = Object.keys(relaysConnectionStatus).filter(
|
|
||||||
(key) =>
|
|
||||||
relaysConnectionStatus[key] === RelayConnectionState.NotConnected
|
|
||||||
)
|
|
||||||
|
|
||||||
if (notConnectedRelays.length) {
|
|
||||||
nostrController.connectToRelays(notConnectedRelays)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup func
|
|
||||||
return () => {
|
|
||||||
isMounted = false
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
dispatch,
|
|
||||||
usersPubkey,
|
|
||||||
relaysState?.map,
|
|
||||||
relaysState?.mapUpdated,
|
|
||||||
nostrController,
|
|
||||||
relaysConnectionStatus
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Display notification if an empty relay map has been received
|
// Display notification if an empty relay map has been received
|
||||||
@ -175,24 +78,23 @@ export const RelaysPage = () => {
|
|||||||
|
|
||||||
if (usersPubkey) {
|
if (usersPubkey) {
|
||||||
// Publish updated relay map.
|
// Publish updated relay map.
|
||||||
const relayMapPublishingRes = await nostrController
|
const relayMapPublishingRes = await publishRelayMap(
|
||||||
.publishRelayMap(relayMapCopy, usersPubkey, [relay])
|
relayMapCopy,
|
||||||
.catch((err) => handlePublishRelayMapError(err))
|
usersPubkey,
|
||||||
|
[relay]
|
||||||
|
).catch((err) => handlePublishRelayMapError(err))
|
||||||
|
|
||||||
if (relayMapPublishingRes) {
|
if (relayMapPublishingRes) {
|
||||||
toast.success(relayMapPublishingRes)
|
toast.success(relayMapPublishingRes)
|
||||||
|
|
||||||
setRelayMap(relayMapCopy)
|
|
||||||
|
|
||||||
dispatch(setRelayMapAction(relayMapCopy))
|
dispatch(setRelayMapAction(relayMapCopy))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nostrController.disconnectFromRelays([relay])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const handlePublishRelayMapError = (err: any) => {
|
const handlePublishRelayMapError = (err: any) => {
|
||||||
const errorPrefix = 'Error while publishing Relay Map'
|
const errorPrefix = 'Error while publishing Relay Map'
|
||||||
|
|
||||||
@ -224,15 +126,14 @@ export const RelaysPage = () => {
|
|||||||
|
|
||||||
if (usersPubkey) {
|
if (usersPubkey) {
|
||||||
// Publish updated relay map
|
// Publish updated relay map
|
||||||
const relayMapPublishingRes = await nostrController
|
const relayMapPublishingRes = await publishRelayMap(
|
||||||
.publishRelayMap(relayMapCopy, usersPubkey)
|
relayMapCopy,
|
||||||
.catch((err) => handlePublishRelayMapError(err))
|
usersPubkey
|
||||||
|
).catch((err) => handlePublishRelayMapError(err))
|
||||||
|
|
||||||
if (relayMapPublishingRes) {
|
if (relayMapPublishingRes) {
|
||||||
toast.success(relayMapPublishingRes)
|
toast.success(relayMapPublishingRes)
|
||||||
|
|
||||||
setRelayMap(relayMapCopy)
|
|
||||||
|
|
||||||
dispatch(setRelayMapAction(relayMapCopy))
|
dispatch(setRelayMapAction(relayMapCopy))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,29 +157,25 @@ export const RelaysPage = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (relayURI && usersPubkey) {
|
} else if (relayURI && usersPubkey) {
|
||||||
const connectionStatus = await nostrController.connectToRelays([relayURI])
|
const relay = await relayController.connectRelay(relayURI)
|
||||||
|
|
||||||
if (
|
if (relay && relay.connected) {
|
||||||
connectionStatus &&
|
|
||||||
connectionStatus[relayURI] &&
|
|
||||||
connectionStatus[relayURI] === RelayConnectionState.Connected
|
|
||||||
) {
|
|
||||||
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
||||||
|
|
||||||
relayMapCopy[relayURI] = { write: true, read: true }
|
relayMapCopy[relayURI] = { write: true, read: true }
|
||||||
|
|
||||||
// Publish updated relay map
|
// Publish updated relay map
|
||||||
const relayMapPublishingRes = await nostrController
|
const relayMapPublishingRes = await publishRelayMap(
|
||||||
.publishRelayMap(relayMapCopy, usersPubkey)
|
relayMapCopy,
|
||||||
.catch((err) => handlePublishRelayMapError(err))
|
usersPubkey
|
||||||
|
).catch((err) => handlePublishRelayMapError(err))
|
||||||
|
|
||||||
if (relayMapPublishingRes) {
|
if (relayMapPublishingRes) {
|
||||||
setRelayMap(relayMapCopy)
|
|
||||||
setNewRelayURI('')
|
setNewRelayURI('')
|
||||||
|
|
||||||
dispatch(setRelayMapAction(relayMapCopy))
|
dispatch(setRelayMapAction(relayMapCopy))
|
||||||
|
|
||||||
nostrController.getRelayInfo([relayURI])
|
getRelayInfo([relayURI])
|
||||||
|
|
||||||
toast.success(relayMapPublishingRes)
|
toast.success(relayMapPublishingRes)
|
||||||
}
|
}
|
||||||
@ -292,29 +189,6 @@ export const RelaysPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle relay open and close state
|
|
||||||
const handleRelayInfo = (relay: string) => {
|
|
||||||
if (relaysInfo) {
|
|
||||||
const info = relaysInfo[relay]
|
|
||||||
|
|
||||||
if (info) {
|
|
||||||
let displayRelaysInfoCopy: string[] = JSON.parse(
|
|
||||||
JSON.stringify(displayRelaysInfo)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (displayRelaysInfoCopy.includes(relay)) {
|
|
||||||
displayRelaysInfoCopy = displayRelaysInfoCopy.filter(
|
|
||||||
(rel) => rel !== relay
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
displayRelaysInfoCopy.push(relay)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDisplayRelaysInfo(displayRelaysInfoCopy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={styles.container}>
|
<Container className={styles.container}>
|
||||||
<Box className={styles.relayAddContainer}>
|
<Box className={styles.relayAddContainer}>
|
||||||
@ -343,177 +217,211 @@ export const RelaysPage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
{relayMap && (
|
{relayMap && (
|
||||||
<Box className={styles.relaysContainer}>
|
<Box className={styles.relaysContainer}>
|
||||||
{Object.keys(relayMap).map((relay, i) => (
|
{Object.keys(relayMap).map((relay) => (
|
||||||
<Box className={styles.relay} key={`relay_${i}`}>
|
<RelayItem
|
||||||
<List>
|
key={relay}
|
||||||
<ListItem>
|
relayURI={relay}
|
||||||
<span
|
isWriteRelay={relayMap[relay].write}
|
||||||
className={[
|
relayInfo={relaysInfo ? relaysInfo[relay] : undefined}
|
||||||
styles.connectionStatus,
|
handleLeaveRelay={handleLeaveRelay}
|
||||||
relaysConnectionStatus
|
handleRelayWriteChange={handleRelayWriteChange}
|
||||||
? relaysConnectionStatus[relay] ===
|
/>
|
||||||
RelayConnectionState.Connected
|
|
||||||
? styles.connectionStatusConnected
|
|
||||||
: styles.connectionStatusNotConnected
|
|
||||||
: styles.connectionStatusUnknown
|
|
||||||
].join(' ')}
|
|
||||||
/>
|
|
||||||
{relaysInfo &&
|
|
||||||
relaysInfo[relay] &&
|
|
||||||
relaysInfo[relay].limitation &&
|
|
||||||
relaysInfo[relay].limitation?.payment_required && (
|
|
||||||
<Tooltip title="Paid Relay" arrow placement="top">
|
|
||||||
<ElectricBoltIcon
|
|
||||||
className={styles.lightningIcon}
|
|
||||||
color="warning"
|
|
||||||
onClick={() => handleRelayInfo(relay)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ListItemText primary={relay} />
|
|
||||||
|
|
||||||
<Box
|
|
||||||
className={styles.leaveRelayContainer}
|
|
||||||
onClick={() => handleLeaveRelay(relay)}
|
|
||||||
>
|
|
||||||
<LogoutIcon />
|
|
||||||
<span>Leave</span>
|
|
||||||
</Box>
|
|
||||||
</ListItem>
|
|
||||||
<Divider className={styles.relayDivider} />
|
|
||||||
<ListItem>
|
|
||||||
<ListItemText
|
|
||||||
primary="Publish to this relay?"
|
|
||||||
secondary={
|
|
||||||
relaysInfo && relaysInfo[relay] ? (
|
|
||||||
<span
|
|
||||||
onClick={() => handleRelayInfo(relay)}
|
|
||||||
className={styles.showInfo}
|
|
||||||
>
|
|
||||||
Show info{' '}
|
|
||||||
{displayRelaysInfo.includes(relay) ? (
|
|
||||||
<KeyboardArrowUpIcon
|
|
||||||
className={styles.showInfoIcon}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<KeyboardArrowDownIcon
|
|
||||||
className={styles.showInfoIcon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Switch
|
|
||||||
checked={relayMap[relay].write}
|
|
||||||
onChange={(event) => handleRelayWriteChange(relay, event)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
{displayRelaysInfo.includes(relay) && (
|
|
||||||
<>
|
|
||||||
<Divider className={styles.relayDivider} />
|
|
||||||
<ListItem>
|
|
||||||
<Box className={styles.relayInfoContainer}>
|
|
||||||
{relaysInfo &&
|
|
||||||
relaysInfo[relay] &&
|
|
||||||
Object.keys(relaysInfo[relay]).map((key: string) => {
|
|
||||||
const infoTitle = capitalizeFirstLetter(
|
|
||||||
key.replace('_', ' ')
|
|
||||||
)
|
|
||||||
let infoValue = (relaysInfo[relay] as any)[key]
|
|
||||||
|
|
||||||
switch (key) {
|
|
||||||
case 'pubkey':
|
|
||||||
infoValue = shorten(hexToNpub(infoValue), 15)
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'limitation':
|
|
||||||
infoValue = (
|
|
||||||
<ul key={`${i}_${key}`}>
|
|
||||||
{Object.keys(infoValue).map((valueKey) => (
|
|
||||||
<li key={`${i}_${key}_${valueKey}`}>
|
|
||||||
<span
|
|
||||||
className={styles.relayInfoSubTitle}
|
|
||||||
>
|
|
||||||
{capitalizeFirstLetter(
|
|
||||||
valueKey.split('_').join(' ')
|
|
||||||
)}
|
|
||||||
:
|
|
||||||
</span>{' '}
|
|
||||||
{`${infoValue[valueKey]}`}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'fees':
|
|
||||||
infoValue = (
|
|
||||||
<ul>
|
|
||||||
{Object.keys(infoValue).map((valueKey) => (
|
|
||||||
<li key={`${i}_${key}_${valueKey}`}>
|
|
||||||
<span
|
|
||||||
className={styles.relayInfoSubTitle}
|
|
||||||
>
|
|
||||||
{capitalizeFirstLetter(
|
|
||||||
valueKey.split('_').join(' ')
|
|
||||||
)}
|
|
||||||
:
|
|
||||||
</span>{' '}
|
|
||||||
{`${infoValue[valueKey].map((fee: RelayFee) => `${fee.amount} ${fee.unit}`)}`}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(infoValue)) {
|
|
||||||
infoValue = infoValue.join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span key={`${i}_${key}_container`}>
|
|
||||||
<span className={styles.relayInfoTitle}>
|
|
||||||
{infoTitle}:
|
|
||||||
</span>{' '}
|
|
||||||
{infoValue}
|
|
||||||
{key === 'pubkey' ? (
|
|
||||||
<ContentCopyIcon
|
|
||||||
className={styles.copyItem}
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(
|
|
||||||
hexToNpub(
|
|
||||||
(relaysInfo[relay] as any)[key]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
toast.success('Copied to clipboard', {
|
|
||||||
autoClose: 1000,
|
|
||||||
hideProgressBar: true
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</ListItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RelayItemProp = {
|
||||||
|
relayURI: string
|
||||||
|
isWriteRelay: boolean
|
||||||
|
relayInfo?: RelayInfo
|
||||||
|
handleLeaveRelay: (relay: string) => void
|
||||||
|
handleRelayWriteChange: (
|
||||||
|
relay: string,
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const RelayItem = ({
|
||||||
|
relayURI,
|
||||||
|
isWriteRelay,
|
||||||
|
relayInfo,
|
||||||
|
handleLeaveRelay,
|
||||||
|
handleRelayWriteChange
|
||||||
|
}: RelayItemProp) => {
|
||||||
|
const [relayConnectionStatus, setRelayConnectionStatus] =
|
||||||
|
useState<RelayConnectionState>()
|
||||||
|
|
||||||
|
const [displayRelayInfo, setDisplayRelayInfo] = useState(false)
|
||||||
|
|
||||||
|
useDidMount(() => {
|
||||||
|
relayController.connectRelay(relayURI).then((relay) => {
|
||||||
|
if (relay && relay.connected) {
|
||||||
|
setRelayConnectionStatus(RelayConnectionState.Connected)
|
||||||
|
} else {
|
||||||
|
setRelayConnectionStatus(RelayConnectionState.NotConnected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={styles.relay}>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
styles.connectionStatus,
|
||||||
|
relayConnectionStatus
|
||||||
|
? relayConnectionStatus === RelayConnectionState.Connected
|
||||||
|
? styles.connectionStatusConnected
|
||||||
|
: styles.connectionStatusNotConnected
|
||||||
|
: styles.connectionStatusUnknown
|
||||||
|
].join(' ')}
|
||||||
|
/>
|
||||||
|
{relayInfo &&
|
||||||
|
relayInfo.limitation &&
|
||||||
|
relayInfo.limitation?.payment_required && (
|
||||||
|
<Tooltip title="Paid Relay" arrow placement="top">
|
||||||
|
<ElectricBoltIcon
|
||||||
|
className={styles.lightningIcon}
|
||||||
|
color="warning"
|
||||||
|
onClick={() => setDisplayRelayInfo((prev) => !prev)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ListItemText primary={relayURI} />
|
||||||
|
|
||||||
|
<Box
|
||||||
|
className={styles.leaveRelayContainer}
|
||||||
|
onClick={() => handleLeaveRelay(relayURI)}
|
||||||
|
>
|
||||||
|
<LogoutIcon />
|
||||||
|
<span>Leave</span>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
<Divider className={styles.relayDivider} />
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary="Publish to this relay?"
|
||||||
|
secondary={
|
||||||
|
relayInfo ? (
|
||||||
|
<span
|
||||||
|
onClick={() => setDisplayRelayInfo((prev) => !prev)}
|
||||||
|
className={styles.showInfo}
|
||||||
|
>
|
||||||
|
Show info{' '}
|
||||||
|
{displayRelayInfo ? (
|
||||||
|
<KeyboardArrowUpIcon className={styles.showInfoIcon} />
|
||||||
|
) : (
|
||||||
|
<KeyboardArrowDownIcon className={styles.showInfoIcon} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
checked={isWriteRelay}
|
||||||
|
onChange={(event) => handleRelayWriteChange(relayURI, event)}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
{displayRelayInfo && (
|
||||||
|
<>
|
||||||
|
<Divider className={styles.relayDivider} />
|
||||||
|
<ListItem>
|
||||||
|
<Box className={styles.relayInfoContainer}>
|
||||||
|
{relayInfo &&
|
||||||
|
Object.keys(relayInfo).map((key: string) => {
|
||||||
|
const infoTitle = capitalizeFirstLetter(
|
||||||
|
key.replace('_', ' ')
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let infoValue = (relayInfo as any)[key]
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'pubkey':
|
||||||
|
infoValue = shorten(hexToNpub(infoValue), 15)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'limitation':
|
||||||
|
infoValue = (
|
||||||
|
<ul>
|
||||||
|
{Object.keys(infoValue).map((valueKey) => (
|
||||||
|
<li key={`${relayURI}_${key}_${valueKey}`}>
|
||||||
|
<span className={styles.relayInfoSubTitle}>
|
||||||
|
{capitalizeFirstLetter(
|
||||||
|
valueKey.split('_').join(' ')
|
||||||
|
)}
|
||||||
|
:
|
||||||
|
</span>{' '}
|
||||||
|
{`${infoValue[valueKey]}`}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'fees':
|
||||||
|
infoValue = (
|
||||||
|
<ul>
|
||||||
|
{Object.keys(infoValue).map((valueKey) => (
|
||||||
|
<li key={`${relayURI}_${key}_${valueKey}`}>
|
||||||
|
<span className={styles.relayInfoSubTitle}>
|
||||||
|
{capitalizeFirstLetter(
|
||||||
|
valueKey.split('_').join(' ')
|
||||||
|
)}
|
||||||
|
:
|
||||||
|
</span>{' '}
|
||||||
|
{`${infoValue[valueKey].map((fee: RelayFee) => `${fee.amount} ${fee.unit}`)}`}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(infoValue)) {
|
||||||
|
infoValue = infoValue.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span key={`${relayURI}_${key}_container`}>
|
||||||
|
<span className={styles.relayInfoTitle}>
|
||||||
|
{infoTitle}:
|
||||||
|
</span>{' '}
|
||||||
|
{infoValue}
|
||||||
|
{key === 'pubkey' ? (
|
||||||
|
<ContentCopyIcon
|
||||||
|
className={styles.copyItem}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
hexToNpub((relayInfo as any)[key])
|
||||||
|
)
|
||||||
|
|
||||||
|
toast.success('Copied to clipboard', {
|
||||||
|
autoClose: 1000,
|
||||||
|
hideProgressBar: true
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { EventTemplate, Filter, kinds, nip19 } from 'nostr-tools'
|
import { EventTemplate, Filter, kinds, nip19 } from 'nostr-tools'
|
||||||
import { queryNip05, unixNow } from '.'
|
import { compareObjects, queryNip05, unixNow } from '.'
|
||||||
import {
|
import {
|
||||||
MetadataController,
|
MetadataController,
|
||||||
NostrController,
|
NostrController,
|
||||||
relayController
|
relayController
|
||||||
} from '../controllers'
|
} from '../controllers'
|
||||||
import { NostrJoiningBlock } from '../types'
|
import { NostrJoiningBlock, RelayInfoObject } from '../types'
|
||||||
import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
|
import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
|
||||||
|
import store from '../store/store'
|
||||||
|
import { setRelayInfoAction } from '../store/actions'
|
||||||
|
|
||||||
export const getNostrJoiningBlockNumber = async (
|
export const getNostrJoiningBlockNumber = async (
|
||||||
hexKey: string
|
hexKey: string
|
||||||
@ -133,3 +135,94 @@ export const getNostrJoiningBlockNumber = async (
|
|||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets information about relays into relays.info app state.
|
||||||
|
* @param relayURIs - relay URIs to get information about
|
||||||
|
*/
|
||||||
|
export const getRelayInfo = async (relayURIs: string[]) => {
|
||||||
|
// initialize job request
|
||||||
|
const jobEventTemplate: EventTemplate = {
|
||||||
|
content: '',
|
||||||
|
created_at: unixNow(),
|
||||||
|
kind: 68001,
|
||||||
|
tags: [
|
||||||
|
['i', `${JSON.stringify(relayURIs)}`],
|
||||||
|
['j', 'relay-info']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
|
// sign job request event
|
||||||
|
const jobSignedEvent = await nostrController.signEvent(jobEventTemplate)
|
||||||
|
|
||||||
|
const relays = [
|
||||||
|
'wss://relay.damus.io',
|
||||||
|
'wss://relay.primal.net',
|
||||||
|
'wss://relayable.org'
|
||||||
|
]
|
||||||
|
|
||||||
|
// publish job request
|
||||||
|
await relayController.publish(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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -603,7 +603,7 @@ export const updateUsersAppData = async (meta: Meta) => {
|
|||||||
|
|
||||||
const publishResult = await Promise.race([
|
const publishResult = await Promise.race([
|
||||||
relayController.publish(signedEvent, writeRelays),
|
relayController.publish(signedEvent, writeRelays),
|
||||||
timeout(1000 * 30)
|
timeout(40 * 1000)
|
||||||
]).catch((err) => {
|
]).catch((err) => {
|
||||||
console.log('err :>> ', err)
|
console.log('err :>> ', err)
|
||||||
if (err.message === 'Timeout') {
|
if (err.message === 'Timeout') {
|
||||||
@ -936,7 +936,7 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
|
|||||||
// Publish the notification event to the recipient's read relays
|
// Publish the notification event to the recipient's read relays
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
relayController.publish(wrappedEvent, relaySet.read),
|
relayController.publish(wrappedEvent, relaySet.read),
|
||||||
timeout(1000 * 30)
|
timeout(40 * 1000)
|
||||||
]).catch((err) => {
|
]).catch((err) => {
|
||||||
// Log an error if publishing the notification event fails
|
// Log an error if publishing the notification event fails
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Event, Filter } from 'nostr-tools'
|
import { Event, Filter, kinds, UnsignedEvent } from 'nostr-tools'
|
||||||
import { RelayList } from 'nostr-tools/kinds'
|
import { RelayList } from 'nostr-tools/kinds'
|
||||||
import { relayController } from '../controllers/RelayController.ts'
|
import { getRelayInfo, unixNow } from '.'
|
||||||
|
import { NostrController, relayController } from '../controllers'
|
||||||
import { localCache } from '../services'
|
import { localCache } from '../services'
|
||||||
import { setMostPopularRelaysAction } from '../store/actions'
|
import { setMostPopularRelaysAction } from '../store/actions'
|
||||||
import store from '../store/store'
|
import store from '../store/store'
|
||||||
import { RelayMap, RelayReadStats, RelaySet, RelayStats } from '../types'
|
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'
|
||||||
|
|
||||||
const READ_MARKER = 'read'
|
const READ_MARKER = 'read'
|
||||||
const WRITE_MARKER = 'write'
|
const WRITE_MARKER = 'write'
|
||||||
@ -154,12 +155,131 @@ const getMostPopularRelays = async (
|
|||||||
return apiTopRelays
|
return apiTopRelays
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides relay map.
|
||||||
|
* @param npub - user's npub
|
||||||
|
* @returns - promise that resolves into relay map and a timestamp when it has been updated.
|
||||||
|
*/
|
||||||
|
const getRelayMap = async (
|
||||||
|
npub: string
|
||||||
|
): Promise<{ map: RelayMap; mapUpdated?: number }> => {
|
||||||
|
const mostPopularRelays = await getMostPopularRelays()
|
||||||
|
|
||||||
|
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
|
||||||
|
const eventFilter: Filter = {
|
||||||
|
kinds: [kinds.RelayList],
|
||||||
|
authors: [npub]
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = await relayController
|
||||||
|
.fetchEvent(eventFilter, mostPopularRelays)
|
||||||
|
.catch((err) => {
|
||||||
|
return Promise.reject(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
// Handle founded 10002 event
|
||||||
|
const relaysMap: RelayMap = {}
|
||||||
|
|
||||||
|
// 'r' stands for 'relay'
|
||||||
|
const relayTags = event.tags.filter((tag) => tag[0] === 'r')
|
||||||
|
|
||||||
|
relayTags.forEach((tag) => {
|
||||||
|
const uri = tag[1]
|
||||||
|
const relayType = tag[2]
|
||||||
|
|
||||||
|
// if 3rd element of relay tag is undefined, relay is WRITE and READ
|
||||||
|
relaysMap[uri] = {
|
||||||
|
write: relayType ? relayType === 'write' : true,
|
||||||
|
read: relayType ? relayType === 'read' : true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.keys(relaysMap).forEach((relayUrl) => {
|
||||||
|
relayController.connectRelay(relayUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
getRelayInfo(Object.keys(relaysMap))
|
||||||
|
|
||||||
|
return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at })
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({ map: getDefaultRelayMap() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publishes relay map.
|
||||||
|
* @param relayMap - relay map.
|
||||||
|
* @param npub - user's npub.
|
||||||
|
* @param extraRelaysToPublish - optional relays to publish relay map.
|
||||||
|
* @returns - promise that resolves into a string representing publishing result.
|
||||||
|
*/
|
||||||
|
const publishRelayMap = async (
|
||||||
|
relayMap: RelayMap,
|
||||||
|
npub: string,
|
||||||
|
extraRelaysToPublish?: string[]
|
||||||
|
): Promise<string> => {
|
||||||
|
const timestamp = unixNow()
|
||||||
|
const relayURIs = Object.keys(relayMap)
|
||||||
|
|
||||||
|
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
|
||||||
|
const tags: string[][] = relayURIs.map((relayURI) =>
|
||||||
|
[
|
||||||
|
'r',
|
||||||
|
relayURI,
|
||||||
|
relayMap[relayURI].read && relayMap[relayURI].write
|
||||||
|
? ''
|
||||||
|
: relayMap[relayURI].write
|
||||||
|
? 'write'
|
||||||
|
: 'read'
|
||||||
|
].filter((value) => value !== '')
|
||||||
|
)
|
||||||
|
|
||||||
|
const newRelayMapEvent: UnsignedEvent = {
|
||||||
|
kind: kinds.RelayList,
|
||||||
|
tags,
|
||||||
|
content: '',
|
||||||
|
pubkey: npub,
|
||||||
|
created_at: timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
const signedEvent = await nostrController.signEvent(newRelayMapEvent)
|
||||||
|
|
||||||
|
let relaysToPublish = relayURIs
|
||||||
|
|
||||||
|
// Add extra relays if provided
|
||||||
|
if (extraRelaysToPublish) {
|
||||||
|
relaysToPublish = [...relaysToPublish, ...extraRelaysToPublish]
|
||||||
|
}
|
||||||
|
|
||||||
|
// If relay map is empty, use most popular relay URIs
|
||||||
|
if (!relaysToPublish.length) {
|
||||||
|
relaysToPublish = await getMostPopularRelays()
|
||||||
|
}
|
||||||
|
|
||||||
|
const publishResult = await relayController.publish(
|
||||||
|
signedEvent,
|
||||||
|
relaysToPublish
|
||||||
|
)
|
||||||
|
|
||||||
|
if (publishResult && publishResult.length) {
|
||||||
|
return Promise.resolve(
|
||||||
|
`Relay Map published on: ${publishResult.join('\n')}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject('Publishing updated relay map was unsuccessful.')
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
findRelayListAndUpdateCache,
|
findRelayListAndUpdateCache,
|
||||||
findRelayListInCache,
|
findRelayListInCache,
|
||||||
getDefaultRelayMap,
|
getDefaultRelayMap,
|
||||||
getDefaultRelaySet,
|
getDefaultRelaySet,
|
||||||
getMostPopularRelays,
|
getMostPopularRelays,
|
||||||
|
getRelayMap,
|
||||||
|
publishRelayMap,
|
||||||
getUserRelaySet,
|
getUserRelaySet,
|
||||||
isOlderThanOneWeek
|
isOlderThanOneWeek
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user