From a94403b3a1bd3ec65435d1b4067ad8f90a8bb2f8 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 29 Jul 2022 16:33:07 +0200 Subject: [PATCH] Wrote some utility functions to convert clightning output to our db schema --- .../lightning/clightning/clightning-client.ts | 4 + .../clightning/clightning-convert.ts | 95 +++++++++++++++++++ .../lightning/clightning}/jsonrpc.ts | 12 +-- backend/src/config.ts | 8 ++ 4 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 backend/src/api/lightning/clightning/clightning-client.ts create mode 100644 backend/src/api/lightning/clightning/clightning-convert.ts rename backend/src/{rpc-api/core-lightning => api/lightning/clightning}/jsonrpc.ts (94%) diff --git a/backend/src/api/lightning/clightning/clightning-client.ts b/backend/src/api/lightning/clightning/clightning-client.ts new file mode 100644 index 000000000..2b974bca0 --- /dev/null +++ b/backend/src/api/lightning/clightning/clightning-client.ts @@ -0,0 +1,4 @@ +import config from '../../../config'; +import CLightningClient from './jsonrpc'; + +export default new CLightningClient(config.CLIGHTNING.SOCKET); diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts new file mode 100644 index 000000000..34ef6f942 --- /dev/null +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -0,0 +1,95 @@ +import logger from "../../../logger"; +import { ILightningApi } from "../lightning-api.interface"; + +export function convertNode(clNode: any): ILightningApi.Node { + return { + alias: clNode.alias ?? '', + color: `#${clNode.color ?? ''}`, + features: [], // TODO parse and return clNode.feature + public_key: clNode.nodeid, + sockets: clNode.addresses?.map(addr => `${addr.address}:${addr.port}`) ?? [], + updated_at: new Date((clNode?.last_timestamp ?? 0) * 1000).toUTCString(), + }; +} + +export function convertAndmergeBidirectionalChannels(clChannels: any[]): ILightningApi.Channel[] { + const consolidatedChannelList: ILightningApi.Channel[] = []; + const clChannelsDict = {}; + const clChannelsDictCount = {}; + + for (const clChannel of clChannels) { + if (!clChannelsDict[clChannel.short_channel_id]) { + clChannelsDict[clChannel.short_channel_id] = clChannel; + clChannelsDictCount[clChannel.short_channel_id] = 1; + } else { + consolidatedChannelList.push( + buildBidirectionalChannel(clChannel, clChannelsDict[clChannel.short_channel_id]) + ); + delete clChannelsDict[clChannel.short_channel_id]; + clChannelsDictCount[clChannel.short_channel_id]++; + } + } + const bidirectionalChannelsCount = consolidatedChannelList.length; + + for (const short_channel_id of Object.keys(clChannelsDict)) { + consolidatedChannelList.push(buildUnidirectionalChannel(clChannelsDict[short_channel_id])); + } + const unidirectionalChannelsCount = consolidatedChannelList.length - bidirectionalChannelsCount; + + logger.debug(`clightning knows ${clChannels.length} channels. ` + + `We found ${bidirectionalChannelsCount} bidirectional channels ` + + `and ${unidirectionalChannelsCount} unidirectional channels.`); + + return consolidatedChannelList; +} + +function buildBidirectionalChannel(clChannelA: any, clChannelB: any): ILightningApi.Channel { + const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0); + + return { + id: clChannelA.short_channel_id, + capacity: clChannelA.satoshis, + transaction_id: '', // TODO + transaction_vout: 0, // TODO + updated_at: new Date(lastUpdate * 1000).toUTCString(), + policies: [ + convertPolicy(clChannelA), + convertPolicy(clChannelB) + ] + }; +} + +function buildUnidirectionalChannel(clChannel: any): ILightningApi.Channel { + return { + id: clChannel.short_channel_id, + capacity: clChannel.satoshis, + policies: [convertPolicy(clChannel), getEmptyPolicy()], + transaction_id: '', // TODO + transaction_vout: 0, // TODO + updated_at: new Date((clChannel.last_update ?? 0) * 1000).toUTCString(), + }; +} + +function convertPolicy(clChannel: any): ILightningApi.Policy { + return { + public_key: clChannel.source, + base_fee_mtokens: clChannel.base_fee_millisatoshi, + fee_rate: clChannel.fee_per_millionth, + is_disabled: !clChannel.active, + max_htlc_mtokens: clChannel.htlc_maximum_msat.slice(0, -4), + min_htlc_mtokens: clChannel.htlc_minimum_msat.slice(0, -4), + updated_at: new Date((clChannel.last_update ?? 0) * 1000).toUTCString(), + }; +} + +function getEmptyPolicy(): ILightningApi.Policy { + return { + public_key: 'null', + base_fee_mtokens: '0', + fee_rate: 0, + is_disabled: true, + max_htlc_mtokens: '0', + min_htlc_mtokens: '0', + updated_at: new Date(0).toUTCString(), + }; +} diff --git a/backend/src/rpc-api/core-lightning/jsonrpc.ts b/backend/src/api/lightning/clightning/jsonrpc.ts similarity index 94% rename from backend/src/rpc-api/core-lightning/jsonrpc.ts rename to backend/src/api/lightning/clightning/jsonrpc.ts index 037dfff75..d0b187a54 100644 --- a/backend/src/rpc-api/core-lightning/jsonrpc.ts +++ b/backend/src/api/lightning/clightning/jsonrpc.ts @@ -1,3 +1,5 @@ +// Imported from https://github.com/shesek/lightning-client-js + 'use strict'; const methods = [ @@ -96,7 +98,7 @@ import { createConnection, Socket } from 'net'; import { homedir } from 'os'; import path from 'path'; import { createInterface, Interface } from 'readline'; -import logger from '../../logger'; +import logger from '../../../logger'; class LightningError extends Error { type: string = 'lightning'; @@ -113,7 +115,7 @@ const defaultRpcPath = path.join(homedir(), '.lightning') , fStat = (...p) => statSync(path.join(...p)) , fExists = (...p) => existsSync(path.join(...p)) -class CLightningClient extends EventEmitter { +export default class CLightningClient extends EventEmitter { private rpcPath: string; private reconnectWait: number; private reconnectTimeout; @@ -182,7 +184,7 @@ class CLightningClient extends EventEmitter { return; } const data = JSON.parse(line); - logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`); + // logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`); _self.emit('res:' + data.id, data); }); } @@ -210,7 +212,7 @@ class CLightningClient extends EventEmitter { }, this.reconnectWait * 1000); } - call(method, args = []): Promise { + call(method, args = []): Promise { const _self = this; const callInt = ++this.reqcount; @@ -245,5 +247,3 @@ methods.forEach(k => { return this.call(k, args); }; }); - -export default new CLightningClient(); diff --git a/backend/src/config.ts b/backend/src/config.ts index d480e6c51..b42a45ab2 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -38,6 +38,9 @@ interface IConfig { MACAROON_PATH: string; REST_API_URL: string; }; + CLIGHTNING: { + SOCKET: string; + }; ELECTRUM: { HOST: string; PORT: number; @@ -186,6 +189,9 @@ const defaults: IConfig = { 'MACAROON_PATH': '', 'REST_API_URL': 'https://localhost:8080', }, + 'CLIGHTNING': { + 'SOCKET': '', + }, 'SOCKS5PROXY': { 'ENABLED': false, 'USE_ONION': true, @@ -226,6 +232,7 @@ class Config implements IConfig { BISQ: IConfig['BISQ']; LIGHTNING: IConfig['LIGHTNING']; LND: IConfig['LND']; + CLIGHTNING: IConfig['CLIGHTNING']; SOCKS5PROXY: IConfig['SOCKS5PROXY']; PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER']; EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER']; @@ -244,6 +251,7 @@ class Config implements IConfig { this.BISQ = configs.BISQ; this.LIGHTNING = configs.LIGHTNING; this.LND = configs.LND; + this.CLIGHTNING = configs.CLIGHTNING; this.SOCKS5PROXY = configs.SOCKS5PROXY; this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER; this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;