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: custom invoice expiry for reverse swaps #669

Merged
merged 1 commit into from
Sep 27, 2024
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
6 changes: 6 additions & 0 deletions lib/api/v2/routers/SwapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,9 @@ class SwapRouter extends RouterBase {
* descriptionHash:
* type: string
* description: Description hash for the invoice. Takes precedence over "description" if both are specified
* invoiceExpiry:
* type: number
* description: Expiry of the invoice in seconds
* webhook:
* $ref: '#/components/schemas/WebhookData'
*/
Expand Down Expand Up @@ -1814,6 +1817,7 @@ class SwapRouter extends RouterBase {
routingNode,
preimageHash,
claimAddress,
invoiceExpiry,
invoiceAmount,
onchainAmount,
claimCovenant,
Expand All @@ -1831,6 +1835,7 @@ class SwapRouter extends RouterBase {
{ name: 'routingNode', type: 'string', optional: true },
{ name: 'claimAddress', type: 'string', optional: true },
{ name: 'invoiceAmount', type: 'number', optional: true },
{ name: 'invoiceExpiry', type: 'number', optional: true },
{ name: 'onchainAmount', type: 'number', optional: true },
{ name: 'claimCovenant', type: 'boolean', optional: true },
{ name: 'descriptionHash', type: 'string', hex: true, optional: true },
Expand All @@ -1856,6 +1861,7 @@ class SwapRouter extends RouterBase {
invoiceAmount,
onchainAmount,
claimCovenant,
invoiceExpiry,
claimPublicKey,
descriptionHash,
userAddress: address,
Expand Down
2 changes: 1 addition & 1 deletion lib/cli/ethereum/EthereumUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const getBoltzFilePath = (file: string): string =>
path.join(process.env.HOME!, '.boltz', file);

const getBoltzWallet = (): HDNodeWallet => {
for (const file in ['seedEvm.dat', 'seed.dat']) {
for (const file of ['seedEvm.dat', 'seed.dat']) {
const filePath = getBoltzFilePath(file);

if (existsSync(filePath)) {
Expand Down
30 changes: 23 additions & 7 deletions lib/service/InvoiceExpiryHelper.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { getPairId } from '../Utils';
import { PairConfig } from '../consts/Types';
import Errors from '../swap/Errors';
import TimeoutDeltaProvider from './TimeoutDeltaProvider';

class InvoiceExpiryHelper {
private static readonly defaultInvoiceExpiry = 3600;
private static readonly minInvoiceExpiry = 60;
private static readonly defaultInvoiceExpiry = 3_600;

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

Expand All @@ -28,12 +30,6 @@ class InvoiceExpiryHelper {
}
}

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

/**
* Calculates the expiry of an invoice
* Reference: https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#tagged-fields
Expand All @@ -55,6 +51,26 @@ class InvoiceExpiryHelper {

return invoiceExpiry;
};

public getExpiry = (pair: string, customExpiry?: number): number => {
michael1011 marked this conversation as resolved.
Show resolved Hide resolved
if (customExpiry !== undefined) {
if (!this.isValidExpiry(pair, customExpiry)) {
throw Errors.INVALID_INVOICE_EXPIRY();
}

return customExpiry;
}

return (
this.invoiceExpiry.get(pair) || InvoiceExpiryHelper.defaultInvoiceExpiry
);
};

private isValidExpiry = (pair: string, expiry: number) =>
expiry >= InvoiceExpiryHelper.minInvoiceExpiry &&
expiry <=
(this.invoiceExpiry.get(pair) ||
InvoiceExpiryHelper.defaultInvoiceExpiry);
}

export default InvoiceExpiryHelper;
3 changes: 3 additions & 0 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,8 @@ class Service {
descriptionHash?: Buffer;

webHook?: WebHookData;

invoiceExpiry?: number;
}): Promise<{
id: string;
invoice: string;
Expand Down Expand Up @@ -1782,6 +1784,7 @@ class Service {
userAddress: args.userAddress,
claimAddress: args.claimAddress,
preimageHash: args.preimageHash,
invoiceExpiry: args.invoiceExpiry,
claimPublicKey: args.claimPublicKey,
descriptionHash: args.descriptionHash,
claimCovenant: args.claimCovenant || false,
Expand Down
4 changes: 4 additions & 0 deletions lib/swap/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,8 @@ export default {
message: 'invalid description hash',
code: concatErrorCode(ErrorCodePrefix.Swap, 24),
}),
INVALID_INVOICE_EXPIRY: (): Error => ({
message: 'invalid invoice expiry',
code: concatErrorCode(ErrorCodePrefix.Swap, 25),
}),
};
4 changes: 3 additions & 1 deletion lib/swap/SwapManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,8 @@ class SwapManager {

memo?: string;
descriptionHash?: Buffer;

invoiceExpiry?: number;
}): Promise<CreatedReverseSwap> => {
const { sendingCurrency, receivingCurrency } = this.getCurrencies(
args.baseCurrency,
Expand Down Expand Up @@ -730,7 +732,7 @@ class SwapManager {
args.holdInvoiceAmount,
args.preimageHash,
args.lightningTimeoutBlockDelta,
this.invoiceExpiryHelper.getExpiry(pair),
this.invoiceExpiryHelper.getExpiry(pair, args.invoiceExpiry),
hints.invoiceMemo,
hints.invoiceDescriptionHash,
hints.routingHint,
Expand Down
4 changes: 4 additions & 0 deletions swagger-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -2364,6 +2364,10 @@
"type": "string",
"description": "Description hash for the invoice. Takes precedence over \"description\" if both are specified"
},
"invoiceExpiry": {
"type": "number",
"description": "Expiry of the invoice in seconds"
},
"webhook": {
"$ref": "#/components/schemas/WebhookData"
}
Expand Down
3 changes: 2 additions & 1 deletion test/unit/api/v2/routers/SwapRouter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,7 @@ describe('SwapRouter', () => {
${'could not parse hex string: preimageHash'} | ${{ to: 'L-BTC', from: 'BTC', preimageHash: 'notHex' }}
${'could not parse hex string: claimPublicKey'} | ${{ to: 'L-BTC', from: 'BTC', preimageHash: '00', claimPublicKey: 'notHex' }}
${'could not parse hex string: addressSignature'} | ${{ to: 'L-BTC', from: 'BTC', preimageHash: '00', claimPublicKey: '0011', addressSignature: 'notHex' }}
${'invalid parameter: invoiceExpiry'} | ${{ to: 'L-BTC', from: 'BTC', preimageHash: '00', claimPublicKey: '0011', invoiceExpiry: '123' }}
${'invalid parameter: description'} | ${{ to: 'L-BTC', from: 'BTC', preimageHash: '00', claimPublicKey: '0011', description: 123 }}
${'invalid parameter: claimCovenant'} | ${{ to: 'L-BTC', from: 'BTC', preimageHash: '00', claimPublicKey: '0011', claimCovenant: 123 }}
${'invalid parameter: claimCovenant'} | ${{ to: 'L-BTC', from: 'BTC', preimageHash: '00', claimPublicKey: '0011', claimCovenant: 'notBool' }}
Expand All @@ -905,7 +906,7 @@ describe('SwapRouter', () => {
);

test.each([1, 2, 3, 21, 31, 33, 64])(
'should not create reverse swaps preimage hash length != 32',
'should not create reverse swaps with preimage hash length != 32',
async (length) => {
await expect(
swapRouter['createReverse'](
Expand Down
30 changes: 30 additions & 0 deletions test/unit/service/InvoiceExpiryHelper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getPairId } from '../../../lib/Utils';
import { PairConfig } from '../../../lib/consts/Types';
import InvoiceExpiryHelper from '../../../lib/service/InvoiceExpiryHelper';
import TimeoutDeltaProvider from '../../../lib/service/TimeoutDeltaProvider';
import Errors from '../../../lib/swap/Errors';

jest.mock('../../../lib/service/TimeoutDeltaProvider', () => {
return jest.fn().mockImplementation(() => ({
Expand Down Expand Up @@ -61,6 +62,11 @@ describe('InvoiceExpiryHelper', () => {
},
);

test('should coalesce set invoice expiry', () => {
const expiry = 61;
expect(helper.getExpiry(getPairId(pairs[0]), expiry)).toEqual(expiry);
});

test('should use 50% of swap timeout for invoice expiry', () => {
expect(helper.getExpiry(getPairId(pairs[2]))).toEqual((144 * 10 * 60) / 2);
});
Expand All @@ -70,6 +76,15 @@ describe('InvoiceExpiryHelper', () => {
expect(helper.getExpiry('DOGE/BTC')).toEqual(3600);
});

test.each([-1, 0, 1, Number.MAX_SAFE_INTEGER])(
'should throw when custom expiry is invalid',
(expiry) => {
expect(() => helper.getExpiry('BTC/BTC', expiry)).toThrow(
Errors.INVALID_INVOICE_EXPIRY().message,
);
},
);

test.each`
timestamp | timeExpiryDate | expected
${120} | ${360} | ${360}
Expand All @@ -86,4 +101,19 @@ describe('InvoiceExpiryHelper', () => {
).toEqual(expected);
},
);

test.each`
expiry | valid
${-1} | ${false}
${0} | ${false}
${1} | ${false}
${59} | ${false}
${60} | ${true}
${61} | ${true}
${100} | ${true}
${123} | ${true}
${124} | ${false}
`('should determine if expiry $expiry is valid', ({ expiry, valid }) => {
expect(helper['isValidExpiry']('BTC/BTC', expiry)).toEqual(valid);
});
});
5 changes: 5 additions & 0 deletions test/unit/swap/SwapManager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ describe('SwapManager', () => {
deferredClaimSymbols: [],
} as any,
{} as any,
{} as any,
{} as any,
);

manager['currencies'].set(btcCurrency.symbol, btcCurrency);
Expand Down Expand Up @@ -1019,6 +1021,7 @@ describe('SwapManager', () => {
const onchainTimeoutBlockDelta = 140;
const lightningTimeoutBlockDelta = 143;
const percentageFee = 1;
const invoiceExpiry = 6_111;

const reverseSwap = await manager.createReverseSwap({
orderSide,
Expand All @@ -1027,6 +1030,7 @@ describe('SwapManager', () => {
quoteCurrency,
onchainAmount,
percentageFee,
invoiceExpiry,
holdInvoiceAmount,
onchainTimeoutBlockDelta,
lightningTimeoutBlockDelta,
Expand All @@ -1050,6 +1054,7 @@ describe('SwapManager', () => {
expect(mockGetExpiry).toHaveBeenCalledTimes(1);
expect(mockGetExpiry).toHaveBeenCalledWith(
getPairId({ base: baseCurrency, quote: quoteCurrency }),
invoiceExpiry,
);

expect(mockAddHoldInvoice).toHaveBeenCalledTimes(1);
Expand Down