Skip to content

Commit

Permalink
feat: add new lnurl processor
Browse files Browse the repository at this point in the history
  • Loading branch information
im-adithya committed Feb 24, 2023
1 parent d803428 commit 6bd859f
Show file tree
Hide file tree
Showing 18 changed files with 166 additions and 56 deletions.
9 changes: 9 additions & 0 deletions migrations/20230213103904_add_verify_url_to_invoices_table.js
Original file line number Diff line number Diff line change
@@ -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')
})
}
16 changes: 9 additions & 7 deletions resources/invoices.html
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ <h2 class="text-danger">Invoice expired!</h2>
if (event.pubkey === relayPubkey) {
paid = true

clearTimeout(timeout)
if (expiresAt) clearTimeout(timeout)

hide('pending')
show('paid')
Expand Down Expand Up @@ -213,12 +213,14 @@ <h2 class="text-danger">Invoice expired!</h2>
}
}

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}`,
Expand Down
5 changes: 3 additions & 2 deletions src/@types/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface CreateInvoiceResponse {
confirmedAt?: Date | null
createdAt: Date
rawResponse?: string
verifyURL?: string
}

export interface CreateInvoiceRequest {
Expand All @@ -20,9 +21,9 @@ export interface CreateInvoiceRequest {
requestId?: string
}

export type GetInvoiceResponse = Invoice
export type GetInvoiceResponse = Partial<Invoice>

export interface IPaymentsProcessor {
createInvoice(request: CreateInvoiceRequest): Promise<CreateInvoiceResponse>
getInvoice(invoiceId: string): Promise<GetInvoiceResponse>
getInvoice(invoice: Invoice): Promise<GetInvoiceResponse>
}
2 changes: 2 additions & 0 deletions src/@types/invoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface Invoice {
expiresAt: Date | null
updatedAt: Date
createdAt: Date
verifyURL?: string
}

export interface DBInvoice {
Expand All @@ -39,4 +40,5 @@ export interface DBInvoice {
expires_at: Date
updated_at: Date
created_at: Date
verify_url: string
}
4 changes: 2 additions & 2 deletions src/@types/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { Invoice } from './invoice'
import { Pubkey } from './base'

export interface IPaymentsService {
getInvoiceFromPaymentsProcessor(invoiceId: string): Promise<Invoice>
getInvoiceFromPaymentsProcessor(invoice: Invoice): Promise<Partial<Invoice>>
createInvoice(
pubkey: Pubkey,
amount: bigint,
description: string,
): Promise<Invoice>
updateInvoice(invoice: Invoice): Promise<void>
updateInvoice(invoice: Partial<Invoice>): Promise<void>
confirmInvoice(
invoice: Pick<Invoice, 'id' | 'amountPaid' | 'confirmedAt'>,
): Promise<void>
Expand Down
5 changes: 5 additions & 0 deletions src/@types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ export interface Payments {
feeSchedules: FeeSchedules
}

export interface LnurlPaymentsProcessor {
invoiceURL: string
}

export interface ZebedeePaymentsProcessor {
baseURL: string
callbackBaseURL: string
Expand All @@ -154,6 +158,7 @@ export interface LNbitsPaymentProcessor {
}

export interface PaymentsProcessors {
lnurl?: LnurlPaymentsProcessor,
zebedee?: ZebedeePaymentsProcessor
lnbits?: LNbitsPaymentProcessor
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/maintenance-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ 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)
Expand Down
1 change: 1 addition & 0 deletions src/constants/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export enum EventTags {
}

export enum PaymentsProcessors {
LNURL = 'lnurl',
ZEBEDEE = 'zebedee',
LNBITS = 'lnbits',
}
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/invoices/post-invoice-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
16 changes: 16 additions & 0 deletions src/factories/payments-processor-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -44,6 +45,19 @@ const getLNbitsAxiosConfig = (settings: Settings): CreateAxiosDefaults<any> => {
}
}

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) {
Expand Down Expand Up @@ -98,6 +112,8 @@ export const createPaymentsProcessor = (): IPaymentsProcessor => {
}

switch (settings.payments?.processor) {
case 'lnurl':
return createLnurlPaymentsProcessor(settings)
case 'zebedee':
return createZebedeePaymentsProcessor(settings)
case 'lnbits':
Expand Down
44 changes: 22 additions & 22 deletions src/payments-processors/lnbits-payment-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,29 +45,29 @@ export class LNbitsPaymentsProcesor implements IPaymentsProcessor {
private settings: Factory<Settings>
) {}

public async getInvoice(invoiceId: string): Promise<GetInvoiceResponse> {
debug('get invoice: %s', invoiceId)
public async getInvoice(invoice: Invoice): Promise<GetInvoiceResponse> {
debug('get invoice: %s', invoice.id)
try {
const response = await this.httpClient.get(`/api/v1/payments/${invoiceId}`, {
const response = await this.httpClient.get(`/api/v1/payments/${invoice.id}`, {
maxRedirects: 1,
})
const invoice = new LNbitsInvoice()
const invoiceResult = 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
invoiceResult.id = data.details.payment_hash
invoiceResult.pubkey = data.details.extra.internalId
invoiceResult.bolt11 = data.details.bolt11
invoiceResult.amountRequested = BigInt(Math.floor(data.details.amount / 1000))
if (data.paid) invoiceResult.amountPaid = BigInt(Math.floor(data.details.amount / 1000))
invoiceResult.unit = InvoiceUnit.SATS
invoiceResult.status = data.paid?InvoiceStatus.COMPLETED:InvoiceStatus.PENDING
invoiceResult.description = data.details.memo
invoiceResult.confirmedAt = data.paid ? new Date(data.details.time * 1000) : null
invoiceResult.expiresAt = new Date(data.details.expiry * 1000)
invoiceResult.createdAt = new Date(data.details.time * 1000)
invoiceResult.updatedAt = new Date()
return invoiceResult
} catch (error) {
console.error(`Unable to get invoice ${invoiceId}. Reason:`, error)
console.error(`Unable to get invoice ${invoice.id}. Reason:`, error)

throw error
}
Expand Down Expand Up @@ -104,13 +104,13 @@ export class LNbitsPaymentsProcesor implements IPaymentsProcessor {

debug('response: %o', response.data)

const invoiceResponse = await this.httpClient.get(`/api/v1/payments/${encodeURIComponent(response.data.payment_hash)}`, {
const invoiceResult = await this.httpClient.get(`/api/v1/payments/${encodeURIComponent(response.data.payment_hash)}`, {
maxRedirects: 1,
})
debug('invoice data response: %o', invoiceResponse.data)
debug('invoice data response: %o', invoiceResult.data)

const invoice = new LNbitsCreateInvoiceResponse()
const data = invoiceResponse.data
const data = invoiceResult.data
invoice.id = data.details.payment_hash
invoice.pubkey = data.details.extra.internalId
invoice.bolt11 = data.details.bolt11
Expand All @@ -122,7 +122,7 @@ export class LNbitsPaymentsProcesor implements IPaymentsProcessor {
invoice.expiresAt = new Date(data.details.expiry * 1000)
invoice.createdAt = new Date(data.details.time * 1000)
invoice.rawResponse = JSON.stringify({
invoiceResponse: invoiceResponse.data,
invoiceResult: invoiceResult.data,
createData: response.data,
})

Expand Down
69 changes: 69 additions & 0 deletions src/payments-processors/lnurl-payments-processor.ts
Original file line number Diff line number Diff line change
@@ -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('alby-payments-processor')

export class LnurlPaymentsProcesor implements IPaymentsProcessor {
public constructor(
private httpClient: AxiosInstance,
private settings: Factory<Settings>
) {}

public async getInvoice(invoice: Invoice): Promise<GetInvoiceResponse> {
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<any> {
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=${requestId}`)

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
}
}
}
8 changes: 5 additions & 3 deletions src/payments-processors/null-payments-processor.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients'
import { InvoiceStatus, InvoiceUnit } from '../@types/invoice'
import { Invoice, InvoiceStatus, InvoiceUnit } from '../@types/invoice'

export class NullPaymentsProcessor implements IPaymentsProcessor {
public async getInvoice(invoiceId: string): Promise<GetInvoiceResponse> {
public async getInvoice(invoice: Invoice): Promise<GetInvoiceResponse> {
const date = new Date()
return {
id: invoiceId,
id: invoice.id,
pubkey: '',
bolt11: '',
description: '',
Expand All @@ -16,6 +16,7 @@ export class NullPaymentsProcessor implements IPaymentsProcessor {
confirmedAt: null,
createdAt: date,
updatedAt: date,
verifyURL: '',
}
}

Expand All @@ -32,6 +33,7 @@ export class NullPaymentsProcessor implements IPaymentsProcessor {
rawResponse: '',
confirmedAt: null,
createdAt: new Date(),
verifyURL: '',
}
}
}
6 changes: 3 additions & 3 deletions src/payments-processors/payments-procesor.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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 {
public constructor(
private readonly processor: IPaymentsProcessor
) {}

public async getInvoice(invoiceId: string): Promise<Invoice> {
return this.processor.getInvoice(invoiceId)
public async getInvoice(invoice: Invoice): Promise<GetInvoiceResponse> {
return this.processor.getInvoice(invoice)
}

public async createInvoice(request: CreateInvoiceRequest): Promise<CreateInvoiceResponse> {
Expand Down
9 changes: 5 additions & 4 deletions src/payments-processors/zebedee-payments-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Factory } from '../@types/base'
import { CreateInvoiceRequest, CreateInvoiceResponse, GetInvoiceResponse, IPaymentsProcessor } from '../@types/clients'
import { createLogger } from '../factories/logger-factory'
import { fromZebedeeInvoice } from '../utils/transform'
import { Invoice } from '../@types/invoice'
import { Settings } from '../@types/settings'

const debug = createLogger('zebedee-payments-processor')
Expand All @@ -14,17 +15,17 @@ export class ZebedeePaymentsProcesor implements IPaymentsProcessor {
private settings: Factory<Settings>
) {}

public async getInvoice(invoiceId: string): Promise<GetInvoiceResponse> {
debug('get invoice: %s', invoiceId)
public async getInvoice(invoice: Invoice): Promise<GetInvoiceResponse> {
debug('get invoice: %s', invoice.id)

try {
const response = await this.httpClient.get(`/v0/charges/${invoiceId}`, {
const response = await this.httpClient.get(`/v0/charges/${invoice.id}`, {
maxRedirects: 1,
})

return fromZebedeeInvoice(response.data.data)
} catch (error) {
console.error(`Unable to get invoice ${invoiceId}. Reason:`, error)
console.error(`Unable to get invoice ${invoice.id}. Reason:`, error)

throw error
}
Expand Down
Loading

0 comments on commit 6bd859f

Please sign in to comment.