fix: issues with invoices (#271)

* fix: issues with invoices

* chore: add invoice event tag

* chore: add sub limits

* chore: cleanup invoices page

* chore: use mergeDeepLeft when updating invoice

* chore: ignore whitelisted pubkey for adminssion fee

* chore: use secp256k1 bytesToHex

* fix: insecure derivation from secret

* fix: tests

* chore: consistent returns

* test: fix intg tests

* fix: intg tests

* chore: set SECRET for intg tests
This commit is contained in:
Ricardo Arturo Cabral Mejía
2023-04-07 12:48:28 -04:00
committed by GitHub
parent f23740073f
commit e1561e78fd
28 changed files with 213 additions and 181 deletions

View File

@@ -82,14 +82,14 @@ limits:
- 10
- - 40
- 49
maxLength: 65536
maxLength: 102400
- description: 96 KB for event kind ranges 11-39 and 50-max
kinds:
- - 11
- 39
- - 50
- 9007199254740991
maxLength: 98304
maxLength: 102400
rateLimits:
- description: 6 events/min for event kinds 0, 3, 40 and 41
kinds:
@@ -143,6 +143,10 @@ limits:
subscription:
maxSubscriptions: 10
maxFilters: 10
maxFilterValues: 2500
maxSubscriptionIdLength: 256
maxLimit: 5000
minPrefixLength: 4
message:
rateLimits:
- description: 240 raw messages/min

View File

@@ -107,8 +107,11 @@
var timeout
var paid = false
var fallbackTimeout
var now = Math.floor(Date.now()/1000)
console.log('invoice id', reference)
console.log('pubkey', pubkey)
console.log('bolt11', invoice)
function getBackoffTime() {
return 5000 + Math.floor(Math.random() * 5000)
@@ -149,7 +152,8 @@
var socket = new WebSocket(relayUrl)
socket.onopen = () => {
console.log('connected')
socket.send(JSON.stringify(['REQ', 'payment', { kinds: [4], authors: [relayPubkey], '#c': [reference], limit: 1 }]))
var subscription = ['REQ', 'payment', { kinds: [402], '#p': [pubkey], since: now - 60 }]
socket.send(JSON.stringify(subscription))
}
socket.onmessage = (raw) => {
@@ -162,16 +166,22 @@
switch (message[0]) {
case 'EVENT': {
// TODO: validate event
const event = message[2]
// TODO: validate signature
if (event.pubkey === relayPubkey) {
paid = true
if (
event.pubkey === relayPubkey
&& event.kind === 402
) {
const pubkeyTag = event.tags.find((t) => t[0] === 'p' && t[1] === pubkey)
const invoiceTag = event.tags.find((t) => t[0] === 'bolt11' && t[1] === invoice)
if (expiresAt) clearTimeout(timeout)
if (pubkeyTag && invoiceTag) {
paid = true
hide('pending')
show('paid')
if (expiresAt) clearTimeout(timeout)
hide('pending')
show('paid')
}
}
}
break;

View File

@@ -1,17 +1,23 @@
import { Knex } from 'knex'
import { SocketAddress } from 'net'
import { EventTags } from '../constants/base'
export type EventId = string
export type Pubkey = string
export type TagName = string
export type TagName = EventTags | string
export type Signature = string
export type Tag = TagBase & string[]
export type Secret = string
export interface TagBase {
0: TagName
[index: number]: string
type ExtraTagValues = {
[index in Range<2, 100>]?: string
}
export interface TagBase extends ExtraTagValues {
0: TagName;
1: string
}
type Enumerate<

View File

@@ -13,7 +13,6 @@ export interface IPaymentsService {
confirmInvoice(
invoice: Pick<Invoice, 'id' | 'amountPaid' | 'confirmedAt'>,
): Promise<void>
sendNewInvoiceNotification(invoice: Invoice): Promise<void>
sendInvoiceUpdateNotification(invoice: Invoice): Promise<void>
getPendingInvoices(): Promise<Invoice[]>
}

View File

@@ -82,6 +82,10 @@ export interface EventLimits {
export interface ClientSubscriptionLimits {
maxSubscriptions?: number
maxFilters?: number
maxFilterValues?: number
maxLimit?: number
minPrefixLength?: number
maxSubscriptionIdLength?: number
}
export interface ClientLimits {

View File

@@ -1,5 +1,5 @@
import { mergeDeepLeft, path, pipe } from 'ramda'
import { IRunnable } from '../@types/base'
import { path } from 'ramda'
import { createLogger } from '../factories/logger-factory'
import { delayMs } from '../utils/misc'
@@ -47,21 +47,27 @@ export class MaintenanceWorker implements IRunnable {
for (const invoice of invoices) {
debug('invoice %s: %o', invoice.id, invoice)
try {
debug('getting invoice %s from payment processor', invoice.id)
debug('getting invoice %s from payment processor: %o', invoice.id, invoice)
const updatedInvoice = await this.paymentsService.getInvoiceFromPaymentsProcessor(invoice)
await delay()
debug('updating invoice status %s: %o', invoice.id, invoice)
debug('updating invoice status %s: %o', updatedInvoice.id, updatedInvoice)
await this.paymentsService.updateInvoiceStatus(updatedInvoice)
if (
invoice.status !== updatedInvoice.status
&& updatedInvoice.status == InvoiceStatus.COMPLETED
&& invoice.confirmedAt
&& updatedInvoice.confirmedAt
) {
debug('confirming invoice %s & notifying %s', invoice.id, invoice.pubkey)
const update = pipe(
mergeDeepLeft(updatedInvoice),
mergeDeepLeft({ amountPaid: invoice.amountRequested }),
)(invoice)
await Promise.all([
this.paymentsService.confirmInvoice(invoice),
this.paymentsService.sendInvoiceUpdateNotification(invoice),
this.paymentsService.confirmInvoice(update),
this.paymentsService.sendInvoiceUpdateNotification(update),
])
await delay()

View File

@@ -27,7 +27,6 @@ export enum EventKinds {
PARAMETERIZED_REPLACEABLE_FIRST = 30000,
PARAMETERIZED_REPLACEABLE_LAST = 39999,
USER_APPLICATION_FIRST = 40000,
USER_APPLICATION_LAST = Number.MAX_SAFE_INTEGER,
}
export enum EventTags {
@@ -37,6 +36,7 @@ export enum EventTags {
Delegation = 'delegation',
Deduplication = 'd',
Expiration = 'expiration',
Invoice = 'bolt11',
}
export enum PaymentsProcessors {

View File

@@ -136,7 +136,12 @@ export class PostInvoiceController implements IController {
}
let invoice: Invoice
const amount = admissionFee.reduce((sum, fee) => sum + BigInt(fee.amount), 0n)
const amount = admissionFee.reduce((sum, fee) => {
return fee.enabled && !fee.whitelists?.pubkeys?.includes(pubkey)
? BigInt(fee.amount) + sum
: sum
}, 0n)
try {
const description = `${relayName} Admission Fee for ${toBech32('npub')(pubkey)}`
@@ -145,8 +150,6 @@ export class PostInvoiceController implements IController {
amount,
description,
)
await this.paymentsService.sendNewInvoiceNotification(invoice)
} catch (error) {
console.error('Unable to create invoice. Reason:', error)
response

View File

@@ -1,6 +1,6 @@
import { Event, ExpiringEvent } from '../@types/event'
import { EventRateLimit, FeeSchedule, Settings } from '../@types/settings'
import { getEventExpiration, getEventProofOfWork, getPubkeyProofOfWork, isEventIdValid, isEventKindOrRangeMatch, isEventSignatureValid, isExpiredEvent } from '../utils/event'
import { getEventExpiration, getEventProofOfWork, getPubkeyProofOfWork, getPublicKey, getRelayPrivateKey, isEventIdValid, isEventKindOrRangeMatch, isEventSignatureValid, isExpiredEvent } from '../utils/event'
import { IEventStrategy, IMessageHandler } from '../@types/message-handlers'
import { ContextMetadataKey } from '../constants/base'
import { createCommandResult } from '../utils/messages'
@@ -79,7 +79,15 @@ export class EventMessageHandler implements IMessageHandler {
}
}
protected getRelayPublicKey(): string {
const relayPrivkey = getRelayPrivateKey(this.settings().info.relay_url)
return getPublicKey(relayPrivkey)
}
protected canAcceptEvent(event: Event): string | undefined {
if (this.getRelayPublicKey() === event.pubkey) {
return
}
const now = Math.floor(Date.now()/1000)
const limits = this.settings().limits?.event ?? {}
@@ -185,6 +193,10 @@ export class EventMessageHandler implements IMessageHandler {
}
protected async isRateLimited(event: Event): Promise<boolean> {
if (this.getRelayPublicKey() === event.pubkey) {
return false
}
const { whitelists, rateLimits } = this.settings().limits?.event ?? {}
if (!rateLimits || !rateLimits.length) {
return false
@@ -249,6 +261,10 @@ export class EventMessageHandler implements IMessageHandler {
return
}
if (this.getRelayPublicKey() === event.pubkey) {
return
}
const isApplicableFee = (feeSchedule: FeeSchedule) =>
feeSchedule.enabled
&& !feeSchedule.whitelists?.pubkeys?.some((prefix) => event.pubkey.startsWith(prefix))

View File

@@ -17,6 +17,8 @@ export const rootRequestHandler = (request: Request, response: Response, next: N
paymentsUrl.protocol = paymentsUrl.protocol === 'wss:' ? 'https:' : 'http:'
paymentsUrl.pathname = '/invoices'
const content = settings.limits?.event?.content
const relayInformationDocument = {
name,
description,
@@ -29,12 +31,14 @@ export const rootRequestHandler = (request: Request, response: Response, next: N
limitation: {
max_message_length: settings.network.maxPayloadSize,
max_subscriptions: settings.limits?.client?.subscription?.maxSubscriptions,
max_filters: settings.limits?.client?.subscription?.maxFilters,
max_limit: 5000,
max_subid_length: 256,
min_prefix: 4,
max_filters: settings.limits?.client?.subscription?.maxFilterValues,
max_limit: settings.limits?.client?.subscription?.maxLimit,
max_subid_length: settings.limits?.client?.subscription?.maxSubscriptionIdLength,
min_prefix: settings.limits?.client?.subscription?.minPrefixLength,
max_event_tags: 2500,
max_content_length: 102400,
max_content_length: Array.isArray(content)
? content[0].maxLength // best guess since we have per-kind limits
: content?.maxLength,
min_pow_difficulty: settings.limits?.event?.eventId?.minLeadingZeroBits,
auth_required: false,
payment_required: settings.payments?.enabled,

View File

@@ -87,23 +87,33 @@ export class SubscribeMessageHandler implements IMessageHandler, IAbortable {
private canSubscribe(subscriptionId: SubscriptionId, filters: SubscriptionFilter[]): string | undefined {
const subscriptions = this.webSocket.getSubscriptions()
const existingSubscription = subscriptions.get(subscriptionId)
const subscriptionLimits = this.settings().limits?.client?.subscription
if (existingSubscription?.length && equals(filters, existingSubscription)) {
return `Duplicate subscription ${subscriptionId}: Ignorning`
}
const maxSubscriptions = this.settings().limits?.client?.subscription?.maxSubscriptions ?? 0
const maxSubscriptions = subscriptionLimits?.maxSubscriptions ?? 0
if (maxSubscriptions > 0
&& !existingSubscription?.length && subscriptions.size + 1 > maxSubscriptions
) {
return `Too many subscriptions: Number of subscriptions must be less than or equal to ${maxSubscriptions}`
}
const maxFilters = this.settings().limits?.client?.subscription?.maxFilters ?? 0
const maxFilters = subscriptionLimits?.maxFilters ?? 0
if (maxFilters > 0) {
if (filters.length > maxFilters) {
return `Too many filters: Number of filters per susbscription must be less then or equal to ${maxFilters}`
}
}
if (
typeof subscriptionLimits.maxSubscriptionIdLength === 'number'
&& subscriptionId.length > subscriptionLimits.maxSubscriptionIdLength
) {
return `Subscription ID too long: Subscription ID must be less or equal to ${subscriptionLimits.maxSubscriptionIdLength}`
}
}
}

View File

@@ -23,7 +23,8 @@ export class LnurlPaymentsProcesor implements IPaymentsProcessor {
return {
id: invoice.id,
status: response.data.settled ? InvoiceStatus['COMPLETED'] : InvoiceStatus['PENDING'],
confirmedAt: response.data.settled ? new Date() : undefined,
status: response.data.settled ? InvoiceStatus.COMPLETED : InvoiceStatus.PENDING,
}
} catch (error) {
console.error(`Unable to get invoice ${invoice.id}. Reason:`, error)

View File

@@ -10,7 +10,7 @@ export const kindSchema = Schema.number().min(0).multiple(1).label('kind')
export const signatureSchema = Schema.string().case('lower').hex().length(128).label('sig')
export const subscriptionSchema = Schema.string().min(1).max(255).label('subscriptionId')
export const subscriptionSchema = Schema.string().min(1).label('subscriptionId')
const seconds = (value: any, helpers: any) => (Number.isSafeInteger(value) && Math.log10(value) < 10) ? value : helpers.error('any.invalid')
@@ -20,5 +20,4 @@ export const createdAtSchema = Schema.number().min(0).multiple(1).custom(seconds
export const tagSchema = Schema.array()
.ordered(Schema.string().max(255).required().label('identifier'))
.items(Schema.string().allow('').max(1024).label('value'))
.max(10)
.label('tag')

View File

@@ -31,10 +31,9 @@ export const eventSchema = Schema.object({
pubkey: pubkeySchema.required(),
created_at: createdAtSchema.required(),
kind: kindSchema.required(),
tags: Schema.array().items(tagSchema).max(2500).required(),
tags: Schema.array().items(tagSchema).required(),
content: Schema.string()
.allow('')
.max(100 * 1024) // 100 kB
.required(),
sig: signatureSchema.required(),
}).unknown(false)

View File

@@ -3,10 +3,10 @@ import Schema from 'joi'
import { createdAtSchema, kindSchema, prefixSchema } from './base-schema'
export const filterSchema = Schema.object({
ids: Schema.array().items(prefixSchema.label('prefixOrId')).max(1000),
authors: Schema.array().items(prefixSchema.label('prefixOrAuthor')).max(1000),
kinds: Schema.array().items(kindSchema).max(20),
ids: Schema.array().items(prefixSchema.label('prefixOrId')),
authors: Schema.array().items(prefixSchema.label('prefixOrAuthor')),
kinds: Schema.array().items(kindSchema),
since: createdAtSchema,
until: createdAtSchema,
limit: Schema.number().min(0).multiple(1).max(5000),
}).pattern(/^#[a-z]$/, Schema.array().items(Schema.string().max(1024)).max(256))
limit: Schema.number().min(0).multiple(1),
}).pattern(/^#[a-z]$/, Schema.array().items(Schema.string().max(1024)))

View File

@@ -1,17 +1,16 @@
import { andThen, pipe } from 'ramda'
import { broadcastEvent, encryptKind4Event, getPublicKey, getRelayPrivateKey, identifyEvent, signEvent } from '../utils/event'
import { andThen, otherwise, pipe } from 'ramda'
import { broadcastEvent, getPublicKey, getRelayPrivateKey, identifyEvent, signEvent } from '../utils/event'
import { DatabaseClient, Pubkey } from '../@types/base'
import { FeeSchedule, Settings } from '../@types/settings'
import { IEventRepository, IInvoiceRepository, IUserRepository } from '../@types/repositories'
import { Invoice, InvoiceStatus, InvoiceUnit } from '../@types/invoice'
import { Event, ExpiringEvent, UnidentifiedEvent } from '../@types/event'
import { EventExpirationTimeMetadataKey, EventKinds, EventTags } from '../constants/base'
import { createLogger } from '../factories/logger-factory'
import { EventKinds } from '../constants/base'
import { IPaymentsProcessor } from '../@types/clients'
import { IPaymentsService } from '../@types/services'
import { toBech32 } from '../utils/transform'
import { Transaction } from '../database/transaction'
import { UnidentifiedEvent } from '../@types/event'
const debug = createLogger('payments-service')
@@ -54,7 +53,7 @@ export class PaymentsService implements IPaymentsService {
amount: bigint,
description: string,
): Promise<Invoice> {
debug('create invoice for %s for %s: %d', pubkey, amount.toString(), description)
debug('create invoice for %s for %s: %s', pubkey, amount.toString(), description)
const transaction = new Transaction(this.dbClient)
try {
@@ -220,68 +219,6 @@ export class PaymentsService implements IPaymentsService {
}
}
public async sendNewInvoiceNotification(invoice: Invoice): Promise<void> {
debug('invoice created notification %s: %o', invoice.id, invoice)
const currentSettings = this.settings()
const {
info: {
relay_url: relayUrl,
name: relayName,
},
} = currentSettings
const relayPrivkey = getRelayPrivateKey(relayUrl)
const relayPubkey = getPublicKey(relayPrivkey)
let unit: string = invoice.unit
let amount: bigint = invoice.amountRequested
if (invoice.unit === InvoiceUnit.MSATS) {
amount /= 1000n
unit = 'sats'
}
const url = new URL(relayUrl)
const terms = new URL(relayUrl)
terms.protocol = ['https', 'wss'].includes(url.protocol)
? 'https'
: 'http'
terms.pathname += 'terms'
const unsignedInvoiceEvent: UnidentifiedEvent = {
pubkey: relayPubkey,
kind: EventKinds.ENCRYPTED_DIRECT_MESSAGE,
created_at: Math.floor(invoice.createdAt.getTime() / 1000),
content: `From: ${toBech32('npub')(relayPubkey)}@${url.hostname} (${relayName})
To: ${toBech32('npub')(invoice.pubkey)}@${url.hostname}
🧾 Admission Fee Invoice
Amount: ${amount.toString()} ${unit}
⚠️ By paying this invoice, you confirm that you have read and agree to the Terms of Service:
${terms.toString()}
${invoice.expiresAt ? `
⏳ Expires at ${invoice.expiresAt.toISOString()}` : ''}
${invoice.bolt11}`,
tags: [
['p', invoice.pubkey],
['bolt11', invoice.bolt11],
],
}
const persistEvent = this.eventRepository.create.bind(this.eventRepository)
await pipe(
identifyEvent,
andThen(encryptKind4Event(relayPrivkey, invoice.pubkey)),
andThen(signEvent(relayPrivkey)),
andThen(broadcastEvent),
andThen(persistEvent),
)(unsignedInvoiceEvent)
}
public async sendInvoiceUpdateNotification(invoice: Invoice): Promise<void> {
debug('invoice updated notification %s: %o', invoice.id, invoice)
const currentSettings = this.settings()
@@ -289,7 +226,6 @@ ${invoice.bolt11}`,
const {
info: {
relay_url: relayUrl,
name: relayName,
},
} = currentSettings
@@ -309,31 +245,36 @@ ${invoice.bolt11}`,
unit = InvoiceUnit.SATS
}
const url = new URL(relayUrl)
const now = new Date()
const expiration = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate())
const unsignedInvoiceEvent: UnidentifiedEvent = {
const unsignedInvoiceEvent: UnidentifiedEvent & Pick<ExpiringEvent, typeof EventExpirationTimeMetadataKey> = {
pubkey: relayPubkey,
kind: EventKinds.ENCRYPTED_DIRECT_MESSAGE,
created_at: Math.floor(invoice.createdAt.getTime() / 1000),
content: `🧾 Admission Fee Invoice Paid for ${relayPubkey}@${url.hostname} (${relayName})
Amount received: ${amount.toString()} ${unit}
Thanks!`,
kind: EventKinds.INVOICE_UPDATE,
created_at: Math.floor(now.getTime() / 1000),
content: `Invoice paid: ${amount.toString()} ${unit}`,
tags: [
['p', invoice.pubkey],
['c', invoice.id],
[EventTags.Pubkey, invoice.pubkey],
[EventTags.Invoice, invoice.bolt11],
[EventTags.Expiration, Math.floor(expiration.getTime() / 1000).toString()],
],
[EventExpirationTimeMetadataKey]: expiration.getTime() / 1000,
}
const persistEvent = this.eventRepository.create.bind(this.eventRepository)
const persistEvent = async (event: Event) => {
await this.eventRepository.create(event)
return event
}
const logError = (error: Error) => console.error('Unable to send notification', error)
await pipe(
identifyEvent,
andThen(encryptKind4Event(relayPrivkey, invoice.pubkey)),
andThen(signEvent(relayPrivkey)),
andThen(broadcastEvent),
andThen(persistEvent),
andThen(broadcastEvent),
otherwise(logError),
)(unsignedInvoiceEvent)
}
}

View File

@@ -181,15 +181,33 @@ export const identifyEvent = async (event: UnidentifiedEvent): Promise<UnsignedE
return { ...event, id }
}
export function getRelayPrivateKey(relayUrl: string): string {
if (process.env.RELAY_PRIVATE_KEY) {
return process.env.RELAY_PRIVATE_KEY
let privateKeyCache: string | undefined
export function getRelayPrivateKey(secret?: string): string {
if (privateKeyCache) {
return privateKeyCache
}
return deriveFromSecret(relayUrl).toString('hex')
if (process.env.RELAY_PRIVATE_KEY) {
privateKeyCache = process.env.RELAY_PRIVATE_KEY
return privateKeyCache
}
privateKeyCache = deriveFromSecret(secret).toString('hex')
return privateKeyCache
}
export const getPublicKey = (privkey: string | Buffer) => Buffer.from(secp256k1.getPublicKey(privkey, true)).subarray(1).toString('hex')
const publicKeyCache: Record<string, string> = {}
export const getPublicKey = (privkey: string) => {
if (privkey in publicKeyCache) {
return publicKeyCache[privkey]
}
publicKeyCache[privkey] = secp256k1.utils.bytesToHex(secp256k1.getPublicKey(privkey, true).subarray(1))
return publicKeyCache[privkey]
}
export const signEvent = (privkey: string | Buffer | undefined) => async (event: UnsignedEvent): Promise<Event> => {
const sig = await secp256k1.schnorr.sign(event.id, privkey as any)

View File

@@ -1,11 +1,15 @@
import { createHmac } from 'crypto'
export function deriveFromSecret(purpose: string | Buffer): Buffer {
return hmacSha256(process.env.SECRET as string, purpose)
if (!process.env.SECRET) {
throw new Error('SECRET environment variable not set')
}
return hmacSha256(process.env.SECRET, purpose)
}
export function hmacSha256(secret: string | Buffer, data: string | Buffer): Buffer {
return createHmac('sha256', secret)
.update(data)
.digest()
return createHmac('sha256', secret)
.update(data)
.digest()
}

View File

@@ -17,8 +17,6 @@ export class SlidingWindowRateLimiter implements IRateLimiter {
const timestamp = Date.now()
const { period } = options
debug('add %d hits on %s bucket', step, key)
const [,, entries] = await Promise.all([
this.cache.removeRangeByScoreFromSortedSet(key, 0, timestamp - period),
this.cache.addToSortedSet(key, { [`${timestamp}:${step}`]: timestamp.toString() }),

View File

@@ -54,7 +54,7 @@ export async function createEvent(input: Partial<Event>, privkey: any): Promise<
}
export function createIdentity(name: string) {
const hmac = createHmac('sha256', process.env.SECRET ?? Math.random().toString())
const hmac = createHmac('sha256', Math.random().toString())
hmac.update(name)
const privkey = hmac.digest().toString('hex')
const pubkey = Buffer.from(secp256k1.getPublicKey(privkey, true)).toString('hex').substring(2)

View File

@@ -72,7 +72,6 @@ Feature: NIP-01
And Alice subscribes to text_note events from Bob and set_metadata events from Charlie
Then Alice receives 2 events from Bob and Charlie
@test
Scenario: Alice is interested in Bob's events from back in November
Given someone called Alice
And someone called Bob

View File

@@ -12,9 +12,10 @@ Feature: NIP-16 Event treatment
Scenario: Charlie sends an ephemeral event
Given someone called Charlie
And Charlie subscribes to author Charlie
Given someone called Alice
And Alice subscribes to author Charlie
When Charlie sends a ephemeral_event_0 event with content "now you see me"
Then Charlie receives a ephemeral_event_0 event from Charlie with content "now you see me"
Then Charlie unsubscribes from author Charlie
When Charlie subscribes to author Charlie
Then Charlie receives 0 ephemeral_event_0 events and EOSE
Then Alice receives a ephemeral_event_0 event from Charlie with content "now you see me"
Then Alice unsubscribes from author Charlie
When Alice subscribes to author Charlie
Then Alice receives 0 ephemeral_event_0 events and EOSE

View File

@@ -35,6 +35,7 @@ export const streams = new WeakMap<WebSocket, Observable<unknown>>()
BeforeAll({ timeout: 1000 }, async function () {
process.env.RELAY_PORT = '18808'
process.env.SECRET = Math.random().toString().repeat(6)
cacheClient = getCacheClient()
dbClient = getMasterDbClient()
rrDbClient = getReadReplicaDbClient()
@@ -43,11 +44,12 @@ BeforeAll({ timeout: 1000 }, async function () {
const settings = SettingsStatic.createSettings()
SettingsStatic._settings = pipe(
assocPath( ['limits', 'event', 'createdAt', 'maxPositiveDelta'], 0),
assocPath( ['limits', 'message', 'rateLimits'], []),
assocPath( ['limits', 'event', 'rateLimits'], []),
assocPath( ['limits', 'invoice', 'rateLimits'], []),
assocPath( ['limits', 'connection', 'rateLimits'], []),
assocPath(['payments', 'enabled'], false),
assocPath(['limits', 'event', 'createdAt', 'maxPositiveDelta'], 0),
assocPath(['limits', 'message', 'rateLimits'], []),
assocPath(['limits', 'event', 'rateLimits'], []),
assocPath(['limits', 'invoice', 'rateLimits'], []),
assocPath(['limits', 'connection', 'rateLimits'], []),
)(settings) as any
worker = workerFactory()
@@ -80,11 +82,10 @@ After(async function () {
const dbClient = getMasterDbClient()
await dbClient('events')
.where({
event_pubkey: Object
.whereIn('event_pubkey', Object
.values(this.parameters.identities as Record<string, { pubkey: string }>)
.map(({ pubkey }) => Buffer.from(pubkey, 'hex')),
}).del()
).delete()
this.parameters.identities = {}
})
@@ -94,14 +95,14 @@ Given(/someone called (\w+)/, async function(name: string) {
this.parameters.clients[name] = connection
this.parameters.subscriptions[name] = []
this.parameters.events[name] = []
const subject = new Subject()
connection.once('close', subject.next.bind(subject))
const close = new Subject()
connection.once('close', close.next.bind(close))
const project = (raw: MessageEvent) => JSON.parse(raw.data.toString('utf8'))
const projection = (raw: MessageEvent) => JSON.parse(raw.data.toString('utf8'))
const replaySubject = new ReplaySubject(2, 1000)
fromEvent(connection, 'message').pipe(map(project) as any,takeUntil(subject)).subscribe(replaySubject)
fromEvent(connection, 'message').pipe(map(projection) as any,takeUntil(close)).subscribe(replaySubject)
streams.set(
connection,

View File

@@ -11,6 +11,7 @@ import { IncomingEventMessage, MessageType } from '../../../src/@types/messages'
import { DelegatedEventMessageHandler } from '../../../src/handlers/delegated-event-message-handler'
import { Event } from '../../../src/@types/event'
import { EventMessageHandler } from '../../../src/handlers/event-message-handler'
import { EventTags } from '../../../src/constants/base'
import { IUserRepository } from '../../../src/@types/repositories'
import { WebSocketAdapterEvent } from '../../../src/constants/adapter'
@@ -38,7 +39,7 @@ describe('DelegatedEventMessageHandler', () => {
pubkey: 'f'.repeat(64),
sig: 'f'.repeat(128),
tags: [
['delegation', 'delegator', 'rune', 'signature'],
[EventTags.Delegation, 'delegator', 'rune', 'signature'],
],
}
})
@@ -192,7 +193,7 @@ describe('DelegatedEventMessageHandler', () => {
'kind': 1,
'tags': [
[
'delegation',
EventTags.Delegation,
'86f0689bd48dcd19c67a19d994f938ee34f251d8c39976290955ff585f2db42e',
'kind=1&created_at>1640995200',
'c33c88ba78ec3c760e49db591ac5f7b129e3887c8af7729795e85a0588007e5ac89b46549232d8f918eefd73e726cb450135314bfda419c030d0b6affe401ec1',

View File

@@ -26,11 +26,17 @@ describe('EventMessageHandler', () => {
let event: Event
let message: IncomingEventMessage
let sandbox: Sinon.SinonSandbox
let origEnv: NodeJS.ProcessEnv
let originalConsoleWarn: (message?: any, ...optionalParams: any[]) => void | undefined = undefined
beforeEach(() => {
sandbox = Sinon.createSandbox()
origEnv = { ...process.env }
process.env = {
// deepcode ignore HardcodedNonCryptoSecret/test: <please specify a reason of ignoring this>
SECRET: 'changeme',
}
originalConsoleWarn = console.warn
console.warn = () => undefined
event = {
@@ -45,6 +51,7 @@ describe('EventMessageHandler', () => {
})
afterEach(() => {
process.env = origEnv
console.warn = originalConsoleWarn
sandbox.restore()
})
@@ -75,7 +82,9 @@ describe('EventMessageHandler', () => {
webSocket as any,
strategyFactoryStub,
userRepository,
() => ({}) as any,
() => ({
info: { relay_url: 'relay_url' },
}) as any,
() => ({ hit: async () => false })
)
})
@@ -128,7 +137,7 @@ describe('EventMessageHandler', () => {
expect(isUserAdmitted).to.have.been.calledWithExactly(event)
expect(strategyFactoryStub).not.to.have.been.called
})
it('rejects event if it is expired', async () => {
isEventValidStub.resolves(undefined)
@@ -223,6 +232,9 @@ describe('EventMessageHandler', () => {
},
}
settings = {
info: {
relay_url: 'relay_url',
},
limits: {
event: eventLimits,
},
@@ -690,6 +702,9 @@ describe('EventMessageHandler', () => {
rateLimits: [],
}
settings = {
info: {
relay_url: 'relay_url',
},
limits: {
event: eventLimits,
},

View File

@@ -11,7 +11,7 @@ chai.use(sinonChai)
const { expect } = chai
import { ContextMetadataKey, EventDeduplicationMetadataKey } from '../../../src/constants/base'
import { ContextMetadataKey, EventDeduplicationMetadataKey, EventTags } from '../../../src/constants/base'
import { DatabaseClient } from '../../../src/@types/base'
import { EventRepository } from '../../../src/repositories/event-repository'
@@ -383,12 +383,12 @@ describe('EventRepository', () => {
kind: 1,
tags: [
[
'p',
EventTags.Pubkey,
'8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9',
'wss://nostr-pub.wellorder.net',
],
[
'e',
EventTags.Event,
'7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96',
'wss://nostr-relay.untethr.me',
],
@@ -417,12 +417,12 @@ describe('EventRepository', () => {
kind: 1,
tags: [
[
'p',
EventTags.Pubkey,
'8355095016fddbe31fcf1453b26f613553e9758cf2263e190eac8fd96a3d3de9',
'wss://nostr-pub.wellorder.net',
],
[
'e',
EventTags.Event,
'7377fa81fc6c7ae7f7f4ef8938d4a603f7bf98183b35ab128235cc92d4bebf96',
'wss://nostr-relay.untethr.me',
],

View File

@@ -1,8 +1,9 @@
import { assocPath, omit, range } from 'ramda'
import { assocPath, omit } from 'ramda'
import { expect } from 'chai'
import { Event } from '../../../src/@types/event'
import { eventSchema } from '../../../src/schemas/event-schema'
import { EventTags } from '../../../src/constants/base'
import { validateSchema } from '../../../src/utils/validation'
describe('NIP-01', () => {
@@ -16,31 +17,31 @@ describe('NIP-01', () => {
'kind': 7,
'tags': [
[
'e',
EventTags.Event,
'c58e83bb744e4c29642db7a5c3bd1519516ad5c51f6ba5f90c451d03c1961210',
'',
'root',
],
[
'e',
EventTags.Event,
'd0d78967b734628cec7bdfa2321c71c1f1c48e211b4b54333c3b0e94e7e99166',
'',
'reply',
],
[
'p',
EventTags.Pubkey,
'edfa27d49d2af37ee331e1225bb6ed1912c6d999281b36d8018ad99bc3573c29',
],
[
'p',
EventTags.Pubkey,
'32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245',
],
[
'e',
EventTags.Event,
'6fed2aae1e4f7d8b535774e4f7061c10e2ff20df1ef047da09462c7937925cd5',
],
[
'p',
EventTags.Pubkey,
'2ef93f01cd2493e04235a6b87b10d3c4a74e2a7eb7c3caf168268f6af73314b5',
],
],
@@ -115,7 +116,6 @@ describe('NIP-01', () => {
],
tag: [
{ message: 'must be an array', transform: assocPath(['tags', 0], null) },
{ message: 'must contain less than or equal to 10 items', transform: assocPath(['tags', 0], range(0, 11).map(() => 'x')) },
],
identifier: [
{ message: 'must be a string', transform: assocPath(['tags', 0, 0], null) },

View File

@@ -1,4 +1,4 @@
import { assocPath, range } from 'ramda'
import { assocPath } from 'ramda'
import { expect } from 'chai'
import { filterSchema } from '../../../src/schemas/filter-schema'
@@ -32,7 +32,6 @@ describe('NIP-01', () => {
const cases = {
ids: [
{ message: 'must be an array', transform: assocPath(['ids'], null) },
{ message: 'must contain less than or equal to 1000 items', transform: assocPath(['ids'], range(0, 1001).map(() => 'ffff')) },
],
prefixOrId: [
{ message: 'length must be less than or equal to 64 characters long', transform: assocPath(['ids', 0], 'f'.repeat(65)) },
@@ -41,7 +40,6 @@ describe('NIP-01', () => {
],
authors: [
{ message: 'must be an array', transform: assocPath(['authors'], null) },
{ message: 'must contain less than or equal to 1000 items', transform: assocPath(['authors'], range(0, 1001).map(() => 'ffff')) },
],
prefixOrAuthor: [
{ message: 'length must be less than or equal to 64 characters long', transform: assocPath(['authors', 0], 'f'.repeat(65)) },
@@ -50,7 +48,6 @@ describe('NIP-01', () => {
],
kinds: [
{ message: 'must be an array', transform: assocPath(['kinds'], null) },
{ message: 'must contain less than or equal to 20 items', transform: assocPath(['kinds'], range(0, 21).map(() => 1)) },
],
kind: [
{ message: 'must be greater than or equal to 0', transform: assocPath(['kinds', 0], -1) },
@@ -73,11 +70,9 @@ describe('NIP-01', () => {
{ message: 'must be a number', transform: assocPath(['limit'], null) },
{ message: 'must be greater than or equal to 0', transform: assocPath(['limit'], -1) },
{ message: 'must be a multiple of 1', transform: assocPath(['limit'], Math.PI) },
{ message: 'must be less than or equal to 5000', transform: assocPath(['limit'], 5001) },
],
'#e': [
{ message: 'must be an array', transform: assocPath(['#e'], null) },
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['#e'], range(0, 1024 + 1).map(() => 'f')) },
],
'#e[0]': [
{ message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#e', 0], 'f'.repeat(1024 + 1)) },
@@ -85,7 +80,6 @@ describe('NIP-01', () => {
],
'#p': [
{ message: 'must be an array', transform: assocPath(['#p'], null) },
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['#p'], range(0, 1024 + 1).map(() => 'f')) },
],
'#p[0]': [
{ message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#p', 0], 'f'.repeat(1024 + 1)) },
@@ -93,7 +87,6 @@ describe('NIP-01', () => {
],
'#r': [
{ message: 'must be an array', transform: assocPath(['#r'], null) },
{ message: 'must contain less than or equal to 256 items', transform: assocPath(['#r'], range(0, 1024 + 1).map(() => 'f')) },
],
'#r[0]': [
{ message: 'length must be less than or equal to 1024 characters long', transform: assocPath(['#r', 0], 'f'.repeat(1024 + 1)) },