diff --git a/electron/appIpcListener.ts b/electron/appIpcListener.ts index 9d8fd50..9903f48 100644 --- a/electron/appIpcListener.ts +++ b/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 => { console.warn('opwnWindow', args); diff --git a/src/components/designer/Sidebar.tsx b/src/components/designer/Sidebar.tsx index 234d36e..854f78d 100644 --- a/src/components/designer/Sidebar.tsx +++ b/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 = ({ network, chart }) => { return ; } else if (node && node.implementation === 'LND') { return ; + } else if (node && node.implementation === 'c-lightning') { + return ; } } else if (type === 'link' && id) { const link = chart.links[id]; diff --git a/src/components/designer/clightning/CLightningDetails.tsx b/src/components/designer/clightning/CLightningDetails.tsx new file mode 100644 index 0000000..7099051 --- /dev/null +++ b/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 = ({ 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: ( + + ), + }, + ]; + + 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)} + + + ); + const channels = ( + + {`${numActiveChannels} / ${numPendingChannels} / ${numInactiveChannels}`} + + ); + details.push( + { label: l('alias'), value: alias }, + { label: l('pubkey'), value: pubkey }, + { label: l('channels'), value: channels }, + ); + if (warningBitcoindSync) { + const synced = ( + {warningBitcoindSync} + ); + details.push({ label: l('syncedToChain'), value: synced }); + } + } + } + + return ( + + {getInfoAsync.loading && } + {node.status === Status.Starting && ( + } + /> + )} + {node.status === Status.Error && node.errorMsg && ( + + )} + {getInfoAsync.error && ( + + )} + + + ); +}; + +export default CLightningDetails; diff --git a/src/components/network/NewNetwork.tsx b/src/components/network/NewNetwork.tsx index 5b46ebe..4489fe9 100644 --- a/src/components/network/NewNetwork.tsx +++ b/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 = ({ form }) => { - - {form.getFieldDecorator('lightningdNodes', { + + {form.getFieldDecorator('clightningNodes', { rules: [{ required: true, message: l('cmps.forms.required') }], initialValue: 1, })()} diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 7f1e051..7636944 100644 --- a/src/i18n/locales/en-US.json +++ b/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", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 8953885..4c53cd4 100644 --- a/src/i18n/locales/es.json +++ b/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", diff --git a/src/lib/clightning/clightningService.ts b/src/lib/clightning/clightningService.ts new file mode 100644 index 0000000..5da9013 --- /dev/null +++ b/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 { + 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 { + 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(); diff --git a/src/lib/clightning/clightningTypes.ts b/src/lib/clightning/clightningTypes.ts new file mode 100644 index 0000000..e6498a8 --- /dev/null +++ b/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; +} diff --git a/src/lib/clightning/index.ts b/src/lib/clightning/index.ts new file mode 100644 index 0000000..4c5a96c --- /dev/null +++ b/src/lib/clightning/index.ts @@ -0,0 +1 @@ +export { default as clightningService } from './clightningService'; diff --git a/src/lib/docker/composeFile.ts b/src/lib/docker/composeFile.ts index aa274b6..9b1a15e 100644 --- a/src/lib/docker/composeFile.ts +++ b/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); } } diff --git a/src/lib/docker/dockerService.spec.ts b/src/lib/docker/dockerService.spec.ts index 71a3c53..078dd23 100644 --- a/src/lib/docker/dockerService.spec.ts +++ b/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'; diff --git a/src/lib/docker/dockerService.ts b/src/lib/docker/dockerService.ts index 577c239..ead375a 100644 --- a/src/lib/docker/dockerService.ts +++ b/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); } diff --git a/src/lib/docker/nodeTemplates.ts b/src/lib/docker/nodeTemplates.ts index 1d45ef5..24ec4ea 100644 --- a/src/lib/docker/nodeTemplates.ts +++ b/src/lib/docker/nodeTemplates.ts @@ -92,7 +92,7 @@ export const lnd = ( ], }); -export const lightningd = ( +export const clightning = ( name: string, container: string, version: string, diff --git a/src/lib/lightningd/index.ts b/src/lib/lightningd/index.ts deleted file mode 100644 index c41edb0..0000000 --- a/src/lib/lightningd/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as lightningdService } from './lightningdService'; diff --git a/src/lib/lightningd/lightningdService.ts b/src/lib/lightningd/lightningdService.ts deleted file mode 100644 index ed61c24..0000000 --- a/src/lib/lightningd/lightningdService.ts +++ /dev/null @@ -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(); diff --git a/src/resources/lightningd.png b/src/resources/clightning.png similarity index 100% rename from src/resources/lightningd.png rename to src/resources/clightning.png diff --git a/src/shared/types.ts b/src/shared/types.ts index ed144c4..8a5231e 100644 --- a/src/shared/types.ts +++ b/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; }; diff --git a/src/store/index.ts b/src/store/index.ts index 52ba854..33ed908 100644 --- a/src/store/index.ts +++ b/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 }); diff --git a/src/store/models/clightning.ts b/src/store/models/clightning.ts new file mode 100644 index 0000000..36ac4e7 --- /dev/null +++ b/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; + setInfo: Action; + getInfo: Thunk; +} + +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; diff --git a/src/store/models/designer.spec.ts b/src/store/models/designer.spec.ts index 9777df9..243d901 100644 --- a/src/store/models/designer.spec.ts +++ b/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); diff --git a/src/store/models/index.ts b/src/store/models/index.ts index 6bfbeef..f9a6542 100644 --- a/src/store/models/index.ts +++ b/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): RootModel => { network: networkModel, bitcoind: bitcoindModel, lnd: lndModel, + clightning: clightningModel, designer: designerModel, modals: modalsModel, }; diff --git a/src/store/models/network.spec.ts b/src/store/models/network.spec.ts index fb0eeda..7598be6 100644 --- a/src/store/models/network.spec.ts +++ b/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, }; diff --git a/src/store/models/network.ts b/src/store/models/network.ts index 0824191..31bf6e9 100644 --- a/src/store/models/network.ts +++ b/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) diff --git a/src/types/index.ts b/src/types/index.ts index 6307708..a30c1e2 100644 --- a/src/types/index.ts +++ b/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; } +export interface CLightningLibrary { + waitUntilOnline: (node: CLightningNode) => Promise; + getInfo: (node: CLightningNode) => Promise; +} + export interface StoreInjections { ipc: IpcSender; dockerService: DockerLibrary; bitcoindService: BitcoindLibrary; lndService: LndLibrary; + clightningService: CLightningLibrary; } export interface NetworksFile { diff --git a/src/utils/chart.ts b/src/utils/chart.ts index d2a23fe..8b1b701 100644 --- a/src/utils/chart.ts +++ b/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', diff --git a/src/utils/constants.ts b/src/utils/constants.ts index efb35b8..027e308 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -36,7 +36,7 @@ export const BasePorts = { rest: 8081, grpc: 10001, }, - lightningd: { + clightning: { rest: 8181, }, }; diff --git a/src/utils/network.ts b/src/utils/network.ts index 0f040e7..4b20956 100644 --- a/src/utils/network.ts +++ b/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 n.status !== Status.Started); @@ -256,13 +256,13 @@ export const getOpenPorts = async (network: Network): Promise 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 }; }); } } diff --git a/src/utils/objects.ts b/src/utils/objects.ts new file mode 100644 index 0000000..3a18c69 --- /dev/null +++ b/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 = {}; + // 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; + return arr.map(i => { + return snakeKeysToCamel(i); + }); + } + + return arg; +}; diff --git a/src/utils/tests.tsx b/src/utils/tests.tsx index fd22aec..992a0ad 100644 --- a/src/utils/tests.tsx +++ b/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(), + }, }; /**