feat: added ndkContext and used it in relays page
This commit is contained in:
parent
52f8b92c5d
commit
3c061d5920
102
package-lock.json
generated
102
package-lock.json
generated
@ -20,12 +20,14 @@
|
||||
"@mui/lab": "5.0.0-alpha.166",
|
||||
"@mui/material": "5.15.11",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@nostr-dev-kit/ndk": "2.5.0",
|
||||
"@nostr-dev-kit/ndk": "2.10.0",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@reduxjs/toolkit": "2.2.1",
|
||||
"axios": "^1.7.4",
|
||||
"crypto-hash": "3.0.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dexie": "4.0.8",
|
||||
"dnd-core": "16.0.1",
|
||||
"file-saver": "2.0.5",
|
||||
"idb": "8.0.0",
|
||||
@ -1710,65 +1712,79 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.5.0.tgz",
|
||||
"integrity": "sha512-A2nRgjjLScDhGZGPWx8xUIJM66dJWScdWQoCn/tI1Gtwpple+C2Jp7C9t3mb0oF3bwd2nsV6qwS//wdrH8QvYQ==",
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.10.0.tgz",
|
||||
"integrity": "sha512-TqCAAo6ylORraAXrzRkCGFN2xTMiFbdER8Y8CtUT0HwOpFG/Wn+PBNeDeDmqkl/6LaPdeyXmVwCWj2KcUjIwYA==",
|
||||
"dependencies": {
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.3.1",
|
||||
"@noble/secp256k1": "^2.0.0",
|
||||
"@scure/base": "^1.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"light-bolt11-decoder": "^3.0.0",
|
||||
"node-fetch": "^3.3.1",
|
||||
"nostr-tools": "^1.15.0",
|
||||
"nostr-tools": "^2.7.1",
|
||||
"tseep": "^1.1.1",
|
||||
"typescript-lru-cache": "^2.0.0",
|
||||
"utf8-buffer": "^1.0.0",
|
||||
"websocket-polyfill": "^0.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk/node_modules/@noble/ciphers": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
|
||||
"integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
"node_modules/@nostr-dev-kit/ndk-cache-dexie": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.1.tgz",
|
||||
"integrity": "sha512-tUwEy68bd9GL5JVuZIjcpdwuDEBnaXen3WJ64/GRDtbyE1RB01Y6hHC7IQC9bcQ6SC7XBGyPd+2nuTyR7+Mffg==",
|
||||
"dependencies": {
|
||||
"@nostr-dev-kit/ndk": "2.10.0",
|
||||
"debug": "^4.3.4",
|
||||
"dexie": "^4.0.2",
|
||||
"nostr-tools": "^2.4.0",
|
||||
"typescript-lru-cache": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk/node_modules/@noble/curves": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
|
||||
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz",
|
||||
"integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.3.1"
|
||||
"@noble/hashes": "1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk/node_modules/@noble/hashes": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
||||
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz",
|
||||
"integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz",
|
||||
"integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==",
|
||||
"version": "2.10.4",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.10.4.tgz",
|
||||
"integrity": "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg==",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "0.2.0",
|
||||
"@noble/curves": "1.1.0",
|
||||
"@noble/ciphers": "^0.5.1",
|
||||
"@noble/curves": "1.2.0",
|
||||
"@noble/hashes": "1.3.1",
|
||||
"@scure/base": "1.1.1",
|
||||
"@scure/bip32": "1.3.1",
|
||||
"@scure/bip39": "1.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"nostr-wasm": "0.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.0.0"
|
||||
},
|
||||
@ -1778,6 +1794,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@noble/curves": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.3.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@noble/hashes": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
||||
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/fontkit": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@pdf-lib/fontkit/-/fontkit-1.1.1.tgz",
|
||||
@ -3850,6 +3899,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dexie": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz",
|
||||
"integrity": "sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ=="
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
|
@ -30,12 +30,14 @@
|
||||
"@mui/lab": "5.0.0-alpha.166",
|
||||
"@mui/material": "5.15.11",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@nostr-dev-kit/ndk": "2.5.0",
|
||||
"@nostr-dev-kit/ndk": "2.10.0",
|
||||
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@reduxjs/toolkit": "2.2.1",
|
||||
"axios": "^1.7.4",
|
||||
"crypto-hash": "3.0.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dexie": "4.0.8",
|
||||
"dnd-core": "16.0.1",
|
||||
"file-saver": "2.0.5",
|
||||
"idb": "8.0.0",
|
||||
|
243
src/contexts/NDKContext.tsx
Normal file
243
src/contexts/NDKContext.tsx
Normal file
@ -0,0 +1,243 @@
|
||||
import NDK, {
|
||||
getRelayListForUser,
|
||||
NDKEvent,
|
||||
NDKFilter,
|
||||
NDKRelaySet,
|
||||
NDKSubscriptionCacheUsage,
|
||||
NDKUser,
|
||||
NDKUserProfile
|
||||
} from '@nostr-dev-kit/ndk'
|
||||
|
||||
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'
|
||||
|
||||
import { Dexie } from 'dexie'
|
||||
import { createContext, ReactNode, useEffect, useMemo } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { UserRelaysType } from '../types'
|
||||
import {
|
||||
DEFAULT_LOOK_UP_RELAY_LIST,
|
||||
hexToNpub,
|
||||
orderEventsChronologically,
|
||||
timeout
|
||||
} from '../utils'
|
||||
|
||||
export interface NDKContextType {
|
||||
ndk: NDK
|
||||
fetchEvents: (filter: NDKFilter) => Promise<NDKEvent[]>
|
||||
fetchEvent: (filter: NDKFilter) => Promise<NDKEvent | null>
|
||||
fetchEventsFromUserRelays: (
|
||||
filter: NDKFilter | NDKFilter[],
|
||||
hexKey: string,
|
||||
userRelaysType: UserRelaysType
|
||||
) => Promise<NDKEvent[]>
|
||||
fetchEventFromUserRelays: (
|
||||
filter: NDKFilter | NDKFilter[],
|
||||
hexKey: string,
|
||||
userRelaysType: UserRelaysType
|
||||
) => Promise<NDKEvent | null>
|
||||
findMetadata: (pubkey: string) => Promise<NDKUserProfile | null>
|
||||
publish: (event: NDKEvent, explicitRelayUrls?: string[]) => Promise<string[]>
|
||||
}
|
||||
|
||||
// Create the context with an initial value of `null`
|
||||
export const NDKContext = createContext<NDKContextType | null>(null)
|
||||
|
||||
// Create a provider component to wrap around parts of your app
|
||||
export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
|
||||
useEffect(() => {
|
||||
window.onunhandledrejection = async (event: PromiseRejectionEvent) => {
|
||||
event.preventDefault()
|
||||
if (event.reason?.name === Dexie.errnames.DatabaseClosed) {
|
||||
console.log(
|
||||
'Could not open Dexie DB, probably version change. Deleting old DB and reloading...'
|
||||
)
|
||||
await Dexie.delete('degmod-db')
|
||||
// Must reload to open a brand new DB
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const ndk = useMemo(() => {
|
||||
localStorage.setItem('debug', '*')
|
||||
const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'degmod-db' })
|
||||
dexieAdapter.locking = true
|
||||
const ndk = new NDK({
|
||||
enableOutboxModel: true,
|
||||
autoConnectUserRelays: true,
|
||||
autoFetchUserMutelist: true,
|
||||
explicitRelayUrls: [...DEFAULT_LOOK_UP_RELAY_LIST],
|
||||
cacheAdapter: dexieAdapter
|
||||
})
|
||||
ndk.connect()
|
||||
|
||||
return ndk
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Asynchronously retrieves multiple event based on a provided filter.
|
||||
*
|
||||
* @param filter - The filter criteria to find the event.
|
||||
* @returns Returns a promise that resolves to the found event or null if not found.
|
||||
*/
|
||||
const fetchEvents = async (filter: NDKFilter): Promise<NDKEvent[]> => {
|
||||
return ndk
|
||||
.fetchEvents(filter, {
|
||||
closeOnEose: true,
|
||||
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
|
||||
})
|
||||
.then((ndkEventSet) => {
|
||||
const ndkEvents = Array.from(ndkEventSet)
|
||||
return orderEventsChronologically(ndkEvents)
|
||||
})
|
||||
.catch((err) => {
|
||||
// Log the error and show a notification if fetching fails
|
||||
console.error('An error occurred in fetching events', err)
|
||||
toast.error('An error occurred in fetching events') // Show error notification
|
||||
return [] // Return an empty array in case of an error
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously retrieves an event based on a provided filter.
|
||||
*
|
||||
* @param filter - The filter criteria to find the event.
|
||||
* @returns Returns a promise that resolves to the found event or null if not found.
|
||||
*/
|
||||
const fetchEvent = async (filter: NDKFilter) => {
|
||||
const events = await fetchEvents(filter)
|
||||
if (events.length === 0) return null
|
||||
return events[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously retrieves multiple events from the user's relays based on a specified filter.
|
||||
* The function first retrieves the user's relays, and then fetches the events using the provided filter.
|
||||
*
|
||||
* @param filter - The event filter to use when fetching the event (e.g., kinds, authors).
|
||||
* @param hexKey - The hexadecimal representation of the user's public key.
|
||||
* @param userRelaysType - The type of relays to search (e.g., write, read).
|
||||
* @returns A promise that resolves with an array of events.
|
||||
*/
|
||||
const fetchEventsFromUserRelays = async (
|
||||
filter: NDKFilter | NDKFilter[],
|
||||
hexKey: string,
|
||||
userRelaysType: UserRelaysType
|
||||
): Promise<NDKEvent[]> => {
|
||||
// Find the user's relays (10s timeout).
|
||||
const relayUrls = await Promise.race([
|
||||
getRelayListForUser(hexKey, ndk),
|
||||
timeout(3000)
|
||||
])
|
||||
.then((ndkRelayList) => {
|
||||
if (ndkRelayList) return ndkRelayList[userRelaysType]
|
||||
return [] // Return an empty array if ndkRelayList is undefined
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
`An error occurred in fetching user's (${hexKey}) ${userRelaysType}`,
|
||||
err
|
||||
)
|
||||
return [] as string[]
|
||||
})
|
||||
|
||||
return ndk
|
||||
.fetchEvents(
|
||||
filter,
|
||||
{ closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL },
|
||||
relayUrls.length
|
||||
? NDKRelaySet.fromRelayUrls(relayUrls, ndk, true)
|
||||
: undefined
|
||||
)
|
||||
.then((ndkEventSet) => {
|
||||
const ndkEvents = Array.from(ndkEventSet)
|
||||
return orderEventsChronologically(ndkEvents)
|
||||
})
|
||||
.catch((err) => {
|
||||
// Log the error and show a notification if fetching fails
|
||||
console.error('An error occurred in fetching events', err)
|
||||
toast.error('An error occurred in fetching events') // Show error notification
|
||||
return [] // Return an empty array in case of an error
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an event from the user's relays based on a specified filter.
|
||||
* The function first retrieves the user's relays, and then fetches the event using the provided filter.
|
||||
*
|
||||
* @param filter - The event filter to use when fetching the event (e.g., kinds, authors).
|
||||
* @param hexKey - The hexadecimal representation of the user's public key.
|
||||
* @param userRelaysType - The type of relays to search (e.g., write, read).
|
||||
* @returns A promise that resolves to the fetched event or null if the operation fails.
|
||||
*/
|
||||
const fetchEventFromUserRelays = async (
|
||||
filter: NDKFilter | NDKFilter[],
|
||||
hexKey: string,
|
||||
userRelaysType: UserRelaysType
|
||||
) => {
|
||||
const events = await fetchEventsFromUserRelays(
|
||||
filter,
|
||||
hexKey,
|
||||
userRelaysType
|
||||
)
|
||||
if (events.length === 0) return null
|
||||
return events[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds metadata for a given pubkey.
|
||||
*
|
||||
* @param hexKey - The pubkey to search for metadata.
|
||||
* @returns A promise that resolves to the metadata event.
|
||||
*/
|
||||
const findMetadata = async (
|
||||
pubkey: string
|
||||
): Promise<NDKUserProfile | null> => {
|
||||
const npub = hexToNpub(pubkey)
|
||||
|
||||
const user = new NDKUser({ npub })
|
||||
user.ndk = ndk
|
||||
|
||||
return await user.fetchProfile()
|
||||
}
|
||||
|
||||
const publish = async (
|
||||
event: NDKEvent,
|
||||
explicitRelayUrls?: string[]
|
||||
): Promise<string[]> => {
|
||||
if (!event.sig) throw new Error('Before publishing first sign the event!')
|
||||
|
||||
let ndkRelaySet: NDKRelaySet | undefined
|
||||
|
||||
if (explicitRelayUrls && explicitRelayUrls.length > 0) {
|
||||
ndkRelaySet = NDKRelaySet.fromRelayUrls(explicitRelayUrls, ndk)
|
||||
}
|
||||
|
||||
return event
|
||||
.publish(ndkRelaySet, 10000)
|
||||
.then((res) => {
|
||||
const relaysPublishedOn = Array.from(res)
|
||||
return relaysPublishedOn.map((relay) => relay.url)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`An error occurred in publishing event`, err)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<NDKContext.Provider
|
||||
value={{
|
||||
ndk,
|
||||
fetchEvents,
|
||||
fetchEvent,
|
||||
fetchEventsFromUserRelays,
|
||||
fetchEventFromUserRelays,
|
||||
findMetadata,
|
||||
publish
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NDKContext.Provider>
|
||||
)
|
||||
}
|
@ -1,2 +1,4 @@
|
||||
export * from './store'
|
||||
export * from './useDidMount'
|
||||
export * from './useDvm'
|
||||
export * from './useNDKContext'
|
||||
|
98
src/hooks/useDvm.ts
Normal file
98
src/hooks/useDvm.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
|
||||
import { EventTemplate } from 'nostr-tools'
|
||||
import { NostrController } from '../controllers'
|
||||
import { setRelayInfoAction } from '../store/actions'
|
||||
import { RelayInfoObject } from '../types'
|
||||
import { compareObjects, unixNow } from '../utils'
|
||||
import { useAppDispatch, useAppSelector } from './store'
|
||||
import { useNDKContext } from './useNDKContext'
|
||||
|
||||
export const useDvm = () => {
|
||||
const dvmRelays = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.primal.net',
|
||||
'wss://relayable.org'
|
||||
]
|
||||
|
||||
const relayInfo = useAppSelector((state) => state.relays.info)
|
||||
|
||||
const { ndk, publish } = useNDKContext()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
/**
|
||||
* Sets information about relays into relays.info app state.
|
||||
* @param relayURIs - relay URIs to get information about
|
||||
*/
|
||||
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)
|
||||
|
||||
// publish job request
|
||||
const ndkEvent = new NDKEvent(ndk, jobSignedEvent)
|
||||
await publish(ndkEvent, dvmRelays)
|
||||
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
// filter for getting DVM job's result
|
||||
const sub = ndk.subscribe({
|
||||
kinds: [68002 as number],
|
||||
'#e': [jobSignedEvent.id],
|
||||
'#p': [jobSignedEvent.pubkey]
|
||||
})
|
||||
|
||||
// asynchronously get relay info 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 newRelaysInfo: RelayInfoObject
|
||||
|
||||
try {
|
||||
newRelaysInfo = JSON.parse(dvmJobResult)
|
||||
} catch (error) {
|
||||
return Promise.reject(`Invalid relay(s) information.`)
|
||||
}
|
||||
|
||||
if (newRelaysInfo && !compareObjects(relayInfo, newRelaysInfo)) {
|
||||
dispatch(setRelayInfoAction(newRelaysInfo))
|
||||
}
|
||||
}
|
||||
|
||||
return { getRelayInfo }
|
||||
}
|
13
src/hooks/useNDKContext.ts
Normal file
13
src/hooks/useNDKContext.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { NDKContext, NDKContextType } from '../contexts/NDKContext'
|
||||
import { useContext } from 'react'
|
||||
|
||||
export const useNDKContext = () => {
|
||||
const ndkContext = useContext(NDKContext)
|
||||
|
||||
if (!ndkContext)
|
||||
throw new Error(
|
||||
'NDKContext should not be used in out component tree hierarchy'
|
||||
)
|
||||
|
||||
return { ...ndkContext } as NDKContextType
|
||||
}
|
@ -11,6 +11,7 @@ import './index.css'
|
||||
import store from './store/store.ts'
|
||||
import { theme } from './theme'
|
||||
import { saveState } from './utils'
|
||||
import { NDKContextProvider } from './contexts/NDKContext'
|
||||
|
||||
store.subscribe(
|
||||
_.throttle(() => {
|
||||
@ -28,7 +29,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<CssVarsProvider theme={theme}>
|
||||
<HashRouter>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
<NDKContextProvider>
|
||||
<App />
|
||||
</NDKContextProvider>
|
||||
<ToastContainer />
|
||||
</Provider>
|
||||
</HashRouter>
|
||||
|
@ -13,26 +13,45 @@ import Switch from '@mui/material/Switch'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { Container } from '../../../components/Container'
|
||||
import { relayController } from '../../../controllers'
|
||||
import { useAppDispatch, useAppSelector, useDidMount } from '../../../hooks'
|
||||
import {
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
useDidMount,
|
||||
useDvm,
|
||||
useNDKContext
|
||||
} from '../../../hooks'
|
||||
import { setRelayMapAction } from '../../../store/actions'
|
||||
import { RelayConnectionState, RelayFee, RelayInfo } from '../../../types'
|
||||
import {
|
||||
RelayConnectionState,
|
||||
RelayFee,
|
||||
RelayInfo,
|
||||
RelayMap
|
||||
} from '../../../types'
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
compareObjects,
|
||||
getRelayInfo,
|
||||
getRelayMap,
|
||||
hexToNpub,
|
||||
normalizeWebSocketURL,
|
||||
publishRelayMap,
|
||||
shorten
|
||||
shorten,
|
||||
timeout
|
||||
} from '../../../utils'
|
||||
import styles from './style.module.scss'
|
||||
import { Footer } from '../../../components/Footer/Footer'
|
||||
import {
|
||||
getRelayListForUser,
|
||||
NDKRelayList,
|
||||
NDKRelayStatus
|
||||
} from '@nostr-dev-kit/ndk'
|
||||
|
||||
export const RelaysPage = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { ndk, publish } = useNDKContext()
|
||||
const { getRelayInfo } = useDvm()
|
||||
|
||||
const usersPubkey = useAppSelector((state) => state.auth?.usersPubkey)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const [ndkRelayList, setNDKRelayList] = useState<NDKRelayList | null>(null)
|
||||
|
||||
const [newRelayURI, setNewRelayURI] = useState<string>()
|
||||
const [newRelayURIerror, setNewRelayURIerror] = useState<string>()
|
||||
@ -42,22 +61,74 @@ export const RelaysPage = () => {
|
||||
|
||||
const webSocketPrefix = 'wss://'
|
||||
|
||||
useDidMount(() => {
|
||||
// fetch relay list from relays
|
||||
useEffect(() => {
|
||||
if (usersPubkey) {
|
||||
getRelayMap(usersPubkey).then((newRelayMap) => {
|
||||
if (!compareObjects(relayMap, newRelayMap.map)) {
|
||||
dispatch(setRelayMapAction(newRelayMap.map))
|
||||
Promise.race([getRelayListForUser(usersPubkey, ndk), timeout(10000)])
|
||||
.then((res) => {
|
||||
setNDKRelayList(res)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(
|
||||
`An error occurred in fetching user relay list: ${
|
||||
err.message || err
|
||||
}`
|
||||
)
|
||||
setNDKRelayList(new NDKRelayList(ndk))
|
||||
})
|
||||
}
|
||||
}, [usersPubkey, ndk])
|
||||
|
||||
// construct the RelayMap from newly received NDKRelayList event
|
||||
// and compare it with existing relay map in redux store
|
||||
// if there are any differences then update the redux store with
|
||||
// new relay map
|
||||
useEffect(() => {
|
||||
if (ndkRelayList) {
|
||||
const newRelayMap: RelayMap = {}
|
||||
|
||||
ndkRelayList.readRelayUrls.forEach((relayUrl) => {
|
||||
const normalizedUrl = normalizeWebSocketURL(relayUrl)
|
||||
|
||||
newRelayMap[normalizedUrl] = {
|
||||
read: true,
|
||||
write: false
|
||||
}
|
||||
})
|
||||
|
||||
ndkRelayList.writeRelayUrls.forEach((relayUrl) => {
|
||||
const normalizedUrl = normalizeWebSocketURL(relayUrl)
|
||||
|
||||
const existing = newRelayMap[normalizedUrl]
|
||||
if (existing) {
|
||||
existing.write = true
|
||||
} else {
|
||||
newRelayMap[normalizedUrl] = {
|
||||
read: false,
|
||||
write: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!compareObjects(relayMap, newRelayMap)) {
|
||||
dispatch(setRelayMapAction(newRelayMap))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// we want to run this effect only when ndkRelayList is changed
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ndkRelayList])
|
||||
|
||||
useEffect(() => {
|
||||
if (!relayMap) return
|
||||
|
||||
// Display notification if an empty relay map has been received
|
||||
if (relayMap && Object.keys(relayMap).length === 0) {
|
||||
if (Object.keys(relayMap).length === 0) {
|
||||
relayRequirementWarning()
|
||||
} else {
|
||||
getRelayInfo(Object.keys(relayMap))
|
||||
}
|
||||
}, [relayMap])
|
||||
}, [relayMap, getRelayInfo])
|
||||
|
||||
const relayRequirementWarning = () =>
|
||||
toast.warning('At least one write relay is needed for SIGit to work.')
|
||||
@ -85,7 +156,8 @@ export const RelaysPage = () => {
|
||||
const relayMapPublishingRes = await publishRelayMap(
|
||||
relayMapCopy,
|
||||
usersPubkey,
|
||||
[relay]
|
||||
ndk,
|
||||
publish
|
||||
).catch((err) => handlePublishRelayMapError(err))
|
||||
|
||||
if (relayMapPublishingRes) {
|
||||
@ -132,7 +204,9 @@ export const RelaysPage = () => {
|
||||
// Publish updated relay map
|
||||
const relayMapPublishingRes = await publishRelayMap(
|
||||
relayMapCopy,
|
||||
usersPubkey
|
||||
usersPubkey,
|
||||
ndk,
|
||||
publish
|
||||
).catch((err) => handlePublishRelayMapError(err))
|
||||
|
||||
if (relayMapPublishingRes) {
|
||||
@ -161,9 +235,10 @@ export const RelaysPage = () => {
|
||||
)
|
||||
}
|
||||
} else if (relayURI && usersPubkey) {
|
||||
const relay = await relayController.connectRelay(relayURI)
|
||||
const ndkRelay = ndk.pool.getRelay(relayURI)
|
||||
await ndkRelay.connect(5000)
|
||||
|
||||
if (relay && relay.connected) {
|
||||
if (ndkRelay.status >= NDKRelayStatus.CONNECTED) {
|
||||
const relayMapCopy = JSON.parse(JSON.stringify(relayMap))
|
||||
|
||||
relayMapCopy[relayURI] = { write: true, read: true }
|
||||
@ -171,7 +246,9 @@ export const RelaysPage = () => {
|
||||
// Publish updated relay map
|
||||
const relayMapPublishingRes = await publishRelayMap(
|
||||
relayMapCopy,
|
||||
usersPubkey
|
||||
usersPubkey,
|
||||
ndk,
|
||||
publish
|
||||
).catch((err) => handlePublishRelayMapError(err))
|
||||
|
||||
if (relayMapPublishingRes) {
|
||||
@ -256,19 +333,36 @@ const RelayItem = ({
|
||||
handleLeaveRelay,
|
||||
handleRelayWriteChange
|
||||
}: RelayItemProp) => {
|
||||
const { ndk } = useNDKContext()
|
||||
|
||||
const [relayConnectionStatus, setRelayConnectionStatus] =
|
||||
useState<RelayConnectionState>()
|
||||
|
||||
const [displayRelayInfo, setDisplayRelayInfo] = useState(false)
|
||||
|
||||
useDidMount(() => {
|
||||
relayController.connectRelay(relayURI).then((relay) => {
|
||||
if (relay && relay.connected) {
|
||||
const ndkPool = ndk.pool
|
||||
|
||||
ndkPool.on('relay:connect', (relay) => {
|
||||
if (relay.url === relayURI) {
|
||||
setRelayConnectionStatus(RelayConnectionState.Connected)
|
||||
} else {
|
||||
}
|
||||
})
|
||||
|
||||
ndkPool.on('relay:disconnect', (relay) => {
|
||||
if (relay.url === relayURI) {
|
||||
setRelayConnectionStatus(RelayConnectionState.NotConnected)
|
||||
}
|
||||
})
|
||||
|
||||
const relay = ndkPool.getRelay(relayURI)
|
||||
if (relay) {
|
||||
setRelayConnectionStatus(
|
||||
relay.status >= NDKRelayStatus.CONNECTED
|
||||
? RelayConnectionState.Connected
|
||||
: RelayConnectionState.NotConnected
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
|
@ -1,3 +1,9 @@
|
||||
export enum UserRelaysType {
|
||||
Read = 'readRelayUrls',
|
||||
Write = 'writeRelayUrls',
|
||||
Both = 'bothRelayUrls'
|
||||
}
|
||||
|
||||
export interface RelaySet {
|
||||
read: string[]
|
||||
write: string[]
|
||||
|
@ -35,6 +35,7 @@ import { parseJson, removeLeadingSlash } from './string'
|
||||
import { timeout } from './utils'
|
||||
import { getHash } from './hash'
|
||||
import { SIGIT_BLOSSOM } from './const.ts'
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
|
||||
/**
|
||||
* Generates a `d` tag for userAppData
|
||||
@ -989,3 +990,24 @@ export const getProfileUsername = (
|
||||
truncate(profile?.display_name || profile?.name || hexToNpub(npub), {
|
||||
length: 16
|
||||
})
|
||||
|
||||
/**
|
||||
* Orders an array of NDKEvent objects chronologically based on their `created_at` property.
|
||||
*
|
||||
* @param events - The array of NDKEvent objects to be sorted.
|
||||
* @param reverse - Optional flag to reverse the sorting order.
|
||||
* If true, sorts in ascending order (oldest first), otherwise sorts in descending order (newest first).
|
||||
*
|
||||
* @returns The sorted array of events.
|
||||
*/
|
||||
export function orderEventsChronologically(
|
||||
events: NDKEvent[],
|
||||
reverse: boolean = false
|
||||
): NDKEvent[] {
|
||||
events.sort((e1: NDKEvent, e2: NDKEvent) => {
|
||||
if (reverse) return e1.created_at! - e2.created_at!
|
||||
else return e2.created_at! - e1.created_at!
|
||||
})
|
||||
|
||||
return events
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
ONE_WEEK_IN_MS,
|
||||
SIGIT_RELAY
|
||||
} from './const'
|
||||
import NDK, { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
|
||||
const READ_MARKER = 'read'
|
||||
const WRITE_MARKER = 'write'
|
||||
@ -176,7 +177,8 @@ const getRelayMap = async (
|
||||
const publishRelayMap = async (
|
||||
relayMap: RelayMap,
|
||||
npub: string,
|
||||
extraRelaysToPublish?: string[]
|
||||
ndk: NDK,
|
||||
publish: (event: NDKEvent) => Promise<string[]>
|
||||
): Promise<string> => {
|
||||
const timestamp = unixNow()
|
||||
const relayURIs = Object.keys(relayMap)
|
||||
@ -205,21 +207,8 @@ const publishRelayMap = async (
|
||||
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 = DEFAULT_LOOK_UP_RELAY_LIST
|
||||
}
|
||||
const publishResult = await relayController.publish(
|
||||
signedEvent,
|
||||
relaysToPublish
|
||||
)
|
||||
const ndkEvent = new NDKEvent(ndk, signedEvent)
|
||||
const publishResult = await publish(ndkEvent)
|
||||
|
||||
if (publishResult && publishResult.length) {
|
||||
return Promise.resolve(
|
||||
|
Loading…
Reference in New Issue
Block a user