Browse Source

feat(clightning): display c-lightning node info in the sidebar

master
jamaljsr 5 years ago
parent
commit
c3a7621964
  1. 4
      electron/appIpcListener.ts
  2. 5
      src/components/designer/Sidebar.tsx
  3. 115
      src/components/designer/clightning/CLightningDetails.tsx
  4. 6
      src/components/network/NewNetwork.tsx
  5. 14
      src/i18n/locales/en-US.json
  6. 14
      src/i18n/locales/es.json
  7. 45
      src/lib/clightning/clightningService.ts
  8. 21
      src/lib/clightning/clightningTypes.ts
  9. 1
      src/lib/clightning/index.ts
  10. 8
      src/lib/docker/composeFile.ts
  11. 2
      src/lib/docker/dockerService.spec.ts
  12. 4
      src/lib/docker/dockerService.ts
  13. 2
      src/lib/docker/nodeTemplates.ts
  14. 1
      src/lib/lightningd/index.ts
  15. 23
      src/lib/lightningd/lightningdService.ts
  16. 0
      src/resources/clightning.png
  17. 4
      src/shared/types.ts
  18. 2
      src/store/index.ts
  19. 41
      src/store/models/clightning.ts
  20. 4
      src/store/models/designer.spec.ts
  21. 3
      src/store/models/index.ts
  22. 2
      src/store/models/network.spec.ts
  23. 21
      src/store/models/network.ts
  24. 16
      src/types/index.ts
  25. 4
      src/utils/chart.ts
  26. 2
      src/utils/constants.ts
  27. 34
      src/utils/network.ts
  28. 39
      src/utils/objects.ts
  29. 6
      src/utils/tests.tsx

4
electron/appIpcListener.ts

@ -1,9 +1,9 @@
import { BrowserWindow, IpcMain } from 'electron';
import { debug } from 'electron-log';
import windowState from 'electron-window-state';
import { ipcChannels } from '../src/shared';
import { BASE_URL, APP_ROOT } from './constants';
import { join } from 'path';
import { ipcChannels } from '../src/shared';
import { APP_ROOT, BASE_URL } from './constants';
const openWindow = async (args: { url: string }): Promise<boolean> => {
console.warn('opwnWindow', args);

5
src/components/designer/Sidebar.tsx

@ -1,11 +1,12 @@
import React, { useMemo } from 'react';
import { IChart } from '@mrblenny/react-flow-chart';
import { CLightningNode, LndNode } from 'shared/types';
import { Network } from 'types';
import BitcoindDetails from './bitcoind/BitcoindDetails';
import CLightningDetails from './clightning/CLightningDetails';
import DefaultSidebar from './default/DefaultSidebar';
import LinkDetails from './link/LinkDetails';
import LndDetails from './lnd/LndDetails';
import { LndNode } from 'shared/types';
interface Props {
network: Network;
@ -23,6 +24,8 @@ const Sidebar: React.FC<Props> = ({ network, chart }) => {
return <BitcoindDetails node={node} />;
} else if (node && node.implementation === 'LND') {
return <LndDetails node={node as LndNode} />;
} else if (node && node.implementation === 'c-lightning') {
return <CLightningDetails node={node as CLightningNode} />;
}
} else if (type === 'link' && id) {
const link = chart.links[id];

115
src/components/designer/clightning/CLightningDetails.tsx

@ -0,0 +1,115 @@
import React from 'react';
import { useAsync } from 'react-async-hook';
import { Alert, Icon, Tooltip } from 'antd';
import { usePrefixedTranslation } from 'hooks';
import { CLightningNode, Status } from 'shared/types';
import { useStoreActions, useStoreState } from 'store';
import { ellipseInner } from 'utils/strings';
import { CopyIcon, DetailsList, Loader, StatusBadge } from 'components/common';
import { DetailValues } from 'components/common/DetailsList';
import SidebarCard from '../SidebarCard';
interface Props {
node: CLightningNode;
}
const CLightningDetails: React.FC<Props> = ({ node }) => {
const { l } = usePrefixedTranslation('cmps.designer.clightning.CLightningDetails');
const { getInfo } = useStoreActions(s => s.clightning);
const { nodes } = useStoreState(s => s.clightning);
const getInfoAsync = useAsync(
async (node: CLightningNode) => {
if (node.status === Status.Started) {
return await getInfo(node);
}
},
[node],
);
const details: DetailValues = [
{ label: l('nodeType'), value: node.type },
{ label: l('implementation'), value: node.implementation },
{ label: l('version'), value: `v${node.version}` },
{
label: l('status'),
value: (
<StatusBadge
status={node.status}
text={l(`enums.status.${Status[node.status]}`)}
/>
),
},
];
const nodeState = nodes[node.name];
if (node.status === Status.Started && nodeState) {
if (nodeState.info) {
const {
id,
alias,
warningBitcoindSync,
numPendingChannels,
numActiveChannels,
numInactiveChannels,
} = nodeState.info;
const pubkey = (
<>
{ellipseInner(id)}
<CopyIcon value={id} label="PubKey" />
</>
);
const channels = (
<Tooltip title={l('channelsTooltip')}>
{`${numActiveChannels} / ${numPendingChannels} / ${numInactiveChannels}`}
</Tooltip>
);
details.push(
{ label: l('alias'), value: alias },
{ label: l('pubkey'), value: pubkey },
{ label: l('channels'), value: channels },
);
if (warningBitcoindSync) {
const synced = (
<Tooltip title={warningBitcoindSync}>{warningBitcoindSync}</Tooltip>
);
details.push({ label: l('syncedToChain'), value: synced });
}
}
}
return (
<SidebarCard title={node.name}>
{getInfoAsync.loading && <Loader />}
{node.status === Status.Starting && (
<Alert
type="info"
showIcon
closable={false}
message={l('waitingNotice')}
icon={<Icon type="loading" />}
/>
)}
{node.status === Status.Error && node.errorMsg && (
<Alert
type="error"
message={l('startError')}
description={node.errorMsg}
closable={false}
showIcon
/>
)}
{getInfoAsync.error && (
<Alert
type="error"
closable={false}
message={l('getInfoErr')}
description={getInfoAsync.error.message}
/>
)}
<DetailsList details={details} />
</SidebarCard>
);
};
export default CLightningDetails;

6
src/components/network/NewNetwork.tsx

@ -19,7 +19,7 @@ const Styled = {
interface FormProps {
name: string;
lndNodes: number;
lightningdNodes: number;
clightningNodes: number;
bitcoindNodes: number;
}
@ -59,8 +59,8 @@ const NewNetwork: React.SFC<FormComponentProps> = ({ form }) => {
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label={l('lightningdNodesLabel')}>
{form.getFieldDecorator('lightningdNodes', {
<Form.Item label={l('clightningNodesLabel')}>
{form.getFieldDecorator('clightningNodes', {
rules: [{ required: true, message: l('cmps.forms.required') }],
initialValue: 1,
})(<InputNumber min={0} max={10} />)}

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

@ -25,6 +25,18 @@
"cmps.designer.bitcoind.MineBlocksInput.label": "Manually Mine Blocks",
"cmps.designer.bitcoind.MineBlocksInput.error": "Unable to mine blocks",
"cmps.designer.bitcoind.MineBlocksInput.btn": "Mine",
"cmps.designer.clightning.CLightningDetails.nodeType": "Node Type",
"cmps.designer.clightning.CLightningDetails.implementation": "Implementation",
"cmps.designer.clightning.CLightningDetails.version": "Version",
"cmps.designer.clightning.CLightningDetails.status": "Status",
"cmps.designer.clightning.CLightningDetails.alias": "Alias",
"cmps.designer.clightning.CLightningDetails.pubkey": "Pubkey",
"cmps.designer.clightning.CLightningDetails.syncedToChain": "Sync Status",
"cmps.designer.clightning.CLightningDetails.channels": "Channels",
"cmps.designer.clightning.CLightningDetails.channelsTooltip": "Active / Pending / Inactive",
"cmps.designer.clightning.CLightningDetails.waitingNotice": "Waiting for lightningd to come online",
"cmps.designer.clightning.CLightningDetails.getInfoErr": "Unable to connect to node",
"cmps.designer.clightning.CLightningDetails.startError": "Unable to connect to c-lightning node",
"cmps.designer.default.DefaultSidebar.title": "Network Designer",
"cmps.designer.default.DefaultSidebar.mainDesc": "Click on an element in the designer to see details",
"cmps.designer.default.DefaultSidebar.addNodesTitle": "Add Nodes",
@ -153,7 +165,7 @@
"cmps.network.NewNetwork.nameLabel": "Network Name",
"cmps.network.NewNetwork.namePhldr": "My Lightning Simnet",
"cmps.network.NewNetwork.lndNodesLabel": "How many LND nodes?",
"cmps.network.NewNetwork.lightningdNodesLabel": "How many c-lightning nodes?",
"cmps.network.NewNetwork.clightningNodesLabel": "How many c-lightning nodes?",
"cmps.network.NewNetwork.bitcoindNodesLabel": "How many bitcoind nodes?",
"cmps.network.NewNetwork.bitcoindNodesSoon": "Coming Soon",
"cmps.network.NewNetwork.btnCreate": "Create",

14
src/i18n/locales/es.json

@ -25,6 +25,18 @@
"cmps.designer.bitcoind.MineBlocksInput.label": "Bloques de mina manual",
"cmps.designer.bitcoind.MineBlocksInput.error": "Incapaz de extraer bloques",
"cmps.designer.bitcoind.MineBlocksInput.btn": "Mina",
"cmps.designer.clightning.CLightningDetails.nodeType": "Tipo de nodo",
"cmps.designer.clightning.CLightningDetails.implementation": "Implementación",
"cmps.designer.clightning.CLightningDetails.version": "Versión",
"cmps.designer.clightning.CLightningDetails.status": "Estado",
"cmps.designer.clightning.CLightningDetails.alias": "Alias",
"cmps.designer.clightning.CLightningDetails.pubkey": "Pubkey",
"cmps.designer.clightning.CLightningDetails.syncedToChain": "Estado de sincronización",
"cmps.designer.clightning.CLightningDetails.channels": "Canales",
"cmps.designer.clightning.CLightningDetails.channelsTooltip": "Activo / Pendiente / Inactivo",
"cmps.designer.clightning.CLightningDetails.waitingNotice": "Esperando a que lightningd se conecte",
"cmps.designer.clightning.CLightningDetails.getInfoErr": "No se puede conectar al nodo",
"cmps.designer.clightning.CLightningDetails.startError": "No se puede conectar al nodo c-lightning",
"cmps.designer.default.DefaultSidebar.title": "Diseñador de red",
"cmps.designer.default.DefaultSidebar.mainDesc": "Haga clic en un elemento en el diseñador para ver detalles",
"cmps.designer.default.DefaultSidebar.addNodesTitle": "Agregar nodos",
@ -153,7 +165,7 @@
"cmps.network.NewNetwork.nameLabel": "Nombre de red",
"cmps.network.NewNetwork.namePhldr": "Mi Lightning Simnet",
"cmps.network.NewNetwork.lndNodesLabel": "¿Cuántos nodos de LND?",
"cmps.network.NewNetwork.lightningdNodesLabel": "¿Cuántos nodos de c-lightning?",
"cmps.network.NewNetwork.clightningNodesLabel": "¿Cuántos nodos de c-lightning?",
"cmps.network.NewNetwork.bitcoindNodesLabel": "¿Cuántos nodos de bitcoind?",
"cmps.network.NewNetwork.bitcoindNodesSoon": "Próximamente",
"cmps.network.NewNetwork.btnCreate": "Crear",

45
src/lib/clightning/clightningService.ts

@ -0,0 +1,45 @@
import { CLightningNode } from 'shared/types';
import { CLightningLibrary } from 'types';
import { waitFor } from 'utils/async';
import { read } from 'utils/files';
import { snakeKeysToCamel } from 'utils/objects';
import * as CLN from './clightningTypes';
class CLightningService implements CLightningLibrary {
async getInfo(node: CLightningNode): Promise<CLN.GetInfoResponse> {
return await this.request(node, 'getinfo');
}
/**
* Helper function to continually query the node until a successful
* response is received or it times out
*/
async waitUntilOnline(
node: CLightningNode,
interval = 3 * 1000, // check every 3 seconds
timeout = 30 * 1000, // timeout after 30 seconds
): Promise<void> {
return waitFor(
async () => {
await this.getInfo(node);
},
interval,
timeout,
);
}
private async request(node: CLightningNode, path: string) {
const { paths, ports } = node;
const url = `http://127.0.0.1:${ports.rest}/v1/${path}`;
const macaroon = await read(paths.macaroon, 'base64');
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
macaroon,
},
});
return snakeKeysToCamel(await response.json());
}
}
export default new CLightningService();

21
src/lib/clightning/clightningTypes.ts

@ -0,0 +1,21 @@
export interface GetInfoResponse {
id: string;
alias: string;
color: string;
numPeers: number;
numPendingChannels: number;
numActiveChannels: number;
numInactiveChannels: number;
address: string[];
binding: {
type: string;
address: string;
port: number;
}[];
version: string;
blockheight: number;
network: string;
msatoshiFeesCollected: number;
feesCollectedMsat: string;
warningBitcoindSync: string;
}

1
src/lib/clightning/index.ts

@ -0,0 +1 @@
export { default as clightningService } from './clightningService';

8
src/lib/docker/composeFile.ts

@ -1,7 +1,7 @@
import { BitcoinNode, CommonNode, LightningdNode, LndNode } from 'shared/types';
import { BitcoinNode, CLightningNode, CommonNode, LndNode } from 'shared/types';
import { getContainerName } from 'utils/network';
/* eslint-disable @typescript-eslint/camelcase */
import { bitcoind, lightningd, lnd } from './nodeTemplates';
import { bitcoind, clightning, lnd } from './nodeTemplates';
export interface ComposeService {
image: string;
@ -49,7 +49,7 @@ class ComposeFile {
this.content.services[name] = lnd(name, container, version, backendName, rest, grpc);
}
addClightning(node: LightningdNode, backend: CommonNode) {
addClightning(node: CLightningNode, backend: CommonNode) {
const {
name,
version,
@ -57,7 +57,7 @@ class ComposeFile {
} = node;
const container = getContainerName(node);
const backendName = getContainerName(backend);
this.content.services[name] = lightningd(name, container, version, backendName, rest);
this.content.services[name] = clightning(name, container, version, backendName, rest);
}
}

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

@ -167,7 +167,7 @@ describe('DockerService', () => {
id: 1,
name: 'my network',
lndNodes: 1,
lightningdNodes: 0,
clightningNodes: 0,
bitcoindNodes: 1,
});
net.nodes.lightning[0].backendName = 'invalid';

4
src/lib/docker/dockerService.ts

@ -6,7 +6,7 @@ import * as compose from 'docker-compose';
import Dockerode from 'dockerode';
import yaml from 'js-yaml';
import os from 'os';
import { CommonNode, LightningdNode, LndNode } from 'shared/types';
import { CLightningNode, CommonNode, LndNode } from 'shared/types';
import stripAnsi from 'strip-ansi';
import { DockerLibrary, DockerVersions, Network, NetworksFile } from 'types';
import { networksPath } from 'utils/config';
@ -83,7 +83,7 @@ class DockerService implements DockerLibrary {
file.addLnd(lnd, backend);
}
if (node.implementation === 'c-lightning') {
const cln = node as LightningdNode;
const cln = node as CLightningNode;
const backend = bitcoin.find(n => n.name === cln.backendName) || bitcoin[0];
file.addClightning(cln, backend);
}

2
src/lib/docker/nodeTemplates.ts

@ -92,7 +92,7 @@ export const lnd = (
],
});
export const lightningd = (
export const clightning = (
name: string,
container: string,
version: string,

1
src/lib/lightningd/index.ts

@ -1 +0,0 @@
export { default as lightningdService } from './lightningdService';

23
src/lib/lightningd/lightningdService.ts

@ -1,23 +0,0 @@
import { LightningdNode } from 'shared/types';
import { read } from 'utils/files';
class LightningdService {
async getInfo(node: LightningdNode) {
return await this.request(node, 'getinfo');
}
private async request(node: LightningdNode, path: string) {
const { paths, ports } = node;
const url = `http://127.0.0.1:${ports.rest}/v1/${path}`;
const macaroon = await read(paths.macaroon, 'base64');
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
macaroon,
},
});
return await response.json();
}
}
export default new LightningdService();

0
src/resources/lightningd.png → src/resources/clightning.png

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

4
src/shared/types.ts

@ -30,7 +30,7 @@ export enum LndVersion {
'0.7.1-beta' = '0.7.1-beta',
}
export enum LightningdVersion {
export enum CLightningVersion {
latest = '0.7.3',
'0.7.3' = '0.7.3',
}
@ -47,7 +47,7 @@ export interface LndNode extends LightningNode {
};
}
export interface LightningdNode extends LightningNode {
export interface CLightningNode extends LightningNode {
paths: {
macaroon: string;
};

2
src/store/index.ts

@ -3,6 +3,7 @@ import { createStore, createTypedHooks } from 'easy-peasy';
import { createHashHistory, History } from 'history';
import { createLogger } from 'redux-logger';
import { bitcoindService } from 'lib/bitcoin';
import { clightningService } from 'lib/clightning';
import { dockerService } from 'lib/docker';
import { createIpcSender } from 'lib/ipc/ipcService';
import { lndService } from 'lib/lnd';
@ -53,6 +54,7 @@ const injections: StoreInjections = {
dockerService,
bitcoindService,
lndService,
clightningService,
};
const store = createReduxStore({ injections });

41
src/store/models/clightning.ts

@ -0,0 +1,41 @@
import { Action, action, Thunk, thunk } from 'easy-peasy';
import { CLightningNode } from 'shared/types';
import * as CLN from 'lib/clightning/clightningTypes';
import { StoreInjections } from 'types';
import { RootModel } from './';
export interface CLightningNodeMapping {
[key: string]: CLightningNodeModel;
}
export interface CLightningNodeModel {
info?: CLN.GetInfoResponse;
}
export interface CLightningModel {
nodes: CLightningNodeMapping;
removeNode: Action<CLightningModel, string>;
setInfo: Action<CLightningModel, { node: CLightningNode; info: CLN.GetInfoResponse }>;
getInfo: Thunk<CLightningModel, CLightningNode, StoreInjections, RootModel>;
}
const lndModel: CLightningModel = {
// state properties
nodes: {},
// reducer actions (mutations allowed thx to immer)
removeNode: action((state, name) => {
if (state.nodes[name]) {
delete state.nodes[name];
}
}),
setInfo: action((state, { node, info }) => {
if (!state.nodes[node.name]) state.nodes[node.name] = {};
state.nodes[node.name].info = info;
}),
getInfo: thunk(async (actions, node, { injections }) => {
const info = await injections.clightningService.getInfo(node);
actions.setInfo({ node, info });
}),
};
export default lndModel;

4
src/store/models/designer.spec.ts

@ -61,7 +61,7 @@ describe('Designer model', () => {
await addNetwork({
name: 'test',
lndNodes: 2,
lightningdNodes: 0,
clightningNodes: 0,
bitcoindNodes: 1,
});
});
@ -97,7 +97,7 @@ describe('Designer model', () => {
await addNetwork({
name: 'test 2',
lndNodes: 2,
lightningdNodes: 0,
clightningNodes: 0,
bitcoindNodes: 1,
});
store.getActions().designer.setActiveId(firstNetwork().id);

3
src/store/models/index.ts

@ -4,6 +4,7 @@ import { History } from 'history';
import { AnyAction } from 'redux';
import appModel, { AppModel } from './app';
import bitcoindModel, { BitcoindModel } from './bitcoind';
import clightningModel, { CLightningModel } from './clightning';
import designerModel, { DesignerModel } from './designer';
import lndModel, { LndModel } from './lnd';
import modalsModel, { ModalsModel } from './modals';
@ -15,6 +16,7 @@ export interface RootModel {
network: NetworkModel;
bitcoind: BitcoindModel;
lnd: LndModel;
clightning: CLightningModel;
designer: DesignerModel;
modals: ModalsModel;
}
@ -26,6 +28,7 @@ export const createModel = (history: History<any>): RootModel => {
network: networkModel,
bitcoind: bitcoindModel,
lnd: lndModel,
clightning: clightningModel,
designer: designerModel,
modals: modalsModel,
};

2
src/store/models/network.spec.ts

@ -39,7 +39,7 @@ describe('Network model', () => {
const addNetworkArgs = {
name: 'test',
lndNodes: 2,
lightningdNodes: 0,
clightningNodes: 0,
bitcoindNodes: 1,
};

21
src/store/models/network.ts

@ -22,7 +22,7 @@ const { l } = prefixTranslation('store.models.network');
interface AddNetworkArgs {
name: string;
lndNodes: number;
lightningdNodes: number;
clightningNodes: number;
bitcoindNodes: number;
}
@ -105,13 +105,13 @@ const networkModel: NetworkModel = {
};
await injections.dockerService.saveNetworks(data);
}),
add: action((state, { name, lndNodes, lightningdNodes, bitcoindNodes }) => {
add: action((state, { name, lndNodes, clightningNodes, bitcoindNodes }) => {
const nextId = Math.max(0, ...state.networks.map(n => n.id)) + 1;
const network = createNetwork({
id: nextId,
name,
lndNodes,
lightningdNodes,
clightningNodes,
bitcoindNodes,
});
state.networks.push(network);
@ -206,7 +206,8 @@ 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);
const { lnd, clightning, bitcoind } = groupNodes(network);
// wait for lnd nodes to come online before updating their status
for (const node of lnd) {
// use .then() to continue execution while the promises are waiting to complete
@ -217,8 +218,18 @@ const networkModel: NetworkModel = {
actions.setStatus({ id, status: Status.Error, only: node.name, error }),
);
}
// wait for lnd nodes to come online before updating their status
for (const node of clightning) {
// use .then() to continue execution while the promises are waiting to complete
injections.clightningService
.waitUntilOnline(node)
.then(() => actions.setStatus({ id, status: Status.Started, only: node.name }))
.catch(error =>
actions.setStatus({ id, status: Status.Error, only: node.name, error }),
);
}
// wait for bitcoind nodes to come online before updating their status
for (const node of network.nodes.bitcoin) {
for (const node of bitcoind) {
// use .then() to continue execution while the promises are waiting to complete
injections.bitcoindService
.waitUntilOnline(node.ports.rpc)

16
src/types/index.ts

@ -1,7 +1,15 @@
import { IChart } from '@mrblenny/react-flow-chart';
import * as LND from '@radar/lnrpc';
import { ChainInfo, WalletInfo } from 'bitcoin-core';
import { BitcoinNode, CommonNode, LightningNode, LndNode, Status } from 'shared/types';
import {
BitcoinNode,
CLightningNode,
CommonNode,
LightningNode,
LndNode,
Status,
} from 'shared/types';
import * as CLN from 'lib/clightning/clightningTypes';
import { IpcSender } from 'lib/ipc/ipcService';
export interface LocaleConfig {
@ -58,11 +66,17 @@ export interface LndLibrary {
onNodesDeleted: (nodes: LndNode[]) => Promise<void>;
}
export interface CLightningLibrary {
waitUntilOnline: (node: CLightningNode) => Promise<void>;
getInfo: (node: CLightningNode) => Promise<CLN.GetInfoResponse>;
}
export interface StoreInjections {
ipc: IpcSender;
dockerService: DockerLibrary;
bitcoindService: BitcoindLibrary;
lndService: LndLibrary;
clightningService: CLightningLibrary;
}
export interface NetworksFile {

4
src/utils/chart.ts

@ -4,7 +4,7 @@ import { BitcoinNode, LightningNode } from 'shared/types';
import { LndNodeMapping } from 'store/models/lnd';
import { Network } from 'types';
import btclogo from 'resources/bitcoin.svg';
import lightningdLogo from 'resources/lightningd.png';
import clightningLogo from 'resources/clightning.png';
import lndLogo from 'resources/lnd.png';
export interface LinkProperties {
@ -36,7 +36,7 @@ export const snap = (position: IPosition, config?: IConfig) =>
: position;
export const createLightningChartNode = (ln: LightningNode) => {
const logo = ln.implementation === 'c-lightning' ? lightningdLogo : lndLogo;
const logo = ln.implementation === 'c-lightning' ? clightningLogo : lndLogo;
const node: INode = {
id: ln.name,
type: 'lightning',

2
src/utils/constants.ts

@ -36,7 +36,7 @@ export const BasePorts = {
rest: 8081,
grpc: 10001,
},
lightningd: {
clightning: {
rest: 8181,
},
};

34
src/utils/network.ts

@ -2,9 +2,9 @@ import { join } from 'path';
import detectPort from 'detect-port';
import {
BitcoinNode,
CLightningNode,
CLightningVersion,
CommonNode,
LightningdNode,
LightningdVersion,
LndNode,
LndVersion,
Status,
@ -23,9 +23,9 @@ export const groupNodes = (network: Network) => {
return {
bitcoind: bitcoin.filter(n => n.implementation === 'bitcoind') as BitcoinNode[],
lnd: lightning.filter(n => n.implementation === 'LND') as LndNode[],
lightningd: lightning.filter(
clightning: lightning.filter(
n => n.implementation === 'c-lightning',
) as LightningdNode[],
) as CLightningNode[],
eclair: lightning.filter(n => n.implementation === 'eclair'),
};
};
@ -74,11 +74,11 @@ export const createLndNetworkNode = (
};
};
export const createLightningdNetworkNode = (
export const createCLightningNetworkNode = (
network: Network,
version: LightningdVersion,
version: CLightningVersion,
status: Status,
): LightningdNode => {
): CLightningNode => {
const { bitcoin, lightning } = network.nodes;
const id = lightning.length ? Math.max(...lightning.map(n => n.id)) + 1 : 0;
const name = getName(id);
@ -96,7 +96,7 @@ export const createLightningdNetworkNode = (
macaroon: join(nodePath, 'rest-api', 'access.macaroon'),
},
ports: {
rest: BasePorts.lightningd.rest + id,
rest: BasePorts.clightning.rest + id,
},
};
};
@ -123,11 +123,11 @@ export const createNetwork = (config: {
id: number;
name: string;
lndNodes: number;
lightningdNodes: number;
clightningNodes: number;
bitcoindNodes: number;
status?: Status;
}): Network => {
const { id, name, lndNodes, lightningdNodes, bitcoindNodes } = config;
const { id, name, lndNodes, clightningNodes, bitcoindNodes } = config;
// need explicit undefined check because Status.Starting is 0
const status = config.status !== undefined ? config.status : Status.Stopped;
@ -152,9 +152,9 @@ export const createNetwork = (config: {
);
});
range(lightningdNodes).forEach(() => {
range(clightningNodes).forEach(() => {
network.nodes.lightning.push(
createLightningdNetworkNode(network, LightningdVersion.latest, status),
createCLightningNetworkNode(network, CLightningVersion.latest, status),
);
});
@ -231,7 +231,7 @@ export const getOpenPorts = async (network: Network): Promise<OpenPorts | undefi
}
}
let { lnd, lightningd } = groupNodes(network);
let { lnd, clightning } = groupNodes(network);
// filter out nodes that are already started since their ports are in use by themselves
lnd = lnd.filter(n => n.status !== Status.Started);
@ -256,13 +256,13 @@ export const getOpenPorts = async (network: Network): Promise<OpenPorts | undefi
}
}
lightningd = lightningd.filter(n => n.status !== Status.Started);
if (lightningd.length) {
const existingPorts = lightningd.map(n => n.ports.rest);
clightning = clightning.filter(n => n.status !== Status.Started);
if (clightning.length) {
const existingPorts = clightning.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 };
ports[clightning[index].name] = { rpc: port };
});
}
}

39
src/utils/objects.ts

@ -0,0 +1,39 @@
const isArray = (arg: any) => Array.isArray(arg);
const isObject = (arg: any) =>
arg === Object(arg) && !isArray(arg) && typeof arg !== 'function';
/**
* Converts a string from snake case to camel case
* ex: 'my_fav_text' -> 'myFavText'
* @param key the string to convert from snake_case to camelCase
*/
const toCamel = (key: string) => {
// use regex to replace underscore + lowercase char with uppercase char
// ex: _a -> A
return key.replace(/([_][a-z])/gi, underChar => {
return underChar.substring(1).toUpperCase();
});
};
/**
* Recursively converts all the keys in the provided object to be camelCase
* @param arg the object to convert
*/
export const snakeKeysToCamel = (arg: any): any => {
if (isObject(arg)) {
const newObj: Record<string, any> = {};
// convert each key to camel case
Object.keys(arg).forEach(k => {
newObj[toCamel(k)] = snakeKeysToCamel(arg[k]);
});
return newObj;
} else if (isArray(arg)) {
const arr = arg as Array<any>;
return arr.map(i => {
return snakeKeysToCamel(i);
});
}
return arg;
};

6
src/utils/tests.tsx

@ -14,7 +14,7 @@ export const getNetwork = (networkId = 1, name?: string, status?: Status): Netwo
id: networkId,
name: name || 'my-test',
lndNodes: 2,
lightningdNodes: 0,
clightningNodes: 0,
bitcoindNodes: 1,
status,
});
@ -57,6 +57,10 @@ export const injections: StoreInjections = {
listChannels: jest.fn(),
pendingChannels: jest.fn(),
},
clightningService: {
waitUntilOnline: jest.fn(),
getInfo: jest.fn(),
},
};
/**

Loading…
Cancel
Save