diff --git a/.changeset/dvm-nip89-discovery.md b/.changeset/dvm-nip89-discovery.md new file mode 100644 index 00000000..02c4454c --- /dev/null +++ b/.changeset/dvm-nip89-discovery.md @@ -0,0 +1,5 @@ +--- +"nostream": minor +--- + +feat(dvm): add NIP-89 handler recommendation/information kinds and verify passive accept-and-serve diff --git a/src/constants/base.ts b/src/constants/base.ts index 0715ecc0..4a99f83f 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -68,6 +68,9 @@ export enum EventKinds { PARAMETERIZED_REPLACEABLE_FIRST = 30000, // Marmot Protocol MIP-00: KeyPackage (addressable, replaces legacy 443) MARMOT_KEY_PACKAGE = 30443, + // NIP-89: Recommended Application Handlers + HANDLER_RECOMMENDATION = 31989, + HANDLER_INFORMATION = 31990, PARAMETERIZED_REPLACEABLE_LAST = 39999, USER_APPLICATION_FIRST = 40000, } diff --git a/test/integration/features/nip-89/nip-89.feature b/test/integration/features/nip-89/nip-89.feature new file mode 100644 index 00000000..a5035746 --- /dev/null +++ b/test/integration/features/nip-89/nip-89.feature @@ -0,0 +1,42 @@ +Feature: NIP-89 Recommended Application Handlers (passive accept-and-serve) + Scenario: A DVM worker publishes a handler information event + Given someone called Alice + When Alice sends a handler_information event with content "1" and tag d containing "dvm-worker" + And Alice subscribes to author Alice + Then Alice receives a handler_information event from Alice with content "1" and tag d containing "dvm-worker" + + Scenario: A newer handler information event replaces the older one + Given someone called Alice + And someone called Bob + When Alice sends a handler_information event with content "first" and tag d containing "dvm-worker" + And Alice sends a handler_information event with content "second" and tag d containing "dvm-worker" + And Bob subscribes to author Alice + Then Bob receives a handler_information event from Alice with content "second" and tag d containing "dvm-worker" + + Scenario: A user publishes a handler recommendation event + Given someone called Alice + When Alice sends a handler_recommendation event with content "" and tag d containing "5000" + And Alice subscribes to author Alice + Then Alice receives a handler_recommendation event from Alice with content "" and tag d containing "5000" + + Scenario: A newer handler recommendation event replaces the older one + Given someone called Alice + And someone called Bob + When Alice sends a handler_recommendation event with content "" and tag d containing "5001" + And Alice sends a handler_recommendation event with content "updated" and tag d containing "5001" + And Bob subscribes to author Alice + Then Bob receives a handler_recommendation event from Alice with content "updated" and tag d containing "5001" + + Scenario: A client filters by kind alone to discover handler information events + Given someone called Alice + And someone called Bob + When Alice sends a handler_information event with content "for-kind-filter" and tag d containing "kind-filter-test" + And Bob subscribes to handler_information events + Then Bob receives a handler_information event from Alice with content "for-kind-filter" and tag d containing "kind-filter-test" + + Scenario: A client filters by kind alone to discover handler recommendation events + Given someone called Alice + And someone called Bob + When Alice sends a handler_recommendation event with content "" and tag d containing "5002" + And Bob subscribes to handler_recommendation events + Then Bob receives a handler_recommendation event from Alice with content "" and tag d containing "5002" diff --git a/test/integration/features/nip-89/nip-89.feature.ts b/test/integration/features/nip-89/nip-89.feature.ts new file mode 100644 index 00000000..53ed604c --- /dev/null +++ b/test/integration/features/nip-89/nip-89.feature.ts @@ -0,0 +1,89 @@ +import { Then, When, World } from '@cucumber/cucumber' +import { expect } from 'chai' +import WebSocket from 'ws' + +import { createEvent, createSubscription, sendEvent, waitForNextEvent } from '../helpers' +import { EventKinds } from '../../../../src/constants/base' +import { Event } from '../../../../src/@types/event' + +When( + /^(\w+) sends a handler_information event with content "([^"]*)" and tag (\w) containing "([^"]+)"$/, + async function (name: string, content: string, tag: string, value: string) { + const ws = this.parameters.clients[name] as WebSocket + const { pubkey, privkey } = this.parameters.identities[name] + + const event: Event = await createEvent( + { pubkey, kind: EventKinds.HANDLER_INFORMATION, content, tags: [[tag, value]] }, + privkey, + ) + + await sendEvent(ws, event) + this.parameters.events[name].push(event) + }, +) + +Then( + /(\w+) receives a handler_information event from (\w+) with content "([^"]*?)" and tag (\w+) containing "([^"]+?)"/, + async function (name: string, author: string, content: string, tagName: string, tagValue: string) { + const ws = this.parameters.clients[name] as WebSocket + const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1] + const receivedEvent = await waitForNextEvent(ws, subscription.name) + + expect(receivedEvent.kind).to.equal(EventKinds.HANDLER_INFORMATION) + expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey) + expect(receivedEvent.content).to.equal(content) + expect(receivedEvent.tags[0]).to.deep.equal([tagName, tagValue]) + }, +) + +When( + /^(\w+) sends a handler_recommendation event with content "([^"]*)" and tag (\w) containing "([^"]+)"$/, + async function (name: string, content: string, tag: string, value: string) { + const ws = this.parameters.clients[name] as WebSocket + const { pubkey, privkey } = this.parameters.identities[name] + + const event: Event = await createEvent( + { pubkey, kind: EventKinds.HANDLER_RECOMMENDATION, content, tags: [[tag, value]] }, + privkey, + ) + + await sendEvent(ws, event) + this.parameters.events[name].push(event) + }, +) + +Then( + /(\w+) receives a handler_recommendation event from (\w+) with content "([^"]*?)" and tag (\w+) containing "([^"]+?)"/, + async function (name: string, author: string, content: string, tagName: string, tagValue: string) { + const ws = this.parameters.clients[name] as WebSocket + const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1] + const receivedEvent = await waitForNextEvent(ws, subscription.name) + + expect(receivedEvent.kind).to.equal(EventKinds.HANDLER_RECOMMENDATION) + expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey) + expect(receivedEvent.content).to.equal(content) + expect(receivedEvent.tags[0]).to.deep.equal([tagName, tagValue]) + }, +) + +When( + /^(\w+) subscribes to handler_information events$/, + async function (this: World>, name: string) { + const ws = this.parameters.clients[name] as WebSocket + const subscription = { name: `test-${Math.random()}`, filters: [{ kinds: [EventKinds.HANDLER_INFORMATION] }] } + this.parameters.subscriptions[name].push(subscription) + + await createSubscription(ws, subscription.name, subscription.filters) + }, +) + +When( + /^(\w+) subscribes to handler_recommendation events$/, + async function (this: World>, name: string) { + const ws = this.parameters.clients[name] as WebSocket + const subscription = { name: `test-${Math.random()}`, filters: [{ kinds: [EventKinds.HANDLER_RECOMMENDATION] }] } + this.parameters.subscriptions[name].push(subscription) + + await createSubscription(ws, subscription.name, subscription.filters) + }, +) diff --git a/test/unit/factories/event-strategy-factory.spec.ts b/test/unit/factories/event-strategy-factory.spec.ts index e24c0294..fcfbbf64 100644 --- a/test/unit/factories/event-strategy-factory.spec.ts +++ b/test/unit/factories/event-strategy-factory.spec.ts @@ -161,4 +161,14 @@ describe('eventStrategyFactory', () => { event.kind = EventKinds.DVM_JOB_REQUEST_LAST expect(factory([event, adapter])).to.be.an.instanceOf(DvmJobRequestEventStrategy) }) + + it('returns ParameterizedReplaceableEventStrategy given a handler recommendation event (NIP-89, kind 31989)', () => { + event.kind = EventKinds.HANDLER_RECOMMENDATION + expect(factory([event, adapter])).to.be.an.instanceOf(ParameterizedReplaceableEventStrategy) + }) + + it('returns ParameterizedReplaceableEventStrategy given a handler information event (NIP-89, kind 31990)', () => { + event.kind = EventKinds.HANDLER_INFORMATION + expect(factory([event, adapter])).to.be.an.instanceOf(ParameterizedReplaceableEventStrategy) + }) })