Skip to content

Commit c726126

Browse files
committed
loop: add swap params for conf target and dest addr
1 parent 75f0c83 commit c726126

File tree

4 files changed

+151
-8
lines changed

4 files changed

+151
-8
lines changed

app/src/__tests__/store/buildSwapStore.spec.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { waitFor } from '@testing-library/react';
55
import Big from 'big.js';
66
import { BalanceMode } from 'util/constants';
77
import { injectIntoGrpcUnary } from 'util/tests';
8-
import { lndChannel, loopInTerms } from 'util/tests/sampleData';
8+
import { lndChannel, loopInTerms, loopOutTerms } from 'util/tests/sampleData';
99
import { BuildSwapStore, createStore, Store } from 'store';
1010
import { Channel } from 'store/models';
1111
import { SWAP_ABORT_DELAY } from 'store/stores/buildSwapStore';
@@ -83,7 +83,12 @@ describe('BuildSwapStore', () => {
8383
expect(store.terms.out).toEqual({ min: Big(0), max: Big(0) });
8484
await store.getTerms();
8585
expect(store.terms.in).toEqual({ min: Big(250000), max: Big(1000000) });
86-
expect(store.terms.out).toEqual({ min: Big(250000), max: Big(1000000) });
86+
expect(store.terms.out).toEqual({
87+
min: Big(250000),
88+
max: Big(1000000),
89+
minCltv: 20,
90+
maxCltv: 60,
91+
});
8792
});
8893

8994
it('should handle errors fetching loop terms', async () => {
@@ -116,6 +121,68 @@ describe('BuildSwapStore', () => {
116121
expect(+store.amountForSelected).toBe(loopInTerms.maxSwapAmount);
117122
});
118123

124+
it('should validate the conf target', async () => {
125+
const { minCltvDelta, maxCltvDelta } = loopOutTerms;
126+
expect(store.confTarget).toBeUndefined();
127+
128+
let target = maxCltvDelta - 10;
129+
store.setConfTarget(target);
130+
expect(store.confTarget).toBe(target);
131+
132+
store.setDirection(SwapDirection.OUT);
133+
await store.getTerms();
134+
135+
store.setConfTarget(target);
136+
expect(store.confTarget).toBe(target);
137+
138+
target = minCltvDelta - 10;
139+
expect(() => store.setConfTarget(target)).toThrow();
140+
141+
target = maxCltvDelta + 10;
142+
expect(() => store.setConfTarget(target)).toThrow();
143+
});
144+
145+
it('should submit the Loop Out conf target', async () => {
146+
const target = 23;
147+
store.setDirection(SwapDirection.OUT);
148+
store.setAmount(Big(500000));
149+
150+
expect(store.confTarget).toBeUndefined();
151+
store.setConfTarget(target);
152+
expect(store.confTarget).toBe(target);
153+
154+
let reqTarget = '';
155+
// mock the grpc unary function in order to capture the supplied dest
156+
// passed in with the API request
157+
injectIntoGrpcUnary((desc, props) => {
158+
reqTarget = (props.request.toObject() as any).sweepConfTarget;
159+
});
160+
161+
store.requestSwap();
162+
await waitFor(() => expect(reqTarget).toBe(target));
163+
});
164+
165+
it('should submit the Loop Out address', async () => {
166+
const addr = 'xyzabc';
167+
store.setDirection(SwapDirection.OUT);
168+
store.setAmount(Big(500000));
169+
170+
expect(store.loopOutAddress).toBeUndefined();
171+
store.setLoopOutAddress(addr);
172+
expect(store.loopOutAddress).toBe(addr);
173+
// store.goToNextStep();
174+
175+
let reqAddr = '';
176+
// mock the grpc unary function in order to capture the supplied dest
177+
// passed in with the API request
178+
injectIntoGrpcUnary((desc, props) => {
179+
reqAddr = (props.request.toObject() as any).dest;
180+
});
181+
182+
store.requestSwap();
183+
await waitFor(() => expect(reqAddr).toBe(addr));
184+
});
185+
119186
it('should select all channels with the same peer for loop in', () => {
120187
const channels = rootStore.channelStore.sortedChannels;
121188
channels[1].remotePubkey = channels[0].remotePubkey;

app/src/api/loop.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,27 @@ class LoopApi extends BaseApi<LoopEvents> {
5151
/**
5252
* call the Loop `GetLoopInQuote` RPC and return the response
5353
*/
54-
async getLoopInQuote(amount: Big): Promise<LOOP.InQuoteResponse.AsObject> {
54+
async getLoopInQuote(
55+
amount: Big,
56+
confTarget?: number,
57+
): Promise<LOOP.InQuoteResponse.AsObject> {
5558
const req = new LOOP.QuoteRequest();
5659
req.setAmt(+amount);
60+
if (confTarget) req.setConfTarget(confTarget);
5761
const res = await this._grpc.request(SwapClient.GetLoopInQuote, req, this._meta);
5862
return res.toObject();
5963
}
6064

6165
/**
6266
* call the Loop `LoopOutQuote` RPC and return the response
6367
*/
64-
async getLoopOutQuote(amount: Big): Promise<LOOP.OutQuoteResponse.AsObject> {
68+
async getLoopOutQuote(
69+
amount: Big,
70+
confTarget?: number,
71+
): Promise<LOOP.OutQuoteResponse.AsObject> {
6572
const req = new LOOP.QuoteRequest();
6673
req.setAmt(+amount);
74+
if (confTarget) req.setConfTarget(confTarget);
6775
const res = await this._grpc.request(SwapClient.LoopOutQuote, req, this._meta);
6876
return res.toObject();
6977
}
@@ -75,12 +83,14 @@ class LoopApi extends BaseApi<LoopEvents> {
7583
amount: Big,
7684
quote: Quote,
7785
lastHop?: string,
86+
confTarget?: number,
7887
): Promise<LOOP.SwapResponse.AsObject> {
7988
const req = new LOOP.LoopInRequest();
8089
req.setAmt(+amount);
8190
req.setMaxSwapFee(+quote.swapFee);
8291
req.setMaxMinerFee(+quote.minerFee);
8392
if (lastHop) req.setLastHop(Buffer.from(lastHop, 'hex').toString('base64'));
93+
if (confTarget) req.setHtlcConfTarget(confTarget);
8494
const res = await this._grpc.request(SwapClient.LoopIn, req, this._meta);
8595
return res.toObject();
8696
}
@@ -93,6 +103,8 @@ class LoopApi extends BaseApi<LoopEvents> {
93103
quote: Quote,
94104
chanIds: number[],
95105
deadline: number,
106+
confTarget?: number,
107+
destAddress?: string,
96108
): Promise<LOOP.SwapResponse.AsObject> {
97109
const req = new LOOP.LoopOutRequest();
98110
req.setAmt(+amount);
@@ -103,6 +115,8 @@ class LoopApi extends BaseApi<LoopEvents> {
103115
req.setMaxPrepayRoutingFee(this._calcRoutingFee(+quote.prepayAmount));
104116
req.setOutgoingChanSetList(chanIds);
105117
req.setSwapPublicationDeadline(deadline);
118+
if (confTarget) req.setSweepConfTarget(confTarget);
119+
if (destAddress) req.setDest(destAddress);
106120

107121
const res = await this._grpc.request(SwapClient.LoopOut, req, this._meta);
108122
return res.toObject();

app/src/store/stores/buildSwapStore.ts

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ class BuildSwapStore {
3535
/** the amount to swap */
3636
@observable amount: Big = Big(0);
3737

38+
/** the confirmation target of the on-chain txn used in the swap */
39+
@observable confTarget?: number;
40+
41+
/** the on-chain address to send funds to during a loop out swap */
42+
@observable loopOutAddress?: string;
43+
3844
/** the min/max amount this node is allowed to swap */
3945
@observable terms: SwapTerms = {
4046
in: { min: Big(0), max: Big(0) },
@@ -302,6 +308,38 @@ class BuildSwapStore {
302308
this.amount = amount;
303309
}
304310

311+
/**
312+
* Set the confirmation target for the swap
313+
*/
314+
@action.bound
315+
setConfTarget(target?: number) {
316+
// ensure the Loop Out target is between the CLTV min & max
317+
if (
318+
this.direction === SwapDirection.OUT &&
319+
this.terms.out.minCltv &&
320+
this.terms.out.maxCltv &&
321+
target !== undefined &&
322+
(isNaN(target) ||
323+
this.terms.out.minCltv > target ||
324+
this.terms.out.maxCltv < target)
325+
) {
326+
throw new Error(
327+
`Confirmation target must be between ${this.terms.out.minCltv} and ${this.terms.out.maxCltv}.`,
328+
);
329+
}
330+
this.confTarget = target;
331+
this._store.log.info(`updated buildSwapStore.confTarget`, this.confTarget);
332+
}
333+
334+
/**
335+
* Set the on-chain destination address for the loop out swap
336+
*/
337+
@action.bound
338+
setLoopOutAddress(address: string) {
339+
this.loopOutAddress = address;
340+
this._store.log.info(`updated buildSwapStore.loopOutAddress`, this.loopOutAddress);
341+
}
342+
305343
/**
306344
* Navigate to the next step in the wizard
307345
*/
@@ -340,6 +378,8 @@ class BuildSwapStore {
340378
this.currentStep = BuildSwapSteps.Closed;
341379
this.selectedChanIds = [];
342380
this.amount = Big(0);
381+
this.confTarget = undefined;
382+
this.loopOutAddress = undefined;
343383
this.quote.swapFee = Big(0);
344384
this.quote.minerFee = Big(0);
345385
this.quote.prepayAmount = Big(0);
@@ -364,6 +404,8 @@ class BuildSwapStore {
364404
out: {
365405
min: Big(outTerms.minSwapAmount),
366406
max: Big(outTerms.maxSwapAmount),
407+
minCltv: outTerms.minCltvDelta,
408+
maxCltv: outTerms.maxCltvDelta,
367409
},
368410
};
369411
this._store.log.info('updated store.terms', toJS(this.terms));
@@ -385,14 +427,20 @@ class BuildSwapStore {
385427
try {
386428
let quote: Quote;
387429
if (direction === SwapDirection.IN) {
388-
const inQuote = await this._store.api.loop.getLoopInQuote(amount);
430+
const inQuote = await this._store.api.loop.getLoopInQuote(
431+
amount,
432+
this.confTarget,
433+
);
389434
quote = {
390435
swapFee: Big(inQuote.swapFeeSat),
391436
minerFee: Big(inQuote.htlcPublishFeeSat),
392437
prepayAmount: Big(0),
393438
};
394439
} else {
395-
const outQuote = await this._store.api.loop.getLoopOutQuote(amount);
440+
const outQuote = await this._store.api.loop.getLoopOutQuote(
441+
amount,
442+
this.confTarget,
443+
);
396444
quote = {
397445
swapFee: Big(outQuote.swapFeeSat),
398446
minerFee: Big(outQuote.htlcSweepFeeSat),
@@ -430,7 +478,12 @@ class BuildSwapStore {
430478
try {
431479
let res: SwapResponse.AsObject;
432480
if (direction === SwapDirection.IN) {
433-
res = await this._store.api.loop.loopIn(amount, quote, this.loopInLastHop);
481+
res = await this._store.api.loop.loopIn(
482+
amount,
483+
quote,
484+
this.loopInLastHop,
485+
this.confTarget,
486+
);
434487
// save the channels that were used in the swap. for Loop In all channels
435488
// with the same peer will be used
436489
this._store.swapStore.addSwappedChannels(res.id, this.selectedChanIds);
@@ -442,7 +495,14 @@ class BuildSwapStore {
442495
this._store.nodeStore.network === 'regtest' ? 0 : Date.now() + thirtyMins;
443496
// convert the selected channel ids to numbers
444497
const chanIds = this.selectedChanIds.map(v => parseInt(v));
445-
res = await this._store.api.loop.loopOut(amount, quote, chanIds, deadline);
498+
res = await this._store.api.loop.loopOut(
499+
amount,
500+
quote,
501+
chanIds,
502+
deadline,
503+
this.confTarget,
504+
this.loopOutAddress,
505+
);
446506
// save the channels that were used in the swap
447507
this._store.swapStore.addSwappedChannels(res.id, this.selectedChanIds);
448508
}

app/src/types/state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export interface SwapTerms {
1919
out: {
2020
min: Big;
2121
max: Big;
22+
minCltv?: number;
23+
maxCltv?: number;
2224
};
2325
}
2426

0 commit comments

Comments
 (0)