-
+
+
+
-
-
-
+
+
+
+
+
+
+
+ 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 @@
Node Alias |
+ Status |
Fee Rate |
Channel ID |
Capacity |
@@ -14,6 +15,11 @@
{{ channel.alias_left }}
|
+
+ Inactive
+ Active
+ Closed
+ |
{{ channel.node1_fee_rate / 10000 | number }}%
|
@@ -22,6 +28,11 @@
{{ channel.alias_right }}
|
+
+ Inactive
+ Active
+ Closed
+ |
{{ channel.node2_fee_rate / 10000 | number }}%
|
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(