Merge branch 'master' into ops/add-core-lightning
This commit is contained in:
commit
e0d677b01c
@ -79,8 +79,8 @@
|
|||||||
},
|
},
|
||||||
"LND": {
|
"LND": {
|
||||||
"TLS_CERT_PATH": "tls.cert",
|
"TLS_CERT_PATH": "tls.cert",
|
||||||
"MACAROON_PATH": "admin.macaroon",
|
"MACAROON_PATH": "readonly.macaroon",
|
||||||
"SOCKET": "localhost:10009"
|
"REST_API_URL": "https://localhost:8080"
|
||||||
},
|
},
|
||||||
"SOCKS5PROXY": {
|
"SOCKS5PROXY": {
|
||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
|
930
backend/package-lock.json
generated
930
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,8 @@
|
|||||||
"mempool",
|
"mempool",
|
||||||
"blockchain",
|
"blockchain",
|
||||||
"explorer",
|
"explorer",
|
||||||
"liquid"
|
"liquid",
|
||||||
|
"lightning"
|
||||||
],
|
],
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -34,10 +35,9 @@
|
|||||||
"@types/node": "^16.11.41",
|
"@types/node": "^16.11.41",
|
||||||
"axios": "~0.27.2",
|
"axios": "~0.27.2",
|
||||||
"bitcoinjs-lib": "6.0.1",
|
"bitcoinjs-lib": "6.0.1",
|
||||||
"bolt07": "^1.8.1",
|
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
"express": "^4.18.0",
|
"express": "^4.18.0",
|
||||||
"lightning": "^5.16.3",
|
"fast-xml-parser": "^4.0.9",
|
||||||
"maxmind": "^4.3.6",
|
"maxmind": "^4.3.6",
|
||||||
"mysql2": "2.3.3",
|
"mysql2": "2.3.3",
|
||||||
"node-worker-threads-pool": "^1.5.1",
|
"node-worker-threads-pool": "^1.5.1",
|
||||||
|
@ -22,6 +22,8 @@ import poolsParser from './pools-parser';
|
|||||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
import mining from './mining/mining';
|
import mining from './mining/mining';
|
||||||
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
||||||
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
|
import priceUpdater from '../tasks/price-updater';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
@ -457,6 +459,19 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
||||||
|
|
||||||
|
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||||
|
if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
|
||||||
|
await blocksRepository.$saveBlockPrices([{
|
||||||
|
height: blockExtended.height,
|
||||||
|
priceId: lastestPriceId,
|
||||||
|
}]);
|
||||||
|
} else {
|
||||||
|
logger.info(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`)
|
||||||
|
setTimeout(() => {
|
||||||
|
indexer.runSingleTask('blocksPrices');
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
// Save blocks summary for visualization if it's enabled
|
// Save blocks summary for visualization if it's enabled
|
||||||
if (Common.blocksSummariesIndexingEnabled() === true) {
|
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { convertChannelId } from './lightning/clightning/clightning-convert';
|
||||||
export class Common {
|
export class Common {
|
||||||
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
|
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
|
||||||
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
|
'144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49'
|
||||||
@ -184,4 +185,37 @@ export class Common {
|
|||||||
config.MEMPOOL.BLOCKS_SUMMARIES_INDEXING === true
|
config.MEMPOOL.BLOCKS_SUMMARIES_INDEXING === true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static setDateMidnight(date: Date): void {
|
||||||
|
date.setUTCHours(0);
|
||||||
|
date.setUTCMinutes(0);
|
||||||
|
date.setUTCSeconds(0);
|
||||||
|
date.setUTCMilliseconds(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static channelShortIdToIntegerId(id: string): string {
|
||||||
|
if (config.LIGHTNING.BACKEND === 'lnd') {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
return convertChannelId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decodes a channel id returned by lnd as uint64 to a short channel id */
|
||||||
|
static channelIntegerIdToShortId(id: string): string {
|
||||||
|
if (config.LIGHTNING.BACKEND === 'cln') {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = BigInt(id);
|
||||||
|
return [
|
||||||
|
n >> 40n, // nth block
|
||||||
|
(n >> 16n) & 0xffffffn, // nth tx of the block
|
||||||
|
n & 0xffffn // nth output of the tx
|
||||||
|
].join('x');
|
||||||
|
}
|
||||||
|
|
||||||
|
static utcDateToMysql(date?: number): string {
|
||||||
|
const d = new Date((date || 0) * 1000);
|
||||||
|
return d.toISOString().split('T')[0] + ' ' + d.toTimeString().split(' ')[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import logger from '../logger';
|
|||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 33;
|
private static currentVersion = 36;
|
||||||
private queryTimeout = 120000;
|
private queryTimeout = 120000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -311,6 +311,19 @@ class DatabaseMigration {
|
|||||||
if (databaseSchemaVersion < 33 && isBitcoin == true) {
|
if (databaseSchemaVersion < 33 && isBitcoin == true) {
|
||||||
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
|
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 34 && isBitcoin == true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 35 && isBitcoin == true) {
|
||||||
|
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
|
||||||
|
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 36 && isBitcoin == true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import DB from '../../database';
|
import DB from '../../database';
|
||||||
|
import nodesApi from './nodes.api';
|
||||||
|
import { ResultSetHeader } from 'mysql2';
|
||||||
|
import { ILightningApi } from '../lightning/lightning-api.interface';
|
||||||
|
import { Common } from '../common';
|
||||||
|
|
||||||
class ChannelsApi {
|
class ChannelsApi {
|
||||||
public async $getAllChannels(): Promise<any[]> {
|
public async $getAllChannels(): Promise<any[]> {
|
||||||
@ -181,15 +185,57 @@ class ChannelsApi {
|
|||||||
|
|
||||||
public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> {
|
public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
// Default active and inactive channels
|
let channelStatusFilter;
|
||||||
let statusQuery = '< 2';
|
if (status === 'open') {
|
||||||
// Closed channels only
|
channelStatusFilter = '< 2';
|
||||||
if (status === 'closed') {
|
} else if (status === 'closed') {
|
||||||
statusQuery = '= 2';
|
channelStatusFilter = '= 2';
|
||||||
}
|
}
|
||||||
const query = `SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.*, ns1.channels AS channels_left, ns1.capacity AS capacity_left, ns2.channels AS channels_right, ns2.capacity AS capacity_right FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key LEFT JOIN node_stats AS ns1 ON ns1.public_key = channels.node1_public_key LEFT JOIN node_stats AS ns2 ON ns2.public_key = channels.node2_public_key WHERE (ns1.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node1_public_key) AND ns2.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node2_public_key)) AND (node1_public_key = ? OR node2_public_key = ?) AND status ${statusQuery} ORDER BY channels.capacity DESC LIMIT ?, ?`;
|
|
||||||
const [rows]: any = await DB.query(query, [public_key, public_key, index, length]);
|
// Channels originating from node
|
||||||
const channels = rows.map((row) => this.convertChannel(row));
|
let query = `
|
||||||
|
SELECT node2.alias, node2.public_key, channels.status, channels.node1_fee_rate,
|
||||||
|
channels.capacity, channels.short_id, channels.id
|
||||||
|
FROM channels
|
||||||
|
JOIN nodes AS node2 ON node2.public_key = channels.node2_public_key
|
||||||
|
WHERE node1_public_key = ? AND channels.status ${channelStatusFilter}
|
||||||
|
`;
|
||||||
|
const [channelsFromNode]: any = await DB.query(query, [public_key, index, length]);
|
||||||
|
|
||||||
|
// Channels incoming to node
|
||||||
|
query = `
|
||||||
|
SELECT node1.alias, node1.public_key, channels.status, channels.node2_fee_rate,
|
||||||
|
channels.capacity, channels.short_id, channels.id
|
||||||
|
FROM channels
|
||||||
|
JOIN nodes AS node1 ON node1.public_key = channels.node1_public_key
|
||||||
|
WHERE node2_public_key = ? AND channels.status ${channelStatusFilter}
|
||||||
|
`;
|
||||||
|
const [channelsToNode]: any = await DB.query(query, [public_key, index, length]);
|
||||||
|
|
||||||
|
let allChannels = channelsFromNode.concat(channelsToNode);
|
||||||
|
allChannels.sort((a, b) => {
|
||||||
|
return b.capacity - a.capacity;
|
||||||
|
});
|
||||||
|
allChannels = allChannels.slice(index, index + length);
|
||||||
|
|
||||||
|
const channels: any[] = []
|
||||||
|
for (const row of allChannels) {
|
||||||
|
const activeChannelsStats: any = await nodesApi.$getActiveChannelsStats(row.public_key);
|
||||||
|
channels.push({
|
||||||
|
status: row.status,
|
||||||
|
capacity: row.capacity ?? 0,
|
||||||
|
short_id: row.short_id,
|
||||||
|
id: row.id,
|
||||||
|
fee_rate: row.node1_fee_rate ?? row.node2_fee_rate ?? 0,
|
||||||
|
node: {
|
||||||
|
alias: row.alias.length > 0 ? row.alias : row.public_key.slice(0, 20),
|
||||||
|
public_key: row.public_key,
|
||||||
|
channels: activeChannelsStats.active_channel_count ?? 0,
|
||||||
|
capacity: activeChannelsStats.capacity ?? 0,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return channels;
|
return channels;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$getChannelsForNode error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$getChannelsForNode error: ' + (e instanceof Error ? e.message : e));
|
||||||
@ -205,7 +251,12 @@ class ChannelsApi {
|
|||||||
if (status === 'closed') {
|
if (status === 'closed') {
|
||||||
statusQuery = '= 2';
|
statusQuery = '= 2';
|
||||||
}
|
}
|
||||||
const query = `SELECT COUNT(*) AS count FROM channels WHERE (node1_public_key = ? OR node2_public_key = ?) AND status ${statusQuery}`;
|
const query = `
|
||||||
|
SELECT COUNT(*) AS count
|
||||||
|
FROM channels
|
||||||
|
WHERE (node1_public_key = ? OR node2_public_key = ?)
|
||||||
|
AND status ${statusQuery}
|
||||||
|
`;
|
||||||
const [rows]: any = await DB.query(query, [public_key, public_key]);
|
const [rows]: any = await DB.query(query, [public_key, public_key]);
|
||||||
return rows[0]['count'];
|
return rows[0]['count'];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -254,6 +305,135 @@ class ChannelsApi {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save or update a channel present in the graph
|
||||||
|
*/
|
||||||
|
public async $saveChannel(channel: ILightningApi.Channel): Promise<void> {
|
||||||
|
const [ txid, vout ] = channel.chan_point.split(':');
|
||||||
|
|
||||||
|
const policy1: Partial<ILightningApi.RoutingPolicy> = channel.node1_policy || {};
|
||||||
|
const policy2: Partial<ILightningApi.RoutingPolicy> = channel.node2_policy || {};
|
||||||
|
|
||||||
|
const query = `INSERT INTO channels
|
||||||
|
(
|
||||||
|
id,
|
||||||
|
short_id,
|
||||||
|
capacity,
|
||||||
|
transaction_id,
|
||||||
|
transaction_vout,
|
||||||
|
updated_at,
|
||||||
|
status,
|
||||||
|
node1_public_key,
|
||||||
|
node1_base_fee_mtokens,
|
||||||
|
node1_cltv_delta,
|
||||||
|
node1_fee_rate,
|
||||||
|
node1_is_disabled,
|
||||||
|
node1_max_htlc_mtokens,
|
||||||
|
node1_min_htlc_mtokens,
|
||||||
|
node1_updated_at,
|
||||||
|
node2_public_key,
|
||||||
|
node2_base_fee_mtokens,
|
||||||
|
node2_cltv_delta,
|
||||||
|
node2_fee_rate,
|
||||||
|
node2_is_disabled,
|
||||||
|
node2_max_htlc_mtokens,
|
||||||
|
node2_min_htlc_mtokens,
|
||||||
|
node2_updated_at
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
capacity = ?,
|
||||||
|
updated_at = ?,
|
||||||
|
status = 1,
|
||||||
|
node1_public_key = ?,
|
||||||
|
node1_base_fee_mtokens = ?,
|
||||||
|
node1_cltv_delta = ?,
|
||||||
|
node1_fee_rate = ?,
|
||||||
|
node1_is_disabled = ?,
|
||||||
|
node1_max_htlc_mtokens = ?,
|
||||||
|
node1_min_htlc_mtokens = ?,
|
||||||
|
node1_updated_at = ?,
|
||||||
|
node2_public_key = ?,
|
||||||
|
node2_base_fee_mtokens = ?,
|
||||||
|
node2_cltv_delta = ?,
|
||||||
|
node2_fee_rate = ?,
|
||||||
|
node2_is_disabled = ?,
|
||||||
|
node2_max_htlc_mtokens = ?,
|
||||||
|
node2_min_htlc_mtokens = ?,
|
||||||
|
node2_updated_at = ?
|
||||||
|
;`;
|
||||||
|
|
||||||
|
await DB.query(query, [
|
||||||
|
Common.channelShortIdToIntegerId(channel.channel_id),
|
||||||
|
Common.channelIntegerIdToShortId(channel.channel_id),
|
||||||
|
channel.capacity,
|
||||||
|
txid,
|
||||||
|
vout,
|
||||||
|
Common.utcDateToMysql(channel.last_update),
|
||||||
|
channel.node1_pub,
|
||||||
|
policy1.fee_base_msat,
|
||||||
|
policy1.time_lock_delta,
|
||||||
|
policy1.fee_rate_milli_msat,
|
||||||
|
policy1.disabled,
|
||||||
|
policy1.max_htlc_msat,
|
||||||
|
policy1.min_htlc,
|
||||||
|
Common.utcDateToMysql(policy1.last_update),
|
||||||
|
channel.node2_pub,
|
||||||
|
policy2.fee_base_msat,
|
||||||
|
policy2.time_lock_delta,
|
||||||
|
policy2.fee_rate_milli_msat,
|
||||||
|
policy2.disabled,
|
||||||
|
policy2.max_htlc_msat,
|
||||||
|
policy2.min_htlc,
|
||||||
|
Common.utcDateToMysql(policy2.last_update),
|
||||||
|
channel.capacity,
|
||||||
|
Common.utcDateToMysql(channel.last_update),
|
||||||
|
channel.node1_pub,
|
||||||
|
policy1.fee_base_msat,
|
||||||
|
policy1.time_lock_delta,
|
||||||
|
policy1.fee_rate_milli_msat,
|
||||||
|
policy1.disabled,
|
||||||
|
policy1.max_htlc_msat,
|
||||||
|
policy1.min_htlc,
|
||||||
|
Common.utcDateToMysql(policy1.last_update),
|
||||||
|
channel.node2_pub,
|
||||||
|
policy2.fee_base_msat,
|
||||||
|
policy2.time_lock_delta,
|
||||||
|
policy2.fee_rate_milli_msat,
|
||||||
|
policy2.disabled,
|
||||||
|
policy2.max_htlc_msat,
|
||||||
|
policy2.min_htlc,
|
||||||
|
Common.utcDateToMysql(policy2.last_update)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set all channels not in `graphChannelsIds` as inactive (status = 0)
|
||||||
|
*/
|
||||||
|
public async $setChannelsInactive(graphChannelsIds: string[]): Promise<void> {
|
||||||
|
if (graphChannelsIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await DB.query<ResultSetHeader>(`
|
||||||
|
UPDATE channels
|
||||||
|
SET status = 0
|
||||||
|
WHERE short_id NOT IN (
|
||||||
|
${graphChannelsIds.map(id => `"${id}"`).join(',')}
|
||||||
|
)
|
||||||
|
AND status != 2
|
||||||
|
`);
|
||||||
|
if (result[0].changedRows ?? 0 > 0) {
|
||||||
|
logger.info(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ChannelsApi();
|
export default new ChannelsApi();
|
||||||
|
@ -46,9 +46,11 @@ class ChannelsRoutes {
|
|||||||
}
|
}
|
||||||
const index = parseInt(typeof req.query.index === 'string' ? req.query.index : '0', 10) || 0;
|
const index = parseInt(typeof req.query.index === 'string' ? req.query.index : '0', 10) || 0;
|
||||||
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
|
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
|
||||||
const length = 25;
|
const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status);
|
||||||
const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, length, status);
|
|
||||||
const channelsCount = await channelsApi.$getChannelsCountForNode(req.query.public_key, status);
|
const channelsCount = await channelsApi.$getChannelsCountForNode(req.query.public_key, status);
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.header('X-Total-Count', channelsCount.toString());
|
res.header('X-Total-Count', channelsCount.toString());
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1,24 +1,18 @@
|
|||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import DB from '../../database';
|
import DB from '../../database';
|
||||||
|
import { ResultSetHeader } from 'mysql2';
|
||||||
|
import { ILightningApi } from '../lightning/lightning-api.interface';
|
||||||
|
|
||||||
class NodesApi {
|
class NodesApi {
|
||||||
public async $getNode(public_key: string): Promise<any> {
|
public async $getNode(public_key: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const query = `
|
// General info
|
||||||
SELECT nodes.*, geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city,
|
let query = `
|
||||||
geo_names_country.names as country, geo_names_subdivision.names as subdivision,
|
SELECT public_key, alias, UNIX_TIMESTAMP(first_seen) AS first_seen,
|
||||||
(SELECT Count(*)
|
UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets,
|
||||||
FROM channels
|
as_number, city_id, country_id, subdivision_id, longitude, latitude,
|
||||||
WHERE channels.status = 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_closed_count,
|
geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city,
|
||||||
(SELECT Count(*)
|
geo_names_country.names as country, geo_names_subdivision.names as subdivision
|
||||||
FROM channels
|
|
||||||
WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_active_count,
|
|
||||||
(SELECT Sum(capacity)
|
|
||||||
FROM channels
|
|
||||||
WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS capacity,
|
|
||||||
(SELECT Avg(capacity)
|
|
||||||
FROM channels
|
|
||||||
WHERE status = 1 AND ( node1_public_key = ? OR node2_public_key = ? )) AS channels_capacity_avg
|
|
||||||
FROM nodes
|
FROM nodes
|
||||||
LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
|
LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
|
||||||
LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
|
LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
|
||||||
@ -27,21 +21,70 @@ class NodesApi {
|
|||||||
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
||||||
WHERE public_key = ?
|
WHERE public_key = ?
|
||||||
`;
|
`;
|
||||||
const [rows]: any = await DB.query(query, [public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key]);
|
let [rows]: any[] = await DB.query(query, [public_key]);
|
||||||
if (rows.length > 0) {
|
if (rows.length === 0) {
|
||||||
rows[0].as_organization = JSON.parse(rows[0].as_organization);
|
throw new Error(`This node does not exist, or our node is not seeing it yet`);
|
||||||
rows[0].subdivision = JSON.parse(rows[0].subdivision);
|
|
||||||
rows[0].city = JSON.parse(rows[0].city);
|
|
||||||
rows[0].country = JSON.parse(rows[0].country);
|
|
||||||
return rows[0];
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
const node = rows[0];
|
||||||
|
node.as_organization = JSON.parse(node.as_organization);
|
||||||
|
node.subdivision = JSON.parse(node.subdivision);
|
||||||
|
node.city = JSON.parse(node.city);
|
||||||
|
node.country = JSON.parse(node.country);
|
||||||
|
|
||||||
|
// Active channels and capacity
|
||||||
|
const activeChannelsStats: any = await this.$getActiveChannelsStats(public_key);
|
||||||
|
node.active_channel_count = activeChannelsStats.active_channel_count ?? 0;
|
||||||
|
node.capacity = activeChannelsStats.capacity ?? 0;
|
||||||
|
|
||||||
|
// Opened channels count
|
||||||
|
query = `
|
||||||
|
SELECT count(short_id) as opened_channel_count
|
||||||
|
FROM channels
|
||||||
|
WHERE status != 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
|
||||||
|
`;
|
||||||
|
[rows] = await DB.query(query, [public_key, public_key]);
|
||||||
|
node.opened_channel_count = 0;
|
||||||
|
if (rows.length > 0) {
|
||||||
|
node.opened_channel_count = rows[0].opened_channel_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closed channels count
|
||||||
|
query = `
|
||||||
|
SELECT count(short_id) as closed_channel_count
|
||||||
|
FROM channels
|
||||||
|
WHERE status = 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
|
||||||
|
`;
|
||||||
|
[rows] = await DB.query(query, [public_key, public_key]);
|
||||||
|
node.closed_channel_count = 0;
|
||||||
|
if (rows.length > 0) {
|
||||||
|
node.closed_channel_count = rows[0].closed_channel_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$getNode error: ' + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getActiveChannelsStats(node_public_key: string): Promise<unknown> {
|
||||||
|
const query = `
|
||||||
|
SELECT count(short_id) as active_channel_count, sum(capacity) as capacity
|
||||||
|
FROM channels
|
||||||
|
WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?)
|
||||||
|
`;
|
||||||
|
const [rows]: any[] = await DB.query(query, [node_public_key, node_public_key]);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
return {
|
||||||
|
active_channel_count: rows[0].active_channel_count,
|
||||||
|
capacity: rows[0].capacity
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $getAllNodes(): Promise<any> {
|
public async $getAllNodes(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT * FROM nodes`;
|
const query = `SELECT * FROM nodes`;
|
||||||
@ -55,7 +98,12 @@ class NodesApi {
|
|||||||
|
|
||||||
public async $getNodeStats(public_key: string): Promise<any> {
|
public async $getNodeStats(public_key: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT UNIX_TIMESTAMP(added) AS added, capacity, channels FROM node_stats WHERE public_key = ? ORDER BY added DESC`;
|
const query = `
|
||||||
|
SELECT UNIX_TIMESTAMP(added) AS added, capacity, channels
|
||||||
|
FROM node_stats
|
||||||
|
WHERE public_key = ?
|
||||||
|
ORDER BY added DESC
|
||||||
|
`;
|
||||||
const [rows]: any = await DB.query(query, [public_key]);
|
const [rows]: any = await DB.query(query, [public_key]);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -66,8 +114,19 @@ class NodesApi {
|
|||||||
|
|
||||||
public async $getTopCapacityNodes(): Promise<any> {
|
public async $getTopCapacityNodes(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT nodes.*, node_stats.capacity, node_stats.channels FROM nodes LEFT JOIN node_stats ON node_stats.public_key = nodes.public_key ORDER BY node_stats.added DESC, node_stats.capacity DESC LIMIT 10`;
|
let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats');
|
||||||
const [rows]: any = await DB.query(query);
|
const latestDate = rows[0].maxAdded;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels
|
||||||
|
FROM node_stats
|
||||||
|
JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||||
|
WHERE added = FROM_UNIXTIME(${latestDate})
|
||||||
|
ORDER BY capacity DESC
|
||||||
|
LIMIT 10;
|
||||||
|
`;
|
||||||
|
[rows] = await DB.query(query);
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$getTopCapacityNodes error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$getTopCapacityNodes error: ' + (e instanceof Error ? e.message : e));
|
||||||
@ -77,8 +136,19 @@ class NodesApi {
|
|||||||
|
|
||||||
public async $getTopChannelsNodes(): Promise<any> {
|
public async $getTopChannelsNodes(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT nodes.*, node_stats.capacity, node_stats.channels FROM nodes LEFT JOIN node_stats ON node_stats.public_key = nodes.public_key ORDER BY node_stats.added DESC, node_stats.channels DESC LIMIT 10`;
|
let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats');
|
||||||
const [rows]: any = await DB.query(query);
|
const latestDate = rows[0].maxAdded;
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels
|
||||||
|
FROM node_stats
|
||||||
|
JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||||
|
WHERE added = FROM_UNIXTIME(${latestDate})
|
||||||
|
ORDER BY channels DESC
|
||||||
|
LIMIT 10;
|
||||||
|
`;
|
||||||
|
[rows] = await DB.query(query);
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e));
|
||||||
@ -163,8 +233,8 @@ class NodesApi {
|
|||||||
public async $getNodesPerCountry(countryId: string) {
|
public async $getNodesPerCountry(countryId: string) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
SELECT node_stats.public_key, node_stats.capacity, node_stats.channels, nodes.alias,
|
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||||
UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
||||||
geo_names_city.names as city
|
geo_names_city.names as city
|
||||||
FROM node_stats
|
FROM node_stats
|
||||||
JOIN (
|
JOIN (
|
||||||
@ -172,7 +242,7 @@ class NodesApi {
|
|||||||
FROM node_stats
|
FROM node_stats
|
||||||
GROUP BY public_key
|
GROUP BY public_key
|
||||||
) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added
|
) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added
|
||||||
JOIN nodes ON nodes.public_key = node_stats.public_key
|
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||||
JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||||
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||||
WHERE geo_names_country.id = ?
|
WHERE geo_names_country.id = ?
|
||||||
@ -193,8 +263,8 @@ class NodesApi {
|
|||||||
public async $getNodesPerISP(ISPId: string) {
|
public async $getNodesPerISP(ISPId: string) {
|
||||||
try {
|
try {
|
||||||
const query = `
|
const query = `
|
||||||
SELECT node_stats.public_key, node_stats.capacity, node_stats.channels, nodes.alias,
|
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||||
UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
||||||
geo_names_city.names as city, geo_names_country.names as country
|
geo_names_city.names as city, geo_names_country.names as country
|
||||||
FROM node_stats
|
FROM node_stats
|
||||||
JOIN (
|
JOIN (
|
||||||
@ -253,6 +323,66 @@ class NodesApi {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save or update a node present in the graph
|
||||||
|
*/
|
||||||
|
public async $saveNode(node: ILightningApi.Node): Promise<void> {
|
||||||
|
try {
|
||||||
|
const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? '';
|
||||||
|
const query = `INSERT INTO nodes(
|
||||||
|
public_key,
|
||||||
|
first_seen,
|
||||||
|
updated_at,
|
||||||
|
alias,
|
||||||
|
color,
|
||||||
|
sockets,
|
||||||
|
status
|
||||||
|
)
|
||||||
|
VALUES (?, NOW(), FROM_UNIXTIME(?), ?, ?, ?, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE updated_at = FROM_UNIXTIME(?), alias = ?, color = ?, sockets = ?, status = 1`;
|
||||||
|
|
||||||
|
await DB.query(query, [
|
||||||
|
node.pub_key,
|
||||||
|
node.last_update,
|
||||||
|
node.alias,
|
||||||
|
node.color,
|
||||||
|
sockets,
|
||||||
|
node.last_update,
|
||||||
|
node.alias,
|
||||||
|
node.color,
|
||||||
|
sockets,
|
||||||
|
]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$saveNode() error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set all nodes not in `nodesPubkeys` as inactive (status = 0)
|
||||||
|
*/
|
||||||
|
public async $setNodesInactive(graphNodesPubkeys: string[]): Promise<void> {
|
||||||
|
if (graphNodesPubkeys.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await DB.query<ResultSetHeader>(`
|
||||||
|
UPDATE nodes
|
||||||
|
SET status = 0
|
||||||
|
WHERE public_key NOT IN (
|
||||||
|
${graphNodesPubkeys.map(pubkey => `"${pubkey}"`).join(',')}
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
if (result[0].changedRows ?? 0 > 0) {
|
||||||
|
logger.info(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$setNodesInactive() error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new NodesApi();
|
export default new NodesApi();
|
||||||
|
@ -35,6 +35,9 @@ class NodesRoutes {
|
|||||||
res.status(404).send('Node not found');
|
res.status(404).send('Node not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(node);
|
res.json(node);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
@ -44,6 +47,9 @@ class NodesRoutes {
|
|||||||
private async $getHistoricalNodeStats(req: Request, res: Response) {
|
private async $getHistoricalNodeStats(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const statistics = await nodesApi.$getNodeStats(req.params.public_key);
|
const statistics = await nodesApi.$getNodeStats(req.params.public_key);
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(statistics);
|
res.json(statistics);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
272
backend/src/api/lightning/clightning/clightning-client.ts
Normal file
272
backend/src/api/lightning/clightning/clightning-client.ts
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
// Imported from https://github.com/shesek/lightning-client-js
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const methods = [
|
||||||
|
'addgossip',
|
||||||
|
'autocleaninvoice',
|
||||||
|
'check',
|
||||||
|
'checkmessage',
|
||||||
|
'close',
|
||||||
|
'connect',
|
||||||
|
'createinvoice',
|
||||||
|
'createinvoicerequest',
|
||||||
|
'createoffer',
|
||||||
|
'createonion',
|
||||||
|
'decode',
|
||||||
|
'decodepay',
|
||||||
|
'delexpiredinvoice',
|
||||||
|
'delinvoice',
|
||||||
|
'delpay',
|
||||||
|
'dev-listaddrs',
|
||||||
|
'dev-rescan-outputs',
|
||||||
|
'disableoffer',
|
||||||
|
'disconnect',
|
||||||
|
'estimatefees',
|
||||||
|
'feerates',
|
||||||
|
'fetchinvoice',
|
||||||
|
'fundchannel',
|
||||||
|
'fundchannel_cancel',
|
||||||
|
'fundchannel_complete',
|
||||||
|
'fundchannel_start',
|
||||||
|
'fundpsbt',
|
||||||
|
'getchaininfo',
|
||||||
|
'getinfo',
|
||||||
|
'getlog',
|
||||||
|
'getrawblockbyheight',
|
||||||
|
'getroute',
|
||||||
|
'getsharedsecret',
|
||||||
|
'getutxout',
|
||||||
|
'help',
|
||||||
|
'invoice',
|
||||||
|
'keysend',
|
||||||
|
'legacypay',
|
||||||
|
'listchannels',
|
||||||
|
'listconfigs',
|
||||||
|
'listforwards',
|
||||||
|
'listfunds',
|
||||||
|
'listinvoices',
|
||||||
|
'listnodes',
|
||||||
|
'listoffers',
|
||||||
|
'listpays',
|
||||||
|
'listpeers',
|
||||||
|
'listsendpays',
|
||||||
|
'listtransactions',
|
||||||
|
'multifundchannel',
|
||||||
|
'multiwithdraw',
|
||||||
|
'newaddr',
|
||||||
|
'notifications',
|
||||||
|
'offer',
|
||||||
|
'offerout',
|
||||||
|
'openchannel_abort',
|
||||||
|
'openchannel_bump',
|
||||||
|
'openchannel_init',
|
||||||
|
'openchannel_signed',
|
||||||
|
'openchannel_update',
|
||||||
|
'pay',
|
||||||
|
'payersign',
|
||||||
|
'paystatus',
|
||||||
|
'ping',
|
||||||
|
'plugin',
|
||||||
|
'reserveinputs',
|
||||||
|
'sendinvoice',
|
||||||
|
'sendonion',
|
||||||
|
'sendonionmessage',
|
||||||
|
'sendpay',
|
||||||
|
'sendpsbt',
|
||||||
|
'sendrawtransaction',
|
||||||
|
'setchannelfee',
|
||||||
|
'signmessage',
|
||||||
|
'signpsbt',
|
||||||
|
'stop',
|
||||||
|
'txdiscard',
|
||||||
|
'txprepare',
|
||||||
|
'txsend',
|
||||||
|
'unreserveinputs',
|
||||||
|
'utxopsbt',
|
||||||
|
'waitanyinvoice',
|
||||||
|
'waitblockheight',
|
||||||
|
'waitinvoice',
|
||||||
|
'waitsendpay',
|
||||||
|
'withdraw'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
import EventEmitter from 'events';
|
||||||
|
import { existsSync, statSync } from 'fs';
|
||||||
|
import { createConnection, Socket } from 'net';
|
||||||
|
import { homedir } from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import { createInterface, Interface } from 'readline';
|
||||||
|
import logger from '../../../logger';
|
||||||
|
import { AbstractLightningApi } from '../lightning-api-abstract-factory';
|
||||||
|
import { ILightningApi } from '../lightning-api.interface';
|
||||||
|
import { convertAndmergeBidirectionalChannels, convertNode } from './clightning-convert';
|
||||||
|
|
||||||
|
class LightningError extends Error {
|
||||||
|
type: string = 'lightning';
|
||||||
|
message: string = 'lightning-client error';
|
||||||
|
|
||||||
|
constructor(error) {
|
||||||
|
super();
|
||||||
|
this.type = error.type;
|
||||||
|
this.message = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultRpcPath = path.join(homedir(), '.lightning')
|
||||||
|
, fStat = (...p) => statSync(path.join(...p))
|
||||||
|
, fExists = (...p) => existsSync(path.join(...p))
|
||||||
|
|
||||||
|
export default class CLightningClient extends EventEmitter implements AbstractLightningApi {
|
||||||
|
private rpcPath: string;
|
||||||
|
private reconnectWait: number;
|
||||||
|
private reconnectTimeout;
|
||||||
|
private reqcount: number;
|
||||||
|
private client: Socket;
|
||||||
|
private rl: Interface;
|
||||||
|
private clientConnectionPromise: Promise<unknown>;
|
||||||
|
|
||||||
|
constructor(rpcPath = defaultRpcPath) {
|
||||||
|
if (!path.isAbsolute(rpcPath)) {
|
||||||
|
throw new Error('The rpcPath must be an absolute path');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fExists(rpcPath) || !fStat(rpcPath).isSocket()) {
|
||||||
|
// network directory provided, use the lightning-rpc within in
|
||||||
|
if (fExists(rpcPath, 'lightning-rpc')) {
|
||||||
|
rpcPath = path.join(rpcPath, 'lightning-rpc');
|
||||||
|
}
|
||||||
|
|
||||||
|
// main data directory provided, default to using the bitcoin mainnet subdirectory
|
||||||
|
// to be removed in v0.2.0
|
||||||
|
else if (fExists(rpcPath, 'bitcoin', 'lightning-rpc')) {
|
||||||
|
logger.warn(`[CLightningClient] ${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`)
|
||||||
|
logger.warn(`[CLightningClient] specifying the main lightning data directory is deprecated, please specify the network directory explicitly.\n`)
|
||||||
|
rpcPath = path.join(rpcPath, 'bitcoin', 'lightning-rpc')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`[CLightningClient] Connecting to ${rpcPath}`);
|
||||||
|
|
||||||
|
super();
|
||||||
|
this.rpcPath = rpcPath;
|
||||||
|
this.reconnectWait = 0.5;
|
||||||
|
this.reconnectTimeout = null;
|
||||||
|
this.reqcount = 0;
|
||||||
|
|
||||||
|
const _self = this;
|
||||||
|
|
||||||
|
this.client = createConnection(rpcPath).on(
|
||||||
|
'error', () => {
|
||||||
|
_self.increaseWaitTime();
|
||||||
|
_self.reconnect();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.rl = createInterface({ input: this.client }).on(
|
||||||
|
'error', () => {
|
||||||
|
_self.increaseWaitTime();
|
||||||
|
_self.reconnect();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.clientConnectionPromise = new Promise<void>(resolve => {
|
||||||
|
_self.client.on('connect', () => {
|
||||||
|
logger.info(`[CLightningClient] Lightning client connected`);
|
||||||
|
_self.reconnectWait = 1;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
_self.client.on('end', () => {
|
||||||
|
logger.err('[CLightningClient] Lightning client connection closed, reconnecting');
|
||||||
|
_self.increaseWaitTime();
|
||||||
|
_self.reconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
_self.client.on('error', error => {
|
||||||
|
logger.err(`[CLightningClient] Lightning client connection error: ${error}`);
|
||||||
|
_self.increaseWaitTime();
|
||||||
|
_self.reconnect();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rl.on('line', line => {
|
||||||
|
line = line.trim();
|
||||||
|
if (!line) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = JSON.parse(line);
|
||||||
|
// logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`);
|
||||||
|
_self.emit('res:' + data.id, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
increaseWaitTime(): void {
|
||||||
|
if (this.reconnectWait >= 16) {
|
||||||
|
this.reconnectWait = 16;
|
||||||
|
} else {
|
||||||
|
this.reconnectWait *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnect(): void {
|
||||||
|
const _self = this;
|
||||||
|
|
||||||
|
if (this.reconnectTimeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnectTimeout = setTimeout(() => {
|
||||||
|
logger.debug('[CLightningClient] Trying to reconnect...');
|
||||||
|
|
||||||
|
_self.client.connect(_self.rpcPath);
|
||||||
|
_self.reconnectTimeout = null;
|
||||||
|
}, this.reconnectWait * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
call(method, args = []): Promise<any> {
|
||||||
|
const _self = this;
|
||||||
|
|
||||||
|
const callInt = ++this.reqcount;
|
||||||
|
const sendObj = {
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method,
|
||||||
|
params: args,
|
||||||
|
id: '' + callInt
|
||||||
|
};
|
||||||
|
|
||||||
|
// logger.debug(`[CLightningClient] #${callInt} --> ${method} ${args}`);
|
||||||
|
|
||||||
|
// Wait for the client to connect
|
||||||
|
return this.clientConnectionPromise
|
||||||
|
.then(() => new Promise((resolve, reject) => {
|
||||||
|
// Wait for a response
|
||||||
|
this.once('res:' + callInt, res => res.error == null
|
||||||
|
? resolve(res.result)
|
||||||
|
: reject(new LightningError(res.error))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send the command
|
||||||
|
_self.client.write(JSON.stringify(sendObj));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
|
||||||
|
const listnodes: any[] = await this.call('listnodes');
|
||||||
|
const listchannels: any[] = await this.call('listchannels');
|
||||||
|
const channelsList = await convertAndmergeBidirectionalChannels(listchannels['channels']);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: listnodes['nodes'].map(node => convertNode(node)),
|
||||||
|
edges: channelsList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const protify = s => s.replace(/-([a-z])/g, m => m[1].toUpperCase());
|
||||||
|
|
||||||
|
methods.forEach(k => {
|
||||||
|
CLightningClient.prototype[protify(k)] = function (...args: any) {
|
||||||
|
return this.call(k, args);
|
||||||
|
};
|
||||||
|
});
|
138
backend/src/api/lightning/clightning/clightning-convert.ts
Normal file
138
backend/src/api/lightning/clightning/clightning-convert.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { ILightningApi } from '../lightning-api.interface';
|
||||||
|
import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher';
|
||||||
|
import logger from '../../../logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a clightning "listnode" entry to a lnd node entry
|
||||||
|
*/
|
||||||
|
export function convertNode(clNode: any): ILightningApi.Node {
|
||||||
|
return {
|
||||||
|
alias: clNode.alias ?? '',
|
||||||
|
color: `#${clNode.color ?? ''}`,
|
||||||
|
features: [], // TODO parse and return clNode.feature
|
||||||
|
pub_key: clNode.nodeid,
|
||||||
|
addresses: clNode.addresses?.map((addr) => {
|
||||||
|
return {
|
||||||
|
network: addr.type,
|
||||||
|
addr: `${addr.address}:${addr.port}`
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
last_update: clNode?.last_timestamp ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert clightning "listchannels" response to lnd "describegraph.edges" format
|
||||||
|
*/
|
||||||
|
export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise<ILightningApi.Channel[]> {
|
||||||
|
logger.info('Converting clightning nodes and channels to lnd graph format');
|
||||||
|
|
||||||
|
let loggerTimer = new Date().getTime() / 1000;
|
||||||
|
let channelProcessed = 0;
|
||||||
|
|
||||||
|
const consolidatedChannelList: ILightningApi.Channel[] = [];
|
||||||
|
const clChannelsDict = {};
|
||||||
|
const clChannelsDictCount = {};
|
||||||
|
|
||||||
|
for (const clChannel of clChannels) {
|
||||||
|
if (!clChannelsDict[clChannel.short_channel_id]) {
|
||||||
|
clChannelsDict[clChannel.short_channel_id] = clChannel;
|
||||||
|
clChannelsDictCount[clChannel.short_channel_id] = 1;
|
||||||
|
} else {
|
||||||
|
consolidatedChannelList.push(
|
||||||
|
await buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id])
|
||||||
|
);
|
||||||
|
delete clChannelsDict[clChannel.short_channel_id];
|
||||||
|
clChannelsDictCount[clChannel.short_channel_id]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Building complete channels from clightning output. Channels processed: ${channelProcessed + 1} of ${clChannels.length}`);
|
||||||
|
loggerTimer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
++channelProcessed;
|
||||||
|
}
|
||||||
|
|
||||||
|
channelProcessed = 0;
|
||||||
|
const keys = Object.keys(clChannelsDict);
|
||||||
|
for (const short_channel_id of keys) {
|
||||||
|
consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id]));
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`);
|
||||||
|
loggerTimer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return consolidatedChannelList;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertChannelId(channelId): string {
|
||||||
|
if (channelId.indexOf('/') !== -1) {
|
||||||
|
channelId = channelId.slice(0, -2);
|
||||||
|
}
|
||||||
|
const s = channelId.split('x').map(part => BigInt(part));
|
||||||
|
return ((s[0] << 40n) | (s[1] << 16n) | s[2]).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format
|
||||||
|
* In this case, clightning knows the channel policy for both nodes
|
||||||
|
*/
|
||||||
|
async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILightningApi.Channel> {
|
||||||
|
const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0);
|
||||||
|
|
||||||
|
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannelA.short_channel_id);
|
||||||
|
const parts = clChannelA.short_channel_id.split('x');
|
||||||
|
const outputIdx = parts[2];
|
||||||
|
|
||||||
|
return {
|
||||||
|
channel_id: clChannelA.short_channel_id,
|
||||||
|
capacity: clChannelA.satoshis,
|
||||||
|
last_update: lastUpdate,
|
||||||
|
node1_policy: convertPolicy(clChannelA),
|
||||||
|
node2_policy: convertPolicy(clChannelB),
|
||||||
|
chan_point: `${tx.txid}:${outputIdx}`,
|
||||||
|
node1_pub: clChannelA.source,
|
||||||
|
node2_pub: clChannelB.source,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format
|
||||||
|
* In this case, clightning knows the channel policy of only one node
|
||||||
|
*/
|
||||||
|
async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel> {
|
||||||
|
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id);
|
||||||
|
const parts = clChannel.short_channel_id.split('x');
|
||||||
|
const outputIdx = parts[2];
|
||||||
|
|
||||||
|
return {
|
||||||
|
channel_id: clChannel.short_channel_id,
|
||||||
|
capacity: clChannel.satoshis,
|
||||||
|
last_update: clChannel.last_update ?? 0,
|
||||||
|
node1_policy: convertPolicy(clChannel),
|
||||||
|
node2_policy: null,
|
||||||
|
chan_point: `${tx.txid}:${outputIdx}`,
|
||||||
|
node1_pub: clChannel.source,
|
||||||
|
node2_pub: clChannel.destination,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a clightning "listnode" response to a lnd channel policy format
|
||||||
|
*/
|
||||||
|
function convertPolicy(clChannel: any): ILightningApi.RoutingPolicy {
|
||||||
|
return {
|
||||||
|
time_lock_delta: 0, // TODO
|
||||||
|
min_htlc: clChannel.htlc_minimum_msat.slice(0, -4),
|
||||||
|
max_htlc_msat: clChannel.htlc_maximum_msat.slice(0, -4),
|
||||||
|
fee_base_msat: clChannel.base_fee_millisatoshi,
|
||||||
|
fee_rate_milli_msat: clChannel.fee_per_millionth,
|
||||||
|
disabled: !clChannel.active,
|
||||||
|
last_update: clChannel.last_update ?? 0,
|
||||||
|
};
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
import { ILightningApi } from './lightning-api.interface';
|
import { ILightningApi } from './lightning-api.interface';
|
||||||
|
|
||||||
export interface AbstractLightningApi {
|
export interface AbstractLightningApi {
|
||||||
$getNetworkInfo(): Promise<ILightningApi.NetworkInfo>;
|
|
||||||
$getNetworkGraph(): Promise<ILightningApi.NetworkGraph>;
|
$getNetworkGraph(): Promise<ILightningApi.NetworkGraph>;
|
||||||
$getInfo(): Promise<ILightningApi.Info>;
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
|
import CLightningClient from './clightning/clightning-client';
|
||||||
import { AbstractLightningApi } from './lightning-api-abstract-factory';
|
import { AbstractLightningApi } from './lightning-api-abstract-factory';
|
||||||
import LndApi from './lnd/lnd-api';
|
import LndApi from './lnd/lnd-api';
|
||||||
|
|
||||||
function lightningApiFactory(): AbstractLightningApi {
|
function lightningApiFactory(): AbstractLightningApi {
|
||||||
switch (config.LIGHTNING.BACKEND) {
|
switch (config.LIGHTNING.ENABLED === true && config.LIGHTNING.BACKEND) {
|
||||||
|
case 'cln':
|
||||||
|
return new CLightningClient(config.CLIGHTNING.SOCKET);
|
||||||
case 'lnd':
|
case 'lnd':
|
||||||
default:
|
default:
|
||||||
return new LndApi();
|
return new LndApi();
|
||||||
|
@ -1,71 +1,85 @@
|
|||||||
export namespace ILightningApi {
|
export namespace ILightningApi {
|
||||||
export interface NetworkInfo {
|
export interface NetworkInfo {
|
||||||
average_channel_size: number;
|
graph_diameter: number;
|
||||||
channel_count: number;
|
avg_out_degree: number;
|
||||||
max_channel_size: number;
|
max_out_degree: number;
|
||||||
median_channel_size: number;
|
num_nodes: number;
|
||||||
min_channel_size: number;
|
num_channels: number;
|
||||||
node_count: number;
|
total_network_capacity: string;
|
||||||
not_recently_updated_policy_count: number;
|
avg_channel_size: number;
|
||||||
total_capacity: number;
|
min_channel_size: string;
|
||||||
|
max_channel_size: string;
|
||||||
|
median_channel_size_sat: string;
|
||||||
|
num_zombie_chans: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NetworkGraph {
|
export interface NetworkGraph {
|
||||||
channels: Channel[];
|
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
|
edges: Channel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Channel {
|
export interface Channel {
|
||||||
id: string;
|
channel_id: string;
|
||||||
capacity: number;
|
chan_point: string;
|
||||||
policies: Policy[];
|
last_update: number;
|
||||||
transaction_id: string;
|
node1_pub: string;
|
||||||
transaction_vout: number;
|
node2_pub: string;
|
||||||
updated_at?: string;
|
capacity: string;
|
||||||
|
node1_policy: RoutingPolicy | null;
|
||||||
|
node2_policy: RoutingPolicy | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Policy {
|
export interface RoutingPolicy {
|
||||||
public_key: string;
|
time_lock_delta: number;
|
||||||
base_fee_mtokens?: string;
|
min_htlc: string;
|
||||||
cltv_delta?: number;
|
fee_base_msat: string;
|
||||||
fee_rate?: number;
|
fee_rate_milli_msat: string;
|
||||||
is_disabled?: boolean;
|
disabled: boolean;
|
||||||
max_htlc_mtokens?: string;
|
max_htlc_msat: string;
|
||||||
min_htlc_mtokens?: string;
|
last_update: number;
|
||||||
updated_at?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Node {
|
export interface Node {
|
||||||
|
last_update: number;
|
||||||
|
pub_key: string;
|
||||||
alias: string;
|
alias: string;
|
||||||
|
addresses: {
|
||||||
|
network: string;
|
||||||
|
addr: string;
|
||||||
|
}[];
|
||||||
color: string;
|
color: string;
|
||||||
features: Feature[];
|
features: { [key: number]: Feature };
|
||||||
public_key: string;
|
|
||||||
sockets: string[];
|
|
||||||
updated_at?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Info {
|
export interface Info {
|
||||||
chains: string[];
|
identity_pubkey: string;
|
||||||
color: string;
|
|
||||||
active_channels_count: number;
|
|
||||||
alias: string;
|
alias: string;
|
||||||
current_block_hash: string;
|
num_pending_channels: number;
|
||||||
current_block_height: number;
|
num_active_channels: number;
|
||||||
features: Feature[];
|
num_peers: number;
|
||||||
is_synced_to_chain: boolean;
|
block_height: number;
|
||||||
is_synced_to_graph: boolean;
|
block_hash: string;
|
||||||
latest_block_at: string;
|
synced_to_chain: boolean;
|
||||||
peers_count: number;
|
testnet: boolean;
|
||||||
pending_channels_count: number;
|
uris: string[];
|
||||||
public_key: string;
|
best_header_timestamp: string;
|
||||||
uris: any[];
|
|
||||||
version: string;
|
version: string;
|
||||||
|
num_inactive_channels: number;
|
||||||
|
chains: {
|
||||||
|
chain: string;
|
||||||
|
network: string;
|
||||||
|
}[];
|
||||||
|
color: string;
|
||||||
|
synced_to_graph: boolean;
|
||||||
|
features: { [key: number]: Feature };
|
||||||
|
commit_hash: string;
|
||||||
|
/** Available on LND since v0.15.0-beta */
|
||||||
|
require_htlc_interceptor?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Feature {
|
export interface Feature {
|
||||||
bit: number;
|
name: string;
|
||||||
is_known: boolean;
|
|
||||||
is_required: boolean;
|
is_required: boolean;
|
||||||
type?: string;
|
is_known: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,44 +1,40 @@
|
|||||||
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
import { Agent } from 'https';
|
||||||
|
import * as fs from 'fs';
|
||||||
import { AbstractLightningApi } from '../lightning-api-abstract-factory';
|
import { AbstractLightningApi } from '../lightning-api-abstract-factory';
|
||||||
import { ILightningApi } from '../lightning-api.interface';
|
import { ILightningApi } from '../lightning-api.interface';
|
||||||
import * as fs from 'fs';
|
|
||||||
import { authenticatedLndGrpc, getWalletInfo, getNetworkGraph, getNetworkInfo } from 'lightning';
|
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import logger from '../../../logger';
|
|
||||||
|
|
||||||
class LndApi implements AbstractLightningApi {
|
class LndApi implements AbstractLightningApi {
|
||||||
private lnd: any;
|
axiosConfig: AxiosRequestConfig = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!config.LIGHTNING.ENABLED) {
|
if (config.LIGHTNING.ENABLED) {
|
||||||
return;
|
this.axiosConfig = {
|
||||||
}
|
headers: {
|
||||||
try {
|
'Grpc-Metadata-macaroon': fs.readFileSync(config.LND.MACAROON_PATH).toString('hex')
|
||||||
const tls = fs.readFileSync(config.LND.TLS_CERT_PATH).toString('base64');
|
},
|
||||||
const macaroon = fs.readFileSync(config.LND.MACAROON_PATH).toString('base64');
|
httpsAgent: new Agent({
|
||||||
|
ca: fs.readFileSync(config.LND.TLS_CERT_PATH)
|
||||||
const { lnd } = authenticatedLndGrpc({
|
}),
|
||||||
cert: tls,
|
timeout: 10000
|
||||||
macaroon: macaroon,
|
};
|
||||||
socket: config.LND.SOCKET,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.lnd = lnd;
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('Could not initiate the LND service handler: ' + (e instanceof Error ? e.message : e));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async $getNetworkInfo(): Promise<ILightningApi.NetworkInfo> {
|
async $getNetworkInfo(): Promise<ILightningApi.NetworkInfo> {
|
||||||
return await getNetworkInfo({ lnd: this.lnd });
|
return axios.get<ILightningApi.NetworkInfo>(config.LND.REST_API_URL + '/v1/graph/info', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $getInfo(): Promise<ILightningApi.Info> {
|
async $getInfo(): Promise<ILightningApi.Info> {
|
||||||
// @ts-ignore
|
return axios.get<ILightningApi.Info>(config.LND.REST_API_URL + '/v1/getinfo', this.axiosConfig)
|
||||||
return await getWalletInfo({ lnd: this.lnd });
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
|
async $getNetworkGraph(): Promise<ILightningApi.NetworkGraph> {
|
||||||
return await getNetworkGraph({ lnd: this.lnd });
|
return axios.get<ILightningApi.NetworkGraph>(config.LND.REST_API_URL + '/v1/graph', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -473,7 +473,7 @@ class Mining {
|
|||||||
|
|
||||||
for (const block of blocksWithoutPrices) {
|
for (const block of blocksWithoutPrices) {
|
||||||
// Quick optimisation, out mtgox feed only goes back to 2010-07-19 02:00:00, so skip the first 68951 blocks
|
// Quick optimisation, out mtgox feed only goes back to 2010-07-19 02:00:00, so skip the first 68951 blocks
|
||||||
if (block.height < 68951) {
|
if (['mainnet', 'testnet'].includes(config.MEMPOOL.NETWORK) && block.height < 68951) {
|
||||||
blocksPrices.push({
|
blocksPrices.push({
|
||||||
height: block.height,
|
height: block.height,
|
||||||
priceId: prices[0].id,
|
priceId: prices[0].id,
|
||||||
@ -492,11 +492,11 @@ class Mining {
|
|||||||
|
|
||||||
if (blocksPrices.length >= 100000) {
|
if (blocksPrices.length >= 100000) {
|
||||||
totalInserted += blocksPrices.length;
|
totalInserted += blocksPrices.length;
|
||||||
|
let logStr = `Linking ${blocksPrices.length} blocks to their closest price`;
|
||||||
if (blocksWithoutPrices.length > 200000) {
|
if (blocksWithoutPrices.length > 200000) {
|
||||||
logger.debug(`Linking ${blocksPrices.length} newly indexed blocks to their closest price | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`);
|
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
|
||||||
} else {
|
|
||||||
logger.debug(`Linking ${blocksPrices.length} newly indexed blocks to their closest price`);
|
|
||||||
}
|
}
|
||||||
|
logger.debug(logStr);
|
||||||
await BlocksRepository.$saveBlockPrices(blocksPrices);
|
await BlocksRepository.$saveBlockPrices(blocksPrices);
|
||||||
blocksPrices.length = 0;
|
blocksPrices.length = 0;
|
||||||
}
|
}
|
||||||
@ -504,11 +504,11 @@ class Mining {
|
|||||||
|
|
||||||
if (blocksPrices.length > 0) {
|
if (blocksPrices.length > 0) {
|
||||||
totalInserted += blocksPrices.length;
|
totalInserted += blocksPrices.length;
|
||||||
|
let logStr = `Linking ${blocksPrices.length} blocks to their closest price`;
|
||||||
if (blocksWithoutPrices.length > 200000) {
|
if (blocksWithoutPrices.length > 200000) {
|
||||||
logger.debug(`Linking ${blocksPrices.length} newly indexed blocks to their closest price | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`);
|
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
|
||||||
} else {
|
|
||||||
logger.debug(`Linking ${blocksPrices.length} newly indexed blocks to their closest price`);
|
|
||||||
}
|
}
|
||||||
|
logger.debug(logStr);
|
||||||
await BlocksRepository.$saveBlockPrices(blocksPrices);
|
await BlocksRepository.$saveBlockPrices(blocksPrices);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -31,10 +31,16 @@ interface IConfig {
|
|||||||
LIGHTNING: {
|
LIGHTNING: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
BACKEND: 'lnd' | 'cln' | 'ldk';
|
BACKEND: 'lnd' | 'cln' | 'ldk';
|
||||||
|
TOPOLOGY_FOLDER: string;
|
||||||
|
STATS_REFRESH_INTERVAL: number;
|
||||||
|
GRAPH_REFRESH_INTERVAL: number;
|
||||||
};
|
};
|
||||||
LND: {
|
LND: {
|
||||||
TLS_CERT_PATH: string;
|
TLS_CERT_PATH: string;
|
||||||
MACAROON_PATH: string;
|
MACAROON_PATH: string;
|
||||||
|
REST_API_URL: string;
|
||||||
|
};
|
||||||
|
CLIGHTNING: {
|
||||||
SOCKET: string;
|
SOCKET: string;
|
||||||
};
|
};
|
||||||
ELECTRUM: {
|
ELECTRUM: {
|
||||||
@ -177,12 +183,18 @@ const defaults: IConfig = {
|
|||||||
},
|
},
|
||||||
'LIGHTNING': {
|
'LIGHTNING': {
|
||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
'BACKEND': 'lnd'
|
'BACKEND': 'lnd',
|
||||||
|
'TOPOLOGY_FOLDER': '',
|
||||||
|
'STATS_REFRESH_INTERVAL': 600,
|
||||||
|
'GRAPH_REFRESH_INTERVAL': 600,
|
||||||
},
|
},
|
||||||
'LND': {
|
'LND': {
|
||||||
'TLS_CERT_PATH': '',
|
'TLS_CERT_PATH': '',
|
||||||
'MACAROON_PATH': '',
|
'MACAROON_PATH': '',
|
||||||
'SOCKET': 'localhost:10009',
|
'REST_API_URL': 'https://localhost:8080',
|
||||||
|
},
|
||||||
|
'CLIGHTNING': {
|
||||||
|
'SOCKET': '',
|
||||||
},
|
},
|
||||||
'SOCKS5PROXY': {
|
'SOCKS5PROXY': {
|
||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
@ -224,6 +236,7 @@ class Config implements IConfig {
|
|||||||
BISQ: IConfig['BISQ'];
|
BISQ: IConfig['BISQ'];
|
||||||
LIGHTNING: IConfig['LIGHTNING'];
|
LIGHTNING: IConfig['LIGHTNING'];
|
||||||
LND: IConfig['LND'];
|
LND: IConfig['LND'];
|
||||||
|
CLIGHTNING: IConfig['CLIGHTNING'];
|
||||||
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
|
SOCKS5PROXY: IConfig['SOCKS5PROXY'];
|
||||||
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
|
||||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||||
@ -242,6 +255,7 @@ class Config implements IConfig {
|
|||||||
this.BISQ = configs.BISQ;
|
this.BISQ = configs.BISQ;
|
||||||
this.LIGHTNING = configs.LIGHTNING;
|
this.LIGHTNING = configs.LIGHTNING;
|
||||||
this.LND = configs.LND;
|
this.LND = configs.LND;
|
||||||
|
this.CLIGHTNING = configs.CLIGHTNING;
|
||||||
this.SOCKS5PROXY = configs.SOCKS5PROXY;
|
this.SOCKS5PROXY = configs.SOCKS5PROXY;
|
||||||
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
|
||||||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import config from './config';
|
import config from './config';
|
||||||
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
|
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
import { PoolOptions } from 'mysql2/typings/mysql';
|
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
|
||||||
|
|
||||||
class DB {
|
class DB {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -28,7 +28,9 @@ import { PoolOptions } from 'mysql2/typings/mysql';
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async query(query, params?) {
|
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
|
||||||
|
OkPacket[] | ResultSetHeader>(query, params?): Promise<[T, FieldPacket[]]>
|
||||||
|
{
|
||||||
this.checkDBFlag();
|
this.checkDBFlag();
|
||||||
const pool = await this.getPool();
|
const pool = await this.getPool();
|
||||||
return pool.query(query, params);
|
return pool.query(query, params);
|
||||||
|
@ -28,12 +28,13 @@ import nodesRoutes from './api/explorer/nodes.routes';
|
|||||||
import channelsRoutes from './api/explorer/channels.routes';
|
import channelsRoutes from './api/explorer/channels.routes';
|
||||||
import generalLightningRoutes from './api/explorer/general.routes';
|
import generalLightningRoutes from './api/explorer/general.routes';
|
||||||
import lightningStatsUpdater from './tasks/lightning/stats-updater.service';
|
import lightningStatsUpdater from './tasks/lightning/stats-updater.service';
|
||||||
import nodeSyncService from './tasks/lightning/node-sync.service';
|
import networkSyncService from './tasks/lightning/network-sync.service';
|
||||||
import statisticsRoutes from "./api/statistics/statistics.routes";
|
import statisticsRoutes from './api/statistics/statistics.routes';
|
||||||
import miningRoutes from "./api/mining/mining-routes";
|
import miningRoutes from './api/mining/mining-routes';
|
||||||
import bisqRoutes from "./api/bisq/bisq.routes";
|
import bisqRoutes from './api/bisq/bisq.routes';
|
||||||
import liquidRoutes from "./api/liquid/liquid.routes";
|
import liquidRoutes from './api/liquid/liquid.routes';
|
||||||
import bitcoinRoutes from "./api/bitcoin/bitcoin.routes";
|
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
|
||||||
|
import fundingTxFetcher from "./tasks/lightning/sync-tasks/funding-tx-fetcher";
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@ -136,8 +137,7 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.LIGHTNING.ENABLED) {
|
if (config.LIGHTNING.ENABLED) {
|
||||||
nodeSyncService.$startService()
|
this.$runLightningBackend();
|
||||||
.then(() => lightningStatsUpdater.$startService());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
|
this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
|
||||||
@ -183,6 +183,18 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $runLightningBackend() {
|
||||||
|
try {
|
||||||
|
await fundingTxFetcher.$init();
|
||||||
|
await networkSyncService.$startService();
|
||||||
|
await lightningStatsUpdater.$startService();
|
||||||
|
} catch(e) {
|
||||||
|
logger.err(`Lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
await Common.sleep$(1000 * 60);
|
||||||
|
this.$runLightningBackend();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
setUpWebsocketHandling() {
|
setUpWebsocketHandling() {
|
||||||
if (this.wss) {
|
if (this.wss) {
|
||||||
websocketHandler.setWebsocketServer(this.wss);
|
websocketHandler.setWebsocketServer(this.wss);
|
||||||
|
@ -6,13 +6,12 @@ import logger from './logger';
|
|||||||
import HashratesRepository from './repositories/HashratesRepository';
|
import HashratesRepository from './repositories/HashratesRepository';
|
||||||
import bitcoinClient from './api/bitcoin/bitcoin-client';
|
import bitcoinClient from './api/bitcoin/bitcoin-client';
|
||||||
import priceUpdater from './tasks/price-updater';
|
import priceUpdater from './tasks/price-updater';
|
||||||
|
import PricesRepository from './repositories/PricesRepository';
|
||||||
|
|
||||||
class Indexer {
|
class Indexer {
|
||||||
runIndexer = true;
|
runIndexer = true;
|
||||||
indexerRunning = false;
|
indexerRunning = false;
|
||||||
|
tasksRunning: string[] = [];
|
||||||
constructor() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public reindex() {
|
public reindex() {
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
@ -20,6 +19,28 @@ class Indexer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async runSingleTask(task: 'blocksPrices') {
|
||||||
|
if (!Common.indexingEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task === 'blocksPrices' && !this.tasksRunning.includes(task)) {
|
||||||
|
this.tasksRunning.push(task);
|
||||||
|
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||||
|
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
|
||||||
|
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask != task)
|
||||||
|
this.runSingleTask('blocksPrices');
|
||||||
|
}, 10000);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Blocks prices indexer will run now`)
|
||||||
|
await mining.$indexBlockPrices();
|
||||||
|
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask != task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async $run() {
|
public async $run() {
|
||||||
if (!Common.indexingEnabled() || this.runIndexer === false ||
|
if (!Common.indexingEnabled() || this.runIndexer === false ||
|
||||||
this.indexerRunning === true || mempool.hasPriority()
|
this.indexerRunning === true || mempool.hasPriority()
|
||||||
@ -50,7 +71,7 @@ class Indexer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await mining.$indexBlockPrices();
|
this.runSingleTask('blocksPrices');
|
||||||
await mining.$indexDifficultyAdjustments();
|
await mining.$indexDifficultyAdjustments();
|
||||||
await this.$resetHashratesIndexingState(); // TODO - Remove this as it's not efficient
|
await this.$resetHashratesIndexingState(); // TODO - Remove this as it's not efficient
|
||||||
await mining.$generateNetworkHashrateHistory();
|
await mining.$generateNetworkHashrateHistory();
|
||||||
|
@ -27,6 +27,11 @@ class PricesRepository {
|
|||||||
return oldestRow[0] ? oldestRow[0].time : 0;
|
return oldestRow[0] ? oldestRow[0].time : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getLatestPriceId(): Promise<number | null> {
|
||||||
|
const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
|
||||||
|
return oldestRow[0] ? oldestRow[0].id : null;
|
||||||
|
}
|
||||||
|
|
||||||
public async $getLatestPriceTime(): Promise<number> {
|
public async $getLatestPriceTime(): Promise<number> {
|
||||||
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
|
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
|
||||||
return oldestRow[0] ? oldestRow[0].time : 0;
|
return oldestRow[0] ? oldestRow[0].time : 0;
|
||||||
|
@ -1,53 +1,43 @@
|
|||||||
import { chanNumber } from 'bolt07';
|
|
||||||
import DB from '../../database';
|
import DB from '../../database';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import channelsApi from '../../api/explorer/channels.api';
|
import channelsApi from '../../api/explorer/channels.api';
|
||||||
import bitcoinClient from '../../api/bitcoin/bitcoin-client';
|
|
||||||
import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
|
import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
|
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
|
||||||
import lightningApi from '../../api/lightning/lightning-api-factory';
|
|
||||||
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
|
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
|
||||||
import { $lookupNodeLocation } from './sync-tasks/node-locations';
|
import { $lookupNodeLocation } from './sync-tasks/node-locations';
|
||||||
|
import lightningApi from '../../api/lightning/lightning-api-factory';
|
||||||
|
import nodesApi from '../../api/explorer/nodes.api';
|
||||||
|
import { ResultSetHeader } from 'mysql2';
|
||||||
|
import fundingTxFetcher from './sync-tasks/funding-tx-fetcher';
|
||||||
|
|
||||||
|
class NetworkSyncService {
|
||||||
|
loggerTimer = 0;
|
||||||
|
|
||||||
class NodeSyncService {
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
public async $startService() {
|
public async $startService(): Promise<void> {
|
||||||
logger.info('Starting node sync service');
|
logger.info('Starting lightning network sync service');
|
||||||
|
|
||||||
await this.$runUpdater();
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
|
||||||
setInterval(async () => {
|
await this.$runTasks();
|
||||||
await this.$runUpdater();
|
|
||||||
}, 1000 * 60 * 60);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $runUpdater() {
|
private async $runTasks(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.info(`Updating nodes and channels...`);
|
logger.info(`Updating nodes and channels`);
|
||||||
|
|
||||||
const networkGraph = await lightningApi.$getNetworkGraph();
|
const networkGraph = await lightningApi.$getNetworkGraph();
|
||||||
|
if (networkGraph.nodes.length === 0 || networkGraph.edges.length === 0) {
|
||||||
for (const node of networkGraph.nodes) {
|
logger.info(`LN Network graph is empty, retrying in 10 seconds`);
|
||||||
await this.$saveNode(node);
|
setTimeout(() => { this.$runTasks(); }, 10000);
|
||||||
}
|
return;
|
||||||
logger.info(`Nodes updated.`);
|
|
||||||
|
|
||||||
if (config.MAXMIND.ENABLED) {
|
|
||||||
await $lookupNodeLocation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const graphChannelsIds: string[] = [];
|
await this.$updateNodesList(networkGraph.nodes);
|
||||||
for (const channel of networkGraph.channels) {
|
await this.$updateChannelsList(networkGraph.edges);
|
||||||
await this.$saveChannel(channel);
|
await this.$deactivateChannelsWithoutActiveNodes();
|
||||||
graphChannelsIds.push(channel.id);
|
|
||||||
}
|
|
||||||
await this.$setChannelsInactive(graphChannelsIds);
|
|
||||||
|
|
||||||
logger.info(`Channels updated.`);
|
|
||||||
|
|
||||||
await this.$findInactiveNodesAndChannels();
|
|
||||||
await this.$lookUpCreationDateFromChain();
|
await this.$lookUpCreationDateFromChain();
|
||||||
await this.$updateNodeFirstSeen();
|
await this.$updateNodeFirstSeen();
|
||||||
await this.$scanForClosedChannels();
|
await this.$scanForClosedChannels();
|
||||||
@ -56,85 +46,183 @@ class NodeSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the `nodes` table to reflect the current network graph state
|
||||||
|
*/
|
||||||
|
private async $updateNodesList(nodes: ILightningApi.Node[]): Promise<void> {
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
const graphNodesPubkeys: string[] = [];
|
||||||
|
for (const node of nodes) {
|
||||||
|
await nodesApi.$saveNode(node);
|
||||||
|
graphNodesPubkeys.push(node.pub_key);
|
||||||
|
++progress;
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Updating node ${progress}/${nodes.length}`);
|
||||||
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info(`${progress} nodes updated`);
|
||||||
|
|
||||||
|
// If a channel if not present in the graph, mark it as inactive
|
||||||
|
nodesApi.$setNodesInactive(graphNodesPubkeys);
|
||||||
|
|
||||||
|
if (config.MAXMIND.ENABLED) {
|
||||||
|
$lookupNodeLocation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the `channels` table to reflect the current network graph state
|
||||||
|
*/
|
||||||
|
private async $updateChannelsList(channels: ILightningApi.Channel[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
const graphChannelsIds: string[] = [];
|
||||||
|
for (const channel of channels) {
|
||||||
|
await channelsApi.$saveChannel(channel);
|
||||||
|
graphChannelsIds.push(channel.channel_id);
|
||||||
|
++progress;
|
||||||
|
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Updating channel ${progress}/${channels.length}`);
|
||||||
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`${progress} channels updated`);
|
||||||
|
|
||||||
|
// If a channel if not present in the graph, mark it as inactive
|
||||||
|
channelsApi.$setChannelsInactive(graphChannelsIds);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot update channel list. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.STATS_REFRESH_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method look up the creation date of the earliest channel of the node
|
// This method look up the creation date of the earliest channel of the node
|
||||||
// and update the node to that date in order to get the earliest first seen date
|
// and update the node to that date in order to get the earliest first seen date
|
||||||
private async $updateNodeFirstSeen() {
|
private async $updateNodeFirstSeen(): Promise<void> {
|
||||||
|
let progress = 0;
|
||||||
|
let updated = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [nodes]: any[] = await DB.query(`SELECT nodes.public_key, UNIX_TIMESTAMP(nodes.first_seen) AS first_seen, (SELECT UNIX_TIMESTAMP(created) FROM channels WHERE channels.node1_public_key = nodes.public_key ORDER BY created ASC LIMIT 1) AS created1, (SELECT UNIX_TIMESTAMP(created) FROM channels WHERE channels.node2_public_key = nodes.public_key ORDER BY created ASC LIMIT 1) AS created2 FROM nodes`);
|
const [nodes]: any[] = await DB.query(`
|
||||||
|
SELECT nodes.public_key, UNIX_TIMESTAMP(nodes.first_seen) AS first_seen,
|
||||||
|
(
|
||||||
|
SELECT MIN(UNIX_TIMESTAMP(created))
|
||||||
|
FROM channels
|
||||||
|
WHERE channels.node1_public_key = nodes.public_key
|
||||||
|
) AS created1,
|
||||||
|
(
|
||||||
|
SELECT MIN(UNIX_TIMESTAMP(created))
|
||||||
|
FROM channels
|
||||||
|
WHERE channels.node2_public_key = nodes.public_key
|
||||||
|
) AS created2
|
||||||
|
FROM nodes
|
||||||
|
`);
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
let lowest = 0;
|
const lowest = Math.min(
|
||||||
if (node.created1) {
|
node.created1 ?? Number.MAX_SAFE_INTEGER,
|
||||||
if (node.created2 && node.created2 < node.created1) {
|
node.created2 ?? Number.MAX_SAFE_INTEGER,
|
||||||
lowest = node.created2;
|
node.first_seen ?? Number.MAX_SAFE_INTEGER
|
||||||
} else {
|
);
|
||||||
lowest = node.created1;
|
if (lowest < node.first_seen) {
|
||||||
}
|
|
||||||
} else if (node.created2) {
|
|
||||||
lowest = node.created2;
|
|
||||||
}
|
|
||||||
if (lowest && lowest < node.first_seen) {
|
|
||||||
const query = `UPDATE nodes SET first_seen = FROM_UNIXTIME(?) WHERE public_key = ?`;
|
const query = `UPDATE nodes SET first_seen = FROM_UNIXTIME(?) WHERE public_key = ?`;
|
||||||
const params = [lowest, node.public_key];
|
const params = [lowest, node.public_key];
|
||||||
await DB.query(query, params);
|
await DB.query(query, params);
|
||||||
}
|
}
|
||||||
|
++progress;
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Updating node first seen date ${progress}/${nodes.length}`);
|
||||||
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
++updated;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.info(`Node first seen dates scan complete.`);
|
logger.info(`Updated ${updated} node first seen dates`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $lookUpCreationDateFromChain() {
|
private async $lookUpCreationDateFromChain(): Promise<void> {
|
||||||
logger.info(`Running channel creation date lookup...`);
|
let progress = 0;
|
||||||
|
|
||||||
|
logger.info(`Running channel creation date lookup`);
|
||||||
try {
|
try {
|
||||||
const channels = await channelsApi.$getChannelsWithoutCreatedDate();
|
const channels = await channelsApi.$getChannelsWithoutCreatedDate();
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
const transaction = await bitcoinClient.getRawTransaction(channel.transaction_id, 1);
|
const transaction = await fundingTxFetcher.$fetchChannelOpenTx(channel.short_id);
|
||||||
await DB.query(`UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`, [transaction.blocktime, channel.id]);
|
await DB.query(`
|
||||||
|
UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`,
|
||||||
|
[transaction.timestamp, channel.id]
|
||||||
|
);
|
||||||
|
++progress;
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Updating channel creation date ${progress}/${channels.length}`);
|
||||||
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.info(`Channel creation dates scan complete.`);
|
logger.info(`Updated ${channels.length} channels' creation date`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$setCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$lookUpCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Looking for channels whos nodes are inactive
|
/**
|
||||||
private async $findInactiveNodesAndChannels(): Promise<void> {
|
* If a channel does not have any active node linked to it, then also
|
||||||
logger.info(`Running inactive channels scan...`);
|
* mark that channel as inactive
|
||||||
|
*/
|
||||||
|
private async $deactivateChannelsWithoutActiveNodes(): Promise<void> {
|
||||||
|
logger.info(`Find channels which nodes are offline`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
const result = await DB.query<ResultSetHeader>(`
|
||||||
const [channels]: [ILightningApi.Channel[]] = await DB.query(`
|
UPDATE channels
|
||||||
SELECT channels.id
|
SET status = 0
|
||||||
FROM channels
|
|
||||||
WHERE channels.status = 1
|
WHERE channels.status = 1
|
||||||
AND (
|
AND (
|
||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM nodes
|
FROM nodes
|
||||||
WHERE nodes.public_key = channels.node1_public_key
|
WHERE nodes.public_key = channels.node1_public_key
|
||||||
|
AND nodes.status = 1
|
||||||
) = 0
|
) = 0
|
||||||
OR (
|
OR (
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM nodes
|
FROM nodes
|
||||||
WHERE nodes.public_key = channels.node2_public_key
|
WHERE nodes.public_key = channels.node2_public_key
|
||||||
|
AND nodes.status = 1
|
||||||
) = 0)
|
) = 0)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
for (const channel of channels) {
|
if (result[0].changedRows ?? 0 > 0) {
|
||||||
await this.$updateChannelStatus(channel.id, 0);
|
logger.info(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`);
|
||||||
}
|
}
|
||||||
logger.info(`Inactive channels scan complete.`);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$findInactiveNodesAndChannels() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$deactivateChannelsWithoutActiveNodes() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $scanForClosedChannels(): Promise<void> {
|
private async $scanForClosedChannels(): Promise<void> {
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(`Starting closed channels scan...`);
|
logger.info(`Starting closed channels scan...`);
|
||||||
const channels = await channelsApi.$getChannelsByStatus(0);
|
const channels = await channelsApi.$getChannelsByStatus(0);
|
||||||
@ -148,6 +236,13 @@ class NodeSyncService {
|
|||||||
await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]);
|
await DB.query(`UPDATE channels SET closing_transaction_id = ? WHERE id = ?`, [spendingTx.txid, channel.id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
++progress;
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Checking if channel has been closed ${progress}/${channels.length}`);
|
||||||
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.info(`Closed channels scan complete.`);
|
logger.info(`Closed channels scan complete.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -165,6 +260,9 @@ class NodeSyncService {
|
|||||||
if (!config.ESPLORA.REST_API_URL) {
|
if (!config.ESPLORA.REST_API_URL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(`Started running closed channel forensics...`);
|
logger.info(`Started running closed channel forensics...`);
|
||||||
const channels = await channelsApi.$getClosedChannelsWithoutReason();
|
const channels = await channelsApi.$getClosedChannelsWithoutReason();
|
||||||
@ -210,6 +308,13 @@ class NodeSyncService {
|
|||||||
logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
|
logger.debug('Setting closing reason ' + reason + ' for channel: ' + channel.id + '.');
|
||||||
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
++progress;
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Updating channel closed channel forensics ${progress}/${channels.length}`);
|
||||||
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.info(`Closed channels forensics scan complete.`);
|
logger.info(`Closed channels forensics scan complete.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -264,164 +369,6 @@ class NodeSyncService {
|
|||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $saveChannel(channel: ILightningApi.Channel): Promise<void> {
|
|
||||||
const fromChannel = chanNumber({ channel: channel.id }).number;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const query = `INSERT INTO channels
|
|
||||||
(
|
|
||||||
id,
|
|
||||||
short_id,
|
|
||||||
capacity,
|
|
||||||
transaction_id,
|
|
||||||
transaction_vout,
|
|
||||||
updated_at,
|
|
||||||
status,
|
|
||||||
node1_public_key,
|
|
||||||
node1_base_fee_mtokens,
|
|
||||||
node1_cltv_delta,
|
|
||||||
node1_fee_rate,
|
|
||||||
node1_is_disabled,
|
|
||||||
node1_max_htlc_mtokens,
|
|
||||||
node1_min_htlc_mtokens,
|
|
||||||
node1_updated_at,
|
|
||||||
node2_public_key,
|
|
||||||
node2_base_fee_mtokens,
|
|
||||||
node2_cltv_delta,
|
|
||||||
node2_fee_rate,
|
|
||||||
node2_is_disabled,
|
|
||||||
node2_max_htlc_mtokens,
|
|
||||||
node2_min_htlc_mtokens,
|
|
||||||
node2_updated_at
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
capacity = ?,
|
|
||||||
updated_at = ?,
|
|
||||||
status = 1,
|
|
||||||
node1_public_key = ?,
|
|
||||||
node1_base_fee_mtokens = ?,
|
|
||||||
node1_cltv_delta = ?,
|
|
||||||
node1_fee_rate = ?,
|
|
||||||
node1_is_disabled = ?,
|
|
||||||
node1_max_htlc_mtokens = ?,
|
|
||||||
node1_min_htlc_mtokens = ?,
|
|
||||||
node1_updated_at = ?,
|
|
||||||
node2_public_key = ?,
|
|
||||||
node2_base_fee_mtokens = ?,
|
|
||||||
node2_cltv_delta = ?,
|
|
||||||
node2_fee_rate = ?,
|
|
||||||
node2_is_disabled = ?,
|
|
||||||
node2_max_htlc_mtokens = ?,
|
|
||||||
node2_min_htlc_mtokens = ?,
|
|
||||||
node2_updated_at = ?
|
|
||||||
;`;
|
|
||||||
|
|
||||||
await DB.query(query, [
|
|
||||||
fromChannel,
|
|
||||||
channel.id,
|
|
||||||
channel.capacity,
|
|
||||||
channel.transaction_id,
|
|
||||||
channel.transaction_vout,
|
|
||||||
channel.updated_at ? this.utcDateToMysql(channel.updated_at) : 0,
|
|
||||||
channel.policies[0].public_key,
|
|
||||||
channel.policies[0].base_fee_mtokens,
|
|
||||||
channel.policies[0].cltv_delta,
|
|
||||||
channel.policies[0].fee_rate,
|
|
||||||
channel.policies[0].is_disabled,
|
|
||||||
channel.policies[0].max_htlc_mtokens,
|
|
||||||
channel.policies[0].min_htlc_mtokens,
|
|
||||||
channel.policies[0].updated_at ? this.utcDateToMysql(channel.policies[0].updated_at) : 0,
|
|
||||||
channel.policies[1].public_key,
|
|
||||||
channel.policies[1].base_fee_mtokens,
|
|
||||||
channel.policies[1].cltv_delta,
|
|
||||||
channel.policies[1].fee_rate,
|
|
||||||
channel.policies[1].is_disabled,
|
|
||||||
channel.policies[1].max_htlc_mtokens,
|
|
||||||
channel.policies[1].min_htlc_mtokens,
|
|
||||||
channel.policies[1].updated_at ? this.utcDateToMysql(channel.policies[1].updated_at) : 0,
|
|
||||||
channel.capacity,
|
|
||||||
channel.updated_at ? this.utcDateToMysql(channel.updated_at) : 0,
|
|
||||||
channel.policies[0].public_key,
|
|
||||||
channel.policies[0].base_fee_mtokens,
|
|
||||||
channel.policies[0].cltv_delta,
|
|
||||||
channel.policies[0].fee_rate,
|
|
||||||
channel.policies[0].is_disabled,
|
|
||||||
channel.policies[0].max_htlc_mtokens,
|
|
||||||
channel.policies[0].min_htlc_mtokens,
|
|
||||||
channel.policies[0].updated_at ? this.utcDateToMysql(channel.policies[0].updated_at) : 0,
|
|
||||||
channel.policies[1].public_key,
|
|
||||||
channel.policies[1].base_fee_mtokens,
|
|
||||||
channel.policies[1].cltv_delta,
|
|
||||||
channel.policies[1].fee_rate,
|
|
||||||
channel.policies[1].is_disabled,
|
|
||||||
channel.policies[1].max_htlc_mtokens,
|
|
||||||
channel.policies[1].min_htlc_mtokens,
|
|
||||||
channel.policies[1].updated_at ? this.utcDateToMysql(channel.policies[1].updated_at) : 0,
|
|
||||||
]);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$saveChannel() error: ' + (e instanceof Error ? e.message : e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async $updateChannelStatus(channelShortId: string, status: number): Promise<void> {
|
|
||||||
try {
|
|
||||||
await DB.query(`UPDATE channels SET status = ? WHERE id = ?`, [status, channelShortId]);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$updateChannelStatus() error: ' + (e instanceof Error ? e.message : e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async $setChannelsInactive(graphChannelsIds: string[]): Promise<void> {
|
|
||||||
try {
|
|
||||||
await DB.query(`
|
|
||||||
UPDATE channels
|
|
||||||
SET status = 0
|
|
||||||
WHERE short_id NOT IN (
|
|
||||||
${graphChannelsIds.map(id => `"${id}"`).join(',')}
|
|
||||||
)
|
|
||||||
AND status != 2
|
|
||||||
`);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async $saveNode(node: ILightningApi.Node): Promise<void> {
|
|
||||||
try {
|
|
||||||
const updatedAt = node.updated_at ? this.utcDateToMysql(node.updated_at) : '0000-00-00 00:00:00';
|
|
||||||
const sockets = node.sockets.join(',');
|
|
||||||
const query = `INSERT INTO nodes(
|
|
||||||
public_key,
|
|
||||||
first_seen,
|
|
||||||
updated_at,
|
|
||||||
alias,
|
|
||||||
color,
|
|
||||||
sockets
|
|
||||||
)
|
|
||||||
VALUES (?, NOW(), ?, ?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = ?, alias = ?, color = ?, sockets = ?;`;
|
|
||||||
|
|
||||||
await DB.query(query, [
|
|
||||||
node.public_key,
|
|
||||||
updatedAt,
|
|
||||||
node.alias,
|
|
||||||
node.color,
|
|
||||||
sockets,
|
|
||||||
updatedAt,
|
|
||||||
node.alias,
|
|
||||||
node.color,
|
|
||||||
sockets,
|
|
||||||
]);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$saveNode() error: ' + (e instanceof Error ? e.message : e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private utcDateToMysql(dateString: string): string {
|
|
||||||
const d = new Date(Date.parse(dateString));
|
|
||||||
return d.toISOString().split('T')[0] + ' ' + d.toTimeString().split(' ')[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new NodeSyncService();
|
export default new NetworkSyncService();
|
@ -1,350 +1,33 @@
|
|||||||
|
|
||||||
import DB from '../../database';
|
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
import lightningApi from '../../api/lightning/lightning-api-factory';
|
import lightningApi from '../../api/lightning/lightning-api-factory';
|
||||||
import channelsApi from '../../api/explorer/channels.api';
|
import LightningStatsImporter from './sync-tasks/stats-importer';
|
||||||
import * as net from 'net';
|
import config from '../../config';
|
||||||
|
import { Common } from '../../api/common';
|
||||||
|
|
||||||
class LightningStatsUpdater {
|
class LightningStatsUpdater {
|
||||||
hardCodedStartTime = '2018-01-12';
|
public async $startService(): Promise<void> {
|
||||||
|
|
||||||
public async $startService() {
|
|
||||||
logger.info('Starting Lightning Stats service');
|
logger.info('Starting Lightning Stats service');
|
||||||
let isInSync = false;
|
|
||||||
let error: any;
|
|
||||||
try {
|
|
||||||
error = null;
|
|
||||||
isInSync = await this.$lightningIsSynced();
|
|
||||||
} catch (e) {
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
if (!isInSync) {
|
|
||||||
if (error) {
|
|
||||||
logger.warn('Was not able to fetch Lightning Node status: ' + (error instanceof Error ? error.message : error) + '. Retrying in 1 minute...');
|
|
||||||
} else {
|
|
||||||
logger.notice('The Lightning graph is not yet in sync. Retrying in 1 minute...');
|
|
||||||
}
|
|
||||||
setTimeout(() => this.$startService(), 60 * 1000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.$populateHistoricalStatistics();
|
await this.$runTasks();
|
||||||
await this.$populateHistoricalNodeStatistics();
|
LightningStatsImporter.$run();
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$runTasks();
|
|
||||||
}, this.timeUntilMidnight());
|
|
||||||
}
|
|
||||||
|
|
||||||
private timeUntilMidnight(): number {
|
|
||||||
const date = new Date();
|
|
||||||
this.setDateMidnight(date);
|
|
||||||
date.setUTCHours(24);
|
|
||||||
return date.getTime() - new Date().getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
private setDateMidnight(date: Date): void {
|
|
||||||
date.setUTCHours(0);
|
|
||||||
date.setUTCMinutes(0);
|
|
||||||
date.setUTCSeconds(0);
|
|
||||||
date.setUTCMilliseconds(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async $lightningIsSynced(): Promise<boolean> {
|
|
||||||
const nodeInfo = await lightningApi.$getInfo();
|
|
||||||
return nodeInfo.is_synced_to_chain && nodeInfo.is_synced_to_graph;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $runTasks(): Promise<void> {
|
private async $runTasks(): Promise<void> {
|
||||||
await this.$logLightningStatsDaily();
|
await this.$logStatsDaily();
|
||||||
await this.$logNodeStatsDaily();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => { this.$runTasks(); }, 1000 * config.LIGHTNING.STATS_REFRESH_INTERVAL);
|
||||||
this.$runTasks();
|
|
||||||
}, this.timeUntilMidnight());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $logLightningStatsDaily() {
|
/**
|
||||||
try {
|
* Update the latest entry for each node every config.LIGHTNING.STATS_REFRESH_INTERVAL seconds
|
||||||
logger.info(`Running lightning daily stats log...`);
|
*/
|
||||||
|
private async $logStatsDaily(): Promise<void> {
|
||||||
const networkGraph = await lightningApi.$getNetworkGraph();
|
const date = new Date();
|
||||||
let total_capacity = 0;
|
Common.setDateMidnight(date);
|
||||||
for (const channel of networkGraph.channels) {
|
const networkGraph = await lightningApi.$getNetworkGraph();
|
||||||
if (channel.capacity) {
|
LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
|
||||||
total_capacity += channel.capacity;
|
|
||||||
}
|
logger.info(`Updated latest network stats`);
|
||||||
}
|
|
||||||
|
|
||||||
let clearnetNodes = 0;
|
|
||||||
let torNodes = 0;
|
|
||||||
let unannouncedNodes = 0;
|
|
||||||
for (const node of networkGraph.nodes) {
|
|
||||||
let isUnnanounced = true;
|
|
||||||
for (const socket of node.sockets) {
|
|
||||||
const hasOnion = socket.indexOf('.onion') !== -1;
|
|
||||||
if (hasOnion) {
|
|
||||||
torNodes++;
|
|
||||||
isUnnanounced = false;
|
|
||||||
}
|
|
||||||
const hasClearnet = [4, 6].includes(net.isIP(socket.split(':')[0]));
|
|
||||||
if (hasClearnet) {
|
|
||||||
clearnetNodes++;
|
|
||||||
isUnnanounced = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isUnnanounced) {
|
|
||||||
unannouncedNodes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const channelStats = await channelsApi.$getChannelsStats();
|
|
||||||
|
|
||||||
const query = `INSERT INTO lightning_stats(
|
|
||||||
added,
|
|
||||||
channel_count,
|
|
||||||
node_count,
|
|
||||||
total_capacity,
|
|
||||||
tor_nodes,
|
|
||||||
clearnet_nodes,
|
|
||||||
unannounced_nodes,
|
|
||||||
avg_capacity,
|
|
||||||
avg_fee_rate,
|
|
||||||
avg_base_fee_mtokens,
|
|
||||||
med_capacity,
|
|
||||||
med_fee_rate,
|
|
||||||
med_base_fee_mtokens
|
|
||||||
)
|
|
||||||
VALUES (NOW() - INTERVAL 1 DAY, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
||||||
|
|
||||||
await DB.query(query, [
|
|
||||||
networkGraph.channels.length,
|
|
||||||
networkGraph.nodes.length,
|
|
||||||
total_capacity,
|
|
||||||
torNodes,
|
|
||||||
clearnetNodes,
|
|
||||||
unannouncedNodes,
|
|
||||||
channelStats.avgCapacity,
|
|
||||||
channelStats.avgFeeRate,
|
|
||||||
channelStats.avgBaseFee,
|
|
||||||
channelStats.medianCapacity,
|
|
||||||
channelStats.medianFeeRate,
|
|
||||||
channelStats.medianBaseFee,
|
|
||||||
]);
|
|
||||||
logger.info(`Lightning daily stats done.`);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$logLightningStatsDaily() error: ' + (e instanceof Error ? e.message : e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async $logNodeStatsDaily() {
|
|
||||||
try {
|
|
||||||
logger.info(`Running daily node stats update...`);
|
|
||||||
|
|
||||||
const query = `
|
|
||||||
SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left,
|
|
||||||
c2.channels_capacity_right
|
|
||||||
FROM nodes
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left
|
|
||||||
FROM channels
|
|
||||||
WHERE channels.status = 1
|
|
||||||
GROUP BY node1_public_key
|
|
||||||
) c1 ON c1.node1_public_key = nodes.public_key
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right
|
|
||||||
FROM channels WHERE channels.status = 1 GROUP BY node2_public_key
|
|
||||||
) c2 ON c2.node2_public_key = nodes.public_key
|
|
||||||
`;
|
|
||||||
|
|
||||||
const [nodes]: any = await DB.query(query);
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
await DB.query(
|
|
||||||
`INSERT INTO node_stats(public_key, added, capacity, channels) VALUES (?, NOW() - INTERVAL 1 DAY, ?, ?)`,
|
|
||||||
[node.public_key, (parseInt(node.channels_capacity_left || 0, 10)) + (parseInt(node.channels_capacity_right || 0, 10)),
|
|
||||||
node.channels_count_left + node.channels_count_right]);
|
|
||||||
}
|
|
||||||
logger.info('Daily node stats has updated.');
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only run this on first launch
|
|
||||||
private async $populateHistoricalStatistics() {
|
|
||||||
try {
|
|
||||||
const [rows]: any = await DB.query(`SELECT COUNT(*) FROM lightning_stats`);
|
|
||||||
// Only run if table is empty
|
|
||||||
if (rows[0]['COUNT(*)'] > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.info(`Running historical stats population...`);
|
|
||||||
|
|
||||||
const [channels]: any = await DB.query(`SELECT capacity, created, closing_date FROM channels ORDER BY created ASC`);
|
|
||||||
const [nodes]: any = await DB.query(`SELECT first_seen, sockets FROM nodes ORDER BY first_seen ASC`);
|
|
||||||
|
|
||||||
const date: Date = new Date(this.hardCodedStartTime);
|
|
||||||
const currentDate = new Date();
|
|
||||||
this.setDateMidnight(currentDate);
|
|
||||||
|
|
||||||
while (date < currentDate) {
|
|
||||||
let totalCapacity = 0;
|
|
||||||
let channelsCount = 0;
|
|
||||||
|
|
||||||
for (const channel of channels) {
|
|
||||||
if (new Date(channel.created) > date) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (channel.closing_date === null || new Date(channel.closing_date) > date) {
|
|
||||||
totalCapacity += channel.capacity;
|
|
||||||
channelsCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let nodeCount = 0;
|
|
||||||
let clearnetNodes = 0;
|
|
||||||
let torNodes = 0;
|
|
||||||
let unannouncedNodes = 0;
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (new Date(node.first_seen) > date) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
nodeCount++;
|
|
||||||
|
|
||||||
const sockets = node.sockets.split(',');
|
|
||||||
let isUnnanounced = true;
|
|
||||||
for (const socket of sockets) {
|
|
||||||
const hasOnion = socket.indexOf('.onion') !== -1;
|
|
||||||
if (hasOnion) {
|
|
||||||
torNodes++;
|
|
||||||
isUnnanounced = false;
|
|
||||||
}
|
|
||||||
const hasClearnet = [4, 6].includes(net.isIP(socket.substring(0, socket.lastIndexOf(':'))));
|
|
||||||
if (hasClearnet) {
|
|
||||||
clearnetNodes++;
|
|
||||||
isUnnanounced = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isUnnanounced) {
|
|
||||||
unannouncedNodes++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = `INSERT INTO lightning_stats(
|
|
||||||
added,
|
|
||||||
channel_count,
|
|
||||||
node_count,
|
|
||||||
total_capacity,
|
|
||||||
tor_nodes,
|
|
||||||
clearnet_nodes,
|
|
||||||
unannounced_nodes,
|
|
||||||
avg_capacity,
|
|
||||||
avg_fee_rate,
|
|
||||||
avg_base_fee_mtokens,
|
|
||||||
med_capacity,
|
|
||||||
med_fee_rate,
|
|
||||||
med_base_fee_mtokens
|
|
||||||
)
|
|
||||||
VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
||||||
|
|
||||||
const rowTimestamp = date.getTime() / 1000; // Save timestamp for the row insertion down below
|
|
||||||
|
|
||||||
date.setUTCDate(date.getUTCDate() + 1);
|
|
||||||
|
|
||||||
// Last iteration, save channels stats
|
|
||||||
const channelStats = (date >= currentDate ? await channelsApi.$getChannelsStats() : undefined);
|
|
||||||
|
|
||||||
await DB.query(query, [
|
|
||||||
rowTimestamp,
|
|
||||||
channelsCount,
|
|
||||||
nodeCount,
|
|
||||||
totalCapacity,
|
|
||||||
torNodes,
|
|
||||||
clearnetNodes,
|
|
||||||
unannouncedNodes,
|
|
||||||
channelStats?.avgCapacity ?? 0,
|
|
||||||
channelStats?.avgFeeRate ?? 0,
|
|
||||||
channelStats?.avgBaseFee ?? 0,
|
|
||||||
channelStats?.medianCapacity ?? 0,
|
|
||||||
channelStats?.medianFeeRate ?? 0,
|
|
||||||
channelStats?.medianBaseFee ?? 0,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Historical stats populated.');
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$populateHistoricalData() error: ' + (e instanceof Error ? e.message : e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async $populateHistoricalNodeStatistics() {
|
|
||||||
try {
|
|
||||||
const [rows]: any = await DB.query(`SELECT COUNT(*) FROM node_stats`);
|
|
||||||
// Only run if table is empty
|
|
||||||
if (rows[0]['COUNT(*)'] > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.info(`Running historical node stats population...`);
|
|
||||||
|
|
||||||
const [nodes]: any = await DB.query(`SELECT public_key, first_seen, alias FROM nodes ORDER BY first_seen ASC`);
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
const [channels]: any = await DB.query(`SELECT capacity, created, closing_date FROM channels WHERE node1_public_key = ? OR node2_public_key = ? ORDER BY created ASC`, [node.public_key, node.public_key]);
|
|
||||||
|
|
||||||
const date: Date = new Date(this.hardCodedStartTime);
|
|
||||||
const currentDate = new Date();
|
|
||||||
this.setDateMidnight(currentDate);
|
|
||||||
|
|
||||||
let lastTotalCapacity = 0;
|
|
||||||
let lastChannelsCount = 0;
|
|
||||||
|
|
||||||
while (date < currentDate) {
|
|
||||||
let totalCapacity = 0;
|
|
||||||
let channelsCount = 0;
|
|
||||||
for (const channel of channels) {
|
|
||||||
if (new Date(channel.created) > date) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (channel.closing_date !== null && new Date(channel.closing_date) < date) {
|
|
||||||
date.setUTCDate(date.getUTCDate() + 1);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
totalCapacity += channel.capacity;
|
|
||||||
channelsCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastTotalCapacity === totalCapacity && lastChannelsCount === channelsCount) {
|
|
||||||
date.setUTCDate(date.getUTCDate() + 1);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastTotalCapacity = totalCapacity;
|
|
||||||
lastChannelsCount = channelsCount;
|
|
||||||
|
|
||||||
const query = `INSERT INTO node_stats(
|
|
||||||
public_key,
|
|
||||||
added,
|
|
||||||
capacity,
|
|
||||||
channels
|
|
||||||
)
|
|
||||||
VALUES (?, FROM_UNIXTIME(?), ?, ?)`;
|
|
||||||
|
|
||||||
await DB.query(query, [
|
|
||||||
node.public_key,
|
|
||||||
date.getTime() / 1000,
|
|
||||||
totalCapacity,
|
|
||||||
channelsCount,
|
|
||||||
]);
|
|
||||||
date.setUTCDate(date.getUTCDate() + 1);
|
|
||||||
}
|
|
||||||
logger.debug('Updated node_stats for: ' + node.alias);
|
|
||||||
}
|
|
||||||
logger.info('Historical stats populated.');
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$populateHistoricalNodeData() error: ' + (e instanceof Error ? e.message : e));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
118
backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts
Normal file
118
backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { existsSync, promises } from 'fs';
|
||||||
|
import bitcoinClient from '../../../api/bitcoin/bitcoin-client';
|
||||||
|
import { Common } from '../../../api/common';
|
||||||
|
import config from '../../../config';
|
||||||
|
import logger from '../../../logger';
|
||||||
|
|
||||||
|
const fsPromises = promises;
|
||||||
|
|
||||||
|
const BLOCKS_CACHE_MAX_SIZE = 100;
|
||||||
|
const CACHE_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/ln-funding-txs-cache.json';
|
||||||
|
|
||||||
|
class FundingTxFetcher {
|
||||||
|
private running = false;
|
||||||
|
private blocksCache = {};
|
||||||
|
private channelNewlyProcessed = 0;
|
||||||
|
public fundingTxCache = {};
|
||||||
|
|
||||||
|
async $init(): Promise<void> {
|
||||||
|
// Load funding tx disk cache
|
||||||
|
if (Object.keys(this.fundingTxCache).length === 0 && existsSync(CACHE_FILE_NAME)) {
|
||||||
|
try {
|
||||||
|
this.fundingTxCache = JSON.parse(await fsPromises.readFile(CACHE_FILE_NAME, 'utf-8'));
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Unable to parse channels funding txs disk cache. Starting from scratch`);
|
||||||
|
this.fundingTxCache = {};
|
||||||
|
}
|
||||||
|
logger.debug(`Imported ${Object.keys(this.fundingTxCache).length} funding tx amount from the disk cache`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $fetchChannelsFundingTxs(channelIds: string[]): Promise<void> {
|
||||||
|
if (this.running) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.running = true;
|
||||||
|
|
||||||
|
const globalTimer = new Date().getTime() / 1000;
|
||||||
|
let cacheTimer = new Date().getTime() / 1000;
|
||||||
|
let loggerTimer = new Date().getTime() / 1000;
|
||||||
|
let channelProcessed = 0;
|
||||||
|
this.channelNewlyProcessed = 0;
|
||||||
|
for (const channelId of channelIds) {
|
||||||
|
await this.$fetchChannelOpenTx(channelId);
|
||||||
|
++channelProcessed;
|
||||||
|
|
||||||
|
let elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
elapsedSeconds = Math.round((new Date().getTime() / 1000) - globalTimer);
|
||||||
|
logger.info(`Indexing channels funding tx ${channelProcessed + 1} of ${channelIds.length} ` +
|
||||||
|
`(${Math.floor(channelProcessed / channelIds.length * 10000) / 100}%) | ` +
|
||||||
|
`elapsed: ${elapsedSeconds} seconds`
|
||||||
|
);
|
||||||
|
loggerTimer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsedSeconds = Math.round((new Date().getTime() / 1000) - cacheTimer);
|
||||||
|
if (elapsedSeconds > 60) {
|
||||||
|
logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`);
|
||||||
|
fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache));
|
||||||
|
cacheTimer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.channelNewlyProcessed > 0) {
|
||||||
|
logger.info(`Indexed ${this.channelNewlyProcessed} additional channels funding tx`);
|
||||||
|
logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`);
|
||||||
|
fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number}> {
|
||||||
|
if (channelId.indexOf('x') === -1) {
|
||||||
|
channelId = Common.channelIntegerIdToShortId(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fundingTxCache[channelId]) {
|
||||||
|
return this.fundingTxCache[channelId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = channelId.split('x');
|
||||||
|
const blockHeight = parts[0];
|
||||||
|
const txIdx = parts[1];
|
||||||
|
const outputIdx = parts[2];
|
||||||
|
|
||||||
|
let block = this.blocksCache[blockHeight];
|
||||||
|
// Fetch it from core
|
||||||
|
if (!block) {
|
||||||
|
const blockHash = await bitcoinClient.getBlockHash(parseInt(blockHeight, 10));
|
||||||
|
block = await bitcoinClient.getBlock(blockHash, 1);
|
||||||
|
}
|
||||||
|
this.blocksCache[block.height] = block;
|
||||||
|
|
||||||
|
const blocksCacheHashes = Object.keys(this.blocksCache).sort((a, b) => parseInt(b) - parseInt(a)).reverse();
|
||||||
|
if (blocksCacheHashes.length > BLOCKS_CACHE_MAX_SIZE) {
|
||||||
|
for (let i = 0; i < 10; ++i) {
|
||||||
|
delete this.blocksCache[blocksCacheHashes[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const txid = block.tx[txIdx];
|
||||||
|
const rawTx = await bitcoinClient.getRawTransaction(txid);
|
||||||
|
const tx = await bitcoinClient.decodeRawTransaction(rawTx);
|
||||||
|
|
||||||
|
this.fundingTxCache[channelId] = {
|
||||||
|
timestamp: block.time,
|
||||||
|
txid: txid,
|
||||||
|
value: tx.vout[outputIdx].value,
|
||||||
|
};
|
||||||
|
|
||||||
|
++this.channelNewlyProcessed;
|
||||||
|
|
||||||
|
return this.fundingTxCache[channelId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new FundingTxFetcher;
|
@ -6,7 +6,10 @@ import DB from '../../../database';
|
|||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
|
|
||||||
export async function $lookupNodeLocation(): Promise<void> {
|
export async function $lookupNodeLocation(): Promise<void> {
|
||||||
logger.info(`Running node location updater using Maxmind...`);
|
let loggerTimer = new Date().getTime() / 1000;
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
logger.info(`Running node location updater using Maxmind`);
|
||||||
try {
|
try {
|
||||||
const nodes = await nodesApi.$getAllNodes();
|
const nodes = await nodesApi.$getAllNodes();
|
||||||
const lookupCity = await maxmind.open<CityResponse>(config.MAXMIND.GEOLITE2_CITY);
|
const lookupCity = await maxmind.open<CityResponse>(config.MAXMIND.GEOLITE2_CITY);
|
||||||
@ -18,21 +21,24 @@ export async function $lookupNodeLocation(): Promise<void> {
|
|||||||
for (const socket of sockets) {
|
for (const socket of sockets) {
|
||||||
const ip = socket.substring(0, socket.lastIndexOf(':')).replace('[', '').replace(']', '');
|
const ip = socket.substring(0, socket.lastIndexOf(':')).replace('[', '').replace(']', '');
|
||||||
const hasClearnet = [4, 6].includes(net.isIP(ip));
|
const hasClearnet = [4, 6].includes(net.isIP(ip));
|
||||||
|
|
||||||
if (hasClearnet && ip !== '127.0.1.1' && ip !== '127.0.0.1') {
|
if (hasClearnet && ip !== '127.0.1.1' && ip !== '127.0.0.1') {
|
||||||
const city = lookupCity.get(ip);
|
const city = lookupCity.get(ip);
|
||||||
const asn = lookupAsn.get(ip);
|
const asn = lookupAsn.get(ip);
|
||||||
const isp = lookupIsp.get(ip);
|
const isp = lookupIsp.get(ip);
|
||||||
|
|
||||||
if (city && (asn || isp)) {
|
if (city && (asn || isp)) {
|
||||||
const query = `UPDATE nodes SET
|
const query = `
|
||||||
as_number = ?,
|
UPDATE nodes SET
|
||||||
city_id = ?,
|
as_number = ?,
|
||||||
country_id = ?,
|
city_id = ?,
|
||||||
subdivision_id = ?,
|
country_id = ?,
|
||||||
longitude = ?,
|
subdivision_id = ?,
|
||||||
latitude = ?,
|
longitude = ?,
|
||||||
accuracy_radius = ?
|
latitude = ?,
|
||||||
WHERE public_key = ?`;
|
accuracy_radius = ?
|
||||||
|
WHERE public_key = ?
|
||||||
|
`;
|
||||||
|
|
||||||
const params = [
|
const params = [
|
||||||
isp?.autonomous_system_number ?? asn?.autonomous_system_number,
|
isp?.autonomous_system_number ?? asn?.autonomous_system_number,
|
||||||
@ -46,25 +52,25 @@ export async function $lookupNodeLocation(): Promise<void> {
|
|||||||
];
|
];
|
||||||
await DB.query(query, params);
|
await DB.query(query, params);
|
||||||
|
|
||||||
// Store Continent
|
// Store Continent
|
||||||
if (city.continent?.geoname_id) {
|
if (city.continent?.geoname_id) {
|
||||||
await DB.query(
|
await DB.query(
|
||||||
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'continent', ?)`,
|
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'continent', ?)`,
|
||||||
[city.continent?.geoname_id, JSON.stringify(city.continent?.names)]);
|
[city.continent?.geoname_id, JSON.stringify(city.continent?.names)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store Country
|
// Store Country
|
||||||
if (city.country?.geoname_id) {
|
if (city.country?.geoname_id) {
|
||||||
await DB.query(
|
await DB.query(
|
||||||
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'country', ?)`,
|
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'country', ?)`,
|
||||||
[city.country?.geoname_id, JSON.stringify(city.country?.names)]);
|
[city.country?.geoname_id, JSON.stringify(city.country?.names)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store Country ISO code
|
// Store Country ISO code
|
||||||
if (city.country?.iso_code) {
|
if (city.country?.iso_code) {
|
||||||
await DB.query(
|
await DB.query(
|
||||||
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'country_iso_code', ?)`,
|
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'country_iso_code', ?)`,
|
||||||
[city.country?.geoname_id, city.country?.iso_code]);
|
[city.country?.geoname_id, city.country?.iso_code]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store Division
|
// Store Division
|
||||||
@ -88,10 +94,17 @@ export async function $lookupNodeLocation(): Promise<void> {
|
|||||||
[isp?.autonomous_system_number ?? asn?.autonomous_system_number, JSON.stringify(isp?.isp ?? asn?.autonomous_system_organization)]);
|
[isp?.autonomous_system_number ?? asn?.autonomous_system_number, JSON.stringify(isp?.isp ?? asn?.autonomous_system_organization)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
++progress;
|
||||||
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
||||||
|
if (elapsedSeconds > 10) {
|
||||||
|
logger.info(`Updating node location data ${progress}/${nodes.length}`);
|
||||||
|
loggerTimer = new Date().getTime() / 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.info(`Node location data updated.`);
|
logger.info(`${progress} nodes location data updated`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$lookupNodeLocation() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$lookupNodeLocation() error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
|
411
backend/src/tasks/lightning/sync-tasks/stats-importer.ts
Normal file
411
backend/src/tasks/lightning/sync-tasks/stats-importer.ts
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
import DB from '../../../database';
|
||||||
|
import { promises } from 'fs';
|
||||||
|
import { XMLParser } from 'fast-xml-parser';
|
||||||
|
import logger from '../../../logger';
|
||||||
|
import fundingTxFetcher from './funding-tx-fetcher';
|
||||||
|
import config from '../../../config';
|
||||||
|
|
||||||
|
const fsPromises = promises;
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
id: string;
|
||||||
|
timestamp: number;
|
||||||
|
features: string;
|
||||||
|
rgb_color: string;
|
||||||
|
alias: string;
|
||||||
|
addresses: unknown[];
|
||||||
|
out_degree: number;
|
||||||
|
in_degree: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Channel {
|
||||||
|
channel_id: string;
|
||||||
|
node1_pub: string;
|
||||||
|
node2_pub: string;
|
||||||
|
timestamp: number;
|
||||||
|
features: string;
|
||||||
|
fee_base_msat: number;
|
||||||
|
fee_rate_milli_msat: number;
|
||||||
|
htlc_minimim_msat: number;
|
||||||
|
cltv_expiry_delta: number;
|
||||||
|
htlc_maximum_msat: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LightningStatsImporter {
|
||||||
|
topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER;
|
||||||
|
parser = new XMLParser();
|
||||||
|
|
||||||
|
async $run(): Promise<void> {
|
||||||
|
logger.info(`Importing historical lightning stats`);
|
||||||
|
|
||||||
|
const [channels]: any[] = await DB.query('SELECT short_id from channels;');
|
||||||
|
logger.info('Caching funding txs for currently existing channels');
|
||||||
|
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
|
||||||
|
|
||||||
|
await this.$importHistoricalLightningStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate LN network stats for one day
|
||||||
|
*/
|
||||||
|
public async computeNetworkStats(timestamp: number, networkGraph): Promise<unknown> {
|
||||||
|
// Node counts and network shares
|
||||||
|
let clearnetNodes = 0;
|
||||||
|
let torNodes = 0;
|
||||||
|
let clearnetTorNodes = 0;
|
||||||
|
let unannouncedNodes = 0;
|
||||||
|
|
||||||
|
for (const node of networkGraph.nodes) {
|
||||||
|
let hasOnion = false;
|
||||||
|
let hasClearnet = false;
|
||||||
|
let isUnnanounced = true;
|
||||||
|
|
||||||
|
for (const socket of (node.addresses ?? [])) {
|
||||||
|
hasOnion = hasOnion || ['torv2', 'torv3'].includes(socket.network);
|
||||||
|
hasClearnet = hasClearnet || ['ipv4', 'ipv6'].includes(socket.network);
|
||||||
|
}
|
||||||
|
if (hasOnion && hasClearnet) {
|
||||||
|
clearnetTorNodes++;
|
||||||
|
isUnnanounced = false;
|
||||||
|
} else if (hasOnion) {
|
||||||
|
torNodes++;
|
||||||
|
isUnnanounced = false;
|
||||||
|
} else if (hasClearnet) {
|
||||||
|
clearnetNodes++;
|
||||||
|
isUnnanounced = false;
|
||||||
|
}
|
||||||
|
if (isUnnanounced) {
|
||||||
|
unannouncedNodes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channels and node historical stats
|
||||||
|
const nodeStats = {};
|
||||||
|
let capacity = 0;
|
||||||
|
let avgFeeRate = 0;
|
||||||
|
let avgBaseFee = 0;
|
||||||
|
const capacities: number[] = [];
|
||||||
|
const feeRates: number[] = [];
|
||||||
|
const baseFees: number[] = [];
|
||||||
|
const alreadyCountedChannels = {};
|
||||||
|
|
||||||
|
for (const channel of networkGraph.edges) {
|
||||||
|
let short_id = channel.channel_id;
|
||||||
|
if (short_id.indexOf('/') !== -1) {
|
||||||
|
short_id = short_id.slice(0, -2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id);
|
||||||
|
if (!tx) {
|
||||||
|
logger.err(`Unable to fetch funding tx for channel ${short_id}. Capacity and creation date is unknown. Skipping channel.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeStats[channel.node1_pub]) {
|
||||||
|
nodeStats[channel.node1_pub] = {
|
||||||
|
capacity: 0,
|
||||||
|
channels: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!nodeStats[channel.node2_pub]) {
|
||||||
|
nodeStats[channel.node2_pub] = {
|
||||||
|
capacity: 0,
|
||||||
|
channels: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alreadyCountedChannels[short_id]) {
|
||||||
|
capacity += Math.round(tx.value * 100000000);
|
||||||
|
capacities.push(Math.round(tx.value * 100000000));
|
||||||
|
alreadyCountedChannels[short_id] = true;
|
||||||
|
|
||||||
|
nodeStats[channel.node1_pub].capacity += Math.round(tx.value * 100000000);
|
||||||
|
nodeStats[channel.node1_pub].channels++;
|
||||||
|
nodeStats[channel.node2_pub].capacity += Math.round(tx.value * 100000000);
|
||||||
|
nodeStats[channel.node2_pub].channels++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel.node1_policy !== undefined) { // Coming from the node
|
||||||
|
for (const policy of [channel.node1_policy, channel.node2_policy]) {
|
||||||
|
if (policy && policy.fee_rate_milli_msat < 5000) {
|
||||||
|
avgFeeRate += parseInt(policy.fee_rate_milli_msat, 10);
|
||||||
|
feeRates.push(parseInt(policy.fee_rate_milli_msat, 10));
|
||||||
|
}
|
||||||
|
if (policy && policy.fee_base_msat < 5000) {
|
||||||
|
avgBaseFee += parseInt(policy.fee_base_msat, 10);
|
||||||
|
baseFees.push(parseInt(policy.fee_base_msat, 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // Coming from the historical import
|
||||||
|
if (channel.fee_rate_milli_msat < 5000) {
|
||||||
|
avgFeeRate += parseInt(channel.fee_rate_milli_msat, 10);
|
||||||
|
feeRates.push(parseInt(channel.fee_rate_milli_msat), 10);
|
||||||
|
}
|
||||||
|
if (channel.fee_base_msat < 5000) {
|
||||||
|
avgBaseFee += parseInt(channel.fee_base_msat, 10);
|
||||||
|
baseFees.push(parseInt(channel.fee_base_msat), 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
avgFeeRate /= Math.max(networkGraph.edges.length, 1);
|
||||||
|
avgBaseFee /= Math.max(networkGraph.edges.length, 1);
|
||||||
|
const medCapacity = capacities.sort((a, b) => b - a)[Math.round(capacities.length / 2 - 1)];
|
||||||
|
const medFeeRate = feeRates.sort((a, b) => b - a)[Math.round(feeRates.length / 2 - 1)];
|
||||||
|
const medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)];
|
||||||
|
const avgCapacity = Math.round(capacity / Math.max(capacities.length, 1));
|
||||||
|
|
||||||
|
let query = `INSERT INTO lightning_stats(
|
||||||
|
added,
|
||||||
|
channel_count,
|
||||||
|
node_count,
|
||||||
|
total_capacity,
|
||||||
|
tor_nodes,
|
||||||
|
clearnet_nodes,
|
||||||
|
unannounced_nodes,
|
||||||
|
clearnet_tor_nodes,
|
||||||
|
avg_capacity,
|
||||||
|
avg_fee_rate,
|
||||||
|
avg_base_fee_mtokens,
|
||||||
|
med_capacity,
|
||||||
|
med_fee_rate,
|
||||||
|
med_base_fee_mtokens
|
||||||
|
)
|
||||||
|
VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
added = FROM_UNIXTIME(?),
|
||||||
|
channel_count = ?,
|
||||||
|
node_count = ?,
|
||||||
|
total_capacity = ?,
|
||||||
|
tor_nodes = ?,
|
||||||
|
clearnet_nodes = ?,
|
||||||
|
unannounced_nodes = ?,
|
||||||
|
clearnet_tor_nodes = ?,
|
||||||
|
avg_capacity = ?,
|
||||||
|
avg_fee_rate = ?,
|
||||||
|
avg_base_fee_mtokens = ?,
|
||||||
|
med_capacity = ?,
|
||||||
|
med_fee_rate = ?,
|
||||||
|
med_base_fee_mtokens = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
await DB.query(query, [
|
||||||
|
timestamp,
|
||||||
|
capacities.length,
|
||||||
|
networkGraph.nodes.length,
|
||||||
|
capacity,
|
||||||
|
torNodes,
|
||||||
|
clearnetNodes,
|
||||||
|
unannouncedNodes,
|
||||||
|
clearnetTorNodes,
|
||||||
|
avgCapacity,
|
||||||
|
avgFeeRate,
|
||||||
|
avgBaseFee,
|
||||||
|
medCapacity,
|
||||||
|
medFeeRate,
|
||||||
|
medBaseFee,
|
||||||
|
timestamp,
|
||||||
|
capacities.length,
|
||||||
|
networkGraph.nodes.length,
|
||||||
|
capacity,
|
||||||
|
torNodes,
|
||||||
|
clearnetNodes,
|
||||||
|
unannouncedNodes,
|
||||||
|
clearnetTorNodes,
|
||||||
|
avgCapacity,
|
||||||
|
avgFeeRate,
|
||||||
|
avgBaseFee,
|
||||||
|
medCapacity,
|
||||||
|
medFeeRate,
|
||||||
|
medBaseFee,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const public_key of Object.keys(nodeStats)) {
|
||||||
|
query = `INSERT INTO node_stats(
|
||||||
|
public_key,
|
||||||
|
added,
|
||||||
|
capacity,
|
||||||
|
channels
|
||||||
|
)
|
||||||
|
VALUES (?, FROM_UNIXTIME(?), ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
added = FROM_UNIXTIME(?),
|
||||||
|
capacity = ?,
|
||||||
|
channels = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
await DB.query(query, [
|
||||||
|
public_key,
|
||||||
|
timestamp,
|
||||||
|
nodeStats[public_key].capacity,
|
||||||
|
nodeStats[public_key].channels,
|
||||||
|
timestamp,
|
||||||
|
nodeStats[public_key].capacity,
|
||||||
|
nodeStats[public_key].channels,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
added: timestamp,
|
||||||
|
node_count: networkGraph.nodes.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import topology files LN historical data into the database
|
||||||
|
*/
|
||||||
|
async $importHistoricalLightningStats(): Promise<void> {
|
||||||
|
let latestNodeCount = 1;
|
||||||
|
|
||||||
|
const fileList = await fsPromises.readdir(this.topologiesFolder);
|
||||||
|
// Insert history from the most recent to the oldest
|
||||||
|
// This also put the .json cached files first
|
||||||
|
fileList.sort().reverse();
|
||||||
|
|
||||||
|
const [rows]: any[] = await DB.query(`
|
||||||
|
SELECT UNIX_TIMESTAMP(added) AS added, node_count
|
||||||
|
FROM lightning_stats
|
||||||
|
ORDER BY added DESC
|
||||||
|
`);
|
||||||
|
const existingStatsTimestamps = {};
|
||||||
|
for (const row of rows) {
|
||||||
|
existingStatsTimestamps[row.added] = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For logging purpose
|
||||||
|
let processed = 10;
|
||||||
|
let totalProcessed = -1;
|
||||||
|
|
||||||
|
for (const filename of fileList) {
|
||||||
|
processed++;
|
||||||
|
totalProcessed++;
|
||||||
|
|
||||||
|
const timestamp = parseInt(filename.split('_')[1], 10);
|
||||||
|
|
||||||
|
// Stats exist already, don't calculate/insert them
|
||||||
|
if (existingStatsTimestamps[timestamp] !== undefined) {
|
||||||
|
latestNodeCount = existingStatsTimestamps[timestamp].node_count;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Reading ${this.topologiesFolder}/${filename}`);
|
||||||
|
const fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8');
|
||||||
|
|
||||||
|
let graph;
|
||||||
|
if (filename.indexOf('.json') !== -1) {
|
||||||
|
try {
|
||||||
|
graph = JSON.parse(fileContent);
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
graph = this.parseFile(fileContent);
|
||||||
|
if (!graph) {
|
||||||
|
logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await fsPromises.writeFile(`${this.topologiesFolder}/${filename}.json`, JSON.stringify(graph));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestamp > 1556316000) {
|
||||||
|
// "No, the reason most likely is just that I started collection in 2019,
|
||||||
|
// so what I had before that is just the survivors from before, which weren't that many"
|
||||||
|
const diffRatio = graph.nodes.length / latestNodeCount;
|
||||||
|
if (diffRatio < 0.9) {
|
||||||
|
// Ignore drop of more than 90% of the node count as it's probably a missing data point
|
||||||
|
logger.debug(`Nodes count diff ratio threshold reached, ignore the data for this day ${graph.nodes.length} nodes vs ${latestNodeCount}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
latestNodeCount = graph.nodes.length;
|
||||||
|
|
||||||
|
const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`;
|
||||||
|
logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`);
|
||||||
|
|
||||||
|
if (processed > 10) {
|
||||||
|
logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
|
||||||
|
processed = 0;
|
||||||
|
} else {
|
||||||
|
logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
|
||||||
|
}
|
||||||
|
await fundingTxFetcher.$fetchChannelsFundingTxs(graph.edges.map(channel => channel.channel_id.slice(0, -2)));
|
||||||
|
const stat = await this.computeNetworkStats(timestamp, graph);
|
||||||
|
|
||||||
|
existingStatsTimestamps[timestamp] = stat;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Lightning network stats historical import completed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the file content into XML, and return a list of nodes and channels
|
||||||
|
*/
|
||||||
|
private parseFile(fileContent): any {
|
||||||
|
const graph = this.parser.parse(fileContent);
|
||||||
|
if (Object.keys(graph).length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes: Node[] = [];
|
||||||
|
const channels: Channel[] = [];
|
||||||
|
|
||||||
|
// If there is only one entry, the parser does not return an array, so we override this
|
||||||
|
if (!Array.isArray(graph.graphml.graph.node)) {
|
||||||
|
graph.graphml.graph.node = [graph.graphml.graph.node];
|
||||||
|
}
|
||||||
|
if (!Array.isArray(graph.graphml.graph.edge)) {
|
||||||
|
graph.graphml.graph.edge = [graph.graphml.graph.edge];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of graph.graphml.graph.node) {
|
||||||
|
if (!node.data) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const addresses: unknown[] = [];
|
||||||
|
const sockets = node.data[5].split(',');
|
||||||
|
for (const socket of sockets) {
|
||||||
|
const parts = socket.split('://');
|
||||||
|
addresses.push({
|
||||||
|
network: parts[0],
|
||||||
|
addr: parts[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
nodes.push({
|
||||||
|
id: node.data[0],
|
||||||
|
timestamp: node.data[1],
|
||||||
|
features: node.data[2],
|
||||||
|
rgb_color: node.data[3],
|
||||||
|
alias: node.data[4],
|
||||||
|
addresses: addresses,
|
||||||
|
out_degree: node.data[6],
|
||||||
|
in_degree: node.data[7],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const channel of graph.graphml.graph.edge) {
|
||||||
|
if (!channel.data) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
channels.push({
|
||||||
|
channel_id: channel.data[0],
|
||||||
|
node1_pub: channel.data[1],
|
||||||
|
node2_pub: channel.data[2],
|
||||||
|
timestamp: channel.data[3],
|
||||||
|
features: channel.data[4],
|
||||||
|
fee_base_msat: channel.data[5],
|
||||||
|
fee_rate_milli_msat: channel.data[6],
|
||||||
|
htlc_minimim_msat: channel.data[7],
|
||||||
|
cltv_expiry_delta: channel.data[8],
|
||||||
|
htlc_maximum_msat: channel.data[9],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: nodes,
|
||||||
|
edges: channels,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new LightningStatsImporter;
|
@ -1,4 +1,5 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import { Common } from '../api/common';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import PricesRepository from '../repositories/PricesRepository';
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
@ -34,10 +35,10 @@ export interface Prices {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PriceUpdater {
|
class PriceUpdater {
|
||||||
historyInserted: boolean = false;
|
public historyInserted = false;
|
||||||
lastRun: number = 0;
|
lastRun = 0;
|
||||||
lastHistoricalRun: number = 0;
|
lastHistoricalRun = 0;
|
||||||
running: boolean = false;
|
running = false;
|
||||||
feeds: PriceFeed[] = [];
|
feeds: PriceFeed[] = [];
|
||||||
currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
|
currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
|
||||||
latestPrices: Prices;
|
latestPrices: Prices;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<ng-container *ngIf="{ val: network$ | async } as network">
|
<ng-container *ngIf="{ val: network$ | async } as network">
|
||||||
<header>
|
<header>
|
||||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||||
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]">
|
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]">
|
||||||
<ng-template [ngIf]="subdomain">
|
<ng-template [ngIf]="subdomain">
|
||||||
<div class="subdomain_container">
|
<div class="subdomain_container">
|
||||||
<img [src]="'/api/v1/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
|
<img [src]="'/api/v1/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
|
||||||
|
@ -9,6 +9,7 @@ fa-icon {
|
|||||||
.navbar {
|
.navbar {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
min-height: 64px;
|
min-height: 64px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.nav-item {
|
li.nav-item {
|
||||||
@ -86,6 +87,13 @@ li.nav-item {
|
|||||||
height: 65px;
|
height: 65px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-brand.dual-logos {
|
||||||
|
justify-content: space-between;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
box-shadow: 0px 0px 15px 0px #000;
|
box-shadow: 0px 0px 15px 0px #000;
|
||||||
}
|
}
|
||||||
|
@ -2,24 +2,24 @@
|
|||||||
<form [formGroup]="channelStatusForm" class="formRadioGroup float-right">
|
<form [formGroup]="channelStatusForm" class="formRadioGroup float-right">
|
||||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status">
|
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||||
<input ngbButton type="radio" [value]="'open'" fragment="open"> Open
|
<input ngbButton type="radio" [value]="'open'" fragment="open" i18n="open">Open
|
||||||
</label>
|
</label>
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||||
<input ngbButton type="radio" [value]="'closed'" fragment="closed"> Closed
|
<input ngbButton type="radio" [value]="'closed'" fragment="closed" i18n="closed">Closed
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<table class="table table-borderless" *ngIf="response.channels.length > 1">
|
<table class="table table-borderless" *ngIf="response.channels.length > 0">
|
||||||
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
|
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let channel of response.channels; let i = index;">
|
<tr *ngFor="let channel of response.channels; let i = index;">
|
||||||
<ng-container *ngTemplateOutlet="tableTemplate; context: { $implicit: channel, node: channel.node_left.public_key === publicKey ? channel.node_right : channel.node_left }"></ng-container>
|
<ng-container *ngTemplateOutlet="tableTemplate; context: { $implicit: channel, node: channel.node }"></ng-container>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<ngb-pagination *ngIf="response.channels.length > 1" class="pagination-container float-right" [size]="paginationSize" [collectionSize]="response.totalItems" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
<ngb-pagination *ngIf="response.channels.length > 0" class="pagination-container float-right" [size]="paginationSize" [collectionSize]="response.totalItems" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
||||||
|
|
||||||
<table class="table table-borderless" *ngIf="response.channels.length === 0">
|
<table class="table table-borderless" *ngIf="response.channels.length === 0">
|
||||||
<div class="d-flex justify-content-center" i18n="lightning.empty-channels-list">No channels to display</div>
|
<div class="d-flex justify-content-center" i18n="lightning.empty-channels-list">No channels to display</div>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<th class="alias text-left" i18n="nodes.alias">Node Alias</th>
|
<th class="alias text-left" i18n="nodes.alias">Node Alias</th>
|
||||||
<th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction"> </th>
|
<th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction"> </th>
|
||||||
<th class="alias text-left d-none d-md-table-cell" i18n="nodes.alias">Status</th>
|
<th class="alias text-left d-none d-md-table-cell" i18n="status">Status</th>
|
||||||
<th class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th>
|
<th class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th>
|
||||||
<th class="capacity text-right d-none d-md-table-cell" i18n="nodes.capacity">Capacity</th>
|
<th class="capacity text-right d-none d-md-table-cell" i18n="nodes.capacity">Capacity</th>
|
||||||
<th class="capacity text-right" i18n="channels.id">Channel ID</th>
|
<th class="capacity text-right" i18n="channels.id">Channel ID</th>
|
||||||
@ -42,31 +42,41 @@
|
|||||||
<div>{{ node.alias || '?' }}</div>
|
<div>{{ node.alias || '?' }}</div>
|
||||||
<div class="second-line">
|
<div class="second-line">
|
||||||
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
|
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
|
||||||
<span>{{ node.public_key | shortenString : 10 }}</span>
|
<span>{{ node.public_key | shortenString : publicKeySize }}</span>
|
||||||
</a>
|
</a>
|
||||||
<app-clipboard [text]="node.public_key" size="small"></app-clipboard>
|
<app-clipboard [text]="node.public_key" size="small"></app-clipboard>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="alias text-left d-none d-md-table-cell">
|
<td class="alias text-left d-none d-md-table-cell">
|
||||||
<div class="second-line">{{ node.channels }} channels</div>
|
<div class="second-line">{{ node.channels }} channels</div>
|
||||||
<div class="second-line"><app-amount [satoshis]="node.capacity" digitsInfo="1.2-2"></app-amount></div>
|
<div class="second-line">
|
||||||
|
<app-amount *ngIf="node.capacity > 100000000; else smallnode" [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||||
|
<ng-template #smallnode>
|
||||||
|
{{ node.capacity | amountShortener: 1 }}
|
||||||
|
<span class="sats" i18n="shared.sats">sats</span>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-md-table-cell">
|
<td class="d-none d-md-table-cell">
|
||||||
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
|
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0" i18n="lightning.inactive">Inactive</span>
|
||||||
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
|
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1" i18n="lightning.active">Active</span>
|
||||||
<ng-template [ngIf]="channel.status === 2">
|
<ng-template [ngIf]="channel.status === 2">
|
||||||
<span class="badge rounded-pill badge-secondary" *ngIf="!channel.closing_reason; else closingReason">Closed</span>
|
<span class="badge rounded-pill badge-secondary" *ngIf="!channel.closing_reason; else closingReason" i18n="lightning.closed">Closed</span>
|
||||||
<ng-template #closingReason>
|
<ng-template #closingReason>
|
||||||
<app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
<app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
<td class="capacity text-left d-none d-md-table-cell">
|
<td class="capacity text-left d-none d-md-table-cell">
|
||||||
{{ node.fee_rate }} <span class="symbol">ppm ({{ node.fee_rate / 10000 | number }}%)</span>
|
{{ channel.fee_rate }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="capacity text-right d-none d-md-table-cell">
|
<td class="capacity text-right d-none d-md-table-cell">
|
||||||
<app-amount [satoshis]="channel.capacity" digitsInfo="1.2-2"></app-amount>
|
<app-amount *ngIf="channel.capacity > 100000000; else smallchannel" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||||
</td>
|
<ng-template #smallchannel>
|
||||||
|
{{ channel.capacity | amountShortener: 1 }}
|
||||||
|
<span class="sats" i18n="shared.sats">sats</span>
|
||||||
|
</ng-template>
|
||||||
|
</td>
|
||||||
<td class="capacity text-right">
|
<td class="capacity text-right">
|
||||||
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.short_id }}</a>
|
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.short_id }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
.second-line {
|
.second-line {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sats {
|
||||||
|
color: #ffffff66;
|
||||||
|
font-size: 12px;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
import { BehaviorSubject, combineLatest, merge, Observable, of } from 'rxjs';
|
import { BehaviorSubject, merge, Observable } from 'rxjs';
|
||||||
import { map, startWith, switchMap } from 'rxjs/operators';
|
import { map, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { isMobile } from 'src/app/shared/common.utils';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -18,11 +19,13 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
paginationSize: 'sm' | 'lg' = 'md';
|
paginationSize: 'sm' | 'lg' = 'md';
|
||||||
paginationMaxSize = 10;
|
paginationMaxSize = 10;
|
||||||
itemsPerPage = 25;
|
itemsPerPage = 10;
|
||||||
page = 1;
|
page = 1;
|
||||||
channelsPage$ = new BehaviorSubject<number>(1);
|
channelsPage$ = new BehaviorSubject<number>(1);
|
||||||
channelStatusForm: FormGroup;
|
channelStatusForm: FormGroup;
|
||||||
defaultStatus = 'open';
|
defaultStatus = 'open';
|
||||||
|
status = 'open';
|
||||||
|
publicKeySize = 25;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
@ -31,9 +34,12 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
|||||||
this.channelStatusForm = this.formBuilder.group({
|
this.channelStatusForm = this.formBuilder.group({
|
||||||
status: [this.defaultStatus],
|
status: [this.defaultStatus],
|
||||||
});
|
});
|
||||||
|
if (isMobile()) {
|
||||||
|
this.publicKeySize = 12;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
if (document.body.clientWidth < 670) {
|
if (document.body.clientWidth < 670) {
|
||||||
this.paginationSize = 'sm';
|
this.paginationSize = 'sm';
|
||||||
this.paginationMaxSize = 3;
|
this.paginationMaxSize = 3;
|
||||||
@ -41,28 +47,36 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
this.channelStatusForm.get('status').setValue(this.defaultStatus, { emitEvent: false })
|
this.channelStatusForm.get('status').setValue(this.defaultStatus, { emitEvent: false });
|
||||||
this.channelsStatusChangedEvent.emit(this.defaultStatus);
|
this.channelsPage$.next(1);
|
||||||
|
|
||||||
this.channels$ = combineLatest([
|
this.channels$ = merge(
|
||||||
this.channelsPage$,
|
this.channelsPage$,
|
||||||
this.channelStatusForm.get('status').valueChanges.pipe(startWith(this.defaultStatus))
|
this.channelStatusForm.get('status').valueChanges,
|
||||||
])
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(([page, status]) => {
|
tap((val) => {
|
||||||
this.channelsStatusChangedEvent.emit(status);
|
if (typeof val === 'string') {
|
||||||
return this.lightningApiService.getChannelsByNodeId$(this.publicKey, (page -1) * this.itemsPerPage, status);
|
this.status = val;
|
||||||
|
this.page = 1;
|
||||||
|
} else if (typeof val === 'number') {
|
||||||
|
this.page = val;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
switchMap(() => {
|
||||||
|
this.channelsStatusChangedEvent.emit(this.status);
|
||||||
|
return this.lightningApiService.getChannelsByNodeId$(this.publicKey, (this.page - 1) * this.itemsPerPage, this.status);
|
||||||
}),
|
}),
|
||||||
map((response) => {
|
map((response) => {
|
||||||
return {
|
return {
|
||||||
channels: response.body,
|
channels: response.body,
|
||||||
totalItems: parseInt(response.headers.get('x-total-count'), 10)
|
totalItems: parseInt(response.headers.get('x-total-count'), 10) + 1
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pageChange(page: number) {
|
pageChange(page: number): void {
|
||||||
this.channelsPage$.next(page);
|
this.channelsPage$.next(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,9 @@
|
|||||||
<div class="title-container mb-2" *ngIf="!error">
|
<div class="title-container mb-2" *ngIf="!error">
|
||||||
<h1 class="mb-0">{{ node.alias }}</h1>
|
<h1 class="mb-0">{{ node.alias }}</h1>
|
||||||
<span class="tx-link">
|
<span class="tx-link">
|
||||||
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">{{ node.public_key | shortenString : 12
|
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
|
||||||
}}</a>
|
{{ node.public_key | shortenString : publicKeySize }}
|
||||||
|
</a>
|
||||||
<app-clipboard [text]="node.public_key"></app-clipboard>
|
<app-clipboard [text]="node.public_key"></app-clipboard>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -22,23 +23,23 @@
|
|||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="address.total-received">Total capacity</td>
|
<td i18n="lightning.active-capacity">Active capacity</td>
|
||||||
<td>
|
<td>
|
||||||
<app-sats [satoshis]="node.capacity"></app-sats>
|
<app-sats [satoshis]="node.capacity"></app-sats>
|
||||||
<app-fiat [value]="node.capacity" digitsInfo="1.0-0"></app-fiat>
|
<app-fiat [value]="node.capacity" digitsInfo="1.0-0"></app-fiat>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="address.total-sent">Total channels</td>
|
<td i18n="lightning.active-channels">Active channels</td>
|
||||||
<td>
|
<td>
|
||||||
{{ node.channel_active_count }}
|
{{ node.active_channel_count }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="address.total-received">Average channel size</td>
|
<td i18n="lightning.active-channels-avg">Average channel size</td>
|
||||||
<td>
|
<td>
|
||||||
<app-sats [satoshis]="node.channels_capacity_avg"></app-sats>
|
<app-sats [satoshis]="node.avgCapacity"></app-sats>
|
||||||
<app-fiat [value]="node.channels_capacity_avg" digitsInfo="1.0-0"></app-fiat>
|
<app-fiat [value]="node.avgCapacity" digitsInfo="1.0-0"></app-fiat>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="node.country && node.city && node.subdivision">
|
<tr *ngIf="node.country && node.city && node.subdivision">
|
||||||
@ -71,13 +72,13 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td i18n="address.total-received">First seen</td>
|
<td i18n="address.total-received">First seen</td>
|
||||||
<td>
|
<td>
|
||||||
<app-timestamp [dateString]="node.first_seen"></app-timestamp>
|
<app-timestamp [unixTime]="node.first_seen"></app-timestamp>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="address.total-sent">Last update</td>
|
<td i18n="address.total-sent">Last update</td>
|
||||||
<td>
|
<td>
|
||||||
<app-timestamp [dateString]="node.updated_at"></app-timestamp>
|
<app-timestamp [unixTime]="node.updated_at"></app-timestamp>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@ -101,9 +102,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<div class="input-group mt-3" *ngIf="!error && node.socketsObject.length">
|
||||||
|
|
||||||
<div class="input-group mb-3" *ngIf="!error && node.socketsObject.length">
|
|
||||||
<div class="d-inline-block" ngbDropdown #myDrop="ngbDropdown"
|
<div class="d-inline-block" ngbDropdown #myDrop="ngbDropdown"
|
||||||
*ngIf="node.socketsObject.length > 1; else noDropdown">
|
*ngIf="node.socketsObject.length > 1; else noDropdown">
|
||||||
<button class="btn btn-secondary dropdown-toggle" type="button" aria-expanded="false" ngbDropdownAnchor
|
<button class="btn btn-secondary dropdown-toggle" type="button" aria-expanded="false" ngbDropdownAnchor
|
||||||
@ -132,24 +131,16 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key"></app-nodes-channels-map>
|
||||||
|
|
||||||
<app-node-statistics-chart [publicKey]="node.public_key" *ngIf="!error"></app-node-statistics-chart>
|
<app-node-statistics-chart [publicKey]="node.public_key" *ngIf="!error"></app-node-statistics-chart>
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between" *ngIf="!error">
|
<div class="d-flex justify-content-between" *ngIf="!error">
|
||||||
<h2>Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})</h2>
|
<h2>Channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})</h2>
|
||||||
<div class="d-flex justify-content-end">
|
|
||||||
<app-toggle [textLeft]="'List'" [textRight]="'Map'" (toggleStatusChanged)="channelsListModeChange($event)"></app-toggle>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-nodes-channels-map *ngIf="channelsListMode === 'map' && !error" [style]="'nodepage'" [publicKey]="node.public_key">
|
<app-channels-list *ngIf="!error" [publicKey]="node.public_key"
|
||||||
</app-nodes-channels-map>
|
|
||||||
<app-channels-list *ngIf="channelsListMode === 'list' && !error" [publicKey]="node.public_key"
|
|
||||||
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"></app-channels-list>
|
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"></app-channels-list>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
@ -5,6 +5,7 @@ import { catchError, map, switchMap } from 'rxjs/operators';
|
|||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
|
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
|
import { isMobile } from '../../shared/common.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-node',
|
selector: 'app-node',
|
||||||
@ -18,16 +19,21 @@ export class NodeComponent implements OnInit {
|
|||||||
publicKey$: Observable<string>;
|
publicKey$: Observable<string>;
|
||||||
selectedSocketIndex = 0;
|
selectedSocketIndex = 0;
|
||||||
qrCodeVisible = false;
|
qrCodeVisible = false;
|
||||||
channelsListMode = 'list';
|
|
||||||
channelsListStatus: string;
|
channelsListStatus: string;
|
||||||
error: Error;
|
error: Error;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
|
|
||||||
|
publicKeySize = 99;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
) { }
|
) {
|
||||||
|
if (isMobile()) {
|
||||||
|
this.publicKeySize = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.node$ = this.activatedRoute.paramMap
|
this.node$ = this.activatedRoute.paramMap
|
||||||
@ -59,6 +65,7 @@ export class NodeComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
node.socketsObject = socketsObject;
|
node.socketsObject = socketsObject;
|
||||||
|
node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
|
||||||
return node;
|
return node;
|
||||||
}),
|
}),
|
||||||
catchError(err => {
|
catchError(err => {
|
||||||
@ -75,14 +82,6 @@ export class NodeComponent implements OnInit {
|
|||||||
this.selectedSocketIndex = index;
|
this.selectedSocketIndex = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
channelsListModeChange(toggle) {
|
|
||||||
if (toggle === true) {
|
|
||||||
this.channelsListMode = 'map';
|
|
||||||
} else {
|
|
||||||
this.channelsListMode = 'list';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChannelsListStatusChanged(e) {
|
onChannelsListStatusChanged(e) {
|
||||||
this.channelsListStatus = e;
|
this.channelsListStatus = e;
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,6 @@
|
|||||||
<div *ngIf="style === 'graph'" class="card-header">
|
<div *ngIf="style === 'graph'" class="card-header">
|
||||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||||
<span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
|
<span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
|
||||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px">
|
|
||||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true" (click)="onSaveChart()"></fa-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,7 +20,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.full-container.nodepage {
|
.full-container.nodepage {
|
||||||
margin-top: 50px;
|
margin-top: 25px;
|
||||||
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-container.widget {
|
.full-container.widget {
|
||||||
|
@ -3,7 +3,6 @@ import { SeoService } from 'src/app/services/seo.service';
|
|||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
import { Observable, switchMap, tap, zip } from 'rxjs';
|
import { Observable, switchMap, tap, zip } from 'rxjs';
|
||||||
import { AssetsService } from 'src/app/services/assets.service';
|
import { AssetsService } from 'src/app/services/assets.service';
|
||||||
import { download } from 'src/app/shared/graphs.utils';
|
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { StateService } from 'src/app/services/state.service';
|
import { StateService } from 'src/app/services/state.service';
|
||||||
@ -21,6 +20,11 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
@Input() publicKey: string | undefined;
|
@Input() publicKey: string | undefined;
|
||||||
|
|
||||||
observable$: Observable<any>;
|
observable$: Observable<any>;
|
||||||
|
|
||||||
|
center: number[] | undefined;
|
||||||
|
zoom: number | undefined;
|
||||||
|
channelWidth = 0.6;
|
||||||
|
channelOpacity = 0.1;
|
||||||
|
|
||||||
chartInstance = undefined;
|
chartInstance = undefined;
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
@ -42,6 +46,9 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
ngOnDestroy(): void {}
|
ngOnDestroy(): void {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.center = this.style === 'widget' ? [0, 40] : [0, 5];
|
||||||
|
this.zoom = this.style === 'widget' ? 3.5 : 1.3;
|
||||||
|
|
||||||
if (this.style === 'graph') {
|
if (this.style === 'graph') {
|
||||||
this.seoService.setTitle($localize`Lightning nodes channels world map`);
|
this.seoService.setTitle($localize`Lightning nodes channels world map`);
|
||||||
}
|
}
|
||||||
@ -52,31 +59,63 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
return zip(
|
return zip(
|
||||||
this.assetsService.getWorldMapJson$,
|
this.assetsService.getWorldMapJson$,
|
||||||
this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined),
|
this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined),
|
||||||
|
[params.get('public_key') ?? undefined]
|
||||||
).pipe(tap((data) => {
|
).pipe(tap((data) => {
|
||||||
registerMap('world', data[0]);
|
registerMap('world', data[0]);
|
||||||
|
|
||||||
const channelsLoc = [];
|
const channelsLoc = [];
|
||||||
const nodes = [];
|
const nodes = [];
|
||||||
const nodesPubkeys = {};
|
const nodesPubkeys = {};
|
||||||
|
let thisNodeGPS: number[] | undefined = undefined;
|
||||||
for (const channel of data[1]) {
|
for (const channel of data[1]) {
|
||||||
channelsLoc.push([[channel[2], channel[3]], [channel[6], channel[7]]]);
|
if (!thisNodeGPS && data[2] === channel[0]) {
|
||||||
|
thisNodeGPS = [channel[2], channel[3]];
|
||||||
|
} else if (!thisNodeGPS && data[2] === channel[4]) {
|
||||||
|
thisNodeGPS = [channel[6], channel[7]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// We add a bit of noise so nodes at the same location are not all
|
||||||
|
// on top of each other
|
||||||
|
let random = Math.random() * 2 * Math.PI;
|
||||||
|
let random2 = Math.random() * 0.01;
|
||||||
|
|
||||||
if (!nodesPubkeys[channel[0]]) {
|
if (!nodesPubkeys[channel[0]]) {
|
||||||
nodes.push({
|
nodes.push([
|
||||||
publicKey: channel[0],
|
channel[2] + random2 * Math.cos(random),
|
||||||
name: channel[1],
|
channel[3] + random2 * Math.sin(random),
|
||||||
value: [channel[2], channel[3]],
|
1,
|
||||||
});
|
channel[0],
|
||||||
nodesPubkeys[channel[0]] = true;
|
channel[1]
|
||||||
|
]);
|
||||||
|
nodesPubkeys[channel[0]] = nodes[nodes.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
random = Math.random() * 2 * Math.PI;
|
||||||
|
random2 = Math.random() * 0.01;
|
||||||
|
|
||||||
if (!nodesPubkeys[channel[4]]) {
|
if (!nodesPubkeys[channel[4]]) {
|
||||||
nodes.push({
|
nodes.push([
|
||||||
publicKey: channel[4],
|
channel[6] + random2 * Math.cos(random),
|
||||||
name: channel[5],
|
channel[7] + random2 * Math.sin(random),
|
||||||
value: [channel[6], channel[7]],
|
1,
|
||||||
});
|
channel[4],
|
||||||
nodesPubkeys[channel[4]] = true;
|
channel[5]
|
||||||
|
]);
|
||||||
|
nodesPubkeys[channel[4]] = nodes[nodes.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const channelLoc = [];
|
||||||
|
channelLoc.push(nodesPubkeys[channel[0]].slice(0, 2));
|
||||||
|
channelLoc.push(nodesPubkeys[channel[4]].slice(0, 2));
|
||||||
|
channelsLoc.push(channelLoc);
|
||||||
}
|
}
|
||||||
|
if (this.style === 'nodepage' && thisNodeGPS) {
|
||||||
|
this.center = [thisNodeGPS[0], thisNodeGPS[1]];
|
||||||
|
this.zoom = 10;
|
||||||
|
this.channelWidth = 1;
|
||||||
|
this.channelOpacity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
this.prepareChartOptions(nodes, channelsLoc);
|
this.prepareChartOptions(nodes, channelsLoc);
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
@ -98,85 +137,84 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
silent: true,
|
silent: this.style === 'widget',
|
||||||
title: title ?? undefined,
|
title: title ?? undefined,
|
||||||
geo3D: {
|
tooltip: {},
|
||||||
map: 'world',
|
geo: {
|
||||||
shading: 'color',
|
animation: false,
|
||||||
silent: true,
|
silent: true,
|
||||||
postEffect: {
|
center: this.center,
|
||||||
enable: true,
|
zoom: this.zoom,
|
||||||
bloom: {
|
tooltip: {
|
||||||
intensity: 0.1,
|
show: true
|
||||||
}
|
|
||||||
},
|
|
||||||
viewControl: {
|
|
||||||
center: this.style === 'widget' ? [0, 0, -10] : undefined,
|
|
||||||
minDistance: this.style === 'widget' ? 22 : 0.1,
|
|
||||||
maxDistance: this.style === 'widget' ? 22 : 60,
|
|
||||||
distance: this.style === 'widget' ? 22 : 60,
|
|
||||||
alpha: 90,
|
|
||||||
panMouseButton: 'left',
|
|
||||||
rotateMouseButton: undefined,
|
|
||||||
zoomSensivity: 0.5,
|
|
||||||
},
|
},
|
||||||
|
map: 'world',
|
||||||
|
roam: this.style === 'widget' ? false : true,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: 'white',
|
|
||||||
opacity: 0.02,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: 'black',
|
borderColor: 'black',
|
||||||
|
color: '#ffffff44'
|
||||||
},
|
},
|
||||||
regionHeight: 0.01,
|
scaleLimit: {
|
||||||
|
min: 1.3,
|
||||||
|
max: 100000,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
large: true,
|
||||||
type: 'lines3D',
|
progressive: 200,
|
||||||
coordinateSystem: 'geo3D',
|
type: 'scatter',
|
||||||
blendMode: 'lighter',
|
data: nodes,
|
||||||
lineStyle: {
|
coordinateSystem: 'geo',
|
||||||
width: 1,
|
geoIndex: 0,
|
||||||
opacity: ['widget', 'graph'].includes(this.style) ? 0.025 : 1,
|
symbolSize: 4,
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||||
|
borderRadius: 4,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
textStyle: {
|
||||||
|
color: '#b1b1b1',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
borderColor: '#000',
|
||||||
|
formatter: (value) => {
|
||||||
|
const data = value.data;
|
||||||
|
const alias = data[4].length > 0 ? data[4] : data[3].slice(0, 20);
|
||||||
|
return `<b style="color: white">${alias}</b>`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data: channels
|
itemStyle: {
|
||||||
|
color: 'white',
|
||||||
|
borderColor: 'black',
|
||||||
|
borderWidth: 2,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
blendMode: 'lighter',
|
||||||
|
zlevel: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
large: true,
|
||||||
type: 'scatter3D',
|
progressive: 200,
|
||||||
symbol: 'circle',
|
silent: true,
|
||||||
blendMode: 'lighter',
|
type: 'lines',
|
||||||
coordinateSystem: 'geo3D',
|
coordinateSystem: 'geo',
|
||||||
symbolSize: 3,
|
data: channels,
|
||||||
itemStyle: {
|
lineStyle: {
|
||||||
color: '#BBFFFF',
|
opacity: this.channelOpacity,
|
||||||
opacity: 1,
|
width: this.channelWidth,
|
||||||
borderColor: '#FFFFFF00',
|
curveness: 0,
|
||||||
|
color: '#466d9d',
|
||||||
},
|
},
|
||||||
data: nodes,
|
blendMode: 'lighter',
|
||||||
emphasis: {
|
tooltip: {
|
||||||
label: {
|
show: false,
|
||||||
position: 'top',
|
},
|
||||||
color: 'white',
|
zlevel: 2,
|
||||||
fontSize: 16,
|
}
|
||||||
formatter: function(value) {
|
|
||||||
return value.name;
|
|
||||||
},
|
|
||||||
show: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('window:wheel', ['$event'])
|
|
||||||
onWindowScroll(e): void {
|
|
||||||
// Not very smooth when using the mouse
|
|
||||||
if (this.style === 'widget' && e.target.tagName === 'CANVAS') {
|
|
||||||
window.scrollBy({left: 0, top: e.deltaY, behavior: 'auto'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChartInit(ec) {
|
onChartInit(ec) {
|
||||||
if (this.chartInstance !== undefined) {
|
if (this.chartInstance !== undefined) {
|
||||||
return;
|
return;
|
||||||
@ -192,32 +230,34 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.chartInstance.on('click', (e) => {
|
this.chartInstance.on('click', (e) => {
|
||||||
if (e.data && e.data.publicKey) {
|
if (e.data) {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/node/${e.data.publicKey}`);
|
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/node/${e.data[3]}`);
|
||||||
this.router.navigate([url]);
|
this.router.navigate([url]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
onSaveChart() {
|
this.chartInstance.on('georoam', (e) => {
|
||||||
// @ts-ignore
|
if (!e.zoom || this.style === 'nodepage') {
|
||||||
const prevBottom = this.chartOptions.grid.bottom;
|
return;
|
||||||
const now = new Date();
|
}
|
||||||
// @ts-ignore
|
|
||||||
this.chartOptions.grid.bottom = 30;
|
const speed = 0.005;
|
||||||
this.chartOptions.backgroundColor = '#11131f';
|
const chartOptions = {
|
||||||
this.chartInstance.setOption(this.chartOptions);
|
series: this.chartOptions.series
|
||||||
download(this.chartInstance.getDataURL({
|
};
|
||||||
pixelRatio: 2,
|
|
||||||
excludeComponents: ['dataZoom'],
|
chartOptions.series[1].lineStyle.opacity += e.zoom > 1 ? speed : -speed;
|
||||||
}), `lightning-nodes-heatmap-clearnet-${Math.round(now.getTime() / 1000)}.svg`);
|
chartOptions.series[1].lineStyle.width += e.zoom > 1 ? speed : -speed;
|
||||||
// @ts-ignore
|
chartOptions.series[0].symbolSize += e.zoom > 1 ? speed * 10 : -speed * 10;
|
||||||
this.chartOptions.grid.bottom = prevBottom;
|
chartOptions.series[1].lineStyle.opacity = Math.max(0.05, Math.min(0.5, chartOptions.series[1].lineStyle.opacity));
|
||||||
this.chartOptions.backgroundColor = 'none';
|
chartOptions.series[1].lineStyle.width = Math.max(0.5, Math.min(1, chartOptions.series[1].lineStyle.width));
|
||||||
this.chartInstance.setOption(this.chartOptions);
|
chartOptions.series[0].symbolSize = Math.max(4, Math.min(5.5, chartOptions.series[0].symbolSize));
|
||||||
|
|
||||||
|
this.chartInstance.setOption(chartOptions);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ export class EnterpriseService {
|
|||||||
this.subdomain = subdomain;
|
this.subdomain = subdomain;
|
||||||
this.fetchSubdomainInfo();
|
this.fetchSubdomainInfo();
|
||||||
this.disableSubnetworks();
|
this.disableSubnetworks();
|
||||||
} else {
|
} else if (document.location.hostname === 'mempool.space') {
|
||||||
this.insertMatomo();
|
this.insertMatomo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,7 +49,7 @@ export class EnterpriseService {
|
|||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
window.location.href = 'https://mempool.space';
|
window.location.href = 'https://mempool.space' + window.location.pathname;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
3
frontend/src/app/shared/common.utils.ts
Normal file
3
frontend/src/app/shared/common.utils.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function isMobile() {
|
||||||
|
return (window.innerWidth <= 767.98);
|
||||||
|
}
|
@ -1,6 +1,10 @@
|
|||||||
|
# start elements on reboot
|
||||||
@reboot sleep 60 ; /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1
|
@reboot sleep 60 ; /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1
|
||||||
@reboot sleep 60 ; /usr/local/bin/elementsd -chain=liquidtestnet >/dev/null 2>&1
|
@reboot sleep 60 ; /usr/local/bin/elementsd -chain=liquidtestnet >/dev/null 2>&1
|
||||||
|
|
||||||
|
# start electrs on reboot
|
||||||
@reboot sleep 90 ; screen -dmS liquidv1 /elements/electrs/electrs-start-liquid
|
@reboot sleep 90 ; screen -dmS liquidv1 /elements/electrs/electrs-start-liquid
|
||||||
@reboot sleep 90 ; screen -dmS liquidtestnet /elements/electrs/electrs-start-liquidtestnet
|
@reboot sleep 90 ; screen -dmS liquidtestnet /elements/electrs/electrs-start-liquidtestnet
|
||||||
6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1
|
|
||||||
6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1
|
# hourly asset update and electrs restart
|
||||||
|
6 * * * * cd $HOME/asset_registry_db && git pull --quiet origin master && cd $HOME/asset_registry_testnet_db && git pull --quiet origin master && killall electrs
|
||||||
|
@ -40,6 +40,9 @@ BISQ_INSTALL=ON
|
|||||||
ELEMENTS_INSTALL=ON
|
ELEMENTS_INSTALL=ON
|
||||||
CLN_INSTALL=ON
|
CLN_INSTALL=ON
|
||||||
|
|
||||||
|
# install UNFURL
|
||||||
|
UNFURL_INSTALL=ON
|
||||||
|
|
||||||
# configure 4 network instances
|
# configure 4 network instances
|
||||||
BITCOIN_MAINNET_ENABLE=ON
|
BITCOIN_MAINNET_ENABLE=ON
|
||||||
BITCOIN_MAINNET_MINFEE_ENABLE=ON
|
BITCOIN_MAINNET_MINFEE_ENABLE=ON
|
||||||
@ -50,8 +53,10 @@ ELEMENTS_LIQUID_ENABLE=ON
|
|||||||
ELEMENTS_LIQUIDTESTNET_ENABLE=ON
|
ELEMENTS_LIQUIDTESTNET_ENABLE=ON
|
||||||
|
|
||||||
# enable lightmode and disable compaction to fit on 1TB SSD drive
|
# enable lightmode and disable compaction to fit on 1TB SSD drive
|
||||||
|
BITCOIN_ELECTRS_INSTALL=ON
|
||||||
BITCOIN_ELECTRS_LIGHT_MODE=ON
|
BITCOIN_ELECTRS_LIGHT_MODE=ON
|
||||||
BITCOIN_ELECTRS_COMPACTION=OFF
|
BITCOIN_ELECTRS_COMPACTION=OFF
|
||||||
|
ELEMENTS_ELECTRS_INSTALL=ON
|
||||||
ELEMENTS_ELECTRS_LIGHT_MODE=ON
|
ELEMENTS_ELECTRS_LIGHT_MODE=ON
|
||||||
ELEMENTS_ELECTRS_COMPACTION=OFF
|
ELEMENTS_ELECTRS_COMPACTION=OFF
|
||||||
|
|
||||||
@ -178,7 +183,6 @@ case $OS in
|
|||||||
ROOT_USER=root
|
ROOT_USER=root
|
||||||
ROOT_GROUP=wheel
|
ROOT_GROUP=wheel
|
||||||
ROOT_HOME=/root
|
ROOT_HOME=/root
|
||||||
TOR_HOME=/var/db/tor
|
|
||||||
TOR_CONFIGURATION=/usr/local/etc/tor/torrc
|
TOR_CONFIGURATION=/usr/local/etc/tor/torrc
|
||||||
TOR_RESOURCES=/var/db/tor
|
TOR_RESOURCES=/var/db/tor
|
||||||
TOR_PKG=tor
|
TOR_PKG=tor
|
||||||
@ -195,7 +199,6 @@ case $OS in
|
|||||||
ROOT_USER=root
|
ROOT_USER=root
|
||||||
ROOT_GROUP=root
|
ROOT_GROUP=root
|
||||||
ROOT_HOME=/root
|
ROOT_HOME=/root
|
||||||
TOR_HOME=/etc/tor
|
|
||||||
TOR_CONFIGURATION=/etc/tor/torrc
|
TOR_CONFIGURATION=/etc/tor/torrc
|
||||||
TOR_RESOURCES=/var/lib/tor
|
TOR_RESOURCES=/var/lib/tor
|
||||||
TOR_PKG=tor
|
TOR_PKG=tor
|
||||||
@ -285,6 +288,14 @@ BISQ_USER=bisq
|
|||||||
BISQ_GROUP=bisq
|
BISQ_GROUP=bisq
|
||||||
# bisq home folder, needs about 1GB
|
# bisq home folder, needs about 1GB
|
||||||
BISQ_HOME=/bisq
|
BISQ_HOME=/bisq
|
||||||
|
# tor HS folder
|
||||||
|
BISQ_TOR_HS=bisq
|
||||||
|
|
||||||
|
# Unfurl user/group
|
||||||
|
UNFURL_USER=unfurl
|
||||||
|
UNFURL_GROUP=unfurl
|
||||||
|
# Unfurl home folder
|
||||||
|
UNFURL_HOME=/unfurl
|
||||||
|
|
||||||
# liquid user/group
|
# liquid user/group
|
||||||
ELEMENTS_USER=elements
|
ELEMENTS_USER=elements
|
||||||
@ -295,6 +306,8 @@ ELEMENTS_HOME=/elements
|
|||||||
ELECTRS_HOME=/electrs
|
ELECTRS_HOME=/electrs
|
||||||
# elements electrs source/binaries
|
# elements electrs source/binaries
|
||||||
ELEMENTS_ELECTRS_HOME=${ELEMENTS_HOME}/electrs
|
ELEMENTS_ELECTRS_HOME=${ELEMENTS_HOME}/electrs
|
||||||
|
# tor HS folder
|
||||||
|
LIQUID_TOR_HS=liquid
|
||||||
|
|
||||||
# minfee user/group
|
# minfee user/group
|
||||||
MINFEE_USER=minfee
|
MINFEE_USER=minfee
|
||||||
@ -323,6 +336,13 @@ BISQ_REPO_BRANCH=master
|
|||||||
BISQ_LATEST_RELEASE=master
|
BISQ_LATEST_RELEASE=master
|
||||||
echo -n '.'
|
echo -n '.'
|
||||||
|
|
||||||
|
UNFURL_REPO_URL=https://github.com/mempool/mempool
|
||||||
|
UNFURL_REPO_NAME=unfurl
|
||||||
|
UNFURL_REPO_BRANCH=master
|
||||||
|
#UNFURL_LATEST_RELEASE=$(curl -s https://api.github.com/repos/mempool/mempool/releases/latest|grep tag_name|head -1|cut -d '"' -f4)
|
||||||
|
UNFURL_LATEST_RELEASE=master
|
||||||
|
echo -n '.'
|
||||||
|
|
||||||
ELEMENTS_REPO_URL=https://github.com/ElementsProject/elements
|
ELEMENTS_REPO_URL=https://github.com/ElementsProject/elements
|
||||||
ELEMENTS_REPO_NAME=elements
|
ELEMENTS_REPO_NAME=elements
|
||||||
ELEMENTS_REPO_BRANCH=master
|
ELEMENTS_REPO_BRANCH=master
|
||||||
@ -359,6 +379,10 @@ DEBIAN_PKG+=(libboost-system-dev libboost-filesystem-dev libboost-chrono-dev lib
|
|||||||
DEBIAN_PKG+=(nodejs npm mariadb-server nginx-core python3-certbot-nginx rsync ufw)
|
DEBIAN_PKG+=(nodejs npm mariadb-server nginx-core python3-certbot-nginx rsync ufw)
|
||||||
DEBIAN_PKG+=(geoipupdate)
|
DEBIAN_PKG+=(geoipupdate)
|
||||||
|
|
||||||
|
DEBIAN_UNFURL_PKG=()
|
||||||
|
DEBIAN_UNFURL_PKG+=(cups chromium-bsu libatk1.0 libatk-bridge2.0 libxkbcommon-dev libxcomposite-dev)
|
||||||
|
DEBIAN_UNFURL_PKG+=(libxdamage-dev libxrandr-dev libgbm-dev libpango1.0-dev libasound-dev)
|
||||||
|
|
||||||
# packages needed for mempool ecosystem
|
# packages needed for mempool ecosystem
|
||||||
FREEBSD_PKG=()
|
FREEBSD_PKG=()
|
||||||
FREEBSD_PKG+=(zsh sudo git screen curl wget calc neovim)
|
FREEBSD_PKG+=(zsh sudo git screen curl wget calc neovim)
|
||||||
@ -729,6 +753,7 @@ Liquid:Enable Elements Liquid:ON
|
|||||||
Liquidtestnet:Enable Elements Liquidtestnet:ON
|
Liquidtestnet:Enable Elements Liquidtestnet:ON
|
||||||
CoreLN:Enable Core Lightning:ON
|
CoreLN:Enable Core Lightning:ON
|
||||||
Bisq:Enable Bisq:ON
|
Bisq:Enable Bisq:ON
|
||||||
|
Unfurl:Enable Unfurl:ON
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat $input | sed -e 's/^/"/' -e 's/:/" "/g' -e 's/$/"/' >$output
|
cat $input | sed -e 's/^/"/' -e 's/:/" "/g' -e 's/$/"/' >$output
|
||||||
@ -806,6 +831,17 @@ if grep CoreLN $tempfile >/dev/null 2>&1;then
|
|||||||
CLN_INSTALL=ON
|
CLN_INSTALL=ON
|
||||||
else
|
else
|
||||||
CLN_INSTALL=OFF
|
CLN_INSTALL=OFF
|
||||||
|
|
||||||
|
if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
||||||
|
BITCOIN_ELECTRS_INSTALL=ON
|
||||||
|
else
|
||||||
|
BITCOIN_ELECTRS_INSTALL=OFF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON -o "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
||||||
|
ELEMENTS_ELECTRS_INSTALL=ON
|
||||||
|
else
|
||||||
|
ELEMENTS_ELECTRS_INSTALL=OFF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if grep Bisq $tempfile >/dev/null 2>&1;then
|
if grep Bisq $tempfile >/dev/null 2>&1;then
|
||||||
@ -816,6 +852,12 @@ else
|
|||||||
BISQ_MAINNET_ENABLE=OFF
|
BISQ_MAINNET_ENABLE=OFF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if grep Unfurl $tempfile >/dev/null 2>&1;then
|
||||||
|
UNFURL_INSTALL=ON
|
||||||
|
else
|
||||||
|
UNFURL_INSTALL=OFF
|
||||||
|
fi
|
||||||
|
|
||||||
##################
|
##################
|
||||||
## dialog part 2 #
|
## dialog part 2 #
|
||||||
##################
|
##################
|
||||||
@ -963,15 +1005,34 @@ if [ "${TOR_INSTALL}" = ON ];then
|
|||||||
osPackageInstall "${TOR_PKG}"
|
osPackageInstall "${TOR_PKG}"
|
||||||
|
|
||||||
echo "[*] Installing Tor base configuration"
|
echo "[*] Installing Tor base configuration"
|
||||||
osSudo "${ROOT_USER}" install -c -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/torrc" "${TOR_HOME}/torrc"
|
osSudo "${ROOT_USER}" install -c -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/torrc" "${TOR_CONFIGURATION}"
|
||||||
|
osSudo "${ROOT_USER}" sed -i.orig "s!__TOR_RESOURCES__!${TOR_RESOURCES}!" "${TOR_CONFIGURATION}"
|
||||||
|
|
||||||
echo "[*] Adding Tor HS configuration"
|
echo "[*] Adding Tor HS configuration for Mempool"
|
||||||
if ! grep "${MEMPOOL_TOR_HS}" /etc/tor/torrc >/dev/null 2>&1;then
|
if [ "${MEMPOOL_ENABLE}" = "ON" ];then
|
||||||
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${MEMPOOL_TOR_HS}/ >> ${TOR_CONFIGURATION}"
|
if ! grep "${MEMPOOL_TOR_HS}" "${TOR_CONFIGURATION}" >/dev/null 2>&1;then
|
||||||
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServicePort 80 127.0.0.1:81 >> ${TOR_CONFIGURATION}"
|
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${MEMPOOL_TOR_HS}/ >> ${TOR_CONFIGURATION}"
|
||||||
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONFIGURATION}"
|
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServicePort 80 127.0.0.1:81 >> ${TOR_CONFIGURATION}"
|
||||||
else
|
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONFIGURATION}"
|
||||||
osSudo "${ROOT_USER}" sed -i.orig "s!__TOR_RESOURCES__!${TOR_RESOURCES}!" "${TOR_CONFIGURATION}"
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Adding Tor HS configuration for Bisq"
|
||||||
|
if [ "${BISQ_ENABLE}" = "ON" ];then
|
||||||
|
if ! grep "${BISQ_TOR_HS}" "${TOR_CONFIGURATION}" >/dev/null 2>&1;then
|
||||||
|
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${BISQ_TOR_HS}/ >> ${TOR_CONFIGURATION}"
|
||||||
|
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServicePort 80 127.0.0.1:82 >> ${TOR_CONFIGURATION}"
|
||||||
|
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONFIGURATION}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Adding Tor HS configuration for Liquid"
|
||||||
|
if [ "${LIQUID_ENABLE}" = "ON" ];then
|
||||||
|
if ! grep "${LIQUID_TOR_HS}" "${TOR_CONFIGURATION}" >/dev/null 2>&1;then
|
||||||
|
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${LIQUID_TOR_HS}/ >> ${TOR_CONFIGURATION}"
|
||||||
|
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServicePort 80 127.0.0.1:83 >> ${TOR_CONFIGURATION}"
|
||||||
|
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONFIGURATION}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case $OS in
|
case $OS in
|
||||||
@ -1097,65 +1158,72 @@ fi
|
|||||||
# Bitcoin -> Electrs installation #
|
# Bitcoin -> Electrs installation #
|
||||||
###################################
|
###################################
|
||||||
|
|
||||||
echo "[*] Creating Bitcoin Electrs data folder"
|
if [ "${BITCOIN_ELECTRS_INSTALL}" = ON ];then
|
||||||
osSudo "${ROOT_USER}" mkdir -p "${BITCOIN_ELECTRS_HOME}"
|
|
||||||
osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${BITCOIN_ELECTRS_HOME}"
|
echo "[*] Creating Bitcoin Electrs data folder"
|
||||||
if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
|
osSudo "${ROOT_USER}" mkdir -p "${BITCOIN_ELECTRS_HOME}"
|
||||||
osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_MAINNET_DATA}"
|
osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${BITCOIN_ELECTRS_HOME}"
|
||||||
|
if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
|
||||||
|
osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_MAINNET_DATA}"
|
||||||
|
fi
|
||||||
|
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
|
||||||
|
osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_TESTNET_DATA}"
|
||||||
|
fi
|
||||||
|
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
||||||
|
osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_SIGNET_DATA}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Cloning Bitcoin Electrs repo from ${BITCOIN_ELECTRS_REPO_URL}"
|
||||||
|
osSudo "${BITCOIN_USER}" git config --global advice.detachedHead false
|
||||||
|
osSudo "${BITCOIN_USER}" git clone --branch "${BITCOIN_ELECTRS_REPO_BRANCH}" "${BITCOIN_ELECTRS_REPO_URL}" "${BITCOIN_HOME}/${BITCOIN_ELECTRS_REPO_NAME}"
|
||||||
|
|
||||||
|
echo "[*] Checking out Electrs ${BITCOIN_ELECTRS_LATEST_RELEASE}"
|
||||||
|
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_HOME}/${BITCOIN_ELECTRS_REPO_NAME} && git checkout ${BITCOIN_ELECTRS_LATEST_RELEASE}"
|
||||||
|
|
||||||
|
case $OS in
|
||||||
|
FreeBSD)
|
||||||
|
echo "[*] Installing Rust from pkg install"
|
||||||
|
;;
|
||||||
|
Debian)
|
||||||
|
echo "[*] Installing Rust from rustup.rs"
|
||||||
|
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "[*] Building Bitcoin Electrs release binary"
|
||||||
|
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version" || true
|
||||||
|
|
||||||
|
case $OS in
|
||||||
|
FreeBSD)
|
||||||
|
echo "[*] Patching Bitcoin Electrs code for FreeBSD"
|
||||||
|
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\""
|
||||||
|
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i.bak -e s/Snappy/None/ db.rs && rm db.rs.bak"
|
||||||
|
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i.bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak"
|
||||||
|
;;
|
||||||
|
Debian)
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "[*] Building Bitcoin Electrs release binary"
|
||||||
|
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version"
|
||||||
fi
|
fi
|
||||||
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
|
|
||||||
osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_TESTNET_DATA}"
|
|
||||||
fi
|
|
||||||
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
|
||||||
osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_SIGNET_DATA}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[*] Cloning Bitcoin Electrs repo from ${BITCOIN_ELECTRS_REPO_URL}"
|
|
||||||
osSudo "${BITCOIN_USER}" git config --global advice.detachedHead false
|
|
||||||
osSudo "${BITCOIN_USER}" git clone --branch "${BITCOIN_ELECTRS_REPO_BRANCH}" "${BITCOIN_ELECTRS_REPO_URL}" "${BITCOIN_HOME}/${BITCOIN_ELECTRS_REPO_NAME}"
|
|
||||||
|
|
||||||
echo "[*] Checking out Electrs ${BITCOIN_ELECTRS_LATEST_RELEASE}"
|
|
||||||
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_HOME}/${BITCOIN_ELECTRS_REPO_NAME} && git checkout ${BITCOIN_ELECTRS_LATEST_RELEASE}"
|
|
||||||
|
|
||||||
case $OS in
|
|
||||||
FreeBSD)
|
|
||||||
echo "[*] Installing Rust from pkg install"
|
|
||||||
;;
|
|
||||||
Debian)
|
|
||||||
echo "[*] Installing Rust from rustup.rs"
|
|
||||||
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "[*] Building Bitcoin Electrs release binary"
|
|
||||||
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version" || true
|
|
||||||
|
|
||||||
case $OS in
|
|
||||||
FreeBSD)
|
|
||||||
echo "[*] Patching Bitcoin Electrs code for FreeBSD"
|
|
||||||
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\""
|
|
||||||
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i.bak -e s/Snappy/None/ db.rs && rm db.rs.bak"
|
|
||||||
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i.bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak"
|
|
||||||
;;
|
|
||||||
Debian)
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "[*] Building Bitcoin Electrs release binary"
|
|
||||||
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version"
|
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
# Liquid -> Electrs installation #
|
# Liquid -> Electrs installation #
|
||||||
##################################
|
##################################
|
||||||
|
|
||||||
if [ "${ELEMENTS_INSTALL}" = ON ;then
|
if [ "${ELEMENTS_ELECTRS_INSTALL}" = ON ];then
|
||||||
|
|
||||||
echo "[*] Creating Liquid Electrs data folder"
|
echo "[*] Creating Liquid Electrs data folder"
|
||||||
osSudo "${ROOT_USER}" mkdir -p "${ELEMENTS_ELECTRS_HOME}"
|
osSudo "${ROOT_USER}" mkdir -p "${ELEMENTS_ELECTRS_HOME}"
|
||||||
osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELEMENTS_HOME}"
|
osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELEMENTS_HOME}"
|
||||||
osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELEMENTS_ELECTRS_HOME}"
|
osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELEMENTS_ELECTRS_HOME}"
|
||||||
osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUID_DATA}"
|
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
||||||
osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUIDTESTNET_DATA}"
|
osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUID_DATA}"
|
||||||
|
fi
|
||||||
|
if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
||||||
|
osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUIDTESTNET_DATA}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[*] Cloning Liquid Electrs repo from ${ELEMENTS_ELECTRS_REPO_URL}"
|
echo "[*] Cloning Liquid Electrs repo from ${ELEMENTS_ELECTRS_REPO_URL}"
|
||||||
osSudo "${ELEMENTS_USER}" git config --global advice.detachedHead false
|
osSudo "${ELEMENTS_USER}" git config --global advice.detachedHead false
|
||||||
@ -1296,6 +1364,50 @@ if [ "${BISQ_INSTALL}" = ON ];then
|
|||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
#######################
|
||||||
|
# Unfurl installation #
|
||||||
|
#######################
|
||||||
|
|
||||||
|
if [ "${UNFURL_INSTALL}" = ON ];then
|
||||||
|
|
||||||
|
echo "[*] Creating Unfurl user"
|
||||||
|
osGroupCreate "${UNFURL_GROUP}"
|
||||||
|
osUserCreate "${UNFURL_USER}" "${UNFURL_HOME}" "${UNFURL_GROUP}"
|
||||||
|
osSudo "${ROOT_USER}" chsh -s `which zsh` "${UNFURL_USER}"
|
||||||
|
|
||||||
|
echo "[*] Creating Unfurl folder"
|
||||||
|
osSudo "${ROOT_USER}" mkdir -p "${UNFURL_HOME}"
|
||||||
|
osSudo "${ROOT_USER}" chown -R "${UNFURL_USER}:${UNFURL_GROUP}" "${UNFURL_HOME}"
|
||||||
|
osSudo "${UNFURL_USER}" touch "${UNFURL_HOME}/.zshrc"
|
||||||
|
|
||||||
|
echo "[*] Insalling Unfurl source"
|
||||||
|
case $OS in
|
||||||
|
|
||||||
|
FreeBSD)
|
||||||
|
echo "[*] FIXME: Unfurl must be installed manually on FreeBSD"
|
||||||
|
;;
|
||||||
|
|
||||||
|
Debian)
|
||||||
|
echo "[*] Installing packages for Unfurl"
|
||||||
|
osPackageInstall ${DEBIAN_UNFURL_PKG[@]}
|
||||||
|
echo "[*] Cloning Mempool (Unfurl) repo from ${UNFURL_REPO_URL}"
|
||||||
|
osSudo "${UNFURL_USER}" git config --global pull.rebase true
|
||||||
|
osSudo "${UNFURL_USER}" git config --global advice.detachedHead false
|
||||||
|
osSudo "${UNFURL_USER}" git clone --branch "${UNFURL_REPO_BRANCH}" "${UNFURL_REPO_URL}" "${UNFURL_HOME}/${UNFURL_REPO_NAME}"
|
||||||
|
osSudo "${UNFURL_USER}" ln -s unfurl/production/unfurl-build upgrade
|
||||||
|
osSudo "${UNFURL_USER}" ln -s unfurl/production/unfurl-kill stop
|
||||||
|
osSudo "${UNFURL_USER}" ln -s unfurl/production/unfurl-start start
|
||||||
|
echo "[*] Installing nvm.sh from GitHub"
|
||||||
|
osSudo "${UNFURL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh'
|
||||||
|
|
||||||
|
echo "[*] Building NodeJS via nvm.sh"
|
||||||
|
osSudo "${UNFURL_USER}" zsh -c 'source ~/.zshrc ; nvm install v16.16.0 --shared-zlib'
|
||||||
|
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
################################
|
################################
|
||||||
# Bitcoin instance for Mainnet #
|
# Bitcoin instance for Mainnet #
|
||||||
################################
|
################################
|
||||||
|
13
production/mempool-config.unfurl.json
Normal file
13
production/mempool-config.unfurl.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"SERVER": {
|
||||||
|
"HOST": "https://mempool.space",
|
||||||
|
"HTTP_PORT": 8001
|
||||||
|
},
|
||||||
|
"MEMPOOL": {
|
||||||
|
"HTTP_HOST": "https://mempool.space",
|
||||||
|
"HTTP_PORT": 443
|
||||||
|
},
|
||||||
|
"PUPPETEER": {
|
||||||
|
"CLUSTER_SIZE": 8
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,12 @@
|
|||||||
|
# start on reboot
|
||||||
@reboot sleep 10 ; $HOME/start
|
@reboot sleep 10 ; $HOME/start
|
||||||
37 13 * * * sleep 30 ; /mempool/mempool.space/backup >/dev/null 2>&1 &
|
|
||||||
|
# start cache warmer on reboot
|
||||||
@reboot sleep 180 ; /mempool/mempool/production/nginx-cache-warmer >/dev/null 2>&1 &
|
@reboot sleep 180 ; /mempool/mempool/production/nginx-cache-warmer >/dev/null 2>&1 &
|
||||||
|
|
||||||
|
# daily backup
|
||||||
|
37 13 * * * sleep 30 ; /mempool/mempool.space/backup >/dev/null 2>&1 &
|
||||||
|
|
||||||
|
# hourly liquid asset update
|
||||||
|
6 * * * * cd $HOME/liquid/frontend && npm run sync-assets && rsync -av $HOME/liquid/frontend/dist/mempool/browser/en-US/resources/assets* $HOME/public_html/liquid/en-US/resources/ >/dev/null 2>&1
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
/var/log/mempool 640 10 * @T00 C
|
/var/log/mempool 644 10 * @T00 C
|
||||||
/var/log/mempool.debug 640 10 * @T00 C
|
/var/log/mempool.debug 644 10 * @T00 C
|
||||||
|
@ -13,11 +13,3 @@ CookieAuthFileGroupReadable 1
|
|||||||
HiddenServiceDir __TOR_RESOURCES__/mempool
|
HiddenServiceDir __TOR_RESOURCES__/mempool
|
||||||
HiddenServicePort 80 127.0.0.1:81
|
HiddenServicePort 80 127.0.0.1:81
|
||||||
HiddenServiceVersion 3
|
HiddenServiceVersion 3
|
||||||
|
|
||||||
HiddenServiceDir __TOR_RESOURCES__/bisq
|
|
||||||
HiddenServicePort 80 127.0.0.1:82
|
|
||||||
HiddenServiceVersion 3
|
|
||||||
|
|
||||||
HiddenServiceDir __TOR_RESOURCES__/liquid
|
|
||||||
HiddenServicePort 80 127.0.0.1:83
|
|
||||||
HiddenServiceVersion 3
|
|
||||||
|
62
production/unfurl-build
Executable file
62
production/unfurl-build
Executable file
@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:$HOME/bin
|
||||||
|
HOSTNAME=$(hostname)
|
||||||
|
LOCATION=$(hostname|cut -d . -f2)
|
||||||
|
LOCKFILE="${HOME}/lock"
|
||||||
|
REF=$(echo "${1:=origin/master}"|sed -e 's!:!/!')
|
||||||
|
|
||||||
|
if [ -f "${LOCKFILE}" ];then
|
||||||
|
echo "upgrade already running? check lockfile ${LOCKFILE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# on exit, remove lockfile but preserve exit code
|
||||||
|
trap "rv=\$?; rm -f "${LOCKFILE}"; exit \$rv" INT TERM EXIT
|
||||||
|
|
||||||
|
# create lockfile
|
||||||
|
touch "${LOCKFILE}"
|
||||||
|
|
||||||
|
# notify logged in users
|
||||||
|
echo "Upgrading unfurler to ${REF}" | wall
|
||||||
|
|
||||||
|
update_repo()
|
||||||
|
{
|
||||||
|
echo "[*] Upgrading unfurler to ${REF}"
|
||||||
|
cd "$HOME/unfurl/unfurler" || exit 1
|
||||||
|
|
||||||
|
git fetch origin || exit 1
|
||||||
|
for remote in origin;do
|
||||||
|
git remote add "${remote}" "https://github.com/${remote}/mempool" >/dev/null 2>&1
|
||||||
|
git fetch "${remote}" || exit 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $(git tag -l "${REF}") ];then
|
||||||
|
git reset --hard "tags/${REF}" || exit 1
|
||||||
|
elif [ $(git branch -r -l "origin/${REF}") ];then
|
||||||
|
git reset --hard "origin/${REF}" || exit 1
|
||||||
|
else
|
||||||
|
git reset --hard "${REF}" || exit 1
|
||||||
|
fi
|
||||||
|
export HASH=$(git rev-parse HEAD)
|
||||||
|
}
|
||||||
|
|
||||||
|
build_backend()
|
||||||
|
{
|
||||||
|
echo "[*] Building backend for unfurler"
|
||||||
|
[ -z "${HASH}" ] && exit 1
|
||||||
|
cd "$HOME/unfurl/unfurler" || exit 1
|
||||||
|
if [ ! -e "config.json" ];then
|
||||||
|
cp "${HOME}/unfurl/production/mempool-config.unfurl.json" "config.json"
|
||||||
|
fi
|
||||||
|
npm install || exit 1
|
||||||
|
npm run build || exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
update_repo
|
||||||
|
build_backend
|
||||||
|
|
||||||
|
# notify everyone
|
||||||
|
echo "${HOSTNAME} unfurl updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general mempool.dev
|
||||||
|
echo "${HOSTNAME} unfurl updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general "mempool.ops.${LOCATION}"
|
||||||
|
|
||||||
|
exit 0
|
2
production/unfurl-kill
Executable file
2
production/unfurl-kill
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
killall sh node
|
6
production/unfurl-start
Executable file
6
production/unfurl-start
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
export NVM_DIR="$HOME/.nvm"
|
||||||
|
source "$NVM_DIR/nvm.sh"
|
||||||
|
|
||||||
|
cd "${HOME}/unfurl/unfurler/" && \
|
||||||
|
screen -dmS "unfurl" sh -c 'while true;do npm run start-production;sleep 1;done'
|
Loading…
x
Reference in New Issue
Block a user