sigit.io/src/controllers/MetadataController.ts

297 lines
7.9 KiB
TypeScript

import {
Filter,
SimplePool,
VerifiedEvent,
kinds,
validateEvent,
verifyEvent,
Event,
EventTemplate,
nip19
} from 'nostr-tools'
import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types'
import { NostrController } from '.'
import { toast } from 'react-toastify'
import { queryNip05 } from '../utils'
import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
export class MetadataController {
private nostrController: NostrController
private specialMetadataRelay = 'wss://purplepag.es'
constructor() {
this.nostrController = NostrController.getInstance()
}
public findMetadata = async (hexKey: string) => {
const eventFilter: Filter = {
kinds: [kinds.Metadata],
authors: [hexKey]
}
const pool = new SimplePool()
const metadataEvent = await pool
.get([this.specialMetadataRelay], eventFilter)
.catch((err) => {
console.error(err)
return null
})
if (
metadataEvent &&
validateEvent(metadataEvent) &&
verifyEvent(metadataEvent)
) {
return metadataEvent
}
const mostPopularRelays = import.meta.env.VITE_MOST_POPULAR_RELAYS
const hardcodedPopularRelays = (mostPopularRelays || '').split(' ')
const relays = [...hardcodedPopularRelays]
const events = await pool.querySync(relays, eventFilter).catch((err) => {
console.error(err)
return null
})
if (events && events.length) {
events.sort((a, b) => b.created_at - a.created_at)
for (const event of events) {
if (validateEvent(event) && verifyEvent(event)) {
return event
}
}
}
throw new Error('Mo metadata found.')
}
public findRelayListMetadata = async (hexKey: string) => {
const eventFilter: Filter = {
kinds: [kinds.RelayList],
authors: [hexKey]
}
const pool = new SimplePool()
let relayEvent = await pool
.get([this.specialMetadataRelay], eventFilter)
.catch((err) => {
console.error(err)
return null
})
if (!relayEvent) {
const mostPopularRelays = import.meta.env.VITE_MOST_POPULAR_RELAYS
const hardcodedPopularRelays = (mostPopularRelays || '').split(' ')
const relays = [...hardcodedPopularRelays]
relayEvent = await pool.get(relays, eventFilter).catch((err) => {
console.error(err)
return null
})
}
if (relayEvent) {
const relaySet: RelaySet = {
read: [],
write: []
}
// a list of r tags with relay URIs and a read or write marker.
const relayTags = relayEvent.tags.filter((tag) => tag[0] === 'r')
// Relays marked as read / write are called READ / WRITE relays, respectively
relayTags.forEach((tag) => {
if (tag.length >= 3) {
const marker = tag[2]
if (marker === 'read') {
relaySet.read.push(tag[1])
} else if (marker === 'write') {
relaySet.write.push(tag[1])
}
}
// If the marker is omitted, the relay is used for both purposes
if (tag.length === 2) {
relaySet.read.push(tag[1])
relaySet.write.push(tag[1])
}
})
return relaySet
}
throw new Error('No relay list metadata found.')
}
public extractProfileMetadataContent = (event: VerifiedEvent) => {
try {
return JSON.parse(event.content) as ProfileMetadata
} catch (error) {
console.log('error in parsing metadata event content :>> ', error)
return null
}
}
/**
* Function will not sign provided event if the SIG exists
*/
public publishMetadataEvent = async (event: Event) => {
let signedMetadataEvent = event
if (event.sig.length < 1) {
const timestamp = Math.floor(Date.now() / 1000)
// Metadata event to publish to the wss://purplepag.es relay
const newMetadataEvent: Event = {
...event,
created_at: timestamp
}
signedMetadataEvent = await this.nostrController.signEvent(
newMetadataEvent
)
}
await this.nostrController
.publishEvent(signedMetadataEvent, [this.specialMetadataRelay])
.then((relays) => {
toast.success(`Metadata event published on: ${relays.join('\n')}`)
})
.catch((err) => {
toast.error(err.message)
})
}
public getNostrJoiningBlockNumber = async (
hexKey: string
): Promise<NostrJoiningBlock | null> => {
const relaySet = await this.findRelayListMetadata(hexKey)
const userRelays: string[] = []
// find user's relays
if (relaySet.write.length > 0) {
userRelays.push(...relaySet.write)
} else {
const metadata = await this.findMetadata(hexKey)
const metadataContent = this.extractProfileMetadataContent(metadata)
if (metadataContent?.nip05) {
const nip05Profile = await queryNip05(metadataContent.nip05)
if (nip05Profile && nip05Profile.pubkey === hexKey) {
userRelays.push(...nip05Profile.relays)
}
}
}
if (userRelays.length === 0) return null
// filter for finding user's first kind 1 event
const eventFilter: Filter = {
kinds: [kinds.ShortTextNote],
authors: [hexKey]
}
const pool = new SimplePool()
// find user's kind 1 events published on user's relays
const events = await pool.querySync(userRelays, eventFilter)
if (events && events.length) {
// sort events by created_at time in ascending order
events.sort((a, b) => a.created_at - b.created_at)
// get first ever event published on user's relays
const event = events[0]
const { created_at } = event
// initialize job request
const jobEventTemplate: EventTemplate = {
content: '',
created_at: Math.round(Date.now() / 1000),
kind: 68001,
tags: [
['i', `${created_at * 1000}`],
['j', 'blockChain-block-number']
]
}
// sign job request event
const jobSignedEvent = await this.nostrController.signEvent(
jobEventTemplate
)
const relays = [
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://relayable.org'
]
// publish job request
await this.nostrController.publishEvent(jobSignedEvent, relays)
console.log('jobSignedEvent :>> ', jobSignedEvent)
const subscribeWithTimeout = (
subscription: NDKSubscription,
timeoutMs: number
): Promise<string> => {
return new Promise((resolve, reject) => {
const eventHandler = (event: NDKEvent) => {
subscription.stop()
resolve(event.content)
}
subscription.on('event', eventHandler)
// Set up a timeout to stop the subscription after a specified time
const timeout = setTimeout(() => {
subscription.stop() // Stop the subscription
reject(new Error('Subscription timed out')) // Reject the promise with a timeout error
}, timeoutMs)
// Handle subscription close event
subscription.on('close', () => clearTimeout(timeout))
})
}
const dvmNDK = new NDK({
explicitRelayUrls: relays
})
await dvmNDK.connect(2000)
// filter for getting DVM job's result
const sub = dvmNDK.subscribe({
kinds: [68002 as number],
'#e': [jobSignedEvent.id],
'#p': [jobSignedEvent.pubkey]
})
// asynchronously get block number from dvm job with 20 seconds timeout
const dvmJobResult = await subscribeWithTimeout(sub, 20000)
const encodedEventPointer = nip19.neventEncode({
id: event.id,
relays: userRelays,
author: event.pubkey,
kind: event.kind
})
return {
block: parseInt(dvmJobResult),
encodedEventPointer
}
}
return null
}
public validate = (event: Event) => validateEvent(event) && verifyEvent(event)
}