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({