Skip to content

Commit

Permalink
feat: support parameterized replaceable evts
Browse files Browse the repository at this point in the history
  • Loading branch information
cameri committed Nov 6, 2022
1 parent 4227937 commit 8f29440
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 8 deletions.
11 changes: 11 additions & 0 deletions migrations/20221030_134400_add_deduplication_to_events_table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.alterTable('events', function (table) {
table.jsonb('event_deduplication').nullable()
})
}

exports.down = function (knex) {
return knex.schema.alterTable('events', function (table) {
table.dropColumn('event_deduplication')
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
exports.up = async function (knex) {
// NIP-33: Parameterized Replaceable Events

return knex.schema
.raw('DROP INDEX IF EXISTS replaceable_events_idx')
.raw(
`CREATE UNIQUE INDEX replaceable_events_idx
ON events ( 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);`,
)
}

exports.down = function (knex) {
return knex.schema
.raw('DROP INDEX IF EXISTS replaceable_events_idx')
.raw(
'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);',
)
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
15,
16,
22,
26
26,
33
],
"main": "src/index.ts",
"scripts": {
Expand Down
7 changes: 6 additions & 1 deletion src/@types/event.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EventDelegatorMetadataKey, EventKinds } from '../constants/base'
import { EventDeduplicationMetadataKey, EventDelegatorMetadataKey, EventKinds } from '../constants/base'
import { EventId, Pubkey, Tag } from './base'


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

export interface ParameterizedReplaceableEvent extends Event {
[EventDeduplicationMetadataKey]: string[]
}

export interface DBEvent {
id: string
event_id: Buffer
Expand All @@ -26,6 +30,7 @@ export interface DBEvent {
event_tags: Tag[]
event_signature: Buffer
event_delegator?: Buffer | null
event_deduplication?: string | null
first_seen: Date
deleted_at: Date
}
Expand Down
3 changes: 2 additions & 1 deletion src/constants/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export enum EventTags {
Pubkey = 'p',
// Multicast = 'm',
Delegation = 'delegation',
Deduplication = 'd',
}

export const EventDelegatorMetadataKey = Symbol('Delegator')

export const EventDeduplicationMetadataKey = Symbol('Deduplication')
5 changes: 4 additions & 1 deletion src/factories/event-strategy-factory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isDeleteEvent, isEphemeralEvent, isReplaceableEvent } from '../utils/event'
import { isDeleteEvent, isEphemeralEvent, isParameterizedReplaceableEvent, isReplaceableEvent } 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'
Expand All @@ -7,6 +7,7 @@ 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'
import { ReplaceableEventStrategy } from '../handlers/event-strategies/replaceable-event-strategy'


Expand All @@ -20,6 +21,8 @@ export const eventStrategyFactory = (
return new EphemeralEventStrategy(adapter)
} else if (isDeleteEvent(event)) {
return new DeleteEventStrategy(adapter, eventRepository)
} else if (isParameterizedReplaceableEvent(event)) {
return new ParameterizedReplaceableEventStrategy(adapter, eventRepository)
}

return new DefaultEventStrategy(adapter, eventRepository)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Event, ParameterizedReplaceableEvent } from '../../@types/event'
import { EventDeduplicationMetadataKey, EventTags } from '../../constants/base'
import { createLogger } from '../../factories/logger-factory'
import { IEventRepository } from '../../@types/repositories'
import { IEventStrategy } from '../../@types/message-handlers'
import { IWebSocketAdapter } from '../../@types/adapters'
import { WebSocketAdapterEvent } from '../../constants/adapter'

const debug = createLogger('parameterized-replaceable-event-strategy')

export class ParameterizedReplaceableEventStrategy implements IEventStrategy<Event, Promise<void>> {
public constructor(
private readonly webSocket: IWebSocketAdapter,
private readonly eventRepository: IEventRepository,
) { }

public async execute(event: Event): Promise<void> {
debug('received event: %o', event)

const [, ...deduplication] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication) ?? [null, '']

const parameterizedReplaceableEvent: ParameterizedReplaceableEvent = {
...event,
[EventDeduplicationMetadataKey]: deduplication,
}

const count = await this.eventRepository.upsert(parameterizedReplaceableEvent)
if (!count) {
return
}

this.webSocket.emit(WebSocketAdapterEvent.Broadcast, event)
}
}
20 changes: 16 additions & 4 deletions src/repositories/event-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
modulo,
nth,
omit,
paths,
pipe,
prop,
propSatisfies,
Expand All @@ -28,10 +29,10 @@ import {

import { DatabaseClient, EventId } from '../@types/base'
import { DBEvent, Event } from '../@types/event'
import { EventDeduplicationMetadataKey, EventDelegatorMetadataKey } from '../constants/base'
import { IEventRepository, IQueryResult } from '../@types/repositories'
import { toBuffer, toJSON } from '../utils/transform'
import { createLogger } from '../factories/logger-factory'
import { EventDelegatorMetadataKey } from '../constants/base'
import { isGenericTagQuery } from '../utils/filter'
import { SubscriptionFilter } from '../@types/subscription'

Expand Down Expand Up @@ -157,11 +158,11 @@ export class EventRepository implements IEventRepository {
}

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

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

public upsert(event: Event): Promise<number> {
debug('upserting event: %o', event)

const toJSON = (input: any) => JSON.stringify(input)

const row = applySpec({
Expand All @@ -201,13 +203,23 @@ export class EventRepository implements IEventRepository {
pipe(prop(EventDelegatorMetadataKey as any), toBuffer),
always(null),
),
event_deduplication: ifElse(
propSatisfies(isNil, EventDeduplicationMetadataKey),
pipe(paths([['pubkey'], ['kind']]), toJSON),
pipe(prop(EventDeduplicationMetadataKey as any), toJSON),
),
})(event)

const query = this.dbClient('events')
.insert(row)
// NIP-16: Replaceable Events
.onConflict(this.dbClient.raw('(event_pubkey, event_kind) WHERE event_kind = 0 OR event_kind = 3 OR event_kind >= 10000 AND event_kind < 2000'))
.merge(omit(['event_pubkey', 'event_kind'])(row))
// NIP-33: Parameterized Replaceable Events
.onConflict(
this.dbClient.raw(
'(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)'
)
)
.merge(omit(['event_pubkey', 'event_kind', 'event_deduplication'])(row))
.where('events.event_created_at', '<', row.event_created_at)

const promise = query.then(prop('rowCount') as () => number)
Expand Down
4 changes: 4 additions & 0 deletions src/utils/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ export const isEphemeralEvent = (event: Event): boolean => {
return event.kind >= 20000 && event.kind < 30000
}

export const isParameterizedReplaceableEvent = (event: Event): boolean => {
return event.kind >= 30000 && event.kind < 40000
}

export const isDeleteEvent = (event: Event): boolean => {
return event.kind === EventKinds.DELETE
}
Expand Down
13 changes: 13 additions & 0 deletions test/unit/utils/event.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isEventIdValid,
isEventMatchingFilter,
isEventSignatureValid,
isParameterizedReplaceableEvent,
isReplaceableEvent,
serializeEvent,
} from '../../../src/utils/event'
Expand Down Expand Up @@ -475,3 +476,15 @@ describe('NIP-09', () => {
})
})
})

describe('NIP-33', () => {
describe('isParameterizedReplaceableEvent', () => {
it('returns true if event is a parameterized replaceable event', () => {
expect(isParameterizedReplaceableEvent({ kind: 30000 } as any)).to.be.true
})

it('returns false if event is a parameterized replaceable event', () => {
expect(isParameterizedReplaceableEvent({ kind: 40000 } as any)).to.be.false
})
})
})

0 comments on commit 8f29440

Please sign in to comment.