From 5d6a3580a6b3c97afc7a1f015458fb0a51101f52 Mon Sep 17 00:00:00 2001 From: Davinci Date: Thu, 9 May 2024 15:47:46 +0200 Subject: [PATCH 01/14] fix: when decrypting file, have better error messages --- src/pages/decrypt/index.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/decrypt/index.tsx b/src/pages/decrypt/index.tsx index 6a25cab..7d954dd 100644 --- a/src/pages/decrypt/index.tsx +++ b/src/pages/decrypt/index.tsx @@ -62,7 +62,15 @@ export const DecryptZip = () => { encryptionKey ).catch((err) => { console.log('err in decryption:>> ', err) - toast.error(err.message || 'An error occurred while decrypting file.') + + if (err.message.toLowerCase().includes('expected')) { + toast.error(`The Key seems to be invalid length or format`) + } else if (err.message.includes('The JWK "alg" member was inconsistent')) { + toast.error(`The Key seems to be invalid.`) + } else { + toast.error(err.message || 'An error occurred while decrypting file.') + } + setIsLoading(false) return null }) From 2c2eeba83f2fb6cdf73228733448228151f1de0f Mon Sep 17 00:00:00 2001 From: Davinci Date: Mon, 13 May 2024 07:50:32 +0200 Subject: [PATCH 02/14] fix: increased timeout for extension user prompt --- src/utils/misc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 4ec79e2..c54ce18 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -88,7 +88,7 @@ export const sendDM = async ( const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error('Timeout occurred')) - }, 15000) // Timeout duration = 15 seconds + }, 60000) // Timeout duration = 60 seconds }) // Encrypt the DM content, with timeout From 041bd0daff4ad06b5ef54798f4c43642c05a2d25 Mon Sep 17 00:00:00 2001 From: Davinci Date: Mon, 13 May 2024 08:00:44 +0200 Subject: [PATCH 03/14] feat(profile): picture upload, robohash, website, npub cash --- package-lock.json | 13 ++++ package.json | 2 + src/controllers/ApiController.ts | 80 ++++++++++++++++++++++++ src/controllers/NostrController.ts | 35 +++++++++++ src/pages/profile/index.tsx | 99 +++++++++++++++++++++++++++++- src/types/media.ts | 19 ++++++ src/types/profile.ts | 1 + 7 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 src/controllers/ApiController.ts create mode 100644 src/types/media.ts diff --git a/package-lock.json b/package-lock.json index 7bc19cd..f1556fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@reduxjs/toolkit": "2.2.1", "axios": "1.6.7", "crypto-hash": "3.0.0", + "crypto-js": "^4.2.0", "file-saver": "2.0.5", "jszip": "3.10.1", "lodash": "4.17.21", @@ -31,6 +32,7 @@ "tseep": "1.2.1" }, "devDependencies": { + "@types/crypto-js": "^4.2.2", "@types/file-saver": "2.0.7", "@types/lodash": "4.14.202", "@types/react": "^18.2.56", @@ -1938,6 +1940,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -2630,6 +2638,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", diff --git a/package.json b/package.json index fb823c4..20655c0 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@reduxjs/toolkit": "2.2.1", "axios": "1.6.7", "crypto-hash": "3.0.0", + "crypto-js": "^4.2.0", "file-saver": "2.0.5", "jszip": "3.10.1", "lodash": "4.17.21", @@ -34,6 +35,7 @@ "tseep": "1.2.1" }, "devDependencies": { + "@types/crypto-js": "^4.2.2", "@types/file-saver": "2.0.7", "@types/lodash": "4.14.202", "@types/react": "^18.2.56", diff --git a/src/controllers/ApiController.ts b/src/controllers/ApiController.ts new file mode 100644 index 0000000..83fd0c7 --- /dev/null +++ b/src/controllers/ApiController.ts @@ -0,0 +1,80 @@ +import axios, { AxiosError, AxiosInstance } from "axios" +import { PostMediaResponse } from "../types/media" +import sha256 from 'crypto-js/sha256' +import Hex from 'crypto-js/enc-hex' +import { NostrController } from "." + +export class ApiController { + private mediaUrl: string = 'https://media.nquiz.io' + private api: AxiosInstance + private baseUrl: string = '' + + private nostrController = NostrController.getInstance() + + constructor(baseUrl?: string) { + this.baseUrl = baseUrl ?? this.baseUrl + + this.api = axios.create({ + withCredentials: true, + baseURL: this.baseUrl, + headers: { + 'Content-Type': 'application/json' + } + }) + } + + uploadMediaImage = async ( + file: Buffer, + ): Promise => { + const mediaUrl = this.mediaUrl + + const formData = new FormData() + const imageBlob = new Blob([file]) + formData.append('mediafile', imageBlob) + + const endpointUrl = 'v1/media' + const tags = [ + ['u', `${mediaUrl}/${endpointUrl}`.replace('https', 'http')], //nostrcheck api, gets http protocol because it is inside a docker container in localhost + ['method', 'POST'], + ['payload', this.hashString('{}')] + ] + + const authToken = await this.nostrController.createNostrHttpAuthToken(tags) + + console.info('Auth token created for media server.', authToken) + + return axios + .post(`${mediaUrl}/api/v1/media`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: `Nostr ${authToken}` + } + }) + .then((res) => { + console.info( + `POST ${mediaUrl}/api/v1/media | res.data:`, + res.data + ) + // Create correct api url, because res.url is not including 'api/v1/' + if (!res || !res.data) { + console.warn('Image upload: no data returned after upload') + } + + const data = res.data + data.directLink = `${data.url.replace(mediaUrl, `${mediaUrl}/api/v1`)}` + + return data + }) + .catch((err: AxiosError) => { + console.info(`POST ${mediaUrl}/api/v1/media | err:`, err) + throw { + code: 500, + message: err.response?.data || err.response || err.message || err + } + }) + } + + hashString = (payload: any): string => { + return sha256(payload).toString(Hex) + } +} \ No newline at end of file diff --git a/src/controllers/NostrController.ts b/src/controllers/NostrController.ts index a2b0f1e..5016ac8 100644 --- a/src/controllers/NostrController.ts +++ b/src/controllers/NostrController.ts @@ -359,4 +359,39 @@ export class NostrController extends EventEmitter { generateDelegatedKey = (): string => { return NDKPrivateKeySigner.generate().privateKey! } + + /** + * Creates Nostr HTTP Auth token + * @param npub npub in hex format + * @param nostrTags tags to be included in the authevent (auth token) + */ +createNostrHttpAuthToken = async ( + nostrTags: string[][] = [] +): Promise => { + const createdAt = Math.round(Date.now() / 1000) + + let authEvent = new NDKEvent(undefined) + authEvent.kind = 27235 + authEvent.tags = nostrTags + authEvent.content = `sigit-${createdAt}` + authEvent.created_at = createdAt + + await this.signEvent(authEvent.rawEvent() as UnsignedEvent) + + console.info('Signed auth event') + + const base64Encoded = this.base64EncodeSignedEvent(authEvent.rawEvent()) + + return base64Encoded +} + +base64EncodeSignedEvent = (event: NostrEvent) => { + try { + const authEventSerialized = JSON.stringify(event) + const token = btoa(authEventSerialized) + return token + } catch (error) { + throw new Error('An error occurred in JSON.stringy of signedAuthEvent') + } +} } diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index e3cb803..bd64e06 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -1,5 +1,5 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy' -import { List, ListItem, ListSubheader, TextField } from '@mui/material' +import { CircularProgress, List, ListItem, ListSubheader, TextField } from '@mui/material' import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' import { useParams } from 'react-router-dom' @@ -15,6 +15,8 @@ import { Dispatch } from '../../store/store' import { setMetadataEvent } from '../../store/actions' import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoginMethods } from '../../store/auth/types' +import { ApiController } from '../../controllers/ApiController' +import { PostMediaResponse } from '../../types/media' export const ProfilePage = () => { const { npub } = useParams() @@ -27,9 +29,11 @@ export const ProfilePage = () => { const [pubkey, setPubkey] = useState() const [profileMetadata, setProfileMetadata] = useState() const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) + const [avatarLoading, setAvatarLoading] = useState(false) const metadataState = useSelector((state: State) => state.metadata) const keys = useSelector((state: State) => state.auth?.keyPair) const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth) + const [uploadingImage, setUploadingImage] = useState() const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false) @@ -55,6 +59,10 @@ export const ProfilePage = () => { metadataState as VerifiedEvent ) if (metadataContent) { + if (!metadataContent.lud16 || metadataContent.lud16.length < 1) { + metadataContent.lud16 = getLud16() + } + setProfileMetadata(metadataContent) setIsLoading(false) } @@ -74,7 +82,13 @@ export const ProfilePage = () => { if (metadataEvent) { const metadataContent = metadataController.extractProfileMetadataContent(metadataEvent) - if (metadataContent) setProfileMetadata(metadataContent) + if (metadataContent) { + if (!metadataContent.lud16 || metadataContent.lud16.length < 1) { + metadataContent.lud16 = getLud16() + } + + setProfileMetadata(metadataContent) + } } setIsLoading(false) @@ -84,6 +98,16 @@ export const ProfilePage = () => { } }, [isUsersOwnProfile, metadataState, pubkey, metadataController]) + const getLud16 = () => { + let lud16 = '' + + if (npub) { + if (npub) lud16 = `${npub}@npub.cash` + } + + return lud16 + } + const editItem = ( key: keyof ProfileMetadata, label: string, @@ -178,6 +202,59 @@ export const ProfilePage = () => { setSavingProfileMetadata(false) } + const onProfileImageChange = (event: any) => { + const file = event?.target?.files[0] + + if (file) { + setUploadingImage(true) + + const apiController = new ApiController() + + apiController.uploadMediaImage(file).then((imageResponse: PostMediaResponse) => { + if (imageResponse && imageResponse.directLink) { + setTimeout(() => { + setProfileMetadata((prev) => ({ + ...prev, + picture: imageResponse.directLink + })) + }, 200) + } else { + toast.error(`Uploading image failed. Please try again.`) + } + }) + .catch((err) => { + console.error('Error uploading image to media server.', err) + }) + .finally(() => { + setUploadingImage(false) + }) + } + } + + const progressSpinner = (show = false) => { + if (!show) return undefined + + return + } + + const generateRobotAvatar = () => { + setAvatarLoading(true) + + const robotAvatarLink = `https://robohash.org/${npub}.png?set=set3` + + setProfileMetadata((prev) => ({ + ...prev, + picture: '' + })) + + setTimeout(() => { + setProfileMetadata((prev) => ({ + ...prev, + picture: robotAvatarLink + })) + }) + } + return ( <> {isLoading && } @@ -214,10 +291,27 @@ export const ProfilePage = () => { onError={(event: any) => { event.target.src = placeholderAvatar }} + onLoad={() => { + setAvatarLoading(false) + }} className={styles.img} src={profileMetadata.picture || placeholderAvatar} alt='Profile Image' /> + + Generate Robot Avatar + + {editItem('name', 'Username')} @@ -225,6 +319,7 @@ export const ProfilePage = () => { {editItem('nip05', 'Nostr Address (nip05)')} {editItem('lud16', 'Lightning Address (lud16)')} {editItem('about', 'About', true, 4)} + {editItem('website', 'Website')} {isUsersOwnProfile && ( <> {usersPubkey && diff --git a/src/types/media.ts b/src/types/media.ts new file mode 100644 index 0000000..07e25e7 --- /dev/null +++ b/src/types/media.ts @@ -0,0 +1,19 @@ +export interface PostMediaResponse { + result: boolean + description: string + status: string + id: number + pubkey: string + url: string + directLink: string + hash: string + magnet: string + tags: any[] +} + +export interface GetMediaResponse extends PostMediaResponse {} + +export interface MediaErrorResponse { + description: string + result: boolean +} diff --git a/src/types/profile.ts b/src/types/profile.ts index 46f120f..e65d602 100644 --- a/src/types/profile.ts +++ b/src/types/profile.ts @@ -3,6 +3,7 @@ export interface ProfileMetadata { display_name?: string picture?: string about?: string + website?: string nip05?: string lud16?: string } From 413c9cc5756a7cfacb651de76d3e25d4578d9730 Mon Sep 17 00:00:00 2001 From: Davinci Date: Mon, 13 May 2024 08:02:35 +0200 Subject: [PATCH 04/14] chore: lint --- package.json | 1 + src/controllers/NostrController.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 20655c0..a4b9dd8 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "vite", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { diff --git a/src/controllers/NostrController.ts b/src/controllers/NostrController.ts index 5016ac8..27ec0e8 100644 --- a/src/controllers/NostrController.ts +++ b/src/controllers/NostrController.ts @@ -370,7 +370,7 @@ createNostrHttpAuthToken = async ( ): Promise => { const createdAt = Math.round(Date.now() / 1000) - let authEvent = new NDKEvent(undefined) + const authEvent = new NDKEvent(undefined) authEvent.kind = 27235 authEvent.tags = nostrTags authEvent.content = `sigit-${createdAt}` From 1fbc2414fe45b1825e8e35ab6c2cf379290f8572 Mon Sep 17 00:00:00 2001 From: Davinci Date: Mon, 13 May 2024 09:52:07 +0200 Subject: [PATCH 05/14] chore: removed sufficient if --- src/pages/profile/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 4ca9a4b..d444a60 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -125,9 +125,7 @@ export const ProfilePage = () => { const getLud16 = () => { let lud16 = '' - if (npub) { - if (npub) lud16 = `${npub}@npub.cash` - } + if (npub) lud16 = `${npub}@npub.cash` return lud16 } From 2221b22285def582b924ba63c851e1b4695d7443 Mon Sep 17 00:00:00 2001 From: Davinci Date: Mon, 13 May 2024 10:11:39 +0200 Subject: [PATCH 06/14] chore: media server protocol fix, removed npub cash --- src/controllers/ApiController.ts | 2 +- src/pages/profile/index.tsx | 16 ---------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/controllers/ApiController.ts b/src/controllers/ApiController.ts index 83fd0c7..e2cdace 100644 --- a/src/controllers/ApiController.ts +++ b/src/controllers/ApiController.ts @@ -34,7 +34,7 @@ export class ApiController { const endpointUrl = 'v1/media' const tags = [ - ['u', `${mediaUrl}/${endpointUrl}`.replace('https', 'http')], //nostrcheck api, gets http protocol because it is inside a docker container in localhost + ['u', `${mediaUrl}/${endpointUrl}`], ['method', 'POST'], ['payload', this.hashString('{}')] ] diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index d444a60..18927b0 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -83,10 +83,6 @@ export const ProfilePage = () => { metadataState as VerifiedEvent ) if (metadataContent) { - if (!metadataContent.lud16 || metadataContent.lud16.length < 1) { - metadataContent.lud16 = getLud16() - } - setProfileMetadata(metadataContent) setIsLoading(false) } @@ -107,10 +103,6 @@ export const ProfilePage = () => { const metadataContent = metadataController.extractProfileMetadataContent(metadataEvent) if (metadataContent) { - if (!metadataContent.lud16 || metadataContent.lud16.length < 1) { - metadataContent.lud16 = getLud16() - } - setProfileMetadata(metadataContent) } } @@ -122,14 +114,6 @@ export const ProfilePage = () => { } }, [isUsersOwnProfile, metadataState, pubkey, metadataController]) - const getLud16 = () => { - let lud16 = '' - - if (npub) lud16 = `${npub}@npub.cash` - - return lud16 - } - const editItem = ( key: keyof ProfileMetadata, label: string, From 1f9954befd01c4e9f90330f3db89aa75f0039bbe Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Tue, 14 May 2024 14:27:05 +0500 Subject: [PATCH 07/14] feat: update signing flow --- src/components/AppBar/AppBar.tsx | 140 +---- src/pages/home/index.tsx | 495 +---------------- src/pages/home/style.module.scss | 31 +- src/pages/sign/index.tsx | 596 +++++++++++++++------ src/pages/sign/style.module.scss | 34 +- src/pages/verify/index.tsx | 817 +++++++++++++++++++++++++++++ src/pages/verify/style.module.scss | 18 + src/routes/index.tsx | 16 +- src/types/core.ts | 18 + src/types/index.ts | 1 + src/utils/misc.ts | 12 +- src/utils/string.ts | 4 +- 12 files changed, 1336 insertions(+), 846 deletions(-) create mode 100644 src/pages/verify/index.tsx create mode 100644 src/pages/verify/style.module.scss create mode 100644 src/types/core.ts diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index c916977..3f1c858 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -1,13 +1,9 @@ -import { Menu as MenuIcon } from '@mui/icons-material' import { AppBar as AppBarMui, Box, Button, - IconButton, Menu, MenuItem, - Tab, - Tabs, Toolbar, Typography } from '@mui/material' @@ -19,14 +15,10 @@ import { State } from '../../store/rootReducer' import { Dispatch } from '../../store/store' import Username from '../username' -import { Link, useLocation, useNavigate } from 'react-router-dom' +import { Link, useNavigate } from 'react-router-dom' import nostrichAvatar from '../../assets/images/avatar.png' import { NostrController } from '../../controllers' -import { - appPrivateRoutes, - appPublicRoutes, - getProfileRoute -} from '../../routes' +import { appPublicRoutes, getProfileRoute } from '../../routes' import { clearAuthToken, saveNsecBunkerDelegatedKey, @@ -34,20 +26,14 @@ import { } from '../../utils' import styles from './style.module.scss' -const validTabs = [appPrivateRoutes.homePage, appPrivateRoutes.decryptZip] - export const AppBar = () => { const navigate = useNavigate() - const { pathname } = useLocation() - const [tabValue, setTabValue] = useState( - validTabs.includes(pathname) ? pathname : '/' - ) + const dispatch: Dispatch = useDispatch() const [username, setUsername] = useState('') const [userAvatar, setUserAvatar] = useState(nostrichAvatar) const [anchorElUser, setAnchorElUser] = useState(null) - const [anchorElNav, setAnchorElNav] = useState(null) const authState = useSelector((state: State) => state.auth) const metadataState = useSelector((state: State) => state.metadata) @@ -66,18 +52,10 @@ export const AppBar = () => { setAnchorElUser(event.currentTarget) } - const handleOpenNavMenu = (event: React.MouseEvent) => { - setAnchorElNav(event.currentTarget) - } - const handleCloseUserMenu = () => { setAnchorElUser(null) } - const handleCloseNavMenu = () => { - setAnchorElNav(null) - } - const handleProfile = () => { const hexKey = authState?.usersPubkey if (hexKey) navigate(getProfileRoute(hexKey)) @@ -111,116 +89,8 @@ export const AppBar = () => { return ( - - - Logo navigate('/')} /> - - - {isAuthenticated && ( - setTabValue(value)} - > - - - - - )} - - - - {!isAuthenticated && ( - - Logo navigate('/')} - /> - - )} - - {isAuthenticated && ( - <> - - - - - - - - - - - - - - - - - - - )} + + Logo navigate('/')} /> diff --git a/src/pages/home/index.tsx b/src/pages/home/index.tsx index 69ab3f9..438d9c4 100644 --- a/src/pages/home/index.tsx +++ b/src/pages/home/index.tsx @@ -1,489 +1,20 @@ -import { Clear } from '@mui/icons-material' -import { - Box, - Button, - FormControl, - IconButton, - InputLabel, - List, - ListItem, - ListSubheader, - MenuItem, - Select, - TextField, - Typography -} from '@mui/material' -import JSZip from 'jszip' -import { MuiFileInput } from 'mui-file-input' -import { useEffect, useState } from 'react' -import { useSelector } from 'react-redux' -import { Link } from 'react-router-dom' -import { toast } from 'react-toastify' -import placeholderAvatar from '../../assets/images/nostr-logo.jpg' -import { LoadingSpinner } from '../../components/LoadingSpinner' -import { MetadataController, NostrController } from '../../controllers' -import { getProfileRoute } from '../../routes' -import { State } from '../../store/rootReducer' -import { ProfileMetadata } from '../../types' -import { - encryptArrayBuffer, - generateEncryptionKey, - getHash, - pubToHex, - queryNip05, - sendDM, - shorten, - signEventForMetaFile, - uploadToFileStorage -} from '../../utils' +import { Box, Button } from '@mui/material' import styles from './style.module.scss' - -enum SelectionType { - signer = 'Signer', - viewer = 'Viewer' -} +import { useNavigate } from 'react-router-dom' +import { appPrivateRoutes } from '../../routes' export const HomePage = () => { - const [inputValue, setInputValue] = useState('') - const [type, setType] = useState(SelectionType.signer) - const [error, setError] = useState() - - const [signers, setSigners] = useState([]) - const [viewers, setViewers] = useState([]) - - const [selectedFiles, setSelectedFiles] = useState([]) - - const [isLoading, setIsLoading] = useState(false) - const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') - const [authUrl, setAuthUrl] = useState() - - const usersPubkey = useSelector((state: State) => state.auth.usersPubkey) - - const nostrController = NostrController.getInstance() - - const handleAddClick = async () => { - setError(undefined) - - const addPubkey = (pubkey: string) => { - const addElement = (prev: string[]) => { - // if key is already in the list just return that - if (prev.includes(pubkey)) return prev - - return [...prev, pubkey] - } - if (type === SelectionType.signer) { - setSigners(addElement) - } else { - setViewers(addElement) - } - } - - if (inputValue.startsWith('npub')) { - const pubkey = await pubToHex(inputValue) - if (pubkey) { - addPubkey(pubkey) - setInputValue('') - } else { - setError('Provided npub is not valid. Please enter correct npub.') - } - return - } - - if (inputValue.includes('@')) { - setIsLoading(true) - setLoadingSpinnerDesc('Querying for nip05') - const nip05Profile = await queryNip05(inputValue) - .catch((err) => { - console.error(`error occurred in querying nip05: ${inputValue}`, err) - return null - }) - .finally(() => { - setIsLoading(false) - setLoadingSpinnerDesc('') - }) - - if (nip05Profile) { - const pubkey = nip05Profile.pubkey - addPubkey(pubkey) - setInputValue('') - } else { - setError('Provided nip05 is not valid. Please enter correct nip05.') - } - return - } - - setError('Invalid input! Make sure to provide correct npub or nip05.') - } - - const handleRemove = (pubkey: string, selectionType: SelectionType) => { - if (selectionType === SelectionType.signer) { - setSigners((prev) => prev.filter((signer) => signer !== pubkey)) - } else { - setViewers((prev) => prev.filter((viewer) => viewer !== pubkey)) - } - } - - const handleSelectFiles = (files: File[]) => { - setSelectedFiles((prev) => { - const prevFileNames = prev.map((file) => file.name) - - const newFiles = files.filter( - (file) => !prevFileNames.includes(file.name) - ) - - return [...prev, ...newFiles] - }) - } - - const handleRemoveFile = (fileToRemove: File) => { - setSelectedFiles((prevFiles) => - prevFiles.filter((file) => file.name !== fileToRemove.name) - ) - } - - const handleSubmit = async () => { - if (signers.length === 0) { - toast.error('No signer is provided. At least provide one signer.') - return - } - - if (viewers.length === 0) { - toast.error('No viewer is provided. At least provide one viewer.') - return - } - - if (selectedFiles.length === 0) { - toast.error('No file is provided. At least provide one file.') - return - } - - setIsLoading(true) - setLoadingSpinnerDesc('Generating hashes for files') - - const fileHashes: { [key: string]: string } = {} - - for (const file of selectedFiles) { - const arraybuffer = await file.arrayBuffer().catch((err) => { - console.log( - `err while getting arrayBuffer of file ${file.name} :>> `, - err - ) - toast.error( - err.message || `err while getting arrayBuffer of file ${file.name}` - ) - return null - }) - - if (!arraybuffer) return - - const hash = await getHash(arraybuffer) - - if (!hash) { - setIsLoading(false) - return - } - - fileHashes[file.name] = hash - } - - const zip = new JSZip() - - selectedFiles.forEach((file) => { - zip.file(`files/${file.name}`, file) - }) - - setLoadingSpinnerDesc('Signing nostr event') - const signedEvent = await signEventForMetaFile( - signers[0], - fileHashes, - nostrController, - setIsLoading - ) - - if (!signedEvent) return - - const meta = { - signers, - viewers, - fileHashes, - submittedBy: usersPubkey, - signedEvents: { - [signedEvent.pubkey]: JSON.stringify(signedEvent, null, 2) - } - } - - try { - const stringifiedMeta = JSON.stringify(meta, null, 2) - zip.file('meta.json', stringifiedMeta) - - const metaHash = await getHash(stringifiedMeta) - if (!metaHash) return - - const metaHashJson = { - [usersPubkey!]: metaHash - } - - zip.file('hashes.json', JSON.stringify(metaHashJson, null, 2)) - } catch (err) { - toast.error('An error occurred in converting meta json to string') - } - - setLoadingSpinnerDesc('Generating zip file') - - const arraybuffer = await zip - .generateAsync({ - type: 'arraybuffer', - compression: 'DEFLATE', - compressionOptions: { - level: 6 - } - }) - .catch((err) => { - console.log('err in zip:>> ', err) - setIsLoading(false) - toast.error(err.message || 'Error occurred in generating zip file') - return null - }) - - if (!arraybuffer) return - - const encryptionKey = await generateEncryptionKey() - - setLoadingSpinnerDesc('Encrypting zip file') - const encryptedArrayBuffer = await encryptArrayBuffer( - arraybuffer, - encryptionKey - ) - - const blob = new Blob([encryptedArrayBuffer]) - - setLoadingSpinnerDesc('Uploading zip file to file storage.') - const fileUrl = await uploadToFileStorage(blob, nostrController) - .then((url) => { - toast.success('zip file uploaded to file storage') - return url - }) - .catch((err) => { - console.log('err in upload:>> ', err) - setIsLoading(false) - toast.error(err.message || 'Error occurred in uploading zip file') - return null - }) - - if (!fileUrl) return - - setLoadingSpinnerDesc('Sending DM to first signer') - await sendDM( - fileUrl, - encryptionKey, - signers[0], - nostrController, - true, - setAuthUrl - ) - - setIsLoading(false) - } - - if (authUrl) { - return ( -