import NDK, { NDKEvent, NDKNip46Signer, NDKPrivateKeySigner, NostrEvent } from '@nostr-dev-kit/ndk' import { Event, EventTemplate, Relay, UnsignedEvent, finalizeEvent, nip19 } from 'nostr-tools' import { updateNsecbunkerPubkey } from '../store/actions' import { AuthState, LoginMethods } from '../store/auth/types' import store from '../store/store' import { SignedEvent } from '../types' import { getNsecBunkerDelegatedKey, verifySignedEvent } from '../utils' export class NostrController { private static instance: NostrController private bunkerNDK: NDK | undefined private remoteSigner: NDKNip46Signer | undefined private constructor() {} public nsecBunkerInit = async (relays: string[]) => { // Don't reinstantiate bunker NDK if exists with same relays if ( this.bunkerNDK && this.bunkerNDK.explicitRelayUrls?.length === relays.length && this.bunkerNDK.explicitRelayUrls?.every((relay) => relays.includes(relay)) ) return this.bunkerNDK = new NDK({ explicitRelayUrls: relays }) try { await this.bunkerNDK .connect(2000) .then(() => { console.log( `Successfully connected to the nsecBunker relays: ${relays.join( ',' )}` ) }) .catch((err) => { console.error( `Error connecting to the nsecBunker relays: ${relays.join( ',' )} ${err}` ) }) } catch (err) { console.error(err) } } /** * Creates nSecBunker signer instance for the given npub * Or if npub omitted it will return existing signer * If neither, error will be thrown * @param npub nPub / public key in hex format * @returns nsecBunker Signer instance */ public createNsecBunkerSigner = async ( npub: string | undefined ): Promise => { const nsecBunkerDelegatedKey = getNsecBunkerDelegatedKey() return new Promise((resolve, reject) => { if (!nsecBunkerDelegatedKey) { reject('nsecBunker delegated key is not found in the browser.') return } const localSigner = new NDKPrivateKeySigner(nsecBunkerDelegatedKey) if (!npub) { if (this.remoteSigner) resolve(this.remoteSigner) const npubFromStorage = (store.getState().auth as AuthState) .nsecBunkerPubkey if (npubFromStorage) { npub = npubFromStorage } else { reject( 'No signer instance present, no npub provided by user or found in the browser.' ) return } } else { store.dispatch(updateNsecbunkerPubkey(npub)) } // Pubkey of a key pair stored in nsecbunker that will be used to sign event with const appPubkeyOrToken = npub.includes('npub') ? npub : nip19.npubEncode(npub) /** * When creating and NDK instance we create new connection to the relay * To prevent too much connections and hitting rate limits, if npub against which we sign * we will reuse existing instance. Otherwise we will create new NDK and signer instance. */ if (!this.remoteSigner || this.remoteSigner?.remotePubkey !== npub) { this.remoteSigner = new NDKNip46Signer( this.bunkerNDK!, appPubkeyOrToken, localSigner ) } /** * when nsecbunker-delegated-key is regenerated we have to reinitialize the remote signer */ if (this.remoteSigner.localSigner !== localSigner) { this.remoteSigner = new NDKNip46Signer( this.bunkerNDK!, appPubkeyOrToken, localSigner ) } resolve(this.remoteSigner) }) } /** * Signs the nostr event and returns the sig and id or full raw nostr event * @param npub stored in nsecBunker to sign with * @param event to be signed * @param returnFullEvent whether to return full raw nostr event or just SIG and ID values */ public signWithNsecBunker = async ( npub: string | undefined, event: NostrEvent, returnFullEvent = true ): Promise<{ id: string; sig: string } | NostrEvent> => { return new Promise((resolve, reject) => { this.createNsecBunkerSigner(npub) .then(async (signer) => { const ndkEvent = new NDKEvent(undefined, event) const timeout = setTimeout(() => { reject('Timeout occurred while waiting for event signing') }, 60000) // 60000 ms (1 min) = 1000 * 60 await ndkEvent.sign(signer).catch((err) => { clearTimeout(timeout) reject(err) return }) clearTimeout(timeout) if (returnFullEvent) { resolve(ndkEvent.rawEvent()) } else { resolve({ id: ndkEvent.id, sig: ndkEvent.sig! }) } }) .catch((err) => { reject(err) }) }) } public static getInstance(): NostrController { if (!NostrController.instance) { NostrController.instance = new NostrController() } return NostrController.instance } /** * Function will publish provided event to the provided relay */ publishEvent = async (event: Event, relayUrl: string) => { const relay = await Relay.connect(relayUrl) await relay.publish(event) relay.close() return `event published to relay: ${relayUrl}` } /** * Signs an event with private key (if it is present in local storage) or * with browser extension (if it is present) or * with nSecBunker instance. * @param event - unsigned nostr event. * @returns - a promised that is resolved with signed nostr event. */ signEvent = async ( event: UnsignedEvent | EventTemplate ): Promise => { const loginMethod = (store.getState().auth as AuthState).loginMethod if (!loginMethod) { return Promise.reject('No login method found in the browser storage') } if (loginMethod === LoginMethods.nsecBunker) { // Check if nsecBunker is available if (!this.bunkerNDK) { return Promise.reject( `Login method is ${loginMethod} but bunkerNDK is not created` ) } if (!this.remoteSigner) { return Promise.reject( `Login method is ${loginMethod} but bunkerNDK is not created` ) } const signedEvent = await this.signWithNsecBunker( '', event as NostrEvent ).catch((err) => { throw err }) return Promise.resolve(signedEvent as SignedEvent) } else if (loginMethod === LoginMethods.privateKey) { const keys = (store.getState().auth as AuthState).keyPair if (!keys) { return Promise.reject( `Login method is ${loginMethod}, but keys are not found` ) } const { private: nsec } = keys const privateKey = nip19.decode(nsec).data as Uint8Array const signedEvent = finalizeEvent(event, privateKey) verifySignedEvent(signedEvent) return Promise.resolve(signedEvent) } else if (loginMethod === LoginMethods.extension) { if (!window.nostr) { return Promise.reject( `Login method is ${loginMethod} but window.nostr is not present. Make sure extension is working properly` ) } return (await window.nostr .signEvent(event as NostrEvent) .catch((err: any) => { console.log('Error while signing event: ', err) throw err })) as Event } else { return Promise.reject( `We could not sign the event, none of the signing methods are available` ) } } /** * Function will capture the public key from the nostr extension or if no extension present * function wil capture the public key from the local storage */ capturePublicKey = async (): Promise => { if (window.nostr) { const pubKey = await window.nostr.getPublicKey().catch((err: any) => { return Promise.reject(err.message) }) if (!pubKey) { return Promise.reject('Error getting public key, user canceled') } return Promise.resolve(pubKey) } return Promise.reject( 'window.nostr object not present. Make sure you have an nostr extension installed.' ) } /** * Generates NDK Private Signer * @returns nSecBunker delegated key */ generateDelegatedKey = (): string => { return NDKPrivateKeySigner.generate().privateKey! } }