Merge Lightning backend into Mempool backend
This commit is contained in:
parent
faafa6db3b
commit
a238420d7f
@ -66,6 +66,15 @@
|
|||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
|
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
|
||||||
},
|
},
|
||||||
|
"LIGHTNING": {
|
||||||
|
"ENABLED": false,
|
||||||
|
"BACKEND": "lnd"
|
||||||
|
},
|
||||||
|
"LND_NODE_AUTH": {
|
||||||
|
"TLS_CERT_PATH": "tls.cert",
|
||||||
|
"MACAROON_PATH": "admin.macaroon",
|
||||||
|
"SOCKET": "localhost:10009"
|
||||||
|
},
|
||||||
"SOCKS5PROXY": {
|
"SOCKS5PROXY": {
|
||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
"USE_ONION": true,
|
"USE_ONION": true,
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
"bolt07": "^1.8.1",
|
"bolt07": "^1.8.1",
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
"express": "^4.18.0",
|
"express": "^4.18.0",
|
||||||
|
"ln-service": "^53.17.4",
|
||||||
"mysql2": "2.3.3",
|
"mysql2": "2.3.3",
|
||||||
"node-worker-threads-pool": "^1.5.1",
|
"node-worker-threads-pool": "^1.5.1",
|
||||||
"socks-proxy-agent": "~7.0.0",
|
"socks-proxy-agent": "~7.0.0",
|
||||||
|
@ -13,6 +13,7 @@ export interface AbstractBitcoinApi {
|
|||||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$getAddressPrefix(prefix: string): string[];
|
$getAddressPrefix(prefix: string): string[];
|
||||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||||
|
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
||||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||||
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||||
}
|
}
|
||||||
|
@ -130,6 +130,16 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return this.bitcoindClient.sendRawTransaction(rawTransaction);
|
return this.bitcoindClient.sendRawTransaction(rawTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||||
|
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
|
||||||
|
return {
|
||||||
|
spent: txOut === null,
|
||||||
|
status: {
|
||||||
|
confirmed: true,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
async $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
||||||
const outSpends: IEsploraApi.Outspend[] = [];
|
const outSpends: IEsploraApi.Outspend[] = [];
|
||||||
const tx = await this.$getRawTransaction(txId, true, false);
|
const tx = await this.$getRawTransaction(txId, true, false);
|
||||||
@ -195,7 +205,9 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
sequence: vin.sequence,
|
sequence: vin.sequence,
|
||||||
txid: vin.txid || '',
|
txid: vin.txid || '',
|
||||||
vout: vin.vout || 0,
|
vout: vin.vout || 0,
|
||||||
witness: vin.txinwitness,
|
witness: vin.txinwitness || [],
|
||||||
|
inner_redeemscript_asm: '',
|
||||||
|
inner_witnessscript_asm: '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -25,10 +25,10 @@ export namespace IEsploraApi {
|
|||||||
is_coinbase: boolean;
|
is_coinbase: boolean;
|
||||||
scriptsig: string;
|
scriptsig: string;
|
||||||
scriptsig_asm: string;
|
scriptsig_asm: string;
|
||||||
inner_redeemscript_asm?: string;
|
inner_redeemscript_asm: string;
|
||||||
inner_witnessscript_asm?: string;
|
inner_witnessscript_asm: string;
|
||||||
sequence: any;
|
sequence: any;
|
||||||
witness?: string[];
|
witness: string[];
|
||||||
prevout: Vout | null;
|
prevout: Vout | null;
|
||||||
// Elements
|
// Elements
|
||||||
is_pegin?: boolean;
|
is_pegin?: boolean;
|
||||||
|
@ -66,6 +66,11 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||||
|
return axios.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
|
}
|
||||||
|
|
||||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
||||||
return axios.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
|
return axios.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
|
@ -4,7 +4,7 @@ import logger from '../logger';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 24;
|
private static currentVersion = 25;
|
||||||
private queryTimeout = 120000;
|
private queryTimeout = 120000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -248,6 +248,15 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
|
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
|
||||||
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
|
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 25 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery(`INSERT INTO state VALUES('last_node_stats', 0, '1970-01-01');`);
|
||||||
|
await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
|
||||||
|
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
|
||||||
|
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
|
||||||
|
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -569,6 +578,82 @@ class DatabaseMigration {
|
|||||||
adjustment float NOT NULL,
|
adjustment float NOT NULL,
|
||||||
PRIMARY KEY (height),
|
PRIMARY KEY (height),
|
||||||
INDEX (time)
|
INDEX (time)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateLightningStatisticsQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS lightning_stats (
|
||||||
|
id int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
added datetime NOT NULL,
|
||||||
|
channel_count int(11) NOT NULL,
|
||||||
|
node_count int(11) NOT NULL,
|
||||||
|
total_capacity double unsigned NOT NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateNodesQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS nodes (
|
||||||
|
public_key varchar(66) NOT NULL,
|
||||||
|
first_seen datetime NOT NULL,
|
||||||
|
updated_at datetime NOT NULL,
|
||||||
|
alias varchar(200) CHARACTER SET utf8mb4 NOT NULL,
|
||||||
|
color varchar(200) NOT NULL,
|
||||||
|
sockets text DEFAULT NULL,
|
||||||
|
PRIMARY KEY (public_key),
|
||||||
|
KEY alias (alias(10))
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateChannelsQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS channels (
|
||||||
|
id bigint(11) unsigned NOT NULL,
|
||||||
|
short_id varchar(15) NOT NULL DEFAULT '',
|
||||||
|
capacity bigint(20) unsigned NOT NULL,
|
||||||
|
transaction_id varchar(64) NOT NULL,
|
||||||
|
transaction_vout int(11) NOT NULL,
|
||||||
|
updated_at datetime DEFAULT NULL,
|
||||||
|
created datetime DEFAULT NULL,
|
||||||
|
status int(11) NOT NULL DEFAULT 0,
|
||||||
|
closing_transaction_id varchar(64) DEFAULT NULL,
|
||||||
|
closing_date datetime DEFAULT NULL,
|
||||||
|
closing_reason int(11) DEFAULT NULL,
|
||||||
|
node1_public_key varchar(66) NOT NULL,
|
||||||
|
node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
|
||||||
|
node1_cltv_delta int(11) DEFAULT NULL,
|
||||||
|
node1_fee_rate bigint(11) DEFAULT NULL,
|
||||||
|
node1_is_disabled tinyint(1) DEFAULT NULL,
|
||||||
|
node1_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
|
||||||
|
node1_min_htlc_mtokens bigint(20) DEFAULT NULL,
|
||||||
|
node1_updated_at datetime DEFAULT NULL,
|
||||||
|
node2_public_key varchar(66) NOT NULL,
|
||||||
|
node2_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
|
||||||
|
node2_cltv_delta int(11) DEFAULT NULL,
|
||||||
|
node2_fee_rate bigint(11) DEFAULT NULL,
|
||||||
|
node2_is_disabled tinyint(1) DEFAULT NULL,
|
||||||
|
node2_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
|
||||||
|
node2_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
|
||||||
|
node2_updated_at datetime DEFAULT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY node1_public_key (node1_public_key),
|
||||||
|
KEY node2_public_key (node2_public_key),
|
||||||
|
KEY status (status),
|
||||||
|
KEY short_id (short_id),
|
||||||
|
KEY transaction_id (transaction_id),
|
||||||
|
KEY closing_transaction_id (closing_transaction_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateNodesStatsQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS node_stats (
|
||||||
|
id int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
public_key varchar(66) NOT NULL DEFAULT '',
|
||||||
|
added date NOT NULL,
|
||||||
|
capacity bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||||
|
channels int(11) unsigned NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY added (added,public_key),
|
||||||
|
KEY public_key (public_key)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ class ChannelsApi {
|
|||||||
|
|
||||||
public async $getClosedChannelsWithoutReason(): Promise<any[]> {
|
public async $getClosedChannelsWithoutReason(): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason IS NULL`;
|
const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason IS NULL AND closing_transaction_id != ''`;
|
||||||
const [rows]: any = await DB.query(query);
|
const [rows]: any = await DB.query(query);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
@ -1,16 +1,16 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { Express, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import channelsApi from './channels.api';
|
import channelsApi from './channels.api';
|
||||||
|
|
||||||
class ChannelsRoutes {
|
class ChannelsRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
public initRoutes(app: Express) {
|
public initRoutes(app: Application) {
|
||||||
app
|
app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/txids', this.$getChannelsByTransactionIds)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/txids', this.$getChannelsByTransactionIds)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/search/:search', this.$searchChannelsById)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/search/:search', this.$searchChannelsById)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/:short_id', this.$getChannel)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannelsForNode)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels', this.$getChannelsForNode)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,17 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { Express, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import nodesApi from './nodes.api';
|
import nodesApi from './nodes.api';
|
||||||
import channelsApi from './channels.api';
|
import channelsApi from './channels.api';
|
||||||
import statisticsApi from './statistics.api';
|
import statisticsApi from './statistics.api';
|
||||||
class GeneralRoutes {
|
class GeneralLightningRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
public initRoutes(app: Express) {
|
public initRoutes(app: Application) {
|
||||||
app
|
app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'search', this.$searchNodesAndChannels)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/search', this.$searchNodesAndChannels)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics', this.$getStatistics)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/statistics/latest', this.$getGeneralStats)
|
||||||
;
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/statistics', this.$getStatistics)
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $searchNodesAndChannels(req: Request, res: Response) {
|
private async $searchNodesAndChannels(req: Request, res: Response) {
|
||||||
@ -38,6 +39,15 @@ class GeneralRoutes {
|
|||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $getGeneralStats(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const statistics = await statisticsApi.$getLatestStatistics();
|
||||||
|
res.json(statistics);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new GeneralRoutes();
|
export default new GeneralLightningRoutes();
|
@ -46,20 +46,6 @@ class NodesApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getLatestStatistics(): Promise<any> {
|
|
||||||
try {
|
|
||||||
const [rows]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1`);
|
|
||||||
const [rows2]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1 OFFSET 72`);
|
|
||||||
return {
|
|
||||||
latest: rows[0],
|
|
||||||
previous: rows2[0],
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$getLatestStatistics error: ' + (e instanceof Error ? e.message : e));
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async $searchNodeByPublicKeyOrAlias(search: string) {
|
public async $searchNodeByPublicKeyOrAlias(search: string) {
|
||||||
try {
|
try {
|
||||||
const searchStripped = search.replace('%', '') + '%';
|
const searchStripped = search.replace('%', '') + '%';
|
@ -1,17 +1,16 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { Express, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import nodesApi from './nodes.api';
|
import nodesApi from './nodes.api';
|
||||||
class NodesRoutes {
|
class NodesRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
public initRoutes(app: Express) {
|
public initRoutes(app: Application) {
|
||||||
app
|
app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'nodes/search/:search', this.$searchNode)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/top', this.$getTopNodes)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key/statistics', this.$getHistoricalNodeStats)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode)
|
;
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $searchNode(req: Request, res: Response) {
|
private async $searchNode(req: Request, res: Response) {
|
||||||
@ -45,15 +44,6 @@ class NodesRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getGeneralStats(req: Request, res: Response) {
|
|
||||||
try {
|
|
||||||
const statistics = await nodesApi.$getLatestStatistics();
|
|
||||||
res.json(statistics);
|
|
||||||
} catch (e) {
|
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async $getTopNodes(req: Request, res: Response) {
|
private async $getTopNodes(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const topCapacityNodes = await nodesApi.$getTopCapacityNodes();
|
const topCapacityNodes = await nodesApi.$getTopCapacityNodes();
|
32
backend/src/api/explorer/statistics.api.ts
Normal file
32
backend/src/api/explorer/statistics.api.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import logger from '../../logger';
|
||||||
|
import DB from '../../database';
|
||||||
|
|
||||||
|
class StatisticsApi {
|
||||||
|
public async $getStatistics(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, node_count, total_capacity FROM lightning_stats ORDER BY id DESC`;
|
||||||
|
const [rows]: any = await DB.query(query);
|
||||||
|
return rows;
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getStatistics error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getLatestStatistics(): Promise<any> {
|
||||||
|
try {
|
||||||
|
const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1`);
|
||||||
|
const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1 OFFSET 72`);
|
||||||
|
return {
|
||||||
|
latest: rows[0],
|
||||||
|
previous: rows2[0],
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getLatestStatistics error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new StatisticsApi();
|
@ -3,7 +3,7 @@ import { AbstractLightningApi } from './lightning-api-abstract-factory';
|
|||||||
import LndApi from './lnd/lnd-api';
|
import LndApi from './lnd/lnd-api';
|
||||||
|
|
||||||
function lightningApiFactory(): AbstractLightningApi {
|
function lightningApiFactory(): AbstractLightningApi {
|
||||||
switch (config.MEMPOOL.BACKEND) {
|
switch (config.LIGHTNING.BACKEND) {
|
||||||
case 'lnd':
|
case 'lnd':
|
||||||
default:
|
default:
|
||||||
return new LndApi();
|
return new LndApi();
|
@ -8,14 +8,17 @@ import logger from '../../../logger';
|
|||||||
class LndApi implements AbstractLightningApi {
|
class LndApi implements AbstractLightningApi {
|
||||||
private lnd: any;
|
private lnd: any;
|
||||||
constructor() {
|
constructor() {
|
||||||
|
if (!config.LIGHTNING.ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const tls = fs.readFileSync(config.LN_NODE_AUTH.TLS_CERT_PATH).toString('base64');
|
const tls = fs.readFileSync(config.LND_NODE_AUTH.TLS_CERT_PATH).toString('base64');
|
||||||
const macaroon = fs.readFileSync(config.LN_NODE_AUTH.MACAROON_PATH).toString('base64');
|
const macaroon = fs.readFileSync(config.LND_NODE_AUTH.MACAROON_PATH).toString('base64');
|
||||||
|
|
||||||
const { lnd } = lnService.authenticatedLndGrpc({
|
const { lnd } = lnService.authenticatedLndGrpc({
|
||||||
cert: tls,
|
cert: tls,
|
||||||
macaroon: macaroon,
|
macaroon: macaroon,
|
||||||
socket: config.LN_NODE_AUTH.SOCKET,
|
socket: config.LND_NODE_AUTH.SOCKET,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.lnd = lnd;
|
this.lnd = lnd;
|
@ -27,6 +27,15 @@ interface IConfig {
|
|||||||
ESPLORA: {
|
ESPLORA: {
|
||||||
REST_API_URL: string;
|
REST_API_URL: string;
|
||||||
};
|
};
|
||||||
|
LIGHTNING: {
|
||||||
|
ENABLED: boolean;
|
||||||
|
BACKEND: 'lnd' | 'cln' | 'ldk';
|
||||||
|
};
|
||||||
|
LND_NODE_AUTH: {
|
||||||
|
TLS_CERT_PATH: string;
|
||||||
|
MACAROON_PATH: string;
|
||||||
|
SOCKET: string;
|
||||||
|
};
|
||||||
ELECTRUM: {
|
ELECTRUM: {
|
||||||
HOST: string;
|
HOST: string;
|
||||||
PORT: number;
|
PORT: number;
|
||||||
@ -158,6 +167,15 @@ const defaults: IConfig = {
|
|||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
|
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
|
||||||
},
|
},
|
||||||
|
'LIGHTNING': {
|
||||||
|
'ENABLED': false,
|
||||||
|
'BACKEND': 'lnd'
|
||||||
|
},
|
||||||
|
'LND_NODE_AUTH': {
|
||||||
|
'TLS_CERT_PATH': '',
|
||||||
|
'MACAROON_PATH': '',
|
||||||
|
'SOCKET': 'localhost:10009',
|
||||||
|
},
|
||||||
'SOCKS5PROXY': {
|
'SOCKS5PROXY': {
|
||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
'USE_ONION': true,
|
'USE_ONION': true,
|
||||||
@ -166,11 +184,11 @@ const defaults: IConfig = {
|
|||||||
'USERNAME': '',
|
'USERNAME': '',
|
||||||
'PASSWORD': ''
|
'PASSWORD': ''
|
||||||
},
|
},
|
||||||
"PRICE_DATA_SERVER": {
|
'PRICE_DATA_SERVER': {
|
||||||
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
|
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
|
||||||
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
|
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
|
||||||
},
|
},
|
||||||
"EXTERNAL_DATA_SERVER": {
|
'EXTERNAL_DATA_SERVER': {
|
||||||
'MEMPOOL_API': 'https://mempool.space/api/v1',
|
'MEMPOOL_API': 'https://mempool.space/api/v1',
|
||||||
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
|
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
|
||||||
'LIQUID_API': 'https://liquid.network/api/v1',
|
'LIQUID_API': 'https://liquid.network/api/v1',
|
||||||
@ -190,6 +208,8 @@ class Config implements IConfig {
|
|||||||
SYSLOG: IConfig['SYSLOG'];
|
SYSLOG: IConfig['SYSLOG'];
|
||||||
STATISTICS: IConfig['STATISTICS'];
|
STATISTICS: IConfig['STATISTICS'];
|
||||||
BISQ: IConfig['BISQ'];
|
BISQ: IConfig['BISQ'];
|
||||||
|
LIGHTNING: IConfig['LIGHTNING'];
|
||||||
|
LND_NODE_AUTH: IConfig['LND_NODE_AUTH'];
|
||||||
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
|
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
|
||||||
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
||||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||||
@ -205,6 +225,8 @@ class Config implements IConfig {
|
|||||||
this.SYSLOG = configs.SYSLOG;
|
this.SYSLOG = configs.SYSLOG;
|
||||||
this.STATISTICS = configs.STATISTICS;
|
this.STATISTICS = configs.STATISTICS;
|
||||||
this.BISQ = configs.BISQ;
|
this.BISQ = configs.BISQ;
|
||||||
|
this.LIGHTNING = configs.LIGHTNING;
|
||||||
|
this.LND_NODE_AUTH = configs.LND_NODE_AUTH;
|
||||||
this.SOCKS5PROXY = configs.SOCKS5PROXY;
|
this.SOCKS5PROXY = configs.SOCKS5PROXY;
|
||||||
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
||||||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||||
|
@ -29,6 +29,11 @@ import poolsUpdater from './tasks/pools-updater';
|
|||||||
import indexer from './indexer';
|
import indexer from './indexer';
|
||||||
import priceUpdater from './tasks/price-updater';
|
import priceUpdater from './tasks/price-updater';
|
||||||
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
|
||||||
|
import nodeSyncService from './tasks/lightning/node-sync.service';
|
||||||
|
import lightningStatsUpdater from './tasks/lightning/stats-updater.service';
|
||||||
|
import nodesRoutes from './api/explorer/nodes.routes';
|
||||||
|
import channelsRoutes from './api/explorer/channels.routes';
|
||||||
|
import generalLightningRoutes from './api/explorer/general.routes';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@ -130,6 +135,13 @@ class Server {
|
|||||||
bisqMarkets.startBisqService();
|
bisqMarkets.startBisqService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.LIGHTNING.ENABLED) {
|
||||||
|
nodeSyncService.$startService()
|
||||||
|
.then(() => {
|
||||||
|
lightningStatsUpdater.$startService();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
|
this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
|
||||||
if (worker) {
|
if (worker) {
|
||||||
logger.info(`Mempool Server worker #${process.pid} started`);
|
logger.info(`Mempool Server worker #${process.pid} started`);
|
||||||
@ -362,6 +374,12 @@ class Server {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.LIGHTNING.ENABLED) {
|
||||||
|
generalLightningRoutes.initRoutes(this.app);
|
||||||
|
nodesRoutes.initRoutes(this.app);
|
||||||
|
channelsRoutes.initRoutes(this.app);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { chanNumber } from 'bolt07';
|
import { chanNumber } from 'bolt07';
|
||||||
import DB from '../database';
|
import DB from '../../database';
|
||||||
import logger from '../logger';
|
import logger from '../../logger';
|
||||||
import lightningApi from '../api/lightning/lightning-api-factory';
|
import channelsApi from '../../api/explorer/channels.api';
|
||||||
import { ILightningApi } from '../api/lightning/lightning-api.interface';
|
import bitcoinClient from '../../api/bitcoin/bitcoin-client';
|
||||||
import channelsApi from '../api/explorer/channels.api';
|
import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
|
||||||
import bitcoinClient from '../api/bitcoin/bitcoin-client';
|
import config from '../../config';
|
||||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
|
||||||
import config from '../config';
|
import lightningApi from '../../api/lightning/lightning-api-factory';
|
||||||
import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
|
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
|
||||||
|
|
||||||
class NodeSyncService {
|
class NodeSyncService {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
@ -15,43 +15,36 @@ class NodeSyncService {
|
|||||||
public async $startService() {
|
public async $startService() {
|
||||||
logger.info('Starting node sync service');
|
logger.info('Starting node sync service');
|
||||||
|
|
||||||
await this.$updateNodes();
|
await this.$runUpdater();
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
await this.$updateNodes();
|
await this.$runUpdater();
|
||||||
}, 1000 * 60 * 60);
|
}, 1000 * 60 * 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $updateNodes() {
|
private async $runUpdater() {
|
||||||
try {
|
try {
|
||||||
|
logger.info(`Updating nodes and channels...`);
|
||||||
|
|
||||||
const networkGraph = await lightningApi.$getNetworkGraph();
|
const networkGraph = await lightningApi.$getNetworkGraph();
|
||||||
|
|
||||||
for (const node of networkGraph.nodes) {
|
for (const node of networkGraph.nodes) {
|
||||||
await this.$saveNode(node);
|
await this.$saveNode(node);
|
||||||
}
|
}
|
||||||
logger.debug(`Nodes updated`);
|
logger.info(`Nodes updated.`);
|
||||||
|
|
||||||
await this.$setChannelsInactive();
|
await this.$setChannelsInactive();
|
||||||
|
|
||||||
for (const channel of networkGraph.channels) {
|
for (const channel of networkGraph.channels) {
|
||||||
await this.$saveChannel(channel);
|
await this.$saveChannel(channel);
|
||||||
}
|
}
|
||||||
logger.debug(`Channels updated`);
|
logger.info(`Channels updated.`);
|
||||||
|
|
||||||
await this.$findInactiveNodesAndChannels();
|
await this.$findInactiveNodesAndChannels();
|
||||||
logger.debug(`Inactive channels scan complete`);
|
|
||||||
|
|
||||||
await this.$lookUpCreationDateFromChain();
|
await this.$lookUpCreationDateFromChain();
|
||||||
logger.debug(`Channel creation dates scan complete`);
|
|
||||||
|
|
||||||
await this.$updateNodeFirstSeen();
|
await this.$updateNodeFirstSeen();
|
||||||
logger.debug(`Node first seen dates scan complete`);
|
|
||||||
|
|
||||||
await this.$scanForClosedChannels();
|
await this.$scanForClosedChannels();
|
||||||
logger.debug(`Closed channels scan complete`);
|
|
||||||
|
|
||||||
await this.$runClosedChannelsForensics();
|
await this.$runClosedChannelsForensics();
|
||||||
logger.debug(`Closed channels forensics scan complete`);
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e));
|
||||||
@ -80,18 +73,21 @@ class NodeSyncService {
|
|||||||
await DB.query(query, params);
|
await DB.query(query, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger.info(`Node first seen dates scan complete.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $lookUpCreationDateFromChain() {
|
private async $lookUpCreationDateFromChain() {
|
||||||
|
logger.info(`Running channel creation date lookup...`);
|
||||||
try {
|
try {
|
||||||
const channels = await channelsApi.$getChannelsWithoutCreatedDate();
|
const channels = await channelsApi.$getChannelsWithoutCreatedDate();
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
const transaction = await bitcoinClient.getRawTransaction(channel.transaction_id, 1);
|
const transaction = await bitcoinClient.getRawTransaction(channel.transaction_id, 1);
|
||||||
await DB.query(`UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`, [transaction.blocktime, channel.id]);
|
await DB.query(`UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`, [transaction.blocktime, channel.id]);
|
||||||
}
|
}
|
||||||
|
logger.info(`Channel creation dates scan complete.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$setCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$setCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
@ -99,6 +95,8 @@ class NodeSyncService {
|
|||||||
|
|
||||||
// Looking for channels whos nodes are inactive
|
// Looking for channels whos nodes are inactive
|
||||||
private async $findInactiveNodesAndChannels(): Promise<void> {
|
private async $findInactiveNodesAndChannels(): Promise<void> {
|
||||||
|
logger.info(`Running inactive channels scan...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const [channels]: [ILightningApi.Channel[]] = await DB.query(`SELECT channels.id FROM channels WHERE channels.status = 1 AND ((SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node1_public_key) = 0 OR (SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node2_public_key) = 0)`);
|
const [channels]: [ILightningApi.Channel[]] = await DB.query(`SELECT channels.id FROM channels WHERE channels.status = 1 AND ((SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node1_public_key) = 0 OR (SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node2_public_key) = 0)`);
|
||||||
@ -106,6 +104,7 @@ class NodeSyncService {
|
|||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
await this.$updateChannelStatus(channel.id, 0);
|
await this.$updateChannelStatus(channel.id, 0);
|
||||||
}
|
}
|
||||||
|
logger.info(`Inactive channels scan complete.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$findInactiveNodesAndChannels() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$findInactiveNodesAndChannels() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
@ -113,6 +112,7 @@ class NodeSyncService {
|
|||||||
|
|
||||||
private async $scanForClosedChannels(): Promise<void> {
|
private async $scanForClosedChannels(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
logger.info(`Starting closed channels scan...`);
|
||||||
const channels = await channelsApi.$getChannelsByStatus(0);
|
const channels = await channelsApi.$getChannelsByStatus(0);
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
|
const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
|
||||||
@ -125,6 +125,7 @@ class NodeSyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger.info(`Closed channels scan complete.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
@ -140,8 +141,8 @@ class NodeSyncService {
|
|||||||
if (!config.ESPLORA.REST_API_URL) {
|
if (!config.ESPLORA.REST_API_URL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
logger.info(`Started running closed channel forensics...`);
|
||||||
const channels = await channelsApi.$getClosedChannelsWithoutReason();
|
const channels = await channelsApi.$getClosedChannelsWithoutReason();
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
let reason = 0;
|
let reason = 0;
|
||||||
@ -186,6 +187,7 @@ class NodeSyncService {
|
|||||||
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger.info(`Closed channels forensics scan complete.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
|
import logger from "../../logger";
|
||||||
import DB from '../database';
|
import DB from "../../database";
|
||||||
import logger from '../logger';
|
import lightningApi from "../../api/lightning/lightning-api-factory";
|
||||||
import lightningApi from '../api/lightning/lightning-api-factory';
|
|
||||||
|
|
||||||
class LightningStatsUpdater {
|
class LightningStatsUpdater {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
@ -29,6 +28,8 @@ class LightningStatsUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async $logNodeStatsDaily() {
|
private async $logNodeStatsDaily() {
|
||||||
|
logger.info(`Running daily node stats update...`);
|
||||||
|
|
||||||
const currentDate = new Date().toISOString().split('T')[0];
|
const currentDate = new Date().toISOString().split('T')[0];
|
||||||
try {
|
try {
|
||||||
const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`);
|
const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`);
|
||||||
@ -52,7 +53,7 @@ class LightningStatsUpdater {
|
|||||||
node.channels_count_left + node.channels_count_right]);
|
node.channels_count_left + node.channels_count_right]);
|
||||||
}
|
}
|
||||||
await DB.query(`UPDATE state SET string = ? WHERE name = 'last_node_stats'`, [currentDate]);
|
await DB.query(`UPDATE state SET string = ? WHERE name = 'last_node_stats'`, [currentDate]);
|
||||||
logger.debug('Daily node stats has updated.');
|
logger.info('Daily node stats has updated.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
@ -60,9 +61,11 @@ class LightningStatsUpdater {
|
|||||||
|
|
||||||
// We only run this on first launch
|
// We only run this on first launch
|
||||||
private async $populateHistoricalData() {
|
private async $populateHistoricalData() {
|
||||||
|
logger.info(`Running historical stats population...`);
|
||||||
|
|
||||||
const startTime = '2018-01-13';
|
const startTime = '2018-01-13';
|
||||||
try {
|
try {
|
||||||
const [rows]: any = await DB.query(`SELECT COUNT(*) FROM statistics`);
|
const [rows]: any = await DB.query(`SELECT COUNT(*) FROM lightning_stats`);
|
||||||
// Only store once per day
|
// Only store once per day
|
||||||
if (rows[0]['COUNT(*)'] > 0) {
|
if (rows[0]['COUNT(*)'] > 0) {
|
||||||
return;
|
return;
|
||||||
@ -86,7 +89,7 @@ class LightningStatsUpdater {
|
|||||||
channelsCount++;
|
channelsCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = `INSERT INTO statistics(
|
const query = `INSERT INTO lightning_stats(
|
||||||
added,
|
added,
|
||||||
channel_count,
|
channel_count,
|
||||||
node_count,
|
node_count,
|
||||||
@ -117,7 +120,7 @@ class LightningStatsUpdater {
|
|||||||
nodeCount++;
|
nodeCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = `UPDATE statistics SET node_count = ? WHERE added = FROM_UNIXTIME(?)`;
|
const query = `UPDATE lightning_stats SET node_count = ? WHERE added = FROM_UNIXTIME(?)`;
|
||||||
|
|
||||||
await DB.query(query, [
|
await DB.query(query, [
|
||||||
nodeCount,
|
nodeCount,
|
||||||
@ -128,13 +131,15 @@ class LightningStatsUpdater {
|
|||||||
date.setDate(date.getDate() + 1);
|
date.setDate(date.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('Historical stats populated.');
|
logger.info('Historical stats populated.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$populateHistoricalData() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$populateHistoricalData() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $logLightningStatsDaily() {
|
private async $logLightningStatsDaily() {
|
||||||
|
logger.info(`Running lightning daily stats log...`);
|
||||||
|
|
||||||
const currentDate = new Date().toISOString().split('T')[0];
|
const currentDate = new Date().toISOString().split('T')[0];
|
||||||
try {
|
try {
|
||||||
const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`);
|
const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`);
|
||||||
@ -151,7 +156,7 @@ class LightningStatsUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = `INSERT INTO statistics(
|
const query = `INSERT INTO lightning_stats(
|
||||||
added,
|
added,
|
||||||
channel_count,
|
channel_count,
|
||||||
node_count,
|
node_count,
|
||||||
@ -164,8 +169,9 @@ class LightningStatsUpdater {
|
|||||||
networkGraph.nodes.length,
|
networkGraph.nodes.length,
|
||||||
total_capacity,
|
total_capacity,
|
||||||
]);
|
]);
|
||||||
|
logger.info(`Lightning daily stats done.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$logLightningStats() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$logLightningStatsDaily() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -103,13 +103,13 @@ if (configContent && configContent.BASE_MODULE === 'bisq') {
|
|||||||
|
|
||||||
PROXY_CONFIG.push(...[
|
PROXY_CONFIG.push(...[
|
||||||
{
|
{
|
||||||
context: ['/lightning/api/v1/**'],
|
context: ['/testnet/api/v1/lightning/**'],
|
||||||
target: `http://localhost:8899`,
|
target: `http://localhost:8999`,
|
||||||
secure: false,
|
secure: false,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
proxyTimeout: 30000,
|
proxyTimeout: 30000,
|
||||||
pathRewrite: {
|
pathRewrite: {
|
||||||
"^/lightning/api": "/api"
|
"^/testnet": ""
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,23 +1,33 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { StateService } from '../services/state.service';
|
||||||
const API_BASE_URL = '/lightning/api/v1';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class LightningApiService {
|
export class LightningApiService {
|
||||||
|
private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private httpClient: HttpClient,
|
private httpClient: HttpClient,
|
||||||
) { }
|
private stateService: StateService,
|
||||||
|
) {
|
||||||
|
this.apiBasePath = ''; // assume mainnet by default
|
||||||
|
this.stateService.networkChanged$.subscribe((network) => {
|
||||||
|
if (network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND) {
|
||||||
|
network = '';
|
||||||
|
}
|
||||||
|
this.apiBasePath = network ? '/' + network : '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getNode$(publicKey: string): Observable<any> {
|
getNode$(publicKey: string): Observable<any> {
|
||||||
return this.httpClient.get<any>(API_BASE_URL + '/nodes/' + publicKey);
|
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannel$(shortId: string): Observable<any> {
|
getChannel$(shortId: string): Observable<any> {
|
||||||
return this.httpClient.get<any>(API_BASE_URL + '/channels/' + shortId);
|
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/channels/' + shortId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
|
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
|
||||||
@ -27,22 +37,22 @@ export class LightningApiService {
|
|||||||
.set('status', status)
|
.set('status', status)
|
||||||
;
|
;
|
||||||
|
|
||||||
return this.httpClient.get<any>(API_BASE_URL + '/channels', { params, observe: 'response' });
|
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/channels', { params, observe: 'response' });
|
||||||
}
|
}
|
||||||
|
|
||||||
getLatestStatistics$(): Observable<any> {
|
getLatestStatistics$(): Observable<any> {
|
||||||
return this.httpClient.get<any>(API_BASE_URL + '/statistics/latest');
|
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/statistics/latest');
|
||||||
}
|
}
|
||||||
|
|
||||||
listNodeStats$(publicKey: string): Observable<any> {
|
listNodeStats$(publicKey: string): Observable<any> {
|
||||||
return this.httpClient.get<any>(API_BASE_URL + '/nodes/' + publicKey + '/statistics');
|
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
|
||||||
}
|
}
|
||||||
|
|
||||||
listTopNodes$(): Observable<any> {
|
listTopNodes$(): Observable<any> {
|
||||||
return this.httpClient.get<any>(API_BASE_URL + '/nodes/top');
|
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/top');
|
||||||
}
|
}
|
||||||
|
|
||||||
listStatistics$(): Observable<any> {
|
listStatistics$(): Observable<any> {
|
||||||
return this.httpClient.get<any>(API_BASE_URL + '/statistics');
|
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/statistics');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,12 +237,12 @@ export class ApiService {
|
|||||||
txIds.forEach((txId: string) => {
|
txIds.forEach((txId: string) => {
|
||||||
params = params.append('txId[]', txId);
|
params = params.append('txId[]', txId);
|
||||||
});
|
});
|
||||||
return this.httpClient.get<{ inputs: any[], outputs: any[] }>(this.apiBaseUrl + this.apiBasePath + '/lightning/api/v1/channels/txids/', { params });
|
return this.httpClient.get<{ inputs: any[], outputs: any[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
lightningSearch$(searchText: string): Observable<any[]> {
|
lightningSearch$(searchText: string): Observable<any[]> {
|
||||||
let params = new HttpParams().set('searchText', searchText);
|
let params = new HttpParams().set('searchText', searchText);
|
||||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/lightning/api/v1/search', { params });
|
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
48
lightning-backend/.gitignore
vendored
48
lightning-backend/.gitignore
vendored
@ -1,48 +0,0 @@
|
|||||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# production config and external assets
|
|
||||||
*.json
|
|
||||||
!mempool-config.sample.json
|
|
||||||
!package.json
|
|
||||||
!package-lock.json
|
|
||||||
!tslint.json
|
|
||||||
!tsconfig.json
|
|
||||||
|
|
||||||
# compiled output
|
|
||||||
/dist
|
|
||||||
/tmp
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
|
|
||||||
# IDEs and editors
|
|
||||||
/.idea
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.c9/
|
|
||||||
*.launch
|
|
||||||
.settings/
|
|
||||||
|
|
||||||
# IDE - VSCode
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
|
|
||||||
# misc
|
|
||||||
/.sass-cache
|
|
||||||
/connect.lock
|
|
||||||
/coverage/*
|
|
||||||
/libpeerconnection.log
|
|
||||||
npm-debug.log
|
|
||||||
testem.log
|
|
||||||
/typings
|
|
||||||
|
|
||||||
# e2e
|
|
||||||
/e2e/*.js
|
|
||||||
/e2e/*.map
|
|
||||||
|
|
||||||
#System Files
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"MEMPOOL": {
|
|
||||||
"NETWORK": "mainnet",
|
|
||||||
"BACKEND": "lnd",
|
|
||||||
"HTTP_PORT": 8899,
|
|
||||||
"API_URL_PREFIX": "/api/v1/",
|
|
||||||
"STDOUT_LOG_MIN_PRIORITY": "debug"
|
|
||||||
},
|
|
||||||
"ESPLORA": {
|
|
||||||
"REST_API_URL": ""
|
|
||||||
},
|
|
||||||
"SYSLOG": {
|
|
||||||
"ENABLED": false,
|
|
||||||
"HOST": "127.0.0.1",
|
|
||||||
"PORT": 514,
|
|
||||||
"MIN_PRIORITY": "info",
|
|
||||||
"FACILITY": "local7"
|
|
||||||
},
|
|
||||||
"LN_NODE_AUTH": {
|
|
||||||
"TLS_CERT_PATH": "",
|
|
||||||
"MACAROON_PATH": "",
|
|
||||||
"SOCKET": "localhost:10009"
|
|
||||||
},
|
|
||||||
"CORE_RPC": {
|
|
||||||
"HOST": "127.0.0.1",
|
|
||||||
"PORT": 8332,
|
|
||||||
"USERNAME": "mempool",
|
|
||||||
"PASSWORD": "mempool"
|
|
||||||
},
|
|
||||||
"DATABASE": {
|
|
||||||
"HOST": "127.0.0.1",
|
|
||||||
"PORT": 3306,
|
|
||||||
"SOCKET": "/var/run/mysql/mysql.sock",
|
|
||||||
"DATABASE": "lightning",
|
|
||||||
"USERNAME": "root",
|
|
||||||
"PASSWORD": "root"
|
|
||||||
}
|
|
||||||
}
|
|
3291
lightning-backend/package-lock.json
generated
3291
lightning-backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "lightning-backend",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Backend for the Mempool Lightning Explorer",
|
|
||||||
"license": "AGPL-3.0",
|
|
||||||
"main": "index.ts",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"tsc": "./node_modules/typescript/bin/tsc",
|
|
||||||
"build": "npm run tsc",
|
|
||||||
"start": "node --max-old-space-size=2048 dist/index.js"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/express": "^4.17.13",
|
|
||||||
"@types/node": "^17.0.24"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^0.27.2",
|
|
||||||
"express": "^4.17.3",
|
|
||||||
"ln-service": "^53.11.0",
|
|
||||||
"mysql2": "^2.3.3",
|
|
||||||
"typescript": "^4.6.3"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import { IEsploraApi } from './esplora-api.interface';
|
|
||||||
|
|
||||||
export interface AbstractBitcoinApi {
|
|
||||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
|
||||||
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
|
||||||
$getBlockHeightTip(): Promise<number>;
|
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
|
||||||
$getBlockHash(height: number): Promise<string>;
|
|
||||||
$getBlockHeader(hash: string): Promise<string>;
|
|
||||||
$getBlock(hash: string): Promise<IEsploraApi.Block>;
|
|
||||||
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
|
||||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
|
||||||
$getAddressPrefix(prefix: string): string[];
|
|
||||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
|
||||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
|
||||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
|
||||||
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
|
||||||
}
|
|
||||||
export interface BitcoinRpcCredentials {
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
user: string;
|
|
||||||
pass: string;
|
|
||||||
timeout: number;
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import config from '../../config';
|
|
||||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
|
||||||
import EsploraApi from './esplora-api';
|
|
||||||
import BitcoinApi from './bitcoin-api';
|
|
||||||
import bitcoinClient from './bitcoin-client';
|
|
||||||
|
|
||||||
function bitcoinApiFactory(): AbstractBitcoinApi {
|
|
||||||
if (config.ESPLORA.REST_API_URL) {
|
|
||||||
return new EsploraApi();
|
|
||||||
} else {
|
|
||||||
return new BitcoinApi(bitcoinClient);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default bitcoinApiFactory();
|
|
@ -1,175 +0,0 @@
|
|||||||
export namespace IBitcoinApi {
|
|
||||||
export interface MempoolInfo {
|
|
||||||
loaded: boolean; // (boolean) True if the mempool is fully loaded
|
|
||||||
size: number; // (numeric) Current tx count
|
|
||||||
bytes: number; // (numeric) Sum of all virtual transaction sizes as defined in BIP 141.
|
|
||||||
usage: number; // (numeric) Total memory usage for the mempool
|
|
||||||
total_fee: number; // (numeric) Total fees of transactions in the mempool
|
|
||||||
maxmempool: number; // (numeric) Maximum memory usage for the mempool
|
|
||||||
mempoolminfee: number; // (numeric) Minimum fee rate in BTC/kB for tx to be accepted.
|
|
||||||
minrelaytxfee: number; // (numeric) Current minimum relay fee for transactions
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RawMempool { [txId: string]: MempoolEntry; }
|
|
||||||
|
|
||||||
export interface MempoolEntry {
|
|
||||||
vsize: number; // (numeric) virtual transaction size as defined in BIP 141.
|
|
||||||
weight: number; // (numeric) transaction weight as defined in BIP 141.
|
|
||||||
time: number; // (numeric) local time transaction entered pool in seconds since 1 Jan 1970 GMT
|
|
||||||
height: number; // (numeric) block height when transaction entered pool
|
|
||||||
descendantcount: number; // (numeric) number of in-mempool descendant transactions (including this one)
|
|
||||||
descendantsize: number; // (numeric) virtual transaction size of in-mempool descendants (including this one)
|
|
||||||
ancestorcount: number; // (numeric) number of in-mempool ancestor transactions (including this one)
|
|
||||||
ancestorsize: number; // (numeric) virtual transaction size of in-mempool ancestors (including this one)
|
|
||||||
wtxid: string; // (string) hash of serialized transactionumber; including witness data
|
|
||||||
fees: {
|
|
||||||
base: number; // (numeric) transaction fee in BTC
|
|
||||||
modified: number; // (numeric) transaction fee with fee deltas used for mining priority in BTC
|
|
||||||
ancestor: number; // (numeric) modified fees (see above) of in-mempool ancestors (including this one) in BTC
|
|
||||||
descendant: number; // (numeric) modified fees (see above) of in-mempool descendants (including this one) in BTC
|
|
||||||
};
|
|
||||||
depends: string[]; // (string) parent transaction id
|
|
||||||
spentby: string[]; // (array) unconfirmed transactions spending outputs from this transaction
|
|
||||||
'bip125-replaceable': boolean; // (boolean) Whether this transaction could be replaced due to BIP125 (replace-by-fee)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Block {
|
|
||||||
hash: string; // (string) the block hash (same as provided)
|
|
||||||
confirmations: number; // (numeric) The number of confirmations, or -1 if the block is not on the main chain
|
|
||||||
size: number; // (numeric) The block size
|
|
||||||
strippedsize: number; // (numeric) The block size excluding witness data
|
|
||||||
weight: number; // (numeric) The block weight as defined in BIP 141
|
|
||||||
height: number; // (numeric) The block height or index
|
|
||||||
version: number; // (numeric) The block version
|
|
||||||
versionHex: string; // (string) The block version formatted in hexadecimal
|
|
||||||
merkleroot: string; // (string) The merkle root
|
|
||||||
tx: Transaction[];
|
|
||||||
time: number; // (numeric) The block time expressed in UNIX epoch time
|
|
||||||
mediantime: number; // (numeric) The median block time expressed in UNIX epoch time
|
|
||||||
nonce: number; // (numeric) The nonce
|
|
||||||
bits: string; // (string) The bits
|
|
||||||
difficulty: number; // (numeric) The difficulty
|
|
||||||
chainwork: string; // (string) Expected number of hashes required to produce the chain up to this block (in hex)
|
|
||||||
nTx: number; // (numeric) The number of transactions in the block
|
|
||||||
previousblockhash: string; // (string) The hash of the previous block
|
|
||||||
nextblockhash: string; // (string) The hash of the next block
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Transaction {
|
|
||||||
in_active_chain: boolean; // (boolean) Whether specified block is in the active chain or not
|
|
||||||
hex: string; // (string) The serialized, hex-encoded data for 'txid'
|
|
||||||
txid: string; // (string) The transaction id (same as provided)
|
|
||||||
hash: string; // (string) The transaction hash (differs from txid for witness transactions)
|
|
||||||
size: number; // (numeric) The serialized transaction size
|
|
||||||
vsize: number; // (numeric) The virtual transaction size (differs from size for witness transactions)
|
|
||||||
weight: number; // (numeric) The transaction's weight (between vsize*4-3 and vsize*4)
|
|
||||||
version: number; // (numeric) The version
|
|
||||||
locktime: number; // (numeric) The lock time
|
|
||||||
vin: Vin[];
|
|
||||||
vout: Vout[];
|
|
||||||
blockhash: string; // (string) the block hash
|
|
||||||
confirmations: number; // (numeric) The confirmations
|
|
||||||
blocktime: number; // (numeric) The block time expressed in UNIX epoch time
|
|
||||||
time: number; // (numeric) Same as blocktime
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VerboseBlock extends Block {
|
|
||||||
tx: VerboseTransaction[]; // The transactions in the format of the getrawtransaction RPC. Different from verbosity = 1 "tx" result
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VerboseTransaction extends Transaction {
|
|
||||||
fee?: number; // (numeric) The transaction fee in BTC, omitted if block undo data is not available
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Vin {
|
|
||||||
txid?: string; // (string) The transaction id
|
|
||||||
vout?: number; // (string)
|
|
||||||
scriptSig?: { // (json object) The script
|
|
||||||
asm: string; // (string) asm
|
|
||||||
hex: string; // (string) hex
|
|
||||||
};
|
|
||||||
sequence: number; // (numeric) The script sequence number
|
|
||||||
txinwitness?: string[]; // (string) hex-encoded witness data
|
|
||||||
coinbase?: string;
|
|
||||||
is_pegin?: boolean; // (boolean) Elements peg-in
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Vout {
|
|
||||||
value: number; // (numeric) The value in BTC
|
|
||||||
n: number; // (numeric) index
|
|
||||||
asset?: string; // (string) Elements asset id
|
|
||||||
scriptPubKey: { // (json object)
|
|
||||||
asm: string; // (string) the asm
|
|
||||||
hex: string; // (string) the hex
|
|
||||||
reqSigs?: number; // (numeric) The required sigs
|
|
||||||
type: string; // (string) The type, eg 'pubkeyhash'
|
|
||||||
address?: string; // (string) bitcoin address
|
|
||||||
addresses?: string[]; // (string) bitcoin addresses
|
|
||||||
pegout_chain?: string; // (string) Elements peg-out chain
|
|
||||||
pegout_addresses?: string[]; // (string) Elements peg-out addresses
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AddressInformation {
|
|
||||||
isvalid: boolean; // (boolean) If the address is valid or not. If not, this is the only property returned.
|
|
||||||
isvalid_parent?: boolean; // (boolean) Elements only
|
|
||||||
address: string; // (string) The bitcoin address validated
|
|
||||||
scriptPubKey: string; // (string) The hex-encoded scriptPubKey generated by the address
|
|
||||||
isscript: boolean; // (boolean) If the key is a script
|
|
||||||
iswitness: boolean; // (boolean) If the address is a witness
|
|
||||||
witness_version?: number; // (numeric, optional) The version number of the witness program
|
|
||||||
witness_program: string; // (string, optional) The hex value of the witness program
|
|
||||||
confidential_key?: string; // (string) Elements only
|
|
||||||
unconfidential?: string; // (string) Elements only
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChainTips {
|
|
||||||
height: number; // (numeric) height of the chain tip
|
|
||||||
hash: string; // (string) block hash of the tip
|
|
||||||
branchlen: number; // (numeric) zero for main chain, otherwise length of branch connecting the tip to the main chain
|
|
||||||
status: 'invalid' | 'headers-only' | 'valid-headers' | 'valid-fork' | 'active';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlockchainInfo {
|
|
||||||
chain: number; // (string) current network name as defined in BIP70 (main, test, regtest)
|
|
||||||
blocks: number; // (numeric) the current number of blocks processed in the server
|
|
||||||
headers: number; // (numeric) the current number of headers we have validated
|
|
||||||
bestblockhash: string, // (string) the hash of the currently best block
|
|
||||||
difficulty: number; // (numeric) the current difficulty
|
|
||||||
mediantime: number; // (numeric) median time for the current best block
|
|
||||||
verificationprogress: number; // (numeric) estimate of verification progress [0..1]
|
|
||||||
initialblockdownload: boolean; // (bool) (debug information) estimate of whether this node is in Initial Block Download mode.
|
|
||||||
chainwork: string // (string) total amount of work in active chain, in hexadecimal
|
|
||||||
size_on_disk: number; // (numeric) the estimated size of the block and undo files on disk
|
|
||||||
pruned: number; // (boolean) if the blocks are subject to pruning
|
|
||||||
pruneheight: number; // (numeric) lowest-height complete block stored (only present if pruning is enabled)
|
|
||||||
automatic_pruning: number; // (boolean) whether automatic pruning is enabled (only present if pruning is enabled)
|
|
||||||
prune_target_size: number; // (numeric) the target size used by pruning (only present if automatic pruning is enabled)
|
|
||||||
softforks: SoftFork[]; // (array) status of softforks in progress
|
|
||||||
bip9_softforks: { [name: string]: Bip9SoftForks[] } // (object) status of BIP9 softforks in progress
|
|
||||||
warnings: string; // (string) any network and blockchain warnings.
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SoftFork {
|
|
||||||
id: string; // (string) name of softfork
|
|
||||||
version: number; // (numeric) block version
|
|
||||||
reject: { // (object) progress toward rejecting pre-softfork blocks
|
|
||||||
status: boolean; // (boolean) true if threshold reached
|
|
||||||
},
|
|
||||||
}
|
|
||||||
interface Bip9SoftForks {
|
|
||||||
status: number; // (string) one of defined, started, locked_in, active, failed
|
|
||||||
bit: number; // (numeric) the bit (0-28) in the block version field used to signal this softfork (only for started status)
|
|
||||||
startTime: number; // (numeric) the minimum median time past of a block at which the bit gains its meaning
|
|
||||||
timeout: number; // (numeric) the median time past of a block at which the deployment is considered failed if not yet locked in
|
|
||||||
since: number; // (numeric) height of the first block to which the status applies
|
|
||||||
statistics: { // (object) numeric statistics about BIP9 signalling for a softfork (only for started status)
|
|
||||||
period: number; // (numeric) the length in blocks of the BIP9 signalling period
|
|
||||||
threshold: number; // (numeric) the number of blocks with the version bit set required to activate the feature
|
|
||||||
elapsed: number; // (numeric) the number of blocks elapsed since the beginning of the current period
|
|
||||||
count: number; // (numeric) the number of blocks with the version bit set in the current period
|
|
||||||
possible: boolean; // (boolean) returns false if there are not enough blocks left in this period to pass activation threshold
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,313 +0,0 @@
|
|||||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
|
||||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
|
||||||
import { IBitcoinApi } from './bitcoin-api.interface';
|
|
||||||
import { IEsploraApi } from './esplora-api.interface';
|
|
||||||
|
|
||||||
class BitcoinApi implements AbstractBitcoinApi {
|
|
||||||
protected bitcoindClient: any;
|
|
||||||
|
|
||||||
constructor(bitcoinClient: any) {
|
|
||||||
this.bitcoindClient = bitcoinClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
$getAddressPrefix(prefix: string): string[] {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$getBlock(hash: string): Promise<IEsploraApi.Block> {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
|
|
||||||
return this.bitcoindClient.getRawTransaction(txId, true)
|
|
||||||
.then((transaction: IBitcoinApi.Transaction) => {
|
|
||||||
if (skipConversion) {
|
|
||||||
transaction.vout.forEach((vout) => {
|
|
||||||
vout.value = Math.round(vout.value * 100000000);
|
|
||||||
});
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
return this.$convertTransaction(transaction, addPrevout, lazyPrevouts);
|
|
||||||
})
|
|
||||||
.catch((e: Error) => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$getBlockHeightTip(): Promise<number> {
|
|
||||||
return this.bitcoindClient.getChainTips()
|
|
||||||
.then((result: IBitcoinApi.ChainTips[]) => {
|
|
||||||
return result.find(tip => tip.status === 'active')!.height;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
|
||||||
return this.bitcoindClient.getBlock(hash, 1)
|
|
||||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getRawBlock(hash: string): Promise<string> {
|
|
||||||
return this.bitcoindClient.getBlock(hash, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getBlockHash(height: number): Promise<string> {
|
|
||||||
return this.bitcoindClient.getBlockHash(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getBlockHeader(hash: string): Promise<string> {
|
|
||||||
return this.bitcoindClient.getBlockHeader(hash, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
|
||||||
throw new Error('Method getAddress not supported by the Bitcoin RPC API.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
|
|
||||||
throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
|
||||||
return this.bitcoindClient.getRawMemPool();
|
|
||||||
}
|
|
||||||
|
|
||||||
$sendRawTransaction(rawTransaction: string): Promise<string> {
|
|
||||||
return this.bitcoindClient.sendRawTransaction(rawTransaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
|
||||||
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
|
|
||||||
return {
|
|
||||||
spent: txOut === null,
|
|
||||||
status: {
|
|
||||||
confirmed: true,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
|
||||||
const outSpends: IEsploraApi.Outspend[] = [];
|
|
||||||
const tx = await this.$getRawTransaction(txId, true, false);
|
|
||||||
for (let i = 0; i < tx.vout.length; i++) {
|
|
||||||
if (tx.status && tx.status.block_height === 0) {
|
|
||||||
outSpends.push({
|
|
||||||
spent: false
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const txOut = await this.bitcoindClient.getTxOut(txId, i);
|
|
||||||
outSpends.push({
|
|
||||||
spent: txOut === null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return outSpends;
|
|
||||||
}
|
|
||||||
|
|
||||||
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
|
|
||||||
const outspends: IEsploraApi.Outspend[][] = [];
|
|
||||||
for (const tx of txId) {
|
|
||||||
const outspend = await this.$getOutspends(tx);
|
|
||||||
outspends.push(outspend);
|
|
||||||
}
|
|
||||||
return outspends;
|
|
||||||
}
|
|
||||||
|
|
||||||
$getEstimatedHashrate(blockHeight: number): Promise<number> {
|
|
||||||
// 120 is the default block span in Core
|
|
||||||
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
|
|
||||||
let esploraTransaction: IEsploraApi.Transaction = {
|
|
||||||
txid: transaction.txid,
|
|
||||||
version: transaction.version,
|
|
||||||
locktime: transaction.locktime,
|
|
||||||
size: transaction.size,
|
|
||||||
weight: transaction.weight,
|
|
||||||
fee: 0,
|
|
||||||
vin: [],
|
|
||||||
vout: [],
|
|
||||||
status: { confirmed: false },
|
|
||||||
};
|
|
||||||
|
|
||||||
esploraTransaction.vout = transaction.vout.map((vout) => {
|
|
||||||
return {
|
|
||||||
value: Math.round(vout.value * 100000000),
|
|
||||||
scriptpubkey: vout.scriptPubKey.hex,
|
|
||||||
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
|
|
||||||
: vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
|
|
||||||
scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
|
|
||||||
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
esploraTransaction.vin = transaction.vin.map((vin) => {
|
|
||||||
return {
|
|
||||||
is_coinbase: !!vin.coinbase,
|
|
||||||
prevout: null,
|
|
||||||
scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
|
|
||||||
scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.hex) || '',
|
|
||||||
sequence: vin.sequence,
|
|
||||||
txid: vin.txid || '',
|
|
||||||
vout: vin.vout || 0,
|
|
||||||
witness: vin.txinwitness,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (transaction.confirmations) {
|
|
||||||
esploraTransaction.status = {
|
|
||||||
confirmed: true,
|
|
||||||
block_height: -1,
|
|
||||||
block_hash: transaction.blockhash,
|
|
||||||
block_time: transaction.blocktime,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addPrevout) {
|
|
||||||
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, false, lazyPrevouts);
|
|
||||||
} else if (!transaction.confirmations) {
|
|
||||||
// esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
return esploraTransaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
private translateScriptPubKeyType(outputType: string): string {
|
|
||||||
const map = {
|
|
||||||
'pubkey': 'p2pk',
|
|
||||||
'pubkeyhash': 'p2pkh',
|
|
||||||
'scripthash': 'p2sh',
|
|
||||||
'witness_v0_keyhash': 'v0_p2wpkh',
|
|
||||||
'witness_v0_scripthash': 'v0_p2wsh',
|
|
||||||
'witness_v1_taproot': 'v1_p2tr',
|
|
||||||
'nonstandard': 'nonstandard',
|
|
||||||
'multisig': 'multisig',
|
|
||||||
'nulldata': 'op_return'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (map[outputType]) {
|
|
||||||
return map[outputType];
|
|
||||||
} else {
|
|
||||||
return 'unknown';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean, lazyPrevouts: boolean): Promise<IEsploraApi.Transaction> {
|
|
||||||
if (transaction.vin[0].is_coinbase) {
|
|
||||||
transaction.fee = 0;
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
let totalIn = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < transaction.vin.length; i++) {
|
|
||||||
if (lazyPrevouts && i > 12) {
|
|
||||||
transaction.vin[i].lazy = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
|
|
||||||
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
|
|
||||||
this.addInnerScriptsToVin(transaction.vin[i]);
|
|
||||||
totalIn += innerTx.vout[transaction.vin[i].vout].value;
|
|
||||||
}
|
|
||||||
if (lazyPrevouts && transaction.vin.length > 12) {
|
|
||||||
transaction.fee = -1;
|
|
||||||
} else {
|
|
||||||
const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);
|
|
||||||
transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
|
|
||||||
}
|
|
||||||
return transaction;
|
|
||||||
}
|
|
||||||
|
|
||||||
private convertScriptSigAsm(hex: string): string {
|
|
||||||
const buf = Buffer.from(hex, 'hex');
|
|
||||||
|
|
||||||
const b: string[] = [];
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
while (i < buf.length) {
|
|
||||||
const op = buf[i];
|
|
||||||
if (op >= 0x01 && op <= 0x4e) {
|
|
||||||
i++;
|
|
||||||
let push: number;
|
|
||||||
if (op === 0x4c) {
|
|
||||||
push = buf.readUInt8(i);
|
|
||||||
b.push('OP_PUSHDATA1');
|
|
||||||
i += 1;
|
|
||||||
} else if (op === 0x4d) {
|
|
||||||
push = buf.readUInt16LE(i);
|
|
||||||
b.push('OP_PUSHDATA2');
|
|
||||||
i += 2;
|
|
||||||
} else if (op === 0x4e) {
|
|
||||||
push = buf.readUInt32LE(i);
|
|
||||||
b.push('OP_PUSHDATA4');
|
|
||||||
i += 4;
|
|
||||||
} else {
|
|
||||||
push = op;
|
|
||||||
b.push('OP_PUSHBYTES_' + push);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = buf.slice(i, i + push);
|
|
||||||
if (data.length !== push) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
b.push(data.toString('hex'));
|
|
||||||
i += data.length;
|
|
||||||
} else {
|
|
||||||
if (op === 0x00) {
|
|
||||||
b.push('OP_0');
|
|
||||||
} else if (op === 0x4f) {
|
|
||||||
b.push('OP_PUSHNUM_NEG1');
|
|
||||||
} else if (op === 0xb1) {
|
|
||||||
b.push('OP_CLTV');
|
|
||||||
} else if (op === 0xb2) {
|
|
||||||
b.push('OP_CSV');
|
|
||||||
} else if (op === 0xba) {
|
|
||||||
b.push('OP_CHECKSIGADD');
|
|
||||||
} else {
|
|
||||||
const opcode = bitcoinjs.script.toASM([ op ]);
|
|
||||||
if (opcode && op < 0xfd) {
|
|
||||||
if (/^OP_(\d+)$/.test(opcode)) {
|
|
||||||
b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
|
|
||||||
} else {
|
|
||||||
b.push(opcode);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
b.push('OP_RETURN_' + op);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
private addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
|
|
||||||
if (!vin.prevout) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vin.prevout.scriptpubkey_type === 'p2sh') {
|
|
||||||
const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
|
|
||||||
vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
|
|
||||||
if (vin.witness && vin.witness.length > 2) {
|
|
||||||
const witnessScript = vin.witness[vin.witness.length - 1];
|
|
||||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
|
|
||||||
const witnessScript = vin.witness[vin.witness.length - 1];
|
|
||||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness && vin.witness.length > 1) {
|
|
||||||
const witnessScript = vin.witness[vin.witness.length - 2];
|
|
||||||
vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BitcoinApi;
|
|
@ -1,12 +0,0 @@
|
|||||||
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);
|
|
@ -1,172 +0,0 @@
|
|||||||
export namespace IEsploraApi {
|
|
||||||
export interface Transaction {
|
|
||||||
txid: string;
|
|
||||||
version: number;
|
|
||||||
locktime: number;
|
|
||||||
size: number;
|
|
||||||
weight: number;
|
|
||||||
fee: number;
|
|
||||||
vin: Vin[];
|
|
||||||
vout: Vout[];
|
|
||||||
status: Status;
|
|
||||||
hex?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Recent {
|
|
||||||
txid: string;
|
|
||||||
fee: number;
|
|
||||||
vsize: number;
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Vin {
|
|
||||||
txid: string;
|
|
||||||
vout: number;
|
|
||||||
is_coinbase: boolean;
|
|
||||||
scriptsig: string;
|
|
||||||
scriptsig_asm: string;
|
|
||||||
inner_redeemscript_asm: string;
|
|
||||||
inner_witnessscript_asm: string;
|
|
||||||
sequence: any;
|
|
||||||
witness: string[];
|
|
||||||
prevout: Vout | null;
|
|
||||||
// Elements
|
|
||||||
is_pegin?: boolean;
|
|
||||||
issuance?: Issuance;
|
|
||||||
// Custom
|
|
||||||
lazy?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Issuance {
|
|
||||||
asset_id: string;
|
|
||||||
is_reissuance: string;
|
|
||||||
asset_blinding_nonce: string;
|
|
||||||
asset_entropy: string;
|
|
||||||
contract_hash: string;
|
|
||||||
assetamount?: number;
|
|
||||||
assetamountcommitment?: string;
|
|
||||||
tokenamount?: number;
|
|
||||||
tokenamountcommitment?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Vout {
|
|
||||||
scriptpubkey: string;
|
|
||||||
scriptpubkey_asm: string;
|
|
||||||
scriptpubkey_type: string;
|
|
||||||
scriptpubkey_address: string;
|
|
||||||
value: number;
|
|
||||||
// Elements
|
|
||||||
valuecommitment?: number;
|
|
||||||
asset?: string;
|
|
||||||
pegout?: Pegout;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Pegout {
|
|
||||||
genesis_hash: string;
|
|
||||||
scriptpubkey: string;
|
|
||||||
scriptpubkey_asm: string;
|
|
||||||
scriptpubkey_address: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Status {
|
|
||||||
confirmed: boolean;
|
|
||||||
block_height?: number;
|
|
||||||
block_hash?: string;
|
|
||||||
block_time?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Block {
|
|
||||||
id: string;
|
|
||||||
height: number;
|
|
||||||
version: number;
|
|
||||||
timestamp: number;
|
|
||||||
bits: number;
|
|
||||||
nonce: number;
|
|
||||||
difficulty: number;
|
|
||||||
merkle_root: string;
|
|
||||||
tx_count: number;
|
|
||||||
size: number;
|
|
||||||
weight: number;
|
|
||||||
previousblockhash: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Address {
|
|
||||||
address: string;
|
|
||||||
chain_stats: ChainStats;
|
|
||||||
mempool_stats: MempoolStats;
|
|
||||||
electrum?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChainStats {
|
|
||||||
funded_txo_count: number;
|
|
||||||
funded_txo_sum: number;
|
|
||||||
spent_txo_count: number;
|
|
||||||
spent_txo_sum: number;
|
|
||||||
tx_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MempoolStats {
|
|
||||||
funded_txo_count: number;
|
|
||||||
funded_txo_sum: number;
|
|
||||||
spent_txo_count: number;
|
|
||||||
spent_txo_sum: number;
|
|
||||||
tx_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Outspend {
|
|
||||||
spent: boolean;
|
|
||||||
txid?: string;
|
|
||||||
vin?: number;
|
|
||||||
status?: Status;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Asset {
|
|
||||||
asset_id: string;
|
|
||||||
issuance_txin: IssuanceTxin;
|
|
||||||
issuance_prevout: IssuancePrevout;
|
|
||||||
reissuance_token: string;
|
|
||||||
contract_hash: string;
|
|
||||||
status: Status;
|
|
||||||
chain_stats: AssetStats;
|
|
||||||
mempool_stats: AssetStats;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssetExtended extends Asset {
|
|
||||||
name: string;
|
|
||||||
ticker: string;
|
|
||||||
precision: number;
|
|
||||||
entity: Entity;
|
|
||||||
version: number;
|
|
||||||
issuer_pubkey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Entity {
|
|
||||||
domain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IssuanceTxin {
|
|
||||||
txid: string;
|
|
||||||
vin: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IssuancePrevout {
|
|
||||||
txid: string;
|
|
||||||
vout: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AssetStats {
|
|
||||||
tx_count: number;
|
|
||||||
issuance_count: number;
|
|
||||||
issued_amount: number;
|
|
||||||
burned_amount: number;
|
|
||||||
has_blinded_issuances: boolean;
|
|
||||||
reissuance_tokens: number;
|
|
||||||
burned_reissuance_tokens: number;
|
|
||||||
peg_in_count: number;
|
|
||||||
peg_in_amount: number;
|
|
||||||
peg_out_count: number;
|
|
||||||
peg_out_amount: number;
|
|
||||||
burn_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
import config from '../../config';
|
|
||||||
import axios, { AxiosRequestConfig } from 'axios';
|
|
||||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
|
||||||
import { IEsploraApi } from './esplora-api.interface';
|
|
||||||
|
|
||||||
class ElectrsApi implements AbstractBitcoinApi {
|
|
||||||
axiosConfig: AxiosRequestConfig = {
|
|
||||||
timeout: 10000,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() { }
|
|
||||||
|
|
||||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
|
||||||
return axios.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig)
|
|
||||||
.then((response) => response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
|
|
||||||
return axios.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
|
|
||||||
.then((response) => response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getBlockHeightTip(): Promise<number> {
|
|
||||||
return axios.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
|
|
||||||
.then((response) => response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
|
||||||
return axios.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
|
|
||||||
.then((response) => response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getBlockHash(height: number): Promise<string> {
|
|
||||||
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig)
|
|
||||||
.then((response) => response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getBlockHeader(hash: string): Promise<string> {
|
|
||||||
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig)
|
|
||||||
.then((response) => response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getBlock(hash: string): Promise<IEsploraApi.Block> {
|
|
||||||
return axios.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
|
|
||||||
.then((response) => response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getAddress(address: string): Promise<IEsploraApi.Address> {
|
|
||||||
throw new Error('Method getAddress not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
|
|
||||||
throw new Error('Method getAddressTransactions not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$getAddressPrefix(prefix: string): string[] {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$sendRawTransaction(rawTransaction: string): Promise<string> {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
|
||||||
return axios.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig)
|
|
||||||
.then((response) => response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
|
||||||
return axios.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
|
|
||||||
.then((response) => response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
|
|
||||||
const outspends: IEsploraApi.Outspend[][] = [];
|
|
||||||
for (const tx of txId) {
|
|
||||||
const outspend = await this.$getOutspends(tx);
|
|
||||||
outspends.push(outspend);
|
|
||||||
}
|
|
||||||
return outspends;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ElectrsApi;
|
|
@ -1,92 +0,0 @@
|
|||||||
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'
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
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;
|
|
@ -1,162 +0,0 @@
|
|||||||
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
|
|
@ -1,17 +0,0 @@
|
|||||||
import logger from '../../logger';
|
|
||||||
import DB from '../../database';
|
|
||||||
|
|
||||||
class StatisticsApi {
|
|
||||||
public async $getStatistics(): Promise<any> {
|
|
||||||
try {
|
|
||||||
const query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, node_count, total_capacity FROM statistics ORDER BY id DESC`;
|
|
||||||
const [rows]: any = await DB.query(query);
|
|
||||||
return rows;
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$getStatistics error: ' + (e instanceof Error ? e.message : e));
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new StatisticsApi();
|
|
@ -1,110 +0,0 @@
|
|||||||
const configFile = require('../mempool-config.json');
|
|
||||||
|
|
||||||
interface IConfig {
|
|
||||||
MEMPOOL: {
|
|
||||||
NETWORK: 'mainnet' | 'testnet' | 'signet';
|
|
||||||
BACKEND: 'lnd' | 'cln' | 'ldk';
|
|
||||||
HTTP_PORT: number;
|
|
||||||
API_URL_PREFIX: string;
|
|
||||||
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
|
|
||||||
};
|
|
||||||
ESPLORA: {
|
|
||||||
REST_API_URL: string;
|
|
||||||
};
|
|
||||||
SYSLOG: {
|
|
||||||
ENABLED: boolean;
|
|
||||||
HOST: string;
|
|
||||||
PORT: number;
|
|
||||||
MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
|
|
||||||
FACILITY: string;
|
|
||||||
};
|
|
||||||
LN_NODE_AUTH: {
|
|
||||||
TLS_CERT_PATH: string;
|
|
||||||
MACAROON_PATH: string;
|
|
||||||
SOCKET: string;
|
|
||||||
};
|
|
||||||
CORE_RPC: {
|
|
||||||
HOST: string;
|
|
||||||
PORT: number;
|
|
||||||
USERNAME: string;
|
|
||||||
PASSWORD: string;
|
|
||||||
};
|
|
||||||
DATABASE: {
|
|
||||||
HOST: string,
|
|
||||||
SOCKET: string,
|
|
||||||
PORT: number;
|
|
||||||
DATABASE: string;
|
|
||||||
USERNAME: string;
|
|
||||||
PASSWORD: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaults: IConfig = {
|
|
||||||
'MEMPOOL': {
|
|
||||||
'NETWORK': 'mainnet',
|
|
||||||
'BACKEND': 'lnd',
|
|
||||||
'HTTP_PORT': 8999,
|
|
||||||
'API_URL_PREFIX': '/api/v1/',
|
|
||||||
'STDOUT_LOG_MIN_PRIORITY': 'debug',
|
|
||||||
},
|
|
||||||
'ESPLORA': {
|
|
||||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
|
||||||
},
|
|
||||||
'SYSLOG': {
|
|
||||||
'ENABLED': true,
|
|
||||||
'HOST': '127.0.0.1',
|
|
||||||
'PORT': 514,
|
|
||||||
'MIN_PRIORITY': 'info',
|
|
||||||
'FACILITY': 'local7'
|
|
||||||
},
|
|
||||||
'LN_NODE_AUTH': {
|
|
||||||
'TLS_CERT_PATH': '',
|
|
||||||
'MACAROON_PATH': '',
|
|
||||||
'SOCKET': 'localhost:10009',
|
|
||||||
},
|
|
||||||
'CORE_RPC': {
|
|
||||||
'HOST': '127.0.0.1',
|
|
||||||
'PORT': 8332,
|
|
||||||
'USERNAME': 'mempool',
|
|
||||||
'PASSWORD': 'mempool'
|
|
||||||
},
|
|
||||||
'DATABASE': {
|
|
||||||
'HOST': '127.0.0.1',
|
|
||||||
'SOCKET': '',
|
|
||||||
'PORT': 3306,
|
|
||||||
'DATABASE': 'mempool',
|
|
||||||
'USERNAME': 'mempool',
|
|
||||||
'PASSWORD': 'mempool'
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
class Config implements IConfig {
|
|
||||||
MEMPOOL: IConfig['MEMPOOL'];
|
|
||||||
ESPLORA: IConfig['ESPLORA'];
|
|
||||||
SYSLOG: IConfig['SYSLOG'];
|
|
||||||
LN_NODE_AUTH: IConfig['LN_NODE_AUTH'];
|
|
||||||
CORE_RPC: IConfig['CORE_RPC'];
|
|
||||||
DATABASE: IConfig['DATABASE'];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
const configs = this.merge(configFile, defaults);
|
|
||||||
this.MEMPOOL = configs.MEMPOOL;
|
|
||||||
this.ESPLORA = configs.ESPLORA;
|
|
||||||
this.SYSLOG = configs.SYSLOG;
|
|
||||||
this.LN_NODE_AUTH = configs.LN_NODE_AUTH;
|
|
||||||
this.CORE_RPC = configs.CORE_RPC;
|
|
||||||
this.DATABASE = configs.DATABASE;
|
|
||||||
}
|
|
||||||
|
|
||||||
merge = (...objects: object[]): IConfig => {
|
|
||||||
// @ts-ignore
|
|
||||||
return objects.reduce((prev, next) => {
|
|
||||||
Object.keys(prev).forEach(key => {
|
|
||||||
next[key] = { ...next[key], ...prev[key] };
|
|
||||||
});
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new Config();
|
|
@ -1,260 +0,0 @@
|
|||||||
import config from './config';
|
|
||||||
import DB from './database';
|
|
||||||
import logger from './logger';
|
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
|
|
||||||
|
|
||||||
class DatabaseMigration {
|
|
||||||
private static currentVersion = 1;
|
|
||||||
private queryTimeout = 120000;
|
|
||||||
|
|
||||||
constructor() { }
|
|
||||||
/**
|
|
||||||
* Entry point
|
|
||||||
*/
|
|
||||||
public async $initializeOrMigrateDatabase(): Promise<void> {
|
|
||||||
logger.debug('MIGRATIONS: Running migrations');
|
|
||||||
|
|
||||||
await this.$printDatabaseVersion();
|
|
||||||
|
|
||||||
// First of all, if the `state` database does not exist, create it so we can track migration version
|
|
||||||
if (!await this.$checkIfTableExists('state')) {
|
|
||||||
logger.debug('MIGRATIONS: `state` table does not exist. Creating it.');
|
|
||||||
try {
|
|
||||||
await this.$createMigrationStateTable();
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('MIGRATIONS: Unable to create `state` table, aborting in 10 seconds. ' + e);
|
|
||||||
await sleep(10000);
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
logger.debug('MIGRATIONS: `state` table initialized.');
|
|
||||||
}
|
|
||||||
|
|
||||||
let databaseSchemaVersion = 0;
|
|
||||||
try {
|
|
||||||
databaseSchemaVersion = await this.$getSchemaVersionFromDatabase();
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('MIGRATIONS: Unable to get current database migration version, aborting in 10 seconds. ' + e);
|
|
||||||
await sleep(10000);
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
|
|
||||||
logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
|
|
||||||
if (databaseSchemaVersion >= DatabaseMigration.currentVersion) {
|
|
||||||
logger.debug('MIGRATIONS: Nothing to do.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, create missing tables. Those queries cannot be wrapped into a transaction unfortunately
|
|
||||||
try {
|
|
||||||
await this.$createMissingTablesAndIndexes(databaseSchemaVersion);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('MIGRATIONS: Unable to create required tables, aborting in 10 seconds. ' + e);
|
|
||||||
await sleep(10000);
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
|
|
||||||
logger.notice('MIGRATIONS: Upgrading datababse schema');
|
|
||||||
try {
|
|
||||||
await this.$migrateTableSchemaFromVersion(databaseSchemaVersion);
|
|
||||||
logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create all missing tables
|
|
||||||
*/
|
|
||||||
private async $createMissingTablesAndIndexes(databaseSchemaVersion: number) {
|
|
||||||
try {
|
|
||||||
await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
|
|
||||||
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
|
|
||||||
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
|
|
||||||
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Small query execution wrapper to log all executed queries
|
|
||||||
*/
|
|
||||||
private async $executeQuery(query: string, silent: boolean = false): Promise<any> {
|
|
||||||
if (!silent) {
|
|
||||||
logger.debug('MIGRATIONS: Execute query:\n' + query);
|
|
||||||
}
|
|
||||||
return DB.query({ sql: query, timeout: this.queryTimeout });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if 'table' exists in the database
|
|
||||||
*/
|
|
||||||
private async $checkIfTableExists(table: string): Promise<boolean> {
|
|
||||||
const query = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${config.DATABASE.DATABASE}' AND TABLE_NAME = '${table}'`;
|
|
||||||
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
|
|
||||||
return rows[0]['COUNT(*)'] === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current database version
|
|
||||||
*/
|
|
||||||
private async $getSchemaVersionFromDatabase(): Promise<number> {
|
|
||||||
const query = `SELECT number FROM state WHERE name = 'schema_version';`;
|
|
||||||
const [rows] = await this.$executeQuery(query, true);
|
|
||||||
return rows[0]['number'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the `state` table
|
|
||||||
*/
|
|
||||||
private async $createMigrationStateTable(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const query = `CREATE TABLE IF NOT EXISTS state (
|
|
||||||
name varchar(25) NOT NULL,
|
|
||||||
number int(11) NULL,
|
|
||||||
string varchar(100) NULL,
|
|
||||||
CONSTRAINT name_unique UNIQUE (name)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
|
||||||
await this.$executeQuery(query);
|
|
||||||
|
|
||||||
// Set initial values
|
|
||||||
await this.$executeQuery(`INSERT INTO state VALUES('schema_version', 0, NULL);`);
|
|
||||||
await this.$executeQuery(`INSERT INTO state VALUES('last_node_stats', 0, '1970-01-01');`);
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We actually execute the migrations queries here
|
|
||||||
*/
|
|
||||||
private async $migrateTableSchemaFromVersion(version: number): Promise<void> {
|
|
||||||
const transactionQueries: string[] = [];
|
|
||||||
for (const query of this.getMigrationQueriesFromVersion(version)) {
|
|
||||||
transactionQueries.push(query);
|
|
||||||
}
|
|
||||||
transactionQueries.push(this.getUpdateToLatestSchemaVersionQuery());
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.$executeQuery('START TRANSACTION;');
|
|
||||||
for (const query of transactionQueries) {
|
|
||||||
await this.$executeQuery(query);
|
|
||||||
}
|
|
||||||
await this.$executeQuery('COMMIT;');
|
|
||||||
} catch (e) {
|
|
||||||
await this.$executeQuery('ROLLBACK;');
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate migration queries based on schema version
|
|
||||||
*/
|
|
||||||
private getMigrationQueriesFromVersion(version: number): string[] {
|
|
||||||
const queries: string[] = [];
|
|
||||||
return queries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the schema version in the database
|
|
||||||
*/
|
|
||||||
private getUpdateToLatestSchemaVersionQuery(): string {
|
|
||||||
return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Print current database version
|
|
||||||
*/
|
|
||||||
private async $printDatabaseVersion() {
|
|
||||||
try {
|
|
||||||
const [rows] = await this.$executeQuery('SELECT VERSION() as version;', true);
|
|
||||||
logger.debug(`MIGRATIONS: Database engine version '${rows[0].version}'`);
|
|
||||||
} catch (e) {
|
|
||||||
logger.debug(`MIGRATIONS: Could not fetch database engine version. ` + e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCreateStatisticsQuery(): string {
|
|
||||||
return `CREATE TABLE IF NOT EXISTS statistics (
|
|
||||||
id int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
added datetime NOT NULL,
|
|
||||||
channel_count int(11) NOT NULL,
|
|
||||||
node_count int(11) NOT NULL,
|
|
||||||
total_capacity double unsigned NOT NULL,
|
|
||||||
PRIMARY KEY (id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCreateNodesQuery(): string {
|
|
||||||
return `CREATE TABLE IF NOT EXISTS nodes (
|
|
||||||
public_key varchar(66) NOT NULL,
|
|
||||||
first_seen datetime NOT NULL,
|
|
||||||
updated_at datetime NOT NULL,
|
|
||||||
alias varchar(200) CHARACTER SET utf8mb4 NOT NULL,
|
|
||||||
color varchar(200) NOT NULL,
|
|
||||||
sockets text DEFAULT NULL,
|
|
||||||
PRIMARY KEY (public_key),
|
|
||||||
KEY alias (alias(10))
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCreateChannelsQuery(): string {
|
|
||||||
return `CREATE TABLE IF NOT EXISTS channels (
|
|
||||||
id bigint(11) unsigned NOT NULL,
|
|
||||||
short_id varchar(15) NOT NULL DEFAULT '',
|
|
||||||
capacity bigint(20) unsigned NOT NULL,
|
|
||||||
transaction_id varchar(64) NOT NULL,
|
|
||||||
transaction_vout int(11) NOT NULL,
|
|
||||||
updated_at datetime DEFAULT NULL,
|
|
||||||
created datetime DEFAULT NULL,
|
|
||||||
status int(11) NOT NULL DEFAULT 0,
|
|
||||||
closing_transaction_id varchar(64) DEFAULT NULL,
|
|
||||||
closing_date datetime DEFAULT NULL,
|
|
||||||
closing_reason int(11) DEFAULT NULL,
|
|
||||||
node1_public_key varchar(66) NOT NULL,
|
|
||||||
node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
|
|
||||||
node1_cltv_delta int(11) DEFAULT NULL,
|
|
||||||
node1_fee_rate bigint(11) DEFAULT NULL,
|
|
||||||
node1_is_disabled tinyint(1) DEFAULT NULL,
|
|
||||||
node1_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
|
|
||||||
node1_min_htlc_mtokens bigint(20) DEFAULT NULL,
|
|
||||||
node1_updated_at datetime DEFAULT NULL,
|
|
||||||
node2_public_key varchar(66) NOT NULL,
|
|
||||||
node2_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
|
|
||||||
node2_cltv_delta int(11) DEFAULT NULL,
|
|
||||||
node2_fee_rate bigint(11) DEFAULT NULL,
|
|
||||||
node2_is_disabled tinyint(1) DEFAULT NULL,
|
|
||||||
node2_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
|
|
||||||
node2_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
|
|
||||||
node2_updated_at datetime DEFAULT NULL,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
KEY node1_public_key (node1_public_key),
|
|
||||||
KEY node2_public_key (node2_public_key),
|
|
||||||
KEY status (status),
|
|
||||||
KEY short_id (short_id),
|
|
||||||
KEY transaction_id (transaction_id),
|
|
||||||
KEY closing_transaction_id (closing_transaction_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCreateNodesStatsQuery(): string {
|
|
||||||
return `CREATE TABLE IF NOT EXISTS node_stats (
|
|
||||||
id int(11) unsigned NOT NULL AUTO_INCREMENT,
|
|
||||||
public_key varchar(66) NOT NULL DEFAULT '',
|
|
||||||
added date NOT NULL,
|
|
||||||
capacity bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
||||||
channels int(11) unsigned NOT NULL DEFAULT 0,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY added (added,public_key),
|
|
||||||
KEY public_key (public_key)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new DatabaseMigration();
|
|
@ -1,51 +0,0 @@
|
|||||||
import config from './config';
|
|
||||||
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
|
|
||||||
import logger from './logger';
|
|
||||||
import { PoolOptions } from 'mysql2/typings/mysql';
|
|
||||||
|
|
||||||
class DB {
|
|
||||||
constructor() {
|
|
||||||
if (config.DATABASE.SOCKET !== '') {
|
|
||||||
this.poolConfig.socketPath = config.DATABASE.SOCKET;
|
|
||||||
} else {
|
|
||||||
this.poolConfig.host = config.DATABASE.HOST;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private pool: Pool | null = null;
|
|
||||||
private poolConfig: PoolOptions = {
|
|
||||||
port: config.DATABASE.PORT,
|
|
||||||
database: config.DATABASE.DATABASE,
|
|
||||||
user: config.DATABASE.USERNAME,
|
|
||||||
password: config.DATABASE.PASSWORD,
|
|
||||||
connectionLimit: 10,
|
|
||||||
supportBigNumbers: true,
|
|
||||||
timezone: '+00:00',
|
|
||||||
};
|
|
||||||
|
|
||||||
public async query(query, params?) {
|
|
||||||
const pool = await this.getPool();
|
|
||||||
return pool.query(query, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkDbConnection() {
|
|
||||||
try {
|
|
||||||
await this.query('SELECT ?', [1]);
|
|
||||||
logger.info('Database connection established.');
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('Could not connect to database: ' + (e instanceof Error ? e.message : e));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getPool(): Promise<Pool> {
|
|
||||||
if (this.pool === null) {
|
|
||||||
this.pool = createPool(this.poolConfig);
|
|
||||||
this.pool.on('connection', function (newConnection: PoolConnection) {
|
|
||||||
newConnection.query(`SET time_zone='+00:00'`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.pool;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new DB();
|
|
@ -1,23 +0,0 @@
|
|||||||
import DB from './database';
|
|
||||||
import databaseMigration from './database-migration';
|
|
||||||
import statsUpdater from './tasks/stats-updater.service';
|
|
||||||
import nodeSyncService from './tasks/node-sync.service';
|
|
||||||
import server from './server';
|
|
||||||
|
|
||||||
class LightningServer {
|
|
||||||
constructor() {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
await DB.checkDbConnection();
|
|
||||||
await databaseMigration.$initializeOrMigrateDatabase();
|
|
||||||
|
|
||||||
nodeSyncService.$startService();
|
|
||||||
statsUpdater.$startService();
|
|
||||||
|
|
||||||
server.startServer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lightningServer = new LightningServer();
|
|
@ -1,145 +0,0 @@
|
|||||||
import config from './config';
|
|
||||||
import * as dgram from 'dgram';
|
|
||||||
|
|
||||||
class Logger {
|
|
||||||
static priorities = {
|
|
||||||
emerg: 0,
|
|
||||||
alert: 1,
|
|
||||||
crit: 2,
|
|
||||||
err: 3,
|
|
||||||
warn: 4,
|
|
||||||
notice: 5,
|
|
||||||
info: 6,
|
|
||||||
debug: 7
|
|
||||||
};
|
|
||||||
static facilities = {
|
|
||||||
kern: 0,
|
|
||||||
user: 1,
|
|
||||||
mail: 2,
|
|
||||||
daemon: 3,
|
|
||||||
auth: 4,
|
|
||||||
syslog: 5,
|
|
||||||
lpr: 6,
|
|
||||||
news: 7,
|
|
||||||
uucp: 8,
|
|
||||||
local0: 16,
|
|
||||||
local1: 17,
|
|
||||||
local2: 18,
|
|
||||||
local3: 19,
|
|
||||||
local4: 20,
|
|
||||||
local5: 21,
|
|
||||||
local6: 22,
|
|
||||||
local7: 23
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
public emerg: ((msg: string) => void);
|
|
||||||
// @ts-ignore
|
|
||||||
public alert: ((msg: string) => void);
|
|
||||||
// @ts-ignore
|
|
||||||
public crit: ((msg: string) => void);
|
|
||||||
// @ts-ignore
|
|
||||||
public err: ((msg: string) => void);
|
|
||||||
// @ts-ignore
|
|
||||||
public warn: ((msg: string) => void);
|
|
||||||
// @ts-ignore
|
|
||||||
public notice: ((msg: string) => void);
|
|
||||||
// @ts-ignore
|
|
||||||
public info: ((msg: string) => void);
|
|
||||||
// @ts-ignore
|
|
||||||
public debug: ((msg: string) => void);
|
|
||||||
|
|
||||||
private name = 'mempool';
|
|
||||||
private client: dgram.Socket;
|
|
||||||
private network: string;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
let prio;
|
|
||||||
for (prio in Logger.priorities) {
|
|
||||||
if (true) {
|
|
||||||
this.addprio(prio);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.client = dgram.createSocket('udp4');
|
|
||||||
this.network = this.getNetwork();
|
|
||||||
}
|
|
||||||
|
|
||||||
private addprio(prio): void {
|
|
||||||
this[prio] = (function(_this) {
|
|
||||||
return function(msg) {
|
|
||||||
return _this.msg(prio, msg);
|
|
||||||
};
|
|
||||||
})(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getNetwork(): string {
|
|
||||||
if (config.MEMPOOL.NETWORK && config.MEMPOOL.NETWORK !== 'mainnet') {
|
|
||||||
return config.MEMPOOL.NETWORK;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private msg(priority, msg) {
|
|
||||||
let consolemsg, prionum, syslogmsg;
|
|
||||||
if (typeof msg === 'string' && msg.length > 0) {
|
|
||||||
while (msg[msg.length - 1].charCodeAt(0) === 10) {
|
|
||||||
msg = msg.slice(0, msg.length - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const network = this.network ? ' <' + this.network + '>' : '';
|
|
||||||
prionum = Logger.priorities[priority] || Logger.priorities.info;
|
|
||||||
consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${msg}`;
|
|
||||||
|
|
||||||
if (config.SYSLOG.ENABLED && Logger.priorities[priority] <= Logger.priorities[config.SYSLOG.MIN_PRIORITY]) {
|
|
||||||
syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`;
|
|
||||||
this.syslog(syslogmsg);
|
|
||||||
}
|
|
||||||
if (Logger.priorities[priority] > Logger.priorities[config.MEMPOOL.STDOUT_LOG_MIN_PRIORITY]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (priority === 'warning') {
|
|
||||||
priority = 'warn';
|
|
||||||
}
|
|
||||||
if (priority === 'debug') {
|
|
||||||
priority = 'info';
|
|
||||||
}
|
|
||||||
if (priority === 'err') {
|
|
||||||
priority = 'error';
|
|
||||||
}
|
|
||||||
return (console[priority] || console.error)(consolemsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private syslog(msg) {
|
|
||||||
let msgbuf;
|
|
||||||
msgbuf = Buffer.from(msg);
|
|
||||||
this.client.send(msgbuf, 0, msgbuf.length, config.SYSLOG.PORT, config.SYSLOG.HOST, function(err, bytes) {
|
|
||||||
if (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private leadZero(n: number): number | string {
|
|
||||||
if (n < 10) {
|
|
||||||
return '0' + n;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ts() {
|
|
||||||
let day, dt, hours, minutes, month, months, seconds;
|
|
||||||
dt = new Date();
|
|
||||||
hours = this.leadZero(dt.getHours());
|
|
||||||
minutes = this.leadZero(dt.getMinutes());
|
|
||||||
seconds = this.leadZero(dt.getSeconds());
|
|
||||||
month = dt.getMonth();
|
|
||||||
day = dt.getDate();
|
|
||||||
if (day < 10) {
|
|
||||||
day = ' ' + day;
|
|
||||||
}
|
|
||||||
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
||||||
return months[month] + ' ' + day + ' ' + hours + ':' + minutes + ':' + seconds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new Logger();
|
|
@ -1,40 +0,0 @@
|
|||||||
import { Express, Request, Response, NextFunction } from 'express';
|
|
||||||
import * as express from 'express';
|
|
||||||
import * as http from 'http';
|
|
||||||
import logger from './logger';
|
|
||||||
import config from './config';
|
|
||||||
import generalRoutes from './api/explorer/general.routes';
|
|
||||||
import nodesRoutes from './api/explorer/nodes.routes';
|
|
||||||
import channelsRoutes from './api/explorer/channels.routes';
|
|
||||||
|
|
||||||
class Server {
|
|
||||||
private server: http.Server | undefined;
|
|
||||||
private app: Express = express();
|
|
||||||
|
|
||||||
public startServer() {
|
|
||||||
this.app
|
|
||||||
.use((req: Request, res: Response, next: NextFunction) => {
|
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
||||||
next();
|
|
||||||
})
|
|
||||||
.use(express.urlencoded({ extended: true }))
|
|
||||||
.use(express.text())
|
|
||||||
;
|
|
||||||
|
|
||||||
this.server = http.createServer(this.app);
|
|
||||||
|
|
||||||
this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
|
|
||||||
logger.notice(`Mempool Lightning is running on port ${config.MEMPOOL.HTTP_PORT}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.initRoutes();
|
|
||||||
}
|
|
||||||
|
|
||||||
private initRoutes() {
|
|
||||||
generalRoutes.initRoutes(this.app);
|
|
||||||
nodesRoutes.initRoutes(this.app);
|
|
||||||
channelsRoutes.initRoutes(this.app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new Server();
|
|
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "esnext",
|
|
||||||
"lib": ["es2019", "dom"],
|
|
||||||
"strict": true,
|
|
||||||
"noImplicitAny": false,
|
|
||||||
"sourceMap": false,
|
|
||||||
"outDir": "dist",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"typeRoots": [
|
|
||||||
"node_modules/@types"
|
|
||||||
],
|
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"dist/**"
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
{
|
|
||||||
"rules": {
|
|
||||||
"arrow-return-shorthand": true,
|
|
||||||
"callable-types": true,
|
|
||||||
"class-name": true,
|
|
||||||
"comment-format": [
|
|
||||||
true,
|
|
||||||
"check-space"
|
|
||||||
],
|
|
||||||
"curly": true,
|
|
||||||
"deprecation": {
|
|
||||||
"severity": "warn"
|
|
||||||
},
|
|
||||||
"eofline": true,
|
|
||||||
"forin": false,
|
|
||||||
"import-blacklist": [
|
|
||||||
true,
|
|
||||||
"rxjs",
|
|
||||||
"rxjs/Rx"
|
|
||||||
],
|
|
||||||
"import-spacing": true,
|
|
||||||
"indent": [
|
|
||||||
true,
|
|
||||||
"spaces"
|
|
||||||
],
|
|
||||||
"interface-over-type-literal": true,
|
|
||||||
"label-position": true,
|
|
||||||
"max-line-length": [
|
|
||||||
true,
|
|
||||||
140
|
|
||||||
],
|
|
||||||
"member-access": false,
|
|
||||||
"member-ordering": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
"order": [
|
|
||||||
"static-field",
|
|
||||||
"instance-field",
|
|
||||||
"static-method",
|
|
||||||
"instance-method"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-arg": true,
|
|
||||||
"no-bitwise": true,
|
|
||||||
"no-console": [
|
|
||||||
true,
|
|
||||||
"debug",
|
|
||||||
"info",
|
|
||||||
"time",
|
|
||||||
"timeEnd",
|
|
||||||
"trace"
|
|
||||||
],
|
|
||||||
"no-construct": true,
|
|
||||||
"no-debugger": true,
|
|
||||||
"no-duplicate-super": true,
|
|
||||||
"no-empty": false,
|
|
||||||
"no-empty-interface": true,
|
|
||||||
"no-eval": true,
|
|
||||||
"no-inferrable-types": false,
|
|
||||||
"no-misused-new": true,
|
|
||||||
"no-non-null-assertion": true,
|
|
||||||
"no-shadowed-variable": true,
|
|
||||||
"no-string-literal": false,
|
|
||||||
"no-string-throw": true,
|
|
||||||
"no-switch-case-fall-through": true,
|
|
||||||
"no-trailing-whitespace": true,
|
|
||||||
"no-unnecessary-initializer": true,
|
|
||||||
"no-unused-expression": true,
|
|
||||||
"no-use-before-declare": true,
|
|
||||||
"no-var-keyword": true,
|
|
||||||
"object-literal-sort-keys": false,
|
|
||||||
"one-line": [
|
|
||||||
true,
|
|
||||||
"check-open-brace",
|
|
||||||
"check-catch",
|
|
||||||
"check-else",
|
|
||||||
"check-whitespace"
|
|
||||||
],
|
|
||||||
"prefer-const": true,
|
|
||||||
"quotemark": [
|
|
||||||
true,
|
|
||||||
"single"
|
|
||||||
],
|
|
||||||
"radix": true,
|
|
||||||
"semicolon": [
|
|
||||||
true,
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"triple-equals": [
|
|
||||||
true,
|
|
||||||
"allow-null-check"
|
|
||||||
],
|
|
||||||
"typedef-whitespace": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
"call-signature": "nospace",
|
|
||||||
"index-signature": "nospace",
|
|
||||||
"parameter": "nospace",
|
|
||||||
"property-declaration": "nospace",
|
|
||||||
"variable-declaration": "nospace"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"unified-signatures": true,
|
|
||||||
"variable-name": false,
|
|
||||||
"whitespace": [
|
|
||||||
true,
|
|
||||||
"check-branch",
|
|
||||||
"check-decl",
|
|
||||||
"check-operator",
|
|
||||||
"check-separator",
|
|
||||||
"check-type"
|
|
||||||
],
|
|
||||||
"directive-selector": [
|
|
||||||
true,
|
|
||||||
"attribute",
|
|
||||||
"app",
|
|
||||||
"camelCase"
|
|
||||||
],
|
|
||||||
"component-selector": [
|
|
||||||
true,
|
|
||||||
"element",
|
|
||||||
"app",
|
|
||||||
"kebab-case"
|
|
||||||
],
|
|
||||||
"no-output-on-prefix": true,
|
|
||||||
"use-input-property-decorator": true,
|
|
||||||
"use-output-property-decorator": true,
|
|
||||||
"use-host-property-decorator": true,
|
|
||||||
"no-input-rename": true,
|
|
||||||
"no-output-rename": true,
|
|
||||||
"use-life-cycle-interface": true,
|
|
||||||
"use-pipe-transform-interface": true,
|
|
||||||
"component-class-suffix": true,
|
|
||||||
"directive-class-suffix": true
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user