chore: removed relay controller in favor of NDKContext
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 47s
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 47s
This commit is contained in:
parent
0ea6ba0033
commit
e8a53bc73e
@ -244,6 +244,9 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
let ndkRelaySet: NDKRelaySet | undefined
|
let ndkRelaySet: NDKRelaySet | undefined
|
||||||
|
|
||||||
if (explicitRelayUrls && explicitRelayUrls.length > 0) {
|
if (explicitRelayUrls && explicitRelayUrls.length > 0) {
|
||||||
|
if (!explicitRelayUrls.includes(SIGIT_RELAY)) {
|
||||||
|
explicitRelayUrls = [...explicitRelayUrls, SIGIT_RELAY]
|
||||||
|
}
|
||||||
ndkRelaySet = NDKRelaySet.fromRelayUrls(explicitRelayUrls, ndk)
|
ndkRelaySet = NDKRelaySet.fromRelayUrls(explicitRelayUrls, ndk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,242 +0,0 @@
|
|||||||
import { Event, Filter, Relay } from 'nostr-tools'
|
|
||||||
import {
|
|
||||||
settleAllFullfilfedPromises,
|
|
||||||
normalizeWebSocketURL,
|
|
||||||
timeout
|
|
||||||
} from '../utils'
|
|
||||||
import { SIGIT_RELAY } from '../utils/const'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Singleton class to manage relay operations.
|
|
||||||
*/
|
|
||||||
export class RelayController {
|
|
||||||
private static instance: RelayController
|
|
||||||
private pendingConnections = new Map<string, Promise<Relay | null>>() // Track pending connections
|
|
||||||
public connectedRelays = new Map<string, Relay>()
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the singleton instance of RelayController.
|
|
||||||
*
|
|
||||||
* @returns The singleton instance of RelayController.
|
|
||||||
*/
|
|
||||||
public static getInstance(): RelayController {
|
|
||||||
if (!RelayController.instance) {
|
|
||||||
RelayController.instance = new RelayController()
|
|
||||||
}
|
|
||||||
return RelayController.instance
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connects to a relay server if not already connected.
|
|
||||||
*
|
|
||||||
* This method checks if a relay with the given URL is already in the list of connected relays.
|
|
||||||
* If it is not connected, it attempts to establish a new connection.
|
|
||||||
* On successful connection, the relay is added to the list of connected relays and returned.
|
|
||||||
* If the connection fails, an error is logged and `null` is returned.
|
|
||||||
*
|
|
||||||
* @param relayUrl - The URL of the relay server to connect to.
|
|
||||||
* @returns A promise that resolves to the connected relay object if successful, or `null` if the connection fails.
|
|
||||||
*/
|
|
||||||
public connectRelay = async (relayUrl: string): Promise<Relay | null> => {
|
|
||||||
const normalizedWebSocketURL = normalizeWebSocketURL(relayUrl)
|
|
||||||
const relay = this.connectedRelays.get(normalizedWebSocketURL)
|
|
||||||
|
|
||||||
if (relay) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there's already a pending connection for this relay URL
|
|
||||||
if (this.pendingConnections.has(relayUrl)) {
|
|
||||||
// Return the existing promise to avoid making another connection
|
|
||||||
return this.pendingConnections.get(relayUrl)!
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new connection promise and store it in pendingConnections
|
|
||||||
const connectionPromise = Relay.connect(relayUrl)
|
|
||||||
.then((relay) => {
|
|
||||||
if (relay.connected) {
|
|
||||||
// Add the newly connected relay to the connected relays map
|
|
||||||
this.connectedRelays.set(relayUrl, relay)
|
|
||||||
|
|
||||||
// Return the newly connected relay
|
|
||||||
return relay
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// Log an error message if the connection fails
|
|
||||||
console.error(`Relay connection failed: ${relayUrl}`, err)
|
|
||||||
|
|
||||||
// Return null to indicate connection failure
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
// Remove the connection from pendingConnections once it settles
|
|
||||||
this.pendingConnections.delete(relayUrl)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.pendingConnections.set(relayUrl, connectionPromise)
|
|
||||||
return connectionPromise
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously retrieves multiple event from a set of relays based on a provided filter.
|
|
||||||
* If no relays are specified, it defaults to using connected relays.
|
|
||||||
*
|
|
||||||
* @param filter - The filter criteria to find the event.
|
|
||||||
* @param relays - An optional array of relay URLs to search for the event.
|
|
||||||
* @returns Returns a promise that resolves with an array of events.
|
|
||||||
*/
|
|
||||||
fetchEvents = async (
|
|
||||||
filter: Filter,
|
|
||||||
relayUrls: string[] = []
|
|
||||||
): Promise<Event[]> => {
|
|
||||||
if (!relayUrls.includes(SIGIT_RELAY)) {
|
|
||||||
/**
|
|
||||||
* NOTE: To avoid side-effects on external relayUrls array passed as argument
|
|
||||||
* re-assigned relayUrls with added sigit relay instead of just appending to same array
|
|
||||||
*/
|
|
||||||
|
|
||||||
relayUrls = [...relayUrls, SIGIT_RELAY] // Add app relay to relays array if not exists already
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect to all specified relays
|
|
||||||
const relays = await settleAllFullfilfedPromises(
|
|
||||||
relayUrls,
|
|
||||||
this.connectRelay
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if any relays are connected
|
|
||||||
if (relays.length === 0) {
|
|
||||||
throw new Error('No relay is connected to fetch events!')
|
|
||||||
}
|
|
||||||
|
|
||||||
const events: Event[] = []
|
|
||||||
const eventIds = new Set<string>() // To keep track of event IDs and avoid duplicates
|
|
||||||
|
|
||||||
// Create a promise for each relay subscription
|
|
||||||
const subPromises = relays.map((relay) => {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
if (!relay.connected) {
|
|
||||||
console.log(`${relay.url} : Not connected!`, 'Skipping subscription')
|
|
||||||
return resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to the relay with the specified filter
|
|
||||||
const sub = relay.subscribe([filter], {
|
|
||||||
// Handle incoming events
|
|
||||||
onevent: (e) => {
|
|
||||||
// Add the event to the array if it's not a duplicate
|
|
||||||
if (!eventIds.has(e.id)) {
|
|
||||||
eventIds.add(e.id) // Record the event ID
|
|
||||||
events.push(e) // Add the event to the array
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Handle the End-Of-Stream (EOSE) message
|
|
||||||
oneose: () => {
|
|
||||||
sub.close() // Close the subscription
|
|
||||||
resolve() // Resolve the promise when EOSE is received
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// add a 30 sec of timeout to subscription
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!sub.closed) {
|
|
||||||
sub.close()
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}, 30 * 1000)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for all subscriptions to complete
|
|
||||||
await Promise.allSettled(subPromises)
|
|
||||||
|
|
||||||
// It is possible that different relays will send different events and events array may contain more events then specified limit in filter
|
|
||||||
// To fix this issue we'll first sort these events and then return only limited events
|
|
||||||
if (filter.limit) {
|
|
||||||
// Sort events by creation date in descending order
|
|
||||||
events.sort((a, b) => b.created_at - a.created_at)
|
|
||||||
|
|
||||||
return events.slice(0, filter.limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
return events
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously retrieves an event from a set of relays based on a provided filter.
|
|
||||||
* If no relays are specified, it defaults to using connected relays.
|
|
||||||
*
|
|
||||||
* @param filter - The filter criteria to find the event.
|
|
||||||
* @param relays - An optional array of relay URLs to search for the event.
|
|
||||||
* @returns Returns a promise that resolves to the found event or null if not found.
|
|
||||||
*/
|
|
||||||
fetchEvent = async (
|
|
||||||
filter: Filter,
|
|
||||||
relays: string[] = []
|
|
||||||
): Promise<Event | null> => {
|
|
||||||
const events = await this.fetchEvents(filter, relays)
|
|
||||||
|
|
||||||
// Sort events by creation date in descending order
|
|
||||||
events.sort((a, b) => b.created_at - a.created_at)
|
|
||||||
|
|
||||||
// Return the most recent event, or null if no events were received
|
|
||||||
return events[0] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
publish = async (
|
|
||||||
event: Event,
|
|
||||||
relayUrls: string[] = []
|
|
||||||
): Promise<string[]> => {
|
|
||||||
if (!relayUrls.includes(SIGIT_RELAY)) {
|
|
||||||
/**
|
|
||||||
* NOTE: To avoid side-effects on external relayUrls array passed as argument
|
|
||||||
* re-assigned relayUrls with added sigit relay instead of just appending to same array
|
|
||||||
*/
|
|
||||||
relayUrls = [...relayUrls, SIGIT_RELAY] // Add app relay to relays array if not exists already
|
|
||||||
}
|
|
||||||
|
|
||||||
// connect to all specified relays
|
|
||||||
const relays = await settleAllFullfilfedPromises(
|
|
||||||
relayUrls,
|
|
||||||
this.connectRelay
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if any relays are connected
|
|
||||||
if (relays.length === 0) {
|
|
||||||
throw new Error('No relay is connected to publish event!')
|
|
||||||
}
|
|
||||||
|
|
||||||
const publishedOnRelays: string[] = [] // List to track which relays successfully published the event
|
|
||||||
|
|
||||||
// Create a promise for publishing the event to each connected relay
|
|
||||||
const publishPromises = relays.map(async (relay) => {
|
|
||||||
try {
|
|
||||||
await Promise.race([
|
|
||||||
relay.publish(event), // Publish the event to the relay
|
|
||||||
timeout(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
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Failed to publish event on relay: ${relay.url}`, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for all publish operations to complete (either fulfilled or rejected)
|
|
||||||
await Promise.allSettled(publishPromises)
|
|
||||||
|
|
||||||
// Return the list of relay URLs where the event was published
|
|
||||||
return publishedOnRelays
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const relayController = RelayController.getInstance()
|
|
@ -1,2 +1 @@
|
|||||||
export * from './NostrController'
|
export * from './NostrController'
|
||||||
export * from './RelayController'
|
|
||||||
|
@ -3,4 +3,5 @@ export * from './useAuth'
|
|||||||
export * from './useDidMount'
|
export * from './useDidMount'
|
||||||
export * from './useDvm'
|
export * from './useDvm'
|
||||||
export * from './useLogout'
|
export * from './useLogout'
|
||||||
|
export * from './useNDK'
|
||||||
export * from './useNDKContext'
|
export * from './useNDKContext'
|
||||||
|
415
src/hooks/useNDK.ts
Normal file
415
src/hooks/useNDK.ts
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
|
import { bytesToHex } from '@noble/hashes/utils'
|
||||||
|
import { NDKEvent, NDKFilter, NDKKind } from '@nostr-dev-kit/ndk'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import {
|
||||||
|
Event,
|
||||||
|
generateSecretKey,
|
||||||
|
getPublicKey,
|
||||||
|
kinds,
|
||||||
|
UnsignedEvent
|
||||||
|
} from 'nostr-tools'
|
||||||
|
|
||||||
|
import { useAppDispatch, useAppSelector, useNDKContext } from '.'
|
||||||
|
import { NostrController } from '../controllers'
|
||||||
|
import {
|
||||||
|
updateProcessedGiftWraps,
|
||||||
|
updateUserAppData as updateUserAppDataAction
|
||||||
|
} from '../store/actions'
|
||||||
|
import { Keys } from '../store/auth/types'
|
||||||
|
import { Meta, UserAppData, UserRelaysType } from '../types'
|
||||||
|
import {
|
||||||
|
countLeadingZeroes,
|
||||||
|
createWrap,
|
||||||
|
deleteBlossomFile,
|
||||||
|
getDTagForUserAppData,
|
||||||
|
getUserAppDataFromBlossom,
|
||||||
|
hexToNpub,
|
||||||
|
parseJson,
|
||||||
|
unixNow,
|
||||||
|
uploadUserAppDataToBlossom
|
||||||
|
} from '../utils'
|
||||||
|
|
||||||
|
export const useNDK = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { ndk, fetchEvent, fetchEventsFromUserRelays, publish } =
|
||||||
|
useNDKContext()
|
||||||
|
const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
|
||||||
|
const appData = useAppSelector((state) => state.userAppData)
|
||||||
|
const processedEvents = useAppSelector(
|
||||||
|
(state) => state.userAppData?.processedGiftWraps
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches user application data based on user's public key.
|
||||||
|
*
|
||||||
|
* @returns The user application data or null if an error occurs or no data is found.
|
||||||
|
*/
|
||||||
|
const getUsersAppData = useCallback(async (): Promise<UserAppData | null> => {
|
||||||
|
if (!usersPubkey) return null
|
||||||
|
|
||||||
|
// Generate an identifier for the user's nip78
|
||||||
|
const dTag = await getDTagForUserAppData()
|
||||||
|
if (!dTag) return null
|
||||||
|
|
||||||
|
// Define a filter for fetching events
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
kinds: [NDKKind.AppSpecificData],
|
||||||
|
authors: [usersPubkey],
|
||||||
|
'#d': [dTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedContent = await fetchEvent(filter)
|
||||||
|
.then((event) => {
|
||||||
|
if (event) return event.content
|
||||||
|
|
||||||
|
// If no event is found, return an empty stringified object
|
||||||
|
return '{}'
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// Log error and show a toast notification if fetching event fails
|
||||||
|
console.log(`An error occurred in finding kind 30078 event`, err)
|
||||||
|
toast.error(
|
||||||
|
'An error occurred in finding kind 30078 event for data storage'
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return null if encrypted content retrieval fails
|
||||||
|
if (!encryptedContent) return null
|
||||||
|
|
||||||
|
// Handle case where the encrypted content is an empty object
|
||||||
|
if (encryptedContent === '{}') {
|
||||||
|
// Generate ephemeral key pair
|
||||||
|
const secret = generateSecretKey()
|
||||||
|
const pubKey = getPublicKey(secret)
|
||||||
|
|
||||||
|
return {
|
||||||
|
sigits: {},
|
||||||
|
processedGiftWraps: [],
|
||||||
|
blossomUrls: [],
|
||||||
|
keyPair: {
|
||||||
|
private: bytesToHex(secret),
|
||||||
|
public: pubKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an instance of the NostrController
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
|
||||||
|
// Decrypt the encrypted content
|
||||||
|
const decrypted = await nostrController
|
||||||
|
.nip04Decrypt(usersPubkey, encryptedContent)
|
||||||
|
.catch((err) => {
|
||||||
|
// Log error and show a toast notification if decryption fails
|
||||||
|
console.log('An error occurred while decrypting app data', err)
|
||||||
|
toast.error('An error occurred while decrypting app data')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return null if decryption fails
|
||||||
|
if (!decrypted) return null
|
||||||
|
|
||||||
|
// Parse the decrypted content
|
||||||
|
const parsedContent = await parseJson<{
|
||||||
|
blossomUrls: string[]
|
||||||
|
keyPair: Keys
|
||||||
|
}>(decrypted).catch((err) => {
|
||||||
|
// Log error and show a toast notification if parsing fails
|
||||||
|
console.log(
|
||||||
|
'An error occurred in parsing the content of kind 30078 event',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
toast.error(
|
||||||
|
'An error occurred in parsing the content of kind 30078 event'
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return null if parsing fails
|
||||||
|
if (!parsedContent) return null
|
||||||
|
|
||||||
|
const { blossomUrls, keyPair } = parsedContent
|
||||||
|
|
||||||
|
// Return null if no blossom URLs are found
|
||||||
|
if (blossomUrls.length === 0) return null
|
||||||
|
|
||||||
|
// Fetch additional user app data from the first blossom URL
|
||||||
|
const dataFromBlossom = await getUserAppDataFromBlossom(
|
||||||
|
blossomUrls[0],
|
||||||
|
keyPair.private
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return null if fetching data from blossom fails
|
||||||
|
if (!dataFromBlossom) return null
|
||||||
|
|
||||||
|
const { sigits, processedGiftWraps } = dataFromBlossom
|
||||||
|
|
||||||
|
// Return the final user application data
|
||||||
|
return {
|
||||||
|
blossomUrls,
|
||||||
|
keyPair,
|
||||||
|
sigits,
|
||||||
|
processedGiftWraps
|
||||||
|
}
|
||||||
|
}, [usersPubkey, fetchEvent])
|
||||||
|
|
||||||
|
const updateUsersAppData = useCallback(
|
||||||
|
async (meta: Meta) => {
|
||||||
|
if (!appData || !appData.keyPair || !usersPubkey) return null
|
||||||
|
|
||||||
|
const sigits = _.cloneDeep(appData.sigits)
|
||||||
|
|
||||||
|
const createSignatureEvent = await parseJson<Event>(
|
||||||
|
meta.createSignature
|
||||||
|
).catch((err) => {
|
||||||
|
console.log('err in parsing the createSignature event:>> ', err)
|
||||||
|
toast.error(
|
||||||
|
err.message || 'error occurred in parsing the create signature event'
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!createSignatureEvent) return null
|
||||||
|
|
||||||
|
const id = createSignatureEvent.id
|
||||||
|
let isUpdated = false
|
||||||
|
|
||||||
|
// check if sigit already exists
|
||||||
|
if (id in sigits) {
|
||||||
|
// update meta only if incoming meta is more recent
|
||||||
|
// than already existing one
|
||||||
|
const existingMeta = sigits[id]
|
||||||
|
if (existingMeta.modifiedAt < meta.modifiedAt) {
|
||||||
|
sigits[id] = meta
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sigits[id] = meta
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUpdated) return null
|
||||||
|
|
||||||
|
const blossomUrls = [...appData.blossomUrls]
|
||||||
|
|
||||||
|
const newBlossomUrl = await uploadUserAppDataToBlossom(
|
||||||
|
sigits,
|
||||||
|
appData.processedGiftWraps,
|
||||||
|
appData.keyPair.private
|
||||||
|
).catch((err) => {
|
||||||
|
console.log(
|
||||||
|
'An error occurred in uploading user app data file to blossom server',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
toast.error(
|
||||||
|
'An error occurred in uploading user app data file to blossom server'
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!newBlossomUrl) return null
|
||||||
|
|
||||||
|
// insert new blossom url at the start of the array
|
||||||
|
blossomUrls.unshift(newBlossomUrl)
|
||||||
|
|
||||||
|
// only keep last 10 blossom urls, delete older ones
|
||||||
|
if (blossomUrls.length > 10) {
|
||||||
|
const filesToDelete = blossomUrls.splice(10)
|
||||||
|
filesToDelete.forEach((url) => {
|
||||||
|
deleteBlossomFile(url, appData.keyPair!.private).catch((err) => {
|
||||||
|
console.log(
|
||||||
|
'An error occurred in removing old file of user app data from blossom server',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt content for storing in kind 30078 event
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
const encryptedContent = await nostrController
|
||||||
|
.nip04Encrypt(
|
||||||
|
usersPubkey,
|
||||||
|
JSON.stringify({
|
||||||
|
blossomUrls,
|
||||||
|
keyPair: appData.keyPair
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(
|
||||||
|
'An error occurred in encryption of content for app data',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
toast.error(
|
||||||
|
err.message ||
|
||||||
|
'An error occurred in encryption of content for app data'
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!encryptedContent) return null
|
||||||
|
|
||||||
|
// generate the identifier for user's appData event
|
||||||
|
const dTag = await getDTagForUserAppData()
|
||||||
|
if (!dTag) return null
|
||||||
|
|
||||||
|
const updatedEvent: UnsignedEvent = {
|
||||||
|
kind: kinds.Application,
|
||||||
|
pubkey: usersPubkey,
|
||||||
|
created_at: unixNow(),
|
||||||
|
tags: [['d', dTag]],
|
||||||
|
content: encryptedContent
|
||||||
|
}
|
||||||
|
|
||||||
|
const signedEvent = await nostrController
|
||||||
|
.signEvent(updatedEvent)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('An error occurred in signing event', err)
|
||||||
|
toast.error(err.message || 'An error occurred in signing event')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!signedEvent) return null
|
||||||
|
|
||||||
|
const ndkEvent = new NDKEvent(ndk, signedEvent)
|
||||||
|
const publishResult = await publish(ndkEvent)
|
||||||
|
|
||||||
|
if (publishResult.length === 0) {
|
||||||
|
toast.error(
|
||||||
|
'An unexpected error occurred in publishing updated app data '
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!publishResult) return null
|
||||||
|
|
||||||
|
// update redux store
|
||||||
|
dispatch(
|
||||||
|
updateUserAppDataAction({
|
||||||
|
sigits,
|
||||||
|
blossomUrls,
|
||||||
|
processedGiftWraps: [...appData.processedGiftWraps],
|
||||||
|
keyPair: {
|
||||||
|
...appData.keyPair
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return signedEvent
|
||||||
|
},
|
||||||
|
[appData, dispatch, ndk, publish, usersPubkey]
|
||||||
|
)
|
||||||
|
|
||||||
|
const processReceivedEvent = useCallback(
|
||||||
|
async (event: NDKEvent, difficulty: number = 5) => {
|
||||||
|
// Abort processing if userAppData is undefined
|
||||||
|
if (!processedEvents) return
|
||||||
|
|
||||||
|
if (processedEvents.includes(event.id)) return
|
||||||
|
|
||||||
|
dispatch(updateProcessedGiftWraps([...processedEvents, event.id]))
|
||||||
|
|
||||||
|
// validate PoW
|
||||||
|
// Count the number of leading zero bits in the hash
|
||||||
|
const leadingZeroes = countLeadingZeroes(event.id)
|
||||||
|
if (leadingZeroes < difficulty) return
|
||||||
|
|
||||||
|
// decrypt the content of gift wrap event
|
||||||
|
const nostrController = NostrController.getInstance()
|
||||||
|
const decrypted = await nostrController.nip44Decrypt(
|
||||||
|
event.pubkey,
|
||||||
|
event.content
|
||||||
|
)
|
||||||
|
|
||||||
|
const internalUnsignedEvent = await parseJson<UnsignedEvent>(
|
||||||
|
decrypted
|
||||||
|
).catch((err) => {
|
||||||
|
console.log(
|
||||||
|
'An error occurred in parsing the internal unsigned event',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938) return
|
||||||
|
|
||||||
|
const meta = await parseJson<Meta>(internalUnsignedEvent.content).catch(
|
||||||
|
(err) => {
|
||||||
|
console.log(
|
||||||
|
'An error occurred in parsing the internal unsigned event',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!meta) return
|
||||||
|
|
||||||
|
await updateUsersAppData(meta)
|
||||||
|
},
|
||||||
|
[dispatch, processedEvents, updateUsersAppData]
|
||||||
|
)
|
||||||
|
|
||||||
|
const subscribeForSigits = useCallback(
|
||||||
|
async (pubkey: string) => {
|
||||||
|
// Define the filter for the subscription
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
kinds: [1059 as NDKKind],
|
||||||
|
'#p': [pubkey]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the received event synchronously
|
||||||
|
const events = await fetchEventsFromUserRelays(
|
||||||
|
filter,
|
||||||
|
pubkey,
|
||||||
|
UserRelaysType.Read
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const e of events) {
|
||||||
|
await processReceivedEvent(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fetchEventsFromUserRelays, processReceivedEvent]
|
||||||
|
)
|
||||||
|
|
||||||
|
const sendNotification = useCallback(
|
||||||
|
async (receiver: string, meta: Meta) => {
|
||||||
|
if (!usersPubkey) return
|
||||||
|
|
||||||
|
// Create an unsigned event object with the provided metadata
|
||||||
|
const unsignedEvent: UnsignedEvent = {
|
||||||
|
kind: 938,
|
||||||
|
pubkey: usersPubkey,
|
||||||
|
content: JSON.stringify(meta),
|
||||||
|
tags: [],
|
||||||
|
created_at: unixNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the unsigned event with the receiver's information
|
||||||
|
const wrappedEvent = createWrap(unsignedEvent, receiver)
|
||||||
|
|
||||||
|
// Publish the notification event to the recipient's read relays
|
||||||
|
const ndkEvent = new NDKEvent(ndk, wrappedEvent)
|
||||||
|
await publish(ndkEvent).catch((err) => {
|
||||||
|
// Log an error if publishing the notification event fails
|
||||||
|
console.log(
|
||||||
|
`An error occurred while publishing notification event for ${hexToNpub(receiver)}`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[ndk, publish, usersPubkey]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
getUsersAppData,
|
||||||
|
subscribeForSigits,
|
||||||
|
updateUsersAppData,
|
||||||
|
sendNotification
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ import {
|
|||||||
useAppSelector,
|
useAppSelector,
|
||||||
useAuth,
|
useAuth,
|
||||||
useLogout,
|
useLogout,
|
||||||
|
useNDK,
|
||||||
useNDKContext
|
useNDKContext
|
||||||
} from '../hooks'
|
} from '../hooks'
|
||||||
|
|
||||||
@ -30,12 +31,7 @@ import {
|
|||||||
import { LoginMethod } from '../store/auth/types'
|
import { LoginMethod } from '../store/auth/types'
|
||||||
import { setUserRobotImage } from '../store/userRobotImage/action'
|
import { setUserRobotImage } from '../store/userRobotImage/action'
|
||||||
|
|
||||||
import {
|
import { getRoboHashPicture, loadState } from '../utils'
|
||||||
getRoboHashPicture,
|
|
||||||
getUsersAppData,
|
|
||||||
loadState,
|
|
||||||
subscribeForSigits
|
|
||||||
} from '../utils'
|
|
||||||
|
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
|
|
||||||
@ -44,8 +40,9 @@ export const MainLayout = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const logout = useLogout()
|
const logout = useLogout()
|
||||||
const { findMetadata, getNDKRelayList } = useNDKContext()
|
const { findMetadata } = useNDKContext()
|
||||||
const { authAndGetMetadataAndRelaysMap } = useAuth()
|
const { authAndGetMetadataAndRelaysMap } = useAuth()
|
||||||
|
const { getUsersAppData, subscribeForSigits } = useNDK()
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState(`Loading App`)
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState(`Loading App`)
|
||||||
@ -191,13 +188,13 @@ export const MainLayout = () => {
|
|||||||
if (pubkey && !hasSubscribed.current) {
|
if (pubkey && !hasSubscribed.current) {
|
||||||
// Call `subscribeForSigits` only if it hasn't been called before
|
// Call `subscribeForSigits` only if it hasn't been called before
|
||||||
// #193 disabled websocket subscribtion, until #194 is done
|
// #193 disabled websocket subscribtion, until #194 is done
|
||||||
subscribeForSigits(pubkey, getNDKRelayList)
|
subscribeForSigits(pubkey)
|
||||||
|
|
||||||
// Mark `subscribeForSigits` as called
|
// Mark `subscribeForSigits` as called
|
||||||
hasSubscribed.current = true
|
hasSubscribed.current = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [authState, isLoggedIn, usersAppData, getNDKRelayList])
|
}, [authState, isLoggedIn, usersAppData, subscribeForSigits])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When authState change user logged in / or app reloaded
|
* When authState change user logged in / or app reloaded
|
||||||
@ -214,7 +211,7 @@ export const MainLayout = () => {
|
|||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc(`Loading SIGit history...`)
|
setLoadingSpinnerDesc(`Loading SIGit history...`)
|
||||||
getUsersAppData(getNDKRelayList)
|
getUsersAppData()
|
||||||
.then((appData) => {
|
.then((appData) => {
|
||||||
if (appData) {
|
if (appData) {
|
||||||
dispatch(updateUserAppData(appData))
|
dispatch(updateUserAppData(appData))
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
import type { Identifier, XYCoord } from 'dnd-core'
|
import type { Identifier, XYCoord } from 'dnd-core'
|
||||||
import saveAs from 'file-saver'
|
import saveAs from 'file-saver'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import { Event } from 'nostr-tools'
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { DndProvider, useDrag, useDrop } from 'react-dnd'
|
import { DndProvider, useDrag, useDrop } from 'react-dnd'
|
||||||
import { MultiBackend } from 'react-dnd-multi-backend'
|
import { MultiBackend } from 'react-dnd-multi-backend'
|
||||||
@ -20,7 +19,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { UserAvatar } from '../../components/UserAvatar'
|
import { UserAvatar } from '../../components/UserAvatar'
|
||||||
import { NostrController, RelayController } from '../../controllers'
|
import { NostrController } from '../../controllers'
|
||||||
import { appPrivateRoutes } from '../../routes'
|
import { appPrivateRoutes } from '../../routes'
|
||||||
import {
|
import {
|
||||||
CreateSignatureEventContent,
|
CreateSignatureEventContent,
|
||||||
@ -28,6 +27,7 @@ import {
|
|||||||
Meta,
|
Meta,
|
||||||
SignedEvent,
|
SignedEvent,
|
||||||
User,
|
User,
|
||||||
|
UserRelaysType,
|
||||||
UserRole
|
UserRole
|
||||||
} from '../../types'
|
} from '../../types'
|
||||||
import {
|
import {
|
||||||
@ -42,9 +42,7 @@ import {
|
|||||||
unixNow,
|
unixNow,
|
||||||
npubToHex,
|
npubToHex,
|
||||||
queryNip05,
|
queryNip05,
|
||||||
sendNotification,
|
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
updateUsersAppData,
|
|
||||||
uploadToFileStorage,
|
uploadToFileStorage,
|
||||||
DEFAULT_TOOLBOX,
|
DEFAULT_TOOLBOX,
|
||||||
settleAllFullfilfedPromises
|
settleAllFullfilfedPromises
|
||||||
@ -75,15 +73,17 @@ import { Autocomplete } from '@mui/lab'
|
|||||||
import _, { truncate } from 'lodash'
|
import _, { truncate } from 'lodash'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { AvatarIconButton } from '../../components/UserAvatarIconButton'
|
import { AvatarIconButton } from '../../components/UserAvatarIconButton'
|
||||||
import { NDKUserProfile } from '@nostr-dev-kit/ndk'
|
import { NDKUserProfile, NostrEvent } from '@nostr-dev-kit/ndk'
|
||||||
import { useNDKContext } from '../../hooks/useNDKContext.ts'
|
import { useNDKContext } from '../../hooks/useNDKContext.ts'
|
||||||
|
import { useNDK } from '../../hooks/useNDK.ts'
|
||||||
|
|
||||||
type FoundUser = Event & { npub: string }
|
type FoundUser = NostrEvent & { npub: string }
|
||||||
|
|
||||||
export const CreatePage = () => {
|
export const CreatePage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { findMetadata, getNDKRelayList } = useNDKContext()
|
const { findMetadata, fetchEventsFromUserRelays } = useNDKContext()
|
||||||
|
const { updateUsersAppData, sendNotification } = useNDK()
|
||||||
|
|
||||||
const { uploadedFiles } = location.state || {}
|
const { uploadedFiles } = location.state || {}
|
||||||
const [currentFile, setCurrentFile] = useState<File>()
|
const [currentFile, setCurrentFile] = useState<File>()
|
||||||
@ -155,24 +155,20 @@ export const CreatePage = () => {
|
|||||||
|
|
||||||
setSearchUsersLoading(true)
|
setSearchUsersLoading(true)
|
||||||
|
|
||||||
const relayController = RelayController.getInstance()
|
|
||||||
|
|
||||||
const searchTerm = searchString.trim()
|
const searchTerm = searchString.trim()
|
||||||
|
|
||||||
const ndkRelayList = await getNDKRelayList(usersPubkey)
|
fetchEventsFromUserRelays(
|
||||||
|
{
|
||||||
relayController
|
kinds: [0],
|
||||||
.fetchEvents(
|
search: searchTerm
|
||||||
{
|
},
|
||||||
kinds: [0],
|
usersPubkey,
|
||||||
search: searchTerm
|
UserRelaysType.Write
|
||||||
},
|
)
|
||||||
[...ndkRelayList.writeRelayUrls]
|
|
||||||
)
|
|
||||||
.then((events) => {
|
.then((events) => {
|
||||||
console.log('events', events)
|
const nostrEvents = events.map((event) => event.rawEvent())
|
||||||
|
|
||||||
const fineFilteredEvents: FoundUser[] = events
|
const fineFilteredEvents = nostrEvents
|
||||||
.filter((event) => {
|
.filter((event) => {
|
||||||
const lowercaseContent = event.content.toLowerCase()
|
const lowercaseContent = event.content.toLowerCase()
|
||||||
|
|
||||||
@ -189,15 +185,15 @@ export const CreatePage = () => {
|
|||||||
lowercaseContent.includes(`"nip05":"${searchTerm.toLowerCase()}"`)
|
lowercaseContent.includes(`"nip05":"${searchTerm.toLowerCase()}"`)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.reduce((uniqueEvents: FoundUser[], event: Event) => {
|
.reduce((uniqueEvents, event) => {
|
||||||
if (!uniqueEvents.some((e: Event) => e.pubkey === event.pubkey)) {
|
if (!uniqueEvents.some((e) => e.pubkey === event.pubkey)) {
|
||||||
uniqueEvents.push({
|
uniqueEvents.push({
|
||||||
...event,
|
...event,
|
||||||
npub: hexToNpub(event.pubkey)
|
npub: hexToNpub(event.pubkey)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return uniqueEvents
|
return uniqueEvents
|
||||||
}, [])
|
}, [] as FoundUser[])
|
||||||
|
|
||||||
console.log('fineFilteredEvents', fineFilteredEvents)
|
console.log('fineFilteredEvents', fineFilteredEvents)
|
||||||
setFoundUsers(fineFilteredEvents)
|
setFoundUsers(fineFilteredEvents)
|
||||||
@ -773,9 +769,7 @@ export const CreatePage = () => {
|
|||||||
: viewers.map((viewer) => viewer.pubkey)
|
: viewers.map((viewer) => viewer.pubkey)
|
||||||
).filter((receiver) => receiver !== usersPubkey)
|
).filter((receiver) => receiver !== usersPubkey)
|
||||||
|
|
||||||
return receivers.map((receiver) =>
|
return receivers.map((receiver) => sendNotification(receiver, meta))
|
||||||
sendNotification(receiver, meta, getNDKRelayList)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractNostrId = (stringifiedEvent: string): string => {
|
const extractNostrId = (stringifiedEvent: string): string => {
|
||||||
@ -965,12 +959,11 @@ export const CreatePage = () => {
|
|||||||
setUserSearchInput(value)
|
setUserSearchInput(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseContent = (event: Event) => {
|
const parseContent = (event: NostrEvent) => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(event.content)
|
return JSON.parse(event.content)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return undefined
|
return undefined
|
||||||
console.error(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,17 +21,12 @@ import {
|
|||||||
useNDKContext
|
useNDKContext
|
||||||
} from '../../../hooks'
|
} from '../../../hooks'
|
||||||
import { setRelayMapAction } from '../../../store/actions'
|
import { setRelayMapAction } from '../../../store/actions'
|
||||||
import {
|
import { RelayConnectionState, RelayFee, RelayInfo } from '../../../types'
|
||||||
RelayConnectionState,
|
|
||||||
RelayFee,
|
|
||||||
RelayInfo,
|
|
||||||
RelayMap
|
|
||||||
} from '../../../types'
|
|
||||||
import {
|
import {
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
compareObjects,
|
compareObjects,
|
||||||
|
getRelayMapFromNDKRelayList,
|
||||||
hexToNpub,
|
hexToNpub,
|
||||||
normalizeWebSocketURL,
|
|
||||||
publishRelayMap,
|
publishRelayMap,
|
||||||
shorten,
|
shorten,
|
||||||
timeout
|
timeout
|
||||||
@ -85,30 +80,7 @@ export const RelaysPage = () => {
|
|||||||
// new relay map
|
// new relay map
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ndkRelayList) {
|
if (ndkRelayList) {
|
||||||
const newRelayMap: RelayMap = {}
|
const newRelayMap = getRelayMapFromNDKRelayList(ndkRelayList)
|
||||||
|
|
||||||
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)) {
|
if (!compareObjects(relayMap, newRelayMap)) {
|
||||||
dispatch(setRelayMapAction(newRelayMap))
|
dispatch(setRelayMapAction(newRelayMap))
|
||||||
|
@ -29,9 +29,7 @@ import {
|
|||||||
npubToHex,
|
npubToHex,
|
||||||
parseJson,
|
parseJson,
|
||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
sendNotification,
|
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
updateUsersAppData,
|
|
||||||
findOtherUserMarks,
|
findOtherUserMarks,
|
||||||
timeout,
|
timeout,
|
||||||
processMarks
|
processMarks
|
||||||
@ -56,7 +54,7 @@ import {
|
|||||||
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
|
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
|
||||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||||
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
import { MARK_TYPE_CONFIG } from '../../components/MarkTypeStrategy/MarkStrategy.tsx'
|
||||||
import { useNDKContext } from '../../hooks/useNDKContext.ts'
|
import { useNDK } from '../../hooks/useNDK.ts'
|
||||||
|
|
||||||
enum SignedStatus {
|
enum SignedStatus {
|
||||||
Fully_Signed,
|
Fully_Signed,
|
||||||
@ -68,7 +66,7 @@ export const SignPage = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const { getNDKRelayList } = useNDKContext()
|
const { updateUsersAppData, sendNotification } = useNDK()
|
||||||
|
|
||||||
const usersAppData = useAppSelector((state) => state.userAppData)
|
const usersAppData = useAppSelector((state) => state.userAppData)
|
||||||
|
|
||||||
@ -783,7 +781,7 @@ export const SignPage = () => {
|
|||||||
setLoadingSpinnerDesc('Sending notifications')
|
setLoadingSpinnerDesc('Sending notifications')
|
||||||
const users = Array.from(userSet)
|
const users = Array.from(userSet)
|
||||||
const promises = users.map((user) =>
|
const promises = users.map((user) =>
|
||||||
sendNotification(npubToHex(user)!, meta, getNDKRelayList)
|
sendNotification(npubToHex(user)!, meta)
|
||||||
)
|
)
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -21,15 +21,13 @@ import {
|
|||||||
readContentOfZipEntry,
|
readContentOfZipEntry,
|
||||||
signEventForMetaFile,
|
signEventForMetaFile,
|
||||||
getCurrentUserFiles,
|
getCurrentUserFiles,
|
||||||
updateUsersAppData,
|
npubToHex
|
||||||
npubToHex,
|
|
||||||
sendNotification
|
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import styles from './style.module.scss'
|
import styles from './style.module.scss'
|
||||||
import { useLocation, useParams } from 'react-router-dom'
|
import { useLocation, useParams } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
||||||
import { useAppSelector, useNDKContext } from '../../hooks'
|
import { useAppSelector, useNDK } from '../../hooks'
|
||||||
import { getLastSignersSig } from '../../utils/sign.ts'
|
import { getLastSignersSig } from '../../utils/sign.ts'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { Container } from '../../components/Container'
|
import { Container } from '../../components/Container'
|
||||||
@ -166,7 +164,7 @@ const SlimPdfView = ({
|
|||||||
export const VerifyPage = () => {
|
export const VerifyPage = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const { getNDKRelayList } = useNDKContext()
|
const { updateUsersAppData, sendNotification } = useNDK()
|
||||||
|
|
||||||
const usersAppData = useAppSelector((state) => state.userAppData)
|
const usersAppData = useAppSelector((state) => state.userAppData)
|
||||||
const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
|
const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
|
||||||
@ -365,7 +363,7 @@ export const VerifyPage = () => {
|
|||||||
|
|
||||||
const users = Array.from(userSet)
|
const users = Array.from(userSet)
|
||||||
const promises = users.map((user) =>
|
const promises = users.map((user) =>
|
||||||
sendNotification(npubToHex(user)!, updatedMeta, getNDKRelayList)
|
sendNotification(npubToHex(user)!, updatedMeta)
|
||||||
)
|
)
|
||||||
|
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
import { hexToBytes } from '@noble/hashes/utils'
|
||||||
|
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import _, { truncate } from 'lodash'
|
import { truncate } from 'lodash'
|
||||||
import {
|
import {
|
||||||
Event,
|
Event,
|
||||||
EventTemplate,
|
EventTemplate,
|
||||||
Filter,
|
|
||||||
UnsignedEvent,
|
UnsignedEvent,
|
||||||
finalizeEvent,
|
finalizeEvent,
|
||||||
generateSecretKey,
|
generateSecretKey,
|
||||||
getEventHash,
|
getEventHash,
|
||||||
getPublicKey,
|
getPublicKey,
|
||||||
kinds,
|
|
||||||
nip04,
|
nip04,
|
||||||
nip19,
|
nip19,
|
||||||
nip44,
|
nip44,
|
||||||
@ -18,25 +17,16 @@ import {
|
|||||||
} from 'nostr-tools'
|
} from 'nostr-tools'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { NIP05_REGEX } from '../constants'
|
import { NIP05_REGEX } from '../constants'
|
||||||
import { NostrController, relayController } from '../controllers'
|
|
||||||
import {
|
|
||||||
updateProcessedGiftWraps,
|
|
||||||
updateUserAppData as updateUserAppDataAction
|
|
||||||
} from '../store/actions'
|
|
||||||
import { Keys } from '../store/auth/types'
|
|
||||||
import store from '../store/store'
|
import store from '../store/store'
|
||||||
import { Meta, ProfileMetadata, SignedEvent, UserAppData } from '../types'
|
import { Meta, ProfileMetadata, SignedEvent } from '../types'
|
||||||
import { getDefaultRelayMap } from './relays'
|
|
||||||
import { parseJson, removeLeadingSlash } from './string'
|
|
||||||
import { timeout } from './utils'
|
|
||||||
import { getHash } from './hash'
|
|
||||||
import { SIGIT_BLOSSOM } from './const.ts'
|
import { SIGIT_BLOSSOM } from './const.ts'
|
||||||
import { Hexpubkey, NDKEvent, NDKRelayList } from '@nostr-dev-kit/ndk'
|
import { getHash } from './hash'
|
||||||
|
import { parseJson, removeLeadingSlash } from './string'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a `d` tag for userAppData
|
* Generates a `d` tag for userAppData
|
||||||
*/
|
*/
|
||||||
const getDTagForUserAppData = async (): Promise<string | null> => {
|
export const getDTagForUserAppData = async (): Promise<string | null> => {
|
||||||
const isLoggedIn = store.getState().auth.loggedIn
|
const isLoggedIn = store.getState().auth.loggedIn
|
||||||
const pubkey = store.getState().auth?.usersPubkey
|
const pubkey = store.getState().auth?.usersPubkey
|
||||||
|
|
||||||
@ -325,309 +315,7 @@ export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const deleteBlossomFile = async (url: string, privateKey: string) => {
|
||||||
* Fetches user application data based on user's public key and stored metadata.
|
|
||||||
*
|
|
||||||
* @returns The user application data or null if an error occurs or no data is found.
|
|
||||||
*/
|
|
||||||
export const getUsersAppData = async (
|
|
||||||
getNDKRelayList: (pubkey: Hexpubkey) => Promise<NDKRelayList>
|
|
||||||
): Promise<UserAppData | null> => {
|
|
||||||
// Initialize an array to hold relay URLs
|
|
||||||
const relays: string[] = []
|
|
||||||
|
|
||||||
// Retrieve the user's public key and relay map from the Redux store
|
|
||||||
const usersPubkey = store.getState().auth.usersPubkey!
|
|
||||||
const relayMap = store.getState().relays?.map
|
|
||||||
|
|
||||||
// Check if relayMap is undefined in the Redux store
|
|
||||||
if (!relayMap) {
|
|
||||||
// If relayMap is not present, get relay list using NDKContext
|
|
||||||
|
|
||||||
const ndkRelayList = await getNDKRelayList(usersPubkey)
|
|
||||||
|
|
||||||
// Ensure that the relay list is not empty
|
|
||||||
if (ndkRelayList.writeRelayUrls.length === 0) return null
|
|
||||||
|
|
||||||
// Add write relays to the relays array
|
|
||||||
relays.push(...ndkRelayList.writeRelayUrls)
|
|
||||||
|
|
||||||
// // Ensure that the relay list is not empty
|
|
||||||
} else {
|
|
||||||
// If relayMap exists, filter and add write relays from the stored map
|
|
||||||
const writeRelays = Object.keys(relayMap).filter(
|
|
||||||
(key) => relayMap[key].write
|
|
||||||
)
|
|
||||||
relays.push(...writeRelays)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate an identifier for the user's nip78
|
|
||||||
const dTag = await getDTagForUserAppData()
|
|
||||||
if (!dTag) return null
|
|
||||||
|
|
||||||
// Define a filter for fetching events
|
|
||||||
const filter: Filter = {
|
|
||||||
kinds: [kinds.Application],
|
|
||||||
'#d': [dTag]
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptedContent = await relayController
|
|
||||||
.fetchEvent(filter, relays)
|
|
||||||
.then((event) => {
|
|
||||||
if (event) return event.content
|
|
||||||
|
|
||||||
// If no event is found, return an empty stringified object
|
|
||||||
return '{}'
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// Log error and show a toast notification if fetching event fails
|
|
||||||
console.log(`An error occurred in finding kind 30078 event`, err)
|
|
||||||
toast.error(
|
|
||||||
'An error occurred in finding kind 30078 event for data storage'
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return null if encrypted content retrieval fails
|
|
||||||
if (!encryptedContent) return null
|
|
||||||
|
|
||||||
// Handle case where the encrypted content is an empty object
|
|
||||||
if (encryptedContent === '{}') {
|
|
||||||
// Generate ephemeral key pair
|
|
||||||
const secret = generateSecretKey()
|
|
||||||
const pubKey = getPublicKey(secret)
|
|
||||||
|
|
||||||
return {
|
|
||||||
sigits: {},
|
|
||||||
processedGiftWraps: [],
|
|
||||||
blossomUrls: [],
|
|
||||||
keyPair: {
|
|
||||||
private: bytesToHex(secret),
|
|
||||||
public: pubKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get an instance of the NostrController
|
|
||||||
const nostrController = NostrController.getInstance()
|
|
||||||
|
|
||||||
// Decrypt the encrypted content
|
|
||||||
const decrypted = await nostrController
|
|
||||||
.nip04Decrypt(usersPubkey, encryptedContent)
|
|
||||||
.catch((err) => {
|
|
||||||
// Log error and show a toast notification if decryption fails
|
|
||||||
console.log('An error occurred while decrypting app data', err)
|
|
||||||
toast.error('An error occurred while decrypting app data')
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return null if decryption fails
|
|
||||||
if (!decrypted) return null
|
|
||||||
|
|
||||||
// Parse the decrypted content
|
|
||||||
const parsedContent = await parseJson<{
|
|
||||||
blossomUrls: string[]
|
|
||||||
keyPair: Keys
|
|
||||||
}>(decrypted).catch((err) => {
|
|
||||||
// Log error and show a toast notification if parsing fails
|
|
||||||
console.log(
|
|
||||||
'An error occurred in parsing the content of kind 30078 event',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
toast.error('An error occurred in parsing the content of kind 30078 event')
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return null if parsing fails
|
|
||||||
if (!parsedContent) return null
|
|
||||||
|
|
||||||
const { blossomUrls, keyPair } = parsedContent
|
|
||||||
|
|
||||||
// Return null if no blossom URLs are found
|
|
||||||
if (blossomUrls.length === 0) return null
|
|
||||||
|
|
||||||
// Fetch additional user app data from the first blossom URL
|
|
||||||
const dataFromBlossom = await getUserAppDataFromBlossom(
|
|
||||||
blossomUrls[0],
|
|
||||||
keyPair.private
|
|
||||||
)
|
|
||||||
|
|
||||||
// Return null if fetching data from blossom fails
|
|
||||||
if (!dataFromBlossom) return null
|
|
||||||
|
|
||||||
const { sigits, processedGiftWraps } = dataFromBlossom
|
|
||||||
|
|
||||||
// Return the final user application data
|
|
||||||
return {
|
|
||||||
blossomUrls,
|
|
||||||
keyPair,
|
|
||||||
sigits,
|
|
||||||
processedGiftWraps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateUsersAppData = async (meta: Meta) => {
|
|
||||||
const appData = store.getState().userAppData
|
|
||||||
if (!appData || !appData.keyPair) return null
|
|
||||||
|
|
||||||
const sigits = _.cloneDeep(appData.sigits)
|
|
||||||
|
|
||||||
const createSignatureEvent = await parseJson<Event>(
|
|
||||||
meta.createSignature
|
|
||||||
).catch((err) => {
|
|
||||||
console.log('err in parsing the createSignature event:>> ', err)
|
|
||||||
toast.error(
|
|
||||||
err.message || 'error occurred in parsing the create signature event'
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!createSignatureEvent) return null
|
|
||||||
|
|
||||||
const id = createSignatureEvent.id
|
|
||||||
let isUpdated = false
|
|
||||||
|
|
||||||
// check if sigit already exists
|
|
||||||
if (id in sigits) {
|
|
||||||
// update meta only if incoming meta is more recent
|
|
||||||
// than already existing one
|
|
||||||
const existingMeta = sigits[id]
|
|
||||||
if (existingMeta.modifiedAt < meta.modifiedAt) {
|
|
||||||
sigits[id] = meta
|
|
||||||
isUpdated = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sigits[id] = meta
|
|
||||||
isUpdated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isUpdated) return null
|
|
||||||
|
|
||||||
const blossomUrls = [...appData.blossomUrls]
|
|
||||||
|
|
||||||
const newBlossomUrl = await uploadUserAppDataToBlossom(
|
|
||||||
sigits,
|
|
||||||
appData.processedGiftWraps,
|
|
||||||
appData.keyPair.private
|
|
||||||
).catch((err) => {
|
|
||||||
console.log(
|
|
||||||
'An error occurred in uploading user app data file to blossom server',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
toast.error(
|
|
||||||
'An error occurred in uploading user app data file to blossom server'
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!newBlossomUrl) return null
|
|
||||||
|
|
||||||
// insert new blossom url at the start of the array
|
|
||||||
blossomUrls.unshift(newBlossomUrl)
|
|
||||||
|
|
||||||
// only keep last 10 blossom urls, delete older ones
|
|
||||||
if (blossomUrls.length > 10) {
|
|
||||||
const filesToDelete = blossomUrls.splice(10)
|
|
||||||
filesToDelete.forEach((url) => {
|
|
||||||
deleteBlossomFile(url, appData.keyPair!.private).catch((err) => {
|
|
||||||
console.log(
|
|
||||||
'An error occurred in removing old file of user app data from blossom server',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const usersPubkey = store.getState().auth.usersPubkey!
|
|
||||||
|
|
||||||
// encrypt content for storing in kind 30078 event
|
|
||||||
const nostrController = NostrController.getInstance()
|
|
||||||
const encryptedContent = await nostrController
|
|
||||||
.nip04Encrypt(
|
|
||||||
usersPubkey,
|
|
||||||
JSON.stringify({
|
|
||||||
blossomUrls,
|
|
||||||
keyPair: appData.keyPair
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(
|
|
||||||
'An error occurred in encryption of content for app data',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
toast.error(
|
|
||||||
err.message || 'An error occurred in encryption of content for app data'
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!encryptedContent) return null
|
|
||||||
|
|
||||||
// generate the identifier for user's appData event
|
|
||||||
const dTag = await getDTagForUserAppData()
|
|
||||||
if (!dTag) return null
|
|
||||||
|
|
||||||
const updatedEvent: UnsignedEvent = {
|
|
||||||
kind: kinds.Application,
|
|
||||||
pubkey: usersPubkey!,
|
|
||||||
created_at: unixNow(),
|
|
||||||
tags: [['d', dTag]],
|
|
||||||
content: encryptedContent
|
|
||||||
}
|
|
||||||
|
|
||||||
const signedEvent = await nostrController
|
|
||||||
.signEvent(updatedEvent)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log('An error occurred in signing event', err)
|
|
||||||
toast.error(err.message || 'An error occurred in signing event')
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!signedEvent) return null
|
|
||||||
|
|
||||||
const relayMap = store.getState().relays.map || getDefaultRelayMap()
|
|
||||||
const writeRelays = Object.keys(relayMap).filter((key) => relayMap[key].write)
|
|
||||||
|
|
||||||
const publishResult = await Promise.race([
|
|
||||||
relayController.publish(signedEvent, writeRelays),
|
|
||||||
timeout(40 * 1000)
|
|
||||||
]).catch((err) => {
|
|
||||||
console.log('err :>> ', err)
|
|
||||||
if (err.message === 'Timeout') {
|
|
||||||
toast.error('Timeout occurred in publishing updated app data')
|
|
||||||
} else if (Array.isArray(err)) {
|
|
||||||
err.forEach((errResult) => {
|
|
||||||
toast.error(
|
|
||||||
`Publishing to ${errResult.relay} caused the following error: ${errResult.error}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
toast.error(
|
|
||||||
'An unexpected error occurred in publishing updated app data '
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!publishResult) return null
|
|
||||||
|
|
||||||
// update redux store
|
|
||||||
store.dispatch(
|
|
||||||
updateUserAppDataAction({
|
|
||||||
sigits,
|
|
||||||
blossomUrls,
|
|
||||||
processedGiftWraps: [...appData.processedGiftWraps],
|
|
||||||
keyPair: {
|
|
||||||
...appData.keyPair
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return signedEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteBlossomFile = async (url: string, privateKey: string) => {
|
|
||||||
const pathname = new URL(url).pathname
|
const pathname = new URL(url).pathname
|
||||||
const hash = removeLeadingSlash(pathname)
|
const hash = removeLeadingSlash(pathname)
|
||||||
|
|
||||||
@ -662,7 +350,7 @@ const deleteBlossomFile = async (url: string, privateKey: string) => {
|
|||||||
* @param privateKey - The private key used for encryption.
|
* @param privateKey - The private key used for encryption.
|
||||||
* @returns A promise that resolves to the URL of the uploaded file.
|
* @returns A promise that resolves to the URL of the uploaded file.
|
||||||
*/
|
*/
|
||||||
const uploadUserAppDataToBlossom = async (
|
export const uploadUserAppDataToBlossom = async (
|
||||||
sigits: { [key: string]: Meta },
|
sigits: { [key: string]: Meta },
|
||||||
processedGiftWraps: string[],
|
processedGiftWraps: string[],
|
||||||
privateKey: string
|
privateKey: string
|
||||||
@ -730,7 +418,10 @@ const uploadUserAppDataToBlossom = async (
|
|||||||
* @param privateKey - The private key used for decryption.
|
* @param privateKey - The private key used for decryption.
|
||||||
* @returns A promise that resolves to the decrypted and parsed user application data.
|
* @returns A promise that resolves to the decrypted and parsed user application data.
|
||||||
*/
|
*/
|
||||||
const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
|
export const getUserAppDataFromBlossom = async (
|
||||||
|
url: string,
|
||||||
|
privateKey: string
|
||||||
|
) => {
|
||||||
// Initialize errorCode to track HTTP error codes
|
// Initialize errorCode to track HTTP error codes
|
||||||
let errorCode = 0
|
let errorCode = 0
|
||||||
|
|
||||||
@ -799,133 +490,6 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
|
|||||||
return parsedContent
|
return parsedContent
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to subscribe to sigits notifications for a specified public key.
|
|
||||||
* @param pubkey - The public key to subscribe to.
|
|
||||||
* @returns A promise that resolves when the subscription is successful.
|
|
||||||
*/
|
|
||||||
export const subscribeForSigits = async (
|
|
||||||
pubkey: string,
|
|
||||||
getNDKRelayList: (pubkey: Hexpubkey) => Promise<NDKRelayList>
|
|
||||||
) => {
|
|
||||||
const ndkRelayList = await getNDKRelayList(pubkey)
|
|
||||||
|
|
||||||
// Ensure relay list is not empty
|
|
||||||
if (ndkRelayList.readRelayUrls.length === 0) return
|
|
||||||
|
|
||||||
// Define the filter for the subscription
|
|
||||||
const filter: Filter = {
|
|
||||||
kinds: [1059],
|
|
||||||
'#p': [pubkey]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the received event synchronously
|
|
||||||
const events = await relayController.fetchEvents(filter, [
|
|
||||||
...ndkRelayList.readRelayUrls
|
|
||||||
])
|
|
||||||
for (const e of events) {
|
|
||||||
await processReceivedEvent(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Async processing of the events has a race condition
|
|
||||||
// relayController.subscribeForEvents(filter, relaySet.read, (event) => {
|
|
||||||
// processReceivedEvent(event)
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
|
|
||||||
const processedEvents = store.getState().userAppData?.processedGiftWraps
|
|
||||||
|
|
||||||
// Abort processing if userAppData is undefined
|
|
||||||
if (!processedEvents) return
|
|
||||||
|
|
||||||
if (processedEvents.includes(event.id)) return
|
|
||||||
|
|
||||||
store.dispatch(updateProcessedGiftWraps([...processedEvents, event.id]))
|
|
||||||
|
|
||||||
// validate PoW
|
|
||||||
// Count the number of leading zero bits in the hash
|
|
||||||
const leadingZeroes = countLeadingZeroes(event.id)
|
|
||||||
if (leadingZeroes < difficulty) return
|
|
||||||
|
|
||||||
// decrypt the content of gift wrap event
|
|
||||||
const nostrController = NostrController.getInstance()
|
|
||||||
const decrypted = await nostrController.nip44Decrypt(
|
|
||||||
event.pubkey,
|
|
||||||
event.content
|
|
||||||
)
|
|
||||||
|
|
||||||
const internalUnsignedEvent = await parseJson<UnsignedEvent>(decrypted).catch(
|
|
||||||
(err) => {
|
|
||||||
console.log(
|
|
||||||
'An error occurred in parsing the internal unsigned event',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938) return
|
|
||||||
|
|
||||||
const meta = await parseJson<Meta>(internalUnsignedEvent.content).catch(
|
|
||||||
(err) => {
|
|
||||||
console.log(
|
|
||||||
'An error occurred in parsing the internal unsigned event',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!meta) return
|
|
||||||
|
|
||||||
await updateUsersAppData(meta)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to send a notification to a specified receiver.
|
|
||||||
* @param receiver - The recipient's public key.
|
|
||||||
* @param meta - Metadata associated with the notification.
|
|
||||||
*/
|
|
||||||
export const sendNotification = async (
|
|
||||||
receiver: string,
|
|
||||||
meta: Meta,
|
|
||||||
getNDKRelayList: (pubkey: Hexpubkey) => Promise<NDKRelayList>
|
|
||||||
) => {
|
|
||||||
// Retrieve the user's public key from the state
|
|
||||||
const usersPubkey = store.getState().auth.usersPubkey!
|
|
||||||
|
|
||||||
// Create an unsigned event object with the provided metadata
|
|
||||||
const unsignedEvent: UnsignedEvent = {
|
|
||||||
kind: 938,
|
|
||||||
pubkey: usersPubkey,
|
|
||||||
content: JSON.stringify(meta),
|
|
||||||
tags: [],
|
|
||||||
created_at: unixNow()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap the unsigned event with the receiver's information
|
|
||||||
const wrappedEvent = createWrap(unsignedEvent, receiver)
|
|
||||||
|
|
||||||
const ndkRelayList = await getNDKRelayList(receiver)
|
|
||||||
|
|
||||||
// Ensure relay list is not empty
|
|
||||||
if (ndkRelayList.readRelayUrls.length === 0) return
|
|
||||||
|
|
||||||
// Publish the notification event to the recipient's read relays
|
|
||||||
await Promise.race([
|
|
||||||
relayController.publish(wrappedEvent, [...ndkRelayList.readRelayUrls]),
|
|
||||||
timeout(40 * 1000)
|
|
||||||
]).catch((err) => {
|
|
||||||
// Log an error if publishing the notification event fails
|
|
||||||
console.log(
|
|
||||||
`An error occurred while publishing notification event for ${hexToNpub(receiver)}`,
|
|
||||||
err
|
|
||||||
)
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show user's name, first available in order: display_name, name, or npub as fallback
|
* Show user's name, first available in order: display_name, name, or npub as fallback
|
||||||
* @param npub User identifier, it can be either pubkey or npub1 (we only show npub)
|
* @param npub User identifier, it can be either pubkey or npub1 (we only show npub)
|
||||||
|
Loading…
Reference in New Issue
Block a user