store-sigits and update working flow #116
@ -67,6 +67,7 @@ export const UserComponent = ({ pubkey, name, image }: UserProps) => {
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
e.stopPropagation()
|
||||
// navigate to user's profile
|
||||
navigate(getProfileRoute(pubkey))
|
||||
}
|
||||
|
||||
|
@ -304,95 +304,135 @@ export class NostrController extends EventEmitter {
|
||||
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) => {
|
||||
// Retrieve the current login method from the application's redux state.
|
||||
const loginMethod = (store.getState().auth as AuthState).loginMethod
|
||||
|
||||
// Handle encryption when the login method is via an extension.
|
||||
if (loginMethod === LoginMethods.extension) {
|
||||
const nostr = this.getNostrObject()
|
||||
|
||||
// Check if the nostr object supports NIP-44 encryption.
|
||||
if (!nostr.nip44) {
|
||||
throw new Error(
|
||||
`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)
|
||||
return encrypted as string
|
||||
}
|
||||
|
||||
// Handle encryption when the login method is via a private key.
|
||||
if (loginMethod === LoginMethods.privateKey) {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
// Check if the private and public key pair is available.
|
||||
if (!keys) {
|
||||
throw new Error(
|
||||
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
|
||||
)
|
||||
}
|
||||
|
||||
// Decode the private key.
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
// Generate the conversation key using NIP-44 utilities.
|
||||
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
|
||||
privateKey,
|
||||
receiver
|
||||
)
|
||||
|
||||
// Encrypt the content using the generated conversation key.
|
||||
const encrypted = nip44.v2.encrypt(content, nip44ConversationKey)
|
||||
|
||||
return encrypted
|
||||
}
|
||||
|
||||
// Throw an error if the login method is nsecBunker (not supported).
|
||||
if (loginMethod === LoginMethods.nsecBunker) {
|
||||
throw new Error(
|
||||
`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')
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
// Retrieve the current login method from the application's redux state.
|
||||
const loginMethod = (store.getState().auth as AuthState).loginMethod
|
||||
|
||||
// Handle decryption when the login method is via an extension.
|
||||
if (loginMethod === LoginMethods.extension) {
|
||||
const nostr = this.getNostrObject()
|
||||
|
||||
// Check if the nostr object supports NIP-44 decryption.
|
||||
if (!nostr.nip44) {
|
||||
throw new Error(
|
||||
`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)
|
||||
return decrypted as string
|
||||
}
|
||||
|
||||
// Handle decryption when the login method is via a private key.
|
||||
if (loginMethod === LoginMethods.privateKey) {
|
||||
const keys = (store.getState().auth as AuthState).keyPair
|
||||
|
||||
// Check if the private and public key pair is available.
|
||||
if (!keys) {
|
||||
throw new Error(
|
||||
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
|
||||
)
|
||||
}
|
||||
|
||||
// Decode the private key.
|
||||
const { private: nsec } = keys
|
||||
const privateKey = nip19.decode(nsec).data as Uint8Array
|
||||
|
||||
// Generate the conversation key using NIP-44 utilities.
|
||||
const nip44ConversationKey = nip44.v2.utils.getConversationKey(
|
||||
privateKey,
|
||||
sender
|
||||
)
|
||||
|
||||
// Decrypt the content using the generated conversation key.
|
||||
const decrypted = nip44.v2.decrypt(content, nip44ConversationKey)
|
||||
|
||||
return decrypted
|
||||
}
|
||||
|
||||
// Throw an error if the login method is nsecBunker (not supported).
|
||||
if (loginMethod === LoginMethods.nsecBunker) {
|
||||
throw new Error(
|
||||
`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')
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ import { toast } from 'react-toastify'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { UserComponent } from '../../components/username'
|
||||
import { MetadataController, NostrController } from '../../controllers'
|
||||
import { appPrivateRoutes } from '../../routes'
|
||||
import { State } from '../../store/rootReducer'
|
||||
import {
|
||||
CreateSignatureEventContent,
|
||||
@ -42,6 +43,7 @@ import {
|
||||
} from '../../types'
|
||||
import {
|
||||
encryptArrayBuffer,
|
||||
formatTimestamp,
|
||||
generateEncryptionKey,
|
||||
generateKeys,
|
||||
generateKeysFile,
|
||||
@ -58,24 +60,6 @@ import {
|
||||
uploadToFileStorage
|
||||
} from '../../utils'
|
||||
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 = () => {
|
||||
const navigate = useNavigate()
|
||||
@ -87,7 +71,7 @@ export const CreatePage = () => {
|
||||
|
||||
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 [userInput, setUserInput] = useState('')
|
||||
|
@ -42,6 +42,12 @@ enum SignedStatus {
|
||||
export const SignPage = () => {
|
||||
const navigate = useNavigate()
|
||||
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 {
|
||||
meta: metaInNavState,
|
||||
arrayBuffer: decryptedArrayBuffer,
|
||||
|
@ -44,6 +44,10 @@ export const VerifyPage = () => {
|
||||
)
|
||||
|
||||
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 [isLoading, setIsLoading] = useState(false)
|
||||
|
@ -181,10 +181,18 @@ export const generateKeys = async (
|
||||
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) => {
|
||||
// Parse the create signature event from the metadata
|
||||
const createSignatureEvent = await parseJson<Event>(
|
||||
meta.createSignature
|
||||
).catch((err) => {
|
||||
// Log and display an error message if parsing fails
|
||||
console.log('err in parsing the createSignature event:>> ', err)
|
||||
toast.error(
|
||||
err.message || 'error occurred in parsing the create signature event'
|
||||
@ -192,17 +200,21 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
|
||||
return null
|
||||
})
|
||||
|
||||
// Return null if the create signature event could not be parsed
|
||||
if (!createSignatureEvent) return null
|
||||
|
||||
// Verify the validity of the create signature event
|
||||
const isValidCreateSignature = verifyEvent(createSignatureEvent)
|
||||
if (!isValidCreateSignature) {
|
||||
toast.error('Create signature is invalid')
|
||||
return null
|
||||
}
|
||||
|
||||
// Parse the content of the create signature event
|
||||
const createSignatureContent = await parseJson<CreateSignatureEventContent>(
|
||||
createSignatureEvent.content
|
||||
).catch((err) => {
|
||||
// Log and display an error message if parsing fails
|
||||
console.log(`err in parsing the createSignature event's content :>> `, err)
|
||||
toast.error(
|
||||
`error occurred in parsing the create signature event's content`
|
||||
@ -210,29 +222,38 @@ export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
|
||||
return null
|
||||
})
|
||||
|
||||
// Return null if the create signature content could not be parsed
|
||||
if (!createSignatureContent) return null
|
||||
|
||||
// Extract the ZIP URL from the create signature content
|
||||
const zipUrl = createSignatureContent.zipUrl
|
||||
|
||||
// Retrieve the user's public key from the state
|
||||
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
|
||||
const usersNpub = hexToNpub(usersPubkey)
|
||||
|
||||
// Return null if the metadata does not contain keys
|
||||
if (!meta.keys) return null
|
||||
|
||||
const { sender, keys } = meta.keys
|
||||
|
||||
// Check if the user's public key is in the keys object
|
||||
if (usersNpub in keys) {
|
||||
// Instantiate the NostrController to decrypt the encryption key
|
||||
const nostrController = NostrController.getInstance()
|
||||
const decrypted = await nostrController
|
||||
.nip04Decrypt(sender, keys[usersNpub])
|
||||
.catch((err) => {
|
||||
// Log and display an error message if decryption fails
|
||||
console.log('An error occurred in decrypting encryption key', err)
|
||||
toast.error('An error occurred in decrypting encryption key')
|
||||
return null
|
||||
})
|
||||
|
||||
// Return null if the encryption key could not be decrypted
|
||||
if (!decrypted) return null
|
||||
|
||||
// Return the parsed and decrypted data
|
||||
return {
|
||||
createSignatureEvent,
|
||||
createSignatureContent,
|
||||
|
@ -493,8 +493,10 @@ export const updateUsersAppData = async (meta: Meta) => {
|
||||
|
||||
if (!newBlossomUrl) return null
|
||||
|
||||
// insert new blossom url at the start of the array
|
||||
blossomUrls.unshift(newBlossomUrl)
|
||||
|
||||
// only keep last 10 blossom urls, delete older ones
|
||||
if (blossomUrls.length > 10) {
|
||||
const filesToDelete = blossomUrls.splice(10)
|
||||
filesToDelete.forEach((url) => {
|
||||
@ -509,6 +511,7 @@ export const updateUsersAppData = async (meta: Meta) => {
|
||||
|
||||
const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
|
||||
|
||||
// encrypt content for storing in kind 30078 event
|
||||
const nostrController = NostrController.getInstance()
|
||||
const encryptedContent = await nostrController
|
||||
.nip04Encrypt(
|
||||
@ -624,24 +627,37 @@ const deleteBlossomFile = async (url: string, privateKey: string) => {
|
||||
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 (
|
||||
sigits: { [key: string]: Meta },
|
||||
processedGiftWraps: string[],
|
||||
privateKey: string
|
||||
) => {
|
||||
// Create an object containing the sigits and processed gift wraps
|
||||
const obj = {
|
||||
sigits,
|
||||
processedGiftWraps
|
||||
}
|
||||
// Convert the object to a JSON string
|
||||
const stringified = JSON.stringify(obj)
|
||||
|
||||
// Convert the private key from hex to bytes
|
||||
const secretKey = hexToBytes(privateKey)
|
||||
// Encrypt the JSON string using the secret key
|
||||
const encrypted = nip44.v2.encrypt(
|
||||
stringified,
|
||||
nip44ConversationKey(secretKey, getPublicKey(secretKey))
|
||||
)
|
||||
|
||||
// Create a blob from the encrypted data
|
||||
const blob = new Blob([encrypted], { type: 'application/octet-stream' })
|
||||
// Create a file from the blob
|
||||
const file = new File([blob], 'encrypted.txt', {
|
||||
type: 'application/octet-stream'
|
||||
})
|
||||
@ -659,8 +675,10 @@ const uploadUserAppDataToBlossom = async (
|
||||
]
|
||||
}
|
||||
|
||||
// Finalize the event with the private key
|
||||
const authEvent = finalizeEvent(event, hexToBytes(privateKey))
|
||||
|
||||
// URL of the file storage service
|
||||
const FILE_STORAGE_URL = 'https://blossom.sigit.io'
|
||||
|
||||
// Upload the file to the file storage service using Axios
|
||||
@ -674,22 +692,34 @@ const uploadUserAppDataToBlossom = async (
|
||||
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) => {
|
||||
// Initialize errorCode to track HTTP error codes
|
||||
let errorCode = 0
|
||||
|
||||
// Fetch the encrypted data from the provided URL
|
||||
const encrypted = await axios
|
||||
.get(url, {
|
||||
responseType: 'blob'
|
||||
responseType: 'blob' // Expect a blob response
|
||||
})
|
||||
.then(async (res) => {
|
||||
// Convert the blob response to a File object
|
||||
const file = new File([res.data], 'encrypted.txt')
|
||||
// Read the text content from the file
|
||||
const text = await file.text()
|
||||
return text
|
||||
})
|
||||
.catch((err) => {
|
||||
// Log and display an error message if the request fails
|
||||
console.error(`error occurred in getting file from ${url}`, err)
|
||||
toast.error(err.message || `error occurred in getting file from ${url}`)
|
||||
|
||||
// Set errorCode to the HTTP status code if available
|
||||
if (err.request) {
|
||||
const { status } = err.request
|
||||
errorCode = status
|
||||
@ -698,6 +728,7 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
|
||||
return null
|
||||
})
|
||||
|
||||
// Return a default value if the requested resource is not found (404)
|
||||
if (errorCode === 404) {
|
||||
return {
|
||||
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
|
||||
|
||||
// Convert the private key from hex to bytes
|
||||
const secret = hexToBytes(privateKey)
|
||||
// Get the public key corresponding to the private key
|
||||
const pubkey = getPublicKey(secret)
|
||||
|
||||
// Decrypt the encrypted data using the secret and public key
|
||||
const decrypted = nip44.v2.decrypt(
|
||||
encrypted,
|
||||
nip44ConversationKey(secret, pubkey)
|
||||
)
|
||||
|
||||
// Parse the decrypted JSON content
|
||||
const parsedContent = await parseJson<{
|
||||
sigits: { [key: string]: Meta }
|
||||
processedGiftWraps: string[]
|
||||
}>(decrypted).catch((err) => {
|
||||
// Log and display an error message if parsing fails
|
||||
console.log(
|
||||
'An error occurred in parsing the user app data content from blossom server',
|
||||
err
|
||||
@ -729,15 +766,22 @@ const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
|
||||
return null
|
||||
})
|
||||
|
||||
// Return the parsed content
|
||||
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) => {
|
||||
// Get relay list metadata
|
||||
// Instantiate the MetadataController to retrieve relay list metadata
|
||||
const metadataController = new MetadataController()
|
||||
const relaySet = await metadataController
|
||||
.findRelayListMetadata(pubkey)
|
||||
.catch((err) => {
|
||||
// Log an error if retrieving relay list metadata fails
|
||||
console.log(
|
||||
`An error occurred while finding relay list metadata for ${hexToNpub(pubkey)}`,
|
||||
err
|
||||
@ -751,15 +795,20 @@ export const subscribeForSigits = async (pubkey: string) => {
|
||||
// Ensure relay list is not empty
|
||||
if (relaySet.read.length === 0) return
|
||||
|
||||
// Define the filter for the subscription
|
||||
const filter: Filter = {
|
||||
kinds: [1059],
|
||||
'#p': [pubkey]
|
||||
}
|
||||
|
||||
// Instantiate a new SimplePool for the subscription
|
||||
const pool = new SimplePool()
|
||||
|
||||
// Subscribe to the specified relays with the defined filter
|
||||
return pool.subscribeMany(relaySet.read, [filter], {
|
||||
// Define a callback function to handle received events
|
||||
onevent: (event) => {
|
||||
processReceivedEvent(event)
|
||||
processReceivedEvent(event) // Process the received event
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -810,9 +859,16 @@ const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
|
||||
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) => {
|
||||
// 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,
|
||||
@ -821,13 +877,15 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
|
||||
created_at: now()
|
||||
}
|
||||
|
||||
// Wrap the unsigned event with the receiver's information
|
||||
const wrappedEvent = createWrap(unsignedEvent, receiver)
|
||||
|
||||
// Get relay list metadata
|
||||
// Instantiate the MetadataController to retrieve relay list metadata
|
||||
const metadataController = new MetadataController()
|
||||
const relaySet = await metadataController
|
||||
.findRelayListMetadata(receiver)
|
||||
.catch((err) => {
|
||||
// Log an error if retrieving relay list metadata fails
|
||||
console.log(
|
||||
`An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
|
||||
err
|
||||
@ -845,10 +903,12 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
|
||||
// Publish the notification event to the recipient's read relays
|
||||
const nostrController = NostrController.getInstance()
|
||||
|
||||
// Attempt to publish the event to the relays, with a timeout of 2 minutes
|
||||
await Promise.race([
|
||||
nostrController.publishEvent(wrappedEvent, relaySet.read),
|
||||
timeout(1000 * 60 * 2)
|
||||
]).catch((err) => {
|
||||
// Log an error if publishing the notification event fails
|
||||
console.log(
|
||||
`An error occurred while publishing notification event for ${hexToNpub(receiver)}`,
|
||||
err
|
||||
|
Loading…
Reference in New Issue
Block a user