Browse Source

feat(clightning): implement c-lightning openChannel

master
jamaljsr 5 years ago
parent
commit
47de591a10
  1. 10
      src/components/designer/lnd/actions/OpenChannelModal.tsx
  2. 45
      src/lib/clightning/clightningApi.ts
  3. 78
      src/lib/clightning/clightningService.ts
  4. 11
      src/lib/clightning/types.ts
  5. 3
      src/lib/lightning/notImplementedService.ts
  6. 11
      src/lib/lnd/lndService.ts
  7. 3
      src/store/models/designer.ts
  8. 5
      src/store/models/lnd.ts
  9. 1
      src/types/index.ts
  10. 1
      src/utils/tests/renderWithProviders.tsx

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

@ -6,7 +6,6 @@ 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';
@ -31,8 +30,7 @@ const OpenChannelModal: React.FC<Props> = ({ network, form }) => {
const getBalancesAsync = useAsync(async () => {
if (!visible) return;
const { lnd } = groupNodes(network);
for (const node of lnd) {
for (const node of network.nodes.lightning) {
await getWalletBalance(node);
}
}, [network.nodes, visible]);
@ -61,9 +59,9 @@ const OpenChannelModal: React.FC<Props> = ({ network, form }) => {
form.validateFields((err, values: FormFields) => {
if (err) return;
const { lnd } = groupNodes(network);
const fromNode = lnd.find(n => n.name === values.from);
const toNode = lnd.find(n => n.name === values.to);
const { lightning } = network.nodes;
const fromNode = lightning.find(n => n.name === values.from);
const toNode = lightning.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 });

45
src/lib/clightning/clightningApi.ts

@ -0,0 +1,45 @@
import { debug } from 'electron-log';
import { CLightningNode } from 'shared/types';
import { read } from 'utils/files';
import { snakeKeysToCamel } from 'utils/objects';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
const request = async <T>(
node: CLightningNode,
method: HttpMethod,
path: string,
body?: object,
): Promise<T> => {
debug(`c-lightning API Request`);
const url = `http://127.0.0.1:${node.ports.rest}/v1/${path}`;
debug(` - url: ${url}`);
if (body) debug(` - body: ${JSON.stringify(body, null, 2)}`);
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
macaroon: await read(node.paths.macaroon, 'base64'),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await response.json();
debug(` - resp: ${JSON.stringify(json, null, 2)}`);
return snakeKeysToCamel(json) as T;
};
export const httpGet = async <T>(node: CLightningNode, path: string): Promise<T> => {
return request<T>(node, 'GET', path);
};
export const httpPost = async <T>(
node: CLightningNode,
path: string,
body: object,
): Promise<T> => {
return request<T>(node, 'POST', path, body);
};

78
src/lib/clightning/clightningService.ts

@ -1,10 +1,8 @@
import { debug } from 'electron-log';
import { CLightningNode, LightningNode } from 'shared/types';
import * as PLN from 'lib/lightning/types';
import { LightningService } from 'types';
import { waitFor } from 'utils/async';
import { read } from 'utils/files';
import { snakeKeysToCamel } from 'utils/objects';
import { httpGet, httpPost } from './clightningApi';
import * as CLN from './types';
const ChannelStateToStatus: Record<
@ -24,7 +22,7 @@ const ChannelStateToStatus: Record<
class CLightningService implements LightningService {
async getInfo(node: LightningNode): Promise<PLN.LightningNodeInfo> {
const info = await this.request<CLN.GetInfoResponse>(node, 'getinfo');
const info = await httpGet<CLN.GetInfoResponse>(this.cast(node), 'getinfo');
return {
pubkey: info.id,
alias: info.alias,
@ -39,7 +37,7 @@ class CLightningService implements LightningService {
}
async getBalances(node: LightningNode): Promise<PLN.LightningNodeBalances> {
const balances = await this.request<CLN.GetBalanceResponse>(node, 'getBalance');
const balances = await httpGet<CLN.GetBalanceResponse>(this.cast(node), 'getBalance');
return {
total: balances.totalBalance.toString(),
confirmed: balances.confBalance.toString(),
@ -48,23 +46,25 @@ class CLightningService implements LightningService {
}
async getNewAddress(node: LightningNode): Promise<PLN.LightningNodeAddress> {
const address = await this.request<PLN.LightningNodeAddress>(node, 'newaddr');
const address = await httpGet<PLN.LightningNodeAddress>(this.cast(node), 'newaddr');
return address;
}
async getChannels(node: LightningNode): Promise<PLN.LightningNodeChannel[]> {
const channels = await this.request<CLN.GetChannelsResponse[]>(
node,
const channels = await httpGet<CLN.GetChannelsResponse[]>(
this.cast(node),
'channel/listChannels',
);
return channels
.filter(c => ChannelStateToStatus[c.state] !== 'Closed')
.map(c => {
const status = ChannelStateToStatus[c.state];
// c-lightning doesn't return the output index. hard-code to 0
const channelPoint = `${c.fundingTxid}:0`;
return {
pending: status !== 'Open',
uniqueId: c.fundingTxid.slice(-12),
channelPoint: c.fundingTxid,
uniqueId: channelPoint.slice(-12),
channelPoint,
pubkey: c.id,
capacity: this.toSats(c.msatoshiTotal),
localBalance: this.toSats(c.msatoshiToUs),
@ -75,7 +75,7 @@ class CLightningService implements LightningService {
}
async getPeers(node: LightningNode): Promise<PLN.LightningNodePeer[]> {
const peers = await this.request<CLN.Peer[]>(node, 'peer/listPeers');
const peers = await httpGet<CLN.Peer[]>(this.cast(node), 'peer/listPeers');
return peers
.filter(p => p.connected)
.map(p => ({
@ -84,21 +84,40 @@ class CLightningService implements LightningService {
}));
}
async connectPeer(node: LightningNode, toRpcUrl: string): Promise<void> {
const body = { id: toRpcUrl };
await httpPost<{ id: string }>(this.cast(node), 'peer/connect', body);
}
async openChannel(
from: LightningNode,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
toRpcUrl: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
amount: string,
): Promise<PLN.LightningNodeChannelPoint> {
/* Sample output
{
"tx": "020000000001018375970adb157e76bdf1214d7b874dd7870fc29fc7797be37965ffd2dc534d4f0100000000ffffffff0290d0030000000000220020af14c0deff170638da0652d69766cbe38cfc1e9653f5297723ea717379ed6fc116710b000000000016001406586c3ae5327631466f3db75f949416c2c03e8502483045022100f6066c5a93363b0888aaf6abf02e77ffcb590f2bf2d6a7d9ee6dc6984332a3760220310ef982ccc6f8d04905863dba9e850f5e77af4c935ee624d9bda385da134b83012103016ad498f69789e2fb373fdedaa06c1a3acde26c4e810509357677e91d91aaf100000000",
"txid": "2b15604898e54dd0157d01a08b8f4cd1862b621506e80eb078b92c7259033bcd",
"channel_id": "cd3b0359722cb978b00ee80615622b86d14c8f8ba0017d15d04de5984860152b"
}
*/
throw new Error(`openChannel is not implemented for ${from.implementation} nodes`);
// get peers of source node
const clnFrom = this.cast(from);
const peers = await this.getPeers(clnFrom);
// get pubkey of dest node
const [toPubKey] = toRpcUrl.split('@');
// add peer if not connected
if (!peers.some(p => p.pubkey === toPubKey)) {
await this.connectPeer(clnFrom, toRpcUrl);
}
// open the channel
const body: CLN.OpenChannelRequest = { id: toPubKey, satoshis: amount };
const res = await httpPost<CLN.OpenChannelResponse>(
this.cast(from),
'channel/openChannel',
body,
);
return {
txid: res.txid,
// c-lightning doesn't return the output index. hard-code to 0
index: 0,
};
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -131,23 +150,6 @@ class CLightningService implements LightningService {
);
}
private async request<T>(node: LightningNode, path: string) {
debug(`c-lightning API Request`);
const { paths, ports } = this.cast(node);
const url = `http://127.0.0.1:${ports.rest}/v1/${path}`;
debug(` - url: ${url}`);
const macaroon = await read(paths.macaroon, 'base64');
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
macaroon,
},
});
const json = await response.json();
debug(` - resp: ${JSON.stringify(json, null, 2)}`);
return snakeKeysToCamel(json) as T;
}
private toSats(msats: number): string {
return (msats / 1000).toString();
}

11
src/lib/clightning/types.ts

@ -74,3 +74,14 @@ export interface Peer {
localfeatures: string;
globalfeatures: string;
}
export interface OpenChannelRequest {
id: string;
satoshis: string;
}
export interface OpenChannelResponse {
tx: string;
txid: string;
channelId: string;
}

3
src/lib/lightning/notImplementedService.ts

@ -27,6 +27,9 @@ class NotImplementedService implements LightningService {
getPeers(node: LightningNode): Promise<PLN.LightningNodePeer[]> {
throw new Error(`getPeers is not implemented for ${node.implementation} nodes`);
}
connectPeer(node: LightningNode, toRpcUrl: string): Promise<void> {
throw new Error(`connectPeer is not implemented for ${node.implementation} nodes`);
}
openChannel(
from: LightningNode,
toRpcUrl: string,

11
src/lib/lnd/lndService.ts

@ -61,6 +61,12 @@ class LndService implements LightningService {
}));
}
async connectPeer(node: LightningNode, toRpcUrl: string): Promise<void> {
const [toPubKey, host] = toRpcUrl.split('@');
const addr: LND.LightningAddress = { pubkey: toPubKey, host };
await proxy.connectPeer(this.cast(node), { addr });
}
async openChannel(
from: LightningNode,
toRpcUrl: string,
@ -71,11 +77,10 @@ class LndService implements LightningService {
const peers = await this.getPeers(lndFrom);
// get pubkey of dest node
const [toPubKey, host] = toRpcUrl.split('@');
const [toPubKey] = toRpcUrl.split('@');
// add peer if not connected
if (!peers.some(p => p.pubkey === toPubKey)) {
const addr: LND.LightningAddress = { pubkey: toPubKey, host };
await proxy.connectPeer(lndFrom, { addr });
await this.connectPeer(lndFrom, toRpcUrl);
}
// open channel

3
src/store/models/designer.ts

@ -15,7 +15,6 @@ import { LndNode, Status } from 'shared/types';
import { Network, StoreInjections } from 'types';
import { createLightningChartNode, 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 './';
@ -98,7 +97,7 @@ const designerModel: DesignerModel = {
syncChart: thunk(
async (actions, network, { getState, getStoreState, getStoreActions }) => {
// fetch data from all of the nodes
await Promise.all(groupNodes(network).lnd.map(getStoreActions().lnd.getAllInfo));
await Promise.all(network.nodes.lightning.map(getStoreActions().lnd.getAllInfo));
const nodesData = getStoreState().lnd.nodes;
const { allCharts } = getState();

5
src/store/models/lnd.ts

@ -125,9 +125,8 @@ const lndModel: LndModel = {
}
// get the rpcUrl of the destination node
const toNode = getStoreState().lnd.nodes[to.name];
if (!toNode || !toNode.info) {
await actions.getInfo(to);
}
if (!toNode || !toNode.info) await actions.getInfo(to);
// cast because it should never be undefined after calling getInfo above
const { rpcUrl } = getStoreState().lnd.nodes[to.name].info as LightningNodeInfo;
// open the channel via LND
const api = injections.lightningFactory.getService(from);

1
src/types/index.ts

@ -53,6 +53,7 @@ export interface LightningService {
getNewAddress: (node: LightningNode) => Promise<PLN.LightningNodeAddress>;
getChannels: (node: LightningNode) => Promise<PLN.LightningNodeChannel[]>;
getPeers: (node: LightningNode) => Promise<PLN.LightningNodePeer[]>;
connectPeer: (node: LightningNode, toRpcUrl: string) => Promise<void>;
openChannel: (
from: LightningNode,
toRpcUrl: string,

1
src/utils/tests/renderWithProviders.tsx

@ -13,6 +13,7 @@ export const lightningServiceMock: jest.Mocked<LightningService> = {
getNewAddress: jest.fn(),
getChannels: jest.fn(),
getPeers: jest.fn(),
connectPeer: jest.fn(),
openChannel: jest.fn(),
closeChannel: jest.fn(),
waitUntilOnline: jest.fn(),

Loading…
Cancel
Save