diff --git a/migrations/20230213103904_add_verify_url_to_invoices_table.js b/migrations/20230213103904_add_verify_url_to_invoices_table.js new file mode 100644 index 00000000..7ed03bf7 --- /dev/null +++ b/migrations/20230213103904_add_verify_url_to_invoices_table.js @@ -0,0 +1,9 @@ +exports.up = function (knex) { + return knex.raw('ALTER TABLE invoices ADD verify_url TEXT;') +} + +exports.down = function (knex) { + return knex.schema.alterTable('invoices', function (table) { + table.dropColumn('verify_url') + }) +} diff --git a/resources/default-settings.yaml b/resources/default-settings.yaml index 4f76cecf..a27ddd32 100755 --- a/resources/default-settings.yaml +++ b/resources/default-settings.yaml @@ -28,6 +28,8 @@ paymentsProcessors: lnbits: baseURL: https://lnbits.your-domain.com/ callbackBaseURL: https://nostream.your-domain.com/callbacks/lnbits + lnurl: + invoiceURL: https://getalby.com/lnurlp/your-username network: maxPayloadSize: 524288 # Comment the next line if using CloudFlare proxy diff --git a/resources/invoices.html b/resources/invoices.html index 71d1c003..48ceca3c 100644 --- a/resources/invoices.html +++ b/resources/invoices.html @@ -168,7 +168,7 @@

Invoice expired!

if (event.pubkey === relayPubkey) { paid = true - clearTimeout(timeout) + if (expiresAt) clearTimeout(timeout) hide('pending') show('paid') @@ -213,12 +213,14 @@

Invoice expired!

} } - const expiry = (new Date(expiresAt).getTime() - new Date().getTime()) - console.log('expiry at', expiresAt, Math.floor(expiry / 1000)) - timeout = setTimeout(() => { - hide('pending') - show('expired') - }, expiry) + if (expiresAt) { + const expiry = (new Date(expiresAt).getTime() - new Date().getTime()) + console.log('expiry at', expiresAt, Math.floor(expiry / 1000)) + timeout = setTimeout(() => { + hide('pending') + show('expired') + }, expiry) + } new QRCode(document.getElementById("invoice"), { text: `lightning:${invoice}`, diff --git a/src/@types/clients.ts b/src/@types/clients.ts index 00fbc1a1..8f7ad151 100644 --- a/src/@types/clients.ts +++ b/src/@types/clients.ts @@ -12,6 +12,7 @@ export interface CreateInvoiceResponse { confirmedAt?: Date | null createdAt: Date rawResponse?: string + verifyURL?: string } export interface CreateInvoiceRequest { @@ -20,9 +21,9 @@ export interface CreateInvoiceRequest { requestId?: string } -export type GetInvoiceResponse = Invoice +export type GetInvoiceResponse = Partial export interface IPaymentsProcessor { createInvoice(request: CreateInvoiceRequest): Promise - getInvoice(invoiceId: string): Promise + getInvoice(invoice: string | Invoice): Promise } diff --git a/src/@types/invoice.ts b/src/@types/invoice.ts index dbf9ec69..14b472bb 100644 --- a/src/@types/invoice.ts +++ b/src/@types/invoice.ts @@ -24,6 +24,7 @@ export interface Invoice { expiresAt: Date | null updatedAt: Date createdAt: Date + verifyURL?: string } export interface DBInvoice { @@ -39,4 +40,5 @@ export interface DBInvoice { expires_at: Date updated_at: Date created_at: Date + verify_url: string } diff --git a/src/@types/services.ts b/src/@types/services.ts index 4d3fb5d2..8058f5ae 100644 --- a/src/@types/services.ts +++ b/src/@types/services.ts @@ -2,13 +2,14 @@ import { Invoice } from './invoice' import { Pubkey } from './base' export interface IPaymentsService { - getInvoiceFromPaymentsProcessor(invoiceId: string): Promise + getInvoiceFromPaymentsProcessor(invoice: string | Invoice): Promise> createInvoice( pubkey: Pubkey, amount: bigint, description: string, ): Promise - updateInvoice(invoice: Invoice): Promise + updateInvoice(invoice: Partial): Promise + updateInvoiceStatus(invoice: Partial): Promise confirmInvoice( invoice: Pick, ): Promise diff --git a/src/@types/settings.ts b/src/@types/settings.ts index da59af30..a0ddec7c 100644 --- a/src/@types/settings.ts +++ b/src/@types/settings.ts @@ -142,6 +142,10 @@ export interface Payments { feeSchedules: FeeSchedules } +export interface LnurlPaymentsProcessor { + invoiceURL: string +} + export interface ZebedeePaymentsProcessor { baseURL: string callbackBaseURL: string @@ -154,6 +158,7 @@ export interface LNbitsPaymentProcessor { } export interface PaymentsProcessors { + lnurl?: LnurlPaymentsProcessor, zebedee?: ZebedeePaymentsProcessor lnbits?: LNbitsPaymentProcessor } diff --git a/src/app/maintenance-worker.ts b/src/app/maintenance-worker.ts index bc7ddc50..4570a072 100644 --- a/src/app/maintenance-worker.ts +++ b/src/app/maintenance-worker.ts @@ -48,10 +48,10 @@ export class MaintenanceWorker implements IRunnable { debug('invoice %s: %o', invoice.id, invoice) try { debug('getting invoice %s from payment processor', invoice.id) - const updatedInvoice = await this.paymentsService.getInvoiceFromPaymentsProcessor(invoice.id) + const updatedInvoice = await this.paymentsService.getInvoiceFromPaymentsProcessor(invoice) await delay() - debug('updating invoice %s: %o', invoice.id, invoice) - await this.paymentsService.updateInvoice(updatedInvoice) + debug('updating invoice status %s: %o', invoice.id, invoice) + await this.paymentsService.updateInvoiceStatus(updatedInvoice) if ( invoice.status !== updatedInvoice.status diff --git a/src/constants/base.ts b/src/constants/base.ts index 569da881..e23f4bb6 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -40,6 +40,7 @@ export enum EventTags { } export enum PaymentsProcessors { + LNURL = 'lnurl', ZEBEDEE = 'zebedee', LNBITS = 'lnbits', } diff --git a/src/controllers/callbacks/lnbits-callback-controller.ts b/src/controllers/callbacks/lnbits-callback-controller.ts index a4e3a480..54fc141c 100644 --- a/src/controllers/callbacks/lnbits-callback-controller.ts +++ b/src/controllers/callbacks/lnbits-callback-controller.ts @@ -1,9 +1,9 @@ import { Request, Response } from 'express' +import { Invoice, InvoiceStatus } from '../../@types/invoice' 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') @@ -72,8 +72,8 @@ export class LNbitsCallbackController implements IController { invoice.amountPaid = invoice.amountRequested try { - await this.paymentsService.confirmInvoice(invoice) - await this.paymentsService.sendInvoiceUpdateNotification(invoice) + await this.paymentsService.confirmInvoice(invoice as Invoice) + await this.paymentsService.sendInvoiceUpdateNotification(invoice as Invoice) } catch (error) { console.error(`Unable to confirm invoice ${invoice.id}`, error) diff --git a/src/controllers/invoices/post-invoice-controller.ts b/src/controllers/invoices/post-invoice-controller.ts index b4817e59..646ba85e 100644 --- a/src/controllers/invoices/post-invoice-controller.ts +++ b/src/controllers/invoices/post-invoice-controller.ts @@ -165,7 +165,7 @@ export class PostInvoiceController implements IController { relay_url: relayUrl, pubkey, relay_pubkey: relayPubkey, - expires_at: invoice.expiresAt?.toISOString(), + expires_at: invoice.expiresAt?.toISOString() ?? '', invoice: invoice.bolt11, amount: amount / 1000n, } diff --git a/src/factories/payments-processor-factory.ts b/src/factories/payments-processor-factory.ts index e588eeac..a24c7500 100644 --- a/src/factories/payments-processor-factory.ts +++ b/src/factories/payments-processor-factory.ts @@ -5,6 +5,7 @@ import { createLogger } from './logger-factory' import { createSettings } from './settings-factory' import { IPaymentsProcessor } from '../@types/clients' import { LNbitsPaymentsProcesor } from '../payments-processors/lnbits-payment-processor' +import { LnurlPaymentsProcesor } from '../payments-processors/lnurl-payments-processor' import { NullPaymentsProcessor } from '../payments-processors/null-payments-processor' import { PaymentsProcessor } from '../payments-processors/payments-procesor' import { Settings } from '../@types/settings' @@ -44,6 +45,19 @@ const getLNbitsAxiosConfig = (settings: Settings): CreateAxiosDefaults => { } } +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() + + const app = new LnurlPaymentsProcesor(client, createSettings) + + return new PaymentsProcessor(app) +} + 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) { @@ -98,6 +112,8 @@ export const createPaymentsProcessor = (): IPaymentsProcessor => { } switch (settings.payments?.processor) { + case 'lnurl': + return createLnurlPaymentsProcessor(settings) case 'zebedee': return createZebedeePaymentsProcessor(settings) case 'lnbits': diff --git a/src/payments-processors/lnurl-payments-processor.ts b/src/payments-processors/lnurl-payments-processor.ts new file mode 100644 index 00000000..7c0b275c --- /dev/null +++ b/src/payments-processors/lnurl-payments-processor.ts @@ -0,0 +1,69 @@ +import { AxiosInstance } from 'axios' +import { Factory } from '../@types/base' + +import { CreateInvoiceRequest, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients' +import { Invoice, InvoiceStatus, InvoiceUnit } from '../@types/invoice' +import { createLogger } from '../factories/logger-factory' +import { randomUUID } from 'crypto' +import { Settings } from '../@types/settings' + +const debug = createLogger('lnurl-payments-processor') + +export class LnurlPaymentsProcesor implements IPaymentsProcessor { + public constructor( + private httpClient: AxiosInstance, + private settings: Factory + ) {} + + public async getInvoice(invoice: Invoice): Promise { + debug('get invoice: %s', invoice.id) + + try { + const response = await this.httpClient.get(invoice.verifyURL) + + return { + id: invoice.id, + status: response.data.settled ? InvoiceStatus['COMPLETED'] : InvoiceStatus['PENDING'], + } + } catch (error) { + console.error(`Unable to get invoice ${invoice.id}. Reason:`, error) + + throw error + } + } + + public async createInvoice(request: CreateInvoiceRequest): Promise { + debug('create invoice: %o', request) + const { + amount: amountMsats, + description, + requestId, + } = request + + try { + const response = await this.httpClient.get(`${this.settings().paymentsProcessors?.lnurl?.invoiceURL}/callback?amount=${amountMsats}&comment=${description}`) + + const result = { + id: randomUUID(), + pubkey: requestId, + bolt11: response.data.pr, + amountRequested: amountMsats, + description, + unit: InvoiceUnit.MSATS, + status: InvoiceStatus.PENDING, + expiresAt: null, + confirmedAt: null, + createdAt: new Date(), + verifyURL: response.data.verify, + } + + debug('result: %o', result) + + return result + } catch (error) { + console.error('Unable to request invoice. Reason:', error.message) + + throw error + } + } +} diff --git a/src/payments-processors/null-payments-processor.ts b/src/payments-processors/null-payments-processor.ts index 69f9f6df..9e5b4a9f 100644 --- a/src/payments-processors/null-payments-processor.ts +++ b/src/payments-processors/null-payments-processor.ts @@ -16,6 +16,7 @@ export class NullPaymentsProcessor implements IPaymentsProcessor { confirmedAt: null, createdAt: date, updatedAt: date, + verifyURL: '', } } @@ -32,6 +33,7 @@ export class NullPaymentsProcessor implements IPaymentsProcessor { rawResponse: '', confirmedAt: null, createdAt: new Date(), + verifyURL: '', } } } diff --git a/src/payments-processors/payments-procesor.ts b/src/payments-processors/payments-procesor.ts index ad64f489..22c41ef3 100644 --- a/src/payments-processors/payments-procesor.ts +++ b/src/payments-processors/payments-procesor.ts @@ -1,4 +1,4 @@ -import { CreateInvoiceRequest, CreateInvoiceResponse, IPaymentsProcessor } from '../@types/clients' +import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients' import { Invoice } from '../@types/invoice' export class PaymentsProcessor implements IPaymentsProcessor { @@ -6,8 +6,8 @@ export class PaymentsProcessor implements IPaymentsProcessor { private readonly processor: IPaymentsProcessor ) {} - public async getInvoice(invoiceId: string): Promise { - return this.processor.getInvoice(invoiceId) + public async getInvoice(invoice: string | Invoice): Promise { + return this.processor.getInvoice(invoice) } public async createInvoice(request: CreateInvoiceRequest): Promise { diff --git a/src/repositories/invoice-repository.ts b/src/repositories/invoice-repository.ts index e493c425..381457a5 100644 --- a/src/repositories/invoice-repository.ts +++ b/src/repositories/invoice-repository.ts @@ -3,7 +3,6 @@ import { applySpec, ifElse, is, - isNil, omit, pipe, prop, @@ -92,17 +91,10 @@ export class InvoiceRepository implements IInvoiceRepository { status: prop('status'), description: prop('description'), // confirmed_at: prop('confirmedAt'), - expires_at: ifElse( - propSatisfies(isNil, 'expiresAt'), - always(undefined), - prop('expiresAt'), - ), + expires_at: prop('expiresAt'), updated_at: always(new Date()), - created_at: ifElse( - propSatisfies(isNil, 'createdAt'), - always(undefined), - prop('createdAt'), - ), + created_at: prop('createdAt'), + verify_url: prop('verifyURL'), })(invoice) debug('row: %o', row) @@ -120,6 +112,7 @@ export class InvoiceRepository implements IInvoiceRepository { 'description', 'expires_at', 'created_at', + 'verify_url', ])(row) ) diff --git a/src/services/payments-service.ts b/src/services/payments-service.ts index e7c52273..7e03e067 100644 --- a/src/services/payments-service.ts +++ b/src/services/payments-service.ts @@ -36,10 +36,12 @@ export class PaymentsService implements IPaymentsService { } } - public async getInvoiceFromPaymentsProcessor(invoiceId: string): Promise { - debug('get invoice %s from payment processor', invoiceId) + public async getInvoiceFromPaymentsProcessor(invoice: Invoice): Promise> { + debug('get invoice %s from payment processor', invoice.id) try { - return await this.paymentsProcessor.getInvoice(invoiceId) + return await this.paymentsProcessor.getInvoice( + this.settings().payments?.processor === 'lnurl' ? invoice : invoice.id + ) } catch (error) { console.log('Unable to get invoice from payments processor. Reason:', error) @@ -82,6 +84,7 @@ export class PaymentsService implements IPaymentsService { expiresAt: invoiceResponse.expiresAt, updatedAt: date, createdAt: date, + verifyURL: invoiceResponse.verifyURL, }, transaction.transaction, ) @@ -99,6 +102,7 @@ export class PaymentsService implements IPaymentsService { expiresAt: invoiceResponse.expiresAt, updatedAt: date, createdAt: invoiceResponse.createdAt, + verifyURL: invoiceResponse.verifyURL, } } catch (error) { await transaction.rollback() @@ -108,7 +112,7 @@ export class PaymentsService implements IPaymentsService { } } - public async updateInvoice(invoice: Invoice): Promise { + public async updateInvoice(invoice: Partial): Promise { debug('update invoice %s: %o', invoice.id, invoice) try { await this.invoiceRepository.upsert({ @@ -129,6 +133,21 @@ export class PaymentsService implements IPaymentsService { } } + public async updateInvoiceStatus(invoice: Partial): Promise { + debug('update invoice %s: %o', invoice.id, invoice) + try { + const fullInvoice = await this.invoiceRepository.findById(invoice.id) + await this.invoiceRepository.upsert({ + ...fullInvoice, + status: invoice.status, + updatedAt: new Date(), + }) + } catch (error) { + console.error('Unable to update invoice. Reason:', error) + throw error + } + } + public async confirmInvoice( invoice: Invoice, ): Promise { diff --git a/src/utils/transform.ts b/src/utils/transform.ts index a5b8ccf9..81b04a69 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -31,6 +31,7 @@ export const fromDBInvoice = applySpec({ expiresAt: prop('expires_at'), updatedAt: prop('updated_at'), createdAt: prop('created_at'), + verifyURL: prop('verify_url'), }) export const fromDBUser = applySpec({