From 1eed099059fe169e166c979e5900ceeb6da557b7 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Fri, 10 May 2024 15:16:28 +0500 Subject: [PATCH 1/3] feat: show block number on user profile --- src/controllers/MetadataController.ts | 107 +++++++++++++++++++++++++- src/pages/profile/index.tsx | 38 ++++++++- 2 files changed, 141 insertions(+), 4 deletions(-) diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index b2c116d..511b1a2 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -5,11 +5,14 @@ import { kinds, validateEvent, verifyEvent, - Event + 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 @@ -163,5 +166,107 @@ export class MetadataController { }) } + public getNostrJoiningBlockNumber = async (hexKey: string) => { + const relaySet = await this.findRelayListMetadata(hexKey) + + const relays: string[] = [] + + 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 + + const eventFilter: Filter = { + kinds: [kinds.ShortTextNote], + authors: [hexKey] + } + + const pool = new SimplePool() + const events = await pool.querySync(relays, eventFilter) + if (events && events.length) { + events.sort((a, b) => a.created_at - b.created_at) + + const event = events[0] + const { created_at } = event + + const jobEventTemplate: EventTemplate = { + content: '', + created_at: Math.round(Date.now() / 1000), + kind: 68001, + tags: [ + ['i', `${created_at * 1000}`], + ['j', 'blockChain-block-number'] + ] + } + + const jobSignedEvent = await this.nostrController.signEvent( + jobEventTemplate + ) + + const relays = [ + 'wss://relay.damus.io', + 'wss://relay.primal.net', + 'wss://relayable.org' + ] + + 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) + + const sub = dvmNDK.subscribe({ + kinds: [68002 as number], + '#e': [jobSignedEvent.id], + '#p': [jobSignedEvent.pubkey] + }) + + const dvmJobResult = await subscribeWithTimeout(sub, 10000) + + return parseInt(dvmJobResult) + } + + return null + } + public validate = (event: Event) => validateEvent(event) && verifyEvent(event) } diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index e3cb803..257419e 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -1,5 +1,12 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy' -import { List, ListItem, ListSubheader, TextField } from '@mui/material' +import { + List, + ListItem, + ListSubheader, + TextField, + Typography, + useTheme +} from '@mui/material' import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' import { useParams } from 'react-router-dom' @@ -17,6 +24,8 @@ import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoginMethods } from '../../store/auth/types' export const ProfilePage = () => { + const theme = useTheme() + const { npub } = useParams() const dispatch: Dispatch = useDispatch() @@ -25,6 +34,7 @@ export const ProfilePage = () => { const nostrController = NostrController.getInstance() const [pubkey, setPubkey] = useState() + const [blockNumber, setBlockNumber] = useState(null) const [profileMetadata, setProfileMetadata] = useState() const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) const metadataState = useSelector((state: State) => state.metadata) @@ -50,6 +60,18 @@ export const ProfilePage = () => { }, [npub, usersPubkey]) useEffect(() => { + if (pubkey) { + metadataController + .getNostrJoiningBlockNumber(pubkey) + .then((res) => { + setBlockNumber(res) + }) + .catch((err) => { + // todo: handle error + console.log('err :>> ', err) + }) + } + if (isUsersOwnProfile && metadataState) { const metadataContent = metadataController.extractProfileMetadataContent( metadataState as VerifiedEvent @@ -206,8 +228,7 @@ export const ProfilePage = () => { sx={{ marginTop: 1, display: 'flex', - flexDirection: 'column', - gap: 2 + flexDirection: 'column' }} > { src={profileMetadata.picture || placeholderAvatar} alt='Profile Image' /> + {blockNumber && ( + + On nostr since {blockNumber.toLocaleString()} + + )} {editItem('name', 'Username')} From 5c36554b663dc9f9fb9722e2fc965015ea920d67 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Fri, 10 May 2024 15:27:34 +0500 Subject: [PATCH 2/3] chore: add comments --- src/controllers/MetadataController.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index 511b1a2..8c10ce6 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -171,6 +171,7 @@ export class MetadataController { const relays: string[] = [] + // find user's relays if (relaySet.write.length > 0) { relays.push(...relaySet.write) } else { @@ -188,19 +189,25 @@ export class MetadataController { 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), @@ -211,6 +218,7 @@ export class MetadataController { ] } + // sign job request event const jobSignedEvent = await this.nostrController.signEvent( jobEventTemplate ) @@ -221,6 +229,7 @@ export class MetadataController { 'wss://relayable.org' ] + // publish job request await this.nostrController.publishEvent(jobSignedEvent, relays) console.log('jobSignedEvent :>> ', jobSignedEvent) @@ -254,12 +263,14 @@ export class MetadataController { 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) From 37bc205ce4e8ea8eb1cbcd5f1a57703501bd7c52 Mon Sep 17 00:00:00 2001 From: SwiftHawk Date: Fri, 10 May 2024 15:57:04 +0500 Subject: [PATCH 3/3] feat: make block number link that will refernce to the event --- src/controllers/MetadataController.ts | 35 ++++++++++++++++++--------- src/pages/profile/index.tsx | 16 +++++++----- src/types/nostr.ts | 5 ++++ 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/controllers/MetadataController.ts b/src/controllers/MetadataController.ts index 8c10ce6..7dd1285 100644 --- a/src/controllers/MetadataController.ts +++ b/src/controllers/MetadataController.ts @@ -6,9 +6,10 @@ import { validateEvent, verifyEvent, Event, - EventTemplate + EventTemplate, + nip19 } from 'nostr-tools' -import { ProfileMetadata, RelaySet } from '../types' +import { NostrJoiningBlock, ProfileMetadata, RelaySet } from '../types' import { NostrController } from '.' import { toast } from 'react-toastify' import { queryNip05 } from '../utils' @@ -166,14 +167,16 @@ export class MetadataController { }) } - public getNostrJoiningBlockNumber = async (hexKey: string) => { + public getNostrJoiningBlockNumber = async ( + hexKey: string + ): Promise => { const relaySet = await this.findRelayListMetadata(hexKey) - const relays: string[] = [] + const userRelays: string[] = [] // find user's relays if (relaySet.write.length > 0) { - relays.push(...relaySet.write) + userRelays.push(...relaySet.write) } else { const metadata = await this.findMetadata(hexKey) const metadataContent = this.extractProfileMetadataContent(metadata) @@ -182,12 +185,12 @@ export class MetadataController { const nip05Profile = await queryNip05(metadataContent.nip05) if (nip05Profile && nip05Profile.pubkey === hexKey) { - relays.push(...nip05Profile.relays) + userRelays.push(...nip05Profile.relays) } } } - if (relays.length === 0) return null + if (userRelays.length === 0) return null // filter for finding user's first kind 1 event const eventFilter: Filter = { @@ -198,7 +201,7 @@ export class MetadataController { const pool = new SimplePool() // find user's kind 1 events published on user's relays - const events = await pool.querySync(relays, eventFilter) + 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) @@ -270,10 +273,20 @@ export class MetadataController { '#p': [jobSignedEvent.pubkey] }) - // asynchronously get block number from dvm job with 10 seconds timeout - const dvmJobResult = await subscribeWithTimeout(sub, 10000) + // asynchronously get block number from dvm job with 20 seconds timeout + const dvmJobResult = await subscribeWithTimeout(sub, 20000) - return parseInt(dvmJobResult) + const encodedEventPointer = nip19.neventEncode({ + id: event.id, + relays: userRelays, + author: event.pubkey, + kind: event.kind + }) + + return { + block: parseInt(dvmJobResult), + encodedEventPointer + } } return null diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 257419e..6ad9c2f 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -9,11 +9,11 @@ import { } from '@mui/material' import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' -import { useParams } from 'react-router-dom' +import { Link, useParams } from 'react-router-dom' import { toast } from 'react-toastify' import placeholderAvatar from '../../assets/images/nostr-logo.jpg' import { MetadataController, NostrController } from '../../controllers' -import { ProfileMetadata } from '../../types' +import { NostrJoiningBlock, ProfileMetadata } from '../../types' import styles from './style.module.scss' import { useDispatch, useSelector } from 'react-redux' import { State } from '../../store/rootReducer' @@ -34,7 +34,8 @@ export const ProfilePage = () => { const nostrController = NostrController.getInstance() const [pubkey, setPubkey] = useState() - const [blockNumber, setBlockNumber] = useState(null) + const [nostrJoiningBlock, setNostrJoiningBlock] = + useState(null) const [profileMetadata, setProfileMetadata] = useState() const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) const metadataState = useSelector((state: State) => state.metadata) @@ -64,7 +65,7 @@ export const ProfilePage = () => { metadataController .getNostrJoiningBlockNumber(pubkey) .then((res) => { - setBlockNumber(res) + setNostrJoiningBlock(res) }) .catch((err) => { // todo: handle error @@ -239,15 +240,18 @@ export const ProfilePage = () => { src={profileMetadata.picture || placeholderAvatar} alt='Profile Image' /> - {blockNumber && ( + {nostrJoiningBlock && ( - On nostr since {blockNumber.toLocaleString()} + On nostr since {nostrJoiningBlock.block.toLocaleString()} )} diff --git a/src/types/nostr.ts b/src/types/nostr.ts index 87a9b27..67572d8 100644 --- a/src/types/nostr.ts +++ b/src/types/nostr.ts @@ -12,3 +12,8 @@ export interface RelaySet { read: string[] write: string[] } + +export interface NostrJoiningBlock { + block: number + encodedEventPointer: string +}