Channel status

This commit is contained in:
softsimon 2022-05-01 15:35:28 +04:00
parent 795bb6a7a6
commit 65c731e1ad
No known key found for this signature in database
GPG Key ID: 488D7DCFB5A430D7
11 changed files with 502 additions and 91 deletions

View File

@ -2,7 +2,9 @@
<div class="mb-2">
<h1 i18n="shared.address" class="mb-0">Channel <a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.id }}</a> <app-clipboard [text]="channel.id"></app-clipboard></h1>
<div class="badges">
<span class="badge rounded-pill badge-success">Open</span>
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
</div>
</div>
@ -46,98 +48,95 @@
</div>
<br>
<h2>Peers</h2>
<div class="box">
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="address.total-sent">Node</td>
<td>
{{ channel.alias_left }}
<br>
<a [routerLink]="['/lightning/node' | relativeUrl, channel.node1_public_key]" >
{{ channel.node1_public_key | shortenString : 18 }}
</a>
<app-clipboard [text]="channel.node1_public_key"></app-clipboard>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Fee rate</td>
<td>
{{ channel.node1_fee_rate / 10000 | number }}%
</td>
</tr>
<tr>
<td i18n="address.total-sent">Base fee</td>
<td>
<app-sats [satoshis]="channel.node1_base_fee_mtokens / 1000"></app-sats>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Min HTLC</td>
<td>
<app-sats [satoshis]="channel.node1_min_htlc_mtokens / 1000"></app-sats>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Max HTLC</td>
<td>
<app-sats [satoshis]="channel.node1_max_htlc_mtokens / 1000"></app-sats>
</td>
</tr>
</tbody>
</table>
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<div class="mb-2">
<h2 class="mb-0">{{ channel.alias_left }}</h2>
<a [routerLink]="['/lightning/node' | relativeUrl, channel.node1_public_key]" >
{{ channel.node1_public_key | shortenString : 18 }}
</a>
<app-clipboard [text]="channel.node1_public_key"></app-clipboard>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="address.total-sent">Node</td>
<td>
{{ channel.alias_right }}
<br>
<a [routerLink]="['/lightning/node' | relativeUrl, channel.node2_public_key]" >
{{ channel.node2_public_key | shortenString : 18 }}
</a>
<app-clipboard [text]="channel.node1_public_key"></app-clipboard>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Fee rate</td>
<td>
{{ channel.node2_fee_rate / 10000 | number }}%
</td>
</tr>
<tr>
<td i18n="address.total-sent">Base fee</td>
<td>
<app-sats [satoshis]="channel.node2_base_fee_mtokens / 1000"></app-sats>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Min HTLC</td>
<td>
<app-sats [satoshis]="channel.node2_min_htlc_mtokens / 1000"></app-sats>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Max HTLC</td>
<td>
<app-sats [satoshis]="channel.node2_max_htlc_mtokens / 1000"></app-sats>
</td>
</tr>
</tbody>
</table>
<div class="box">
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="address.total-sent">Fee rate</td>
<td>
{{ channel.node1_fee_rate / 10000 | number }}%
</td>
</tr>
<tr>
<td i18n="address.total-sent">Base fee</td>
<td>
<app-sats [satoshis]="channel.node1_base_fee_mtokens / 1000"></app-sats>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Min HTLC</td>
<td>
<app-sats [satoshis]="channel.node1_min_htlc_mtokens / 1000"></app-sats>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Max HTLC</td>
<td>
<app-sats [satoshis]="channel.node1_max_htlc_mtokens / 1000"></app-sats>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col">
<div class="mb-2">
<h2 class="mb-0">{{ channel.alias_right }}</h2>
<a [routerLink]="['/lightning/node' | relativeUrl, channel.node2_public_key]" >
{{ channel.node2_public_key | shortenString : 18 }}
</a>
<app-clipboard [text]="channel.node1_public_key"></app-clipboard>
</div>
<div class="box">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="address.total-sent">Fee rate</td>
<td>
{{ channel.node2_fee_rate / 10000 | number }}%
</td>
</tr>
<tr>
<td i18n="address.total-sent">Base fee</td>
<td>
<app-sats [satoshis]="channel.node2_base_fee_mtokens / 1000"></app-sats>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Min HTLC</td>
<td>
<app-sats [satoshis]="channel.node2_min_htlc_mtokens / 1000"></app-sats>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Max HTLC</td>
<td>
<app-sats [satoshis]="channel.node2_max_htlc_mtokens / 1000"></app-sats>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,3 +1,3 @@
.badges {
font-size: 18px;
font-size: 20px;
}

View File

@ -3,6 +3,7 @@
<table class="table table-borderless">
<thead>
<th class="alias text-left" i18n="nodes.alias">Node Alias</th>
<th class="alias text-left" i18n="nodes.alias">Status</th>
<th class="channels text-right" i18n="channels.rate">Fee Rate</th>
<th class="capacity text-right" i18n="channels.id">Channel ID</th>
<th class="capacity text-right" i18n="nodes.capacity">Capacity</th>
@ -14,6 +15,11 @@
<td class="alias text-left">
<a [routerLink]="['/lightning/node' | relativeUrl, channel.node1_public_key]">{{ channel.alias_left }}</a>
</td>
<td>
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
</td>
<td class="capacity text-right">
{{ channel.node1_fee_rate / 10000 | number }}%
</td>
@ -22,6 +28,11 @@
<td class="alias text-left" *ngIf="channel.node1_public_key === publicKey">
<a [routerLink]="['/lightning/node' | relativeUrl, channel.node2_public_key]">{{ channel.alias_right }}</a>
</td>
<td>
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
</td>
<td class="capacity text-right">
{{ channel.node2_fee_rate / 10000 | number }}%
</td>

View File

@ -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,

View File

@ -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);

View File

@ -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'
}

View File

@ -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;

View File

@ -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

View File

@ -2,6 +2,28 @@ import logger from '../../logger';
import DB from '../../database';
class ChannelsApi {
public async $getAllChannels(): Promise<any[]> {
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<any[]> {
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<any> {
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 = ?`;

View File

@ -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;
}

View File

@ -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<void> {
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<void> {
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<void> {
try {
const updatedAt = this.utcDateToMysql(node.updated_at);
const query = `INSERT INTO nodes(