diff --git a/.eslintrc.js b/.eslintrc.js index dcd8fc0..42d4f25 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,21 +1,36 @@ module.exports = { - parser: "@typescript-eslint/parser", + parser: '@typescript-eslint/parser', parserOptions: { - sourceType: "module", + sourceType: 'module', }, - plugins: ["@typescript-eslint/eslint-plugin"], - extends: ["plugin:@typescript-eslint/recommended"], + plugins: ['@typescript-eslint/eslint-plugin'], + extends: ['plugin:@typescript-eslint/recommended'], root: true, env: { node: true, }, - ignorePatterns: [".eslintrc.js", "dist", "tslint.json", "node_modules"], + ignorePatterns: ['dist', 'tslint.json', 'node_modules'], rules: { - "@typescript-eslint/interface-name-prefix": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], - "no-console": "off", - semi: ["error", "never"], - quotes: ["error", "single", { avoidEscape: true }] + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'no-console': 'off', + semi: ['error', 'never'], + quotes: ['error', 'single', { avoidEscape: true }], + 'sort-imports': ['error', { + ignoreCase: true, + allowSeparatedGroups: true, + }], + curly: [2, 'multi-line'], + 'max-len': [ + 'error', + { + code: 120, + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreRegExpLiterals: true, + }, + ], + 'comma-dangle': ['error', 'always-multiline'], }, -}; +} diff --git a/.mocharc.js b/.mocharc.js index 628e8fb..63dde7c 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -1,6 +1,6 @@ module.exports = { extension: ['ts'], - require: ['ts-node/register', 'source-map-support/register'], + require: ['ts-node/register'], reporter: 'mochawesome', slow: 75, sorted: true, diff --git a/README.md b/README.md index e23c6e3..d6c52f7 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ NIPs with a relay-specific implementation are listed here. - [x] NIP-15: End of Stored Events Notice - [x] NIP-16: Event Treatment - [ ] NIP-25: Reactions -- [x] NIP-27: Multicasting (Experimental) +- [x] NIP-26: Delegated Event Signing +- [ ] NIP-27: Restricted Events (Experimental) ## Requirements diff --git a/migrations/20220825_204900_add_delegator_to_events_table.js b/migrations/20220825_204900_add_delegator_to_events_table.js new file mode 100644 index 0000000..2763b4b --- /dev/null +++ b/migrations/20220825_204900_add_delegator_to_events_table.js @@ -0,0 +1,12 @@ +exports.up = function (knex) { + // NIP-26: Delegated Event Signing + return knex.schema.alterTable('events', function (table) { + table.binary('event_delegator').nullable().index() + }) +} + +exports.down = function (knex) { + return knex.schema.alterTable('events', function (table) { + table.dropColumn('event_delegator') + }) +} diff --git a/src/@types/adapters.ts b/src/@types/adapters.ts index 20a132c..cc9a7ef 100644 --- a/src/@types/adapters.ts +++ b/src/@types/adapters.ts @@ -1,5 +1,6 @@ import { EventEmitter } from 'node:stream' import { WebSocket } from 'ws' + import { Event } from './event' import { OutgoingMessage } from './messages' diff --git a/src/@types/event.ts b/src/@types/event.ts index 8c9b749..89fcd33 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -1,4 +1,4 @@ -import { EventKinds } from '../constants/base' +import { EventDelegatorMetadataKey, EventKinds } from '../constants/base' import { EventId, Pubkey, Tag } from './base' @@ -10,6 +10,7 @@ export interface Event { tags: Tag[] sig: string content: string + [EventDelegatorMetadataKey]?: Pubkey } export interface DBEvent { @@ -21,6 +22,7 @@ export interface DBEvent { event_content: string event_tags: Tag[] event_signature: Buffer + event_delegator?: Buffer | null first_seen: Date } diff --git a/src/@types/messages.ts b/src/@types/messages.ts index b673026..85202c8 100644 --- a/src/@types/messages.ts +++ b/src/@types/messages.ts @@ -1,6 +1,6 @@ -import { Range } from './base' +import { SubscriptionFilter, SubscriptionId } from './subscription' import { Event } from './event' -import { SubscriptionId, SubscriptionFilter } from './subscription' +import { Range } from './base' export enum MessageType { REQ = 'REQ', diff --git a/src/@types/repositories.ts b/src/@types/repositories.ts index b06f3f4..c12ec68 100644 --- a/src/@types/repositories.ts +++ b/src/@types/repositories.ts @@ -1,6 +1,7 @@ import { PassThrough } from 'stream' -import { EventId, Pubkey } from './base' + import { DBEvent, Event } from './event' +import { EventId, Pubkey } from './base' import { SubscriptionFilter } from './subscription' export type ExposedPromiseKeys = 'then' | 'catch' | 'finally' diff --git a/src/@types/subscription.ts b/src/@types/subscription.ts index 0bdcf0f..e78efb2 100644 --- a/src/@types/subscription.ts +++ b/src/@types/subscription.ts @@ -1,5 +1,5 @@ -import { EventKinds } from '../constants/base' import { EventId, Pubkey } from './base' +import { EventKinds } from '../constants/base' export type SubscriptionId = string diff --git a/src/adapters/web-server-adapter.ts b/src/adapters/web-server-adapter.ts index 39cf07c..74ef12b 100644 --- a/src/adapters/web-server-adapter.ts +++ b/src/adapters/web-server-adapter.ts @@ -1,9 +1,9 @@ -import { IncomingMessage, Server, ServerResponse } from 'http' import { Duplex, EventEmitter } from 'stream' - +import { IncomingMessage, Server, ServerResponse } from 'http' import packageJson from '../../package.json' -import { Settings } from '../settings' + import { IWebServerAdapter } from '../@types/adapters' +import { Settings } from '../utils/settings' export class WebServerAdapter extends EventEmitter implements IWebServerAdapter { @@ -12,8 +12,8 @@ export class WebServerAdapter extends EventEmitter implements IWebServerAdapter ) { super() this.webServer.on('request', this.onWebServerRequest.bind(this)) - this.webServer.on('clientError', this.onWebServerSocketError.bind(this)) - this.webServer.on('close', this.onClose.bind(this)) + .on('clientError', this.onWebServerSocketError.bind(this)) + .on('close', this.onClose.bind(this)) } public listen(port: number): void { @@ -32,7 +32,7 @@ export class WebServerAdapter extends EventEmitter implements IWebServerAdapter description, pubkey, contact, - supported_nips: [1, 2, 4, 9, 11, 12, 15, 16], + supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 26], software: packageJson.repository.url, version: packageJson.version, } diff --git a/src/adapters/web-socket-adapter.ts b/src/adapters/web-socket-adapter.ts index c9f9674..78df626 100644 --- a/src/adapters/web-socket-adapter.ts +++ b/src/adapters/web-socket-adapter.ts @@ -1,23 +1,27 @@ import { EventEmitter } from 'stream' +import { IncomingMessage as IncomingHttpMessage } from 'http' import { WebSocket } from 'ws' -import { IWebSocketAdapter, IWebSocketServerAdapter } from '../@types/adapters' -import { Factory } from '../@types/base' -import { Event } from '../@types/event' -import { IMessageHandler, IAbortable } from '../@types/message-handlers' +import { IAbortable, IMessageHandler } from '../@types/message-handlers' import { IncomingMessage, OutgoingMessage } from '../@types/messages' +import { IWebSocketAdapter, IWebSocketServerAdapter } from '../@types/adapters' import { SubscriptionFilter, SubscriptionId } from '../@types/subscription' -import { createOutgoingEventMessage } from '../messages' -import { messageSchema } from '../schemas/message-schema' -import { isEventMatchingFilter } from '../utils/event' import { attemptValidation } from '../utils/validation' +import { createOutgoingEventMessage } from '../utils/messages' +import { Event } from '../@types/event' +import { Factory } from '../@types/base' +import { isEventMatchingFilter } from '../utils/event' +import { messageSchema } from '../schemas/message-schema' export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter { + private id: string + private clientAddress: string private alive: boolean private subscriptions: Map> public constructor( private readonly client: WebSocket, + private readonly request: IncomingHttpMessage, private readonly webSocketServer: IWebSocketServerAdapter, private readonly createMessageHandler: Factory, ) { @@ -25,11 +29,19 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter this.alive = true this.subscriptions = new Map() + this.id = Buffer.from(request.headers['sec-websocket-key'], 'base64').toString('hex') + this.clientAddress = request.headers['x-forwarded-for'] as string + + console.log('id', this.id, 'clientAddress', this.clientAddress) + console.log('listener count:', this.client.listenerCount('close')) + this.client .on('message', this.onClientMessage.bind(this)) .on('close', this.onClientClose.bind(this)) .on('pong', this.onClientPong.bind(this)) + console.log('+listener count:', this.client.listenerCount('close')) + this .on('heartbeat', this.onHeartbeat.bind(this)) .on('subscribe', this.onSubscribed.bind(this)) @@ -60,6 +72,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter } public sendMessage(message: OutgoingMessage): void { + console.log('sending message', message) this.client.send(JSON.stringify(message)) } @@ -86,12 +99,11 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter try { const message = attemptValidation(messageSchema)(JSON.parse(raw.toString('utf-8'))) - console.debug('message received:', message[0]) - const messageHandler = this.createMessageHandler([message, this]) as IMessageHandler & IAbortable if (typeof messageHandler.abort === 'function') { abort = messageHandler.abort.bind(messageHandler) this.client.prependOnceListener('close', abort) + console.log('+listener count:', this.client.listenerCount('close')) } await messageHandler?.handleMessage(message) @@ -107,6 +119,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter if (abort) { this.client.removeListener('close', abort) } + console.log('-listener count:', this.client.listenerCount('close')) } } @@ -121,5 +134,6 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter this.removeAllListeners() this.client.removeAllListeners() + console.log('-listener count:', this.client.listenerCount('close')) } } diff --git a/src/adapters/web-socket-server-adapter.ts b/src/adapters/web-socket-server-adapter.ts index 52cf39f..35ad862 100644 --- a/src/adapters/web-socket-server-adapter.ts +++ b/src/adapters/web-socket-server-adapter.ts @@ -1,11 +1,11 @@ -import { Server } from 'http' +import { IncomingMessage, Server } from 'http' import WebSocket, { OPEN, WebSocketServer } from 'ws' -import { Event } from '../@types/event' import { IWebSocketAdapter, IWebSocketServerAdapter } from '../@types/adapters' -import { WebServerAdapter } from './web-server-adapter' +import { Event } from '../@types/event' import { Factory } from '../@types/base' import { propEq } from 'ramda' +import { WebServerAdapter } from './web-server-adapter' const WSS_CLIENT_HEALTH_PROBE_INTERVAL = 30000 @@ -18,7 +18,10 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock public constructor( webServer: Server, private readonly webSocketServer: WebSocketServer, - private readonly createWebSocketAdapter: Factory + private readonly createWebSocketAdapter: Factory< + IWebSocketAdapter, + [WebSocket, IncomingMessage, IWebSocketServerAdapter] + > ) { super(webServer) @@ -49,10 +52,10 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock }) } - private onWebSocketServerConnection(client: WebSocket) { + private onWebSocketServerConnection(client: WebSocket, req: IncomingMessage) { console.debug(`new client - ${this.getConnectedClients()} connected / ${this.webSocketServer.clients.size} total`) - this.webSocketsAdapters.set(client, this.createWebSocketAdapter([client, this])) + this.webSocketsAdapters.set(client, this.createWebSocketAdapter([client, req, this])) } private onWebSocketServerHeartbeat() { diff --git a/src/constants/base.ts b/src/constants/base.ts index 7fefea7..55e281b 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -11,5 +11,9 @@ export enum EventKinds { export enum EventTags { Event = 'e', Pubkey = 'p', - Multicast = 'm', + // Multicast = 'm', + Delegation = 'delegation', } + +export const EventDelegatorMetadataKey = Symbol('Delegator') + diff --git a/src/factories/event-strategy-factory.ts b/src/factories/event-strategy-factory.ts index 841eb1d..8d29f52 100644 --- a/src/factories/event-strategy-factory.ts +++ b/src/factories/event-strategy-factory.ts @@ -1,28 +1,29 @@ +import { isDeleteEvent, isEphemeralEvent, isNullEvent, isReplaceableEvent } from '../utils/event' import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy' +import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy' import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy' +import { Event } from '../@types/event' +import { Factory } from '../@types/base' +import { IEventRepository } from '../@types/repositories' +import { IEventStrategy } from '../@types/message-handlers' +import { IWebSocketAdapter } from '../@types/adapters' import { NullEventStrategy } from '../handlers/event-strategies/null-event-strategy' import { ReplaceableEventStrategy } from '../handlers/event-strategies/replaceable-event-strategy' -import { Factory } from '../@types/base' -import { Event } from '../@types/event' -import { IEventStrategy } from '../@types/message-handlers' -import { IEventRepository } from '../@types/repositories' -import { isDeleteEvent, isEphemeralEvent, isNullEvent, isReplaceableEvent } from '../utils/event' -import { IWebSocketAdapter } from '../@types/adapters' -import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy' export const eventStrategyFactory = ( eventRepository: IEventRepository, -): Factory>, [Event, IWebSocketAdapter]> => ([event, adapter]: [Event, IWebSocketAdapter]) => { - if (isReplaceableEvent(event)) { - return new ReplaceableEventStrategy(adapter, eventRepository) - } else if (isEphemeralEvent(event)) { - return new EphemeralEventStrategy(adapter) - } else if (isNullEvent(event)) { - return new NullEventStrategy() - } else if (isDeleteEvent(event)) { - return new DeleteEventStrategy(eventRepository) - } +): Factory>, [Event, IWebSocketAdapter]> => + ([event, adapter]: [Event, IWebSocketAdapter]) => { + if (isReplaceableEvent(event)) { + return new ReplaceableEventStrategy(adapter, eventRepository) + } else if (isEphemeralEvent(event)) { + return new EphemeralEventStrategy(adapter) + } else if (isNullEvent(event)) { + return new NullEventStrategy() + } else if (isDeleteEvent(event)) { + return new DeleteEventStrategy(eventRepository) + } - return new DefaultEventStrategy(adapter, eventRepository) -} + return new DefaultEventStrategy(adapter, eventRepository) + } diff --git a/src/factories/message-handler-factory.ts b/src/factories/message-handler-factory.ts index c276a71..15792dc 100644 --- a/src/factories/message-handler-factory.ts +++ b/src/factories/message-handler-factory.ts @@ -1,10 +1,10 @@ -import { EventMessageHandler } from '../handlers/event-message-handler' -import { SubscribeMessageHandler } from '../handlers/subscribe-message-handler' -import { UnsubscribeMessageHandler } from '../handlers/unsubscribe-message-handler' import { IncomingMessage, MessageType } from '../@types/messages' +import { EventMessageHandler } from '../handlers/event-message-handler' +import { eventStrategyFactory } from './event-strategy-factory' import { IEventRepository } from '../@types/repositories' import { IWebSocketAdapter } from '../@types/adapters' -import { eventStrategyFactory } from './event-strategy-factory' +import { SubscribeMessageHandler } from '../handlers/subscribe-message-handler' +import { UnsubscribeMessageHandler } from '../handlers/unsubscribe-message-handler' export const messageHandlerFactory = ( diff --git a/src/factories/websocket-adapter-factory.ts b/src/factories/websocket-adapter-factory.ts index c1dd389..c0d5c36 100644 --- a/src/factories/websocket-adapter-factory.ts +++ b/src/factories/websocket-adapter-factory.ts @@ -1,15 +1,18 @@ +import { IncomingMessage } from 'http' import { WebSocket } from 'ws' -import { IWebSocketServerAdapter } from '../@types/adapters' + import { IEventRepository } from '../@types/repositories' -import { WebSocketAdapter } from '../adapters/web-socket-adapter' +import { IWebSocketServerAdapter } from '../@types/adapters' import { messageHandlerFactory } from './message-handler-factory' +import { WebSocketAdapter } from '../adapters/web-socket-adapter' export const webSocketAdapterFactory = ( eventRepository: IEventRepository, -) => ([client, webSocketServerAdapter]: [WebSocket, IWebSocketServerAdapter,]) => +) => ([client, request, webSocketServerAdapter]: [WebSocket, IncomingMessage, IWebSocketServerAdapter]) => new WebSocketAdapter( client, + request, webSocketServerAdapter, messageHandlerFactory(eventRepository) ) diff --git a/src/handlers/event-message-handler.ts b/src/handlers/event-message-handler.ts index afbc1a3..ab1a524 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -1,8 +1,9 @@ -import { IMessageHandler, IEventStrategy } from '../@types/message-handlers' -import { IncomingEventMessage } from '../@types/messages' +import { EventDelegatorMetadataKey, EventTags } from '../constants/base' +import { IEventStrategy, IMessageHandler } from '../@types/message-handlers' +import { isDelegatedEvent, isDelegatedEventValid, isEventIdValid, isEventSignatureValid } from '../utils/event' import { Event } from '../@types/event' import { Factory } from '../@types/base' -import { isEventIdValid, isEventSignatureValid } from '../utils/event' +import { IncomingEventMessage } from '../@types/messages' import { IWebSocketAdapter } from '../@types/adapters' export class EventMessageHandler implements IMessageHandler { @@ -19,6 +20,16 @@ export class EventMessageHandler implements IMessageHandler { return } + if (isDelegatedEvent(event)) { + if (await isDelegatedEventValid(event)) { + const [, delegator] = event.tags.find((tag) => tag.length === 4 && tag[0] === EventTags.Delegation) + event[EventDelegatorMetadataKey] = delegator + } else { + console.warn(`Delegated event ${event.id} from ${event.pubkey} is not valid`) + return + } + } + const strategy = this.strategyFactory([event, this.webSocket]) if (typeof strategy?.execute !== 'function') { diff --git a/src/handlers/event-strategies/default-event-strategy.ts b/src/handlers/event-strategies/default-event-strategy.ts index 05d54c7..d9a5db7 100644 --- a/src/handlers/event-strategies/default-event-strategy.ts +++ b/src/handlers/event-strategies/default-event-strategy.ts @@ -1,7 +1,7 @@ -import { IWebSocketAdapter } from '../../@types/adapters' import { Event } from '../../@types/event' -import { IEventStrategy } from '../../@types/message-handlers' import { IEventRepository } from '../../@types/repositories' +import { IEventStrategy } from '../../@types/message-handlers' +import { IWebSocketAdapter } from '../../@types/adapters' export class DefaultEventStrategy implements IEventStrategy> { diff --git a/src/handlers/event-strategies/delete-event-strategy.ts b/src/handlers/event-strategies/delete-event-strategy.ts index 1535770..58fab84 100644 --- a/src/handlers/event-strategies/delete-event-strategy.ts +++ b/src/handlers/event-strategies/delete-event-strategy.ts @@ -1,7 +1,7 @@ import { Event } from '../../@types/event' -import { IEventStrategy } from '../../@types/message-handlers' -import { IEventRepository } from '../../@types/repositories' import { EventTags } from '../../constants/base' +import { IEventRepository } from '../../@types/repositories' +import { IEventStrategy } from '../../@types/message-handlers' export class DeleteEventStrategy implements IEventStrategy> { diff --git a/src/handlers/event-strategies/ephemeral-event-strategy.ts b/src/handlers/event-strategies/ephemeral-event-strategy.ts index 7fffb57..80afbc7 100644 --- a/src/handlers/event-strategies/ephemeral-event-strategy.ts +++ b/src/handlers/event-strategies/ephemeral-event-strategy.ts @@ -1,6 +1,6 @@ -import { IWebSocketAdapter } from '../../@types/adapters' import { Event } from '../../@types/event' import { IEventStrategy } from '../../@types/message-handlers' +import { IWebSocketAdapter } from '../../@types/adapters' export class EphemeralEventStrategy implements IEventStrategy> { diff --git a/src/handlers/event-strategies/replaceable-event-strategy.ts b/src/handlers/event-strategies/replaceable-event-strategy.ts index 6b8118f..3ceb33d 100644 --- a/src/handlers/event-strategies/replaceable-event-strategy.ts +++ b/src/handlers/event-strategies/replaceable-event-strategy.ts @@ -1,6 +1,6 @@ import { Event } from '../../@types/event' -import { IEventStrategy } from '../../@types/message-handlers' import { IEventRepository } from '../../@types/repositories' +import { IEventStrategy } from '../../@types/message-handlers' import { IWebSocketAdapter } from '../../@types/adapters' diff --git a/src/handlers/subscribe-message-handler.ts b/src/handlers/subscribe-message-handler.ts index 88c7421..a897767 100644 --- a/src/handlers/subscribe-message-handler.ts +++ b/src/handlers/subscribe-message-handler.ts @@ -1,15 +1,15 @@ +import { anyPass, map } from 'ramda' import { pipeline } from 'stream/promises' -import { createOutgoingEventMessage, createEndOfStoredEventsNoticeMessage } from '../messages' +import { createEndOfStoredEventsNoticeMessage, createOutgoingEventMessage } from '../utils/messages' import { IAbortable, IMessageHandler } from '../@types/message-handlers' -import { SubscribeMessage } from '../@types/messages' -import { IWebSocketAdapter } from '../@types/adapters' -import { IEventRepository } from '../@types/repositories' -import { SubscriptionId, SubscriptionFilter } from '../@types/subscription' import { isEventMatchingFilter, toNostrEvent } from '../utils/event' import { streamEach, streamEnd, streamFilter, streamMap } from '../utils/stream' +import { SubscriptionFilter, SubscriptionId } from '../@types/subscription' import { Event } from '../@types/event' -import { anyPass, map } from 'ramda' +import { IEventRepository } from '../@types/repositories' +import { IWebSocketAdapter } from '../@types/adapters' +import { SubscribeMessage } from '../@types/messages' export class SubscribeMessageHandler implements IMessageHandler, IAbortable { diff --git a/src/index.ts b/src/index.ts index 491e7af..67701ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ import * as http from 'http' import { WebSocketServer } from 'ws' -import { getDbClient } from './database/client' import { EventRepository } from './repositories/event-repository' -import { WebSocketServerAdapter } from './adapters/web-socket-server-adapter' +import { getDbClient } from './database/client' import { webSocketAdapterFactory } from './factories/websocket-adapter-factory' +import { WebSocketServerAdapter } from './adapters/web-socket-server-adapter' const server = http.createServer() const wss = new WebSocketServer({ server, maxPayload: 1024 * 1024 }) @@ -23,8 +23,12 @@ adapter.listen(port) process.on('SIGINT', async function () { console.log('\rCaught interrupt signal') wss.clients.forEach((client) => client.terminate()) - await new Promise((resolve, reject) => wss.close((error?: Error) => void (error instanceof Error) ? reject(error) : resolve(undefined))) - await new Promise((resolve, reject) => server.close((error?: Error) => void (error instanceof Error) ? reject(error) : resolve(undefined))) + await new Promise((resolve, reject) => + wss.close((error?: Error) => void (error instanceof Error) ? reject(error) : resolve(undefined)) + ) + await new Promise((resolve, reject) => + server.close((error?: Error) => void (error instanceof Error) ? reject(error) : resolve(undefined)) + ) dbClient.destroy() process.exit() }) diff --git a/src/repositories/event-repository.ts b/src/repositories/event-repository.ts index 465b1f8..8353ef3 100644 --- a/src/repositories/event-repository.ts +++ b/src/repositories/event-repository.ts @@ -1,12 +1,39 @@ +import { + __, + always, + applySpec, + complement, + cond, + equals, + evolve, + filter, + forEach, + forEachObjIndexed, + groupBy, + identity, + ifElse, + invoker, + is, + isEmpty, + isNil, + modulo, + nth, + omit, + pipe, + prop, + propSatisfies, + T, + toPairs, +} from 'ramda' import { Knex } from 'knex' -import { __, applySpec, equals, modulo, omit, pipe, prop, cond, always, groupBy, T, evolve, forEach, isEmpty, forEachObjIndexed, isNil, complement, toPairs, filter, nth, ifElse, invoker, identity } from 'ramda' -import { EventId } from '../@types/base' import { DBEvent, Event } from '../@types/event' import { IEventRepository, IQueryResult } from '../@types/repositories' -import { SubscriptionFilter } from '../@types/subscription' -import { isGenericTagQuery } from '../utils/filter' import { toBuffer, toJSON } from '../utils/transform' +import { EventDelegatorMetadataKey } from '../constants/base' +import { EventId } from '../@types/base' +import { isGenericTagQuery } from '../utils/filter' +import { SubscriptionFilter } from '../@types/subscription' const even = pipe(modulo(__, 2), equals(0)) @@ -31,7 +58,7 @@ export class EventRepository implements IEventRepository { const queries = filters.map((currentFilter) => { const builder = this.dbClient('events') - forEachObjIndexed((tableField: string, filterName: string) => { + forEachObjIndexed((tableFields: string[], filterName: string) => { builder.andWhere((bd) => { cond([ [isEmpty, () => void bd.whereRaw('1 = 0')], @@ -40,27 +67,38 @@ export class EventRepository implements IEventRepository { pipe( groupByLengthSpec, evolve({ - exact: (pubkeys: string[]) => void bd.whereIn(tableField, pubkeys.map(toBuffer)), - even: forEach((prefix: string) => void bd.orWhereRaw( - `substring("${tableField}" from 1 for ?) = ?`, - [prefix.length >> 1, toBuffer(prefix)] - )), - odd: forEach((prefix: string) => void bd.orWhereRaw( - `substring("${tableField}" from 1 for ?) BETWEEN ? AND ?`, - [ - (prefix.length >> 1) + 1, - `\\x${prefix}0`, - `\\x${prefix}f` - ], - )), + exact: (pubkeys: string[]) => + tableFields.forEach((tableField) => + void bd.orWhereIn(tableField, pubkeys.map(toBuffer)) + ), + even: forEach((prefix: string) => + tableFields.forEach((tableField) => + void bd.orWhereRaw( + `substring("${tableField}" from 1 for ?) = ?`, + [prefix.length >> 1, toBuffer(prefix)] + ) + ) + ), + odd: forEach((prefix: string) => + tableFields.forEach((tableField) => + void bd.orWhereRaw( + `substring("${tableField}" from 1 for ?) BETWEEN ? AND ?`, + [ + (prefix.length >> 1) + 1, + `\\x${prefix}0`, + `\\x${prefix}f`, + ], + ) + ) + ), }), ), ], ])(currentFilter[filterName] as string[]) }) })({ - authors: 'event_pubkey', - ids: 'event_id', + authors: ['event_pubkey', 'event_delegator'], + ids: ['event_id'], }) if (Array.isArray(currentFilter.kinds)) { @@ -95,7 +133,7 @@ export class EventRepository implements IEventRepository { forEach((criterion: string[]) => void orWhereRaw( '"event_tags" @> ?', [ - JSON.stringify([[filterName[1], criterion]]) as any + JSON.stringify([[filterName[1], criterion]]) as any, ], bd, )), @@ -111,6 +149,7 @@ export class EventRepository implements IEventRepository { if (subqueries.length) { query.union(subqueries, true) } + console.log(query.toString()) return query } @@ -128,6 +167,11 @@ export class EventRepository implements IEventRepository { event_tags: pipe(prop('tags'), toJSON), event_content: prop('content'), event_signature: pipe(prop('sig'), toBuffer), + event_delegator: ifElse( + propSatisfies(is(String), EventDelegatorMetadataKey), + pipe(prop(EventDelegatorMetadataKey as any), toBuffer), + always(null), + ), })(event) return this.dbClient('events') @@ -148,6 +192,11 @@ export class EventRepository implements IEventRepository { event_tags: pipe(prop('tags'), toJSON), event_content: prop('content'), event_signature: pipe(prop('sig'), toBuffer), + event_delegator: ifElse( + propSatisfies(is(String), EventDelegatorMetadataKey), + pipe(prop(EventDelegatorMetadataKey as any), toBuffer), + always(null), + ), })(event) return this.dbClient('events') diff --git a/src/schemas/event-schema.ts b/src/schemas/event-schema.ts index f849eb6..5dc4621 100644 --- a/src/schemas/event-schema.ts +++ b/src/schemas/event-schema.ts @@ -1,4 +1,5 @@ import Schema from 'joi' + import { idSchema, kindSchema, diff --git a/src/schemas/message-schema.ts b/src/schemas/message-schema.ts index 25b3803..4dfe1ce 100644 --- a/src/schemas/message-schema.ts +++ b/src/schemas/message-schema.ts @@ -1,9 +1,9 @@ -import Joi from 'joi' import Schema from 'joi' -import { MessageType } from '../@types/messages' -import { subscriptionSchema } from './base-schema' + import { eventSchema } from './event-schema' import { filterSchema } from './filter-schema' +import { MessageType } from '../@types/messages' +import { subscriptionSchema } from './base-schema' export const eventMessageSchema = Schema.array().ordered( Schema.string().valid('EVENT').required(), @@ -22,10 +22,19 @@ export const closeMessageSchema = Schema.array().ordered( ).label('CLOSE message') export const messageSchema = Schema.alternatives() - .conditional(Joi.ref('.'), { + .conditional(Schema.ref('.'), { switch: [ - { is: Joi.array().ordered(Joi.string().equal(MessageType.EVENT)).items(Joi.any()), then: eventMessageSchema }, - { is: Joi.array().ordered(Joi.string().equal(MessageType.REQ)).items(Joi.any()), then: reqMessageSchema }, - { is: Joi.array().ordered(Joi.string().equal(MessageType.CLOSE)).items(Joi.any()), then: closeMessageSchema }, + { + is: Schema.array().ordered(Schema.string().equal(MessageType.EVENT)).items(Schema.any()), + then: eventMessageSchema, + }, + { + is: Schema.array().ordered(Schema.string().equal(MessageType.REQ)).items(Schema.any()), + then: reqMessageSchema, + }, + { + is: Schema.array().ordered(Schema.string().equal(MessageType.CLOSE)).items(Schema.any()), + then: closeMessageSchema, + }, ], }) diff --git a/src/utils/event.ts b/src/utils/event.ts index 8f7a639..8ec60f3 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -1,11 +1,12 @@ import * as secp256k1 from '@noble/secp256k1' -import { applySpec, pipe, prop } from 'ramda' +import { applySpec, converge, curry, mergeLeft, nth, omit, pipe, prop, reduceBy } from 'ramda' import { CanonicalEvent, Event } from '../@types/event' -import { SubscriptionFilter } from '../@types/subscription' import { EventKinds, EventTags } from '../constants/base' -import { isGenericTagQuery } from './filter' import { fromBuffer } from './transform' +import { isGenericTagQuery } from './filter' +import { Rune } from './runes' +import { SubscriptionFilter } from '../@types/subscription' export const serializeEvent = (event: Partial): CanonicalEvent => [ 0, @@ -41,13 +42,6 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Eve return false } - if ( - Array.isArray(filter.authors) && - !filter.authors.some(startsWith(event.pubkey)) - ) { - return false - } - if (typeof filter.since === 'number' && event.created_at < filter.since) { return false } @@ -56,18 +50,34 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Eve return false } - // NIP-27: Multicast - const targetMulticastGroups: string[] = event.tags.reduce( - (acc, tag) => (tag[0] === EventTags.Multicast) - ? [...acc, tag[1]] - : acc, - [] as string[] - ) + if (Array.isArray(filter.authors)) { + if ( + !filter.authors.some(startsWith(event.pubkey)) + ) { + if (isDelegatedEvent(event)) { + const delegation = event.tags.find((tag) => tag[0] === EventTags.Delegation) - if (targetMulticastGroups.length && !Array.isArray(filter['#m'])) { - return false + if (!filter.authors.some(startsWith(delegation[1]))) { + return false + } + } else { + return false + } + } } + // NIP-27: Multicast + // const targetMulticastGroups: string[] = event.tags.reduce( + // (acc, tag) => (tag[0] === EventTags.Multicast) + // ? [...acc, tag[1]] + // : acc, + // [] as string[] + // ) + + // if (targetMulticastGroups.length && !Array.isArray(filter['#m'])) { + // return false + // } + // NIP-01: Support #e and #p tags // NIP-12: Support generic tag queries @@ -89,6 +99,61 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Eve return true } +export const isDelegatedEvent = (event: Event): boolean => { + return event.tags.some((tag) => tag.length === 4 && tag[0] === EventTags.Delegation) +} + +export const isDelegatedEventValid = async (event: Event): Promise => { + const delegation = event.tags.find((tag) => tag.length === 4 && tag[0] === EventTags.Delegation) + if (!delegation) { + return false + } + + const serializedDelegationTag = `nostr:${delegation[0]}:${event.pubkey}:${delegation[2]}` + + const token = await secp256k1.utils.sha256(Buffer.from(serializedDelegationTag)) + + // Token generation to be decided: + // const serializedDelegationTag = [ + // delegation[0], // 'delegation' + // delegation[1], // + // event.pubkey, // + // delegation[2], // + // ] + // const token = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializedDelegationTag))) + + // Validate delegation signature + const verification = await secp256k1.schnorr.verify(delegation[3], token, delegation[1]) + if (!verification) { + return false + } + + // Validate rune + const runifiedEvent = (converge( + curry(mergeLeft), + [ + omit(['tags']), + pipe( + prop('tags') as any, + reduceBy( + (acc, tag) => ([...acc, tag[1]]), + [], + nth(0), + ), + ), + ], + ) as any)(event) + + try { + const [result] = Rune.from(delegation[2]).test(runifiedEvent) + + return result + } catch (error) { + console.error('Invalid rune') + return false + } +} + export const isEventIdValid = async (event: Event): Promise => { const id = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializeEvent(event)))) diff --git a/src/messages.ts b/src/utils/messages.ts similarity index 80% rename from src/messages.ts rename to src/utils/messages.ts index 4df944c..3d5db6e 100644 --- a/src/messages.ts +++ b/src/utils/messages.ts @@ -1,11 +1,11 @@ -import { Event } from './@types/event' -import { SubscriptionId } from './@types/subscription' import { EndOfStoredEventsNotice, MessageType, Notice, OutgoingMessage, -} from './@types/messages' +} from '../@types/messages' +import { Event } from '../@types/event' +import { SubscriptionId } from '../@types/subscription' export const createNotice = (notice: string): Notice => { return [MessageType.NOTICE, notice] diff --git a/src/utils/runes.ts b/src/utils/runes.ts new file mode 100644 index 0000000..b4ef37a --- /dev/null +++ b/src/utils/runes.ts @@ -0,0 +1,242 @@ + +const punctuations = /[!"#\$%&'()*+-.\/:;<=>?@\[\\\]^`{|}~]/ + +const hasPunctuation = (input) => punctuations.test(input) + +// Reference: https://github.com/rustyrussell/runes/blob/master/runes/runes.py + +export class Alternative { + public constructor( + private readonly field: string, + private readonly cond: string, + private readonly value: string, + ) { + if (Array.from(this.field).some(hasPunctuation)) { + throw Error('Field is not valid') + } + + if (!new Set(['!', '=', '/', '^', '$', '~', '<', '>', '}', '{', '#']).has(this.cond)) { + throw new Error('Cond not valid') + } + } + + public test(values: Record): string | undefined { + if (this.cond === '#') { + return + } + + const why = (cond: boolean, field: string, explanation: string): string | undefined => + (cond) ? undefined : `${field}: ${explanation}` + + if (!(this.field in values)) { + return why(this.cond === '!', this.field, 'is missing') + } + + if (typeof values[this.field] === 'function') { + return values[this.field](this) + } + + const val = String(values[this.field]) + + switch (this.cond) { + case '!': + return why(false, this.field, 'is present') + case '=': + return why(val === this.value, this.field, `!= ${this.value}`) + case '/': + return why(val !== this.value, this.field, `= ${this.value}`) + case '^': + return why(val.startsWith(this.value), this.field, `does not start with ${this.value}`) + case '$': + return why(val.endsWith(this.value), this.field, `does not end with ${this.value}`) + case '~': + return why(values[this.field].includes(this.value), this.field, `does not contain ${this.value}`) + case '<': + case '>': + const actualInt = Number.parseInt(val) + if (Number.isNaN(actualInt)) { + return why(false, this.field, 'not an integer field') + } + const restrictionVal = Number.parseInt(this.value) + if (Number.isNaN(restrictionVal)) { + return why(false, this.field, 'not a valid integer') + } + + if (this.cond === '<') { + return why(actualInt < restrictionVal, this.field, `>= ${restrictionVal}`) + } else { + return why(actualInt > restrictionVal, this.field, `<= ${restrictionVal}`) + } + case '{': + return why(val < this.value, this.field, `is the same or ordered after ${this.value}`) + case '{': + return why(val > this.value, this.field, `is the same or ordered before ${this.value}`) + default: + throw new Error('Invalid condition') + } + } + + public encode(): string { + return `${this.field}${this.cond}${this.value.replace(/[\\|&]/g, '\\$&')}` + } + + public valueOf(): string { + return this.encode() + } + + public toString() { + return this.encode() + } + + public static decode(encodedStr: string): [Alternative, string] { + let cond = undefined + let endOff = 0 + + while (endOff < encodedStr.length) { + if (hasPunctuation(encodedStr[endOff])) { + cond = encodedStr[endOff] + break + } + endOff++ + } + + if (typeof cond === 'undefined') { + throw new Error(`${encodedStr} does not contain any operator`) + } + + const field = encodedStr.slice(0, endOff++) + + let value = '' + + while (endOff < encodedStr.length) { + if (encodedStr[endOff] === '|') { + endOff++ + break + } + + if (encodedStr[endOff] === '&') { + break + } + + if (encodedStr[endOff] === '\\') { + endOff++ + } + + value += encodedStr[endOff++] + } + + return [new Alternative(field, cond, value), encodedStr.slice(endOff)] + } + + public static from(encodedStr: string): Alternative { + const [field, cond, value] = encodedStr.replace(/\s+/g, '').split(new RegExp(`(${punctuations.source})`, 'g')) + + return new Alternative(field, cond, value) + } + +} + +export class Restriction { + public constructor( + private readonly alternatives: Alternative[] + ) { + if (!alternatives.length) { + throw new Error('Restriction must have some alternatives') + } + } + + public test(values: Record): string | undefined { + const reasons: string[] = [] + for (const alternative of this.alternatives) { + const reason = alternative.test(values) + if (typeof reason === 'undefined') { + return + } + reasons.push(reason) + } + + return reasons.join(' AND ') + } + + public encode(): string { + return this.alternatives.map((alternative) => alternative.encode()).join('|') + } + + public valueOf(): string { + return this.encode() + } + + public toString() { + return this.encode() + } + + public static decode(encodedStr: string): [Restriction, string] { + let encStr = encodedStr + let alternative: Alternative + const alternatives: Alternative[] = [] + while (encStr.length) { + if (encStr.startsWith('&')) { + encStr = encStr.slice(1) + break + } + + [alternative, encStr] = Alternative.decode(encStr) + + alternatives.push(alternative) + } + + return [new Restriction(alternatives), encStr] + } + + public static from(encodedStr: string): Restriction { + const [restriction, remainder] = Restriction.decode(encodedStr.replace(/\s+/g, '')) + + if (remainder.length) { + throw new Error(`Restriction had extra characters at end: ${remainder}`) + } + + return restriction + } +} + +export class Rune { + public constructor( + private readonly restrictions: Restriction[] = [] + ) { } + + public test(values: Record): [boolean, string] { + for (const restriction of this.restrictions) { + const reasons = restriction.test(values) + if (typeof reasons !== 'undefined') { + return [false, reasons] + } + } + + return [true, ''] + } + + public encode() { + return this.restrictions.map((restriction) => restriction.encode()).join('&') + } + + public valueOf() { + return this.encode() + } + + public toString() { + return this.encode() + } + + public static from(encodedStr: string): Rune { + const restrictions: Restriction[] = [] + let restriction: Restriction + let encStr = encodedStr.replace(/\s+/g, '') + + while (encStr.length) { + [restriction, encStr] = Restriction.decode(encStr) + restrictions.push(restriction) + } + + return new Rune(restrictions) + } +} diff --git a/src/settings.ts b/src/utils/settings.ts similarity index 93% rename from src/settings.ts rename to src/utils/settings.ts index c24da90..d292b38 100644 --- a/src/settings.ts +++ b/src/utils/settings.ts @@ -1,10 +1,10 @@ -import { readFileSync } from 'fs' import { homedir } from 'os' import { join } from 'path' import { mergeDeepRight } from 'ramda' +import { readFileSync } from 'fs' -import packageJson from '../package.json' -import { ISettings } from './@types/settings' +import { ISettings } from '../@types/settings' +import packageJson from '../../package.json' let _settings: ISettings diff --git a/src/utils/stream.ts b/src/utils/stream.ts index 5dc8933..237577d 100644 --- a/src/utils/stream.ts +++ b/src/utils/stream.ts @@ -4,7 +4,7 @@ export const streamMap = (fn: (chunk) => any) => new Transform({ objectMode: true, transform(chunk, _encoding, callback) { callback(null, fn(chunk)) - } + }, }) export const streamEach = (writeFn: (chunk: any) => void) => new PassThrough({ diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 4f27258..df5a440 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -8,4 +8,5 @@ const getValidationConfig = () => ({ export const validateSchema = (schema: Joi.Schema) => (input: any) => schema.validate(input, getValidationConfig()) -export const attemptValidation = (schema: Joi.Schema) => (input: any) => Joi.attempt(input, schema, getValidationConfig()) +export const attemptValidation = (schema: Joi.Schema) => + (input: any) => Joi.attempt(input, schema, getValidationConfig()) diff --git a/test/unit/data/events.ts b/test/unit/data/events.ts index 957d3c2..f3a02f7 100644 --- a/test/unit/data/events.ts +++ b/test/unit/data/events.ts @@ -8,7 +8,7 @@ export const getEvents = (): Event[] => [ 'kind': 0, 'tags': [], 'content': '{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}', - 'sig': 'd1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9' + 'sig': 'd1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9', }, { 'id': 'cf8de9db67a1d7203512d1d81e6190f5e53abfdc0ac90275f67172b65a5b09a0', @@ -17,7 +17,7 @@ export const getEvents = (): Event[] => [ 'kind': 1, 'tags': [['r', 'https://fiatjaf.com']], 'content': 'r', - 'sig': '53d12018d036092794366283eca36df4e0cabd014b6e91bbf684c8bb9bbbe9dedafa77b6b928587e11e05e036227598dded8713e8da17d55076e12242b361542' + 'sig': '53d12018d036092794366283eca36df4e0cabd014b6e91bbf684c8bb9bbbe9dedafa77b6b928587e11e05e036227598dded8713e8da17d55076e12242b361542', }, { 'id': '444b1e4cf4eea42d35c7f1be58ab9cf6a942153593251d66e0471084a3430dae', @@ -26,7 +26,7 @@ export const getEvents = (): Event[] => [ 'kind': 2, 'tags': [], 'content': 'wss://relay.damus.io', - 'sig': '1d8625765364edffa42f83fa1e53bf3486e7fb94eec065dd0a00b48dd777702fafbfa1063ef27f1dd27b3892132e4d1703fb0da2bfb98b70045f826ee76d5526' + 'sig': '1d8625765364edffa42f83fa1e53bf3486e7fb94eec065dd0a00b48dd777702fafbfa1063ef27f1dd27b3892132e4d1703fb0da2bfb98b70045f826ee76d5526', }, { 'id': '0d684e8ec2431de586aa3cafbee2f6d308d19b28805e53deabcac3220e9136a5', @@ -37,766 +37,766 @@ export const getEvents = (): Event[] => [ [ 'p', 'b34417513f66497d7b0e1a8406b6689ac32afb184027717e57d281ea19186315', - 'wss://nostr-relay.untethr.me' + 'wss://nostr-relay.untethr.me', ], [ 'p', '2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5', - 'wss://nostr.rocks' + 'wss://nostr.rocks', ], [ 'p', '13e7f234ef71ffd63fdf3fec4eaec6fdea9bb850a37ba1a854a62b934c97855e', - 'wss://nostr-relay.untethr.me' + 'wss://nostr-relay.untethr.me', ], [ 'p', '40e162e0a8d139c9ef1d1bcba5265d1953be1381fb4acd227d8f3c391f9b9486', - 'wss://nostr-relay.untethr.me' + 'wss://nostr-relay.untethr.me', ], [ 'p', '42a0825e980b9f97943d2501d99c3a3859d4e68cd6028c02afe58f96ba661a9d', - 'wss://nostr-relay.untethr.me' + 'wss://nostr-relay.untethr.me', ], [ 'p', '3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', 'ed04f9c719af697ac1c045bfff5f841cdf61a0b0d2170c9970f0ce0a04f708bf', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', '76f5960d381e7146b7f374a4a65afa403038441b46933840c71e436facb82ae7', - 'wss://nostr.bitcoiner.social' + 'wss://nostr.bitcoiner.social', ], [ 'p', 'c697f7f5f59de8ddb93c6b74fdd759ab2dc654bc36315f39770c214607fcd65e', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', 'd3fe840f672c191849f8500762d81af8a258e673b7ff07cf9ce1211c2d0f493d', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', '14347702b99786cc0ee644620a5f71bc6a88e2882491f57c372f1deaed198701', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', 'cbc5ef6b01cbd1ffa2cb95a954f04c385a936c1a86e1bb9ccdf2cf0f4ebeaccb', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', '8ff7a6132ffe1bb3600aa20496ab648f1daf6b50ceaa8054a37e6a0b1f7ee491', - 'wss://nostr.bitcoiner.social' + 'wss://nostr.bitcoiner.social', ], [ 'p', '1f7dfb1b51bd4fb5d15245b28d86fab670a677580e2a0633a2cf76509d02471c', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', 'f5424d002fd0d48fadd6e54879387714c54bfa46535976ff2b385843aaddf8e5', - 'wss://nostr.rocks' + 'wss://nostr.rocks', ], [ 'p', '9aeb3bb495f09be3799048c3ef76649917efc46a8c8a69fefc31a7d012f6eccb', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', 'c181af1aca3a13243a9ef9c302d5e988eaec25caa60c9923e5faed097e52cd69', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', 'a4cb51f4618cfcd16b2d3171c466179bed8e197c43b8598823b04de266cef110', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', '1221fd0054a6c8ebd07b39c5eeea388f7f0244409f8cd8649ac22fcd668d02f6', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', 'f61abb9886e1f4cd5d20419c197d5d7f3649addab24b6a32a2367124ca3194b4', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', 'b175db709771d32bbe7d8599e0c41f3f8768cc3a8333603d93c6d72d41c42f76', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', '57225e0adcbad1fddf8d9ba1f5f36d657f134b7e0ea7aed6c0eb7013e4ef45f1', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', '6446d04ecf9e0bb72c5ae218df9fc6c0a273149d9ecbfbe42519c53667b4405a', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', 'e0d05a5b8c7789eb83f87672f4eb0dca78f99292ab038e5c66f84d97d77b95ae', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', '46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', - '4d5ce768123563bc583697db5e84841fb528f7b708d966f2e546286ce3c72077' + '4d5ce768123563bc583697db5e84841fb528f7b708d966f2e546286ce3c72077', ], [ 'p', '8d233d8babe9f40f170c5b0706fd4832869e07d040cfcd6b702d57e070aad1cb', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', '566516663d91d4fef824eaeccbf9c2631a8d8a2efee8048ca5ee6095e6e5c843', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', '0000a0fa65fcccd99e6fd32fc7870339af40f4a94703ea30999fc5c091daa222', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', - 'd987084c48390a290f5d2a34603ae64f55137d9b4affced8c0eae030eb222a25' + 'd987084c48390a290f5d2a34603ae64f55137d9b4affced8c0eae030eb222a25', ], [ 'p', '3878d95db7b854c3a0d3b2d6b7bf9bf28b36162be64326f5521ba71cf3b45a69', - 'wss://nostr.rocks' + 'wss://nostr.rocks', ], [ 'p', - '7f0be893dc501f391260aa2088de28b35280dfd4ae8f8bfa9bdbb7319952755b' + '7f0be893dc501f391260aa2088de28b35280dfd4ae8f8bfa9bdbb7319952755b', ], [ 'p', '44c39a01cbdeb70905aaa9cbd614a1ef39d0f4386d0dee9d7493e6e680548eb9', - 'wss://nostr-relay.untethr.me' + 'wss://nostr-relay.untethr.me', ], [ 'p', - '484712e818a8373182c64e53c0d1fb9cec5de96daa2d39424b42d7b0dcd8e6c9' + '484712e818a8373182c64e53c0d1fb9cec5de96daa2d39424b42d7b0dcd8e6c9', ], [ 'p', 'b2222fc7844fef7b002440b3216213d9b01dcf5e412a604ddfa50967db4d8bd6', - 'wss://nostr-relay.untethr.me' + 'wss://nostr-relay.untethr.me', ], [ 'p', - '78aa0c9a0fe2d2476469db25f19a293a6606c113fe2e87e17b8ab51cb120dbb7' + '78aa0c9a0fe2d2476469db25f19a293a6606c113fe2e87e17b8ab51cb120dbb7', ], [ 'p', '9ec7a778167afb1d30c4833de9322da0c08ba71a69e1911d5578d3144bb56437', - 'wss://nostr.rocks' + 'wss://nostr.rocks', ], [ 'p', - '57f03c1604d109be088dbac71371b6939833dd24fdcf2886d3382a0479c0d4de' + '57f03c1604d109be088dbac71371b6939833dd24fdcf2886d3382a0479c0d4de', ], [ 'p', - '778fdd199044a2e8dc3cfac3c274f5577ed78c22fb3b5ccb13df6956980eff4c' + '778fdd199044a2e8dc3cfac3c274f5577ed78c22fb3b5ccb13df6956980eff4c', ], [ 'p', 'e76e705283775febf3d5f4f97662648582d42ff822435924f21a47c8d46c5921', - 'wss://nostr-relay.untethr.me' + 'wss://nostr-relay.untethr.me', ], [ 'p', 'e794d71b8f7426a291004f592b758438a25d0012e5bb969e53307b3785fd5211', - 'wss://nostr-pub.wellorder.net' + 'wss://nostr-pub.wellorder.net', ], [ 'p', '88a2c3b420b4a027706a98600d1fd744ac6cfd12e201b74189be5ef4b2b3aa45', - 'wss://nostr-relay.untethr.me' + 'wss://nostr-relay.untethr.me', ], [ 'p', - '004db7605cfeba09b15625deb77c9369029f370591d68231b7c4dfd43f8f6f4f' + '004db7605cfeba09b15625deb77c9369029f370591d68231b7c4dfd43f8f6f4f', ], [ 'p', 'b238e136091cb01cd21606dac1a2f503f504e7e8e7c75d98fcefd30aed084a1c', - 'wss://nostr-relay.untethr.me' + 'wss://nostr-relay.untethr.me', ], [ 'p', - '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245' + '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', ], [ 'p', - 'b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a' + 'b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a', ], [ 'p', - 'dd81a8bacbab0b5c3007d1672fb8301383b4e9583d431835985057223eb298a5' + 'dd81a8bacbab0b5c3007d1672fb8301383b4e9583d431835985057223eb298a5', ], [ 'p', - 'ed1d0e1f743a7d19aa2dfb0162df73bacdbc699f67cc55bb91a98c35f7deac69' + 'ed1d0e1f743a7d19aa2dfb0162df73bacdbc699f67cc55bb91a98c35f7deac69', ], [ 'p', - 'b2d1d0fc5b771a7041054ebded57bc3bf20f69ccbb9dc9b8ef432801d247df7c' + 'b2d1d0fc5b771a7041054ebded57bc3bf20f69ccbb9dc9b8ef432801d247df7c', ], [ 'p', - 'd947d8f1be338c5cff194a6630453fa43c924eb9f58c339c68b26b2193efa276' + 'd947d8f1be338c5cff194a6630453fa43c924eb9f58c339c68b26b2193efa276', ], [ 'p', - '6112a73a50518ed631dc6804a238525acdf10f26343199bc25ed7c9f5a0685c5' + '6112a73a50518ed631dc6804a238525acdf10f26343199bc25ed7c9f5a0685c5', ], [ 'p', - '22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793' + '22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793', ], [ 'p', - '1bbb8324577ac089607e45813bac499ebdab4621d029f8c02b2c82b4410fd3f4' + '1bbb8324577ac089607e45813bac499ebdab4621d029f8c02b2c82b4410fd3f4', ], [ 'p', - 'e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216' + 'e668a111aa647e63ef587c17fb0e2513d5c2859cd8d389563c7640ffea1fc216', ], [ 'p', - '2508ed2c2ab3f6728a880fafbc0895a2afeacbb74eb69847255fb60564af0d85' + '2508ed2c2ab3f6728a880fafbc0895a2afeacbb74eb69847255fb60564af0d85', ], [ 'p', - 'c2bb5d6529095edbfbdbe3f136175c146c6706526325b32da881c7c34c7b1ab8' + 'c2bb5d6529095edbfbdbe3f136175c146c6706526325b32da881c7c34c7b1ab8', ], [ 'p', - '8f87ac34eb27a86fc917866fbc9016429bd89cf1d0d27a038a8eaac4c62c63e5' + '8f87ac34eb27a86fc917866fbc9016429bd89cf1d0d27a038a8eaac4c62c63e5', ], [ 'p', - '52cb4b34775fa781b6a964bda0432dbcdfede7a59bf8dfc279cbff0ad8fb09ff' + '52cb4b34775fa781b6a964bda0432dbcdfede7a59bf8dfc279cbff0ad8fb09ff', ], [ 'p', - '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d' + '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d', ], [ 'p', - '7e88f589d2677ea4a863c72af5d0e85fbe1d3db111667c50d33fa42196a1afc0' + '7e88f589d2677ea4a863c72af5d0e85fbe1d3db111667c50d33fa42196a1afc0', ], [ 'p', - 'f0bed2e11260f0f77f781db928f40a34c18713fda1918d3be996f91d0776e985' + 'f0bed2e11260f0f77f781db928f40a34c18713fda1918d3be996f91d0776e985', ], [ 'p', - '565152b2d1793a253cba282588a4b287b0ab2acbe7faa7021ea0dced39d33716' + '565152b2d1793a253cba282588a4b287b0ab2acbe7faa7021ea0dced39d33716', ], [ 'p', - 'd9c8c00017a2a345c2f32132436a26e1c72cb7a57e7b6b316f62dee2f8bcf8dd' + 'd9c8c00017a2a345c2f32132436a26e1c72cb7a57e7b6b316f62dee2f8bcf8dd', ], [ 'p', - 'b28a0714f86fd344a7ecad9566c2e33f8485ef560a702e15c3f537914abc152d' + 'b28a0714f86fd344a7ecad9566c2e33f8485ef560a702e15c3f537914abc152d', ], [ 'p', - '7e7272c475d920ad408e7a6faf9a123aa7b882cba7151e6105a0fc9d212fb240' + '7e7272c475d920ad408e7a6faf9a123aa7b882cba7151e6105a0fc9d212fb240', ], [ 'p', - 'ea42658e9a1291a32d1b74793edaef3d8757589a32b16931cacd85ba5470ea7c' + 'ea42658e9a1291a32d1b74793edaef3d8757589a32b16931cacd85ba5470ea7c', ], [ 'p', - 'aff9a9f017f32b2e8b60754a4102db9d9cf9ff2b967804b50e070780aa45c9a8' + 'aff9a9f017f32b2e8b60754a4102db9d9cf9ff2b967804b50e070780aa45c9a8', ], [ 'p', - 'b74848fa6f8975f00b04ce12ccbe18673ad1f4511f66d4e5a3a151720fdce62a' + 'b74848fa6f8975f00b04ce12ccbe18673ad1f4511f66d4e5a3a151720fdce62a', ], [ 'p', - '7e3b8e221023e92c297cb35937d88e495de780ac3190c23e1e2e1e6274f43f59' + '7e3b8e221023e92c297cb35937d88e495de780ac3190c23e1e2e1e6274f43f59', ], [ 'p', - '547fcc5c7e655fe7c83da5a812e6332f0a4779c87bf540d8e75a4edbbf36fe4a' + '547fcc5c7e655fe7c83da5a812e6332f0a4779c87bf540d8e75a4edbbf36fe4a', ], [ 'p', - 'a12535e8bf4f712211b68f7fe7303d03c3c5cfe8155116d553fe6b8adba85d41' + 'a12535e8bf4f712211b68f7fe7303d03c3c5cfe8155116d553fe6b8adba85d41', ], [ 'p', - '772405d14585d9d8fe481cef6ce560b83f03c24f0efc179415530d54eee97534' + '772405d14585d9d8fe481cef6ce560b83f03c24f0efc179415530d54eee97534', ], [ 'p', - '2163edbd81fa58e64c7e38bf968dda1b2f42811b78ea06accd32007bbb8a018b' + '2163edbd81fa58e64c7e38bf968dda1b2f42811b78ea06accd32007bbb8a018b', ], [ 'p', - 'e37d948a0eee45e6cd113faaad934fcf17a97de2236c655b70650d4252daa9d3' + 'e37d948a0eee45e6cd113faaad934fcf17a97de2236c655b70650d4252daa9d3', ], [ 'p', - 'e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2' + 'e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2', ], [ 'p', - '4557aab9aae76a892e01568064a9e262e613690421a79e584b8cc4c5ca9afb7e' + '4557aab9aae76a892e01568064a9e262e613690421a79e584b8cc4c5ca9afb7e', ], [ 'p', - '9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31' + '9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31', ], [ 'p', - '1265c1c3d41f0f05bf306224ec40628231a5086a2eaa36643b3982a4eba19c9f' + '1265c1c3d41f0f05bf306224ec40628231a5086a2eaa36643b3982a4eba19c9f', ], [ 'p', - '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9' + '04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9', ], [ 'p', - 'd3646691ba5b1d796c1e1b3430df00fe1189ec9c232877adde18c8f656af18f0' + 'd3646691ba5b1d796c1e1b3430df00fe1189ec9c232877adde18c8f656af18f0', ], [ 'p', - 'b7c66ce6f7bbe034e96be54c2ffc0adf631a889abc0834ba1431171b67c489aa' + 'b7c66ce6f7bbe034e96be54c2ffc0adf631a889abc0834ba1431171b67c489aa', ], [ 'p', - '8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9' + '8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9', ], [ 'p', - '06fca9f06f74cf86a16fe4c2feec508700643e2b105b519fd93d35332c51ad53' + '06fca9f06f74cf86a16fe4c2feec508700643e2b105b519fd93d35332c51ad53', ], [ 'p', - '6b0d4c8d9dc59e110d380b0429a02891f1341a0fa2ba1b1cf83a3db4d47e3964' + '6b0d4c8d9dc59e110d380b0429a02891f1341a0fa2ba1b1cf83a3db4d47e3964', ], [ 'p', - '35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f' + '35d26e4690cbe1a898af61cc3515661eb5fa763b57bd0b42e45099c8b32fd50f', ], [ 'p', - 'dcecb5c4c228e15a1f04305c34b39b7ff67675544cb7dc74dd5c715cf62ada74' + 'dcecb5c4c228e15a1f04305c34b39b7ff67675544cb7dc74dd5c715cf62ada74', ], [ 'p', - 'b2c61317687060b2b7e9cb7f7fde04f30bab23e12bf471f8d356000ca2b12b4a' + 'b2c61317687060b2b7e9cb7f7fde04f30bab23e12bf471f8d356000ca2b12b4a', ], [ 'p', - '51fc7209201b1414f721c3d2d2b3430699b1e6317716c5182cc1d7945072e358' + '51fc7209201b1414f721c3d2d2b3430699b1e6317716c5182cc1d7945072e358', ], [ 'p', - 'ce5061bfcc16476b9bde3f1d5b3ec7730c4361cf8c827fbd9c14eb8c7003a1de' + 'ce5061bfcc16476b9bde3f1d5b3ec7730c4361cf8c827fbd9c14eb8c7003a1de', ], [ 'p', - '0810b5bc4cddc3e7624a1f6acbdccdc95c6e9409c144ce83365ee04a3a63314e' + '0810b5bc4cddc3e7624a1f6acbdccdc95c6e9409c144ce83365ee04a3a63314e', ], [ 'p', - '975bbd239f0b7e25a080675d3db5892492ea9e9c7705c819ba3dafd8de95f3d9' + '975bbd239f0b7e25a080675d3db5892492ea9e9c7705c819ba3dafd8de95f3d9', ], [ 'p', - '76f928b303b095a6f17784151acd9a5127d183cb5f989a173b00bd0c12d07e83' + '76f928b303b095a6f17784151acd9a5127d183cb5f989a173b00bd0c12d07e83', ], [ 'p', - 'd4d4fdde8ab4924b1e452e896709a3bd236da4c0576274b52af5992d4d34762c' + 'd4d4fdde8ab4924b1e452e896709a3bd236da4c0576274b52af5992d4d34762c', ], [ 'p', - 'ac9ec020170155f0feb347f0d777ee5fc38dd1f36353093046323646cff5169f' + 'ac9ec020170155f0feb347f0d777ee5fc38dd1f36353093046323646cff5169f', ], [ 'p', - 'd91191e30e00444b942c0e82cad470b32af171764c2275bee0bd99377efd4075' + 'd91191e30e00444b942c0e82cad470b32af171764c2275bee0bd99377efd4075', ], [ 'p', - 'ea75802dd1c86933c1e20c582541bb283d44c88e3445ed90d4375fc3d973f3a0' + 'ea75802dd1c86933c1e20c582541bb283d44c88e3445ed90d4375fc3d973f3a0', ], [ 'p', - '9682c33f9024dadb1bffdf762c3156e26b4aa340de8d06c91ca537fcc0fdb3a9' + '9682c33f9024dadb1bffdf762c3156e26b4aa340de8d06c91ca537fcc0fdb3a9', ], [ 'p', - 'a8f14f05c64f9e62bdada89c21a52f09aa5d7948b47ccf52da1be16b0de9efac' + 'a8f14f05c64f9e62bdada89c21a52f09aa5d7948b47ccf52da1be16b0de9efac', ], [ 'p', - '80482e60178c2ce996da6d67577f56a2b2c47ccb1c84c81f2b7960637cb71b78' + '80482e60178c2ce996da6d67577f56a2b2c47ccb1c84c81f2b7960637cb71b78', ], [ 'p', - 'b10c0000079a83cf26815dc7538818d8d56a2983e374e30a4143e50060978457' + 'b10c0000079a83cf26815dc7538818d8d56a2983e374e30a4143e50060978457', ], [ 'p', - 'ae683cd251952448ad0d7b8ed6c2e0f8ab451578250cb35f0c977275b56b056e' + 'ae683cd251952448ad0d7b8ed6c2e0f8ab451578250cb35f0c977275b56b056e', ], [ 'p', - '954aaf69c2e7c9fb3f9998f61944ab8ab08ce3c8679ecd985e4486a6eb696217' + '954aaf69c2e7c9fb3f9998f61944ab8ab08ce3c8679ecd985e4486a6eb696217', ], [ 'p', - 'd7f0e3917c466f1e2233e9624fbd6d4bd1392dbcfcaf3574f457569d496cb731' + 'd7f0e3917c466f1e2233e9624fbd6d4bd1392dbcfcaf3574f457569d496cb731', ], [ 'p', - '104749bc9151a0e54b9845ee50fc4b559439dd1ada006e36a6c49ad3ea16a55c' + '104749bc9151a0e54b9845ee50fc4b559439dd1ada006e36a6c49ad3ea16a55c', ], [ 'p', - 'cf9413eb6bbe55c8a3c10119ec0635e134fa266f2c50f825d7225da9b92ecc4e' + 'cf9413eb6bbe55c8a3c10119ec0635e134fa266f2c50f825d7225da9b92ecc4e', ], [ 'p', - 'bae77874946ec111f94be59aef282de092dc4baf213f8ecb8c9e15cb7ed7304e' + 'bae77874946ec111f94be59aef282de092dc4baf213f8ecb8c9e15cb7ed7304e', ], [ 'p', - '44bb2dd1615ed2a527946c41d854995f18866a8feffa88eb375728c20aeea30c' + '44bb2dd1615ed2a527946c41d854995f18866a8feffa88eb375728c20aeea30c', ], [ 'p', - '62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49' + '62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49', ], [ 'p', - '9a29ee8c3771573e5306bb7701182e970b188ce3552713ca68a157ebc3c0bf75' + '9a29ee8c3771573e5306bb7701182e970b188ce3552713ca68a157ebc3c0bf75', ], [ 'p', - 'e3f0c72e7b653f395f64e03519bae3efeac184bcf0b3f38bdccb62a4d2aa5d30' + 'e3f0c72e7b653f395f64e03519bae3efeac184bcf0b3f38bdccb62a4d2aa5d30', ], [ 'p', - '9b9f5f1ec13105c8d1c2ea16aa952e98640b170b871420980ea11b18eb1f1e03' + '9b9f5f1ec13105c8d1c2ea16aa952e98640b170b871420980ea11b18eb1f1e03', ], [ 'p', - '2b36fb6ae1022d0d4eac2a9f13fc2638f3350acc9b07bdca1de43a7c63429644' + '2b36fb6ae1022d0d4eac2a9f13fc2638f3350acc9b07bdca1de43a7c63429644', ], [ 'p', - 'f00c952da33c06e02c930f76aba1085021b98075657daaff8ad119edcfde691e' + 'f00c952da33c06e02c930f76aba1085021b98075657daaff8ad119edcfde691e', ], [ 'p', - '8837f562e064282e4fb9902ae6062ee436a53236909a68c6d19564df6c208fbe' + '8837f562e064282e4fb9902ae6062ee436a53236909a68c6d19564df6c208fbe', ], [ 'p', - 'f43c1f9bff677b8f27b602725ea0ad51af221344f69a6b352a74991a4479bac3' + 'f43c1f9bff677b8f27b602725ea0ad51af221344f69a6b352a74991a4479bac3', ], [ 'p', - '66e346dfe3a4e572359519f086bf45771a19224343183aa1c86b9f9e31b78ac9' + '66e346dfe3a4e572359519f086bf45771a19224343183aa1c86b9f9e31b78ac9', ], [ 'p', - '8c24f2bf7df33aea0f05706162176343f34389d95ca5696dba1c2768887f586f' + '8c24f2bf7df33aea0f05706162176343f34389d95ca5696dba1c2768887f586f', ], [ 'p', - '343558f07b07ffcb24b27b73812d74d4ff8f46e81ea903f1e7f37d30d907bcfc' + '343558f07b07ffcb24b27b73812d74d4ff8f46e81ea903f1e7f37d30d907bcfc', ], [ 'p', - '57400e5b11c8b52ed04765df605fe9c30aa50abdeacff49d3de6b58359c907ed' + '57400e5b11c8b52ed04765df605fe9c30aa50abdeacff49d3de6b58359c907ed', ], [ 'p', - '4535551a40271b059ab92b71e7ab7e8700061a2d91b0d20f313ef82f052eb085' + '4535551a40271b059ab92b71e7ab7e8700061a2d91b0d20f313ef82f052eb085', ], [ 'p', - '8431af1a305fd23b869a12ad87118f78d87bec6e2a431e38fd1fabdac281ff45' + '8431af1a305fd23b869a12ad87118f78d87bec6e2a431e38fd1fabdac281ff45', ], [ 'p', - '4b12f6132a5ba813bdf55bcbf9d1acfefb02dabf67191dad71b455668c429b36' + '4b12f6132a5ba813bdf55bcbf9d1acfefb02dabf67191dad71b455668c429b36', ], [ 'p', - '747adf8e9036ed78b47eca762bf80bc41af34df6da7bd44876cf2d27e6b7dd64' + '747adf8e9036ed78b47eca762bf80bc41af34df6da7bd44876cf2d27e6b7dd64', ], [ 'p', - 'b832d7fdcf4f6fed87ccfc6e10426710b968d6c260206fecb24aa096879c44ce' + 'b832d7fdcf4f6fed87ccfc6e10426710b968d6c260206fecb24aa096879c44ce', ], [ 'p', - '09e935f7c01fda340051a4700cfb9dde533202bdf56808f68cafef6bae07a5bd' + '09e935f7c01fda340051a4700cfb9dde533202bdf56808f68cafef6bae07a5bd', ], [ 'p', - '2b26251002f9bdd990da1990bcc378ac5c816f1446e82167819ab60c4b9a6ca9' + '2b26251002f9bdd990da1990bcc378ac5c816f1446e82167819ab60c4b9a6ca9', ], [ 'p', - '2183e94758481d0f124fbd93c56ccaa45e7e545ceeb8d52848f98253f497b975' + '2183e94758481d0f124fbd93c56ccaa45e7e545ceeb8d52848f98253f497b975', ], [ 'p', - '2bee8a0f48dcc76df4385df95ee184331e41fbde0731164c6627512b9b34f005' + '2bee8a0f48dcc76df4385df95ee184331e41fbde0731164c6627512b9b34f005', ], [ 'p', - 'd0cb47a354003467a3a7cbc50ddc0c29250851f9040656bad9d0ab7adb5b7382' + 'd0cb47a354003467a3a7cbc50ddc0c29250851f9040656bad9d0ab7adb5b7382', ], [ 'p', - '47bae3a008414e24b4d91c8c170f7fce777dedc6780a462d010761dca6482327' + '47bae3a008414e24b4d91c8c170f7fce777dedc6780a462d010761dca6482327', ], [ 'p', - '38b07a31f3b23dbeb9f59deb7bec5b993173fb4022206980f3809d0b68abf959' + '38b07a31f3b23dbeb9f59deb7bec5b993173fb4022206980f3809d0b68abf959', ], [ 'p', - 'e6a92d8b6c20426f78bba8510ccdc73df5122814a3bac1d553adebac67a92b27' + 'e6a92d8b6c20426f78bba8510ccdc73df5122814a3bac1d553adebac67a92b27', ], [ 'p', - 'ad5aab5be883a571ea37b231cd996d37522e77d0f121cedfd6787b91d848268e' + 'ad5aab5be883a571ea37b231cd996d37522e77d0f121cedfd6787b91d848268e', ], [ 'p', - '6d334336f9ba6c35fdc3b87950721b123f56f0d686fe9a5b4c95d2568b2398d8' + '6d334336f9ba6c35fdc3b87950721b123f56f0d686fe9a5b4c95d2568b2398d8', ], [ 'p', - 'c8b430569a2c95aa8d6eceea67f40c16e17f1ac10755fcf17f2ba772f3febd96' + 'c8b430569a2c95aa8d6eceea67f40c16e17f1ac10755fcf17f2ba772f3febd96', ], [ 'p', - '3b6a202702bc8c236ff2900aa564575fe56ae5a9e5b8386d3307c79b392674ab' + '3b6a202702bc8c236ff2900aa564575fe56ae5a9e5b8386d3307c79b392674ab', ], [ 'p', - 'd97cd1bcc21e393e5a8b053fba9db385ace78710ba68a6bc7828d57ad82e88bd' + 'd97cd1bcc21e393e5a8b053fba9db385ace78710ba68a6bc7828d57ad82e88bd', ], [ 'p', - '3235036bd0957dfb27ccda02d452d7c763be40c91a1ac082ba6983b25238388c' + '3235036bd0957dfb27ccda02d452d7c763be40c91a1ac082ba6983b25238388c', ], [ 'p', - 'b99b149370e4f8533ce53d143af3f39e1f2628a39847f7fdd7544c9585da9299' + 'b99b149370e4f8533ce53d143af3f39e1f2628a39847f7fdd7544c9585da9299', ], [ 'p', - 'e4c47aedea8ea54255f5ba07a77053b24553e9b975435e56da343da19aec7881' + 'e4c47aedea8ea54255f5ba07a77053b24553e9b975435e56da343da19aec7881', ], [ 'p', - '552b4d02f9db02f11bda4b4c1cdefe8852c6c6b6ca0e03b7013f182c854413b7' + '552b4d02f9db02f11bda4b4c1cdefe8852c6c6b6ca0e03b7013f182c854413b7', ], [ 'p', - '3f8e32d654fbc0da5fd570d70381a3e59843b208c5574a74a2305527bce8382b' + '3f8e32d654fbc0da5fd570d70381a3e59843b208c5574a74a2305527bce8382b', ], [ 'p', - '84620a7b6a3d42b96b3e8a392fabca1e476e9049188808b0ecf3d64d36efffd1' + '84620a7b6a3d42b96b3e8a392fabca1e476e9049188808b0ecf3d64d36efffd1', ], [ 'p', - '047f497e13073d4303383c7abcc296a3b5b5956d243eafa6423c675a831a5cc1' + '047f497e13073d4303383c7abcc296a3b5b5956d243eafa6423c675a831a5cc1', ], [ 'p', - 'b875065f96ff58c82e951f543857515798f5e50c6903d9602b425e2cd957f1ce' + 'b875065f96ff58c82e951f543857515798f5e50c6903d9602b425e2cd957f1ce', ], [ 'p', - 'edfa27d49d2af37ee331e1225bb6ed1912c6d999281b36d8018ad99bc3573c29' + 'edfa27d49d2af37ee331e1225bb6ed1912c6d999281b36d8018ad99bc3573c29', ], [ 'p', - '8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168' + '8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168', ], [ 'p', - 'bc1f8b83991f46f6f2f2b4569314d50b229e9f2761716ca56d4572a190801a44' + 'bc1f8b83991f46f6f2f2b4569314d50b229e9f2761716ca56d4572a190801a44', ], [ 'p', - '84fe3febc748470ff1a363db8a375ffa1ff86603f2653d1c3c311ad0a70b5d0c' + '84fe3febc748470ff1a363db8a375ffa1ff86603f2653d1c3c311ad0a70b5d0c', ], [ 'p', - 'd543c820050efd6d2c1536b0990111ac293a4431e6a12929432366e0aa8001e7' + 'd543c820050efd6d2c1536b0990111ac293a4431e6a12929432366e0aa8001e7', ], [ 'p', - '7cf68b47a2b243d06322bfdb6a1c2422fb8b3a18d18a5c90c27b59e8f612553e' + '7cf68b47a2b243d06322bfdb6a1c2422fb8b3a18d18a5c90c27b59e8f612553e', ], [ 'p', - 'f0c864cf573de171053bef4df3b31c6593337a097fbbd9f20d78506e490c6b64' + 'f0c864cf573de171053bef4df3b31c6593337a097fbbd9f20d78506e490c6b64', ], [ 'p', - '3702743c98430ba152e635b081637716a3c949c13ad3ad1e6c80e6e7d41fbc8a' + '3702743c98430ba152e635b081637716a3c949c13ad3ad1e6c80e6e7d41fbc8a', ], [ 'p', - '2a043132d98c2457fb3581fdeddab380a8eda3760b2605f676be5059ed260066' + '2a043132d98c2457fb3581fdeddab380a8eda3760b2605f676be5059ed260066', ], [ 'p', - 'c5072866b41d6b88ab2ffee16ad7cb648f940867371a7808aaa94cf7d01f4188' + 'c5072866b41d6b88ab2ffee16ad7cb648f940867371a7808aaa94cf7d01f4188', ], [ 'p', - '51535ad9f0e13a810f73ea8829a79b3733bd1fffb767c4885990b02f59103a13' + '51535ad9f0e13a810f73ea8829a79b3733bd1fffb767c4885990b02f59103a13', ], [ 'p', - '3707f1efc7515524dce41d3bf50bfd9fdaed3494620b5f94fcf16d2766da4ec2' + '3707f1efc7515524dce41d3bf50bfd9fdaed3494620b5f94fcf16d2766da4ec2', ], [ 'p', - 'dbab9040bc1f0c436b0f92f517702498358edc1fde2c7884d0e1036c739d44f3' + 'dbab9040bc1f0c436b0f92f517702498358edc1fde2c7884d0e1036c739d44f3', ], [ 'p', - '904ea00a4a245559d6184be5c6e2cf2c66ea7fc91eb5f1eb5349506d19d63a11' + '904ea00a4a245559d6184be5c6e2cf2c66ea7fc91eb5f1eb5349506d19d63a11', ], [ 'p', - '9ac12013d20fae4f8829ba4e5ba6343e410288d3a0752d6143386d2c1af1f57e' + '9ac12013d20fae4f8829ba4e5ba6343e410288d3a0752d6143386d2c1af1f57e', ], [ 'p', - '7bc0ff3de7b2205ed8bc366f7657138eacb5164d43d9580b8f5b47b7e6a7c235' + '7bc0ff3de7b2205ed8bc366f7657138eacb5164d43d9580b8f5b47b7e6a7c235', ], [ 'p', - 'c5cfda98d01f152b3493d995eed4cdb4d9e55a973925f6f9ea24769a5a21e778' + 'c5cfda98d01f152b3493d995eed4cdb4d9e55a973925f6f9ea24769a5a21e778', ], [ 'p', - '887645fef0ce0c3c1218d2f5d8e6132a19304cdc57cd20281d082f38cfea0072' + '887645fef0ce0c3c1218d2f5d8e6132a19304cdc57cd20281d082f38cfea0072', ], [ 'p', - '88a502f72f216c93eb840fa805c1a215b97e0800ab2dfa017450d38cb4b60a03' + '88a502f72f216c93eb840fa805c1a215b97e0800ab2dfa017450d38cb4b60a03', ], [ 'p', - '3f152ab665d1079108529ff6bf0ba48809b6788b22ab8a3d76f7a3f63bec19a0' + '3f152ab665d1079108529ff6bf0ba48809b6788b22ab8a3d76f7a3f63bec19a0', ], [ 'p', - '27da3f032e0fea007947b0da12f1183630c5a2da79d7202b96f35f16ef6ce48e' + '27da3f032e0fea007947b0da12f1183630c5a2da79d7202b96f35f16ef6ce48e', ], [ 'p', - 'de29897a4a9086a1c5e8f6c7d06691afeda77103eea35eabecbfda21189fa995' + 'de29897a4a9086a1c5e8f6c7d06691afeda77103eea35eabecbfda21189fa995', ], [ 'p', - '0a2bfced3f7c8a08d88a697da80d7d85f12e69260cf308de27da1f5b6f65bf00' + '0a2bfced3f7c8a08d88a697da80d7d85f12e69260cf308de27da1f5b6f65bf00', ], [ 'p', - '95405f16211a88c869ec87b684cb450136b7bf2420e236f9ec793385893d01e8' + '95405f16211a88c869ec87b684cb450136b7bf2420e236f9ec793385893d01e8', ], [ 'p', - 'f9e24c0a9544d119b4f0e31ceac53d1b650c763e378541e1dfde402e350f5792' + 'f9e24c0a9544d119b4f0e31ceac53d1b650c763e378541e1dfde402e350f5792', ], [ 'p', - '7f3bd39154ce2994d67bc89b782c12871bcd7a30093b4700b07c438fb7b906db' + '7f3bd39154ce2994d67bc89b782c12871bcd7a30093b4700b07c438fb7b906db', ], [ 'p', - '1d914450975db68d850f13a8950abda9dc6a1b140de6460634f839c49f5de958' + '1d914450975db68d850f13a8950abda9dc6a1b140de6460634f839c49f5de958', ], [ 'p', - '545320c902a7c7de8f44c6c3c0e7870b72e8ddfdd203139db18b5d518f6771c1' + '545320c902a7c7de8f44c6c3c0e7870b72e8ddfdd203139db18b5d518f6771c1', ], [ 'p', - 'e740b0275f467618fdebf8ad54cb597deabbca2a0490d314e509730c50118499' + 'e740b0275f467618fdebf8ad54cb597deabbca2a0490d314e509730c50118499', ], [ 'p', - '179744407ac4fda143a8635e7ae9c9eabf3ab107a818a4f740a9e46b39412a42' + '179744407ac4fda143a8635e7ae9c9eabf3ab107a818a4f740a9e46b39412a42', ], [ 'p', - '2d11d3a3123287b478e19e9ef011bceb48e8f14a0d58e22bd156f35a839c5640' + '2d11d3a3123287b478e19e9ef011bceb48e8f14a0d58e22bd156f35a839c5640', ], [ 'p', - 'ce5a47f6328beab97310a27269c4725988ced2aec93fcd3ab01282f667d696c3' + 'ce5a47f6328beab97310a27269c4725988ced2aec93fcd3ab01282f667d696c3', ], [ 'p', - 'c7eda660a6bc8270530e82b4a7712acdea2e31dc0a56f8dc955ac009efd97c86' + 'c7eda660a6bc8270530e82b4a7712acdea2e31dc0a56f8dc955ac009efd97c86', ], [ 'p', - 'e8caa2028a7090ffa85f1afee67451b309ba2f9dee655ec8f7e0a02c29388180' + 'e8caa2028a7090ffa85f1afee67451b309ba2f9dee655ec8f7e0a02c29388180', ], [ 'p', - '9c8e6bcf8438812fe44ccd32ba4208b3c72193a944d7e6f68ff311b48a28523e' + '9c8e6bcf8438812fe44ccd32ba4208b3c72193a944d7e6f68ff311b48a28523e', ], [ 'p', - '7215b2db8754494fd3452b7f2d28b56e23863b95446bf68d79f980a7ad5ec7cd' - ] + '7215b2db8754494fd3452b7f2d28b56e23863b95446bf68d79f980a7ad5ec7cd', + ], ], 'content': '{"wss://rsslay.fiatjaf.com":{"read":true,"write":false},"wss://nostr-pub.wellorder.net":{"read":true,"write":true},"wss://expensive-relay.fiatjaf.com":{"read":true,"write":true},"wss://nostr.rocks":{"read":true,"write":true},"wss://nostr-relay.untethr.me\\t":{"read":true,"write":true},"wss://relayer.fiatjaf.com":{"read":true,"write":true},"wss://nostr-relay.untethr.me":{"read":true,"write":true},"wss://nostr-relay.wlvs.space":{"read":true,"write":true},"wss://nostr.openchain.fr":{"read":true,"write":true},"wss://relay.futohq.com":{"read":true,"write":true}}', - 'sig': 'f5935788cf7a5a402b14f3199f2ecb2f181f710a475693f2866fe3cd8bdaf900ec9edb9f831d23783023e0aa9011fe403fbaa4e4c93562d56ac8f463fd201e3d' + 'sig': 'f5935788cf7a5a402b14f3199f2ecb2f181f710a475693f2866fe3cd8bdaf900ec9edb9f831d23783023e0aa9011fe403fbaa4e4c93562d56ac8f463fd201e3d', }, { 'id': 'f937a7ca5e109b4527849681ceedea944abd5a2e516d3383cb17e7e189736e3b', @@ -807,11 +807,11 @@ export const getEvents = (): Event[] => [ [ 'p', '14347702b99786cc0ee644620a5f71bc6a88e2882491f57c372f1deaed198701', - '' - ] + '', + ], ], 'content': '+of2PlIcxGeMRExh7kpacc4fkZurwj8yL+uChrregn2DDbeSRE2rQV7SG1GQRUn5mq3gtOuX9P8tP0MzJbuXfqBryK2gRKJdyG7Yphmq5gods458VVME2yLMcUjAFU4P?iv=rPLf0PBhDYYub6BiJSiq4w==', - 'sig': '632754a45a8556e408ceaa9a8e5c7b443044cb37a1c58126f96c4a44c87c1285e00c8997a7c9bd44325ef8782a4cf494c2bed3d7e5d94385d80c1b1d3795be30' + 'sig': '632754a45a8556e408ceaa9a8e5c7b443044cb37a1c58126f96c4a44c87c1285e00c8997a7c9bd44325ef8782a4cf494c2bed3d7e5d94385d80c1b1d3795be30', }, { pubkey: 'f6f33f0b9cac10e1136c620501721565f561e564554a9a35ad9b190bd743b4c2', @@ -820,12 +820,12 @@ export const getEvents = (): Event[] => [ tags: [ [ 'e', - '2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5' - ] + '2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5', + ], ], content: '', sig: 'b6fc44d7b1bcab4ef9b40d3c5a92afce9d778964f5a477437af037aa3dd3de7f7498a1c56ea816e49cf5705252fe8dcd77384bb91580277ff576d60367047ee1', - id: '20942205680e130a7602fd735fe715f52edf814a0b6e6e7f0990a02b257504ed' + id: '20942205680e130a7602fd735fe715f52edf814a0b6e6e7f0990a02b257504ed', }, { 'id': '444cec7f44c53eee60ba62858920c74173aa6bbb76c622f484a88cfcca2e07ad', @@ -834,11 +834,11 @@ export const getEvents = (): Event[] => [ 'kind': 5, 'tags': [ [ - 'e', '9fafc99518ce02cb52a4e3befe82ca84088a79cc45e5340ebf5af042b464d84f' - ] + 'e', '9fafc99518ce02cb52a4e3befe82ca84088a79cc45e5340ebf5af042b464d84f', + ], ], 'content': 'This is a demonstration of NIP-09.', - 'sig': '45cfbfcb202521d87a2d0bf70eabb2533c7993f239065538fa9d336aef74160c072596f1792e95682b2098b9a339df03f1ca480c859a46c6f10543398f12c213' + 'sig': '45cfbfcb202521d87a2d0bf70eabb2533c7993f239065538fa9d336aef74160c072596f1792e95682b2098b9a339df03f1ca480c859a46c6f10543398f12c213', }, { 'id': '23677e3d035be5de01172de203103e292126d542897086bf797d8794fe6b1081', @@ -848,15 +848,15 @@ export const getEvents = (): Event[] => [ 'tags': [ [ 'e', - '8da089fad0df548e490d93eccc413ecee63cc9da4901051b0bdcb801032f05d3' + '8da089fad0df548e490d93eccc413ecee63cc9da4901051b0bdcb801032f05d3', ], [ 'p', - '4d5ce768123563bc583697db5e84841fb528f7b708d966f2e546286ce3c72077' - ] + '4d5ce768123563bc583697db5e84841fb528f7b708d966f2e546286ce3c72077', + ], ], 'content': '{"pubkey":"4d5ce768123563bc583697db5e84841fb528f7b708d966f2e546286ce3c72077","content":"sometimes people just need a reason to believe ","id":"8da089fad0df548e490d93eccc413ecee63cc9da4901051b0bdcb801032f05d3","created_at":1660406626,"sig":"18ce5648b6c434258cf347c38a2939579ffea1211a1d20e5159c2b8a28960c053607916eeffa71d4d20f7f0b30bb4b34cf7965e254b4c41057730cb13f77b69d","kind":1,"tags":[]}', - 'sig': '75f9117d90adc8ac768983cfce19e5156a0f62ecfe6c1e2d33d62ef1c438b83e87551916f1d2e62513f899d706dd54a98af0b5ce5dce3fba299b3e62791e6e8e' + 'sig': '75f9117d90adc8ac768983cfce19e5156a0f62ecfe6c1e2d33d62ef1c438b83e87551916f1d2e62513f899d706dd54a98af0b5ce5dce3fba299b3e62791e6e8e', }, { 'id': '1a621c1ff8f6ea2641205bcb8a2908c80f7e70338179ac6f0dab8dfebf748132', @@ -868,32 +868,32 @@ export const getEvents = (): Event[] => [ 'e', 'dc191f093c4e8932434aa939431be375a40eded7877ce03b0c549ff98de8460c', '', - 'root' + 'root', ], [ 'e', '834c0da081608ba0587f330a0e9038a983bb2f331bd3ca0af13acf923205afd9', '', - 'reply' + 'reply', ], [ 'p', - 'c7eda660a6bc8270530e82b4a7712acdea2e31dc0a56f8dc955ac009efd97c86' + 'c7eda660a6bc8270530e82b4a7712acdea2e31dc0a56f8dc955ac009efd97c86', ], [ 'p', - '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245' + '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', ], [ 'e', - 'c7499698cb59ab0e1dc3b15fa5ad1373bdb6d45e1a85f6c24da783bd2e13c2db' + 'c7499698cb59ab0e1dc3b15fa5ad1373bdb6d45e1a85f6c24da783bd2e13c2db', ], [ 'p', - '2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5' - ] + '2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5', + ], ], 'content': '', - 'sig': '7bfc0ec98e6adcfc1ea9a8848b1e88ff3ded36175e7b3641791383f9eb88e362aae2909db1fb9138349170035dff63308ce6ba991c98752c1e4dbf8ad0f66583' - } + 'sig': '7bfc0ec98e6adcfc1ea9a8848b1e88ff3ded36175e7b3641791383f9eb88e362aae2909db1fb9138349170035dff63308ce6ba991c98752c1e4dbf8ad0f66583', + }, ] diff --git a/test/unit/repositories/event-repository.spec.ts b/test/unit/repositories/event-repository.spec.ts index 4b69fbb..e32dc60 100644 --- a/test/unit/repositories/event-repository.spec.ts +++ b/test/unit/repositories/event-repository.spec.ts @@ -1,7 +1,8 @@ import * as chai from 'chai' -import knex, { Knex } from 'knex' import * as sinon from 'sinon' +import knex, { Knex } from 'knex' import sinonChai from 'sinon-chai' + import { Event } from '../../../src/@types/event' import { IEventRepository } from '../../../src/@types/repositories' import { SubscriptionFilter } from '../../../src/@types/subscription' @@ -21,7 +22,7 @@ describe('EventRepository', () => { sandbox = sinon.createSandbox() dbClient = knex({ - client: 'pg' + client: 'pg', }) repository = new EventRepository(dbClient) @@ -70,7 +71,7 @@ describe('EventRepository', () => { const query = repository.findByFilters(filters).toString() - expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\')) order by "event_created_at" asc') + expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\') or "event_delegator" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\')) order by "event_created_at" asc') }) it('selects events by two authors', () => { @@ -78,14 +79,14 @@ describe('EventRepository', () => { { authors: [ '22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793', - '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245' - ] - } + '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', + ], + }, ] const query = repository.findByFilters(filters).toString() - expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\')) order by "event_created_at" asc') + expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\') or "event_delegator" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\')) order by "event_created_at" asc') }) it('selects events by one author prefix (even length)', () => { @@ -93,13 +94,13 @@ describe('EventRepository', () => { { authors: [ '22e804', - ] - } + ], + }, ] const query = repository.findByFilters(filters).toString() - expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\') order by "event_created_at" asc') + expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\' or substring("event_delegator" from 1 for 3) = X\'22e804\') order by "event_created_at" asc') }) it('selects events by one author prefix (odd length)', () => { @@ -107,13 +108,13 @@ describe('EventRepository', () => { { authors: [ '22e804f', - ] - } + ], + }, ] const query = repository.findByFilters(filters).toString() - expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x22e804f0\' AND E\'\\\\x22e804ff\') order by "event_created_at" asc') + expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x22e804f0\' AND E\'\\\\x22e804ff\' or substring("event_delegator" from 1 for 4) BETWEEN E\'\\\\x22e804f0\' AND E\'\\\\x22e804ff\') order by "event_created_at" asc') }) it('selects events by two author prefix (first even, second odd)', () => { @@ -122,13 +123,13 @@ describe('EventRepository', () => { authors: [ '22e804', '32e1827', - ] - } + ], + }, ] const query = repository.findByFilters(filters).toString() - expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\' or substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x32e18270\' AND E\'\\\\x32e1827f\') order by "event_created_at" asc') + expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\' or substring("event_delegator" from 1 for 3) = X\'22e804\' or substring("event_pubkey" from 1 for 4) BETWEEN E\'\\\\x32e18270\' AND E\'\\\\x32e1827f\' or substring("event_delegator" from 1 for 4) BETWEEN E\'\\\\x32e18270\' AND E\'\\\\x32e1827f\') order by "event_created_at" asc') }) }) @@ -154,9 +155,9 @@ describe('EventRepository', () => { { ids: [ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' - ] - } + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + ], + }, ] const query = repository.findByFilters(filters).toString() @@ -169,8 +170,8 @@ describe('EventRepository', () => { { ids: [ 'abcd', - ] - } + ], + }, ] const query = repository.findByFilters(filters).toString() @@ -183,8 +184,8 @@ describe('EventRepository', () => { { ids: [ 'abc', - ] - } + ], + }, ] const query = repository.findByFilters(filters).toString() @@ -198,8 +199,8 @@ describe('EventRepository', () => { ids: [ 'abcdef', 'abc', - ] - } + ], + }, ] const query = repository.findByFilters(filters).toString() @@ -359,7 +360,7 @@ describe('EventRepository', () => { const query = repository.findByFilters(filters).toString() - expect(query).to.equal('(select * from "events" where "event_kind" in (1)) union (select * from "events" where (substring("event_id" from 1 for 3) BETWEEN E\'\\\\xaaaaa0\' AND E\'\\\\xaaaaaf\') order by "event_created_at" asc) union (select * from "events" where (substring("event_pubkey" from 1 for 3) BETWEEN E\'\\\\xbbbbb0\' AND E\'\\\\xbbbbbf\') order by "event_created_at" asc) union (select * from "events" where "event_created_at" >= 1000 order by "event_created_at" asc) union (select * from "events" where "event_created_at" <= 1000 order by "event_created_at" asc) union (select * from "events" order by "event_created_at" DESC limit 1000) order by "event_created_at" asc') + expect(query).to.equal('(select * from "events" where "event_kind" in (1)) union (select * from "events" where (substring("event_id" from 1 for 3) BETWEEN E\'\\\\xaaaaa0\' AND E\'\\\\xaaaaaf\') order by "event_created_at" asc) union (select * from "events" where (substring("event_pubkey" from 1 for 3) BETWEEN E\'\\\\xbbbbb0\' AND E\'\\\\xbbbbbf\' or substring("event_delegator" from 1 for 3) BETWEEN E\'\\\\xbbbbb0\' AND E\'\\\\xbbbbbf\') order by "event_created_at" asc) union (select * from "events" where "event_created_at" >= 1000 order by "event_created_at" asc) union (select * from "events" where "event_created_at" <= 1000 order by "event_created_at" asc) union (select * from "events" order by "event_created_at" DESC limit 1000) order by "event_created_at" asc') }) }) }) @@ -430,7 +431,7 @@ describe('EventRepository', () => { const query = (repository as any).insert(event).toString() - expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags") values (\'I\'\'ve set up mirroring between relays: https://i.imgur.com/HxCDipB.png\', 1648351380, X\'6b3cdd0302ded8068ad3f0269c74423ca4fee460f800f3d90103b63f14400407\', 1, X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'b37adfed0e6398546d623536f9ddc92b95b7dc71927e1123266332659253ecd0ffa91ddf2c0a82a8426c5b363139d28534d6cac893b8a810149557a3f6d36768\', \'[["p","8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9","wss://nostr-pub.wellorder.net"],["e","7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96","wss://nostr-relay.untethr.me"]]\') on conflict do nothing') + expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags") values (\'I\'\'ve set up mirroring between relays: https://i.imgur.com/HxCDipB.png\', 1648351380, NULL, X\'6b3cdd0302ded8068ad3f0269c74423ca4fee460f800f3d90103b63f14400407\', 1, X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'b37adfed0e6398546d623536f9ddc92b95b7dc71927e1123266332659253ecd0ffa91ddf2c0a82a8426c5b363139d28534d6cac893b8a810149557a3f6d36768\', \'[["p","8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9","wss://nostr-pub.wellorder.net"],["e","7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96","wss://nostr-relay.untethr.me"]]\') on conflict do nothing') }) }) }) diff --git a/test/unit/schemas/event-schema.spec.ts b/test/unit/schemas/event-schema.spec.ts index 3623f7d..887e3e7 100644 --- a/test/unit/schemas/event-schema.spec.ts +++ b/test/unit/schemas/event-schema.spec.ts @@ -1,5 +1,6 @@ -import { expect } from 'chai' import { assocPath, omit, range } from 'ramda' +import { expect } from 'chai' + import { Event } from '../../../src/@types/event' import { eventSchema } from '../../../src/schemas/event-schema' import { validateSchema } from '../../../src/utils/validation' @@ -18,33 +19,33 @@ describe('NIP-01', () => { 'e', 'c58e83bb744e4c29642db7a5c3bd1519516ad5c51f6ba5f90c451d03c1961210', '', - 'root' + 'root', ], [ 'e', 'd0d78967b734628cec7bdfa2321c71c1f1c48e211b4b54333c3b0e94e7e99166', '', - 'reply' + 'reply', ], [ 'p', - 'edfa27d49d2af37ee331e1225bb6ed1912c6d999281b36d8018ad99bc3573c29' + 'edfa27d49d2af37ee331e1225bb6ed1912c6d999281b36d8018ad99bc3573c29', ], [ 'p', - '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245' + '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245', ], [ 'e', - '6fed2aae1e4f7d8b535774e4f7061c10e2ff20df1ef047da09462c7937925cd5' + '6fed2aae1e4f7d8b535774e4f7061c10e2ff20df1ef047da09462c7937925cd5', ], [ 'p', - '2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5' - ] + '2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5', + ], ], 'content': '', - 'sig': '313a9b8cd68267a51da84e292c0937d1f3686c6757c4584f50fcedad2b13fad755e6226924f79880fb5aa9de95c04231a4823981513ac9e7092bad7488282a96' + 'sig': '313a9b8cd68267a51da84e292c0937d1f3686c6757c4584f50fcedad2b13fad755e6226924f79880fb5aa9de95c04231a4823981513ac9e7092bad7488282a96', } }) @@ -57,66 +58,66 @@ describe('NIP-01', () => { const cases = { id: [ - { message: 'must be a string', transform: assocPath(['id'], null), }, - { message: 'must only contain lowercase characters', transform: assocPath(['id'], 'F'.repeat(64)), }, - { message: 'must only contain hexadecimal characters', transform: assocPath(['id'], 'not hex'), }, - { message: 'length must be 64 characters long', transform: assocPath(['id'], 'f'.repeat(65)), }, - { message: 'length must be 64 characters long', transform: assocPath(['id'], 'f'.repeat(63)), }, - { message: 'is not allowed to be empty', transform: assocPath(['id'], ''), }, - { message: 'is required', transform: omit(['id']), }, + { message: 'must be a string', transform: assocPath(['id'], null) }, + { message: 'must only contain lowercase characters', transform: assocPath(['id'], 'F'.repeat(64)) }, + { message: 'must only contain hexadecimal characters', transform: assocPath(['id'], 'not hex') }, + { message: 'length must be 64 characters long', transform: assocPath(['id'], 'f'.repeat(65)) }, + { message: 'length must be 64 characters long', transform: assocPath(['id'], 'f'.repeat(63)) }, + { message: 'is not allowed to be empty', transform: assocPath(['id'], '') }, + { message: 'is required', transform: omit(['id']) }, ], pubkey: [ - { message: 'must be a string', transform: assocPath(['pubkey'], null), }, - { message: 'must only contain lowercase characters', transform: assocPath(['pubkey'], 'F'.repeat(64)), }, - { message: 'must only contain hexadecimal characters', transform: assocPath(['pubkey'], 'not hex'), }, - { message: 'length must be 64 characters long', transform: assocPath(['pubkey'], 'f'.repeat(65)), }, - { message: 'length must be 64 characters long', transform: assocPath(['pubkey'], 'f'.repeat(63)), }, - { message: 'is not allowed to be empty', transform: assocPath(['pubkey'], ''), }, - { message: 'is required', transform: omit(['pubkey']), }, + { message: 'must be a string', transform: assocPath(['pubkey'], null) }, + { message: 'must only contain lowercase characters', transform: assocPath(['pubkey'], 'F'.repeat(64)) }, + { message: 'must only contain hexadecimal characters', transform: assocPath(['pubkey'], 'not hex') }, + { message: 'length must be 64 characters long', transform: assocPath(['pubkey'], 'f'.repeat(65)) }, + { message: 'length must be 64 characters long', transform: assocPath(['pubkey'], 'f'.repeat(63)) }, + { message: 'is not allowed to be empty', transform: assocPath(['pubkey'], '') }, + { message: 'is required', transform: omit(['pubkey']) }, ], created_at: [ - { message: 'must be a number', transform: assocPath(['created_at'], null), }, - { message: 'must be greater than or equal to 0', transform: assocPath(['created_at'], -1), }, - { message: 'must be a multiple of 1', transform: assocPath(['created_at'], Math.PI), }, - { message: 'is required', transform: omit(['created_at']), }, + { message: 'must be a number', transform: assocPath(['created_at'], null) }, + { message: 'must be greater than or equal to 0', transform: assocPath(['created_at'], -1) }, + { message: 'must be a multiple of 1', transform: assocPath(['created_at'], Math.PI) }, + { message: 'is required', transform: omit(['created_at']) }, ], kind: [ - { message: 'must be a number', transform: assocPath(['kind'], null), }, - { message: 'must be greater than or equal to 0', transform: assocPath(['kind'], -1), }, - { message: 'must be a multiple of 1', transform: assocPath(['kind'], Math.PI), }, - { message: 'is required', transform: omit(['kind']), }, + { message: 'must be a number', transform: assocPath(['kind'], null) }, + { message: 'must be greater than or equal to 0', transform: assocPath(['kind'], -1) }, + { message: 'must be a multiple of 1', transform: assocPath(['kind'], Math.PI) }, + { message: 'is required', transform: omit(['kind']) }, ], content: [ - { message: 'must be a string', transform: assocPath(['content'], null), }, - { message: 'length must be less than or equal to 65536 characters long', transform: assocPath(['content'], ' '.repeat(64 * 1024 + 1)), }, - { message: 'is required', transform: omit(['content']), }, + { message: 'must be a string', transform: assocPath(['content'], null) }, + { message: 'length must be less than or equal to 65536 characters long', transform: assocPath(['content'], ' '.repeat(64 * 1024 + 1)) }, + { message: 'is required', transform: omit(['content']) }, ], sig: [ - { message: 'must be a string', transform: assocPath(['sig'], null), }, - { message: 'must only contain lowercase characters', transform: assocPath(['sig'], 'F'.repeat(128)), }, - { message: 'must only contain hexadecimal characters', transform: assocPath(['sig'], 'not hex'), }, - { message: 'length must be 128 characters long', transform: assocPath(['sig'], 'f'.repeat(129)), }, - { message: 'length must be 128 characters long', transform: assocPath(['sig'], 'f'.repeat(127)), }, - { message: 'is not allowed to be empty', transform: assocPath(['sig'], ''), }, - { message: 'is required', transform: omit(['sig']), }, + { message: 'must be a string', transform: assocPath(['sig'], null) }, + { message: 'must only contain lowercase characters', transform: assocPath(['sig'], 'F'.repeat(128)) }, + { message: 'must only contain hexadecimal characters', transform: assocPath(['sig'], 'not hex') }, + { message: 'length must be 128 characters long', transform: assocPath(['sig'], 'f'.repeat(129)) }, + { message: 'length must be 128 characters long', transform: assocPath(['sig'], 'f'.repeat(127)) }, + { message: 'is not allowed to be empty', transform: assocPath(['sig'], '') }, + { message: 'is required', transform: omit(['sig']) }, ], tags: [ - { message: 'must be an array', transform: assocPath(['tags'], null), }, - { message: 'is required', transform: omit(['tags']), }, + { message: 'must be an array', transform: assocPath(['tags'], null) }, + { message: 'is required', transform: omit(['tags']) }, { message: 'must contain less than or equal to 500 items', transform: assocPath(['tags'], range(0, 501).map(() => (['x', 'x']))) }, ], tag: [ - { message: 'must be an array', transform: assocPath(['tags', 0], null), }, + { message: 'must be an array', transform: assocPath(['tags', 0], null) }, { message: 'must contain less than or equal to 10 items', transform: assocPath(['tags', 0], range(0, 11).map(() => 'x')) }, ], identifier: [ - { message: 'must be a string', transform: assocPath(['tags', 0, 0], null), }, - { message: 'length must be less than or equal to 255 characters long', transform: assocPath(['tags', 0, 0], ' '.repeat(256)), }, - { message: 'is not allowed to be empty', transform: assocPath(['tags', 0, 0], ''), }, + { message: 'must be a string', transform: assocPath(['tags', 0, 0], null) }, + { message: 'length must be less than or equal to 255 characters long', transform: assocPath(['tags', 0, 0], ' '.repeat(256)) }, + { message: 'is not allowed to be empty', transform: assocPath(['tags', 0, 0], '') }, ], value: [ - { message: 'must be a string', transform: assocPath(['tags', 0, 1], null), }, - { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['tags', 0, 1], ' '.repeat(1024 + 1)), }, + { message: 'must be a string', transform: assocPath(['tags', 0, 1], null) }, + { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['tags', 0, 1], ' '.repeat(1024 + 1)) }, ], } diff --git a/test/unit/schemas/filter-schema.spec.ts b/test/unit/schemas/filter-schema.spec.ts index d05948d..24e1615 100644 --- a/test/unit/schemas/filter-schema.spec.ts +++ b/test/unit/schemas/filter-schema.spec.ts @@ -1,7 +1,8 @@ -import { expect } from 'chai' import { assocPath, range } from 'ramda' -import { SubscriptionFilter } from '../../../src/@types/subscription' +import { expect } from 'chai' + import { filterSchema } from '../../../src/schemas/filter-schema' +import { SubscriptionFilter } from '../../../src/@types/subscription' import { validateSchema } from '../../../src/utils/validation' describe('NIP-01', () => { @@ -30,71 +31,71 @@ describe('NIP-01', () => { const cases = { ids: [ - { message: 'must be an array', transform: assocPath(['ids'], null), }, - { message: 'must contain less than or equal to 256 items', transform: assocPath(['ids'], range(0, 257).map(() => 'f')), }, + { message: 'must be an array', transform: assocPath(['ids'], null) }, + { message: 'must contain less than or equal to 256 items', transform: assocPath(['ids'], range(0, 257).map(() => 'f')) }, ], prefixOrId: [ - { message: 'length must be less than or equal to 64 characters long', transform: assocPath(['ids', 0], 'f'.repeat(65)), }, - { message: 'must only contain hexadecimal characters', transform: assocPath(['ids', 0], 'not hex'), }, - { message: 'is not allowed to be empty', transform: assocPath(['ids', 0], ''), }, + { message: 'length must be less than or equal to 64 characters long', transform: assocPath(['ids', 0], 'f'.repeat(65)) }, + { message: 'must only contain hexadecimal characters', transform: assocPath(['ids', 0], 'not hex') }, + { message: 'is not allowed to be empty', transform: assocPath(['ids', 0], '') }, ], authors: [ - { message: 'must be an array', transform: assocPath(['authors'], null), }, - { message: 'must contain less than or equal to 256 items', transform: assocPath(['authors'], range(0, 257).map(() => 'f')), }, + { message: 'must be an array', transform: assocPath(['authors'], null) }, + { message: 'must contain less than or equal to 256 items', transform: assocPath(['authors'], range(0, 257).map(() => 'f')) }, ], prefixOrAuthor: [ - { message: 'length must be less than or equal to 64 characters long', transform: assocPath(['authors', 0], 'f'.repeat(65)), }, - { message: 'must only contain hexadecimal characters', transform: assocPath(['authors', 0], 'not hex'), }, - { message: 'is not allowed to be empty', transform: assocPath(['authors', 0], ''), }, + { message: 'length must be less than or equal to 64 characters long', transform: assocPath(['authors', 0], 'f'.repeat(65)) }, + { message: 'must only contain hexadecimal characters', transform: assocPath(['authors', 0], 'not hex') }, + { message: 'is not allowed to be empty', transform: assocPath(['authors', 0], '') }, ], kinds: [ - { message: 'must be an array', transform: assocPath(['kinds'], null), }, - { message: 'must contain less than or equal to 20 items', transform: assocPath(['kinds'], range(0, 21).map(() => 1)), }, + { message: 'must be an array', transform: assocPath(['kinds'], null) }, + { message: 'must contain less than or equal to 20 items', transform: assocPath(['kinds'], range(0, 21).map(() => 1)) }, ], kind: [ - { message: 'must be greater than or equal to 0', transform: assocPath(['kinds', 0], -1), }, - { message: 'must be a number', transform: assocPath(['kinds', 0], null), }, - { message: 'must be a multiple of 1', transform: assocPath(['kinds', 0], Math.PI), }, + { message: 'must be greater than or equal to 0', transform: assocPath(['kinds', 0], -1) }, + { message: 'must be a number', transform: assocPath(['kinds', 0], null) }, + { message: 'must be a multiple of 1', transform: assocPath(['kinds', 0], Math.PI) }, ], since: [ - { message: 'must be a number', transform: assocPath(['since'], null), }, - { message: 'must be greater than or equal to 0', transform: assocPath(['since'], -1), }, - { message: 'must be a multiple of 1', transform: assocPath(['since'], Math.PI), }, + { message: 'must be a number', transform: assocPath(['since'], null) }, + { message: 'must be greater than or equal to 0', transform: assocPath(['since'], -1) }, + { message: 'must be a multiple of 1', transform: assocPath(['since'], Math.PI) }, ], until: [ - { message: 'must be a number', transform: assocPath(['until'], null), }, - { message: 'must be greater than or equal to 0', transform: assocPath(['until'], -1), }, - { message: 'must be a multiple of 1', transform: assocPath(['until'], Math.PI), }, + { message: 'must be a number', transform: assocPath(['until'], null) }, + { message: 'must be greater than or equal to 0', transform: assocPath(['until'], -1) }, + { message: 'must be a multiple of 1', transform: assocPath(['until'], Math.PI) }, ], limit: [ - { message: 'must be a number', transform: assocPath(['limit'], null), }, - { message: 'must be greater than or equal to 1', transform: assocPath(['limit'], -1), }, - { message: 'must be a multiple of 1', transform: assocPath(['limit'], Math.PI), }, - { message: 'must be less than or equal to 10000', transform: assocPath(['limit'], 10001), }, + { message: 'must be a number', transform: assocPath(['limit'], null) }, + { message: 'must be greater than or equal to 1', transform: assocPath(['limit'], -1) }, + { message: 'must be a multiple of 1', transform: assocPath(['limit'], Math.PI) }, + { message: 'must be less than or equal to 10000', transform: assocPath(['limit'], 10001) }, ], '#e': [ - { message: 'must be an array', transform: assocPath(['#e'], null), }, - { message: 'must contain less than or equal to 256 items', transform: assocPath(['#e'], range(0, 1024 + 1).map(() => 'f')), }, + { message: 'must be an array', transform: assocPath(['#e'], null) }, + { message: 'must contain less than or equal to 256 items', transform: assocPath(['#e'], range(0, 1024 + 1).map(() => 'f')) }, ], '#e[0]': [ - { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#e', 0], 'f'.repeat(1024 + 1)), }, - { message: 'is not allowed to be empty', transform: assocPath(['#e', 0], ''), }, + { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#e', 0], 'f'.repeat(1024 + 1)) }, + { message: 'is not allowed to be empty', transform: assocPath(['#e', 0], '') }, ], '#p': [ - { message: 'must be an array', transform: assocPath(['#p'], null), }, - { message: 'must contain less than or equal to 256 items', transform: assocPath(['#p'], range(0, 1024 + 1).map(() => 'f')), }, + { message: 'must be an array', transform: assocPath(['#p'], null) }, + { message: 'must contain less than or equal to 256 items', transform: assocPath(['#p'], range(0, 1024 + 1).map(() => 'f')) }, ], '#p[0]': [ - { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#p', 0], 'f'.repeat(1024 + 1)), }, - { message: 'is not allowed to be empty', transform: assocPath(['#p', 0], ''), }, + { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#p', 0], 'f'.repeat(1024 + 1)) }, + { message: 'is not allowed to be empty', transform: assocPath(['#p', 0], '') }, ], '#r': [ - { message: 'must be an array', transform: assocPath(['#r'], null), }, - { message: 'must contain less than or equal to 256 items', transform: assocPath(['#r'], range(0, 1024 + 1).map(() => 'f')), }, + { message: 'must be an array', transform: assocPath(['#r'], null) }, + { message: 'must contain less than or equal to 256 items', transform: assocPath(['#r'], range(0, 1024 + 1).map(() => 'f')) }, ], '#r[0]': [ - { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#r', 0], 'f'.repeat(1024 + 1)), }, - { message: 'is not allowed to be empty', transform: assocPath(['#r', 0], ''), }, + { message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#r', 0], 'f'.repeat(1024 + 1)) }, + { message: 'is not allowed to be empty', transform: assocPath(['#r', 0], '') }, ], } diff --git a/test/unit/schemas/message-schema.spec.ts b/test/unit/schemas/message-schema.spec.ts index 0ce9c56..f003a04 100644 --- a/test/unit/schemas/message-schema.spec.ts +++ b/test/unit/schemas/message-schema.spec.ts @@ -1,11 +1,11 @@ import { expect } from 'chai' import { range } from 'ramda' -import { Event } from '../../../src/@types/event' +import { Event } from '../../../src/@types/event' +import { getEvents } from '../data/events' import { IncomingMessage } from '../../../src/@types/messages' import { messageSchema } from '../../../src/schemas/message-schema' import { validateSchema } from '../../../src/utils/validation' -import { getEvents } from '../data/events' describe('NIP-01', () => { let message: IncomingMessage @@ -20,7 +20,7 @@ describe('NIP-01', () => { events.forEach((event) => { message = [ 'EVENT', - event + event, ] as any const result = validateSchema(messageSchema)(message) diff --git a/test/unit/utils/event.spec.ts b/test/unit/utils/event.spec.ts index 276fc80..fc42aa8 100644 --- a/test/unit/utils/event.spec.ts +++ b/test/unit/utils/event.spec.ts @@ -1,6 +1,17 @@ import { expect } from 'chai' -import { Event, CanonicalEvent } from '../../../src/@types/event' -import { isEphemeralEvent, isEventIdValid, isEventMatchingFilter, isEventSignatureValid, isNullEvent, isReplaceableEvent, serializeEvent } from '../../../src/utils/event' + +import { CanonicalEvent, Event } from '../../../src/@types/event' +import { + isDelegatedEvent, + isDelegatedEventValid, + isEphemeralEvent, + isEventIdValid, + isEventMatchingFilter, + isEventSignatureValid, + isNullEvent, + isReplaceableEvent, + serializeEvent, +} from '../../../src/utils/event' import { EventKinds } from '../../../src/constants/base' describe('NIP-01', () => { @@ -226,7 +237,7 @@ describe('NIP-01', () => { 'kind': 1, 'tags': [], 'content': 'learning terraform rn!', - 'sig': 'ec8b2bc640c8c7e92fbc0e0a6f539da2635068a99809186f15106174d727456132977c78f3371d0ab01c108173df75750f33d8e04c4d7980bbb3fb70ba1e3848' + 'sig': 'ec8b2bc640c8c7e92fbc0e0a6f539da2635068a99809186f15106174d727456132977c78f3371d0ab01c108173df75750f33d8e04c4d7980bbb3fb70ba1e3848', } }) @@ -256,7 +267,7 @@ describe('NIP-01', () => { 'kind': 1, 'tags': [], 'content': 'learning terraform rn!', - 'sig': 'ec8b2bc640c8c7e92fbc0e0a6f539da2635068a99809186f15106174d727456132977c78f3371d0ab01c108173df75750f33d8e04c4d7980bbb3fb70ba1e3848' + 'sig': 'ec8b2bc640c8c7e92fbc0e0a6f539da2635068a99809186f15106174d727456132977c78f3371d0ab01c108173df75750f33d8e04c4d7980bbb3fb70ba1e3848', } }) @@ -335,55 +346,89 @@ describe('isNullEvent', () => { }) }) -describe('NIP-27', () => { - describe('isEventMatchingFilter', () => { - describe('#m filter', () => { - let event: Event - beforeEach(() => { - event = { - tags: [ - [ - 'm', - 'group', - ], - ], - } as any - }) +// describe('NIP-27', () => { +// describe('isEventMatchingFilter', () => { +// describe('#m filter', () => { +// let event: Event +// beforeEach(() => { +// event = { +// tags: [ +// [ +// 'm', +// 'group', +// ], +// ], +// } as any +// }) - it('returns true given non-multicast event and there is no #m filter', () => { - event.tags = [] - expect(isEventMatchingFilter({})(event)).to.be.true - }) +// it('returns true given non-multicast event and there is no #m filter', () => { +// event.tags = [] +// expect(isEventMatchingFilter({})(event)).to.be.true +// }) - it('returns true given multicast event and contained in #m filter', () => { - expect(isEventMatchingFilter({ '#m': ['group'] })(event)).to.be.true - }) +// it('returns true given multicast event and contained in #m filter', () => { +// expect(isEventMatchingFilter({ '#m': ['group'] })(event)).to.be.true +// }) - it('returns true given multicast event and contained second in #m filter', () => { - expect(isEventMatchingFilter({ '#m': ['some group', 'group'] })(event)).to.be.true - }) +// it('returns true given multicast event and contained second in #m filter', () => { +// expect(isEventMatchingFilter({ '#m': ['some group', 'group'] })(event)).to.be.true +// }) - it('returns false given multicast event and not contained in #m filter', () => { - expect(isEventMatchingFilter({ '#m': ['other group'] })(event)).to.be.false - }) +// it('returns false given multicast event and not contained in #m filter', () => { +// expect(isEventMatchingFilter({ '#m': ['other group'] })(event)).to.be.false +// }) - it('returns false if given multicast event and there is no #m filter', () => { - expect(isEventMatchingFilter({})(event)).to.be.false - }) +// it('returns false if given multicast event and there is no #m filter', () => { +// expect(isEventMatchingFilter({})(event)).to.be.false +// }) - it('returns false if given multicast event and #m filter is empty', () => { - expect(isEventMatchingFilter({ '#m': [] })(event)).to.be.false - }) +// it('returns false if given multicast event and #m filter is empty', () => { +// expect(isEventMatchingFilter({ '#m': [] })(event)).to.be.false +// }) - it('returns false given non-multicast event and filter contains some group', () => { - event.tags = [] - expect(isEventMatchingFilter({ '#m': ['group'] })(event)).to.be.false - }) +// it('returns false given non-multicast event and filter contains some group', () => { +// event.tags = [] +// expect(isEventMatchingFilter({ '#m': ['group'] })(event)).to.be.false +// }) - it('returns false given non-multicast event and filter is empty', () => { - event.tags = [] - expect(isEventMatchingFilter({ '#m': [] })(event)).to.be.false - }) +// it('returns false given non-multicast event and filter is empty', () => { +// event.tags = [] +// expect(isEventMatchingFilter({ '#m': [] })(event)).to.be.false +// }) +// }) +// }) +// }) + +describe('NIP-26', () => { + let event: Event + beforeEach(() => { + event = { + 'id': 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc', + 'pubkey': '62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49', + 'created_at': 1660896109, + 'kind': 1, + 'tags': [ + [ + 'delegation', + '86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e', + 'kind=1&created_at>1640995200', + 'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1', + ], + ], + 'content': 'Hello world', + 'sig': 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6', + } + }) + + describe('isDelegatedEvent', () => { + it('returns true if event contains delegation tag', () => { + expect(isDelegatedEvent(event)).to.be.true + }) + }) + + describe('isDelegatedEventValid', () => { + it('resolves with true if delegated event is valid', async () => { + expect(await isDelegatedEventValid(event)).to.be.true }) }) }) diff --git a/test/unit/utils/transform.spec.ts b/test/unit/utils/transform.spec.ts index aac1a6c..12aac51 100644 --- a/test/unit/utils/transform.spec.ts +++ b/test/unit/utils/transform.spec.ts @@ -1,4 +1,5 @@ import { expect } from 'chai' + import { fromBuffer, toBuffer, toJSON } from '../../../src/utils/transform'