Skip to content

Commit

Permalink
feat: fallback for timeouts when creating invoices (#398)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 authored Oct 9, 2023
1 parent 05811c4 commit a501908
Show file tree
Hide file tree
Showing 20 changed files with 795 additions and 185 deletions.
2 changes: 1 addition & 1 deletion lib/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class BaseClient extends EventEmitter {
return this.status === ClientStatus.OutOfSync;
}

protected setClientStatus = (status: ClientStatus): void => {
public setClientStatus = (status: ClientStatus): void => {
this.status = status;
};

Expand Down
21 changes: 21 additions & 0 deletions lib/PromiseUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const racePromise = async <T>(
promise: (() => Promise<T>) | Promise<T>,
raceHandler: (reason?: any) => void,
raceTimeout: number,
): Promise<T> => {
let timeout: NodeJS.Timeout | undefined = undefined;

const timeoutPromise = new Promise<T>((_, reject) => {
timeout = setTimeout(() => raceHandler(reject), raceTimeout);
});

const res = await Promise.race([
promise instanceof Promise ? promise : promise(),
timeoutPromise,
]);

if (timeout !== undefined) {
clearTimeout(timeout);
}
return res;
};
8 changes: 4 additions & 4 deletions lib/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ export const calculateUtxoTransactionFee = async (
chainClient: ChainClient,
transaction: Transaction,
): Promise<number> => {
let fee = 0;
let fee = 0n;

for (const input of transaction.ins) {
const inputId = transactionHashToId(input.hash);
Expand All @@ -525,14 +525,14 @@ export const calculateUtxoTransactionFee = async (

const spentOutput = inputTransaction.outs[input.index];

fee += spentOutput.value;
fee += BigInt(spentOutput.value);
}

transaction.outs.forEach((output) => {
fee -= output.value;
fee -= BigInt(output.value);
});

return fee;
return Number(fee);
};

export const calculateLiquidTransactionFee = (
Expand Down
12 changes: 9 additions & 3 deletions lib/lightning/ClnClient.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import fs from 'fs';
import bolt11 from 'bolt11';
import {
Metadata,
credentials,
ChannelCredentials,
ClientReadableStream,
credentials,
Metadata,
} from '@grpc/grpc-js';
import Errors from './Errors';
import Logger from '../Logger';
import BaseClient from '../BaseClient';
import { ClientStatus } from '../consts/Enums';
import * as noderpc from '../proto/cln/node_pb';
import { ListfundsOutputs } from '../proto/cln/node_pb';
import * as holdrpc from '../proto/hold/hold_pb';
import { grpcOptions, unaryCall } from './GrpcUtils';
import { NodeClient } from '../proto/cln/node_grpc_pb';
import { HoldClient } from '../proto/hold/hold_grpc_pb';
import { ListfundsOutputs } from '../proto/cln/node_pb';
import * as primitivesrpc from '../proto/cln/primitives_pb';
import { decodeInvoice, formatError, getHexString } from '../Utils';
import { WalletBalance } from '../wallet/providers/WalletProviderInterface';
Expand Down Expand Up @@ -145,6 +145,8 @@ class ClnClient
if (startSubscriptions) {
this.subscribeTrackHoldInvoices();
}

this.setClientStatus(ClientStatus.Connected);
} catch (error) {
this.setClientStatus(ClientStatus.Disconnected);

Expand Down Expand Up @@ -318,6 +320,10 @@ class ClnClient
});
};

public stop = (): void => {
// Just here for interface compatibility;
};

public routingHints = async (node: string): Promise<HopHint[][]> => {
const req = new holdrpc.RoutingHintsRequest();
req.setNode(node);
Expand Down
5 changes: 5 additions & 0 deletions lib/lightning/LightningClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import bolt11 from 'bolt11';
import * as lndrpc from '../proto/lnd/rpc_pb';
import { ClientStatus } from '../consts/Enums';
import { BalancerFetcher } from '../wallet/providers/WalletProviderInterface';

enum InvoiceState {
Expand Down Expand Up @@ -111,6 +112,9 @@ interface LightningClient extends BalancerFetcher {

symbol: string;

isConnected(): boolean;
setClientStatus(status: ClientStatus): void;

connect(startSubscriptions?: boolean): Promise<boolean>;
disconnect(): void;

Expand Down Expand Up @@ -151,6 +155,7 @@ interface LightningClient extends BalancerFetcher {
}

interface RoutingHintsProvider {
stop(): void;
routingHints(nodeId: string): Promise<HopHint[][]>;
}

Expand Down
6 changes: 3 additions & 3 deletions lib/service/TimeoutDeltaProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,11 @@ class TimeoutDeltaProvider {

const decodedInvoice =
await NodeSwitch.fallback(currency)!.decodeInvoice(invoice);
const lightningClient = NodeSwitch.fallback(
const lightningClient = this.nodeSwitch.switch(
currency,
this.nodeSwitch.switch(currency, decodedInvoice.value, referralId),
decodedInvoice.value,
referralId,
);

const [routeTimeLock, chainInfo] = await Promise.all([
this.checkRoutability(
lightningClient,
Expand Down
8 changes: 8 additions & 0 deletions lib/swap/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,12 @@ export default {
message: 'transaction fee is too low',
code: concatErrorCode(ErrorCodePrefix.Swap, 16),
}),
LIGHTNING_CLIENT_CALL_TIMEOUT: (): Error => ({
message: 'lightning client call timeout',
code: concatErrorCode(ErrorCodePrefix.Swap, 17),
}),
NO_AVAILABLE_LIGHTNING_CLIENT: (): Error => ({
message: 'no available lightning client',
code: concatErrorCode(ErrorCodePrefix.Swap, 18),
}),
};
131 changes: 131 additions & 0 deletions lib/swap/NodeFallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import Errors from './Errors';
import Logger from '../Logger';
import NodeSwitch from './NodeSwitch';
import { racePromise } from '../PromiseUtils';
import { ClientStatus } from '../consts/Enums';
import RoutingHints from './routing/RoutingHints';
import { Currency } from '../wallet/WalletManager';
import { NodeType } from '../db/models/ReverseSwap';
import { HopHint, LightningClient } from '../lightning/LightningClient';

type InvoiceWithRoutingHints = {
paymentRequest: string;
routingHints: HopHint[][] | undefined;
};

type HolisticInvoice = InvoiceWithRoutingHints & {
nodeType: NodeType;
lightningClient: LightningClient;
};

class NodeFallback {
private static readonly addInvoiceTimeout = 10_000;

constructor(
private logger: Logger,
private nodeSwitch: NodeSwitch,
private routingHints: RoutingHints,
) {}

public getReverseSwapInvoice = async (
id: string,
referralId: string | undefined,
routingNode: string | undefined,
currency: Currency,
holdInvoiceAmount: number,
preimageHash: Buffer,
cltvExpiry?: number,
expiry?: number,
memo?: string,
): Promise<HolisticInvoice> => {
let nodeForSwap = this.nodeSwitch.getNodeForReverseSwap(
id,
currency,
holdInvoiceAmount,
referralId,
);

while (nodeForSwap.lightningClient !== undefined) {
try {
return {
...nodeForSwap,
...(await this.addHoldInvoice(
nodeForSwap.nodeType,
nodeForSwap.lightningClient,
routingNode,
currency,
holdInvoiceAmount,
preimageHash,
cltvExpiry,
expiry,
memo,
)),
};
} catch (e) {
if (
(e as any).message === Errors.LIGHTNING_CLIENT_CALL_TIMEOUT().message
) {
this.logger.warn(
`${nodeForSwap.lightningClient.serviceName()} invoice creation timed out after ${
NodeFallback.addInvoiceTimeout
}ms; trying next node`,
);
nodeForSwap.lightningClient.setClientStatus(
ClientStatus.Disconnected,
);
nodeForSwap = this.nodeSwitch.getNodeForReverseSwap(
id,
currency,
holdInvoiceAmount,
referralId,
);

continue;
}

throw e;
}
}

throw Errors.NO_AVAILABLE_LIGHTNING_CLIENT();
};

private addHoldInvoice = async (
nodeType: NodeType,
lightningClient: LightningClient,
routingNode: string | undefined,
currency: Currency,
holdInvoiceAmount: number,
preimageHash: Buffer,
cltvExpiry?: number,
expiry?: number,
memo?: string,
): Promise<InvoiceWithRoutingHints> => {
const routingHints =
routingNode !== undefined
? await this.routingHints.getRoutingHints(
currency.symbol,
routingNode,
nodeType,
)
: undefined;

return racePromise<InvoiceWithRoutingHints>(
async (): Promise<InvoiceWithRoutingHints> => ({
routingHints,
paymentRequest: await lightningClient.addHoldInvoice(
holdInvoiceAmount,
preimageHash,
cltvExpiry,
expiry,
memo,
routingHints,
),
}),
(reject) => reject(Errors.LIGHTNING_CLIENT_CALL_TIMEOUT()),
NodeFallback.addInvoiceTimeout,
);
};
}

export default NodeFallback;
29 changes: 22 additions & 7 deletions lib/swap/NodeSwitch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Errors from './Errors';
import Logger from '../Logger';
import { SwapType } from '../db/models/Swap';
import { Currency } from '../wallet/WalletManager';
Expand Down Expand Up @@ -94,24 +95,38 @@ class NodeSwitch {
currency: Currency,
amount?: number,
referralId?: string,
): LightningClient | undefined => {
): LightningClient => {
if (referralId && this.referralIds.has(referralId)) {
return NodeSwitch.switchOnNodeType(
return NodeSwitch.fallback(
currency,
this.referralIds.get(referralId)!,
NodeSwitch.switchOnNodeType(
currency,
this.referralIds.get(referralId)!,
),
);
}

return (amount || 0) > this.clnAmountThreshold
? currency.lndClient
: currency.clnClient;
return NodeSwitch.fallback(
currency,
(amount || 0) > this.clnAmountThreshold
? currency.lndClient
: currency.clnClient,
);
};

public static fallback = (
currency: Currency,
client?: LightningClient,
): LightningClient => {
return (client || currency.lndClient || currency.clnClient)!;
const clients = [client, currency.lndClient, currency.clnClient]
.filter((client): client is LightningClient => client !== undefined)
.filter((client) => client.isConnected());

if (clients.length === 0) {
throw Errors.NO_AVAILABLE_LIGHTNING_CLIENT();
}

return clients[0]!;
};

private static switchOnNodeType = (
Expand Down
Loading

0 comments on commit a501908

Please sign in to comment.