feat!: remove nip-26 delegation support (#350)

BREAKING CHANGE: NIP-26 support has been removed. Delegated events will not be handled differently.
This commit is contained in:
Ricardo Arturo Cabral Mejía 2024-01-12 16:12:04 -05:00 committed by GitHub
parent ed30823511
commit 6760ab0e10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 35 additions and 561 deletions

View File

@ -57,7 +57,7 @@ NIPs with a relay-specific implementation are listed here.
- [x] NIP-16: Event Treatment
- [x] NIP-20: Command Results
- [x] NIP-22: Event `created_at` Limits
- [x] NIP-26: Delegated Event Signing
- [ ] NIP-26: Delegated Event Signing (REMOVED)
- [x] NIP-28: Public Chat
- [x] NIP-33: Parameterized Replaceable Events
- [x] NIP-40: Expiration Timestamp

View File

@ -0,0 +1,18 @@
exports.up = async function (knex) {
await knex.schema
.raw('DROP INDEX IF EXISTS pubkey_delegator_kind_idx;')
await knex.schema.alterTable('events', function (table) {
table.dropColumn('event_delegator')
})
}
exports.down = async function (knex) {
await knex.schema.alterTable('events', function (table) {
table.binary('event_delegator').nullable().index()
})
await knex.schema
.raw(
`CREATE UNIQUE INDEX pubkey_delegator_kind_idx
ON events ( event_pubkey, event_delegator, event_kind );`,
)
}

View File

@ -13,7 +13,6 @@
16,
20,
22,
26,
28,
33,
40

View File

@ -1,5 +1,5 @@
import { ContextMetadata, EventId, Pubkey, Tag } from './base'
import { ContextMetadataKey, EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base'
import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base'
export interface BaseEvent {
id: EventId
@ -21,10 +21,6 @@ export type UnsignedEvent = Omit<Event, 'sig'>
export type UnidentifiedEvent = Omit<UnsignedEvent, 'id'>
export interface DelegatedEvent extends Event {
[EventDelegatorMetadataKey]?: Pubkey
}
export interface ExpiringEvent extends Event {
[EventExpirationTimeMetadataKey]?: number
}
@ -42,7 +38,6 @@ export interface DBEvent {
event_content: string
event_tags: Tag[]
event_signature: Buffer
event_delegator?: Buffer | null
event_deduplication?: string | null
first_seen: Date
deleted_at?: Date

View File

@ -5,7 +5,7 @@ export type SubscriptionId = string
export interface SubscriptionFilter {
ids?: EventId[]
kinds?: EventKinds[]
kinds?: (EventKinds | number)[]
since?: number
until?: number
authors?: Pubkey[]

View File

@ -37,7 +37,6 @@ export enum EventTags {
Event = 'e',
Pubkey = 'p',
// Multicast = 'm',
Delegation = 'delegation',
Deduplication = 'd',
Expiration = 'expiration',
Invoice = 'bolt11',
@ -49,7 +48,6 @@ export enum PaymentsProcessors {
LNBITS = 'lnbits',
}
export const EventDelegatorMetadataKey = Symbol('Delegator')
export const EventDeduplicationMetadataKey = Symbol('Deduplication')
export const ContextMetadataKey = Symbol('Context')
export const EventExpirationTimeMetadataKey = Symbol('Expiration')

View File

@ -1,21 +0,0 @@
import { isDeleteEvent, isEphemeralEvent, isReplaceableEvent } from '../utils/event'
import { DefaultEventStrategy } from '../handlers/event-strategies/default-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'
export const delegatedEventStrategyFactory = (
eventRepository: IEventRepository,
): Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]> =>
([event, adapter]: [Event, IWebSocketAdapter]) => {
if (isEphemeralEvent(event)) {
return new EphemeralEventStrategy(adapter)
} else if (isReplaceableEvent(event) || isDeleteEvent(event)) {
return
}
return new DefaultEventStrategy(adapter, eventRepository)
}

View File

@ -1,11 +1,8 @@
import { IEventRepository, IUserRepository } from '../@types/repositories'
import { IncomingMessage, MessageType } from '../@types/messages'
import { createSettings } from './settings-factory'
import { DelegatedEventMessageHandler } from '../handlers/delegated-event-message-handler'
import { delegatedEventStrategyFactory } from './delegated-event-strategy-factory'
import { EventMessageHandler } from '../handlers/event-message-handler'
import { eventStrategyFactory } from './event-strategy-factory'
import { isDelegatedEvent } from '../utils/event'
import { IWebSocketAdapter } from '../@types/adapters'
import { slidingWindowRateLimiterFactory } from './rate-limiter-factory'
import { SubscribeMessageHandler } from '../handlers/subscribe-message-handler'
@ -18,16 +15,6 @@ export const messageHandlerFactory = (
switch (message[0]) {
case MessageType.EVENT:
{
if (isDelegatedEvent(message[1])) {
return new DelegatedEventMessageHandler(
adapter,
delegatedEventStrategyFactory(eventRepository),
userRepository,
createSettings,
slidingWindowRateLimiterFactory,
)
}
return new EventMessageHandler(
adapter,
eventStrategyFactory(eventRepository),
@ -39,7 +26,7 @@ export const messageHandlerFactory = (
case MessageType.REQ:
return new SubscribeMessageHandler(adapter, eventRepository, createSettings)
case MessageType.CLOSE:
return new UnsubscribeMessageHandler(adapter,)
return new UnsubscribeMessageHandler(adapter)
default:
throw new Error(`Unknown message type: ${String(message[0]).substring(0, 64)}`)
}

View File

@ -1,75 +0,0 @@
import { EventDelegatorMetadataKey, EventTags } from '../constants/base'
import { createCommandResult } from '../utils/messages'
import { createLogger } from '../factories/logger-factory'
import { DelegatedEvent } from '../@types/event'
import { EventMessageHandler } from './event-message-handler'
import { IMessageHandler } from '../@types/message-handlers'
import { IncomingEventMessage } from '../@types/messages'
import { isDelegatedEventValid } from '../utils/event'
import { WebSocketAdapterEvent } from '../constants/adapter'
const debug = createLogger('delegated-event-message-handler')
export class DelegatedEventMessageHandler extends EventMessageHandler implements IMessageHandler {
public async handleMessage(message: IncomingEventMessage): Promise<void> {
const [, event] = message
let reason = await this.isEventValid(event)
if (reason) {
debug('event %s rejected: %s', event.id, reason)
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
return
}
if (await this.isRateLimited(event)) {
debug('event %s rejected: rate-limited')
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'rate-limited: slow down'))
return
}
reason = this.canAcceptEvent(event)
if (reason) {
debug('event %s rejected: %s', event.id, reason)
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
return
}
reason = await this.isUserAdmitted(event)
if (reason) {
debug('event %s rejected: %s', event.id, reason)
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, reason))
return
}
const [, delegator] = event.tags.find((tag) => tag.length === 4 && tag[0] === EventTags.Delegation)
const delegatedEvent: DelegatedEvent = {
...event,
[EventDelegatorMetadataKey]: delegator,
}
const strategy = this.strategyFactory([delegatedEvent, this.webSocket])
if (typeof strategy?.execute !== 'function') {
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'error: event not supported'))
return
}
try {
await strategy.execute(delegatedEvent)
} catch (error) {
console.error('error handling message', message, error)
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, false, 'error: unable to process event'))
}
}
protected async isEventValid(event: DelegatedEvent): Promise<string | undefined> {
const reason = await super.isEventValid(event)
if (reason) {
return reason
}
if (!await isDelegatedEventValid(event)) {
return 'invalid: delegation verification failed'
}
}
}

View File

@ -28,7 +28,7 @@ import {
toPairs,
} from 'ramda'
import { ContextMetadataKey, EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventExpirationTimeMetadataKey } from '../constants/base'
import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey } from '../constants/base'
import { DatabaseClient, EventId } from '../@types/base'
import { DBEvent, Event } from '../@types/event'
import { IEventRepository, IQueryResult } from '../@types/repositories'
@ -105,7 +105,7 @@ export class EventRepository implements IEventRepository {
])(currentFilter[filterName] as string[])
})
})({
authors: ['event_pubkey', 'event_delegator'],
authors: ['event_pubkey'],
ids: ['event_id'],
})
@ -180,11 +180,6 @@ 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),
),
remote_address: path([ContextMetadataKey as any, 'remoteAddress', 'address']),
expires_at: ifElse(
propSatisfies(is(Number), EventExpirationTimeMetadataKey),
@ -212,11 +207,6 @@ 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_deduplication: ifElse(
propSatisfies(isNil, EventDeduplicationMetadataKey),
pipe(paths([['pubkey'], ['kind']]), toJSON),

View File

@ -1,6 +1,6 @@
import * as secp256k1 from '@noble/secp256k1'
import { applySpec, converge, curry, mergeLeft, nth, omit, pipe, prop, reduceBy } from 'ramda'
import { applySpec, pipe, prop } from 'ramda'
import { CanonicalEvent, DBEvent, Event, UnidentifiedEvent, UnsignedEvent } from '../@types/event'
import { createCipheriv, getRandomValues } from 'crypto'
import { EventId, Pubkey, Tag } from '../@types/base'
@ -12,7 +12,6 @@ import { EventKindsRange } from '../@types/settings'
import { fromBuffer } from './transform'
import { getLeadingZeroBits } from './proof-of-work'
import { isGenericTagQuery } from './filter'
import { RuneLike } from './runes/rune-like'
import { SubscriptionFilter } from '../@types/subscription'
import { WebSocketServerAdapterEvent } from '../constants/adapter'
@ -68,18 +67,7 @@ export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Eve
if (
!filter.authors.some(startsWith(event.pubkey))
) {
if (isDelegatedEvent(event)) {
const delegation = event.tags.find((tag) => tag[0] === EventTags.Delegation)
if (typeof delegation === 'undefined') {
return false
}
if (!filter.authors.some(startsWith(delegation[1]))) {
return false
}
} else {
return false
}
return false
}
}
@ -116,51 +104,6 @@ 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<boolean> => {
const delegation = event.tags.find((tag) => tag.length === 4 && tag[0] === EventTags.Delegation)
if (!delegation) {
return false
}
// Validate rune
const runifiedEvent = (converge(
curry(mergeLeft),
[
omit(['tags']),
pipe(
prop('tags') as any,
reduceBy<EventTags, string[]>(
(acc, tag) => ([...acc, tag[1]]),
[],
nth(0) as any,
),
),
],
) as any)(event)
let result: boolean
try {
[result] = RuneLike.from(delegation[2]).test(runifiedEvent)
} catch (error) {
result = false
}
if (!result) {
return false
}
const serializedDelegationTag = `nostr:${delegation[0]}:${event.pubkey}:${delegation[2]}`
const token = await secp256k1.utils.sha256(Buffer.from(serializedDelegationTag))
return secp256k1.schnorr.verify(delegation[3], token, delegation[1])
}
export const getEventHash = async (event: Event | UnidentifiedEvent | UnsignedEvent): Promise<string> => {
const id = await secp256k1.utils.sha256(Buffer.from(JSON.stringify(serializeEvent(event))))

View File

@ -1,46 +0,0 @@
import { expect } from 'chai'
import { DefaultEventStrategy } from '../../../src/handlers/event-strategies/default-event-strategy'
import { delegatedEventStrategyFactory } from '../../../src/factories/delegated-event-strategy-factory'
import { EphemeralEventStrategy } from '../../../src/handlers/event-strategies/ephemeral-event-strategy'
import { Event } from '../../../src/@types/event'
import { EventKinds } from '../../../src/constants/base'
import { Factory } from '../../../src/@types/base'
import { IEventRepository } from '../../../src/@types/repositories'
import { IEventStrategy } from '../../../src/@types/message-handlers'
import { IWebSocketAdapter } from '../../../src/@types/adapters'
describe('delegatedEventStrategyFactory', () => {
let eventRepository: IEventRepository
let event: Event
let adapter: IWebSocketAdapter
let factory: Factory<IEventStrategy<Event, Promise<void>>, [Event, IWebSocketAdapter]>
beforeEach(() => {
eventRepository = {} as any
event = {} as any
adapter = {} as any
factory = delegatedEventStrategyFactory(eventRepository)
})
it('returns EphemeralEventStrategy given a set_metadata event', () => {
event.kind = EventKinds.EPHEMERAL_FIRST
expect(factory([event, adapter])).to.be.an.instanceOf(EphemeralEventStrategy)
})
it('returns DefaultEventStrategy given a text_note event', () => {
event.kind = EventKinds.TEXT_NOTE
expect(factory([event, adapter])).to.be.an.instanceOf(DefaultEventStrategy)
})
it('returns undefined given a replaceable event', () => {
event.kind = EventKinds.REPLACEABLE_FIRST
expect(factory([event, adapter])).to.be.undefined
})
it('returns undefined given a delete event', () => {
event.kind = EventKinds.DELETE
expect(factory([event, adapter])).to.be.undefined
})
})

View File

@ -2,10 +2,8 @@ import { expect } from 'chai'
import { IEventRepository, IUserRepository } from '../../../src/@types/repositories'
import { IncomingMessage, MessageType } from '../../../src/@types/messages'
import { DelegatedEventMessageHandler } from '../../../src/handlers/delegated-event-message-handler'
import { Event } from '../../../src/@types/event'
import { EventMessageHandler } from '../../../src/handlers/event-message-handler'
import { EventTags } from '../../../src/constants/base'
import { IWebSocketAdapter } from '../../../src/@types/adapters'
import { messageHandlerFactory } from '../../../src/factories/message-handler-factory'
import { SubscribeMessageHandler } from '../../../src/handlers/subscribe-message-handler'
@ -38,18 +36,6 @@ describe('messageHandlerFactory', () => {
expect(factory([message, adapter])).to.be.an.instanceOf(EventMessageHandler)
})
it('returns DelegatedEventMessageHandler when given an EVENT message with delegated event', () => {
event.tags = [
[EventTags.Delegation, '', '', ''],
]
message = [
MessageType.EVENT,
event,
]
expect(factory([message, adapter])).to.be.an.instanceOf(DelegatedEventMessageHandler)
})
it('returns SubscribeMessageHandler when given a REQ message', () => {
message = [
MessageType.REQ,

View File

@ -1,229 +0,0 @@
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import EventEmitter from 'events'
import Sinon from 'sinon'
import sinonChai from 'sinon-chai'
chai.use(sinonChai)
chai.use(chaiAsPromised)
import { IncomingEventMessage, MessageType } from '../../../src/@types/messages'
import { DelegatedEventMessageHandler } from '../../../src/handlers/delegated-event-message-handler'
import { Event } from '../../../src/@types/event'
import { EventMessageHandler } from '../../../src/handlers/event-message-handler'
import { EventTags } from '../../../src/constants/base'
import { IUserRepository } from '../../../src/@types/repositories'
import { WebSocketAdapterEvent } from '../../../src/constants/adapter'
const { expect } = chai
describe('DelegatedEventMessageHandler', () => {
let webSocket: EventEmitter
let handler: DelegatedEventMessageHandler
let userRepository: IUserRepository
let event: Event
let message: IncomingEventMessage
let sandbox: Sinon.SinonSandbox
let originalConsoleWarn: any = undefined
beforeEach(() => {
sandbox = Sinon.createSandbox()
originalConsoleWarn = console.warn
console.warn = () => undefined
event = {
content: 'hello',
created_at: 1665546189,
id: 'f'.repeat(64),
kind: 1,
pubkey: 'f'.repeat(64),
sig: 'f'.repeat(128),
tags: [
[EventTags.Delegation, 'delegator', 'rune', 'signature'],
],
}
})
afterEach(() => {
console.warn = originalConsoleWarn
sandbox.restore()
})
describe('handleMessage', () => {
let canAcceptEventStub: Sinon.SinonStub
let isEventValidStub: Sinon.SinonStub
let strategyFactoryStub: Sinon.SinonStub
let onMessageSpy: Sinon.SinonSpy
let strategyExecuteStub: Sinon.SinonStub
let isRateLimitedStub: Sinon.SinonStub
let isUserAdmitted: Sinon.SinonStub
beforeEach(() => {
canAcceptEventStub = sandbox.stub(DelegatedEventMessageHandler.prototype, 'canAcceptEvent' as any)
isEventValidStub = sandbox.stub(DelegatedEventMessageHandler.prototype, 'isEventValid' as any)
isUserAdmitted = sandbox.stub(EventMessageHandler.prototype, 'isUserAdmitted' as any)
strategyExecuteStub = sandbox.stub()
strategyFactoryStub = sandbox.stub().returns({
execute: strategyExecuteStub,
})
onMessageSpy = sandbox.fake.returns(undefined)
webSocket = new EventEmitter()
webSocket.on(WebSocketAdapterEvent.Message, onMessageSpy)
message = [MessageType.EVENT, event]
isRateLimitedStub = sandbox.stub(EventMessageHandler.prototype, 'isRateLimited' as any)
handler = new DelegatedEventMessageHandler(
webSocket as any,
strategyFactoryStub,
userRepository,
() => ({}) as any,
() => ({ hit: async () => false }),
)
})
afterEach(() => {
isEventValidStub.restore()
canAcceptEventStub.restore()
webSocket.removeAllListeners()
})
it('rejects event if it can\'t be accepted', async () => {
canAcceptEventStub.returns('reason')
await handler.handleMessage(message)
expect(canAcceptEventStub).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).to.have.been.calledOnceWithExactly(
[
MessageType.OK,
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
false,
'reason',
],
)
expect(strategyFactoryStub).not.to.have.been.called
})
it('rejects event if invalid', async () => {
isEventValidStub.returns('reason')
await handler.handleMessage(message)
expect(isEventValidStub).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).not.to.have.been.calledOnceWithExactly()
expect(strategyFactoryStub).not.to.have.been.called
})
it('rejects event if rate-limited', async () => {
isRateLimitedStub.resolves(true)
await handler.handleMessage(message)
expect(isRateLimitedStub).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).to.have.been.calledOnceWithExactly(
[MessageType.OK, event.id, false, 'rate-limited: slow down'],
)
expect(strategyFactoryStub).not.to.have.been.called
})
it('rejects event is user is not admitted', async () => {
isUserAdmitted.resolves('not admitted')
await handler.handleMessage(message)
expect(isRateLimitedStub).to.have.been.calledOnceWithExactly(event)
expect(isUserAdmitted).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).to.have.been.calledOnceWithExactly([
MessageType.OK,
event.id,
false,
'not admitted',
])
})
it('does not call strategy if none given', async () => {
isEventValidStub.returns(undefined)
canAcceptEventStub.returns(undefined)
strategyFactoryStub.returns(undefined)
await handler.handleMessage(message)
expect(isEventValidStub).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).not.to.have.been.calledOnceWithExactly()
expect(strategyFactoryStub).to.have.been.calledOnceWithExactly([
event,
webSocket,
])
expect(strategyExecuteStub).not.to.have.been.called
})
it('calls strategy with event', async () => {
isEventValidStub.returns(undefined)
canAcceptEventStub.returns(undefined)
await handler.handleMessage(message)
expect(isEventValidStub).to.have.been.calledOnceWithExactly(event)
expect(onMessageSpy).not.to.have.been.calledOnceWithExactly()
expect(strategyFactoryStub).to.have.been.calledOnceWithExactly([
event,
webSocket,
])
expect(strategyExecuteStub).to.have.been.calledOnceWithExactly(event)
})
it('does not reject if strategy rejects', async () => {
const error = new Error('mistakes were made')
isEventValidStub.returns(undefined)
canAcceptEventStub.returns(undefined)
strategyExecuteStub.rejects(error)
return expect(handler.handleMessage(message)).to.eventually.be.fulfilled
})
})
describe('isEventValid', () => {
let parentIsEventValidStub: Sinon.SinonStub
beforeEach(() => {
parentIsEventValidStub = Sinon.stub(EventMessageHandler.prototype, 'isEventValid' as any)
event = {
'id': 'a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc',
'pubkey': '62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49',
'created_at': 1660896109,
'kind': 1,
'tags': [
[
EventTags.Delegation,
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e',
'kind=1&created_at>1640995200',
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1',
],
],
'content': 'Hello world',
'sig': 'cd4a3cd20dc61dcbc98324de561a07fd23b3d9702115920c0814b5fb822cc5b7c5bcdaf3fa326d24ed50c5b9c8214d66c75bae34e3a84c25e4d122afccb66eb6',
}
})
afterEach(() => {
parentIsEventValidStub.restore()
})
it('returns undefined if event and delegate tag is valid', async () => {
parentIsEventValidStub.resolves(undefined)
expect(await (handler as any).isEventValid(event)).to.be.undefined
})
it('returns reason if event is not valid', () => {
parentIsEventValidStub.resolves('reason')
return expect((handler as any).isEventValid(event)).to.eventually.equal('reason')
})
it('returns reason if delegate signature is not valid', () => {
parentIsEventValidStub.resolves(undefined)
event.tags[0][3] = 'wrong sig'
return expect((handler as any).isEventValid(event)).to.eventually.equal('invalid: delegation verification failed')
})
})
})

View File

@ -74,7 +74,7 @@ describe('EventRepository', () => {
const query = repository.findByFilters(filters).toString()
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 limit 500')
expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\')) order by "event_created_at" asc limit 500')
})
it('selects events by two authors', () => {
@ -89,7 +89,7 @@ describe('EventRepository', () => {
const query = repository.findByFilters(filters).toString()
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 limit 500')
expect(query).to.equal('select * from "events" where ("event_pubkey" in (X\'22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793\', X\'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\')) order by "event_created_at" asc limit 500')
})
it('selects events by one author prefix (even length)', () => {
@ -103,7 +103,7 @@ describe('EventRepository', () => {
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_delegator" from 1 for 3) = X\'22e804\') order by "event_created_at" asc limit 500')
expect(query).to.equal('select * from "events" where (substring("event_pubkey" from 1 for 3) = X\'22e804\') order by "event_created_at" asc limit 500')
})
it('selects events by one author prefix (odd length)', () => {
@ -117,7 +117,7 @@ describe('EventRepository', () => {
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\' or substring("event_delegator" from 1 for 4) BETWEEN E\'\\\\x22e804f0\' AND E\'\\\\x22e804ff\') order by "event_created_at" asc limit 500')
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 limit 500')
})
it('selects events by two author prefix (first even, second odd)', () => {
@ -132,7 +132,7 @@ describe('EventRepository', () => {
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_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 limit 500')
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 limit 500')
})
})
@ -363,7 +363,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 limit 500) 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 limit 500) union (select * from "events" where "event_created_at" >= 1000 order by "event_created_at" asc limit 500) union (select * from "events" where "event_created_at" <= 1000 order by "event_created_at" asc limit 500) union (select * from "events" order by "event_created_at" DESC limit 1000) order by "event_created_at" asc limit 500')
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 limit 500) union (select * from "events" where (substring("event_pubkey" from 1 for 3) BETWEEN E\'\\\\xbbbbb0\' AND E\'\\\\xbbbbbf\') order by "event_created_at" asc limit 500) union (select * from "events" where "event_created_at" >= 1000 order by "event_created_at" asc limit 500) union (select * from "events" where "event_created_at" <= 1000 order by "event_created_at" asc limit 500) union (select * from "events" order by "event_created_at" DESC limit 1000) order by "event_created_at" asc limit 500')
})
})
})
@ -435,7 +435,7 @@ describe('EventRepository', () => {
const query = (repository as any).insert(event).toString()
expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") 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"]]\', NULL, \'::1\') on conflict do nothing')
expect(query).to.equal('insert into "events" ("event_content", "event_created_at", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") 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"]]\', NULL, \'::1\') on conflict do nothing')
})
})
@ -462,7 +462,7 @@ describe('EventRepository', () => {
const query = repository.upsert(event).toString()
expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503",0]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626')
expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503",0]\', X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626')
})
it('replaces event based on event_pubkey, event_kind and event_deduplication', () => {
@ -480,7 +480,7 @@ describe('EventRepository', () => {
const query = repository.upsert(event).toString()
expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_delegator", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["deduplication"]\', NULL, X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"event_delegator" = NULL,"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626')
expect(query).to.equal('insert into "events" ("deleted_at", "event_content", "event_created_at", "event_deduplication", "event_id", "event_kind", "event_pubkey", "event_signature", "event_tags", "expires_at", "remote_address") values (NULL, \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\', 1564498626, \'["deduplication"]\', X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\', 0, X\'55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503\', X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\', \'[]\', NULL, \'::1\') on conflict (event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR event_kind = 41 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000) do update set "event_id" = X\'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a\',"event_created_at" = 1564498626,"event_tags" = \'[]\',"event_content" = \'{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}\',"event_signature" = X\'d1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9\',"remote_address" = \'::1\',"expires_at" = NULL,"deleted_at" = NULL where "events"."event_created_at" < 1564498626')
})
})
})

View File

@ -3,8 +3,6 @@ import { expect } from 'chai'
import { CanonicalEvent, Event } from '../../../src/@types/event'
import {
getEventExpiration,
isDelegatedEvent,
isDelegatedEventValid,
isDeleteEvent,
isEphemeralEvent,
isEventIdValid,
@ -400,75 +398,6 @@ describe('NIP-16', () => {
// })
// })
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
})
it('resolves with false if no delegation tag is found', async () => {
event.tags = []
expect(await isDelegatedEventValid(event)).to.be.false
})
it('resolves with false if delegation signature is invalid', async () => {
event.tags[0][3] = 'f'
expect(await isDelegatedEventValid(event)).to.be.false
})
it('resolves with false if delegation rule is not a valid rune', async () => {
event.tags[0][2] = '@'
expect(await isDelegatedEventValid(event)).to.be.false
})
it('resolves with false if no delegation rule does not match', async () => {
event.tags[0][2] = 'a=1'
expect(await isDelegatedEventValid(event)).to.be.false
})
})
describe('isEventMatchingFilter', () => {
it('returns true if author is delegator', () => {
expect(
isEventMatchingFilter({ authors: ['86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e'] })(event)
).to.be.true
})
it('returns false if author is not delegator', () => {
expect(
isEventMatchingFilter({ authors: ['e8b487c079b0f67c695ae6c4c2552a47f38adfa2533cc5926bd2c102942fdcb7'] })(event)
).to.be.false
})
})
})
describe('NIP-09', () => {
describe('isDeleteEvent', () => {
it('returns true if event is kind 5', () => {