Skip to content

Commit

Permalink
feat: custom invoice expiry for reverse swaps
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Sep 25, 2024
1 parent c560000 commit 190467e
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 7 deletions.
14 changes: 14 additions & 0 deletions lib/api/v2/routers/SwapRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SwapRepository from '../../../db/repositories/SwapRepository';
import RateProviderTaproot from '../../../rates/providers/RateProviderTaproot';
import CountryCodes from '../../../service/CountryCodes';
import Errors from '../../../service/Errors';
import InvoiceExpiryHelper from '../../../service/InvoiceExpiryHelper';
import Service, { WebHookData } from '../../../service/Service';
import ChainSwapSigner from '../../../service/cooperative/ChainSwapSigner';
import MusigSigner, {
Expand Down Expand Up @@ -768,6 +769,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 +1818,7 @@ class SwapRouter extends RouterBase {
routingNode,
preimageHash,
claimAddress,
invoiceExpiry,
invoiceAmount,
onchainAmount,
claimCovenant,
Expand All @@ -1831,6 +1836,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 @@ -1841,6 +1847,13 @@ class SwapRouter extends RouterBase {

checkPreimageHashLength(preimageHash);

if (
invoiceExpiry !== undefined &&
!InvoiceExpiryHelper.isValidExpiry(invoiceExpiry)
) {
throw 'invalid invoice expiry';
}

const { pairId, orderSide } = this.service.convertToPairAndSide(from, to);
const webHookData = this.parseWebHook(webhook);

Expand All @@ -1856,6 +1869,7 @@ class SwapRouter extends RouterBase {
invoiceAmount,
onchainAmount,
claimCovenant,
invoiceExpiry,
claimPublicKey,
descriptionHash,
userAddress: address,
Expand Down
16 changes: 11 additions & 5 deletions lib/service/InvoiceExpiryHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,9 @@ class InvoiceExpiryHelper {
}
}

public getExpiry = (pair: string): number => {
return (
this.invoiceExpiry.get(pair) || InvoiceExpiryHelper.defaultInvoiceExpiry
);
};
// 43 200 seconds = 12 hours
public static isValidExpiry = (expiry: number) =>
expiry >= 60 && expiry <= 43_200;

/**
* Calculates the expiry of an invoice
Expand All @@ -55,6 +53,14 @@ class InvoiceExpiryHelper {

return invoiceExpiry;
};

public getExpiry = (pair: string, customExpiry?: number): number => {
return (
customExpiry ||
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: 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
21 changes: 20 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 All @@ -921,6 +922,24 @@ describe('SwapRouter', () => {
},
);

test.each([0, 1, 2, 60 * 60 * 24])(
'should not create reverse swaps with invalid invoice expiry',
async (invoiceExpiry) => {
await expect(
swapRouter['createReverse'](
mockRequest({
to: 'L-BTC',
from: 'BTC',
invoiceExpiry,
claimPublicKey: '21',
preimageHash: getHexString(randomBytes(32)),
}),
mockResponse(),
),
).rejects.toEqual('invalid invoice expiry');
},
);

test('should create reverse swaps', async () => {
const reqBody = {
to: 'L-BTC',
Expand Down
21 changes: 21 additions & 0 deletions test/unit/service/InvoiceExpiryHelper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ describe('InvoiceExpiryHelper', () => {
},
);

test('should coalesce set invoice expiry', () => {
const expiry = 6_111;
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 @@ -86,4 +91,20 @@ describe('InvoiceExpiryHelper', () => {
).toEqual(expected);
},
);

test.each`
expiry | valid
${-1} | ${false}
${0} | ${false}
${1} | ${false}
${59} | ${false}
${60} | ${true}
${61} | ${true}
${3_600} | ${true}
${60 * 60 * 12 - 1} | ${true}
${60 * 60 * 12} | ${true}
${60 * 60 * 12 + 1} | ${false}
`('should determine if expiry $expiry is valid', ({ expiry, valid }) => {
expect(InvoiceExpiryHelper.isValidExpiry(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

0 comments on commit 190467e

Please sign in to comment.