mirror of
https://github.com/mempool/mempool.git
synced 2025-01-19 05:34:03 +01:00
Merge Lightning backend into Mempool backend
This commit is contained in:
parent
faafa6db3b
commit
a238420d7f
@ -66,6 +66,15 @@
|
||||
"ENABLED": false,
|
||||
"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": {
|
||||
"ENABLED": false,
|
||||
"USE_ONION": true,
|
||||
|
@ -37,6 +37,7 @@
|
||||
"bolt07": "^1.8.1",
|
||||
"crypto-js": "^4.0.0",
|
||||
"express": "^4.18.0",
|
||||
"ln-service": "^53.17.4",
|
||||
"mysql2": "2.3.3",
|
||||
"node-worker-threads-pool": "^1.5.1",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
|
@ -13,6 +13,7 @@ export interface AbstractBitcoinApi {
|
||||
$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[][]>;
|
||||
}
|
||||
|
@ -130,6 +130,16 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
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);
|
||||
@ -195,7 +205,9 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
sequence: vin.sequence,
|
||||
txid: vin.txid || '',
|
||||
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;
|
||||
scriptsig: string;
|
||||
scriptsig_asm: string;
|
||||
inner_redeemscript_asm?: string;
|
||||
inner_witnessscript_asm?: string;
|
||||
inner_redeemscript_asm: string;
|
||||
inner_witnessscript_asm: string;
|
||||
sequence: any;
|
||||
witness?: string[];
|
||||
witness: string[];
|
||||
prevout: Vout | null;
|
||||
// Elements
|
||||
is_pegin?: boolean;
|
||||
|
@ -66,6 +66,11 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
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);
|
||||
|
@ -4,7 +4,7 @@ import logger from '../logger';
|
||||
import { Common } from './common';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 24;
|
||||
private static currentVersion = 25;
|
||||
private queryTimeout = 120000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@ -248,6 +248,15 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('DROP TABLE IF EXISTS `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) {
|
||||
throw e;
|
||||
}
|
||||
@ -569,6 +578,82 @@ class DatabaseMigration {
|
||||
adjustment float NOT NULL,
|
||||
PRIMARY KEY (height),
|
||||
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;`;
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ class ChannelsApi {
|
||||
|
||||
public async $getClosedChannelsWithoutReason(): Promise<any[]> {
|
||||
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);
|
||||
return rows;
|
||||
} catch (e) {
|
@ -1,16 +1,16 @@
|
||||
import config from '../../config';
|
||||
import { Express, Request, Response } from 'express';
|
||||
import { Application, Request, Response } from 'express';
|
||||
import channelsApi from './channels.api';
|
||||
|
||||
class ChannelsRoutes {
|
||||
constructor() { }
|
||||
|
||||
public initRoutes(app: Express) {
|
||||
public initRoutes(app: Application) {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/txids', this.$getChannelsByTransactionIds)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/search/:search', this.$searchChannelsById)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannelsForNode)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/txids', this.$getChannelsByTransactionIds)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/search/:search', this.$searchChannelsById)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/:short_id', this.$getChannel)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels', this.$getChannelsForNode)
|
||||
;
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
import config from '../../config';
|
||||
import { Express, Request, Response } from 'express';
|
||||
import { Application, Request, Response } from 'express';
|
||||
import nodesApi from './nodes.api';
|
||||
import channelsApi from './channels.api';
|
||||
import statisticsApi from './statistics.api';
|
||||
class GeneralRoutes {
|
||||
class GeneralLightningRoutes {
|
||||
constructor() { }
|
||||
|
||||
public initRoutes(app: Express) {
|
||||
public initRoutes(app: Application) {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'search', this.$searchNodesAndChannels)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics', this.$getStatistics)
|
||||
;
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/search', this.$searchNodesAndChannels)
|
||||
.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) {
|
||||
@ -38,6 +39,15 @@ class GeneralRoutes {
|
||||
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) {
|
||||
try {
|
||||
const searchStripped = search.replace('%', '') + '%';
|
@ -1,17 +1,16 @@
|
||||
import config from '../../config';
|
||||
import { Express, Request, Response } from 'express';
|
||||
import { Application, Request, Response } from 'express';
|
||||
import nodesApi from './nodes.api';
|
||||
class NodesRoutes {
|
||||
constructor() { }
|
||||
|
||||
public initRoutes(app: Express) {
|
||||
public initRoutes(app: Application) {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'nodes/search/:search', this.$searchNode)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key/statistics', this.$getHistoricalNodeStats)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode)
|
||||
;
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/top', this.$getTopNodes)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
|
||||
;
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
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';
|
||||
|
||||
function lightningApiFactory(): AbstractLightningApi {
|
||||
switch (config.MEMPOOL.BACKEND) {
|
||||
switch (config.LIGHTNING.BACKEND) {
|
||||
case 'lnd':
|
||||
default:
|
||||
return new LndApi();
|
@ -8,14 +8,17 @@ import logger from '../../../logger';
|
||||
class LndApi implements AbstractLightningApi {
|
||||
private lnd: any;
|
||||
constructor() {
|
||||
if (!config.LIGHTNING.ENABLED) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const tls = fs.readFileSync(config.LN_NODE_AUTH.TLS_CERT_PATH).toString('base64');
|
||||
const macaroon = fs.readFileSync(config.LN_NODE_AUTH.MACAROON_PATH).toString('base64');
|
||||
const tls = fs.readFileSync(config.LND_NODE_AUTH.TLS_CERT_PATH).toString('base64');
|
||||
const macaroon = fs.readFileSync(config.LND_NODE_AUTH.MACAROON_PATH).toString('base64');
|
||||
|
||||
const { lnd } = lnService.authenticatedLndGrpc({
|
||||
cert: tls,
|
||||
macaroon: macaroon,
|
||||
socket: config.LN_NODE_AUTH.SOCKET,
|
||||
socket: config.LND_NODE_AUTH.SOCKET,
|
||||
});
|
||||
|
||||
this.lnd = lnd;
|
@ -27,6 +27,15 @@ interface IConfig {
|
||||
ESPLORA: {
|
||||
REST_API_URL: string;
|
||||
};
|
||||
LIGHTNING: {
|
||||
ENABLED: boolean;
|
||||
BACKEND: 'lnd' | 'cln' | 'ldk';
|
||||
};
|
||||
LND_NODE_AUTH: {
|
||||
TLS_CERT_PATH: string;
|
||||
MACAROON_PATH: string;
|
||||
SOCKET: string;
|
||||
};
|
||||
ELECTRUM: {
|
||||
HOST: string;
|
||||
PORT: number;
|
||||
@ -158,6 +167,15 @@ const defaults: IConfig = {
|
||||
'ENABLED': false,
|
||||
'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': {
|
||||
'ENABLED': false,
|
||||
'USE_ONION': true,
|
||||
@ -166,11 +184,11 @@ const defaults: IConfig = {
|
||||
'USERNAME': '',
|
||||
'PASSWORD': ''
|
||||
},
|
||||
"PRICE_DATA_SERVER": {
|
||||
'PRICE_DATA_SERVER': {
|
||||
'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
|
||||
'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
|
||||
},
|
||||
"EXTERNAL_DATA_SERVER": {
|
||||
'EXTERNAL_DATA_SERVER': {
|
||||
'MEMPOOL_API': 'https://mempool.space/api/v1',
|
||||
'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
|
||||
'LIQUID_API': 'https://liquid.network/api/v1',
|
||||
@ -190,6 +208,8 @@ class Config implements IConfig {
|
||||
SYSLOG: IConfig['SYSLOG'];
|
||||
STATISTICS: IConfig['STATISTICS'];
|
||||
BISQ: IConfig['BISQ'];
|
||||
LIGHTNING: IConfig['LIGHTNING'];
|
||||
LND_NODE_AUTH: IConfig['LND_NODE_AUTH'];
|
||||
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
|
||||
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||
@ -205,6 +225,8 @@ class Config implements IConfig {
|
||||
this.SYSLOG = configs.SYSLOG;
|
||||
this.STATISTICS = configs.STATISTICS;
|
||||
this.BISQ = configs.BISQ;
|
||||
this.LIGHTNING = configs.LIGHTNING;
|
||||
this.LND_NODE_AUTH = configs.LND_NODE_AUTH;
|
||||
this.SOCKS5PROXY = configs.SOCKS5PROXY;
|
||||
this.PRICE_DATA_SERVER = configs.PRICE_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 priceUpdater from './tasks/price-updater';
|
||||
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 {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@ -130,6 +135,13 @@ class Server {
|
||||
bisqMarkets.startBisqService();
|
||||
}
|
||||
|
||||
if (config.LIGHTNING.ENABLED) {
|
||||
nodeSyncService.$startService()
|
||||
.then(() => {
|
||||
lightningStatsUpdater.$startService();
|
||||
});
|
||||
}
|
||||
|
||||
this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
|
||||
if (worker) {
|
||||
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)
|
||||
;
|
||||
}
|
||||
|
||||
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 DB from '../database';
|
||||
import logger from '../logger';
|
||||
import lightningApi from '../api/lightning/lightning-api-factory';
|
||||
import { ILightningApi } from '../api/lightning/lightning-api.interface';
|
||||
import channelsApi from '../api/explorer/channels.api';
|
||||
import bitcoinClient from '../api/bitcoin/bitcoin-client';
|
||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||
import config from '../config';
|
||||
import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
|
||||
import DB from '../../database';
|
||||
import logger from '../../logger';
|
||||
import channelsApi from '../../api/explorer/channels.api';
|
||||
import bitcoinClient from '../../api/bitcoin/bitcoin-client';
|
||||
import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
|
||||
import config from '../../config';
|
||||
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
|
||||
import lightningApi from '../../api/lightning/lightning-api-factory';
|
||||
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
|
||||
|
||||
class NodeSyncService {
|
||||
constructor() {}
|
||||
@ -15,43 +15,36 @@ class NodeSyncService {
|
||||
public async $startService() {
|
||||
logger.info('Starting node sync service');
|
||||
|
||||
await this.$updateNodes();
|
||||
await this.$runUpdater();
|
||||
|
||||
setInterval(async () => {
|
||||
await this.$updateNodes();
|
||||
await this.$runUpdater();
|
||||
}, 1000 * 60 * 60);
|
||||
}
|
||||
|
||||
private async $updateNodes() {
|
||||
private async $runUpdater() {
|
||||
try {
|
||||
logger.info(`Updating nodes and channels...`);
|
||||
|
||||
const networkGraph = await lightningApi.$getNetworkGraph();
|
||||
|
||||
for (const node of networkGraph.nodes) {
|
||||
await this.$saveNode(node);
|
||||
}
|
||||
logger.debug(`Nodes updated`);
|
||||
logger.info(`Nodes updated.`);
|
||||
|
||||
await this.$setChannelsInactive();
|
||||
|
||||
for (const channel of networkGraph.channels) {
|
||||
await this.$saveChannel(channel);
|
||||
}
|
||||
logger.debug(`Channels updated`);
|
||||
logger.info(`Channels updated.`);
|
||||
|
||||
await this.$findInactiveNodesAndChannels();
|
||||
logger.debug(`Inactive channels scan complete`);
|
||||
|
||||
await this.$lookUpCreationDateFromChain();
|
||||
logger.debug(`Channel creation dates scan complete`);
|
||||
|
||||
await this.$updateNodeFirstSeen();
|
||||
logger.debug(`Node first seen dates scan complete`);
|
||||
|
||||
await this.$scanForClosedChannels();
|
||||
logger.debug(`Closed channels scan complete`);
|
||||
|
||||
await this.$runClosedChannelsForensics();
|
||||
logger.debug(`Closed channels forensics scan complete`);
|
||||
|
||||
} catch (e) {
|
||||
logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e));
|
||||
@ -80,18 +73,21 @@ class NodeSyncService {
|
||||
await DB.query(query, params);
|
||||
}
|
||||
}
|
||||
logger.info(`Node first seen dates scan complete.`);
|
||||
} catch (e) {
|
||||
logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
private async $lookUpCreationDateFromChain() {
|
||||
logger.info(`Running channel creation date lookup...`);
|
||||
try {
|
||||
const channels = await channelsApi.$getChannelsWithoutCreatedDate();
|
||||
for (const channel of channels) {
|
||||
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]);
|
||||
}
|
||||
logger.info(`Channel creation dates scan complete.`);
|
||||
} catch (e) {
|
||||
logger.err('$setCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@ -99,6 +95,8 @@ class NodeSyncService {
|
||||
|
||||
// Looking for channels whos nodes are inactive
|
||||
private async $findInactiveNodesAndChannels(): Promise<void> {
|
||||
logger.info(`Running inactive channels scan...`);
|
||||
|
||||
try {
|
||||
// @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)`);
|
||||
@ -106,6 +104,7 @@ class NodeSyncService {
|
||||
for (const channel of channels) {
|
||||
await this.$updateChannelStatus(channel.id, 0);
|
||||
}
|
||||
logger.info(`Inactive channels scan complete.`);
|
||||
} catch (e) {
|
||||
logger.err('$findInactiveNodesAndChannels() error: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@ -113,6 +112,7 @@ class NodeSyncService {
|
||||
|
||||
private async $scanForClosedChannels(): Promise<void> {
|
||||
try {
|
||||
logger.info(`Starting closed channels scan...`);
|
||||
const channels = await channelsApi.$getChannelsByStatus(0);
|
||||
for (const channel of channels) {
|
||||
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) {
|
||||
logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@ -140,8 +141,8 @@ class NodeSyncService {
|
||||
if (!config.ESPLORA.REST_API_URL) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Started running closed channel forensics...`);
|
||||
const channels = await channelsApi.$getClosedChannelsWithoutReason();
|
||||
for (const channel of channels) {
|
||||
let reason = 0;
|
||||
@ -186,6 +187,7 @@ class NodeSyncService {
|
||||
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
||||
}
|
||||
}
|
||||
logger.info(`Closed channels forensics scan complete.`);
|
||||
} catch (e) {
|
||||
logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import lightningApi from '../api/lightning/lightning-api-factory';
|
||||
import logger from "../../logger";
|
||||
import DB from "../../database";
|
||||
import lightningApi from "../../api/lightning/lightning-api-factory";
|
||||
|
||||
class LightningStatsUpdater {
|
||||
constructor() {}
|
||||
@ -29,6 +28,8 @@ class LightningStatsUpdater {
|
||||
}
|
||||
|
||||
private async $logNodeStatsDaily() {
|
||||
logger.info(`Running daily node stats update...`);
|
||||
|
||||
const currentDate = new Date().toISOString().split('T')[0];
|
||||
try {
|
||||
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]);
|
||||
}
|
||||
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) {
|
||||
logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
@ -60,9 +61,11 @@ class LightningStatsUpdater {
|
||||
|
||||
// We only run this on first launch
|
||||
private async $populateHistoricalData() {
|
||||
logger.info(`Running historical stats population...`);
|
||||
|
||||
const startTime = '2018-01-13';
|
||||
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
|
||||
if (rows[0]['COUNT(*)'] > 0) {
|
||||
return;
|
||||
@ -86,7 +89,7 @@ class LightningStatsUpdater {
|
||||
channelsCount++;
|
||||
}
|
||||
|
||||
const query = `INSERT INTO statistics(
|
||||
const query = `INSERT INTO lightning_stats(
|
||||
added,
|
||||
channel_count,
|
||||
node_count,
|
||||
@ -117,7 +120,7 @@ class LightningStatsUpdater {
|
||||
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, [
|
||||
nodeCount,
|
||||
@ -128,13 +131,15 @@ class LightningStatsUpdater {
|
||||
date.setDate(date.getDate() + 1);
|
||||
}
|
||||
|
||||
logger.debug('Historical stats populated.');
|
||||
logger.info('Historical stats populated.');
|
||||
} catch (e) {
|
||||
logger.err('$populateHistoricalData() error: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
private async $logLightningStatsDaily() {
|
||||
logger.info(`Running lightning daily stats log...`);
|
||||
|
||||
const currentDate = new Date().toISOString().split('T')[0];
|
||||
try {
|
||||
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,
|
||||
channel_count,
|
||||
node_count,
|
||||
@ -164,8 +169,9 @@ class LightningStatsUpdater {
|
||||
networkGraph.nodes.length,
|
||||
total_capacity,
|
||||
]);
|
||||
logger.info(`Lightning daily stats done.`);
|
||||
} 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(...[
|
||||
{
|
||||
context: ['/lightning/api/v1/**'],
|
||||
target: `http://localhost:8899`,
|
||||
context: ['/testnet/api/v1/lightning/**'],
|
||||
target: `http://localhost:8999`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/lightning/api": "/api"
|
||||
"^/testnet": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1,23 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
const API_BASE_URL = '/lightning/api/v1';
|
||||
import { StateService } from '../services/state.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LightningApiService {
|
||||
private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet
|
||||
|
||||
constructor(
|
||||
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> {
|
||||
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> {
|
||||
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> {
|
||||
@ -27,22 +37,22 @@ export class LightningApiService {
|
||||
.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> {
|
||||
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> {
|
||||
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> {
|
||||
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> {
|
||||
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) => {
|
||||
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[]> {
|
||||
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…
Reference in New Issue
Block a user