import { Filter, SimplePool, VerifiedEvent, kinds, validateEvent, verifyEvent, Event, EventTemplate } from 'nostr-tools' import { 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) => { const relaySet = await this.findRelayListMetadata(hexKey) const relays: string[] = [] // find user's relays if (relaySet.write.length > 0) { relays.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) { relays.push(...nip05Profile.relays) } } } if (relays.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(relays, 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 => { 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 10 seconds timeout const dvmJobResult = await subscribeWithTimeout(sub, 10000) return parseInt(dvmJobResult) } return null } public validate = (event: Event) => validateEvent(event) && verifyEvent(event) }