From 89b2e110837ae3d61478d8bf06ae538116f74332 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 12 Jul 2022 09:03:39 +0200 Subject: [PATCH 01/52] [Hashrate chart] Fix javascript error if difficulty array is empty --- .../hashrate-chart/hashrate-chart.component.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts index 385be0669..2a7cc07f2 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -109,7 +109,7 @@ export class HashrateChartComponent implements OnInit { while (hashIndex < data.hashrates.length) { diffFixed.push({ timestamp: data.hashrates[hashIndex].timestamp, - difficulty: data.difficulty[data.difficulty.length - 1].difficulty + difficulty: data.difficulty.length > 0 ? data.difficulty[data.difficulty.length - 1].difficulty : null }); ++hashIndex; } @@ -231,11 +231,15 @@ export class HashrateChartComponent implements OnInit { } else if (tick.seriesIndex === 1) { // Difficulty let difficultyPowerOfTen = hashratePowerOfTen; let difficulty = tick.data[1]; - if (this.isMobile()) { - difficultyPowerOfTen = selectPowerOfTen(tick.data[1]); - difficulty = Math.round(tick.data[1] / difficultyPowerOfTen.divider); + if (difficulty === null) { + difficultyString = `${tick.marker} ${tick.seriesName}: No data
`; + } else { + if (this.isMobile()) { + difficultyPowerOfTen = selectPowerOfTen(tick.data[1]); + difficulty = Math.round(tick.data[1] / difficultyPowerOfTen.divider); + } + difficultyString = `${tick.marker} ${tick.seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}
`; } - difficultyString = `${tick.marker} ${tick.seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}
`; } else if (tick.seriesIndex === 2) { // Hashrate MA let hashrate = tick.data[1]; if (this.isMobile()) { From 1be7c953ead536494d2503402ee2f7e274b9db4b Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 7 Jul 2022 19:11:42 +0200 Subject: [PATCH 02/52] Save current progress on the block audit page --- backend/src/api/blocks.ts | 2 +- backend/src/api/database-migration.ts | 5 +- backend/src/api/mining/mining-routes.ts | 15 ++- backend/src/api/websocket-handler.ts | 14 ++ .../repositories/BlocksAuditsRepository.ts | 25 ++++ .../repositories/BlocksSummariesRepository.ts | 20 ++- frontend/src/app/app-routing.module.ts | 28 ++++ .../block-audit/block-audit.component.html | 111 ++++++++++++++++ .../block-audit/block-audit.component.scss | 40 ++++++ .../block-audit/block-audit.component.ts | 120 ++++++++++++++++++ .../block-overview-graph.component.ts | 3 +- .../block-overview-graph/tx-view.ts | 12 ++ .../app/components/block/block.component.ts | 9 +- .../src/app/interfaces/node-api.interface.ts | 9 ++ .../src/app/interfaces/websocket.interface.ts | 1 + frontend/src/app/services/api.service.ts | 6 + frontend/src/app/shared/graphs.utils.ts | 6 + frontend/src/app/shared/shared.module.ts | 3 + 18 files changed, 412 insertions(+), 17 deletions(-) create mode 100644 frontend/src/app/components/block-audit/block-audit.component.html create mode 100644 frontend/src/app/components/block-audit/block-audit.component.scss create mode 100644 frontend/src/app/components/block-audit/block-audit.component.ts diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index c8c6f4a98..30f9fbf78 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -578,7 +578,7 @@ class Blocks { // Index the response if needed if (Common.blocksSummariesIndexingEnabled() === true) { - await BlocksSummariesRepository.$saveSummary(block.height, summary); + await BlocksSummariesRepository.$saveSummary(block.height, summary, null); } return summary.transactions; diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index e0a592b01..0b43095cb 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -4,7 +4,7 @@ import logger from '../logger'; import { Common } from './common'; class DatabaseMigration { - private static currentVersion = 31; + private static currentVersion = 32; private queryTimeout = 120000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -297,7 +297,10 @@ class DatabaseMigration { await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE'); await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`'); await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices')); + } + if (databaseSchemaVersion < 32 && isBitcoin == true) { + await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"'); } } diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index 02262353f..f52d42d1f 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -26,7 +26,8 @@ class MiningRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', this.$getDifficultyAdjustments) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', this.$getHistoricalBlockPrediction) - ; + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/audit/:hash', this.$getBlockAudit) + ; } private async $getPool(req: Request, res: Response): Promise { @@ -233,6 +234,18 @@ class MiningRoutes { res.status(500).send(e instanceof Error ? e.message : e); } } + + public async $getBlockAudit(req: Request, res: Response) { + try { + const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); + res.json(audit); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } } export default new MiningRoutes(); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 300341ef5..dee44ba63 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -17,6 +17,7 @@ import rbfCache from './rbf-cache'; import difficultyAdjustment from './difficulty-adjustment'; import feeApi from './fee-api'; import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository'; +import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; class WebsocketHandler { private wss: WebSocket.Server | undefined; @@ -442,6 +443,19 @@ class WebsocketHandler { mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); if (Common.indexingEnabled()) { + const stripped = _mempoolBlocks[0].transactions.map((tx) => { + return { + txid: tx.txid, + vsize: tx.vsize, + fee: tx.fee ? Math.round(tx.fee) : 0, + value: tx.value, + }; + }); + BlocksSummariesRepository.$saveSummary(block.height, null, { + id: block.id, + transactions: stripped + }); + BlocksAuditsRepository.$saveAudit({ time: block.timestamp, height: block.height, diff --git a/backend/src/repositories/BlocksAuditsRepository.ts b/backend/src/repositories/BlocksAuditsRepository.ts index 31d8ec785..54b723959 100644 --- a/backend/src/repositories/BlocksAuditsRepository.ts +++ b/backend/src/repositories/BlocksAuditsRepository.ts @@ -1,3 +1,4 @@ +import transactionUtils from '../api/transaction-utils'; import DB from '../database'; import logger from '../logger'; import { BlockAudit } from '../mempool.interfaces'; @@ -45,6 +46,30 @@ class BlocksAuditRepositories { throw e; } } + + public async $getBlockAudit(hash: string): Promise { + try { + const [rows]: any[] = await DB.query( + `SELECT blocks.height, blocks.hash as id, UNIX_TIMESTAMP(blocks.blockTimestamp) as timestamp, blocks.size, + blocks.weight, blocks.tx_count, + transactions, template, missing_txs as missingTxs, added_txs as addedTxs, match_rate as matchRate + FROM blocks_audits + JOIN blocks ON blocks.hash = blocks_audits.hash + JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash + WHERE blocks_audits.hash = "${hash}" + `); + + rows[0].missingTxs = JSON.parse(rows[0].missingTxs); + rows[0].addedTxs = JSON.parse(rows[0].addedTxs); + rows[0].transactions = JSON.parse(rows[0].transactions); + rows[0].template = JSON.parse(rows[0].template); + + return rows[0]; + } catch (e: any) { + logger.err(`Cannot fetch block audit from db. Reason: ` + (e instanceof Error ? e.message : e)); + throw e; + } + } } export default new BlocksAuditRepositories(); diff --git a/backend/src/repositories/BlocksSummariesRepository.ts b/backend/src/repositories/BlocksSummariesRepository.ts index 66c6b97f2..2f47f7a35 100644 --- a/backend/src/repositories/BlocksSummariesRepository.ts +++ b/backend/src/repositories/BlocksSummariesRepository.ts @@ -17,14 +17,24 @@ class BlocksSummariesRepository { return undefined; } - public async $saveSummary(height: number, summary: BlockSummary) { + public async $saveSummary(height: number, mined: BlockSummary | null = null, template: BlockSummary | null = null) { + const blockId = mined?.id ?? template?.id; try { - await DB.query(`INSERT INTO blocks_summaries VALUE (?, ?, ?)`, [height, summary.id, JSON.stringify(summary.transactions)]); + const [dbSummary]: any[] = await DB.query(`SELECT * FROM blocks_summaries WHERE id = "${blockId}"`); + if (dbSummary.length === 0) { // First insertion + await DB.query(`INSERT INTO blocks_summaries VALUE (?, ?, ?, ?)`, [ + height, blockId, JSON.stringify(mined?.transactions ?? []), JSON.stringify(template?.transactions ?? []) + ]); + } else if (mined !== null) { // Update mined block summary + await DB.query(`UPDATE blocks_summaries SET transactions = ? WHERE id = "${mined.id}"`, [JSON.stringify(mined?.transactions)]); + } else if (template !== null) { // Update template block summary + await DB.query(`UPDATE blocks_summaries SET template = ? WHERE id = "${template.id}"`, [JSON.stringify(template?.transactions)]); + } } catch (e: any) { if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart - logger.debug(`Cannot save block summary for ${summary.id} because it has already been indexed, ignoring`); + logger.debug(`Cannot save block summary for ${blockId} because it has already been indexed, ignoring`); } else { - logger.debug(`Cannot save block summary for ${summary.id}. Reason: ${e instanceof Error ? e.message : e}`); + logger.debug(`Cannot save block summary for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`); throw e; } } @@ -44,7 +54,7 @@ class BlocksSummariesRepository { /** * Delete blocks from the database from blockHeight */ - public async $deleteBlocksFrom(blockHeight: number) { + public async $deleteBlocksFrom(blockHeight: number) { logger.info(`Delete newer blocks summary from height ${blockHeight} from the database`); try { diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 564b8653b..1a658c44b 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -3,6 +3,7 @@ import { Routes, RouterModule, PreloadAllModules } from '@angular/router'; import { StartComponent } from './components/start/start.component'; import { TransactionComponent } from './components/transaction/transaction.component'; import { BlockComponent } from './components/block/block.component'; +import { BlockAuditComponent } from './components/block-audit/block-audit.component'; import { AddressComponent } from './components/address/address.component'; import { MasterPageComponent } from './components/master-page/master-page.component'; import { AboutComponent } from './components/about/about.component'; @@ -88,6 +89,15 @@ let routes: Routes = [ }, ], }, + { + path: 'block-audit', + children: [ + { + path: ':id', + component: BlockAuditComponent, + }, + ], + }, { path: 'docs', loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) @@ -182,6 +192,15 @@ let routes: Routes = [ }, ], }, + { + path: 'block-audit', + children: [ + { + path: ':id', + component: BlockAuditComponent, + }, + ], + }, { path: 'docs', loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) @@ -273,6 +292,15 @@ let routes: Routes = [ }, ], }, + { + path: 'block-audit', + children: [ + { + path: ':id', + component: BlockAuditComponent + }, + ], + }, { path: 'docs', loadChildren: () => import('./docs/docs.module').then(m => m.DocsModule) diff --git a/frontend/src/app/components/block-audit/block-audit.component.html b/frontend/src/app/components/block-audit/block-audit.component.html new file mode 100644 index 000000000..0ee6bef44 --- /dev/null +++ b/frontend/src/app/components/block-audit/block-audit.component.html @@ -0,0 +1,111 @@ +
+ +
+
+

+ + Block +   + {{ blockAudit.height }} +   + Template vs Mined + +

+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + +
Hash{{ blockAudit.id | shortenString : 13 }} + +
Timestamp + ‎{{ blockAudit.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} +
+ ( + ) +
+
Size
Weight
+
+ + +
+ + + + + + + + + + + + + + + + + + + +
Transactions{{ blockAudit.tx_count }}
Match rate{{ blockAudit.matchRate }}%
Missing txs{{ blockAudit.missingTxs.length }}
Added txs{{ blockAudit.addedTxs.length }}
+
+
+
+ + + +
+ + +
+
+ +
+ +
+ + +
+ +
+
+
+ + + +
\ No newline at end of file diff --git a/frontend/src/app/components/block-audit/block-audit.component.scss b/frontend/src/app/components/block-audit/block-audit.component.scss new file mode 100644 index 000000000..7ec503891 --- /dev/null +++ b/frontend/src/app/components/block-audit/block-audit.component.scss @@ -0,0 +1,40 @@ +.title-block { + border-top: none; +} + +.table { + tr td { + &:last-child { + text-align: right; + @media (min-width: 768px) { + text-align: left; + } + } + } +} + +.block-tx-title { + display: flex; + justify-content: space-between; + flex-direction: column; + position: relative; + @media (min-width: 550px) { + flex-direction: row; + } + h2 { + line-height: 1; + margin: 0; + position: relative; + padding-bottom: 10px; + @media (min-width: 550px) { + padding-bottom: 0px; + align-self: end; + } + } +} + +.menu-button { + @media (min-width: 768px) { + max-width: 150px; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/block-audit/block-audit.component.ts b/frontend/src/app/components/block-audit/block-audit.component.ts new file mode 100644 index 000000000..044552a3b --- /dev/null +++ b/frontend/src/app/components/block-audit/block-audit.component.ts @@ -0,0 +1,120 @@ +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map, share, switchMap, tap } from 'rxjs/operators'; +import { BlockAudit, TransactionStripped } from 'src/app/interfaces/node-api.interface'; +import { ApiService } from 'src/app/services/api.service'; +import { StateService } from 'src/app/services/state.service'; +import { detectWebGL } from 'src/app/shared/graphs.utils'; +import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; +import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component'; + +@Component({ + selector: 'app-block-audit', + templateUrl: './block-audit.component.html', + styleUrls: ['./block-audit.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 100; + } + `], +}) +export class BlockAuditComponent implements OnInit, OnDestroy { + blockAudit: BlockAudit = undefined; + transactions: string[]; + auditObservable$: Observable; + + paginationMaxSize: number; + page = 1; + itemsPerPage: number; + + mode: 'missing' | 'added' = 'missing'; + isLoading = true; + webGlEnabled = true; + isMobile = window.innerWidth <= 767.98; + + @ViewChild('blockGraphTemplate') blockGraphTemplate: BlockOverviewGraphComponent; + @ViewChild('blockGraphMined') blockGraphMined: BlockOverviewGraphComponent; + + constructor( + private route: ActivatedRoute, + public stateService: StateService, + private router: Router, + private apiService: ApiService + ) { + this.webGlEnabled = detectWebGL(); + } + + ngOnDestroy(): void { + } + + ngOnInit(): void { + this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; + this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE; + + this.auditObservable$ = this.route.paramMap.pipe( + switchMap((params: ParamMap) => { + const blockHash: string = params.get('id') || ''; + return this.apiService.getBlockAudit$(blockHash) + .pipe( + map((response) => { + const blockAudit = response.body; + for (let i = 0; i < blockAudit.template.length; ++i) { + if (blockAudit.missingTxs.includes(blockAudit.template[i].txid)) { + blockAudit.template[i].status = 'missing'; + } else if (blockAudit.addedTxs.includes(blockAudit.template[i].txid)) { + blockAudit.template[i].status = 'added'; + } else { + blockAudit.template[i].status = 'found'; + } + } + for (let i = 0; i < blockAudit.transactions.length; ++i) { + if (blockAudit.missingTxs.includes(blockAudit.transactions[i].txid)) { + blockAudit.transactions[i].status = 'missing'; + } else if (blockAudit.addedTxs.includes(blockAudit.transactions[i].txid)) { + blockAudit.transactions[i].status = 'added'; + } else { + blockAudit.transactions[i].status = 'found'; + } + } + return blockAudit; + }), + tap((blockAudit) => { + this.changeMode(this.mode); + if (this.blockGraphTemplate) { + this.blockGraphTemplate.destroy(); + this.blockGraphTemplate.setup(blockAudit.template); + } + if (this.blockGraphMined) { + this.blockGraphMined.destroy(); + this.blockGraphMined.setup(blockAudit.transactions); + } + this.isLoading = false; + }), + ); + }), + share() + ); + } + + onResize(event: any) { + this.isMobile = event.target.innerWidth <= 767.98; + this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5; + } + + changeMode(mode: 'missing' | 'added') { + this.router.navigate([], { fragment: mode }); + this.mode = mode; + } + + onTxClick(event: TransactionStripped): void { + const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`); + this.router.navigate([url]); + } + + pageChange(page: number, target: HTMLElement) { + } +} diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index e2774ac03..6e62a2fd0 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -1,6 +1,5 @@ import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy } from '@angular/core'; -import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface'; -import { WebsocketService } from 'src/app/services/websocket.service'; +import { TransactionStripped } from 'src/app/interfaces/websocket.interface'; import { FastVertexArray } from './fast-vertex-array'; import BlockScene from './block-scene'; import TxSprite from './tx-sprite'; diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index 03718d79d..edcbf2f58 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -25,6 +25,7 @@ export default class TxView implements TransactionStripped { vsize: number; value: number; feerate: number; + status?: 'found' | 'missing' | 'added'; initialised: boolean; vertexArray: FastVertexArray; @@ -43,6 +44,7 @@ export default class TxView implements TransactionStripped { this.vsize = tx.vsize; this.value = tx.value; this.feerate = tx.fee / tx.vsize; + this.status = tx.status; this.initialised = false; this.vertexArray = vertexArray; @@ -140,6 +142,16 @@ export default class TxView implements TransactionStripped { } getColor(): Color { + // Block audit + if (this.status === 'found') { + // return hexToColor('1a4987'); + } else if (this.status === 'missing') { + return hexToColor('039BE5'); + } else if (this.status === 'added') { + return hexToColor('D81B60'); + } + + // Block component const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1; return hexToColor(mempoolFeeColors[feeLevelIndex] || mempoolFeeColors[mempoolFeeColors.length - 1]); } diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 1032998ef..621956d20 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -12,6 +12,7 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url. import { BlockExtended, TransactionStripped } from 'src/app/interfaces/node-api.interface'; import { ApiService } from 'src/app/services/api.service'; import { BlockOverviewGraphComponent } from 'src/app/components/block-overview-graph/block-overview-graph.component'; +import { detectWebGL } from 'src/app/shared/graphs.utils'; @Component({ selector: 'app-block', @@ -390,10 +391,4 @@ export class BlockComponent implements OnInit, OnDestroy { const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`); this.router.navigate([url]); } -} - -function detectWebGL() { - const canvas = document.createElement('canvas'); - const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); - return (gl && gl instanceof WebGLRenderingContext); -} +} \ No newline at end of file diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 6d3e7c0d8..5f3506db5 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -128,11 +128,20 @@ export interface BlockExtended extends Block { extras?: BlockExtension; } +export interface BlockAudit extends BlockExtended { + missingTxs: string[], + addedTxs: string[], + matchRate: number, + template: TransactionStripped[], + transactions: TransactionStripped[], +} + export interface TransactionStripped { txid: string; fee: number; vsize: number; value: number; + status?: 'found' | 'missing' | 'added'; } export interface RewardStats { diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index d7f0addea..e4ceefb44 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -70,6 +70,7 @@ export interface TransactionStripped { fee: number; vsize: number; value: number; + status?: 'found' | 'missing' | 'added'; } export interface IBackendInfo { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index ddeb538d9..8e3da7e09 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -228,6 +228,12 @@ export class ApiService { ); } + getBlockAudit$(hash: string) : Observable { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/` + hash, { observe: 'response' } + ); + } + getRewardStats$(blockCount: number = 144): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`); } diff --git a/frontend/src/app/shared/graphs.utils.ts b/frontend/src/app/shared/graphs.utils.ts index 5f23ed800..2e103ecda 100644 --- a/frontend/src/app/shared/graphs.utils.ts +++ b/frontend/src/app/shared/graphs.utils.ts @@ -84,3 +84,9 @@ export const download = (href, name) => { a.click(); document.body.removeChild(a); }; + +export function detectWebGL() { + const canvas = document.createElement('canvas'); + const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); + return (gl && gl instanceof WebGLRenderingContext); +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 77e4cb046..cec162ad9 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -44,6 +44,7 @@ import { StartComponent } from '../components/start/start.component'; import { TransactionComponent } from '../components/transaction/transaction.component'; import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component'; import { BlockComponent } from '../components/block/block.component'; +import { BlockAuditComponent } from '../components/block-audit/block-audit.component'; import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; import { AddressComponent } from '../components/address/address.component'; @@ -114,6 +115,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component'; StartComponent, TransactionComponent, BlockComponent, + BlockAuditComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, TransactionsListComponent, @@ -213,6 +215,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component'; StartComponent, TransactionComponent, BlockComponent, + BlockAuditComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, TransactionsListComponent, From 16c6f030db988d8383fdd10106a4dabb2907452e Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 15 Jul 2022 12:01:21 +0200 Subject: [PATCH 03/52] Fix block prediction chart when no or few data is available --- .../block-prediction-graph.component.ts | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.ts b/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.ts index fe5a9f40c..c708c7574 100644 --- a/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.ts +++ b/frontend/src/app/components/block-prediction-graph/block-prediction-graph.component.ts @@ -98,7 +98,21 @@ export class BlockPredictionGraphComponent implements OnInit { } prepareChartOptions(data) { + let title: object; + if (data.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: $localize`No data to display yet. Try again later.`, + left: 'center', + top: 'center' + }; + } + this.chartOptions = { + title: data.length === 0 ? title : undefined, animation: false, grid: { top: 30, @@ -133,14 +147,13 @@ export class BlockPredictionGraphComponent implements OnInit { return tooltip; } }, - xAxis: { + xAxis: data.length === 0 ? undefined : { name: formatterXAxisLabel(this.locale, this.timespan), nameLocation: 'middle', nameTextStyle: { padding: [10, 0, 0, 0], }, type: 'category', - boundaryGap: false, axisLine: { onZero: true }, axisLabel: { formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)), @@ -152,7 +165,7 @@ export class BlockPredictionGraphComponent implements OnInit { }, data: data.map(prediction => prediction[0]) }, - yAxis: [ + yAxis: data.length === 0 ? undefined : [ { type: 'value', axisLabel: { @@ -170,7 +183,7 @@ export class BlockPredictionGraphComponent implements OnInit { }, }, ], - series: [ + series: data.length === 0 ? undefined : [ { zlevel: 0, name: $localize`Match rate`, @@ -183,9 +196,10 @@ export class BlockPredictionGraphComponent implements OnInit { })), type: 'bar', barWidth: '90%', + barMaxWidth: 50, }, ], - dataZoom: [{ + dataZoom: data.length === 0 ? undefined : [{ type: 'inside', realtime: true, zoomLock: true, From 2872d2e29903b98b9da22a91b6a6a0bb75401255 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 15 Jul 2022 11:03:59 +0200 Subject: [PATCH 04/52] Refactor BlocksSummariesRepository::$saveSummary --- backend/src/api/blocks.ts | 2 +- backend/src/api/websocket-handler.ts | 9 ++++++--- .../src/repositories/BlocksSummariesRepository.ts | 14 +++++++------- .../app/components/block-overview-graph/tx-view.ts | 4 +--- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 30f9fbf78..e40977c6c 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -578,7 +578,7 @@ class Blocks { // Index the response if needed if (Common.blocksSummariesIndexingEnabled() === true) { - await BlocksSummariesRepository.$saveSummary(block.height, summary, null); + await BlocksSummariesRepository.$saveSummary({height: block.height, mined: summary}); } return summary.transactions; diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index dee44ba63..4896ee058 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -451,9 +451,12 @@ class WebsocketHandler { value: tx.value, }; }); - BlocksSummariesRepository.$saveSummary(block.height, null, { - id: block.id, - transactions: stripped + BlocksSummariesRepository.$saveSummary({ + height: block.height, + template: { + id: block.id, + transactions: stripped + } }); BlocksAuditsRepository.$saveAudit({ diff --git a/backend/src/repositories/BlocksSummariesRepository.ts b/backend/src/repositories/BlocksSummariesRepository.ts index 2f47f7a35..28b3cc7eb 100644 --- a/backend/src/repositories/BlocksSummariesRepository.ts +++ b/backend/src/repositories/BlocksSummariesRepository.ts @@ -17,18 +17,18 @@ class BlocksSummariesRepository { return undefined; } - public async $saveSummary(height: number, mined: BlockSummary | null = null, template: BlockSummary | null = null) { - const blockId = mined?.id ?? template?.id; + public async $saveSummary(params: { height: number, mined?: BlockSummary, template?: BlockSummary}) { + const blockId = params.mined?.id ?? params.template?.id; try { const [dbSummary]: any[] = await DB.query(`SELECT * FROM blocks_summaries WHERE id = "${blockId}"`); if (dbSummary.length === 0) { // First insertion await DB.query(`INSERT INTO blocks_summaries VALUE (?, ?, ?, ?)`, [ - height, blockId, JSON.stringify(mined?.transactions ?? []), JSON.stringify(template?.transactions ?? []) + params.height, blockId, JSON.stringify(params.mined?.transactions ?? []), JSON.stringify(params.template?.transactions ?? []) ]); - } else if (mined !== null) { // Update mined block summary - await DB.query(`UPDATE blocks_summaries SET transactions = ? WHERE id = "${mined.id}"`, [JSON.stringify(mined?.transactions)]); - } else if (template !== null) { // Update template block summary - await DB.query(`UPDATE blocks_summaries SET template = ? WHERE id = "${template.id}"`, [JSON.stringify(template?.transactions)]); + } else if (params.mined !== undefined) { // Update mined block summary + await DB.query(`UPDATE blocks_summaries SET transactions = ? WHERE id = "${params.mined.id}"`, [JSON.stringify(params.mined.transactions)]); + } else if (params.template !== undefined) { // Update template block summary + await DB.query(`UPDATE blocks_summaries SET template = ? WHERE id = "${params.template.id}"`, [JSON.stringify(params.template?.transactions)]); } } catch (e: any) { if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index edcbf2f58..c0b980d5c 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -143,9 +143,7 @@ export default class TxView implements TransactionStripped { getColor(): Color { // Block audit - if (this.status === 'found') { - // return hexToColor('1a4987'); - } else if (this.status === 'missing') { + if (this.status === 'missing') { return hexToColor('039BE5'); } else if (this.status === 'added') { return hexToColor('D81B60'); From d6158060e7d386b4463348ccca6730b7aed765c4 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Sat, 16 Jul 2022 09:27:07 +0200 Subject: [PATCH 05/52] Ignore Kraken historical price without USD --- backend/src/tasks/price-feeds/kraken-api.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/tasks/price-feeds/kraken-api.ts b/backend/src/tasks/price-feeds/kraken-api.ts index ce76d62c2..ddb3c4f65 100644 --- a/backend/src/tasks/price-feeds/kraken-api.ts +++ b/backend/src/tasks/price-feeds/kraken-api.ts @@ -62,7 +62,7 @@ class KrakenApi implements PriceFeed { // CHF weekly price history goes back to timestamp 1575504000 (December 5, 2019) // AUD weekly price history goes back to timestamp 1591833600 (June 11, 2020) - const priceHistory: any = {}; // map: timestamp -> Prices + let priceHistory: any = {}; // map: timestamp -> Prices for (const currency of this.currencies) { const response = await query(this.urlHist.replace('{GRANULARITY}', '10080') + currency); @@ -83,6 +83,10 @@ class KrakenApi implements PriceFeed { } for (const time in priceHistory) { + if (priceHistory[time].USD === -1) { + delete priceHistory[time]; + continue; + } await PricesRepository.$savePrices(parseInt(time, 10), priceHistory[time]); } From 2fd34cbd91656695b93ff431368bc93c1f441d6e Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 12 Jul 2022 22:32:13 +0200 Subject: [PATCH 06/52] Get nodes count per AS by calling `/lightning/nodes/asShare` API --- backend/src/api/explorer/nodes.api.ts | 29 ++++++++++++++++++++++++ backend/src/api/explorer/nodes.routes.ts | 13 +++++++++++ 2 files changed, 42 insertions(+) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 590ed1f20..f76975d0a 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -93,6 +93,35 @@ class NodesApi { throw e; } } + + public async $getNodesAsShare() { + try { + let query = `SELECT names, COUNT(*) as nodesCount from nodes + JOIN geo_names ON geo_names.id = nodes.as_number + GROUP BY as_number + ORDER BY COUNT(*) DESC + LIMIT 20 + `; + 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); + + const nodesPerAs: any[] = []; + for (const as of nodesCountPerAS) { + nodesPerAs.push({ + name: JSON.parse(as.names), + count: as.nodesCount, + share: Math.floor(as.nodesCount / nodesWithAS[0].total * 10000) / 100, + }) + } + + return nodesPerAs; + } catch (e) { + logger.err(`Cannot get nodes grouped by AS. Reason: ${e instanceof Error ? e.message : e}`); + throw e; + } + } } export default new NodesApi(); diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index 6c79c8201..d2960155b 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -8,6 +8,7 @@ class NodesRoutes { app .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/asShare', this.$getNodesAsShare) .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) ; @@ -56,6 +57,18 @@ class NodesRoutes { res.status(500).send(e instanceof Error ? e.message : e); } } + + private async $getNodesAsShare(req: Request, res: Response) { + try { + const nodesPerAs = await nodesApi.$getNodesAsShare(); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); + res.json(nodesPerAs); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } } export default new NodesRoutes(); From 28cf0f71eb2193f6e42dfe15e4946f25b6707584 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Sat, 16 Jul 2022 10:44:05 +0200 Subject: [PATCH 07/52] Add nodes AS share chart and table component --- backend/src/api/explorer/nodes.api.ts | 1 - .../components/graphs/graphs.component.html | 2 + .../src/app/graphs/graphs.routing.module.ts | 5 + .../src/app/lightning/lightning.module.ts | 2 + .../nodes-per-as-chart.component.html | 41 ++++ .../nodes-per-as-chart.component.scss | 36 +++ .../nodes-per-as-chart.component.ts | 210 ++++++++++++++++++ frontend/src/app/services/api.service.ts | 3 + 8 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.html create mode 100644 frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.scss create mode 100644 frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.ts diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index f76975d0a..3bfa4d50e 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -100,7 +100,6 @@ class NodesApi { JOIN geo_names ON geo_names.id = nodes.as_number GROUP BY as_number ORDER BY COUNT(*) DESC - LIMIT 20 `; const [nodesCountPerAS]: any = await DB.query(query); diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html index 6f93676f6..c75aac232 100644 --- a/frontend/src/app/components/graphs/graphs.component.html +++ b/frontend/src/app/components/graphs/graphs.component.html @@ -34,6 +34,8 @@ i18n="lightning.nodes-networks">Nodes per network Network capacity + Nodes per AS diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts index 8bf531d80..193c6ab61 100644 --- a/frontend/src/app/graphs/graphs.routing.module.ts +++ b/frontend/src/app/graphs/graphs.routing.module.ts @@ -20,6 +20,7 @@ import { TelevisionComponent } from '../components/television/television.compone import { DashboardComponent } from '../dashboard/dashboard.component'; import { NodesNetworksChartComponent } from '../lightning/nodes-networks-chart/nodes-networks-chart.component'; import { LightningStatisticsChartComponent } from '../lightning/statistics-chart/lightning-statistics-chart.component'; +import { NodesPerAsChartComponent } from '../lightning/nodes-per-as-chart/nodes-per-as-chart.component'; const browserWindow = window || {}; // @ts-ignore @@ -99,6 +100,10 @@ const routes: Routes = [ path: 'lightning/capacity', component: LightningStatisticsChartComponent, }, + { + path: 'lightning/nodes-per-as', + component: NodesPerAsChartComponent, + }, { path: '', redirectTo: 'mempool', diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index 9146975e4..1cf9992f6 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -18,6 +18,7 @@ import { NodeStatisticsChartComponent } from './node-statistics-chart/node-stati import { GraphsModule } from '../graphs/graphs.module'; import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component'; import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component'; +import { NodesPerAsChartComponent } from '../lightning/nodes-per-as-chart/nodes-per-as-chart.component'; @NgModule({ declarations: [ LightningDashboardComponent, @@ -33,6 +34,7 @@ import { ChannelsStatisticsComponent } from './channels-statistics/channels-stat LightningStatisticsChartComponent, NodesNetworksChartComponent, ChannelsStatisticsComponent, + NodesPerAsChartComponent, ], imports: [ CommonModule, diff --git a/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.html b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.html new file mode 100644 index 000000000..16ba4fea6 --- /dev/null +++ b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.html @@ -0,0 +1,41 @@ +
+ +
+ Nodes per AS + +
+ +
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + +
RankNameHashrateNodes
{{ asEntry.rank }}{{ asEntry.name }}{{ asEntry.share }}%{{ asEntry.count }}
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.scss b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.scss new file mode 100644 index 000000000..832880122 --- /dev/null +++ b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.scss @@ -0,0 +1,36 @@ +.card-header { + border-bottom: 0; + font-size: 18px; + @media (min-width: 465px) { + font-size: 20px; + } +} + +.full-container { + padding: 0px 15px; + width: 100%; + height: calc(100% - 140px); + @media (max-width: 992px) { + height: calc(100% - 190px); + }; + @media (max-width: 575px) { + height: calc(100% - 230px); + }; +} + +.chart { + max-height: 400px; + @media (max-width: 767.98px) { + max-height: 230px; + margin-top: -35px; + } +} + +.bottom-padding { + @media (max-width: 992px) { + padding-bottom: 65px + }; + @media (max-width: 576px) { + padding-bottom: 65px + }; +} diff --git a/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.ts b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.ts new file mode 100644 index 000000000..cc8d5e759 --- /dev/null +++ b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.ts @@ -0,0 +1,210 @@ +import { ChangeDetectionStrategy, Component, OnInit, HostBinding } from '@angular/core'; +import { EChartsOption, PieSeriesOption } from 'echarts'; +import { map, Observable, share, 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'; +import { download } from 'src/app/shared/graphs.utils'; + +@Component({ + selector: 'app-nodes-per-as-chart', + templateUrl: './nodes-per-as-chart.component.html', + styleUrls: ['./nodes-per-as-chart.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NodesPerAsChartComponent implements OnInit { + miningWindowPreference: string; + + isLoading = true; + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + timespan = ''; + chartInstance: any = undefined; + + @HostBinding('attr.dir') dir = 'ltr'; + + nodesPerAsObservable$: Observable; + + constructor( + private apiService: ApiService, + private seoService: SeoService, + ) { + } + + ngOnInit(): void { + this.seoService.setTitle($localize`Nodes per AS`); + + this.nodesPerAsObservable$ = this.apiService.getNodesPerAs() + .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) { + const shareThreshold = this.isMobile() ? 2 : 1; + const data: object[] = []; + let totalShareOther = 0; + let totalNodeOther = 0; + + let edgeDistance: string | number = '10%'; + if (this.isMobile()) { + edgeDistance = 0; + } + + as.forEach((as) => { + if (as.share < shareThreshold) { + totalShareOther += as.share; + totalNodeOther += as.count; + return; + } + data.push({ + value: as.share, + name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`), + label: { + overflow: 'truncate', + color: '#b1b1b1', + alignTo: 'edge', + edgeDistance: edgeDistance, + }, + tooltip: { + show: !this.isMobile(), + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + }, + borderColor: '#000', + formatter: () => { + return `${as.name} (${as.share}%)
` + + $localize`${as.count.toString()} nodes`; + } + }, + data: as.slug, + } as PieSeriesOption); + }); + + // 'Other' + data.push({ + itemStyle: { + color: 'grey', + }, + value: totalShareOther, + name: 'Other' + (this.isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`), + label: { + overflow: 'truncate', + color: '#b1b1b1', + alignTo: 'edge', + edgeDistance: edgeDistance + }, + tooltip: { + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + }, + borderColor: '#000', + formatter: () => { + return `${'Other'} (${totalShareOther.toFixed(2)}%)
` + + totalNodeOther.toString() + ` nodes`; + } + }, + } as PieSeriesOption); + + return data; + } + + prepareChartOptions(as) { + let pieSize = ['20%', '80%']; // Desktop + if (this.isMobile()) { + pieSize = ['15%', '60%']; + } + + this.chartOptions = { + color: chartColors, + tooltip: { + trigger: 'item', + textStyle: { + align: 'left', + } + }, + series: [ + { + zlevel: 0, + minShowLabelAngle: 3.6, + name: 'Mining pool', + type: 'pie', + radius: pieSize, + data: this.generateChartSerieData(as), + labelLine: { + lineStyle: { + width: 2, + }, + length: this.isMobile() ? 1 : 20, + length2: this.isMobile() ? 1 : undefined, + }, + label: { + fontSize: 14, + }, + itemStyle: { + borderRadius: 1, + borderWidth: 1, + borderColor: '#000', + }, + emphasis: { + itemStyle: { + shadowBlur: 40, + shadowColor: 'rgba(0, 0, 0, 0.75)', + }, + labelLine: { + lineStyle: { + width: 4, + } + } + } + } + ], + }; + } + + isMobile() { + return (window.innerWidth <= 767.98); + } + + onChartInit(ec) { + if (this.chartInstance !== undefined) { + return; + } + this.chartInstance = ec; + } + + onSaveChart() { + const now = new Date(); + this.chartOptions.backgroundColor = '#11131f'; + this.chartInstance.setOption(this.chartOptions); + download(this.chartInstance.getDataURL({ + pixelRatio: 2, + excludeComponents: ['dataZoom'], + }), `ln-nodes-per-as-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`); + this.chartOptions.backgroundColor = 'none'; + this.chartInstance.setOption(this.chartOptions); + } + + isEllipsisActive(e) { + return (e.offsetWidth < e.scrollWidth); + } +} + diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 8e3da7e09..48f23a94f 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -251,4 +251,7 @@ 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/asShare'); + } } From 3edd6f23a556edcc34795f981114d4af6dffa2dd Mon Sep 17 00:00:00 2001 From: nymkappa Date: Sat, 16 Jul 2022 11:32:48 +0200 Subject: [PATCH 08/52] Add capacity per AS --- backend/src/api/explorer/nodes.api.ts | 7 +++++-- .../nodes-per-as-chart.component.html | 16 +++++++++------- .../nodes-per-as-chart.component.ts | 6 +++++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 3bfa4d50e..c3b3f8124 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -96,10 +96,12 @@ class NodesApi { public async $getNodesAsShare() { try { - let query = `SELECT names, COUNT(*) as nodesCount from nodes + let query = `SELECT names, COUNT(DISTINCT nodes.public_key) as nodesCount, SUM(capacity) 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 as_number - ORDER BY COUNT(*) DESC + ORDER BY COUNT(DISTINCT nodes.public_key) DESC `; const [nodesCountPerAS]: any = await DB.query(query); @@ -112,6 +114,7 @@ class NodesApi { name: JSON.parse(as.names), count: as.nodesCount, share: Math.floor(as.nodesCount / nodesWithAS[0].total * 10000) / 100, + capacity: as.capacity, }) } diff --git a/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.html b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.html index 16ba4fea6..3ea6f1e29 100644 --- a/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.html +++ b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.html @@ -21,18 +21,20 @@ - - - - + + + + + - + - - + + +
RankNameHashrateNodesRankNameShareNodesCapacity
{{ asEntry.rank }}{{ asEntry.rank }} {{ asEntry.name }}{{ asEntry.share }}%{{ asEntry.count }}{{ asEntry.share }}%{{ asEntry.count }}
diff --git a/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.ts b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.ts index cc8d5e759..ac94dfac4 100644 --- a/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.ts @@ -5,6 +5,7 @@ import { chartColors } from 'src/app/app.constants'; import { ApiService } from 'src/app/services/api.service'; import { SeoService } from 'src/app/services/seo.service'; import { download } from 'src/app/shared/graphs.utils'; +import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; @Component({ selector: 'app-nodes-per-as-chart', @@ -30,6 +31,7 @@ export class NodesPerAsChartComponent implements OnInit { constructor( private apiService: ApiService, private seoService: SeoService, + private amountShortenerPipe: AmountShortenerPipe ) { } @@ -89,7 +91,9 @@ export class NodesPerAsChartComponent implements OnInit { borderColor: '#000', formatter: () => { return `${as.name} (${as.share}%)
` + - $localize`${as.count.toString()} nodes`; + $localize`${as.count.toString()} nodes
` + + $localize`${this.amountShortenerPipe.transform(as.capacity / 100000000, 2)} BTC capacity` + ; } }, data: as.slug, From 0c71e11cda1dd77c8b902497e36ed729d154322b Mon Sep 17 00:00:00 2001 From: nymkappa Date: Sat, 16 Jul 2022 21:00:32 +0200 Subject: [PATCH 09/52] Move TV button to `/graphs/mempool` graph page --- .../app/components/master-page/master-page.component.html | 3 --- .../app/components/statistics/statistics.component.html | 7 +++++++ .../src/app/components/statistics/statistics.component.ts | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) 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 564bd1b1e..a4979e00d 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -44,9 +44,6 @@ - diff --git a/frontend/src/app/components/statistics/statistics.component.html b/frontend/src/app/components/statistics/statistics.component.html index ddeda4d80..dc168b877 100644 --- a/frontend/src/app/components/statistics/statistics.component.html +++ b/frontend/src/app/components/statistics/statistics.component.html @@ -11,6 +11,13 @@
+
+ +