Browse Source

refactor(network): update Network type for multiple lightning nodes

master
jamaljsr 5 years ago
parent
commit
641cb55404
  1. 1
      .gitignore
  2. 3
      src/components/designer/Sidebar.tsx
  3. 3
      src/components/designer/lnd/LndDetails.spec.tsx
  4. 4
      src/components/designer/lnd/actions/Deposit.spec.tsx
  5. 11
      src/components/designer/lnd/actions/OpenChannelModal.tsx
  6. 4
      src/components/designer/lnd/actions/RemoveNode.spec.tsx
  7. 3
      src/components/terminal/OpenTerminalButton.spec.tsx
  8. 3
      src/lib/docker/composeFile.spec.ts
  9. 2
      src/lib/docker/dockerService.spec.ts
  10. 3
      src/lib/lnd/lndProxyClient.spec.ts
  11. 5
      src/lib/lnd/lndService.spec.ts
  12. 1
      src/shared/types.ts
  13. 4
      src/store/models/designer.ts
  14. 3
      src/store/models/lnd.spec.ts
  15. 35
      src/store/models/network.ts
  16. 4
      src/types/index.ts
  17. 4
      src/utils/chart.ts
  18. 4
      src/utils/network.spec.ts
  19. 48
      src/utils/network.ts

1
.gitignore

@ -82,6 +82,7 @@ typings/
.env.development.local
.env.test.local
.env.production.local
docker/docker-compose.yml
npm-debug.log*
yarn-debug.log*

3
src/components/designer/Sidebar.tsx

@ -5,6 +5,7 @@ import BitcoindDetails from './bitcoind/BitcoindDetails';
import DefaultSidebar from './default/DefaultSidebar';
import LinkDetails from './link/LinkDetails';
import LndDetails from './lnd/LndDetails';
import { LndNode } from 'shared/types';
interface Props {
network: Network;
@ -21,7 +22,7 @@ const Sidebar: React.FC<Props> = ({ network, chart }) => {
if (node && node.implementation === 'bitcoind') {
return <BitcoindDetails node={node} />;
} else if (node && node.implementation === 'LND') {
return <LndDetails node={node} />;
return <LndDetails node={node as LndNode} />;
}
} else if (type === 'link' && id) {
const link = chart.links[id];

3
src/components/designer/lnd/LndDetails.spec.tsx

@ -5,6 +5,7 @@ import { defaultInfo, defaultListChannels, defaultPendingChannels } from 'shared
import { Status } from 'shared/types';
import { LndLibrary } from 'types';
import * as files from 'utils/files';
import { groupNodes } from 'utils/network';
import { getNetwork, injections, renderWithProviders } from 'utils/tests';
import LndDetails from './LndDetails';
@ -26,7 +27,7 @@ describe('LndDetails', () => {
},
},
};
const node = network.nodes.lightning[0];
const node = groupNodes(network).lnd[0];
const cmp = <LndDetails node={node} />;
const result = renderWithProviders(cmp, { initialState });
return {

4
src/components/designer/lnd/actions/Deposit.spec.tsx

@ -2,6 +2,7 @@ import React from 'react';
import { fireEvent, waitForElement } from '@testing-library/dom';
import { defaultInfo } from 'shared';
import { BitcoindLibrary, LndLibrary } from 'types';
import { groupNodes } from 'utils/network';
import { getNetwork, injections, renderWithProviders } from 'utils/tests';
import { Deposit } from './';
@ -16,7 +17,8 @@ describe('Deposit', () => {
networks: [network],
},
};
const cmp = <Deposit node={network.nodes.lightning[0]} />;
const node = groupNodes(network).lnd[0];
const cmp = <Deposit node={node} />;
const result = renderWithProviders(cmp, { initialState });
return {
...result,

11
src/components/designer/lnd/actions/OpenChannelModal.tsx

@ -6,6 +6,7 @@ import { usePrefixedTranslation } from 'hooks';
import { useStoreActions, useStoreState } from 'store';
import { OpenChannelPayload } from 'store/models/lnd';
import { Network } from 'types';
import { groupNodes } from 'utils/network';
import { Loader } from 'components/common';
import LightningNodeSelect from 'components/common/form/LightningNodeSelect';
@ -30,8 +31,8 @@ const OpenChannelModal: React.FC<Props> = ({ network, form }) => {
const getBalancesAsync = useAsync(async () => {
if (!visible) return;
const { lightning } = network.nodes;
for (const node of lightning) {
const { lnd } = groupNodes(network);
for (const node of lnd) {
await getWalletBalance(node);
}
}, [network.nodes, visible]);
@ -60,9 +61,9 @@ const OpenChannelModal: React.FC<Props> = ({ network, form }) => {
form.validateFields((err, values: FormFields) => {
if (err) return;
const { lightning } = network.nodes;
const fromNode = lightning.find(n => n.name === values.from);
const toNode = lightning.find(n => n.name === values.to);
const { lnd } = groupNodes(network);
const fromNode = lnd.find(n => n.name === values.from);
const toNode = lnd.find(n => n.name === values.to);
if (!fromNode || !toNode) return;
const autoFund = showDeposit && values.autoFund;
openChanAsync.execute({ from: fromNode, to: toNode, sats: values.sats, autoFund });

4
src/components/designer/lnd/actions/RemoveNode.spec.tsx

@ -4,6 +4,7 @@ import { ipcChannels, withDefaults } from 'shared';
import { Status } from 'shared/types';
import { DockerLibrary, LndLibrary } from 'types';
import { initChartFromNetwork } from 'utils/chart';
import { groupNodes } from 'utils/network';
import {
getNetwork,
injections,
@ -32,7 +33,8 @@ describe('RemoveNode', () => {
activeId: 1,
},
};
const node = network.nodes.lightning[status === Status.Started ? 0 : 1];
const { lnd } = groupNodes(network);
const node = lnd[status === Status.Started ? 0 : 1];
const cmp = <RemoveNode node={node} />;
const result = renderWithProviders(cmp, { initialState });
return {

3
src/components/terminal/OpenTerminalButton.spec.tsx

@ -3,6 +3,7 @@ import { fireEvent, wait } from '@testing-library/dom';
import { ipcChannels } from 'shared';
import { BitcoinNode, LndNode } from 'shared/types';
import { Network } from 'types';
import { groupNodes } from 'utils/network';
import { getNetwork, injections, renderWithProviders } from 'utils/tests';
import OpenTerminalButton from './OpenTerminalButton';
@ -29,7 +30,7 @@ describe('OpenTerminalButton', () => {
});
it('should render lnd help text', () => {
const { getByText } = renderComponent(n => n.nodes.lightning[0]);
const { getByText } = renderComponent(n => groupNodes(n).lnd[0]);
const help = getByText("Run 'lncli' commands directly on the node");
expect(help).toBeInTheDocument();
});

3
src/lib/docker/composeFile.spec.ts

@ -1,3 +1,4 @@
import { groupNodes } from 'utils/network';
import { getNetwork } from 'utils/tests';
import ComposeFile from './composeFile';
@ -5,7 +6,7 @@ describe('ComposeFile', () => {
let composeFile = new ComposeFile();
const network = getNetwork();
const btcNode = network.nodes.bitcoin[0];
const lndNode = network.nodes.lightning[0];
const lndNode = groupNodes(network).lnd[0];
beforeEach(() => {
composeFile = new ComposeFile();

2
src/lib/docker/dockerService.spec.ts

@ -180,7 +180,7 @@ describe('DockerService', () => {
});
it('should not save unknown lightning implementation', () => {
network.nodes.lightning[0].implementation = 'c-lightning';
network.nodes.lightning[0].implementation = 'eclair';
dockerService.saveComposeFile(network);
expect(filesMock.write).toBeCalledWith(
expect.stringContaining('docker-compose.yml'),

3
src/lib/lnd/lndProxyClient.spec.ts

@ -1,10 +1,11 @@
import { ipcChannels } from 'shared';
import { IpcSender } from 'lib/ipc/ipcService';
import { groupNodes } from 'utils/network';
import { getNetwork } from 'utils/tests';
import lndProxyClient from './lndProxyClient';
describe('LndService', () => {
const node = getNetwork().nodes.lightning[0];
const node = groupNodes(getNetwork()).lnd[0];
let actualIpc: IpcSender;
beforeEach(() => {

5
src/lib/lnd/lndService.spec.ts

@ -1,3 +1,4 @@
import { groupNodes } from 'utils/network';
import { getNetwork } from 'utils/tests';
import lndProxyClient from './lndProxyClient';
import lndService from './lndService';
@ -5,7 +6,7 @@ import lndService from './lndService';
jest.mock('./lndProxyClient');
describe('LndService', () => {
const [node, node2] = getNetwork().nodes.lightning;
const [node, node2] = groupNodes(getNetwork()).lnd;
it('should get node info', async () => {
const expected = { identityPubkey: 'asdf' };
@ -51,7 +52,7 @@ describe('LndService', () => {
it('should call onNodesDeleted', async () => {
const network = getNetwork();
await lndService.onNodesDeleted(network.nodes.lightning);
await lndService.onNodesDeleted(groupNodes(network).lnd);
const [n1, n2] = network.nodes.lightning;
expect(lndProxyClient.onNodesDeleted).toBeCalledWith([n1, n2]);
});

1
src/shared/types.ts

@ -21,6 +21,7 @@ export interface LightningNode extends CommonNode {
type: 'lightning';
implementation: 'LND' | 'c-lightning' | 'eclair';
backendName: string;
ports: Record<string, number | undefined>;
}
export enum LndVersion {

4
src/store/models/designer.ts

@ -15,6 +15,7 @@ import { LndNode, Status } from 'shared/types';
import { Network, StoreInjections } from 'types';
import { createLndChartNode, rotate, snap, updateChartFromLnd } from 'utils/chart';
import { LOADING_NODE_ID } from 'utils/constants';
import { groupNodes } from 'utils/network';
import { prefixTranslation } from 'utils/translate';
import { RootModel } from './';
@ -97,8 +98,7 @@ const designerModel: DesignerModel = {
syncChart: thunk(
async (actions, network, { getState, getStoreState, getStoreActions }) => {
// fetch data from all of the nodes
const lndNodes = network.nodes.lightning.filter(n => n.implementation === 'LND');
await Promise.all(lndNodes.map(getStoreActions().lnd.getAllInfo));
await Promise.all(groupNodes(network).lnd.map(getStoreActions().lnd.getAllInfo));
const nodesData = getStoreState().lnd.nodes;
const { allCharts } = getState();

3
src/store/models/lnd.spec.ts

@ -1,6 +1,7 @@
import * as LND from '@radar/lnrpc';
import { createStore } from 'easy-peasy';
import { defaultInfo, ipcChannels, withDefaults } from 'shared';
import { LndNode } from 'shared/types';
import { BitcoindLibrary, LndLibrary } from 'types';
import * as asyncUtil from 'utils/async';
import { getNetwork, injections } from 'utils/tests';
@ -24,7 +25,7 @@ describe('LND Model', () => {
};
// initialize store for type inference
let store = createStore(rootModel, { injections, initialState });
const node = initialState.network.networks[0].nodes.lightning[0];
const node = initialState.network.networks[0].nodes.lightning[0] as LndNode;
beforeEach(() => {
// reset the store before each test run

35
src/store/models/network.ts

@ -2,7 +2,7 @@ import { info } from 'electron-log';
import { join } from 'path';
import { push } from 'connected-react-router';
import { Action, action, Computed, computed, Thunk, thunk } from 'easy-peasy';
import { CommonNode, LndNode, LndVersion, Status } from 'shared/types';
import { CommonNode, LightningNode, LndNode, LndVersion, Status } from 'shared/types';
import { Network, StoreInjections } from 'types';
import { initChartFromNetwork } from 'utils/chart';
import { rm } from 'utils/files';
@ -10,6 +10,7 @@ import {
createLndNetworkNode,
createNetwork,
getOpenPorts,
groupNodes,
OpenPorts,
} from 'utils/network';
import { prefixTranslation } from 'utils/translate';
@ -46,7 +47,7 @@ export interface NetworkModel {
RootModel,
Promise<LndNode>
>;
removeNode: Thunk<NetworkModel, { node: LndNode }, StoreInjections, RootModel>;
removeNode: Thunk<NetworkModel, { node: LightningNode }, StoreInjections, RootModel>;
setStatus: Action<
NetworkModel,
{ id: number; status: Status; only?: string; all?: boolean; error?: Error }
@ -138,13 +139,18 @@ const networkModel: NetworkModel = {
const network = networks.find(n => n.id === node.networkId);
if (!network) throw new Error(l('networkByIdErr', { networkId: node.networkId }));
network.nodes.lightning = network.nodes.lightning.filter(n => n !== node);
getStoreActions().lnd.removeNode(node.name);
if (node.implementation === 'LND') getStoreActions().lnd.removeNode(node.name);
await injections.dockerService.removeNode(network, node);
getStoreActions().designer.removeNode(node.name);
actions.setNetworks([...networks]);
await actions.save();
if (node.implementation === 'LND') {
rm(join(network.path, 'volumes', 'lnd', node.name));
await injections.lndService.onNodesDeleted([node, ...network.nodes.lightning]);
await injections.lndService.onNodesDeleted([
node as LndNode,
...groupNodes(network).lnd,
]);
}
if (network.status === Status.Started) {
getStoreActions().designer.syncChart(network);
}
@ -193,27 +199,28 @@ const networkModel: NetworkModel = {
await getStoreActions().app.getDockerImages();
// set the status of only the network to Started
actions.setStatus({ id, status: Status.Started, all: false });
const { lnd } = groupNodes(network);
// wait for lnd nodes to come online before updating their status
for (const lnd of network.nodes.lightning) {
for (const node of lnd) {
// use .then() to continue execution while the promises are waiting to complete
injections.lndService
.waitUntilOnline(lnd)
.then(() => actions.setStatus({ id, status: Status.Started, only: lnd.name }))
.waitUntilOnline(node)
.then(() => actions.setStatus({ id, status: Status.Started, only: node.name }))
.catch(error =>
actions.setStatus({ id, status: Status.Error, only: lnd.name, error }),
actions.setStatus({ id, status: Status.Error, only: node.name, error }),
);
}
// wait for bitcoind nodes to come online before updating their status
for (const bitcoind of network.nodes.bitcoin) {
for (const node of network.nodes.bitcoin) {
// use .then() to continue execution while the promises are waiting to complete
injections.bitcoindService
.waitUntilOnline(bitcoind.ports.rpc)
.waitUntilOnline(node.ports.rpc)
.then(() => {
actions.setStatus({ id, status: Status.Started, only: bitcoind.name });
return getStoreActions().bitcoind.getInfo(bitcoind);
actions.setStatus({ id, status: Status.Started, only: node.name });
return getStoreActions().bitcoind.getInfo(node);
})
.catch(error =>
actions.setStatus({ id, status: Status.Error, only: bitcoind.name, error }),
actions.setStatus({ id, status: Status.Error, only: node.name, error }),
);
}
} catch (e) {
@ -266,7 +273,7 @@ const networkModel: NetworkModel = {
actions.setNetworks(newNetworks);
getStoreActions().designer.removeChart(networkId);
await actions.save();
await injections.lndService.onNodesDeleted(network.nodes.lightning);
await injections.lndService.onNodesDeleted(groupNodes(network).lnd);
}),
};

4
src/types/index.ts

@ -1,7 +1,7 @@
import { IChart } from '@mrblenny/react-flow-chart';
import * as LND from '@radar/lnrpc';
import { ChainInfo, WalletInfo } from 'bitcoin-core';
import { BitcoinNode, CommonNode, LndNode, Status } from 'shared/types';
import { BitcoinNode, CommonNode, LightningNode, LndNode, Status } from 'shared/types';
import { IpcSender } from 'lib/ipc/ipcService';
export interface LocaleConfig {
@ -18,7 +18,7 @@ export interface Network {
path: string;
nodes: {
bitcoin: BitcoinNode[];
lightning: LndNode[];
lightning: LightningNode[];
};
}

4
src/utils/chart.ts

@ -1,6 +1,6 @@
import { IChart, IConfig, ILink, INode, IPosition } from '@mrblenny/react-flow-chart';
import { Channel, PendingChannel } from '@radar/lnrpc';
import { BitcoinNode, LndNode } from 'shared/types';
import { BitcoinNode, LightningNode } from 'shared/types';
import { LndNodeMapping } from 'store/models/lnd';
import { Network } from 'types';
import btclogo from 'resources/bitcoin.svg';
@ -34,7 +34,7 @@ export const snap = (position: IPosition, config?: IConfig) =>
? { x: Math.round(position.x / 20) * 20, y: Math.round(position.y / 20) * 20 }
: position;
export const createLndChartNode = (lnd: LndNode) => {
export const createLndChartNode = (lnd: LightningNode) => {
const node: INode = {
id: lnd.name,
type: 'lightning',

4
src/utils/network.spec.ts

@ -1,5 +1,5 @@
import detectPort from 'detect-port';
import { Status } from 'shared/types';
import { LndNode, Status } from 'shared/types';
import { Network } from 'types';
import { getOpenPortRange, getOpenPorts, OpenPorts } from './network';
import { getNetwork } from './tests';
@ -77,7 +77,7 @@ describe('Network Utils', () => {
// alice ports should not be changed
expect(ports[network.nodes.lightning[0].name]).toBeUndefined();
// bob ports should change
const lnd2 = network.nodes.lightning[1];
const lnd2 = network.nodes.lightning[1] as LndNode;
expect(ports[lnd2.name].grpc).toBe(lnd2.ports.grpc + 1);
expect(ports[lnd2.name].rest).toBe(lnd2.ports.rest + 1);
});

48
src/utils/network.ts

@ -1,6 +1,13 @@
import { join } from 'path';
import detectPort from 'detect-port';
import { BitcoinNode, CommonNode, LndNode, LndVersion, Status } from 'shared/types';
import {
BitcoinNode,
CommonNode,
LightningdNode,
LndNode,
LndVersion,
Status,
} from 'shared/types';
import { Network } from 'types';
import { networksPath } from './config';
import { getName } from './names';
@ -9,6 +16,18 @@ import { range } from './numbers';
export const getContainerName = (node: CommonNode) =>
`polar-n${node.networkId}-${node.name}`;
export const groupNodes = (network: Network) => {
const { bitcoin, lightning } = network.nodes;
return {
bitcoind: bitcoin.filter(n => n.implementation === 'bitcoind') as BitcoinNode[],
lnd: lightning.filter(n => n.implementation === 'LND') as LndNode[],
lightningd: lightning.filter(
n => n.implementation === 'c-lightning',
) as LightningdNode[],
eclair: lightning.filter(n => n.implementation === 'eclair'),
};
};
// long path games
const getFilePaths = (name: string, network: Network) => {
// returns /volumes/lnd/lnd-1
@ -176,29 +195,42 @@ export const getOpenPorts = async (network: Network): Promise<OpenPorts | undefi
}
}
let { lnd, lightningd } = groupNodes(network);
// filter out nodes that are already started since their ports are in use by themselves
const lightning = network.nodes.lightning.filter(n => n.status !== Status.Started);
if (lightning.length) {
let existingPorts = lightning.map(n => n.ports.grpc);
lnd = lnd.filter(n => n.status !== Status.Started);
if (lnd.length) {
let existingPorts = lnd.map(n => n.ports.grpc);
let openPorts = await getOpenPortRange(existingPorts);
if (openPorts.join() !== existingPorts.join()) {
openPorts.forEach((port, index) => {
ports[lightning[index].name] = { grpc: port };
ports[lnd[index].name] = { grpc: port };
});
}
existingPorts = lightning.map(n => n.ports.rest);
existingPorts = lnd.map(n => n.ports.rest);
openPorts = await getOpenPortRange(existingPorts);
if (openPorts.join() !== existingPorts.join()) {
openPorts.forEach((port, index) => {
ports[lightning[index].name] = {
...(ports[lightning[index].name] || {}),
ports[lnd[index].name] = {
...(ports[lnd[index].name] || {}),
rest: port,
};
});
}
}
lightningd = lightningd.filter(n => n.status !== Status.Started);
if (lightningd.length) {
const existingPorts = lightningd.map(n => n.ports.rest);
const openPorts = await getOpenPortRange(existingPorts);
if (openPorts.join() !== existingPorts.join()) {
openPorts.forEach((port, index) => {
ports[lightningd[index].name] = { rpc: port };
});
}
}
// return undefined if no ports where updated
return Object.keys(ports).length > 0 ? ports : undefined;
};

Loading…
Cancel
Save