2024-07-05 08:38:04 +00:00
|
|
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
2024-03-05 10:08:33 +00:00
|
|
|
import axios from 'axios'
|
2024-07-05 08:38:04 +00:00
|
|
|
import _ from 'lodash'
|
2024-06-28 09:24:14 +00:00
|
|
|
import {
|
|
|
|
Event,
|
|
|
|
EventTemplate,
|
|
|
|
Filter,
|
|
|
|
SimplePool,
|
|
|
|
UnsignedEvent,
|
|
|
|
finalizeEvent,
|
|
|
|
generateSecretKey,
|
|
|
|
getEventHash,
|
|
|
|
getPublicKey,
|
|
|
|
kinds,
|
|
|
|
nip19,
|
|
|
|
nip44,
|
|
|
|
verifyEvent
|
|
|
|
} from 'nostr-tools'
|
|
|
|
import { toast } from 'react-toastify'
|
2024-05-27 09:32:24 +00:00
|
|
|
import { NIP05_REGEX } from '../constants'
|
2024-06-28 09:24:14 +00:00
|
|
|
import { MetadataController, NostrController } from '../controllers'
|
2024-07-05 08:38:04 +00:00
|
|
|
import {
|
|
|
|
updateProcessedGiftWraps,
|
|
|
|
updateUserAppData as updateUserAppDataAction
|
|
|
|
} from '../store/actions'
|
|
|
|
import { AuthState, Keys } from '../store/auth/types'
|
2024-06-28 09:24:14 +00:00
|
|
|
import { RelaysState } from '../store/relays/types'
|
|
|
|
import store from '../store/store'
|
2024-07-05 08:38:04 +00:00
|
|
|
import { Meta, SignedEvent, UserAppData } from '../types'
|
2024-06-28 09:24:14 +00:00
|
|
|
import { getHash } from './hash'
|
2024-07-05 08:38:04 +00:00
|
|
|
import { parseJson, removeLeadingSlash } from './string'
|
2024-07-08 11:50:38 +00:00
|
|
|
import { timeout } from './utils'
|
2024-02-28 16:49:44 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param hexKey hex private or public key
|
|
|
|
* @returns whether or not is key valid
|
|
|
|
*/
|
|
|
|
const validateHex = (hexKey: string) => {
|
|
|
|
return hexKey.match(/^[a-f0-9]{64}$/)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* NPUB provided - it will convert NPUB to HEX
|
|
|
|
* HEX provided - it will return HEX
|
|
|
|
*
|
2024-04-08 12:45:51 +00:00
|
|
|
* @param pubKey in NPUB, HEX format
|
2024-02-28 16:49:44 +00:00
|
|
|
* @returns HEX format
|
|
|
|
*/
|
2024-05-17 08:34:56 +00:00
|
|
|
export const npubToHex = (pubKey: string): string | null => {
|
2024-02-28 16:49:44 +00:00
|
|
|
// If key is NPUB
|
2024-05-17 08:34:56 +00:00
|
|
|
if (pubKey.startsWith('npub1')) {
|
2024-02-28 16:49:44 +00:00
|
|
|
try {
|
|
|
|
return nip19.decode(pubKey).data as string
|
|
|
|
} catch (error) {
|
2024-05-17 08:34:56 +00:00
|
|
|
return null
|
2024-02-28 16:49:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// valid hex key
|
2024-05-17 08:34:56 +00:00
|
|
|
if (validateHex(pubKey)) return pubKey
|
2024-02-28 16:49:44 +00:00
|
|
|
|
|
|
|
// Not a valid hex key
|
2024-05-17 08:34:56 +00:00
|
|
|
return null
|
2024-02-28 16:49:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If NSEC key is provided function will convert it to HEX
|
|
|
|
* If HEX key Is provided function will validate the HEX format and return
|
|
|
|
*
|
|
|
|
* @param nsec or private key in HEX format
|
|
|
|
*/
|
|
|
|
export const nsecToHex = (nsec: string): string | null => {
|
|
|
|
// If key is NSEC
|
|
|
|
if (nsec.startsWith('nsec')) {
|
|
|
|
try {
|
|
|
|
return nip19.decode(nsec).data as string
|
|
|
|
} catch (error) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// since it's not NSEC key we check if it's a valid hex key
|
|
|
|
if (validateHex(nsec)) return nsec
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2024-05-17 08:34:56 +00:00
|
|
|
export const hexToNpub = (hexPubkey: string): `npub1${string}` => {
|
|
|
|
if (hexPubkey.startsWith('npub1')) return hexPubkey as `npub1${string}`
|
2024-03-01 10:16:35 +00:00
|
|
|
|
|
|
|
return nip19.npubEncode(hexPubkey)
|
|
|
|
}
|
|
|
|
|
2024-02-28 16:49:44 +00:00
|
|
|
export const verifySignedEvent = (event: SignedEvent) => {
|
|
|
|
const isGood = verifyEvent(event)
|
|
|
|
|
|
|
|
if (!isGood) {
|
|
|
|
throw new Error(
|
|
|
|
'Signed event did not pass verification. Check sig, id and pubkey.'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2024-03-05 10:08:33 +00:00
|
|
|
|
2024-05-27 09:32:24 +00:00
|
|
|
/**
|
|
|
|
* Function to query NIP-05 data and return the public key and relays.
|
|
|
|
*
|
|
|
|
* @param {string} nip05 - The NIP-05 identifier in the format "name@domain".
|
|
|
|
* @returns {Promise<{ pubkey: string, relays: string[] }>} - The public key and an array of relay URLs.
|
|
|
|
* @throws Will throw an error if the NIP-05 identifier is invalid or if there is an issue with the network request.
|
|
|
|
*/
|
2024-03-05 10:08:33 +00:00
|
|
|
export const queryNip05 = async (
|
|
|
|
nip05: string
|
|
|
|
): Promise<{
|
|
|
|
pubkey: string
|
|
|
|
relays: string[]
|
|
|
|
}> => {
|
|
|
|
const match = nip05.match(NIP05_REGEX)
|
2024-05-27 09:32:24 +00:00
|
|
|
|
|
|
|
// Throw an error if the NIP-05 identifier is invalid
|
2024-03-05 10:08:33 +00:00
|
|
|
if (!match) throw new Error('Invalid nip05')
|
|
|
|
|
2024-05-27 09:32:24 +00:00
|
|
|
// Destructure the match result, assigning default value '_' to name if not provided
|
2024-06-28 22:44:06 +00:00
|
|
|
const [name = '_', domain] = match
|
2024-05-27 09:32:24 +00:00
|
|
|
|
|
|
|
// Construct the URL to query the NIP-05 data
|
2024-03-05 10:08:33 +00:00
|
|
|
const url = `https://${domain}/.well-known/nostr.json?name=${name}`
|
2024-05-27 09:32:24 +00:00
|
|
|
|
|
|
|
// Perform the network request to get the NIP-05 data
|
2024-03-05 10:08:33 +00:00
|
|
|
const res = await axios(url)
|
|
|
|
.then((res) => {
|
|
|
|
return res.data
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.log('err :>> ', err)
|
|
|
|
throw err
|
|
|
|
})
|
|
|
|
|
2024-05-27 09:32:24 +00:00
|
|
|
// Extract the public key from the response data
|
2024-03-05 10:08:33 +00:00
|
|
|
const pubkey = res.names[name]
|
|
|
|
const relays: string[] = []
|
|
|
|
|
2024-05-27 09:32:24 +00:00
|
|
|
// If a public key is found
|
2024-03-05 10:08:33 +00:00
|
|
|
if (pubkey) {
|
2024-05-27 09:32:24 +00:00
|
|
|
// Function to add relays if they exist and are not empty
|
|
|
|
const addRelays = (relayList?: string[]) => {
|
|
|
|
if (relayList && relayList.length > 0) {
|
|
|
|
relays.push(...relayList)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for user-specific relays in the NIP-46 section of the response data
|
|
|
|
if (res.nip46) {
|
|
|
|
addRelays(res.nip46[pubkey] as string[])
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for user-specific relays in the relays section of the response data if not found in NIP-46
|
|
|
|
if (relays.length === 0 && res.relays) {
|
|
|
|
addRelays(res.relays[pubkey] as string[])
|
|
|
|
}
|
|
|
|
|
|
|
|
// If no user-specific relays are found, check for root user relays
|
|
|
|
if (relays.length === 0) {
|
2024-03-05 10:08:33 +00:00
|
|
|
const root = res.names['_']
|
|
|
|
if (root) {
|
2024-05-27 09:32:24 +00:00
|
|
|
// Check for root user relays in both NIP-46 and relays sections
|
|
|
|
addRelays(res.nip46?.[root] as string[])
|
|
|
|
addRelays(res.relays?.[root] as string[])
|
2024-03-05 10:08:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-27 09:32:24 +00:00
|
|
|
// Return the public key and the array of relays
|
2024-03-05 10:08:33 +00:00
|
|
|
return {
|
|
|
|
pubkey,
|
|
|
|
relays
|
|
|
|
}
|
|
|
|
}
|
2024-03-19 10:27:18 +00:00
|
|
|
|
|
|
|
export const base64EncodeSignedEvent = (event: SignedEvent) => {
|
|
|
|
try {
|
|
|
|
const authEventSerialized = JSON.stringify(event)
|
|
|
|
const token = btoa(authEventSerialized)
|
|
|
|
return token
|
|
|
|
} catch (error) {
|
|
|
|
throw new Error('An error occurred in JSON.stringify of signedAuthEvent')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export const base64DecodeAuthToken = (authToken: string): SignedEvent => {
|
|
|
|
const decodedToken = atob(authToken)
|
|
|
|
|
|
|
|
try {
|
|
|
|
const signedEvent = JSON.parse(decodedToken)
|
|
|
|
return signedEvent
|
|
|
|
} catch (error) {
|
|
|
|
throw new Error('An error occurred in JSON.parse of the auth token')
|
|
|
|
}
|
|
|
|
}
|
2024-05-15 11:57:54 +00:00
|
|
|
|
2024-05-16 06:55:24 +00:00
|
|
|
/**
|
2024-05-17 08:34:56 +00:00
|
|
|
* @param pubkey in hex or npub format
|
2024-05-16 06:55:24 +00:00
|
|
|
* @returns robohash.org url for the avatar
|
|
|
|
*/
|
2024-05-17 11:35:37 +00:00
|
|
|
export const getRoboHashPicture = (
|
|
|
|
pubkey?: string,
|
|
|
|
set: number = 1
|
|
|
|
): string => {
|
2024-05-17 11:33:01 +00:00
|
|
|
if (!pubkey) return ''
|
2024-05-16 06:55:24 +00:00
|
|
|
const npub = hexToNpub(pubkey)
|
2024-05-17 07:37:30 +00:00
|
|
|
return `https://robohash.org/${npub}.png?set=set${set}`
|
2024-05-15 11:57:54 +00:00
|
|
|
}
|
2024-06-28 09:24:14 +00:00
|
|
|
|
|
|
|
export const now = () => Math.round(Date.now() / 1000)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate nip44 conversation key
|
|
|
|
* @param privateKey
|
|
|
|
* @param publicKey
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
export const nip44ConversationKey = (
|
|
|
|
privateKey: Uint8Array,
|
|
|
|
publicKey: string
|
|
|
|
) => nip44.v2.utils.getConversationKey(privateKey, publicKey)
|
|
|
|
|
|
|
|
export const nip44Encrypt = (
|
2024-07-05 08:38:04 +00:00
|
|
|
data: UnsignedEvent,
|
2024-06-28 09:24:14 +00:00
|
|
|
privateKey: Uint8Array,
|
|
|
|
publicKey: string
|
|
|
|
) =>
|
|
|
|
nip44.v2.encrypt(
|
|
|
|
JSON.stringify(data),
|
|
|
|
nip44ConversationKey(privateKey, publicKey)
|
|
|
|
)
|
|
|
|
|
|
|
|
export const nip44Decrypt = (data: Event, privateKey: Uint8Array) =>
|
|
|
|
JSON.parse(
|
|
|
|
nip44.v2.decrypt(
|
|
|
|
data.content,
|
|
|
|
nip44ConversationKey(privateKey, data.pubkey)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
// Function to count leading zero bits
|
|
|
|
export const countLeadingZeroes = (hex: string) => {
|
|
|
|
let count = 0
|
|
|
|
|
|
|
|
for (let i = 0; i < hex.length; i++) {
|
|
|
|
const nibble = parseInt(hex[i], 16)
|
|
|
|
if (nibble === 0) {
|
|
|
|
count += 4
|
|
|
|
} else {
|
|
|
|
count += Math.clz32(nibble) - 28
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return count
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function to create a wrapped event with PoW
|
|
|
|
* @param event Original event to be wrapped
|
|
|
|
* @param receiver Public key of the receiver
|
|
|
|
* @param difficulty PoW difficulty level (default is 20)
|
|
|
|
* @returns
|
|
|
|
*/
|
|
|
|
//
|
2024-07-05 08:38:04 +00:00
|
|
|
export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => {
|
2024-06-28 09:24:14 +00:00
|
|
|
// Generate a random secret key and its corresponding public key
|
|
|
|
const randomKey = generateSecretKey()
|
|
|
|
const pubkey = getPublicKey(randomKey)
|
|
|
|
|
|
|
|
// Encrypt the event content using nip44 encryption
|
2024-07-05 08:38:04 +00:00
|
|
|
const content = nip44Encrypt(unsignedEvent, randomKey, receiver)
|
2024-06-28 09:24:14 +00:00
|
|
|
|
|
|
|
// Initialize nonce and leadingZeroes for PoW calculation
|
|
|
|
let nonce = 0
|
|
|
|
let leadingZeroes = 0
|
2024-07-05 08:38:04 +00:00
|
|
|
const difficulty = Math.floor(Math.random() * 10) + 5 // random number between 5 & 10
|
2024-06-28 09:24:14 +00:00
|
|
|
|
|
|
|
// Loop until a valid PoW hash is found
|
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
|
|
while (true) {
|
|
|
|
// Create an unsigned event with the necessary fields
|
2024-07-05 08:38:04 +00:00
|
|
|
const event: UnsignedEvent = {
|
2024-06-28 09:24:14 +00:00
|
|
|
kind: 1059, // Event kind
|
|
|
|
content, // Encrypted content
|
|
|
|
pubkey, // Public key of the creator
|
2024-07-05 08:38:04 +00:00
|
|
|
created_at: now(), // Current timestamp
|
2024-06-28 09:24:14 +00:00
|
|
|
tags: [
|
|
|
|
// Tags including receiver and nonce
|
|
|
|
['p', receiver],
|
|
|
|
['nonce', nonce.toString(), difficulty.toString()]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
// Calculate the SHA-256 hash of the unsigned event
|
2024-07-05 08:38:04 +00:00
|
|
|
const hash = getEventHash(event)
|
2024-06-28 09:24:14 +00:00
|
|
|
|
|
|
|
// Count the number of leading zero bits in the hash
|
|
|
|
leadingZeroes = countLeadingZeroes(hash)
|
|
|
|
|
|
|
|
// Check if the leading zero bits meet the required difficulty
|
|
|
|
if (leadingZeroes >= difficulty) {
|
|
|
|
// Finalize the event (sign it) and return the result
|
2024-07-05 08:38:04 +00:00
|
|
|
return finalizeEvent(event, randomKey)
|
2024-06-28 09:24:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Increment the nonce for the next iteration
|
|
|
|
nonce++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-05 08:38:04 +00:00
|
|
|
export const getUsersAppData = async (): Promise<UserAppData | null> => {
|
2024-06-28 09:24:14 +00:00
|
|
|
const relays: string[] = []
|
|
|
|
|
|
|
|
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
|
|
|
|
const relayMap = store.getState().relays?.map
|
|
|
|
|
|
|
|
const nostrController = NostrController.getInstance()
|
|
|
|
|
|
|
|
// check if relaysMap in redux store is undefined
|
|
|
|
if (!relayMap) {
|
|
|
|
const metadataController = new MetadataController()
|
|
|
|
const relaySet = await metadataController
|
|
|
|
.findRelayListMetadata(usersPubkey)
|
|
|
|
.catch((err) => {
|
|
|
|
console.log(
|
|
|
|
`An error occurred while finding relay list metadata for ${hexToNpub(usersPubkey)}`,
|
|
|
|
err
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
// Return if metadata retrieval failed
|
2024-07-05 08:38:04 +00:00
|
|
|
if (!relaySet) return null
|
2024-06-28 09:24:14 +00:00
|
|
|
|
|
|
|
// Ensure relay list is not empty
|
2024-07-05 08:38:04 +00:00
|
|
|
if (relaySet.write.length === 0) return null
|
2024-06-28 09:24:14 +00:00
|
|
|
|
|
|
|
relays.push(...relaySet.write)
|
|
|
|
} else {
|
|
|
|
// filter write relays from user's relayMap stored in redux store
|
|
|
|
const writeRelays = Object.keys(relayMap).filter(
|
|
|
|
(key) => relayMap[key].write
|
|
|
|
)
|
|
|
|
|
|
|
|
relays.push(...writeRelays)
|
|
|
|
}
|
|
|
|
|
|
|
|
// generate an identifier for user's nip78
|
2024-07-05 08:38:04 +00:00
|
|
|
const hash = await getHash('938' + usersPubkey)
|
2024-06-28 09:24:14 +00:00
|
|
|
if (!hash) return null
|
|
|
|
|
|
|
|
const filter: Filter = {
|
|
|
|
kinds: [kinds.Application],
|
|
|
|
'#d': [hash]
|
|
|
|
}
|
|
|
|
|
|
|
|
const encryptedContent = await nostrController
|
|
|
|
.getEvent(filter, relays)
|
|
|
|
.then((event) => {
|
|
|
|
if (event) return event.content
|
|
|
|
|
|
|
|
// if person is using sigit for first time its possible that event is null
|
|
|
|
// so we'll return empty stringified object
|
|
|
|
return '{}'
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
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
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!encryptedContent) return null
|
|
|
|
|
|
|
|
if (encryptedContent === '{}') {
|
2024-07-05 08:38:04 +00:00
|
|
|
const secret = generateSecretKey()
|
|
|
|
const pubKey = getPublicKey(secret)
|
|
|
|
|
|
|
|
return {
|
|
|
|
sigits: {},
|
|
|
|
processedGiftWraps: [],
|
|
|
|
blossomUrls: [],
|
|
|
|
keyPair: {
|
|
|
|
private: bytesToHex(secret),
|
|
|
|
public: pubKey
|
|
|
|
}
|
|
|
|
}
|
2024-06-28 09:24:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const decrypted = await nostrController
|
|
|
|
.nip04Decrypt(usersPubkey, encryptedContent)
|
|
|
|
.catch((err) => {
|
|
|
|
console.log('An error occurred while decrypting app data', err)
|
|
|
|
toast.error('An error occurred while decrypting app data')
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!decrypted) return null
|
|
|
|
|
|
|
|
const parsedContent = await parseJson<{
|
2024-07-05 08:38:04 +00:00
|
|
|
blossomUrls: string[]
|
|
|
|
keyPair: Keys
|
2024-06-28 09:24:14 +00:00
|
|
|
}>(decrypted).catch((err) => {
|
|
|
|
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
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!parsedContent) return null
|
|
|
|
|
2024-07-05 08:38:04 +00:00
|
|
|
const { blossomUrls, keyPair } = parsedContent
|
|
|
|
|
|
|
|
if (blossomUrls.length === 0) return null
|
|
|
|
|
|
|
|
const dataFromBlossom = await getUserAppDataFromBlossom(
|
|
|
|
blossomUrls[0],
|
|
|
|
keyPair.private
|
|
|
|
)
|
|
|
|
|
|
|
|
if (!dataFromBlossom) return null
|
|
|
|
|
|
|
|
const { sigits, processedGiftWraps } = dataFromBlossom
|
|
|
|
|
|
|
|
return {
|
|
|
|
blossomUrls,
|
|
|
|
keyPair,
|
|
|
|
sigits,
|
|
|
|
processedGiftWraps
|
|
|
|
}
|
2024-06-28 09:24:14 +00:00
|
|
|
}
|
|
|
|
|
2024-07-05 08:38:04 +00:00
|
|
|
export const updateUsersAppData = async (meta: Meta) => {
|
|
|
|
const appData = store.getState().userAppData
|
|
|
|
if (!appData || !appData.keyPair) return null
|
2024-06-28 09:24:14 +00:00
|
|
|
|
2024-07-05 08:38:04 +00:00
|
|
|
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]
|
2024-06-28 09:24:14 +00:00
|
|
|
if (existingMeta.modifiedAt < meta.modifiedAt) {
|
2024-07-05 08:38:04 +00:00
|
|
|
sigits[id] = meta
|
|
|
|
isUpdated = true
|
2024-06-28 09:24:14 +00:00
|
|
|
}
|
|
|
|
} else {
|
2024-07-05 08:38:04 +00:00
|
|
|
sigits[id] = meta
|
|
|
|
isUpdated = true
|
2024-06-28 09:24:14 +00:00
|
|
|
}
|
|
|
|
|
2024-07-05 08:38:04 +00:00
|
|
|
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
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// insert new blossom url at the start of the array
|
2024-07-05 08:38:04 +00:00
|
|
|
blossomUrls.unshift(newBlossomUrl)
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// only keep last 10 blossom urls, delete older ones
|
2024-07-05 08:38:04 +00:00
|
|
|
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
|
|
|
|
)
|
|
|
|
})
|
|
|
|
})
|
2024-06-28 09:24:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// encrypt content for storing in kind 30078 event
|
2024-06-28 09:24:14 +00:00
|
|
|
const nostrController = NostrController.getInstance()
|
|
|
|
const encryptedContent = await nostrController
|
2024-07-05 08:38:04 +00:00
|
|
|
.nip04Encrypt(
|
|
|
|
usersPubkey,
|
|
|
|
JSON.stringify({
|
|
|
|
blossomUrls,
|
|
|
|
keyPair: appData.keyPair
|
|
|
|
})
|
|
|
|
)
|
2024-06-28 09:24:14 +00:00
|
|
|
.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
|
2024-07-05 08:38:04 +00:00
|
|
|
const hash = await getHash('938' + usersPubkey)
|
2024-06-28 09:24:14 +00:00
|
|
|
if (!hash) return null
|
|
|
|
|
|
|
|
const updatedEvent: UnsignedEvent = {
|
|
|
|
kind: kinds.Application,
|
|
|
|
pubkey: usersPubkey!,
|
|
|
|
created_at: now(),
|
|
|
|
tags: [['d', hash]],
|
|
|
|
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 as RelaysState).map!
|
|
|
|
const writeRelays = Object.keys(relayMap).filter((key) => relayMap[key].write)
|
|
|
|
|
2024-07-08 11:50:38 +00:00
|
|
|
console.log(`publishing event kind: ${kinds.Application}`)
|
|
|
|
const publishResult = await Promise.race([
|
|
|
|
nostrController.publishEvent(signedEvent, writeRelays),
|
2024-07-09 12:17:57 +00:00
|
|
|
timeout(1000 * 30)
|
2024-07-08 11:50:38 +00:00
|
|
|
]).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) => {
|
2024-06-28 09:24:14 +00:00
|
|
|
toast.error(
|
|
|
|
`Publishing to ${errResult.relay} caused the following error: ${errResult.error}`
|
|
|
|
)
|
|
|
|
})
|
2024-07-08 11:50:38 +00:00
|
|
|
} else {
|
|
|
|
toast.error(
|
|
|
|
'An unexpected error occurred in publishing updated app data '
|
|
|
|
)
|
|
|
|
}
|
2024-06-28 09:24:14 +00:00
|
|
|
|
2024-07-08 11:50:38 +00:00
|
|
|
return null
|
|
|
|
})
|
2024-06-28 09:24:14 +00:00
|
|
|
|
|
|
|
if (!publishResult) return null
|
|
|
|
|
2024-07-05 08:38:04 +00:00
|
|
|
// update redux store
|
|
|
|
store.dispatch(
|
|
|
|
updateUserAppDataAction({
|
|
|
|
sigits,
|
|
|
|
blossomUrls,
|
|
|
|
processedGiftWraps: [...appData.processedGiftWraps],
|
|
|
|
keyPair: {
|
|
|
|
...appData.keyPair
|
|
|
|
}
|
|
|
|
})
|
|
|
|
)
|
|
|
|
|
2024-06-28 09:24:14 +00:00
|
|
|
return signedEvent
|
|
|
|
}
|
|
|
|
|
2024-07-05 08:38:04 +00:00
|
|
|
const deleteBlossomFile = async (url: string, privateKey: string) => {
|
|
|
|
const pathname = new URL(url).pathname
|
|
|
|
const hash = removeLeadingSlash(pathname)
|
|
|
|
|
|
|
|
// Define event metadata for authorization
|
|
|
|
const event: EventTemplate = {
|
|
|
|
kind: 24242,
|
|
|
|
content: 'Authorize Upload',
|
|
|
|
created_at: now(),
|
|
|
|
tags: [
|
|
|
|
['t', 'delete'],
|
|
|
|
['expiration', String(now() + 60 * 5)], // Set expiration time to 5 minutes from now
|
|
|
|
['x', hash]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
const authEvent = finalizeEvent(event, hexToBytes(privateKey))
|
|
|
|
|
|
|
|
// delete the file stored on file storage service using Axios
|
|
|
|
const response = await axios.delete(url, {
|
|
|
|
headers: {
|
|
|
|
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
console.log('response.data :>> ', response.data)
|
|
|
|
}
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
/**
|
|
|
|
* Function to upload user application data to the Blossom server.
|
|
|
|
* @param sigits - An object containing metadata for the user application data.
|
|
|
|
* @param processedGiftWraps - An array of processed gift wrap IDs.
|
|
|
|
* @param privateKey - The private key used for encryption.
|
|
|
|
* @returns A promise that resolves to the URL of the uploaded file.
|
|
|
|
*/
|
2024-07-05 08:38:04 +00:00
|
|
|
const uploadUserAppDataToBlossom = async (
|
|
|
|
sigits: { [key: string]: Meta },
|
|
|
|
processedGiftWraps: string[],
|
|
|
|
privateKey: string
|
|
|
|
) => {
|
2024-07-08 20:16:47 +00:00
|
|
|
// Create an object containing the sigits and processed gift wraps
|
2024-07-05 08:38:04 +00:00
|
|
|
const obj = {
|
|
|
|
sigits,
|
|
|
|
processedGiftWraps
|
|
|
|
}
|
2024-07-08 20:16:47 +00:00
|
|
|
// Convert the object to a JSON string
|
2024-07-05 08:38:04 +00:00
|
|
|
const stringified = JSON.stringify(obj)
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Convert the private key from hex to bytes
|
2024-07-05 08:38:04 +00:00
|
|
|
const secretKey = hexToBytes(privateKey)
|
2024-07-08 20:16:47 +00:00
|
|
|
// Encrypt the JSON string using the secret key
|
2024-07-05 08:38:04 +00:00
|
|
|
const encrypted = nip44.v2.encrypt(
|
|
|
|
stringified,
|
|
|
|
nip44ConversationKey(secretKey, getPublicKey(secretKey))
|
|
|
|
)
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Create a blob from the encrypted data
|
2024-07-05 08:38:04 +00:00
|
|
|
const blob = new Blob([encrypted], { type: 'application/octet-stream' })
|
2024-07-08 20:16:47 +00:00
|
|
|
// Create a file from the blob
|
2024-07-05 08:38:04 +00:00
|
|
|
const file = new File([blob], 'encrypted.txt', {
|
|
|
|
type: 'application/octet-stream'
|
|
|
|
})
|
|
|
|
|
|
|
|
// Define event metadata for authorization
|
|
|
|
const event: EventTemplate = {
|
|
|
|
kind: 24242,
|
|
|
|
content: 'Authorize Upload',
|
|
|
|
created_at: now(),
|
|
|
|
tags: [
|
|
|
|
['t', 'upload'],
|
|
|
|
['expiration', String(now() + 60 * 5)], // Set expiration time to 5 minutes from now
|
|
|
|
['name', file.name],
|
|
|
|
['size', String(file.size)]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Finalize the event with the private key
|
2024-07-05 08:38:04 +00:00
|
|
|
const authEvent = finalizeEvent(event, hexToBytes(privateKey))
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// URL of the file storage service
|
2024-07-05 08:38:04 +00:00
|
|
|
const FILE_STORAGE_URL = 'https://blossom.sigit.io'
|
|
|
|
|
|
|
|
// Upload the file to the file storage service using Axios
|
|
|
|
const response = await axios.put(`${FILE_STORAGE_URL}/upload`, file, {
|
|
|
|
headers: {
|
|
|
|
Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
// Return the URL of the uploaded file
|
|
|
|
return response.data.url as string
|
|
|
|
}
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
/**
|
|
|
|
* Function to retrieve and decrypt user application data from Blossom server.
|
|
|
|
* @param url - The URL to fetch the encrypted data from.
|
|
|
|
* @param privateKey - The private key used for decryption.
|
|
|
|
* @returns A promise that resolves to the decrypted and parsed user application data.
|
|
|
|
*/
|
2024-07-05 08:38:04 +00:00
|
|
|
const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
|
2024-07-08 20:16:47 +00:00
|
|
|
// Initialize errorCode to track HTTP error codes
|
2024-07-08 11:50:38 +00:00
|
|
|
let errorCode = 0
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Fetch the encrypted data from the provided URL
|
2024-07-05 08:38:04 +00:00
|
|
|
const encrypted = await axios
|
|
|
|
.get(url, {
|
2024-07-08 20:16:47 +00:00
|
|
|
responseType: 'blob' // Expect a blob response
|
2024-07-05 08:38:04 +00:00
|
|
|
})
|
|
|
|
.then(async (res) => {
|
2024-07-08 20:16:47 +00:00
|
|
|
// Convert the blob response to a File object
|
2024-07-05 08:38:04 +00:00
|
|
|
const file = new File([res.data], 'encrypted.txt')
|
2024-07-08 20:16:47 +00:00
|
|
|
// Read the text content from the file
|
2024-07-05 08:38:04 +00:00
|
|
|
const text = await file.text()
|
|
|
|
return text
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
2024-07-08 20:16:47 +00:00
|
|
|
// Log and display an error message if the request fails
|
2024-07-05 08:38:04 +00:00
|
|
|
console.error(`error occurred in getting file from ${url}`, err)
|
|
|
|
toast.error(err.message || `error occurred in getting file from ${url}`)
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Set errorCode to the HTTP status code if available
|
2024-07-08 11:50:38 +00:00
|
|
|
if (err.request) {
|
|
|
|
const { status } = err.request
|
|
|
|
errorCode = status
|
|
|
|
}
|
|
|
|
|
2024-07-05 08:38:04 +00:00
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Return a default value if the requested resource is not found (404)
|
2024-07-08 11:50:38 +00:00
|
|
|
if (errorCode === 404) {
|
|
|
|
return {
|
|
|
|
sigits: {},
|
|
|
|
processedGiftWraps: []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Return null if the encrypted data could not be retrieved
|
2024-07-05 08:38:04 +00:00
|
|
|
if (!encrypted) return null
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Convert the private key from hex to bytes
|
2024-07-05 08:38:04 +00:00
|
|
|
const secret = hexToBytes(privateKey)
|
2024-07-08 20:16:47 +00:00
|
|
|
// Get the public key corresponding to the private key
|
2024-07-05 08:38:04 +00:00
|
|
|
const pubkey = getPublicKey(secret)
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Decrypt the encrypted data using the secret and public key
|
2024-07-05 08:38:04 +00:00
|
|
|
const decrypted = nip44.v2.decrypt(
|
|
|
|
encrypted,
|
|
|
|
nip44ConversationKey(secret, pubkey)
|
|
|
|
)
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Parse the decrypted JSON content
|
2024-07-05 08:38:04 +00:00
|
|
|
const parsedContent = await parseJson<{
|
|
|
|
sigits: { [key: string]: Meta }
|
|
|
|
processedGiftWraps: string[]
|
|
|
|
}>(decrypted).catch((err) => {
|
2024-07-08 20:16:47 +00:00
|
|
|
// Log and display an error message if parsing fails
|
2024-07-05 08:38:04 +00:00
|
|
|
console.log(
|
|
|
|
'An error occurred in parsing the user app data content from blossom server',
|
|
|
|
err
|
|
|
|
)
|
|
|
|
toast.error(
|
|
|
|
'An error occurred in parsing the user app data content from blossom server'
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Return the parsed content
|
2024-07-05 08:38:04 +00:00
|
|
|
return parsedContent
|
|
|
|
}
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2024-06-28 09:24:14 +00:00
|
|
|
export const subscribeForSigits = async (pubkey: string) => {
|
2024-07-08 20:16:47 +00:00
|
|
|
// Instantiate the MetadataController to retrieve relay list metadata
|
2024-06-28 09:24:14 +00:00
|
|
|
const metadataController = new MetadataController()
|
|
|
|
const relaySet = await metadataController
|
|
|
|
.findRelayListMetadata(pubkey)
|
|
|
|
.catch((err) => {
|
2024-07-08 20:16:47 +00:00
|
|
|
// Log an error if retrieving relay list metadata fails
|
2024-06-28 09:24:14 +00:00
|
|
|
console.log(
|
2024-06-28 09:25:41 +00:00
|
|
|
`An error occurred while finding relay list metadata for ${hexToNpub(pubkey)}`,
|
2024-06-28 09:24:14 +00:00
|
|
|
err
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
// Return if metadata retrieval failed
|
|
|
|
if (!relaySet) return
|
|
|
|
|
|
|
|
// Ensure relay list is not empty
|
|
|
|
if (relaySet.read.length === 0) return
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Define the filter for the subscription
|
2024-06-28 09:24:14 +00:00
|
|
|
const filter: Filter = {
|
|
|
|
kinds: [1059],
|
|
|
|
'#p': [pubkey]
|
|
|
|
}
|
2024-07-08 20:16:47 +00:00
|
|
|
|
|
|
|
// Instantiate a new SimplePool for the subscription
|
2024-06-28 09:24:14 +00:00
|
|
|
const pool = new SimplePool()
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Subscribe to the specified relays with the defined filter
|
2024-07-05 08:38:04 +00:00
|
|
|
return pool.subscribeMany(relaySet.read, [filter], {
|
2024-07-08 20:16:47 +00:00
|
|
|
// Define a callback function to handle received events
|
2024-06-28 09:24:14 +00:00
|
|
|
onevent: (event) => {
|
2024-07-08 20:16:47 +00:00
|
|
|
processReceivedEvent(event) // Process the received event
|
2024-06-28 09:24:14 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-07-05 08:38:04 +00:00
|
|
|
const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
|
|
|
|
const processedEvents = (store.getState().userAppData as UserAppData)
|
|
|
|
.processedGiftWraps
|
|
|
|
if (processedEvents.includes(event.id)) return
|
|
|
|
|
|
|
|
store.dispatch(updateProcessedGiftWraps([...processedEvents, event.id]))
|
|
|
|
|
2024-06-28 09:24:14 +00:00
|
|
|
// validate PoW
|
|
|
|
// Count the number of leading zero bits in the hash
|
|
|
|
const leadingZeroes = countLeadingZeroes(event.id)
|
|
|
|
if (leadingZeroes < difficulty) return
|
|
|
|
|
2024-07-05 08:38:04 +00:00
|
|
|
// decrypt the content of gift wrap event
|
2024-06-28 09:24:14 +00:00
|
|
|
const nostrController = NostrController.getInstance()
|
2024-07-05 08:38:04 +00:00
|
|
|
const decrypted = await nostrController.nip44Decrypt(
|
2024-06-28 09:24:14 +00:00
|
|
|
event.pubkey,
|
|
|
|
event.content
|
|
|
|
)
|
2024-07-05 08:38:04 +00:00
|
|
|
|
|
|
|
const internalUnsignedEvent = await parseJson<UnsignedEvent>(decrypted).catch(
|
|
|
|
(err) => {
|
|
|
|
console.log(
|
|
|
|
'An error occurred in parsing the internal unsigned event',
|
|
|
|
err
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
}
|
2024-06-28 09:24:14 +00:00
|
|
|
)
|
2024-07-05 08:38:04 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
updateUsersAppData(meta)
|
2024-06-28 09:24:14 +00:00
|
|
|
}
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
/**
|
|
|
|
* Function to send a notification to a specified receiver.
|
|
|
|
* @param receiver - The recipient's public key.
|
|
|
|
* @param meta - Metadata associated with the notification.
|
|
|
|
*/
|
2024-07-05 08:38:04 +00:00
|
|
|
export const sendNotification = async (receiver: string, meta: Meta) => {
|
2024-07-08 20:16:47 +00:00
|
|
|
// Retrieve the user's public key from the state
|
2024-06-28 09:24:14 +00:00
|
|
|
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Create an unsigned event object with the provided metadata
|
2024-06-28 09:24:14 +00:00
|
|
|
const unsignedEvent: UnsignedEvent = {
|
2024-07-05 08:38:04 +00:00
|
|
|
kind: 938,
|
2024-06-28 09:24:14 +00:00
|
|
|
pubkey: usersPubkey,
|
2024-07-05 08:38:04 +00:00
|
|
|
content: JSON.stringify(meta),
|
|
|
|
tags: [],
|
2024-06-28 09:24:14 +00:00
|
|
|
created_at: now()
|
|
|
|
}
|
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Wrap the unsigned event with the receiver's information
|
2024-07-05 08:38:04 +00:00
|
|
|
const wrappedEvent = createWrap(unsignedEvent, receiver)
|
2024-06-28 09:24:14 +00:00
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Instantiate the MetadataController to retrieve relay list metadata
|
2024-06-28 09:24:14 +00:00
|
|
|
const metadataController = new MetadataController()
|
|
|
|
const relaySet = await metadataController
|
|
|
|
.findRelayListMetadata(receiver)
|
|
|
|
.catch((err) => {
|
2024-07-08 20:16:47 +00:00
|
|
|
// Log an error if retrieving relay list metadata fails
|
2024-06-28 09:24:14 +00:00
|
|
|
console.log(
|
|
|
|
`An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
|
|
|
|
err
|
|
|
|
)
|
|
|
|
return null
|
|
|
|
})
|
|
|
|
|
|
|
|
// Return if metadata retrieval failed
|
|
|
|
if (!relaySet) return
|
|
|
|
|
|
|
|
// Ensure relay list is not empty
|
|
|
|
if (relaySet.read.length === 0) return
|
|
|
|
|
2024-07-08 11:50:38 +00:00
|
|
|
console.log('Publishing notifications')
|
2024-06-28 09:24:14 +00:00
|
|
|
// Publish the notification event to the recipient's read relays
|
2024-07-05 08:38:04 +00:00
|
|
|
const nostrController = NostrController.getInstance()
|
2024-07-08 11:50:38 +00:00
|
|
|
|
2024-07-08 20:16:47 +00:00
|
|
|
// Attempt to publish the event to the relays, with a timeout of 2 minutes
|
2024-07-08 11:50:38 +00:00
|
|
|
await Promise.race([
|
|
|
|
nostrController.publishEvent(wrappedEvent, relaySet.read),
|
2024-07-09 12:17:57 +00:00
|
|
|
timeout(1000 * 30)
|
2024-07-08 11:50:38 +00:00
|
|
|
]).catch((err) => {
|
2024-07-08 20:16:47 +00:00
|
|
|
// Log an error if publishing the notification event fails
|
2024-07-08 11:50:38 +00:00
|
|
|
console.log(
|
|
|
|
`An error occurred while publishing notification event for ${hexToNpub(receiver)}`,
|
|
|
|
err
|
|
|
|
)
|
|
|
|
throw err
|
|
|
|
})
|
2024-06-28 09:24:14 +00:00
|
|
|
}
|