diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 40687060f..7a9589348 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -108,14 +108,23 @@ class Blocks { blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0); blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]); - const stats = await bitcoinClient.getBlockStats(block.id); const coinbaseRaw: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(transactions[0].txid, true); blockExtended.extras.coinbaseRaw = coinbaseRaw.hex; - blockExtended.extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles - blockExtended.extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(); - blockExtended.extras.totalFees = stats.totalfee; - blockExtended.extras.avgFee = stats.avgfee; - blockExtended.extras.avgFeeRate = stats.avgfeerate; + + if (block.height === 0) { + blockExtended.extras.medianFee = 0; // 50th percentiles + blockExtended.extras.feeRange = [0, 0, 0, 0, 0, 0, 0]; + blockExtended.extras.totalFees = 0; + blockExtended.extras.avgFee = 0; + blockExtended.extras.avgFeeRate = 0; + } else { + const stats = await bitcoinClient.getBlockStats(block.id); + blockExtended.extras.medianFee = stats.feerate_percentiles[2]; // 50th percentiles + blockExtended.extras.feeRange = [stats.minfeerate, stats.feerate_percentiles, stats.maxfeerate].flat(); + blockExtended.extras.totalFees = stats.totalfee; + blockExtended.extras.avgFee = stats.avgfee; + blockExtended.extras.avgFeeRate = stats.avgfeerate; + } if (Common.indexingEnabled()) { let pool: PoolTag; @@ -336,10 +345,13 @@ class Blocks { await blocksRepository.$saveBlockInDatabase(blockExtended); - return blockExtended; + return this.prepareBlock(blockExtended); } - public async $getBlocksExtras(fromHeight: number): Promise { + public async $getBlocksExtras(fromHeight: number, limit: number = 15): Promise { + // Note - This API is breaking if indexing is not available. For now it is okay because we only + // use it for the mining pages, and mining pages should not be available if indexing is turned off. + // I'll need to fix it before we refactor the block(s) related pages try { loadingIndicators.setProgress('blocks', 0); @@ -360,10 +372,10 @@ class Blocks { } let nextHash = startFromHash; - for (let i = 0; i < 10 && currentHeight >= 0; i++) { + for (let i = 0; i < limit && currentHeight >= 0; i++) { let block = this.getBlocks().find((b) => b.height === currentHeight); if (!block && Common.indexingEnabled()) { - block = this.prepareBlock(await this.$indexBlock(currentHeight)); + block = await this.$indexBlock(currentHeight); } else if (!block) { block = this.prepareBlock(await bitcoinApi.$getBlock(nextHash)); } @@ -383,24 +395,25 @@ class Blocks { private prepareBlock(block: any): BlockExtended { return { id: block.id ?? block.hash, // hash for indexed block - timestamp: block?.timestamp ?? block?.blockTimestamp, // blockTimestamp for indexed block - height: block?.height, - version: block?.version, - bits: block?.bits, - nonce: block?.nonce, - difficulty: block?.difficulty, - merkle_root: block?.merkle_root, - tx_count: block?.tx_count, - size: block?.size, - weight: block?.weight, - previousblockhash: block?.previousblockhash, + timestamp: block.timestamp ?? block.blockTimestamp, // blockTimestamp for indexed block + height: block.height, + version: block.version, + bits: block.bits, + nonce: block.nonce, + difficulty: block.difficulty, + merkle_root: block.merkle_root, + tx_count: block.tx_count, + size: block.size, + weight: block.weight, + previousblockhash: block.previousblockhash, extras: { - medianFee: block?.medianFee, - feeRange: block?.feeRange ?? [], // TODO - reward: block?.reward, + medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee, + feeRange: block.feeRange ?? block.fee_range ?? block?.extras?.feeSpan, + reward: block.reward ?? block?.extras?.reward, + totalFees: block.totalFees ?? block?.fees ?? block?.extras.totalFees, pool: block?.extras?.pool ?? (block?.pool_id ? { - id: block?.pool_id, - name: block?.pool_name, + id: block.pool_id, + name: block.pool_name, } : undefined), } }; diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 0cab3c0db..041086f73 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -277,7 +277,10 @@ class BlocksRepository { const connection = await DB.pool.getConnection(); try { const [rows]: any[] = await connection.query(` - SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.addresses as pool_addresses, pools.regexes as pool_regexes + SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, + pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, + pools.addresses as pool_addresses, pools.regexes as pool_regexes, + previous_block_hash as previousblockhash FROM blocks JOIN pools ON blocks.pool_id = pools.id WHERE height = ${height}; diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 710cd8378..c6b3656e7 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -658,7 +658,7 @@ class Routes { public async getBlocksExtras(req: Request, res: Response) { try { - res.json(await blocks.$getBlocksExtras(parseInt(req.params.height, 10))) + res.json(await blocks.$getBlocksExtras(parseInt(req.params.height, 10), 15)); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 19285fc8f..1ef7e5fe0 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -31,6 +31,7 @@ import { MiningDashboardComponent } from './components/mining-dashboard/mining-d import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component'; import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component'; import { MiningStartComponent } from './components/mining-start/mining-start.component'; +import { BlocksList } from './components/blocks-list/blocks-list.component'; let routes: Routes = [ { @@ -75,6 +76,10 @@ let routes: Routes = [ path: 'mining', component: MiningStartComponent, children: [ + { + path: 'blocks', + component: BlocksList, + }, { path: 'hashrate', component: HashrateChartComponent, @@ -190,6 +195,10 @@ let routes: Routes = [ path: 'mining', component: MiningStartComponent, children: [ + { + path: 'blocks', + component: BlocksList, + }, { path: 'hashrate', component: HashrateChartComponent, @@ -299,6 +308,10 @@ let routes: Routes = [ path: 'mining', component: MiningStartComponent, children: [ + { + path: 'blocks', + component: BlocksList, + }, { path: 'hashrate', component: HashrateChartComponent, @@ -630,7 +643,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { initialNavigation: 'enabled', scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled' -})], + })], exports: [RouterModule] }) export class AppRoutingModule { } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 0dfc853cc..3affdc7ba 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -76,6 +76,7 @@ import { MiningStartComponent } from './components/mining-start/mining-start.com import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe'; import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments-table/difficulty-adjustments-table.components'; +import { BlocksList } from './components/blocks-list/blocks-list.component'; @NgModule({ declarations: [ @@ -133,6 +134,7 @@ import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments- MiningStartComponent, AmountShortenerPipe, DifficultyAdjustmentsTable, + BlocksList, ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.html b/frontend/src/app/components/blocks-list/blocks-list.component.html new file mode 100644 index 000000000..f50a5fff2 --- /dev/null +++ b/frontend/src/app/components/blocks-list/blocks-list.component.html @@ -0,0 +1,96 @@ +
+

Blocks

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Height + PoolTimestampMined + RewardFeesTxsSize
+ {{ block.height + }} + + + + {{ block.extras.pool.name }} + + + ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} + + + + + + + + {{ block.tx_count | number }} + +
+
+
+
+
+ + + + + + + + + + + + + + + + +
+ + + +
+ +
\ No newline at end of file diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.scss b/frontend/src/app/components/blocks-list/blocks-list.component.scss new file mode 100644 index 000000000..9414348c1 --- /dev/null +++ b/frontend/src/app/components/blocks-list/blocks-list.component.scss @@ -0,0 +1,124 @@ +.container-xl { + max-width: 1400px; + padding-bottom: 100px; +} +.container-xl.widget { + padding-left: 0px; + padding-bottom: 0px; +} + +.container { + max-width: 100%; +} + +td { + padding-top: 0.7rem !important; + padding-bottom: 0.7rem !important; +} + +.clear-link { + color: white; +} + +.disabled { + pointer-events: none; + opacity: 0.5; +} + +.progress { + background-color: #2d3348; +} + +.pool { + width: 17%; +} +.pool.widget { + width: 40%; + @media (max-width: 576px) { + padding-left: 30px; + width: 60%; + } +} +.pool-name { + display: inline-block; + vertical-align: text-top; + padding-left: 10px; +} + +.height { + width: 10%; + @media (max-width: 1100px) { + width: 10%; + } +} +.height.widget { + width: 20%; + @media (max-width: 576px) { + width: 10%; + } +} + +.timestamp { + @media (max-width: 900px) { + display: none; + } +} + +.mined { + width: 13%; + @media (max-width: 576px) { + display: none; + } +} + +.txs { + padding-right: 40px; + @media (max-width: 1100px) { + padding-right: 10px; + } + @media (max-width: 875px) { + display: none; + } +} +.txs.widget { + padding-right: 0; + @media (max-width: 650px) { + display: none; + } +} + +.fees { + @media (max-width: 650px) { + display: none; + } +} +.fees.widget { + width: 20%; +} + +.reward { + @media (max-width: 576px) { + width: 7%; + padding-right: 30px; + } +} +.reward.widget { + width: 20%; + @media (max-width: 576px) { + width: 30%; + padding-right: 0; + } +} + +.size { + width: 12%; + @media (max-width: 1000px) { + width: 15%; + } + @media (max-width: 650px) { + width: 20%; + } + @media (max-width: 450px) { + display: none; + } +} diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.ts b/frontend/src/app/components/blocks-list/blocks-list.component.ts new file mode 100644 index 000000000..72727b734 --- /dev/null +++ b/frontend/src/app/components/blocks-list/blocks-list.component.ts @@ -0,0 +1,98 @@ +import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; +import { BehaviorSubject, combineLatest, Observable, timer } from 'rxjs'; +import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators'; +import { BlockExtended } from 'src/app/interfaces/node-api.interface'; +import { ApiService } from 'src/app/services/api.service'; +import { StateService } from 'src/app/services/state.service'; +import { WebsocketService } from 'src/app/services/websocket.service'; + +@Component({ + selector: 'app-blocks-list', + templateUrl: './blocks-list.component.html', + styleUrls: ['./blocks-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BlocksList implements OnInit { + @Input() widget: boolean = false; + + blocks$: Observable = undefined; + + isLoading = true; + fromBlockHeight = undefined; + paginationMaxSize: number; + page = 1; + lastPage = 1; + blocksCount: number; + fromHeightSubject: BehaviorSubject = new BehaviorSubject(this.fromBlockHeight); + skeletonLines: number[] = []; + + constructor( + private apiService: ApiService, + private websocketService: WebsocketService, + public stateService: StateService, + ) { + } + + ngOnInit(): void { + this.websocketService.want(['blocks']); + + this.skeletonLines = this.widget === true ? [...Array(5).keys()] : [...Array(15).keys()]; + this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; + + this.blocks$ = combineLatest([ + this.fromHeightSubject.pipe( + switchMap((fromBlockHeight) => { + this.isLoading = true; + return this.apiService.getBlocks$(this.page === 1 ? undefined : fromBlockHeight) + .pipe( + tap(blocks => { + if (this.blocksCount === undefined) { + this.blocksCount = blocks[0].height; + } + this.isLoading = false; + }), + map(blocks => { + for (const block of blocks) { + // @ts-ignore: Need to add an extra field for the template + block.extras.pool.logo = `./resources/mining-pools/` + + block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; + } + if (this.widget) { + return blocks.slice(0, 5); + } + return blocks; + }), + retryWhen(errors => errors.pipe(delayWhen(() => timer(1000)))) + ) + }) + ), + this.stateService.blocks$ + .pipe( + skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1), + ), + ]) + .pipe( + scan((acc, blocks) => { + if (this.page > 1 || acc.length === 0 || (this.page === 1 && this.lastPage !== 1)) { + this.lastPage = this.page; + return blocks[0]; + } + this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height); + // @ts-ignore: Need to add an extra field for the template + blocks[1][0].extras.pool.logo = `./resources/mining-pools/` + + blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'; + acc.unshift(blocks[1][0]); + acc = acc.slice(0, this.widget ? 5 : 15); + return acc; + }, []) + ); + } + + pageChange(page: number) { + this.fromHeightSubject.next(this.blocksCount - (page - 1) * 15); + } + + trackByBlock(index: number, block: BlockExtended) { + return block.height; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html index 68209fed0..1c8fb2c9d 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html @@ -95,7 +95,7 @@ -
+ + + +
+
+
+
+ Latest blocks +
+ + +
+
@@ -115,7 +129,7 @@ Adjustments -
diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 858da3273..142f26807 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -151,6 +151,13 @@ export class ApiService { ); } + getBlocks$(from: number): Observable { + return this.httpClient.get( + this.apiBasePath + this.apiBasePath + `/api/v1/blocks-extras` + + (from !== undefined ? `/${from}` : ``) + ); + } + getHistoricalDifficulty$(interval: string | undefined): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty` +