From 8c8de774c1f15fc6b032163195aa23d4fe04a3f4 Mon Sep 17 00:00:00 2001 From: michael1011 Date: Mon, 20 Jan 2025 12:47:52 +0100 Subject: [PATCH] feat: be gentler to xpay --- lib/db/Migration.ts | 14 +++- lib/db/models/LightningPayment.ts | 6 ++ .../LightningPaymentRepository.ts | 9 ++- lib/lightning/PendingPaymentTracker.ts | 12 ++++ lib/service/Service.ts | 19 +++--- lib/service/TimeoutDeltaProvider.ts | 5 +- lib/swap/NodeSwitch.ts | 65 +++++++++++++++---- lib/swap/PaymentHandler.ts | 4 +- package-lock.json | 8 +-- package.json | 2 +- 10 files changed, 109 insertions(+), 35 deletions(-) diff --git a/lib/db/Migration.ts b/lib/db/Migration.ts index af12a79b..d7e877be 100644 --- a/lib/db/Migration.ts +++ b/lib/db/Migration.ts @@ -93,7 +93,7 @@ const decodeInvoice = ( // TODO: integration tests for actual migrations class Migration { - private static latestSchemaVersion = 14; + private static latestSchemaVersion = 15; private toBackFill: number[] = []; @@ -657,6 +657,18 @@ class Migration { break; } + case 14: { + await this.sequelize + .getQueryInterface() + .addColumn(LightningPayment.tableName, 'retries', { + type: new DataTypes.INTEGER(), + allowNull: true, + }); + + await this.finishMigration(versionRow.version, currencies); + break; + } + default: throw `found unexpected database version ${versionRow.version}`; } diff --git a/lib/db/models/LightningPayment.ts b/lib/db/models/LightningPayment.ts index f1e58eee..ff65f6b8 100644 --- a/lib/db/models/LightningPayment.ts +++ b/lib/db/models/LightningPayment.ts @@ -14,6 +14,7 @@ type LightningPaymentType = { node: NodeType; status: LightningPaymentStatus; error?: string; + retries: number | null; }; class LightningPayment extends Model implements LightningPaymentType { @@ -21,6 +22,7 @@ class LightningPayment extends Model implements LightningPaymentType { public node!: NodeType; public status!: LightningPaymentStatus; public error?: string; + public retries!: number | null; public createdAt!: Date; public updatedAt!: Date; @@ -58,6 +60,10 @@ class LightningPayment extends Model implements LightningPaymentType { type: new DataTypes.STRING(), allowNull: true, }, + retries: { + type: new DataTypes.INTEGER(), + allowNull: true, + }, }, { sequelize, diff --git a/lib/db/repositories/LightningPaymentRepository.ts b/lib/db/repositories/LightningPaymentRepository.ts index ae048178..dc90d3fd 100644 --- a/lib/db/repositories/LightningPaymentRepository.ts +++ b/lib/db/repositories/LightningPaymentRepository.ts @@ -13,7 +13,7 @@ enum Errors { class LightningPaymentRepository { public static create = async ( - data: Omit, 'error'>, + data: Omit, 'error'>, 'retries'>, ) => { const existing = await LightningPayment.findOne({ where: { @@ -24,6 +24,7 @@ class LightningPaymentRepository { if (existing === null) { return LightningPayment.create({ ...data, + retries: 1, status: LightningPaymentStatus.Pending, }); } @@ -33,6 +34,7 @@ class LightningPaymentRepository { } return existing.update({ + retries: (existing.retries || 0) + 1, status: LightningPaymentStatus.Pending, }); }; @@ -69,6 +71,11 @@ class LightningPaymentRepository { public static findByPreimageHash = (preimageHash: string) => LightningPayment.findAll({ where: { preimageHash } }); + public static findByPreimageHashAndNode = ( + preimageHash: string, + node: NodeType, + ) => LightningPayment.findOne({ where: { preimageHash, node } }); + public static findByStatus = (status: LightningPaymentStatus) => LightningPayment.findAll({ where: { status }, diff --git a/lib/lightning/PendingPaymentTracker.ts b/lib/lightning/PendingPaymentTracker.ts index 24f422a8..bce55e4c 100644 --- a/lib/lightning/PendingPaymentTracker.ts +++ b/lib/lightning/PendingPaymentTracker.ts @@ -189,6 +189,18 @@ class PendingPaymentTracker { const isPermanentError = this.lightningTrackers[lightningClient.type].isPermanentError(e); + + // CLN xpay does throw errors while the payment is still pending + if (lightningClient.type === NodeType.CLN && !isPermanentError) { + this.lightningTrackers[lightningClient.type].watchPayment( + lightningClient, + swap.invoice!, + preimageHash, + ); + + return undefined; + } + await LightningPaymentRepository.setStatus( preimageHash, lightningClient.type, diff --git a/lib/service/Service.ts b/lib/service/Service.ts index 28f7fc43..fd6531ef 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -1325,9 +1325,9 @@ class Service { ]); swap.invoiceAmount = msatToSat(decodedInvoice.amountMsat); - const lightningClient = this.nodeSwitch.getSwapNode( + const lightningClient = await this.nodeSwitch.getSwapNode( this.currencies.get(lightningCurrency)!, - decodedInvoice.type, + decodedInvoice, swap, ); @@ -1399,13 +1399,14 @@ class Service { swap.invoiceAmount = msatToSat(decodedInvoice.amountMsat); - const { destination, features } = await this.nodeSwitch - .getSwapNode( - getCurrency(this.currencies, lightningCurrency)!, - decodedInvoice.type, - swap, - ) - .decodeInvoice(invoice); + const lightningClient = await this.nodeSwitch.getSwapNode( + getCurrency(this.currencies, lightningCurrency)!, + decodedInvoice, + swap, + ); + + const { destination, features } = + await lightningClient.decodeInvoice(invoice); if (this.nodeInfo.isOurNode(destination)) { throw Errors.DESTINATION_BOLTZ_NODE(); diff --git a/lib/service/TimeoutDeltaProvider.ts b/lib/service/TimeoutDeltaProvider.ts index c72cb56c..b0f16fc8 100644 --- a/lib/service/TimeoutDeltaProvider.ts +++ b/lib/service/TimeoutDeltaProvider.ts @@ -290,12 +290,11 @@ class TimeoutDeltaProvider { const decodedInvoice = await this.sidecar.decodeInvoiceOrOffer(invoice); const amountSat = msatToSat(decodedInvoice.amountMsat); - const lightningClient = this.nodeSwitch.getSwapNode( + const lightningClient = await this.nodeSwitch.getSwapNode( currency, - decodedInvoice.type, + decodedInvoice, { referral: referralId, - invoiceAmount: amountSat, }, ); diff --git a/lib/swap/NodeSwitch.ts b/lib/swap/NodeSwitch.ts index 41a4fb5c..e1eefa58 100644 --- a/lib/swap/NodeSwitch.ts +++ b/lib/swap/NodeSwitch.ts @@ -1,7 +1,10 @@ import Logger from '../Logger'; +import { getHexString } from '../Utils'; import ReverseSwap, { NodeType } from '../db/models/ReverseSwap'; +import LightningPaymentRepository from '../db/repositories/LightningPaymentRepository'; +import { msatToSat } from '../lightning/ChannelUtils'; import { LightningClient } from '../lightning/LightningClient'; -import { InvoiceType } from '../sidecar/DecodedInvoice'; +import DecodedInvoice, { InvoiceType } from '../sidecar/DecodedInvoice'; import { Currency } from '../wallet/WalletManager'; import Errors from './Errors'; @@ -14,6 +17,7 @@ type NodeSwitchConfig = { class NodeSwitch { private static readonly defaultClnAmountThreshold = 1_000_000; + private static readonly maxClnRetries = 2; private readonly clnAmountThreshold: number; private readonly referralIds = new Map(); @@ -21,7 +25,7 @@ class NodeSwitch { private readonly swapNode?: NodeType; constructor( - private logger: Logger, + private readonly logger: Logger, cfg?: NodeSwitchConfig, ) { this.clnAmountThreshold = @@ -65,19 +69,52 @@ class NodeSwitch { ); }; - public getSwapNode = ( + public getSwapNode = async ( currency: Currency, - invoiceType: InvoiceType, - swap: { id?: string; invoiceAmount?: number; referral?: string }, - ): LightningClient => { - const client = NodeSwitch.fallback( - currency, - invoiceType === InvoiceType.Bolt11 - ? this.swapNode !== undefined - ? NodeSwitch.switchOnNodeType(currency, this.swapNode) - : this.switch(currency, swap.invoiceAmount, swap.referral) - : currency.clnClient, - ); + decoded: DecodedInvoice, + swap: { + id?: string; + referral?: string; + }, + ): Promise => { + const selectNode = (preferredNode?: NodeType) => { + return NodeSwitch.fallback( + currency, + decoded.type === InvoiceType.Bolt11 + ? preferredNode !== undefined + ? NodeSwitch.switchOnNodeType(currency, preferredNode) + : this.switch( + currency, + msatToSat(decoded.amountMsat), + swap.referral, + ) + : currency.clnClient, + ); + }; + + let client = selectNode(this.swapNode); + + // Go easy on CLN xpay + if (client.type === NodeType.CLN && decoded.type === InvoiceType.Bolt11) { + if (decoded.paymentHash !== undefined) { + const existingPayment = + await LightningPaymentRepository.findByPreimageHashAndNode( + getHexString(decoded.paymentHash), + client.type, + ); + + if ( + existingPayment?.retries !== null && + existingPayment?.retries !== undefined && + existingPayment.retries > NodeSwitch.maxClnRetries + ) { + this.logger.debug( + `Max CLN retries reached for invoice with hash ${getHexString(decoded.paymentHash)}; preferring LND`, + ); + client = selectNode(NodeType.LND); + } + } + } if (swap.id !== undefined) { this.logger.debug( diff --git a/lib/swap/PaymentHandler.ts b/lib/swap/PaymentHandler.ts index 8216ded0..06ec12dc 100644 --- a/lib/swap/PaymentHandler.ts +++ b/lib/swap/PaymentHandler.ts @@ -119,9 +119,9 @@ class PaymentHandler { ); const lightningCurrency = this.currencies.get(lightningSymbol)!; - const lightningClient = this.nodeSwitch.getSwapNode( + const lightningClient = await this.nodeSwitch.getSwapNode( lightningCurrency, - (await this.sidecar.decodeInvoiceOrOffer(swap.invoice!)).type, + await this.sidecar.decodeInvoiceOrOffer(swap.invoice!), swap, ); diff --git a/package-lock.json b/package-lock.json index 75c3f610..b3f68927 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,7 +70,7 @@ "cross-os": "^1.5.0", "eslint": "^9.18.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "^28.10.0", + "eslint-plugin-jest": "^28.11.0", "eslint-plugin-n": "^17.15.1", "eslint-plugin-node": "^11.1.0", "git-cliff": "^2.7.0", @@ -5711,9 +5711,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.10.0.tgz", - "integrity": "sha512-hyMWUxkBH99HpXT3p8hc7REbEZK3D+nk8vHXGgpB+XXsi0gO4PxMSP+pjfUzb67GnV9yawV9a53eUmcde1CCZA==", + "version": "28.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", + "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 580c261b..412d15c7 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "cross-os": "^1.5.0", "eslint": "^9.18.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "^28.10.0", + "eslint-plugin-jest": "^28.11.0", "eslint-plugin-n": "^17.15.1", "eslint-plugin-node": "^11.1.0", "git-cliff": "^2.7.0",