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>
This commit is contained in:
Ricardo Arturo Cabral Mejía 2023-05-15 11:07:28 -04:00 committed by GitHub
parent 62c1dbe22c
commit 52aac39875
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 395 additions and 72 deletions

View File

@ -86,6 +86,18 @@ Install Docker from their [official guide](https://docs.docker.com/engine/instal
- [Set up a Paid Nostr relay with Nostream and ZBD](https://andreneves.xyz/p/how-to-setup-a-paid-nostr-relay) by [André Neves](https://snort.social/p/npub1rvg76s0gz535txd9ypg2dfqv0x7a80ar6e096j3v343xdxyrt4ksmkxrck) (CTO & Co-Founder at [ZEBEDEE](https://zebedee.io/))
- [Set up a Nostr relay in under 5 minutes](https://andreneves.xyz/p/set-up-a-nostr-relay-server-in-under) by [André Neves](https://twitter.com/andreneves) (CTO & Co-Founder at [ZEBEDEE](https://zebedee.io/))
### Accepting payments
1. Zebedee
- You must set ZEBEDEE_API_KEY with an API Key from one of your projects in your Zebedee Developer Dashboard. Contact @foxp2zeb on Telegram or npub1rvg76s0gz535txd9ypg2dfqv0x7a80ar6e096j3v343xdxyrt4ksmkxrck on Nostr requesting access to the Zebedee Developer Dashboard. See the Zebedee full guide on how to set up a paid relay.
2. Nodeless.io
- Sign up for a new account at https://nodeless.io and create a new store. Take note of the store ID.
- Create a store webhook and make sure to check all of the events. Grab the store webhook secret.
- Go to Profile > API Tokens and generate a new key and keep note of it.
- Set NODELESS_API_KEY and NODELESS_WEBHOOK_SECRET environment variables with generated key and webhook secret, respectively.
- On your .nostr/settings.yaml file, update the field `paymentsProcessors.nodeless.storeId1 with your store ID.
## Quick Start (Docker Compose)
Install Docker following the [official guide](https://docs.docker.com/engine/install/).
@ -201,10 +213,7 @@ You may want to use `openssl rand -hex 128` to generate a secret.
# Secret shortened for brevity
```
In addition, if using Zebedee for payments, you must also set ZEBEDEE_API_KEY with
an API Key from one of your projects in your Zebedee Developer Dashboard. Contact
@foxp2zeb on Telegram or npub1rvg76s0gz535txd9ypg2dfqv0x7a80ar6e096j3v343xdxyrt4ksmkxrck on Nostr requesting
access to the Zebedee Developer Dashboard.
### Initializing the database
Create `nostr_ts_relay` database:

View File

@ -46,6 +46,9 @@ services:
TOR_CONTROL_PORT: 9051
TOR_PASSWORD: nostr_ts_relay
HIDDEN_SERVICE_PORT: 80
# Nodeless.io
NODELESS_API_KEY: ${NODELESS_API_KEY}
NODELESS_WEBHOOK_SECRET: ${NODELESS_WEBHOOK_SECRET}
# Enable DEBUG for troubleshooting. Examples:
# DEBUG: "primary:*"
# DEBUG: "worker:*"

View File

@ -29,6 +29,9 @@ paymentsProcessors:
callbackBaseURL: https://nostream.your-domain.com/callbacks/lnbits
lnurl:
invoiceURL: https://getalby.com/lnurlp/your-username
nodeless:
baseURL: https://nodeless.io
storeId: your-nodeless-io-store-id
network:
maxPayloadSize: 524288
# Comment the next line if using CloudFlare proxy

View File

@ -58,7 +58,7 @@
<button id="submitBtn" class="btn btn-lg btn-warning" type="submit">Pay {{amount}} sats</button>
</div>
</div>
<div class="row">
<div class="row d-none" id="powered-by-zebedee">
<div class="d-flex justify-content-center mb-3 mt-4">
<a href="https://zeb.gg/nostr-zbd-quickstart" target="_blank">
<img class="poweredbyzbd-img" src="https://cdn.zebedee.io/an/nostr/poweredbyzbd.png" />
@ -129,6 +129,7 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js" integrity="sha384-cuYeSxntonz0PPNlHhBs68uyIAVpIIOZZ5JqeqvYYIcEL727kskC66kF92t6Xl2V" crossorigin="anonymous"></script>
<script>
var processor = "{{processor}}"
function attemptGetPubkey() {
const maxRetries = 10
function getPubKey(retries) {
@ -147,6 +148,9 @@
function onLoad() {
setTimeout(attemptGetPubkey, 300)
if (processor === 'zebedee') {
document.getElementById('powered-by-zebedee').classList.remove('d-none')
}
}
</script>
</body>

View File

@ -86,7 +86,7 @@
<button class="btn btn-lg btn-primary" type="submit">Get another invoice</button>
</div>
</div>
<div class="row">
<div class="row d-none" id="powered-by-zebedee">
<div class="d-flex justify-content-center mb-3 mt-4">
<a href="https://zeb.gg/nostr-zbd-quickstart" target="_blank">
<img class="poweredbyzbd-img" src="https://cdn.zebedee.io/an/nostr/poweredbyzbd.png" />
@ -104,6 +104,7 @@
var invoice = "{{invoice}}";
var pubkey = "{{pubkey}}"
var expiresAt = "{{expires_at}}"
var processor = "{{processor}}"
var timeout
var paid = false
var fallbackTimeout
@ -254,6 +255,9 @@
sendPayment().catch(() => {
document.getElementById('sendPaymentBtn').classList.remove('d-none')
})
if (processor === 'zebedee') {
document.getElementById('powered-by-zebedee').classList.remove('d-none')
}
</script>
</body>
</html>

View File

@ -8,7 +8,8 @@ export enum InvoiceUnit {
export enum InvoiceStatus {
PENDING = 'pending',
COMPLETED = 'completed'
COMPLETED = 'completed',
EXPIRED = 'expired',
}
export interface Invoice {

View File

@ -23,6 +23,10 @@ export interface IEventRepository {
export interface IInvoiceRepository {
findById(id: string, client?: DatabaseClient): Promise<Invoice | undefined>
upsert(invoice: Partial<Invoice>, client?: DatabaseClient): Promise<number>
updateStatus(
invoice: Pick<Invoice, 'id' | 'status'>,
client?: DatabaseClient,
): Promise<Invoice | undefined>
confirmInvoice(
invoiceId: string,
amountReceived: bigint,

View File

@ -9,7 +9,7 @@ export interface IPaymentsService {
description: string,
): Promise<Invoice>
updateInvoice(invoice: Partial<Invoice>): Promise<void>
updateInvoiceStatus(invoice: Partial<Invoice>): Promise<void>
updateInvoiceStatus(invoice: Pick<Invoice, 'id' | 'status'>): Promise<Invoice>
confirmInvoice(
invoice: Pick<Invoice, 'id' | 'amountPaid' | 'confirmedAt'>,
): Promise<void>

View File

@ -157,15 +157,26 @@ export interface ZebedeePaymentsProcessor {
ipWhitelist: string[]
}
export interface LNbitsPaymentProcessor {
export interface NodelessPaymentsProcessor {
baseURL: string
storeId: string
}
export interface LNbitsPaymentsProcessor {
baseURL: string
callbackBaseURL: string
}
export interface NodelessPaymentsProcessor {
baseURL: string
storeId: string
}
export interface PaymentsProcessors {
lnurl?: LnurlPaymentsProcessor,
zebedee?: ZebedeePaymentsProcessor
lnbits?: LNbitsPaymentProcessor
lnbits?: LNbitsPaymentsProcessor
nodeless?: NodelessPaymentsProcessor
}
export interface Local {

View File

@ -33,10 +33,10 @@ export class WebServerAdapter extends EventEmitter implements IWebServerAdapter
}
private onClientError(error: Error, socket: Duplex) {
console.error('web-server-adapter: client socket error:', error)
if (error['code'] === 'ECONNRESET' || !socket.writable) {
return
}
console.error('web-server-adapter: client socket error:', error)
socket.end('HTTP/1.1 400 Bad Request\r\nContent-Type: text/html\r\n')
}

View File

@ -51,7 +51,13 @@ export class MaintenanceWorker implements IRunnable {
const updatedInvoice = await this.paymentsService.getInvoiceFromPaymentsProcessor(invoice)
await delay()
debug('updating invoice status %s: %o', updatedInvoice.id, updatedInvoice)
await this.paymentsService.updateInvoiceStatus(updatedInvoice)
if (typeof updatedInvoice.id !== 'string' || typeof updatedInvoice.status !== 'string') {
continue
}
const { id, status } = updatedInvoice
await this.paymentsService.updateInvoiceStatus({ id, status })
if (
invoice.status !== updatedInvoice.status

View File

@ -0,0 +1,86 @@
import { always, applySpec, ifElse, is, path, prop, propEq, propSatisfies } from 'ramda'
import { Request, Response } from 'express'
import { Invoice, InvoiceStatus } from '../../@types/invoice'
import { createLogger } from '../../factories/logger-factory'
import { fromNodelessInvoice } from '../../utils/transform'
import { IController } from '../../@types/controllers'
import { IPaymentsService } from '../../@types/services'
const debug = createLogger('nodeless-callback-controller')
export class NodelessCallbackController implements IController {
public constructor(
private readonly paymentsService: IPaymentsService,
) {}
// TODO: Validate
public async handleRequest(
request: Request,
response: Response,
) {
debug('callback request headers: %o', request.headers)
debug('callback request body: %O', request.body)
const nodelessInvoice = applySpec({
id: prop('uuid'),
status: prop('status'),
satsAmount: prop('amount'),
metadata: prop('metadata'),
paidAt: ifElse(
propEq('status', 'paid'),
always(new Date().toISOString()),
always(null),
),
createdAt: ifElse(
propSatisfies(is(String), 'createdAt'),
prop('createdAt'),
path(['metadata', 'createdAt']),
),
})(request.body)
debug('nodeless invoice: %O', nodelessInvoice)
const invoice = fromNodelessInvoice(nodelessInvoice)
debug('invoice: %O', invoice)
let updatedInvoice: Invoice
try {
updatedInvoice = await this.paymentsService.updateInvoiceStatus(invoice)
debug('updated invoice: %O', updatedInvoice)
} catch (error) {
console.error(`Unable to persist invoice ${invoice.id}`, error)
throw error
}
if (
updatedInvoice.status !== InvoiceStatus.COMPLETED
&& !updatedInvoice.confirmedAt
) {
response
.status(200)
.send()
return
}
invoice.amountPaid = invoice.amountRequested
updatedInvoice.amountPaid = invoice.amountRequested
try {
await this.paymentsService.confirmInvoice(invoice)
await this.paymentsService.sendInvoiceUpdateNotification(updatedInvoice)
} catch (error) {
console.error(`Unable to confirm invoice ${invoice.id}`, error)
throw error
}
response
.status(200)
.setHeader('content-type', 'application/json; charset=utf8')
.send('{"status":"ok"}')
}
}

View File

@ -171,6 +171,7 @@ export class PostInvoiceController implements IController {
expires_at: invoice.expiresAt?.toISOString() ?? '',
invoice: invoice.bolt11,
amount: amount / 1000n,
processor: currentSettings.payments.processor,
}
const body = Object

View File

@ -0,0 +1,7 @@
import { createPaymentsService } from './payments-service-factory'
import { IController } from '../@types/controllers'
import { NodelessCallbackController } from '../controllers/callbacks/nodeless-callback-controller'
export const createNodelessCallbackController = (): IController => new NodelessCallbackController(
createPaymentsService(),
)

View File

@ -6,8 +6,8 @@ import { createSettings } from './settings-factory'
import { IPaymentsProcessor } from '../@types/clients'
import { LNbitsPaymentsProcesor } from '../payments-processors/lnbits-payment-processor'
import { LnurlPaymentsProcesor } from '../payments-processors/lnurl-payments-processor'
import { NodelessPaymentsProcesor } from '../payments-processors/nodeless-payments-processor'
import { NullPaymentsProcessor } from '../payments-processors/null-payments-processor'
import { PaymentsProcessor } from '../payments-processors/payments-procesor'
import { Settings } from '../@types/settings'
import { ZebedeePaymentsProcesor } from '../payments-processors/zebedee-payments-processor'
@ -30,6 +30,24 @@ const getZebedeeAxiosConfig = (settings: Settings): CreateAxiosDefaults<any> =>
}
}
const getNodelessAxiosConfig = (settings: Settings): CreateAxiosDefaults<any> => {
if (!process.env.NODELESS_API_KEY) {
const error = new Error('NODELESS_API_KEY must be set.')
console.error('Unable to get Nodeless config.', error)
throw error
}
return {
headers: {
'content-type': 'application/json',
'authorization': `Bearer ${process.env.NODELESS_API_KEY}`,
'accept': 'application/json',
},
baseURL: path(['paymentsProcessors', 'nodeless', 'baseURL'], settings),
maxRedirects: 1,
}
}
const getLNbitsAxiosConfig = (settings: Settings): CreateAxiosDefaults<any> => {
if (!process.env.LNBITS_API_KEY) {
throw new Error('LNBITS_API_KEY must be set to an invoice or admin key.')
@ -53,9 +71,7 @@ const createLnurlPaymentsProcessor = (settings: Settings): IPaymentsProcessor =>
const client = axios.create()
const app = new LnurlPaymentsProcesor(client, createSettings)
return new PaymentsProcessor(app)
return new LnurlPaymentsProcesor(client, createSettings)
}
const createZebedeePaymentsProcessor = (settings: Settings): IPaymentsProcessor => {
@ -81,9 +97,7 @@ const createZebedeePaymentsProcessor = (settings: Settings): IPaymentsProcessor
debug('config: %o', config)
const client = axios.create(config)
const zpp = new ZebedeePaymentsProcesor(client, createSettings)
return new PaymentsProcessor(zpp)
return new ZebedeePaymentsProcesor(client, createSettings)
}
const createLNbitsPaymentProcessor = (settings: Settings): IPaymentsProcessor => {
@ -99,9 +113,13 @@ const createLNbitsPaymentProcessor = (settings: Settings): IPaymentsProcessor =>
debug('config: %o', config)
const client = axios.create(config)
const pp = new LNbitsPaymentsProcesor(client, createSettings)
return new LNbitsPaymentsProcesor(client, createSettings)
}
return new PaymentsProcessor(pp)
const createNodelessPaymentsProcessor = (settings: Settings): IPaymentsProcessor => {
const client = axios.create(getNodelessAxiosConfig(settings))
return new NodelessPaymentsProcesor(client, createSettings)
}
export const createPaymentsProcessor = (): IPaymentsProcessor => {
@ -118,6 +136,8 @@ export const createPaymentsProcessor = (): IPaymentsProcessor => {
return createZebedeePaymentsProcessor(settings)
case 'lnbits':
return createLNbitsPaymentProcessor(settings)
case 'nodeless':
return createNodelessPaymentsProcessor(settings)
default:
return new NullPaymentsProcessor()
}

View File

@ -1,12 +1,9 @@
import express from 'express'
import helmet from 'helmet'
import { createLogger } from './logger-factory'
import { createSettings } from './settings-factory'
import router from '../routes'
const debug = createLogger('web-app-factory')
export const createWebApp = () => {
const app = express()
app
@ -31,8 +28,6 @@ export const createWebApp = () => {
'font-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'],
}
debug('CSP directives: %o', directives)
return helmet.contentSecurityPolicy({ directives })(req, res, next)
})
.use('/favicon.ico', express.static('./resources/favicon.ico'))

View File

@ -17,6 +17,7 @@ export const getInvoiceRequestHandler = (_req: Request, res: Response, next: Nex
const feeSchedule = path<FeeSchedule>(['payments', 'feeSchedules', 'admission', '0'], settings)
pageCache = readFileSync('./resources/index.html', 'utf8')
.replaceAll('{{name}}', name)
.replaceAll('{{processor}}', settings.payments.processor)
.replaceAll('{{amount}}', (BigInt(feeSchedule.amount) / 1000n).toString())
}

View File

@ -0,0 +1,18 @@
import { Request, Response } from 'express'
import { createNodelessCallbackController } from '../../factories/nodeless-callback-controller-factory'
export const postNodelessCallbackRequestHandler = async (
req: Request,
res: Response,
) => {
const controller = createNodelessCallbackController()
try {
await controller.handleRequest(req, res)
} catch (error) {
res
.status(500)
.setHeader('content-type', 'text/plain')
.send('Error handling request')
}
}

View File

@ -12,7 +12,7 @@ export const postZebedeeCallbackRequestHandler = async (
} catch (error) {
res
.status(500)
.setHeader('content-type', 'text-plain')
.setHeader('content-type', 'text/plain')
.send('Error handling request')
}
}

View File

@ -12,7 +12,7 @@ export const rateLimiterMiddleware = async (request: Request, response: Response
const clientAddress = getRemoteAddress(request, currentSettings).split(',')[0]
debug('request received from %s: %O', clientAddress, request.headers)
debug('request received from %s: %o', clientAddress, request.headers)
if (await isRateLimited(clientAddress, currentSettings)) {
response.destroy()

View File

@ -0,0 +1,78 @@
import { AxiosInstance } from 'axios'
import { Factory } from '../@types/base'
import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients'
import { createLogger } from '../factories/logger-factory'
import { fromNodelessInvoice } from '../utils/transform'
import { Settings } from '../@types/settings'
const debug = createLogger('nodeless-payments-processor')
export class NodelessPaymentsProcesor implements IPaymentsProcessor {
public constructor(
private httpClient: AxiosInstance,
private settings: Factory<Settings>
) {}
public async getInvoice(invoiceId: string): Promise<GetInvoiceResponse> {
debug('get invoice: %s', invoiceId)
const { storeId } = this.settings().paymentsProcessors.nodeless
try {
const response = await this.httpClient.get(`/api/v1/store/${storeId}/invoice/${invoiceId}`, {
maxRedirects: 1,
})
return fromNodelessInvoice(response.data.data)
} catch (error) {
console.error(`Unable to get invoice ${invoiceId}. Reason:`, error)
throw error
}
}
public async createInvoice(request: CreateInvoiceRequest): Promise<CreateInvoiceResponse> {
debug('create invoice: %O', request)
const {
amount: amountMsats,
description,
requestId,
} = request
const amountSats = Number(amountMsats / 1000n)
const body = {
amount: amountSats,
currency: 'SATS',
metadata: {
description,
requestId,
unit: 'sats',
createdAt: new Date().toISOString(),
},
}
const { storeId } = this.settings().paymentsProcessors.nodeless
try {
debug('request body: %O', body)
const response = await this.httpClient.post(`/api/v1/store/${storeId}/invoice`, body, {
maxRedirects: 1,
})
debug('response headers: %O', response.headers)
debug('response data: %O', response.data)
const result = fromNodelessInvoice(response.data.data)
debug('invoice: %O', result)
return result
} catch (error) {
console.error('Unable to request invoice. Reason:', error.message)
throw error
}
}
}

View File

@ -1,16 +0,0 @@
import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients'
import { Invoice } from '../@types/invoice'
export class PaymentsProcessor implements IPaymentsProcessor {
public constructor(
private readonly processor: IPaymentsProcessor
) {}
public async getInvoice(invoice: string | Invoice): Promise<GetInvoiceResponse> {
return this.processor.getInvoice(invoice)
}
public async createInvoice(request: CreateInvoiceRequest): Promise<CreateInvoiceResponse> {
return this.processor.createInvoice(request)
}
}

View File

@ -1,8 +1,10 @@
import {
always,
applySpec,
head,
ifElse,
is,
map,
omit,
pipe,
prop,
@ -75,6 +77,31 @@ export class InvoiceRepository implements IInvoiceRepository {
return dbInvoices.map(fromDBInvoice)
}
public updateStatus(
invoice: Invoice,
client: DatabaseClient = this.dbClient,
): Promise<Invoice | undefined> {
debug('updating invoice status: %o', invoice)
const query = client<DBInvoice>('invoices')
.update({
status: invoice.status,
updated_at: new Date(),
})
.where('id', invoice.id)
.limit(1)
.returning(['*'])
return {
then: <T1, T2>(
onfulfilled: (value: Invoice | undefined) => T1 | PromiseLike<T1>,
onrejected: (reason: any) => T2 | PromiseLike<T2>
) => query.then(pipe(map(fromDBInvoice), head)).then(onfulfilled, onrejected),
catch: <T>(onrejected: (reason: any) => T | PromiseLike<T>) => query.catch(onrejected),
toString: (): string => query.toString(),
} as Promise<Invoice | undefined>
}
public upsert(
invoice: Invoice,
client: DatabaseClient = this.dbClient

View File

@ -5,13 +5,14 @@ import { createLogger } from '../../factories/logger-factory'
import { createSettings } from '../../factories/settings-factory'
import { getRemoteAddress } from '../../utils/http'
import { postLNbitsCallbackRequestHandler } from '../../handlers/request-handlers/post-lnbits-callback-request-handler'
import { postNodelessCallbackRequestHandler } from '../../handlers/request-handlers/post-nodeless-callback-request-handler'
import { postZebedeeCallbackRequestHandler } from '../../handlers/request-handlers/post-zebedee-callback-request-handler'
const debug = createLogger('routes-callbacks')
const router = Router()
router
.post('/zebedee', json(), (req, res) => {
.post('/zebedee', json(), async (req, res) => {
const settings = createSettings()
const { ipWhitelist = [] } = settings.paymentsProcessors?.zebedee ?? {}
const remoteAddress = getRemoteAddress(req, settings)
@ -33,9 +34,9 @@ router
return
}
postZebedeeCallbackRequestHandler(req, res)
return postZebedeeCallbackRequestHandler(req, res)
})
.post('/lnbits', json(), (req, res) => {
.post('/lnbits', json(), async (req, res) => {
const settings = createSettings()
const remoteAddress = getRemoteAddress(req, settings)
const paymentProcessor = settings.payments?.processor ?? 'null'
@ -49,7 +50,7 @@ router
}
let validationPassed = false
if (typeof req.query.hmac === 'string' && req.query.hmac.match(/^[0-9]{1,20}:[0-9a-f]{64}$/)) {
const split = req.query.hmac.split(':')
if (hmacSha256(deriveFromSecret('lnbits-callback-hmac-key'), split[0]).toString('hex') === split[1]) {
@ -66,7 +67,36 @@ router
.send('Forbidden')
return
}
postLNbitsCallbackRequestHandler(req, res)
return postLNbitsCallbackRequestHandler(req, res)
})
.post('/nodeless', json({
verify(req, _res, buf) {
(req as any).rawBody = buf
},
}), async (req, res) => {
const settings = createSettings()
const paymentProcessor = settings.payments?.processor
const expected = hmacSha256(process.env.NODELESS_WEBHOOK_SECRET, (req as any).rawBody).toString('hex')
const actual = req.headers['nodeless-signature']
if (expected !== actual) {
console.error('nodeless callback request rejected: signature mismatch:', { expected, actual })
res
.status(403)
.send('Forbidden')
return
}
if (paymentProcessor !== 'nodeless') {
debug('denied request from %s to /callbacks/nodeless which is not the current payment processor')
res
.status(403)
.send('Forbidden')
return
}
return postNodelessCallbackRequestHandler(req, res)
})
export default router

View File

@ -114,17 +114,9 @@ export class PaymentsService implements IPaymentsService {
public async updateInvoice(invoice: Partial<Invoice>): Promise<void> {
debug('update invoice %s: %o', invoice.id, invoice)
try {
await this.invoiceRepository.upsert({
await this.invoiceRepository.updateStatus({
id: invoice.id,
pubkey: invoice.pubkey,
bolt11: invoice.bolt11,
amountRequested: invoice.amountRequested,
description: invoice.description,
unit: invoice.unit,
status: invoice.status,
expiresAt: invoice.expiresAt,
updatedAt: new Date(),
createdAt: invoice.createdAt,
})
} catch (error) {
console.error('Unable to update invoice. Reason:', error)
@ -132,15 +124,10 @@ export class PaymentsService implements IPaymentsService {
}
}
public async updateInvoiceStatus(invoice: Partial<Invoice>): Promise<void> {
public async updateInvoiceStatus(invoice: Pick<Invoice, 'id' | 'status'>): Promise<Invoice> {
debug('update invoice %s: %o', invoice.id, invoice)
try {
const fullInvoice = await this.invoiceRepository.findById(invoice.id)
await this.invoiceRepository.upsert({
...fullInvoice,
status: invoice.status,
updatedAt: new Date(),
})
return await this.invoiceRepository.updateStatus(invoice)
} catch (error) {
console.error('Unable to update invoice. Reason:', error)
throw error
@ -150,13 +137,13 @@ export class PaymentsService implements IPaymentsService {
public async confirmInvoice(
invoice: Invoice,
): Promise<void> {
debug('confirm invoice %s: %o', invoice.id, invoice)
debug('confirm invoice %s: %O', invoice.id, invoice)
const transaction = new Transaction(this.dbClient)
try {
if (!invoice.confirmedAt) {
throw new Error('Invoince confirmation date is not set')
throw new Error('Invoice confirmation date is not set')
}
if (invoice.status !== InvoiceStatus.COMPLETED) {
throw new Error(`Invoice is not complete: ${invoice.status}`)

View File

@ -1,7 +1,7 @@
import { always, applySpec, ifElse, is, isNil, path, pipe, prop, propSatisfies } from 'ramda'
import { always, applySpec, cond, equals, ifElse, is, isNil, path, pipe, prop, propSatisfies, T } from 'ramda'
import { bech32 } from 'bech32'
import { Invoice } from '../@types/invoice'
import { Invoice, InvoiceStatus } from '../@types/invoice'
import { User } from '../@types/user'
export const toJSON = (input: any) => JSON.stringify(input)
@ -10,10 +10,12 @@ export const toBuffer = (input: any) => Buffer.from(input, 'hex')
export const fromBuffer = (input: Buffer) => input.toString('hex')
export const toBigInt = (input: string): bigint => BigInt(input)
export const toBigInt = (input: string | number): bigint => BigInt(input)
export const fromBigInt = (input: bigint) => input.toString()
const addTime = (ms: number) => (input: Date) => new Date(input.getTime() + ms)
export const fromDBInvoice = applySpec<Invoice>({
id: prop('id') as () => string,
pubkey: pipe(prop('pubkey') as () => Buffer, fromBuffer),
@ -84,3 +86,43 @@ export const fromZebedeeInvoice = applySpec<Invoice>({
),
rawRespose: toJSON,
})
export const fromNodelessInvoice = applySpec<Invoice>({
id: prop('id'),
pubkey: path(['metadata', 'requestId']),
bolt11: prop('lightningInvoice'),
amountRequested: pipe(prop('satsAmount') as () => number, toBigInt),
description: path(['metadata', 'description']),
unit: path(['metadata', 'unit']),
status: pipe(
prop('status'),
cond([
[equals('new'), always(InvoiceStatus.PENDING)],
[equals('pending_confirmation'), always(InvoiceStatus.PENDING)],
[equals('underpaid'), always(InvoiceStatus.PENDING)],
[equals('in_flight'), always(InvoiceStatus.PENDING)],
[equals('paid'), always(InvoiceStatus.COMPLETED)],
[equals('overpaid'), always(InvoiceStatus.COMPLETED)],
[equals('expired'), always(InvoiceStatus.EXPIRED)],
]),
),
expiresAt: ifElse(
propSatisfies(is(String), 'expiresAt'),
pipe(prop('expiresAt'), toDate),
ifElse(
propSatisfies(is(String), 'createdAt'),
pipe(prop('createdAt'), toDate, addTime(15 * 60000)),
always(null),
),
),
confirmedAt: cond([
[propSatisfies(is(String), 'paidAt'), pipe(prop('paidAt'), toDate)],
[T, always(null)],
]),
createdAt: ifElse(
propSatisfies(is(String), 'createdAt'),
pipe(prop('createdAt'), toDate),
always(null),
),
// rawResponse: toJSON,
})

View File

@ -1,5 +1,5 @@
import * as secp256k1 from '@noble/secp256k1'
import { createHash, createHmac, Hash } from 'crypto'
import { createHash, createHmac, getRandomValues, Hash } from 'crypto'
import { Observable } from 'rxjs'
import WebSocket from 'ws'
@ -54,7 +54,9 @@ export async function createEvent(input: Partial<Event>, privkey: any): Promise<
}
export function createIdentity(name: string) {
const hmac = createHmac('sha256', Math.random().toString())
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)