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: be gentler to xpay #792

Merged
merged 1 commit into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
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
20 changes: 19 additions & 1 deletion lib/lightning/PendingPaymentTracker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Logger from '../Logger';
import { racePromise } from '../PromiseUtils';
import { getHexBuffer, getHexString } from '../Utils';
import { formatError, getHexBuffer, getHexString } from '../Utils';
import DefaultMap from '../consts/DefaultMap';
import LightningPayment, {
LightningPaymentStatus,
Expand Down Expand Up @@ -178,7 +178,9 @@ class PendingPaymentTracker {
paymentPromise !== undefined
) {
this.lightningTrackers[lightningClient.type].trackPayment(
lightningClient,
preimageHash,
swap.invoice!,
paymentPromise,
);
this.logger.verbose(
Expand All @@ -189,6 +191,22 @@ 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 &&
formatError(e).includes('xpay')
) {
this.lightningTrackers[lightningClient.type].watchPayment(
lightningClient,
swap.invoice!,
preimageHash,
);

return undefined;
}

await LightningPaymentRepository.setStatus(
preimageHash,
lightningClient.type,
Expand Down
23 changes: 22 additions & 1 deletion lib/lightning/paymentTrackers/ClnPendingPaymentTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Logger from '../../Logger';
import { formatError } from '../../Utils';
import { NodeType } from '../../db/models/ReverseSwap';
import LightningNursery from '../../swap/LightningNursery';
import { LightningClient } from '../LightningClient';
import { LightningClient, PaymentResponse } from '../LightningClient';
import ClnClient from '../cln/ClnClient';
import NodePendingPendingTracker from './NodePendingPaymentTracker';

Expand Down Expand Up @@ -33,6 +33,27 @@ class ClnPendingPaymentTracker extends NodePendingPendingTracker {
clearInterval(this.checkInterval as unknown as number);
};

public trackPayment = (
client: LightningClient,
preimageHash: string,
invoice: string,
promise: Promise<PaymentResponse>,
): void => {
promise
.then((result) => this.handleSucceededPayment(preimageHash, result))
.catch((error) => {
// CLN xpay throws errors while the payment is still pending
if (
!this.isPermanentError(error) &&
formatError(error).includes('xpay')
) {
this.watchPayment(client, invoice, preimageHash);
} else {
this.handleFailedPayment(preimageHash, error);
}
});
};

public watchPayment = (
client: LightningClient,
invoice: string,
Expand Down
13 changes: 12 additions & 1 deletion lib/lightning/paymentTrackers/LndPendingPaymentTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { formatError, getHexBuffer } from '../../Utils';
import { NodeType, nodeTypeToPrettyString } from '../../db/models/ReverseSwap';
import { Payment, PaymentFailureReason } from '../../proto/lnd/rpc_pb';
import LightningNursery from '../../swap/LightningNursery';
import { LightningClient } from '../LightningClient';
import { LightningClient, PaymentResponse } from '../LightningClient';
import LndClient from '../LndClient';
import NodePendingPendingTracker from './NodePendingPaymentTracker';

Expand All @@ -12,6 +12,17 @@ class LndPendingPaymentTracker extends NodePendingPendingTracker {
super(logger, NodeType.LND);
}

public trackPayment = (
_client: LightningClient,
preimageHash: string,
_invoice: string,
promise: Promise<PaymentResponse>,
): void => {
promise
.then((result) => this.handleSucceededPayment(preimageHash, result))
.catch((error) => this.handleFailedPayment(preimageHash, error));
};

public watchPayment = (
client: LightningClient,
_: string,
Expand Down
16 changes: 7 additions & 9 deletions lib/lightning/paymentTrackers/NodePendingPaymentTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ abstract class NodePendingPendingTracker {
protected readonly nodeType: NodeType,
) {}

public abstract trackPayment(
client: LightningClient,
preimageHash: string,
invoice: string,
promise: Promise<PaymentResponse>,
): void;

public abstract watchPayment(
client: LightningClient,
invoice: string,
Expand All @@ -21,15 +28,6 @@ abstract class NodePendingPendingTracker {

public abstract parseErrorMessage(error: unknown): string;

public trackPayment = (
preimageHash: string,
promise: Promise<PaymentResponse>,
) => {
promise
.then((result) => this.handleSucceededPayment(preimageHash, result))
.catch((error) => this.handleFailedPayment(preimageHash, error));
};

protected handleSucceededPayment = async (
preimageHash: string,
result: PaymentResponse,
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
70 changes: 56 additions & 14 deletions lib/swap/NodeSwitch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import Logger from '../Logger';
import { getHexString } from '../Utils';
import { SwapType, swapTypeToPrettyString } from '../consts/Enums';
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 +18,15 @@ type NodeSwitchConfig = {

class NodeSwitch {
private static readonly defaultClnAmountThreshold = 1_000_000;
private static readonly maxClnRetries = 1;

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 +70,56 @@ 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
) {
const identifier =
swap.id !== undefined
? `of ${swapTypeToPrettyString(SwapType.Submarine)} Swap ${swap.id}`
: `with hash ${getHexString(decoded.paymentHash)}`;
this.logger.debug(
`Max CLN retries reached for invoice ${identifier}; 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
Loading
Loading