Browse Source

feat(network): implement node removal

feat/auto-update
jamaljsr 5 years ago
parent
commit
fcb0749bff
  1. 1
      TODO.md
  2. 6
      src/components/designer/lnd/actions/RemoveNode.tsx
  3. 2
      src/components/network/NewNetwork.tsx
  4. 3
      src/i18n/locales/en-US.json
  5. 8
      src/i18n/locales/es.json
  6. 37
      src/lib/docker/dockerService.ts
  7. 5
      src/lib/lnd/lndService.ts
  8. 14
      src/store/models/designer.ts
  9. 6
      src/store/models/lnd.ts
  10. 18
      src/store/models/network.ts
  11. 5
      src/types/index.ts
  12. 11
      src/utils/chart.ts
  13. 13
      src/utils/network.ts
  14. 1
      src/utils/tests.tsx

1
TODO.md

@ -2,6 +2,7 @@
Small Stuff Small Stuff
- dont allow open channel if both nodes aren't Started
- implement real-time channel updates from LND via GRPC streams - implement real-time channel updates from LND via GRPC streams
- implement option to auto-mine every X minutes - implement option to auto-mine every X minutes
- switch renovatebot to dependabot and use automatic security fixes - switch renovatebot to dependabot and use automatic security fixes

6
src/components/designer/lnd/actions/RemoveNode.tsx

@ -18,17 +18,19 @@ interface Props {
const RemoveNode: React.FC<Props> = ({ node }) => { const RemoveNode: React.FC<Props> = ({ node }) => {
const { l } = usePrefixedTranslation('cmps.designer.lnd.actions.RemoveNode'); const { l } = usePrefixedTranslation('cmps.designer.lnd.actions.RemoveNode');
const { notify } = useStoreActions(s => s.app); const { notify } = useStoreActions(s => s.app);
const { removeNode } = useStoreActions(s => s.network);
const showRemoveModal = () => { const showRemoveModal = () => {
const { name } = node; const { name } = node;
Modal.confirm({ Modal.confirm({
title: l('confirmText', { name }), title: l('confirmTitle', { name }),
content: l('confirmText'),
okText: l('confirmBtn'), okText: l('confirmBtn'),
okType: 'danger', okType: 'danger',
cancelText: l('cancelBtn'), cancelText: l('cancelBtn'),
onOk: async () => { onOk: async () => {
try { try {
// await closeChannel({ node: from as LndNode, channelPoint }); await removeNode({ node });
notify({ message: l('success', { name }) }); notify({ message: l('success', { name }) });
} catch (error) { } catch (error) {
notify({ message: l('error'), error }); notify({ message: l('error'), error });

2
src/components/network/NewNetwork.tsx

@ -53,7 +53,7 @@ const NewNetwork: React.SFC<FormComponentProps> = ({ form }) => {
<Form.Item label={l('lndNodesLabel')}> <Form.Item label={l('lndNodesLabel')}>
{form.getFieldDecorator('lndNodes', { {form.getFieldDecorator('lndNodes', {
rules: [{ required: true, message: l('cmps.forms.required') }], rules: [{ required: true, message: l('cmps.forms.required') }],
initialValue: 2, initialValue: 3,
})(<InputNumber min={1} max={10} />)} })(<InputNumber min={1} max={10} />)}
</Form.Item> </Form.Item>
</Col> </Col>

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

@ -74,7 +74,8 @@
"cmps.designer.lnd.actions.OpenChannelModal.submitError": "Unable to open the channel", "cmps.designer.lnd.actions.OpenChannelModal.submitError": "Unable to open the channel",
"cmps.designer.lnd.actions.RemoveNode.title": "Remove Node From Network", "cmps.designer.lnd.actions.RemoveNode.title": "Remove Node From Network",
"cmps.designer.lnd.actions.RemoveNode.btnText": "Remove", "cmps.designer.lnd.actions.RemoveNode.btnText": "Remove",
"cmps.designer.lnd.actions.RemoveNode.confirmText": "Are you sure you want to remove {{name}} from the network?", "cmps.designer.lnd.actions.RemoveNode.confirmTitle": "Are you sure you want to remove {{name}} from the network?",
"cmps.designer.lnd.actions.RemoveNode.confirmText": "Any open channels with this node will be inoperable and not displayed in the designer.",
"cmps.designer.lnd.actions.RemoveNode.confirmBtn": "Yes", "cmps.designer.lnd.actions.RemoveNode.confirmBtn": "Yes",
"cmps.designer.lnd.actions.RemoveNode.cancelBtn": "Cancel", "cmps.designer.lnd.actions.RemoveNode.cancelBtn": "Cancel",
"cmps.designer.lnd.actions.RemoveNode.success": "The node {{name}} have been removed from the network", "cmps.designer.lnd.actions.RemoveNode.success": "The node {{name}} have been removed from the network",

8
src/i18n/locales/es.json

@ -72,6 +72,14 @@
"cmps.designer.lnd.actions.OpenChannelModal.okBtn": "Canal abierto", "cmps.designer.lnd.actions.OpenChannelModal.okBtn": "Canal abierto",
"cmps.designer.lnd.actions.OpenChannelModal.balancesError": "No es posible recuperar saldos de nodos", "cmps.designer.lnd.actions.OpenChannelModal.balancesError": "No es posible recuperar saldos de nodos",
"cmps.designer.lnd.actions.OpenChannelModal.submitError": "No se puede abrir el canal.", "cmps.designer.lnd.actions.OpenChannelModal.submitError": "No se puede abrir el canal.",
"cmps.designer.lnd.actions.RemoveNode.title": "Eliminar nodo de la red",
"cmps.designer.lnd.actions.RemoveNode.btnText": "Eliminar",
"cmps.designer.lnd.actions.RemoveNode.confirmTitle": "¿Estás seguro de que deseas eliminar {{name}} de la red?",
"cmps.designer.lnd.actions.RemoveNode.confirmText": "Cualquier canal abierto con este nodo será inoperable y no se mostrará en el diseñador.",
"cmps.designer.lnd.actions.RemoveNode.confirmBtn": "Si",
"cmps.designer.lnd.actions.RemoveNode.cancelBtn": "Cancelar",
"cmps.designer.lnd.actions.RemoveNode.success": "El nodo {{name}} se ha eliminado de la red",
"cmps.designer.lnd.actions.RemoveNode.error": "No se puede eliminar el nodo",
"cmps.designer.lnd.connect.FilePaths.tlsCert": "Certificado TLS", "cmps.designer.lnd.connect.FilePaths.tlsCert": "Certificado TLS",
"cmps.designer.lnd.connect.FilePaths.adminMacaroon": "Admin Macarrón", "cmps.designer.lnd.connect.FilePaths.adminMacaroon": "Admin Macarrón",
"cmps.designer.lnd.connect.FilePaths.readOnlyMacaroon": "Macarrón de solo lectura", "cmps.designer.lnd.connect.FilePaths.readOnlyMacaroon": "Macarrón de solo lectura",

37
src/lib/docker/dockerService.ts

@ -4,7 +4,7 @@ import { join } from 'path';
import * as compose from 'docker-compose'; import * as compose from 'docker-compose';
import Dockerode from 'dockerode'; import Dockerode from 'dockerode';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { LndNode } from 'shared/types'; import { CommonNode, LndNode } from 'shared/types';
import stripAnsi from 'strip-ansi'; import stripAnsi from 'strip-ansi';
import { DockerLibrary, DockerVersions, Network, NetworksFile } from 'types'; import { DockerLibrary, DockerVersions, Network, NetworksFile } from 'types';
import { networksPath } from 'utils/config'; import { networksPath } from 'utils/config';
@ -84,7 +84,7 @@ class DockerService implements DockerLibrary {
const yml = yaml.dump(file.content); const yml = yaml.dump(file.content);
const path = join(network.path, 'docker-compose.yml'); const path = join(network.path, 'docker-compose.yml');
await write(path, yml); await write(path, yml);
info(`created compose file for '${network.name}' at '${path}'`); info(`saved compose file for '${network.name}' at '${path}'`);
} }
/** /**
@ -109,6 +109,24 @@ class DockerService implements DockerLibrary {
info(`Network stopped:\n ${result.out || result.err}`); info(`Network stopped:\n ${result.out || result.err}`);
} }
/**
* Removes a single service from the network using docker-compose
* @param network the network containing the node
* @param node the node to remove
*/
async removeNode(network: Network, node: CommonNode) {
info(`Stopping docker container for ${node.name}`);
info(` - path: ${network.path}`);
let result = await this.execute(compose.stopOne, node.name, this.getArgs(network));
info(`Container stopped:\n ${result.out || result.err}`);
info(`Removing stopped docker containers`);
result = await this.execute(compose.rm, this.getArgs(network));
info(`Removed:\n ${result.out || result.err}`);
await this.saveComposeFile(network);
}
/** /**
* Saves the given networks to disk * Saves the given networks to disk
* @param networks the list of networks to save * @param networks the list of networks to save
@ -127,9 +145,9 @@ class DockerService implements DockerLibrary {
const path = join(networksPath, 'networks.json'); const path = join(networksPath, 'networks.json');
if (await exists(path)) { if (await exists(path)) {
const json = await read(path); const json = await read(path);
const networks = JSON.parse(json); const data = JSON.parse(json);
info(`loaded ${networks.length} networks from '${path}'`); info(`loaded ${data.networks.length} networks from '${path}'`);
return networks; return data;
} else { } else {
info(`skipped loading networks because the file '${path}' doesn't exist`); info(`skipped loading networks because the file '${path}' doesn't exist`);
return { networks: [], charts: {} }; return { networks: [], charts: {} };
@ -141,12 +159,13 @@ class DockerService implements DockerLibrary {
* @param cmd the compose function to call * @param cmd the compose function to call
* @param args the arguments to the compose function * @param args the arguments to the compose function
*/ */
private async execute<A>( private async execute<A, B>(
cmd: (args: A) => Promise<compose.IDockerComposeResult>, cmd: (arg1: A, arg2?: B) => Promise<compose.IDockerComposeResult>,
args: A, arg1: A,
arg2?: B,
): Promise<compose.IDockerComposeResult> { ): Promise<compose.IDockerComposeResult> {
try { try {
const result = await cmd(args); const result = await cmd(arg1, arg2);
result.out = stripAnsi(result.out); result.out = stripAnsi(result.out);
result.err = stripAnsi(result.err); result.err = stripAnsi(result.err);
return result; return result;

5
src/lib/lnd/lndService.ts

@ -1,6 +1,6 @@
import * as LND from '@radar/lnrpc'; import * as LND from '@radar/lnrpc';
import { LndNode } from 'shared/types'; import { LndNode } from 'shared/types';
import { LndLibrary, Network } from 'types'; import { LndLibrary } from 'types';
import { waitFor } from 'utils/async'; import { waitFor } from 'utils/async';
import { getContainerName } from 'utils/network'; import { getContainerName } from 'utils/network';
import { lndProxyClient as proxy } from './'; import { lndProxyClient as proxy } from './';
@ -68,8 +68,7 @@ class LndService implements LndLibrary {
return await proxy.pendingChannels(node); return await proxy.pendingChannels(node);
} }
async onNodesDeleted(network: Network): Promise<void> { async onNodesDeleted(nodes: LndNode[]): Promise<void> {
const nodes = network.nodes.lightning.filter(n => n.implementation === 'LND');
return await proxy.onNodesDeleted(nodes); return await proxy.onNodesDeleted(nodes);
} }

14
src/store/models/designer.ts

@ -133,10 +133,16 @@ const designerModel: DesignerModel = {
delete state.allCharts[state.activeId].links[linkId]; delete state.allCharts[state.activeId].links[linkId];
}), }),
removeNode: action((state, nodeId) => { removeNode: action((state, nodeId) => {
// this action is used when a node is dropped onto the canvas. const chart = state.allCharts[state.activeId];
// remove the node created in the chart once the async loading if (chart.selected && chart.selected.id === nodeId) {
// has been completed chart.selected = {};
delete state.allCharts[state.activeId].nodes[nodeId]; }
delete chart.nodes[nodeId];
Object.values(chart.links).forEach(link => {
if ([link.to.nodeId, link.from.nodeId].includes(nodeId)) {
delete chart.links[link.id];
}
});
}), }),
addLndNode: action((state, { lndNode, position }) => { addLndNode: action((state, { lndNode, position }) => {
const chart = state.allCharts[state.activeId]; const chart = state.allCharts[state.activeId];

6
src/store/models/lnd.ts

@ -37,6 +37,7 @@ export interface OpenChannelPayload {
export interface LndModel { export interface LndModel {
nodes: LndNodeMapping; nodes: LndNodeMapping;
removeNode: Action<LndModel, string>;
setInfo: Action<LndModel, { node: LndNode; info: LND.GetInfoResponse }>; setInfo: Action<LndModel, { node: LndNode; info: LND.GetInfoResponse }>;
getInfo: Thunk<LndModel, LndNode, StoreInjections, RootModel>; getInfo: Thunk<LndModel, LndNode, StoreInjections, RootModel>;
setWalletBalance: Action< setWalletBalance: Action<
@ -61,6 +62,11 @@ const lndModel: LndModel = {
// state properties // state properties
nodes: {}, nodes: {},
// reducer actions (mutations allowed thx to immer) // reducer actions (mutations allowed thx to immer)
removeNode: action((state, name) => {
if (state.nodes[name]) {
delete state.nodes[name];
}
}),
setInfo: action((state, { node, info }) => { setInfo: action((state, { node, info }) => {
if (!state.nodes[node.name]) state.nodes[node.name] = {}; if (!state.nodes[node.name]) state.nodes[node.name] = {};
state.nodes[node.name].info = info; state.nodes[node.name].info = info;

18
src/store/models/network.ts

@ -45,6 +45,7 @@ export interface NetworkModel {
RootModel, RootModel,
Promise<LndNode> Promise<LndNode>
>; >;
removeNode: Thunk<NetworkModel, { node: LndNode }, StoreInjections, RootModel>;
setStatus: Action< setStatus: Action<
NetworkModel, NetworkModel,
{ id: number; status: Status; only?: string; all?: boolean; error?: Error } { id: number; status: Status; only?: string; all?: boolean; error?: Error }
@ -130,6 +131,21 @@ const networkModel: NetworkModel = {
await injections.dockerService.saveComposeFile(network); await injections.dockerService.saveComposeFile(network);
return node; return node;
}), }),
removeNode: thunk(
async (actions, { node }, { getState, injections, getStoreActions }) => {
const networks = getState().networks;
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);
await injections.dockerService.removeNode(network, node);
actions.setNetworks([...networks]);
await actions.save();
getStoreActions().designer.removeNode(node.name);
getStoreActions().designer.syncChart(network);
await injections.lndService.onNodesDeleted([node, ...network.nodes.lightning]);
},
),
setStatus: action((state, { id, status, only, all = true, error }) => { setStatus: action((state, { id, status, only, all = true, error }) => {
const network = state.networks.find(n => n.id === id); const network = state.networks.find(n => n.id === id);
if (!network) throw new Error(l('networkByIdErr', { networkId: id })); if (!network) throw new Error(l('networkByIdErr', { networkId: id }));
@ -246,7 +262,7 @@ const networkModel: NetworkModel = {
actions.setNetworks(newNetworks); actions.setNetworks(newNetworks);
getStoreActions().designer.removeChart(networkId); getStoreActions().designer.removeChart(networkId);
await actions.save(); await actions.save();
await injections.lndService.onNodesDeleted(network); await injections.lndService.onNodesDeleted(network.nodes.lightning);
}), }),
}; };

5
src/types/index.ts

@ -1,7 +1,7 @@
import { IChart } from '@mrblenny/react-flow-chart'; import { IChart } from '@mrblenny/react-flow-chart';
import * as LND from '@radar/lnrpc'; import * as LND from '@radar/lnrpc';
import { ChainInfo, WalletInfo } from 'bitcoin-core'; import { ChainInfo, WalletInfo } from 'bitcoin-core';
import { BitcoinNode, LndNode, Status } from 'shared/types'; import { BitcoinNode, CommonNode, LndNode, Status } from 'shared/types';
import { IpcSender } from 'lib/ipc/ipcService'; import { IpcSender } from 'lib/ipc/ipcService';
export interface LocaleConfig { export interface LocaleConfig {
@ -33,6 +33,7 @@ export interface DockerLibrary {
saveComposeFile: (network: Network) => Promise<void>; saveComposeFile: (network: Network) => Promise<void>;
start: (network: Network) => Promise<void>; start: (network: Network) => Promise<void>;
stop: (network: Network) => Promise<void>; stop: (network: Network) => Promise<void>;
removeNode: (network: Network, node: CommonNode) => Promise<void>;
saveNetworks: (networks: NetworksFile) => Promise<void>; saveNetworks: (networks: NetworksFile) => Promise<void>;
loadNetworks: () => Promise<NetworksFile>; loadNetworks: () => Promise<NetworksFile>;
} }
@ -54,7 +55,7 @@ export interface LndLibrary {
closeChannel: (node: LndNode, channelPoint: string) => Promise<any>; closeChannel: (node: LndNode, channelPoint: string) => Promise<any>;
listChannels: (node: LndNode) => Promise<LND.ListChannelsResponse>; listChannels: (node: LndNode) => Promise<LND.ListChannelsResponse>;
pendingChannels: (node: LndNode) => Promise<LND.PendingChannelsResponse>; pendingChannels: (node: LndNode) => Promise<LND.PendingChannelsResponse>;
onNodesDeleted: (network: Network) => Promise<void>; onNodesDeleted: (nodes: LndNode[]) => Promise<void>;
} }
export interface StoreInjections { export interface StoreInjections {

11
src/utils/chart.ts

@ -222,10 +222,13 @@ export const updateChartFromLnd = (chart: IChart, lndData: LndNodeMapping): ICha
...waitingClose.map(pluckChan).map(mapPendingChannel('Waiting to Close')), ...waitingClose.map(pluckChan).map(mapPendingChannel('Waiting to Close')),
]; ];
allChannels.forEach(channel => { allChannels
updateLinksAndPorts(channel, pubkeys, nodes, fromNode, links); // ignore channels to nodes that no longer exist in the network
createdLinkIds.push(channel.uniqueId); .filter(c => !!pubkeys[c.pubkey])
}); .forEach(channel => {
updateLinksAndPorts(channel, pubkeys, nodes, fromNode, links);
createdLinkIds.push(channel.uniqueId);
});
nodes[fromName] = { nodes[fromName] = {
...fromNode, ...fromNode,

13
src/utils/network.ts

@ -32,21 +32,22 @@ export const createLndNetworkNode = (
version: LndVersion, version: LndVersion,
status: Status, status: Status,
): LndNode => { ): LndNode => {
const index = network.nodes.lightning.length; const { bitcoin, lightning } = network.nodes;
const name = `lnd-${index + 1}`; const id = lightning.length ? Math.max(...lightning.map(n => n.id)) + 1 : 0;
const name = `lnd-${id + 1}`;
return { return {
id: index, id,
networkId: network.id, networkId: network.id,
name: name, name: name,
type: 'lightning', type: 'lightning',
implementation: 'LND', implementation: 'LND',
version, version,
status, status,
backendName: network.nodes.bitcoin[0].name, backendName: bitcoin[0].name,
paths: getFilePaths(name, network), paths: getFilePaths(name, network),
ports: { ports: {
rest: 8081 + index, rest: 8081 + id,
grpc: 10001 + index, grpc: 10001 + id,
}, },
}; };
}; };

1
src/utils/tests.tsx

@ -34,6 +34,7 @@ export const injections: StoreInjections = {
saveComposeFile: jest.fn(), saveComposeFile: jest.fn(),
start: jest.fn(), start: jest.fn(),
stop: jest.fn(), stop: jest.fn(),
removeNode: jest.fn(),
saveNetworks: jest.fn(), saveNetworks: jest.fn(),
loadNetworks: jest.fn(), loadNetworks: jest.fn(),
}, },

Loading…
Cancel
Save