Skip to content

Commit

Permalink
feat(taro): allow updating the LND backend of Taro nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
amovfx authored Feb 25, 2023
1 parent 832bd65 commit e030170
Show file tree
Hide file tree
Showing 27 changed files with 973 additions and 39 deletions.
5 changes: 3 additions & 2 deletions src/components/common/form/LightningNodeSelect.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('LightningNodeSelect', () => {
name="from"
label="Source"
initialValue={initialValue}
implementation="LND"
nodes={nodes}
/>
</Form>
Expand Down Expand Up @@ -56,7 +57,7 @@ describe('LightningNodeSelect', () => {
alice: {
walletBalance: defaultStateBalances({ confirmed: '100' }),
},
bob: {
dave: {
walletBalance: defaultStateBalances({ confirmed: '200' }),
},
};
Expand All @@ -71,7 +72,7 @@ describe('LightningNodeSelect', () => {
// click on bob option
// Select renders two lists of the options to the dom. click on the
// second one if it exists, otherwise click the only one
fireEvent.click(getAllByText('bob')[1]);
fireEvent.click(getAllByText('dave')[1]);
// confirm the balance updates
expect(await findByText('Balance: 200 sats')).toBeInTheDocument();
});
Expand Down
8 changes: 6 additions & 2 deletions src/components/common/form/LightningNodeSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react';
import { Form, Select } from 'antd';
import { SelectProps, SelectValue } from 'antd/lib/select';
import { usePrefixedTranslation } from 'hooks';
import { Status } from 'shared/types';
import { LightningNode, Status } from 'shared/types';
import { LightningNodeBalances } from 'lib/lightning/types';
import { LightningNodeModel } from 'store/models/lightning';
import { Network } from 'types';
Expand All @@ -13,6 +13,7 @@ export interface Props extends SelectProps<SelectValue> {
name: string;
label?: string;
nodeStatus?: Status;
implementation?: LightningNode['implementation'];
initialValue?: string;
nodes?: {
[key: string]: LightningNodeModel;
Expand All @@ -24,6 +25,7 @@ const LightningNodeSelect: React.FC<Props> = ({
name,
label,
nodeStatus,
implementation,
initialValue,
nodes,
onChange,
Expand Down Expand Up @@ -51,7 +53,9 @@ const LightningNodeSelect: React.FC<Props> = ({
if (nodeStatus !== undefined) {
lnNodes = lnNodes.filter(n => n.status === nodeStatus);
}

if (implementation) {
lnNodes = lnNodes.filter(n => n.implementation === implementation);
}
return (
<Form.Item
name={name}
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/form/TaroDataSelect.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from 'utils/tests';
import TaroDataSelect from './TaroDataSelect';

describe('TaroNodeSelect', () => {
describe('TaroDataSelect', () => {
const renderComponent = (selectBalances = true) => {
const network = getNetwork(1, 'test network', Status.Started, 2);
const initialState = {
Expand Down
45 changes: 45 additions & 0 deletions src/components/common/form/TaroNodeSelect.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { Form } from 'antd';
import { Status } from 'shared/types';
import { getNetwork } from 'utils/tests';
import TaroNodeSelect from './TaroNodeSelect';

describe('TaroNodeSelect', () => {
const renderComponent = () => {
const network = getNetwork(1, 'test network', Status.Stopped, 3);
const TestForm = () => {
const [form] = Form.useForm();
return (
<Form form={form}>
<TaroNodeSelect
network={network}
name="from"
label="Taro Nodes"
nodeStatus={Status.Stopped}
/>
</Form>
);
};
const result = render(<TestForm />);
return {
...result,
network,
};
};

it('should display the label and input', () => {
const { getByText, getByLabelText } = renderComponent();
expect(getByText('Taro Nodes')).toBeInTheDocument();
expect(getByLabelText('Taro Nodes')).toBeInTheDocument();
});

it('should display the nodes', async () => {
const { getAllByText, getByLabelText } = renderComponent();
fireEvent.mouseDown(getByLabelText('Taro Nodes'));
expect(getAllByText('alice-taro')[0]).toBeInTheDocument();
expect(getAllByText('bob-taro')[0]).toBeInTheDocument();
expect(getAllByText('carol-taro')[0]).toBeInTheDocument();
fireEvent.click(getAllByText('carol-taro')[0]);
});
});
52 changes: 52 additions & 0 deletions src/components/common/form/TaroNodeSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import { Form, Select } from 'antd';
import { SelectProps, SelectValue } from 'antd/lib/select';
import { usePrefixedTranslation } from 'hooks';
import { Status } from 'shared/types';
import { Network } from 'types';

export interface Props extends SelectProps<SelectValue> {
network: Network;
name: string;
label?: string;
nodeStatus?: Status;
initialValue?: string;
}

const TaroNodeSelect: React.FC<Props> = ({
network,
name,
label,
nodeStatus,
onChange,
...rest
}) => {
const { l } = usePrefixedTranslation('cmps.common.form.TaroNodeSelect');

const handleChange = (value: SelectValue, option: any) => {
if (onChange) onChange(value, option);
};

let taroNodes = network.nodes.taro;
if (nodeStatus !== undefined) {
taroNodes = taroNodes.filter(n => n.status === nodeStatus);
}

return (
<Form.Item
name={name}
label={label}
rules={[{ required: true, message: l('cmps.forms.required') }]}
>
<Select {...rest} onChange={handleChange}>
{taroNodes.map(node => (
<Select.Option key={node.name} value={node.name}>
{node.name}
</Select.Option>
))}
</Select>
</Form.Item>
);
};

export default TaroNodeSelect;
70 changes: 68 additions & 2 deletions src/components/designer/LinkContextMenu.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import React from 'react';
import { ILink } from '@mrblenny/react-flow-chart';
import { fireEvent } from '@testing-library/dom';
import { waitFor } from '@testing-library/react';
import { Status } from 'shared/types';
import { initChartFromNetwork } from 'utils/chart';
import * as files from 'utils/files';
import { getNetwork, renderWithProviders } from 'utils/tests';
import LinkContextMenu from './LinkContextMenu';

jest.mock('utils/files', () => ({
exists: jest.fn(),
}));
const filesMock = files as jest.Mocked<typeof files>;

describe('LinkContextMenu', () => {
const createChannelLink = (): ILink => ({
id: 'alice-carol',
Expand All @@ -28,14 +36,30 @@ describe('LinkContextMenu', () => {
type: 'backend',
},
});
const createTaroBackendLink = (): ILink => ({
id: 'alice-taro-alice',
from: { nodeId: 'alice-taro', portId: 'lndbackend' },
to: { nodeId: 'alice', portId: 'lndbackend' },
properties: {
type: 'lndbackend',
},
});
const renderComponent = (link: ILink, activeId?: number) => {
const network = getNetwork(1, 'test network');
const network = getNetwork(1, 'test network', Status.Started, 2);
const chart = initChartFromNetwork(network);
chart.links[link.id] = link;
const initialState = {
network: {
networks: [network],
},
taro: {
nodes: {
'alice-taro': {
assets: [],
balances: [],
},
},
},
designer: {
activeId: activeId || network.id,
allCharts: {
Expand All @@ -51,7 +75,7 @@ describe('LinkContextMenu', () => {
const result = renderWithProviders(cmp, { initialState });
// always open the context menu for all tests
fireEvent.contextMenu(result.getByText('test-child'));
return result;
return { ...result, network };
};

it('should not render menu with no network', () => {
Expand Down Expand Up @@ -81,4 +105,46 @@ describe('LinkContextMenu', () => {
const { queryByText } = renderComponent(link);
expect(queryByText('Close Channel')).not.toBeInTheDocument();
});
describe('Change Taro Backend Option', () => {
it('should display the correct options for a taro backend connection when network is stopped', async () => {
filesMock.exists.mockResolvedValue(Promise.resolve(false));
const { getByText, store, network } = renderComponent(createTaroBackendLink());
store.getActions().network.setStatus({ id: network.id, status: Status.Stopped });
await waitFor(() => {
expect(store.getState().network.networkById(network.id).status).toBe(
Status.Stopped,
);
});
fireEvent.click(getByText('Change Taro Backend'));
await waitFor(() => {
expect(store.getState().modals.changeTaroBackend.visible).toBe(true);
});
});
it('should display the correct options for a taro backend connection', async () => {
filesMock.exists.mockResolvedValue(Promise.resolve(false));
const { getByText } = renderComponent(createTaroBackendLink());
fireEvent.click(getByText('Change Taro Backend'));
await waitFor(() => {
expect(
getByText('The network must be stopped to change alice-taro bankend'),
).toBeInTheDocument();
});
});
it('should display an error when option is clicked', async () => {
filesMock.exists.mockResolvedValue(Promise.resolve(true));
const { getByText, store, network } = renderComponent(createTaroBackendLink());
expect(store.getState().modals.changeTaroBackend.visible).toBe(false);
await waitFor(() => {
store.getActions().network.setStatus({ id: network.id, status: Status.Started });
});
fireEvent.click(getByText('Change Taro Backend'));
await waitFor(() => {
expect(
getByText(
'Can only change Taro Backend before the network is started. admin.macaroon is present',
),
).toBeInTheDocument();
});
});
});
});
9 changes: 9 additions & 0 deletions src/components/designer/LinkContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Dropdown, MenuProps } from 'antd';
import { useStoreState } from 'store';
import { LinkProperties } from 'utils/chart';
import ChangeBackendButton from './link/ChangeBackendButton';
import ChangeTaroBackendButton from './link/ChangeTaroBackendButton';
import CloseChannelButton from './link/CloseChannelButton';

interface Props {
Expand Down Expand Up @@ -37,6 +38,14 @@ const LinkContextMenu: React.FC<Props> = ({ link, children }) => {
backendName={link.to.nodeId as string}
/>
);
} else if (type === 'lndbackend') {
menuItem = (
<ChangeTaroBackendButton
type="menu"
taroName={link.from.nodeId}
lndName={link.to.nodeId as string}
/>
);
}

// don't add a context menu if there is no menu item
Expand Down
9 changes: 9 additions & 0 deletions src/components/designer/NetworkDesigner.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ describe('NetworkDesigner Component', () => {
expect(await findByText('BOLT 11 Invoice')).toBeInTheDocument();
fireEvent.click(getByText('Cancel'));
});
it('should display the ChangeTaroBackend modal', async () => {
const { findAllByText, findByText, getAllByText, store } = renderComponent();
expect(await findByText('backend1')).toBeInTheDocument();
act(() => {
store.getActions().modals.showChangeTaroBackend({});
});
expect(await findAllByText('Change Taro Node Backend')).toHaveLength(1);
fireEvent.click(getAllByText('Cancel')[0]);
});

it('should display the ChangeBackend modal', async () => {
const { getByText, findByText, store } = renderComponent();
Expand Down
9 changes: 8 additions & 1 deletion src/components/designer/NetworkDesigner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import {
PayInvoiceModal,
} from './lightning/actions';
import Sidebar from './Sidebar';
import { MintAssetModal, NewAddressModal, SendAssetModal } from './taro/actions';
import {
ChangeTaroBackendModal,
MintAssetModal,
NewAddressModal,
SendAssetModal,
} from './taro/actions';

const Styled = {
Designer: styled.div`
Expand Down Expand Up @@ -55,6 +60,7 @@ const NetworkDesigner: React.FC<Props> = ({ network, updateStateDelay = 3000 })
changeBackend,
sendOnChain,
advancedOptions,
changeTaroBackend,
} = useStoreState(s => s.modals);

const { save } = useStoreActions(s => s.network);
Expand Down Expand Up @@ -100,6 +106,7 @@ const NetworkDesigner: React.FC<Props> = ({ network, updateStateDelay = 3000 })
{advancedOptions.visible && <AdvancedOptionsModal network={network} />}
{mintAsset.visible && <MintAssetModal network={network} />}
{newAddress.visible && <NewAddressModal network={network} />}
{changeTaroBackend.visible && <ChangeTaroBackendModal network={network} />}
{sendAsset.visible && <SendAssetModal network={network} />}
</Styled.Designer>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/designer/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const Sidebar: React.FC<Props> = ({ network, chart }) => {
}
} else if (type === 'link' && id) {
const link = chart.links[id];
return <LinkDetails link={link} network={network} />;
return link && <LinkDetails link={link} network={network} />;
}

return <DefaultSidebar />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ describe('ChangeBackendModal', () => {
expect(getByText('Cancel')).toBeInTheDocument();
changeSelect('Lightning Node', 'erin');
expect(getByText(warning)).toBeInTheDocument();
changeSelect('Lightning Node', 'alice');
expect(queryByText(warning)).not.toBeInTheDocument();
});

it('should not display the compatibility warning', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const ChangeBackendModal: React.FC<Props> = ({ network }) => {
const requiredVersion = compatibility[ln.version];
if (!isVersionCompatible(backend.version, requiredVersion)) {
setCompatWarning(l('compatWarning', { ln, backend, requiredVersion }));
} else {
setCompatWarning(undefined);
}
}
}
Expand Down
Loading

0 comments on commit e030170

Please sign in to comment.