Compare commits

..

No commits in common. "35f6a639e6aebbcf897b050e1f55b01d98aa9cd6" and "7bb83695c9e41627df1aab665abcf6b6cdbd4078" have entirely different histories.

10 changed files with 158 additions and 122 deletions

View File

@ -420,10 +420,12 @@ export const DrawPDFFields = (props: Props) => {
{users {users
.filter((u) => u.role === UserRole.signer) .filter((u) => u.role === UserRole.signer)
.map((user, index) => { .map((user, index) => {
const npub = hexToNpub(user.pubkey) let displayValue = truncate(
let displayValue = truncate(npub, { hexToNpub(user.pubkey),
{
length: 16 length: 16
}) }
)
const metadata = props.metadata[user.pubkey] const metadata = props.metadata[user.pubkey]
@ -431,8 +433,7 @@ export const DrawPDFFields = (props: Props) => {
displayValue = truncate( displayValue = truncate(
metadata.name || metadata.name ||
metadata.display_name || metadata.display_name ||
metadata.username || metadata.username,
npub,
{ {
length: 16 length: 16
} }

View File

@ -15,16 +15,15 @@ import {
findRelayListAndUpdateCache, findRelayListAndUpdateCache,
findRelayListInCache, findRelayListInCache,
getDefaultRelaySet, getDefaultRelaySet,
getMostPopularRelays,
getUserRelaySet, getUserRelaySet,
isOlderThanOneDay, isOlderThanOneWeek,
unixNow unixNow
} from '../utils' } from '../utils'
import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const'
export class MetadataController extends EventEmitter { export class MetadataController extends EventEmitter {
private nostrController: NostrController private nostrController: NostrController
private specialMetadataRelay = 'wss://purplepag.es' private specialMetadataRelay = 'wss://purplepag.es'
private pendingFetches = new Map<string, Promise<Event | null>>() // Track pending fetches
constructor() { constructor() {
super() super()
@ -43,55 +42,70 @@ export class MetadataController extends EventEmitter {
hexKey: string, hexKey: string,
currentEvent: Event | null currentEvent: Event | null
): Promise<Event | null> { ): Promise<Event | null> {
// Return the ongoing fetch promise if one exists for the same hexKey
if (this.pendingFetches.has(hexKey)) {
return this.pendingFetches.get(hexKey)!
}
// Define the event filter to only include metadata events authored by the given key // Define the event filter to only include metadata events authored by the given key
const eventFilter: Filter = { const eventFilter: Filter = {
kinds: [kinds.Metadata], kinds: [kinds.Metadata], // Only metadata events
authors: [hexKey] authors: [hexKey] // Authored by the specified key
} }
const fetchPromise = relayController // Try to get the metadata event from a special relay (wss://purplepag.es)
.fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST) const metadataEvent = await relayController
.fetchEvent(eventFilter, [this.specialMetadataRelay])
.catch((err) => { .catch((err) => {
console.error(err) console.error(err) // Log any errors
return null return null // Return null if an error occurs
})
.finally(() => {
this.pendingFetches.delete(hexKey)
}) })
this.pendingFetches.set(hexKey, fetchPromise) // If a valid metadata event is found from the special relay
const metadataEvent = await fetchPromise
if ( if (
metadataEvent && metadataEvent &&
validateEvent(metadataEvent) && validateEvent(metadataEvent) && // Validate the event
verifyEvent(metadataEvent) verifyEvent(metadataEvent) // Verify the event's authenticity
) { ) {
// If there's no current event or the new metadata event is more recent
if ( if (
!currentEvent || !currentEvent ||
metadataEvent.created_at >= currentEvent.created_at metadataEvent.created_at >= currentEvent.created_at
) { ) {
// Handle the new metadata event
this.handleNewMetadataEvent(metadataEvent) this.handleNewMetadataEvent(metadataEvent)
} }
return metadataEvent return metadataEvent
} }
// todo/implement: if no valid metadata event is found in DEFAULT_LOOK_UP_RELAY_LIST // If no valid metadata event is found from the special relay, get the most popular relays
// try to query user relay list const mostPopularRelays = await getMostPopularRelays()
// if current event is null we should cache empty metadata event for provided hexKey // Query the most popular relays for metadata events
if (!currentEvent) {
const emptyMetadata = this.getEmptyMetadataEvent(hexKey) const events = await relayController
this.handleNewMetadataEvent(emptyMetadata as VerifiedEvent) .fetchEvents(eventFilter, mostPopularRelays)
.catch((err) => {
console.error(err) // Log any errors
return null // Return null if an error occurs
})
// If events are found from the popular relays
if (events && events.length) {
events.sort((a, b) => b.created_at - a.created_at) // Sort events by creation date (descending)
// Iterate through the events
for (const event of events) {
// If the event is valid, authentic, and more recent than the current event
if (
validateEvent(event) &&
verifyEvent(event) &&
(!currentEvent || event.created_at > currentEvent.created_at)
) {
// Handle the new metadata event
this.handleNewMetadataEvent(event)
return event
}
}
} }
return currentEvent return currentEvent // Return the current event if no newer event is found
} }
/** /**
@ -116,8 +130,8 @@ export class MetadataController extends EventEmitter {
// If cached metadata is found, check its validity // If cached metadata is found, check its validity
if (cachedMetadataEvent) { if (cachedMetadataEvent) {
// Check if the cached metadata is older than one day // Check if the cached metadata is older than one week
if (isOlderThanOneDay(cachedMetadataEvent.cachedAt)) { if (isOlderThanOneWeek(cachedMetadataEvent.cachedAt)) {
// If older than one week, find the metadata from relays in background // If older than one week, find the metadata from relays in background
this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event) this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event)
@ -147,7 +161,11 @@ export class MetadataController extends EventEmitter {
public findRelayListMetadata = async (hexKey: string): Promise<RelaySet> => { public findRelayListMetadata = async (hexKey: string): Promise<RelaySet> => {
const relayEvent = const relayEvent =
(await findRelayListInCache(hexKey)) || (await findRelayListInCache(hexKey)) ||
(await findRelayListAndUpdateCache(DEFAULT_LOOK_UP_RELAY_LIST, hexKey)) (await findRelayListAndUpdateCache(
[this.specialMetadataRelay],
hexKey
)) ||
(await findRelayListAndUpdateCache(await getMostPopularRelays(), hexKey))
return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet() return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet()
} }
@ -198,13 +216,13 @@ export class MetadataController extends EventEmitter {
public validate = (event: Event) => validateEvent(event) && verifyEvent(event) public validate = (event: Event) => validateEvent(event) && verifyEvent(event)
public getEmptyMetadataEvent = (pubkey?: string): Event => { public getEmptyMetadataEvent = (): Event => {
return { return {
content: '', content: '',
created_at: new Date().valueOf(), created_at: new Date().valueOf(),
id: '', id: '',
kind: 0, kind: 0,
pubkey: pubkey || '', pubkey: '',
sig: '', sig: '',
tags: [] tags: []
} }

View File

@ -7,7 +7,6 @@ import { SIGIT_RELAY } from '../utils/const'
*/ */
export class RelayController { export class RelayController {
private static instance: RelayController private static instance: RelayController
private pendingConnections = new Map<string, Promise<Relay | null>>() // Track pending connections
public connectedRelays = new Map<string, Relay>() public connectedRelays = new Map<string, Relay>()
private constructor() {} private constructor() {}
@ -36,26 +35,23 @@ export class RelayController {
* @returns A promise that resolves to the connected relay object if successful, or `null` if the connection fails. * @returns A promise that resolves to the connected relay object if successful, or `null` if the connection fails.
*/ */
public connectRelay = async (relayUrl: string): Promise<Relay | null> => { public connectRelay = async (relayUrl: string): Promise<Relay | null> => {
// Check if a relay with the same URL is already connected
const normalizedWebSocketURL = normalizeWebSocketURL(relayUrl) const normalizedWebSocketURL = normalizeWebSocketURL(relayUrl)
const relay = this.connectedRelays.get(normalizedWebSocketURL) const relay = this.connectedRelays.get(normalizedWebSocketURL)
if (relay) { if (relay) {
// If a relay is found in connectedRelay map and is connected, just return it
if (relay.connected) return relay if (relay.connected) return relay
// If relay is found in connectedRelay map but not connected, // If relay is found in connectedRelay map but not connected,
// remove it from map and call connectRelay method again // remove it from map and call connectRelay method again
this.connectedRelays.delete(relayUrl) this.connectedRelays.delete(relayUrl)
return this.connectRelay(relayUrl) return this.connectRelay(relayUrl)
} }
// Check if there's already a pending connection for this relay URL // Attempt to connect to the relay using the provided URL
if (this.pendingConnections.has(relayUrl)) { const newRelay = await Relay.connect(relayUrl)
// Return the existing promise to avoid making another connection
return this.pendingConnections.get(relayUrl)!
}
// Create a new connection promise and store it in pendingConnections
const connectionPromise = Relay.connect(relayUrl)
.then((relay) => { .then((relay) => {
if (relay.connected) { if (relay.connected) {
// Add the newly connected relay to the connected relays map // Add the newly connected relay to the connected relays map
@ -74,13 +70,8 @@ export class RelayController {
// Return null to indicate connection failure // Return null to indicate connection failure
return null return null
}) })
.finally(() => {
// Remove the connection from pendingConnections once it settles
this.pendingConnections.delete(relayUrl)
})
this.pendingConnections.set(relayUrl, connectionPromise) return newRelay
return connectionPromise
} }
/** /**
@ -95,17 +86,8 @@ export class RelayController {
filter: Filter, filter: Filter,
relayUrls: string[] = [] relayUrls: string[] = []
): Promise<Event[]> => { ): Promise<Event[]> => {
if (!relayUrls.includes(SIGIT_RELAY)) { // Add app relay to relays array and connect to all specified relays
/** const relayPromises = [...relayUrls, SIGIT_RELAY].map((relayUrl) =>
* NOTE: To avoid side-effects on external relayUrls array passed as argument
* re-assigned relayUrls with added sigit relay instead of just appending to same array
*/
relayUrls = [...relayUrls, SIGIT_RELAY] // Add app relay to relays array if not exists already
}
// connect to all specified relays
const relayPromises = relayUrls.map((relayUrl) =>
this.connectRelay(relayUrl) this.connectRelay(relayUrl)
) )
@ -219,18 +201,11 @@ export class RelayController {
relayUrls: string[] = [], relayUrls: string[] = [],
eventHandler: (event: Event) => void eventHandler: (event: Event) => void
) => { ) => {
if (!relayUrls.includes(SIGIT_RELAY)) { // Add app relay to relays array and connect to all specified relays
/**
* NOTE: To avoid side-effects on external relayUrls array passed as argument
* re-assigned relayUrls with added sigit relay instead of just appending to same array
*/
relayUrls = [...relayUrls, SIGIT_RELAY] // Add app relay to relays array if not exists already
}
// connect to all specified relays const relayPromises = [...relayUrls, SIGIT_RELAY].map((relayUrl) =>
const relayPromises = relayUrls.map((relayUrl) => { this.connectRelay(relayUrl)
return this.connectRelay(relayUrl) )
})
// Use Promise.allSettled to wait for all promises to settle // Use Promise.allSettled to wait for all promises to settle
const results = await Promise.allSettled(relayPromises) const results = await Promise.allSettled(relayPromises)
@ -283,16 +258,9 @@ export class RelayController {
event: Event, event: Event,
relayUrls: string[] = [] relayUrls: string[] = []
): Promise<string[]> => { ): Promise<string[]> => {
if (!relayUrls.includes(SIGIT_RELAY)) { // Add app relay to relays array and connect to all specified relays
/**
* NOTE: To avoid side-effects on external relayUrls array passed as argument
* re-assigned relayUrls with added sigit relay instead of just appending to same array
*/
relayUrls = [...relayUrls, SIGIT_RELAY] // Add app relay to relays array if not exists already
}
// connect to all specified relays const relayPromises = [...relayUrls, SIGIT_RELAY].map((relayUrl) =>
const relayPromises = relayUrls.map((relayUrl) =>
this.connectRelay(relayUrl) this.connectRelay(relayUrl)
) )

View File

@ -15,6 +15,7 @@ export const SET_USER_ROBOT_IMAGE = 'SET_USER_ROBOT_IMAGE'
export const SET_RELAY_MAP = 'SET_RELAY_MAP' export const SET_RELAY_MAP = 'SET_RELAY_MAP'
export const SET_RELAY_INFO = 'SET_RELAY_INFO' export const SET_RELAY_INFO = 'SET_RELAY_INFO'
export const SET_RELAY_MAP_UPDATED = 'SET_RELAY_MAP_UPDATED' export const SET_RELAY_MAP_UPDATED = 'SET_RELAY_MAP_UPDATED'
export const SET_MOST_POPULAR_RELAYS = 'SET_MOST_POPULAR_RELAYS'
export const UPDATE_USER_APP_DATA = 'UPDATE_USER_APP_DATA' export const UPDATE_USER_APP_DATA = 'UPDATE_USER_APP_DATA'
export const UPDATE_PROCESSED_GIFT_WRAPS = 'UPDATE_PROCESSED_GIFT_WRAPS' export const UPDATE_PROCESSED_GIFT_WRAPS = 'UPDATE_PROCESSED_GIFT_WRAPS'

View File

@ -1,6 +1,7 @@
import * as ActionTypes from '../actionTypes' import * as ActionTypes from '../actionTypes'
import { import {
SetRelayMapAction, SetRelayMapAction,
SetMostPopularRelaysAction,
SetRelayInfoAction, SetRelayInfoAction,
SetRelayMapUpdatedAction SetRelayMapUpdatedAction
} from './types' } from './types'
@ -18,6 +19,13 @@ export const setRelayInfoAction = (
payload payload
}) })
export const setMostPopularRelaysAction = (
payload: string[]
): SetMostPopularRelaysAction => ({
type: ActionTypes.SET_MOST_POPULAR_RELAYS,
payload
})
export const setRelayMapUpdatedAction = (): SetRelayMapUpdatedAction => ({ export const setRelayMapUpdatedAction = (): SetRelayMapUpdatedAction => ({
type: ActionTypes.SET_RELAY_MAP_UPDATED type: ActionTypes.SET_RELAY_MAP_UPDATED
}) })

View File

@ -4,6 +4,7 @@ import { RelaysDispatchTypes, RelaysState } from './types'
const initialState: RelaysState = { const initialState: RelaysState = {
map: undefined, map: undefined,
mapUpdated: undefined, mapUpdated: undefined,
mostPopular: undefined,
info: undefined info: undefined
} }
@ -24,6 +25,9 @@ const reducer = (
info: { ...state.info, ...action.payload } info: { ...state.info, ...action.payload }
} }
case ActionTypes.SET_MOST_POPULAR_RELAYS:
return { ...state, mostPopular: [...action.payload] }
case ActionTypes.RESTORE_STATE: case ActionTypes.RESTORE_STATE:
return action.payload.relays return action.payload.relays

View File

@ -5,6 +5,7 @@ import { RelayMap, RelayInfoObject } from '../../types'
export type RelaysState = { export type RelaysState = {
map?: RelayMap map?: RelayMap
mapUpdated?: number mapUpdated?: number
mostPopular?: string[]
info?: RelayInfoObject info?: RelayInfoObject
} }
@ -13,6 +14,11 @@ export interface SetRelayMapAction {
payload: RelayMap payload: RelayMap
} }
export interface SetMostPopularRelaysAction {
type: typeof ActionTypes.SET_MOST_POPULAR_RELAYS
payload: string[]
}
export interface SetRelayInfoAction { export interface SetRelayInfoAction {
type: typeof ActionTypes.SET_RELAY_INFO type: typeof ActionTypes.SET_RELAY_INFO
payload: RelayInfoObject payload: RelayInfoObject
@ -26,4 +32,5 @@ export type RelaysDispatchTypes =
| SetRelayMapAction | SetRelayMapAction
| SetRelayInfoAction | SetRelayInfoAction
| SetRelayMapUpdatedAction | SetRelayMapUpdatedAction
| SetMostPopularRelaysAction
| RestoreState | RestoreState

View File

@ -11,18 +11,7 @@ export const DEFLATE = 'DEFLATE'
/** /**
* Number of milliseconds in one week. * Number of milliseconds in one week.
* Calc based on: 7 * 24 * 60 * 60 * 1000
*/ */
export const ONE_WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000 export const ONE_WEEK_IN_MS: number = 604800000
export const SIGIT_RELAY: string = 'wss://relay.sigit.io'
/**
* Number of milliseconds in one day.
*/
export const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000
export const SIGIT_RELAY = 'wss://relay.sigit.io'
export const DEFAULT_LOOK_UP_RELAY_LIST = [
SIGIT_RELAY,
'wss://user.kindpag.es',
'wss://purplepag.es'
]

View File

@ -33,7 +33,6 @@ import { Meta, SignedEvent, UserAppData } from '../types'
import { getHash } from './hash' import { getHash } from './hash'
import { parseJson, removeLeadingSlash } from './string' import { parseJson, removeLeadingSlash } from './string'
import { timeout } from './utils' import { timeout } from './utils'
import { getDefaultRelayMap } from './relays'
/** /**
* @param hexKey hex private or public key * @param hexKey hex private or public key
@ -599,8 +598,7 @@ export const updateUsersAppData = async (meta: Meta) => {
if (!signedEvent) return null if (!signedEvent) return null
const relayMap = const relayMap = (store.getState().relays as RelaysState).map!
(store.getState().relays as RelaysState).map || getDefaultRelayMap()
const writeRelays = Object.keys(relayMap).filter((key) => relayMap[key].write) const writeRelays = Object.keys(relayMap).filter((key) => relayMap[key].write)
const publishResult = await Promise.race([ const publishResult = await Promise.race([

View File

@ -1,14 +1,13 @@
import axios from 'axios'
import { Event, Filter, kinds, UnsignedEvent } from 'nostr-tools' import { Event, Filter, kinds, UnsignedEvent } from 'nostr-tools'
import { RelayList } from 'nostr-tools/kinds' import { RelayList } from 'nostr-tools/kinds'
import { getRelayInfo, unixNow } from '.' import { getRelayInfo, unixNow } from '.'
import { NostrController, relayController } from '../controllers' import { NostrController, relayController } from '../controllers'
import { localCache } from '../services' import { localCache } from '../services'
import { RelayMap, RelaySet } from '../types' import { setMostPopularRelaysAction } from '../store/actions'
import { import store from '../store/store'
DEFAULT_LOOK_UP_RELAY_LIST, import { RelayMap, RelayReadStats, RelaySet, RelayStats } from '../types'
ONE_WEEK_IN_MS, import { ONE_WEEK_IN_MS, SIGIT_RELAY } from './const'
SIGIT_RELAY
} from './const'
const READ_MARKER = 'read' const READ_MARKER = 'read'
const WRITE_MARKER = 'write' const WRITE_MARKER = 'write'
@ -30,7 +29,6 @@ const findRelayListAndUpdateCache = async (
authors: [hexKey] authors: [hexKey]
} }
console.count('findRelayListAndUpdateCache')
const event = await relayController.fetchEvent(eventFilter, lookUpRelays) const event = await relayController.fetchEvent(eventFilter, lookUpRelays)
if (event) { if (event) {
await localCache.addUserRelayListMetadata(event) await localCache.addUserRelayListMetadata(event)
@ -92,10 +90,6 @@ const isOlderThanOneWeek = (cachedAt: number) => {
return Date.now() - cachedAt < ONE_WEEK_IN_MS return Date.now() - cachedAt < ONE_WEEK_IN_MS
} }
const isOlderThanOneDay = (cachedAt: number) => {
return Date.now() - cachedAt < ONE_WEEK_IN_MS
}
const isRelayTag = (tag: string[]): boolean => tag[0] === 'r' const isRelayTag = (tag: string[]): boolean => tag[0] === 'r'
const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => { const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => {
@ -116,6 +110,51 @@ const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => {
return obj return obj
} }
/**
* Provides most popular relays.
* @param numberOfTopRelays - number representing how many most popular relays to provide
* @returns - promise that resolves into an array of most popular relays
*/
const getMostPopularRelays = async (
numberOfTopRelays: number = 30
): Promise<string[]> => {
const mostPopularRelaysState = store.getState().relays?.mostPopular
// return most popular relays from app state if present
if (mostPopularRelaysState) return mostPopularRelaysState
// relays in env
const { VITE_MOST_POPULAR_RELAYS } = import.meta.env
const hardcodedPopularRelays = (VITE_MOST_POPULAR_RELAYS || '').split(' ')
const url = `https://stats.nostr.band/stats_api?method=stats`
const response = await axios.get<RelayStats>(url).catch(() => undefined)
if (!response) {
return hardcodedPopularRelays //return hardcoded relay list
}
const data = response.data
if (!data) {
return hardcodedPopularRelays //return hardcoded relay list
}
const apiTopRelays = data.relay_stats.user_picks.read_relays
.slice(0, numberOfTopRelays)
.map((relay: RelayReadStats) => relay.d)
if (!apiTopRelays.length) {
return Promise.reject(`Couldn't fetch popular relays.`)
}
if (store.getState().auth?.loggedIn) {
store.dispatch(setMostPopularRelaysAction(apiTopRelays))
}
return apiTopRelays
}
/** /**
* Provides relay map. * Provides relay map.
* @param npub - user's npub * @param npub - user's npub
@ -124,6 +163,8 @@ const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => {
const getRelayMap = async ( const getRelayMap = async (
npub: string npub: string
): Promise<{ map: RelayMap; mapUpdated?: number }> => { ): Promise<{ map: RelayMap; mapUpdated?: number }> => {
const mostPopularRelays = await getMostPopularRelays()
// More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
const eventFilter: Filter = { const eventFilter: Filter = {
kinds: [kinds.RelayList], kinds: [kinds.RelayList],
@ -131,7 +172,7 @@ const getRelayMap = async (
} }
const event = await relayController const event = await relayController
.fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST) .fetchEvent(eventFilter, mostPopularRelays)
.catch((err) => { .catch((err) => {
return Promise.reject(err) return Promise.reject(err)
}) })
@ -154,9 +195,9 @@ const getRelayMap = async (
} }
}) })
Object.keys(relaysMap).forEach((relayUrl) => Object.keys(relaysMap).forEach((relayUrl) => {
relayController.connectRelay(relayUrl) relayController.connectRelay(relayUrl)
) })
getRelayInfo(Object.keys(relaysMap)) getRelayInfo(Object.keys(relaysMap))
@ -214,8 +255,9 @@ const publishRelayMap = async (
// If relay map is empty, use most popular relay URIs // If relay map is empty, use most popular relay URIs
if (!relaysToPublish.length) { if (!relaysToPublish.length) {
relaysToPublish = DEFAULT_LOOK_UP_RELAY_LIST relaysToPublish = await getMostPopularRelays()
} }
const publishResult = await relayController.publish( const publishResult = await relayController.publish(
signedEvent, signedEvent,
relaysToPublish relaysToPublish
@ -235,9 +277,9 @@ export {
findRelayListInCache, findRelayListInCache,
getDefaultRelayMap, getDefaultRelayMap,
getDefaultRelaySet, getDefaultRelaySet,
getMostPopularRelays,
getRelayMap, getRelayMap,
publishRelayMap,
getUserRelaySet, getUserRelaySet,
isOlderThanOneDay, isOlderThanOneWeek
isOlderThanOneWeek,
publishRelayMap
} }