From af775353a5994ef5c46bb6afb6210c017f21337e Mon Sep 17 00:00:00 2001 From: Vikash Siwach Date: Fri, 10 Apr 2026 05:13:31 +0530 Subject: [PATCH 1/2] feat: add NIP-62 vanish event support (#418) --- 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 | 48 ++++++++++-- .../event-strategies/vanish-event-strategy.ts | 33 ++++++++ src/repositories/event-repository.ts | 26 ++++++- src/utils/event.ts | 20 ++++- .../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 | 64 +++++++++++++++- 13 files changed, 335 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..43d1ba22 100644 --- a/src/handlers/event-message-handler.ts +++ b/src/handlers/event-message-handler.ts @@ -1,15 +1,25 @@ -import { Event, ExpiringEvent } from '../@types/event' +import { ContextMetadataKey, EventExpirationTimeMetadataKey, EventKinds } 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, +} 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 +29,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 +68,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 +208,26 @@ export class EventMessageHandler implements IMessageHandler { if (!await isEventSignatureValid(event)) { return 'invalid: event signature verification failed' } + + if (event.kind === EventKinds.REQUEST_TO_VANISH && !isRequestToVanishEvent(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..1bdde933 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,22 @@ export const isDeleteEvent = (event: Event): boolean => { return event.kind === EventKinds.DELETE } +export const isRequestToVanishEvent = (event: Event, relayUrl?: string): boolean => { + if (event.kind !== EventKinds.REQUEST_TO_VANISH) { + return false + } + + if (typeof relayUrl === 'undefined') { + return true + } + + 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..b24c7f0f 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,10 @@ import { isExpiredEvent, isParameterizedReplaceableEvent, isReplaceableEvent, + isRequestToVanishEvent, serializeEvent, } from '../../../src/utils/event' -import { EventKinds } from '../../../src/constants/base' +import { expect } from 'chai' describe('NIP-01', () => { describe('serializeEvent', () => { @@ -416,6 +416,64 @@ 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 + }) + + it('returns true when event contains the relay URL', () => { + const event: Event = { + kind: 62, + tags: [[EventTags.Relay, 'relay_url']], + } as any + expect(isRequestToVanishEvent(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(isRequestToVanishEvent(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(isRequestToVanishEvent(event, 'relay_url')).to.be.false + }) + + it('returns false when there are no relay tags', () => { + const event: Event = { + kind: 62, + tags: [], + } as any + 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 + }) + }) +}) + describe('NIP-33', () => { describe('isParameterizedReplaceableEvent', () => { it('returns true if event is a parameterized replaceable event', () => { From 318155a37a06776fee96ae0710371c729cc914b7 Mon Sep 17 00:00:00 2001 From: github-manager Date: Thu, 9 Apr 2026 20:11:24 -0400 Subject: [PATCH 2/2] ci: remove sonarcloud step from checks workflow --- .github/workflows/checks.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 638fa39a..d1d571b9 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -129,25 +129,6 @@ jobs: with: name: integration-coverage-lcov path: .coverage/integration/lcov.info - sonarcloud: - name: Sonarcloud - needs: [test-units-and-cover, test-integrations-and-cover] - if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - uses: actions/download-artifact@v4 - name: Download unit & integration coverage reports - with: - path: .coverage - - name: SonarCloud Scan - uses: sonarsource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} post-tests: name: Post Tests needs: [test-units-and-cover, test-integrations-and-cover]