diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index c50680147..c775135cb 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -2,7 +2,9 @@

Channel {{ channel.id }}

- Open + Inactive + Active + Closed
@@ -46,98 +48,95 @@
-

Peers

-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - -
Node - {{ channel.alias_left }} -
- - {{ channel.node1_public_key | shortenString : 18 }} - - -
Fee rate - {{ channel.node1_fee_rate / 10000 | number }}% -
Base fee - -
Min HTLC - -
Max HTLC - -
+
+
+ -
-
- - - - - - - - - - - - - - - - - - - - - - - -
Node - {{ channel.alias_right }} -
- - {{ channel.node2_public_key | shortenString : 18 }} - - -
Fee rate - {{ channel.node2_fee_rate / 10000 | number }}% -
Base fee - -
Min HTLC - -
Max HTLC - -
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
Fee rate + {{ channel.node1_fee_rate / 10000 | number }}% +
Base fee + +
Min HTLC + +
Max HTLC + +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + +
Fee rate + {{ channel.node2_fee_rate / 10000 | number }}% +
Base fee + +
Min HTLC + +
Max HTLC + +
+
+
- +
diff --git a/frontend/src/app/lightning/channel/channel.component.scss b/frontend/src/app/lightning/channel/channel.component.scss index ccf88f131..a6878a23c 100644 --- a/frontend/src/app/lightning/channel/channel.component.scss +++ b/frontend/src/app/lightning/channel/channel.component.scss @@ -1,3 +1,3 @@ .badges { - font-size: 18px; + font-size: 20px; } \ No newline at end of file diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index 77073ca64..2ca201ca6 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -3,6 +3,7 @@ + @@ -14,6 +15,11 @@ + @@ -22,6 +28,11 @@ + diff --git a/lightning-backend/mempool-config.sample.json b/lightning-backend/mempool-config.sample.json index 4ad9ffdd1..eac34ddf4 100644 --- a/lightning-backend/mempool-config.sample.json +++ b/lightning-backend/mempool-config.sample.json @@ -17,6 +17,12 @@ "TSL_CERT_PATH": "", "MACAROON_PATH": "" }, + "CORE_RPC": { + "HOST": "127.0.0.1", + "PORT": 8332, + "USERNAME": "mempool", + "PASSWORD": "mempool" + }, "DATABASE": { "HOST": "127.0.0.1", "PORT": 3306, diff --git a/lightning-backend/src/api/bitcoin/bitcoin-client.ts b/lightning-backend/src/api/bitcoin/bitcoin-client.ts new file mode 100644 index 000000000..43e76a041 --- /dev/null +++ b/lightning-backend/src/api/bitcoin/bitcoin-client.ts @@ -0,0 +1,12 @@ +import config from '../../config'; +const bitcoin = require('./rpc-api/index'); + +const nodeRpcCredentials: any = { + host: config.CORE_RPC.HOST, + port: config.CORE_RPC.PORT, + user: config.CORE_RPC.USERNAME, + pass: config.CORE_RPC.PASSWORD, + timeout: 60000, +}; + +export default new bitcoin.Client(nodeRpcCredentials); diff --git a/lightning-backend/src/api/bitcoin/rpc-api/commands.ts b/lightning-backend/src/api/bitcoin/rpc-api/commands.ts new file mode 100644 index 000000000..ea9bd7bf0 --- /dev/null +++ b/lightning-backend/src/api/bitcoin/rpc-api/commands.ts @@ -0,0 +1,92 @@ +module.exports = { + addMultiSigAddress: 'addmultisigaddress', + addNode: 'addnode', // bitcoind v0.8.0+ + backupWallet: 'backupwallet', + createMultiSig: 'createmultisig', + createRawTransaction: 'createrawtransaction', // bitcoind v0.7.0+ + decodeRawTransaction: 'decoderawtransaction', // bitcoind v0.7.0+ + decodeScript: 'decodescript', + dumpPrivKey: 'dumpprivkey', + dumpWallet: 'dumpwallet', // bitcoind v0.9.0+ + encryptWallet: 'encryptwallet', + estimateFee: 'estimatefee', // bitcoind v0.10.0x + estimatePriority: 'estimatepriority', // bitcoind v0.10.0+ + generate: 'generate', // bitcoind v0.11.0+ + getAccount: 'getaccount', + getAccountAddress: 'getaccountaddress', + getAddedNodeInfo: 'getaddednodeinfo', // bitcoind v0.8.0+ + getAddressesByAccount: 'getaddressesbyaccount', + getBalance: 'getbalance', + getBestBlockHash: 'getbestblockhash', // bitcoind v0.9.0+ + getBlock: 'getblock', + getBlockStats: 'getblockstats', + getBlockFilter: 'getblockfilter', + getBlockchainInfo: 'getblockchaininfo', // bitcoind v0.9.2+ + getBlockCount: 'getblockcount', + getBlockHash: 'getblockhash', + getBlockHeader: 'getblockheader', + getBlockTemplate: 'getblocktemplate', // bitcoind v0.7.0+ + getChainTips: 'getchaintips', // bitcoind v0.10.0+ + getChainTxStats: 'getchaintxstats', + getConnectionCount: 'getconnectioncount', + getDifficulty: 'getdifficulty', + getGenerate: 'getgenerate', + getInfo: 'getinfo', + getMempoolAncestors: 'getmempoolancestors', + getMempoolDescendants: 'getmempooldescendants', + getMempoolEntry: 'getmempoolentry', + getMempoolInfo: 'getmempoolinfo', // bitcoind v0.10+ + getMiningInfo: 'getmininginfo', + getNetTotals: 'getnettotals', + getNetworkInfo: 'getnetworkinfo', // bitcoind v0.9.2+ + getNetworkHashPs: 'getnetworkhashps', // bitcoind v0.9.0+ + getNewAddress: 'getnewaddress', + getPeerInfo: 'getpeerinfo', // bitcoind v0.7.0+ + getRawChangeAddress: 'getrawchangeaddress', // bitcoin v0.9+ + getRawMemPool: 'getrawmempool', // bitcoind v0.7.0+ + getRawTransaction: 'getrawtransaction', // bitcoind v0.7.0+ + getReceivedByAccount: 'getreceivedbyaccount', + getReceivedByAddress: 'getreceivedbyaddress', + getTransaction: 'gettransaction', + getTxOut: 'gettxout', // bitcoind v0.7.0+ + getTxOutProof: 'gettxoutproof', // bitcoind v0.11.0+ + getTxOutSetInfo: 'gettxoutsetinfo', // bitcoind v0.7.0+ + getUnconfirmedBalance: 'getunconfirmedbalance', // bitcoind v0.9.0+ + getWalletInfo: 'getwalletinfo', // bitcoind v0.9.2+ + help: 'help', + importAddress: 'importaddress', // bitcoind v0.10.0+ + importPrivKey: 'importprivkey', + importWallet: 'importwallet', // bitcoind v0.9.0+ + keypoolRefill: 'keypoolrefill', + keyPoolRefill: 'keypoolrefill', + listAccounts: 'listaccounts', + listAddressGroupings: 'listaddressgroupings', // bitcoind v0.7.0+ + listLockUnspent: 'listlockunspent', // bitcoind v0.8.0+ + listReceivedByAccount: 'listreceivedbyaccount', + listReceivedByAddress: 'listreceivedbyaddress', + listSinceBlock: 'listsinceblock', + listTransactions: 'listtransactions', + listUnspent: 'listunspent', // bitcoind v0.7.0+ + lockUnspent: 'lockunspent', // bitcoind v0.8.0+ + move: 'move', + ping: 'ping', // bitcoind v0.9.0+ + prioritiseTransaction: 'prioritisetransaction', // bitcoind v0.10.0+ + sendFrom: 'sendfrom', + sendMany: 'sendmany', + sendRawTransaction: 'sendrawtransaction', // bitcoind v0.7.0+ + sendToAddress: 'sendtoaddress', + setAccount: 'setaccount', + setGenerate: 'setgenerate', + setTxFee: 'settxfee', + signMessage: 'signmessage', + signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+ + stop: 'stop', + submitBlock: 'submitblock', // bitcoind v0.7.0+ + validateAddress: 'validateaddress', + verifyChain: 'verifychain', // bitcoind v0.9.0+ + verifyMessage: 'verifymessage', + verifyTxOutProof: 'verifytxoutproof', // bitcoind v0.11.0+ + walletLock: 'walletlock', + walletPassphrase: 'walletpassphrase', + walletPassphraseChange: 'walletpassphrasechange' +} diff --git a/lightning-backend/src/api/bitcoin/rpc-api/index.ts b/lightning-backend/src/api/bitcoin/rpc-api/index.ts new file mode 100644 index 000000000..131e1a048 --- /dev/null +++ b/lightning-backend/src/api/bitcoin/rpc-api/index.ts @@ -0,0 +1,61 @@ +var commands = require('./commands') +var rpc = require('./jsonrpc') + +// ===----------------------------------------------------------------------===// +// JsonRPC +// ===----------------------------------------------------------------------===// +function Client (opts) { + // @ts-ignore + this.rpc = new rpc.JsonRPC(opts) +} + +// ===----------------------------------------------------------------------===// +// cmd +// ===----------------------------------------------------------------------===// +Client.prototype.cmd = function () { + var args = [].slice.call(arguments) + var cmd = args.shift() + + callRpc(cmd, args, this.rpc) +} + +// ===----------------------------------------------------------------------===// +// callRpc +// ===----------------------------------------------------------------------===// +function callRpc (cmd, args, rpc) { + var fn = args[args.length - 1] + + // If the last argument is a callback, pop it from the args list + if (typeof fn === 'function') { + args.pop() + } else { + fn = function () {} + } + + return rpc.call(cmd, args, function () { + var args = [].slice.call(arguments) + // @ts-ignore + args.unshift(null) + // @ts-ignore + fn.apply(this, args) + }, function (err) { + fn(err) + }) +} + +// ===----------------------------------------------------------------------===// +// Initialize wrappers +// ===----------------------------------------------------------------------===// +(function () { + for (var protoFn in commands) { + (function (protoFn) { + Client.prototype[protoFn] = function () { + var args = [].slice.call(arguments) + return callRpc(commands[protoFn], args, this.rpc) + } + })(protoFn) + } +})() + +// Export! +module.exports.Client = Client; diff --git a/lightning-backend/src/api/bitcoin/rpc-api/jsonrpc.ts b/lightning-backend/src/api/bitcoin/rpc-api/jsonrpc.ts new file mode 100644 index 000000000..4f7a38baa --- /dev/null +++ b/lightning-backend/src/api/bitcoin/rpc-api/jsonrpc.ts @@ -0,0 +1,162 @@ +var http = require('http') +var https = require('https') + +var JsonRPC = function (opts) { + // @ts-ignore + this.opts = opts || {} + // @ts-ignore + this.http = this.opts.ssl ? https : http +} + +JsonRPC.prototype.call = function (method, params) { + return new Promise((resolve, reject) => { + var time = Date.now() + var requestJSON + + if (Array.isArray(method)) { + // multiple rpc batch call + requestJSON = [] + method.forEach(function (batchCall, i) { + requestJSON.push({ + id: time + '-' + i, + method: batchCall.method, + params: batchCall.params + }) + }) + } else { + // single rpc call + requestJSON = { + id: time, + method: method, + params: params + } + } + + // First we encode the request into JSON + requestJSON = JSON.stringify(requestJSON) + + // prepare request options + var requestOptions = { + host: this.opts.host || 'localhost', + port: this.opts.port || 8332, + method: 'POST', + path: '/', + headers: { + 'Host': this.opts.host || 'localhost', + 'Content-Length': requestJSON.length + }, + agent: false, + rejectUnauthorized: this.opts.ssl && this.opts.sslStrict !== false + } + + if (this.opts.ssl && this.opts.sslCa) { + // @ts-ignore + requestOptions.ca = this.opts.sslCa + } + + // use HTTP auth if user and password set + if (this.opts.user && this.opts.pass) { + // @ts-ignore + requestOptions.auth = this.opts.user + ':' + this.opts.pass + } + + // Now we'll make a request to the server + var cbCalled = false + var request = this.http.request(requestOptions) + + // start request timeout timer + var reqTimeout = setTimeout(function () { + if (cbCalled) return + cbCalled = true + request.abort() + var err = new Error('ETIMEDOUT') + // @ts-ignore + err.code = 'ETIMEDOUT' + reject(err) + }, this.opts.timeout || 30000) + + // set additional timeout on socket in case of remote freeze after sending headers + request.setTimeout(this.opts.timeout || 30000, function () { + if (cbCalled) return + cbCalled = true + request.abort() + var err = new Error('ESOCKETTIMEDOUT') + // @ts-ignore + err.code = 'ESOCKETTIMEDOUT' + reject(err) + }) + + request.on('error', function (err) { + if (cbCalled) return + cbCalled = true + clearTimeout(reqTimeout) + reject(err) + }) + + request.on('response', function (response) { + clearTimeout(reqTimeout) + + // We need to buffer the response chunks in a nonblocking way. + var buffer = '' + response.on('data', function (chunk) { + buffer = buffer + chunk + }) + // When all the responses are finished, we decode the JSON and + // depending on whether it's got a result or an error, we call + // emitSuccess or emitError on the promise. + response.on('end', function () { + var err + + if (cbCalled) return + cbCalled = true + + try { + var decoded = JSON.parse(buffer) + } catch (e) { + if (response.statusCode !== 200) { + err = new Error('Invalid params, response status code: ' + response.statusCode) + err.code = -32602 + reject(err) + } else { + err = new Error('Problem parsing JSON response from server') + err.code = -32603 + reject(err) + } + return + } + + if (!Array.isArray(decoded)) { + decoded = [decoded] + } + + // iterate over each response, normally there will be just one + // unless a batch rpc call response is being processed + decoded.forEach(function (decodedResponse, i) { + if (decodedResponse.hasOwnProperty('error') && decodedResponse.error != null) { + if (reject) { + err = new Error(decodedResponse.error.message || '') + if (decodedResponse.error.code) { + err.code = decodedResponse.error.code + } + reject(err) + } + } else if (decodedResponse.hasOwnProperty('result')) { + // @ts-ignore + resolve(decodedResponse.result, response.headers) + } else { + if (reject) { + err = new Error(decodedResponse.error.message || '') + if (decodedResponse.error.code) { + err.code = decodedResponse.error.code + } + reject(err) + } + } + }) + }) + }) + request.end(requestJSON); + }); +} + +module.exports.JsonRPC = JsonRPC diff --git a/lightning-backend/src/api/nodes/channels.api.ts b/lightning-backend/src/api/nodes/channels.api.ts index 9f02981c3..157cbc97a 100644 --- a/lightning-backend/src/api/nodes/channels.api.ts +++ b/lightning-backend/src/api/nodes/channels.api.ts @@ -2,6 +2,28 @@ import logger from '../../logger'; import DB from '../../database'; class ChannelsApi { + public async $getAllChannels(): Promise { + try { + const query = `SELECT * FROM channels`; + const [rows]: any = await DB.query(query); + return rows; + } catch (e) { + logger.err('$getChannel error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + + public async $getChannelsByStatus(status: number): Promise { + try { + const query = `SELECT * FROM channels WHERE status = ?`; + const [rows]: any = await DB.query(query, [status]); + return rows; + } catch (e) { + logger.err('$getChannel error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + public async $getChannel(shortId: string): Promise { try { const query = `SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.* FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key WHERE channels.id = ?`; diff --git a/lightning-backend/src/config.ts b/lightning-backend/src/config.ts index 48c237174..9b71dd977 100644 --- a/lightning-backend/src/config.ts +++ b/lightning-backend/src/config.ts @@ -19,6 +19,12 @@ interface IConfig { TSL_CERT_PATH: string; MACAROON_PATH: string; }; + CORE_RPC: { + HOST: string; + PORT: number; + USERNAME: string; + PASSWORD: string; + }; DATABASE: { HOST: string, SOCKET: string, @@ -48,6 +54,12 @@ const defaults: IConfig = { 'TSL_CERT_PATH': '', 'MACAROON_PATH': '', }, + 'CORE_RPC': { + 'HOST': '127.0.0.1', + 'PORT': 8332, + 'USERNAME': 'mempool', + 'PASSWORD': 'mempool' + }, 'DATABASE': { 'HOST': '127.0.0.1', 'SOCKET': '', @@ -62,6 +74,7 @@ class Config implements IConfig { MEMPOOL: IConfig['MEMPOOL']; SYSLOG: IConfig['SYSLOG']; LN_NODE_AUTH: IConfig['LN_NODE_AUTH']; + CORE_RPC: IConfig['CORE_RPC']; DATABASE: IConfig['DATABASE']; constructor() { @@ -69,6 +82,7 @@ class Config implements IConfig { this.MEMPOOL = configs.MEMPOOL; this.SYSLOG = configs.SYSLOG; this.LN_NODE_AUTH = configs.LN_NODE_AUTH; + this.CORE_RPC = configs.CORE_RPC; this.DATABASE = configs.DATABASE; } diff --git a/lightning-backend/src/tasks/node-sync.service.ts b/lightning-backend/src/tasks/node-sync.service.ts index 90e353028..cd257a483 100644 --- a/lightning-backend/src/tasks/node-sync.service.ts +++ b/lightning-backend/src/tasks/node-sync.service.ts @@ -3,6 +3,8 @@ import DB from '../database'; import logger from '../logger'; import lightningApi from '../api/lightning/lightning-api-factory'; import { ILightningApi } from '../api/lightning/lightning-api.interface'; +import channelsApi from '../api/nodes/channels.api'; +import bitcoinClient from '../api/bitcoin/bitcoin-client'; class NodeSyncService { constructor() {} @@ -25,15 +27,35 @@ class NodeSyncService { await this.$saveNode(node); } + await this.$setChannelsInactive(); + for (const channel of networkGraph.channels) { await this.$saveChannel(channel); } + + await this.$scanForClosedChannels(); + + } catch (e) { + logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e)); + } + } + + private async $scanForClosedChannels() { + try { + const channels = await channelsApi.$getChannelsByStatus(0); + for (const channel of channels) { + const outspends = await bitcoinClient.getTxOut(channel.transaction_id, channel.transaction_vout); + if (outspends === null) { + logger.debug('Marking channel: ' + channel.id + ' as closed.'); + await DB.query(`UPDATE channels SET status = 2 WHERE id = ?`, [channel.id]); + } + } } catch (e) { logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e)); } } - private async $saveChannel(channel: ILightningApi.Channel) { + private async $saveChannel(channel: ILightningApi.Channel): Promise { try { const d = new Date(Date.parse(channel.updated_at)); const query = `INSERT INTO channels @@ -43,6 +65,7 @@ class NodeSyncService { transaction_id, transaction_vout, updated_at, + status, node1_public_key, node1_base_fee_mtokens, node1_cltv_delta, @@ -60,10 +83,11 @@ class NodeSyncService { node2_min_htlc_mtokens, node2_updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE capacity = ?, updated_at = ?, + status = 1, node1_public_key = ?, node1_base_fee_mtokens = ?, node1_cltv_delta = ?, @@ -128,7 +152,15 @@ class NodeSyncService { } } - private async $saveNode(node: ILightningApi.Node) { + private async $setChannelsInactive(): Promise { + try { + await DB.query(`UPDATE channels SET status = 0 WHERE status = 1`); + } catch (e) { + logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e)); + } + } + + private async $saveNode(node: ILightningApi.Node): Promise { try { const updatedAt = this.utcDateToMysql(node.updated_at); const query = `INSERT INTO nodes(
Node AliasStatus Fee Rate Channel ID Capacity {{ channel.alias_left }} + Inactive + Active + Closed + {{ channel.node1_fee_rate / 10000 | number }}% {{ channel.alias_right }} + Inactive + Active + Closed + {{ channel.node2_fee_rate / 10000 | number }}%