Merge branch 'master' into nymkappa/bugfix/missing-variable-ln
This commit is contained in:
commit
14bf256ab8
@ -248,7 +248,6 @@ class DatabaseMigration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 25 && isBitcoin === true) {
|
if (databaseSchemaVersion < 25 && isBitcoin === true) {
|
||||||
await this.$executeQuery(`INSERT INTO state VALUES('last_node_stats', 0, '1970-01-01');`);
|
|
||||||
await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
|
await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
|
||||||
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
|
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
|
||||||
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
|
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
|
||||||
|
@ -61,9 +61,14 @@ class ChannelsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getChannelsByStatus(status: number): Promise<any[]> {
|
public async $getChannelsByStatus(status: number | number[]): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT * FROM channels WHERE status = ?`;
|
let query: string;
|
||||||
|
if (Array.isArray(status)) {
|
||||||
|
query = `SELECT * FROM channels WHERE status IN (${status.join(',')})`;
|
||||||
|
} else {
|
||||||
|
query = `SELECT * FROM channels WHERE status = ?`;
|
||||||
|
}
|
||||||
const [rows]: any = await DB.query(query, [status]);
|
const [rows]: any = await DB.query(query, [status]);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -218,23 +223,25 @@ class ChannelsApi {
|
|||||||
|
|
||||||
// Channels originating from node
|
// Channels originating from node
|
||||||
let query = `
|
let query = `
|
||||||
SELECT node2.alias, node2.public_key, channels.status, channels.node1_fee_rate,
|
SELECT COALESCE(node2.alias, SUBSTRING(node2_public_key, 0, 20)) AS alias, COALESCE(node2.public_key, node2_public_key) AS public_key,
|
||||||
|
channels.status, channels.node1_fee_rate,
|
||||||
channels.capacity, channels.short_id, channels.id
|
channels.capacity, channels.short_id, channels.id
|
||||||
FROM channels
|
FROM channels
|
||||||
JOIN nodes AS node2 ON node2.public_key = channels.node2_public_key
|
LEFT JOIN nodes AS node2 ON node2.public_key = channels.node2_public_key
|
||||||
WHERE node1_public_key = ? AND channels.status ${channelStatusFilter}
|
WHERE node1_public_key = ? AND channels.status ${channelStatusFilter}
|
||||||
`;
|
`;
|
||||||
const [channelsFromNode]: any = await DB.query(query, [public_key, index, length]);
|
const [channelsFromNode]: any = await DB.query(query, [public_key]);
|
||||||
|
|
||||||
// Channels incoming to node
|
// Channels incoming to node
|
||||||
query = `
|
query = `
|
||||||
SELECT node1.alias, node1.public_key, channels.status, channels.node2_fee_rate,
|
SELECT COALESCE(node1.alias, SUBSTRING(node1_public_key, 0, 20)) AS alias, COALESCE(node1.public_key, node1_public_key) AS public_key,
|
||||||
|
channels.status, channels.node2_fee_rate,
|
||||||
channels.capacity, channels.short_id, channels.id
|
channels.capacity, channels.short_id, channels.id
|
||||||
FROM channels
|
FROM channels
|
||||||
JOIN nodes AS node1 ON node1.public_key = channels.node1_public_key
|
LEFT JOIN nodes AS node1 ON node1.public_key = channels.node1_public_key
|
||||||
WHERE node2_public_key = ? AND channels.status ${channelStatusFilter}
|
WHERE node2_public_key = ? AND channels.status ${channelStatusFilter}
|
||||||
`;
|
`;
|
||||||
const [channelsToNode]: any = await DB.query(query, [public_key, index, length]);
|
const [channelsToNode]: any = await DB.query(query, [public_key]);
|
||||||
|
|
||||||
let allChannels = channelsFromNode.concat(channelsToNode);
|
let allChannels = channelsFromNode.concat(channelsToNode);
|
||||||
allChannels.sort((a, b) => {
|
allChannels.sort((a, b) => {
|
||||||
@ -337,7 +344,7 @@ class ChannelsApi {
|
|||||||
/**
|
/**
|
||||||
* Save or update a channel present in the graph
|
* Save or update a channel present in the graph
|
||||||
*/
|
*/
|
||||||
public async $saveChannel(channel: ILightningApi.Channel): Promise<void> {
|
public async $saveChannel(channel: ILightningApi.Channel, status = 1): Promise<void> {
|
||||||
const [ txid, vout ] = channel.chan_point.split(':');
|
const [ txid, vout ] = channel.chan_point.split(':');
|
||||||
|
|
||||||
const policy1: Partial<ILightningApi.RoutingPolicy> = channel.node1_policy || {};
|
const policy1: Partial<ILightningApi.RoutingPolicy> = channel.node1_policy || {};
|
||||||
@ -369,11 +376,11 @@ class ChannelsApi {
|
|||||||
node2_min_htlc_mtokens,
|
node2_min_htlc_mtokens,
|
||||||
node2_updated_at
|
node2_updated_at
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ${status}, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
capacity = ?,
|
capacity = ?,
|
||||||
updated_at = ?,
|
updated_at = ?,
|
||||||
status = 1,
|
status = ${status},
|
||||||
node1_public_key = ?,
|
node1_public_key = ?,
|
||||||
node1_base_fee_mtokens = ?,
|
node1_base_fee_mtokens = ?,
|
||||||
node1_cltv_delta = ?,
|
node1_cltv_delta = ?,
|
||||||
|
@ -169,7 +169,7 @@ class NodesApi {
|
|||||||
let query: string;
|
let query: string;
|
||||||
if (full === false) {
|
if (full === false) {
|
||||||
query = `
|
query = `
|
||||||
SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
|
SELECT nodes.public_key as publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
|
||||||
node_stats.channels
|
node_stats.channels
|
||||||
FROM node_stats
|
FROM node_stats
|
||||||
JOIN nodes ON nodes.public_key = node_stats.public_key
|
JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||||
@ -296,19 +296,24 @@ class NodesApi {
|
|||||||
|
|
||||||
if (!ispList[isp1]) {
|
if (!ispList[isp1]) {
|
||||||
ispList[isp1] = {
|
ispList[isp1] = {
|
||||||
id: channel.isp1ID,
|
id: channel.isp1ID.toString(),
|
||||||
capacity: 0,
|
capacity: 0,
|
||||||
channels: 0,
|
channels: 0,
|
||||||
nodes: {},
|
nodes: {},
|
||||||
};
|
};
|
||||||
|
} else if (ispList[isp1].id.indexOf(channel.isp1ID) === -1) {
|
||||||
|
ispList[isp1].id += ',' + channel.isp1ID.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ispList[isp2]) {
|
if (!ispList[isp2]) {
|
||||||
ispList[isp2] = {
|
ispList[isp2] = {
|
||||||
id: channel.isp2ID,
|
id: channel.isp2ID.toString(),
|
||||||
capacity: 0,
|
capacity: 0,
|
||||||
channels: 0,
|
channels: 0,
|
||||||
nodes: {},
|
nodes: {},
|
||||||
};
|
};
|
||||||
|
} else if (ispList[isp2].id.indexOf(channel.isp2ID) === -1) {
|
||||||
|
ispList[isp2].id += ',' + channel.isp2ID.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
ispList[isp1].capacity += channel.capacity;
|
ispList[isp1].capacity += channel.capacity;
|
||||||
@ -387,7 +392,8 @@ class NodesApi {
|
|||||||
const query = `
|
const query = `
|
||||||
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||||
nodes.alias, 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, geo_names_country.names as country,
|
||||||
|
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
|
||||||
FROM node_stats
|
FROM node_stats
|
||||||
JOIN (
|
JOIN (
|
||||||
SELECT public_key, MAX(added) as last_added
|
SELECT public_key, MAX(added) as last_added
|
||||||
@ -395,15 +401,19 @@ class NodesApi {
|
|||||||
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
|
||||||
RIGHT 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'
|
LEFT 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'
|
||||||
|
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_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
|
||||||
WHERE geo_names_country.id = ?
|
WHERE geo_names_country.id = ?
|
||||||
ORDER BY capacity DESC
|
ORDER BY capacity DESC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const [rows]: any = await DB.query(query, [countryId]);
|
const [rows]: any = await DB.query(query, [countryId]);
|
||||||
for (let i = 0; i < rows.length; ++i) {
|
for (let i = 0; i < rows.length; ++i) {
|
||||||
|
rows[i].country = JSON.parse(rows[i].country);
|
||||||
rows[i].city = JSON.parse(rows[i].city);
|
rows[i].city = JSON.parse(rows[i].city);
|
||||||
|
rows[i].subdivision = JSON.parse(rows[i].subdivision);
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -417,7 +427,8 @@ class NodesApi {
|
|||||||
const query = `
|
const query = `
|
||||||
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||||
nodes.alias, 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,
|
||||||
|
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
|
||||||
FROM node_stats
|
FROM node_stats
|
||||||
JOIN (
|
JOIN (
|
||||||
SELECT public_key, MAX(added) as last_added
|
SELECT public_key, MAX(added) as last_added
|
||||||
@ -425,8 +436,10 @@ class NodesApi {
|
|||||||
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
|
||||||
RIGHT 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'
|
LEFT 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'
|
||||||
|
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_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
|
||||||
WHERE nodes.as_number IN (?)
|
WHERE nodes.as_number IN (?)
|
||||||
ORDER BY capacity DESC
|
ORDER BY capacity DESC
|
||||||
`;
|
`;
|
||||||
@ -435,6 +448,7 @@ class NodesApi {
|
|||||||
for (let i = 0; i < rows.length; ++i) {
|
for (let i = 0; i < rows.length; ++i) {
|
||||||
rows[i].country = JSON.parse(rows[i].country);
|
rows[i].country = JSON.parse(rows[i].country);
|
||||||
rows[i].city = JSON.parse(rows[i].city);
|
rows[i].city = JSON.parse(rows[i].city);
|
||||||
|
rows[i].subdivision = JSON.parse(rows[i].subdivision);
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -189,7 +189,7 @@ class Server {
|
|||||||
await networkSyncService.$startService();
|
await networkSyncService.$startService();
|
||||||
await lightningStatsUpdater.$startService();
|
await lightningStatsUpdater.$startService();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
logger.err(`Lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
|
logger.err(`Nodejs lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
await Common.sleep$(1000 * 60);
|
await Common.sleep$(1000 * 60);
|
||||||
this.$runLightningBackend();
|
this.$runLightningBackend();
|
||||||
};
|
};
|
||||||
|
@ -95,11 +95,19 @@ class NetworkSyncService {
|
|||||||
*/
|
*/
|
||||||
private async $updateChannelsList(channels: ILightningApi.Channel[]): Promise<void> {
|
private async $updateChannelsList(channels: ILightningApi.Channel[]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const [closedChannelsRaw]: any[] = await DB.query(`SELECT id FROM channels WHERE status = 2`);
|
||||||
|
const closedChannels = {};
|
||||||
|
for (const closedChannel of closedChannelsRaw) {
|
||||||
|
closedChannels[Common.channelShortIdToIntegerId(closedChannel.id)] = true;
|
||||||
|
}
|
||||||
|
|
||||||
let progress = 0;
|
let progress = 0;
|
||||||
|
|
||||||
const graphChannelsIds: string[] = [];
|
const graphChannelsIds: string[] = [];
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
|
if (!closedChannels[channel.channel_id]) {
|
||||||
await channelsApi.$saveChannel(channel);
|
await channelsApi.$saveChannel(channel);
|
||||||
|
}
|
||||||
graphChannelsIds.push(channel.channel_id);
|
graphChannelsIds.push(channel.channel_id);
|
||||||
++progress;
|
++progress;
|
||||||
|
|
||||||
@ -232,8 +240,8 @@ class NetworkSyncService {
|
|||||||
let progress = 0;
|
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, 1]);
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
|
const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
|
||||||
if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
|
if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
|
||||||
|
@ -5,6 +5,8 @@ import fundingTxFetcher from './funding-tx-fetcher';
|
|||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import { ILightningApi } from '../../../api/lightning/lightning-api.interface';
|
import { ILightningApi } from '../../../api/lightning/lightning-api.interface';
|
||||||
import { isIP } from 'net';
|
import { isIP } from 'net';
|
||||||
|
import { Common } from '../../../api/common';
|
||||||
|
import channelsApi from '../../../api/explorer/channels.api';
|
||||||
|
|
||||||
const fsPromises = promises;
|
const fsPromises = promises;
|
||||||
|
|
||||||
@ -22,7 +24,8 @@ class LightningStatsImporter {
|
|||||||
/**
|
/**
|
||||||
* Generate LN network stats for one day
|
* Generate LN network stats for one day
|
||||||
*/
|
*/
|
||||||
public async computeNetworkStats(timestamp: number, networkGraph: ILightningApi.NetworkGraph): Promise<unknown> {
|
public async computeNetworkStats(timestamp: number,
|
||||||
|
networkGraph: ILightningApi.NetworkGraph, isHistorical: boolean = false): Promise<unknown> {
|
||||||
// Node counts and network shares
|
// Node counts and network shares
|
||||||
let clearnetNodes = 0;
|
let clearnetNodes = 0;
|
||||||
let torNodes = 0;
|
let torNodes = 0;
|
||||||
@ -66,18 +69,46 @@ class LightningStatsImporter {
|
|||||||
const baseFees: number[] = [];
|
const baseFees: number[] = [];
|
||||||
const alreadyCountedChannels = {};
|
const alreadyCountedChannels = {};
|
||||||
|
|
||||||
for (const channel of networkGraph.edges) {
|
const [channelsInDbRaw]: any[] = await DB.query(`SELECT short_id, created FROM channels`);
|
||||||
let short_id = channel.channel_id;
|
const channelsInDb = {};
|
||||||
if (short_id.indexOf('/') !== -1) {
|
for (const channel of channelsInDbRaw) {
|
||||||
short_id = short_id.slice(0, -2);
|
channelsInDb[channel.short_id] = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const channel of networkGraph.edges) {
|
||||||
|
const short_id = Common.channelIntegerIdToShortId(channel.channel_id);
|
||||||
|
|
||||||
const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id);
|
const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id);
|
||||||
if (!tx) {
|
if (!tx) {
|
||||||
logger.err(`Unable to fetch funding tx for channel ${short_id}. Capacity and creation date is unknown. Skipping channel.`);
|
logger.err(`Unable to fetch funding tx for channel ${short_id}. Capacity and creation date is unknown. Skipping channel.`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Channel is already in db, check if we need to update 'created' field
|
||||||
|
if (isHistorical === true) {
|
||||||
|
//@ts-ignore
|
||||||
|
if (channelsInDb[short_id] && channel.timestamp < channel.created) {
|
||||||
|
await DB.query(`
|
||||||
|
UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.short_id = ?`,
|
||||||
|
//@ts-ignore
|
||||||
|
[channel.timestamp, short_id]
|
||||||
|
);
|
||||||
|
} else if (!channelsInDb[short_id]) {
|
||||||
|
await channelsApi.$saveChannel({
|
||||||
|
channel_id: short_id,
|
||||||
|
chan_point: `${tx.txid}:${short_id.split('x')[2]}`,
|
||||||
|
//@ts-ignore
|
||||||
|
last_update: channel.timestamp,
|
||||||
|
node1_pub: channel.node1_pub,
|
||||||
|
node2_pub: channel.node2_pub,
|
||||||
|
capacity: (tx.value * 100000000).toString(),
|
||||||
|
node1_policy: null,
|
||||||
|
node2_policy: null,
|
||||||
|
}, 0);
|
||||||
|
channelsInDb[channel.channel_id] = channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!nodeStats[channel.node1_pub]) {
|
if (!nodeStats[channel.node1_pub]) {
|
||||||
nodeStats[channel.node1_pub] = {
|
nodeStats[channel.node1_pub] = {
|
||||||
capacity: 0,
|
capacity: 0,
|
||||||
@ -102,7 +133,7 @@ class LightningStatsImporter {
|
|||||||
nodeStats[channel.node2_pub].channels++;
|
nodeStats[channel.node2_pub].channels++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channel.node1_policy !== undefined) { // Coming from the node
|
if (isHistorical === false) { // Coming from the node
|
||||||
for (const policy of [channel.node1_policy, channel.node2_policy]) {
|
for (const policy of [channel.node1_policy, channel.node2_policy]) {
|
||||||
if (policy && parseInt(policy.fee_rate_milli_msat, 10) < 5000) {
|
if (policy && parseInt(policy.fee_rate_milli_msat, 10) < 5000) {
|
||||||
avgFeeRate += parseInt(policy.fee_rate_milli_msat, 10);
|
avgFeeRate += parseInt(policy.fee_rate_milli_msat, 10);
|
||||||
@ -113,30 +144,42 @@ class LightningStatsImporter {
|
|||||||
baseFees.push(parseInt(policy.fee_base_msat, 10));
|
baseFees.push(parseInt(policy.fee_base_msat, 10));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // Coming from the historical import
|
} else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (channel.fee_rate_milli_msat < 5000) {
|
if (channel.node1_policy.fee_rate_milli_msat < 5000) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
avgFeeRate += parseInt(channel.fee_rate_milli_msat, 10);
|
avgFeeRate += parseInt(channel.node1_policy.fee_rate_milli_msat, 10);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
feeRates.push(parseInt(channel.fee_rate_milli_msat), 10);
|
feeRates.push(parseInt(channel.node1_policy.fee_rate_milli_msat), 10);
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (channel.fee_base_msat < 5000) {
|
if (channel.node1_policy.fee_base_msat < 5000) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
avgBaseFee += parseInt(channel.fee_base_msat, 10);
|
avgBaseFee += parseInt(channel.node1_policy.fee_base_msat, 10);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
baseFees.push(parseInt(channel.fee_base_msat), 10);
|
baseFees.push(parseInt(channel.node1_policy.fee_base_msat), 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let medCapacity = 0;
|
||||||
|
let medFeeRate = 0;
|
||||||
|
let medBaseFee = 0;
|
||||||
|
let avgCapacity = 0;
|
||||||
|
|
||||||
avgFeeRate /= Math.max(networkGraph.edges.length, 1);
|
avgFeeRate /= Math.max(networkGraph.edges.length, 1);
|
||||||
avgBaseFee /= 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)];
|
if (capacities.length > 0) {
|
||||||
const medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)];
|
medCapacity = capacities.sort((a, b) => b - a)[Math.round(capacities.length / 2 - 1)];
|
||||||
const avgCapacity = Math.round(capacity / Math.max(capacities.length, 1));
|
avgCapacity = Math.round(capacity / Math.max(capacities.length, 1));
|
||||||
|
}
|
||||||
|
if (feeRates.length > 0) {
|
||||||
|
medFeeRate = feeRates.sort((a, b) => b - a)[Math.round(feeRates.length / 2 - 1)];
|
||||||
|
}
|
||||||
|
if (baseFees.length > 0) {
|
||||||
|
medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
let query = `INSERT INTO lightning_stats(
|
let query = `INSERT INTO lightning_stats(
|
||||||
added,
|
added,
|
||||||
@ -319,7 +362,7 @@ class LightningStatsImporter {
|
|||||||
logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
|
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)));
|
await fundingTxFetcher.$fetchChannelsFundingTxs(graph.edges.map(channel => channel.channel_id.slice(0, -2)));
|
||||||
const stat = await this.computeNetworkStats(timestamp, graph);
|
const stat = await this.computeNetworkStats(timestamp, graph, true);
|
||||||
|
|
||||||
existingStatsTimestamps[timestamp] = stat;
|
existingStatsTimestamps[timestamp] = stat;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<table class="table table-borderless smaller-text table-sm table-tx-vin">
|
<table class="table table-borderless smaller-text table-sm table-tx-vin">
|
||||||
<tbody>
|
<tbody>
|
||||||
<ng-template ngFor let-vin [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length>12)?tx.vin.slice(0, 10): tx.vin.slice(0, 12)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
|
<ng-template ngFor let-vin [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
|
||||||
<tr [ngClass]="{
|
<tr [ngClass]="{
|
||||||
'assetBox': assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded,
|
'assetBox': assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded,
|
||||||
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
|
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
|
||||||
@ -146,9 +146,9 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<tr *ngIf="tx.vin.length > 12 && tx['@vinLimit']">
|
<tr *ngIf="tx.vin.length > rowLimit && tx['@vinLimit']">
|
||||||
<td colspan="3" class="text-center">
|
<td colspan="3" class="text-center">
|
||||||
<button class="btn btn-sm btn-primary mt-2" (click)="loadMoreInputs(tx);"><span i18n="show-all">Show all</span> ({{ tx.vin.length - 10 }})</button>
|
<button class="btn btn-sm btn-primary mt-2" (click)="loadMoreInputs(tx);"><span i18n="show-all">Show all</span> ({{ tx.vin.length }})</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -158,7 +158,7 @@
|
|||||||
<div class="col mobile-bottomcol">
|
<div class="col mobile-bottomcol">
|
||||||
<table class="table table-borderless smaller-text table-sm table-tx-vout">
|
<table class="table table-borderless smaller-text table-sm table-tx-vout">
|
||||||
<tbody>
|
<tbody>
|
||||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] && !outputIndex ? ((tx.vout.length > 12) ? tx.vout.slice(0, 10) : tx.vout.slice(0, 12)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
|
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] && !outputIndex ? ((tx.vout.length > rowLimit) ? tx.vout.slice(0, rowLimit - 2) : tx.vout.slice(0, rowLimit)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
|
||||||
<tr [ngClass]="{
|
<tr [ngClass]="{
|
||||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||||
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
|
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
|
||||||
@ -257,9 +257,9 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<tr *ngIf="tx.vout.length > 12 && tx['@voutLimit'] && !outputIndex">
|
<tr *ngIf="tx.vout.length > rowLimit && tx['@voutLimit'] && !outputIndex">
|
||||||
<td colspan="3" class="text-center">
|
<td colspan="3" class="text-center">
|
||||||
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length - 10 }})</button>
|
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length }})</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -26,6 +26,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
@Input() paginated = false;
|
@Input() paginated = false;
|
||||||
@Input() outputIndex: number;
|
@Input() outputIndex: number;
|
||||||
@Input() address: string = '';
|
@Input() address: string = '';
|
||||||
|
@Input() rowLimit = 12;
|
||||||
|
@Input() channels: { inputs: any[], outputs: any[] };
|
||||||
|
|
||||||
@Output() loadMore = new EventEmitter();
|
@Output() loadMore = new EventEmitter();
|
||||||
|
|
||||||
@ -36,7 +38,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
showDetails$ = new BehaviorSubject<boolean>(false);
|
showDetails$ = new BehaviorSubject<boolean>(false);
|
||||||
outspends: Outspend[][] = [];
|
outspends: Outspend[][] = [];
|
||||||
assetsMinimal: any;
|
assetsMinimal: any;
|
||||||
channels: { inputs: any[], outputs: any[] };
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
@ -127,8 +128,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
});
|
});
|
||||||
const txIds = this.transactions.map((tx) => tx.txid);
|
const txIds = this.transactions.map((tx) => tx.txid);
|
||||||
this.refreshOutspends$.next(txIds);
|
this.refreshOutspends$.next(txIds);
|
||||||
|
if (!this.channels) {
|
||||||
this.refreshChannels$.next(txIds);
|
this.refreshChannels$.next(txIds);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onScroll() {
|
onScroll() {
|
||||||
const scrollHeight = document.body.scrollHeight;
|
const scrollHeight = document.body.scrollHeight;
|
||||||
|
@ -16,3 +16,9 @@
|
|||||||
color: #ffffff66;
|
color: #ffffff66;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.box {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo"></app-nodes-channels-map>
|
<app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'" [channel]="channelGeo"></app-nodes-channels-map>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
|
||||||
@ -30,32 +30,6 @@
|
|||||||
<td i18n="address.total-sent">Last update</td>
|
<td i18n="address.total-sent">Last update</td>
|
||||||
<td><app-timestamp [dateString]="channel.updated_at"></app-timestamp></td>
|
<td><app-timestamp [dateString]="channel.updated_at"></app-timestamp></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td i18n="address.total-sent">Opening transaction</td>
|
|
||||||
<td>
|
|
||||||
<a [routerLink]="['/tx' | relativeUrl, channel.transaction_id + ':' + channel.transaction_vout]" >
|
|
||||||
<span>{{ channel.transaction_id | shortenString : 10 }}</span>
|
|
||||||
</a>
|
|
||||||
<app-clipboard [text]="channel.transaction_id"></app-clipboard>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<ng-template [ngIf]="channel.closing_transaction_id">
|
|
||||||
<tr *ngIf="channel.closing_transaction_id">
|
|
||||||
<td i18n="address.total-sent">Closing transaction</td>
|
|
||||||
<td>
|
|
||||||
<a [routerLink]="['/tx' | relativeUrl, channel.closing_transaction_id]" >
|
|
||||||
<span>{{ channel.closing_transaction_id | shortenString : 10 }}</span>
|
|
||||||
</a>
|
|
||||||
<app-clipboard [text]="channel.closing_transaction_id"></app-clipboard>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="address.total-sent">Closing type</td>
|
|
||||||
<td>
|
|
||||||
<app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -85,6 +59,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<ng-container *ngIf="transactions$ | async as transactions">
|
||||||
|
<ng-template [ngIf]="transactions[0]">
|
||||||
|
<h3>Opening transaction</h3>
|
||||||
|
<app-transactions-list [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [], outputs: [channel] }"></app-transactions-list>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template [ngIf]="transactions[1]">
|
||||||
|
<div class="closing-header">
|
||||||
|
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
||||||
|
</div>
|
||||||
|
<app-transactions-list [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [channel], outputs: [] }"></app-transactions-list>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
@ -39,3 +39,16 @@ app-fiat {
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.closing-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
h3 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
import { Observable, of } from 'rxjs';
|
import { forkJoin, Observable, of, share, zip } from 'rxjs';
|
||||||
import { catchError, switchMap, tap } from 'rxjs/operators';
|
import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
|
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
|
|
||||||
@ -13,13 +15,15 @@ import { LightningApiService } from '../lightning-api.service';
|
|||||||
})
|
})
|
||||||
export class ChannelComponent implements OnInit {
|
export class ChannelComponent implements OnInit {
|
||||||
channel$: Observable<any>;
|
channel$: Observable<any>;
|
||||||
|
channelGeo$: Observable<number[]>;
|
||||||
|
transactions$: Observable<any>;
|
||||||
error: any = null;
|
error: any = null;
|
||||||
channelGeo: number[] = [];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
|
private electrsApiService: ElectrsApiService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -30,12 +34,22 @@ export class ChannelComponent implements OnInit {
|
|||||||
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
|
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
|
||||||
return this.lightningApiService.getChannel$(params.get('short_id'))
|
return this.lightningApiService.getChannel$(params.get('short_id'))
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((data) => {
|
catchError((err) => {
|
||||||
|
this.error = err;
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
shareReplay(),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.channelGeo$ = this.channel$.pipe(
|
||||||
|
map((data) => {
|
||||||
if (!data.node_left.longitude || !data.node_left.latitude ||
|
if (!data.node_left.longitude || !data.node_left.latitude ||
|
||||||
!data.node_right.longitude || !data.node_right.latitude) {
|
!data.node_right.longitude || !data.node_right.latitude) {
|
||||||
this.channelGeo = [];
|
return [];
|
||||||
} else {
|
} else {
|
||||||
this.channelGeo = [
|
return [
|
||||||
data.node_left.public_key,
|
data.node_left.public_key,
|
||||||
data.node_left.alias,
|
data.node_left.alias,
|
||||||
data.node_left.longitude, data.node_left.latitude,
|
data.node_left.longitude, data.node_left.latitude,
|
||||||
@ -45,12 +59,15 @@ export class ChannelComponent implements OnInit {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError((err) => {
|
|
||||||
this.error = err;
|
|
||||||
return of(null);
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
})
|
|
||||||
|
this.transactions$ = this.channel$.pipe(
|
||||||
|
switchMap((data) => {
|
||||||
|
return zip([
|
||||||
|
data.transaction_id ? this.electrsApiService.getTransaction$(data.transaction_id) : of(null),
|
||||||
|
data.closing_transaction_id ? this.electrsApiService.getTransaction$(data.closing_transaction_id) : of(null),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,11 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<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>
|
<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>
|
||||||
|
@ -47,7 +47,7 @@ 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: true });
|
||||||
this.channelsPage$.next(1);
|
this.channelsPage$.next(1);
|
||||||
|
|
||||||
this.channels$ = merge(
|
this.channels$ = merge(
|
||||||
@ -70,7 +70,7 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
|||||||
map((response) => {
|
map((response) => {
|
||||||
return {
|
return {
|
||||||
channels: response.body,
|
channels: response.body,
|
||||||
totalItems: parseInt(response.headers.get('x-total-count'), 10) + 1
|
totalItems: parseInt(response.headers.get('x-total-count'), 10)
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
|
|||||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { OpenGraphService } from 'src/app/services/opengraph.service';
|
import { OpenGraphService } from 'src/app/services/opengraph.service';
|
||||||
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
|
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
import { isMobile } from '../../shared/common.utils';
|
import { isMobile } from '../../shared/common.utils';
|
||||||
|
|
||||||
|
@ -42,24 +42,10 @@
|
|||||||
<app-fiat [value]="node.avgCapacity" 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.geolocation">
|
||||||
<td i18n="location">Location</td>
|
<td i18n="location">Location</td>
|
||||||
<td>
|
<td>
|
||||||
<span>{{ node.city.en }}, {{ node.subdivision.en }}</span>
|
<app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation>
|
||||||
<br>
|
|
||||||
<a class="d-flex align-items-center" [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
|
|
||||||
<span class="link">{{ node.country.en }}</span>
|
|
||||||
|
|
||||||
<span class="flag">{{ node.flag }}</span>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="node.country && !node.city">
|
|
||||||
<td i18n="location">Location</td>
|
|
||||||
<td>
|
|
||||||
<a [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
|
|
||||||
{{ node.country.en }} {{ node.flag }}
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -3,9 +3,9 @@ import { ActivatedRoute, ParamMap } from '@angular/router';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
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 { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
import { isMobile } from '../../shared/common.utils';
|
import { isMobile } from '../../shared/common.utils';
|
||||||
|
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-node',
|
selector: 'app-node',
|
||||||
@ -58,7 +58,6 @@ export class NodeComponent implements OnInit {
|
|||||||
} else if (socket.indexOf('onion') > -1) {
|
} else if (socket.indexOf('onion') > -1) {
|
||||||
label = 'Tor';
|
label = 'Tor';
|
||||||
}
|
}
|
||||||
node.flag = getFlagEmoji(node.iso_code);
|
|
||||||
socketsObject.push({
|
socketsObject.push({
|
||||||
label: label,
|
label: label,
|
||||||
socket: node.public_key + '@' + socket,
|
socket: node.public_key + '@' + socket,
|
||||||
@ -66,6 +65,19 @@ export class NodeComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
node.socketsObject = socketsObject;
|
node.socketsObject = socketsObject;
|
||||||
node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
|
node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
|
||||||
|
|
||||||
|
if (!node?.country && !node?.city &&
|
||||||
|
!node?.subdivision && !node?.iso) {
|
||||||
|
node.geolocation = null;
|
||||||
|
} else {
|
||||||
|
node.geolocation = <GeolocationData>{
|
||||||
|
country: node.country?.en,
|
||||||
|
city: node.city?.en,
|
||||||
|
subdivision: node.subdivision?.en,
|
||||||
|
iso: node.iso_code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}),
|
}),
|
||||||
catchError(err => {
|
catchError(err => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
|
<div *ngIf="channelsObservable | async">
|
||||||
|
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
|
||||||
<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>
|
||||||
@ -7,8 +7,10 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"
|
||||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
(chartFinished)="onChartFinished($event)">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, HostListener, Input, Output, EventEmitter, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, NgZone, OnInit } from '@angular/core';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
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';
|
||||||
@ -16,14 +16,14 @@ import { isMobile } from 'src/app/shared/common.utils';
|
|||||||
styleUrls: ['./nodes-channels-map.component.scss'],
|
styleUrls: ['./nodes-channels-map.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class NodesChannelsMap implements OnInit, OnDestroy {
|
export class NodesChannelsMap implements OnInit {
|
||||||
@Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph';
|
@Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph';
|
||||||
@Input() publicKey: string | undefined;
|
@Input() publicKey: string | undefined;
|
||||||
@Input() channel: any[] = [];
|
@Input() channel: any[] = [];
|
||||||
@Input() fitContainer = false;
|
@Input() fitContainer = false;
|
||||||
@Output() readyEvent = new EventEmitter();
|
@Output() readyEvent = new EventEmitter();
|
||||||
|
|
||||||
observable$: Observable<any>;
|
channelsObservable: Observable<any>;
|
||||||
|
|
||||||
center: number[] | undefined;
|
center: number[] | undefined;
|
||||||
zoom: number | undefined;
|
zoom: number | undefined;
|
||||||
@ -31,6 +31,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
channelOpacity = 0.1;
|
channelOpacity = 0.1;
|
||||||
channelColor = '#466d9d';
|
channelColor = '#466d9d';
|
||||||
channelCurve = 0;
|
channelCurve = 0;
|
||||||
|
nodeSize = 4;
|
||||||
|
|
||||||
chartInstance = undefined;
|
chartInstance = undefined;
|
||||||
chartOptions: EChartsOption = {};
|
chartOptions: EChartsOption = {};
|
||||||
@ -49,8 +50,6 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.center = this.style === 'widget' ? [0, 40] : [0, 5];
|
this.center = this.style === 'widget' ? [0, 40] : [0, 5];
|
||||||
this.zoom = 1.3;
|
this.zoom = 1.3;
|
||||||
@ -66,7 +65,11 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
this.seoService.setTitle($localize`Lightning nodes channels world map`);
|
this.seoService.setTitle($localize`Lightning nodes channels world map`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.observable$ = this.activatedRoute.paramMap
|
if (['nodepage', 'channelpage'].includes(this.style)) {
|
||||||
|
this.nodeSize = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.channelsObservable = this.activatedRoute.paramMap
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
return zip(
|
return zip(
|
||||||
@ -170,15 +173,8 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
prepareChartOptions(nodes, channels) {
|
prepareChartOptions(nodes, channels) {
|
||||||
let title: object;
|
let title: object;
|
||||||
if (channels.length === 0) {
|
if (channels.length === 0) {
|
||||||
title = {
|
this.chartOptions = null;
|
||||||
textStyle: {
|
return;
|
||||||
color: 'grey',
|
|
||||||
fontSize: 15
|
|
||||||
},
|
|
||||||
text: $localize`No geolocation data available`,
|
|
||||||
left: 'center',
|
|
||||||
top: 'center'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
@ -214,7 +210,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
data: nodes,
|
data: nodes,
|
||||||
coordinateSystem: 'geo',
|
coordinateSystem: 'geo',
|
||||||
geoIndex: 0,
|
geoIndex: 0,
|
||||||
symbolSize: 4,
|
symbolSize: this.nodeSize,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show: true,
|
show: true,
|
||||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||||
|
@ -10,6 +10,7 @@ import { download } from 'src/app/shared/graphs.utils';
|
|||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||||
|
import { isMobile } from 'src/app/shared/common.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-nodes-networks-chart',
|
selector: 'app-nodes-networks-chart',
|
||||||
@ -108,19 +109,19 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareChartOptions(data, maxYAxis) {
|
prepareChartOptions(data, maxYAxis): void {
|
||||||
let title: object;
|
let title: object;
|
||||||
if (data.tor_nodes.length === 0) {
|
if (!this.widget && data.tor_nodes.length === 0) {
|
||||||
title = {
|
title = {
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: 'grey',
|
color: 'grey',
|
||||||
fontSize: 15
|
fontSize: 15
|
||||||
},
|
},
|
||||||
text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`,
|
text: $localize`Indexing in progess`,
|
||||||
left: 'center',
|
left: 'center',
|
||||||
top: 'top',
|
top: 'center',
|
||||||
};
|
};
|
||||||
} else if (this.widget) {
|
} else if (data.tor_nodes.length > 0) {
|
||||||
title = {
|
title = {
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: 'grey',
|
color: 'grey',
|
||||||
@ -140,11 +141,11 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
height: this.widget ? 100 : undefined,
|
height: this.widget ? 100 : undefined,
|
||||||
top: this.widget ? 10 : 40,
|
top: this.widget ? 10 : 40,
|
||||||
bottom: this.widget ? 0 : 70,
|
bottom: this.widget ? 0 : 70,
|
||||||
right: (this.isMobile() && this.widget) ? 35 : this.right,
|
right: (isMobile() && this.widget) ? 35 : this.right,
|
||||||
left: (this.isMobile() && this.widget) ? 40 :this.left,
|
left: (isMobile() && this.widget) ? 40 :this.left,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show: !this.isMobile() || !this.widget,
|
show: !isMobile() || !this.widget,
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
type: 'line'
|
type: 'line'
|
||||||
@ -157,7 +158,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
align: 'left',
|
align: 'left',
|
||||||
},
|
},
|
||||||
borderColor: '#000',
|
borderColor: '#000',
|
||||||
formatter: (ticks) => {
|
formatter: (ticks): string => {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
let tooltip = `<b style="color: white; margin-left: 2px">${date}</b><br>`;
|
let tooltip = `<b style="color: white; margin-left: 2px">${date}</b><br>`;
|
||||||
@ -180,7 +181,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
xAxis: data.tor_nodes.length === 0 ? undefined : {
|
xAxis: data.tor_nodes.length === 0 ? undefined : {
|
||||||
type: 'time',
|
type: 'time',
|
||||||
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
|
splitNumber: (isMobile() || this.widget) ? 5 : 10,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
hideOverlap: true,
|
hideOverlap: true,
|
||||||
}
|
}
|
||||||
@ -372,7 +373,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onChartInit(ec) {
|
onChartInit(ec): void {
|
||||||
if (this.chartInstance !== undefined) {
|
if (this.chartInstance !== undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -384,11 +385,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isMobile() {
|
onSaveChart(): void {
|
||||||
return (window.innerWidth <= 767.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSaveChart() {
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const prevBottom = this.chartOptions.grid.bottom;
|
const prevBottom = this.chartOptions.grid.bottom;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
@ -9,7 +9,7 @@ import { StateService } from 'src/app/services/state.service';
|
|||||||
import { download } from 'src/app/shared/graphs.utils';
|
import { download } from 'src/app/shared/graphs.utils';
|
||||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||||
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 { getFlagEmoji } from 'src/app/shared/graphs.utils';
|
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-nodes-per-country-chart',
|
selector: 'app-nodes-per-country-chart',
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
{{ node.channels }}
|
{{ node.channels }}
|
||||||
</td>
|
</td>
|
||||||
<td class="city text-right text-truncate">
|
<td class="city text-right text-truncate">
|
||||||
{{ node?.city?.en ?? '-' }}
|
<app-geolocation [data]="node.geolocation" [type]="'list-country'"></app-geolocation>
|
||||||
</td>
|
</td>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -3,7 +3,8 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { map, Observable } from 'rxjs';
|
import { map, Observable } from 'rxjs';
|
||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
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/common.utils';
|
||||||
|
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-nodes-per-country',
|
selector: 'app-nodes-per-country',
|
||||||
@ -29,6 +30,16 @@ export class NodesPerCountry implements OnInit {
|
|||||||
name: response.country.en,
|
name: response.country.en,
|
||||||
flag: getFlagEmoji(this.route.snapshot.params.country)
|
flag: getFlagEmoji(this.route.snapshot.params.country)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const i in response.nodes) {
|
||||||
|
response.nodes[i].geolocation = <GeolocationData>{
|
||||||
|
country: response.nodes[i].country?.en,
|
||||||
|
city: response.nodes[i].city?.en,
|
||||||
|
subdivision: response.nodes[i].subdivision?.en,
|
||||||
|
iso: response.nodes[i].iso_code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`);
|
this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`);
|
||||||
return response.nodes;
|
return response.nodes;
|
||||||
})
|
})
|
||||||
|
@ -154,7 +154,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
borderColor: '#000',
|
borderColor: '#000',
|
||||||
formatter: () => {
|
formatter: () => {
|
||||||
return `<b style="color: white">${isp[1]} (${isp[6]}%)</b><br>` +
|
return `<b style="color: white">${isp[1]} (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)</b><br>` +
|
||||||
$localize`${isp[4].toString()} nodes<br>` +
|
$localize`${isp[4].toString()} nodes<br>` +
|
||||||
$localize`${this.amountShortenerPipe.transform(isp[2] / 100000000, 2)} BTC`
|
$localize`${this.amountShortenerPipe.transform(isp[2] / 100000000, 2)} BTC`
|
||||||
;
|
;
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
{{ node.channels }}
|
{{ node.channels }}
|
||||||
</td>
|
</td>
|
||||||
<td class="city text-right text-truncate">
|
<td class="city text-right text-truncate">
|
||||||
{{ node?.city?.en ?? '-' }}
|
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
|
||||||
</td>
|
</td>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { map, Observable } from 'rxjs';
|
import { map, Observable } from 'rxjs';
|
||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
|
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-nodes-per-isp',
|
selector: 'app-nodes-per-isp',
|
||||||
@ -29,6 +30,16 @@ export class NodesPerISP implements OnInit {
|
|||||||
id: this.route.snapshot.params.isp
|
id: this.route.snapshot.params.isp
|
||||||
};
|
};
|
||||||
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
|
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
|
||||||
|
|
||||||
|
for (const i in response.nodes) {
|
||||||
|
response.nodes[i].geolocation = <GeolocationData>{
|
||||||
|
country: response.nodes[i].country?.en,
|
||||||
|
city: response.nodes[i].city?.en,
|
||||||
|
subdivision: response.nodes[i].subdivision?.en,
|
||||||
|
iso: response.nodes[i].iso_code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return response.nodes;
|
return response.nodes;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -48,4 +48,10 @@
|
|||||||
<div class="spinner-border text-light"></div>
|
<div class="spinner-border text-light"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="widget && (capacityObservable$ | async) as stats">
|
||||||
|
<div *ngIf="stats.days === 0" class="indexing-message d-flex" i18n="lightning.indexing-in-progress">
|
||||||
|
Indexing in progress
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
@ -132,3 +132,12 @@
|
|||||||
max-width: 80px;
|
max-width: 80px;
|
||||||
margin: 15px auto 3px;
|
margin: 15px auto 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.indexing-message {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 15px;
|
||||||
|
color: grey;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: calc(50% - 85px);
|
||||||
|
margin-top: -10px;
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||||
import { EChartsOption, graphic } from 'echarts';
|
import { EChartsOption, graphic } from 'echarts';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
@ -10,6 +10,7 @@ import { MiningService } from 'src/app/services/mining.service';
|
|||||||
import { download } from 'src/app/shared/graphs.utils';
|
import { download } from 'src/app/shared/graphs.utils';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||||
|
import { isMobile } from 'src/app/shared/common.utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-lightning-statistics-chart',
|
selector: 'app-lightning-statistics-chart',
|
||||||
@ -96,12 +97,13 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
)
|
share(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareChartOptions(data) {
|
prepareChartOptions(data): void {
|
||||||
let title: object;
|
let title: object;
|
||||||
if (data.channel_count.length === 0) {
|
if (!this.widget && data.channel_count.length === 0) {
|
||||||
title = {
|
title = {
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: 'grey',
|
color: 'grey',
|
||||||
@ -111,7 +113,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
left: 'center',
|
left: 'center',
|
||||||
top: 'center'
|
top: 'center'
|
||||||
};
|
};
|
||||||
} else if (this.widget) {
|
} else if (data.channel_count.length > 0) {
|
||||||
title = {
|
title = {
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: 'grey',
|
color: 'grey',
|
||||||
@ -138,11 +140,11 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
height: this.widget ? 100 : undefined,
|
height: this.widget ? 100 : undefined,
|
||||||
top: this.widget ? 10 : 40,
|
top: this.widget ? 10 : 40,
|
||||||
bottom: this.widget ? 0 : 70,
|
bottom: this.widget ? 0 : 70,
|
||||||
right: (this.isMobile() && this.widget) ? 35 : this.right,
|
right: (isMobile() && this.widget) ? 35 : this.right,
|
||||||
left: (this.isMobile() && this.widget) ? 40 :this.left,
|
left: (isMobile() && this.widget) ? 40 :this.left,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show: !this.isMobile(),
|
show: !isMobile(),
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
type: 'line'
|
type: 'line'
|
||||||
@ -155,7 +157,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
align: 'left',
|
align: 'left',
|
||||||
},
|
},
|
||||||
borderColor: '#000',
|
borderColor: '#000',
|
||||||
formatter: (ticks) => {
|
formatter: (ticks): string => {
|
||||||
let sizeString = '';
|
let sizeString = '';
|
||||||
let weightString = '';
|
let weightString = '';
|
||||||
|
|
||||||
@ -169,16 +171,18 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
|
|
||||||
const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
|
|
||||||
let tooltip = `<b style="color: white; margin-left: 18px">${date}</b><br>
|
const tooltip = `
|
||||||
|
<b style="color: white; margin-left: 18px">${date}</b><br>
|
||||||
<span>${sizeString}</span><br>
|
<span>${sizeString}</span><br>
|
||||||
<span>${weightString}</span>`;
|
<span>${weightString}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
return tooltip;
|
return tooltip;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
xAxis: data.channel_count.length === 0 ? undefined : {
|
xAxis: data.channel_count.length === 0 ? undefined : {
|
||||||
type: 'time',
|
type: 'time',
|
||||||
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
|
splitNumber: (isMobile() || this.widget) ? 5 : 10,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
hideOverlap: true,
|
hideOverlap: true,
|
||||||
}
|
}
|
||||||
@ -315,7 +319,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onChartInit(ec) {
|
onChartInit(ec): void {
|
||||||
if (this.chartInstance !== undefined) {
|
if (this.chartInstance !== undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -327,11 +331,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isMobile() {
|
onSaveChart(): void {
|
||||||
return (window.innerWidth <= 767.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSaveChart() {
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const prevBottom = this.chartOptions.grid.bottom;
|
const prevBottom = this.chartOptions.grid.bottom;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
@ -1,3 +1,120 @@
|
|||||||
export function isMobile() {
|
export function isMobile(): boolean {
|
||||||
return (window.innerWidth <= 767.98);
|
return (window.innerWidth <= 767.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFlagEmoji(countryCode): string {
|
||||||
|
if (!countryCode) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const codePoints = countryCode
|
||||||
|
.toUpperCase()
|
||||||
|
.split('')
|
||||||
|
.map(char => 127397 + char.charCodeAt());
|
||||||
|
return String.fromCodePoint(...codePoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://gist.github.com/calebgrove/c285a9510948b633aa47
|
||||||
|
export function convertRegion(input, to: 'name' | 'abbreviated'): string {
|
||||||
|
if (!input) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const states = [
|
||||||
|
['Alabama', 'AL'],
|
||||||
|
['Alaska', 'AK'],
|
||||||
|
['American Samoa', 'AS'],
|
||||||
|
['Arizona', 'AZ'],
|
||||||
|
['Arkansas', 'AR'],
|
||||||
|
['Armed Forces Americas', 'AA'],
|
||||||
|
['Armed Forces Europe', 'AE'],
|
||||||
|
['Armed Forces Pacific', 'AP'],
|
||||||
|
['California', 'CA'],
|
||||||
|
['Colorado', 'CO'],
|
||||||
|
['Connecticut', 'CT'],
|
||||||
|
['Delaware', 'DE'],
|
||||||
|
['District Of Columbia', 'DC'],
|
||||||
|
['Florida', 'FL'],
|
||||||
|
['Georgia', 'GA'],
|
||||||
|
['Guam', 'GU'],
|
||||||
|
['Hawaii', 'HI'],
|
||||||
|
['Idaho', 'ID'],
|
||||||
|
['Illinois', 'IL'],
|
||||||
|
['Indiana', 'IN'],
|
||||||
|
['Iowa', 'IA'],
|
||||||
|
['Kansas', 'KS'],
|
||||||
|
['Kentucky', 'KY'],
|
||||||
|
['Louisiana', 'LA'],
|
||||||
|
['Maine', 'ME'],
|
||||||
|
['Marshall Islands', 'MH'],
|
||||||
|
['Maryland', 'MD'],
|
||||||
|
['Massachusetts', 'MA'],
|
||||||
|
['Michigan', 'MI'],
|
||||||
|
['Minnesota', 'MN'],
|
||||||
|
['Mississippi', 'MS'],
|
||||||
|
['Missouri', 'MO'],
|
||||||
|
['Montana', 'MT'],
|
||||||
|
['Nebraska', 'NE'],
|
||||||
|
['Nevada', 'NV'],
|
||||||
|
['New Hampshire', 'NH'],
|
||||||
|
['New Jersey', 'NJ'],
|
||||||
|
['New Mexico', 'NM'],
|
||||||
|
['New York', 'NY'],
|
||||||
|
['North Carolina', 'NC'],
|
||||||
|
['North Dakota', 'ND'],
|
||||||
|
['Northern Mariana Islands', 'NP'],
|
||||||
|
['Ohio', 'OH'],
|
||||||
|
['Oklahoma', 'OK'],
|
||||||
|
['Oregon', 'OR'],
|
||||||
|
['Pennsylvania', 'PA'],
|
||||||
|
['Puerto Rico', 'PR'],
|
||||||
|
['Rhode Island', 'RI'],
|
||||||
|
['South Carolina', 'SC'],
|
||||||
|
['South Dakota', 'SD'],
|
||||||
|
['Tennessee', 'TN'],
|
||||||
|
['Texas', 'TX'],
|
||||||
|
['US Virgin Islands', 'VI'],
|
||||||
|
['Utah', 'UT'],
|
||||||
|
['Vermont', 'VT'],
|
||||||
|
['Virginia', 'VA'],
|
||||||
|
['Washington', 'WA'],
|
||||||
|
['West Virginia', 'WV'],
|
||||||
|
['Wisconsin', 'WI'],
|
||||||
|
['Wyoming', 'WY'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// So happy that Canada and the US have distinct abbreviations
|
||||||
|
const provinces = [
|
||||||
|
['Alberta', 'AB'],
|
||||||
|
['British Columbia', 'BC'],
|
||||||
|
['Manitoba', 'MB'],
|
||||||
|
['New Brunswick', 'NB'],
|
||||||
|
['Newfoundland', 'NF'],
|
||||||
|
['Northwest Territory', 'NT'],
|
||||||
|
['Nova Scotia', 'NS'],
|
||||||
|
['Nunavut', 'NU'],
|
||||||
|
['Ontario', 'ON'],
|
||||||
|
['Prince Edward Island', 'PE'],
|
||||||
|
['Quebec', 'QC'],
|
||||||
|
['Saskatchewan', 'SK'],
|
||||||
|
['Yukon', 'YT'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const regions = states.concat(provinces);
|
||||||
|
|
||||||
|
let i; // Reusable loop variable
|
||||||
|
if (to == 'abbreviated') {
|
||||||
|
input = input.replace(/\w\S*/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); });
|
||||||
|
for (i = 0; i < regions.length; i++) {
|
||||||
|
if (regions[i][0] == input) {
|
||||||
|
return (regions[i][1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (to == 'name') {
|
||||||
|
input = input.toUpperCase();
|
||||||
|
for (i = 0; i < regions.length; i++) {
|
||||||
|
if (regions[i][1] == input) {
|
||||||
|
return (regions[i][0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<span [innerHTML]="formattedLocation"></span>
|
@ -0,0 +1,83 @@
|
|||||||
|
import { Component, Input, OnChanges } from '@angular/core';
|
||||||
|
import { convertRegion, getFlagEmoji } from '../../common.utils';
|
||||||
|
|
||||||
|
export interface GeolocationData {
|
||||||
|
country: string;
|
||||||
|
city: string;
|
||||||
|
subdivision: string;
|
||||||
|
iso: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-geolocation',
|
||||||
|
templateUrl: './geolocation.component.html',
|
||||||
|
styleUrls: ['./geolocation.component.scss']
|
||||||
|
})
|
||||||
|
export class GeolocationComponent implements OnChanges {
|
||||||
|
@Input() data: GeolocationData;
|
||||||
|
@Input() type: 'node' | 'list-isp' | 'list-country';
|
||||||
|
|
||||||
|
formattedLocation: string = '';
|
||||||
|
|
||||||
|
ngOnChanges(): void {
|
||||||
|
const city = this.data.city ? this.data.city : '';
|
||||||
|
const subdivisionLikeCity = this.data.city === this.data.subdivision;
|
||||||
|
let subdivision = this.data.subdivision;
|
||||||
|
|
||||||
|
if (['US', 'CA'].includes(this.data.iso) === false || (this.type === 'node' && subdivisionLikeCity)) {
|
||||||
|
this.data.subdivision = undefined;
|
||||||
|
} else if (['list-isp', 'list-country'].includes(this.type) === true) {
|
||||||
|
subdivision = convertRegion(this.data.subdivision, 'abbreviated');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type === 'list-country') {
|
||||||
|
if (this.data.city) {
|
||||||
|
this.formattedLocation += ' ' + city;
|
||||||
|
if (this.data.subdivision) {
|
||||||
|
this.formattedLocation += ', ' + subdivision;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.formattedLocation += '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type === 'list-isp') {
|
||||||
|
this.formattedLocation = getFlagEmoji(this.data.iso);
|
||||||
|
if (this.data.city) {
|
||||||
|
this.formattedLocation += ' ' + city;
|
||||||
|
if (this.data.subdivision) {
|
||||||
|
this.formattedLocation += ', ' + subdivision;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.formattedLocation += ' ' + this.data.country;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type === 'node') {
|
||||||
|
const city = this.data.city ? this.data.city : '';
|
||||||
|
|
||||||
|
// City
|
||||||
|
this.formattedLocation = `${city}`;
|
||||||
|
|
||||||
|
// ,Subdivision
|
||||||
|
if (this.formattedLocation.length > 0 && !subdivisionLikeCity) {
|
||||||
|
this.formattedLocation += ', ';
|
||||||
|
}
|
||||||
|
if (!subdivisionLikeCity) {
|
||||||
|
this.formattedLocation += `${subdivision}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// <br>[flag] County
|
||||||
|
if (this.data?.country.length ?? 0 > 0) {
|
||||||
|
if ((this.formattedLocation?.length ?? 0 > 0) && !subdivisionLikeCity) {
|
||||||
|
this.formattedLocation += '<br>';
|
||||||
|
} else if (this.data.city) {
|
||||||
|
this.formattedLocation += ', ';
|
||||||
|
}
|
||||||
|
this.formattedLocation += `${this.data.country} ${getFlagEmoji(this.data.iso)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -91,13 +91,3 @@ export function detectWebGL() {
|
|||||||
return (gl && gl instanceof WebGLRenderingContext);
|
return (gl && gl instanceof WebGLRenderingContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFlagEmoji(countryCode) {
|
|
||||||
if (!countryCode) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const codePoints = countryCode
|
|
||||||
.toUpperCase()
|
|
||||||
.split('')
|
|
||||||
.map(char => 127397 + char.charCodeAt());
|
|
||||||
return String.fromCodePoint(...codePoints);
|
|
||||||
}
|
|
||||||
|
@ -82,6 +82,7 @@ import { SatsComponent } from './components/sats/sats.component';
|
|||||||
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
|
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
|
||||||
import { TimestampComponent } from './components/timestamp/timestamp.component';
|
import { TimestampComponent } from './components/timestamp/timestamp.component';
|
||||||
import { ToggleComponent } from './components/toggle/toggle.component';
|
import { ToggleComponent } from './components/toggle/toggle.component';
|
||||||
|
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -158,6 +159,7 @@ import { ToggleComponent } from './components/toggle/toggle.component';
|
|||||||
SearchResultsComponent,
|
SearchResultsComponent,
|
||||||
TimestampComponent,
|
TimestampComponent,
|
||||||
ToggleComponent,
|
ToggleComponent,
|
||||||
|
GeolocationComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -261,6 +263,7 @@ import { ToggleComponent } from './components/toggle/toggle.component';
|
|||||||
SearchResultsComponent,
|
SearchResultsComponent,
|
||||||
TimestampComponent,
|
TimestampComponent,
|
||||||
ToggleComponent,
|
ToggleComponent,
|
||||||
|
GeolocationComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedModule {
|
export class SharedModule {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user