Skip to content

Commit

Permalink
feat: configurable expiry of reverse swap invoices
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Oct 28, 2023
1 parent c2816d0 commit d3c0a3d
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 73 deletions.
7 changes: 6 additions & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,21 @@ otpsecretpath = "/home/boltz/.boltz/otpSecret.dat"
base = "BTC"
quote = "BTC"
rate = 1
timeoutDelta = 400

maxSwapAmount = 10_000_000
minSwapAmount = 10_000

# Expiry of the invoices generated for reverse swaps of this pair
# If not set, half of the expiry time of the reverse swap will be used
invoiceExpiry = 7200

timeoutDelta = 400
# Alternatively, the timeouts of swaps of a pair can be set like this
# [pairs.timeoutDelta]
# reverse = 1440
# swapMinimal = 1440
# swapMaximal = 2880

[[pairs]]
base = "L-BTC"
quote = "BTC"
Expand Down
9 changes: 6 additions & 3 deletions lib/Boltz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ import GrpcService from './grpc/GrpcService';
import ClnClient from './lightning/ClnClient';
import LndClient from './lightning/LndClient';
import ChainClient from './chain/ChainClient';
import Config, { ConfigType, TokenConfig } from './Config';
import { CurrencyType } from './consts/Enums';
import { formatError, getVersion } from './Utils';
import ElementsClient from './chain/ElementsClient';
import { registerExitHandler } from './ExitHandler';
import BackupScheduler from './backup/BackupScheduler';
import { Ethereum, NetworkDetails, Rsk } from './wallet/ethereum/EvmNetworks';
import Config, { ConfigType, TokenConfig } from './Config';
import { LightningClient } from './lightning/LightningClient';
import EthereumManager from './wallet/ethereum/EthereumManager';
import WalletManager, { Currency } from './wallet/WalletManager';
import ChainTipRepository from './db/repositories/ChainTipRepository';
import NotificationProvider from './notifications/NotificationProvider';
import { Ethereum, NetworkDetails, Rsk } from './wallet/ethereum/EvmNetworks';

class Boltz {
private readonly logger: Logger;
Expand Down Expand Up @@ -187,7 +187,10 @@ class Boltz {
await this.walletManager.init(this.config.currencies);
await this.service.init(this.config.pairs);

await this.service.swapManager.init(Array.from(this.currencies.values()));
await this.service.swapManager.init(
Array.from(this.currencies.values()),
this.config.pairs,
);

await this.notifications.init();

Expand Down
2 changes: 0 additions & 2 deletions lib/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ type CurrencyConfig = BaseCurrencyConfig & {
cln?: ClnConfig;
routingOffsetExceptions?: RoutingOffsetException[];

// Expiry for invoices of this currency in seconds
invoiceExpiry?: number;
// Max fee ratio for LND's sendPayment
maxPaymentFeeRatio?: number;

Expand Down
4 changes: 4 additions & 0 deletions lib/consts/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export type PairConfig = {
// If there is a hardcoded rate the APIs of the exchanges will not be queried
rate?: number;

// Expiry for invoices of this pair in seconds
// Defaults to 50% of the expiry time of reverse swaps
invoiceExpiry?: number;

// The timeout of the swaps on this pair in minutes
timeoutDelta?: PairTimeoutBlocksDelta | number;

Expand Down
2 changes: 1 addition & 1 deletion lib/lightning/LightningClient.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import bolt11 from 'bolt11';
import * as lndrpc from '../proto/lnd/rpc_pb';
import { IBaseClient } from '../BaseClient';
import { ClientStatus } from '../consts/Enums';
import { BalancerFetcher } from '../wallet/providers/WalletProviderInterface';
import { IBaseClient } from '../BaseClient';

enum InvoiceState {
Open,
Expand Down
31 changes: 23 additions & 8 deletions lib/service/InvoiceExpiryHelper.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import { CurrencyConfig } from '../Config';
import { getPairId } from '../Utils';
import { PairConfig } from '../consts/Types';
import TimeoutDeltaProvider from './TimeoutDeltaProvider';

class InvoiceExpiryHelper {
private static readonly defaultInvoiceExpiry = 3600;

private readonly invoiceExpiry = new Map<string, number>();

constructor(currencies: CurrencyConfig[]) {
for (const currency of currencies) {
if (currency.invoiceExpiry) {
this.invoiceExpiry.set(currency.symbol, currency.invoiceExpiry);
constructor(pairs: PairConfig[], timeoutDeltaProvider: TimeoutDeltaProvider) {
for (const pair of pairs) {
const pairId = getPairId(pair);

if (pair.invoiceExpiry) {
this.invoiceExpiry.set(pairId, pair.invoiceExpiry);
continue;
}

const delta = timeoutDeltaProvider.timeoutDeltas.get(getPairId(pair))!
.quote.reverse;

// Convert to seconds and divide by 2
const expiry = Math.ceil(
(delta * TimeoutDeltaProvider.blockTimes.get(pair.quote)! * 60) / 2,
);

this.invoiceExpiry.set(pairId, expiry);
}
}

public getExpiry = (symbol: string): number => {
public getExpiry = (pair: string): number => {
return (
this.invoiceExpiry.get(symbol) || InvoiceExpiryHelper.defaultInvoiceExpiry
this.invoiceExpiry.get(pair) || InvoiceExpiryHelper.defaultInvoiceExpiry
);
};

Expand All @@ -35,7 +50,7 @@ class InvoiceExpiryHelper {
if (timeExpireDate) {
invoiceExpiry = timeExpireDate;
} else {
invoiceExpiry += 3600;
invoiceExpiry += InvoiceExpiryHelper.defaultInvoiceExpiry;
}

return invoiceExpiry;
Expand Down
2 changes: 0 additions & 2 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import LndClient from '../lightning/LndClient';
import ElementsService from './ElementsService';
import SwapOutputType from '../swap/SwapOutputType';
import ElementsClient from '../chain/ElementsClient';
import InvoiceExpiryHelper from './InvoiceExpiryHelper';
import PaymentRequestUtils from './PaymentRequestUtils';
import PairRepository from '../db/repositories/PairRepository';
import SwapRepository from '../db/repositories/SwapRepository';
Expand Down Expand Up @@ -145,7 +144,6 @@ class Service {
this.nodeSwitch,
this.rateProvider,
this.timeoutDeltaProvider,
new InvoiceExpiryHelper(config.currencies),
new SwapOutputType(
config.swapwitnessaddress
? OutputType.Bech32
Expand Down
43 changes: 28 additions & 15 deletions lib/swap/SwapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Swap from '../db/models/Swap';
import NodeSwitch from './NodeSwitch';
import SwapNursery from './SwapNursery';
import NodeFallback from './NodeFallback';
import { PairConfig } from '../consts/Types';
import SwapOutputType from './SwapOutputType';
import RateProvider from '../rates/RateProvider';
import RoutingHints from './routing/RoutingHints';
Expand Down Expand Up @@ -61,15 +62,15 @@ class SwapManager {
public routingHints!: RoutingHints;

private nodeFallback!: NodeFallback;
private invoiceExpiryHelper!: InvoiceExpiryHelper;

constructor(
private logger: Logger,
private walletManager: WalletManager,
private nodeSwitch: NodeSwitch,
private readonly logger: Logger,
private readonly walletManager: WalletManager,
private readonly nodeSwitch: NodeSwitch,
rateProvider: RateProvider,
timeoutDeltaProvider: TimeoutDeltaProvider,
private invoiceExpiryHelper: InvoiceExpiryHelper,
private swapOutputType: SwapOutputType,
private readonly timeoutDeltaProvider: TimeoutDeltaProvider,
private readonly swapOutputType: SwapOutputType,
retryInterval: number,
) {
this.nursery = new SwapNursery(
Expand All @@ -83,7 +84,10 @@ class SwapManager {
);
}

public init = async (currencies: Currency[]): Promise<void> => {
public init = async (
currencies: Currency[],
pairs: PairConfig[],
): Promise<void> => {
currencies.forEach((currency) => {
this.currencies.set(currency.symbol, currency);
});
Expand Down Expand Up @@ -128,6 +132,11 @@ class SwapManager {
this.nodeSwitch,
this.routingHints,
);

this.invoiceExpiryHelper = new InvoiceExpiryHelper(
pairs,
this.timeoutDeltaProvider,
);
};

/**
Expand Down Expand Up @@ -517,6 +526,11 @@ class SwapManager {
);
}

const pair = getPairId({
base: args.baseCurrency,
quote: args.quoteCurrency,
});

const { nodeType, lightningClient, paymentRequest, routingHints } =
await this.nodeFallback.getReverseSwapInvoice(
id,
Expand All @@ -526,7 +540,7 @@ class SwapManager {
args.holdInvoiceAmount,
args.preimageHash,
args.lightningTimeoutBlockDelta,
this.invoiceExpiryHelper.getExpiry(receivingCurrency.symbol),
this.invoiceExpiryHelper.getExpiry(pair),
getSwapMemo(sendingCurrency.symbol, true),
);

Expand All @@ -545,7 +559,7 @@ class SwapManager {
args.prepayMinerFeeInvoiceAmount,
minerFeeInvoicePreimageHash,
undefined,
this.invoiceExpiryHelper.getExpiry(receivingCurrency.symbol),
this.invoiceExpiryHelper.getExpiry(pair),
getPrepayMinerFeeInvoiceMemo(sendingCurrency.symbol),
routingHints,
);
Expand All @@ -554,16 +568,15 @@ class SwapManager {

if (args.prepayMinerFeeOnchainAmount) {
this.logger.debug(
`Sending ${args.prepayMinerFeeOnchainAmount} Ether as prepay miner fee for Reverse Swap: ${id}`,
`Sending ${args.prepayMinerFeeOnchainAmount} ${
this.walletManager.ethereumManagers.find((manager) =>
manager.hasSymbol(sendingCurrency.symbol),
)!.networkDetails.name
} as prepay miner fee for Reverse Swap: ${id}`,
);
}
}

const pair = getPairId({
base: args.baseCurrency,
quote: args.quoteCurrency,
});

let lockupAddress: string;
let timeoutBlockHeight: number;

Expand Down
23 changes: 23 additions & 0 deletions test/integration/lightning/ClnClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { randomBytes } from 'crypto';
import { crypto } from 'bitcoinjs-lib';
import Errors from '../../../lib/lightning/Errors';
import { decodeInvoice, getUnixTime } from '../../../lib/Utils';
import { bitcoinClient, bitcoinLndClient, clnClient } from '../Nodes';
import { InvoiceFeature } from '../../../lib/lightning/LightningClient';
import InvoiceExpiryHelper from '../../../lib/service/InvoiceExpiryHelper';

describe('ClnClient', () => {
beforeAll(async () => {
Expand All @@ -27,6 +29,27 @@ describe('ClnClient', () => {
expect(invoice.startsWith('lnbcrt')).toBeTruthy();
});

test.each`
expiry
${60}
${1200}
${3600}
${43200}
`('should create invoices with expiry $expiry', async ({ expiry }) => {
const invoice = await clnClient.addHoldInvoice(
10_000,
randomBytes(32),
undefined,
expiry,
);
const { timestamp, timeExpireDate } = decodeInvoice(invoice);
expect(
getUnixTime() +
expiry -
InvoiceExpiryHelper.getInvoiceExpiry(timestamp, timeExpireDate),
).toBeLessThanOrEqual(5);
});

test('should fail settle for invalid states', async () => {
await expect(clnClient.settleHoldInvoice(randomBytes(32))).rejects.toEqual(
expect.anything(),
Expand Down
47 changes: 44 additions & 3 deletions test/integration/lightning/LndClient.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as grpc from '@grpc/grpc-js';
import { readFileSync } from 'fs';
import { randomBytes } from 'crypto';
import { getPort } from '../../Utils';
import Logger from '../../../lib/Logger';
import LndClient from '../../../lib/lightning/LndClient';
import { lndDataPath } from '../Nodes';
import { decodeInvoice, getUnixTime } from '../../../lib/Utils';
import { bitcoinClient, bitcoinLndClient, lndDataPath } from '../Nodes';
import InvoiceExpiryHelper from '../../../lib/service/InvoiceExpiryHelper';
import {
LightningClient,
LightningService,
Expand All @@ -15,6 +18,38 @@ import {
} from '../../../lib/proto/lnd/rpc_pb';

describe('LndClient', () => {
beforeAll(async () => {
await bitcoinClient.generate(1);
await bitcoinLndClient.connect(false);
});

afterAll(() => {
bitcoinLndClient.removeAllListeners();
bitcoinLndClient.disconnect();
bitcoinLndClient.disconnect();
});

test.each`
expiry
${60}
${1200}
${3600}
${43200}
`('should create invoices with expiry $expiry', async ({ expiry }) => {
const invoice = await bitcoinLndClient.addHoldInvoice(
10_000,
randomBytes(32),
undefined,
expiry,
);
const { timestamp, timeExpireDate } = decodeInvoice(invoice);
expect(
getUnixTime() +
expiry -
InvoiceExpiryHelper.getInvoiceExpiry(timestamp, timeExpireDate),
).toBeLessThanOrEqual(5);
});

test('should handle messages longer than the default gRPC limit', async () => {
// 4 MB is the default gRPC limit
const defaultGrpcLimit = 1024 * 1024 * 4;
Expand All @@ -31,13 +66,19 @@ describe('LndClient', () => {

// Define all needed methods of the LightningClient to work around gRPC throwing an error
for (const method of Object.keys(LightningClient['service'])) {
serviceImplementation[method] = async (_, callback) => {
serviceImplementation[method] = async (
_: any | null,
callback: (error: any, res: GetInfoResponse) => void,
) => {
// "GetInfo" is the only call the LndClient is using on startup
callback(null, new GetInfoResponse());
};
}

serviceImplementation['getTransactions'] = async (_, callback) => {
serviceImplementation['getTransactions'] = async (
_: any | null,
callback: (error: any, res: TransactionDetails) => void,
) => {
const response = new TransactionDetails();

const randomTransaction = new Transaction();
Expand Down
Loading

0 comments on commit d3c0a3d

Please sign in to comment.