Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: issues with invoices #271

Merged
merged 13 commits into from
Apr 7, 2023
8 changes: 6 additions & 2 deletions resources/default-settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
26 changes: 18 additions & 8 deletions resources/invoices.html
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,11 @@ <h2 class="text-danger">Invoice expired!</h2>
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)
Expand Down Expand Up @@ -149,7 +152,8 @@ <h2 class="text-danger">Invoice expired!</h2>
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) => {
Expand All @@ -162,16 +166,22 @@ <h2 class="text-danger">Invoice expired!</h2>

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;
Expand Down
14 changes: 10 additions & 4 deletions src/@types/base.ts
Original file line number Diff line number Diff line change
@@ -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<
Expand Down
1 change: 0 additions & 1 deletion src/@types/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]>
}
4 changes: 4 additions & 0 deletions src/@types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 12 additions & 6 deletions src/app/maintenance-worker.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion src/constants/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -37,6 +36,7 @@ export enum EventTags {
Delegation = 'delegation',
Deduplication = 'd',
Expiration = 'expiration',
Invoice = 'bolt11',
}

export enum PaymentsProcessors {
Expand Down
9 changes: 6 additions & 3 deletions src/controllers/invoices/post-invoice-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`

Expand All @@ -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
Expand Down
18 changes: 17 additions & 1 deletion src/handlers/event-message-handler.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 ?? {}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
14 changes: 9 additions & 5 deletions src/handlers/request-handlers/root-request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
14 changes: 12 additions & 2 deletions src/handlers/subscribe-message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
}


}
}
3 changes: 2 additions & 1 deletion src/payments-processors/lnurl-payments-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions src/schemas/base-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -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')
3 changes: 1 addition & 2 deletions src/schemas/event-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
10 changes: 5 additions & 5 deletions src/schemas/filter-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Loading