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 a3913b9
Show file tree
Hide file tree
Showing 18 changed files with 346 additions and 86 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
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
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 = 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 +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
Loading

0 comments on commit a3913b9

Please sign in to comment.