import { Button, Divider, TextField } from '@mui/material' import { getPublicKey, nip19 } from 'nostr-tools' import { useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import { useNavigate, useSearchParams } from 'react-router-dom' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { AuthController, MetadataController, NostrController } from '../../controllers' import { updateKeyPair, updateLoginMethod, updateNsecbunkerPubkey, updateNsecbunkerRelays } from '../../store/actions' import { LoginMethods } from '../../store/auth/types' import { Dispatch } from '../../store/store' import { npubToHex, queryNip05, timeout } from '../../utils' import { hexToBytes } from '@noble/hashes/utils' import { NIP05_REGEX } from '../../constants' import styles from './styles.module.scss' import { TimeoutError } from '../../types/errors/TimeoutError' const EXTENSION_LOGIN_DELAY_SECONDS = 5 const EXTENSION_LOGIN_TIMEOUT_SECONDS = EXTENSION_LOGIN_DELAY_SECONDS + 55 export const Nostr = () => { const [searchParams] = useSearchParams() const dispatch: Dispatch = useDispatch() const navigate = useNavigate() const authController = new AuthController() const metadataController = new MetadataController() const nostrController = NostrController.getInstance() const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [isExtensionSlow, setIsExtensionSlow] = useState(false) const [inputValue, setInputValue] = useState('') const [authUrl, setAuthUrl] = useState() const [isNostrExtensionAvailable, setIsNostrExtensionAvailable] = useState(false) useEffect(() => { setTimeout(() => { setIsNostrExtensionAvailable(!!window.nostr) }, 500) }, []) /** * Call login function when enter is pressed */ const handleInputKeyDown = (event: React.KeyboardEvent) => { if (event.code === 'Enter' || event.code === 'NumpadEnter') { event.preventDefault() login() } } const navigateAfterLogin = (path: string) => { const callbackPath = searchParams.get('callbackPath') if (callbackPath) { // base64 decoded path const path = atob(callbackPath) navigate(path) return } navigate(path) } const loginWithExtension = async () => { let waitTimeout: number | undefined try { // Wait EXTENSION_LOGIN_DELAY_SECONDS before showing extension delay message waitTimeout = window.setTimeout(() => { setIsExtensionSlow(true) }, EXTENSION_LOGIN_DELAY_SECONDS * 1000) setIsLoading(true) setLoadingSpinnerDesc('Capturing pubkey from nostr extension') const pubkey = await nostrController.capturePublicKey() dispatch(updateLoginMethod(LoginMethods.extension)) setLoadingSpinnerDesc('Authenticating and finding metadata') const redirectPath = await Promise.race([ authController.authAndGetMetadataAndRelaysMap(pubkey), timeout(EXTENSION_LOGIN_TIMEOUT_SECONDS * 1000) ]) if (redirectPath) { navigateAfterLogin(redirectPath) } } catch (error) { if (error instanceof TimeoutError) { toast.error("Extension didn't respond in time") } else { toast.error('Error capturing public key from nostr extension: ' + error) } } finally { // Clear the wait timeout so we don't change the state unnecessarily window.clearTimeout(waitTimeout) setIsLoading(false) setLoadingSpinnerDesc('') setIsExtensionSlow(false) } } /** * Login with NSEC or HEX private key * @param privateKey in HEX format */ const loginWithNsec = async (privateKey?: Uint8Array) => { let nsec = '' if (privateKey) { nsec = nip19.nsecEncode(privateKey) } else { nsec = inputValue try { privateKey = nip19.decode(nsec).data as Uint8Array } catch (err) { toast.error(`Error decoding the nsec. ${err}`) } } if (!privateKey) { toast.error( 'Snap, we failed to convert the private key you provided. Please make sure key is valid.' ) setIsLoading(false) return } const publickey = getPublicKey(privateKey) dispatch( updateKeyPair({ private: nsec, public: publickey }) ) dispatch(updateLoginMethod(LoginMethods.privateKey)) setIsLoading(true) setLoadingSpinnerDesc('Authenticating and finding metadata') const redirectPath = await authController .authAndGetMetadataAndRelaysMap(publickey) .catch((err) => { toast.error('Error occurred in authentication: ' + err) return null }) if (redirectPath) navigateAfterLogin(redirectPath) setIsLoading(false) setLoadingSpinnerDesc('') } const loginWithNsecBunker = async () => { let relays: string[] | undefined let pubkey: string | undefined setIsLoading(true) const displayError = (message: string) => { toast.error(message) setIsLoading(false) setLoadingSpinnerDesc('') } if (inputValue.match(NIP05_REGEX)) { const nip05Profile = await queryNip05(inputValue).catch((err) => { toast.error('An error occurred while querying nip05 profile: ' + err) return null }) if (nip05Profile) { pubkey = nip05Profile.pubkey relays = nip05Profile.relays } } else if (inputValue.startsWith('npub')) { pubkey = nip19.decode(inputValue).data as string const metadataEvent = await metadataController .findMetadata(pubkey) .catch(() => { return null }) if (!metadataEvent) { return displayError('metadata not found!') } const metadataContent = metadataController.extractProfileMetadataContent(metadataEvent) if (!metadataContent?.nip05) { return displayError('nip05 not present in metadata') } const nip05Profile = await queryNip05(inputValue).catch((err) => { toast.error('An error occurred while querying nip05 profile: ' + err) return null }) if (nip05Profile) { if (nip05Profile.pubkey !== pubkey) { return displayError( 'pubkey in nip05 does not match with provided npub' ) } relays = nip05Profile.relays } } if (!relays || relays.length === 0) { return displayError('No relay found for nsecbunker') } if (!pubkey) { return displayError('pubkey not found') } setLoadingSpinnerDesc('Initializing nsecBunker') await nostrController.nsecBunkerInit(relays) setLoadingSpinnerDesc('Creating nsecbunker singer') await nostrController .createNsecBunkerSigner(pubkey) .then(async (signer) => { signer.on('authUrl', (url: string) => { setAuthUrl(url) }) dispatch(updateLoginMethod(LoginMethods.nsecBunker)) dispatch(updateNsecbunkerPubkey(pubkey)) dispatch(updateNsecbunkerRelays(relays)) setLoadingSpinnerDesc('Authenticating and finding metadata') const redirectPath = await authController .authAndGetMetadataAndRelaysMap(pubkey!) .catch((err) => { toast.error('Error occurred in authentication: ' + err) return null }) if (redirectPath) navigateAfterLogin(redirectPath) }) .catch((err) => { toast.error( 'An error occurred while creating nsecbunker signer: ' + err ) }) .finally(() => { setIsLoading(false) setLoadingSpinnerDesc('') }) } const loginWithBunkerConnectionString = async () => { // Extract the key const keyStartIndex = inputValue.indexOf('bunker://') + 'bunker://'.length const keyEndIndex = inputValue.indexOf('?relay=') const key = inputValue.substring(keyStartIndex, keyEndIndex) const pubkey = npubToHex(key) if (!pubkey) { toast.error('Invalid pubkey in bunker connection string.') setIsLoading(false) return } // Extract the relay value const relayIndex = inputValue.indexOf('relay=') const relay = inputValue.substring( relayIndex + 'relay='.length, inputValue.length ) setIsLoading(true) setLoadingSpinnerDesc('Initializing bunker NDK') await nostrController.nsecBunkerInit([relay]) setLoadingSpinnerDesc('Creating remote signer') await nostrController .createNsecBunkerSigner(pubkey) .then(async (signer) => { signer.on('authUrl', (url: string) => { setAuthUrl(url) }) dispatch(updateLoginMethod(LoginMethods.nsecBunker)) dispatch(updateNsecbunkerPubkey(pubkey)) dispatch(updateNsecbunkerRelays([relay])) setLoadingSpinnerDesc('Authenticating and finding metadata') const redirectPath = await authController .authAndGetMetadataAndRelaysMap(pubkey!) .catch((err) => { toast.error('Error occurred in authentication: ' + err) return null }) if (redirectPath) navigateAfterLogin(redirectPath) }) .catch((err) => { toast.error( 'An error occurred while creating nsecbunker signer: ' + err ) }) .finally(() => { setIsLoading(false) setLoadingSpinnerDesc('') }) } const login = () => { if (inputValue.startsWith('bunker://')) { return loginWithBunkerConnectionString() } if (inputValue.startsWith('nsec')) { return loginWithNsec() } if (inputValue.startsWith('npub')) { return loginWithNsecBunker() } if (inputValue.match(NIP05_REGEX)) { return loginWithNsecBunker() } // Check if maybe hex nsec try { const privateKey = hexToBytes(inputValue) const publickey = getPublicKey(privateKey) if (publickey) return loginWithNsec(privateKey) } catch (err) { console.warn('err', err) } toast.error( 'Invalid format, please use: private key (hex), nsec..., bunker:// or nip05 format.' ) return } if (authUrl) { return (