Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dvm-nip89-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostream": minor
---

feat(dvm): add NIP-89 handler recommendation/information kinds and verify passive accept-and-serve
3 changes: 3 additions & 0 deletions src/constants/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
42 changes: 42 additions & 0 deletions test/integration/features/nip-89/nip-89.feature
Original file line number Diff line number Diff line change
@@ -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"
89 changes: 89 additions & 0 deletions test/integration/features/nip-89/nip-89.feature.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, any>>, 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<Record<string, any>>, 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)
},
)
10 changes: 10 additions & 0 deletions test/unit/factories/event-strategy-factory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
Loading