From b6b3e52436591ff9c6c756a1c9c2cf72c95cdf46 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 31 Jul 2024 15:26:26 +0200 Subject: [PATCH] Shades of green / red on DA blocks --- .../app/components/block/block.component.html | 23 +++++++++++ .../app/components/block/block.component.scss | 8 ++++ .../app/components/block/block.component.ts | 2 +- .../blockchain-blocks.component.ts | 33 ++++++++++++++- ...difficulty-adjustments-table.components.ts | 5 ++- .../mempool-block.component.html | 19 +++++++++ .../mempool-block.component.scss | 8 ++++ .../mempool-block/mempool-block.component.ts | 2 + .../mempool-blocks.component.ts | 40 +++++++++++++++++-- .../src/app/docs/api-docs/api-docs-data.ts | 33 +++++++++++++++ frontend/src/app/services/cache.service.ts | 2 + frontend/src/app/shared/common.utils.ts | 22 ++++++++++ 12 files changed, 190 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 1dd9d8a8d..11114867b 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -78,6 +78,25 @@ + + Adjustment + + +
+ @if (cacheService.daCache[block.height].adjustment > 1) { + + } @else if (cacheService.daCache[block.height].adjustment < 1) { + + } + {{ (cacheService.daCache[block.height].adjustment - 1) * 100 | absolute | number: '1.2-2' }} + % +
+
+ + + + + @@ -193,6 +212,10 @@ + + Difficulty + {{ block.difficulty | amountShortener: 2 }} + diff --git a/frontend/src/app/components/block/block.component.scss b/frontend/src/app/components/block/block.component.scss index fe5318375..e8697d780 100644 --- a/frontend/src/app/components/block/block.component.scss +++ b/frontend/src/app/components/block/block.component.scss @@ -280,3 +280,11 @@ h1 { top: -1px; margin-right: 2px; } + +.retarget-sign { + margin-right: -3px; + &.up { + position: relative; + top: 2px; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 5cba85e90..a58da7153 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -99,7 +99,7 @@ export class BlockComponent implements OnInit, OnDestroy { private relativeUrlPipe: RelativeUrlPipe, private apiService: ApiService, private priceService: PriceService, - private cacheService: CacheService, + public cacheService: CacheService, private servicesApiService: ServicesApiServices, private cd: ChangeDetectorRef, private preloadService: PreloadService, diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index 1a7598079..a7827f1dc 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -1,10 +1,12 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core'; -import { Observable, Subscription, delay, filter, tap } from 'rxjs'; +import { Observable, Subscription, delay, filter, of, retryWhen, switchMap, take, tap, throwError } from 'rxjs'; import { StateService } from '../../services/state.service'; import { specialBlocks } from '../../app.constants'; import { BlockExtended } from '../../interfaces/node-api.interface'; import { Location } from '@angular/common'; import { CacheService } from '../../services/cache.service'; +import { ApiService } from '../../services/api.service'; +import { colorFromRetarget } from '../../shared/common.utils'; interface BlockchainBlock extends BlockExtended { placeholder?: boolean; @@ -77,6 +79,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { constructor( public stateService: StateService, public cacheService: CacheService, + public apiService: ApiService, private cd: ChangeDetectorRef, private location: Location, ) { @@ -334,6 +337,31 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { return this.specialBlocks[height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false; } + isDA(height: number): boolean { + const isDA = height % 2016 === 0 && this.stateService.network === ''; + if (isDA && !this.cacheService.daCache[height]?.exact) { + const estimatedAdjustment = this.cacheService.daCache[height]?.adjustment || 0; + this.cacheService.daCache[height] = { adjustment: estimatedAdjustment, exact: true }; + this.apiService.getDifficultyAdjustmentByHeight$(height).pipe( + switchMap(da => { + const blocksAvailable = (this.height || this.chainTip) && this.blockStyles[(this.height || this.chainTip) - height]; + return blocksAvailable ? of(da) : throwError(() => new Error()); + }), + retryWhen(errors => + errors.pipe( + delay(1000), + take(3) + ) + ), + tap((da) => { + this.cacheService.daCache[height] = { adjustment: da?.adjustment || 1, exact: true }; + this.blockStyles[(this.height || this.chainTip) - height].background = colorFromRetarget(da?.adjustment); + }) + ).subscribe(); + } + return isDA; + } + getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) { if (!block || block.placeholder) { return this.getStyleForPlaceholderBlock(index, animateEnterFrom); @@ -349,7 +377,8 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { return { left: addLeft + this.blockOffset * index + 'px', - background: `repeating-linear-gradient( + background: this.isDA(block.height) ? colorFromRetarget(this.cacheService.daCache[block.height]?.adjustment || 1) : + `repeating-linear-gradient( var(--secondary), var(--secondary) ${greenBackgroundHeight}%, ${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%, diff --git a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts index 438a50f74..cbbf7adcd 100644 --- a/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts +++ b/frontend/src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.components.ts @@ -5,6 +5,7 @@ import { ApiService } from '../../services/api.service'; import { formatNumber } from '@angular/common'; import { selectPowerOfTen } from '../../bitcoin.utils'; import { StateService } from '../../services/state.service'; +import { CacheService } from '../../services/cache.service'; @Component({ selector: 'app-difficulty-adjustments-table', @@ -27,7 +28,8 @@ export class DifficultyAdjustmentsTable implements OnInit { constructor( @Inject(LOCALE_ID) public locale: string, private apiService: ApiService, - public stateService: StateService + public stateService: StateService, + private cacheService: CacheService, ) { } @@ -52,6 +54,7 @@ export class DifficultyAdjustmentsTable implements OnInit { adjustment[2] / selectedPowerOfTen.divider, this.locale, `1.${decimals}-${decimals}`) + selectedPowerOfTen.unit }); + this.cacheService.daCache[adjustment[1]] = { adjustment: adjustment[3], exact: true }; } this.isLoading = false; return tableData.slice(0, 6); diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.html b/frontend/src/app/components/mempool-block/mempool-block.component.html index d2aa1aed2..d5b5b6c6f 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.html +++ b/frontend/src/app/components/mempool-block/mempool-block.component.html @@ -41,6 +41,25 @@ + + Adjustment + + +
+ @if (cacheService.daCache[mempoolBlock.height].adjustment > 1) { + + } @else if (cacheService.daCache[mempoolBlock.height].adjustment < 1) { + + } + {{ (cacheService.daCache[mempoolBlock.height].adjustment - 1) * 100 | absolute | number: '1.2-2' }} + % +
+
+ + + + + diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.scss b/frontend/src/app/components/mempool-block/mempool-block.component.scss index b2f9419b8..c727bce32 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.scss +++ b/frontend/src/app/components/mempool-block/mempool-block.component.scss @@ -36,3 +36,11 @@ h1 { margin: auto; } } + +.retarget-sign { + margin-right: -3px; + &.up { + position: relative; + top: 2px; + } +} diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts index d2e658302..68dea1d1e 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.ts +++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts @@ -9,6 +9,7 @@ import { Observable, BehaviorSubject } from 'rxjs'; import { SeoService } from '../../services/seo.service'; import { seoDescriptionNetwork } from '../../shared/common.utils'; import { WebsocketService } from '../../services/websocket.service'; +import { CacheService } from '../../services/cache.service'; @Component({ selector: 'app-mempool-block', @@ -30,6 +31,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { public stateService: StateService, private seoService: SeoService, private websocketService: WebsocketService, + public cacheService: CacheService, private cd: ChangeDetectorRef, @Inject(PLATFORM_ID) private platformId: Object, ) { diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index af5a91c65..b05bd3f6e 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core'; -import { Subscription, Observable, of, combineLatest } from 'rxjs'; +import { Subscription, Observable, of, combineLatest, throwError } from 'rxjs'; import { MempoolBlock } from '../../interfaces/websocket.interface'; import { StateService } from '../../services/state.service'; import { EtaService } from '../../services/eta.service'; import { Router } from '@angular/router'; -import { delay, filter, map, switchMap, tap } from 'rxjs/operators'; +import { delay, filter, map, retryWhen, switchMap, take, tap } from 'rxjs/operators'; import { feeLevels } from '../../app.constants'; import { specialBlocks } from '../../app.constants'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; @@ -12,6 +12,8 @@ import { Location } from '@angular/common'; import { DifficultyAdjustment, MempoolPosition } from '../../interfaces/node-api.interface'; import { animate, style, transition, trigger } from '@angular/animations'; import { ThemeService } from '../../services/theme.service'; +import { CacheService } from '../../services/cache.service'; +import { colorFromRetarget } from '../../shared/common.utils'; @Component({ selector: 'app-mempool-blocks', @@ -93,6 +95,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { constructor( private router: Router, public stateService: StateService, + public cacheService: CacheService, private etaService: EtaService, private themeService: ThemeService, private cd: ChangeDetectorRef, @@ -387,6 +390,37 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { this.mempoolBlocksFull.forEach((block, i) => this.mempoolBlockStyles.push(this.getStyleForMempoolBlock(block, i))); } + isDA(height: number): boolean { + if (this.chainTip === -1) { + return false; + } + const isDA = height % 2016 === 0 && this.stateService.network === ''; + if (isDA && !this.cacheService.daCache[height]) { + this.cacheService.daCache[height] = { adjustment: 0 }; + this.difficultyAdjustments$.pipe( + filter(da => !!da), + switchMap(da => { + const mempoolBlocksAvailable = this.chainTip && this.mempoolBlockStyles[height - this.chainTip - 1]; + return mempoolBlocksAvailable ? of(da) : throwError(() => new Error()); + }), + retryWhen(errors => + errors.pipe( + delay(100), + take(3) + ) + ), + tap(da => { + const adjustment = parseFloat((1 + da.difficultyChange / 100).toFixed(4)); + if (adjustment !== this.cacheService.daCache[height].adjustment) { + this.cacheService.daCache[height].adjustment = adjustment; + this.mempoolBlockStyles[height - this.chainTip - 1].background = colorFromRetarget(adjustment); + } + }) + ).subscribe(); + } + return isDA; + } + getStyleForMempoolBlock(mempoolBlock: MempoolBlock, index: number) { const emptyBackgroundSpacePercentage = Math.max(100 - mempoolBlock.blockVSize / this.stateService.blockVSize * 100, 0); const usedBlockSpace = 100 - emptyBackgroundSpacePercentage; @@ -410,7 +444,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { return { 'right': this.containerOffset + index * this.blockOffset + 'px', - 'background': backgroundGradients.join(',') + ')' + 'background': this.isDA(mempoolBlock.height) ? colorFromRetarget(this.cacheService.daCache[mempoolBlock.height]?.adjustment || 1) : backgroundGradients.join(',') + ')' }; } diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index 12bb96166..b83b14e28 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -3670,6 +3670,39 @@ export const restApiDocsData = [ } } }, + { + type: "endpoint", + category: "mining", + httpRequestMethod: "GET", + fragment: "get-difficulty-adjustment-by-height", + title: "GET Difficulty Adjustment", + description: { + default: "

Returns difficulty adjustment for the block at the specified :blockHeight. If no adjustment happened at that height, an empty response is returned.

" + }, + urlString: "/v1/mining/difficulty-adjustment/:blockHeight", + showConditions: [""], + showJsExamples: showJsExamplesDefaultFalse, + codeExample: { + default: { + codeTemplate: { + curl: `/api/v1/mining/difficulty-adjustment/%{1}`, + commonJS: ``, + esModule: `` + }, + codeSampleMainnet: { + esModule: [], + commonJS: [], + curl: [`756000`], + response: `{ + "time": "2022-09-28T02:56:34.000Z", + "height": 756000, + "difficulty": 31360548173144.9, + "adjustment": 0.97863 +}` + }, + } + } + }, { type: "endpoint", category: "mining", diff --git a/frontend/src/app/services/cache.service.ts b/frontend/src/app/services/cache.service.ts index f15154b46..b9a491082 100644 --- a/frontend/src/app/services/cache.service.ts +++ b/frontend/src/app/services/cache.service.ts @@ -23,6 +23,7 @@ export class CacheService { blockLoading: { [height: number]: boolean } = {}; copiesInBlockQueue: { [height: number]: number } = {}; blockPriorities: number[] = []; + daCache: { [height: number]: { adjustment: number, exact?: boolean } } = {}; constructor( private stateService: StateService, @@ -128,6 +129,7 @@ export class CacheService { this.blockLoading = {}; this.copiesInBlockQueue = {}; this.blockPriorities = []; + this.daCache = {}; } getCachedBlock(height) { diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 8c69c2319..9fc7aee06 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -240,3 +240,25 @@ export function md5(inputString): string { } return rh(a)+rh(b)+rh(c)+rh(d); } + +export function colorFromRetarget(da: number): string { + const minDA = 0.95; + const maxDA = 1.05; + const midDA = 1; + + const red = { r: 220, g: 53, b: 69 }; + const grey = { r: 108, g: 117, b: 125 }; + const green = { r: 59, g: 204, b: 73 }; + + const interpolateColor = (color1, color2, ratio) => { + ratio = Math.min(1, Math.max(0, ratio)); + const r = Math.round(color1.r + ratio * (color2.r - color1.r)); + const g = Math.round(color1.g + ratio * (color2.g - color1.g)); + const b = Math.round(color1.b + ratio * (color2.b - color1.b)); + return `rgba(${r}, ${g}, ${b}, 0.7)`; + } + + return da <= midDA ? + interpolateColor(red, grey, (da - minDA) / (midDA - minDA)) : + interpolateColor(grey, green, (da - midDA) / (maxDA - midDA)); +}