Compare commits

...

4 Commits

8 changed files with 190 additions and 136 deletions

View File

@ -145,11 +145,20 @@ export class MetadataController extends EventEmitter {
* or a fallback RelaySet with Sigit's Relay * or a fallback RelaySet with Sigit's Relay
*/ */
public findRelayListMetadata = async (hexKey: string): Promise<RelaySet> => { public findRelayListMetadata = async (hexKey: string): Promise<RelaySet> => {
const relayEvent = try {
(await findRelayListInCache(hexKey)) || const relayEvent =
(await findRelayListAndUpdateCache(DEFAULT_LOOK_UP_RELAY_LIST, hexKey)) (await findRelayListInCache(hexKey)) ||
(await findRelayListAndUpdateCache(DEFAULT_LOOK_UP_RELAY_LIST, hexKey))
return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet() return relayEvent
? getUserRelaySet(relayEvent.tags)
: getDefaultRelaySet()
} catch (error) {
throw new Error(
`An error occurred while finding relay list metadata for ${hexKey}`,
{ cause: error }
)
}
} }
public extractProfileMetadataContent = (event: Event) => { public extractProfileMetadataContent = (event: Event) => {

View File

@ -2,7 +2,8 @@ import { Event, Filter, Relay } from 'nostr-tools'
import { import {
settleAllFullfilfedPromises, settleAllFullfilfedPromises,
normalizeWebSocketURL, normalizeWebSocketURL,
timeout publishToRelay,
isPromiseFulfilled
} from '../utils' } from '../utils'
import { SIGIT_RELAY } from '../utils/const' import { SIGIT_RELAY } from '../utils/const'
@ -261,17 +262,17 @@ export class RelayController {
event: Event, event: Event,
relayUrls: string[] = [] relayUrls: string[] = []
): Promise<string[]> => { ): Promise<string[]> => {
if (!relayUrls.includes(SIGIT_RELAY)) { /**
/** * Ensure that the default Sigit Relay is included.
* NOTE: To avoid side-effects on external relayUrls array passed as argument * Copy the array instead of mutating it.
* re-assigned relayUrls with added sigit relay instead of just appending to same array */
*/ const updatedRelayUrls = !relayUrls.includes(SIGIT_RELAY)
relayUrls = [...relayUrls, SIGIT_RELAY] // Add app relay to relays array if not exists already ? [...relayUrls, SIGIT_RELAY]
} : [...relayUrls]
// connect to all specified relays // connect to all specified relays
const relays = await settleAllFullfilfedPromises( const relays = await settleAllFullfilfedPromises(
relayUrls, updatedRelayUrls,
this.connectRelay this.connectRelay
) )
@ -280,26 +281,15 @@ export class RelayController {
throw new Error('No relay is connected to publish event!') throw new Error('No relay is connected to publish event!')
} }
const publishedOnRelays: string[] = [] // List to track which relays successfully published the event const settledPromises: PromiseSettledResult<string>[] =
await Promise.allSettled(
// Create a promise for publishing the event to each connected relay relays.map(async (relay) => publishToRelay(relay, event))
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 the list of relay URLs where the event was published
return publishedOnRelays return settledPromises
.filter(isPromiseFulfilled)
.map((res) => res.value) as string[]
} }
} }

View File

@ -84,6 +84,7 @@ import {
faUpload faUpload
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { SigitFile } from '../../utils/file.ts' import { SigitFile } from '../../utils/file.ts'
import { checkNotifications } from '../../utils/notifications.ts'
export const CreatePage = () => { export const CreatePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -720,7 +721,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) => sendNotification(receiver, meta)) return receivers.map(async (receiver) => sendNotification(receiver, meta))
} }
const handleCreate = async () => { const handleCreate = async () => {
@ -784,16 +785,8 @@ export const CreatePage = () => {
if (!event) return if (!event) return
setLoadingSpinnerDesc('Sending notifications to counterparties') setLoadingSpinnerDesc('Sending notifications to counterparties')
const promises = sendNotifications(meta) const notifications = await Promise.allSettled(sendNotifications(meta))
checkNotifications(notifications)
await Promise.all(promises)
.then(() => {
toast.success('Notifications sent successfully')
})
.catch(() => {
toast.error('Failed to publish notifications')
})
navigate(appPrivateRoutes.sign, { state: { meta: meta } }) navigate(appPrivateRoutes.sign, { state: { meta: meta } })
} else { } else {
const zip = new JSZip() const zip = new JSZip()

View File

@ -53,6 +53,7 @@ import {
SigitFile SigitFile
} from '../../utils/file.ts' } from '../../utils/file.ts'
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
import { checkNotifications } from '../../utils/notifications.ts'
enum SignedStatus { enum SignedStatus {
Fully_Signed, Fully_Signed,
User_Is_Next_Signer, User_Is_Next_Signer,
@ -663,57 +664,22 @@ export const SignPage = () => {
// Handle the online flow: update users app data and send notifications // Handle the online flow: update users app data and send notifications
const handleOnlineFlow = async (meta: Meta) => { const handleOnlineFlow = async (meta: Meta) => {
setLoadingSpinnerDesc('Updating users app data') try {
const updatedEvent = await updateUsersAppData(meta) setLoadingSpinnerDesc('Updating users app data')
if (!updatedEvent) { const updatedEvent = await updateUsersAppData(meta)
if (!updatedEvent) {
throw new Error('There was an error updating user app data.')
}
setLoadingSpinnerDesc('Sending notifications')
const notifications = await notifyUsers(meta)
checkNotifications(notifications)
} catch (error) {
console.error(error)
toast.error('There was an error finalising signatures.')
} finally {
setIsLoading(false) setIsLoading(false)
return
} }
const userSet = new Set<`npub1${string}`>()
if (submittedBy && submittedBy !== usersPubkey) {
userSet.add(hexToNpub(submittedBy))
}
const usersNpub = hexToNpub(usersPubkey!)
const isLastSigner = checkIsLastSigner(signers)
if (isLastSigner) {
signers.forEach((signer) => {
if (signer !== usersNpub) {
userSet.add(signer)
}
})
viewers.forEach((viewer) => {
userSet.add(viewer)
})
} else {
const currentSignerIndex = signers.indexOf(usersNpub)
const prevSigners = signers.slice(0, currentSignerIndex)
prevSigners.forEach((signer) => {
userSet.add(signer)
})
const nextSigner = signers[currentSignerIndex + 1]
userSet.add(nextSigner)
}
setLoadingSpinnerDesc('Sending notifications')
const users = Array.from(userSet)
const promises = users.map((user) =>
sendNotification(npubToHex(user)!, meta)
)
await Promise.all(promises)
.then(() => {
toast.success('Notifications sent successfully')
setMeta(meta)
})
.catch(() => {
toast.error('Failed to publish notifications')
})
setIsLoading(false)
} }
// Check if the current user is the last signer // Check if the current user is the last signer
@ -871,6 +837,64 @@ export const SignPage = () => {
} }
} }
const getUsersToNotify = (): `npub1${string}`[] => {
const userSet = new Set<`npub1${string}`>()
if (submittedBy && submittedBy !== usersPubkey) {
userSet.add(hexToNpub(submittedBy))
}
const usersNpub = hexToNpub(usersPubkey!)
const isLastSigner = checkIsLastSigner(signers)
if (isLastSigner) {
signers.forEach((signer) => {
if (signer !== usersNpub) {
userSet.add(signer)
}
})
viewers.forEach((viewer) => userSet.add(viewer))
} else {
const currentSignerIndex = signers.indexOf(usersNpub)
const prevSigners = signers.slice(0, currentSignerIndex)
prevSigners.forEach((signer) => {
userSet.add(signer)
})
const nextSigner = signers[currentSignerIndex + 1]
userSet.add(nextSigner)
}
return Array.from(userSet)
}
const notifyUsers = async (meta: Meta) => {
try {
const usersToNotify = getUsersToNotify()
return await Promise.allSettled(
usersToNotify.map(async (user) =>
sendNotification(npubToHex(user)!, meta)
)
)
} catch (error) {
throw new Error('There was a problem sending notifications to users', {
cause: error
})
}
}
const handleRenotifyUsers = async () => {
try {
setIsLoading(true)
const notifications = await notifyUsers(meta!)
checkNotifications(notifications)
} catch (error) {
console.error(error)
toast.error('There was an error re-notifying users')
} finally {
setIsLoading(false)
}
}
if (authUrl) { if (authUrl) {
return ( return (
<iframe <iframe
@ -967,6 +991,12 @@ export const SignPage = () => {
</Button> </Button>
</Box> </Box>
)} )}
<Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
<Button onClick={handleRenotifyUsers} variant="contained">
Re-notify Next Signer
</Button>
</Box>
</> </>
)} )}
</Container> </Container>

View File

@ -916,50 +916,33 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
* @param meta - Metadata associated with the notification. * @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 try {
const usersPubkey = (store.getState().auth as AuthState).usersPubkey! // Retrieve the user's public key from the state
const usersPubkey = (store.getState().auth as AuthState).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()
}
// Create an unsigned event object with the provided metadata // Wrap the unsigned event with the receiver's information
const unsignedEvent: UnsignedEvent = { const wrappedEvent = createWrap(unsignedEvent, receiver)
kind: 938,
pubkey: usersPubkey,
content: JSON.stringify(meta),
tags: [],
created_at: unixNow()
}
// Wrap the unsigned event with the receiver's information // Instantiate the MetadataController to retrieve relay list metadata
const wrappedEvent = createWrap(unsignedEvent, receiver) const metadataController = new MetadataController()
// Instantiate the MetadataController to retrieve relay list metadata const relaySet = await metadataController.findRelayListMetadata(receiver)
const metadataController = new MetadataController()
const relaySet = await metadataController return await Promise.race([
.findRelayListMetadata(receiver) relayController.publish(wrappedEvent, relaySet.read),
.catch((err) => { timeout(40 * 1000)
// Log an error if retrieving relay list metadata fails ])
console.log( } catch (error) {
`An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`, throw new Error(`Failed to publish a notification for ${receiver}`, {
err cause: error
)
return null
}) })
}
// Return if metadata retrieval failed
if (!relaySet) return
// Ensure relay list is not empty
if (relaySet.read.length === 0) return
// Publish the notification event to the recipient's read relays
await Promise.race([
relayController.publish(wrappedEvent, relaySet.read),
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
})
} }

View File

@ -0,0 +1,26 @@
import { isPromiseFulfilled, isPromiseRejected } from './utils.ts'
import { toast } from 'react-toastify'
const checkNotifications = (
notifications: PromiseSettledResult<string[]>[]
) => {
if (notifications.every(isPromiseFulfilled)) {
toast.success('Notifications sent successfully')
} else {
logRejectedNotifications(notifications)
toast.error(
'Some notifications failed to publish. Please rebroadcast them again.'
)
}
}
const logRejectedNotifications = (
notifications: PromiseSettledResult<string[]>[]
) => {
notifications
.filter(isPromiseRejected)
.map((res: PromiseRejectedResult) => (res.reason as Error).message)
.forEach((message: string) => console.log(message))
}
export { checkNotifications }

View File

@ -1,6 +1,6 @@
import { Event, Filter, kinds, UnsignedEvent } from 'nostr-tools' import { Event, Filter, kinds, Relay, UnsignedEvent } from 'nostr-tools'
import { RelayList } from 'nostr-tools/kinds' import { RelayList } from 'nostr-tools/kinds'
import { getRelayInfo, unixNow } from '.' import { getRelayInfo, timeout, unixNow } from '.'
import { NostrController, relayController } from '../controllers' import { NostrController, relayController } from '../controllers'
import { localCache } from '../services' import { localCache } from '../services'
import { RelayMap, RelaySet } from '../types' import { RelayMap, RelaySet } from '../types'
@ -229,6 +229,16 @@ const publishRelayMap = async (
return Promise.reject('Publishing updated relay map was unsuccessful.') return Promise.reject('Publishing updated relay map was unsuccessful.')
} }
const publishToRelay = async (relay: Relay, event: Event): Promise<string> => {
try {
await Promise.race([relay.publish(event), timeout(20 * 1000)])
return relay.url
} catch (err) {
console.error(`Failed to publish event on relay: ${relay.url}`, err)
throw new Error(`Failed to publish event on relay: ${relay.url}`)
}
}
export { export {
findRelayListAndUpdateCache, findRelayListAndUpdateCache,
findRelayListInCache, findRelayListInCache,
@ -238,5 +248,6 @@ export {
getUserRelaySet, getUserRelaySet,
isOlderThanOneDay, isOlderThanOneDay,
isOlderThanOneWeek, isOlderThanOneWeek,
publishRelayMap publishRelayMap,
publishToRelay
} }

View File

@ -118,3 +118,15 @@ export const settleAllFullfilfedPromises = async <Item, FulfilledItem = Item>(
return acc return acc
}, []) }, [])
} }
export const isPromiseFulfilled = <T>(
result: PromiseSettledResult<T>
): result is PromiseFulfilledResult<T> => {
return result.status === 'fulfilled'
}
export const isPromiseRejected = <T>(
result: PromiseSettledResult<T>
): result is PromiseRejectedResult => {
return result.status === 'rejected'
}