sigit.io/src/controllers/NostrController.ts

301 lines
8.4 KiB
TypeScript
Raw Normal View History

import NDK, {
NDKEvent,
NDKNip46Signer,
NDKPrivateKeySigner,
NostrEvent
} from '@nostr-dev-kit/ndk'
import {
Event,
EventTemplate,
2024-03-01 15:16:35 +05:00
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<NDKNip46Signer> => {
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
}
2024-03-01 15:16:35 +05:00
/**
* 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<SignedEvent> => {
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`
)
}
2024-03-05 14:08:18 +05:00
const { private: nsec } = keys
const privateKey = nip19.decode(nsec).data as Uint8Array
2024-03-05 14:08:18 +05:00
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<string> => {
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!
}
}