mirror of
https://github.com/Cameri/nostream.git
synced 2025-03-17 13:21:45 +01:00
feat: implement opennode payments processor (#315)
This commit is contained in:
parent
7331f9560f
commit
df1a36437c
25
README.md
25
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)
|
||||
|
@ -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:*"
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
|
69
src/controllers/callbacks/opennode-callback-controller.ts
Normal file
69
src/controllers/callbacks/opennode-callback-controller.ts
Normal file
@ -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')
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
|
34
src/controllers/invoices/get-invoice-controller.ts
Normal file
34
src/controllers/invoices/get-invoice-controller.ts
Normal file
@ -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<void> {
|
||||
const settings = createSettings()
|
||||
|
||||
if (pathEq(['payments', 'enabled'], true, settings)
|
||||
&& pathEq(['payments', 'feeSchedules', 'admission', '0', 'enabled'], true, settings)) {
|
||||
if (!pageCache) {
|
||||
const name = path<string>(['info', 'name'])(settings)
|
||||
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())
|
||||
}
|
||||
|
||||
res.status(200).setHeader('content-type', 'text/html; charset=utf8').send(pageCache)
|
||||
} else {
|
||||
res.status(404).send()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
import { GetInvoiceController } from '../../controllers/invoices/get-invoice-controller'
|
||||
|
||||
export const createGetInvoiceController = () => new GetInvoiceController()
|
@ -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)
|
||||
}
|
@ -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())
|
||||
)
|
||||
}
|
@ -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(),
|
||||
)
|
@ -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(),
|
||||
)
|
||||
}
|
20
src/factories/controllers/post-invoice-controller-factory.ts
Normal file
20
src/factories/controllers/post-invoice-controller-factory.ts
Normal file
@ -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,
|
||||
)
|
||||
}
|
@ -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(),
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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())
|
||||
)
|
||||
}
|
@ -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(),
|
||||
)
|
@ -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<any> => {
|
||||
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<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.')
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
@ -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<any> => {
|
||||
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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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<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,
|
||||
}
|
||||
}
|
||||
|
||||
export const createNodelessPaymentsProcessor = (settings: Settings): IPaymentsProcessor => {
|
||||
const client = axios.create(getNodelessAxiosConfig(settings))
|
||||
|
||||
return new NodelessPaymentsProcesor(client, createSettings)
|
||||
}
|
@ -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<any> => {
|
||||
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)
|
||||
}
|
@ -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<any> => {
|
||||
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)
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
@ -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(),
|
||||
)
|
||||
}
|
@ -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<string>(['info', 'name'])(settings)
|
||||
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())
|
||||
}
|
||||
|
||||
res.status(200).setHeader('content-type', 'text/html; charset=utf8').send(pageCache)
|
||||
} else {
|
||||
res.status(404).send()
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
@ -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)
|
||||
}
|
@ -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')
|
||||
}
|
||||
}
|
@ -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')
|
||||
}
|
||||
}
|
@ -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')
|
||||
}
|
||||
}
|
@ -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')
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import { Factory } from '../../@types/base'
|
||||
import { IController } from '../../@types/controllers'
|
||||
|
||||
export const withController = (controllerFactory: Factory<IController>) => 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')
|
||||
}
|
||||
}
|
68
src/payments-processors/opennode-payments-processor.ts
Normal file
68
src/payments-processors/opennode-payments-processor.ts
Normal file
@ -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<Settings>
|
||||
) {}
|
||||
|
||||
public async getInvoice(invoiceId: string): Promise<GetInvoiceResponse> {
|
||||
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<CreateInvoiceResponse> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
.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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<Invoice>({
|
||||
id: prop('id'),
|
||||
@ -126,3 +126,54 @@ export const fromNodelessInvoice = applySpec<Invoice>({
|
||||
),
|
||||
// rawResponse: toJSON,
|
||||
})
|
||||
|
||||
export const fromOpenNodeInvoice = applySpec<Invoice>({
|
||||
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,
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user