From 2bdcb3fc1476aae93094d3708f97c8039032c159 Mon Sep 17 00:00:00 2001 From: vikashsiwach Date: Thu, 9 Apr 2026 18:16:51 +0530 Subject: [PATCH 1/4] feat: add NIP-62 vanish event support --- src/@types/repositories.ts | 2 + src/constants/base.ts | 4 + src/factories/event-strategy-factory.ts | 7 +- src/factories/message-handler-factory.ts | 1 + src/handlers/event-message-handler.ts | 49 ++++++++++-- .../event-strategies/vanish-event-strategy.ts | 33 ++++++++ src/repositories/event-repository.ts | 26 ++++++- src/utils/event.ts | 16 +++- .../factories/event-strategy-factory.spec.ts | 6 ++ .../handlers/event-message-handler.spec.ts | 20 +++++ .../vanish-event-strategy.spec.ts | 75 +++++++++++++++++++ .../repositories/event-repository.spec.ts | 44 ++++++++++- test/unit/utils/event.spec.ts | 59 ++++++++++++++- 13 files changed, 327 insertions(+), 15 deletions(-) create mode 100644 src/handlers/event-strategies/vanish-event-strategy.ts create mode 100644 test/unit/handlers/event-strategies/vanish-event-strategy.spec.ts diff --git a/src/@types/repositories.ts b/src/@types/repositories.ts index 671dfaee..309648d3 100644 --- a/src/@types/repositories.ts +++ b/src/@types/repositories.ts @@ -17,6 +17,8 @@ export interface IEventRepository { upsert(event: Event): Promise findByFilters(filters: SubscriptionFilter[]): IQueryResult deleteByPubkeyAndIds(pubkey: Pubkey, ids: EventId[]): Promise + deleteByPubkeyExceptKinds(pubkey: Pubkey, excludedKinds: number[]): Promise + hasActiveRequestToVanish(pubkey: Pubkey): Promise } export interface IInvoiceRepository { diff --git a/src/constants/base.ts b/src/constants/base.ts index 8b9adca8..974e6c39 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -7,6 +7,7 @@ export enum EventKinds { DELETE = 5, REPOST = 6, REACTION = 7, + REQUEST_TO_VANISH = 62, // Channels CHANNEL_CREATION = 40, CHANNEL_METADATA = 41, @@ -36,12 +37,15 @@ export enum EventKinds { export enum EventTags { Event = 'e', Pubkey = 'p', + Relay = 'r', // Multicast = 'm', Deduplication = 'd', Expiration = 'expiration', Invoice = 'bolt11', } +export const ALL_RELAYS = 'ALL_RELAYS' + export enum PaymentsProcessors { LNURL = 'lnurl', ZEBEDEE = 'zebedee', diff --git a/src/factories/event-strategy-factory.ts b/src/factories/event-strategy-factory.ts index 91a729f3..1e4b5af9 100644 --- a/src/factories/event-strategy-factory.ts +++ b/src/factories/event-strategy-factory.ts @@ -1,4 +1,4 @@ -import { isDeleteEvent, isEphemeralEvent, isParameterizedReplaceableEvent, isReplaceableEvent } from '../utils/event' +import { isDeleteEvent, isEphemeralEvent, isParameterizedReplaceableEvent, isReplaceableEvent, isRequestToVanishEvent } 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' @@ -9,12 +9,15 @@ import { IEventStrategy } from '../@types/message-handlers' import { IWebSocketAdapter } from '../@types/adapters' import { ParameterizedReplaceableEventStrategy } from '../handlers/event-strategies/parameterized-replaceable-event-strategy' import { ReplaceableEventStrategy } from '../handlers/event-strategies/replaceable-event-strategy' +import { VanishEventStrategy } from '../handlers/event-strategies/vanish-event-strategy' export const eventStrategyFactory = ( eventRepository: IEventRepository, ): Factory>, [Event, IWebSocketAdapter]> => ([event, adapter]: [Event, IWebSocketAdapter]) => { - if (isReplaceableEvent(event)) { + if (isRequestToVanishEvent(event)) { + return new VanishEventStrategy(adapter, eventRepository) + } else if (isReplaceableEvent(event)) { return new ReplaceableEventStrategy(adapter, eventRepository) } else if (isEphemeralEvent(event)) { return new EphemeralEventStrategy(adapter) diff --git a/src/factories/message-handler-factory.ts b/src/factories/message-handler-factory.ts index b1c11e9c..34c37493 100644 --- a/src/factories/message-handler-factory.ts +++ b/src/factories/message-handler-factory.ts @@ -18,6 +18,7 @@ export const messageHandlerFactory = ( return new EventMessageHandler( adapter, eventStrategyFactory(eventRepository), + eventRepository, userRepository, createSettings, slidingWindowRateLimiterFactory, diff --git a/src/handlers/event-message-handler.ts b/src/handlers/event-message-handler.ts index b3ecf7fc..900b8293 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -1,15 +1,26 @@ -import { Event, ExpiringEvent } from '../@types/event' +import { ContextMetadataKey, EventExpirationTimeMetadataKey } from '../constants/base' +import { Event, ExpiringEvent } from '../@types/event' import { EventRateLimit, FeeSchedule, Settings } from '../@types/settings' -import { getEventExpiration, getEventProofOfWork, getPubkeyProofOfWork, getPublicKey, getRelayPrivateKey, isEventIdValid, isEventKindOrRangeMatch, isEventSignatureValid, isExpiredEvent } from '../utils/event' +import { + getEventExpiration, + getEventProofOfWork, + getPubkeyProofOfWork, + getPublicKey, + getRelayPrivateKey, + isEventIdValid, + isEventKindOrRangeMatch, + isEventSignatureValid, + isExpiredEvent, + isRequestToVanishEvent, + isValidRequestToVanishEvent, +} from '../utils/event' +import { IEventRepository, IUserRepository } from '../@types/repositories' import { IEventStrategy, IMessageHandler } from '../@types/message-handlers' -import { ContextMetadataKey } from '../constants/base' import { createCommandResult } from '../utils/messages' import { createLogger } from '../factories/logger-factory' -import { EventExpirationTimeMetadataKey } from '../constants/base' import { Factory } from '../@types/base' import { IncomingEventMessage } from '../@types/messages' import { IRateLimiter } from '../@types/utils' -import { IUserRepository } from '../@types/repositories' import { IWebSocketAdapter } from '../@types/adapters' import { WebSocketAdapterEvent } from '../constants/adapter' @@ -19,6 +30,7 @@ export class EventMessageHandler implements IMessageHandler { public constructor( protected readonly webSocket: IWebSocketAdapter, protected readonly strategyFactory: Factory>, [Event, IWebSocketAdapter]>, + protected readonly eventRepository: IEventRepository, protected readonly userRepository: IUserRepository, private readonly settings: () => Settings, private readonly slidingWindowRateLimiter: Factory, @@ -57,6 +69,13 @@ export class EventMessageHandler implements IMessageHandler { return } + reason = await this.isBlockedByRequestToVanish(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) @@ -190,6 +209,26 @@ export class EventMessageHandler implements IMessageHandler { if (!await isEventSignatureValid(event)) { return 'invalid: event signature verification failed' } + + if (isRequestToVanishEvent(event) && !isValidRequestToVanishEvent(event, this.settings().info.relay_url)) { + return 'invalid: request to vanish relay tag invalid' + } + } + + protected async isBlockedByRequestToVanish(event: Event): Promise { + if (isRequestToVanishEvent(event)) { + return + } + + const relayPubkey = this.getRelayPublicKey() + if (relayPubkey === event.pubkey) { + return + } + + const existingVanishRequest = await this.eventRepository.hasActiveRequestToVanish(event.pubkey) + if (existingVanishRequest) { + return 'blocked: request to vanish active for pubkey' + } } protected async isRateLimited(event: Event): Promise { diff --git a/src/handlers/event-strategies/vanish-event-strategy.ts b/src/handlers/event-strategies/vanish-event-strategy.ts new file mode 100644 index 00000000..32dbb494 --- /dev/null +++ b/src/handlers/event-strategies/vanish-event-strategy.ts @@ -0,0 +1,33 @@ +import { createCommandResult } from '../../utils/messages' +import { createLogger } from '../../factories/logger-factory' +import { Event } from '../../@types/event' +import { EventKinds } from '../../constants/base' +import { IEventRepository } from '../../@types/repositories' +import { IEventStrategy } from '../../@types/message-handlers' +import { IWebSocketAdapter } from '../../@types/adapters' +import { WebSocketAdapterEvent } from '../../constants/adapter' + +const debug = createLogger('vanish-event-strategy') + +export class VanishEventStrategy implements IEventStrategy> { + public constructor( + private readonly webSocket: IWebSocketAdapter, + private readonly eventRepository: IEventRepository, + ) {} + + public async execute(event: Event): Promise { + debug('received request to vanish event: %o', event) + + await this.eventRepository.deleteByPubkeyExceptKinds( + event.pubkey, + [EventKinds.REQUEST_TO_VANISH], + ) + + const count = await this.eventRepository.create(event) + + this.webSocket.emit( + WebSocketAdapterEvent.Message, + createCommandResult(event.id, true, count ? '' : 'duplicate:') + ) + } +} \ No newline at end of file diff --git a/src/repositories/event-repository.ts b/src/repositories/event-repository.ts index 9c645b8a..e17745fa 100644 --- a/src/repositories/event-repository.ts +++ b/src/repositories/event-repository.ts @@ -28,7 +28,7 @@ import { toPairs, } from 'ramda' -import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey } from '../constants/base' +import { ContextMetadataKey, EventDeduplicationMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base' import { DatabaseClient, EventId } from '../@types/base' import { DBEvent, Event } from '../@types/event' import { IEventRepository, IQueryResult } from '../@types/repositories' @@ -246,9 +246,33 @@ export class EventRepository implements IEventRepository { return this.masterDbClient('events') .where('event_pubkey', toBuffer(pubkey)) .whereIn('event_id', map(toBuffer)(eventIdsToDelete)) + .whereNot('event_kind', EventKinds.REQUEST_TO_VANISH) .whereNull('deleted_at') .update({ deleted_at: this.masterDbClient.raw('now()'), }) } + + public deleteByPubkeyExceptKinds(pubkey: string, excludedKinds: number[]): Promise { + debug('deleting events from %s except kinds %o', pubkey, excludedKinds) + + return this.masterDbClient('events') + .where('event_pubkey', toBuffer(pubkey)) + .whereNotIn('event_kind', excludedKinds) + .whereNull('deleted_at') + .update({ + deleted_at: this.masterDbClient.raw('now()'), + }) + } + + public async hasActiveRequestToVanish(pubkey: string): Promise { + const result = await this.readReplicaDbClient('events') + .select('event_id') + .where('event_pubkey', toBuffer(pubkey)) + .where('event_kind', EventKinds.REQUEST_TO_VANISH) + .whereNull('deleted_at') + .first() + + return Boolean(result) + } } diff --git a/src/utils/event.ts b/src/utils/event.ts index 9c4bafd5..c7b4b1c8 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -1,11 +1,9 @@ import * as secp256k1 from '@noble/secp256k1' - +import { ALL_RELAYS, EventKinds, EventTags } from '../constants/base' 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' -import { EventKinds, EventTags } from '../constants/base' - import cluster from 'cluster' import { deriveFromSecret } from './secret' import { EventKindsRange } from '../@types/settings' @@ -227,6 +225,18 @@ export const isDeleteEvent = (event: Event): boolean => { return event.kind === EventKinds.DELETE } +export const isRequestToVanishEvent = (event: Event): boolean => { + return event.kind === EventKinds.REQUEST_TO_VANISH +} + +export const isValidRequestToVanishEvent = (event: Event, relayUrl: string): boolean => { + const relayTags = event.tags + .filter((tag) => tag.length >= 2 && tag[0] === EventTags.Relay) + .map((tag) => tag[1]) + + return relayTags.length > 0 && relayTags.every((relay) => relay === relayUrl || relay === ALL_RELAYS) +} + export const isExpiredEvent = (event: Event): boolean => { if (!event.tags.length) { return false diff --git a/test/unit/factories/event-strategy-factory.spec.ts b/test/unit/factories/event-strategy-factory.spec.ts index c53fe083..46140807 100644 --- a/test/unit/factories/event-strategy-factory.spec.ts +++ b/test/unit/factories/event-strategy-factory.spec.ts @@ -12,6 +12,7 @@ import { IEventStrategy } from '../../../src/@types/message-handlers' import { IWebSocketAdapter } from '../../../src/@types/adapters' import { ParameterizedReplaceableEventStrategy } from '../../../src/handlers/event-strategies/parameterized-replaceable-event-strategy' import { ReplaceableEventStrategy } from '../../../src/handlers/event-strategies/replaceable-event-strategy' +import { VanishEventStrategy } from '../../../src/handlers/event-strategies/vanish-event-strategy' describe('eventStrategyFactory', () => { let eventRepository: IEventRepository @@ -52,6 +53,11 @@ describe('eventStrategyFactory', () => { expect(factory([event, adapter])).to.be.an.instanceOf(DeleteEventStrategy) }) + it('returns VanishEventStrategy given a request to vanish event', () => { + event.kind = EventKinds.REQUEST_TO_VANISH + expect(factory([event, adapter])).to.be.an.instanceOf(VanishEventStrategy) + }) + it('returns ParameterizedReplaceableEventStrategy given a delete event', () => { event.kind = EventKinds.PARAMETERIZED_REPLACEABLE_FIRST expect(factory([event, adapter])).to.be.an.instanceOf(ParameterizedReplaceableEventStrategy) diff --git a/test/unit/handlers/event-message-handler.spec.ts b/test/unit/handlers/event-message-handler.spec.ts index c28d5e0a..6895ec7c 100644 --- a/test/unit/handlers/event-message-handler.spec.ts +++ b/test/unit/handlers/event-message-handler.spec.ts @@ -23,6 +23,7 @@ describe('EventMessageHandler', () => { let webSocket: IWebSocketAdapter let handler: EventMessageHandler let userRepository: IUserRepository + let eventRepository: any let event: Event let message: IncomingEventMessage let sandbox: Sinon.SinonSandbox @@ -69,6 +70,7 @@ describe('EventMessageHandler', () => { canAcceptEventStub = sandbox.stub(EventMessageHandler.prototype, 'canAcceptEvent' as any) isEventValidStub = sandbox.stub(EventMessageHandler.prototype, 'isEventValid' as any) isUserAdmitted = sandbox.stub(EventMessageHandler.prototype, 'isUserAdmitted' as any) + eventRepository = { hasActiveRequestToVanish: sandbox.stub().resolves(false) } strategyExecuteStub = sandbox.stub() strategyFactoryStub = sandbox.stub().returns({ execute: strategyExecuteStub, @@ -81,6 +83,7 @@ describe('EventMessageHandler', () => { handler = new EventMessageHandler( webSocket as any, strategyFactoryStub, + eventRepository, userRepository, () => ({ info: { relay_url: 'relay_url' }, @@ -119,6 +122,20 @@ describe('EventMessageHandler', () => { expect(strategyFactoryStub).not.to.have.been.called }) + it('rejects event if request to vanish is active for pubkey', async () => { + canAcceptEventStub.returns(undefined) + isEventValidStub.resolves(undefined) + eventRepository.hasActiveRequestToVanish.resolves(true) + + await handler.handleMessage(message) + + expect(eventRepository.hasActiveRequestToVanish).to.have.been.calledOnceWithExactly(event.pubkey) + expect(onMessageSpy).to.have.been.calledOnceWithExactly( + [MessageType.OK, event.id, false, 'blocked: request to vanish active for pubkey'], + ) + expect(strategyFactoryStub).not.to.have.been.called + }) + it('rejects event if invalid', async () => { isEventValidStub.resolves('reason') @@ -242,6 +259,7 @@ describe('EventMessageHandler', () => { handler = new EventMessageHandler( {} as any, () => null, + { hasActiveRequestToVanish: async () => false } as any, userRepository, () => settings, () => ({ hit: async () => false }) @@ -717,6 +735,7 @@ describe('EventMessageHandler', () => { handler = new EventMessageHandler( webSocket, () => null, + { hasActiveRequestToVanish: async () => false } as any, userRepository, () => settings, () => ({ hit: rateLimiterHitStub }) @@ -984,6 +1003,7 @@ describe('EventMessageHandler', () => { handler = new EventMessageHandler( webSocket, () => null, + { hasActiveRequestToVanish: async () => false } as any, userRepository, () => settings, () => ({ hit: async () => false }) diff --git a/test/unit/handlers/event-strategies/vanish-event-strategy.spec.ts b/test/unit/handlers/event-strategies/vanish-event-strategy.spec.ts new file mode 100644 index 00000000..2b6aefe2 --- /dev/null +++ b/test/unit/handlers/event-strategies/vanish-event-strategy.spec.ts @@ -0,0 +1,75 @@ +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { Event } from '../../../../src/@types/event' +import { EventKinds } from '../../../../src/constants/base' +import { IWebSocketAdapter } from '../../../../src/@types/adapters' +import { MessageType } from '../../../../src/@types/messages' +import Sinon from 'sinon' +import { VanishEventStrategy } from '../../../../src/handlers/event-strategies/vanish-event-strategy' +import { WebSocketAdapterEvent } from '../../../../src/constants/adapter' + +chai.use(chaiAsPromised) + +const { expect } = chai + +describe('VanishEventStrategy', () => { + let webSocket: IWebSocketAdapter + let eventRepository: any + let webSocketEmitStub: Sinon.SinonStub + let strategy: VanishEventStrategy + let sandbox: Sinon.SinonSandbox + const event: Event = { + id: 'id', + pubkey: 'pubkey', + kind: EventKinds.REQUEST_TO_VANISH, + tags: [[ 'r', 'relay_url' ]], + } as any + + beforeEach(() => { + sandbox = Sinon.createSandbox() + eventRepository = { + deleteByPubkeyExceptKinds: sandbox.stub().resolves(1), + create: sandbox.stub().resolves(1), + } + webSocketEmitStub = sandbox.stub() + webSocket = { + emit: webSocketEmitStub, + } as any + strategy = new VanishEventStrategy(webSocket, eventRepository) + }) + + afterEach(() => { + sandbox.restore() + }) + + it('deletes all events for pubkey except kind 62 events and creates the vanish event', async () => { + await strategy.execute(event) + + expect(eventRepository.deleteByPubkeyExceptKinds).to.have.been.calledOnceWithExactly( + event.pubkey, + [EventKinds.REQUEST_TO_VANISH], + ) + expect(eventRepository.create).to.have.been.calledOnceWithExactly(event) + expect(webSocketEmitStub).to.have.been.calledOnceWithExactly( + WebSocketAdapterEvent.Message, + [MessageType.OK, event.id, true, ''], + ) + }) + + it('does not broadcast the vanish event', async () => { + await strategy.execute(event) + + expect(webSocketEmitStub.calledWith(WebSocketAdapterEvent.Broadcast)).to.be.false + }) + + it('returns duplicate OK if the event already exists', async () => { + eventRepository.create.resolves(0) + + await strategy.execute(event) + + expect(webSocketEmitStub).to.have.been.calledOnceWithExactly( + WebSocketAdapterEvent.Message, + [MessageType.OK, event.id, true, 'duplicate:'], + ) + }) +}) diff --git a/test/unit/repositories/event-repository.spec.ts b/test/unit/repositories/event-repository.spec.ts index abe026f9..9b69292e 100644 --- a/test/unit/repositories/event-repository.spec.ts +++ b/test/unit/repositories/event-repository.spec.ts @@ -443,7 +443,49 @@ describe('EventRepository', () => { it('marks event as deleted by pubkey & event_id if not deleted', () => { const query = repository.deleteByPubkeyAndIds('001122', ['aabbcc', 'ddeeff']).toString() - expect(query).to.equal('update "events" set "deleted_at" = now() where "event_pubkey" = X\'001122\' and "event_id" in (X\'aabbcc\', X\'ddeeff\') and "deleted_at" is null') + expect(query).to.equal('update "events" set "deleted_at" = now() where "event_pubkey" = X\'001122\' and "event_id" in (X\'aabbcc\', X\'ddeeff\') and not "event_kind" = 62 and "deleted_at" is null') + }) + }) + + describe('deleteByPubkeyExceptKinds', () => { + it('marks event as deleted by pubkey except excluded kinds', () => { + const query = repository.deleteByPubkeyExceptKinds('001122', [62]).toString() + + expect(query).to.equal('update "events" set "deleted_at" = now() where "event_pubkey" = X\'001122\' and "event_kind" not in (62) and "deleted_at" is null') + }) + }) + + describe('hasActiveRequestToVanish', () => { + it('checks for an existing active kind 62 event', async () => { + const firstStub = sandbox.stub().resolves({ event_id: Buffer.from('001122', 'hex') }) + const readReplicaStub = sandbox.stub().returns({ + select: sandbox.stub().returnsThis(), + where: sandbox.stub().returnsThis(), + whereNull: sandbox.stub().returnsThis(), + first: firstStub, + }) + repository = new EventRepository({} as any, readReplicaStub as any) + + const result = await repository.hasActiveRequestToVanish('001122') + + expect(result).to.be.true + expect(readReplicaStub).to.have.been.calledOnceWithExactly('events') + expect(firstStub).to.have.been.calledOnce + }) + + it('returns false when no kind 62 event exists', async () => { + const firstStub = sandbox.stub().resolves(undefined) + const readReplicaStub = sandbox.stub().returns({ + select: sandbox.stub().returnsThis(), + where: sandbox.stub().returnsThis(), + whereNull: sandbox.stub().returnsThis(), + first: firstStub, + }) + repository = new EventRepository({} as any, readReplicaStub as any) + + const result = await repository.hasActiveRequestToVanish('001122') + + expect(result).to.be.false }) }) diff --git a/test/unit/utils/event.spec.ts b/test/unit/utils/event.spec.ts index f845b5a5..bed8819a 100644 --- a/test/unit/utils/event.spec.ts +++ b/test/unit/utils/event.spec.ts @@ -1,5 +1,4 @@ -import { expect } from 'chai' - +import { ALL_RELAYS, EventKinds, EventTags } from '../../../src/constants/base' import { CanonicalEvent, Event } from '../../../src/@types/event' import { getEventExpiration, @@ -11,9 +10,11 @@ import { isExpiredEvent, isParameterizedReplaceableEvent, isReplaceableEvent, + isRequestToVanishEvent, + isValidRequestToVanishEvent, serializeEvent, } from '../../../src/utils/event' -import { EventKinds } from '../../../src/constants/base' +import { expect } from 'chai' describe('NIP-01', () => { describe('serializeEvent', () => { @@ -416,6 +417,58 @@ describe('NIP-09', () => { }) }) +describe('NIP-62', () => { + describe('isRequestToVanishEvent', () => { + it('returns true if event is kind 62', () => { + const event: Event = { + kind: 62, + } as any + expect(isRequestToVanishEvent(event)).to.be.true + }) + + it('returns false if event is not kind 62', () => { + const event: Event = { + kind: 1, + } as any + expect(isRequestToVanishEvent(event)).to.be.false + }) + }) + + describe('isValidRequestToVanishEvent', () => { + it('returns true when event contains the relay URL', () => { + const event: Event = { + kind: 62, + tags: [[EventTags.Relay, 'relay_url']], + } as any + expect(isValidRequestToVanishEvent(event, 'relay_url')).to.be.true + }) + + it('returns true when event contains ALL_RELAYS', () => { + const event: Event = { + kind: 62, + tags: [[EventTags.Relay, ALL_RELAYS]], + } as any + expect(isValidRequestToVanishEvent(event, 'relay_url')).to.be.true + }) + + it('returns false when relay tag does not match', () => { + const event: Event = { + kind: 62, + tags: [[EventTags.Relay, 'other_relay_url']], + } as any + expect(isValidRequestToVanishEvent(event, 'relay_url')).to.be.false + }) + + it('returns false when there are no relay tags', () => { + const event: Event = { + kind: 62, + tags: [], + } as any + expect(isValidRequestToVanishEvent(event, 'relay_url')).to.be.false + }) + }) +}) + describe('NIP-33', () => { describe('isParameterizedReplaceableEvent', () => { it('returns true if event is a parameterized replaceable event', () => { From 4b2b032a943d564a0ba1646e2418089c866f692e Mon Sep 17 00:00:00 2001 From: vikashsiwach Date: Thu, 9 Apr 2026 22:07:14 +0530 Subject: [PATCH 2/4] refactor: combine vanish validation and kind check --- src/handlers/event-message-handler.ts | 5 ++--- src/utils/event.ts | 12 ++++++++---- test/unit/utils/event.spec.ts | 19 ++++++++++++------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/handlers/event-message-handler.ts b/src/handlers/event-message-handler.ts index 900b8293..43d1ba22 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -1,4 +1,4 @@ -import { ContextMetadataKey, EventExpirationTimeMetadataKey } from '../constants/base' +import { ContextMetadataKey, EventExpirationTimeMetadataKey, EventKinds } from '../constants/base' import { Event, ExpiringEvent } from '../@types/event' import { EventRateLimit, FeeSchedule, Settings } from '../@types/settings' import { @@ -12,7 +12,6 @@ import { isEventSignatureValid, isExpiredEvent, isRequestToVanishEvent, - isValidRequestToVanishEvent, } from '../utils/event' import { IEventRepository, IUserRepository } from '../@types/repositories' import { IEventStrategy, IMessageHandler } from '../@types/message-handlers' @@ -210,7 +209,7 @@ export class EventMessageHandler implements IMessageHandler { return 'invalid: event signature verification failed' } - if (isRequestToVanishEvent(event) && !isValidRequestToVanishEvent(event, this.settings().info.relay_url)) { + if (event.kind === EventKinds.REQUEST_TO_VANISH && !isRequestToVanishEvent(event, this.settings().info.relay_url)) { return 'invalid: request to vanish relay tag invalid' } } diff --git a/src/utils/event.ts b/src/utils/event.ts index c7b4b1c8..1bdde933 100644 --- a/src/utils/event.ts +++ b/src/utils/event.ts @@ -225,11 +225,15 @@ export const isDeleteEvent = (event: Event): boolean => { return event.kind === EventKinds.DELETE } -export const isRequestToVanishEvent = (event: Event): boolean => { - return event.kind === EventKinds.REQUEST_TO_VANISH -} +export const isRequestToVanishEvent = (event: Event, relayUrl?: string): boolean => { + if (event.kind !== EventKinds.REQUEST_TO_VANISH) { + return false + } + + if (typeof relayUrl === 'undefined') { + return true + } -export const isValidRequestToVanishEvent = (event: Event, relayUrl: string): boolean => { const relayTags = event.tags .filter((tag) => tag.length >= 2 && tag[0] === EventTags.Relay) .map((tag) => tag[1]) diff --git a/test/unit/utils/event.spec.ts b/test/unit/utils/event.spec.ts index bed8819a..b24c7f0f 100644 --- a/test/unit/utils/event.spec.ts +++ b/test/unit/utils/event.spec.ts @@ -11,7 +11,6 @@ import { isParameterizedReplaceableEvent, isReplaceableEvent, isRequestToVanishEvent, - isValidRequestToVanishEvent, serializeEvent, } from '../../../src/utils/event' import { expect } from 'chai' @@ -432,15 +431,13 @@ describe('NIP-62', () => { } as any expect(isRequestToVanishEvent(event)).to.be.false }) - }) - describe('isValidRequestToVanishEvent', () => { it('returns true when event contains the relay URL', () => { const event: Event = { kind: 62, tags: [[EventTags.Relay, 'relay_url']], } as any - expect(isValidRequestToVanishEvent(event, 'relay_url')).to.be.true + expect(isRequestToVanishEvent(event, 'relay_url')).to.be.true }) it('returns true when event contains ALL_RELAYS', () => { @@ -448,7 +445,7 @@ describe('NIP-62', () => { kind: 62, tags: [[EventTags.Relay, ALL_RELAYS]], } as any - expect(isValidRequestToVanishEvent(event, 'relay_url')).to.be.true + expect(isRequestToVanishEvent(event, 'relay_url')).to.be.true }) it('returns false when relay tag does not match', () => { @@ -456,7 +453,7 @@ describe('NIP-62', () => { kind: 62, tags: [[EventTags.Relay, 'other_relay_url']], } as any - expect(isValidRequestToVanishEvent(event, 'relay_url')).to.be.false + expect(isRequestToVanishEvent(event, 'relay_url')).to.be.false }) it('returns false when there are no relay tags', () => { @@ -464,7 +461,15 @@ describe('NIP-62', () => { kind: 62, tags: [], } as any - expect(isValidRequestToVanishEvent(event, 'relay_url')).to.be.false + expect(isRequestToVanishEvent(event, 'relay_url')).to.be.false + }) + + it('returns false when relay URL is provided for non-kind-62 event', () => { + const event: Event = { + kind: 1, + tags: [[EventTags.Relay, 'relay_url']], + } as any + expect(isRequestToVanishEvent(event, 'relay_url')).to.be.false }) }) }) From 3646565a55e344288e947fa7c1d28ebdd49aa556 Mon Sep 17 00:00:00 2001 From: vikashsiwach Date: Fri, 10 Apr 2026 22:48:25 +0530 Subject: [PATCH 3/4] feat: add is_vanished column to users table --- ...0_230000_add_is_vanished_to_users_table.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 migrations/20260410_230000_add_is_vanished_to_users_table.js diff --git a/migrations/20260410_230000_add_is_vanished_to_users_table.js b/migrations/20260410_230000_add_is_vanished_to_users_table.js new file mode 100644 index 00000000..f5f73a53 --- /dev/null +++ b/migrations/20260410_230000_add_is_vanished_to_users_table.js @@ -0,0 +1,30 @@ +exports.up = async function (knex) { + await knex.schema.alterTable('users', (table) => { + table.boolean('is_vanished').notNullable().defaultTo(false) + }) + + await knex.raw(` + UPDATE users u + SET is_vanished = true + FROM events e + WHERE u.pubkey = e.event_pubkey + AND e.event_kind = 62 + AND e.deleted_at IS NULL + `) + + await knex.raw(` + INSERT INTO users (pubkey, is_admitted, balance, is_vanished, created_at, updated_at) + SELECT DISTINCT e.event_pubkey, false, 0, true, NOW(), NOW() + FROM events e + LEFT JOIN users u ON u.pubkey = e.event_pubkey + WHERE e.event_kind = 62 + AND e.deleted_at IS NULL + AND u.pubkey IS NULL + `) +} + +exports.down = function (knex) { + return knex.schema.alterTable('users', (table) => { + table.dropColumn('is_vanished') + }) +} From 8497b1429d5bbeefd535cfdeb35912930736c141 Mon Sep 17 00:00:00 2001 From: vikashsiwach Date: Sun, 12 Apr 2026 03:06:47 +0530 Subject: [PATCH 4/4] feat: add is_vanished optimization with lazy hydration --- ...0_230000_add_is_vanished_to_users_table.js | 20 ++---- src/@types/repositories.ts | 2 + src/@types/user.ts | 2 + .../get-admission-check-controller-factory.ts | 7 +- .../post-invoice-controller-factory.ts | 7 +- src/factories/event-strategy-factory.ts | 5 +- src/factories/message-handler-factory.ts | 2 +- src/factories/payments-service-factory.ts | 4 +- .../static-mirroring.worker-factory.ts | 2 +- src/factories/worker-factory.ts | 2 +- src/handlers/event-message-handler.ts | 4 +- .../event-strategies/vanish-event-strategy.ts | 5 +- src/repositories/user-repository.ts | 72 +++++++++++++++++-- src/utils/transform.ts | 1 + .../factories/event-strategy-factory.spec.ts | 6 +- .../handlers/event-message-handler.spec.ts | 30 +++++--- .../vanish-event-strategy.spec.ts | 16 +++-- 17 files changed, 137 insertions(+), 50 deletions(-) diff --git a/migrations/20260410_230000_add_is_vanished_to_users_table.js b/migrations/20260410_230000_add_is_vanished_to_users_table.js index f5f73a53..2deb8252 100644 --- a/migrations/20260410_230000_add_is_vanished_to_users_table.js +++ b/migrations/20260410_230000_add_is_vanished_to_users_table.js @@ -6,20 +6,12 @@ exports.up = async function (knex) { await knex.raw(` UPDATE users u SET is_vanished = true - FROM events e - WHERE u.pubkey = e.event_pubkey - AND e.event_kind = 62 - AND e.deleted_at IS NULL - `) - - await knex.raw(` - INSERT INTO users (pubkey, is_admitted, balance, is_vanished, created_at, updated_at) - SELECT DISTINCT e.event_pubkey, false, 0, true, NOW(), NOW() - FROM events e - LEFT JOIN users u ON u.pubkey = e.event_pubkey - WHERE e.event_kind = 62 - AND e.deleted_at IS NULL - AND u.pubkey IS NULL + WHERE EXISTS ( + SELECT 1 FROM events e + WHERE e.event_pubkey = u.pubkey + AND e.event_kind = 62 + AND e.deleted_at IS NULL + ) `) } diff --git a/src/@types/repositories.ts b/src/@types/repositories.ts index 4341bb58..ce72d5c9 100644 --- a/src/@types/repositories.ts +++ b/src/@types/repositories.ts @@ -47,4 +47,6 @@ export interface IUserRepository { findByPubkey(pubkey: Pubkey, client?: DatabaseClient): Promise upsert(user: Partial, client?: DatabaseClient): Promise getBalanceByPubkey(pubkey: Pubkey, client?: DatabaseClient): Promise + isVanished(pubkey: Pubkey, client?: DatabaseClient): Promise + setVanished(pubkey: Pubkey, vanished: boolean, client?: DatabaseClient): Promise } diff --git a/src/@types/user.ts b/src/@types/user.ts index 83a5237c..983a3e4a 100644 --- a/src/@types/user.ts +++ b/src/@types/user.ts @@ -3,6 +3,7 @@ import { Pubkey } from './base' export interface User { pubkey: Pubkey isAdmitted: boolean + isVanished: boolean balance: bigint tosAcceptedAt?: Date | null createdAt: Date @@ -12,6 +13,7 @@ export interface User { export interface DBUser { pubkey: Buffer is_admitted: boolean + is_vanished: boolean balance: bigint created_at: Date updated_at: Date diff --git a/src/factories/controllers/get-admission-check-controller-factory.ts b/src/factories/controllers/get-admission-check-controller-factory.ts index c7d2d47e..c1bdd265 100644 --- a/src/factories/controllers/get-admission-check-controller-factory.ts +++ b/src/factories/controllers/get-admission-check-controller-factory.ts @@ -1,12 +1,15 @@ +import { getMasterDbClient, getReadReplicaDbClient } from '../../database/client' import { createSettings } from '../settings-factory' -import { getMasterDbClient } from '../../database/client' +import { EventRepository } from '../../repositories/event-repository' import { GetSubmissionCheckController } from '../../controllers/admission/get-admission-check-controller' import { slidingWindowRateLimiterFactory } from '../rate-limiter-factory' import { UserRepository } from '../../repositories/user-repository' export const createGetAdmissionCheckController = () => { const dbClient = getMasterDbClient() - const userRepository = new UserRepository(dbClient) + const readReplicaDbClient = getReadReplicaDbClient() + const eventRepository = new EventRepository(dbClient, readReplicaDbClient) + const userRepository = new UserRepository(dbClient, eventRepository) return new GetSubmissionCheckController( userRepository, diff --git a/src/factories/controllers/post-invoice-controller-factory.ts b/src/factories/controllers/post-invoice-controller-factory.ts index 50331572..1d5b6593 100644 --- a/src/factories/controllers/post-invoice-controller-factory.ts +++ b/src/factories/controllers/post-invoice-controller-factory.ts @@ -1,6 +1,7 @@ +import { getMasterDbClient, getReadReplicaDbClient } from '../../database/client' import { createPaymentsService } from '../payments-service-factory' import { createSettings } from '../settings-factory' -import { getMasterDbClient } from '../../database/client' +import { EventRepository } from '../../repositories/event-repository' import { IController } from '../../@types/controllers' import { PostInvoiceController } from '../../controllers/invoices/post-invoice-controller' import { slidingWindowRateLimiterFactory } from '../rate-limiter-factory' @@ -8,7 +9,9 @@ import { UserRepository } from '../../repositories/user-repository' export const createPostInvoiceController = (): IController => { const dbClient = getMasterDbClient() - const userRepository = new UserRepository(dbClient) + const readReplicaDbClient = getReadReplicaDbClient() + const eventRepository = new EventRepository(dbClient, readReplicaDbClient) + const userRepository = new UserRepository(dbClient, eventRepository) const paymentsService = createPaymentsService() return new PostInvoiceController( diff --git a/src/factories/event-strategy-factory.ts b/src/factories/event-strategy-factory.ts index 1e4b5af9..52709ac5 100644 --- a/src/factories/event-strategy-factory.ts +++ b/src/factories/event-strategy-factory.ts @@ -1,10 +1,10 @@ +import { IEventRepository, IUserRepository } from '../@types/repositories' import { isDeleteEvent, isEphemeralEvent, isParameterizedReplaceableEvent, isReplaceableEvent, isRequestToVanishEvent } 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 { ParameterizedReplaceableEventStrategy } from '../handlers/event-strategies/parameterized-replaceable-event-strategy' @@ -13,10 +13,11 @@ import { VanishEventStrategy } from '../handlers/event-strategies/vanish-event-s export const eventStrategyFactory = ( eventRepository: IEventRepository, + userRepository: IUserRepository, ): Factory>, [Event, IWebSocketAdapter]> => ([event, adapter]: [Event, IWebSocketAdapter]) => { if (isRequestToVanishEvent(event)) { - return new VanishEventStrategy(adapter, eventRepository) + return new VanishEventStrategy(adapter, eventRepository, userRepository) } else if (isReplaceableEvent(event)) { return new ReplaceableEventStrategy(adapter, eventRepository) } else if (isEphemeralEvent(event)) { diff --git a/src/factories/message-handler-factory.ts b/src/factories/message-handler-factory.ts index 34c37493..a5c1c9fe 100644 --- a/src/factories/message-handler-factory.ts +++ b/src/factories/message-handler-factory.ts @@ -17,7 +17,7 @@ export const messageHandlerFactory = ( { return new EventMessageHandler( adapter, - eventStrategyFactory(eventRepository), + eventStrategyFactory(eventRepository, userRepository), eventRepository, userRepository, createSettings, diff --git a/src/factories/payments-service-factory.ts b/src/factories/payments-service-factory.ts index abd97ddd..1a762e44 100644 --- a/src/factories/payments-service-factory.ts +++ b/src/factories/payments-service-factory.ts @@ -10,9 +10,9 @@ export const createPaymentsService = () => { const dbClient = getMasterDbClient() const rrDbClient = getReadReplicaDbClient() const invoiceRepository = new InvoiceRepository(dbClient) - const userRepository = new UserRepository(dbClient) - const paymentsProcessor = createPaymentsProcessor() const eventRepository = new EventRepository(dbClient, rrDbClient) + const userRepository = new UserRepository(dbClient, eventRepository) + const paymentsProcessor = createPaymentsProcessor() return new PaymentsService( dbClient, diff --git a/src/factories/static-mirroring.worker-factory.ts b/src/factories/static-mirroring.worker-factory.ts index 234430e4..67f7028e 100644 --- a/src/factories/static-mirroring.worker-factory.ts +++ b/src/factories/static-mirroring.worker-factory.ts @@ -8,7 +8,7 @@ export const staticMirroringWorkerFactory = () => { const dbClient = getMasterDbClient() const readReplicaDbClient = getReadReplicaDbClient() const eventRepository = new EventRepository(dbClient, readReplicaDbClient) - const userRepository = new UserRepository(dbClient) + const userRepository = new UserRepository(dbClient, eventRepository) return new StaticMirroringWorker( eventRepository, diff --git a/src/factories/worker-factory.ts b/src/factories/worker-factory.ts index 123e59d2..ec358798 100644 --- a/src/factories/worker-factory.ts +++ b/src/factories/worker-factory.ts @@ -16,7 +16,7 @@ export const workerFactory = (): AppWorker => { const dbClient = getMasterDbClient() const readReplicaDbClient = getReadReplicaDbClient() const eventRepository = new EventRepository(dbClient, readReplicaDbClient) - const userRepository = new UserRepository(dbClient) + const userRepository = new UserRepository(dbClient, eventRepository) const settings = createSettings() diff --git a/src/handlers/event-message-handler.ts b/src/handlers/event-message-handler.ts index 43d1ba22..52320fbc 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -224,8 +224,8 @@ export class EventMessageHandler implements IMessageHandler { return } - const existingVanishRequest = await this.eventRepository.hasActiveRequestToVanish(event.pubkey) - if (existingVanishRequest) { + const isVanished = await this.userRepository.isVanished(event.pubkey) + if (isVanished) { return 'blocked: request to vanish active for pubkey' } } diff --git a/src/handlers/event-strategies/vanish-event-strategy.ts b/src/handlers/event-strategies/vanish-event-strategy.ts index 32dbb494..ed8ca73a 100644 --- a/src/handlers/event-strategies/vanish-event-strategy.ts +++ b/src/handlers/event-strategies/vanish-event-strategy.ts @@ -1,8 +1,8 @@ +import { IEventRepository, IUserRepository } from '../../@types/repositories' import { createCommandResult } from '../../utils/messages' import { createLogger } from '../../factories/logger-factory' import { Event } from '../../@types/event' import { EventKinds } from '../../constants/base' -import { IEventRepository } from '../../@types/repositories' import { IEventStrategy } from '../../@types/message-handlers' import { IWebSocketAdapter } from '../../@types/adapters' import { WebSocketAdapterEvent } from '../../constants/adapter' @@ -13,6 +13,7 @@ export class VanishEventStrategy implements IEventStrategy> public constructor( private readonly webSocket: IWebSocketAdapter, private readonly eventRepository: IEventRepository, + private readonly userRepository: IUserRepository, ) {} public async execute(event: Event): Promise { @@ -25,6 +26,8 @@ export class VanishEventStrategy implements IEventStrategy> const count = await this.eventRepository.create(event) + await this.userRepository.setVanished(event.pubkey, true) + this.webSocket.emit( WebSocketAdapterEvent.Message, createCommandResult(event.id, true, count ? '' : 'duplicate:') diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index fedf40b4..6fa5c76d 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -1,15 +1,18 @@ -import { always, applySpec, omit, pipe, prop } from 'ramda' - +import { always, applySpec, defaultTo, omit, pipe, prop } from 'ramda' import { DatabaseClient, Pubkey } from '../@types/base' import { DBUser, User } from '../@types/user' import { fromDBUser, toBuffer } from '../utils/transform' +import { IEventRepository, IUserRepository } from '../@types/repositories' import { createLogger } from '../factories/logger-factory' -import { IUserRepository } from '../@types/repositories' + const debug = createLogger('user-repository') export class UserRepository implements IUserRepository { - public constructor(private readonly dbClient: DatabaseClient) { } + public constructor( + private readonly dbClient: DatabaseClient, + private readonly eventRepository: IEventRepository, + ) { } public async findByPubkey( pubkey: Pubkey, @@ -28,7 +31,7 @@ export class UserRepository implements IUserRepository { } public async upsert( - user: User, + user: Partial, client: DatabaseClient = this.dbClient, ): Promise { debug('upsert: %o', user) @@ -37,7 +40,8 @@ export class UserRepository implements IUserRepository { const row = applySpec({ pubkey: pipe(prop('pubkey'), toBuffer), - is_admitted: prop('isAdmitted'), + is_admitted: pipe(prop('isAdmitted'), defaultTo(false)), + is_vanished: pipe(prop('isVanished'), defaultTo(false)), tos_accepted_at: prop('tosAcceptedAt'), updated_at: always(date), created_at: always(date), @@ -61,6 +65,62 @@ export class UserRepository implements IUserRepository { } as Promise } + /** + * Returns vanish state from users.is_vanished, or lazily hydrates a user row from events once + * when no users row exists (single upsert; no duplicate inserts). + */ + public async isVanished( + pubkey: Pubkey, + client: DatabaseClient = this.dbClient + ): Promise { + const existing = await this.findByPubkey(pubkey, client) + if (existing) { + return existing.isVanished + } + + const vanishedFromEvents = await this.eventRepository.hasActiveRequestToVanish(pubkey) + await this.upsertVanishState(pubkey, vanishedFromEvents, client) + return vanishedFromEvents + } + + public setVanished( + pubkey: Pubkey, + vanished: boolean, + client: DatabaseClient = this.dbClient + ): Promise { + return this.upsertVanishState(pubkey, vanished, client) + } + + private upsertVanishState( + pubkey: Pubkey, + isVanished: boolean, + client: DatabaseClient, + ): Promise { + debug('upsert vanish state for %s: %o', pubkey, isVanished) + const date = new Date() + + const query = client('users') + .insert({ + pubkey: toBuffer(pubkey), + is_admitted: false, + balance: 0n, + is_vanished: isVanished, + created_at: date, + updated_at: date, + }) + .onConflict('pubkey') + .merge({ + is_vanished: isVanished, + updated_at: date, + }) + + return { + then: (onfulfilled: (value: number) => T1 | PromiseLike, onrejected: (reason: any) => T2 | PromiseLike) => query.then(prop('rowCount') as () => number).then(onfulfilled, onrejected), + catch: (onrejected: (reason: any) => T | PromiseLike) => query.catch(onrejected), + toString: (): string => query.toString(), + } as Promise + } + public async getBalanceByPubkey( pubkey: Pubkey, client: DatabaseClient = this.dbClient diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 33aa9244..9c9d01b2 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -39,6 +39,7 @@ export const fromDBInvoice = applySpec({ export const fromDBUser = applySpec({ pubkey: pipe(prop('pubkey') as () => Buffer, fromBuffer), isAdmitted: prop('is_admitted'), + isVanished: prop('is_vanished'), balance: prop('balance'), createdAt: prop('created_at'), updatedAt: prop('updated_at'), diff --git a/test/unit/factories/event-strategy-factory.spec.ts b/test/unit/factories/event-strategy-factory.spec.ts index 46140807..29c7c818 100644 --- a/test/unit/factories/event-strategy-factory.spec.ts +++ b/test/unit/factories/event-strategy-factory.spec.ts @@ -1,5 +1,6 @@ import { expect } from 'chai' +import { IEventRepository, IUserRepository } from '../../../src/@types/repositories' import { DefaultEventStrategy } from '../../../src/handlers/event-strategies/default-event-strategy' import { DeleteEventStrategy } from '../../../src/handlers/event-strategies/delete-event-strategy' import { EphemeralEventStrategy } from '../../../src/handlers/event-strategies/ephemeral-event-strategy' @@ -7,7 +8,6 @@ import { Event } from '../../../src/@types/event' import { EventKinds } from '../../../src/constants/base' import { eventStrategyFactory } from '../../../src/factories/event-strategy-factory' 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' import { ParameterizedReplaceableEventStrategy } from '../../../src/handlers/event-strategies/parameterized-replaceable-event-strategy' @@ -16,16 +16,18 @@ import { VanishEventStrategy } from '../../../src/handlers/event-strategies/vani describe('eventStrategyFactory', () => { let eventRepository: IEventRepository + let userRepository: IUserRepository let event: Event let adapter: IWebSocketAdapter let factory: Factory>, [Event, IWebSocketAdapter]> beforeEach(() => { eventRepository = {} as any + userRepository = {} as any event = {} as any adapter = {} as any - factory = eventStrategyFactory(eventRepository) + factory = eventStrategyFactory(eventRepository, userRepository) }) it('returns ReplaceableEvent given a set_metadata event', () => { diff --git a/test/unit/handlers/event-message-handler.spec.ts b/test/unit/handlers/event-message-handler.spec.ts index 6895ec7c..983dc7d2 100644 --- a/test/unit/handlers/event-message-handler.spec.ts +++ b/test/unit/handlers/event-message-handler.spec.ts @@ -49,6 +49,9 @@ describe('EventMessageHandler', () => { sig: 'f'.repeat(128), tags: [], } + userRepository = { + isVanished: async () => false, + } as any }) afterEach(() => { @@ -70,7 +73,10 @@ describe('EventMessageHandler', () => { canAcceptEventStub = sandbox.stub(EventMessageHandler.prototype, 'canAcceptEvent' as any) isEventValidStub = sandbox.stub(EventMessageHandler.prototype, 'isEventValid' as any) isUserAdmitted = sandbox.stub(EventMessageHandler.prototype, 'isUserAdmitted' as any) - eventRepository = { hasActiveRequestToVanish: sandbox.stub().resolves(false) } + eventRepository = {} as any + userRepository = { + isVanished: sandbox.stub().resolves(false), + } as any strategyExecuteStub = sandbox.stub() strategyFactoryStub = sandbox.stub().returns({ execute: strategyExecuteStub, @@ -125,11 +131,11 @@ describe('EventMessageHandler', () => { it('rejects event if request to vanish is active for pubkey', async () => { canAcceptEventStub.returns(undefined) isEventValidStub.resolves(undefined) - eventRepository.hasActiveRequestToVanish.resolves(true) + ;(userRepository.isVanished as any).resolves(true) await handler.handleMessage(message) - expect(eventRepository.hasActiveRequestToVanish).to.have.been.calledOnceWithExactly(event.pubkey) + expect(userRepository.isVanished as any).to.have.been.calledOnceWithExactly(event.pubkey) expect(onMessageSpy).to.have.been.calledOnceWithExactly( [MessageType.OK, event.id, false, 'blocked: request to vanish active for pubkey'], ) @@ -259,7 +265,7 @@ describe('EventMessageHandler', () => { handler = new EventMessageHandler( {} as any, () => null, - { hasActiveRequestToVanish: async () => false } as any, + {} as any, userRepository, () => settings, () => ({ hit: async () => false }) @@ -732,10 +738,13 @@ describe('EventMessageHandler', () => { webSocket = { getClientAddress: getClientAddressStub, } as any + userRepository = { + isVanished: async () => false, + } as any handler = new EventMessageHandler( webSocket, () => null, - { hasActiveRequestToVanish: async () => false } as any, + {} as any, userRepository, () => settings, () => ({ hit: rateLimiterHitStub }) @@ -999,11 +1008,12 @@ describe('EventMessageHandler', () => { } as any userRepository = { findByPubkey: userRepositoryFindByPubkeyStub, + isVanished: async () => false, } as any handler = new EventMessageHandler( webSocket, () => null, - { hasActiveRequestToVanish: async () => false } as any, + {} as any, userRepository, () => settings, () => ({ hit: async () => false }) @@ -1084,27 +1094,27 @@ describe('EventMessageHandler', () => { }) it('fulfills with reason if user is not admitted', async () => { - userRepositoryFindByPubkeyStub.resolves({ isAdmitted: false }) + userRepositoryFindByPubkeyStub.resolves({ isAdmitted: false, isVanished: false }) return expect((handler as any).isUserAdmitted(event)).to.eventually.equal('blocked: pubkey not admitted') }) it('fulfills with reason if user is not admitted', async () => { - userRepositoryFindByPubkeyStub.resolves({ isAdmitted: false }) + userRepositoryFindByPubkeyStub.resolves({ isAdmitted: false, isVanished: false }) return expect((handler as any).isUserAdmitted(event)).to.eventually.equal('blocked: pubkey not admitted') }) it('fulfills with reason if user does not meet minimum balance', async () => { settings.limits.event.pubkey.minBalance = 1000n - userRepositoryFindByPubkeyStub.resolves({ isAdmitted: true, balance: 999n }) + userRepositoryFindByPubkeyStub.resolves({ isAdmitted: true, isVanished: false, balance: 999n }) return expect((handler as any).isUserAdmitted(event)).to.eventually.equal('blocked: insufficient balance') }) it('fulfills with undefined if user is admitted', async () => { settings.limits.event.pubkey.minBalance = 0n - userRepositoryFindByPubkeyStub.resolves({ isAdmitted: true }) + userRepositoryFindByPubkeyStub.resolves({ isAdmitted: true, isVanished: false }) return expect((handler as any).isUserAdmitted(event)).to.eventually.be.undefined }) diff --git a/test/unit/handlers/event-strategies/vanish-event-strategy.spec.ts b/test/unit/handlers/event-strategies/vanish-event-strategy.spec.ts index 2b6aefe2..845372cd 100644 --- a/test/unit/handlers/event-strategies/vanish-event-strategy.spec.ts +++ b/test/unit/handlers/event-strategies/vanish-event-strategy.spec.ts @@ -1,20 +1,24 @@ -import chai from 'chai' -import chaiAsPromised from 'chai-as-promised' import { Event } from '../../../../src/@types/event' import { EventKinds } from '../../../../src/constants/base' import { IWebSocketAdapter } from '../../../../src/@types/adapters' import { MessageType } from '../../../../src/@types/messages' -import Sinon from 'sinon' import { VanishEventStrategy } from '../../../../src/handlers/event-strategies/vanish-event-strategy' import { WebSocketAdapterEvent } from '../../../../src/constants/adapter' +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import Sinon from 'sinon' +import sinonChai from 'sinon-chai' + chai.use(chaiAsPromised) +chai.use(sinonChai) const { expect } = chai describe('VanishEventStrategy', () => { let webSocket: IWebSocketAdapter let eventRepository: any + let userRepository: any let webSocketEmitStub: Sinon.SinonStub let strategy: VanishEventStrategy let sandbox: Sinon.SinonSandbox @@ -31,11 +35,14 @@ describe('VanishEventStrategy', () => { deleteByPubkeyExceptKinds: sandbox.stub().resolves(1), create: sandbox.stub().resolves(1), } + userRepository = { + setVanished: sandbox.stub().resolves(1), + } webSocketEmitStub = sandbox.stub() webSocket = { emit: webSocketEmitStub, } as any - strategy = new VanishEventStrategy(webSocket, eventRepository) + strategy = new VanishEventStrategy(webSocket, eventRepository, userRepository) }) afterEach(() => { @@ -50,6 +57,7 @@ describe('VanishEventStrategy', () => { [EventKinds.REQUEST_TO_VANISH], ) expect(eventRepository.create).to.have.been.calledOnceWithExactly(event) + expect(userRepository.setVanished).to.have.been.calledOnceWithExactly(event.pubkey, true) expect(webSocketEmitStub).to.have.been.calledOnceWithExactly( WebSocketAdapterEvent.Message, [MessageType.OK, event.id, true, ''],