Skip to content

Commit

Permalink
feat: be gentler to xpay
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Jan 20, 2025
1 parent 12661c1 commit 8c8de77
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 35 deletions.
14 changes: 13 additions & 1 deletion lib/db/Migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];

Expand Down Expand Up @@ -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}`;
}
Expand Down
6 changes: 6 additions & 0 deletions lib/db/models/LightningPayment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ type LightningPaymentType = {
node: NodeType;
status: LightningPaymentStatus;
error?: string;
retries: number | null;
};

class LightningPayment extends Model implements LightningPaymentType {
public preimageHash!: string;
public node!: NodeType;
public status!: LightningPaymentStatus;
public error?: string;
public retries!: number | null;

public createdAt!: Date;
public updatedAt!: Date;
Expand Down Expand Up @@ -58,6 +60,10 @@ class LightningPayment extends Model implements LightningPaymentType {
type: new DataTypes.STRING(),
allowNull: true,
},
retries: {
type: new DataTypes.INTEGER(),
allowNull: true,
},
},
{
sequelize,
Expand Down
9 changes: 8 additions & 1 deletion lib/db/repositories/LightningPaymentRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ enum Errors {

class LightningPaymentRepository {
public static create = async (
data: Omit<Omit<LightningPaymentType, 'status'>, 'error'>,
data: Omit<Omit<Omit<LightningPaymentType, 'status'>, 'error'>, 'retries'>,
) => {
const existing = await LightningPayment.findOne({
where: {
Expand All @@ -24,6 +24,7 @@ class LightningPaymentRepository {
if (existing === null) {
return LightningPayment.create({
...data,
retries: 1,
status: LightningPaymentStatus.Pending,
});
}
Expand All @@ -33,6 +34,7 @@ class LightningPaymentRepository {
}

return existing.update({
retries: (existing.retries || 0) + 1,
status: LightningPaymentStatus.Pending,
});
};
Expand Down Expand Up @@ -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 },
Expand Down
12 changes: 12 additions & 0 deletions lib/lightning/PendingPaymentTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 10 additions & 9 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);

Expand Down Expand Up @@ -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();
Expand Down
5 changes: 2 additions & 3 deletions lib/service/TimeoutDeltaProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
);

Expand Down
65 changes: 51 additions & 14 deletions lib/swap/NodeSwitch.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -14,14 +17,15 @@ 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<string, NodeType>();

private readonly swapNode?: NodeType;

constructor(
private logger: Logger,
private readonly logger: Logger,
cfg?: NodeSwitchConfig,
) {
this.clnAmountThreshold =
Expand Down Expand Up @@ -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<LightningClient> => {
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(
Expand Down
4 changes: 2 additions & 2 deletions lib/swap/PaymentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);

Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 8c8de77

Please sign in to comment.