2024-02-28 16:49:44 +00:00
|
|
|
import {
|
|
|
|
Filter,
|
|
|
|
SimplePool,
|
|
|
|
VerifiedEvent,
|
|
|
|
kinds,
|
|
|
|
validateEvent,
|
2024-03-01 10:16:35 +00:00
|
|
|
verifyEvent,
|
2024-05-10 10:16:28 +00:00
|
|
|
Event,
|
2024-05-10 10:57:04 +00:00
|
|
|
EventTemplate,
|
|
|
|
nip19
|
2024-02-28 16:49:44 +00:00
|
|
|
} from 'nostr-tools'
|
2024-05-10 10:57:04 +00:00
|
|
|
import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types'
|
2024-03-01 10:16:35 +00:00
|
|
|
import { NostrController } from '.'
|
|
|
|
import { toast } from 'react-toastify'
|
2024-05-10 10:16:28 +00:00
|
|
|
import { queryNip05 } from '../utils'
|
|
|
|
import NDK, { NDKEvent, NDKSubscription } from '@nostr-dev-kit/ndk'
|
2024-02-28 16:49:44 +00:00
|
|
|
|
|
|
|
export class MetadataController {
|
2024-03-01 10:16:35 +00:00
|
|
|
private nostrController: NostrController
|
|
|
|
private specialMetadataRelay = 'wss://purplepag.es'
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.nostrController = NostrController.getInstance()
|
|
|
|
}
|
2024-02-28 16:49:44 +00:00
|
|
|
|
2024-05-16 14:45:00 +00:00
|
|
|
public getEmptyMetadataEvent = (): Event => {
|
|
|
|
return {
|
|
|
|
content: '',
|
|
|
|
created_at: new Date().valueOf(),
|
|
|
|
id: '',
|
|
|
|
kind: 0,
|
|
|
|
pubkey: '',
|
|
|
|
sig: '',
|
|
|
|
tags: []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-28 16:49:44 +00:00
|
|
|
public findMetadata = async (hexKey: string) => {
|
|
|
|
const eventFilter: Filter = {
|
|
|
|
kinds: [kinds.Metadata],
|
|
|
|
authors: [hexKey]
|
|
|
|
}
|
|
|
|
|
|
|
|
const pool = new SimplePool()
|
|
|
|
|
2024-03-19 09:23:34 +00:00
|
|
|
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]
|
|
|
|
|
2024-02-28 16:49:44 +00:00
|
|
|
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.')
|
|
|
|
}
|
|
|
|
|
2024-04-16 06:12:29 +00:00
|
|
|
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.')
|
|
|
|
}
|
|
|
|
|
2024-02-28 16:49:44 +00:00
|
|
|
public extractProfileMetadataContent = (event: VerifiedEvent) => {
|
|
|
|
try {
|
2024-05-17 11:33:01 +00:00
|
|
|
if (!event.content) return {}
|
2024-03-01 10:16:35 +00:00
|
|
|
return JSON.parse(event.content) as ProfileMetadata
|
2024-02-28 16:49:44 +00:00
|
|
|
} catch (error) {
|
|
|
|
console.log('error in parsing metadata event content :>> ', error)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
2024-03-01 10:16:35 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
|
2024-03-19 09:23:34 +00:00
|
|
|
// Metadata event to publish to the wss://purplepag.es relay
|
2024-03-01 10:16:35 +00:00
|
|
|
const newMetadataEvent: Event = {
|
|
|
|
...event,
|
|
|
|
created_at: timestamp
|
|
|
|
}
|
|
|
|
|
2024-05-15 08:50:21 +00:00
|
|
|
signedMetadataEvent =
|
|
|
|
await this.nostrController.signEvent(newMetadataEvent)
|
2024-03-01 10:16:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
await this.nostrController
|
2024-04-16 06:12:29 +00:00
|
|
|
.publishEvent(signedMetadataEvent, [this.specialMetadataRelay])
|
|
|
|
.then((relays) => {
|
|
|
|
toast.success(`Metadata event published on: ${relays.join('\n')}`)
|
2024-03-01 10:16:35 +00:00
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
toast.error(err.message)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-05-10 10:57:04 +00:00
|
|
|
public getNostrJoiningBlockNumber = async (
|
|
|
|
hexKey: string
|
|
|
|
): Promise<NostrJoiningBlock | null> => {
|
2024-05-10 10:16:28 +00:00
|
|
|
const relaySet = await this.findRelayListMetadata(hexKey)
|
|
|
|
|
2024-05-10 10:57:04 +00:00
|
|
|
const userRelays: string[] = []
|
2024-05-10 10:16:28 +00:00
|
|
|
|
2024-05-10 10:27:34 +00:00
|
|
|
// find user's relays
|
2024-05-10 10:16:28 +00:00
|
|
|
if (relaySet.write.length > 0) {
|
2024-05-10 10:57:04 +00:00
|
|
|
userRelays.push(...relaySet.write)
|
2024-05-10 10:16:28 +00:00
|
|
|
} 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) {
|
2024-05-10 10:57:04 +00:00
|
|
|
userRelays.push(...nip05Profile.relays)
|
2024-05-10 10:16:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-10 10:57:04 +00:00
|
|
|
if (userRelays.length === 0) return null
|
2024-05-10 10:16:28 +00:00
|
|
|
|
2024-05-10 10:27:34 +00:00
|
|
|
// filter for finding user's first kind 1 event
|
2024-05-10 10:16:28 +00:00
|
|
|
const eventFilter: Filter = {
|
|
|
|
kinds: [kinds.ShortTextNote],
|
|
|
|
authors: [hexKey]
|
|
|
|
}
|
|
|
|
|
|
|
|
const pool = new SimplePool()
|
2024-05-10 10:27:34 +00:00
|
|
|
|
|
|
|
// find user's kind 1 events published on user's relays
|
2024-05-10 10:57:04 +00:00
|
|
|
const events = await pool.querySync(userRelays, eventFilter)
|
2024-05-10 10:16:28 +00:00
|
|
|
if (events && events.length) {
|
2024-05-10 10:27:34 +00:00
|
|
|
// sort events by created_at time in ascending order
|
2024-05-10 10:16:28 +00:00
|
|
|
events.sort((a, b) => a.created_at - b.created_at)
|
|
|
|
|
2024-05-10 10:27:34 +00:00
|
|
|
// get first ever event published on user's relays
|
2024-05-10 10:16:28 +00:00
|
|
|
const event = events[0]
|
|
|
|
const { created_at } = event
|
|
|
|
|
2024-05-10 10:27:34 +00:00
|
|
|
// initialize job request
|
2024-05-10 10:16:28 +00:00
|
|
|
const jobEventTemplate: EventTemplate = {
|
|
|
|
content: '',
|
|
|
|
created_at: Math.round(Date.now() / 1000),
|
|
|
|
kind: 68001,
|
|
|
|
tags: [
|
|
|
|
['i', `${created_at * 1000}`],
|
|
|
|
['j', 'blockChain-block-number']
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2024-05-10 10:27:34 +00:00
|
|
|
// sign job request event
|
2024-05-15 08:50:21 +00:00
|
|
|
const jobSignedEvent =
|
|
|
|
await this.nostrController.signEvent(jobEventTemplate)
|
2024-05-10 10:16:28 +00:00
|
|
|
|
|
|
|
const relays = [
|
|
|
|
'wss://relay.damus.io',
|
|
|
|
'wss://relay.primal.net',
|
|
|
|
'wss://relayable.org'
|
|
|
|
]
|
|
|
|
|
2024-05-10 10:27:34 +00:00
|
|
|
// publish job request
|
2024-05-10 10:16:28 +00:00
|
|
|
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)
|
|
|
|
|
2024-05-10 10:27:34 +00:00
|
|
|
// filter for getting DVM job's result
|
2024-05-10 10:16:28 +00:00
|
|
|
const sub = dvmNDK.subscribe({
|
|
|
|
kinds: [68002 as number],
|
|
|
|
'#e': [jobSignedEvent.id],
|
|
|
|
'#p': [jobSignedEvent.pubkey]
|
|
|
|
})
|
|
|
|
|
2024-05-10 10:57:04 +00:00
|
|
|
// asynchronously get block number from dvm job with 20 seconds timeout
|
|
|
|
const dvmJobResult = await subscribeWithTimeout(sub, 20000)
|
2024-05-10 10:16:28 +00:00
|
|
|
|
2024-05-10 10:57:04 +00:00
|
|
|
const encodedEventPointer = nip19.neventEncode({
|
|
|
|
id: event.id,
|
|
|
|
relays: userRelays,
|
|
|
|
author: event.pubkey,
|
|
|
|
kind: event.kind
|
|
|
|
})
|
|
|
|
|
|
|
|
return {
|
|
|
|
block: parseInt(dvmJobResult),
|
|
|
|
encodedEventPointer
|
|
|
|
}
|
2024-05-10 10:16:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2024-03-01 10:16:35 +00:00
|
|
|
public validate = (event: Event) => validateEvent(event) && verifyEvent(event)
|
2024-02-28 16:49:44 +00:00
|
|
|
}
|