mirror of
https://github.com/Cameri/nostream.git
synced 2025-09-17 19:13:35 +02:00
Add LNbits payment processor (#194)
* feat: add lnbits payment processor * fix: add lnbits error logging and add lnbits config * feat: use HMAC instead of IP whitelist for LNbits also adds two utility functions and ensures the SECRET environment variable is set. * refactor: remove unnecessary comment * fix(pay-to-relay/lnbits): compare by msat scaled amount * fix: scale balance addition with invoice unit on confirm_invoice
This commit is contained in:
55
migrations/20230205_004400_change_invoice_id_to_string.js
Normal file
55
migrations/20230205_004400_change_invoice_id_to_string.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.raw('ALTER TABLE invoices ALTER COLUMN id TYPE text USING id::text; ALTER TABLE invoices ALTER COLUMN id DROP DEFAULT;')
|
||||||
|
.raw(`CREATE OR REPLACE FUNCTION confirm_invoice(invoice_id TEXT, amount_received BIGINT, confirmation_date TIMESTAMP WITHOUT TIME ZONE)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
payee BYTEA;
|
||||||
|
confirmed_date TIMESTAMP WITHOUT TIME ZONE;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
SELECT "pubkey", "confirmed_at" INTO payee, confirmed_date FROM "invoices" WHERE id = invoice_id;
|
||||||
|
IF confirmed_date IS NULL THEN
|
||||||
|
UPDATE invoices
|
||||||
|
SET
|
||||||
|
"confirmed_at" = confirmation_date,
|
||||||
|
"amount_paid" = amount_received,
|
||||||
|
"updated_at" = now_utc()
|
||||||
|
WHERE id = invoice_id;
|
||||||
|
UPDATE users SET balance = balance + amount_received WHERE "pubkey" = payee;
|
||||||
|
END IF;
|
||||||
|
RETURN 0;
|
||||||
|
END;
|
||||||
|
$$;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.raw('ALTER TABLE invoices ALTER COLUMN id TYPE uuid USING id::uuid; ALTER TABLE invoices ALTER COLUMN id SET DEFAULT uuid_generate_v4();')
|
||||||
|
.raw(`CREATE OR REPLACE FUNCTION confirm_invoice(invoice_id UUID, amount_received BIGINT, confirmation_date TIMESTAMP WITHOUT TIME ZONE)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
payee BYTEA;
|
||||||
|
confirmed_date TIMESTAMP WITHOUT TIME ZONE;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
SELECT "pubkey", "confirmed_at" INTO payee, confirmed_date FROM "invoices" WHERE id = invoice_id;
|
||||||
|
IF confirmed_date IS NULL THEN
|
||||||
|
UPDATE invoices
|
||||||
|
SET
|
||||||
|
"confirmed_at" = confirmation_date,
|
||||||
|
"amount_paid" = amount_received,
|
||||||
|
"updated_at" = now_utc()
|
||||||
|
WHERE id = invoice_id;
|
||||||
|
UPDATE users SET balance = balance + amount_received WHERE "pubkey" = payee;
|
||||||
|
END IF;
|
||||||
|
RETURN 0;
|
||||||
|
END;
|
||||||
|
$$;`)
|
||||||
|
}
|
@@ -0,0 +1,60 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.raw(`CREATE OR REPLACE FUNCTION confirm_invoice(invoice_id TEXT, amount_received BIGINT, confirmation_date TIMESTAMP WITHOUT TIME ZONE)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
payee BYTEA;
|
||||||
|
confirmed_date TIMESTAMP WITHOUT TIME ZONE;
|
||||||
|
unit TEXT;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
SELECT "pubkey", "confirmed_at", "unit" INTO payee, confirmed_date, unit FROM "invoices" WHERE id = invoice_id;
|
||||||
|
IF confirmed_date IS NULL THEN
|
||||||
|
UPDATE invoices
|
||||||
|
SET
|
||||||
|
"confirmed_at" = confirmation_date,
|
||||||
|
"amount_paid" = amount_received,
|
||||||
|
"updated_at" = now_utc()
|
||||||
|
WHERE id = invoice_id;
|
||||||
|
IF unit = 'sats' THEN
|
||||||
|
UPDATE users SET balance = balance + amount_received * 1000 WHERE "pubkey" = payee;
|
||||||
|
ELSIF unit = 'msats' THEN
|
||||||
|
UPDATE users SET balance = balance + amount_received WHERE "pubkey" = payee;
|
||||||
|
ELSIF unit = 'btc' THEN
|
||||||
|
UPDATE users SET balance = balance + amount_received * 100000000 * 1000 WHERE "pubkey" = payee;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
RETURN 0;
|
||||||
|
END;
|
||||||
|
$$;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.raw(`CREATE OR REPLACE FUNCTION confirm_invoice(invoice_id TEXT, amount_received BIGINT, confirmation_date TIMESTAMP WITHOUT TIME ZONE)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
payee BYTEA;
|
||||||
|
confirmed_date TIMESTAMP WITHOUT TIME ZONE;
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
SELECT "pubkey", "confirmed_at" INTO payee, confirmed_date FROM "invoices" WHERE id = invoice_id;
|
||||||
|
IF confirmed_date IS NULL THEN
|
||||||
|
UPDATE invoices
|
||||||
|
SET
|
||||||
|
"confirmed_at" = confirmation_date,
|
||||||
|
"amount_paid" = amount_received,
|
||||||
|
"updated_at" = now_utc()
|
||||||
|
WHERE id = invoice_id;
|
||||||
|
UPDATE users SET balance = balance + amount_received WHERE "pubkey" = payee;
|
||||||
|
END IF;
|
||||||
|
RETURN 0;
|
||||||
|
END;
|
||||||
|
$$;`)
|
||||||
|
}
|
@@ -22,6 +22,9 @@ paymentsProcessors:
|
|||||||
ipWhitelist:
|
ipWhitelist:
|
||||||
- "3.225.112.64"
|
- "3.225.112.64"
|
||||||
- "::ffff:3.225.112.64"
|
- "::ffff:3.225.112.64"
|
||||||
|
lnbits:
|
||||||
|
baseURL: https://lnbits.your-domain.com/
|
||||||
|
callbackBaseURL: https://nostream.your-domain.com/callbacks/lnbits
|
||||||
network:
|
network:
|
||||||
maxPayloadSize: 524288
|
maxPayloadSize: 524288
|
||||||
remoteIpHeader: x-forwarded-for
|
remoteIpHeader: x-forwarded-for
|
||||||
|
@@ -30,8 +30,8 @@ export interface DBInvoice {
|
|||||||
id: string
|
id: string
|
||||||
pubkey: Buffer
|
pubkey: Buffer
|
||||||
bolt11: string
|
bolt11: string
|
||||||
amount_requested: BigInt
|
amount_requested: bigint
|
||||||
amount_paid: BigInt
|
amount_paid: bigint
|
||||||
unit: InvoiceUnit
|
unit: InvoiceUnit
|
||||||
status: InvoiceStatus,
|
status: InvoiceStatus,
|
||||||
description: string
|
description: string
|
||||||
|
@@ -148,8 +148,14 @@ export interface ZebedeePaymentsProcessor {
|
|||||||
ipWhitelist: string[]
|
ipWhitelist: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LNbitsPaymentProcessor {
|
||||||
|
baseURL: string
|
||||||
|
callbackBaseURL: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface PaymentsProcessors {
|
export interface PaymentsProcessors {
|
||||||
zebedee?: ZebedeePaymentsProcessor
|
zebedee?: ZebedeePaymentsProcessor
|
||||||
|
lnbits?: LNbitsPaymentProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Local {
|
export interface Local {
|
||||||
|
@@ -62,6 +62,11 @@ export class App implements IRunnable {
|
|||||||
logCentered(`Pay-to-relay ${pathEq(['payments', 'enabled'], true, settings) ? 'enabled' : 'disabled'}`, width)
|
logCentered(`Pay-to-relay ${pathEq(['payments', 'enabled'], true, settings) ? 'enabled' : 'disabled'}`, width)
|
||||||
logCentered(`Payments provider: ${path(['payments', 'processor'], settings)}`, width)
|
logCentered(`Payments provider: ${path(['payments', 'processor'], settings)}`, width)
|
||||||
|
|
||||||
|
if (typeof this.process.env.SECRET !== 'string' || this.process.env.SECRET === 'changeme') {
|
||||||
|
console.error('Please configure the secret using the SECRET environment variable.')
|
||||||
|
this.process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
const workerCount = process.env.WORKER_COUNT
|
const workerCount = process.env.WORKER_COUNT
|
||||||
? Number(process.env.WORKER_COUNT)
|
? Number(process.env.WORKER_COUNT)
|
||||||
: this.settings().workers?.count || cpus().length
|
: this.settings().workers?.count || cpus().length
|
||||||
|
@@ -41,6 +41,7 @@ export enum EventTags {
|
|||||||
|
|
||||||
export enum PaymentsProcessors {
|
export enum PaymentsProcessors {
|
||||||
ZEBEDEE = 'zebedee',
|
ZEBEDEE = 'zebedee',
|
||||||
|
LNBITS = 'lnbits',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventDelegatorMetadataKey = Symbol('Delegator')
|
export const EventDelegatorMetadataKey = Symbol('Delegator')
|
||||||
|
88
src/controllers/callbacks/lnbits-callback-controller.ts
Normal file
88
src/controllers/callbacks/lnbits-callback-controller.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
|
import { createLogger } from '../../factories/logger-factory'
|
||||||
|
import { IController } from '../../@types/controllers'
|
||||||
|
import { IInvoiceRepository } from '../../@types/repositories'
|
||||||
|
import { InvoiceStatus } from '../../@types/invoice'
|
||||||
|
import { IPaymentsService } from '../../@types/services'
|
||||||
|
|
||||||
|
const debug = createLogger('lnbits-callback-controller')
|
||||||
|
|
||||||
|
export class LNbitsCallbackController implements IController {
|
||||||
|
public constructor(
|
||||||
|
private readonly paymentsService: IPaymentsService,
|
||||||
|
private readonly invoiceRepository: IInvoiceRepository
|
||||||
|
) { }
|
||||||
|
|
||||||
|
// TODO: Validate
|
||||||
|
public async handleRequest(
|
||||||
|
request: Request,
|
||||||
|
response: Response,
|
||||||
|
) {
|
||||||
|
debug('request headers: %o', request.headers)
|
||||||
|
debug('request body: %o', request.body)
|
||||||
|
|
||||||
|
const body = request.body
|
||||||
|
if (!body || typeof body !== 'object' || typeof body.payment_hash !== 'string' || body.payment_hash.length !== 64) {
|
||||||
|
response
|
||||||
|
.status(400)
|
||||||
|
.setHeader('content-type', 'text/plain; charset=utf8')
|
||||||
|
.send('Malformed body')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoice = await this.paymentsService.getInvoiceFromPaymentsProcessor(body.payment_hash)
|
||||||
|
const storedInvoice = await this.invoiceRepository.findById(body.payment_hash)
|
||||||
|
|
||||||
|
if (!storedInvoice) {
|
||||||
|
response
|
||||||
|
.status(404)
|
||||||
|
.setHeader('content-type', 'text/plain; charset=utf8')
|
||||||
|
.send('No such invoice')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.paymentsService.updateInvoice(invoice)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Unable to persist invoice ${invoice.id}`, error)
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
invoice.status !== InvoiceStatus.COMPLETED
|
||||||
|
&& !invoice.confirmedAt
|
||||||
|
) {
|
||||||
|
response
|
||||||
|
.status(200)
|
||||||
|
.send()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storedInvoice.status === InvoiceStatus.COMPLETED) {
|
||||||
|
response
|
||||||
|
.status(409)
|
||||||
|
.setHeader('content-type', 'text/plain; charset=utf8')
|
||||||
|
.send('Invoice is already marked paid')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice.amountPaid = invoice.amountRequested
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.paymentsService.confirmInvoice(invoice)
|
||||||
|
await this.paymentsService.sendInvoiceUpdateNotification(invoice)
|
||||||
|
} 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,10 +1,8 @@
|
|||||||
import { Request, Response } from 'express'
|
|
||||||
import { path } from 'ramda'
|
|
||||||
import { readFileSync } from 'fs'
|
|
||||||
|
|
||||||
import { FeeSchedule, Settings } from '../../@types/settings'
|
import { FeeSchedule, Settings } from '../../@types/settings'
|
||||||
import { fromBech32, toBech32 } from '../../utils/transform'
|
import { fromBech32, toBech32 } from '../../utils/transform'
|
||||||
import { getPrivateKeyFromSecret, getPublicKey } from '../../utils/event'
|
import { getPublicKey, getRelayPrivateKey } from '../../utils/event'
|
||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
import { createLogger } from '../../factories/logger-factory'
|
import { createLogger } from '../../factories/logger-factory'
|
||||||
import { getRemoteAddress } from '../../utils/http'
|
import { getRemoteAddress } from '../../utils/http'
|
||||||
import { IController } from '../../@types/controllers'
|
import { IController } from '../../@types/controllers'
|
||||||
@@ -12,6 +10,8 @@ import { Invoice } from '../../@types/invoice'
|
|||||||
import { IPaymentsService } from '../../@types/services'
|
import { IPaymentsService } from '../../@types/services'
|
||||||
import { IRateLimiter } from '../../@types/utils'
|
import { IRateLimiter } from '../../@types/utils'
|
||||||
import { IUserRepository } from '../../@types/repositories'
|
import { IUserRepository } from '../../@types/repositories'
|
||||||
|
import { path } from 'ramda'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
|
||||||
let pageCache: string
|
let pageCache: string
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ export class PostInvoiceController implements IController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const relayPrivkey = getPrivateKeyFromSecret(process.env.SECRET as string)(relayUrl)
|
const relayPrivkey = getRelayPrivateKey(relayUrl)
|
||||||
const relayPubkey = getPublicKey(relayPrivkey)
|
const relayPubkey = getPublicKey(relayPrivkey)
|
||||||
|
|
||||||
const replacements = {
|
const replacements = {
|
||||||
|
12
src/factories/lnbits-callback-controller-factory.ts
Normal file
12
src/factories/lnbits-callback-controller-factory.ts
Normal file
@@ -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())
|
||||||
|
)
|
||||||
|
}
|
@@ -1,28 +1,7 @@
|
|||||||
import { getMasterDbClient, getReadReplicaDbClient } from '../database/client'
|
import { createPaymentsService } from './payments-service-factory'
|
||||||
import { createPaymentsProcessor } from './payments-processor-factory'
|
|
||||||
import { createSettings } from './settings-factory'
|
import { createSettings } from './settings-factory'
|
||||||
import { EventRepository } from '../repositories/event-repository'
|
|
||||||
import { InvoiceRepository } from '../repositories/invoice-repository'
|
|
||||||
import { MaintenanceWorker } from '../app/maintenance-worker'
|
import { MaintenanceWorker } from '../app/maintenance-worker'
|
||||||
import { PaymentsService } from '../services/payments-service'
|
|
||||||
import { UserRepository } from '../repositories/user-repository'
|
|
||||||
|
|
||||||
export const maintenanceWorkerFactory = () => {
|
export const maintenanceWorkerFactory = () => {
|
||||||
const dbClient = getMasterDbClient()
|
return new MaintenanceWorker(process, createPaymentsService(), createSettings)
|
||||||
const rrDbClient = getReadReplicaDbClient()
|
|
||||||
const paymentsProcessor = createPaymentsProcessor()
|
|
||||||
const userRepository = new UserRepository(dbClient)
|
|
||||||
const invoiceRepository = new InvoiceRepository(dbClient)
|
|
||||||
const eventRepository = new EventRepository(dbClient, rrDbClient)
|
|
||||||
|
|
||||||
const paymentsService = new PaymentsService(
|
|
||||||
dbClient,
|
|
||||||
paymentsProcessor,
|
|
||||||
userRepository,
|
|
||||||
invoiceRepository,
|
|
||||||
eventRepository,
|
|
||||||
createSettings,
|
|
||||||
)
|
|
||||||
|
|
||||||
return new MaintenanceWorker(process, paymentsService, createSettings)
|
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import { path } from 'ramda'
|
|||||||
import { createLogger } from './logger-factory'
|
import { createLogger } from './logger-factory'
|
||||||
import { createSettings } from './settings-factory'
|
import { createSettings } from './settings-factory'
|
||||||
import { IPaymentsProcessor } from '../@types/clients'
|
import { IPaymentsProcessor } from '../@types/clients'
|
||||||
|
import { LNbitsPaymentsProcesor } from '../payments-processors/lnbits-payment-processor'
|
||||||
import { NullPaymentsProcessor } from '../payments-processors/null-payments-processor'
|
import { NullPaymentsProcessor } from '../payments-processors/null-payments-processor'
|
||||||
import { PaymentsProcessor } from '../payments-processors/payments-procesor'
|
import { PaymentsProcessor } from '../payments-processors/payments-procesor'
|
||||||
import { Settings } from '../@types/settings'
|
import { Settings } from '../@types/settings'
|
||||||
@@ -11,7 +12,7 @@ import { ZebedeePaymentsProcesor } from '../payments-processors/zebedee-payments
|
|||||||
|
|
||||||
const debug = createLogger('create-zebedee-payments-processor')
|
const debug = createLogger('create-zebedee-payments-processor')
|
||||||
|
|
||||||
const getConfig = (settings: Settings): CreateAxiosDefaults<any> => {
|
const getZebedeeAxiosConfig = (settings: Settings): CreateAxiosDefaults<any> => {
|
||||||
if (!process.env.ZEBEDEE_API_KEY) {
|
if (!process.env.ZEBEDEE_API_KEY) {
|
||||||
throw new Error('ZEBEDEE_API_KEY must be set.')
|
throw new Error('ZEBEDEE_API_KEY must be set.')
|
||||||
}
|
}
|
||||||
@@ -26,6 +27,21 @@ const getConfig = (settings: Settings): CreateAxiosDefaults<any> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 createZebedeePaymentsProcessor = (settings: Settings): IPaymentsProcessor => {
|
const createZebedeePaymentsProcessor = (settings: Settings): IPaymentsProcessor => {
|
||||||
const callbackBaseURL = path(['paymentsProcessors', 'zebedee', 'callbackBaseURL'], settings) as string | undefined
|
const callbackBaseURL = path(['paymentsProcessors', 'zebedee', 'callbackBaseURL'], settings) as string | undefined
|
||||||
if (typeof callbackBaseURL === 'undefined' || callbackBaseURL.indexOf('nostream.your-domain.com') >= 0) {
|
if (typeof callbackBaseURL === 'undefined' || callbackBaseURL.indexOf('nostream.your-domain.com') >= 0) {
|
||||||
@@ -39,7 +55,7 @@ const createZebedeePaymentsProcessor = (settings: Settings): IPaymentsProcessor
|
|||||||
throw new Error('Unable to create payments processor: Setting paymentsProcessor.zebedee.ipWhitelist is empty.')
|
throw new Error('Unable to create payments processor: Setting paymentsProcessor.zebedee.ipWhitelist is empty.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = getConfig(settings)
|
const config = getZebedeeAxiosConfig(settings)
|
||||||
debug('config: %o', config)
|
debug('config: %o', config)
|
||||||
const client = axios.create(config)
|
const client = axios.create(config)
|
||||||
|
|
||||||
@@ -48,6 +64,21 @@ const createZebedeePaymentsProcessor = (settings: Settings): IPaymentsProcessor
|
|||||||
return new PaymentsProcessor(zpp)
|
return new PaymentsProcessor(zpp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw new Error('Unable to create payments processor: Setting paymentsProcessor.lnbits.callbackBaseURL is not configured.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = getLNbitsAxiosConfig(settings)
|
||||||
|
debug('config: %o', config)
|
||||||
|
const client = axios.create(config)
|
||||||
|
|
||||||
|
const pp = new LNbitsPaymentsProcesor(client, createSettings)
|
||||||
|
|
||||||
|
return new PaymentsProcessor(pp)
|
||||||
|
}
|
||||||
|
|
||||||
export const createPaymentsProcessor = (): IPaymentsProcessor => {
|
export const createPaymentsProcessor = (): IPaymentsProcessor => {
|
||||||
const settings = createSettings()
|
const settings = createSettings()
|
||||||
if (!settings.payments?.enabled) {
|
if (!settings.payments?.enabled) {
|
||||||
@@ -58,6 +89,8 @@ export const createPaymentsProcessor = (): IPaymentsProcessor => {
|
|||||||
switch (settings.payments?.processor) {
|
switch (settings.payments?.processor) {
|
||||||
case 'zebedee':
|
case 'zebedee':
|
||||||
return createZebedeePaymentsProcessor(settings)
|
return createZebedeePaymentsProcessor(settings)
|
||||||
|
case 'lnbits':
|
||||||
|
return createLNbitsPaymentProcessor(settings)
|
||||||
default:
|
default:
|
||||||
return new NullPaymentsProcessor()
|
return new NullPaymentsProcessor()
|
||||||
}
|
}
|
||||||
|
@@ -1,29 +1,15 @@
|
|||||||
import { getMasterDbClient, getReadReplicaDbClient } from '../database/client'
|
import { createPaymentsService } from './payments-service-factory'
|
||||||
import { createPaymentsProcessor } from './payments-processor-factory'
|
|
||||||
import { createSettings } from './settings-factory'
|
import { createSettings } from './settings-factory'
|
||||||
import { EventRepository } from '../repositories/event-repository'
|
import { getMasterDbClient } from '../database/client'
|
||||||
import { IController } from '../@types/controllers'
|
import { IController } from '../@types/controllers'
|
||||||
import { InvoiceRepository } from '../repositories/invoice-repository'
|
|
||||||
import { PaymentsService } from '../services/payments-service'
|
|
||||||
import { PostInvoiceController } from '../controllers/invoices/post-invoice-controller'
|
import { PostInvoiceController } from '../controllers/invoices/post-invoice-controller'
|
||||||
import { slidingWindowRateLimiterFactory } from './rate-limiter-factory'
|
import { slidingWindowRateLimiterFactory } from './rate-limiter-factory'
|
||||||
import { UserRepository } from '../repositories/user-repository'
|
import { UserRepository } from '../repositories/user-repository'
|
||||||
|
|
||||||
export const createPostInvoiceController = (): IController => {
|
export const createPostInvoiceController = (): IController => {
|
||||||
const dbClient = getMasterDbClient()
|
const dbClient = getMasterDbClient()
|
||||||
const rrDbClient = getReadReplicaDbClient()
|
|
||||||
const eventRepository = new EventRepository(dbClient, rrDbClient)
|
|
||||||
const invoiceRepository = new InvoiceRepository(dbClient)
|
|
||||||
const userRepository = new UserRepository(dbClient)
|
const userRepository = new UserRepository(dbClient)
|
||||||
const paymentsProcessor = createPaymentsProcessor()
|
const paymentsService = createPaymentsService()
|
||||||
const paymentsService = new PaymentsService(
|
|
||||||
dbClient,
|
|
||||||
paymentsProcessor,
|
|
||||||
userRepository,
|
|
||||||
invoiceRepository,
|
|
||||||
eventRepository,
|
|
||||||
createSettings,
|
|
||||||
)
|
|
||||||
|
|
||||||
return new PostInvoiceController(
|
return new PostInvoiceController(
|
||||||
userRepository,
|
userRepository,
|
||||||
|
@@ -1,30 +1,9 @@
|
|||||||
import { getMasterDbClient, getReadReplicaDbClient } from '../database/client'
|
import { createPaymentsService } from './payments-service-factory'
|
||||||
import { createPaymentsProcessor } from './payments-processor-factory'
|
|
||||||
import { createSettings } from './settings-factory'
|
|
||||||
import { EventRepository } from '../repositories/event-repository'
|
|
||||||
import { IController } from '../@types/controllers'
|
import { IController } from '../@types/controllers'
|
||||||
import { InvoiceRepository } from '../repositories/invoice-repository'
|
|
||||||
import { PaymentsService } from '../services/payments-service'
|
|
||||||
import { UserRepository } from '../repositories/user-repository'
|
|
||||||
import { ZebedeeCallbackController } from '../controllers/callbacks/zebedee-callback-controller'
|
import { ZebedeeCallbackController } from '../controllers/callbacks/zebedee-callback-controller'
|
||||||
|
|
||||||
export const createZebedeeCallbackController = (): IController => {
|
export const createZebedeeCallbackController = (): IController => {
|
||||||
const dbClient = getMasterDbClient()
|
|
||||||
const rrDbClient = getReadReplicaDbClient()
|
|
||||||
const eventRepository = new EventRepository(dbClient, rrDbClient)
|
|
||||||
const invoiceRepotistory = new InvoiceRepository(dbClient)
|
|
||||||
const userRepository = new UserRepository(dbClient)
|
|
||||||
const paymentsProcessor = createPaymentsProcessor()
|
|
||||||
const paymentsService = new PaymentsService(
|
|
||||||
dbClient,
|
|
||||||
paymentsProcessor,
|
|
||||||
userRepository,
|
|
||||||
invoiceRepotistory,
|
|
||||||
eventRepository,
|
|
||||||
createSettings,
|
|
||||||
)
|
|
||||||
|
|
||||||
return new ZebedeeCallbackController(
|
return new ZebedeeCallbackController(
|
||||||
paymentsService,
|
createPaymentsService(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,20 @@
|
|||||||
|
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')
|
||||||
|
}
|
||||||
|
}
|
136
src/payments-processors/lnbits-payment-processor.ts
Normal file
136
src/payments-processors/lnbits-payment-processor.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients'
|
||||||
|
import { deriveFromSecret, hmacSha256 } from '../utils/secret'
|
||||||
|
import { Invoice, InvoiceStatus, InvoiceUnit } from '../@types/invoice'
|
||||||
|
|
||||||
|
import { AxiosInstance } from 'axios'
|
||||||
|
import { createLogger } from '../factories/logger-factory'
|
||||||
|
import { Factory } from '../@types/base'
|
||||||
|
import { Pubkey } from '../@types/base'
|
||||||
|
import { Settings } from '../@types/settings'
|
||||||
|
|
||||||
|
const debug = createLogger('lnbits-payments-processor')
|
||||||
|
|
||||||
|
export class LNbitsInvoice implements Invoice {
|
||||||
|
id: string
|
||||||
|
pubkey: Pubkey
|
||||||
|
bolt11: string
|
||||||
|
amountRequested: bigint
|
||||||
|
amountPaid?: bigint
|
||||||
|
unit: InvoiceUnit
|
||||||
|
status: InvoiceStatus
|
||||||
|
description: string
|
||||||
|
confirmedAt?: Date | null
|
||||||
|
expiresAt: Date | null
|
||||||
|
updatedAt: Date
|
||||||
|
createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LNbitsCreateInvoiceResponse implements CreateInvoiceResponse {
|
||||||
|
id: string
|
||||||
|
pubkey: string
|
||||||
|
bolt11: string
|
||||||
|
amountRequested: bigint
|
||||||
|
description: string
|
||||||
|
unit: InvoiceUnit
|
||||||
|
status: InvoiceStatus
|
||||||
|
expiresAt: Date | null
|
||||||
|
confirmedAt?: Date | null
|
||||||
|
createdAt: Date
|
||||||
|
rawResponse?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LNbitsPaymentsProcesor 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(`/api/v1/payments/${invoiceId}`, {
|
||||||
|
maxRedirects: 1,
|
||||||
|
})
|
||||||
|
const invoice = new LNbitsInvoice()
|
||||||
|
const data = response.data
|
||||||
|
invoice.id = data.details.payment_hash
|
||||||
|
invoice.pubkey = data.details.extra.internalId
|
||||||
|
invoice.bolt11 = data.details.bolt11
|
||||||
|
invoice.amountRequested = BigInt(Math.floor(data.details.amount / 1000))
|
||||||
|
if (data.paid) invoice.amountPaid = BigInt(Math.floor(data.details.amount / 1000))
|
||||||
|
invoice.unit = InvoiceUnit.SATS
|
||||||
|
invoice.status = data.paid?InvoiceStatus.COMPLETED:InvoiceStatus.PENDING
|
||||||
|
invoice.description = data.details.memo
|
||||||
|
invoice.confirmedAt = data.paid ? new Date(data.details.time * 1000) : null
|
||||||
|
invoice.expiresAt = new Date(data.details.expiry * 1000)
|
||||||
|
invoice.createdAt = new Date(data.details.time * 1000)
|
||||||
|
invoice.updatedAt = new Date()
|
||||||
|
return invoice
|
||||||
|
} 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: internalId,
|
||||||
|
} = request
|
||||||
|
|
||||||
|
const callbackURL = new URL(this.settings().paymentsProcessors?.lnbits?.callbackBaseURL)
|
||||||
|
const hmacExpiry = (Date.now() + (1 * 24 * 60 * 60 * 1000)).toString()
|
||||||
|
callbackURL.searchParams.set('hmac', hmacExpiry + ':' +
|
||||||
|
hmacSha256(deriveFromSecret('lnbits-callback-hmac-key'), hmacExpiry).toString('hex'))
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
amount: Number(amountMsats / 1000n),
|
||||||
|
memo: description,
|
||||||
|
extra: {
|
||||||
|
internalId,
|
||||||
|
},
|
||||||
|
out: false,
|
||||||
|
webhook: callbackURL.toString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debug('request body: %o', body)
|
||||||
|
const response = await this.httpClient.post('/api/v1/payments', body, {
|
||||||
|
maxRedirects: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
debug('response: %o', response.data)
|
||||||
|
|
||||||
|
const invoiceResponse = await this.httpClient.get(`/api/v1/payments/${encodeURIComponent(response.data.payment_hash)}`, {
|
||||||
|
maxRedirects: 1,
|
||||||
|
})
|
||||||
|
debug('invoice data response: %o', invoiceResponse.data)
|
||||||
|
|
||||||
|
const invoice = new LNbitsCreateInvoiceResponse()
|
||||||
|
const data = invoiceResponse.data
|
||||||
|
invoice.id = data.details.payment_hash
|
||||||
|
invoice.pubkey = data.details.extra.internalId
|
||||||
|
invoice.bolt11 = data.details.bolt11
|
||||||
|
invoice.amountRequested = BigInt(Math.floor(data.details.amount / 1000))
|
||||||
|
invoice.unit = InvoiceUnit.SATS
|
||||||
|
invoice.status = data.paid?InvoiceStatus.COMPLETED:InvoiceStatus.PENDING
|
||||||
|
invoice.description = data.details.memo
|
||||||
|
invoice.confirmedAt = null
|
||||||
|
invoice.expiresAt = new Date(data.details.expiry * 1000)
|
||||||
|
invoice.createdAt = new Date(data.details.time * 1000)
|
||||||
|
invoice.rawResponse = JSON.stringify({
|
||||||
|
invoiceResponse: invoiceResponse.data,
|
||||||
|
createData: response.data,
|
||||||
|
})
|
||||||
|
|
||||||
|
return invoice
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Unable to request invoice. Reason:', error.message)
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,18 +1,21 @@
|
|||||||
|
import { deriveFromSecret, hmacSha256 } from '../../utils/secret'
|
||||||
import { json, Router } from 'express'
|
import { json, Router } from 'express'
|
||||||
|
|
||||||
import { createLogger } from '../../factories/logger-factory'
|
import { createLogger } from '../../factories/logger-factory'
|
||||||
import { createSettings } from '../../factories/settings-factory'
|
import { createSettings } from '../../factories/settings-factory'
|
||||||
import { getRemoteAddress } from '../../utils/http'
|
import { getRemoteAddress } from '../../utils/http'
|
||||||
|
import { postLNbitsCallbackRequestHandler } from '../../handlers/request-handlers/post-lnbits-callback-request-handler'
|
||||||
import { postZebedeeCallbackRequestHandler } from '../../handlers/request-handlers/post-zebedee-callback-request-handler'
|
import { postZebedeeCallbackRequestHandler } from '../../handlers/request-handlers/post-zebedee-callback-request-handler'
|
||||||
|
|
||||||
const debug = createLogger('routes-callbacks')
|
const debug = createLogger('routes-callbacks')
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
router
|
router
|
||||||
.use((req, res, next) => {
|
.post('/zebedee', json(), (req, res) => {
|
||||||
const settings = createSettings()
|
const settings = createSettings()
|
||||||
const { ipWhitelist = [] } = settings.paymentsProcessors?.zebedee ?? {}
|
const { ipWhitelist = [] } = settings.paymentsProcessors?.zebedee ?? {}
|
||||||
const remoteAddress = getRemoteAddress(req, settings)
|
const remoteAddress = getRemoteAddress(req, settings)
|
||||||
|
const paymentProcessor = settings.payments?.processor ?? 'null'
|
||||||
|
|
||||||
if (ipWhitelist.length && !ipWhitelist.includes(remoteAddress)) {
|
if (ipWhitelist.length && !ipWhitelist.includes(remoteAddress)) {
|
||||||
debug('unauthorized request from %s to /callbacks/zebedee', remoteAddress)
|
debug('unauthorized request from %s to /callbacks/zebedee', remoteAddress)
|
||||||
@@ -22,9 +25,49 @@ router
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
postZebedeeCallbackRequestHandler(req, res)
|
||||||
|
})
|
||||||
|
.post('/lnbits', json(), (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
|
||||||
|
}
|
||||||
|
postLNbitsCallbackRequestHandler(req, res)
|
||||||
})
|
})
|
||||||
.post('/zebedee', json(), postZebedeeCallbackRequestHandler)
|
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { andThen, pipe } from 'ramda'
|
import { andThen, pipe } from 'ramda'
|
||||||
|
import { broadcastEvent, encryptKind4Event, getPublicKey, getRelayPrivateKey, identifyEvent, signEvent } from '../utils/event'
|
||||||
import { broadcastEvent, encryptKind4Event, getPrivateKeyFromSecret, getPublicKey, identifyEvent, signEvent } from '../utils/event'
|
|
||||||
import { DatabaseClient, Pubkey } from '../@types/base'
|
import { DatabaseClient, Pubkey } from '../@types/base'
|
||||||
import { FeeSchedule, Settings } from '../@types/settings'
|
import { FeeSchedule, Settings } from '../@types/settings'
|
||||||
import { IEventRepository, IInvoiceRepository, IUserRepository } from '../@types/repositories'
|
import { IEventRepository, IInvoiceRepository, IUserRepository } from '../@types/repositories'
|
||||||
import { Invoice, InvoiceStatus, InvoiceUnit } from '../@types/invoice'
|
import { Invoice, InvoiceStatus, InvoiceUnit } from '../@types/invoice'
|
||||||
|
|
||||||
import { createLogger } from '../factories/logger-factory'
|
import { createLogger } from '../factories/logger-factory'
|
||||||
import { EventKinds } from '../constants/base'
|
import { EventKinds } from '../constants/base'
|
||||||
import { IPaymentsProcessor } from '../@types/clients'
|
import { IPaymentsProcessor } from '../@types/clients'
|
||||||
@@ -159,6 +159,14 @@ export class PaymentsService implements IPaymentsService {
|
|||||||
|
|
||||||
const currentSettings = this.settings()
|
const currentSettings = this.settings()
|
||||||
|
|
||||||
|
let amountPaidMsat = invoice.amountPaid
|
||||||
|
|
||||||
|
if (invoice.unit === InvoiceUnit.SATS) {
|
||||||
|
amountPaidMsat *= 1000n
|
||||||
|
} else if (invoice.unit === InvoiceUnit.BTC) {
|
||||||
|
amountPaidMsat *= 1000n * 100000000n
|
||||||
|
}
|
||||||
|
|
||||||
const isApplicableFee = (feeSchedule: FeeSchedule) => feeSchedule.enabled
|
const isApplicableFee = (feeSchedule: FeeSchedule) => feeSchedule.enabled
|
||||||
&& !feeSchedule.whitelists?.pubkeys?.some((prefix) => invoice.pubkey.startsWith(prefix))
|
&& !feeSchedule.whitelists?.pubkeys?.some((prefix) => invoice.pubkey.startsWith(prefix))
|
||||||
const admissionFeeSchedules = currentSettings.payments?.feeSchedules?.admission ?? []
|
const admissionFeeSchedules = currentSettings.payments?.feeSchedules?.admission ?? []
|
||||||
@@ -169,7 +177,7 @@ export class PaymentsService implements IPaymentsService {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
admissionFeeAmount > 0n
|
admissionFeeAmount > 0n
|
||||||
&& invoice.amountPaid >= admissionFeeAmount
|
&& amountPaidMsat >= admissionFeeAmount
|
||||||
) {
|
) {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
// TODO: Convert to stored func
|
// TODO: Convert to stored func
|
||||||
@@ -204,7 +212,7 @@ export class PaymentsService implements IPaymentsService {
|
|||||||
},
|
},
|
||||||
} = currentSettings
|
} = currentSettings
|
||||||
|
|
||||||
const relayPrivkey = getPrivateKeyFromSecret(process.env.SECRET as string)(relayUrl)
|
const relayPrivkey = getRelayPrivateKey(relayUrl)
|
||||||
const relayPubkey = getPublicKey(relayPrivkey)
|
const relayPubkey = getPublicKey(relayPrivkey)
|
||||||
|
|
||||||
let unit: string = invoice.unit
|
let unit: string = invoice.unit
|
||||||
@@ -266,7 +274,7 @@ ${invoice.bolt11}`,
|
|||||||
},
|
},
|
||||||
} = currentSettings
|
} = currentSettings
|
||||||
|
|
||||||
const relayPrivkey = getPrivateKeyFromSecret(process.env.SECRET as string)(relayUrl)
|
const relayPrivkey = getRelayPrivateKey(relayUrl)
|
||||||
const relayPubkey = getPublicKey(relayPrivkey)
|
const relayPubkey = getPublicKey(relayPrivkey)
|
||||||
|
|
||||||
let unit: string = invoice.unit
|
let unit: string = invoice.unit
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
import * as secp256k1 from '@noble/secp256k1'
|
import * as secp256k1 from '@noble/secp256k1'
|
||||||
import { applySpec, converge, curry, mergeLeft, nth, omit, pipe, prop, reduceBy } from 'ramda'
|
|
||||||
import { createCipheriv, createHmac, getRandomValues } from 'crypto'
|
|
||||||
import cluster from 'cluster'
|
|
||||||
|
|
||||||
|
import { applySpec, converge, curry, mergeLeft, nth, omit, pipe, prop, reduceBy } from 'ramda'
|
||||||
import { CanonicalEvent, DBEvent, Event, UnidentifiedEvent, UnsignedEvent } from '../@types/event'
|
import { CanonicalEvent, DBEvent, Event, UnidentifiedEvent, UnsignedEvent } from '../@types/event'
|
||||||
|
import { createCipheriv, getRandomValues } from 'crypto'
|
||||||
import { EventId, Pubkey, Tag } from '../@types/base'
|
import { EventId, Pubkey, Tag } from '../@types/base'
|
||||||
import { EventKinds, EventTags } from '../constants/base'
|
import { EventKinds, EventTags } from '../constants/base'
|
||||||
|
|
||||||
|
import cluster from 'cluster'
|
||||||
|
import { deriveFromSecret } from './secret'
|
||||||
import { EventKindsRange } from '../@types/settings'
|
import { EventKindsRange } from '../@types/settings'
|
||||||
import { fromBuffer } from './transform'
|
import { fromBuffer } from './transform'
|
||||||
import { getLeadingZeroBits } from './proof-of-work'
|
import { getLeadingZeroBits } from './proof-of-work'
|
||||||
@@ -35,9 +37,9 @@ export const toNostrEvent: (event: DBEvent) => Event = applySpec({
|
|||||||
|
|
||||||
export const isEventKindOrRangeMatch = ({ kind }: Event) =>
|
export const isEventKindOrRangeMatch = ({ kind }: Event) =>
|
||||||
(item: EventKinds | EventKindsRange) =>
|
(item: EventKinds | EventKindsRange) =>
|
||||||
typeof item === 'number'
|
typeof item === 'number'
|
||||||
? item === kind
|
? item === kind
|
||||||
: kind >= item[0] && kind <= item[1]
|
: kind >= item[0] && kind <= item[1]
|
||||||
|
|
||||||
export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Event): boolean => {
|
export const isEventMatchingFilter = (filter: SubscriptionFilter) => (event: Event): boolean => {
|
||||||
const startsWith = (input: string) => (prefix: string) => input.startsWith(prefix)
|
const startsWith = (input: string) => (prefix: string) => input.startsWith(prefix)
|
||||||
@@ -179,17 +181,12 @@ export const identifyEvent = async (event: UnidentifiedEvent): Promise<UnsignedE
|
|||||||
return { ...event, id }
|
return { ...event, id }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPrivateKeyFromSecret =
|
export function getRelayPrivateKey(relayUrl: string): string {
|
||||||
(secret: string) => (data: string | Buffer): string => {
|
|
||||||
if (process.env.RELAY_PRIVATE_KEY) {
|
if (process.env.RELAY_PRIVATE_KEY) {
|
||||||
return process.env.RELAY_PRIVATE_KEY
|
return process.env.RELAY_PRIVATE_KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
const hmac = createHmac('sha256', secret)
|
return deriveFromSecret(relayUrl).toString('hex')
|
||||||
|
|
||||||
hmac.update(typeof data === 'string' ? Buffer.from(data) : data)
|
|
||||||
|
|
||||||
return hmac.digest().toString('hex')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPublicKey = (privkey: string | Buffer) => Buffer.from(secp256k1.getPublicKey(privkey, true)).subarray(1).toString('hex')
|
export const getPublicKey = (privkey: string | Buffer) => Buffer.from(secp256k1.getPublicKey(privkey, true)).subarray(1).toString('hex')
|
||||||
@@ -204,7 +201,7 @@ export const encryptKind4Event = (
|
|||||||
receiverPubkey: Pubkey,
|
receiverPubkey: Pubkey,
|
||||||
) => (event: UnsignedEvent): UnsignedEvent => {
|
) => (event: UnsignedEvent): UnsignedEvent => {
|
||||||
const key = secp256k1
|
const key = secp256k1
|
||||||
.getSharedSecret(senderPrivkey,`02${receiverPubkey}`, true)
|
.getSharedSecret(senderPrivkey, `02${receiverPubkey}`, true)
|
||||||
.subarray(1)
|
.subarray(1)
|
||||||
|
|
||||||
const iv = getRandomValues(new Uint8Array(16))
|
const iv = getRandomValues(new Uint8Array(16))
|
||||||
@@ -289,7 +286,7 @@ export const getEventExpiration = (event: Event): number | undefined => {
|
|||||||
const expirationTime = Number(rawExpirationTime)
|
const expirationTime = Number(rawExpirationTime)
|
||||||
if ((Number.isSafeInteger(expirationTime) && Math.log10(expirationTime))) {
|
if ((Number.isSafeInteger(expirationTime) && Math.log10(expirationTime))) {
|
||||||
return expirationTime
|
return expirationTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getEventProofOfWork = (eventId: EventId): number => {
|
export const getEventProofOfWork = (eventId: EventId): number => {
|
||||||
|
11
src/utils/secret.ts
Normal file
11
src/utils/secret.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createHmac } from 'crypto'
|
||||||
|
|
||||||
|
export function deriveFromSecret(purpose: string | Buffer): Buffer {
|
||||||
|
return hmacSha256(process.env.SECRET as string, purpose)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hmacSha256(secret: string | Buffer, data: string | Buffer): Buffer {
|
||||||
|
return createHmac('sha256', secret)
|
||||||
|
.update(data)
|
||||||
|
.digest()
|
||||||
|
}
|
Reference in New Issue
Block a user