Merge branch 'master' into better-block-unfurls

This commit is contained in:
wiz 2022-07-28 01:36:10 +00:00 committed by GitHub
commit 9c44bf171c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 502 additions and 227 deletions

View File

@ -15,10 +15,11 @@
"@typescript-eslint/ban-types": 1, "@typescript-eslint/ban-types": 1,
"@typescript-eslint/no-empty-function": 1, "@typescript-eslint/no-empty-function": 1,
"@typescript-eslint/no-explicit-any": 1, "@typescript-eslint/no-explicit-any": 1,
"@typescript-eslint/no-inferrable-types": 1, "@typescript-eslint/no-inferrable-types": 0,
"@typescript-eslint/no-namespace": 1, "@typescript-eslint/no-namespace": 1,
"@typescript-eslint/no-this-alias": 1, "@typescript-eslint/no-this-alias": 1,
"@typescript-eslint/no-var-requires": 1, "@typescript-eslint/no-var-requires": 1,
"@typescript-eslint/explicit-function-return-type": 1,
"no-console": 1, "no-console": 1,
"no-constant-condition": 1, "no-constant-condition": 1,
"no-dupe-else-if": 1, "no-dupe-else-if": 1,
@ -28,6 +29,8 @@
"no-useless-catch": 1, "no-useless-catch": 1,
"no-var": 1, "no-var": 1,
"prefer-const": 1, "prefer-const": 1,
"prefer-rest-params": 1 "prefer-rest-params": 1,
"quotes": [1, "single", { "allowTemplateLiterals": true }],
"semi": 1
} }
} }

View File

@ -9,6 +9,7 @@ export interface AbstractBitcoinApi {
$getBlockHash(height: number): Promise<string>; $getBlockHash(height: number): Promise<string>;
$getBlockHeader(hash: string): Promise<string>; $getBlockHeader(hash: string): Promise<string>;
$getBlock(hash: string): Promise<IEsploraApi.Block>; $getBlock(hash: string): Promise<IEsploraApi.Block>;
$getRawBlock(hash: string): Promise<string>;
$getAddress(address: string): Promise<IEsploraApi.Address>; $getAddress(address: string): Promise<IEsploraApi.Address>;
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>; $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$getAddressPrefix(prefix: string): string[]; $getAddressPrefix(prefix: string): string[];

View File

@ -77,7 +77,8 @@ class BitcoinApi implements AbstractBitcoinApi {
} }
$getRawBlock(hash: string): Promise<string> { $getRawBlock(hash: string): Promise<string> {
return this.bitcoindClient.getBlock(hash, 0); return this.bitcoindClient.getBlock(hash, 0)
.then((raw: string) => Buffer.from(raw, "hex"));
} }
$getBlockHash(height: number): Promise<string> { $getBlockHash(height: number): Promise<string> {

View File

@ -103,9 +103,10 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', this.getBlockTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', this.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', this.getBlockTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', this.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress) .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions)
@ -470,6 +471,16 @@ class BitcoinRoutes {
} }
} }
private async getRawBlock(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getRawBlock(req.params.hash);
res.setHeader('content-type', 'application/octet-stream');
res.send(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getTxIdsForBlock(req: Request, res: Response) { private async getTxIdsForBlock(req: Request, res: Response) {
try { try {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);

View File

@ -50,6 +50,11 @@ class ElectrsApi implements AbstractBitcoinApi {
.then((response) => response.data); .then((response) => response.data);
} }
$getRawBlock(hash: string): Promise<string> {
return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", this.axiosConfig)
.then((response) => response.data);
}
$getAddress(address: string): Promise<IEsploraApi.Address> { $getAddress(address: string): Promise<IEsploraApi.Address> {
throw new Error('Method getAddress not implemented.'); throw new Error('Method getAddress not implemented.');
} }

View File

@ -9,8 +9,8 @@ class DatabaseMigration {
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
private blocksTruncatedMessage = `'blocks' table has been truncated. Re-indexing from scratch.`; private blocksTruncatedMessage = `'blocks' table has been truncated.`;
private hashratesTruncatedMessage = `'hashrates' table has been truncated. Re-indexing from scratch.`; private hashratesTruncatedMessage = `'hashrates' table has been truncated.`;
/** /**
* Avoid printing multiple time the same message * Avoid printing multiple time the same message
@ -256,7 +256,9 @@ class DatabaseMigration {
} }
if (databaseSchemaVersion < 26 && isBitcoin === true) { if (databaseSchemaVersion < 26 && isBitcoin === true) {
this.uniqueLog(logger.notice, `'lightning_stats' table has been truncated. Will re-generate historical data from scratch.`); if (config.LIGHTNING.ENABLED) {
this.uniqueLog(logger.notice, `'lightning_stats' table has been truncated.`);
}
await this.$executeQuery(`TRUNCATE lightning_stats`); await this.$executeQuery(`TRUNCATE lightning_stats`);
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
@ -273,6 +275,9 @@ class DatabaseMigration {
} }
if (databaseSchemaVersion < 28 && isBitcoin === true) { if (databaseSchemaVersion < 28 && isBitcoin === true) {
if (config.LIGHTNING.ENABLED) {
this.uniqueLog(logger.notice, `'lightning_stats' and 'node_stats' tables have been truncated.`);
}
await this.$executeQuery(`TRUNCATE lightning_stats`); await this.$executeQuery(`TRUNCATE lightning_stats`);
await this.$executeQuery(`TRUNCATE node_stats`); await this.$executeQuery(`TRUNCATE node_stats`);
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`); await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);

View File

@ -5,25 +5,26 @@ class NodesApi {
public async $getNode(public_key: string): Promise<any> { public async $getNode(public_key: string): Promise<any> {
try { try {
const query = ` const query = `
SELECT nodes.*, geo_names_as.names as as_organization, geo_names_city.names as city, SELECT nodes.*, geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city,
geo_names_country.names as country, geo_names_subdivision.names as subdivision, geo_names_country.names as country, geo_names_subdivision.names as subdivision,
(SELECT Count(*) (SELECT Count(*)
FROM channels FROM channels
WHERE channels.status = 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_closed_count, WHERE channels.status = 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_closed_count,
(SELECT Count(*) (SELECT Count(*)
FROM channels FROM channels
WHERE channels.status < 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_active_count, WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_active_count,
(SELECT Sum(capacity) (SELECT Sum(capacity)
FROM channels FROM channels
WHERE channels.status < 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS capacity, WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS capacity,
(SELECT Avg(capacity) (SELECT Avg(capacity)
FROM channels FROM channels
WHERE status < 2 AND ( node1_public_key = ? OR node2_public_key = ? )) AS channels_capacity_avg WHERE status = 1 AND ( node1_public_key = ? OR node2_public_key = ? )) AS channels_capacity_avg
FROM nodes FROM nodes
LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = subdivision_id LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = subdivision_id
LEFT JOIN geo_names geo_names_country on geo_names_country.id = country_id LEFT JOIN geo_names geo_names_country on geo_names_country.id = country_id
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
WHERE public_key = ? WHERE public_key = ?
`; `;
const [rows]: any = await DB.query(query, [public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key]); const [rows]: any = await DB.query(query, [public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key]);
@ -97,29 +98,59 @@ class NodesApi {
} }
} }
public async $getNodesISP() { public async $getNodesISP(groupBy: string, showTor: boolean) {
try { try {
let query = `SELECT GROUP_CONCAT(DISTINCT(nodes.as_number)) as ispId, geo_names.names as names, COUNT(DISTINCT nodes.public_key) as nodesCount, SUM(capacity) as capacity const orderBy = groupBy === 'capacity' ? `CAST(SUM(capacity) as INT)` : `COUNT(DISTINCT nodes.public_key)`;
// Clearnet
let query = `SELECT GROUP_CONCAT(DISTINCT(nodes.as_number)) as ispId, geo_names.names as names,
COUNT(DISTINCT nodes.public_key) as nodesCount, CAST(SUM(capacity) as INT) as capacity
FROM nodes FROM nodes
JOIN geo_names ON geo_names.id = nodes.as_number JOIN geo_names ON geo_names.id = nodes.as_number
JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
GROUP BY geo_names.names GROUP BY geo_names.names
ORDER BY COUNT(DISTINCT nodes.public_key) DESC ORDER BY ${orderBy} DESC
`; `;
const [nodesCountPerAS]: any = await DB.query(query); const [nodesCountPerAS]: any = await DB.query(query);
query = `SELECT COUNT(*) as total FROM nodes WHERE as_number IS NOT NULL`; let total = 0;
const [nodesWithAS]: any = await DB.query(query);
const nodesPerAs: any[] = []; const nodesPerAs: any[] = [];
for (const asGroup of nodesCountPerAS) {
if (groupBy === 'capacity') {
total += asGroup.capacity;
} else {
total += asGroup.nodesCount;
}
}
// Tor
if (showTor) {
query = `SELECT COUNT(DISTINCT nodes.public_key) as nodesCount, CAST(SUM(capacity) as INT) as capacity
FROM nodes
JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
ORDER BY ${orderBy} DESC
`;
const [nodesCountTor]: any = await DB.query(query);
total += groupBy === 'capacity' ? nodesCountTor[0].capacity : nodesCountTor[0].nodesCount;
nodesPerAs.push({
ispId: null,
name: 'Tor',
count: nodesCountTor[0].nodesCount,
share: Math.floor((groupBy === 'capacity' ? nodesCountTor[0].capacity : nodesCountTor[0].nodesCount) / total * 10000) / 100,
capacity: nodesCountTor[0].capacity,
});
}
for (const as of nodesCountPerAS) { for (const as of nodesCountPerAS) {
nodesPerAs.push({ nodesPerAs.push({
ispId: as.ispId, ispId: as.ispId,
name: JSON.parse(as.names), name: JSON.parse(as.names),
count: as.nodesCount, count: as.nodesCount,
share: Math.floor(as.nodesCount / nodesWithAS[0].total * 10000) / 100, share: Math.floor((groupBy === 'capacity' ? as.capacity : as.nodesCount) / total * 10000) / 100,
capacity: as.capacity, capacity: as.capacity,
}) });
} }
return nodesPerAs; return nodesPerAs;

View File

@ -9,10 +9,10 @@ class NodesRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
app app
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/top', this.$getTopNodes) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/top', this.$getTopNodes)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp', this.$getNodesISP) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp-ranking', this.$getISPRanking)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/countries', this.$getNodesCountries) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/countries', this.$getNodesCountries)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
@ -63,9 +63,18 @@ class NodesRoutes {
} }
} }
private async $getNodesISP(req: Request, res: Response) { private async $getISPRanking(req: Request, res: Response): Promise<void> {
try { try {
const nodesPerAs = await nodesApi.$getNodesISP(); const groupBy = req.query.groupBy as string;
const showTor = req.query.showTor as string === 'true' ? true : false;
if (!['capacity', 'node-count'].includes(groupBy)) {
res.status(400).send(`groupBy must be one of 'capacity' or 'node-count'`);
return;
}
const nodesPerAs = await nodesApi.$getNodesISP(groupBy, showTor);
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());

View File

@ -13,7 +13,7 @@ class StatisticsApi {
query += ` WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; query += ` WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
} }
query += ` ORDER BY id DESC`; query += ` ORDER BY added DESC`;
try { try {
const [rows]: any = await DB.query(query); const [rows]: any = await DB.query(query);
@ -26,8 +26,8 @@ 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 id 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 id DESC LIMIT 1 OFFSET 7`); const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY added DESC LIMIT 1 OFFSET 7`);
return { return {
latest: rows[0], latest: rows[0],
previous: rows2[0], previous: rows2[0],

View File

@ -38,11 +38,13 @@ class NodeSyncService {
await $lookupNodeLocation(); await $lookupNodeLocation();
} }
await this.$setChannelsInactive(); const graphChannelsIds: string[] = [];
for (const channel of networkGraph.channels) { for (const channel of networkGraph.channels) {
await this.$saveChannel(channel); await this.$saveChannel(channel);
graphChannelsIds.push(channel.id);
} }
await this.$setChannelsInactive(graphChannelsIds);
logger.info(`Channels updated.`); logger.info(`Channels updated.`);
await this.$findInactiveNodesAndChannels(); await this.$findInactiveNodesAndChannels();
@ -106,7 +108,22 @@ class NodeSyncService {
try { try {
// @ts-ignore // @ts-ignore
const [channels]: [ILightningApi.Channel[]] = await DB.query(`SELECT channels.id FROM channels WHERE channels.status = 1 AND ((SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node1_public_key) = 0 OR (SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node2_public_key) = 0)`); const [channels]: [ILightningApi.Channel[]] = await DB.query(`
SELECT channels.id
FROM channels
WHERE channels.status = 1
AND (
(
SELECT COUNT(*)
FROM nodes
WHERE nodes.public_key = channels.node1_public_key
) = 0
OR (
SELECT COUNT(*)
FROM nodes
WHERE nodes.public_key = channels.node2_public_key
) = 0)
`);
for (const channel of channels) { for (const channel of channels) {
await this.$updateChannelStatus(channel.id, 0); await this.$updateChannelStatus(channel.id, 0);
@ -356,9 +373,16 @@ class NodeSyncService {
} }
} }
private async $setChannelsInactive(): Promise<void> { private async $setChannelsInactive(graphChannelsIds: string[]): Promise<void> {
try { try {
await DB.query(`UPDATE channels SET status = 0 WHERE status = 1`); await DB.query(`
UPDATE channels
SET status = 0
WHERE short_id NOT IN (
${graphChannelsIds.map(id => `"${id}"`).join(',')}
)
AND status != 2
`);
} catch (e) { } catch (e) {
logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e)); logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e));
} }

View File

@ -141,7 +141,22 @@ class LightningStatsUpdater {
try { try {
logger.info(`Running daily node stats update...`); logger.info(`Running daily node stats update...`);
const query = `SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left, c2.channels_capacity_right FROM nodes LEFT JOIN (SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left FROM channels WHERE channels.status < 2 GROUP BY node1_public_key) c1 ON c1.node1_public_key = nodes.public_key LEFT JOIN (SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right FROM channels WHERE channels.status < 2 GROUP BY node2_public_key) c2 ON c2.node2_public_key = nodes.public_key`; const query = `
SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left,
c2.channels_capacity_right
FROM nodes
LEFT JOIN (
SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left
FROM channels
WHERE channels.status = 1
GROUP BY node1_public_key
) c1 ON c1.node1_public_key = nodes.public_key
LEFT JOIN (
SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right
FROM channels WHERE channels.status = 1 GROUP BY node2_public_key
) c2 ON c2.node2_public_key = nodes.public_key
`;
const [nodes]: any = await DB.query(query); const [nodes]: any = await DB.query(query);
for (const node of nodes) { for (const node of nodes) {

View File

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 25, 2022.
Signed: oleonardolima

View File

@ -14,10 +14,11 @@
"@typescript-eslint/ban-types": 1, "@typescript-eslint/ban-types": 1,
"@typescript-eslint/no-empty-function": 1, "@typescript-eslint/no-empty-function": 1,
"@typescript-eslint/no-explicit-any": 1, "@typescript-eslint/no-explicit-any": 1,
"@typescript-eslint/no-inferrable-types": 1, "@typescript-eslint/no-inferrable-types": 0,
"@typescript-eslint/no-namespace": 1, "@typescript-eslint/no-namespace": 1,
"@typescript-eslint/no-this-alias": 1, "@typescript-eslint/no-this-alias": 1,
"@typescript-eslint/no-var-requires": 1, "@typescript-eslint/no-var-requires": 1,
"@typescript-eslint/explicit-function-return-type": 1,
"no-case-declarations": 1, "no-case-declarations": 1,
"no-console": 1, "no-console": 1,
"no-constant-condition": 1, "no-constant-condition": 1,
@ -29,6 +30,8 @@
"no-useless-catch": 1, "no-useless-catch": 1,
"no-var": 1, "no-var": 1,
"prefer-const": 1, "prefer-const": 1,
"prefer-rest-params": 1 "prefer-rest-params": 1,
"quotes": [1, "single", { "allowTemplateLiterals": true }],
"semi": 1
} }
} }

View File

@ -5,9 +5,9 @@ const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH
export function calcSegwitFeeGains(tx: Transaction) { export function calcSegwitFeeGains(tx: Transaction) {
// calculated in weight units // calculated in weight units
let realizedBech32Gains = 0; let realizedSegwitGains = 0;
let potentialBech32Gains = 0; let potentialSegwitGains = 0;
let potentialP2shGains = 0; let potentialP2shSegwitGains = 0;
let potentialTaprootGains = 0; let potentialTaprootGains = 0;
let realizedTaprootGains = 0; let realizedTaprootGains = 0;
@ -24,31 +24,33 @@ export function calcSegwitFeeGains(tx: Transaction) {
const isP2tr = vin.prevout.scriptpubkey_type === 'v1_p2tr'; const isP2tr = vin.prevout.scriptpubkey_type === 'v1_p2tr';
const op = vin.scriptsig ? vin.scriptsig_asm.split(' ')[0] : null; const op = vin.scriptsig ? vin.scriptsig_asm.split(' ')[0] : null;
const isP2sh2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22'; const isP2shP2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22';
const isP2sh2Wsh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34'; const isP2shP2Wsh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34';
switch (true) { switch (true) {
// Native Segwit - P2WPKH/P2WSH (Bech32) // Native Segwit - P2WPKH/P2WSH/P2TR
case isP2wpkh: case isP2wpkh:
case isP2wsh: case isP2wsh:
case isP2tr: case isP2tr:
// maximal gains: the scriptSig is moved entirely to the witness part // maximal gains: the scriptSig is moved entirely to the witness part
realizedBech32Gains += witnessSize(vin) * 3; // if taproot is used savings are 42 WU higher because it produces smaller signatures and doesn't require a pubkey in the witness
// this number is explained above `realizedTaprootGains += 42;`
realizedSegwitGains += (witnessSize(vin) + (isP2tr ? 42 : 0)) * 3;
// XXX P2WSH output creation is more expensive, should we take this into consideration? // XXX P2WSH output creation is more expensive, should we take this into consideration?
break; break;
// Backward compatible Segwit - P2SH-P2WPKH // Backward compatible Segwit - P2SH-P2WPKH
case isP2sh2Wpkh: case isP2shP2Wpkh:
// the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (48 WU) // the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (84 WU)
realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST; realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST;
potentialBech32Gains += P2SH_P2WPKH_COST; potentialSegwitGains += P2SH_P2WPKH_COST;
break; break;
// Backward compatible Segwit - P2SH-P2WSH // Backward compatible Segwit - P2SH-P2WSH
case isP2sh2Wsh: case isP2shP2Wsh:
// the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes // the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes (140 WU)
realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WSH_COST; realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WSH_COST;
potentialBech32Gains += P2SH_P2WSH_COST; potentialSegwitGains += P2SH_P2WSH_COST;
break; break;
// Non-segwit P2PKH/P2SH/P2PK/bare multisig // Non-segwit P2PKH/P2SH/P2PK/bare multisig
@ -56,9 +58,13 @@ export function calcSegwitFeeGains(tx: Transaction) {
case isP2sh: case isP2sh:
case isP2pk: case isP2pk:
case isBareMultisig: { case isBareMultisig: {
const fullGains = scriptSigSize(vin) * 3; let fullGains = scriptSigSize(vin) * 3;
potentialBech32Gains += fullGains; if (isBareMultisig) {
potentialP2shGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST); // a _bare_ multisig has the keys in the output script, but P2SH and P2WSH require them to be in the scriptSig/scriptWitness
fullGains -= vin.prevout.scriptpubkey.length / 2;
}
potentialSegwitGains += fullGains;
potentialP2shSegwitGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST);
break; break;
} }
} }
@ -79,11 +85,11 @@ export function calcSegwitFeeGains(tx: Transaction) {
// TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts // TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
} }
} else { } else {
const script = isP2sh2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm; const script = isP2shP2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm;
let replacementSize: number; let replacementSize: number;
if ( if (
// single sig // single sig
isP2pk || isP2pkh || isP2wpkh || isP2sh2Wpkh || isP2pk || isP2pkh || isP2wpkh || isP2shP2Wpkh ||
// multisig // multisig
isBareMultisig || parseMultisigScript(script) isBareMultisig || parseMultisigScript(script)
) { ) {
@ -105,11 +111,11 @@ export function calcSegwitFeeGains(tx: Transaction) {
// returned as percentage of the total tx weight // returned as percentage of the total tx weight
return { return {
realizedBech32Gains: realizedBech32Gains / (tx.weight + realizedBech32Gains), // percent of the pre-segwit tx size realizedSegwitGains: realizedSegwitGains / (tx.weight + realizedSegwitGains), // percent of the pre-segwit tx size
potentialBech32Gains: potentialBech32Gains / tx.weight, potentialSegwitGains: potentialSegwitGains / tx.weight,
potentialP2shGains: potentialP2shGains / tx.weight, potentialP2shSegwitGains: potentialP2shSegwitGains / tx.weight,
potentialTaprootGains: potentialTaprootGains / tx.weight, potentialTaprootGains: potentialTaprootGains / tx.weight,
realizedTaprootGains: realizedTaprootGains / tx.weight realizedTaprootGains: realizedTaprootGains / (tx.weight + realizedTaprootGains)
}; };
} }
@ -188,12 +194,12 @@ export function moveDec(num: number, n: number) {
return neg + (int || '0') + (frac.length ? '.' + frac : ''); return neg + (int || '0') + (frac.length ? '.' + frac : '');
} }
function zeros(n) { function zeros(n: number) {
return new Array(n + 1).join('0'); return new Array(n + 1).join('0');
} }
// Formats a number for display. Treats the number as a string to avoid rounding errors. // Formats a number for display. Treats the number as a string to avoid rounding errors.
export const formatNumber = (s, precision = null) => { export const formatNumber = (s: number | string, precision: number | null = null) => {
let [ whole, dec ] = s.toString().split('.'); let [ whole, dec ] = s.toString().split('.');
// divide numbers into groups of three separated with a thin space (U+202F, "NARROW NO-BREAK SPACE"), // divide numbers into groups of three separated with a thin space (U+202F, "NARROW NO-BREAK SPACE"),
@ -219,27 +225,27 @@ const witnessSize = (vin: Vin) => vin.witness ? vin.witness.reduce((S, w) => S +
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0; const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
// Power of ten wrapper // Power of ten wrapper
export function selectPowerOfTen(val: number) { export function selectPowerOfTen(val: number): { divider: number, unit: string } {
const powerOfTen = { const powerOfTen = {
exa: Math.pow(10, 18), exa: Math.pow(10, 18),
peta: Math.pow(10, 15), peta: Math.pow(10, 15),
terra: Math.pow(10, 12), tera: Math.pow(10, 12),
giga: Math.pow(10, 9), giga: Math.pow(10, 9),
mega: Math.pow(10, 6), mega: Math.pow(10, 6),
kilo: Math.pow(10, 3), kilo: Math.pow(10, 3),
}; };
let selectedPowerOfTen; let selectedPowerOfTen: { divider: number, unit: string };
if (val < powerOfTen.kilo) { if (val < powerOfTen.kilo) {
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
} else if (val < powerOfTen.mega) { } else if (val < powerOfTen.mega) {
selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' }; selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' };
} else if (val < powerOfTen.giga) { } else if (val < powerOfTen.giga) {
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' }; selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
} else if (val < powerOfTen.terra) { } else if (val < powerOfTen.tera) {
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' }; selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
} else if (val < powerOfTen.peta) { } else if (val < powerOfTen.peta) {
selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' }; selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' };
} else if (val < powerOfTen.exa) { } else if (val < powerOfTen.exa) {
selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' }; selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
} else { } else {

View File

@ -8,8 +8,8 @@
</div> </div>
</ng-template> </ng-template>
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
<img *ngIf="!officialMempoolSpace" src="/resources/mempool-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo"> <img *ngIf="!officialMempoolSpace" src="/resources/mempool-logo.png" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 140px; height: 35px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images> <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
<div class="connection-badge"> <div class="connection-badge">
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div> <div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div> <div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>

View File

@ -78,6 +78,7 @@ li.nav-item {
.navbar-brand { .navbar-brand {
position: relative; position: relative;
height: 65px;
} }
nav { nav {
@ -86,7 +87,7 @@ nav {
.connection-badge { .connection-badge {
position: absolute; position: absolute;
top: 13px; top: 22px;
width: 100%; width: 100%;
} }
@ -150,6 +151,7 @@ nav {
width: 140px; width: 140px;
margin-right: 15px; margin-right: 15px;
text-align: center; text-align: center;
align-self: center;
} }
.logo-holder { .logo-holder {
@ -161,3 +163,9 @@ nav {
flex-direction: row; flex-direction: row;
display: flex; display: flex;
} }
.mempool-logo, app-svg-images {
align-self: center;
width: 140px;
height: 35px;
}

View File

@ -1,12 +1,12 @@
<span *ngIf="segwitGains.realizedBech32Gains && !segwitGains.potentialBech32Gains; else segwitTwo" class="badge badge-success mr-1" i18n-ngbTooltip="ngbTooltip about segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedBech32Gains * 100 | number: '1.0-0' }}% on fees by using native SegWit-Bech32" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span> <span *ngIf="segwitGains.realizedSegwitGains && !segwitGains.potentialSegwitGains; else segwitTwo" class="badge badge-success mr-1" i18n-ngbTooltip="ngbTooltip about segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedSegwitGains * 100 | number: '1.0-0' }}% on fees by using native SegWit" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
<ng-template #segwitTwo> <ng-template #segwitTwo>
<span *ngIf="segwitGains.realizedBech32Gains && segwitGains.potentialBech32Gains; else potentialP2shGains" class="badge badge-warning mr-1" i18n-ngbTooltip="ngbTooltip about double segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedBech32Gains * 100 | number: '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit-Bech32" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span> <span *ngIf="segwitGains.realizedSegwitGains && segwitGains.potentialSegwitGains; else potentialP2shSegwitGains" class="badge badge-warning mr-1" i18n-ngbTooltip="ngbTooltip about double segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedSegwitGains * 100 | number: '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialSegwitGains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
<ng-template #potentialP2shGains> <ng-template #potentialP2shSegwitGains>
<span *ngIf="segwitGains.potentialP2shGains" class="badge badge-danger mr-1" i18n-ngbTooltip="ngbTooltip about missed out gains" ngbTooltip="This transaction could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit-Bech32 or {{ segwitGains.potentialP2shGains * 100 | number: '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del i18n="tx-features.tag.segwit|SegWit">SegWit</del></span> <span *ngIf="segwitGains.potentialP2shSegwitGains" class="badge badge-danger mr-1" i18n-ngbTooltip="ngbTooltip about missed out gains" ngbTooltip="This transaction could save {{ segwitGains.potentialSegwitGains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit or {{ segwitGains.potentialP2shSegwitGains * 100 | number: '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del i18n="tx-features.tag.segwit|SegWit">SegWit</del></span>
</ng-template> </ng-template>
</ng-template> </ng-template>
<span *ngIf="segwitGains.realizedTaprootGains && !segwitGains.potentialTaprootGains; else notFullyTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about privacy and fees saved with taproot" ngbTooltip="This transaction uses Taproot and the user's thereby increased privacy and saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span> <span *ngIf="segwitGains.realizedTaprootGains && !segwitGains.potentialTaprootGains; else notFullyTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about privacy and fees saved with taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy and saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
<ng-template #notFullyTaproot> <ng-template #notFullyTaproot>
<span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about privacy and more fees that could be saved with more taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy and already saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees, but could save an additional {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% by fully using Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span> <span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about privacy and more fees that could be saved with more taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy and already saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees, but could save an additional {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% by fully using Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
<ng-template #noTaproot> <ng-template #noTaproot>

View File

@ -12,9 +12,9 @@ export class TxFeaturesComponent implements OnChanges {
@Input() tx: Transaction; @Input() tx: Transaction;
segwitGains = { segwitGains = {
realizedBech32Gains: 0, realizedSegwitGains: 0,
potentialBech32Gains: 0, potentialSegwitGains: 0,
potentialP2shGains: 0, potentialP2shSegwitGains: 0,
potentialTaprootGains: 0, potentialTaprootGains: 0,
realizedTaprootGains: 0 realizedTaprootGains: 0
}; };

View File

@ -169,9 +169,6 @@ export class NodeStatisticsChartComponent implements OnInit {
}, },
yAxis: data.channels.length === 0 ? undefined : [ yAxis: data.channels.length === 0 ? undefined : [
{ {
min: (value) => {
return value.min * 0.9;
},
type: 'value', type: 'value',
axisLabel: { axisLabel: {
color: 'rgb(110, 112, 121)', color: 'rgb(110, 112, 121)',
@ -188,9 +185,6 @@ export class NodeStatisticsChartComponent implements OnInit {
}, },
}, },
{ {
min: (value) => {
return value.min * 0.9;
},
type: 'value', type: 'value',
position: 'right', position: 'right',
axisLabel: { axisLabel: {
@ -225,15 +219,6 @@ export class NodeStatisticsChartComponent implements OnInit {
opacity: 1, opacity: 1,
width: 1, width: 1,
}, },
data: [{
yAxis: 1,
label: {
position: 'end',
show: true,
color: '#ffffff',
formatter: `1 MB`
}
}],
} }
}, },
{ {

View File

@ -43,11 +43,23 @@
</tr> </tr>
<tr *ngIf="node.country && node.city && node.subdivision"> <tr *ngIf="node.country && node.city && node.subdivision">
<td i18n="location">Location</td> <td i18n="location">Location</td>
<td>{{ node.city.en }}, {{ node.subdivision.en }}<br>{{ node.country.en }}</td> <td>
<span>{{ node.city.en }}, {{ node.subdivision.en }}</span>
<br>
<a class="d-flex align-items-center" [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
<span class="link">{{ node.country.en }}</span>
&nbsp;
<span class="flag">{{ node.flag }}</span>
</a>
</td>
</tr> </tr>
<tr *ngIf="node.country && !node.city"> <tr *ngIf="node.country && !node.city">
<td i18n="location">Location</td> <td i18n="location">Location</td>
<td>{{ node.country.en }}</td> <td>
<a [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
{{ node.country.en }} {{ node.flag }}
</a>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -77,7 +89,9 @@
<tr *ngIf="node.country"> <tr *ngIf="node.country">
<td i18n="isp">ISP</td> <td i18n="isp">ISP</td>
<td> <td>
<a [routerLink]="['/lightning/nodes/isp' | relativeUrl, node.as_number]">
{{ node.as_organization }} [ASN {{node.as_number}}] {{ node.as_organization }} [ASN {{node.as_number}}]
</a>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -126,13 +140,8 @@
<div class="d-flex justify-content-between" *ngIf="!error"> <div class="d-flex justify-content-between" *ngIf="!error">
<h2>Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})</h2> <h2>Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})</h2>
<div class="d-flex align-items-center justify-content-end"> <div class="d-flex justify-content-end">
<span style="margin-bottom: 0.5rem">List</span>&nbsp; <app-toggle [textLeft]="'List'" [textRight]="'Map'" (toggleStatusChanged)="channelsListModeChange($event)"></app-toggle>
<label class="switch">
<input type="checkbox" (change)="channelsListModeChange($event)">
<span class="slider round"></span>
</label>
&nbsp;<span style="margin-bottom: 0.5rem">Map</span>
</div> </div>
</div> </div>

View File

@ -57,66 +57,3 @@ app-fiat {
margin-left: 10px; margin-left: 10px;
} }
} }
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 30px;
height: 17px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 13px;
width: 13px;
left: 2px;
bottom: 2px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(13px);
-ms-transform: translateX(13px);
transform: translateX(13px);
}
/* Rounded sliders */
.slider.round {
border-radius: 17px;
}
.slider.round:before {
border-radius: 50%;
}

View File

@ -3,6 +3,7 @@ import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators'; import { catchError, map, switchMap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
import { LightningApiService } from '../lightning-api.service'; import { LightningApiService } from '../lightning-api.service';
@Component({ @Component({
@ -51,6 +52,7 @@ export class NodeComponent implements OnInit {
} else if (socket.indexOf('onion') > -1) { } else if (socket.indexOf('onion') > -1) {
label = 'Tor'; label = 'Tor';
} }
node.flag = getFlagEmoji(node.iso_code);
socketsObject.push({ socketsObject.push({
label: label, label: label,
socket: node.public_key + '@' + socket, socket: node.public_key + '@' + socket,
@ -73,8 +75,8 @@ export class NodeComponent implements OnInit {
this.selectedSocketIndex = index; this.selectedSocketIndex = index;
} }
channelsListModeChange(e) { channelsListModeChange(toggle) {
if (e.target.checked === true) { if (toggle === true) {
this.channelsListMode = 'map'; this.channelsListMode = 'map';
} else { } else {
this.channelsListMode = 'list'; this.channelsListMode = 'list';

View File

@ -59,7 +59,7 @@ export class NodesNetworksChartComponent implements OnInit {
let firstRun = true; let firstRun = true;
if (this.widget) { if (this.widget) {
this.miningWindowPreference = '1y'; this.miningWindowPreference = '3y';
} else { } else {
this.seoService.setTitle($localize`Lightning nodes per network`); this.seoService.setTitle($localize`Lightning nodes per network`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('all'); this.miningWindowPreference = this.miningService.getDefaultTimespan('all');

View File

@ -7,7 +7,9 @@
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon> <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button> </button>
</div> </div>
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small> <small class="d-block" style="color: #ffffff66; min-height: 25px" i18n="lightning.tor-nodes-excluded">
<span *ngIf="!(showTorObservable$ | async)">(Tor nodes excluded)</span>
</small>
</div> </div>
<div class="container pb-lg-0 bottom-padding"> <div class="container pb-lg-0 bottom-padding">
@ -21,6 +23,11 @@
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>
<div class="d-flex toggle">
<app-toggle [textLeft]="'Show Tor'" [textRight]="" (toggleStatusChanged)="onTorToggleStatusChanged($event)"></app-toggle>
<app-toggle [textLeft]="'Nodes'" [textRight]="'Capacity'" (toggleStatusChanged)="onGroupToggleStatusChanged($event)"></app-toggle>
</div>
<table class="table table-borderless text-center m-auto" style="max-width: 900px"> <table class="table table-borderless text-center m-auto" style="max-width: 900px">
<thead> <thead>
<tr> <tr>
@ -34,8 +41,9 @@
<tbody [attr.data-cy]="'pools-table'" *ngIf="(nodesPerAsObservable$ | async) as asList"> <tbody [attr.data-cy]="'pools-table'" *ngIf="(nodesPerAsObservable$ | async) as asList">
<tr *ngFor="let asEntry of asList"> <tr *ngFor="let asEntry of asList">
<td class="rank text-left pl-0">{{ asEntry.rank }}</td> <td class="rank text-left pl-0">{{ asEntry.rank }}</td>
<td class="name text-left text-truncate" style="max-width: 250px"> <td class="name text-left text-truncate">
<a [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a> <a *ngIf="asEntry.ispId" [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a>
<span *ngIf="!asEntry.ispId">{{ asEntry.name }}</span>
</td> </td>
<td class="share text-right">{{ asEntry.share }}%</td> <td class="share text-right">{{ asEntry.share }}%</td>
<td class="nodes text-right">{{ asEntry.count }}</td> <td class="nodes text-right">{{ asEntry.count }}</td>

View File

@ -45,7 +45,7 @@
.name { .name {
width: 25%; width: 25%;
@media (max-width: 576px) { @media (max-width: 576px) {
width: 80%; width: 70%;
max-width: 150px; max-width: 150px;
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
@ -69,7 +69,17 @@
.capacity { .capacity {
width: 20%; width: 20%;
@media (max-width: 576px) { @media (max-width: 576px) {
width: 10%; width: 20%;
max-width: 100px; max-width: 100px;
} }
} }
.toggle {
justify-content: space-between;
padding-top: 15px;
@media (min-width: 576px) {
padding-bottom: 15px;
padding-left: 105px;
padding-right: 105px;
}
}

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { EChartsOption, PieSeriesOption } from 'echarts'; import { EChartsOption, PieSeriesOption } from 'echarts';
import { map, Observable, share, tap } from 'rxjs'; import { combineLatest, map, Observable, share, Subject, switchMap, tap } from 'rxjs';
import { chartColors } from 'src/app/app.constants'; import { chartColors } from 'src/app/app.constants';
import { ApiService } from 'src/app/services/api.service'; import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
@ -17,19 +17,20 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class NodesPerISPChartComponent implements OnInit { export class NodesPerISPChartComponent implements OnInit {
miningWindowPreference: string;
isLoading = true; isLoading = true;
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
renderer: 'svg', renderer: 'svg',
}; };
timespan = ''; timespan = '';
chartInstance: any = undefined; chartInstance = undefined;
@HostBinding('attr.dir') dir = 'ltr'; @HostBinding('attr.dir') dir = 'ltr';
nodesPerAsObservable$: Observable<any>; nodesPerAsObservable$: Observable<any>;
showTorObservable$: Observable<boolean>;
groupBySubject = new Subject<boolean>();
showTorSubject = new Subject<boolean>();
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
@ -44,7 +45,14 @@ export class NodesPerISPChartComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.seoService.setTitle($localize`Lightning nodes per ISP`); this.seoService.setTitle($localize`Lightning nodes per ISP`);
this.nodesPerAsObservable$ = this.apiService.getNodesPerAs() this.showTorObservable$ = this.showTorSubject.asObservable();
this.nodesPerAsObservable$ = combineLatest([this.groupBySubject, this.showTorSubject])
.pipe(
switchMap((selectedFilters) => {
return this.apiService.getNodesPerAs(
selectedFilters[0] ? 'capacity' : 'node-count',
selectedFilters[1] // Show Tor nodes
)
.pipe( .pipe(
tap(data => { tap(data => {
this.isLoading = false; this.isLoading = false;
@ -55,12 +63,14 @@ export class NodesPerISPChartComponent implements OnInit {
data[i].rank = i + 1; data[i].rank = i + 1;
} }
return data.slice(0, 100); return data.slice(0, 100);
})
);
}), }),
share() share()
); );
} }
generateChartSerieData(as) { generateChartSerieData(as): PieSeriesOption[] {
const shareThreshold = this.isMobile() ? 2 : 0.5; const shareThreshold = this.isMobile() ? 2 : 0.5;
const data: object[] = []; const data: object[] = [];
let totalShareOther = 0; let totalShareOther = 0;
@ -78,6 +88,9 @@ export class NodesPerISPChartComponent implements OnInit {
return; return;
} }
data.push({ data.push({
itemStyle: {
color: as.ispId === null ? '#7D4698' : undefined,
},
value: as.share, value: as.share,
name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`), name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`),
label: { label: {
@ -138,14 +151,14 @@ export class NodesPerISPChartComponent implements OnInit {
return data; return data;
} }
prepareChartOptions(as) { prepareChartOptions(as): void {
let pieSize = ['20%', '80%']; // Desktop let pieSize = ['20%', '80%']; // Desktop
if (this.isMobile()) { if (this.isMobile()) {
pieSize = ['15%', '60%']; pieSize = ['15%', '60%'];
} }
this.chartOptions = { this.chartOptions = {
color: chartColors, color: chartColors.slice(3),
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
textStyle: { textStyle: {
@ -191,18 +204,18 @@ export class NodesPerISPChartComponent implements OnInit {
}; };
} }
isMobile() { isMobile(): boolean {
return (window.innerWidth <= 767.98); return (window.innerWidth <= 767.98);
} }
onChartInit(ec) { onChartInit(ec): void {
if (this.chartInstance !== undefined) { if (this.chartInstance !== undefined) {
return; return;
} }
this.chartInstance = ec; this.chartInstance = ec;
this.chartInstance.on('click', (e) => { this.chartInstance.on('click', (e) => {
if (e.data.data === 9999) { // "Other" if (e.data.data === 9999 || e.data.data === null) { // "Other" or Tor
return; return;
} }
this.zone.run(() => { this.zone.run(() => {
@ -212,7 +225,7 @@ export class NodesPerISPChartComponent implements OnInit {
}); });
} }
onSaveChart() { onSaveChart(): void {
const now = new Date(); const now = new Date();
this.chartOptions.backgroundColor = '#11131f'; this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions); this.chartInstance.setOption(this.chartOptions);
@ -224,8 +237,12 @@ export class NodesPerISPChartComponent implements OnInit {
this.chartInstance.setOption(this.chartOptions); this.chartInstance.setOption(this.chartOptions);
} }
isEllipsisActive(e) { onTorToggleStatusChanged(e): void {
return (e.offsetWidth < e.scrollWidth); this.showTorSubject.next(e);
}
onGroupToggleStatusChanged(e): void {
this.groupBySubject.next(e);
} }
} }

View File

@ -58,7 +58,7 @@ export class LightningStatisticsChartComponent implements OnInit {
let firstRun = true; let firstRun = true;
if (this.widget) { if (this.widget) {
this.miningWindowPreference = '1y'; this.miningWindowPreference = '3y';
} else { } else {
this.seoService.setTitle($localize`Channels and Capacity`); this.seoService.setTitle($localize`Channels and Capacity`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('all'); this.miningWindowPreference = this.miningService.getDefaultTimespan('all');

View File

@ -255,8 +255,9 @@ export class ApiService {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params }); return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params });
} }
getNodesPerAs(): Observable<any> { getNodesPerAs(groupBy: 'capacity' | 'node-count', showTorNodes: boolean): Observable<any> {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp'); return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp-ranking'
+ `?groupBy=${groupBy}&showTor=${showTorNodes}`);
} }
getNodeForCountry$(country: string): Observable<any> { getNodeForCountry$(country: string): Observable<any> {

View File

@ -0,0 +1,8 @@
<div class="d-flex align-items-center">
<span style="margin-bottom: 0.5rem">{{ textLeft }}</span>&nbsp;
<label class="switch">
<input type="checkbox" (change)="onToggleStatusChanged($event)">
<span class="slider round"></span>
</label>
&nbsp;<span style="margin-bottom: 0.5rem">{{ textRight }}</span>
</div>

View File

@ -0,0 +1,62 @@
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 30px;
height: 17px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 13px;
width: 13px;
left: 2px;
bottom: 2px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked+.slider {
background-color: #2196F3;
}
input:focus+.slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked+.slider:before {
-webkit-transform: translateX(13px);
-ms-transform: translateX(13px);
transform: translateX(13px);
}
/* Rounded sliders */
.slider.round {
border-radius: 17px;
}
.slider.round:before {
border-radius: 50%;
}

View File

@ -0,0 +1,21 @@
import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-toggle',
templateUrl: './toggle.component.html',
styleUrls: ['./toggle.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ToggleComponent implements AfterViewInit {
@Output() toggleStatusChanged = new EventEmitter<boolean>();
@Input() textLeft: string;
@Input() textRight: string;
ngAfterViewInit(): void {
this.toggleStatusChanged.emit(false);
}
onToggleStatusChanged(e): void {
this.toggleStatusChanged.emit(e.target.checked);
}
}

View File

@ -92,6 +92,9 @@ export function detectWebGL() {
} }
export function getFlagEmoji(countryCode) { export function getFlagEmoji(countryCode) {
if (!countryCode) {
return '';
}
const codePoints = countryCode const codePoints = countryCode
.toUpperCase() .toUpperCase()
.split('') .split('')

View File

@ -80,6 +80,7 @@ import { ChangeComponent } from '../components/change/change.component';
import { SatsComponent } from './components/sats/sats.component'; import { SatsComponent } from './components/sats/sats.component';
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component'; import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
import { TimestampComponent } from './components/timestamp/timestamp.component'; import { TimestampComponent } from './components/timestamp/timestamp.component';
import { ToggleComponent } from './components/toggle/toggle.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -154,6 +155,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
SatsComponent, SatsComponent,
SearchResultsComponent, SearchResultsComponent,
TimestampComponent, TimestampComponent,
ToggleComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -255,6 +257,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
SatsComponent, SatsComponent,
SearchResultsComponent, SearchResultsComponent,
TimestampComponent, TimestampComponent,
ToggleComponent,
] ]
}) })
export class SharedModule { export class SharedModule {

View File

@ -18,12 +18,18 @@ whitelist=2401:b140::/32
#uacomment=@wiz #uacomment=@wiz
[main] [main]
bind=0.0.0.0:8333
bind=[::]:8333
rpcbind=127.0.0.1:8332 rpcbind=127.0.0.1:8332
rpcbind=[::1]:8332 rpcbind=[::1]:8332
zmqpubrawblock=tcp://127.0.0.1:18332 bind=0.0.0.0:8333
zmqpubrawtx=tcp://127.0.0.1:18333 bind=[::]:8333
zmqpubrawblock=tcp://127.0.0.1:8334
zmqpubrawtx=tcp://127.0.0.1:8335
#addnode=[2401:b140:1::92:201]:8333
#addnode=[2401:b140:1::92:202]:8333
#addnode=[2401:b140:1::92:203]:8333
#addnode=[2401:b140:1::92:204]:8333
#addnode=[2401:b140:1::92:205]:8333
#addnode=[2401:b140:1::92:206]:8333
#addnode=[2401:b140:2::92:201]:8333 #addnode=[2401:b140:2::92:201]:8333
#addnode=[2401:b140:2::92:202]:8333 #addnode=[2401:b140:2::92:202]:8333
#addnode=[2401:b140:2::92:203]:8333 #addnode=[2401:b140:2::92:203]:8333
@ -33,10 +39,18 @@ zmqpubrawtx=tcp://127.0.0.1:18333
[test] [test]
daemon=1 daemon=1
bind=0.0.0.0:18333
bind=[::]:18333
rpcbind=127.0.0.1:18332 rpcbind=127.0.0.1:18332
rpcbind=[::1]:18332 rpcbind=[::1]:18332
bind=0.0.0.0:18333
bind=[::]:18333
zmqpubrawblock=tcp://127.0.0.1:18334
zmqpubrawtx=tcp://127.0.0.1:18335
#addnode=[2401:b140:1::92:201]:18333
#addnode=[2401:b140:1::92:202]:18333
#addnode=[2401:b140:1::92:203]:18333
#addnode=[2401:b140:1::92:204]:18333
#addnode=[2401:b140:1::92:205]:18333
#addnode=[2401:b140:1::92:206]:18333
#addnode=[2401:b140:2::92:201]:18333 #addnode=[2401:b140:2::92:201]:18333
#addnode=[2401:b140:2::92:202]:18333 #addnode=[2401:b140:2::92:202]:18333
#addnode=[2401:b140:2::92:203]:18333 #addnode=[2401:b140:2::92:203]:18333
@ -46,10 +60,18 @@ rpcbind=[::1]:18332
[signet] [signet]
daemon=1 daemon=1
bind=0.0.0.0:38333
bind=[::]:38333
rpcbind=127.0.0.1:38332 rpcbind=127.0.0.1:38332
rpcbind=[::1]:38332 rpcbind=[::1]:38332
bind=0.0.0.0:38333
bind=[::]:38333
zmqpubrawblock=tcp://127.0.0.1:38334
zmqpubrawtx=tcp://127.0.0.1:38335
#addnode=[2401:b140:1::92:201]:38333
#addnode=[2401:b140:1::92:202]:38333
#addnode=[2401:b140:1::92:203]:38333
#addnode=[2401:b140:1::92:204]:38333
#addnode=[2401:b140:1::92:205]:38333
#addnode=[2401:b140:1::92:206]:38333
#addnode=[2401:b140:2::92:201]:38333 #addnode=[2401:b140:2::92:201]:38333
#addnode=[2401:b140:2::92:202]:38333 #addnode=[2401:b140:2::92:202]:38333
#addnode=[2401:b140:2::92:203]:38333 #addnode=[2401:b140:2::92:203]:38333

View File

@ -218,6 +218,21 @@ MYSQL_HOME=/mysql
MYSQL_USER=mysql MYSQL_USER=mysql
MYSQL_GROUP=mysql MYSQL_GROUP=mysql
# mempool mysql user/password
MEMPOOL_MAINNET_USER='mempool'
MEMPOOL_TESTNET_USER='mempool_testnet'
MEMPOOL_SIGNET_USER='mempool_signet'
MEMPOOL_LIQUID_USER='mempool_liquid'
MEMPOOL_LIQUIDTESTNET_USER='mempool_liquidtestnet'
MEMPOOL_BISQ_USER='mempool_bisq'
# generate random hex string
MEMPOOL_MAINNET_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_LIQUID_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 data folder and user/group # mempool data folder and user/group
MEMPOOL_HOME=/mempool MEMPOOL_HOME=/mempool
MEMPOOL_USER=mempool MEMPOOL_USER=mempool
@ -1513,22 +1528,38 @@ esac
mysql << _EOF_ mysql << _EOF_
create database mempool; create database mempool;
grant all on mempool.* to 'mempool'@'localhost' identified by 'mempool'; grant all on mempool.* to '${MEMPOOL_MAINNET_USER}'@'localhost' identified by '${MEMPOOL_MAINNET_PASS}';
create database mempool_testnet; create database mempool_testnet;
grant all on mempool_testnet.* to 'mempool_testnet'@'localhost' identified by 'mempool_testnet'; grant all on mempool_testnet.* to '${MEMPOOL_TESTNET_USER}'@'localhost' identified by '${MEMPOOL_TESTNET_PASS}';
create database mempool_signet; create database mempool_signet;
grant all on mempool_signet.* to 'mempool_signet'@'localhost' identified by 'mempool_signet'; grant all on mempool_signet.* to '${MEMPOOL_SIGNET_USER}'@'localhost' identified by '${MEMPOOL_SIGNET_PASS}';
create database mempool_liquid; create database mempool_liquid;
grant all on mempool_liquid.* to 'mempool_liquid'@'localhost' identified by 'mempool_liquid'; grant all on mempool_liquid.* to '${MEMPOOL_LIQUID_USER}'@'localhost' identified by '${MEMPOOL_LIQUID_PASS}';
create database mempool_liquidtestnet; create database mempool_liquidtestnet;
grant all on mempool_liquidtestnet.* to 'mempool_liquidtestnet'@'localhost' identified by 'mempool_liquidtestnet'; grant all on mempool_liquidtestnet.* to '${MEMPOOL_LIQUIDTESTNET_USER}'@'localhost' identified by '${MEMPOOL_LIQUIDTESTNET_PASS}';
create database mempool_bisq; create database mempool_bisq;
grant all on mempool_bisq.* to 'mempool_bisq'@'localhost' identified by 'mempool_bisq'; grant all on mempool_bisq.* to '${MEMPOOL_BISQ_USER}'@'localhost' identified by '${MEMPOOL_BISQ_PASS}';
_EOF_
echo "[*] save MySQL credentials"
cat > ${MEMPOOL_HOME}/mysql_credentials << _EOF_
declare -x MEMPOOL_MAINNET_USER="${MEMPOOL_MAINNET_USER}"
declare -x MEMPOOL_MAINNET_PASS="${MEMPOOL_MAINNET_PASS}"
declare -x MEMPOOL_TESTNET_USER="${MEMPOOL_TESTNET_USER}"
declare -x MEMPOOL_TESTNET_PASS="${MEMPOOL_TESTNET_PASS}"
declare -x MEMPOOL_SIGNET_USER="${MEMPOOL_SIGNET_USER}"
declare -x MEMPOOL_SIGNET_PASS="${MEMPOOL_SIGNET_PASS}"
declare -x MEMPOOL_LIQUID_USER="${MEMPOOL_LIQUID_USER}"
declare -x MEMPOOL_LIQUID_PASS="${MEMPOOL_LIQUID_PASS}"
declare -x MEMPOOL_LIQUIDTESTNET_USER="${MEMPOOL_LIQUIDTESTNET_USER}"
declare -x MEMPOOL_LIQUIDTESTNET_PASS="${MEMPOOL_LIQUIDTESTNET_PASS}"
declare -x MEMPOOL_BISQ_USER="${MEMPOOL_BISQ_USER}"
declare -x MEMPOOL_BISQ_PASS="${MEMPOOL_BISQ_PASS}"
_EOF_ _EOF_
##### nginx ##### nginx

View File

@ -11,6 +11,9 @@ BITCOIN_RPC_PASS=$(grep '^rpcpassword' /bitcoin/bitcoin.conf | cut -d '=' -f2)
ELEMENTS_RPC_USER=$(grep '^rpcuser' /elements/elements.conf | cut -d '=' -f2) 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
. /mempool/mysql_credentials
if [ -f "${LOCKFILE}" ];then if [ -f "${LOCKFILE}" ];then
echo "upgrade already running? check lockfile ${LOCKFILE}" echo "upgrade already running? check lockfile ${LOCKFILE}"
exit 1 exit 1
@ -73,6 +76,18 @@ build_backend()
-e "s!__BITCOIN_RPC_PASS__!${BITCOIN_RPC_PASS}!" \ -e "s!__BITCOIN_RPC_PASS__!${BITCOIN_RPC_PASS}!" \
-e "s!__ELEMENTS_RPC_USER__!${ELEMENTS_RPC_USER}!" \ -e "s!__ELEMENTS_RPC_USER__!${ELEMENTS_RPC_USER}!" \
-e "s!__ELEMENTS_RPC_PASS__!${ELEMENTS_RPC_PASS}!" \ -e "s!__ELEMENTS_RPC_PASS__!${ELEMENTS_RPC_PASS}!" \
-e "s!__MEMPOOL_MAINNET_USER__!${MEMPOOL_MAINNET_USER}!" \
-e "s!__MEMPOOL_MAINNET_PASS__!${MEMPOOL_MAINNET_PASS}!" \
-e "s!__MEMPOOL_TESTNET_USER__!${MEMPOOL_TESTNET_USER}!" \
-e "s!__MEMPOOL_TESTNET_PASS__!${MEMPOOL_TESTNET_PASS}!" \
-e "s!__MEMPOOL_SIGNET_USER__!${MEMPOOL_SIGNET_USER}!" \
-e "s!__MEMPOOL_SIGNET_PASS__!${MEMPOOL_SIGNET_PASS}!" \
-e "s!__MEMPOOL_LIQUID_USER__!${MEMPOOL_LIQUID_USER}!" \
-e "s!__MEMPOOL_LIQUID_PASS__!${MEMPOOL_LIQUID_PASS}!" \
-e "s!__MEMPOOL_LIQUIDTESTNET_USER__!${LIQUIDTESTNET_USER}!" \
-e "s!__MEMPOOL_LIQUIDTESTNET_PASS__!${MEMPOOL_LIQUIDTESTNET_PASS}!" \
-e "s!__MEMPOOL_BISQ_USER__!${MEMPOOL_BISQ_USER}!" \
-e "s!__MEMPOOL_BISQ_PASS__!${MEMPOOL_BISQ_PASS}!" \
"mempool-config.json" "mempool-config.json"
fi fi
npm install --omit=dev --omit=optional || exit 1 npm install --omit=dev --omit=optional || exit 1

View File

@ -21,8 +21,8 @@
"ENABLED": false, "ENABLED": false,
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 3306, "PORT": 3306,
"USERNAME": "mempool_bisq", "USERNAME": "__MEMPOOL_BISQ_USER__",
"PASSWORD": "mempool_bisq", "PASSWORD": "__MEMPOOL_BISQ_PASS__",
"DATABASE": "mempool_bisq" "DATABASE": "mempool_bisq"
}, },
"STATISTICS": { "STATISTICS": {

View File

@ -28,8 +28,8 @@
"ENABLED": true, "ENABLED": true,
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 3306, "PORT": 3306,
"USERNAME": "mempool_liquid", "USERNAME": "__MEMPOOL_LIQUID_USER__",
"PASSWORD": "mempool_liquid", "PASSWORD": "__MEMPOOL_LIQUID_PASS__",
"DATABASE": "mempool_liquid" "DATABASE": "mempool_liquid"
}, },
"STATISTICS": { "STATISTICS": {

View File

@ -28,8 +28,8 @@
"ENABLED": true, "ENABLED": true,
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 3306, "PORT": 3306,
"USERNAME": "mempool_liquidtestnet", "USERNAME": "__MEMPOOL_LIQUIDTESTNET_USER__",
"PASSWORD": "mempool_liquidtestnet", "PASSWORD": "__MEMPOOL_LIQUIDTESTNET_PASS__",
"DATABASE": "mempool_liquidtestnet" "DATABASE": "mempool_liquidtestnet"
}, },
"STATISTICS": { "STATISTICS": {

View File

@ -32,8 +32,8 @@
"ENABLED": true, "ENABLED": true,
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 3306, "PORT": 3306,
"USERNAME": "mempool", "USERNAME": "__MEMPOOL_MAINNET_USER__",
"PASSWORD": "mempool", "PASSWORD": "__MEMPOOL_MAINNET_PASS__",
"DATABASE": "mempool" "DATABASE": "mempool"
}, },
"STATISTICS": { "STATISTICS": {

View File

@ -24,8 +24,8 @@
"ENABLED": true, "ENABLED": true,
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 3306, "PORT": 3306,
"USERNAME": "mempool_signet", "USERNAME": "__MEMPOOL_SIGNET_USER__",
"PASSWORD": "mempool_signet", "PASSWORD": "__MEMPOOL_SIGNET_PASS__",
"DATABASE": "mempool_signet" "DATABASE": "mempool_signet"
}, },
"STATISTICS": { "STATISTICS": {

View File

@ -24,8 +24,8 @@
"ENABLED": true, "ENABLED": true,
"HOST": "127.0.0.1", "HOST": "127.0.0.1",
"PORT": 3306, "PORT": 3306,
"USERNAME": "mempool_testnet", "USERNAME": "__MEMPOOL_TESTNET_USER__",
"PASSWORD": "mempool_testnet", "PASSWORD": "__MEMPOOL_TESTNET_PASS__",
"DATABASE": "mempool_testnet" "DATABASE": "mempool_testnet"
}, },
"STATISTICS": { "STATISTICS": {

View File

@ -9,6 +9,22 @@ Some additional server configuration is required to properly route requests (see
## Setup ## Setup
### 0. Install deps
For Linux, in addition to NodeJS/npm you'll need at least:
* nginx
* cups
* chromium-bsu
* libatk1.0
* libatk-bridge2.0
* libxkbcommon-dev
* libxcomposite-dev
* libxdamage-dev
* libxrandr-dev
* libgbm-dev
* libpango1.0-dev
* libasound-dev
### 1. Clone Mempool Repository ### 1. Clone Mempool Repository
Get the latest Mempool code: Get the latest Mempool code:

View File

@ -4,7 +4,7 @@
"description": "Renderer for mempool open graph link preview images", "description": "Renderer for mempool open graph link preview images",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/mononaut/mempool-unfurl" "url": "git+https://github.com/mempool/mempool"
}, },
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {