Merge branch 'master' into simon/da-api-handle-error
@ -228,34 +228,75 @@ export class Common {
|
|||||||
return d.toISOString().split('T')[0] + ' ' + d.toTimeString().split(' ')[0];
|
return d.toISOString().split('T')[0] + ' ' + d.toTimeString().split(' ')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
static formatSocket(publicKey: string, socket: {network: string, addr: string}): NodeSocket {
|
static findSocketNetwork(addr: string): {network: string | null, url: string} {
|
||||||
let network: string | null = null;
|
let network: string | null = null;
|
||||||
|
let url = addr.split('://')[1];
|
||||||
|
|
||||||
if (config.LIGHTNING.BACKEND === 'cln') {
|
if (!url) {
|
||||||
network = socket.network;
|
return {
|
||||||
} else if (config.LIGHTNING.BACKEND === 'lnd') {
|
network: null,
|
||||||
if (socket.addr.indexOf('onion') !== -1) {
|
url: addr,
|
||||||
if (socket.addr.split('.')[0].length >= 56) {
|
};
|
||||||
network = 'torv3';
|
}
|
||||||
} else {
|
|
||||||
network = 'torv2';
|
if (addr.indexOf('onion') !== -1) {
|
||||||
}
|
if (url.split('.')[0].length >= 56) {
|
||||||
} else if (socket.addr.indexOf('i2p') !== -1) {
|
network = 'torv3';
|
||||||
network = 'i2p';
|
|
||||||
} else {
|
} else {
|
||||||
const ipv = isIP(socket.addr.split(':')[0]);
|
network = 'torv2';
|
||||||
if (ipv === 4) {
|
|
||||||
network = 'ipv4';
|
|
||||||
} else if (ipv === 6) {
|
|
||||||
network = 'ipv6';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if (addr.indexOf('i2p') !== -1) {
|
||||||
|
network = 'i2p';
|
||||||
|
} else if (addr.indexOf('ipv4') !== -1) {
|
||||||
|
const ipv = isIP(url.split(':')[0]);
|
||||||
|
if (ipv === 4) {
|
||||||
|
network = 'ipv4';
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
network: null,
|
||||||
|
url: addr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (addr.indexOf('ipv6') !== -1) {
|
||||||
|
url = url.split('[')[1].split(']')[0];
|
||||||
|
const ipv = isIP(url);
|
||||||
|
if (ipv === 6) {
|
||||||
|
const parts = addr.split(':');
|
||||||
|
network = 'ipv6';
|
||||||
|
url = `[${url}]:${parts[parts.length - 1]}`;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
network: null,
|
||||||
|
url: addr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
network: null,
|
||||||
|
url: addr,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
publicKey: publicKey,
|
|
||||||
network: network,
|
network: network,
|
||||||
addr: socket.addr,
|
url: url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static formatSocket(publicKey: string, socket: {network: string, addr: string}): NodeSocket {
|
||||||
|
if (config.LIGHTNING.BACKEND === 'cln') {
|
||||||
|
return {
|
||||||
|
publicKey: publicKey,
|
||||||
|
network: socket.network,
|
||||||
|
addr: socket.addr,
|
||||||
|
};
|
||||||
|
} else /* if (config.LIGHTNING.BACKEND === 'lnd') */ {
|
||||||
|
const formatted = this.findSocketNetwork(socket.addr);
|
||||||
|
return {
|
||||||
|
publicKey: publicKey,
|
||||||
|
network: formatted.network,
|
||||||
|
addr: formatted.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ class ChannelsRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getChannelsByTransactionIds(req: Request, res: Response) {
|
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!Array.isArray(req.query.txId)) {
|
if (!Array.isArray(req.query.txId)) {
|
||||||
res.status(400).send('Not an array');
|
res.status(400).send('Not an array');
|
||||||
@ -83,27 +83,26 @@ class ChannelsRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const channels = await channelsApi.$getChannelsByTransactionId(txIds);
|
const channels = await channelsApi.$getChannelsByTransactionId(txIds);
|
||||||
const inputs: any[] = [];
|
const result: any[] = [];
|
||||||
const outputs: any[] = [];
|
|
||||||
for (const txid of txIds) {
|
for (const txid of txIds) {
|
||||||
const foundChannelInputs = channels.find((channel) => channel.closing_transaction_id === txid);
|
const inputs: any = {};
|
||||||
if (foundChannelInputs) {
|
const outputs: any = {};
|
||||||
inputs.push(foundChannelInputs);
|
// Assuming that we only have one lightning close input in each transaction. This may not be true in the future
|
||||||
} else {
|
const foundChannelsFromInput = channels.find((channel) => channel.closing_transaction_id === txid);
|
||||||
inputs.push(null);
|
if (foundChannelsFromInput) {
|
||||||
|
inputs[0] = foundChannelsFromInput;
|
||||||
}
|
}
|
||||||
const foundChannelOutputs = channels.find((channel) => channel.transaction_id === txid);
|
const foundChannelsFromOutputs = channels.filter((channel) => channel.transaction_id === txid);
|
||||||
if (foundChannelOutputs) {
|
for (const output of foundChannelsFromOutputs) {
|
||||||
outputs.push(foundChannelOutputs);
|
outputs[output.transaction_vout] = output;
|
||||||
} else {
|
|
||||||
outputs.push(null);
|
|
||||||
}
|
}
|
||||||
|
result.push({
|
||||||
|
inputs,
|
||||||
|
outputs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json(result);
|
||||||
inputs: inputs,
|
|
||||||
outputs: outputs,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
|
@ -412,6 +412,7 @@ class NodesApi {
|
|||||||
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
|
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
|
||||||
|
FROM nodes
|
||||||
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_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_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
||||||
@ -502,6 +503,18 @@ class NodesApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update node sockets
|
||||||
|
*/
|
||||||
|
public async $updateNodeSockets(publicKey: string, sockets: {network: string; addr: string}[]): Promise<void> {
|
||||||
|
const formattedSockets = (sockets.map(a => a.addr).join(',')) ?? '';
|
||||||
|
try {
|
||||||
|
await DB.query(`UPDATE nodes SET sockets = ? WHERE public_key = ?`, [formattedSockets, publicKey]);
|
||||||
|
} catch (e) {
|
||||||
|
logger.err(`Cannot update node sockets for ${publicKey}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set all nodes not in `nodesPubkeys` as inactive (status = 0)
|
* Set all nodes not in `nodesPubkeys` as inactive (status = 0)
|
||||||
*/
|
*/
|
||||||
|
@ -27,7 +27,7 @@ class StatisticsApi {
|
|||||||
public async $getLatestStatistics(): Promise<any> {
|
public async $getLatestStatistics(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY added DESC LIMIT 1`);
|
const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY added DESC LIMIT 1`);
|
||||||
const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY added DESC LIMIT 1 OFFSET 7`);
|
const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats WHERE DATE(added) = DATE(NOW() - INTERVAL 7 DAY)`);
|
||||||
return {
|
return {
|
||||||
latest: rows[0],
|
latest: rows[0],
|
||||||
previous: rows2[0],
|
previous: rows2[0],
|
||||||
|
@ -74,7 +74,7 @@ class Logger {
|
|||||||
|
|
||||||
private getNetwork(): string {
|
private getNetwork(): string {
|
||||||
if (config.LIGHTNING.ENABLED) {
|
if (config.LIGHTNING.ENABLED) {
|
||||||
return 'lightning';
|
return config.MEMPOOL.NETWORK === 'mainnet' ? 'lightning' : `${config.MEMPOOL.NETWORK}-lightning`;
|
||||||
}
|
}
|
||||||
if (config.BISQ.ENABLED) {
|
if (config.BISQ.ENABLED) {
|
||||||
return 'bisq';
|
return 'bisq';
|
||||||
|
@ -4,6 +4,7 @@ import nodesApi from '../../../api/explorer/nodes.api';
|
|||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import DB from '../../../database';
|
import DB from '../../../database';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
|
import * as IPCheck from '../../../utils/ipcheck.js';
|
||||||
|
|
||||||
export async function $lookupNodeLocation(): Promise<void> {
|
export async function $lookupNodeLocation(): Promise<void> {
|
||||||
let loggerTimer = new Date().getTime() / 1000;
|
let loggerTimer = new Date().getTime() / 1000;
|
||||||
@ -27,6 +28,11 @@ export async function $lookupNodeLocation(): Promise<void> {
|
|||||||
const asn = lookupAsn.get(ip);
|
const asn = lookupAsn.get(ip);
|
||||||
const isp = lookupIsp.get(ip);
|
const isp = lookupIsp.get(ip);
|
||||||
|
|
||||||
|
let asOverwrite: number | null = null;
|
||||||
|
if (asn && (IPCheck.match(ip, '170.75.160.0/20') || IPCheck.match(ip, '172.81.176.0/21'))) {
|
||||||
|
asOverwrite = 394745;
|
||||||
|
}
|
||||||
|
|
||||||
if (city && (asn || isp)) {
|
if (city && (asn || isp)) {
|
||||||
const query = `
|
const query = `
|
||||||
UPDATE nodes SET
|
UPDATE nodes SET
|
||||||
@ -41,7 +47,7 @@ export async function $lookupNodeLocation(): Promise<void> {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const params = [
|
const params = [
|
||||||
isp?.autonomous_system_number ?? asn?.autonomous_system_number,
|
asOverwrite ?? isp?.autonomous_system_number ?? asn?.autonomous_system_number,
|
||||||
city.city?.geoname_id,
|
city.city?.geoname_id,
|
||||||
city.country?.geoname_id,
|
city.country?.geoname_id,
|
||||||
city.subdivisions ? city.subdivisions[0].geoname_id : null,
|
city.subdivisions ? city.subdivisions[0].geoname_id : null,
|
||||||
@ -91,7 +97,7 @@ export async function $lookupNodeLocation(): Promise<void> {
|
|||||||
if (isp?.autonomous_system_organization ?? asn?.autonomous_system_organization) {
|
if (isp?.autonomous_system_organization ?? asn?.autonomous_system_organization) {
|
||||||
await DB.query(
|
await DB.query(
|
||||||
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'as_organization', ?)`,
|
`INSERT IGNORE INTO geo_names (id, type, names) VALUES (?, 'as_organization', ?)`,
|
||||||
[isp?.autonomous_system_number ?? asn?.autonomous_system_number, JSON.stringify(isp?.isp ?? asn?.autonomous_system_organization)]);
|
[asOverwrite ?? isp?.autonomous_system_number ?? asn?.autonomous_system_number, JSON.stringify(isp?.isp ?? asn?.autonomous_system_organization)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import { isIP } from 'net';
|
|||||||
import { Common } from '../../../api/common';
|
import { Common } from '../../../api/common';
|
||||||
import channelsApi from '../../../api/explorer/channels.api';
|
import channelsApi from '../../../api/explorer/channels.api';
|
||||||
import nodesApi from '../../../api/explorer/nodes.api';
|
import nodesApi from '../../../api/explorer/nodes.api';
|
||||||
|
import { ResultSetHeader } from 'mysql2';
|
||||||
|
|
||||||
const fsPromises = promises;
|
const fsPromises = promises;
|
||||||
|
|
||||||
@ -19,7 +20,12 @@ class LightningStatsImporter {
|
|||||||
logger.info('Caching funding txs for currently existing channels');
|
logger.info('Caching funding txs for currently existing channels');
|
||||||
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
|
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
|
||||||
|
|
||||||
|
if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.$importHistoricalLightningStats();
|
await this.$importHistoricalLightningStats();
|
||||||
|
await this.$cleanupIncorrectSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,6 +57,8 @@ class LightningStatsImporter {
|
|||||||
features: node.features,
|
features: node.features,
|
||||||
});
|
});
|
||||||
nodesInDb[node.pub_key] = node;
|
nodesInDb[node.pub_key] = node;
|
||||||
|
} else {
|
||||||
|
await nodesApi.$updateNodeSockets(node.pub_key, node.addresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasOnion = false;
|
let hasOnion = false;
|
||||||
@ -363,10 +371,16 @@ class LightningStatsImporter {
|
|||||||
graph = JSON.parse(fileContent);
|
graph = JSON.parse(fileContent);
|
||||||
graph = await this.cleanupTopology(graph);
|
graph = await this.cleanupTopology(graph);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content`);
|
logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isIncorrectSnapshot(timestamp, graph)) {
|
||||||
|
logger.debug(`Ignoring ${this.topologiesFolder}/${filename}, because we defined it as an incorrect snapshot`);
|
||||||
|
++totalProcessed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!logStarted) {
|
if (!logStarted) {
|
||||||
logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`);
|
logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`);
|
||||||
logStarted = true;
|
logStarted = true;
|
||||||
@ -397,7 +411,7 @@ class LightningStatsImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanupTopology(graph) {
|
cleanupTopology(graph): ILightningApi.NetworkGraph {
|
||||||
const newGraph = {
|
const newGraph = {
|
||||||
nodes: <ILightningApi.Node[]>[],
|
nodes: <ILightningApi.Node[]>[],
|
||||||
edges: <ILightningApi.Channel[]>[],
|
edges: <ILightningApi.Channel[]>[],
|
||||||
@ -407,9 +421,10 @@ class LightningStatsImporter {
|
|||||||
const addressesParts = (node.addresses ?? '').split(',');
|
const addressesParts = (node.addresses ?? '').split(',');
|
||||||
const addresses: any[] = [];
|
const addresses: any[] = [];
|
||||||
for (const address of addressesParts) {
|
for (const address of addressesParts) {
|
||||||
|
const formatted = Common.findSocketNetwork(address);
|
||||||
addresses.push({
|
addresses.push({
|
||||||
network: '',
|
network: formatted.network,
|
||||||
addr: address
|
addr: formatted.url
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -456,6 +471,69 @@ class LightningStatsImporter {
|
|||||||
|
|
||||||
return newGraph;
|
return newGraph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isIncorrectSnapshot(timestamp, graph): boolean {
|
||||||
|
if (timestamp >= 1549065600 /* 2019-02-02 */ && timestamp <= 1550620800 /* 2019-02-20 */ && graph.nodes.length < 2600) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (timestamp >= 1552953600 /* 2019-03-19 */ && timestamp <= 1556323200 /* 2019-05-27 */ && graph.nodes.length < 4000) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (timestamp >= 1557446400 /* 2019-05-10 */ && timestamp <= 1560470400 /* 2019-06-14 */ && graph.nodes.length < 4000) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (timestamp >= 1561680000 /* 2019-06-28 */ && timestamp <= 1563148800 /* 2019-07-15 */ && graph.nodes.length < 4000) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (timestamp >= 1571270400 /* 2019-11-17 */ && timestamp <= 1580601600 /* 2020-02-02 */ && graph.nodes.length < 4500) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (timestamp >= 1591142400 /* 2020-06-03 */ && timestamp <= 1592006400 /* 2020-06-13 */ && graph.nodes.length < 5500) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (timestamp >= 1632787200 /* 2021-09-28 */ && timestamp <= 1633564800 /* 2021-10-07 */ && graph.nodes.length < 13000) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (timestamp >= 1634256000 /* 2021-10-15 */ && timestamp <= 1645401600 /* 2022-02-21 */ && graph.nodes.length < 17000) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (timestamp >= 1654992000 /* 2022-06-12 */ && timestamp <= 1661472000 /* 2022-08-26 */ && graph.nodes.length < 14000) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $cleanupIncorrectSnapshot(): Promise<void> {
|
||||||
|
// We do not run this one automatically because those stats are not supposed to be inserted in the first
|
||||||
|
// place, but I write them here to remind us we manually run those queries
|
||||||
|
|
||||||
|
// DELETE FROM lightning_stats
|
||||||
|
// WHERE (
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1549065600 AND UNIX_TIMESTAMP(added) <= 1550620800 AND node_count < 2600 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1552953600 AND UNIX_TIMESTAMP(added) <= 1556323200 AND node_count < 4000 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1557446400 AND UNIX_TIMESTAMP(added) <= 1560470400 AND node_count < 4000 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1561680000 AND UNIX_TIMESTAMP(added) <= 1563148800 AND node_count < 4000 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1571270400 AND UNIX_TIMESTAMP(added) <= 1580601600 AND node_count < 4500 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1591142400 AND UNIX_TIMESTAMP(added) <= 1592006400 AND node_count < 5500 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1632787200 AND UNIX_TIMESTAMP(added) <= 1633564800 AND node_count < 13000 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1634256000 AND UNIX_TIMESTAMP(added) <= 1645401600 AND node_count < 17000 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1654992000 AND UNIX_TIMESTAMP(added) <= 1661472000 AND node_count < 14000
|
||||||
|
// )
|
||||||
|
|
||||||
|
// DELETE FROM node_stats
|
||||||
|
// WHERE (
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1549065600 AND UNIX_TIMESTAMP(added) <= 1550620800 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1552953600 AND UNIX_TIMESTAMP(added) <= 1556323200 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1557446400 AND UNIX_TIMESTAMP(added) <= 1560470400 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1561680000 AND UNIX_TIMESTAMP(added) <= 1563148800 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1571270400 AND UNIX_TIMESTAMP(added) <= 1580601600 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1591142400 AND UNIX_TIMESTAMP(added) <= 1592006400 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1632787200 AND UNIX_TIMESTAMP(added) <= 1633564800 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1634256000 AND UNIX_TIMESTAMP(added) <= 1645401600 OR
|
||||||
|
// UNIX_TIMESTAMP(added) >= 1654992000 AND UNIX_TIMESTAMP(added) <= 1661472000
|
||||||
|
// )
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new LightningStatsImporter;
|
export default new LightningStatsImporter;
|
||||||
|
119
backend/src/utils/ipcheck.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
var net = require('net');
|
||||||
|
|
||||||
|
var IPCheck = module.exports = function(input) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!(self instanceof IPCheck)) {
|
||||||
|
return new IPCheck(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.input = input;
|
||||||
|
self.parse();
|
||||||
|
};
|
||||||
|
|
||||||
|
IPCheck.prototype.parse = function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!self.input || typeof self.input !== 'string') return self.valid = false;
|
||||||
|
|
||||||
|
var ip;
|
||||||
|
|
||||||
|
var pos = self.input.lastIndexOf('/');
|
||||||
|
if (pos !== -1) {
|
||||||
|
ip = self.input.substring(0, pos);
|
||||||
|
self.mask = +self.input.substring(pos + 1);
|
||||||
|
} else {
|
||||||
|
ip = self.input;
|
||||||
|
self.mask = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ipv = net.isIP(ip);
|
||||||
|
self.valid = !!self.ipv && !isNaN(self.mask);
|
||||||
|
|
||||||
|
if (!self.valid) return;
|
||||||
|
|
||||||
|
// default mask = 32 for ipv4 and 128 for ipv6
|
||||||
|
if (self.mask === null) self.mask = self.ipv === 4 ? 32 : 128;
|
||||||
|
|
||||||
|
if (self.ipv === 4) {
|
||||||
|
// difference between ipv4 and ipv6 masks
|
||||||
|
self.mask += 96;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.mask < 0 || self.mask > 128) {
|
||||||
|
self.valid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.address = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
|
||||||
|
|
||||||
|
if(self.ipv === 4){
|
||||||
|
self.parseIPv4(ip);
|
||||||
|
}else{
|
||||||
|
self.parseIPv6(ip);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
IPCheck.prototype.parseIPv4 = function(ip) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// ipv4 addresses live under ::ffff:0:0
|
||||||
|
self.address[10] = self.address[11] = 0xff;
|
||||||
|
|
||||||
|
var octets = ip.split('.');
|
||||||
|
for (var i = 0; i < 4; i++) {
|
||||||
|
self.address[i + 12] = parseInt(octets[i], 10);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var V6_TRANSITIONAL = /:(\d+\.\d+\.\d+\.\d+)$/;
|
||||||
|
|
||||||
|
IPCheck.prototype.parseIPv6 = function(ip) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var transitionalMatch = V6_TRANSITIONAL.exec(ip);
|
||||||
|
if(transitionalMatch){
|
||||||
|
self.parseIPv4(transitionalMatch[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bits = ip.split(':');
|
||||||
|
if (bits.length < 8) {
|
||||||
|
ip = ip.replace('::', Array(11 - bits.length).join(':'));
|
||||||
|
bits = ip.split(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
var j = 0;
|
||||||
|
for (var i = 0; i < bits.length; i += 1) {
|
||||||
|
var x = bits[i] ? parseInt(bits[i], 16) : 0;
|
||||||
|
self.address[j++] = x >> 8;
|
||||||
|
self.address[j++] = x & 0xff;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
IPCheck.prototype.match = function(cidr) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!(cidr instanceof IPCheck)) cidr = new IPCheck(cidr);
|
||||||
|
if (!self.valid || !cidr.valid) return false;
|
||||||
|
|
||||||
|
var mask = cidr.mask;
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
while (mask >= 8) {
|
||||||
|
if (self.address[i] !== cidr.address[i]) return false;
|
||||||
|
|
||||||
|
i++;
|
||||||
|
mask -= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
var shift = 8 - mask;
|
||||||
|
return (self.address[i] >>> shift) === (cidr.address[i] >>> shift);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
IPCheck.match = function(ip, cidr) {
|
||||||
|
ip = ip instanceof IPCheck ? ip : new IPCheck(ip);
|
||||||
|
return ip.match(cidr);
|
||||||
|
};
|
@ -13,7 +13,8 @@
|
|||||||
"node_modules/@types"
|
"node_modules/@types"
|
||||||
],
|
],
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true,
|
||||||
|
"allowJs": true,
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts"
|
"src/**/*.ts"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
||||||
import { StartComponent } from './components/start/start.component';
|
import { StartComponent } from './components/start/start.component';
|
||||||
import { TransactionComponent } from './components/transaction/transaction.component';
|
import { TransactionComponent } from './components/transaction/transaction.component';
|
||||||
import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component';
|
import { TransactionPreviewComponent } from './components/transaction/transaction-preview.component';
|
||||||
@ -25,6 +26,10 @@ import { AssetsComponent } from './components/assets/assets.component';
|
|||||||
import { AssetComponent } from './components/asset/asset.component';
|
import { AssetComponent } from './components/asset/asset.component';
|
||||||
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
|
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
|
||||||
|
|
||||||
|
const browserWindow = window || {};
|
||||||
|
// @ts-ignore
|
||||||
|
const browserWindowEnv = browserWindow.__env || {};
|
||||||
|
|
||||||
let routes: Routes = [
|
let routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'testnet',
|
path: 'testnet',
|
||||||
@ -32,7 +37,8 @@ let routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
|
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
|
||||||
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
@ -109,7 +115,8 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'docs',
|
path: 'docs',
|
||||||
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule)
|
loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule),
|
||||||
|
data: { preload: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'api',
|
path: 'api',
|
||||||
@ -117,7 +124,8 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'lightning',
|
path: 'lightning',
|
||||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
|
||||||
|
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -410,10 +418,6 @@ let routes: Routes = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const browserWindow = window || {};
|
|
||||||
// @ts-ignore
|
|
||||||
const browserWindowEnv = browserWindow.__env || {};
|
|
||||||
|
|
||||||
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
|
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
|
||||||
routes = [{
|
routes = [{
|
||||||
path: '',
|
path: '',
|
||||||
@ -691,7 +695,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
|||||||
initialNavigation: 'enabled',
|
initialNavigation: 'enabled',
|
||||||
scrollPositionRestoration: 'enabled',
|
scrollPositionRestoration: 'enabled',
|
||||||
anchorScrolling: 'enabled',
|
anchorScrolling: 'enabled',
|
||||||
preloadingStrategy: PreloadAllModules
|
preloadingStrategy: AppPreloadingStrategy
|
||||||
})],
|
})],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule { }
|
export class AppRoutingModule { }
|
||||||
|
@ -18,6 +18,7 @@ import { LanguageService } from './services/language.service';
|
|||||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
||||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||||
|
import { AppPreloadingStrategy } from './app.preloading-strategy';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -44,6 +45,7 @@ import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe
|
|||||||
ShortenStringPipe,
|
ShortenStringPipe,
|
||||||
FiatShortenerPipe,
|
FiatShortenerPipe,
|
||||||
CapAddressPipe,
|
CapAddressPipe,
|
||||||
|
AppPreloadingStrategy,
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
10
frontend/src/app/app.preloading-strategy.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { PreloadingStrategy, Route } from '@angular/router';
|
||||||
|
import { Observable, timer, mergeMap, of } from 'rxjs';
|
||||||
|
|
||||||
|
export class AppPreloadingStrategy implements PreloadingStrategy {
|
||||||
|
preload(route: Route, load: Function): Observable<any> {
|
||||||
|
return route.data && route.data.preload
|
||||||
|
? timer(1500).pipe(mergeMap(() => load()))
|
||||||
|
: of(null);
|
||||||
|
}
|
||||||
|
}
|
@ -61,7 +61,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 1130px) {
|
@media (min-width: 1130px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 1130px) {
|
@media (min-width: 830px) and (max-width: 1130px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup"
|
<form [formGroup]="radioGroupForm" class="formRadioGroup"
|
||||||
[class]="stateService.env.MINING_DASHBOARD ? 'mining' : ''" (click)="saveGraphPreference()">
|
[class]="(stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING) ? 'mining' : 'no-menu'" (click)="saveGraphPreference()">
|
||||||
<div *ngIf="!isMobile()" class="btn-group btn-group-toggle">
|
<div *ngIf="!isMobile()" class="btn-group btn-group-toggle">
|
||||||
<label ngbButtonLabel class="btn-primary btn-sm mr-2">
|
<label ngbButtonLabel class="btn-primary btn-sm mr-2">
|
||||||
<a [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
|
<a [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
|
||||||
|
@ -55,13 +55,19 @@
|
|||||||
.formRadioGroup.mining {
|
.formRadioGroup.mining {
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.formRadioGroup.no-menu {
|
||||||
|
@media (min-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: -33px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.loading{
|
.loading{
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
<div class="page-title">
|
<div class="page-title">
|
||||||
<h1 i18n="shared.transaction">Transaction</h1>
|
<h1 i18n="shared.transaction">Transaction</h1>
|
||||||
|
<a class="tx-link" [routerLink]="['/tx/' | relativeUrl, txId]">
|
||||||
|
<span class="truncated">{{txId.slice(0,-4)}}</span><span class="last-four">{{txId.slice(-4)}}</span>
|
||||||
|
</a>
|
||||||
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
|
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
|
||||||
<app-tx-features [tx]="tx"></app-tx-features>
|
<app-tx-features [tx]="tx"></app-tx-features>
|
||||||
<span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1">
|
<span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1">
|
||||||
@ -13,104 +16,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a [routerLink]="['/tx/' | relativeUrl, txId]" class="tx-link">
|
<div class="top-data row">
|
||||||
{{ txId }}
|
<span class="field col-sm-4 text-left">
|
||||||
</a>
|
<ng-template [ngIf]="isLiquid && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
||||||
|
<ng-template #defaultAmount>
|
||||||
|
<app-amount [satoshis]="totalValue"></app-amount>
|
||||||
|
</ng-template>
|
||||||
|
</span>
|
||||||
|
<span class="field col-sm-4 text-center">‎{{ (tx.status.confirmed ? tx.status.block_time : transactionTime) * 1000 | date:'yyyy-MM-dd HH:mm' }}</span>
|
||||||
|
<span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee </span>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row graph-wrapper">
|
||||||
<div class="col-sm">
|
<tx-bowtie-graph [tx]="tx" [width]="1112" [height]="346" [network]="network"></tx-bowtie-graph>
|
||||||
<table class="table table-borderless table-striped">
|
<div class="above-bow">
|
||||||
<tbody>
|
<p class="field pair">
|
||||||
<tr *ngIf="tx.status.confirmed; else firstSeen">
|
<span [innerHTML]="'‎' + (tx.size | bytes: 2)"></span>
|
||||||
<td i18n="block.timestamp">Timestamp</td>
|
<span [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></span>
|
||||||
<td>
|
</p>
|
||||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
<p class="field" *ngIf="!isCoinbase(tx)">
|
||||||
</td>
|
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||||
</tr>
|
</p>
|
||||||
<ng-template #firstSeen>
|
|
||||||
<tr>
|
|
||||||
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
|
|
||||||
<td *ngIf="transactionTime > 0; else notSeen">
|
|
||||||
‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
|
||||||
</td>
|
|
||||||
<ng-template #notSeen>
|
|
||||||
<td>?</td>
|
|
||||||
</ng-template>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
<tr>
|
|
||||||
<td class="td-width" i18n="dashboard.latest-transactions.amount">Amount</td>
|
|
||||||
<td>
|
|
||||||
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
|
|
||||||
<ng-template #defaultAmount>
|
|
||||||
<app-amount [satoshis]="getTotalTxOutput(tx)"></app-amount>
|
|
||||||
</ng-template>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.size">Size</td>
|
|
||||||
<td [innerHTML]="'‎' + (tx.size | bytes: 2)"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.weight">Weight</td>
|
|
||||||
<td [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td i18n="transaction.inputs">Inputs</td>
|
|
||||||
<td *ngIf="!isCoinbase(tx); else coinbaseInputs">{{ tx.vin.length }}</td>
|
|
||||||
<ng-template #coinbaseInputs>
|
|
||||||
<td i18n="transactions-list.coinbase">Coinbase</td>
|
|
||||||
</ng-template>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="overlaid">
|
||||||
<div class="col-sm">
|
<ng-container [ngSwitch]="extraData">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="opreturns" *ngSwitchCase="'coinbase'">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
|
||||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
|
||||||
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></td>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="!cpfpInfo || (!cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length); else cpfpFee">
|
|
||||||
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
|
||||||
<td>
|
|
||||||
{{ tx.feePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
|
||||||
<ng-template [ngIf]="tx.status.confirmed">
|
|
||||||
|
|
||||||
<app-tx-fee-rating *ngIf="tx.fee && ((cpfpInfo && !cpfpInfo.bestDescendant && !cpfpInfo.ancestors.length) || !cpfpInfo)" [tx]="tx"></app-tx-fee-rating>
|
|
||||||
</ng-template>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<ng-template #cpfpFee>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
<td class="label">Coinbase</td>
|
||||||
<td>
|
<td class="message">{{ tx.vin[0].scriptsig | hex2ascii }}</td>
|
||||||
<div class="effective-fee-container">
|
|
||||||
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
|
||||||
<ng-template [ngIf]="tx.status.confirmed">
|
|
||||||
<app-tx-fee-rating class="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</tbody>
|
||||||
<tr>
|
</table>
|
||||||
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
<table class="opreturns" *ngSwitchCase="'opreturn'">
|
||||||
<td [innerHTML]="'‎' + (tx.weight / 4 | vbytes: 2)"></td>
|
<tbody>
|
||||||
</tr>
|
<ng-container *ngFor="let vout of opReturns.slice(0,3)">
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="transaction.locktime">Locktime</td>
|
<td class="label">OP_RETURN</td>
|
||||||
<td [innerHTML]="'‎' + (tx.locktime | number)"></td>
|
<td *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" class="message">{{ vout.scriptpubkey_asm | hex2ascii }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
</ng-container>
|
||||||
<td i18n="transaction.outputs">Outputs</td>
|
</tbody>
|
||||||
<td>{{ tx.vout.length }}</td>
|
</table>
|
||||||
</tr>
|
</ng-container>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,26 +10,10 @@
|
|||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-small-height {
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-green {
|
|
||||||
color: #1a9436;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-red {
|
|
||||||
color: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.effective-fee-container {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
h2 {
|
h2 {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@ -46,8 +30,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 2px;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 52px;
|
font-size: 52px;
|
||||||
@ -58,6 +43,43 @@
|
|||||||
.features {
|
.features {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-link {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
margin: 0 1em;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.truncated {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-right: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-four {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-data {
|
||||||
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
@ -68,8 +90,76 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
font-size: 32px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
::ng-deep .symbol {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #ffffff66;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pair > *:first-child {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tx-link {
|
.tx-link {
|
||||||
display: inline-block;
|
display: inline;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-wrapper {
|
||||||
|
position: relative;
|
||||||
|
background: #181b2d;
|
||||||
|
padding: 10px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
|
||||||
|
.above-bow {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlaid {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 28px;
|
||||||
|
max-width: 90%;
|
||||||
|
margin: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.opreturns {
|
||||||
|
width: auto;
|
||||||
|
margin: auto;
|
||||||
|
table-layout: auto;
|
||||||
|
background: #2d3348af;
|
||||||
|
border-top-left-radius: 5px;
|
||||||
|
border-top-right-radius: 5px;
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 10px 10px;
|
||||||
|
|
||||||
|
&.message {
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,10 +7,9 @@ import {
|
|||||||
catchError,
|
catchError,
|
||||||
retryWhen,
|
retryWhen,
|
||||||
delay,
|
delay,
|
||||||
map
|
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||||
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from } from 'rxjs';
|
import { of, merge, Subscription, Observable, Subject, from } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { OpenGraphService } from 'src/app/services/opengraph.service';
|
import { OpenGraphService } from 'src/app/services/opengraph.service';
|
||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
@ -37,6 +36,10 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
showCpfpDetails = false;
|
showCpfpDetails = false;
|
||||||
fetchCpfp$ = new Subject<string>();
|
fetchCpfp$ = new Subject<string>();
|
||||||
liquidUnblinding = new LiquidUnblinding();
|
liquidUnblinding = new LiquidUnblinding();
|
||||||
|
isLiquid = false;
|
||||||
|
totalValue: number;
|
||||||
|
opReturns: Vout[];
|
||||||
|
extraData: 'none' | 'coinbase' | 'opreturn';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -49,7 +52,12 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.stateService.networkChanged$.subscribe(
|
this.stateService.networkChanged$.subscribe(
|
||||||
(network) => (this.network = network)
|
(network) => {
|
||||||
|
this.network = network;
|
||||||
|
if (this.network === 'liquid' || this.network == 'liquidtestnet') {
|
||||||
|
this.isLiquid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.fetchCpfpSubscription = this.fetchCpfp$
|
this.fetchCpfpSubscription = this.fetchCpfp$
|
||||||
@ -152,6 +160,9 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
|
||||||
this.isLoadingTx = false;
|
this.isLoadingTx = false;
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
|
this.totalValue = this.tx.vout.reduce((acc, v) => v.value + acc, 0);
|
||||||
|
this.opReturns = this.getOpReturns(this.tx);
|
||||||
|
this.extraData = this.chooseExtraData();
|
||||||
|
|
||||||
if (!tx.status.confirmed && tx.firstSeen) {
|
if (!tx.status.confirmed && tx.firstSeen) {
|
||||||
this.transactionTime = tx.firstSeen;
|
this.transactionTime = tx.firstSeen;
|
||||||
@ -217,6 +228,20 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
|||||||
return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b);
|
return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOpReturns(tx: Transaction): Vout[] {
|
||||||
|
return tx.vout.filter((v) => v.scriptpubkey_type === 'op_return' && v.scriptpubkey_asm !== 'OP_RETURN');
|
||||||
|
}
|
||||||
|
|
||||||
|
chooseExtraData(): 'none' | 'opreturn' | 'coinbase' {
|
||||||
|
if (this.isCoinbase(this.tx)) {
|
||||||
|
return 'coinbase';
|
||||||
|
} else if (this.opReturns?.length) {
|
||||||
|
return 'opreturn';
|
||||||
|
} else {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
this.fetchCpfpSubscription.unsubscribe();
|
this.fetchCpfpSubscription.unsubscribe();
|
||||||
|
@ -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 > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
|
<ng-template ngFor let-vin let-vindex="index" [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 !== ''
|
||||||
@ -77,7 +77,7 @@
|
|||||||
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
|
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div>
|
<div>
|
||||||
<app-address-labels [vin]="vin" [channel]="channels && channels.inputs[i] || null"></app-address-labels>
|
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] || null"></app-address-labels>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -172,7 +172,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<app-address-labels [vout]="vout" [channel]="channels && channels.outputs[i] && channels.outputs[i].transaction_vout === vindex ? channels.outputs[i] : null"></app-address-labels>
|
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #scriptpubkey_type>
|
<ng-template #scriptpubkey_type>
|
||||||
<ng-template [ngIf]="vout.pegout" [ngIfElse]="defaultscriptpubkey_type">
|
<ng-template [ngIf]="vout.pegout" [ngIfElse]="defaultscriptpubkey_type">
|
||||||
@ -212,15 +212,15 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
<td class="arrow-td">
|
<td class="arrow-td">
|
||||||
<span *ngIf="!outspends[i] || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey">
|
<span *ngIf="!tx._outspends || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey">
|
||||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
</span>
|
</span>
|
||||||
<ng-template #outspend>
|
<ng-template #outspend>
|
||||||
<span *ngIf="!outspends[i][vindex] || !outspends[i][vindex].spent; else spent" class="green">
|
<span *ngIf="!tx._outspends[vindex] || !tx._outspends[vindex].spent; else spent" class="green">
|
||||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
</span>
|
</span>
|
||||||
<ng-template #spent>
|
<ng-template #spent>
|
||||||
<a *ngIf="outspends[i][vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, outspends[i][vindex].txid]" class="red">
|
<a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].txid]" class="red">
|
||||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
<ng-template #outputNoTxId>
|
<ng-template #outputNoTxId>
|
||||||
|
@ -27,7 +27,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
@Input() outputIndex: number;
|
@Input() outputIndex: number;
|
||||||
@Input() address: string = '';
|
@Input() address: string = '';
|
||||||
@Input() rowLimit = 12;
|
@Input() rowLimit = 12;
|
||||||
@Input() channels: { inputs: any[], outputs: any[] };
|
|
||||||
|
|
||||||
@Output() loadMore = new EventEmitter();
|
@Output() loadMore = new EventEmitter();
|
||||||
|
|
||||||
@ -36,8 +35,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
|
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
|
||||||
refreshChannels$: ReplaySubject<string[]> = new ReplaySubject();
|
refreshChannels$: ReplaySubject<string[]> = new ReplaySubject();
|
||||||
showDetails$ = new BehaviorSubject<boolean>(false);
|
showDetails$ = new BehaviorSubject<boolean>(false);
|
||||||
outspends: Outspend[][] = [];
|
|
||||||
assetsMinimal: any;
|
assetsMinimal: any;
|
||||||
|
transactionsLength: number = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
@ -47,7 +46,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
private ref: ChangeDetectorRef,
|
private ref: ChangeDetectorRef,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
|
this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block));
|
||||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||||
|
|
||||||
@ -62,14 +61,20 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
.pipe(
|
.pipe(
|
||||||
switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)),
|
switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)),
|
||||||
tap((outspends: Outspend[][]) => {
|
tap((outspends: Outspend[][]) => {
|
||||||
this.outspends = this.outspends.concat(outspends);
|
if (!this.transactions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const transactions = this.transactions.filter((tx) => !tx._outspends);
|
||||||
|
outspends.forEach((outspend, i) => {
|
||||||
|
transactions[i]._outspends = outspend;
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
this.stateService.utxoSpent$
|
this.stateService.utxoSpent$
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((utxoSpent) => {
|
tap((utxoSpent) => {
|
||||||
for (const i in utxoSpent) {
|
for (const i in utxoSpent) {
|
||||||
this.outspends[0][i] = {
|
this.transactions[0]._outspends[i] = {
|
||||||
spent: true,
|
spent: true,
|
||||||
txid: utxoSpent[i].txid,
|
txid: utxoSpent[i].txid,
|
||||||
vin: utxoSpent[i].vin,
|
vin: utxoSpent[i].vin,
|
||||||
@ -81,21 +86,23 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
.pipe(
|
.pipe(
|
||||||
filter(() => this.stateService.env.LIGHTNING),
|
filter(() => this.stateService.env.LIGHTNING),
|
||||||
switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)),
|
switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)),
|
||||||
map((channels) => {
|
tap((channels) => {
|
||||||
this.channels = channels;
|
const transactions = this.transactions.filter((tx) => !tx._channels);
|
||||||
|
channels.forEach((channel, i) => {
|
||||||
|
transactions[i]._channels = channel;
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
,
|
,
|
||||||
).subscribe(() => this.ref.markForCheck());
|
).subscribe(() => this.ref.markForCheck());
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges() {
|
ngOnChanges(): void {
|
||||||
if (!this.transactions || !this.transactions.length) {
|
if (!this.transactions || !this.transactions.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.paginated) {
|
|
||||||
this.outspends = [];
|
this.transactionsLength = this.transactions.length;
|
||||||
}
|
|
||||||
if (this.outputIndex) {
|
if (this.outputIndex) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const assetBoxElements = document.getElementsByClassName('assetBox');
|
const assetBoxElements = document.getElementsByClassName('assetBox');
|
||||||
@ -126,14 +133,19 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
tx['addressValue'] = addressIn - addressOut;
|
tx['addressValue'] = addressIn - addressOut;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const txIds = this.transactions.map((tx) => tx.txid);
|
const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
|
||||||
this.refreshOutspends$.next(txIds);
|
if (txIds.length) {
|
||||||
if (!this.channels) {
|
this.refreshOutspends$.next(txIds);
|
||||||
this.refreshChannels$.next(txIds);
|
}
|
||||||
|
if (this.stateService.env.LIGHTNING) {
|
||||||
|
const txIds = this.transactions.filter((tx) => !tx._channels).map((tx) => tx.txid);
|
||||||
|
if (txIds.length) {
|
||||||
|
this.refreshChannels$.next(txIds);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onScroll() {
|
onScroll(): void {
|
||||||
const scrollHeight = document.body.scrollHeight;
|
const scrollHeight = document.body.scrollHeight;
|
||||||
const scrollTop = document.documentElement.scrollTop;
|
const scrollTop = document.documentElement.scrollTop;
|
||||||
if (scrollHeight > 0){
|
if (scrollHeight > 0){
|
||||||
@ -148,11 +160,11 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
return tx.vout.some((v: any) => v.value === undefined);
|
return tx.vout.some((v: any) => v.value === undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalTxOutput(tx: Transaction) {
|
getTotalTxOutput(tx: Transaction): number {
|
||||||
return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b);
|
return tx.vout.map((v: Vout) => v.value || 0).reduce((a: number, b: number) => a + b);
|
||||||
}
|
}
|
||||||
|
|
||||||
switchCurrency() {
|
switchCurrency(): void {
|
||||||
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -164,7 +176,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
return tx.txid + tx.status.confirmed;
|
return tx.txid + tx.status.confirmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByIndexFn(index: number) {
|
trackByIndexFn(index: number): number {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,7 +189,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
return Math.pow(base, exponent);
|
return Math.pow(base, exponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDetails() {
|
toggleDetails(): void {
|
||||||
if (this.showDetails$.value === true) {
|
if (this.showDetails$.value === true) {
|
||||||
this.showDetails$.next(false);
|
this.showDetails$.next(false);
|
||||||
} else {
|
} else {
|
||||||
@ -185,7 +197,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMoreInputs(tx: Transaction) {
|
loadMoreInputs(tx: Transaction): void {
|
||||||
tx['@vinLimit'] = false;
|
tx['@vinLimit'] = false;
|
||||||
|
|
||||||
this.electrsApiService.getTransaction$(tx.txid)
|
this.electrsApiService.getTransaction$(tx.txid)
|
||||||
@ -196,7 +208,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy(): void {
|
||||||
this.outspendsSubscription.unsubscribe();
|
this.outspendsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
<svg *ngIf="inputs && outputs" class="bowtie" [attr.height]="(height + 10) + 'px'" [attr.width]="width + 'px'">
|
||||||
|
<defs>
|
||||||
|
<marker id="input-arrow" viewBox="-5 -5 10 10"
|
||||||
|
refX="0" refY="0"
|
||||||
|
markerUnits="strokeWidth"
|
||||||
|
markerWidth="1.5" markerHeight="1"
|
||||||
|
orient="auto">
|
||||||
|
<path d="M -5 -5 L 0 0 L -5 5 L 1 5 L 1 -5 Z" stroke-width="0" [attr.fill]="gradient[0]"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="output-arrow" viewBox="-5 -5 10 10"
|
||||||
|
refX="0" refY="0"
|
||||||
|
markerUnits="strokeWidth"
|
||||||
|
markerWidth="1.5" markerHeight="1"
|
||||||
|
orient="auto">
|
||||||
|
<path d="M 1 -5 L 0 -5 L -5 0 L 0 5 L 1 5 Z" stroke-width="0" [attr.fill]="gradient[0]"/>
|
||||||
|
</marker>
|
||||||
|
<marker id="fee-arrow" viewBox="-5 -5 10 10"
|
||||||
|
refX="0" refY="0"
|
||||||
|
markerUnits="strokeWidth"
|
||||||
|
markerWidth="1.5" markerHeight="1"
|
||||||
|
orient="auto">
|
||||||
|
</marker>
|
||||||
|
<linearGradient id="input-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" [attr.stop-color]="gradient[0]" />
|
||||||
|
<stop offset="100%" [attr.stop-color]="gradient[1]" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="output-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" [attr.stop-color]="gradient[1]" />
|
||||||
|
<stop offset="100%" [attr.stop-color]="gradient[0]" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="fee-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" [attr.stop-color]="gradient[1]" />
|
||||||
|
<stop offset="50%" [attr.stop-color]="gradient[1]" />
|
||||||
|
<stop offset="100%" stop-color="transparent" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path [attr.d]="middle.path" class="line middle" [style]="middle.style"/>
|
||||||
|
<ng-container *ngFor="let input of inputs">
|
||||||
|
<path [attr.d]="input.path" class="line {{input.class}}" [style]="input.style" attr.marker-start="url(#{{input.class}}-arrow)"/>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngFor="let output of outputs">
|
||||||
|
<path [attr.d]="output.path" class="line {{output.class}}" [style]="output.style" attr.marker-start="url(#{{output.class}}-arrow)" />
|
||||||
|
</ng-container>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,15 @@
|
|||||||
|
.bowtie {
|
||||||
|
.line {
|
||||||
|
fill: none;
|
||||||
|
|
||||||
|
&.input {
|
||||||
|
stroke: url(#input-gradient);
|
||||||
|
}
|
||||||
|
&.output {
|
||||||
|
stroke: url(#output-gradient);
|
||||||
|
}
|
||||||
|
&.fee {
|
||||||
|
stroke: url(#fee-gradient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,169 @@
|
|||||||
|
import { Component, OnInit, Input, OnChanges } from '@angular/core';
|
||||||
|
import { Transaction } from '../../interfaces/electrs.interface';
|
||||||
|
|
||||||
|
interface SvgLine {
|
||||||
|
path: string;
|
||||||
|
style: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tx-bowtie-graph',
|
||||||
|
templateUrl: './tx-bowtie-graph.component.html',
|
||||||
|
styleUrls: ['./tx-bowtie-graph.component.scss'],
|
||||||
|
})
|
||||||
|
export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||||
|
@Input() tx: Transaction;
|
||||||
|
@Input() network: string;
|
||||||
|
@Input() width = 1200;
|
||||||
|
@Input() height = 600;
|
||||||
|
@Input() combinedWeight = 100;
|
||||||
|
@Input() minWeight = 2; //
|
||||||
|
@Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
|
||||||
|
|
||||||
|
inputs: SvgLine[];
|
||||||
|
outputs: SvgLine[];
|
||||||
|
middle: SvgLine;
|
||||||
|
isLiquid: boolean = false;
|
||||||
|
|
||||||
|
gradientColors = {
|
||||||
|
'': ['#9339f4', '#105fb0'],
|
||||||
|
bisq: ['#9339f4', '#105fb0'],
|
||||||
|
// liquid: ['#116761', '#183550'],
|
||||||
|
liquid: ['#09a197', '#0f62af'],
|
||||||
|
// 'liquidtestnet': ['#494a4a', '#272e46'],
|
||||||
|
'liquidtestnet': ['#d2d2d2', '#979797'],
|
||||||
|
// testnet: ['#1d486f', '#183550'],
|
||||||
|
testnet: ['#4edf77', '#10a0af'],
|
||||||
|
// signet: ['#6f1d5d', '#471850'],
|
||||||
|
signet: ['#d24fc8', '#a84fd2'],
|
||||||
|
};
|
||||||
|
|
||||||
|
gradient: string[] = ['#105fb0', '#105fb0'];
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
|
||||||
|
this.gradient = this.gradientColors[this.network];
|
||||||
|
this.initGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(): void {
|
||||||
|
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
|
||||||
|
this.gradient = this.gradientColors[this.network];
|
||||||
|
this.initGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
initGraph(): void {
|
||||||
|
const totalValue = this.calcTotalValue(this.tx);
|
||||||
|
const voutWithFee = this.tx.vout.map(v => { return { type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output', value: v?.value }; });
|
||||||
|
|
||||||
|
if (this.tx.fee && !this.isLiquid) {
|
||||||
|
voutWithFee.unshift({ type: 'fee', value: this.tx.fee });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inputs = this.initLines('in', this.tx.vin.map(v => { return {type: 'input', value: v?.prevout?.value }; }), totalValue, this.maxStrands);
|
||||||
|
this.outputs = this.initLines('out', voutWithFee, totalValue, this.maxStrands);
|
||||||
|
|
||||||
|
this.middle = {
|
||||||
|
path: `M ${(this.width / 2) - 50} ${(this.height / 2) + 0.5} L ${(this.width / 2) + 50} ${(this.height / 2) + 0.5}`,
|
||||||
|
style: `stroke-width: ${this.combinedWeight + 0.5}; stroke: ${this.gradient[1]}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
calcTotalValue(tx: Transaction): number {
|
||||||
|
const totalOutput = this.tx.vout.reduce((acc, v) => (v.value == null ? 0 : v.value) + acc, 0);
|
||||||
|
// simple sum of outputs + fee for bitcoin
|
||||||
|
if (!this.isLiquid) {
|
||||||
|
return this.tx.fee ? totalOutput + this.tx.fee : totalOutput;
|
||||||
|
} else {
|
||||||
|
const totalInput = this.tx.vin.reduce((acc, v) => (v?.prevout?.value == null ? 0 : v.prevout.value) + acc, 0);
|
||||||
|
const confidentialInputCount = this.tx.vin.reduce((acc, v) => acc + (v?.prevout?.value == null ? 1 : 0), 0);
|
||||||
|
const confidentialOutputCount = this.tx.vout.reduce((acc, v) => acc + (v.value == null ? 1 : 0), 0);
|
||||||
|
|
||||||
|
// if there are unknowns on both sides, the total is indeterminate, so we'll just fudge it
|
||||||
|
if (confidentialInputCount && confidentialOutputCount) {
|
||||||
|
const knownInputCount = (tx.vin.length - confidentialInputCount) || 1;
|
||||||
|
const knownOutputCount = (tx.vout.length - confidentialOutputCount) || 1;
|
||||||
|
// assume confidential inputs/outputs have the same average value as the known ones
|
||||||
|
const adjustedTotalInput = totalInput + ((totalInput / knownInputCount) * confidentialInputCount);
|
||||||
|
const adjustedTotalOutput = totalOutput + ((totalOutput / knownOutputCount) * confidentialOutputCount);
|
||||||
|
return Math.max(adjustedTotalInput, adjustedTotalOutput) || 1;
|
||||||
|
} else {
|
||||||
|
// otherwise knowing the actual total of one side suffices
|
||||||
|
return Math.max(totalInput, totalOutput) || 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initLines(side: 'in' | 'out', xputs: { type: string, value: number | void }[], total: number, maxVisibleStrands: number): SvgLine[] {
|
||||||
|
const lines = [];
|
||||||
|
let unknownCount = 0;
|
||||||
|
let unknownTotal = total == null ? this.combinedWeight : total;
|
||||||
|
xputs.forEach(put => {
|
||||||
|
if (put.value == null) {
|
||||||
|
unknownCount++;
|
||||||
|
} else {
|
||||||
|
unknownTotal -= put.value as number;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const unknownShare = unknownTotal / unknownCount;
|
||||||
|
|
||||||
|
// conceptual weights
|
||||||
|
const weights = xputs.map((put): number => this.combinedWeight * (put.value == null ? unknownShare : put.value as number) / total);
|
||||||
|
// actual displayed line thicknesses
|
||||||
|
const minWeights = weights.map((w) => Math.max(this.minWeight - 1, w) + 1);
|
||||||
|
const visibleStrands = Math.min(maxVisibleStrands, xputs.length);
|
||||||
|
const visibleWeight = minWeights.slice(0, visibleStrands).reduce((acc, v) => v + acc, 0);
|
||||||
|
const gaps = visibleStrands - 1;
|
||||||
|
|
||||||
|
const innerTop = (this.height / 2) - (this.combinedWeight / 2);
|
||||||
|
const innerBottom = innerTop + this.combinedWeight;
|
||||||
|
// tracks the visual bottom of the endpoints of the previous line
|
||||||
|
let lastOuter = 0;
|
||||||
|
let lastInner = innerTop;
|
||||||
|
// gap between strands
|
||||||
|
const spacing = (this.height - visibleWeight) / gaps;
|
||||||
|
|
||||||
|
for (let i = 0; i < xputs.length; i++) {
|
||||||
|
const weight = weights[i];
|
||||||
|
const minWeight = minWeights[i];
|
||||||
|
// set the vertical position of the (center of the) outer side of the line
|
||||||
|
let outer = lastOuter + (minWeight / 2);
|
||||||
|
const inner = Math.min(innerBottom + (minWeight / 2), Math.max(innerTop + (minWeight / 2), lastInner + (weight / 2)));
|
||||||
|
|
||||||
|
// special case to center single input/outputs
|
||||||
|
if (xputs.length === 1) {
|
||||||
|
outer = (this.height / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastOuter += minWeight + spacing;
|
||||||
|
lastInner += weight;
|
||||||
|
lines.push({
|
||||||
|
path: this.makePath(side, outer, inner, minWeight),
|
||||||
|
style: this.makeStyle(minWeight, xputs[i].type),
|
||||||
|
class: xputs[i].type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
makePath(side: 'in' | 'out', outer: number, inner: number, weight: number): string {
|
||||||
|
const start = side === 'in' ? (weight * 0.5) : this.width - (weight * 0.5);
|
||||||
|
const center = this.width / 2 + (side === 'in' ? -45 : 45 );
|
||||||
|
const midpoint = (start + center) / 2;
|
||||||
|
// correct for svg horizontal gradient bug
|
||||||
|
if (Math.round(outer) === Math.round(inner)) {
|
||||||
|
outer -= 1;
|
||||||
|
}
|
||||||
|
return `M ${start} ${outer} C ${midpoint} ${outer}, ${midpoint} ${inner}, ${center} ${inner}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeStyle(minWeight, type): string {
|
||||||
|
if (type === 'fee') {
|
||||||
|
return `stroke-width: ${minWeight}; stroke: url(#fee-gradient)`;
|
||||||
|
} else {
|
||||||
|
return `stroke-width: ${minWeight}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import { IChannel } from './node-api.interface';
|
||||||
|
|
||||||
export interface Transaction {
|
export interface Transaction {
|
||||||
txid: string;
|
txid: string;
|
||||||
version: number;
|
version: number;
|
||||||
@ -19,6 +21,13 @@ export interface Transaction {
|
|||||||
deleteAfter?: number;
|
deleteAfter?: number;
|
||||||
_unblinded?: any;
|
_unblinded?: any;
|
||||||
_deduced?: boolean;
|
_deduced?: boolean;
|
||||||
|
_outspends?: Outspend[];
|
||||||
|
_channels?: TransactionChannels;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionChannels {
|
||||||
|
inputs: { [vin: number]: IChannel };
|
||||||
|
outputs: { [vout: number]: IChannel };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Ancestor {
|
interface Ancestor {
|
||||||
|
@ -189,3 +189,35 @@ export interface IOldestNodes {
|
|||||||
city?: any,
|
city?: any,
|
||||||
country?: any,
|
country?: any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IChannel {
|
||||||
|
id: number;
|
||||||
|
short_id: string;
|
||||||
|
capacity: number;
|
||||||
|
transaction_id: string;
|
||||||
|
transaction_vout: number;
|
||||||
|
closing_transaction_id: string;
|
||||||
|
closing_reason: string;
|
||||||
|
updated_at: string;
|
||||||
|
created: string;
|
||||||
|
status: number;
|
||||||
|
node_left: Node,
|
||||||
|
node_right: Node,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface INode {
|
||||||
|
alias: string;
|
||||||
|
public_key: string;
|
||||||
|
channels: number;
|
||||||
|
capacity: number;
|
||||||
|
base_fee_mtokens: number;
|
||||||
|
cltv_delta: number;
|
||||||
|
fee_rate: number;
|
||||||
|
is_disabled: boolean;
|
||||||
|
max_htlc_mtokens: number;
|
||||||
|
min_htlc_mtokens: number;
|
||||||
|
updated_at: string;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<a [routerLink]="['/lightning/node' | relativeUrl, channel.public_key]" >
|
<a [routerLink]="['/lightning/node' | relativeUrl, channel.public_key]" >
|
||||||
{{ channel.public_key | shortenString : 12 }}
|
{{ channel.public_key | shortenString : 12 }}
|
||||||
</a>
|
</a>
|
||||||
<app-clipboard [text]="channel.node1_public_key"></app-clipboard>
|
<app-clipboard [text]="channel.public_key"></app-clipboard>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-right">
|
<div class="box-right">
|
||||||
<div class="second-line">{{ channel.channels }} channels</div>
|
<div class="second-line">{{ channel.channels }} channels</div>
|
||||||
@ -51,4 +51,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md map-col">
|
<div class="col-md map-col">
|
||||||
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" [placeholder]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
|
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
|
||||||
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
|
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
|
||||||
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
|
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
|
||||||
|
<app-closing-type *ngIf="channel.closing_reason" [type]="channel.closing_reason"></app-closing-type>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
@ -65,13 +66,13 @@
|
|||||||
<ng-container *ngIf="transactions$ | async as transactions">
|
<ng-container *ngIf="transactions$ | async as transactions">
|
||||||
<ng-template [ngIf]="transactions[0]">
|
<ng-template [ngIf]="transactions[0]">
|
||||||
<h3>Opening transaction</h3>
|
<h3>Opening transaction</h3>
|
||||||
<app-transactions-list [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [], outputs: [channel] }"></app-transactions-list>
|
<app-transactions-list [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template [ngIf]="transactions[1]">
|
<ng-template [ngIf]="transactions[1]">
|
||||||
<div class="closing-header">
|
<div class="closing-header">
|
||||||
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
||||||
</div>
|
</div>
|
||||||
<app-transactions-list [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [channel], outputs: [] }"></app-transactions-list>
|
<app-transactions-list [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -30,6 +30,10 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
app-fiat {
|
app-fiat {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
|||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
import { forkJoin, Observable, of, share, zip } from 'rxjs';
|
import { forkJoin, Observable, of, share, zip } from 'rxjs';
|
||||||
import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators';
|
import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { IChannel } from 'src/app/interfaces/node-api.interface';
|
||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
import { ElectrsApiService } from 'src/app/services/electrs-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';
|
||||||
@ -62,10 +63,15 @@ export class ChannelComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.transactions$ = this.channel$.pipe(
|
this.transactions$ = this.channel$.pipe(
|
||||||
switchMap((data) => {
|
switchMap((channel: IChannel) => {
|
||||||
return zip([
|
return zip([
|
||||||
data.transaction_id ? this.electrsApiService.getTransaction$(data.transaction_id) : of(null),
|
channel.transaction_id ? this.electrsApiService.getTransaction$(channel.transaction_id) : of(null),
|
||||||
data.closing_transaction_id ? this.electrsApiService.getTransaction$(data.closing_transaction_id) : of(null),
|
channel.closing_transaction_id ? this.electrsApiService.getTransaction$(channel.closing_transaction_id).pipe(
|
||||||
|
map((tx) => {
|
||||||
|
tx._channels = { inputs: {0: channel}, outputs: {}};
|
||||||
|
return tx;
|
||||||
|
})
|
||||||
|
) : of(null),
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div *ngIf="channels$ | async as response; else skeleton">
|
<div *ngIf="channels$ | async as response; else skeleton" style="position: relative;">
|
||||||
<form [formGroup]="channelStatusForm" class="formRadioGroup float-right">
|
<form [formGroup]="channelStatusForm" class="formRadioGroup">
|
||||||
<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" i18n="open">Open
|
<input ngbButton type="radio" [value]="'open'" fragment="open" i18n="open">Open
|
||||||
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<table class="table table-borderless" *ngIf="response.channels.length > 0">
|
<table class="table table-borderless" *ngIf="response.channels.length > 0" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||||
<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;">
|
||||||
@ -87,8 +87,6 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #skeleton>
|
<ng-template #skeleton>
|
||||||
<h2 class="float-left" i18n="lightning.channels">Channels</h2>
|
|
||||||
|
|
||||||
<table class="table table-borderless">
|
<table class="table table-borderless">
|
||||||
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
|
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -7,3 +7,20 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formRadioGroup {
|
||||||
|
@media (min-width: 435px) {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: -46px;
|
||||||
|
}
|
||||||
|
@media (max-width: 435px) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
@media (max-width: 435px) {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ import { LightningApiService } from '../lightning-api.service';
|
|||||||
export class ChannelsListComponent implements OnInit, OnChanges {
|
export class ChannelsListComponent implements OnInit, OnChanges {
|
||||||
@Input() publicKey: string;
|
@Input() publicKey: string;
|
||||||
@Output() channelsStatusChangedEvent = new EventEmitter<string>();
|
@Output() channelsStatusChangedEvent = new EventEmitter<string>();
|
||||||
|
@Output() loadingEvent = new EventEmitter<boolean>(false);
|
||||||
channels$: Observable<any>;
|
channels$: Observable<any>;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -26,6 +27,7 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
|||||||
defaultStatus = 'open';
|
defaultStatus = 'open';
|
||||||
status = 'open';
|
status = 'open';
|
||||||
publicKeySize = 25;
|
publicKeySize = 25;
|
||||||
|
isLoading = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
@ -56,6 +58,8 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
|||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((val) => {
|
tap((val) => {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.loadingEvent.emit(true);
|
||||||
if (typeof val === 'string') {
|
if (typeof val === 'string') {
|
||||||
this.status = val;
|
this.status = val;
|
||||||
this.page = 1;
|
this.page = 1;
|
||||||
@ -64,10 +68,12 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
this.channelsStatusChangedEvent.emit(this.status);
|
this.channelsStatusChangedEvent.emit(this.status);
|
||||||
return this.lightningApiService.getChannelsByNodeId$(this.publicKey, (this.page - 1) * this.itemsPerPage, this.status);
|
return this.lightningApiService.getChannelsByNodeId$(this.publicKey, (this.page - 1) * this.itemsPerPage, this.status);
|
||||||
}),
|
}),
|
||||||
map((response) => {
|
map((response) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.loadingEvent.emit(false);
|
||||||
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)
|
||||||
|
@ -9,44 +9,44 @@
|
|||||||
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
|
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
|
||||||
|
|
||||||
<div class="fee-estimation-container" *ngIf="mode === 'avg'">
|
<div class="fee-estimation-container" *ngIf="mode === 'avg'">
|
||||||
<div class="item">
|
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||||
<h5 class="card-title" i18n="ln.average-capacity">Avg Capacity</h5>
|
<h5 class="card-title" i18n="ln.average-capacity">Avg Capacity</h5>
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="fee-text">
|
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||||
{{ statistics.latest?.avg_capacity || 0 | number: '1.0-0' }}
|
{{ statistics.latest?.avg_capacity || 0 | number: '1.0-0' }}
|
||||||
<span i18n="shared.sat-vbyte|sat/vB">sats</span>
|
<span i18n="shared.sat-vbyte|sat/vB">sats</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="fiat">
|
<span class="fiat" *ngIf="statistics.previous">
|
||||||
<app-change [current]="statistics.latest?.avg_capacity" [previous]="statistics.previous?.avg_capacity"></app-change>
|
<app-change [current]="statistics.latest?.avg_capacity" [previous]="statistics.previous?.avg_capacity"></app-change>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item">
|
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||||
<h5 class="card-title" i18n="ln.average-feerate">Avg Fee Rate</h5>
|
<h5 class="card-title" i18n="ln.average-feerate">Avg Fee Rate</h5>
|
||||||
<div class="card-text" i18n-ngbTooltip="ln.average-feerate-desc"
|
<div class="card-text" i18n-ngbTooltip="ln.average-feerate-desc"
|
||||||
ngbTooltip="The average fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm"
|
ngbTooltip="The average fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm"
|
||||||
placement="bottom">
|
placement="bottom">
|
||||||
<div class="fee-text">
|
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||||
{{ statistics.latest?.avg_fee_rate || 0 | number: '1.0-0' }}
|
{{ statistics.latest?.avg_fee_rate || 0 | number: '1.0-0' }}
|
||||||
<span i18n="shared.sat-vbyte|sat/vB">ppm</span>
|
<span i18n="shared.sat-vbyte|sat/vB">ppm</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="fiat">
|
<span class="fiat" *ngIf="statistics.previous">
|
||||||
<app-change [current]="statistics.latest?.avg_fee_rate" [previous]="statistics.previous?.avg_fee_rate"></app-change>
|
<app-change [current]="statistics.latest?.avg_fee_rate" [previous]="statistics.previous?.avg_fee_rate"></app-change>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item">
|
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||||
<h5 class="card-title" i18n="ln.average-basefee">Avg Base Fee</h5>
|
<h5 class="card-title" i18n="ln.average-basefee">Avg Base Fee</h5>
|
||||||
<div class="card-text" i18n-ngbTooltip="ln.average-basefee-desc"
|
<div class="card-text" i18n-ngbTooltip="ln.average-basefee-desc"
|
||||||
ngbTooltip="The average base fee charged by routing nodes, ignoring base fees > 5000ppm" placement="bottom">
|
ngbTooltip="The average base fee charged by routing nodes, ignoring base fees > 5000ppm" placement="bottom">
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="fee-text">
|
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||||
{{ statistics.latest?.avg_base_fee_mtokens || 0 | number: '1.0-0' }}
|
{{ statistics.latest?.avg_base_fee_mtokens || 0 | number: '1.0-0' }}
|
||||||
<span i18n="shared.sat-vbyte|sat/vB">msats</span>
|
<span i18n="shared.sat-vbyte|sat/vB">msats</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="fiat">
|
<span class="fiat" *ngIf="statistics.previous">
|
||||||
<app-change [current]="statistics.latest?.avg_base_fee_mtokens" [previous]="statistics.previous?.avg_base_fee_mtokens"></app-change>
|
<app-change [current]="statistics.latest?.avg_base_fee_mtokens" [previous]="statistics.previous?.avg_base_fee_mtokens"></app-change>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -55,43 +55,45 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fee-estimation-container" *ngIf="mode === 'med'">
|
<div class="fee-estimation-container" *ngIf="mode === 'med'">
|
||||||
<div class="item">
|
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||||
<h5 class="card-title" i18n="ln.median-capacity">Med Capacity</h5>
|
<h5 class="card-title" i18n="ln.median-capacity">Med Capacity</h5>
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="fee-text">
|
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||||
{{ statistics.latest?.med_capacity || 0 | number: '1.0-0' }}
|
{{ statistics.latest?.med_capacity || 0 | number: '1.0-0' }}
|
||||||
<span i18n="shared.sat-vbyte|sat/vB">sats</span>
|
<span i18n="shared.sat-vbyte|sat/vB">sats</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="fiat">
|
<span class="fiat" *ngIf="statistics.previous">
|
||||||
<app-change [current]="statistics.latest?.med_capacity" [previous]="statistics.previous?.med_capacity"></app-change>
|
<app-change [current]="statistics.latest?.med_capacity" [previous]="statistics.previous?.med_capacity"></app-change>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
|
||||||
|
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||||
<h5 class="card-title" i18n="ln.average-feerate">Med Fee Rate</h5>
|
<h5 class="card-title" i18n="ln.average-feerate">Med Fee Rate</h5>
|
||||||
<div class="card-text" i18n-ngbTooltip="ln.median-feerate-desc"
|
<div class="card-text" i18n-ngbTooltip="ln.median-feerate-desc"
|
||||||
ngbTooltip="The average fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm"
|
ngbTooltip="The median fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm"
|
||||||
placement="bottom">
|
placement="bottom">
|
||||||
<div class="fee-text">
|
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||||
{{ statistics.latest?.med_fee_rate || 0 | number: '1.0-0' }}
|
{{ statistics.latest?.med_fee_rate || 0 | number: '1.0-0' }}
|
||||||
<span i18n="shared.sat-vbyte|sat/vB">ppm</span>
|
<span i18n="shared.sat-vbyte|sat/vB">ppm</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="fiat">
|
<span class="fiat" *ngIf="statistics.previous">
|
||||||
<app-change [current]="statistics.latest?.med_fee_rate" [previous]="statistics.previous?.med_fee_rate"></app-change>
|
<app-change [current]="statistics.latest?.med_fee_rate" [previous]="statistics.previous?.med_fee_rate"></app-change>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
|
||||||
|
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||||
<h5 class="card-title" i18n="ln.median-basefee">Med Base Fee</h5>
|
<h5 class="card-title" i18n="ln.median-basefee">Med Base Fee</h5>
|
||||||
<div class="card-text" i18n-ngbTooltip="ln.median-basefee-desc"
|
<div class="card-text" i18n-ngbTooltip="ln.median-basefee-desc"
|
||||||
ngbTooltip="The median base fee charged by routing nodes, ignoring base fees > 5000ppm" placement="bottom">
|
ngbTooltip="The median base fee charged by routing nodes, ignoring base fees > 5000ppm" placement="bottom">
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="fee-text">
|
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||||
{{ statistics.latest?.med_base_fee_mtokens || 0 | number: '1.0-0' }}
|
{{ statistics.latest?.med_base_fee_mtokens || 0 | number: '1.0-0' }}
|
||||||
<span i18n="shared.sat-vbyte|sat/vB">msats</span>
|
<span i18n="shared.sat-vbyte|sat/vB">msats</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="fiat">
|
<span class="fiat" *ngIf="statistics.previous">
|
||||||
<app-change [current]="statistics.latest?.med_base_fee_mtokens" [previous]="statistics.previous?.med_base_fee_mtokens"></app-change>
|
<app-change [current]="statistics.latest?.med_base_fee_mtokens" [previous]="statistics.previous?.med_base_fee_mtokens"></app-change>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,6 +18,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fee-estimation-wrapper {
|
||||||
|
min-height: 77px;
|
||||||
|
}
|
||||||
|
|
||||||
.fee-estimation-container {
|
.fee-estimation-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -30,7 +34,10 @@
|
|||||||
width: -webkit-fill-available;
|
width: -webkit-fill-available;
|
||||||
@media (min-width: 376px) {
|
@media (min-width: 376px) {
|
||||||
margin: 0 auto 0px;
|
margin: 0 auto 0px;
|
||||||
}
|
}
|
||||||
|
&.more-padding {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
&:first-child{
|
&:first-child{
|
||||||
display: none;
|
display: none;
|
||||||
@media (min-width: 485px) {
|
@media (min-width: 485px) {
|
||||||
@ -57,6 +64,9 @@
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
padding: 0px 2px;
|
padding: 0px 2px;
|
||||||
|
&.no-border {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.fiat {
|
.fiat {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -1,76 +1,64 @@
|
|||||||
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
|
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
|
||||||
<div class="fee-estimation-container">
|
<div class="fee-estimation-container">
|
||||||
<div class="item">
|
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||||
<h5 class="card-title" i18n="mining.average-fee">Capacity</h5>
|
<h5 class="card-title" i18n="lightning.capacity">Capacity</h5>
|
||||||
<div class="card-text" i18n-ngbTooltip="mining.average-fee" ngbTooltip="Percentage change past week"
|
<div class="card-text" i18n-ngbTooltip="mining.percentage-change-last-week" ngbTooltip="Percentage change past week"
|
||||||
placement="bottom">
|
[disableTooltip]="!statistics.previous" placement="bottom">
|
||||||
<div class="fee-text">
|
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||||
<app-amount [satoshis]="statistics.latest?.total_capacity" digitsInfo="1.2-2"></app-amount>
|
<app-amount [satoshis]="statistics.latest?.total_capacity" digitsInfo="1.2-2"></app-amount>
|
||||||
</div>
|
</div>
|
||||||
<span class="fiat">
|
<span class="fiat" *ngIf="statistics.previous">
|
||||||
<app-change [current]="statistics.latest?.total_capacity" [previous]="statistics.previous?.total_capacity">
|
<app-change [current]="statistics.latest?.total_capacity" [previous]="statistics.previous?.total_capacity">
|
||||||
</app-change>
|
</app-change>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||||
<h5 class="card-title" i18n="mining.rewards">Nodes</h5>
|
<h5 class="card-title" i18n="lightning.nodes">Nodes</h5>
|
||||||
<div class="card-text" i18n-ngbTooltip="mining.rewards-desc" ngbTooltip="Percentage change past week"
|
<div class="card-text" i18n-ngbTooltip="mining.percentage-change-last-week" ngbTooltip="Percentage change past week"
|
||||||
placement="bottom">
|
[disableTooltip]="!statistics.previous">
|
||||||
<div class="fee-text">
|
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||||
{{ statistics.latest?.node_count || 0 | number }}
|
{{ statistics.latest?.node_count || 0 | number }}
|
||||||
</div>
|
</div>
|
||||||
<span class="fiat">
|
<span class="fiat" *ngIf="statistics.previous">
|
||||||
<app-change [current]="statistics.latest?.node_count" [previous]="statistics.previous?.node_count"></app-change>
|
<app-change [current]="statistics.latest?.node_count" [previous]="statistics.previous?.node_count"></app-change>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item" [class]="!statistics.previous ? 'more-padding' : ''">
|
||||||
<h5 class="card-title" i18n="mining.rewards-per-tx">Channels</h5>
|
<h5 class="card-title" i18n="lightning.channels">Channels</h5>
|
||||||
<div class="card-text" i18n-ngbTooltip="mining.rewards-per-tx-desc" ngbTooltip="Percentage change past week"
|
<div class="card-text" i18n-ngbTooltip="mining.percentage-change-last-week" ngbTooltip="Percentage change past week"
|
||||||
placement="bottom">
|
[disableTooltip]="!statistics.previous">
|
||||||
<div class="fee-text">
|
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||||
{{ statistics.latest?.channel_count || 0 | number }}
|
{{ statistics.latest?.channel_count || 0 | number }}
|
||||||
</div>
|
</div>
|
||||||
<span class="fiat">
|
<span class="fiat" *ngIf="statistics.previous">
|
||||||
<app-change [current]="statistics.latest?.channel_count" [previous]="statistics.previous?.channel_count">
|
<app-change [current]="statistics.latest?.channel_count" [previous]="statistics.previous?.channel_count">
|
||||||
</app-change>
|
</app-change>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
|
||||||
<div class="item">
|
|
||||||
<h5 class="card-title" i18n="mining.average-fee">Average Channel</h5>
|
|
||||||
<div class="card-text" i18n-ngbTooltip="mining.average-fee"
|
|
||||||
ngbTooltip="Fee paid on average for each transaction in the past 144 blocks" placement="bottom">
|
|
||||||
<app-amount [satoshis]="statistics.latest.average_channel_size" digitsInfo="1.2-3"></app-amount>
|
|
||||||
<span class="fiat">
|
|
||||||
<app-change [current]="statistics.latest.average_channel_size" [previous]="statistics.previous.average_channel_size"></app-change>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #loadingReward>
|
<ng-template #loadingReward>
|
||||||
<div class="fee-estimation-container loading-container">
|
<div class="fee-estimation-container loading-container">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<h5 class="card-title" i18n="mining.rewards">Nodes</h5>
|
<h5 class="card-title" i18n="lightning.nodes">Nodes</h5>
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<h5 class="card-title" i18n="mining.rewards-per-tx">Channels</h5>
|
<h5 class="card-title" i18n="lightning.channels">Channels</h5>
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<h5 class="card-title" i18n="mining.average-fee">Average Channel</h5>
|
<h5 class="card-title" i18n="lightning.average-channels">Average Channel</h5>
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
|
@ -18,6 +18,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fee-estimation-wrapper {
|
||||||
|
min-height: 77px;
|
||||||
|
}
|
||||||
|
|
||||||
.fee-estimation-container {
|
.fee-estimation-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -30,7 +34,10 @@
|
|||||||
width: -webkit-fill-available;
|
width: -webkit-fill-available;
|
||||||
@media (min-width: 376px) {
|
@media (min-width: 376px) {
|
||||||
margin: 0 auto 0px;
|
margin: 0 auto 0px;
|
||||||
}
|
}
|
||||||
|
&.more-padding {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
&:first-child{
|
&:first-child{
|
||||||
display: none;
|
display: none;
|
||||||
@media (min-width: 485px) {
|
@media (min-width: 485px) {
|
||||||
@ -57,6 +64,9 @@
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
padding: 0px 2px;
|
padding: 0px 2px;
|
||||||
|
&.no-border {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.fiat {
|
.fiat {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md map-col">
|
<div class="col-md map-col">
|
||||||
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" [placeholder]="true" [hasLocation]="!!node.as_number" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="container-xl" *ngIf="(node$ | async) as node">
|
<div class="container-xl" *ngIf="(node$ | async) as node">
|
||||||
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5>
|
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5>
|
||||||
<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 text-truncate">{{ node.alias }}</h1>
|
||||||
<span class="tx-link">
|
<span class="tx-link">
|
||||||
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
|
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">
|
||||||
{{ node.public_key | shortenString : publicKeySize }}
|
{{ node.public_key | shortenString : publicKeySize }}
|
||||||
@ -131,10 +131,9 @@
|
|||||||
<app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
|
<app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
|
|
||||||
<app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels>
|
<app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex">
|
||||||
<h2 *ngIf="channelsListStatus === 'open'">
|
<h2 *ngIf="channelsListStatus === 'open'">
|
||||||
<span i18n="lightning.open-channels">Open channels</span>
|
<span i18n="lightning.open-channels">Open channels</span>
|
||||||
<span> ({{ node.opened_channel_count }})</span>
|
<span> ({{ node.opened_channel_count }})</span>
|
||||||
@ -143,10 +142,13 @@
|
|||||||
<span i18n="lightning.open-channels">Closed channels</span>
|
<span i18n="lightning.open-channels">Closed channels</span>
|
||||||
<span> ({{ node.closed_channel_count }})</span>
|
<span> ({{ node.closed_channel_count }})</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
<div *ngIf="channelListLoading" class="spinner-border ml-3" role="status"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-channels-list [publicKey]="node.public_key"
|
<app-channels-list [publicKey]="node.public_key"
|
||||||
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"></app-channels-list>
|
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"
|
||||||
|
(loadingEvent)="onLoadingEvent($event)"
|
||||||
|
></app-channels-list>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,4 +56,17 @@ app-fiat {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border {
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin-top: 6.5px;
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
margin-top: 2.3px;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
@ -22,7 +22,7 @@ export class NodeComponent implements OnInit {
|
|||||||
channelsListStatus: string;
|
channelsListStatus: string;
|
||||||
error: Error;
|
error: Error;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
|
channelListLoading = false;
|
||||||
publicKeySize = 99;
|
publicKeySize = 99;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -97,4 +97,8 @@ export class NodeComponent implements OnInit {
|
|||||||
onChannelsListStatusChanged(e) {
|
onChannelsListStatusChanged(e) {
|
||||||
this.channelsListStatus = e;
|
this.channelsListStatus = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLoadingEvent(e) {
|
||||||
|
this.channelListLoading = e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ export class NodesChannelsMap implements OnInit {
|
|||||||
@Input() channel: any[] = [];
|
@Input() channel: any[] = [];
|
||||||
@Input() fitContainer = false;
|
@Input() fitContainer = false;
|
||||||
@Input() hasLocation = true;
|
@Input() hasLocation = true;
|
||||||
|
@Input() placeholder = false;
|
||||||
@Output() readyEvent = new EventEmitter();
|
@Output() readyEvent = new EventEmitter();
|
||||||
|
|
||||||
channelsObservable: Observable<any>;
|
channelsObservable: Observable<any>;
|
||||||
@ -201,11 +202,26 @@ export class NodesChannelsMap implements OnInit {
|
|||||||
|
|
||||||
prepareChartOptions(nodes, channels) {
|
prepareChartOptions(nodes, channels) {
|
||||||
let title: object;
|
let title: object;
|
||||||
if (channels.length === 0) {
|
if (channels.length === 0 && !this.placeholder) {
|
||||||
this.chartOptions = null;
|
this.chartOptions = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// empty map fallback
|
||||||
|
if (channels.length === 0 && this.placeholder) {
|
||||||
|
title = {
|
||||||
|
textStyle: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 18
|
||||||
|
},
|
||||||
|
text: $localize`No geolocation data available`,
|
||||||
|
left: 'center',
|
||||||
|
top: 'center'
|
||||||
|
};
|
||||||
|
this.zoom = 1.5;
|
||||||
|
this.center = [0, 20];
|
||||||
|
}
|
||||||
|
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
silent: this.style === 'widget',
|
silent: this.style === 'widget',
|
||||||
title: title ?? undefined,
|
title: title ?? undefined,
|
||||||
@ -222,7 +238,7 @@ export class NodesChannelsMap implements OnInit {
|
|||||||
roam: this.style === 'widget' ? false : true,
|
roam: this.style === 'widget' ? false : true,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderColor: 'black',
|
borderColor: 'black',
|
||||||
color: '#ffffff44'
|
color: '#272b3f'
|
||||||
},
|
},
|
||||||
scaleLimit: {
|
scaleLimit: {
|
||||||
min: 1.3,
|
min: 1.3,
|
||||||
|
@ -1,2 +1,9 @@
|
|||||||
<div *ngIf="channelsObservable$ | async" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
|
<div *ngIf="channelsObservable$ | async" style="min-height: 455px">
|
||||||
|
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
|
||||||
|
<div echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="isLoading" class="text-center loading-spinner">
|
||||||
|
<div class="spinner-border text-light"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
.loading-spinner {
|
||||||
|
min-height: 455px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border {
|
||||||
|
position: relative;
|
||||||
|
top: 225px;
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts';
|
import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts';
|
||||||
import { Observable, tap } from 'rxjs';
|
import { Observable, share, switchMap, tap } from 'rxjs';
|
||||||
import { lerpColor } from 'src/app/shared/graphs.utils';
|
import { lerpColor } 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 { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
@ -25,7 +25,7 @@ export class NodeChannels implements OnChanges {
|
|||||||
};
|
};
|
||||||
|
|
||||||
channelsObservable$: Observable<any>;
|
channelsObservable$: Observable<any>;
|
||||||
isLoading: true;
|
isLoading = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
@ -41,9 +41,20 @@ export class NodeChannels implements OnChanges {
|
|||||||
|
|
||||||
this.channelsObservable$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey, -1, 'active')
|
this.channelsObservable$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey, -1, 'active')
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((response) => {
|
switchMap((response) => {
|
||||||
const biggestCapacity = response.body[0].capacity;
|
this.isLoading = true;
|
||||||
this.prepareChartOptions(response.body.map(channel => {
|
if ((response.body?.length ?? 0) <= 0) {
|
||||||
|
this.isLoading = false;
|
||||||
|
return [''];
|
||||||
|
}
|
||||||
|
return [response.body];
|
||||||
|
}),
|
||||||
|
tap((body: any[]) => {
|
||||||
|
if (body.length === 0 || body[0].length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const biggestCapacity = body[0].capacity;
|
||||||
|
this.prepareChartOptions(body.map(channel => {
|
||||||
return {
|
return {
|
||||||
name: channel.node.alias,
|
name: channel.node.alias,
|
||||||
value: channel.capacity,
|
value: channel.capacity,
|
||||||
@ -54,7 +65,9 @@ export class NodeChannels implements OnChanges {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
})
|
this.isLoading = false;
|
||||||
|
}),
|
||||||
|
share(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,10 +130,6 @@ export class NodeChannels implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChartInit(ec: ECharts): void {
|
onChartInit(ec: ECharts): void {
|
||||||
if (this.chartInstance !== undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chartInstance = ec;
|
this.chartInstance = ec;
|
||||||
|
|
||||||
this.chartInstance.on('click', (e) => {
|
this.chartInstance.on('click', (e) => {
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -300,7 +300,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
{ offset: 1, color: '#D81B60AA' },
|
{ offset: 1, color: '#D81B60AA' },
|
||||||
]),
|
]),
|
||||||
|
|
||||||
smooth: true,
|
smooth: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
zlevel: 1,
|
zlevel: 1,
|
||||||
@ -321,7 +321,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
{ offset: 0, color: '#FFB300' },
|
{ offset: 0, color: '#FFB300' },
|
||||||
{ offset: 1, color: '#FFB300AA' },
|
{ offset: 1, color: '#FFB300AA' },
|
||||||
]),
|
]),
|
||||||
smooth: true,
|
smooth: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
zlevel: 1,
|
zlevel: 1,
|
||||||
@ -342,7 +342,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
|||||||
{ offset: 0, color: '#7D4698' },
|
{ offset: 0, color: '#7D4698' },
|
||||||
{ offset: 1, color: '#7D4698AA' },
|
{ offset: 1, color: '#7D4698AA' },
|
||||||
]),
|
]),
|
||||||
smooth: true,
|
smooth: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
dataZoom: this.widget ? null : [{
|
dataZoom: this.widget ? null : [{
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<div class="card-header" *ngIf="!widget">
|
<div class="card-header" *ngIf="!widget">
|
||||||
<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.top-100-isp-ln">Top 100 ISP hosting LN nodes</span>
|
<span i18n="lightning.top-100-isp-ln">Top 100 ISPs hosting LN nodes</span>
|
||||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
||||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@media (min-width: 991px) {
|
@media (min-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -65px;
|
top: -100px;
|
||||||
}
|
}
|
||||||
@media (min-width: 830px) and (max-width: 991px) {
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -274,7 +274,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
width: 1,
|
width: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
smooth: true,
|
smooth: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
zlevel: 0,
|
zlevel: 0,
|
||||||
@ -288,7 +288,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
|||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
},
|
},
|
||||||
type: 'line',
|
type: 'line',
|
||||||
smooth: true,
|
smooth: false,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
dataZoom: this.widget ? null : [{
|
dataZoom: this.widget ? null : [{
|
||||||
|
@ -242,12 +242,12 @@ export class ApiService {
|
|||||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/enterprise/info/` + name);
|
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/enterprise/info/` + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannelByTxIds$(txIds: string[]): Observable<{ inputs: any[], outputs: any[] }> {
|
getChannelByTxIds$(txIds: string[]): Observable<any[]> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
txIds.forEach((txId: string) => {
|
txIds.forEach((txId: string) => {
|
||||||
params = params.append('txId[]', txId);
|
params = params.append('txId[]', txId);
|
||||||
});
|
});
|
||||||
return this.httpClient.get<{ inputs: any[], outputs: any[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params });
|
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
lightningSearch$(searchText: string): Observable<any[]> {
|
lightningSearch$(searchText: string): Observable<any[]> {
|
||||||
|
@ -153,7 +153,12 @@ export class StateService {
|
|||||||
if (this.env.BASE_MODULE !== 'mempool' && this.env.BASE_MODULE !== 'liquid') {
|
if (this.env.BASE_MODULE !== 'mempool' && this.env.BASE_MODULE !== 'liquid') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const networkMatches = url.match(/^\/(bisq|testnet|liquidtestnet|liquid|signet)/);
|
// horrible network regex breakdown:
|
||||||
|
// /^\/ starts with a forward slash...
|
||||||
|
// (?:[a-z]{2}(?:-[A-Z]{2})?\/)? optional locale prefix (non-capturing)
|
||||||
|
// (?:preview\/)? optional "preview" prefix (non-capturing)
|
||||||
|
// (bisq|testnet|liquidtestnet|liquid|signet)/ network string (captured as networkMatches[1])
|
||||||
|
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet)/);
|
||||||
switch (networkMatches && networkMatches[1]) {
|
switch (networkMatches && networkMatches[1]) {
|
||||||
case 'liquid':
|
case 'liquid':
|
||||||
if (this.network !== 'liquid') {
|
if (this.network !== 'liquid') {
|
||||||
|
@ -63,6 +63,7 @@ import { StatusViewComponent } from '../components/status-view/status-view.compo
|
|||||||
import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
|
import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
|
||||||
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
|
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
|
||||||
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
|
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
|
||||||
|
import { TxBowtieGraphComponent } from '../components/tx-bowtie-graph/tx-bowtie-graph.component';
|
||||||
import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component';
|
import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component';
|
||||||
import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component';
|
import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component';
|
||||||
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
|
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
|
||||||
@ -138,6 +139,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
|||||||
StatusViewComponent,
|
StatusViewComponent,
|
||||||
FeesBoxComponent,
|
FeesBoxComponent,
|
||||||
DifficultyComponent,
|
DifficultyComponent,
|
||||||
|
TxBowtieGraphComponent,
|
||||||
TermsOfServiceComponent,
|
TermsOfServiceComponent,
|
||||||
PrivacyPolicyComponent,
|
PrivacyPolicyComponent,
|
||||||
TrademarkPolicyComponent,
|
TrademarkPolicyComponent,
|
||||||
@ -242,6 +244,7 @@ import { GeolocationComponent } from '../shared/components/geolocation/geolocati
|
|||||||
StatusViewComponent,
|
StatusViewComponent,
|
||||||
FeesBoxComponent,
|
FeesBoxComponent,
|
||||||
DifficultyComponent,
|
DifficultyComponent,
|
||||||
|
TxBowtieGraphComponent,
|
||||||
TermsOfServiceComponent,
|
TermsOfServiceComponent,
|
||||||
PrivacyPolicyComponent,
|
PrivacyPolicyComponent,
|
||||||
TrademarkPolicyComponent,
|
TrademarkPolicyComponent,
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<title>mempool - Bisq Markets</title>
|
<title>mempool - Bisq Markets</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
|
|
||||||
<meta name="description" content="The Mempool Open Source Project™ - our self-hosted explorer for the Bisq Network.">
|
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
|
||||||
|
|
||||||
<meta property="og:image" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
|
<meta property="og:image" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
|
||||||
<meta property="og:image:type" content="image/jpeg" />
|
<meta property="og:image:type" content="image/jpeg" />
|
||||||
@ -14,7 +14,7 @@
|
|||||||
<meta property="twitter:site" content="https://bisq.markets/">
|
<meta property="twitter:site" content="https://bisq.markets/">
|
||||||
<meta property="twitter:creator" content="@bisq_network">
|
<meta property="twitter:creator" content="@bisq_network">
|
||||||
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
||||||
<meta property="twitter:description" content="Our self-hosted markets explorer for the Bisq community.">
|
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
|
||||||
<meta property="twitter:image:src" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
|
<meta property="twitter:image:src" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
|
||||||
<meta property="twitter:domain" content="bisq.markets">
|
<meta property="twitter:domain" content="bisq.markets">
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<title>mempool - Liquid Network</title>
|
<title>mempool - Liquid Network</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
|
|
||||||
<meta name="description" content="The Mempool Open Source Project™ - our self-hosted explorer for the Liquid Network.">
|
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
|
||||||
<meta property="og:image" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
|
<meta property="og:image" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
|
||||||
<meta property="og:image:type" content="image/png" />
|
<meta property="og:image:type" content="image/png" />
|
||||||
<meta property="og:image:width" content="1000" />
|
<meta property="og:image:width" content="1000" />
|
||||||
@ -14,7 +14,7 @@
|
|||||||
<meta property="twitter:site" content="@mempool">
|
<meta property="twitter:site" content="@mempool">
|
||||||
<meta property="twitter:creator" content="@mempool">
|
<meta property="twitter:creator" content="@mempool">
|
||||||
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
||||||
<meta property="twitter:description" content="Our self-hosted network explorer for the Liquid community.">
|
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
|
||||||
<meta property="twitter:image:src" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
|
<meta property="twitter:image:src" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
|
||||||
<meta property="twitter:domain" content="liquid.network">
|
<meta property="twitter:domain" content="liquid.network">
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<title>mempool - Bitcoin Explorer</title>
|
<title>mempool - Bitcoin Explorer</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
|
|
||||||
<meta name="description" content="The Mempool Open Source Project™ - our self-hosted explorer for the Bitcoin community." />
|
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem." />
|
||||||
<meta property="og:image" content="https://mempool.space/resources/mempool-space-preview.png" />
|
<meta property="og:image" content="https://mempool.space/resources/mempool-space-preview.png" />
|
||||||
<meta property="og:image:type" content="image/png" />
|
<meta property="og:image:type" content="image/png" />
|
||||||
<meta property="og:image:width" content="1000" />
|
<meta property="og:image:width" content="1000" />
|
||||||
@ -14,7 +14,7 @@
|
|||||||
<meta property="twitter:site" content="@mempool">
|
<meta property="twitter:site" content="@mempool">
|
||||||
<meta property="twitter:creator" content="@mempool">
|
<meta property="twitter:creator" content="@mempool">
|
||||||
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
||||||
<meta property="twitter:description" content="Our self-hosted mempool explorer for the Bitcoin community." />
|
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
|
||||||
<meta property="twitter:image:src" content="https://mempool.space/resources/mempool-space-preview.png" />
|
<meta property="twitter:image:src" content="https://mempool.space/resources/mempool-space-preview.png" />
|
||||||
<meta property="twitter:domain" content="mempool.space">
|
<meta property="twitter:domain" content="mempool.space">
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 94 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 96 KiB |
BIN
frontend/src/resources/previews/dashboard.png
Normal file
After Width: | Height: | Size: 726 KiB |
BIN
frontend/src/resources/previews/lightning.png
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
frontend/src/resources/previews/mining.png
Normal file
After Width: | Height: | Size: 607 KiB |
@ -705,6 +705,10 @@ th {
|
|||||||
.locktime { color: #ff8c00 }
|
.locktime { color: #ff8c00 }
|
||||||
.reserved { color: #ff8c00 }
|
.reserved { color: #ff8c00 }
|
||||||
|
|
||||||
|
.shortable-address {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
.rtl-layout {
|
.rtl-layout {
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
@ -881,6 +885,7 @@ th {
|
|||||||
|
|
||||||
.shortable-address {
|
.shortable-address {
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lastest-blocks-table {
|
.lastest-blocks-table {
|
||||||
|
@ -48,6 +48,9 @@ BITCOIN_MAINNET_ENABLE=ON
|
|||||||
BITCOIN_MAINNET_MINFEE_ENABLE=ON
|
BITCOIN_MAINNET_MINFEE_ENABLE=ON
|
||||||
BITCOIN_TESTNET_ENABLE=ON
|
BITCOIN_TESTNET_ENABLE=ON
|
||||||
BITCOIN_SIGNET_ENABLE=ON
|
BITCOIN_SIGNET_ENABLE=ON
|
||||||
|
LN_BITCOIN_MAINNET_ENABLE=ON
|
||||||
|
LN_BITCOIN_TESTNET_ENABLE=ON
|
||||||
|
LN_BITCOIN_SIGNET_ENABLE=ON
|
||||||
BISQ_MAINNET_ENABLE=ON
|
BISQ_MAINNET_ENABLE=ON
|
||||||
ELEMENTS_LIQUID_ENABLE=ON
|
ELEMENTS_LIQUID_ENABLE=ON
|
||||||
ELEMENTS_LIQUIDTESTNET_ENABLE=ON
|
ELEMENTS_LIQUIDTESTNET_ENABLE=ON
|
||||||
@ -227,6 +230,9 @@ MYSQL_GROUP=mysql
|
|||||||
MEMPOOL_MAINNET_USER='mempool'
|
MEMPOOL_MAINNET_USER='mempool'
|
||||||
MEMPOOL_TESTNET_USER='mempool_testnet'
|
MEMPOOL_TESTNET_USER='mempool_testnet'
|
||||||
MEMPOOL_SIGNET_USER='mempool_signet'
|
MEMPOOL_SIGNET_USER='mempool_signet'
|
||||||
|
LN_MEMPOOL_MAINNET_USER='mempool_mainnet_lightning'
|
||||||
|
LN_MEMPOOL_TESTNET_USER='mempool_testnet_lightning'
|
||||||
|
LN_MEMPOOL_SIGNET_USER='mempool_signet_lightning'
|
||||||
MEMPOOL_LIQUID_USER='mempool_liquid'
|
MEMPOOL_LIQUID_USER='mempool_liquid'
|
||||||
MEMPOOL_LIQUIDTESTNET_USER='mempool_liquidtestnet'
|
MEMPOOL_LIQUIDTESTNET_USER='mempool_liquidtestnet'
|
||||||
MEMPOOL_BISQ_USER='mempool_bisq'
|
MEMPOOL_BISQ_USER='mempool_bisq'
|
||||||
@ -234,6 +240,9 @@ MEMPOOL_BISQ_USER='mempool_bisq'
|
|||||||
MEMPOOL_MAINNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
MEMPOOL_MAINNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
||||||
MEMPOOL_TESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
MEMPOOL_TESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
||||||
MEMPOOL_SIGNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
MEMPOOL_SIGNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
||||||
|
LN_MEMPOOL_MAINNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
||||||
|
LN_MEMPOOL_TESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
||||||
|
LN_MEMPOOL_SIGNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
||||||
MEMPOOL_LIQUID_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
MEMPOOL_LIQUID_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
||||||
MEMPOOL_LIQUIDTESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
MEMPOOL_LIQUIDTESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
||||||
MEMPOOL_BISQ_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
MEMPOOL_BISQ_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
|
||||||
@ -391,6 +400,10 @@ FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf)
|
|||||||
FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb105-server keybase)
|
FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb105-server keybase)
|
||||||
FREEBSD_PKG+=(geoipupdate)
|
FREEBSD_PKG+=(geoipupdate)
|
||||||
|
|
||||||
|
FREEBSD_UNFURL_PKG=()
|
||||||
|
FREEBSD_UNFURL_PKG+=(nvidia-driver-470-470.129.06 chromium xinit xterm twm ja-sourcehansans-otf)
|
||||||
|
FREEBSD_UNFURL_PKG+=(zh-sourcehansans-sc-otf ko-aleefonts-ttf lohit tlwg-ttf)
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
##### utility functions #####
|
##### utility functions #####
|
||||||
#############################
|
#############################
|
||||||
@ -747,6 +760,9 @@ $CUT >$input <<-EOF
|
|||||||
Tor:Enable Tor v3 HS Onion:ON
|
Tor:Enable Tor v3 HS Onion:ON
|
||||||
Mainnet:Enable Bitcoin Mainnet:ON
|
Mainnet:Enable Bitcoin Mainnet:ON
|
||||||
Mainnet-Minfee:Enable Bitcoin Mainnet Minfee:ON
|
Mainnet-Minfee:Enable Bitcoin Mainnet Minfee:ON
|
||||||
|
LN-Mainnet:Enable Bitcoin Mainnet Lightning:ON
|
||||||
|
LN-Testnet:Enable Bitcoin Testnet Lightning:ON
|
||||||
|
LN-Signet:Enable Bitcoin Signet Lightning:ON
|
||||||
Testnet:Enable Bitcoin Testnet:ON
|
Testnet:Enable Bitcoin Testnet:ON
|
||||||
Signet:Enable Bitcoin Signet:ON
|
Signet:Enable Bitcoin Signet:ON
|
||||||
Liquid:Enable Elements Liquid:ON
|
Liquid:Enable Elements Liquid:ON
|
||||||
@ -809,6 +825,24 @@ else
|
|||||||
BITCOIN_INSTALL=OFF
|
BITCOIN_INSTALL=OFF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if grep LN-Mainnet $tempfile >/dev/null 2>&1;then
|
||||||
|
LN_BITCOIN_MAINNET_ENABLE=ON
|
||||||
|
else
|
||||||
|
LN_BITCOIN_MAINNET_ENABLE=OFF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep LN-Testnet $tempfile >/dev/null 2>&1;then
|
||||||
|
LN_BITCOIN_TESTNET_ENABLE=ON
|
||||||
|
else
|
||||||
|
LN_BITCOIN_TESTNET_ENABLE=OFF
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep LN-Signet $tempfile >/dev/null 2>&1;then
|
||||||
|
LN_BITCOIN_SIGNET_ENABLE=ON
|
||||||
|
else
|
||||||
|
LN_BITCOIN_SIGNET_ENABLE=OFF
|
||||||
|
fi
|
||||||
|
|
||||||
if grep Liquid $tempfile >/dev/null 2>&1;then
|
if grep Liquid $tempfile >/dev/null 2>&1;then
|
||||||
ELEMENTS_LIQUID_ENABLE=ON
|
ELEMENTS_LIQUID_ENABLE=ON
|
||||||
else
|
else
|
||||||
@ -831,6 +865,7 @@ if grep CoreLN $tempfile >/dev/null 2>&1;then
|
|||||||
CLN_INSTALL=ON
|
CLN_INSTALL=ON
|
||||||
else
|
else
|
||||||
CLN_INSTALL=OFF
|
CLN_INSTALL=OFF
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
||||||
BITCOIN_ELECTRS_INSTALL=ON
|
BITCOIN_ELECTRS_INSTALL=ON
|
||||||
@ -1279,17 +1314,20 @@ case $OS in
|
|||||||
echo "[*] Creating Core Lightning user"
|
echo "[*] Creating Core Lightning user"
|
||||||
osGroupCreate "${CLN_GROUP}"
|
osGroupCreate "${CLN_GROUP}"
|
||||||
osUserCreate "${CLN_USER}" "${CLN_HOME}" "${CLN_GROUP}"
|
osUserCreate "${CLN_USER}" "${CLN_HOME}" "${CLN_GROUP}"
|
||||||
|
osSudo "${ROOT_USER}" pw usermod ${MEMPOOL_USER} -G "${CLN_GROUP}"
|
||||||
osSudo "${ROOT_USER}" chsh -s `which zsh` "${CLN_USER}"
|
osSudo "${ROOT_USER}" chsh -s `which zsh` "${CLN_USER}"
|
||||||
echo "export PATH=$PATH:$HOME/.local/bin" >> "${CLN_HOME}/.zshrc"
|
echo "export PATH=$PATH:$HOME/.local/bin" >> "${CLN_HOME}/.zshrc"
|
||||||
|
osSudo "${ROOT_USER}" mkdir -p "${CLN_HOME}/.lightning/{bitcoin,signet,testnet}"
|
||||||
|
osSudo "${ROOT_USER}" chmod 750 "${CLN_HOME}" "${CLN_HOME}/.lightning" "${CLN_HOME}/.lightning/{bitcoin,signet,testnet}"
|
||||||
osSudo "${ROOT_USER}" chown -R "${CLN_USER}:${CLN_GROUP}" "${CLN_HOME}"
|
osSudo "${ROOT_USER}" chown -R "${CLN_USER}:${CLN_GROUP}" "${CLN_HOME}"
|
||||||
|
|
||||||
echo "[*] Installing Core Lightning package"
|
echo "[*] Installing Core Lightning package"
|
||||||
osPackageInstall ${CLN_PKG}
|
osPackageInstall ${CLN_PKG}
|
||||||
|
|
||||||
echo "[*] Installing Core Lightning mainnet Cronjob"
|
echo "[*] Installing Core Lightning mainnet Cronjob"
|
||||||
crontab_cln+='@reboot sleep 30 ; screen -dmS main lightningd --alias `hostname` --bitcoin-datadir /bitcoin\n'
|
crontab_cln+='@reboot sleep 60 ; screen -dmS main lightningd --alias `hostname` --bitcoin-datadir /bitcoin\n'
|
||||||
crontab_cln+='@reboot sleep 60 ; screen -dmS sig lightningd --alias `hostname` --bitcoin-datadir /bitcoin --network signet\n'
|
|
||||||
crontab_cln+='@reboot sleep 90 ; screen -dmS tes lightningd --alias `hostname` --bitcoin-datadir /bitcoin --network testnet\n'
|
crontab_cln+='@reboot sleep 90 ; screen -dmS tes lightningd --alias `hostname` --bitcoin-datadir /bitcoin --network testnet\n'
|
||||||
|
crontab_cln+='@reboot sleep 120 ; screen -dmS sig lightningd --alias `hostname` --bitcoin-datadir /bitcoin --network signet\n'
|
||||||
echo "${crontab_cln}" | crontab -u "${CLN_USER}" -
|
echo "${crontab_cln}" | crontab -u "${CLN_USER}" -
|
||||||
;;
|
;;
|
||||||
Debian)
|
Debian)
|
||||||
@ -1397,7 +1435,42 @@ if [ "${UNFURL_INSTALL}" = ON ];then
|
|||||||
case $OS in
|
case $OS in
|
||||||
|
|
||||||
FreeBSD)
|
FreeBSD)
|
||||||
echo "[*] FIXME: Unfurl must be installed manually on FreeBSD"
|
|
||||||
|
if pciconf -lv|grep -i nvidia >/dev/null 2>&1;then
|
||||||
|
echo "[*] GPU detected: Installing packages for Unfurl"
|
||||||
|
osPackageInstall ${FREEBSD_UNFURL_PKG[@]}
|
||||||
|
|
||||||
|
echo 'allowed_users = anybody' >> /usr/local/etc/X11/Xwrapper.config
|
||||||
|
echo 'kld_list="nvidia"' >> /etc/rc.conf
|
||||||
|
echo 'nvidia_xorg_enable="YES"' >> /etc/rc.conf
|
||||||
|
|
||||||
|
echo "[*] Installing color emoji"
|
||||||
|
osSudo "${ROOT_USER}" curl "https://github.com/samuelngs/apple-emoji-linux/releases/download/ios-15.4/AppleColorEmoji.ttf" -o /usr/local/share/fonts/TTF/AppleColorEmoji.ttf
|
||||||
|
cat >> /usr/local/etc/fonts/conf.d/01-emoji.conf <<EOF
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||||
|
<fontconfig>
|
||||||
|
<match>
|
||||||
|
<test name="family"><string>sans-serif</string></test>
|
||||||
|
<edit name="family" mode="prepend" binding="strong">
|
||||||
|
<string>Apple Color Emoji</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
<match>
|
||||||
|
<test name="family"><string>serif</string></test>
|
||||||
|
<edit name="family" mode="prepend" binding="strong">
|
||||||
|
<string>Apple Color Emoji</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
<match>
|
||||||
|
<test name="family"><string>Apple Color Emoji</string></test>
|
||||||
|
<edit name="family" mode="prepend" binding="strong">
|
||||||
|
<string>Apple Color Emoji</string>
|
||||||
|
</edit>
|
||||||
|
</match>
|
||||||
|
</fontconfig>
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
Debian)
|
Debian)
|
||||||
@ -1671,7 +1744,16 @@ if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${
|
|||||||
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/mainnet"
|
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/mainnet"
|
||||||
|
|
||||||
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Bitcoin Mainnet"
|
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Bitcoin Mainnet"
|
||||||
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME} && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/mainnet && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${LN_BITCOIN_MAINNET_ENABLE}" = ON ];then
|
||||||
|
echo "[*] Creating Mempool instance for Lightning Network on Bitcoin Mainnet"
|
||||||
|
osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false
|
||||||
|
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/mainnet-lightning"
|
||||||
|
|
||||||
|
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Lightning Network on Bitcoin Mainnet"
|
||||||
|
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/mainnet-lightning && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
|
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
|
||||||
@ -1680,7 +1762,16 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
|
|||||||
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/testnet"
|
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/testnet"
|
||||||
|
|
||||||
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Bitcoin Testnet"
|
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Bitcoin Testnet"
|
||||||
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME} && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/testnet && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${LN_BITCOIN_TESTNET_ENABLE}" = ON ];then
|
||||||
|
echo "[*] Creating Mempool instance for Lightning Network on Bitcoin Testnet"
|
||||||
|
osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false
|
||||||
|
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/testnet-lightning"
|
||||||
|
|
||||||
|
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Lightning Network on Bitcoin Testnet"
|
||||||
|
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/testnet-lightning && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
||||||
@ -1689,7 +1780,16 @@ if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
|||||||
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/signet"
|
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/signet"
|
||||||
|
|
||||||
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Bitcoin Signet"
|
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Bitcoin Signet"
|
||||||
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME} && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/signet && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${LN_BITCOIN_SIGNET_ENABLE}" = ON ];then
|
||||||
|
echo "[*] Creating Mempool instance for Lightning Network on Bitcoin Signet"
|
||||||
|
osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false
|
||||||
|
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/signet-lightning"
|
||||||
|
|
||||||
|
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Lightning Network on Bitcoin Signet"
|
||||||
|
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/signet-lightning && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
||||||
@ -1698,7 +1798,7 @@ if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
|||||||
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/liquid"
|
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/liquid"
|
||||||
|
|
||||||
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Liquid"
|
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Liquid"
|
||||||
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME} && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/liquid && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
||||||
@ -1707,7 +1807,7 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
|||||||
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/liquidtestnet"
|
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/liquidtestnet"
|
||||||
|
|
||||||
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Liquid Testnet"
|
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Liquid Testnet"
|
||||||
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME} && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/liquidtestnet && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${BISQ_INSTALL}" = ON ];then
|
if [ "${BISQ_INSTALL}" = ON ];then
|
||||||
@ -1716,7 +1816,7 @@ if [ "${BISQ_INSTALL}" = ON ];then
|
|||||||
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/bisq"
|
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/bisq"
|
||||||
|
|
||||||
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Bisq"
|
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Bisq"
|
||||||
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME} && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/bisq && git checkout ${MEMPOOL_LATEST_RELEASE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
##### mariadb
|
##### mariadb
|
||||||
@ -1742,6 +1842,15 @@ grant all on mempool_testnet.* to '${MEMPOOL_TESTNET_USER}'@'localhost' identifi
|
|||||||
create database mempool_signet;
|
create database mempool_signet;
|
||||||
grant all on mempool_signet.* to '${MEMPOOL_SIGNET_USER}'@'localhost' identified by '${MEMPOOL_SIGNET_PASS}';
|
grant all on mempool_signet.* to '${MEMPOOL_SIGNET_USER}'@'localhost' identified by '${MEMPOOL_SIGNET_PASS}';
|
||||||
|
|
||||||
|
create database mempool_mainnet_lightning;
|
||||||
|
grant all on mempool_mainnet_lightning.* to '${LN_MEMPOOL_MAINNET_USER}'@'%' identified by '${LN_MEMPOOL_MAINNET_PASS}';
|
||||||
|
|
||||||
|
create database mempool_testnet_lightning;
|
||||||
|
grant all on mempool_testnet_lightning.* to '${LN_MEMPOOL_TESTNET_USER}'@'%' identified by '${LN_MEMPOOL_TESTNET_PASS}';
|
||||||
|
|
||||||
|
create database mempool_signet_lightning;
|
||||||
|
grant all on mempool_signet_lightning.* to '${LN_MEMPOOL_SIGNET_USER}'@'%' identified by '${LN_MEMPOOL_SIGNET_PASS}';
|
||||||
|
|
||||||
create database mempool_liquid;
|
create database mempool_liquid;
|
||||||
grant all on mempool_liquid.* to '${MEMPOOL_LIQUID_USER}'@'localhost' identified by '${MEMPOOL_LIQUID_PASS}';
|
grant all on mempool_liquid.* to '${MEMPOOL_LIQUID_USER}'@'localhost' identified by '${MEMPOOL_LIQUID_PASS}';
|
||||||
|
|
||||||
@ -1760,6 +1869,12 @@ declare -x MEMPOOL_TESTNET_USER="${MEMPOOL_TESTNET_USER}"
|
|||||||
declare -x MEMPOOL_TESTNET_PASS="${MEMPOOL_TESTNET_PASS}"
|
declare -x MEMPOOL_TESTNET_PASS="${MEMPOOL_TESTNET_PASS}"
|
||||||
declare -x MEMPOOL_SIGNET_USER="${MEMPOOL_SIGNET_USER}"
|
declare -x MEMPOOL_SIGNET_USER="${MEMPOOL_SIGNET_USER}"
|
||||||
declare -x MEMPOOL_SIGNET_PASS="${MEMPOOL_SIGNET_PASS}"
|
declare -x MEMPOOL_SIGNET_PASS="${MEMPOOL_SIGNET_PASS}"
|
||||||
|
declare -x LN_MEMPOOL_MAINNET_USER="${LN_MEMPOOL_MAINNET_USER}"
|
||||||
|
declare -x LN_MEMPOOL_MAINNET_PASS="${LN_MEMPOOL_MAINNET_PASS}"
|
||||||
|
declare -x LN_MEMPOOL_TESTNET_USER="${LN_MEMPOOL_TESTNET_USER}"
|
||||||
|
declare -x LN_MEMPOOL_TESTNET_PASS="${LN_MEMPOOL_TESTNET_PASS}"
|
||||||
|
declare -x LN_MEMPOOL_SIGNET_USER="${LN_MEMPOOL_SIGNET_USER}"
|
||||||
|
declare -x LN_MEMPOOL_SIGNET_PASS="${LN_MEMPOOL_SIGNET_PASS}"
|
||||||
declare -x MEMPOOL_LIQUID_USER="${MEMPOOL_LIQUID_USER}"
|
declare -x MEMPOOL_LIQUID_USER="${MEMPOOL_LIQUID_USER}"
|
||||||
declare -x MEMPOOL_LIQUID_PASS="${MEMPOOL_LIQUID_PASS}"
|
declare -x MEMPOOL_LIQUID_PASS="${MEMPOOL_LIQUID_PASS}"
|
||||||
declare -x MEMPOOL_LIQUIDTESTNET_USER="${MEMPOOL_LIQUIDTESTNET_USER}"
|
declare -x MEMPOOL_LIQUIDTESTNET_USER="${MEMPOOL_LIQUIDTESTNET_USER}"
|
||||||
@ -1770,24 +1885,32 @@ _EOF_
|
|||||||
|
|
||||||
##### nginx
|
##### nginx
|
||||||
|
|
||||||
echo "[*] Adding Nginx configuration"
|
case $OS in
|
||||||
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/nginx/nginx.conf" "${NGINX_CONFIGURATION}"
|
|
||||||
mkdir -p /var/cache/nginx/services /var/cache/nginx/api
|
FreeBSD)
|
||||||
chown ${NGINX_USER}: /var/cache/nginx/services /var/cache/nginx/api
|
;;
|
||||||
ln -s /mempool/mempool /etc/nginx/mempool
|
|
||||||
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_USER__!${NGINX_USER}!" "${NGINX_CONFIGURATION}"
|
Debian)
|
||||||
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_ETC_FOLDER__!${NGINX_ETC_FOLDER}!" "${NGINX_CONFIGURATION}"
|
echo "[*] Adding Nginx configuration"
|
||||||
if [ "${TOR_INSTALL}" = ON ];then
|
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/nginx/nginx.conf" "${NGINX_CONFIGURATION}"
|
||||||
echo "[*] Read tor v3 onion hostnames"
|
mkdir -p /var/cache/nginx/services /var/cache/nginx/api
|
||||||
NGINX_MEMPOOL_ONION=$(cat "${TOR_RESOURCES}/mempool/hostname")
|
chown ${NGINX_USER}: /var/cache/nginx/services /var/cache/nginx/api
|
||||||
NGINX_BISQ_ONION=$(cat "${TOR_RESOURCES}/bisq/hostname")
|
ln -s /mempool/mempool /etc/nginx/mempool
|
||||||
NGINX_LIQUID_ONION=$(cat "${TOR_RESOURCES}/liquid/hostname")
|
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_USER__!${NGINX_USER}!" "${NGINX_CONFIGURATION}"
|
||||||
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_MEMPOOL_ONION__!${NGINX_MEMPOOL_ONION%.onion}!" "${NGINX_CONFIGURATION}"
|
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_ETC_FOLDER__!${NGINX_ETC_FOLDER}!" "${NGINX_CONFIGURATION}"
|
||||||
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_BISQ_ONION__!${NGINX_BISQ_ONION%.onion}!" "${NGINX_CONFIGURATION}"
|
if [ "${TOR_INSTALL}" = ON ];then
|
||||||
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_LIQUID_ONION__!${NGINX_LIQUID_ONIONi%.onion}!" "${NGINX_CONFIGURATION}"
|
echo "[*] Read tor v3 onion hostnames"
|
||||||
fi
|
NGINX_MEMPOOL_ONION=$(cat "${TOR_RESOURCES}/mempool/hostname")
|
||||||
echo "[*] Restarting Nginx"
|
NGINX_BISQ_ONION=$(cat "${TOR_RESOURCES}/bisq/hostname")
|
||||||
osSudo "${ROOT_USER}" service nginx restart
|
NGINX_LIQUID_ONION=$(cat "${TOR_RESOURCES}/liquid/hostname")
|
||||||
|
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_MEMPOOL_ONION__!${NGINX_MEMPOOL_ONION%.onion}!" "${NGINX_CONFIGURATION}"
|
||||||
|
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_BISQ_ONION__!${NGINX_BISQ_ONION%.onion}!" "${NGINX_CONFIGURATION}"
|
||||||
|
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_LIQUID_ONION__!${NGINX_LIQUID_ONIONi%.onion}!" "${NGINX_CONFIGURATION}"
|
||||||
|
fi
|
||||||
|
echo "[*] Restarting Nginx"
|
||||||
|
osSudo "${ROOT_USER}" service nginx restart
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
##### OS systemd
|
##### OS systemd
|
||||||
|
|
||||||
|
@ -12,7 +12,10 @@ ELEMENTS_RPC_USER=$(grep '^rpcuser' /elements/elements.conf | cut -d '=' -f2)
|
|||||||
ELEMENTS_RPC_PASS=$(grep '^rpcpassword' /elements/elements.conf | cut -d '=' -f2)
|
ELEMENTS_RPC_PASS=$(grep '^rpcpassword' /elements/elements.conf | cut -d '=' -f2)
|
||||||
|
|
||||||
# get mysql credentials
|
# get mysql credentials
|
||||||
. /mempool/mysql_credentials
|
MYSQL_CRED_FILE=${HOME}/mempool/mysql_credentials
|
||||||
|
if [ -f "${MYSQL_CRED_FILE}" ];then
|
||||||
|
. ${MYSQL_CRED_FILE}
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -f "${LOCKFILE}" ];then
|
if [ -f "${LOCKFILE}" ];then
|
||||||
echo "upgrade already running? check lockfile ${LOCKFILE}"
|
echo "upgrade already running? check lockfile ${LOCKFILE}"
|
||||||
@ -63,6 +66,19 @@ build_frontend()
|
|||||||
npm run build || exit 1
|
npm run build || exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
build_unfurler()
|
||||||
|
{
|
||||||
|
local site="$1"
|
||||||
|
echo "[*] Building unfurler for ${site}"
|
||||||
|
[ -z "${HASH}" ] && exit 1
|
||||||
|
cd "$HOME/${site}/unfurler" || exit 1
|
||||||
|
if [ ! -e "config.json" ];then
|
||||||
|
cp "${HOME}/mempool/production/unfurler-config.${site}.json" "config.json"
|
||||||
|
fi
|
||||||
|
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm install || exit 1
|
||||||
|
npm run build || exit 1
|
||||||
|
}
|
||||||
|
|
||||||
build_backend()
|
build_backend()
|
||||||
{
|
{
|
||||||
local site="$1"
|
local site="$1"
|
||||||
@ -82,6 +98,12 @@ build_backend()
|
|||||||
-e "s!__MEMPOOL_TESTNET_PASS__!${MEMPOOL_TESTNET_PASS}!" \
|
-e "s!__MEMPOOL_TESTNET_PASS__!${MEMPOOL_TESTNET_PASS}!" \
|
||||||
-e "s!__MEMPOOL_SIGNET_USER__!${MEMPOOL_SIGNET_USER}!" \
|
-e "s!__MEMPOOL_SIGNET_USER__!${MEMPOOL_SIGNET_USER}!" \
|
||||||
-e "s!__MEMPOOL_SIGNET_PASS__!${MEMPOOL_SIGNET_PASS}!" \
|
-e "s!__MEMPOOL_SIGNET_PASS__!${MEMPOOL_SIGNET_PASS}!" \
|
||||||
|
-e "s!__LN_MEMPOOL_MAINNET_USER__!${LN_MEMPOOL_MAINNET_USER}!" \
|
||||||
|
-e "s!__LN_MEMPOOL_MAINNET_PASS__!${LN_MEMPOOL_MAINNET_PASS}!" \
|
||||||
|
-e "s!__LN_MEMPOOL_TESTNET_USER__!${LN_MEMPOOL_TESTNET_USER}!" \
|
||||||
|
-e "s!__LN_MEMPOOL_TESTNET_PASS__!${LN_MEMPOOL_TESTNET_PASS}!" \
|
||||||
|
-e "s!__LN_MEMPOOL_SIGNET_USER__!${LN_MEMPOOL_SIGNET_USER}!" \
|
||||||
|
-e "s!__LN_MEMPOOL_SIGNET_PASS__!${LN_MEMPOOL_SIGNET_PASS}!" \
|
||||||
-e "s!__MEMPOOL_LIQUID_USER__!${MEMPOOL_LIQUID_USER}!" \
|
-e "s!__MEMPOOL_LIQUID_USER__!${MEMPOOL_LIQUID_USER}!" \
|
||||||
-e "s!__MEMPOOL_LIQUID_PASS__!${MEMPOOL_LIQUID_PASS}!" \
|
-e "s!__MEMPOOL_LIQUID_PASS__!${MEMPOOL_LIQUID_PASS}!" \
|
||||||
-e "s!__MEMPOOL_LIQUIDTESTNET_USER__!${LIQUIDTESTNET_USER}!" \
|
-e "s!__MEMPOOL_LIQUIDTESTNET_USER__!${LIQUIDTESTNET_USER}!" \
|
||||||
@ -128,6 +150,11 @@ for repo in $backend_repos;do
|
|||||||
update_repo "${repo}"
|
update_repo "${repo}"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# build unfurlers
|
||||||
|
for repo in mainnet liquid bisq;do
|
||||||
|
build_unfurler "${repo}"
|
||||||
|
done
|
||||||
|
|
||||||
# build backends
|
# build backends
|
||||||
for repo in $backend_repos;do
|
for repo in $backend_repos;do
|
||||||
build_backend "${repo}"
|
build_backend "${repo}"
|
||||||
|
49
production/mempool-config.mainnet-lightning.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"MEMPOOL": {
|
||||||
|
"NETWORK": "mainnet",
|
||||||
|
"BACKEND": "esplora",
|
||||||
|
"HTTP_PORT": 8993,
|
||||||
|
"INDEXING_BLOCKS_AMOUNT": 0,
|
||||||
|
"API_URL_PREFIX": "/api/v1/"
|
||||||
|
},
|
||||||
|
"SYSLOG": {
|
||||||
|
"MIN_PRIORITY": "debug"
|
||||||
|
},
|
||||||
|
"CORE_RPC": {
|
||||||
|
"PORT": 8332,
|
||||||
|
"USERNAME": "__BITCOIN_RPC_USER__",
|
||||||
|
"PASSWORD": "__BITCOIN_RPC_PASS__"
|
||||||
|
},
|
||||||
|
"ESPLORA": {
|
||||||
|
"REST_API_URL": "http://127.0.0.1:4000"
|
||||||
|
},
|
||||||
|
"LIGHTNING": {
|
||||||
|
"ENABLED": true,
|
||||||
|
"BACKEND": "cln",
|
||||||
|
"GRAPH_REFRESH_INTERVAL": 60,
|
||||||
|
"TOPOLOGY_FOLDER": "/cln/topology/output"
|
||||||
|
},
|
||||||
|
"LND": {
|
||||||
|
"REST_API_URL": "https://127.0.0.1:8888",
|
||||||
|
"TLS_CERT_PATH": "/lnd/.lnd/tls.cert",
|
||||||
|
"MACAROON_PATH": "/lnd/.lnd/data/chain/bitcoin/mainnet/readonly.macaroon"
|
||||||
|
},
|
||||||
|
"CLIGHTNING": {
|
||||||
|
"SOCKET": "/cln/.lightning/bitcoin/lightning-rpc"
|
||||||
|
},
|
||||||
|
"MAXMIND": {
|
||||||
|
"ENABLED": true,
|
||||||
|
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoIP2-City.mmdb"
|
||||||
|
},
|
||||||
|
"STATISTICS": {
|
||||||
|
"ENABLED": false
|
||||||
|
},
|
||||||
|
"DATABASE": {
|
||||||
|
"ENABLED": true,
|
||||||
|
"HOST": "127.0.0.1",
|
||||||
|
"PORT": 3306,
|
||||||
|
"DATABASE": "mempool_mainnet_lightning",
|
||||||
|
"USERNAME": "mempool_mainnet_lightning",
|
||||||
|
"PASSWORD": "mempool_mainnet_lightning"
|
||||||
|
}
|
||||||
|
}
|
44
production/mempool-config.signet-lightning.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"MEMPOOL": {
|
||||||
|
"NETWORK": "signet",
|
||||||
|
"BACKEND": "esplora",
|
||||||
|
"HTTP_PORT": 8991,
|
||||||
|
"INDEXING_BLOCKS_AMOUNT": 0,
|
||||||
|
"API_URL_PREFIX": "/api/v1/"
|
||||||
|
},
|
||||||
|
"SYSLOG": {
|
||||||
|
"MIN_PRIORITY": "debug"
|
||||||
|
},
|
||||||
|
"CORE_RPC": {
|
||||||
|
"PORT": 38332,
|
||||||
|
"USERNAME": "__BITCOIN_RPC_USER__",
|
||||||
|
"PASSWORD": "__BITCOIN_RPC_PASS__"
|
||||||
|
},
|
||||||
|
"ESPLORA": {
|
||||||
|
"REST_API_URL": "http://127.0.0.1:4003"
|
||||||
|
},
|
||||||
|
"LIGHTNING": {
|
||||||
|
"ENABLED": true,
|
||||||
|
"BACKEND": "cln",
|
||||||
|
"GRAPH_REFRESH_INTERVAL": 60,
|
||||||
|
"TOPOLOGY_FOLDER": ""
|
||||||
|
},
|
||||||
|
"CLIGHTNING": {
|
||||||
|
"SOCKET": "/cln/.lightning/signet/lightning-rpc"
|
||||||
|
},
|
||||||
|
"MAXMIND": {
|
||||||
|
"ENABLED": true,
|
||||||
|
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoIP2-City.mmdb"
|
||||||
|
},
|
||||||
|
"STATISTICS": {
|
||||||
|
"ENABLED": false
|
||||||
|
},
|
||||||
|
"DATABASE": {
|
||||||
|
"ENABLED": true,
|
||||||
|
"HOST": "127.0.0.1",
|
||||||
|
"PORT": 3306,
|
||||||
|
"USERNAME": "mempool_signet_lightning",
|
||||||
|
"PASSWORD": "mempool_signet_lightning",
|
||||||
|
"DATABASE": "mempool_signet_lightning"
|
||||||
|
}
|
||||||
|
}
|
44
production/mempool-config.testnet-lightning.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"MEMPOOL": {
|
||||||
|
"NETWORK": "testnet",
|
||||||
|
"BACKEND": "esplora",
|
||||||
|
"HTTP_PORT": 8992,
|
||||||
|
"INDEXING_BLOCKS_AMOUNT": 0,
|
||||||
|
"API_URL_PREFIX": "/api/v1/"
|
||||||
|
},
|
||||||
|
"SYSLOG": {
|
||||||
|
"MIN_PRIORITY": "debug"
|
||||||
|
},
|
||||||
|
"CORE_RPC": {
|
||||||
|
"PORT": 18332,
|
||||||
|
"USERNAME": "__BITCOIN_RPC_USER__",
|
||||||
|
"PASSWORD": "__BITCOIN_RPC_PASS__"
|
||||||
|
},
|
||||||
|
"ESPLORA": {
|
||||||
|
"REST_API_URL": "http://127.0.0.1:4002"
|
||||||
|
},
|
||||||
|
"LIGHTNING": {
|
||||||
|
"ENABLED": true,
|
||||||
|
"BACKEND": "cln",
|
||||||
|
"GRAPH_REFRESH_INTERVAL": 60,
|
||||||
|
"TOPOLOGY_FOLDER": ""
|
||||||
|
},
|
||||||
|
"CLIGHTNING": {
|
||||||
|
"SOCKET": "/cln/.lightning/testnet/lightning-rpc"
|
||||||
|
},
|
||||||
|
"MAXMIND": {
|
||||||
|
"ENABLED": true,
|
||||||
|
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoIP2-City.mmdb"
|
||||||
|
},
|
||||||
|
"STATISTICS": {
|
||||||
|
"ENABLED": false
|
||||||
|
},
|
||||||
|
"DATABASE": {
|
||||||
|
"ENABLED": true,
|
||||||
|
"HOST": "127.0.0.1",
|
||||||
|
"PORT": 3306,
|
||||||
|
"USERNAME": "mempool_testnet_lightning",
|
||||||
|
"PASSWORD": "mempool_testnet_lightning",
|
||||||
|
"DATABASE": "mempool_testnet_lightning"
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,21 @@
|
|||||||
#!/usr/bin/env zsh
|
#!/usr/bin/env zsh
|
||||||
killall sh node
|
|
||||||
|
# kill "while true" loops
|
||||||
|
killall sh
|
||||||
|
|
||||||
|
# kill actual node backends
|
||||||
|
killall node
|
||||||
|
|
||||||
|
# kill unfurler chrome instances
|
||||||
|
killall chrome
|
||||||
|
|
||||||
|
# kill xorg
|
||||||
|
killall xinit
|
||||||
|
|
||||||
|
# kill nginx cache warmer scripts
|
||||||
|
for pid in `ps uaxww|grep warmer|grep zsh|awk '{print $2}'`;do
|
||||||
|
kill $pid
|
||||||
|
done
|
||||||
|
|
||||||
|
# always exit successfully despite above errors
|
||||||
|
exit 0
|
||||||
|
@ -2,7 +2,31 @@
|
|||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
source "$NVM_DIR/nvm.sh"
|
source "$NVM_DIR/nvm.sh"
|
||||||
|
|
||||||
|
# start all mempool backends that exist
|
||||||
for site in mainnet mainnet-lightning testnet testnet-lightning signet signet-lightning bisq liquid liquidtestnet;do
|
for site in mainnet mainnet-lightning testnet testnet-lightning signet signet-lightning bisq liquid liquidtestnet;do
|
||||||
cd "${HOME}/${site}/backend/" && \
|
cd "${HOME}/${site}/backend/" && \
|
||||||
|
echo "starting mempool backend: ${site}" && \
|
||||||
screen -dmS "${site}" sh -c 'while true;do npm run start-production;sleep 1;done'
|
screen -dmS "${site}" sh -c 'while true;do npm run start-production;sleep 1;done'
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# only start xorg if GPU present
|
||||||
|
if pciconf -lv|grep -i nvidia >/dev/null 2>&1;then
|
||||||
|
export DISPLAY=:0
|
||||||
|
screen -dmS x startx
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# start unfurlers for each frontend
|
||||||
|
for site in mainnet liquid bisq;do
|
||||||
|
cd "$HOME/${site}/unfurler" && \
|
||||||
|
echo "starting mempool unfurler: ${site}" && \
|
||||||
|
screen -dmS "unfurler-${site}" sh -c 'while true;do npm run unfurler;sleep 2;done'
|
||||||
|
done
|
||||||
|
|
||||||
|
# start nginx warm cacher
|
||||||
|
for site in mainnet;do
|
||||||
|
echo "starting mempool cache warmer: ${site}"
|
||||||
|
screen -dmS "warmer-${site}" $HOME/mainnet/production/nginx-cache-warmer
|
||||||
|
done
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# start on reboot
|
# start on reboot
|
||||||
@reboot sleep 10 ; $HOME/start
|
@reboot sleep 10 ; $HOME/start
|
||||||
|
|
||||||
# start cache warmer on reboot
|
|
||||||
@reboot sleep 180 ; /mempool/mempool/production/nginx-cache-warmer >/dev/null 2>&1 &
|
|
||||||
|
|
||||||
# daily backup
|
# daily backup
|
||||||
37 13 * * * sleep 30 ; /mempool/mempool.space/backup >/dev/null 2>&1 &
|
37 13 * * * sleep 30 ; /mempool/mempool.space/backup >/dev/null 2>&1 &
|
||||||
|
|
||||||
|
@ -2,6 +2,12 @@
|
|||||||
hostname=$(hostname)
|
hostname=$(hostname)
|
||||||
slugs=(`curl -sSL https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json | jq -r '.slugs[]'`)
|
slugs=(`curl -sSL https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json | jq -r '.slugs[]'`)
|
||||||
|
|
||||||
|
warm()
|
||||||
|
{
|
||||||
|
echo "$1"
|
||||||
|
curl -i -s "$1" | head -1
|
||||||
|
}
|
||||||
|
|
||||||
while true
|
while true
|
||||||
do for url in / \
|
do for url in / \
|
||||||
'/api/v1/blocks' \
|
'/api/v1/blocks' \
|
||||||
@ -79,16 +85,48 @@ do for url in / \
|
|||||||
'/api/v1/mining/difficulty-adjustments/all' \
|
'/api/v1/mining/difficulty-adjustments/all' \
|
||||||
'/api/v1/lightning/channels-geo?style=widget' \
|
'/api/v1/lightning/channels-geo?style=widget' \
|
||||||
'/api/v1/lightning/channels-geo?style=graph' \
|
'/api/v1/lightning/channels-geo?style=graph' \
|
||||||
|
'/api/v1/lightning/statistics/latest' \
|
||||||
|
'/api/v1/lightning/statistics/1m' \
|
||||||
|
'/api/v1/lightning/statistics/3m' \
|
||||||
|
'/api/v1/lightning/statistics/6m' \
|
||||||
|
'/api/v1/lightning/statistics/1y' \
|
||||||
|
'/api/v1/lightning/statistics/2y' \
|
||||||
|
'/api/v1/lightning/statistics/3y' \
|
||||||
|
'/api/v1/lightning/statistics/all' \
|
||||||
|
'/api/v1/lightning/nodes/isp-ranking' \
|
||||||
|
'/api/v1/lightning/nodes/isp/396982,15169' `# Google` \
|
||||||
|
'/api/v1/lightning/nodes/isp/14618,16509' `# Amazon` \
|
||||||
|
'/api/v1/lightning/nodes/isp/39572' `# DataWeb` \
|
||||||
|
'/api/v1/lightning/nodes/isp/14061' `# Digital Ocean` \
|
||||||
|
'/api/v1/lightning/nodes/isp/24940,213230' `# Hetzner` \
|
||||||
|
'/api/v1/lightning/nodes/isp/394745' `# LunaNode` \
|
||||||
|
'/api/v1/lightning/nodes/isp/45102' `# Alibaba` \
|
||||||
|
'/api/v1/lightning/nodes/isp/3209' `# Vodafone Germany` \
|
||||||
|
'/api/v1/lightning/nodes/isp/7922' `# Comcast Cable` \
|
||||||
|
'/api/v1/lightning/nodes/isp/34197' `# SHRD SARL` \
|
||||||
|
'/api/v1/lightning/nodes/isp/42275' `# Three Fourteen SASU` \
|
||||||
|
'/api/v1/lightning/nodes/isp/16276' `# OVH SAS` \
|
||||||
|
'/api/v1/lightning/nodes/isp/11426,11427,20001,20115,11351,10796,33363,12271' `# Spectrum` \
|
||||||
|
'/api/v1/lightning/nodes/isp/701' `# Verizon` \
|
||||||
|
'/api/v1/lightning/nodes/isp/12876' `# Scaleway` \
|
||||||
|
'/api/v1/lightning/nodes/isp/33915' `# Ziggo` \
|
||||||
|
'/api/v1/lightning/nodes/isp/3320' `# Deutsche Telekom AG` \
|
||||||
|
'/api/v1/lightning/nodes/isp/8075' `# Microsoft Azure` \
|
||||||
|
'/api/v1/lightning/nodes/countries' \
|
||||||
|
'/api/v1/lightning/nodes/rankings' \
|
||||||
|
'/api/v1/lightning/nodes/rankings/liquidity' \
|
||||||
|
'/api/v1/lightning/nodes/rankings/connectivity' \
|
||||||
|
'/api/v1/lightning/nodes/rankings/age' \
|
||||||
|
|
||||||
do
|
do
|
||||||
curl -s "https://${hostname}${url}" >/dev/null
|
warm "https://${hostname}${url}"
|
||||||
done
|
done
|
||||||
|
|
||||||
for slug in $slugs
|
for slug in $slugs
|
||||||
do
|
do
|
||||||
curl -s "https://${hostname}/api/v1/mining/pool/${slug}" >/dev/null
|
warm "https://${hostname}/api/v1/mining/pool/${slug}"
|
||||||
curl -s "https://${hostname}/api/v1/mining/pool/${slug}/hashrate" >/dev/null
|
warm "https://${hostname}/api/v1/mining/pool/${slug}/hashrate"
|
||||||
curl -s "https://${hostname}/api/v1/mining/pool/${slug}/blocks" >/dev/null
|
warm "https://${hostname}/api/v1/mining/pool/${slug}/blocks"
|
||||||
done
|
done
|
||||||
|
|
||||||
sleep 10
|
sleep 10
|
||||||
|
@ -70,30 +70,6 @@ location /api/v1/translators {
|
|||||||
proxy_hide_header content-security-policy;
|
proxy_hide_header content-security-policy;
|
||||||
proxy_hide_header x-frame-options;
|
proxy_hide_header x-frame-options;
|
||||||
}
|
}
|
||||||
location /api/v1/enterprise/images {
|
|
||||||
proxy_pass $mempoolSpaceServices;
|
|
||||||
proxy_cache services;
|
|
||||||
proxy_cache_background_update on;
|
|
||||||
proxy_cache_use_stale updating;
|
|
||||||
proxy_cache_valid 200 10m;
|
|
||||||
expires 10m;
|
|
||||||
proxy_hide_header onion-location;
|
|
||||||
proxy_hide_header strict-transport-security;
|
|
||||||
proxy_hide_header content-security-policy;
|
|
||||||
proxy_hide_header x-frame-options;
|
|
||||||
}
|
|
||||||
location /api/v1/enterprise {
|
|
||||||
proxy_pass $mempoolSpaceServices;
|
|
||||||
proxy_cache services;
|
|
||||||
proxy_cache_background_update on;
|
|
||||||
proxy_cache_use_stale updating;
|
|
||||||
proxy_cache_valid 200 5m;
|
|
||||||
expires 5m;
|
|
||||||
proxy_hide_header onion-location;
|
|
||||||
proxy_hide_header strict-transport-security;
|
|
||||||
proxy_hide_header content-security-policy;
|
|
||||||
proxy_hide_header x-frame-options;
|
|
||||||
}
|
|
||||||
location /api/v1/assets {
|
location /api/v1/assets {
|
||||||
proxy_pass $mempoolSpaceServices;
|
proxy_pass $mempoolSpaceServices;
|
||||||
proxy_cache services;
|
proxy_cache services;
|
||||||
|
@ -4,7 +4,7 @@ location /testnet/api/v1/lightning {
|
|||||||
try_files /dev/null @mempool-testnet-api-v1-lightning;
|
try_files /dev/null @mempool-testnet-api-v1-lightning;
|
||||||
}
|
}
|
||||||
location @mempool-testnet-api-v1-lightning {
|
location @mempool-testnet-api-v1-lightning {
|
||||||
proxy_pass $mempoolSignetLightning;
|
proxy_pass $mempoolTestnetLightning;
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
@ -22,6 +22,11 @@ http {
|
|||||||
include mempool/production/nginx/http-proxy-cache.conf;
|
include mempool/production/nginx/http-proxy-cache.conf;
|
||||||
include mempool/production/nginx/http-language.conf;
|
include mempool/production/nginx/http-language.conf;
|
||||||
|
|
||||||
|
# match preview/unfurl bot user-agents
|
||||||
|
map $http_user_agent $unfurlbot {
|
||||||
|
default 0;
|
||||||
|
}
|
||||||
|
|
||||||
# mempool configuration
|
# mempool configuration
|
||||||
include mempool/production/nginx/upstream-mempool.conf;
|
include mempool/production/nginx/upstream-mempool.conf;
|
||||||
|
|
||||||
@ -42,6 +47,7 @@ http {
|
|||||||
|
|
||||||
# for services from mempool.space like contributors on about page
|
# for services from mempool.space like contributors on about page
|
||||||
set $mempoolSpaceServices "https://mempool.space";
|
set $mempoolSpaceServices "https://mempool.space";
|
||||||
|
set $mempoolSpaceUnfurler "http://127.0.0.1:8001";
|
||||||
|
|
||||||
# for mempool daemons, see upstream-mempool.conf
|
# for mempool daemons, see upstream-mempool.conf
|
||||||
set $mempoolMainnet "http://mempool-bitcoin-mainnet";
|
set $mempoolMainnet "http://mempool-bitcoin-mainnet";
|
||||||
@ -77,6 +83,7 @@ http {
|
|||||||
|
|
||||||
# for services from mempool.space like contributors on about page
|
# for services from mempool.space like contributors on about page
|
||||||
set $mempoolSpaceServices "https://mempool.space";
|
set $mempoolSpaceServices "https://mempool.space";
|
||||||
|
set $mempoolSpaceUnfurler "http://127.0.0.1:8001";
|
||||||
|
|
||||||
# for mempool daemons, see upstream-mempool.conf
|
# for mempool daemons, see upstream-mempool.conf
|
||||||
set $mempoolBisq "http://mempool-bitcoin-bisq";
|
set $mempoolBisq "http://mempool-bitcoin-bisq";
|
||||||
@ -105,6 +112,7 @@ http {
|
|||||||
|
|
||||||
# for services from mempool.space like contributors on about page
|
# for services from mempool.space like contributors on about page
|
||||||
set $mempoolSpaceServices "https://mempool.space";
|
set $mempoolSpaceServices "https://mempool.space";
|
||||||
|
set $mempoolSpaceUnfurler "http://127.0.0.1:8001";
|
||||||
|
|
||||||
# for mempool daemons, see upstream-mempool.conf
|
# for mempool daemons, see upstream-mempool.conf
|
||||||
set $mempoolMainnet "http://mempool-liquid-mainnet";
|
set $mempoolMainnet "http://mempool-liquid-mainnet";
|
||||||
|
@ -48,6 +48,9 @@ add_header Vary Cookie;
|
|||||||
# for exact / requests, redirect based on $lang
|
# for exact / requests, redirect based on $lang
|
||||||
# cache redirect for 5 minutes
|
# cache redirect for 5 minutes
|
||||||
location = / {
|
location = / {
|
||||||
|
if ($unfurlbot) {
|
||||||
|
proxy_pass $mempoolSpaceUnfurler;
|
||||||
|
}
|
||||||
if ($lang != '') {
|
if ($lang != '') {
|
||||||
return 302 $scheme://$host/$lang/;
|
return 302 $scheme://$host/$lang/;
|
||||||
}
|
}
|
||||||
@ -59,7 +62,7 @@ location = / {
|
|||||||
# cache /resources/** for 1 week since they don't change often
|
# cache /resources/** for 1 week since they don't change often
|
||||||
location ~ ^/[a-z][a-z]/resources/(.*) {
|
location ~ ^/[a-z][a-z]/resources/(.*) {
|
||||||
try_files $uri /en-US/resources/$1 =404;
|
try_files $uri /en-US/resources/$1 =404;
|
||||||
expires 1w;
|
expires 1w;
|
||||||
}
|
}
|
||||||
# cache /<lang>/main.f40e91d908a068a2.js forever since they never change
|
# cache /<lang>/main.f40e91d908a068a2.js forever since they never change
|
||||||
location ~ ^/([a-z][a-z])/(.+\..+\.(js|css)) {
|
location ~ ^/([a-z][a-z])/(.+\..+\.(js|css)) {
|
||||||
@ -69,11 +72,14 @@ location ~ ^/([a-z][a-z])/(.+\..+\.(js|css)) {
|
|||||||
# cache everything else for 5 minutes
|
# cache everything else for 5 minutes
|
||||||
location ~ ^/([a-z][a-z])$ {
|
location ~ ^/([a-z][a-z])$ {
|
||||||
try_files $uri /$1/index.html /en-US/index.html =404;
|
try_files $uri /$1/index.html /en-US/index.html =404;
|
||||||
expires 5m;
|
expires 5m;
|
||||||
}
|
}
|
||||||
location ~ ^/([a-z][a-z])/ {
|
location ~ ^/([a-z][a-z])/ {
|
||||||
|
if ($unfurlbot) {
|
||||||
|
proxy_pass $mempoolSpaceUnfurler;
|
||||||
|
}
|
||||||
try_files $uri /$1/index.html /en-US/index.html =404;
|
try_files $uri /$1/index.html /en-US/index.html =404;
|
||||||
expires 5m;
|
expires 5m;
|
||||||
}
|
}
|
||||||
|
|
||||||
# cache /resources/** for 1 week since they don't change often
|
# cache /resources/** for 1 week since they don't change often
|
||||||
@ -86,9 +92,24 @@ location ~* ^/.+\..+\.(js|css) {
|
|||||||
try_files /$lang/$uri /en-US/$uri =404;
|
try_files /$lang/$uri /en-US/$uri =404;
|
||||||
expires 1y;
|
expires 1y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# unfurl preview
|
||||||
|
location /preview {
|
||||||
|
try_files /$lang/$uri $uri /en-US/$uri /en-US/index.html =404;
|
||||||
|
expires 10m;
|
||||||
|
}
|
||||||
|
# unfurl renderer
|
||||||
|
location ^~ /render {
|
||||||
|
proxy_pass $mempoolSpaceUnfurler;
|
||||||
|
expires 10m;
|
||||||
|
}
|
||||||
|
|
||||||
# catch-all for all URLs i.e. /address/foo /tx/foo /block/000
|
# catch-all for all URLs i.e. /address/foo /tx/foo /block/000
|
||||||
# cache 5 minutes since they change frequently
|
# cache 5 minutes since they change frequently
|
||||||
location / {
|
location / {
|
||||||
|
if ($unfurlbot) {
|
||||||
|
proxy_pass $mempoolSpaceUnfurler;
|
||||||
|
}
|
||||||
try_files /$lang/$uri $uri /en-US/$uri /en-US/index.html =404;
|
try_files /$lang/$uri $uri /en-US/$uri /en-US/index.html =404;
|
||||||
expires 5m;
|
expires 5m;
|
||||||
}
|
}
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
#!/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
|
|
@ -1,2 +0,0 @@
|
|||||||
#!/usr/bin/env zsh
|
|
||||||
killall sh node
|
|
@ -1,6 +0,0 @@
|
|||||||
#!/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'
|
|
17
production/unfurler-config.bisq.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"SERVER": {
|
||||||
|
"HOST": "https://bisq.fra.mempool.space",
|
||||||
|
"HTTP_PORT": 8002
|
||||||
|
},
|
||||||
|
"MEMPOOL": {
|
||||||
|
"HTTP_HOST": "http://127.0.0.1",
|
||||||
|
"HTTP_PORT": 82,
|
||||||
|
"NETWORK": "bisq"
|
||||||
|
},
|
||||||
|
"PUPPETEER": {
|
||||||
|
"CLUSTER_SIZE": 8,
|
||||||
|
"EXEC_PATH": "/usr/local/bin/chrome",
|
||||||
|
"MAX_PAGE_AGE": 86400,
|
||||||
|
"RENDER_TIMEOUT": 3000
|
||||||
|
}
|
||||||
|
}
|
17
production/unfurler-config.liquid.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"SERVER": {
|
||||||
|
"HOST": "https://liquid.fra.mempool.space",
|
||||||
|
"HTTP_PORT": 8003
|
||||||
|
},
|
||||||
|
"MEMPOOL": {
|
||||||
|
"HTTP_HOST": "http://127.0.0.1",
|
||||||
|
"HTTP_PORT": 83,
|
||||||
|
"NETWORK": "bitcoin"
|
||||||
|
},
|
||||||
|
"PUPPETEER": {
|
||||||
|
"CLUSTER_SIZE": 8,
|
||||||
|
"EXEC_PATH": "/usr/local/bin/chrome",
|
||||||
|
"MAX_PAGE_AGE": 86400,
|
||||||
|
"RENDER_TIMEOUT": 3000
|
||||||
|
}
|
||||||
|
}
|
17
production/unfurler-config.mainnet.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"SERVER": {
|
||||||
|
"HOST": "https://mempool.fra.mempool.space",
|
||||||
|
"HTTP_PORT": 8001
|
||||||
|
},
|
||||||
|
"MEMPOOL": {
|
||||||
|
"HTTP_HOST": "http://127.0.0.1",
|
||||||
|
"HTTP_PORT": 81,
|
||||||
|
"NETWORK": "bitcoin"
|
||||||
|
},
|
||||||
|
"PUPPETEER": {
|
||||||
|
"CLUSTER_SIZE": 8,
|
||||||
|
"EXEC_PATH": "/usr/local/bin/chrome",
|
||||||
|
"MAX_PAGE_AGE": 86400,
|
||||||
|
"RENDER_TIMEOUT": 3000
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@
|
|||||||
"NETWORK": "bitcoin" // "bitcoin" | "liquid" | "bisq" (optional - defaults to "bitcoin")
|
"NETWORK": "bitcoin" // "bitcoin" | "liquid" | "bisq" (optional - defaults to "bitcoin")
|
||||||
},
|
},
|
||||||
"PUPPETEER": {
|
"PUPPETEER": {
|
||||||
|
"DISABLE": false, // optional, boolean, disables puppeteer and /render endpoints
|
||||||
"CLUSTER_SIZE": 2,
|
"CLUSTER_SIZE": 2,
|
||||||
"EXEC_PATH": "/usr/local/bin/chrome", // optional
|
"EXEC_PATH": "/usr/local/bin/chrome", // optional
|
||||||
"MAX_PAGE_AGE": 86400, // maximum lifetime of a page session (in seconds)
|
"MAX_PAGE_AGE": 86400, // maximum lifetime of a page session (in seconds)
|
||||||
|
4
unfurler/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-unfurl",
|
"name": "mempool-unfurl",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mempool-unfurl",
|
"name": "mempool-unfurl",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^16.11.41",
|
"@types/node": "^16.11.41",
|
||||||
"express": "^4.18.0",
|
"express": "^4.18.0",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-unfurl",
|
"name": "mempool-unfurl",
|
||||||
"version": "0.0.2",
|
"version": "0.1.0",
|
||||||
"description": "Renderer for mempool open graph link preview images",
|
"description": "Renderer for mempool open graph link preview images",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -11,7 +11,7 @@
|
|||||||
"tsc": "./node_modules/typescript/bin/tsc",
|
"tsc": "./node_modules/typescript/bin/tsc",
|
||||||
"build": "npm run tsc",
|
"build": "npm run tsc",
|
||||||
"start": "node --max-old-space-size=2048 dist/index.js",
|
"start": "node --max-old-space-size=2048 dist/index.js",
|
||||||
"start-production": "node --max-old-space-size=4096 dist/index.js",
|
"unfurler": "node --max-old-space-size=4096 dist/index.js",
|
||||||
"lint": "./node_modules/.bin/eslint . --ext .ts",
|
"lint": "./node_modules/.bin/eslint . --ext .ts",
|
||||||
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
|
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
|
||||||
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
||||||
|
@ -41,6 +41,6 @@
|
|||||||
"--use-mock-keychain",
|
"--use-mock-keychain",
|
||||||
"--ignore-gpu-blacklist",
|
"--ignore-gpu-blacklist",
|
||||||
"--ignore-gpu-blocklist",
|
"--ignore-gpu-blocklist",
|
||||||
"--use-gl=swiftshader"
|
"--use-gl=egl"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ interface IConfig {
|
|||||||
NETWORK?: string;
|
NETWORK?: string;
|
||||||
};
|
};
|
||||||
PUPPETEER: {
|
PUPPETEER: {
|
||||||
|
DISABLE: boolean;
|
||||||
CLUSTER_SIZE: number;
|
CLUSTER_SIZE: number;
|
||||||
EXEC_PATH?: string;
|
EXEC_PATH?: string;
|
||||||
MAX_PAGE_AGE?: number;
|
MAX_PAGE_AGE?: number;
|
||||||
@ -28,6 +29,7 @@ const defaults: IConfig = {
|
|||||||
'HTTP_PORT': 4200,
|
'HTTP_PORT': 4200,
|
||||||
},
|
},
|
||||||
'PUPPETEER': {
|
'PUPPETEER': {
|
||||||
|
'DISABLE': false,
|
||||||
'CLUSTER_SIZE': 1,
|
'CLUSTER_SIZE': 1,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { Application, Request, Response, NextFunction } from 'express';
|
import { Application, Request, Response, NextFunction } from 'express';
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
|
import * as https from 'https';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import { Cluster } from 'puppeteer-cluster';
|
import { Cluster } from 'puppeteer-cluster';
|
||||||
import ReusablePage from './concurrency/ReusablePage';
|
import ReusablePage from './concurrency/ReusablePage';
|
||||||
import { parseLanguageUrl } from './language/lang';
|
import { parseLanguageUrl } from './language/lang';
|
||||||
|
import { matchRoute } from './routes';
|
||||||
const puppeteerConfig = require('../puppeteer.config.json');
|
const puppeteerConfig = require('../puppeteer.config.json');
|
||||||
|
|
||||||
if (config.PUPPETEER.EXEC_PATH) {
|
if (config.PUPPETEER.EXEC_PATH) {
|
||||||
@ -17,13 +19,13 @@ class Server {
|
|||||||
cluster?: Cluster;
|
cluster?: Cluster;
|
||||||
mempoolHost: string;
|
mempoolHost: string;
|
||||||
network: string;
|
network: string;
|
||||||
defaultImageUrl: string;
|
secureHost = true;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = express();
|
this.app = express();
|
||||||
this.mempoolHost = config.MEMPOOL.HTTP_HOST + (config.MEMPOOL.HTTP_PORT ? ':' + config.MEMPOOL.HTTP_PORT : '');
|
this.mempoolHost = config.MEMPOOL.HTTP_HOST + (config.MEMPOOL.HTTP_PORT ? ':' + config.MEMPOOL.HTTP_PORT : '');
|
||||||
|
this.secureHost = this.mempoolHost.startsWith('https');
|
||||||
this.network = config.MEMPOOL.NETWORK || 'bitcoin';
|
this.network = config.MEMPOOL.NETWORK || 'bitcoin';
|
||||||
this.defaultImageUrl = this.getDefaultImageUrl();
|
|
||||||
this.startServer();
|
this.startServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,12 +39,14 @@ class Server {
|
|||||||
.use(express.text())
|
.use(express.text())
|
||||||
;
|
;
|
||||||
|
|
||||||
this.cluster = await Cluster.launch({
|
if (!config.PUPPETEER.DISABLE) {
|
||||||
concurrency: ReusablePage,
|
this.cluster = await Cluster.launch({
|
||||||
maxConcurrency: config.PUPPETEER.CLUSTER_SIZE,
|
concurrency: ReusablePage,
|
||||||
puppeteerOptions: puppeteerConfig,
|
maxConcurrency: config.PUPPETEER.CLUSTER_SIZE,
|
||||||
});
|
puppeteerOptions: puppeteerConfig,
|
||||||
await this.cluster?.task(async (args) => { return this.clusterTask(args) });
|
});
|
||||||
|
await this.cluster?.task(async (args) => { return this.clusterTask(args) });
|
||||||
|
}
|
||||||
|
|
||||||
this.setUpRoutes();
|
this.setUpRoutes();
|
||||||
|
|
||||||
@ -64,7 +68,11 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setUpRoutes() {
|
setUpRoutes() {
|
||||||
this.app.get('/render*', async (req, res) => { return this.renderPreview(req, res) })
|
if (!config.PUPPETEER.DISABLE) {
|
||||||
|
this.app.get('/render*', async (req, res) => { return this.renderPreview(req, res) })
|
||||||
|
} else {
|
||||||
|
this.app.get('/render*', async (req, res) => { return this.renderDisabled(req, res) })
|
||||||
|
}
|
||||||
this.app.get('*', (req, res) => { return this.renderHTML(req, res) })
|
this.app.get('*', (req, res) => { return this.renderHTML(req, res) })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,13 +119,31 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async renderDisabled(req, res) {
|
||||||
|
res.status(500).send("preview rendering disabled");
|
||||||
|
}
|
||||||
|
|
||||||
async renderPreview(req, res) {
|
async renderPreview(req, res) {
|
||||||
try {
|
try {
|
||||||
const path = req.params[0]
|
const rawPath = req.params[0];
|
||||||
const img = await this.cluster?.execute({ url: this.mempoolHost + path, path: path, action: 'screenshot' });
|
|
||||||
|
let img = null;
|
||||||
|
|
||||||
|
const { lang, path } = parseLanguageUrl(rawPath);
|
||||||
|
const matchedRoute = matchRoute(this.network, path);
|
||||||
|
|
||||||
|
// don't bother unless the route is definitely renderable
|
||||||
|
if (rawPath.includes('/preview/') && matchedRoute.render) {
|
||||||
|
img = await this.cluster?.execute({ url: this.mempoolHost + rawPath, path: rawPath, action: 'screenshot' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!img) {
|
if (!img) {
|
||||||
res.status(500).send('failed to render page preview');
|
// proxy fallback image from the frontend
|
||||||
|
if (this.secureHost) {
|
||||||
|
https.get(config.SERVER.HOST + matchedRoute.fallbackImg, (got) => got.pipe(res));
|
||||||
|
} else {
|
||||||
|
http.get(config.SERVER.HOST + matchedRoute.fallbackImg, (got) => got.pipe(res));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
res.contentType('image/png');
|
res.contentType('image/png');
|
||||||
res.send(img);
|
res.send(img);
|
||||||
@ -137,50 +163,14 @@ class Server {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let previewSupported = true;
|
|
||||||
let mode = 'mainnet'
|
|
||||||
let ogImageUrl = this.defaultImageUrl;
|
|
||||||
let ogTitle;
|
|
||||||
const { lang, path } = parseLanguageUrl(rawPath);
|
const { lang, path } = parseLanguageUrl(rawPath);
|
||||||
const parts = path.slice(1).split('/');
|
const matchedRoute = matchRoute(this.network, path);
|
||||||
|
let ogImageUrl = config.SERVER.HOST + (matchedRoute.staticImg || matchedRoute.fallbackImg);
|
||||||
|
let ogTitle = 'The Mempool Open Source Project™';
|
||||||
|
|
||||||
// handle network mode modifiers
|
if (matchedRoute.render) {
|
||||||
if (['testnet', 'signet'].includes(parts[0])) {
|
|
||||||
mode = parts.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle supported preview routes
|
|
||||||
switch (parts[0]) {
|
|
||||||
case 'block':
|
|
||||||
ogTitle = `Block: ${parts[1]}`;
|
|
||||||
break;
|
|
||||||
case 'address':
|
|
||||||
ogTitle = `Address: ${parts[1]}`;
|
|
||||||
break;
|
|
||||||
case 'tx':
|
|
||||||
ogTitle = `Transaction: ${parts[1]}`;
|
|
||||||
break;
|
|
||||||
case 'lightning':
|
|
||||||
switch (parts[1]) {
|
|
||||||
case 'node':
|
|
||||||
ogTitle = `Lightning Node: ${parts[2]}`;
|
|
||||||
break;
|
|
||||||
case 'channel':
|
|
||||||
ogTitle = `Lightning Channel: ${parts[2]}`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
previewSupported = false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
previewSupported = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previewSupported) {
|
|
||||||
ogImageUrl = `${config.SERVER.HOST}/render/${lang || 'en'}/preview${path}`;
|
ogImageUrl = `${config.SERVER.HOST}/render/${lang || 'en'}/preview${path}`;
|
||||||
ogTitle = `${this.network ? capitalize(this.network) + ' ' : ''}${mode !== 'mainnet' ? capitalize(mode) + ' ' : ''}${ogTitle}`;
|
ogTitle = `${this.network ? capitalize(this.network) + ' ' : ''}${matchedRoute.networkMode !== 'mainnet' ? capitalize(matchedRoute.networkMode) + ' ' : ''}${matchedRoute.title}`;
|
||||||
} else {
|
|
||||||
ogTitle = 'The Mempool Open Source Project™';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send(`
|
res.send(`
|
||||||
@ -189,34 +179,23 @@ class Server {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>${ogTitle}</title>
|
<title>${ogTitle}</title>
|
||||||
<meta name="description" content="The Mempool Open Source Project™ - our self-hosted explorer for the ${capitalize(this.network)} community."/>
|
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem with mempool.space™"/>
|
||||||
<meta property="og:image" content="${ogImageUrl}"/>
|
<meta property="og:image" content="${ogImageUrl}"/>
|
||||||
<meta property="og:image:type" content="image/png"/>
|
<meta property="og:image:type" content="image/png"/>
|
||||||
<meta property="og:image:width" content="${previewSupported ? 1200 : 1000}"/>
|
<meta property="og:image:width" content="${matchedRoute.render ? 1200 : 1000}"/>
|
||||||
<meta property="og:image:height" content="${previewSupported ? 600 : 500}"/>
|
<meta property="og:image:height" content="${matchedRoute.render ? 600 : 500}"/>
|
||||||
<meta property="og:title" content="${ogTitle}">
|
<meta property="og:title" content="${ogTitle}">
|
||||||
<meta property="twitter:card" content="summary_large_image">
|
<meta property="twitter:card" content="summary_large_image">
|
||||||
<meta property="twitter:site" content="@mempool">
|
<meta property="twitter:site" content="@mempool">
|
||||||
<meta property="twitter:creator" content="@mempool">
|
<meta property="twitter:creator" content="@mempool">
|
||||||
<meta property="twitter:title" content="${ogTitle}">
|
<meta property="twitter:title" content="${ogTitle}">
|
||||||
<meta property="twitter:description" content="Our self-hosted mempool explorer for the ${capitalize(this.network)} community."/>
|
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space"/>
|
||||||
<meta property="twitter:image:src" content="${ogImageUrl}"/>
|
<meta property="twitter:image:src" content="${ogImageUrl}"/>
|
||||||
<meta property="twitter:domain" content="mempool.space">
|
<meta property="twitter:domain" content="mempool.space">
|
||||||
<body></body>
|
<body></body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultImageUrl() {
|
|
||||||
switch (this.network) {
|
|
||||||
case 'liquid':
|
|
||||||
return this.mempoolHost + '/resources/liquid/liquid-network-preview.png';
|
|
||||||
case 'bisq':
|
|
||||||
return this.mempoolHost + '/resources/bisq/bisq-markets-preview.png';
|
|
||||||
default:
|
|
||||||
return this.mempoolHost + '/resources/mempool-space-preview.png';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = new Server();
|
const server = new Server();
|
||||||
|
124
unfurler/src/routes.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
interface Match {
|
||||||
|
render: boolean;
|
||||||
|
title: string;
|
||||||
|
fallbackImg: string;
|
||||||
|
staticImg?: string;
|
||||||
|
networkMode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const routes = {
|
||||||
|
block: {
|
||||||
|
render: true,
|
||||||
|
params: 1,
|
||||||
|
getTitle(path) {
|
||||||
|
return `Block: ${path[0]}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
render: true,
|
||||||
|
params: 1,
|
||||||
|
getTitle(path) {
|
||||||
|
return `Address: ${path[0]}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tx: {
|
||||||
|
render: true,
|
||||||
|
params: 1,
|
||||||
|
getTitle(path) {
|
||||||
|
return `Transaction: ${path[0]}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lightning: {
|
||||||
|
title: "Lightning",
|
||||||
|
fallbackImg: '/resources/previews/lightning.png',
|
||||||
|
routes: {
|
||||||
|
node: {
|
||||||
|
render: true,
|
||||||
|
params: 1,
|
||||||
|
getTitle(path) {
|
||||||
|
return `Lightning Node: ${path[0]}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
channel: {
|
||||||
|
render: true,
|
||||||
|
params: 1,
|
||||||
|
getTitle(path) {
|
||||||
|
return `Lightning Channel: ${path[0]}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mining: {
|
||||||
|
title: "Mining",
|
||||||
|
fallbackImg: '/resources/previews/mining.png'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const networks = {
|
||||||
|
bitcoin: {
|
||||||
|
fallbackImg: '/resources/mempool-space-preview.png',
|
||||||
|
staticImg: '/resources/previews/dashboard.png',
|
||||||
|
routes: {
|
||||||
|
...routes // all routes supported
|
||||||
|
}
|
||||||
|
},
|
||||||
|
liquid: {
|
||||||
|
fallbackImg: '/resources/liquid/liquid-network-preview.png',
|
||||||
|
routes: { // only block, address & tx routes supported
|
||||||
|
block: routes.block,
|
||||||
|
address: routes.address,
|
||||||
|
tx: routes.tx
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bisq: {
|
||||||
|
fallbackImg: '/resources/bisq/bisq-markets-preview.png',
|
||||||
|
routes: {} // no routes supported
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function matchRoute(network: string, path: string): Match {
|
||||||
|
const match: Match = {
|
||||||
|
render: false,
|
||||||
|
title: '',
|
||||||
|
fallbackImg: '',
|
||||||
|
networkMode: 'mainnet'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = path.slice(1).split('/').filter(p => p.length);
|
||||||
|
|
||||||
|
if (parts[0] === 'preview') {
|
||||||
|
parts.shift();
|
||||||
|
}
|
||||||
|
if (['testnet', 'signet'].includes(parts[0])) {
|
||||||
|
match.networkMode = parts.shift() || 'mainnet';
|
||||||
|
}
|
||||||
|
|
||||||
|
let route = networks[network] || networks.bitcoin;
|
||||||
|
match.fallbackImg = route.fallbackImg;
|
||||||
|
|
||||||
|
// traverse the route tree until we run out of route or tree, or hit a renderable match
|
||||||
|
while (!route.render && route.routes && parts.length && route.routes[parts[0]]) {
|
||||||
|
route = route.routes[parts[0]];
|
||||||
|
parts.shift();
|
||||||
|
if (route.fallbackImg) {
|
||||||
|
match.fallbackImg = route.fallbackImg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enough route parts left for title & rendering
|
||||||
|
if (route.render && parts.length >= route.params) {
|
||||||
|
match.render = true;
|
||||||
|
}
|
||||||
|
// only use set a static image for exact matches
|
||||||
|
if (!parts.length && route.staticImg) {
|
||||||
|
match.staticImg = route.staticImg;
|
||||||
|
}
|
||||||
|
// apply the title function if present
|
||||||
|
if (route.getTitle && typeof route.getTitle === 'function') {
|
||||||
|
match.title = route.getTitle(parts);
|
||||||
|
} else {
|
||||||
|
match.title = route.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match;
|
||||||
|
}
|