Skip to content

Commit 8f29440

Browse files
committed
feat: support parameterized replaceable evts
1 parent 4227937 commit 8f29440

File tree

10 files changed

+117
-8
lines changed

10 files changed

+117
-8
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
exports.up = function (knex) {
2+
return knex.schema.alterTable('events', function (table) {
3+
table.jsonb('event_deduplication').nullable()
4+
})
5+
}
6+
7+
exports.down = function (knex) {
8+
return knex.schema.alterTable('events', function (table) {
9+
table.dropColumn('event_deduplication')
10+
})
11+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
exports.up = async function (knex) {
2+
// NIP-33: Parameterized Replaceable Events
3+
4+
return knex.schema
5+
.raw('DROP INDEX IF EXISTS replaceable_events_idx')
6+
.raw(
7+
`CREATE UNIQUE INDEX replaceable_events_idx
8+
ON events ( event_pubkey, event_kind, event_deduplication )
9+
WHERE
10+
(
11+
event_kind = 0
12+
OR event_kind = 3
13+
OR (event_kind >= 10000 AND event_kind < 20000)
14+
)
15+
OR (event_kind >= 30000 AND event_kind < 40000);`,
16+
)
17+
}
18+
19+
exports.down = function (knex) {
20+
return knex.schema
21+
.raw('DROP INDEX IF EXISTS replaceable_events_idx')
22+
.raw(
23+
'CREATE UNIQUE INDEX replaceable_events_idx ON events ( event_pubkey, event_kind ) WHERE event_kind = 0 OR event_kind = 3 OR (event_kind >= 10000 AND event_kind < 20000);',
24+
)
25+
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
15,
1313
16,
1414
22,
15-
26
15+
26,
16+
33
1617
],
1718
"main": "src/index.ts",
1819
"scripts": {

src/@types/event.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EventDelegatorMetadataKey, EventKinds } from '../constants/base'
1+
import { EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventKinds } from '../constants/base'
22
import { EventId, Pubkey, Tag } from './base'
33

44

@@ -16,6 +16,10 @@ export interface DelegatedEvent extends Event {
1616
[EventDelegatorMetadataKey]?: Pubkey
1717
}
1818

19+
export interface ParameterizedReplaceableEvent extends Event {
20+
[EventDeduplicationMetadataKey]: string[]
21+
}
22+
1923
export interface DBEvent {
2024
id: string
2125
event_id: Buffer
@@ -26,6 +30,7 @@ export interface DBEvent {
2630
event_tags: Tag[]
2731
event_signature: Buffer
2832
event_delegator?: Buffer | null
33+
event_deduplication?: string | null
2934
first_seen: Date
3035
deleted_at: Date
3136
}

src/constants/base.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export enum EventTags {
1313
Pubkey = 'p',
1414
// Multicast = 'm',
1515
Delegation = 'delegation',
16+
Deduplication = 'd',
1617
}
1718

1819
export const EventDelegatorMetadataKey = Symbol('Delegator')
19-
20+
export const EventDeduplicationMetadataKey = Symbol('Deduplication')

src/factories/event-strategy-factory.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isDeleteEvent, isEphemeralEvent, isReplaceableEvent } from '../utils/event'
1+
import { isDeleteEvent, isEphemeralEvent, isParameterizedReplaceableEvent, isReplaceableEvent } from '../utils/event'
22
import { DefaultEventStrategy } from '../handlers/event-strategies/default-event-strategy'
33
import { DeleteEventStrategy } from '../handlers/event-strategies/delete-event-strategy'
44
import { EphemeralEventStrategy } from '../handlers/event-strategies/ephemeral-event-strategy'
@@ -7,6 +7,7 @@ import { Factory } from '../@types/base'
77
import { IEventRepository } from '../@types/repositories'
88
import { IEventStrategy } from '../@types/message-handlers'
99
import { IWebSocketAdapter } from '../@types/adapters'
10+
import { ParameterizedReplaceableEventStrategy } from '../handlers/event-strategies/parameterized-replaceable-event-strategy'
1011
import { ReplaceableEventStrategy } from '../handlers/event-strategies/replaceable-event-strategy'
1112

1213

@@ -20,6 +21,8 @@ export const eventStrategyFactory = (
2021
return new EphemeralEventStrategy(adapter)
2122
} else if (isDeleteEvent(event)) {
2223
return new DeleteEventStrategy(adapter, eventRepository)
24+
} else if (isParameterizedReplaceableEvent(event)) {
25+
return new ParameterizedReplaceableEventStrategy(adapter, eventRepository)
2326
}
2427

2528
return new DefaultEventStrategy(adapter, eventRepository)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Event, ParameterizedReplaceableEvent } from '../../@types/event'
2+
import { EventDeduplicationMetadataKey, EventTags } from '../../constants/base'
3+
import { createLogger } from '../../factories/logger-factory'
4+
import { IEventRepository } from '../../@types/repositories'
5+
import { IEventStrategy } from '../../@types/message-handlers'
6+
import { IWebSocketAdapter } from '../../@types/adapters'
7+
import { WebSocketAdapterEvent } from '../../constants/adapter'
8+
9+
const debug = createLogger('parameterized-replaceable-event-strategy')
10+
11+
export class ParameterizedReplaceableEventStrategy implements IEventStrategy<Event, Promise<void>> {
12+
public constructor(
13+
private readonly webSocket: IWebSocketAdapter,
14+
private readonly eventRepository: IEventRepository,
15+
) { }
16+
17+
public async execute(event: Event): Promise<void> {
18+
debug('received event: %o', event)
19+
20+
const [, ...deduplication] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication) ?? [null, '']
21+
22+
const parameterizedReplaceableEvent: ParameterizedReplaceableEvent = {
23+
...event,
24+
[EventDeduplicationMetadataKey]: deduplication,
25+
}
26+
27+
const count = await this.eventRepository.upsert(parameterizedReplaceableEvent)
28+
if (!count) {
29+
return
30+
}
31+
32+
this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event)
33+
}
34+
}

src/repositories/event-repository.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
modulo,
2020
nth,
2121
omit,
22+
paths,
2223
pipe,
2324
prop,
2425
propSatisfies,
@@ -28,10 +29,10 @@ import {
2829

2930
import { DatabaseClient, EventId } from '../@types/base'
3031
import { DBEvent, Event } from '../@types/event'
32+
import { EventDeduplicationMetadataKey, EventDelegatorMetadataKey } from '../constants/base'
3133
import { IEventRepository, IQueryResult } from '../@types/repositories'
3234
import { toBuffer, toJSON } from '../utils/transform'
3335
import { createLogger } from '../factories/logger-factory'
34-
import { EventDelegatorMetadataKey } from '../constants/base'
3536
import { isGenericTagQuery } from '../utils/filter'
3637
import { SubscriptionFilter } from '../@types/subscription'
3738

@@ -157,11 +158,11 @@ export class EventRepository implements IEventRepository {
157158
}
158159

159160
public async create(event: Event): Promise<number> {
160-
debug('creating event: %o', event)
161161
return this.insert(event).then(prop('rowCount') as () => number)
162162
}
163163

164164
private insert(event: Event) {
165+
debug('inserting event: %o', event)
165166
const row = applySpec({
166167
event_id: pipe(prop('id'), toBuffer),
167168
event_pubkey: pipe(prop('pubkey'), toBuffer),
@@ -186,6 +187,7 @@ export class EventRepository implements IEventRepository {
186187

187188
public upsert(event: Event): Promise<number> {
188189
debug('upserting event: %o', event)
190+
189191
const toJSON = (input: any) => JSON.stringify(input)
190192

191193
const row = applySpec({
@@ -201,13 +203,23 @@ export class EventRepository implements IEventRepository {
201203
pipe(prop(EventDelegatorMetadataKey as any), toBuffer),
202204
always(null),
203205
),
206+
event_deduplication: ifElse(
207+
propSatisfies(isNil, EventDeduplicationMetadataKey),
208+
pipe(paths([['pubkey'], ['kind']]), toJSON),
209+
pipe(prop(EventDeduplicationMetadataKey as any), toJSON),
210+
),
204211
})(event)
205212

206213
const query = this.dbClient('events')
207214
.insert(row)
208215
// NIP-16: Replaceable Events
209-
.onConflict(this.dbClient.raw('(event_pubkey, event_kind) WHERE event_kind = 0 OR event_kind = 3 OR event_kind >= 10000 AND event_kind < 2000'))
210-
.merge(omit(['event_pubkey', 'event_kind'])(row))
216+
// NIP-33: Parameterized Replaceable Events
217+
.onConflict(
218+
this.dbClient.raw(
219+
'(event_pubkey, event_kind, event_deduplication) WHERE (event_kind = 0 OR event_kind = 3 OR (event_kind >= 10000 AND event_kind < 20000)) OR (event_kind >= 30000 AND event_kind < 40000)'
220+
)
221+
)
222+
.merge(omit(['event_pubkey', 'event_kind', 'event_deduplication'])(row))
211223
.where('events.event_created_at', '<', row.event_created_at)
212224

213225
const promise = query.then(prop('rowCount') as () => number)

src/utils/event.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ export const isEphemeralEvent = (event: Event): boolean => {
174174
return event.kind >= 20000 && event.kind < 30000
175175
}
176176

177+
export const isParameterizedReplaceableEvent = (event: Event): boolean => {
178+
return event.kind >= 30000 && event.kind < 40000
179+
}
180+
177181
export const isDeleteEvent = (event: Event): boolean => {
178182
return event.kind === EventKinds.DELETE
179183
}

test/unit/utils/event.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
isEventIdValid,
1010
isEventMatchingFilter,
1111
isEventSignatureValid,
12+
isParameterizedReplaceableEvent,
1213
isReplaceableEvent,
1314
serializeEvent,
1415
} from '../../../src/utils/event'
@@ -475,3 +476,15 @@ describe('NIP-09', () => {
475476
})
476477
})
477478
})
479+
480+
describe('NIP-33', () => {
481+
describe('isParameterizedReplaceableEvent', () => {
482+
it('returns true if event is a parameterized replaceable event', () => {
483+
expect(isParameterizedReplaceableEvent({ kind: 30000 } as any)).to.be.true
484+
})
485+
486+
it('returns false if event is a parameterized replaceable event', () => {
487+
expect(isParameterizedReplaceableEvent({ kind: 40000 } as any)).to.be.false
488+
})
489+
})
490+
})

0 commit comments

Comments
 (0)