Ricardo Arturo Cabral Mejía 52aac39875
feat: implement nodeless payments processor (#305)
* chore: hide powered by zebedee if payment processor is not

* chore: add nodeless as payments processor to settings

* fix: bad content type on zebedee callback req handler

* chore(release): 1.23.0 [skip ci]

# [1.23.0](https://github.com/Cameri/nostream/compare/v1.22.6...v1.23.0) (2023-05-12)

### Bug Fixes

* add SECRET as env variable ([#298](https://github.com/Cameri/nostream/issues/298)) ([58a1254](58a12546f0))
* invoice auto marked as paid ([be6d6f1](be6d6f1454))
* issues with invoices ([#271](https://github.com/Cameri/nostream/issues/271)) ([e1561e7](e1561e78fd))

### Features

* add LNURL processor ([#202](https://github.com/Cameri/nostream/issues/202)) ([f237400](f23740073f))
* allow lightning zap receipts on paid relays ([#303](https://github.com/Cameri/nostream/issues/303)) ([14bc96f](14bc96f516))

* feat: implement nodeless payments processor

* docs: add accepting payments section

* chore: validate nodeless webhook secret

* chore: hide powered-by-zebedee for non-zebedee processors

---------

Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
2023-05-15 08:07:28 -07:00

211 lines
6.1 KiB
TypeScript

import * as secp256k1 from '@noble/secp256k1'
import { createHash, createHmac, getRandomValues, Hash } from 'crypto'
import { Observable } from 'rxjs'
import WebSocket from 'ws'
import { CommandResult, MessageType, OutgoingMessage } from '../../../src/@types/messages'
import { Event } from '../../../src/@types/event'
import { serializeEvent } from '../../../src/utils/event'
import { streams } from './shared'
import { SubscriptionFilter } from '../../../src/@types/subscription'
secp256k1.utils.sha256Sync = (...messages: Uint8Array[]) =>
messages.reduce((hash: Hash, message: Uint8Array) => hash.update(message), createHash('sha256')).digest()
export async function connect(_name: string): Promise<WebSocket> {
const host = 'ws://localhost:18808'
const ws = new WebSocket(host)
return new Promise<WebSocket>((resolve, reject) => {
ws
.once('open', () => {
resolve(ws)
})
.once('error', reject)
.once('close', () => {
ws.removeAllListeners()
})
})
}
let eventCount = 0
export async function createEvent(input: Partial<Event>, privkey: any): Promise<Event> {
const event: Event = {
pubkey: input.pubkey,
kind: input.kind,
created_at: input.created_at ?? Math.floor(Date.now() / 1000) + eventCount++,
content: input.content ?? '',
tags: input.tags ?? [],
} as any
const id = createHash('sha256').update(
Buffer.from(JSON.stringify(serializeEvent(event)))
).digest().toString('hex')
const sig = Buffer.from(
secp256k1.schnorr.signSync(id, privkey)
).toString('hex')
event.id = id
event.sig = sig
return event
}
export function createIdentity(name: string) {
const buffer = new Uint32Array(10)
getRandomValues(buffer)
const hmac = createHmac('sha256', buffer)
hmac.update(name)
const privkey = hmac.digest().toString('hex')
const pubkey = Buffer.from(secp256k1.getPublicKey(privkey, true)).toString('hex').substring(2)
const author = {
name,
privkey,
pubkey,
}
return author
}
export async function createSubscription(
ws: WebSocket,
subscriptionName: string,
subscriptionFilters: SubscriptionFilter[],
): Promise<void> {
return new Promise<void>((resolve, reject) => {
const data = JSON.stringify([
'REQ',
subscriptionName,
...subscriptionFilters,
])
ws.send(data, (error?: Error) => {
if (error) {
reject(error)
} else {
resolve()
}
})
})
}
export async function waitForEOSE(ws: WebSocket, subscription: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
const observable = streams.get(ws) as Observable<OutgoingMessage>
const sub = observable.subscribe((message: OutgoingMessage) => {
if (message[0] === MessageType.EOSE && message[1] === subscription) {
resolve()
sub.unsubscribe()
} else if (message[0] === MessageType.NOTICE) {
reject(new Error(message[1]))
sub.unsubscribe()
}
})
})
}
export async function sendEvent(ws: WebSocket, event: Event, successful = true) {
return new Promise<OutgoingMessage>((resolve, reject) => {
const observable = streams.get(ws) as Observable<OutgoingMessage>
const sub = observable.subscribe((message: OutgoingMessage) => {
if (message[0] === MessageType.OK && message[1] === event.id) {
if (message[2] === successful) {
sub.unsubscribe()
resolve(message)
} else {
sub.unsubscribe()
reject(new Error(message[3]))
}
} else if (message[0] === MessageType.NOTICE) {
sub.unsubscribe()
reject(new Error(message[1]))
}
})
ws.send(JSON.stringify(['EVENT', event]), (err) => {
if (err) {
sub.unsubscribe()
reject(err)
}
})
})
}
export async function waitForNextEvent(ws: WebSocket, subscription: string, content?: string): Promise<Event> {
return new Promise((resolve, reject) => {
const observable = streams.get(ws) as Observable<OutgoingMessage>
observable.subscribe((message: OutgoingMessage) => {
if (message[0] === MessageType.EVENT && message[1] === subscription) {
const event = message[2] as Event
if (typeof content !== 'string' || event.content === content) {
resolve(message[2])
}
} else if (message[0] === MessageType.NOTICE) {
reject(new Error(message[1]))
}
})
})
}
export async function waitForEventCount(
ws: WebSocket,
subscription: string,
count = 1,
eose = false,
): Promise<Event[]> {
const events: Event[] = []
return new Promise((resolve, reject) => {
const observable = streams.get(ws) as Observable<OutgoingMessage>
observable.subscribe((message: OutgoingMessage) => {
if (message[0] === MessageType.EVENT && message[1] === subscription) {
events.push(message[2])
if (!eose && events.length === count) {
resolve(events)
} else if (events.length > count) {
reject(new Error(`Expected ${count} but got ${events.length} events`))
}
} else if (message[0] === MessageType.EOSE && message[1] === subscription) {
if (!eose) {
reject(new Error('Expected event but received EOSE'))
} else if (events.length !== count) {
reject(new Error(`Expected ${count} but got ${events.length} events before EOSE`))
} else {
resolve(events)
}
} else if (message[0] === MessageType.NOTICE) {
reject(new Error(message[1]))
}
})
})
}
export async function waitForNotice(ws: WebSocket): Promise<string> {
return new Promise<string>((resolve) => {
const observable = streams.get(ws) as Observable<OutgoingMessage>
observable.subscribe((message: OutgoingMessage) => {
if (message[0] === MessageType.NOTICE) {
resolve(message[1])
}
})
})
}
export async function waitForCommand(ws: WebSocket): Promise<CommandResult> {
return new Promise<CommandResult>((resolve) => {
const observable = streams.get(ws) as Observable<OutgoingMessage>
observable.subscribe((message: OutgoingMessage) => {
if (message[0] === MessageType.OK) {
resolve(message)
}
})
})
}