Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add LNURL processor #202

Merged
merged 10 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
im-adithya marked this conversation as resolved.
Show resolved Hide resolved
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() || '',
im-adithya marked this conversation as resolved.
Show resolved Hide resolved
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)}`, {
im-adithya marked this conversation as resolved.
Show resolved Hide resolved
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')
cameri marked this conversation as resolved.
Show resolved Hide resolved

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}`)
im-adithya marked this conversation as resolved.
Show resolved Hide resolved

const result = {
id: randomUUID(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

although this works, I'm concerned we won't be able to look up this invoice on your LNURL provider. Should we instead use UUID v5 which uses namespaces and can be used to generate deterministic UUIDs? That way we can pass the invoice description and always get the same UUID back.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't have a uuid string coming from the response: https://getalby.com/lnurlp/adithya/callback?amount=10000
So we thought of using a random UUID.

instead use UUID v5 which uses namespaces and can be used to generate deterministic UUIDs?

The problem is that we don't return the description (and because of that I changed the getInvoice as well to take the whole invoice instead of just invoice.id)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, this could be a future improvement, not a blocker. But we definitely want to give relay operators the ability to go to their LNURL provider to lookup invoices. If they can at least see descriptions which should include the kind of fee and pubkey then it's all good for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! I'll then change the below endpoint to have request.description instead of requestId

pubkey: requestId,
bolt11: response.data.pr,
amountRequested: amountMsats,
description,
unit: InvoiceUnit['MSATS'],
status: InvoiceStatus['PENDING'],
im-adithya marked this conversation as resolved.
Show resolved Hide resolved
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