diff --git a/backend/.eslintrc b/backend/.eslintrc index d8f453c51..3029ebab6 100644 --- a/backend/.eslintrc +++ b/backend/.eslintrc @@ -15,10 +15,11 @@ "@typescript-eslint/ban-types": 1, "@typescript-eslint/no-empty-function": 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-this-alias": 1, "@typescript-eslint/no-var-requires": 1, + "@typescript-eslint/explicit-function-return-type": 1, "no-console": 1, "no-constant-condition": 1, "no-dupe-else-if": 1, @@ -28,6 +29,8 @@ "no-useless-catch": 1, "no-var": 1, "prefer-const": 1, - "prefer-rest-params": 1 + "prefer-rest-params": 1, + "quotes": [1, "single", { "allowTemplateLiterals": true }], + "semi": 1 } } diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index f3f7011dd..358bd29e4 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -9,6 +9,7 @@ export interface AbstractBitcoinApi { $getBlockHash(height: number): Promise; $getBlockHeader(hash: string): Promise; $getBlock(hash: string): Promise; + $getRawBlock(hash: string): Promise; $getAddress(address: string): Promise; $getAddressTransactions(address: string, lastSeenTxId: string): Promise; $getAddressPrefix(prefix: string): string[]; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 3152954c1..ebde5cc07 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -77,7 +77,8 @@ class BitcoinApi implements AbstractBitcoinApi { } $getRawBlock(hash: string): Promise { - return this.bitcoindClient.getBlock(hash, 0); + return this.bitcoindClient.getBlock(hash, 0) + .then((raw: string) => Buffer.from(raw, "hex")); } $getBlockHash(height: number): Promise { diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index a04a78117..66bcb2569 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -103,9 +103,10 @@ class BitcoinRoutes { .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/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/: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 + 'address/:address', this.getAddress) .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) { try { const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index e8eee343a..ebaf2f6a0 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -50,6 +50,11 @@ class ElectrsApi implements AbstractBitcoinApi { .then((response) => response.data); } + $getRawBlock(hash: string): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", this.axiosConfig) + .then((response) => response.data); + } + $getAddress(address: string): Promise { throw new Error('Method getAddress not implemented.'); } diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index d9be6e1e7..d26bfd6cc 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -9,8 +9,8 @@ class DatabaseMigration { private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; - private blocksTruncatedMessage = `'blocks' table has been truncated. Re-indexing from scratch.`; - private hashratesTruncatedMessage = `'hashrates' table has been truncated. Re-indexing from scratch.`; + private blocksTruncatedMessage = `'blocks' table has been truncated.`; + private hashratesTruncatedMessage = `'hashrates' table has been truncated.`; /** * Avoid printing multiple time the same message @@ -256,7 +256,9 @@ class DatabaseMigration { } 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('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"'); @@ -273,6 +275,9 @@ class DatabaseMigration { } 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 node_stats`); await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`); diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index ec8ee35fb..55b0ba5cb 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -5,25 +5,26 @@ class NodesApi { public async $getNode(public_key: string): Promise { try { 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, (SELECT Count(*) FROM channels WHERE channels.status = 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_closed_count, (SELECT Count(*) 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) 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) 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 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_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_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' 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]); @@ -97,29 +98,59 @@ class NodesApi { } } - public async $getNodesISP() { + public async $getNodesISP(groupBy: string, showTor: boolean) { 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 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 GROUP BY geo_names.names - ORDER BY COUNT(DISTINCT nodes.public_key) DESC - `; + ORDER BY ${orderBy} DESC + `; const [nodesCountPerAS]: any = await DB.query(query); - query = `SELECT COUNT(*) as total FROM nodes WHERE as_number IS NOT NULL`; - const [nodesWithAS]: any = await DB.query(query); - + let total = 0; 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) { nodesPerAs.push({ ispId: as.ispId, name: JSON.parse(as.names), 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, - }) + }); } return nodesPerAs; diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index bbc8efb5a..83e3c393e 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -9,10 +9,10 @@ class NodesRoutes { public initRoutes(app: Application) { app .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/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/:public_key/statistics', this.$getHistoricalNodeStats) .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 { 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('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); diff --git a/backend/src/api/explorer/statistics.api.ts b/backend/src/api/explorer/statistics.api.ts index d29bf1ed4..c2e23848f 100644 --- a/backend/src/api/explorer/statistics.api.ts +++ b/backend/src/api/explorer/statistics.api.ts @@ -13,7 +13,7 @@ class StatisticsApi { query += ` WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; } - query += ` ORDER BY id DESC`; + query += ` ORDER BY added DESC`; try { const [rows]: any = await DB.query(query); @@ -26,8 +26,8 @@ class StatisticsApi { public async $getLatestStatistics(): Promise { try { - const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1`); - const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1 OFFSET 7`); + 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`); return { latest: rows[0], previous: rows2[0], diff --git a/backend/src/tasks/lightning/node-sync.service.ts b/backend/src/tasks/lightning/node-sync.service.ts index 3b2eb18e2..f45473aba 100644 --- a/backend/src/tasks/lightning/node-sync.service.ts +++ b/backend/src/tasks/lightning/node-sync.service.ts @@ -38,11 +38,13 @@ class NodeSyncService { await $lookupNodeLocation(); } - await this.$setChannelsInactive(); - + const graphChannelsIds: string[] = []; for (const channel of networkGraph.channels) { await this.$saveChannel(channel); + graphChannelsIds.push(channel.id); } + await this.$setChannelsInactive(graphChannelsIds); + logger.info(`Channels updated.`); await this.$findInactiveNodesAndChannels(); @@ -106,7 +108,22 @@ class NodeSyncService { try { // @ts-ignore - const [channels]: [ILightningApi.Channel[]] = await DB.query(`SELECT channels.id FROM channels WHERE channels.status = 1 AND ((SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node1_public_key) = 0 OR (SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node2_public_key) = 0)`); + 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) { await this.$updateChannelStatus(channel.id, 0); @@ -356,9 +373,16 @@ class NodeSyncService { } } - private async $setChannelsInactive(): Promise { + private async $setChannelsInactive(graphChannelsIds: string[]): Promise { 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) { logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e)); } diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index c56e8a015..f30da9e96 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -141,7 +141,22 @@ class LightningStatsUpdater { try { logger.info(`Running daily node stats update...`); - const query = `SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left, c2.channels_capacity_right FROM nodes LEFT JOIN (SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left FROM channels WHERE channels.status < 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); for (const node of nodes) { diff --git a/contributors/oleonardolima.txt b/contributors/oleonardolima.txt new file mode 100644 index 000000000..79adbcb78 --- /dev/null +++ b/contributors/oleonardolima.txt @@ -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 diff --git a/frontend/.eslintrc b/frontend/.eslintrc index d0fce56f0..4dbcf98d9 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -14,10 +14,11 @@ "@typescript-eslint/ban-types": 1, "@typescript-eslint/no-empty-function": 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-this-alias": 1, "@typescript-eslint/no-var-requires": 1, + "@typescript-eslint/explicit-function-return-type": 1, "no-case-declarations": 1, "no-console": 1, "no-constant-condition": 1, @@ -29,6 +30,8 @@ "no-useless-catch": 1, "no-var": 1, "prefer-const": 1, - "prefer-rest-params": 1 + "prefer-rest-params": 1, + "quotes": [1, "single", { "allowTemplateLiterals": true }], + "semi": 1 } } diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index b2ea1d7a9..25432565c 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -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) { // calculated in weight units - let realizedBech32Gains = 0; - let potentialBech32Gains = 0; - let potentialP2shGains = 0; + let realizedSegwitGains = 0; + let potentialSegwitGains = 0; + let potentialP2shSegwitGains = 0; let potentialTaprootGains = 0; let realizedTaprootGains = 0; @@ -24,31 +24,33 @@ export function calcSegwitFeeGains(tx: Transaction) { const isP2tr = vin.prevout.scriptpubkey_type === 'v1_p2tr'; const op = vin.scriptsig ? vin.scriptsig_asm.split(' ')[0] : null; - const isP2sh2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22'; - const isP2sh2Wsh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34'; + const isP2shP2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22'; + const isP2shP2Wsh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34'; switch (true) { - // Native Segwit - P2WPKH/P2WSH (Bech32) + // Native Segwit - P2WPKH/P2WSH/P2TR case isP2wpkh: case isP2wsh: case isP2tr: // 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? break; // Backward compatible Segwit - P2SH-P2WPKH - case isP2sh2Wpkh: - // the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (48 WU) - realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST; - potentialBech32Gains += P2SH_P2WPKH_COST; + case isP2shP2Wpkh: + // the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (84 WU) + realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST; + potentialSegwitGains += P2SH_P2WPKH_COST; break; // Backward compatible Segwit - P2SH-P2WSH - case isP2sh2Wsh: - // the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes - realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WSH_COST; - potentialBech32Gains += P2SH_P2WSH_COST; + case isP2shP2Wsh: + // the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes (140 WU) + realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WSH_COST; + potentialSegwitGains += P2SH_P2WSH_COST; break; // Non-segwit P2PKH/P2SH/P2PK/bare multisig @@ -56,9 +58,13 @@ export function calcSegwitFeeGains(tx: Transaction) { case isP2sh: case isP2pk: case isBareMultisig: { - const fullGains = scriptSigSize(vin) * 3; - potentialBech32Gains += fullGains; - potentialP2shGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST); + let fullGains = scriptSigSize(vin) * 3; + if (isBareMultisig) { + // 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; } } @@ -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 } } 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; if ( // single sig - isP2pk || isP2pkh || isP2wpkh || isP2sh2Wpkh || + isP2pk || isP2pkh || isP2wpkh || isP2shP2Wpkh || // multisig isBareMultisig || parseMultisigScript(script) ) { @@ -105,11 +111,11 @@ export function calcSegwitFeeGains(tx: Transaction) { // returned as percentage of the total tx weight return { - realizedBech32Gains: realizedBech32Gains / (tx.weight + realizedBech32Gains), // percent of the pre-segwit tx size - potentialBech32Gains: potentialBech32Gains / tx.weight, - potentialP2shGains: potentialP2shGains / tx.weight, + realizedSegwitGains: realizedSegwitGains / (tx.weight + realizedSegwitGains), // percent of the pre-segwit tx size + potentialSegwitGains: potentialSegwitGains / tx.weight, + potentialP2shSegwitGains: potentialP2shSegwitGains / 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 : ''); } -function zeros(n) { +function zeros(n: number) { return new Array(n + 1).join('0'); } // 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('.'); // 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; // Power of ten wrapper -export function selectPowerOfTen(val: number) { +export function selectPowerOfTen(val: number): { divider: number, unit: string } { const powerOfTen = { exa: Math.pow(10, 18), peta: Math.pow(10, 15), - terra: Math.pow(10, 12), + tera: Math.pow(10, 12), giga: Math.pow(10, 9), mega: Math.pow(10, 6), kilo: Math.pow(10, 3), }; - let selectedPowerOfTen; + let selectedPowerOfTen: { divider: number, unit: string }; if (val < powerOfTen.kilo) { selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling } else if (val < powerOfTen.mega) { selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' }; } else if (val < powerOfTen.giga) { selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' }; - } else if (val < powerOfTen.terra) { + } else if (val < powerOfTen.tera) { selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' }; } else if (val < powerOfTen.peta) { - selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' }; + selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' }; } else if (val < powerOfTen.exa) { selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' }; } else { diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 7be41f233..f152cb7b3 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -8,8 +8,8 @@ - - + +
Offline
Reconnecting...
diff --git a/frontend/src/app/components/master-page/master-page.component.scss b/frontend/src/app/components/master-page/master-page.component.scss index f251215fb..c6a9aaeff 100644 --- a/frontend/src/app/components/master-page/master-page.component.scss +++ b/frontend/src/app/components/master-page/master-page.component.scss @@ -78,6 +78,7 @@ li.nav-item { .navbar-brand { position: relative; + height: 65px; } nav { @@ -86,7 +87,7 @@ nav { .connection-badge { position: absolute; - top: 13px; + top: 22px; width: 100%; } @@ -150,6 +151,7 @@ nav { width: 140px; margin-right: 15px; text-align: center; + align-self: center; } .logo-holder { @@ -161,3 +163,9 @@ nav { flex-direction: row; display: flex; } + +.mempool-logo, app-svg-images { + align-self: center; + width: 140px; + height: 35px; +} diff --git a/frontend/src/app/components/tx-features/tx-features.component.html b/frontend/src/app/components/tx-features/tx-features.component.html index 16dbb66f4..e3569de8d 100644 --- a/frontend/src/app/components/tx-features/tx-features.component.html +++ b/frontend/src/app/components/tx-features/tx-features.component.html @@ -1,12 +1,12 @@ -SegWit +SegWit - SegWit - - SegWit + SegWit + + SegWit -Taproot +Taproot Taproot diff --git a/frontend/src/app/components/tx-features/tx-features.component.ts b/frontend/src/app/components/tx-features/tx-features.component.ts index f73d8ae8a..4c0611971 100644 --- a/frontend/src/app/components/tx-features/tx-features.component.ts +++ b/frontend/src/app/components/tx-features/tx-features.component.ts @@ -12,9 +12,9 @@ export class TxFeaturesComponent implements OnChanges { @Input() tx: Transaction; segwitGains = { - realizedBech32Gains: 0, - potentialBech32Gains: 0, - potentialP2shGains: 0, + realizedSegwitGains: 0, + potentialSegwitGains: 0, + potentialP2shSegwitGains: 0, potentialTaprootGains: 0, realizedTaprootGains: 0 }; diff --git a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts index 15997d3c3..962059c9d 100644 --- a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts +++ b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts @@ -169,9 +169,6 @@ export class NodeStatisticsChartComponent implements OnInit { }, yAxis: data.channels.length === 0 ? undefined : [ { - min: (value) => { - return value.min * 0.9; - }, type: 'value', axisLabel: { color: 'rgb(110, 112, 121)', @@ -188,9 +185,6 @@ export class NodeStatisticsChartComponent implements OnInit { }, }, { - min: (value) => { - return value.min * 0.9; - }, type: 'value', position: 'right', axisLabel: { @@ -225,15 +219,6 @@ export class NodeStatisticsChartComponent implements OnInit { opacity: 1, width: 1, }, - data: [{ - yAxis: 1, - label: { - position: 'end', - show: true, - color: '#ffffff', - formatter: `1 MB` - } - }], } }, { diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index e2132bca5..cb0e5ed43 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -43,11 +43,23 @@ Location - {{ node.city.en }}, {{ node.subdivision.en }}
{{ node.country.en }} + + {{ node.city.en }}, {{ node.subdivision.en }} +
+ + {{ node.country.en }} +   + {{ node.flag }} + + Location - {{ node.country.en }} + + + {{ node.country.en }} {{ node.flag }} + + @@ -77,7 +89,9 @@ ISP - {{ node.as_organization }} [ASN {{node.as_number}}] + + {{ node.as_organization }} [ASN {{node.as_number}}] + @@ -126,13 +140,8 @@

Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})

-
- List  - -  Map +
+
diff --git a/frontend/src/app/lightning/node/node.component.scss b/frontend/src/app/lightning/node/node.component.scss index 0bdb263a8..2b171416f 100644 --- a/frontend/src/app/lightning/node/node.component.scss +++ b/frontend/src/app/lightning/node/node.component.scss @@ -56,67 +56,4 @@ app-fiat { display: inline-block; 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%; -} \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index c70983b54..a8d487938 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; import { catchError, map, switchMap } from 'rxjs/operators'; import { SeoService } from 'src/app/services/seo.service'; +import { getFlagEmoji } from 'src/app/shared/graphs.utils'; import { LightningApiService } from '../lightning-api.service'; @Component({ @@ -51,6 +52,7 @@ export class NodeComponent implements OnInit { } else if (socket.indexOf('onion') > -1) { label = 'Tor'; } + node.flag = getFlagEmoji(node.iso_code); socketsObject.push({ label: label, socket: node.public_key + '@' + socket, @@ -73,8 +75,8 @@ export class NodeComponent implements OnInit { this.selectedSocketIndex = index; } - channelsListModeChange(e) { - if (e.target.checked === true) { + channelsListModeChange(toggle) { + if (toggle === true) { this.channelsListMode = 'map'; } else { this.channelsListMode = 'list'; diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts index 735d4868f..c292d09f7 100644 --- a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts @@ -59,7 +59,7 @@ export class NodesNetworksChartComponent implements OnInit { let firstRun = true; if (this.widget) { - this.miningWindowPreference = '1y'; + this.miningWindowPreference = '3y'; } else { this.seoService.setTitle($localize`Lightning nodes per network`); this.miningWindowPreference = this.miningService.getDefaultTimespan('all'); diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html index 28d314b9c..23f54bbba 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html @@ -7,7 +7,9 @@
- (Tor nodes excluded) + + (Tor nodes excluded) +
@@ -21,6 +23,11 @@
+
+ + +
+ @@ -34,8 +41,9 @@ - diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss index 8e9a9903b..10ad39372 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss @@ -45,7 +45,7 @@ .name { width: 25%; @media (max-width: 576px) { - width: 80%; + width: 70%; max-width: 150px; padding-left: 0; padding-right: 0; @@ -69,7 +69,17 @@ .capacity { width: 20%; @media (max-width: 576px) { - width: 10%; + width: 20%; max-width: 100px; } +} + +.toggle { + justify-content: space-between; + padding-top: 15px; + @media (min-width: 576px) { + padding-bottom: 15px; + padding-left: 105px; + padding-right: 105px; + } } \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts index 63665f69a..6b9d41e74 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core'; import { Router } from '@angular/router'; 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 { ApiService } from 'src/app/services/api.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, }) export class NodesPerISPChartComponent implements OnInit { - miningWindowPreference: string; - isLoading = true; chartOptions: EChartsOption = {}; chartInitOptions = { renderer: 'svg', }; timespan = ''; - chartInstance: any = undefined; + chartInstance = undefined; @HostBinding('attr.dir') dir = 'ltr'; nodesPerAsObservable$: Observable; + showTorObservable$: Observable; + groupBySubject = new Subject(); + showTorSubject = new Subject(); constructor( private apiService: ApiService, @@ -44,23 +45,32 @@ export class NodesPerISPChartComponent implements OnInit { ngOnInit(): void { 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( - tap(data => { - this.isLoading = false; - this.prepareChartOptions(data); - }), - map(data => { - for (let i = 0; i < data.length; ++i) { - data[i].rank = i + 1; - } - return data.slice(0, 100); + switchMap((selectedFilters) => { + return this.apiService.getNodesPerAs( + selectedFilters[0] ? 'capacity' : 'node-count', + selectedFilters[1] // Show Tor nodes + ) + .pipe( + tap(data => { + this.isLoading = false; + this.prepareChartOptions(data); + }), + map(data => { + for (let i = 0; i < data.length; ++i) { + data[i].rank = i + 1; + } + return data.slice(0, 100); + }) + ); }), share() ); } - generateChartSerieData(as) { + generateChartSerieData(as): PieSeriesOption[] { const shareThreshold = this.isMobile() ? 2 : 0.5; const data: object[] = []; let totalShareOther = 0; @@ -78,6 +88,9 @@ export class NodesPerISPChartComponent implements OnInit { return; } data.push({ + itemStyle: { + color: as.ispId === null ? '#7D4698' : undefined, + }, value: as.share, name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`), label: { @@ -138,14 +151,14 @@ export class NodesPerISPChartComponent implements OnInit { return data; } - prepareChartOptions(as) { + prepareChartOptions(as): void { let pieSize = ['20%', '80%']; // Desktop if (this.isMobile()) { pieSize = ['15%', '60%']; } this.chartOptions = { - color: chartColors, + color: chartColors.slice(3), tooltip: { trigger: 'item', textStyle: { @@ -191,18 +204,18 @@ export class NodesPerISPChartComponent implements OnInit { }; } - isMobile() { + isMobile(): boolean { return (window.innerWidth <= 767.98); } - onChartInit(ec) { + onChartInit(ec): void { if (this.chartInstance !== undefined) { return; } this.chartInstance = ec; this.chartInstance.on('click', (e) => { - if (e.data.data === 9999) { // "Other" + if (e.data.data === 9999 || e.data.data === null) { // "Other" or Tor return; } this.zone.run(() => { @@ -212,7 +225,7 @@ export class NodesPerISPChartComponent implements OnInit { }); } - onSaveChart() { + onSaveChart(): void { const now = new Date(); this.chartOptions.backgroundColor = '#11131f'; this.chartInstance.setOption(this.chartOptions); @@ -224,8 +237,12 @@ export class NodesPerISPChartComponent implements OnInit { this.chartInstance.setOption(this.chartOptions); } - isEllipsisActive(e) { - return (e.offsetWidth < e.scrollWidth); + onTorToggleStatusChanged(e): void { + this.showTorSubject.next(e); + } + + onGroupToggleStatusChanged(e): void { + this.groupBySubject.next(e); } } diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts index d95205542..1727d1f68 100644 --- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts @@ -58,7 +58,7 @@ export class LightningStatisticsChartComponent implements OnInit { let firstRun = true; if (this.widget) { - this.miningWindowPreference = '1y'; + this.miningWindowPreference = '3y'; } else { this.seoService.setTitle($localize`Channels and Capacity`); this.miningWindowPreference = this.miningService.getDefaultTimespan('all'); diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index fdb2714bd..844451574 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -255,8 +255,9 @@ export class ApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params }); } - getNodesPerAs(): Observable { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp'); + getNodesPerAs(groupBy: 'capacity' | 'node-count', showTorNodes: boolean): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp-ranking' + + `?groupBy=${groupBy}&showTor=${showTorNodes}`); } getNodeForCountry$(country: string): Observable { diff --git a/frontend/src/app/shared/components/toggle/toggle.component.html b/frontend/src/app/shared/components/toggle/toggle.component.html new file mode 100644 index 000000000..dac33c9d8 --- /dev/null +++ b/frontend/src/app/shared/components/toggle/toggle.component.html @@ -0,0 +1,8 @@ +
+ {{ textLeft }}  + +  {{ textRight }} +
diff --git a/frontend/src/app/shared/components/toggle/toggle.component.scss b/frontend/src/app/shared/components/toggle/toggle.component.scss new file mode 100644 index 000000000..a9c221290 --- /dev/null +++ b/frontend/src/app/shared/components/toggle/toggle.component.scss @@ -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%; +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/toggle/toggle.component.ts b/frontend/src/app/shared/components/toggle/toggle.component.ts new file mode 100644 index 000000000..4bd31ffbd --- /dev/null +++ b/frontend/src/app/shared/components/toggle/toggle.component.ts @@ -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(); + @Input() textLeft: string; + @Input() textRight: string; + + ngAfterViewInit(): void { + this.toggleStatusChanged.emit(false); + } + + onToggleStatusChanged(e): void { + this.toggleStatusChanged.emit(e.target.checked); + } +} diff --git a/frontend/src/app/shared/graphs.utils.ts b/frontend/src/app/shared/graphs.utils.ts index 6909e6fac..019ca49e4 100644 --- a/frontend/src/app/shared/graphs.utils.ts +++ b/frontend/src/app/shared/graphs.utils.ts @@ -92,6 +92,9 @@ export function detectWebGL() { } export function getFlagEmoji(countryCode) { + if (!countryCode) { + return ''; + } const codePoints = countryCode .toUpperCase() .split('') diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index cd087a3c4..df071033e 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -80,6 +80,7 @@ import { ChangeComponent } from '../components/change/change.component'; import { SatsComponent } from './components/sats/sats.component'; import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component'; import { TimestampComponent } from './components/timestamp/timestamp.component'; +import { ToggleComponent } from './components/toggle/toggle.component'; @NgModule({ declarations: [ @@ -154,6 +155,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component'; SatsComponent, SearchResultsComponent, TimestampComponent, + ToggleComponent, ], imports: [ CommonModule, @@ -255,6 +257,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component'; SatsComponent, SearchResultsComponent, TimestampComponent, + ToggleComponent, ] }) export class SharedModule { diff --git a/production/bitcoin.conf b/production/bitcoin.conf index 4cb95eacc..0fa1e943f 100644 --- a/production/bitcoin.conf +++ b/production/bitcoin.conf @@ -18,12 +18,18 @@ whitelist=2401:b140::/32 #uacomment=@wiz [main] -bind=0.0.0.0:8333 -bind=[::]:8333 rpcbind=127.0.0.1:8332 rpcbind=[::1]:8332 -zmqpubrawblock=tcp://127.0.0.1:18332 -zmqpubrawtx=tcp://127.0.0.1:18333 +bind=0.0.0.0:8333 +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:202]:8333 #addnode=[2401:b140:2::92:203]:8333 @@ -33,10 +39,18 @@ zmqpubrawtx=tcp://127.0.0.1:18333 [test] daemon=1 -bind=0.0.0.0:18333 -bind=[::]:18333 rpcbind=127.0.0.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:202]:18333 #addnode=[2401:b140:2::92:203]:18333 @@ -46,10 +60,18 @@ rpcbind=[::1]:18332 [signet] daemon=1 -bind=0.0.0.0:38333 -bind=[::]:38333 rpcbind=127.0.0.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:202]:38333 #addnode=[2401:b140:2::92:203]:38333 diff --git a/production/install b/production/install index fb3aa9281..e9b24bafa 100755 --- a/production/install +++ b/production/install @@ -218,6 +218,21 @@ MYSQL_HOME=/mysql MYSQL_USER=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_HOME=/mempool MEMPOOL_USER=mempool @@ -1513,22 +1528,38 @@ esac mysql << _EOF_ 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; -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; -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; -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; -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; -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_ ##### nginx diff --git a/production/mempool-build-all b/production/mempool-build-all index 5ac25f7e4..c0e9a2c2a 100755 --- a/production/mempool-build-all +++ b/production/mempool-build-all @@ -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_PASS=$(grep '^rpcpassword' /elements/elements.conf | cut -d '=' -f2) +# get mysql credentials +. /mempool/mysql_credentials + if [ -f "${LOCKFILE}" ];then echo "upgrade already running? check lockfile ${LOCKFILE}" exit 1 @@ -73,6 +76,18 @@ build_backend() -e "s!__BITCOIN_RPC_PASS__!${BITCOIN_RPC_PASS}!" \ -e "s!__ELEMENTS_RPC_USER__!${ELEMENTS_RPC_USER}!" \ -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" fi npm install --omit=dev --omit=optional || exit 1 diff --git a/production/mempool-config.bisq.json b/production/mempool-config.bisq.json index 1e91be930..599711764 100644 --- a/production/mempool-config.bisq.json +++ b/production/mempool-config.bisq.json @@ -21,8 +21,8 @@ "ENABLED": false, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool_bisq", - "PASSWORD": "mempool_bisq", + "USERNAME": "__MEMPOOL_BISQ_USER__", + "PASSWORD": "__MEMPOOL_BISQ_PASS__", "DATABASE": "mempool_bisq" }, "STATISTICS": { diff --git a/production/mempool-config.liquid.json b/production/mempool-config.liquid.json index 70ab56625..11ad8ffcd 100644 --- a/production/mempool-config.liquid.json +++ b/production/mempool-config.liquid.json @@ -28,8 +28,8 @@ "ENABLED": true, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool_liquid", - "PASSWORD": "mempool_liquid", + "USERNAME": "__MEMPOOL_LIQUID_USER__", + "PASSWORD": "__MEMPOOL_LIQUID_PASS__", "DATABASE": "mempool_liquid" }, "STATISTICS": { diff --git a/production/mempool-config.liquidtestnet.json b/production/mempool-config.liquidtestnet.json index b3c4dfaaf..7769bfb53 100644 --- a/production/mempool-config.liquidtestnet.json +++ b/production/mempool-config.liquidtestnet.json @@ -28,8 +28,8 @@ "ENABLED": true, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool_liquidtestnet", - "PASSWORD": "mempool_liquidtestnet", + "USERNAME": "__MEMPOOL_LIQUIDTESTNET_USER__", + "PASSWORD": "__MEMPOOL_LIQUIDTESTNET_PASS__", "DATABASE": "mempool_liquidtestnet" }, "STATISTICS": { diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index 4575afdbe..06a14d223 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -32,8 +32,8 @@ "ENABLED": true, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool", - "PASSWORD": "mempool", + "USERNAME": "__MEMPOOL_MAINNET_USER__", + "PASSWORD": "__MEMPOOL_MAINNET_PASS__", "DATABASE": "mempool" }, "STATISTICS": { diff --git a/production/mempool-config.signet.json b/production/mempool-config.signet.json index c1333f45a..f42c4dc50 100644 --- a/production/mempool-config.signet.json +++ b/production/mempool-config.signet.json @@ -24,8 +24,8 @@ "ENABLED": true, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool_signet", - "PASSWORD": "mempool_signet", + "USERNAME": "__MEMPOOL_SIGNET_USER__", + "PASSWORD": "__MEMPOOL_SIGNET_PASS__", "DATABASE": "mempool_signet" }, "STATISTICS": { diff --git a/production/mempool-config.testnet.json b/production/mempool-config.testnet.json index 79190c2de..cc63f93bf 100644 --- a/production/mempool-config.testnet.json +++ b/production/mempool-config.testnet.json @@ -24,8 +24,8 @@ "ENABLED": true, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool_testnet", - "PASSWORD": "mempool_testnet", + "USERNAME": "__MEMPOOL_TESTNET_USER__", + "PASSWORD": "__MEMPOOL_TESTNET_PASS__", "DATABASE": "mempool_testnet" }, "STATISTICS": { diff --git a/unfurler/README.md b/unfurler/README.md index 71b4ae2fc..3a14081d7 100644 --- a/unfurler/README.md +++ b/unfurler/README.md @@ -9,6 +9,22 @@ Some additional server configuration is required to properly route requests (see ## 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 Get the latest Mempool code: diff --git a/unfurler/package.json b/unfurler/package.json index 432c604e3..0d6d938d6 100644 --- a/unfurler/package.json +++ b/unfurler/package.json @@ -4,7 +4,7 @@ "description": "Renderer for mempool open graph link preview images", "repository": { "type": "git", - "url": "git+https://github.com/mononaut/mempool-unfurl" + "url": "git+https://github.com/mempool/mempool" }, "main": "index.ts", "scripts": {
{{ asEntry.rank }} - {{ asEntry.name }} + + {{ asEntry.name }} + {{ asEntry.name }} {{ asEntry.count }}