Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ark #1127

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft

Add Ark #1127

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@
"internalConsoleOptions": "neverOpen",
"env": { "CI": "true" },
"disableOptimisticBPs": true
},
{
"type": "node",
"request": "launch",
"name": "Debug: Polar",
"runtimeExecutable": "/usr/bin/env",
"args": ["-S", "yarn", "run", "dev"],
"smartStep": true,
"sourceMaps": true
}
]
}
2 changes: 1 addition & 1 deletion docker/bitcoind/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ RUN chmod a+x /entrypoint.sh

VOLUME ["/home/bitcoin/.bitcoin"]

EXPOSE 18443 18444 28334 28335
EXPOSE 18443 18444 28334 28335 28336

ENTRYPOINT ["/entrypoint.sh"]

Expand Down
145 changes: 145 additions & 0 deletions electron/ark/arkdProxyServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as ARKD from '@lightningpolar/arkd-api';
import { IpcMain } from 'electron';
import { debug } from 'electron-log';
import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import { convertUInt8ArraysToHex, ipcChannels } from '../../src/shared';
import { ArkdNode } from '../../src/shared/types';
import { toJSON } from '../../src/shared/utils';
import { IpcMappingFor } from '../utils/types';

/**
* mapping of node name and network <-> ArkRpcApis to cache these objects. The getRpc function
* reads from disk, so this gives us a small bit of performance improvement
*/
let rpcCache: Record<string, ARKD.ArkdClient> = {};

interface DefaultArgs {
node: ArkdNode;
}

/**
* Helper function to lookup a node by name in the cache or create it if
* it doesn't exist
*/
const getRpc = async (node: ArkdNode): Promise<ARKD.ArkdClient> => {
const { name, networkId } = node;
const id = `n${networkId}-${name}`;
if (!rpcCache[id]) {
const { ports, paths } = node as ArkdNode;
const options: ARKD.ArkdClientOptions = {
socket: `127.0.0.1:${ports.api}`,
cert: existsSync(paths.tlsCert)
? (await readFile(paths.tlsCert)).toString('hex')
: undefined,
macaroon: existsSync(paths.macaroon)
? (await readFile(paths.macaroon)).toString('hex')
: undefined,
};
rpcCache[id] = ARKD.ArkdClient.create(options);
}
return rpcCache[id];
};

type ArkChannels = typeof ipcChannels.ark;

/**
* A mapping of electron IPC channel names to the functions to execute when
* messages are received
*/
const listeners: IpcMappingFor<ArkChannels, [DefaultArgs]> = {
[ipcChannels.ark.getInfo]: async args => {
const rpc = await getRpc(args.node);
return rpc.arkService.getInfo();
},

[ipcChannels.ark.getWalletBalance]: async args => {
const rpc = await getRpc(args.node);
return rpc.wallet.getBalance();
},

[ipcChannels.ark.waitForReady]: async args => {
const rpc = await getRpc(args.node);
return rpc.arkService.waitForReady(30000);
},

[ipcChannels.ark.getWalletStatus]: async args => {
const rpc = await getRpc(args.node);
return rpc.wallet.getStatus();
},

[ipcChannels.ark.genSeed]: async args => {
const rpc = await getRpc(args.node);
return rpc.wallet.genSeed().then(({ seed }) => seed);
},

[ipcChannels.ark.createWallet]: async args => {
const rpc = await getRpc(args.node);
const { password, seed } = args as any;
return rpc.wallet.create({
password,
seed,
});
},

[ipcChannels.ark.unlockWallet]: async args => {
const rpc = await getRpc(args.node);
const { password } = args as any;
return rpc.wallet.unlock({
password,
});
},

[ipcChannels.ark.lockWallet]: async args => {
const rpc = await getRpc(args.node);
const { password } = args as any;
return rpc.wallet.lock({
password,
});
},
};

/**
* Sets up the IPC listeners for the main process and maps them to async
* functions.
* @param ipc the IPC object of the main process
*/
export const initArkdProxy = (ipc: IpcMain) => {
debug('ArkdProxyServer: initialize');
Object.entries(listeners).forEach(([channel, func]) => {
const requestChan = `arkd-${channel}-request`;
const responseChan = `arkd-${channel}-response`;

debug(`ArkdProxyServer: listening for ipc command "${channel}"`);
ipc.on(requestChan, async (event, ...args) => {
// the a message is received by the main process...
debug(`ArkdProxyServer: received request "${requestChan}"`, toJSON(args));
// inspect the first arg to see if it has a specific channel to reply to
let uniqueChan = responseChan;
if (args && args[0] && args[0].replyTo) {
uniqueChan = args[0].replyTo;
}
try {
// attempt to execute the associated function
let result = await func(...(args as [DefaultArgs]));
// merge the result with default values since LND omits falsy values
debug(`ArkdProxyServer: send response "${uniqueChan}"`, toJSON(result));
// convert UInt8Arrays to hex
result = convertUInt8ArraysToHex(result);
// response to the calling process with a reply
event.reply(uniqueChan, result);
} catch (err: any) {
// reply with an error message if the execution fails
debug(`ArkdProxyServer: send error "${uniqueChan}"`, toJSON(err));
event.reply(uniqueChan, { err: err.message });
}
});
});
};

/**
* Clears the cached rpc instances
*/
export const clearArkdProxyCache = () => {
rpcCache = {};
};
6 changes: 6 additions & 0 deletions electron/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type ValueOf<T> = T[keyof T];

export type IpcMappingFor<
TChan extends { [x: string]: string },
TDefaultArgs extends Array<any> = any,
> = Record<ValueOf<TChan>, (...args: TDefaultArgs) => Promise<any>>;
2 changes: 2 additions & 0 deletions electron/windowManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import windowState from 'electron-window-state';
import { join } from 'path';
import { initAppIpcListener } from './appIpcListener';
import { appMenuTemplate } from './appMenu';
import { initArkdProxy } from './ark/arkdProxyServer';
import { APP_ROOT, BASE_URL, IS_DEV } from './constants';
import { initLitdProxy } from './litd/litdProxyServer';
import {
Expand All @@ -24,6 +25,7 @@ class WindowManager {
initLndProxy(ipcMain);
initTapdProxy(ipcMain);
initLitdProxy(ipcMain);
initArkdProxy(ipcMain);
initAppIpcListener(ipcMain);
initLndSubscriptions(this.sendMessageToRenderer);
});
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,6 @@
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
2 changes: 1 addition & 1 deletion src/components/common/ImageUpdatesModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const mapUpdatesToDetails = (updates: Record<NodeImplementation, string[]>) => {
details.push(
...versions.map(version => ({
label: config.name,
value: `v${version}`,
value: version.startsWith('v') ? version : `v${version}`,
})),
);
});
Expand Down
19 changes: 14 additions & 5 deletions src/components/common/RemoveNode.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import React, { useEffect } from 'react';
import { CloseOutlined } from '@ant-design/icons';
import { Button, Form, Modal } from 'antd';
import { usePrefixedTranslation } from 'hooks';
import { BitcoinNode, CommonNode, LightningNode, Status, TapNode } from 'shared/types';
import React, { useEffect } from 'react';
import {
ArkNode,
BitcoinNode,
CommonNode,
LightningNode,
Status,
TapNode,
} from 'shared/types';
import { useStoreActions } from 'store';

interface Props {
Expand All @@ -13,9 +20,8 @@ interface Props {
const RemoveNode: React.FC<Props> = ({ node, type }) => {
const { l } = usePrefixedTranslation('cmps.common.RemoveNode');
const { notify } = useStoreActions(s => s.app);
const { removeLightningNode, removeBitcoinNode, removeTapNode } = useStoreActions(
s => s.network,
);
const { removeLightningNode, removeBitcoinNode, removeTapNode, removeArkNode } =
useStoreActions(s => s.network);

let modal: any;
const showRemoveModal = () => {
Expand Down Expand Up @@ -45,6 +51,9 @@ const RemoveNode: React.FC<Props> = ({ node, type }) => {
case 'tap':
await removeTapNode({ node: node as TapNode });
break;
case 'ark':
await removeArkNode({ node: node as ArkNode });
break;
default:
throw new Error(l('invalidType', { type: node.type }));
}
Expand Down
9 changes: 6 additions & 3 deletions src/components/designer/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { useMemo } from 'react';
import { IChart } from '@mrblenny/react-flow-chart';
import { BitcoindNode, LightningNode, TapNode } from 'shared/types';
import { ArkNode, BitcoindNode, LightningNode, TapNode } from 'shared/types';
import { Network } from 'types';
import BitcoindDetails from './bitcoin/BitcoinDetails';
import DefaultSidebar from './default/DefaultSidebar';
import LightningDetails from './lightning/LightningDetails';
import LinkDetails from './link/LinkDetails';
import TapDetails from './tap/TapDetails';
import ArkDetails from './ark/ArkDetails';

interface Props {
network: Network;
Expand All @@ -18,14 +19,16 @@ const Sidebar: React.FC<Props> = ({ network, chart }) => {
const { id, type } = chart.selected;

if (type === 'node') {
const { bitcoin, lightning, tap } = network.nodes;
const node = [...bitcoin, ...lightning, ...tap].find(n => n.name === id);
const { bitcoin, lightning, tap, ark } = network.nodes;
const node = [...bitcoin, ...lightning, ...tap, ...ark].find(n => n.name === id);
if (node && node.implementation === 'bitcoind') {
return <BitcoindDetails node={node as BitcoindNode} />;
} else if (node && node.type === 'lightning') {
return <LightningDetails node={node as LightningNode} />;
} else if (node && node.type === 'tap') {
return <TapDetails node={node as TapNode} />;
} else if (node && node.type === 'ark') {
return <ArkDetails node={node as ArkNode} />;
}
} else if (type === 'link' && id) {
const link = chart.links[id];
Expand Down
45 changes: 45 additions & 0 deletions src/components/designer/ark/ActionsTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import styled from '@emotion/styled';
import { Form } from 'antd';
import {
AdvancedOptionsButton,
RemoveNode,
RenameNodeButton,
RestartNode,
} from 'components/common';
import { ViewLogsButton } from 'components/dockerLogs';
import { OpenTerminalButton } from 'components/terminal';
import React from 'react';
import { ArkNode, Status } from 'shared/types';

const Styled = {
Spacer: styled.div`
height: 48px;
`,
};

interface Props {
node: ArkNode;
}

const ActionsTab: React.FC<Props> = ({ node }) => {
return (
<Form labelCol={{ span: 24 }}>
{node.status === Status.Started && (
<>
<p>TODO</p>
<Styled.Spacer />

<OpenTerminalButton node={node} />
<ViewLogsButton node={node} />
<Styled.Spacer />
</>
)}
<RestartNode node={node} />
<RenameNodeButton node={node} />
<AdvancedOptionsButton node={node} />
<RemoveNode node={node} />
</Form>
);
};

export default ActionsTab;
Loading