Skip to content

Commit da3b727

Browse files
committed
loop: add additional options to swap wizard
1 parent c726126 commit da3b727

File tree

9 files changed

+174
-8
lines changed

9 files changed

+174
-8
lines changed

app/src/__tests__/components/loop/SwapWizard.spec.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import { values } from 'mobx';
23
import { SwapDirection } from 'types/state';
34
import { fireEvent } from '@testing-library/react';
45
import Big from 'big.js';
@@ -69,6 +70,46 @@ describe('SwapWizard component', () => {
6970
expect(+build.amountForSelected).toEqual(575000);
7071
expect(getByText(`575,000 sats`)).toBeInTheDocument();
7172
});
73+
74+
it('should show additional options', () => {
75+
const { getByText } = render();
76+
fireEvent.click(getByText('Additional Options'));
77+
expect(getByText('Hide Options')).toBeInTheDocument();
78+
});
79+
80+
it('should store the specified conf target', () => {
81+
const { getByText, getByPlaceholderText } = render();
82+
fireEvent.click(getByText('Additional Options'));
83+
fireEvent.change(getByPlaceholderText('number of blocks (ex: 6)'), {
84+
target: { value: 20 },
85+
});
86+
expect(store.buildSwapStore.confTarget).toBeUndefined();
87+
fireEvent.click(getByText('Next'));
88+
expect(store.buildSwapStore.confTarget).toBe(20);
89+
});
90+
91+
it('should store the specified destination address', () => {
92+
const { getByText, getByPlaceholderText } = render();
93+
fireEvent.click(getByText('Additional Options'));
94+
fireEvent.change(getByPlaceholderText('segwit address'), {
95+
target: { value: 'abcdef' },
96+
});
97+
expect(store.buildSwapStore.loopOutAddress).toBeUndefined();
98+
fireEvent.click(getByText('Next'));
99+
expect(store.buildSwapStore.loopOutAddress).toBe('abcdef');
100+
});
101+
102+
it('should handle invalid conf target', () => {
103+
const { getByText, getByPlaceholderText } = render();
104+
fireEvent.click(getByText('Additional Options'));
105+
fireEvent.change(getByPlaceholderText('number of blocks (ex: 6)'), {
106+
target: { value: 'asdf' },
107+
});
108+
fireEvent.click(getByText('Next'));
109+
expect(values(store.uiStore.alerts)[0].message).toBe(
110+
'Confirmation target must be between 20 and 60.',
111+
);
112+
});
72113
});
73114

74115
describe('Review Step', () => {

app/src/assets/icons/settings.svg

Lines changed: 4 additions & 0 deletions
Loading

app/src/components/base/icons.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ReactComponent as MaximizeIcon } from 'assets/icons/maximize.svg';
1818
import { ReactComponent as MenuIcon } from 'assets/icons/menu.svg';
1919
import { ReactComponent as MinimizeIcon } from 'assets/icons/minimize.svg';
2020
import { ReactComponent as RefreshIcon } from 'assets/icons/refresh-cw.svg';
21+
import { ReactComponent as SettingsIcon } from 'assets/icons/settings.svg';
2122
import { ReactComponent as CancelIcon } from 'assets/icons/slash.svg';
2223
import { styled } from 'components/theme';
2324

@@ -92,3 +93,4 @@ export const Menu = Icon.withComponent(MenuIcon);
9293
export const Minimize = Icon.withComponent(MinimizeIcon);
9394
export const Maximize = Icon.withComponent(MaximizeIcon);
9495
export const Refresh = Icon.withComponent(RefreshIcon);
96+
export const Settings = Icon.withComponent(SettingsIcon);

app/src/components/base/shared.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ export const Input = styled.input`
135135
&:focus {
136136
outline: none;
137137
background-color: ${props => props.theme.colors.overlay};
138+
border-bottom-color: ${props => props.theme.colors.white};
139+
}
140+
141+
&::placeholder {
142+
color: ${props => props.theme.colors.gray};
138143
}
139144
`;
140145

app/src/components/common/Tip.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ const TooltipWrapper: React.FC<Props> = ({
6868
* prop. So we basically proxy the className using the TooltipWrapper
6969
* above, then export this styled component for the rest of the app to use
7070
*/
71-
const Tip = styled(TooltipWrapper)<{ capitalize?: boolean }>`
71+
const Tip = styled(TooltipWrapper)<{ capitalize?: boolean; maxWidth?: number }>`
72+
max-width: ${props => (props.maxWidth ? `${props.maxWidth}px` : 'auto')};
7273
color: ${props => props.theme.colors.blue};
7374
font-family: ${props => props.theme.fonts.open.semiBold};
7475
font-size: ${props => props.theme.sizes.xs};

app/src/components/loop/swap/StepButtons.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,28 @@ import { Button } from 'components/base';
55

66
const Styled = {
77
Wrapper: styled.div`
8-
text-align: right;
8+
display: flex;
9+
justify-content: flex-end;
10+
`,
11+
ExtraContent: styled.div`
12+
flex: 1;
913
`,
1014
};
1115

1216
interface Props {
1317
confirm?: boolean;
1418
onCancel: () => void;
1519
onNext: () => void;
20+
extra?: React.ReactNode;
1621
}
1722

18-
const StepButtons: React.FC<Props> = ({ confirm, onCancel, onNext }) => {
23+
const StepButtons: React.FC<Props> = ({ confirm, onCancel, onNext, extra }) => {
1924
const { l } = usePrefixedTranslation('cmps.loop.swap.StepButtons');
2025

21-
const { Wrapper } = Styled;
26+
const { Wrapper, ExtraContent } = Styled;
2227
return (
2328
<Wrapper>
29+
<ExtraContent>{extra}</ExtraContent>
2430
<Button ghost borderless onClick={onCancel}>
2531
{l('cancel')}
2632
</Button>

app/src/components/loop/swap/SwapConfigStep.tsx

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import React from 'react';
1+
import React, { useCallback, useState } from 'react';
22
import { observer } from 'mobx-react-lite';
33
import { SwapDirection } from 'types/state';
44
import styled from '@emotion/styled';
55
import { usePrefixedTranslation } from 'hooks';
66
import { useStore } from 'store';
7+
import { Button, HeaderFour, HelpCircle, Input, Settings } from 'components/base';
78
import Range from 'components/common/Range';
9+
import Tip from 'components/common/Tip';
810
import StepButtons from './StepButtons';
911
import StepSummary from './StepSummary';
1012

@@ -27,13 +29,53 @@ const Styled = {
2729
flex-direction: column;
2830
justify-content: space-between;
2931
`,
32+
Options: styled.div<{ visible: boolean }>`
33+
padding: ${props => (props.visible ? '30px 0' : '0')};
34+
margin: 30px 0;
35+
background-color: #27273c;
36+
box-shadow: inset rgba(0, 0, 0, 0.5) 0px 0px 5px 0px;
37+
border-radius: 15px;
38+
display: flex;
39+
justify-content: space-between;
40+
overflow: hidden;
41+
height: ${props => (props.visible ? '125px' : '0')};
42+
transition: all 0.3s;
43+
44+
> div {
45+
flex: 1;
46+
margin: 0 30px;
47+
}
48+
`,
49+
SmallInput: styled(Input)`
50+
width: 100%;
51+
font-size: ${props => props.theme.sizes.s};
52+
text-align: left;
53+
border-bottom-width: 1px;
54+
`,
3055
};
3156

3257
const SwapConfigStep: React.FC = () => {
3358
const { l } = usePrefixedTranslation('cmps.loop.swap.SwapConfigStep');
34-
const { buildSwapStore } = useStore();
59+
const { buildSwapStore, uiStore } = useStore();
60+
const [confTarget, setConfTarget] = useState(
61+
(buildSwapStore.confTarget || '').toString(),
62+
);
63+
const [destAddress, setDestAddress] = useState(buildSwapStore.loopOutAddress || '');
3564

36-
const { Wrapper, Summary, Config } = Styled;
65+
const handleNext = useCallback(() => {
66+
try {
67+
if (buildSwapStore.addlOptionsVisible) {
68+
const target = confTarget !== '' ? parseInt(confTarget) : undefined;
69+
buildSwapStore.setConfTarget(target);
70+
buildSwapStore.setLoopOutAddress(destAddress);
71+
}
72+
buildSwapStore.goToNextStep();
73+
} catch (error) {
74+
uiStore.handleError(error);
75+
}
76+
}, [buildSwapStore, confTarget, destAddress, uiStore]);
77+
78+
const { Wrapper, Summary, Config, Options, SmallInput } = Styled;
3779
return (
3880
<Wrapper data-tour="loop-amount">
3981
<Summary>
@@ -56,9 +98,45 @@ const SwapConfigStep: React.FC = () => {
5698
step={buildSwapStore.AMOUNT_INCREMENT}
5799
onChange={buildSwapStore.setAmount}
58100
/>
101+
<Options visible={buildSwapStore.addlOptionsVisible}>
102+
<div>
103+
<HeaderFour>
104+
{l('confTargetLabel')}
105+
<Tip overlay={l('confTargetTip')} capitalize={false} maxWidth={400}>
106+
<HelpCircle />
107+
</Tip>
108+
</HeaderFour>
109+
<SmallInput
110+
placeholder={l('confTargetHint')}
111+
value={confTarget}
112+
onChange={e => setConfTarget(e.target.value)}
113+
/>
114+
</div>
115+
{buildSwapStore.direction === SwapDirection.OUT && (
116+
<div>
117+
<HeaderFour>
118+
{l('destAddrLabel')}
119+
<Tip overlay={l('destAddrTip')} capitalize={false} maxWidth={400}>
120+
<HelpCircle />
121+
</Tip>
122+
</HeaderFour>
123+
<SmallInput
124+
placeholder={l('destAddrHint')}
125+
value={destAddress}
126+
onChange={e => setDestAddress(e.target.value)}
127+
/>
128+
</div>
129+
)}
130+
</Options>
59131
<StepButtons
60132
onCancel={buildSwapStore.cancel}
61-
onNext={buildSwapStore.goToNextStep}
133+
onNext={handleNext}
134+
extra={
135+
<Button ghost borderless onClick={buildSwapStore.toggleAddlOptions}>
136+
<Settings />
137+
{buildSwapStore.addlOptionsVisible ? l('hideOptions') : l('addlOptions')}
138+
</Button>
139+
}
62140
/>
63141
</Config>
64142
</Wrapper>

app/src/i18n/locales/en-US.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@
5252
"cmps.loop.swap.SwapConfigStep.heading": "{{type}} Amount",
5353
"cmps.loop.swap.SwapConfigStep.loopInDesc": "Loop In moves funds onto the Lightning Network from an on-chain wallet to provide outbound capacity to send funds over Lightning. Use the slider to select the amount to Loop In or Loop Out of the Lightning Network",
5454
"cmps.loop.swap.SwapConfigStep.loopOutDesc": "Loop Out moves funds off of the Lightning Network into an on-chain address while providing inbound capacity to receive funds on Lightning. Use the slider to select the amount to Loop In or Loop Out of the Lightning Network",
55+
"cmps.loop.swap.SwapConfigStep.confTargetLabel": "Confirmation Target",
56+
"cmps.loop.swap.SwapConfigStep.confTargetTip": "The number of blocks to target for confirmation of the on-chain swap transaction. Specify a larger number to reduce fees.",
57+
"cmps.loop.swap.SwapConfigStep.confTargetHint": "number of blocks (ex: 6)",
58+
"cmps.loop.swap.SwapConfigStep.destAddrLabel": "Destination",
59+
"cmps.loop.swap.SwapConfigStep.destAddrTip": "The optional address that the looped out funds should be sent to. If left blank the funds will go to LND's wallet.",
60+
"cmps.loop.swap.SwapConfigStep.destAddrHint": "segwit address",
61+
"cmps.loop.swap.SwapConfigStep.addlOptions": "Additional Options",
62+
"cmps.loop.swap.SwapConfigStep.hideOptions": "Hide Options",
5563
"cmps.loop.swap.SwapProcessingStep.loadingMsg": "Submitting Loop",
5664
"cmps.loop.swap.SwapReviewStep.title": "Step 2 of 2",
5765
"cmps.loop.swap.SwapReviewStep.heading": "Review Loop amount and fee",

app/src/store/stores/buildSwapStore.ts

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

38+
/** determines whether to show the swap advanced options */
39+
@observable addlOptionsVisible = false;
40+
3841
/** the confirmation target of the on-chain txn used in the swap */
3942
@observable confTarget?: number;
4043

@@ -308,6 +311,18 @@ class BuildSwapStore {
308311
this.amount = amount;
309312
}
310313

314+
/**
315+
* toggles the advanced options section in the swap config step
316+
*/
317+
@action.bound
318+
toggleAddlOptions() {
319+
this.addlOptionsVisible = !this.addlOptionsVisible;
320+
this._store.log.info(
321+
`updated buildSwapStore.addlOptionsVisible`,
322+
this.addlOptionsVisible,
323+
);
324+
}
325+
311326
/**
312327
* Set the confirmation target for the swap
313328
*/
@@ -348,6 +363,11 @@ class BuildSwapStore {
348363
if (this.currentStep === BuildSwapSteps.ChooseAmount) {
349364
this.amount = this.amountForSelected;
350365
this.getQuote();
366+
// clear the advanced options if values were set, then hidden
367+
if (!this.addlOptionsVisible) {
368+
this.confTarget = undefined;
369+
this.loopOutAddress = undefined;
370+
}
351371
} else if (this.currentStep === BuildSwapSteps.ReviewQuote) {
352372
this.requestSwap();
353373
}
@@ -378,6 +398,7 @@ class BuildSwapStore {
378398
this.currentStep = BuildSwapSteps.Closed;
379399
this.selectedChanIds = [];
380400
this.amount = Big(0);
401+
this.addlOptionsVisible = false;
381402
this.confTarget = undefined;
382403
this.loopOutAddress = undefined;
383404
this.quote.swapFee = Big(0);

0 commit comments

Comments
 (0)