sigit.io/src/utils/nostr.ts

989 lines
27 KiB
TypeScript
Raw Normal View History

2024-07-05 13:38:04 +05:00
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
import axios from 'axios'
import _, { truncate } from 'lodash'
2024-06-28 14:24:14 +05:00
import {
Event,
EventTemplate,
Filter,
UnsignedEvent,
finalizeEvent,
generateSecretKey,
getEventHash,
getPublicKey,
kinds,
nip04,
2024-06-28 14:24:14 +05:00
nip19,
nip44,
verifyEvent
} from 'nostr-tools'
import { toast } from 'react-toastify'
import { NIP05_REGEX } from '../constants'
import {
MetadataController,
NostrController,
relayController
} from '../controllers'
2024-07-05 13:38:04 +05:00
import {
updateProcessedGiftWraps,
updateUserAppData as updateUserAppDataAction
} from '../store/actions'
2024-10-08 20:14:44 +02:00
import { Keys } from '../store/auth/types'
2024-06-28 14:24:14 +05:00
import store from '../store/store'
import { Meta, ProfileMetadata, SignedEvent, UserAppData } from '../types'
import { getDefaultRelayMap } from './relays'
2024-07-05 13:38:04 +05:00
import { parseJson, removeLeadingSlash } from './string'
import { timeout } from './utils'
import { getHash } from './hash'
import { SIGIT_BLOSSOM } from './const.ts'
/**
* Generates a `d` tag for userAppData
*/
const getDTagForUserAppData = async (): Promise<string | null> => {
2024-10-08 20:14:44 +02:00
const isLoggedIn = store.getState().auth.loggedIn
const pubkey = store.getState().auth?.usersPubkey
if (!isLoggedIn || !pubkey) {
throw new Error(
'For generating d tag user must be logged in and a valid pubkey should exists in app Store'
)
}
return getHash(`938_${pubkey}`)
}
/**
* @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
*
* @param pubKey in NPUB, HEX format
* @returns HEX format
*/
export const npubToHex = (pubKey: string): string | null => {
// If key is NPUB
if (pubKey.startsWith('npub1')) {
try {
return nip19.decode(pubKey).data as string
} catch (error) {
return null
}
}
// valid hex key
if (validateHex(pubKey)) return pubKey
// Not a valid hex key
return null
}
/**
* 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
}
export const hexToNpub = (hexPubkey: string): `npub1${string}` => {
if (hexPubkey.startsWith('npub1')) return hexPubkey as `npub1${string}`
2024-03-01 15:16:35 +05:00
return nip19.npubEncode(hexPubkey)
}
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.'
)
}
}
/**
* 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.
*/
export const queryNip05 = async (
nip05: string
): Promise<{
pubkey: string
relays: string[]
}> => {
const match = nip05.match(NIP05_REGEX)
// Throw an error if the NIP-05 identifier is invalid
if (!match) throw new Error('Invalid nip05')
// Destructure the match result, assigning default value '_' to name if not provided
// First variable from the match destructuring is ignored
2024-08-05 09:49:56 +02:00
const [, name = '_', domain] = match
// Construct the URL to query the NIP-05 data
const url = `https://${domain}/.well-known/nostr.json?name=${name}`
// Perform the network request to get the NIP-05 data
const res = await axios(url)
.then((res) => {
return res.data
})
.catch((err) => {
console.log('err :>> ', err)
throw err
})
// Extract the public key from the response data
const pubkey = res.names[name]
const relays: string[] = []
// If a public key is found
if (pubkey) {
// 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) {
const root = res.names['_']
if (root) {
// Check for root user relays in both NIP-46 and relays sections
addRelays(res.nip46?.[root] as string[])
addRelays(res.relays?.[root] as string[])
}
}
}
// Return the public key and the array of relays
return {
pubkey,
relays
}
}
2024-03-19 15:27:18 +05: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')
}
}
/**
* @param pubkey in hex or npub format
* @returns robohash.org url for the avatar
*/
2024-05-17 13:35:37 +02:00
export const getRoboHashPicture = (
pubkey?: string,
set: number = 1
): string => {
if (!pubkey) return ''
const npub = hexToNpub(pubkey)
return `https://robohash.org/${npub}.png?set=set${set}`
}
2024-06-28 14:24:14 +05:00
export const unixNow = () => Math.round(Date.now() / 1000)
export const toUnixTimestamp = (date: number | Date) => {
let time
if (typeof date === 'number') {
time = Math.round(date / 1000)
} else if (date instanceof Date) {
time = Math.round(date.getTime() / 1000)
} else {
throw Error('Unsupported type when converting to unix timestamp')
}
return time
}
export const fromUnixTimestamp = (unix: number) => {
return unix * 1000
}
2024-06-28 14:24:14 +05:00
/**
* 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 13:38:04 +05:00
data: UnsignedEvent,
2024-06-28 14:24:14 +05: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 13:38:04 +05:00
export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => {
2024-06-28 14:24:14 +05: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 13:38:04 +05:00
const content = nip44Encrypt(unsignedEvent, randomKey, receiver)
2024-06-28 14:24:14 +05:00
// Initialize nonce and leadingZeroes for PoW calculation
let nonce = 0
let leadingZeroes = 0
2024-07-05 13:38:04 +05:00
const difficulty = Math.floor(Math.random() * 10) + 5 // random number between 5 & 10
2024-06-28 14:24:14 +05: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 13:38:04 +05:00
const event: UnsignedEvent = {
2024-06-28 14:24:14 +05:00
kind: 1059, // Event kind
content, // Encrypted content
pubkey, // Public key of the creator
created_at: unixNow(), // Current timestamp
2024-06-28 14:24:14 +05: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 13:38:04 +05:00
const hash = getEventHash(event)
2024-06-28 14:24:14 +05: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 13:38:04 +05:00
return finalizeEvent(event, randomKey)
2024-06-28 14:24:14 +05:00
}
// Increment the nonce for the next iteration
nonce++
}
}
/**
* Fetches user application data based on user's public key and stored metadata.
*
* @returns The user application data or null if an error occurs or no data is found.
*/
2024-07-05 13:38:04 +05:00
export const getUsersAppData = async (): Promise<UserAppData | null> => {
// Initialize an array to hold relay URLs
2024-06-28 14:24:14 +05:00
const relays: string[] = []
// Retrieve the user's public key and relay map from the Redux store
2024-10-08 20:14:44 +02:00
const usersPubkey = store.getState().auth.usersPubkey!
2024-06-28 14:24:14 +05:00
const relayMap = store.getState().relays?.map
// Check if relayMap is undefined in the Redux store
2024-06-28 14:24:14 +05:00
if (!relayMap) {
// If relayMap is not present, fetch relay list metadata
const metadataController = MetadataController.getInstance()
2024-06-28 14:24:14 +05:00
const relaySet = await metadataController
.findRelayListMetadata(usersPubkey)
.catch((err) => {
// Log error and return null if fetching metadata fails
2024-06-28 14:24:14 +05:00
console.log(
`An error occurred while finding relay list metadata for ${hexToNpub(usersPubkey)}`,
err
)
return null
})
// Return null if metadata retrieval failed
2024-07-05 13:38:04 +05:00
if (!relaySet) return null
2024-06-28 14:24:14 +05:00
// Ensure that the relay list is not empty
2024-07-05 13:38:04 +05:00
if (relaySet.write.length === 0) return null
2024-06-28 14:24:14 +05:00
// Add write relays to the relays array
2024-06-28 14:24:14 +05:00
relays.push(...relaySet.write)
} else {
// If relayMap exists, filter and add write relays from the stored map
2024-06-28 14:24:14 +05:00
const writeRelays = Object.keys(relayMap).filter(
(key) => relayMap[key].write
)
relays.push(...writeRelays)
}
// Generate an identifier for the user's nip78
const dTag = await getDTagForUserAppData()
if (!dTag) return null
2024-06-28 14:24:14 +05:00
// Define a filter for fetching events
2024-06-28 14:24:14 +05:00
const filter: Filter = {
kinds: [kinds.Application],
'#d': [dTag]
2024-06-28 14:24:14 +05:00
}
const encryptedContent = await relayController
.fetchEvent(filter, relays)
2024-06-28 14:24:14 +05:00
.then((event) => {
if (event) return event.content
// If no event is found, return an empty stringified object
2024-06-28 14:24:14 +05:00
return '{}'
})
.catch((err) => {
// Log error and show a toast notification if fetching event fails
2024-06-28 14:24:14 +05:00
console.log(`An error occurred in finding kind 30078 event`, err)
toast.error(
'An error occurred in finding kind 30078 event for data storage'
)
return null
})
// Return null if encrypted content retrieval fails
2024-06-28 14:24:14 +05:00
if (!encryptedContent) return null
// Handle case where the encrypted content is an empty object
2024-06-28 14:24:14 +05:00
if (encryptedContent === '{}') {
// Generate ephemeral key pair
2024-07-05 13:38:04 +05:00
const secret = generateSecretKey()
const pubKey = getPublicKey(secret)
return {
sigits: {},
processedGiftWraps: [],
blossomUrls: [],
keyPair: {
private: bytesToHex(secret),
public: pubKey
}
}
2024-06-28 14:24:14 +05:00
}
// Get an instance of the NostrController
const nostrController = NostrController.getInstance()
// Decrypt the encrypted content
2024-06-28 14:24:14 +05:00
const decrypted = await nostrController
.nip04Decrypt(usersPubkey, encryptedContent)
.catch((err) => {
// Log error and show a toast notification if decryption fails
2024-06-28 14:24:14 +05:00
console.log('An error occurred while decrypting app data', err)
toast.error('An error occurred while decrypting app data')
return null
})
// Return null if decryption fails
2024-06-28 14:24:14 +05:00
if (!decrypted) return null
// Parse the decrypted content
2024-06-28 14:24:14 +05:00
const parsedContent = await parseJson<{
2024-07-05 13:38:04 +05:00
blossomUrls: string[]
keyPair: Keys
2024-06-28 14:24:14 +05:00
}>(decrypted).catch((err) => {
// Log error and show a toast notification if parsing fails
2024-06-28 14:24:14 +05:00
console.log(
'An error occurred in parsing the content of kind 30078 event',
err
)
toast.error('An error occurred in parsing the content of kind 30078 event')
return null
})
// Return null if parsing fails
2024-06-28 14:24:14 +05:00
if (!parsedContent) return null
2024-07-05 13:38:04 +05:00
const { blossomUrls, keyPair } = parsedContent
// Return null if no blossom URLs are found
2024-07-05 13:38:04 +05:00
if (blossomUrls.length === 0) return null
// Fetch additional user app data from the first blossom URL
2024-07-05 13:38:04 +05:00
const dataFromBlossom = await getUserAppDataFromBlossom(
blossomUrls[0],
keyPair.private
)
// Return null if fetching data from blossom fails
2024-07-05 13:38:04 +05:00
if (!dataFromBlossom) return null
const { sigits, processedGiftWraps } = dataFromBlossom
// Return the final user application data
2024-07-05 13:38:04 +05:00
return {
blossomUrls,
keyPair,
sigits,
processedGiftWraps
}
2024-06-28 14:24:14 +05:00
}
2024-07-05 13:38:04 +05:00
export const updateUsersAppData = async (meta: Meta) => {
const appData = store.getState().userAppData
if (!appData || !appData.keyPair) return null
2024-06-28 14:24:14 +05:00
2024-07-05 13:38:04 +05: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 14:24:14 +05:00
if (existingMeta.modifiedAt < meta.modifiedAt) {
2024-07-05 13:38:04 +05:00
sigits[id] = meta
isUpdated = true
2024-06-28 14:24:14 +05:00
}
} else {
2024-07-05 13:38:04 +05:00
sigits[id] = meta
isUpdated = true
2024-06-28 14:24:14 +05:00
}
2024-07-05 13:38:04 +05: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-09 01:16:47 +05:00
// insert new blossom url at the start of the array
2024-07-05 13:38:04 +05:00
blossomUrls.unshift(newBlossomUrl)
2024-07-09 01:16:47 +05:00
// only keep last 10 blossom urls, delete older ones
2024-07-05 13:38:04 +05: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 14:24:14 +05:00
}
2024-10-08 20:14:44 +02:00
const usersPubkey = store.getState().auth.usersPubkey!
2024-06-28 14:24:14 +05:00
2024-07-09 01:16:47 +05:00
// encrypt content for storing in kind 30078 event
2024-06-28 14:24:14 +05:00
const nostrController = NostrController.getInstance()
const encryptedContent = await nostrController
2024-07-05 13:38:04 +05:00
.nip04Encrypt(
usersPubkey,
JSON.stringify({
blossomUrls,
keyPair: appData.keyPair
})
)
2024-06-28 14:24:14 +05: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
const dTag = await getDTagForUserAppData()
if (!dTag) return null
2024-06-28 14:24:14 +05:00
const updatedEvent: UnsignedEvent = {
kind: kinds.Application,
pubkey: usersPubkey!,
created_at: unixNow(),
tags: [['d', dTag]],
2024-06-28 14:24:14 +05:00
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
2024-10-08 20:14:44 +02:00
const relayMap = store.getState().relays.map || getDefaultRelayMap()
2024-06-28 14:24:14 +05:00
const writeRelays = Object.keys(relayMap).filter((key) => relayMap[key].write)
const publishResult = await Promise.race([
relayController.publish(signedEvent, writeRelays),
timeout(40 * 1000)
]).catch((err) => {
console.log('err :>> ', err)
if (err.message === 'Timeout') {
toast.error('Timeout occurred in publishing updated app data')
} else if (Array.isArray(err)) {
err.forEach((errResult) => {
2024-06-28 14:24:14 +05:00
toast.error(
`Publishing to ${errResult.relay} caused the following error: ${errResult.error}`
)
})
} else {
toast.error(
'An unexpected error occurred in publishing updated app data '
)
}
2024-06-28 14:24:14 +05:00
return null
})
2024-06-28 14:24:14 +05:00
if (!publishResult) return null
2024-07-05 13:38:04 +05:00
// update redux store
store.dispatch(
updateUserAppDataAction({
sigits,
blossomUrls,
processedGiftWraps: [...appData.processedGiftWraps],
keyPair: {
...appData.keyPair
}
})
)
2024-06-28 14:24:14 +05:00
return signedEvent
}
2024-07-05 13:38:04 +05: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: unixNow(),
2024-07-05 13:38:04 +05:00
tags: [
['t', 'delete'],
['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
2024-07-05 13:38:04 +05:00
['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-09 01:16:47 +05: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 13:38:04 +05:00
const uploadUserAppDataToBlossom = async (
sigits: { [key: string]: Meta },
processedGiftWraps: string[],
privateKey: string
) => {
2024-07-09 01:16:47 +05:00
// Create an object containing the sigits and processed gift wraps
2024-07-05 13:38:04 +05:00
const obj = {
sigits,
processedGiftWraps
}
2024-07-09 01:16:47 +05:00
// Convert the object to a JSON string
2024-07-05 13:38:04 +05:00
const stringified = JSON.stringify(obj)
2024-07-09 01:16:47 +05:00
// Convert the private key from hex to bytes
2024-07-05 13:38:04 +05:00
const secretKey = hexToBytes(privateKey)
2024-07-09 01:16:47 +05:00
// Encrypt the JSON string using the secret key
const encrypted = await nip04.encrypt(
secretKey,
getPublicKey(secretKey),
stringified
2024-07-05 13:38:04 +05:00
)
2024-07-09 01:16:47 +05:00
// Create a blob from the encrypted data
2024-07-05 13:38:04 +05:00
const blob = new Blob([encrypted], { type: 'application/octet-stream' })
2024-07-09 01:16:47 +05:00
// Create a file from the blob
2024-07-05 13:38:04 +05:00
const file = new File([blob], 'encrypted.txt', {
type: 'application/octet-stream'
})
const hash = await getHash(await file.arrayBuffer())
if (!hash) {
throw new Error("Can't get file hash.")
}
2024-07-05 13:38:04 +05:00
// Define event metadata for authorization
const event: EventTemplate = {
kind: 24242,
content: 'Authorize Upload',
created_at: unixNow(),
2024-07-05 13:38:04 +05:00
tags: [
['t', 'upload'],
['x', hash],
['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
2024-07-05 13:38:04 +05:00
['name', file.name],
['size', String(file.size)]
]
}
2024-07-09 01:16:47 +05:00
// Finalize the event with the private key
2024-07-05 13:38:04 +05:00
const authEvent = finalizeEvent(event, hexToBytes(privateKey))
// Upload the file to the file storage service using Axios
const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
2024-07-05 13:38:04 +05:00
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-09 01:16:47 +05: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 13:38:04 +05:00
const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
2024-07-09 01:16:47 +05:00
// Initialize errorCode to track HTTP error codes
let errorCode = 0
2024-07-09 01:16:47 +05:00
// Fetch the encrypted data from the provided URL
2024-07-05 13:38:04 +05:00
const encrypted = await axios
.get(url, {
2024-07-09 01:16:47 +05:00
responseType: 'blob' // Expect a blob response
2024-07-05 13:38:04 +05:00
})
.then(async (res) => {
2024-07-09 01:16:47 +05:00
// Convert the blob response to a File object
2024-07-05 13:38:04 +05:00
const file = new File([res.data], 'encrypted.txt')
2024-07-09 01:16:47 +05:00
// Read the text content from the file
2024-07-05 13:38:04 +05:00
const text = await file.text()
return text
})
.catch((err) => {
2024-07-09 01:16:47 +05:00
// Log and display an error message if the request fails
2024-07-05 13:38:04 +05:00
console.error(`error occurred in getting file from ${url}`, err)
toast.error(err.message || `error occurred in getting file from ${url}`)
2024-07-09 01:16:47 +05:00
// Set errorCode to the HTTP status code if available
if (err.request) {
const { status } = err.request
errorCode = status
}
2024-07-05 13:38:04 +05:00
return null
})
2024-07-09 01:16:47 +05:00
// Return a default value if the requested resource is not found (404)
if (errorCode === 404) {
return {
sigits: {},
processedGiftWraps: []
}
}
2024-07-09 01:16:47 +05:00
// Return null if the encrypted data could not be retrieved
2024-07-05 13:38:04 +05:00
if (!encrypted) return null
2024-07-09 01:16:47 +05:00
// Convert the private key from hex to bytes
2024-07-05 13:38:04 +05:00
const secret = hexToBytes(privateKey)
2024-07-09 01:16:47 +05:00
// Get the public key corresponding to the private key
2024-07-05 13:38:04 +05:00
const pubkey = getPublicKey(secret)
2024-07-09 01:16:47 +05:00
// Decrypt the encrypted data using the secret and public key
const decrypted = await nip04.decrypt(secret, pubkey, encrypted)
2024-07-05 13:38:04 +05:00
2024-07-09 01:16:47 +05:00
// Parse the decrypted JSON content
2024-07-05 13:38:04 +05:00
const parsedContent = await parseJson<{
sigits: { [key: string]: Meta }
processedGiftWraps: string[]
}>(decrypted).catch((err) => {
2024-07-09 01:16:47 +05:00
// Log and display an error message if parsing fails
2024-07-05 13:38:04 +05: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-09 01:16:47 +05:00
// Return the parsed content
2024-07-05 13:38:04 +05:00
return parsedContent
}
2024-07-09 01:16:47 +05: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 14:24:14 +05:00
export const subscribeForSigits = async (pubkey: string) => {
2024-07-09 01:16:47 +05:00
// Instantiate the MetadataController to retrieve relay list metadata
const metadataController = MetadataController.getInstance()
2024-06-28 14:24:14 +05:00
const relaySet = await metadataController
.findRelayListMetadata(pubkey)
.catch((err) => {
2024-07-09 01:16:47 +05:00
// Log an error if retrieving relay list metadata fails
2024-06-28 14:24:14 +05:00
console.log(
2024-06-28 14:25:41 +05:00
`An error occurred while finding relay list metadata for ${hexToNpub(pubkey)}`,
2024-06-28 14:24:14 +05: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-09 01:16:47 +05:00
// Define the filter for the subscription
2024-06-28 14:24:14 +05:00
const filter: Filter = {
kinds: [1059],
'#p': [pubkey]
}
2024-07-09 01:16:47 +05:00
fix: processing gift wraps and notifications (#193) This change will potentially close multiple issues related to the gift-wrapped events processing (https://git.nostrdev.com/sigit/sigit.io/issues/168, https://git.nostrdev.com/sigit/sigit.io/issues/158). Further testing will be required to confirm before closing each. The commented-out code causes the race condition during the processing of the gift wraps with sigits. During the processing we perform checks to see if sigit is outdated. In cases where sigit includes multiple signers it's possible for a signer to receive multiple sigit updates at once (especially noticeable for 3rd, 4th signer). Due to async nature of processing we can have same sigit enter processing flow with different states. Since this code also updates user's app state, which includes uploads to the blossom server it takes time to upload local user state which causes both to check against the stale data and un-updated app state. This results in both sigits being "new" and both proceed to update user state and upload app data. We have no guarantees as in which event will update last, meaning that the final state we end up with could be already stale. The issue is also complicated due to the fact that we mark the gift wraps as processed and it's impossible to update the state without creating a new gift wrap with correct state and processing it last to overwrite stale state. This is temporary solution to stop broken sigit states until proper async implementation is ready. Co-authored-by: b Reviewed-on: https://git.nostrdev.com/sigit/sigit.io/pulls/193 Reviewed-by: eugene <eugene@nostrdev.com> Co-authored-by: enes <mulahasanovic@outlook.com> Co-committed-by: enes <mulahasanovic@outlook.com>
2024-09-12 08:26:59 +00:00
// Process the received event synchronously
const events = await relayController.fetchEvents(filter, relaySet.read)
for (const e of events) {
await processReceivedEvent(e)
}
// Async processing of the events has a race condition
// relayController.subscribeForEvents(filter, relaySet.read, (event) => {
// processReceivedEvent(event)
// })
2024-06-28 14:24:14 +05:00
}
2024-07-05 13:38:04 +05:00
const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
2024-10-08 20:14:44 +02:00
const processedEvents = store.getState().userAppData?.processedGiftWraps || []
2024-07-05 13:38:04 +05:00
if (processedEvents.includes(event.id)) return
store.dispatch(updateProcessedGiftWraps([...processedEvents, event.id]))
2024-06-28 14:24:14 +05: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 13:38:04 +05:00
// decrypt the content of gift wrap event
2024-06-28 14:24:14 +05:00
const nostrController = NostrController.getInstance()
2024-07-05 13:38:04 +05:00
const decrypted = await nostrController.nip44Decrypt(
2024-06-28 14:24:14 +05:00
event.pubkey,
event.content
)
2024-07-05 13:38:04 +05: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 14:24:14 +05:00
)
2024-07-05 13:38:04 +05: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
fix: processing gift wraps and notifications (#193) This change will potentially close multiple issues related to the gift-wrapped events processing (https://git.nostrdev.com/sigit/sigit.io/issues/168, https://git.nostrdev.com/sigit/sigit.io/issues/158). Further testing will be required to confirm before closing each. The commented-out code causes the race condition during the processing of the gift wraps with sigits. During the processing we perform checks to see if sigit is outdated. In cases where sigit includes multiple signers it's possible for a signer to receive multiple sigit updates at once (especially noticeable for 3rd, 4th signer). Due to async nature of processing we can have same sigit enter processing flow with different states. Since this code also updates user's app state, which includes uploads to the blossom server it takes time to upload local user state which causes both to check against the stale data and un-updated app state. This results in both sigits being "new" and both proceed to update user state and upload app data. We have no guarantees as in which event will update last, meaning that the final state we end up with could be already stale. The issue is also complicated due to the fact that we mark the gift wraps as processed and it's impossible to update the state without creating a new gift wrap with correct state and processing it last to overwrite stale state. This is temporary solution to stop broken sigit states until proper async implementation is ready. Co-authored-by: b Reviewed-on: https://git.nostrdev.com/sigit/sigit.io/pulls/193 Reviewed-by: eugene <eugene@nostrdev.com> Co-authored-by: enes <mulahasanovic@outlook.com> Co-committed-by: enes <mulahasanovic@outlook.com>
2024-09-12 08:26:59 +00:00
await updateUsersAppData(meta)
2024-06-28 14:24:14 +05:00
}
2024-07-09 01:16:47 +05: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 13:38:04 +05:00
export const sendNotification = async (receiver: string, meta: Meta) => {
2024-07-09 01:16:47 +05:00
// Retrieve the user's public key from the state
2024-10-08 20:14:44 +02:00
const usersPubkey = store.getState().auth.usersPubkey!
2024-06-28 14:24:14 +05:00
2024-07-09 01:16:47 +05:00
// Create an unsigned event object with the provided metadata
2024-06-28 14:24:14 +05:00
const unsignedEvent: UnsignedEvent = {
2024-07-05 13:38:04 +05:00
kind: 938,
2024-06-28 14:24:14 +05:00
pubkey: usersPubkey,
2024-07-05 13:38:04 +05:00
content: JSON.stringify(meta),
tags: [],
created_at: unixNow()
2024-06-28 14:24:14 +05:00
}
2024-07-09 01:16:47 +05:00
// Wrap the unsigned event with the receiver's information
2024-07-05 13:38:04 +05:00
const wrappedEvent = createWrap(unsignedEvent, receiver)
2024-06-28 14:24:14 +05:00
2024-07-09 01:16:47 +05:00
// Instantiate the MetadataController to retrieve relay list metadata
const metadataController = MetadataController.getInstance()
2024-06-28 14:24:14 +05:00
const relaySet = await metadataController
.findRelayListMetadata(receiver)
.catch((err) => {
2024-07-09 01:16:47 +05:00
// Log an error if retrieving relay list metadata fails
2024-06-28 14:24:14 +05: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
// Publish the notification event to the recipient's read relays
await Promise.race([
relayController.publish(wrappedEvent, relaySet.read),
timeout(40 * 1000)
]).catch((err) => {
2024-07-09 01:16:47 +05:00
// Log an error if publishing the notification event fails
console.log(
`An error occurred while publishing notification event for ${hexToNpub(receiver)}`,
err
)
throw err
})
2024-06-28 14:24:14 +05:00
}
/**
* Show user's name, first available in order: display_name, name, or npub as fallback
2024-09-20 11:13:48 +02:00
* @param npub User identifier, it can be either pubkey or npub1 (we only show npub)
* @param profile User profile
*/
export const getProfileUsername = (
2024-09-20 11:13:48 +02:00
npub: `npub1${string}` | string,
profile?: ProfileMetadata
) =>
2024-09-20 11:13:48 +02:00
truncate(profile?.display_name || profile?.name || hexToNpub(npub), {
length: 16
})