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 |
+
+ 0; else loadingAdjustment">
+ 1 ? 'var(--green)' : (cacheService.daCache[block.height].adjustment < 1 ? 'var(--red)' : '')">
+ @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 |
+
+ 0; else loadingAdjustment">
+ 1 ? 'var(--green)' : (cacheService.daCache[mempoolBlock.height].adjustment < 1 ? 'var(--red)' : '')">
+ @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));
+}