375 lines
10 KiB
TypeScript
375 lines
10 KiB
TypeScript
import NDK, {
|
|
NDKEvent,
|
|
NDKNip46Signer,
|
|
NDKPrivateKeySigner,
|
|
NDKUser,
|
|
NostrEvent
|
|
} from '@nostr-dev-kit/ndk'
|
|
import {
|
|
Event,
|
|
EventTemplate,
|
|
SimplePool,
|
|
UnsignedEvent,
|
|
finalizeEvent,
|
|
nip04,
|
|
nip19
|
|
} from 'nostr-tools'
|
|
import { EventEmitter } from 'tseep'
|
|
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 extends EventEmitter {
|
|
private static instance: NostrController
|
|
|
|
private bunkerNDK: NDK | undefined
|
|
private remoteSigner: NDKNip46Signer | undefined
|
|
|
|
private constructor() {
|
|
super()
|
|
}
|
|
|
|
private getNostrObject = () => {
|
|
if (window.nostr) return window.nostr
|
|
|
|
throw new Error(
|
|
`window.nostr object not present. Make sure you have an nostr extension installed/working properly.`
|
|
)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/**
|
|
* Function will publish provided event to the provided relays
|
|
*/
|
|
publishEvent = async (event: Event, relays: string[]) => {
|
|
const simplePool = new SimplePool()
|
|
|
|
const promises = simplePool.publish(relays, event)
|
|
|
|
const results = await Promise.allSettled(promises)
|
|
|
|
const publishedRelays: string[] = []
|
|
|
|
console.log('results of publish event :>> ', results)
|
|
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
publishedRelays.push(relays[index])
|
|
}
|
|
})
|
|
|
|
if (publishedRelays.length === 0) {
|
|
const failedPublishes: any[] = []
|
|
|
|
results.forEach((res, index) => {
|
|
if (res.status === 'rejected') {
|
|
failedPublishes.push({
|
|
relay: relays[index],
|
|
error: res.reason.message
|
|
})
|
|
}
|
|
})
|
|
|
|
throw failedPublishes
|
|
}
|
|
|
|
return publishedRelays
|
|
}
|
|
|
|
/**
|
|
* 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`
|
|
)
|
|
}
|
|
|
|
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) {
|
|
const nostr = this.getNostrObject()
|
|
|
|
return (await 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`
|
|
)
|
|
}
|
|
}
|
|
|
|
nip04Encrypt = async (receiver: string, content: string) => {
|
|
const loginMethod = (store.getState().auth as AuthState).loginMethod
|
|
|
|
if (loginMethod === LoginMethods.extension) {
|
|
const nostr = this.getNostrObject()
|
|
|
|
if (!nostr.nip04) {
|
|
throw new Error(
|
|
`Your nostr extension does not support nip04 encryption & decryption`
|
|
)
|
|
}
|
|
|
|
const encrypted = await nostr.nip04.encrypt(receiver, content)
|
|
return encrypted
|
|
}
|
|
|
|
if (loginMethod === LoginMethods.privateKey) {
|
|
const keyPair = (store.getState().auth as AuthState).keyPair
|
|
|
|
if (!keyPair) {
|
|
throw new Error(
|
|
`Login method is ${LoginMethods.privateKey} but private & public key pair is not found.`
|
|
)
|
|
}
|
|
|
|
const encrypted = await nip04.encrypt(keyPair.private, receiver, content)
|
|
return encrypted
|
|
}
|
|
|
|
if (loginMethod === LoginMethods.nsecBunker) {
|
|
const user = new NDKUser({ pubkey: receiver })
|
|
|
|
this.remoteSigner?.on('authUrl', (authUrl) => {
|
|
this.emit('nsecbunker-auth', authUrl)
|
|
})
|
|
|
|
if (!this.remoteSigner) throw new Error('Remote signer is undefined.')
|
|
const encrypted = await this.remoteSigner.encrypt(user, content)
|
|
|
|
return encrypted
|
|
}
|
|
|
|
throw new Error('Login method is undefined')
|
|
}
|
|
|
|
/**
|
|
* 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> => {
|
|
const nostr = this.getNostrObject()
|
|
const pubKey = await 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)
|
|
}
|
|
|
|
/**
|
|
* Generates NDK Private Signer
|
|
* @returns nSecBunker delegated key
|
|
*/
|
|
generateDelegatedKey = (): string => {
|
|
return NDKPrivateKeySigner.generate().privateKey!
|
|
}
|
|
}
|