diff --git a/README.md b/README.md index 7a805984..0539be5d 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Install Docker from their [official guide](https://docs.docker.com/engine/instal - Set `payments.enabled` to `true` - Set `payments.feeSchedules.admission.enabled` to `true` - Set `limits.event.pubkey.minBalance` to the minimum balance in msats required to accept events (i.e. `1000000` to require a balance of `1000` sats) - - Choose one of the following payment processors: `zebedee`, `nodeless`, `lnbits`, `lnurl` + - Choose one of the following payment processors: `zebedee`, `nodeless`, `opennode`, `lnbits`, `lnurl` 2. [ZEBEDEE](https://zebedee.io) - Complete the step "Before you begin" @@ -113,9 +113,9 @@ Install Docker from their [official guide](https://docs.docker.com/engine/instal - Restart Nostream (`./scripts/stop` followed by `./scripts/start`) - Read the in-depth guide for more information: [Set Up a Paid Nostr Relay with ZEBEDEE API](https://docs.zebedee.io/docs/guides/nostr-relay) -3. [Nodeless.io](https://nodeless.io) +3. [Nodeless](https://nodeless.io/?ref=587f477f-ba1c-4bd3-8986-8302c98f6731) - Complete the step "Before you begin" - - Sign up for a new account at https://nodeless.io, create a new store and take note of the store ID + - [Sign up](https://nodeless.io/?ref=587f477f-ba1c-4bd3-8986-8302c98f6731) for a new account, create a new store and take note of the store ID - Go to Profile > API Tokens and generate a new key and take note of it - Create a store webhook with your Nodeless callback URL (e.g. `https://{YOUR_DOMAIN_HERE}/callbacks/nodeless`) and make sure to enable all of the events. Grab the generated store webhook secret - Set `NODELESS_API_KEY` and `NODELESS_WEBHOOK_SECRET` environment variables with generated API key and webhook secret, respectively @@ -130,9 +130,24 @@ Install Docker from their [official guide](https://docs.docker.com/engine/instal - Set `paymentsProcessors.nodeless.storeId` to your store ID - Restart Nostream (`./scripts/stop` followed by `./scripts/start`) -4. [LNBITS](https://lnbits.com/) +4. [OpenNode](https://www.opennode.com/) + - Complete the step "Before you begin" + - Sign up for a new account and get verified + - Go to Developers > Integrations and setup two-factor authentication + - Create a new API Key with Invoices permission + - Set `OPENNODE_API_KEY` environment variable on your `.env` file + + ``` + OPENNODE_API_KEY={YOUR_OPENNODE_API_KEY} + ``` + + - On your `.nostr/settings.yaml` file make the following changes: + - Set `payments.processor` to `opennode` + - Restart Nostream (`./scripts/stop` followed by `./scripts/start`) + +5. [LNBITS](https://lnbits.com/) - Complete the step "Before you begin" - - Create a new wallet on you public LNbits instance + - Create a new wallet on you public LNbits instance - [Demo](https://legend.lnbits.com/) server must not be used for production - Your instance must be accessible from the internet and have a valid SSL/TLS certificate - Get wallet Invoice/read key (in Api docs section of your wallet) diff --git a/docker-compose.yml b/docker-compose.yml index a3df3e9f..80bd6412 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,9 +46,16 @@ services: TOR_CONTROL_PORT: 9051 TOR_PASSWORD: nostr_ts_relay HIDDEN_SERVICE_PORT: 80 + # Payments Processors + # Zebedee + ZEBEDEE_API_KEY: ${ZEBEDEE_API_KEY} # Nodeless.io NODELESS_API_KEY: ${NODELESS_API_KEY} NODELESS_WEBHOOK_SECRET: ${NODELESS_WEBHOOK_SECRET} + # OpenNode + OPENNODE_API_KEY: ${OPENNODE_API_KEY} + # Lnbits + LNBITS_API_KEY: ${LNBITS_API_KEY} # Enable DEBUG for troubleshooting. Examples: # DEBUG: "primary:*" # DEBUG: "worker:*" diff --git a/resources/default-settings.yaml b/resources/default-settings.yaml index b6923a8b..4f137e48 100755 --- a/resources/default-settings.yaml +++ b/resources/default-settings.yaml @@ -32,6 +32,9 @@ paymentsProcessors: nodeless: baseURL: https://nodeless.io storeId: your-nodeless-io-store-id + opennode: + baseURL: api.opennode.com + callbackBaseURL: https://nostream.your-domain.com/callbacks/opennode network: maxPayloadSize: 524288 # Comment the next line if using CloudFlare proxy diff --git a/src/@types/settings.ts b/src/@types/settings.ts index bb50062e..5255fb63 100644 --- a/src/@types/settings.ts +++ b/src/@types/settings.ts @@ -167,6 +167,11 @@ export interface LNbitsPaymentsProcessor { callbackBaseURL: string } +export interface OpenNodePaymentsProcessor { + baseURL: string + callbackBaseURL: string +} + export interface NodelessPaymentsProcessor { baseURL: string storeId: string @@ -177,6 +182,7 @@ export interface PaymentsProcessors { zebedee?: ZebedeePaymentsProcessor lnbits?: LNbitsPaymentsProcessor nodeless?: NodelessPaymentsProcessor + opennode?: OpenNodePaymentsProcessor } export interface Local { diff --git a/src/app/maintenance-worker.ts b/src/app/maintenance-worker.ts index 65ab8112..a4c5ce7b 100644 --- a/src/app/maintenance-worker.ts +++ b/src/app/maintenance-worker.ts @@ -45,7 +45,6 @@ export class MaintenanceWorker implements IRunnable { let successful = 0 for (const invoice of invoices) { - debug('invoice %s: %o', invoice.id, invoice) try { debug('getting invoice %s from payment processor: %o', invoice.id, invoice) const updatedInvoice = await this.paymentsService.getInvoiceFromPaymentsProcessor(invoice) diff --git a/src/controllers/callbacks/lnbits-callback-controller.ts b/src/controllers/callbacks/lnbits-callback-controller.ts index 54fc141c..465e6de0 100644 --- a/src/controllers/callbacks/lnbits-callback-controller.ts +++ b/src/controllers/callbacks/lnbits-callback-controller.ts @@ -1,7 +1,10 @@ import { Request, Response } from 'express' +import { deriveFromSecret, hmacSha256 } from '../../utils/secret' import { Invoice, InvoiceStatus } from '../../@types/invoice' import { createLogger } from '../../factories/logger-factory' +import { createSettings } from '../../factories/settings-factory' +import { getRemoteAddress } from '../../utils/http' import { IController } from '../../@types/controllers' import { IInvoiceRepository } from '../../@types/repositories' import { IPaymentsService } from '../../@types/services' @@ -22,6 +25,37 @@ export class LNbitsCallbackController implements IController { debug('request headers: %o', request.headers) debug('request body: %o', request.body) + const settings = createSettings() + const remoteAddress = getRemoteAddress(request, settings) + const paymentProcessor = settings.payments?.processor ?? 'null' + + if (paymentProcessor !== 'lnbits') { + debug('denied request from %s to /callbacks/lnbits which is not the current payment processor', remoteAddress) + response + .status(403) + .send('Forbidden') + return + } + + let validationPassed = false + + if (typeof request.query.hmac === 'string' && request.query.hmac.match(/^[0-9]{1,20}:[0-9a-f]{64}$/)) { + const split = request.query.hmac.split(':') + if (hmacSha256(deriveFromSecret('lnbits-callback-hmac-key'), split[0]).toString('hex') === split[1]) { + if (parseInt(split[0]) > Date.now()) { + validationPassed = true + } + } + } + + if (!validationPassed) { + debug('unauthorized request from %s to /callbacks/lnbits', remoteAddress) + response + .status(403) + .send('Forbidden') + return + } + const body = request.body if (!body || typeof body !== 'object' || typeof body.payment_hash !== 'string' || body.payment_hash.length !== 64) { response diff --git a/src/controllers/callbacks/nodeless-callback-controller.ts b/src/controllers/callbacks/nodeless-callback-controller.ts index e8d24186..2e5be0fb 100644 --- a/src/controllers/callbacks/nodeless-callback-controller.ts +++ b/src/controllers/callbacks/nodeless-callback-controller.ts @@ -3,7 +3,9 @@ import { Request, Response } from 'express' import { Invoice, InvoiceStatus } from '../../@types/invoice' import { createLogger } from '../../factories/logger-factory' +import { createSettings } from '../../factories/settings-factory' import { fromNodelessInvoice } from '../../utils/transform' +import { hmacSha256 } from '../../utils/secret' import { IController } from '../../@types/controllers' import { IPaymentsService } from '../../@types/services' @@ -22,6 +24,28 @@ export class NodelessCallbackController implements IController { debug('callback request headers: %o', request.headers) debug('callback request body: %O', request.body) + const settings = createSettings() + const paymentProcessor = settings.payments?.processor + + const expected = hmacSha256(process.env.NODELESS_WEBHOOK_SECRET, (request as any).rawBody).toString('hex') + const actual = request.headers['nodeless-signature'] + + if (expected !== actual) { + console.error('nodeless callback request rejected: signature mismatch:', { expected, actual }) + response + .status(403) + .send('Forbidden') + return + } + + if (paymentProcessor !== 'nodeless') { + debug('denied request from %s to /callbacks/nodeless which is not the current payment processor') + response + .status(403) + .send('Forbidden') + return + } + const nodelessInvoice = applySpec({ id: prop('uuid'), status: prop('status'), diff --git a/src/controllers/callbacks/opennode-callback-controller.ts b/src/controllers/callbacks/opennode-callback-controller.ts new file mode 100644 index 00000000..7456ed45 --- /dev/null +++ b/src/controllers/callbacks/opennode-callback-controller.ts @@ -0,0 +1,69 @@ +import { Request, Response } from 'express' + +import { Invoice, InvoiceStatus } from '../../@types/invoice' +import { createLogger } from '../../factories/logger-factory' +import { fromOpenNodeInvoice } from '../../utils/transform' +import { IController } from '../../@types/controllers' +import { IPaymentsService } from '../../@types/services' + +const debug = createLogger('opennode-callback-controller') + +export class OpenNodeCallbackController implements IController { + public constructor( + private readonly paymentsService: IPaymentsService, + ) {} + + // TODO: Validate + public async handleRequest( + request: Request, + response: Response, + ) { + debug('request headers: %o', request.headers) + debug('request body: %O', request.body) + + const invoice = fromOpenNodeInvoice(request.body) + + debug('invoice', invoice) + + let updatedInvoice: Invoice + try { + updatedInvoice = await this.paymentsService.updateInvoiceStatus(invoice) + } 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({ + id: invoice.id, + amountPaid: updatedInvoice.amountRequested, + confirmedAt: updatedInvoice.confirmedAt, + }) + await this.paymentsService.sendInvoiceUpdateNotification(updatedInvoice) + } catch (error) { + console.error(`Unable to confirm invoice ${invoice.id}`, error) + + throw error + } + + response + .status(200) + .setHeader('content-type', 'text/plain; charset=utf8') + .send('OK') + } +} diff --git a/src/controllers/callbacks/zebedee-callback-controller.ts b/src/controllers/callbacks/zebedee-callback-controller.ts index 9320676f..259ef0d7 100644 --- a/src/controllers/callbacks/zebedee-callback-controller.ts +++ b/src/controllers/callbacks/zebedee-callback-controller.ts @@ -1,9 +1,11 @@ import { Request, Response } from 'express' +import { Invoice, InvoiceStatus } from '../../@types/invoice' import { createLogger } from '../../factories/logger-factory' +import { createSettings } from '../../factories/settings-factory' import { fromZebedeeInvoice } from '../../utils/transform' +import { getRemoteAddress } from '../../utils/http' import { IController } from '../../@types/controllers' -import { InvoiceStatus } from '../../@types/invoice' import { IPaymentsService } from '../../@types/services' const debug = createLogger('zebedee-callback-controller') @@ -21,14 +23,35 @@ export class ZebedeeCallbackController implements IController { debug('request headers: %o', request.headers) debug('request body: %O', request.body) + const settings = createSettings() + + const { ipWhitelist = [] } = settings.paymentsProcessors?.zebedee ?? {} + const remoteAddress = getRemoteAddress(request, settings) + const paymentProcessor = settings.payments?.processor + + if (ipWhitelist.length && !ipWhitelist.includes(remoteAddress)) { + debug('unauthorized request from %s to /callbacks/zebedee', remoteAddress) + response + .status(403) + .send('Forbidden') + return + } + + if (paymentProcessor !== 'zebedee') { + debug('denied request from %s to /callbacks/zebedee which is not the current payment processor', remoteAddress) + response + .status(403) + .send('Forbidden') + return + } + const invoice = fromZebedeeInvoice(request.body) debug('invoice', invoice) + let updatedInvoice: Invoice try { - if (invoice.bolt11) { - await this.paymentsService.updateInvoice(invoice) - } + updatedInvoice = await this.paymentsService.updateInvoiceStatus(invoice) } catch (error) { console.error(`Unable to persist invoice ${invoice.id}`, error) @@ -36,8 +59,8 @@ export class ZebedeeCallbackController implements IController { } if ( - invoice.status !== InvoiceStatus.COMPLETED - && !invoice.confirmedAt + updatedInvoice.status !== InvoiceStatus.COMPLETED + && !updatedInvoice.confirmedAt ) { response .status(200) @@ -47,10 +70,15 @@ export class ZebedeeCallbackController implements IController { } invoice.amountPaid = invoice.amountRequested + updatedInvoice.amountPaid = invoice.amountRequested try { - await this.paymentsService.confirmInvoice(invoice) - await this.paymentsService.sendInvoiceUpdateNotification(invoice) + await this.paymentsService.confirmInvoice({ + id: invoice.id, + confirmedAt: updatedInvoice.confirmedAt, + amountPaid: invoice.amountRequested, + }) + await this.paymentsService.sendInvoiceUpdateNotification(updatedInvoice) } catch (error) { console.error(`Unable to confirm invoice ${invoice.id}`, error) diff --git a/src/controllers/invoices/get-invoice-controller.ts b/src/controllers/invoices/get-invoice-controller.ts new file mode 100644 index 00000000..8ac88505 --- /dev/null +++ b/src/controllers/invoices/get-invoice-controller.ts @@ -0,0 +1,34 @@ +import { path, pathEq } from 'ramda' +import { Request, Response } from 'express' +import { readFileSync } from 'fs' + +import { createSettings } from '../../factories/settings-factory' +import { FeeSchedule } from '../../@types/settings' +import { IController } from '../../@types/controllers' + +let pageCache: string + +export class GetInvoiceController implements IController { + public async handleRequest( + _req: Request, + res: Response, + ): Promise { + const settings = createSettings() + + if (pathEq(['payments', 'enabled'], true, settings) + && pathEq(['payments', 'feeSchedules', 'admission', '0', 'enabled'], true, settings)) { + if (!pageCache) { + const name = path(['info', 'name'])(settings) + const feeSchedule = path(['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()) + } + + res.status(200).setHeader('content-type', 'text/html; charset=utf8').send(pageCache) + } else { + res.status(404).send() + } + } +} \ No newline at end of file diff --git a/src/factories/controllers/get-invoice-controller-factory.ts b/src/factories/controllers/get-invoice-controller-factory.ts new file mode 100644 index 00000000..e4bb3ccd --- /dev/null +++ b/src/factories/controllers/get-invoice-controller-factory.ts @@ -0,0 +1,3 @@ +import { GetInvoiceController } from '../../controllers/invoices/get-invoice-controller' + +export const createGetInvoiceController = () => new GetInvoiceController() diff --git a/src/factories/controllers/get-invoice-status-controller-factory.ts b/src/factories/controllers/get-invoice-status-controller-factory.ts new file mode 100644 index 00000000..cbdc421d --- /dev/null +++ b/src/factories/controllers/get-invoice-status-controller-factory.ts @@ -0,0 +1,11 @@ +import { GetInvoiceStatusController } from '../../controllers/invoices/get-invoice-status-controller' +import { getReadReplicaDbClient } from '../../database/client' +import { InvoiceRepository } from '../../repositories/invoice-repository' + +export const createGetInvoiceStatusController = () => { + const rrDbClient = getReadReplicaDbClient() + + const invoiceRepository = new InvoiceRepository(rrDbClient) + + return new GetInvoiceStatusController(invoiceRepository) +} diff --git a/src/factories/controllers/lnbits-callback-controller-factory.ts b/src/factories/controllers/lnbits-callback-controller-factory.ts new file mode 100644 index 00000000..312b6e86 --- /dev/null +++ b/src/factories/controllers/lnbits-callback-controller-factory.ts @@ -0,0 +1,12 @@ +import { createPaymentsService } from '../payments-service-factory' +import { getMasterDbClient } from '../../database/client' +import { IController } from '../../@types/controllers' +import { InvoiceRepository } from '../../repositories/invoice-repository' +import { LNbitsCallbackController } from '../../controllers/callbacks/lnbits-callback-controller' + +export const createLNbitsCallbackController = (): IController => { + return new LNbitsCallbackController( + createPaymentsService(), + new InvoiceRepository(getMasterDbClient()) + ) +} diff --git a/src/factories/controllers/nodeless-callback-controller-factory.ts b/src/factories/controllers/nodeless-callback-controller-factory.ts new file mode 100644 index 00000000..ab34d980 --- /dev/null +++ b/src/factories/controllers/nodeless-callback-controller-factory.ts @@ -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(), +) diff --git a/src/factories/controllers/opennode-callback-controller-factory.ts b/src/factories/controllers/opennode-callback-controller-factory.ts new file mode 100644 index 00000000..e6829211 --- /dev/null +++ b/src/factories/controllers/opennode-callback-controller-factory.ts @@ -0,0 +1,9 @@ +import { createPaymentsService } from '../payments-service-factory' +import { IController } from '../../@types/controllers' +import { OpenNodeCallbackController } from '../../controllers/callbacks/opennode-callback-controller' + +export const createOpenNodeCallbackController = (): IController => { + return new OpenNodeCallbackController( + createPaymentsService(), + ) +} diff --git a/src/factories/controllers/post-invoice-controller-factory.ts b/src/factories/controllers/post-invoice-controller-factory.ts new file mode 100644 index 00000000..50331572 --- /dev/null +++ b/src/factories/controllers/post-invoice-controller-factory.ts @@ -0,0 +1,20 @@ +import { createPaymentsService } from '../payments-service-factory' +import { createSettings } from '../settings-factory' +import { getMasterDbClient } from '../../database/client' +import { IController } from '../../@types/controllers' +import { PostInvoiceController } from '../../controllers/invoices/post-invoice-controller' +import { slidingWindowRateLimiterFactory } from '../rate-limiter-factory' +import { UserRepository } from '../../repositories/user-repository' + +export const createPostInvoiceController = (): IController => { + const dbClient = getMasterDbClient() + const userRepository = new UserRepository(dbClient) + const paymentsService = createPaymentsService() + + return new PostInvoiceController( + userRepository, + paymentsService, + createSettings, + slidingWindowRateLimiterFactory, + ) +} diff --git a/src/factories/controllers/zebedee-callback-controller-factory.ts b/src/factories/controllers/zebedee-callback-controller-factory.ts new file mode 100644 index 00000000..dd6b19a3 --- /dev/null +++ b/src/factories/controllers/zebedee-callback-controller-factory.ts @@ -0,0 +1,9 @@ +import { createPaymentsService } from '../payments-service-factory' +import { IController } from '../../@types/controllers' +import { ZebedeeCallbackController } from '../../controllers/callbacks/zebedee-callback-controller' + +export const createZebedeeCallbackController = (): IController => { + return new ZebedeeCallbackController( + createPaymentsService(), + ) +} diff --git a/src/factories/get-invoice-status-controller-factory.ts b/src/factories/get-invoice-status-controller-factory.ts deleted file mode 100644 index e8b84383..00000000 --- a/src/factories/get-invoice-status-controller-factory.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { GetInvoiceStatusController } from '../controllers/invoices/get-invoice-status-controller' -import { getReadReplicaDbClient } from '../database/client' -import { InvoiceRepository } from '../repositories/invoice-repository' - -export const createGetInvoiceStatusController = () => { - const rrDbClient = getReadReplicaDbClient() - - const invoiceRepository = new InvoiceRepository(rrDbClient) - - return new GetInvoiceStatusController(invoiceRepository) -} diff --git a/src/factories/lnbits-callback-controller-factory.ts b/src/factories/lnbits-callback-controller-factory.ts deleted file mode 100644 index 326def2b..00000000 --- a/src/factories/lnbits-callback-controller-factory.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createPaymentsService } from './payments-service-factory' -import { getMasterDbClient } from '../database/client' -import { IController } from '../@types/controllers' -import { InvoiceRepository } from '../repositories/invoice-repository' -import { LNbitsCallbackController } from '../controllers/callbacks/lnbits-callback-controller' - -export const createLNbitsCallbackController = (): IController => { - return new LNbitsCallbackController( - createPaymentsService(), - new InvoiceRepository(getMasterDbClient()) - ) -} diff --git a/src/factories/nodeless-callback-controller-factory.ts b/src/factories/nodeless-callback-controller-factory.ts deleted file mode 100644 index 31e55d22..00000000 --- a/src/factories/nodeless-callback-controller-factory.ts +++ /dev/null @@ -1,7 +0,0 @@ -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(), -) diff --git a/src/factories/payments-processor-factory.ts b/src/factories/payments-processor-factory.ts index c3469583..b26f045f 100644 --- a/src/factories/payments-processor-factory.ts +++ b/src/factories/payments-processor-factory.ts @@ -1,129 +1,18 @@ -import axios, { CreateAxiosDefaults } from 'axios' -import { path } from 'ramda' - +import { createLNbitsPaymentProcessor } from './payments-processors/lnbits-payments-processor-factory' +import { createLnurlPaymentsProcessor } from './payments-processors/lnurl-payments-processor-factory' import { createLogger } from './logger-factory' +import { createNodelessPaymentsProcessor } from './payments-processors/nodeless-payments-processor-factory' +import { createOpenNodePaymentsProcessor } from './payments-processors/opennode-payments-processor-factory' import { createSettings } from './settings-factory' +import { createZebedeePaymentsProcessor } from './payments-processors/zebedee-payments-processor-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 { Settings } from '../@types/settings' -import { ZebedeePaymentsProcesor } from '../payments-processors/zebedee-payments-processor' const debug = createLogger('create-payments-processor') -const getZebedeeAxiosConfig = (settings: Settings): CreateAxiosDefaults => { - if (!process.env.ZEBEDEE_API_KEY) { - const error = new Error('ZEBEDEE_API_KEY must be set.') - console.error('Unable to get Zebedee config.', error) - throw error - } - - return { - headers: { - 'content-type': 'application/json', - 'apikey': process.env.ZEBEDEE_API_KEY, - }, - baseURL: path(['paymentsProcessors', 'zebedee', 'baseURL'], settings), - maxRedirects: 1, - } -} - -const getNodelessAxiosConfig = (settings: Settings): CreateAxiosDefaults => { - 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 => { - if (!process.env.LNBITS_API_KEY) { - throw new Error('LNBITS_API_KEY must be set to an invoice or admin key.') - } - - return { - headers: { - 'content-type': 'application/json', - 'x-api-key': process.env.LNBITS_API_KEY, - }, - baseURL: path(['paymentsProcessors', 'lnbits', 'baseURL'], settings), - maxRedirects: 1, - } -} - -const createLnurlPaymentsProcessor = (settings: Settings): IPaymentsProcessor => { - const invoiceURL = path(['paymentsProcessors', 'lnurl', 'invoiceURL'], settings) as string | undefined - if (typeof invoiceURL === 'undefined') { - throw new Error('Unable to create payments processor: Setting paymentsProcessor.lnurl.invoiceURL is not configured.') - } - - const client = axios.create() - - return new LnurlPaymentsProcesor(client, createSettings) -} - -const createZebedeePaymentsProcessor = (settings: Settings): IPaymentsProcessor => { - const callbackBaseURL = path(['paymentsProcessors', 'zebedee', 'callbackBaseURL'], settings) as string | undefined - if (typeof callbackBaseURL === 'undefined' || callbackBaseURL.indexOf('nostream.your-domain.com') >= 0) { - const error = new Error('Setting paymentsProcessor.zebedee.callbackBaseURL is not configured.') - console.error('Unable to create payments processor.', error) - - throw error - } - - if ( - !Array.isArray(settings.paymentsProcessors?.zebedee?.ipWhitelist) - || !settings.paymentsProcessors?.zebedee?.ipWhitelist?.length - ) { - const error = new Error('Setting paymentsProcessor.zebedee.ipWhitelist is empty.') - console.error('Unable to create payments processor.', error) - - throw error - } - - const config = getZebedeeAxiosConfig(settings) - debug('config: %o', config) - const client = axios.create(config) - - return new ZebedeePaymentsProcesor(client, createSettings) -} - -const createLNbitsPaymentProcessor = (settings: Settings): IPaymentsProcessor => { - const callbackBaseURL = path(['paymentsProcessors', 'lnbits', 'callbackBaseURL'], settings) as string | undefined - if (typeof callbackBaseURL === 'undefined' || callbackBaseURL.indexOf('nostream.your-domain.com') >= 0) { - const error = new Error('Setting paymentsProcessor.lnbits.callbackBaseURL is not configured.') - console.error('Unable to create payments processor.', error) - - throw error - } - - const config = getLNbitsAxiosConfig(settings) - debug('config: %o', config) - const client = axios.create(config) - - return new LNbitsPaymentsProcesor(client, createSettings) -} - -const createNodelessPaymentsProcessor = (settings: Settings): IPaymentsProcessor => { - const client = axios.create(getNodelessAxiosConfig(settings)) - - return new NodelessPaymentsProcesor(client, createSettings) -} - export const createPaymentsProcessor = (): IPaymentsProcessor => { debug('create payments processor') + const settings = createSettings() if (!settings.payments?.enabled) { return new NullPaymentsProcessor() @@ -138,6 +27,8 @@ export const createPaymentsProcessor = (): IPaymentsProcessor => { return createLNbitsPaymentProcessor(settings) case 'nodeless': return createNodelessPaymentsProcessor(settings) + case 'opennode': + return createOpenNodePaymentsProcessor(settings) default: return new NullPaymentsProcessor() } diff --git a/src/factories/payments-processors/lnbits-payments-processor-factory.ts b/src/factories/payments-processors/lnbits-payments-processor-factory.ts new file mode 100644 index 00000000..48652077 --- /dev/null +++ b/src/factories/payments-processors/lnbits-payments-processor-factory.ts @@ -0,0 +1,39 @@ +import axios, { CreateAxiosDefaults } from 'axios' +import { path } from 'ramda' + +import { createSettings } from '../settings-factory' +import { IPaymentsProcessor } from '../../@types/clients' +import { LNbitsPaymentsProcesor } from '../../payments-processors/lnbits-payment-processor' +import { Settings } from '../../@types/settings' + + +const getLNbitsAxiosConfig = (settings: Settings): CreateAxiosDefaults => { + if (!process.env.LNBITS_API_KEY) { + throw new Error('LNBITS_API_KEY must be set to an invoice or admin key.') + } + + return { + headers: { + 'content-type': 'application/json', + 'x-api-key': process.env.LNBITS_API_KEY, + }, + baseURL: path(['paymentsProcessors', 'lnbits', 'baseURL'], settings), + maxRedirects: 1, + } +} + +export const createLNbitsPaymentProcessor = (settings: Settings): IPaymentsProcessor => { + const callbackBaseURL = path(['paymentsProcessors', 'lnbits', 'callbackBaseURL'], settings) as string | undefined + if (typeof callbackBaseURL === 'undefined' || callbackBaseURL.indexOf('nostream.your-domain.com') >= 0) { + const error = new Error('Setting paymentsProcessor.lnbits.callbackBaseURL is not configured.') + console.error('Unable to create payments processor.', error) + + throw error + } + + const config = getLNbitsAxiosConfig(settings) + + const client = axios.create(config) + + return new LNbitsPaymentsProcesor(client, createSettings) +} diff --git a/src/factories/payments-processors/lnurl-payments-processor-factory.ts b/src/factories/payments-processors/lnurl-payments-processor-factory.ts new file mode 100644 index 00000000..5ed5ae1c --- /dev/null +++ b/src/factories/payments-processors/lnurl-payments-processor-factory.ts @@ -0,0 +1,18 @@ +import axios from 'axios' +import { path } from 'ramda' + +import { createSettings } from '../settings-factory' +import { IPaymentsProcessor } from '../../@types/clients' +import { LnurlPaymentsProcesor } from '../../payments-processors/lnurl-payments-processor' +import { Settings } from '../../@types/settings' + +export const createLnurlPaymentsProcessor = (settings: Settings): IPaymentsProcessor => { + const invoiceURL = path(['paymentsProcessors', 'lnurl', 'invoiceURL'], settings) as string | undefined + if (typeof invoiceURL === 'undefined') { + throw new Error('Unable to create payments processor: Setting paymentsProcessor.lnurl.invoiceURL is not configured.') + } + + const client = axios.create() + + return new LnurlPaymentsProcesor(client, createSettings) +} diff --git a/src/factories/payments-processors/nodeless-payments-processor-factory.ts b/src/factories/payments-processors/nodeless-payments-processor-factory.ts new file mode 100644 index 00000000..39e8c702 --- /dev/null +++ b/src/factories/payments-processors/nodeless-payments-processor-factory.ts @@ -0,0 +1,31 @@ +import axios, { CreateAxiosDefaults } from 'axios' +import { path } from 'ramda' + +import { createSettings } from '../settings-factory' +import { IPaymentsProcessor } from '../../@types/clients' +import { NodelessPaymentsProcesor } from '../../payments-processors/nodeless-payments-processor' +import { Settings } from '../../@types/settings' + +const getNodelessAxiosConfig = (settings: Settings): CreateAxiosDefaults => { + 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, + } +} + +export const createNodelessPaymentsProcessor = (settings: Settings): IPaymentsProcessor => { + const client = axios.create(getNodelessAxiosConfig(settings)) + + return new NodelessPaymentsProcesor(client, createSettings) +} diff --git a/src/factories/payments-processors/opennode-payments-processor-factory.ts b/src/factories/payments-processors/opennode-payments-processor-factory.ts new file mode 100644 index 00000000..fa89960c --- /dev/null +++ b/src/factories/payments-processors/opennode-payments-processor-factory.ts @@ -0,0 +1,39 @@ +import axios, { CreateAxiosDefaults } from 'axios' +import { path } from 'ramda' + +import { createSettings } from '../settings-factory' +import { IPaymentsProcessor } from '../../@types/clients' +import { OpenNodePaymentsProcesor } from '../../payments-processors/opennode-payments-processor' +import { Settings } from '../../@types/settings' + +const getOpenNodeAxiosConfig = (settings: Settings): CreateAxiosDefaults => { + if (!process.env.OPENNODE_API_KEY) { + const error = new Error('OPENNODE_API_KEY must be set.') + console.error('Unable to get OpenNode config.', error) + throw error + } + + return { + headers: { + 'content-type': 'application/json', + 'authorization': process.env.OPENNODE_API_KEY, + }, + baseURL: path(['paymentsProcessors', 'opennode', 'baseURL'], settings), + maxRedirects: 1, + } +} + +export const createOpenNodePaymentsProcessor = (settings: Settings): IPaymentsProcessor => { + const callbackBaseURL = path(['paymentsProcessors', 'opennode', 'callbackBaseURL'], settings) as string | undefined + if (typeof callbackBaseURL === 'undefined' || callbackBaseURL.indexOf('nostream.your-domain.com') >= 0) { + const error = new Error('Setting paymentsProcessor.opennode.callbackBaseURL is not configured.') + console.error('Unable to create payments processor.', error) + + throw error + } + + const config = getOpenNodeAxiosConfig(settings) + const client = axios.create(config) + + return new OpenNodePaymentsProcesor(client, createSettings) +} diff --git a/src/factories/payments-processors/zebedee-payments-processor-factory.ts b/src/factories/payments-processors/zebedee-payments-processor-factory.ts new file mode 100644 index 00000000..b8380daf --- /dev/null +++ b/src/factories/payments-processors/zebedee-payments-processor-factory.ts @@ -0,0 +1,50 @@ +import axios, { CreateAxiosDefaults } from 'axios' +import { path } from 'ramda' + +import { createSettings } from '../settings-factory' +import { IPaymentsProcessor } from '../../@types/clients' +import { Settings } from '../../@types/settings' +import { ZebedeePaymentsProcesor } from '../../payments-processors/zebedee-payments-processor' + +const getZebedeeAxiosConfig = (settings: Settings): CreateAxiosDefaults => { + if (!process.env.ZEBEDEE_API_KEY) { + const error = new Error('ZEBEDEE_API_KEY must be set.') + console.error('Unable to get Zebedee config.', error) + throw error + } + + return { + headers: { + 'content-type': 'application/json', + 'apikey': process.env.ZEBEDEE_API_KEY, + }, + baseURL: path(['paymentsProcessors', 'zebedee', 'baseURL'], settings), + maxRedirects: 1, + } +} + +export const createZebedeePaymentsProcessor = (settings: Settings): IPaymentsProcessor => { + const callbackBaseURL = path(['paymentsProcessors', 'zebedee', 'callbackBaseURL'], settings) as string | undefined + if (typeof callbackBaseURL === 'undefined' || callbackBaseURL.indexOf('nostream.your-domain.com') >= 0) { + const error = new Error('Setting paymentsProcessor.zebedee.callbackBaseURL is not configured.') + console.error('Unable to create payments processor.', error) + + throw error + } + + if ( + !Array.isArray(settings.paymentsProcessors?.zebedee?.ipWhitelist) + || !settings.paymentsProcessors?.zebedee?.ipWhitelist?.length + ) { + const error = new Error('Setting paymentsProcessor.zebedee.ipWhitelist is empty.') + console.error('Unable to create payments processor.', error) + + throw error + } + + const config = getZebedeeAxiosConfig(settings) + + const client = axios.create(config) + + return new ZebedeePaymentsProcesor(client, createSettings) +} \ No newline at end of file diff --git a/src/factories/post-invoice-controller-factory.ts b/src/factories/post-invoice-controller-factory.ts deleted file mode 100644 index 2cc3f299..00000000 --- a/src/factories/post-invoice-controller-factory.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createPaymentsService } from './payments-service-factory' -import { createSettings } from './settings-factory' -import { getMasterDbClient } from '../database/client' -import { IController } from '../@types/controllers' -import { PostInvoiceController } from '../controllers/invoices/post-invoice-controller' -import { slidingWindowRateLimiterFactory } from './rate-limiter-factory' -import { UserRepository } from '../repositories/user-repository' - -export const createPostInvoiceController = (): IController => { - const dbClient = getMasterDbClient() - const userRepository = new UserRepository(dbClient) - const paymentsService = createPaymentsService() - - return new PostInvoiceController( - userRepository, - paymentsService, - createSettings, - slidingWindowRateLimiterFactory, - ) -} diff --git a/src/factories/zebedee-callback-controller-factory.ts b/src/factories/zebedee-callback-controller-factory.ts deleted file mode 100644 index 72091b06..00000000 --- a/src/factories/zebedee-callback-controller-factory.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createPaymentsService } from './payments-service-factory' -import { IController } from '../@types/controllers' -import { ZebedeeCallbackController } from '../controllers/callbacks/zebedee-callback-controller' - -export const createZebedeeCallbackController = (): IController => { - return new ZebedeeCallbackController( - createPaymentsService(), - ) -} diff --git a/src/handlers/request-handlers/get-invoice-request-handler.ts b/src/handlers/request-handlers/get-invoice-request-handler.ts deleted file mode 100644 index 38c936bc..00000000 --- a/src/handlers/request-handlers/get-invoice-request-handler.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NextFunction, Request, Response } from 'express' -import { path, pathEq } from 'ramda' -import { readFileSync } from 'fs' - -import { createSettings } from '../../factories/settings-factory' -import { FeeSchedule } from '../../@types/settings' - -let pageCache: string - -export const getInvoiceRequestHandler = (_req: Request, res: Response, next: NextFunction) => { - const settings = createSettings() - - if (pathEq(['payments', 'enabled'], true, settings) - && pathEq(['payments', 'feeSchedules', 'admission', '0', 'enabled'], true, settings)) { - if (!pageCache) { - const name = path(['info', 'name'])(settings) - const feeSchedule = path(['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()) - } - - res.status(200).setHeader('content-type', 'text/html; charset=utf8').send(pageCache) - } else { - res.status(404).send() - } - - next() -} diff --git a/src/handlers/request-handlers/get-invoice-status-request-handler.ts b/src/handlers/request-handlers/get-invoice-status-request-handler.ts deleted file mode 100644 index 76393e9f..00000000 --- a/src/handlers/request-handlers/get-invoice-status-request-handler.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Request, Response } from 'express' - -import { createGetInvoiceStatusController } from '../../factories/get-invoice-status-controller-factory' - -export const getInvoiceStatusRequestHandler = async (req: Request, res: Response) => { - const controller = createGetInvoiceStatusController() - - await controller.handleRequest(req, res) -} diff --git a/src/handlers/request-handlers/post-invoice-request-handler.ts b/src/handlers/request-handlers/post-invoice-request-handler.ts deleted file mode 100644 index c0f8d6a8..00000000 --- a/src/handlers/request-handlers/post-invoice-request-handler.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Request, Response } from 'express' -import { createPostInvoiceController } from '../../factories/post-invoice-controller-factory' - -export const postInvoiceRequestHandler = async ( - req: Request, - res: Response, -) => { - const controller = createPostInvoiceController() - - try { - await controller.handleRequest(req, res) - } catch (error) { - console.error('Unable handle request.', error) - res - .status(500) - .setHeader('content-type', 'text-plain') - .send('Error handling request') - } -} diff --git a/src/handlers/request-handlers/post-lnbits-callback-request-handler.ts b/src/handlers/request-handlers/post-lnbits-callback-request-handler.ts deleted file mode 100644 index 4cc152cc..00000000 --- a/src/handlers/request-handlers/post-lnbits-callback-request-handler.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Request, Response } from 'express' - -import { createLNbitsCallbackController } from '../../factories/lnbits-callback-controller-factory' - -export const postLNbitsCallbackRequestHandler = async ( - req: Request, - res: Response, -) => { - const controller = createLNbitsCallbackController() - - try { - await controller.handleRequest(req, res) - } catch (error) { - console.error('error while handling LNbits request: %o', error) - res - .status(500) - .setHeader('content-type', 'text/plain') - .send('Error handling request') - } -} diff --git a/src/handlers/request-handlers/post-nodeless-callback-request-handler.ts b/src/handlers/request-handlers/post-nodeless-callback-request-handler.ts deleted file mode 100644 index 96c10950..00000000 --- a/src/handlers/request-handlers/post-nodeless-callback-request-handler.ts +++ /dev/null @@ -1,18 +0,0 @@ -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') - } -} diff --git a/src/handlers/request-handlers/post-zebedee-callback-request-handler.ts b/src/handlers/request-handlers/post-zebedee-callback-request-handler.ts deleted file mode 100644 index 4bdf2375..00000000 --- a/src/handlers/request-handlers/post-zebedee-callback-request-handler.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Request, Response } from 'express' -import { createZebedeeCallbackController } from '../../factories/zebedee-callback-controller-factory' - -export const postZebedeeCallbackRequestHandler = async ( - req: Request, - res: Response, -) => { - const controller = createZebedeeCallbackController() - - try { - await controller.handleRequest(req, res) - } catch (error) { - res - .status(500) - .setHeader('content-type', 'text/plain') - .send('Error handling request') - } -} diff --git a/src/handlers/request-handlers/with-controller-request-handler.ts b/src/handlers/request-handlers/with-controller-request-handler.ts new file mode 100644 index 00000000..52f55ebc --- /dev/null +++ b/src/handlers/request-handlers/with-controller-request-handler.ts @@ -0,0 +1,18 @@ +import { Request, Response } from 'express' + +import { Factory } from '../../@types/base' +import { IController } from '../../@types/controllers' + +export const withController = (controllerFactory: Factory) => async ( + request: Request, + response: Response, +) => { + try { + return await controllerFactory().handleRequest(request, response) + } catch (error) { + response + .status(500) + .setHeader('content-type', 'text/plain') + .send('Error handling request') + } +} diff --git a/src/payments-processors/opennode-payments-processor.ts b/src/payments-processors/opennode-payments-processor.ts new file mode 100644 index 00000000..f5c340e6 --- /dev/null +++ b/src/payments-processors/opennode-payments-processor.ts @@ -0,0 +1,68 @@ +import { AxiosInstance } from 'axios' +import { Factory } from '../@types/base' + +import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients' +import { createLogger } from '../factories/logger-factory' +import { fromOpenNodeInvoice } from '../utils/transform' +import { Settings } from '../@types/settings' + +const debug = createLogger('opennode-payments-processor') + +export class OpenNodePaymentsProcesor implements IPaymentsProcessor { + public constructor( + private httpClient: AxiosInstance, + private settings: Factory + ) {} + + public async getInvoice(invoiceId: string): Promise { + debug('get invoice: %s', invoiceId) + + try { + const response = await this.httpClient.get(`/v2/charge/${invoiceId}`, { + maxRedirects: 1, + }) + + return fromOpenNodeInvoice(response.data.data) + } catch (error) { + console.error(`Unable to get invoice ${invoiceId}. Reason:`, error) + + throw error + } + } + + public async createInvoice(request: CreateInvoiceRequest): Promise { + debug('create invoice: %o', request) + const { + amount: amountMsats, + description, + requestId, + } = request + + const amountSats = Number(amountMsats / 1000n) + + const body = { + amount: amountSats, + description, + order_id: requestId, + callback_url: this.settings().paymentsProcessors?.opennode?.callbackBaseURL, + ttl: 10, + } + + try { + debug('request body: %o', body) + const response = await this.httpClient.post('/v1/charges', body, { + maxRedirects: 1, + }) + + const result = fromOpenNodeInvoice(response.data.data) + + debug('result: %o', result) + + return result + } catch (error) { + console.error('Unable to request invoice. Reason:', error.message) + + throw error + } + } +} diff --git a/src/routes/callbacks/index.ts b/src/routes/callbacks/index.ts index 0c5e42b8..5b86e0bf 100644 --- a/src/routes/callbacks/index.ts +++ b/src/routes/callbacks/index.ts @@ -1,103 +1,21 @@ -import { deriveFromSecret, hmacSha256 } from '../../utils/secret' import { json, Router } from 'express' -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') +import { createLNbitsCallbackController } from '../../factories/controllers/lnbits-callback-controller-factory' +import { createNodelessCallbackController } from '../../factories/controllers/nodeless-callback-controller-factory' +import { createOpenNodeCallbackController } from '../../factories/controllers/opennode-callback-controller-factory' +import { createZebedeeCallbackController } from '../../factories/controllers/zebedee-callback-controller-factory' +import { withController } from '../../handlers/request-handlers/with-controller-request-handler' const router = Router() -router - .post('/zebedee', json(), async (req, res) => { - const settings = createSettings() - const { ipWhitelist = [] } = settings.paymentsProcessors?.zebedee ?? {} - const remoteAddress = getRemoteAddress(req, settings) - const paymentProcessor = settings.payments?.processor ?? 'null' - - if (ipWhitelist.length && !ipWhitelist.includes(remoteAddress)) { - debug('unauthorized request from %s to /callbacks/zebedee', remoteAddress) - res - .status(403) - .send('Forbidden') - return - } - - if (paymentProcessor !== 'zebedee') { - debug('denied request from %s to /callbacks/zebedee which is not the current payment processor', remoteAddress) - res - .status(403) - .send('Forbidden') - return - } - - return postZebedeeCallbackRequestHandler(req, res) - }) - .post('/lnbits', json(), async (req, res) => { - const settings = createSettings() - const remoteAddress = getRemoteAddress(req, settings) - const paymentProcessor = settings.payments?.processor ?? 'null' - - if (paymentProcessor !== 'lnbits') { - debug('denied request from %s to /callbacks/lnbits which is not the current payment processor', remoteAddress) - res - .status(403) - .send('Forbidden') - return - } - - 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]) { - if (parseInt(split[0]) > Date.now()) { - validationPassed = true - } - } - } - - if (!validationPassed) { - debug('unauthorized request from %s to /callbacks/lnbits', remoteAddress) - res - .status(403) - .send('Forbidden') - return - } - return postLNbitsCallbackRequestHandler(req, res) - }) +router + .post('/zebedee', json(), withController(createZebedeeCallbackController)) + .post('/lnbits', json(), withController(createLNbitsCallbackController)) .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) - }) + }), withController(createNodelessCallbackController)) + .post('/opennode', json(), withController(createOpenNodeCallbackController)) export default router - diff --git a/src/routes/invoices/index.ts b/src/routes/invoices/index.ts index 23a33986..5592d964 100644 --- a/src/routes/invoices/index.ts +++ b/src/routes/invoices/index.ts @@ -1,14 +1,15 @@ import { Router, urlencoded } from 'express' -import { getInvoiceRequestHandler } from '../../handlers/request-handlers/get-invoice-request-handler' -import { getInvoiceStatusRequestHandler } from '../../handlers/request-handlers/get-invoice-status-request-handler' -import { postInvoiceRequestHandler } from '../../handlers/request-handlers/post-invoice-request-handler' +import { createGetInvoiceController } from '../../factories/controllers/get-invoice-controller-factory' +import { createGetInvoiceStatusController } from '../../factories/controllers/get-invoice-status-controller-factory' +import { createPostInvoiceController } from '../../factories/controllers/post-invoice-controller-factory' +import { withController } from '../../handlers/request-handlers/with-controller-request-handler' const invoiceRouter = Router() invoiceRouter - .get('/', getInvoiceRequestHandler) - .get('/:invoiceId/status', getInvoiceStatusRequestHandler) - .post('/', urlencoded({ extended: true }), postInvoiceRequestHandler) + .get('/', withController(createGetInvoiceController)) + .get('/:invoiceId/status', withController(createGetInvoiceStatusController)) + .post('/', urlencoded({ extended: true }), withController(createPostInvoiceController)) export default invoiceRouter diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 237a64b5..664f4ec2 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -1,7 +1,7 @@ -import { always, applySpec, cond, equals, ifElse, is, isNil, path, pipe, prop, propSatisfies, T } from 'ramda' +import { always, applySpec, cond, equals, ifElse, is, isNil, multiply, path, pathSatisfies, pipe, prop, propSatisfies, T } from 'ramda' import { bech32 } from 'bech32' -import { Invoice, InvoiceStatus } from '../@types/invoice' +import { Invoice, InvoiceStatus, InvoiceUnit } from '../@types/invoice' import { User } from '../@types/user' export const toJSON = (input: any) => JSON.stringify(input) @@ -59,7 +59,7 @@ export const toBech32 = (prefix: string) => (input: string): string => { return bech32.encode(prefix, bech32.toWords(Buffer.from(input, 'hex'))) } -export const toDate = (input: string) => new Date(input) +export const toDate = (input: string | number) => new Date(input) export const fromZebedeeInvoice = applySpec({ id: prop('id'), @@ -126,3 +126,54 @@ export const fromNodelessInvoice = applySpec({ ), // rawResponse: toJSON, }) + +export const fromOpenNodeInvoice = applySpec({ + id: prop('id'), + pubkey: prop('order_id'), + bolt11: ifElse( + pathSatisfies(is(String), ['lightning_invoice', 'payreq']), + path(['lightning_invoice', 'payreq']), + path(['lightning', 'payreq']) + ), + amountRequested: pipe( + ifElse( + propSatisfies(is(Number), 'amount'), + prop('amount'), + prop('price'), + ) as () => number, + toBigInt, + ), + description: prop('description'), + unit: always(InvoiceUnit.SATS), + status: pipe( + prop('status'), + cond([ + [equals('expired'), always(InvoiceStatus.EXPIRED)], + [equals('refunded'), always(InvoiceStatus.EXPIRED)], + [equals('unpaid'), always(InvoiceStatus.PENDING)], + [equals('processing'), always(InvoiceStatus.PENDING)], + [equals('underpaid'), always(InvoiceStatus.PENDING)], + [equals('paid'), always(InvoiceStatus.COMPLETED)], + ]), + ), + expiresAt: pipe( + cond([ + [pathSatisfies(is(String), ['lightning', 'expires_at']), path(['lightning', 'expires_at'])], + [pathSatisfies(is(Number), ['lightning_invoice', 'expires_at']), pipe(path(['lightning_invoice', 'expires_at']), multiply(1000))], + ]), + toDate, + ), + confirmedAt: cond([ + [propSatisfies(equals('paid'), 'status'), () => new Date()], + [T, always(null)], + ]), + createdAt: pipe( + ifElse( + propSatisfies(is(Number), 'created_at'), + pipe(prop('created_at'), multiply(1000)), + prop('created_at'), + ), + toDate, + ), + rawResponse: toJSON, +})