chore: add comments

This commit is contained in:
SwiftHawk 2024-07-09 01:16:47 +05:00
parent 8eaf9cb61c
commit 0aaa20092e
7 changed files with 139 additions and 23 deletions

View File

@ -67,6 +67,7 @@ export const UserComponent = ({ pubkey, name, image }: UserProps) => {
const handleClick = (e: React.MouseEvent<HTMLElement, MouseEvent>) => { const handleClick = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
e.stopPropagation() e.stopPropagation()
// navigate to user's profile
navigate(getProfileRoute(pubkey)) navigate(getProfileRoute(pubkey))
} }

View File

@ -304,95 +304,135 @@ export class NostrController extends EventEmitter {
return event return event
} }
/**
* Encrypts the given content for the specified receiver using NIP-44 encryption.
*
* @param receiver The public key of the receiver.
* @param content The content to be encrypted.
* @returns The encrypted content as a string.
* @throws Error if the nostr extension does not support NIP-44, if the private key pair is not found, or if the login method is unsupported.
*/
nip44Encrypt = async (receiver: string, content: string) => { nip44Encrypt = async (receiver: string, content: string) => {
// Retrieve the current login method from the application's redux state.
const loginMethod = (store.getState().auth as AuthState).loginMethod const loginMethod = (store.getState().auth as AuthState).loginMethod
// Handle encryption when the login method is via an extension.
if (loginMethod === LoginMethods.extension) { if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject() const nostr = this.getNostrObject()
// Check if the nostr object supports NIP-44 encryption.
if (!nostr.nip44) { if (!nostr.nip44) {
throw new Error( throw new Error(
`Your nostr extension does not support nip44 encryption & decryption` `Your nostr extension does not support nip44 encryption & decryption`
) )
} }
// Encrypt the content using NIP-44 provided by the nostr extension.
const encrypted = await nostr.nip44.encrypt(receiver, content) const encrypted = await nostr.nip44.encrypt(receiver, content)
return encrypted as string return encrypted as string
} }
// Handle encryption when the login method is via a private key.
if (loginMethod === LoginMethods.privateKey) { if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair const keys = (store.getState().auth as AuthState).keyPair
// Check if the private and public key pair is available.
if (!keys) { if (!keys) {
throw new Error( throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.` `Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
) )
} }
// Decode the private key.
const { private: nsec } = keys const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array const privateKey = nip19.decode(nsec).data as Uint8Array
// Generate the conversation key using NIP-44 utilities.
const nip44ConversationKey = nip44.v2.utils.getConversationKey( const nip44ConversationKey = nip44.v2.utils.getConversationKey(
privateKey, privateKey,
receiver receiver
) )
// Encrypt the content using the generated conversation key.
const encrypted = nip44.v2.encrypt(content, nip44ConversationKey) const encrypted = nip44.v2.encrypt(content, nip44ConversationKey)
return encrypted return encrypted
} }
// Throw an error if the login method is nsecBunker (not supported).
if (loginMethod === LoginMethods.nsecBunker) { if (loginMethod === LoginMethods.nsecBunker) {
throw new Error( throw new Error(
`nip44 encryption is not yet supported for login method '${LoginMethods.nsecBunker}'` `nip44 encryption is not yet supported for login method '${LoginMethods.nsecBunker}'`
) )
} }
// Throw an error if the login method is undefined or unsupported.
throw new Error('Login method is undefined') throw new Error('Login method is undefined')
} }
/**
* Decrypts the given content from the specified sender using NIP-44 decryption.
*
* @param sender The public key of the sender.
* @param content The encrypted content to be decrypted.
* @returns The decrypted content as a string.
* @throws Error if the nostr extension does not support NIP-44, if the private key pair is not found, or if the login method is unsupported.
*/
nip44Decrypt = async (sender: string, content: string) => { nip44Decrypt = async (sender: string, content: string) => {
// Retrieve the current login method from the application's redux state.
const loginMethod = (store.getState().auth as AuthState).loginMethod const loginMethod = (store.getState().auth as AuthState).loginMethod
// Handle decryption when the login method is via an extension.
if (loginMethod === LoginMethods.extension) { if (loginMethod === LoginMethods.extension) {
const nostr = this.getNostrObject() const nostr = this.getNostrObject()
// Check if the nostr object supports NIP-44 decryption.
if (!nostr.nip44) { if (!nostr.nip44) {
throw new Error( throw new Error(
`Your nostr extension does not support nip44 encryption & decryption` `Your nostr extension does not support nip44 encryption & decryption`
) )
} }
// Decrypt the content using NIP-44 provided by the nostr extension.
const decrypted = await nostr.nip44.decrypt(sender, content) const decrypted = await nostr.nip44.decrypt(sender, content)
return decrypted as string return decrypted as string
} }
// Handle decryption when the login method is via a private key.
if (loginMethod === LoginMethods.privateKey) { if (loginMethod === LoginMethods.privateKey) {
const keys = (store.getState().auth as AuthState).keyPair const keys = (store.getState().auth as AuthState).keyPair
// Check if the private and public key pair is available.
if (!keys) { if (!keys) {
throw new Error( throw new Error(
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.` `Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
) )
} }
// Decode the private key.
const { private: nsec } = keys const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array const privateKey = nip19.decode(nsec).data as Uint8Array
// Generate the conversation key using NIP-44 utilities.
const nip44ConversationKey = nip44.v2.utils.getConversationKey( const nip44ConversationKey = nip44.v2.utils.getConversationKey(
privateKey, privateKey,
sender sender
) )
// Decrypt the content using the generated conversation key.
const decrypted = nip44.v2.decrypt(content, nip44ConversationKey) const decrypted = nip44.v2.decrypt(content, nip44ConversationKey)
return decrypted return decrypted
} }
// Throw an error if the login method is nsecBunker (not supported).
if (loginMethod === LoginMethods.nsecBunker) { if (loginMethod === LoginMethods.nsecBunker) {
throw new Error( throw new Error(
`nip44 decryption is not yet supported for login method '${LoginMethods.nsecBunker}'` `nip44 decryption is not yet supported for login method '${LoginMethods.nsecBunker}'`
) )
} }
// Throw an error if the login method is undefined or unsupported.
throw new Error('Login method is undefined') throw new Error('Login method is undefined')
} }

View File

@ -32,6 +32,7 @@ import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { UserComponent } from '../../components/username' import { UserComponent } from '../../components/username'
import { MetadataController, NostrController } from '../../controllers' import { MetadataController, NostrController } from '../../controllers'
import { appPrivateRoutes } from '../../routes'
import { State } from '../../store/rootReducer' import { State } from '../../store/rootReducer'
import { import {
CreateSignatureEventContent, CreateSignatureEventContent,
@ -42,6 +43,7 @@ import {
} from '../../types' } from '../../types'
import { import {
encryptArrayBuffer, encryptArrayBuffer,
formatTimestamp,
generateEncryptionKey, generateEncryptionKey,
generateKeys, generateKeys,
generateKeysFile, generateKeysFile,
@ -58,24 +60,6 @@ import {
uploadToFileStorage uploadToFileStorage
} from '../../utils' } from '../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
import { appPrivateRoutes } from '../../routes'
/**
* Helper function to get the current timestamp in YYMMDD:HHMMSS format
*/
const getFormattedTimestamp = () => {
const now = new Date()
const padZero = (num: number) => (num < 10 ? '0' + num : num)
const year = now.getFullYear()
const month = padZero(now.getMonth() + 1) // Months are zero-indexed
const day = padZero(now.getDate())
const hours = padZero(now.getHours())
const minutes = padZero(now.getMinutes())
const seconds = padZero(now.getSeconds())
return `${year}-${month}-${day}_${hours}:${minutes}:${seconds}`
}
export const CreatePage = () => { export const CreatePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -87,7 +71,7 @@ export const CreatePage = () => {
const [authUrl, setAuthUrl] = useState<string>() const [authUrl, setAuthUrl] = useState<string>()
const [title, setTitle] = useState(`sigit_${getFormattedTimestamp()}`) const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
const [selectedFiles, setSelectedFiles] = useState<File[]>([]) const [selectedFiles, setSelectedFiles] = useState<File[]>([])
const [userInput, setUserInput] = useState('') const [userInput, setUserInput] = useState('')

View File

@ -42,6 +42,12 @@ enum SignedStatus {
export const SignPage = () => { export const SignPage = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
/**
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains keys.json
* arrayBuffer will be received in navigation from create page in offline mode
* meta will be received in navigation from create & home page in online mode
*/
const { const {
meta: metaInNavState, meta: metaInNavState,
arrayBuffer: decryptedArrayBuffer, arrayBuffer: decryptedArrayBuffer,

View File

@ -44,6 +44,10 @@ export const VerifyPage = () => {
) )
const location = useLocation() const location = useLocation()
/**
* uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json
* meta will be received in navigation from create & home page in online mode
*/
const { uploadedZip, meta: metaInNavState } = location.state || {} const { uploadedZip, meta: metaInNavState } = location.state || {}
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)

View File

@ -181,10 +181,18 @@ export const generateKeys = async (
return { sender: getPublicKey(privateKey), keys } return { sender: getPublicKey(privateKey), keys }
} }
/**
* Function to extract the ZIP URL and encryption key from the provided metadata.
* @param meta - The metadata object containing the create signature and encryption keys.
* @returns A promise that resolves to an object containing the create signature event,
* create signature content, ZIP URL, and decrypted encryption key.
*/
export const extractZipUrlAndEncryptionKey = async (meta: Meta) => { export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
// Parse the create signature event from the metadata
const createSignatureEvent = await parseJson<Event>( const createSignatureEvent = await parseJson<Event>(
meta.createSignature meta.createSignature
).catch((err) => { ).catch((err) => {
// Log and display an error message if parsing fails
console.log('err in parsing the createSignature event:>> ', err) console.log('err in parsing the createSignature event:>> ', err)
toast.error( toast.error(
err.message || 'error occurred in parsing the create signature event' err.message || 'error occurred in parsing the create signature event'
@ -192,17 +200,21 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
return null return null
}) })
// Return null if the create signature event could not be parsed
if (!createSignatureEvent) return null if (!createSignatureEvent) return null
// Verify the validity of the create signature event
const isValidCreateSignature = verifyEvent(createSignatureEvent) const isValidCreateSignature = verifyEvent(createSignatureEvent)
if (!isValidCreateSignature) { if (!isValidCreateSignature) {
toast.error('Create signature is invalid') toast.error('Create signature is invalid')
return null return null
} }
// Parse the content of the create signature event
const createSignatureContent = await parseJson<CreateSignatureEventContent>( const createSignatureContent = await parseJson<CreateSignatureEventContent>(
createSignatureEvent.content createSignatureEvent.content
).catch((err) => { ).catch((err) => {
// Log and display an error message if parsing fails
console.log(`err in parsing the createSignature event's content :>> `, err) console.log(`err in parsing the createSignature event's content :>> `, err)
toast.error( toast.error(
`error occurred in parsing the create signature event's content` `error occurred in parsing the create signature event's content`
@ -210,29 +222,38 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
return null return null
}) })
// Return null if the create signature content could not be parsed
if (!createSignatureContent) return null if (!createSignatureContent) return null
// Extract the ZIP URL from the create signature content
const zipUrl = createSignatureContent.zipUrl const zipUrl = createSignatureContent.zipUrl
// Retrieve the user's public key from the state
const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
const usersNpub = hexToNpub(usersPubkey) const usersNpub = hexToNpub(usersPubkey)
// Return null if the metadata does not contain keys
if (!meta.keys) return null if (!meta.keys) return null
const { sender, keys } = meta.keys const { sender, keys } = meta.keys
// Check if the user's public key is in the keys object
if (usersNpub in keys) { if (usersNpub in keys) {
// Instantiate the NostrController to decrypt the encryption key
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
const decrypted = await nostrController const decrypted = await nostrController
.nip04Decrypt(sender, keys[usersNpub]) .nip04Decrypt(sender, keys[usersNpub])
.catch((err) => { .catch((err) => {
// Log and display an error message if decryption fails
console.log('An error occurred in decrypting encryption key', err) console.log('An error occurred in decrypting encryption key', err)
toast.error('An error occurred in decrypting encryption key') toast.error('An error occurred in decrypting encryption key')
return null return null
}) })
// Return null if the encryption key could not be decrypted
if (!decrypted) return null if (!decrypted) return null
// Return the parsed and decrypted data
return { return {
createSignatureEvent, createSignatureEvent,
createSignatureContent, createSignatureContent,

View File

@ -493,8 +493,10 @@ export const updateUsersAppData = async (meta: Meta) => {
if (!newBlossomUrl) return null if (!newBlossomUrl) return null
// insert new blossom url at the start of the array
blossomUrls.unshift(newBlossomUrl) blossomUrls.unshift(newBlossomUrl)
// only keep last 10 blossom urls, delete older ones
if (blossomUrls.length > 10) { if (blossomUrls.length > 10) {
const filesToDelete = blossomUrls.splice(10) const filesToDelete = blossomUrls.splice(10)
filesToDelete.forEach((url) => { filesToDelete.forEach((url) => {
@ -509,6 +511,7 @@ export const updateUsersAppData = async (meta: Meta) => {
const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
// encrypt content for storing in kind 30078 event
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
const encryptedContent = await nostrController const encryptedContent = await nostrController
.nip04Encrypt( .nip04Encrypt(
@ -624,24 +627,37 @@ const deleteBlossomFile = async (url: string, privateKey: string) => {
console.log('response.data :>> ', response.data) console.log('response.data :>> ', response.data)
} }
/**
* 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.
*/
const uploadUserAppDataToBlossom = async ( const uploadUserAppDataToBlossom = async (
sigits: { [key: string]: Meta }, sigits: { [key: string]: Meta },
processedGiftWraps: string[], processedGiftWraps: string[],
privateKey: string privateKey: string
) => { ) => {
// Create an object containing the sigits and processed gift wraps
const obj = { const obj = {
sigits, sigits,
processedGiftWraps processedGiftWraps
} }
// Convert the object to a JSON string
const stringified = JSON.stringify(obj) const stringified = JSON.stringify(obj)
// Convert the private key from hex to bytes
const secretKey = hexToBytes(privateKey) const secretKey = hexToBytes(privateKey)
// Encrypt the JSON string using the secret key
const encrypted = nip44.v2.encrypt( const encrypted = nip44.v2.encrypt(
stringified, stringified,
nip44ConversationKey(secretKey, getPublicKey(secretKey)) nip44ConversationKey(secretKey, getPublicKey(secretKey))
) )
// Create a blob from the encrypted data
const blob = new Blob([encrypted], { type: 'application/octet-stream' }) const blob = new Blob([encrypted], { type: 'application/octet-stream' })
// Create a file from the blob
const file = new File([blob], 'encrypted.txt', { const file = new File([blob], 'encrypted.txt', {
type: 'application/octet-stream' type: 'application/octet-stream'
}) })
@ -659,8 +675,10 @@ const uploadUserAppDataToBlossom = async (
] ]
} }
// Finalize the event with the private key
const authEvent = finalizeEvent(event, hexToBytes(privateKey)) const authEvent = finalizeEvent(event, hexToBytes(privateKey))
// URL of the file storage service
const FILE_STORAGE_URL = 'https://blossom.sigit.io' const FILE_STORAGE_URL = 'https://blossom.sigit.io'
// Upload the file to the file storage service using Axios // Upload the file to the file storage service using Axios
@ -674,22 +692,34 @@ const uploadUserAppDataToBlossom = async (
return response.data.url as string return response.data.url as string
} }
/**
* 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.
*/
const getUserAppDataFromBlossom = async (url: string, privateKey: string) => { const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
// Initialize errorCode to track HTTP error codes
let errorCode = 0 let errorCode = 0
// Fetch the encrypted data from the provided URL
const encrypted = await axios const encrypted = await axios
.get(url, { .get(url, {
responseType: 'blob' responseType: 'blob' // Expect a blob response
}) })
.then(async (res) => { .then(async (res) => {
// Convert the blob response to a File object
const file = new File([res.data], 'encrypted.txt') const file = new File([res.data], 'encrypted.txt')
// Read the text content from the file
const text = await file.text() const text = await file.text()
return text return text
}) })
.catch((err) => { .catch((err) => {
// Log and display an error message if the request fails
console.error(`error occurred in getting file from ${url}`, err) console.error(`error occurred in getting file from ${url}`, err)
toast.error(err.message || `error occurred in getting file from ${url}`) toast.error(err.message || `error occurred in getting file from ${url}`)
// Set errorCode to the HTTP status code if available
if (err.request) { if (err.request) {
const { status } = err.request const { status } = err.request
errorCode = status errorCode = status
@ -698,6 +728,7 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
return null return null
}) })
// Return a default value if the requested resource is not found (404)
if (errorCode === 404) { if (errorCode === 404) {
return { return {
sigits: {}, sigits: {},
@ -705,20 +736,26 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
} }
} }
// Return null if the encrypted data could not be retrieved
if (!encrypted) return null if (!encrypted) return null
// Convert the private key from hex to bytes
const secret = hexToBytes(privateKey) const secret = hexToBytes(privateKey)
// Get the public key corresponding to the private key
const pubkey = getPublicKey(secret) const pubkey = getPublicKey(secret)
// Decrypt the encrypted data using the secret and public key
const decrypted = nip44.v2.decrypt( const decrypted = nip44.v2.decrypt(
encrypted, encrypted,
nip44ConversationKey(secret, pubkey) nip44ConversationKey(secret, pubkey)
) )
// Parse the decrypted JSON content
const parsedContent = await parseJson<{ const parsedContent = await parseJson<{
sigits: { [key: string]: Meta } sigits: { [key: string]: Meta }
processedGiftWraps: string[] processedGiftWraps: string[]
}>(decrypted).catch((err) => { }>(decrypted).catch((err) => {
// Log and display an error message if parsing fails
console.log( console.log(
'An error occurred in parsing the user app data content from blossom server', 'An error occurred in parsing the user app data content from blossom server',
err err
@ -729,15 +766,22 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
return null return null
}) })
// Return the parsed content
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) => { export const subscribeForSigits = async (pubkey: string) => {
// Get relay list metadata // Instantiate the MetadataController to retrieve relay list metadata
const metadataController = new MetadataController() const metadataController = new MetadataController()
const relaySet = await metadataController const relaySet = await metadataController
.findRelayListMetadata(pubkey) .findRelayListMetadata(pubkey)
.catch((err) => { .catch((err) => {
// Log an error if retrieving relay list metadata fails
console.log( console.log(
`An error occurred while finding relay list metadata for ${hexToNpub(pubkey)}`, `An error occurred while finding relay list metadata for ${hexToNpub(pubkey)}`,
err err
@ -751,15 +795,20 @@ export const subscribeForSigits = async (pubkey: string) => {
// Ensure relay list is not empty // Ensure relay list is not empty
if (relaySet.read.length === 0) return if (relaySet.read.length === 0) return
// Define the filter for the subscription
const filter: Filter = { const filter: Filter = {
kinds: [1059], kinds: [1059],
'#p': [pubkey] '#p': [pubkey]
} }
// Instantiate a new SimplePool for the subscription
const pool = new SimplePool() const pool = new SimplePool()
// Subscribe to the specified relays with the defined filter
return pool.subscribeMany(relaySet.read, [filter], { return pool.subscribeMany(relaySet.read, [filter], {
// Define a callback function to handle received events
onevent: (event) => { onevent: (event) => {
processReceivedEvent(event) processReceivedEvent(event) // Process the received event
} }
}) })
} }
@ -810,9 +859,16 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
updateUsersAppData(meta) 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) => { export const sendNotification = async (receiver: string, meta: Meta) => {
// Retrieve the user's public key from the state
const usersPubkey = (store.getState().auth as AuthState).usersPubkey! const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
// Create an unsigned event object with the provided metadata
const unsignedEvent: UnsignedEvent = { const unsignedEvent: UnsignedEvent = {
kind: 938, kind: 938,
pubkey: usersPubkey, pubkey: usersPubkey,
@ -821,13 +877,15 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
created_at: now() created_at: now()
} }
// Wrap the unsigned event with the receiver's information
const wrappedEvent = createWrap(unsignedEvent, receiver) const wrappedEvent = createWrap(unsignedEvent, receiver)
// Get relay list metadata // Instantiate the MetadataController to retrieve relay list metadata
const metadataController = new MetadataController() const metadataController = new MetadataController()
const relaySet = await metadataController const relaySet = await metadataController
.findRelayListMetadata(receiver) .findRelayListMetadata(receiver)
.catch((err) => { .catch((err) => {
// Log an error if retrieving relay list metadata fails
console.log( console.log(
`An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`, `An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
err err
@ -845,10 +903,12 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
// Publish the notification event to the recipient's read relays // Publish the notification event to the recipient's read relays
const nostrController = NostrController.getInstance() const nostrController = NostrController.getInstance()
// Attempt to publish the event to the relays, with a timeout of 2 minutes
await Promise.race([ await Promise.race([
nostrController.publishEvent(wrappedEvent, relaySet.read), nostrController.publishEvent(wrappedEvent, relaySet.read),
timeout(1000 * 60 * 2) timeout(1000 * 60 * 2)
]).catch((err) => { ]).catch((err) => {
// Log an error if publishing the notification event fails
console.log( console.log(
`An error occurred while publishing notification event for ${hexToNpub(receiver)}`, `An error occurred while publishing notification event for ${hexToNpub(receiver)}`,
err err