Skip to content

Commit

Permalink
refactor: handle lightning payment error with correct node
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Jan 24, 2025
1 parent 5701065 commit 7dfeece
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 45 deletions.
51 changes: 44 additions & 7 deletions lib/lightning/PendingPaymentTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,56 @@ class PendingPaymentTracker {
}
};

public sendPayment = async (
public getRelevantNode = async (
lightningCurrency: Currency,
swap: Swap,
lightningClient: LightningClient,
cltvLimit?: number,
outgoingChannelId?: string,
): Promise<PaymentResponse | undefined> => {
preferredNode: LightningClient,
): Promise<{
paymentHash: string;
node: LightningClient;
payments: LightningPayment[];
}> => {
const paymentHash = getHexString(
(await this.sidecar.decodeInvoiceOrOffer(swap.invoice!)).paymentHash!,
);

const payments =
await LightningPaymentRepository.findByPreimageHash(paymentHash);

const existingRelevantAction = payments.find(
(p) =>
p.status === LightningPaymentStatus.Success ||
p.status === LightningPaymentStatus.Pending ||
p.status === LightningPaymentStatus.PermanentFailure,
);
if (existingRelevantAction === undefined) {
return {
payments,
paymentHash,
node: preferredNode,
};
}

const node = [
lightningCurrency.lndClient,
lightningCurrency.clnClient,
].find((n) => n?.type === existingRelevantAction.node);

return {
payments,
paymentHash,
node: node || preferredNode,
};
};

public sendPayment = async (
swap: Swap,
lightningClient: LightningClient,
paymentHash: string,
payments: LightningPayment[],
cltvLimit?: number,
outgoingChannelId?: string,
): Promise<PaymentResponse | undefined> => {
for (const status of [
LightningPaymentStatus.Pending,
LightningPaymentStatus.Success,
Expand All @@ -109,7 +146,7 @@ class PendingPaymentTracker {
return undefined;

case LightningPaymentStatus.Success:
return this.getSuccessfulPaymentDetails(
return await this.getSuccessfulPaymentDetails(
swap.id,
relevant,
lightningClient.symbol,
Expand All @@ -126,7 +163,7 @@ class PendingPaymentTracker {
}
}

return this.sendPaymentWithNode(
return await this.sendPaymentWithNode(
swap,
lightningClient,
paymentHash,
Expand Down
21 changes: 14 additions & 7 deletions lib/swap/PaymentHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,16 @@ class PaymentHandler {
);

const lightningCurrency = this.currencies.get(lightningSymbol)!;
const lightningClient = await this.nodeSwitch.getSwapNode(
lightningCurrency,
await this.sidecar.decodeInvoiceOrOffer(swap.invoice!),
swap,
);
const { node, paymentHash, payments } =
await this.pendingPaymentTracker.getRelevantNode(
lightningCurrency,
swap,
await this.nodeSwitch.getSwapNode(
lightningCurrency,
await this.sidecar.decodeInvoiceOrOffer(swap.invoice!),
swap,
),
);

try {
const cltvLimit = await this.timeoutDeltaProvider.getCltvLimit(swap);
Expand All @@ -136,7 +141,9 @@ class PaymentHandler {
);
const payResponse = await this.pendingPaymentTracker.sendPayment(
swap,
lightningClient,
node,
paymentHash,
payments,
cltvLimit,
outgoingChannelId,
);
Expand All @@ -149,7 +156,7 @@ class PaymentHandler {
swap,
channelCreation,
lightningCurrency,
lightningClient,
node,
error,
outgoingChannelId,
);
Expand Down
150 changes: 134 additions & 16 deletions test/integration/lightning/PendingPaymentTracker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,84 @@ describe('PendingPaymentTracker', () => {
});
});

describe('getRelevantNode', () => {
test.each`
expected | status
${NodeType.CLN} | ${LightningPaymentStatus.Pending}
${NodeType.CLN} | ${LightningPaymentStatus.Success}
${NodeType.CLN} | ${LightningPaymentStatus.PermanentFailure}
`('should get node with status $status', async ({ expected, status }) => {
const invoiceRes = await bitcoinLndClient.addInvoice(1);
const preimageHash = (
await sidecar.decodeInvoiceOrOffer(invoiceRes.paymentRequest)
).paymentHash!;

const swap = await Swap.create({
...createSubmarineSwapData(),
invoice: invoiceRes.paymentRequest,
preimageHash: getHexString(preimageHash),
});

await LightningPayment.create({
node: NodeType.CLN,
preimageHash: swap.preimageHash,
status,
});

const res = await tracker.getRelevantNode(
currencies[0],
swap,
bitcoinLndClient,
);
expect(res.node.type).toEqual(expected);

expect(res.payments).toHaveLength(1);
expect(res.payments[0].node).toEqual(expected);
expect(res.payments).toEqual(
await LightningPaymentRepository.findByPreimageHash(swap.preimageHash),
);

expect(res.paymentHash).toEqual(getHexString(preimageHash));
});

test.each`
expected | status
${NodeType.LND} | ${LightningPaymentStatus.TemporaryFailure}
${NodeType.LND} | ${undefined}
`(
'should get prefered node when status is $status',
async ({ expected, status }) => {
const invoiceRes = await bitcoinLndClient.addInvoice(1);
const preimageHash = (
await sidecar.decodeInvoiceOrOffer(invoiceRes.paymentRequest)
).paymentHash!;

const swap = await Swap.create({
...createSubmarineSwapData(),
invoice: invoiceRes.paymentRequest,
preimageHash: getHexString(preimageHash),
});

if (status !== undefined) {
await LightningPayment.create({
node: NodeType.CLN,
preimageHash: swap.preimageHash,
status,
});
}

const res = await tracker.getRelevantNode(
currencies[0],
swap,
bitcoinLndClient,
);
expect(res.node.type).toEqual(expected);

expect(res.paymentHash).toEqual(getHexString(preimageHash));
},
);
});

describe('sendPayment', () => {
test('should send payments', async () => {
const invoiceRes = await bitcoinLndClient.addInvoice(1);
Expand All @@ -168,7 +246,12 @@ describe('PendingPaymentTracker', () => {
});

await waitForClnChainSync();
const res = await tracker.sendPayment(swap, clnClient);
const res = await tracker.sendPayment(
swap,
clnClient,
getHexString(preimageHash),
[],
);
expect(res).not.toBeUndefined();
expect(typeof res!.feeMsat).toEqual('number');
expect(res!.preimage).toEqual(
Expand Down Expand Up @@ -200,9 +283,9 @@ describe('PendingPaymentTracker', () => {
preimageHash: getHexString(preimageHash),
});

await expect(tracker.sendPayment(swap, clnClient)).rejects.toEqual(
expect.anything(),
);
await expect(
tracker.sendPayment(swap, clnClient, getHexString(preimageHash), []),
).rejects.toEqual(expect.anything());

const payments = await LightningPaymentRepository.findByPreimageHash(
getHexString(preimageHash),
Expand All @@ -229,9 +312,9 @@ describe('PendingPaymentTracker', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
PendingPaymentTracker['raceTimeout'] = 2;
await expect(tracker.sendPayment(swap, clnClient)).resolves.toEqual(
undefined,
);
await expect(
tracker.sendPayment(swap, clnClient, getHexString(preimageHash), []),
).resolves.toEqual(undefined);
await bitcoinLndClient.cancelHoldInvoice(preimageHash);

expect(
Expand Down Expand Up @@ -271,7 +354,14 @@ describe('PendingPaymentTracker', () => {
});

await expect(
tracker.sendPayment(swap, bitcoinLndClient),
tracker.sendPayment(
swap,
bitcoinLndClient,
getHexString(preimageHash),
await LightningPaymentRepository.findByPreimageHash(
swap.preimageHash,
),
),
).resolves.toEqual(undefined);
});

Expand All @@ -298,7 +388,14 @@ describe('PendingPaymentTracker', () => {

const paymentRes = await bitcoinLndClient.sendPayment(invoice);
await expect(
tracker.sendPayment(swap, bitcoinLndClient),
tracker.sendPayment(
swap,
bitcoinLndClient,
getHexString(preimageHash),
await LightningPaymentRepository.findByPreimageHash(
swap.preimageHash,
),
),
).resolves.toEqual(paymentRes);
});

Expand All @@ -321,9 +418,16 @@ describe('PendingPaymentTracker', () => {
const paymentRes = await clnClient.sendPayment(
invoiceRes.paymentRequest,
);
await expect(tracker.sendPayment(swap, clnClient)).resolves.toEqual(
paymentRes,
);
await expect(
tracker.sendPayment(
swap,
clnClient,
swap.preimageHash,
await LightningPaymentRepository.findByPreimageHash(
swap.preimageHash,
),
),
).resolves.toEqual(paymentRes);
});

test('should return undefined when node for fetching success details is not available', async () => {
Expand All @@ -350,9 +454,16 @@ describe('PendingPaymentTracker', () => {
]);

await clnClient.sendPayment(invoiceRes.paymentRequest);
await expect(tracker.sendPayment(swap, clnClient)).resolves.toEqual(
undefined,
);
await expect(
tracker.sendPayment(
swap,
clnClient,
swap.preimageHash,
await LightningPaymentRepository.findByPreimageHash(
swap.preimageHash,
),
),
).resolves.toEqual(undefined);
});
});

Expand All @@ -374,7 +485,14 @@ describe('PendingPaymentTracker', () => {
});

await expect(
tracker.sendPayment(swap, bitcoinLndClient),
tracker.sendPayment(
swap,
bitcoinLndClient,
swap.preimageHash,
await LightningPaymentRepository.findByPreimageHash(
swap.preimageHash,
),
),
).rejects.toEqual(error);
});
});
Expand Down
5 changes: 1 addition & 4 deletions test/integration/lightning/cln/Router.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import { bitcoinClient, bitcoinLndClient, clnClient } from '../../Nodes';
describe('Router', () => {
beforeAll(async () => {
await bitcoinClient.generate(1);
await Promise.all([
clnClient.connect(false),
bitcoinLndClient.connect(false),
]);
await Promise.all([clnClient.connect(), bitcoinLndClient.connect(false)]);
});

afterAll(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { bitcoinClient } from '../../Nodes';

jest.mock('../../../../lib/db/repositories/ChainTipRepository');

class CoopSigner extends CoopSignerBase<Swap, NonNullable<unknown>> {
class CoopSigner extends CoopSignerBase<NonNullable<unknown>> {
constructor(walletManager: WalletManager, swapOutputType: SwapOutputType) {
super(Logger.disabledLogger, walletManager, swapOutputType);
}
Expand Down
5 changes: 1 addition & 4 deletions test/integration/wallet/ethereum/InjectedProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ describe('InjectedProvider', () => {
beforeAll(async () => {
provider = new InjectedProvider(Logger.disabledLogger, Ethereum, {
providerEndpoint,
tokens: [],
etherSwapAddress: '0x',
erc20SwapAddress: '0x',
});
} as never);
await provider.init();
});

Expand Down
7 changes: 1 addition & 6 deletions test/unit/api/v2/ApiV2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,7 @@ describe('ApiV2', () => {
use: jest.fn(),
} as any;

new ApiV2(
Logger.disabledLogger,
{} as any,
{} as any,
{} as any,
).registerRoutes(app);
new ApiV2(Logger.disabledLogger, {} as any, {} as any).registerRoutes(app);

expect(mockGetInfoRouter).toHaveBeenCalledTimes(1);
expect(mockSwapGetRouter).toHaveBeenCalledTimes(1);
Expand Down
4 changes: 4 additions & 0 deletions test/unit/rates/providers/RateProviderBase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class TestRateProvider extends RateProviderBase<any> {
super(currencies, mockedFeeProvider, minSwapSizeMultipliers);
}

public getRate(): number | undefined {
throw new Error('stub');
}

public setHardcodedPair = () => {
throw new Error('stub');
};
Expand Down
Loading

0 comments on commit 7dfeece

Please sign in to comment.