Compare commits
4 Commits
791c213e3a
...
89f6cd1dbe
Author | SHA1 | Date | |
---|---|---|---|
89f6cd1dbe | |||
9cefdad3fc | |||
de7a8a868a | |||
b965623a02 |
@ -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) => {
|
||||||
|
@ -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[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
26
src/utils/notifications.ts
Normal file
26
src/utils/notifications.ts
Normal 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 }
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user