diff --git a/backend/src/api/difficulty-adjustment.ts b/backend/src/api/difficulty-adjustment.ts new file mode 100644 index 000000000..6fc1ed794 --- /dev/null +++ b/backend/src/api/difficulty-adjustment.ts @@ -0,0 +1,71 @@ +import config from '../config'; +import { IDifficultyAdjustment } from '../mempool.interfaces'; +import blocks from './blocks'; + +class DifficultyAdjustmentApi { + constructor() { } + + public getDifficultyAdjustment(): IDifficultyAdjustment { + const DATime = blocks.getLastDifficultyAdjustmentTime(); + const previousRetarget = blocks.getPreviousDifficultyRetarget(); + const blockHeight = blocks.getCurrentBlockHeight(); + const blocksCache = blocks.getBlocks(); + const latestBlock = blocksCache[blocksCache.length - 1]; + + const now = new Date().getTime() / 1000; + const diff = now - DATime; + const blocksInEpoch = blockHeight % 2016; + const progressPercent = (blocksInEpoch >= 0) ? blocksInEpoch / 2016 * 100 : 100; + const remainingBlocks = 2016 - blocksInEpoch; + const nextRetargetHeight = blockHeight + remainingBlocks; + + let difficultyChange = 0; + if (remainingBlocks < 1870) { + if (blocksInEpoch > 0) { + difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100; + } + if (difficultyChange > 300) { + difficultyChange = 300; + } + if (difficultyChange < -75) { + difficultyChange = -75; + } + } + + const timeAvgDiff = difficultyChange * 0.1; + + let timeAvgMins = 10; + + if (timeAvgDiff > 0) { + timeAvgMins -= Math.abs(timeAvgDiff); + } else { + timeAvgMins += Math.abs(timeAvgDiff); + } + + // Testnet difficulty is set to 1 after 20 minutes of no blockSize, + // therefore the time between blocks will always be below 20 minutes (1200s). + let timeOffset = 0; + if (config.MEMPOOL.NETWORK === 'testnet' && now - latestBlock.timestamp + timeAvgMins * 60 > 1200) { + timeOffset = -Math.min(now - latestBlock.timestamp, 1200) * 1000; + timeAvgMins = 20; + } + + const timeAvg = timeAvgMins * 60 * 1000 ; + const remainingTime = (remainingBlocks * timeAvg) + (now * 1000); + const estimatedRetargetDate = remainingTime + now; + + return { + progressPercent, + difficultyChange, + estimatedRetargetDate, + remainingBlocks, + remainingTime, + previousRetarget, + nextRetargetHeight, + timeAvg, + timeOffset, + }; + } +} + +export default new DifficultyAdjustmentApi(); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 87d1fb59f..6e2f4fb24 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -12,6 +12,7 @@ import loadingIndicators from './loading-indicators'; import config from '../config'; import transactionUtils from './transaction-utils'; import rbfCache from './rbf-cache'; +import difficultyAdjustment from './difficulty-adjustment'; class WebsocketHandler { private wss: WebSocket.Server | undefined; @@ -191,14 +192,13 @@ class WebsocketHandler { return { 'mempoolInfo': memPool.getMempoolInfo(), 'vBytesPerSecond': memPool.getVBytesPerSecond(), - 'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(), - 'previousRetarget': blocks.getPreviousDifficultyRetarget(), 'blocks': _blocks, 'conversions': fiatConversion.getConversionRates(), 'mempool-blocks': mempoolBlocks.getMempoolBlocks(), 'transactions': memPool.getLatestTransactions(), 'backendInfo': backendInfo.getBackendInfo(), 'loadingIndicators': loadingIndicators.getLoadingIndicators(), + 'da': difficultyAdjustment.getDifficultyAdjustment(), ...this.extraInitProperties }; } @@ -234,6 +234,7 @@ class WebsocketHandler { const mempoolInfo = memPool.getMempoolInfo(); const vBytesPerSecond = memPool.getVBytesPerSecond(); const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); + const da = difficultyAdjustment.getDifficultyAdjustment(); memPool.handleRbfTransactions(rbfTransactions); this.wss.clients.forEach(async (client: WebSocket) => { @@ -247,6 +248,7 @@ class WebsocketHandler { response['mempoolInfo'] = mempoolInfo; response['vBytesPerSecond'] = vBytesPerSecond; response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx)); + response['da'] = da; } if (client['want-mempool-blocks']) { @@ -410,8 +412,7 @@ class WebsocketHandler { const response = { 'block': block, 'mempoolInfo': memPool.getMempoolInfo(), - 'lastDifficultyAdjustment': blocks.getLastDifficultyAdjustmentTime(), - 'previousRetarget': blocks.getPreviousDifficultyRetarget(), + 'da': difficultyAdjustment.getDifficultyAdjustment(), }; if (mBlocks && client['want-mempool-blocks']) { diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index bda702ccb..b5cbb7e11 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -197,3 +197,15 @@ export interface IBackendInfo { gitCommit: string; version: string; } + +export interface IDifficultyAdjustment { + progressPercent: number; + difficultyChange: number; + estimatedRetargetDate: number; + remainingBlocks: number; + remainingTime: number; + previousRetarget: number; + nextRetargetHeight: number; + timeAvg: number; + timeOffset: number; +} diff --git a/backend/src/routes.ts b/backend/src/routes.ts index c6b3656e7..7f3a98296 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -25,6 +25,7 @@ import axios from 'axios'; import mining from './api/mining'; import BlocksRepository from './repositories/BlocksRepository'; import HashratesRepository from './repositories/HashratesRepository'; +import difficultyAdjustment from './api/difficulty-adjustment'; class Routes { constructor() {} @@ -847,55 +848,7 @@ class Routes { public getDifficultyChange(req: Request, res: Response) { try { - const DATime = blocks.getLastDifficultyAdjustmentTime(); - const previousRetarget = blocks.getPreviousDifficultyRetarget(); - const blockHeight = blocks.getCurrentBlockHeight(); - - const now = new Date().getTime() / 1000; - const diff = now - DATime; - const blocksInEpoch = blockHeight % 2016; - const progressPercent = (blocksInEpoch >= 0) ? blocksInEpoch / 2016 * 100 : 100; - const remainingBlocks = 2016 - blocksInEpoch; - const nextRetargetHeight = blockHeight + remainingBlocks; - - let difficultyChange = 0; - if (remainingBlocks < 1870) { - if (blocksInEpoch > 0) { - difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100; - } - if (difficultyChange > 300) { - difficultyChange = 300; - } - if (difficultyChange < -75) { - difficultyChange = -75; - } - } - - const timeAvgDiff = difficultyChange * 0.1; - - let timeAvgMins = 10; - if (timeAvgDiff > 0) { - timeAvgMins -= Math.abs(timeAvgDiff); - } else { - timeAvgMins += Math.abs(timeAvgDiff); - } - - const timeAvg = timeAvgMins * 60; - const remainingTime = remainingBlocks * timeAvg; - const estimatedRetargetDate = remainingTime + now; - - const result = { - progressPercent, - difficultyChange, - estimatedRetargetDate, - remainingBlocks, - remainingTime, - previousRetarget, - nextRetargetHeight, - timeAvg, - }; - res.json(result); - + res.json(difficultyAdjustment.getDifficultyAdjustment()); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); } diff --git a/frontend/src/app/components/difficulty/difficulty.component.ts b/frontend/src/app/components/difficulty/difficulty.component.ts index b22001ef1..3e949cc5c 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.ts +++ b/frontend/src/app/components/difficulty/difficulty.component.ts @@ -6,12 +6,11 @@ import { StateService } from '../..//services/state.service'; interface EpochProgress { base: string; change: number; - progress: string; + progress: number; remainingBlocks: number; newDifficultyHeight: number; colorAdjustments: string; colorPreviousAdjustments: string; - timeAvg: string; remainingTime: number; previousRetarget: number; blocksUntilHalving: number; @@ -38,85 +37,52 @@ export class DifficultyComponent implements OnInit { ngOnInit(): void { this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$; - this.difficultyEpoch$ = timer(0, 1000) - .pipe( - switchMap(() => combineLatest([ - this.stateService.blocks$.pipe(map(([block]) => block)), - this.stateService.lastDifficultyAdjustment$, - this.stateService.previousRetarget$ - ])), - map(([block, DATime, previousRetarget]) => { - const now = new Date().getTime() / 1000; - const diff = now - DATime; - const blocksInEpoch = block.height % 2016; - const progress = (blocksInEpoch >= 0) ? (blocksInEpoch / 2016 * 100).toFixed(2) : `100`; - const remainingBlocks = 2016 - blocksInEpoch; - const newDifficultyHeight = block.height + remainingBlocks; + this.difficultyEpoch$ = combineLatest([ + this.stateService.blocks$.pipe(map(([block]) => block)), + this.stateService.difficultyAdjustment$, + ]) + .pipe( + map(([block, da]) => { + let colorAdjustments = '#ffffff66'; + if (da.difficultyChange > 0) { + colorAdjustments = '#3bcc49'; + } + if (da.difficultyChange < 0) { + colorAdjustments = '#dc3545'; + } - let change = 0; - if (remainingBlocks < 1870) { - if (blocksInEpoch > 0) { - change = (600 / (diff / blocksInEpoch ) - 1) * 100; - } - if (change > 300) { - change = 300; - } - if (change < -75) { - change = -75; - } + let colorPreviousAdjustments = '#dc3545'; + if (da.previousRetarget) { + if (da.previousRetarget >= 0) { + colorPreviousAdjustments = '#3bcc49'; } - - const timeAvgDiff = change * 0.1; - - let timeAvgMins = 10; - if (timeAvgDiff > 0) { - timeAvgMins -= Math.abs(timeAvgDiff); - } else { - timeAvgMins += Math.abs(timeAvgDiff); - } - - const timeAvg = timeAvgMins.toFixed(0); - const remainingTime = (remainingBlocks * timeAvgMins * 60 * 1000) + (now * 1000); - - let colorAdjustments = '#ffffff66'; - if (change > 0) { - colorAdjustments = '#3bcc49'; - } - if (change < 0) { - colorAdjustments = '#dc3545'; - } - - let colorPreviousAdjustments = '#dc3545'; - if (previousRetarget) { - if (previousRetarget >= 0) { - colorPreviousAdjustments = '#3bcc49'; - } - if (previousRetarget === 0) { - colorPreviousAdjustments = '#ffffff66'; - } - } else { + if (da.previousRetarget === 0) { colorPreviousAdjustments = '#ffffff66'; } + } else { + colorPreviousAdjustments = '#ffffff66'; + } - const blocksUntilHalving = 210000 - (block.height % 210000); - const timeUntilHalving = (blocksUntilHalving * timeAvgMins * 60 * 1000) + (now * 1000); + const timeAvgMins = da.timeAvg; + const now = new Date().getTime() / 1000; + const blocksUntilHalving = 210000 - (block.height % 210000); + const timeUntilHalving = (blocksUntilHalving * timeAvgMins * 60 * 1000) + (now * 1000); - return { - base: `${progress}%`, - change, - progress, - remainingBlocks, - timeAvg, - colorAdjustments, - colorPreviousAdjustments, - blocksInEpoch, - newDifficultyHeight, - remainingTime, - previousRetarget, - blocksUntilHalving, - timeUntilHalving, - }; - }) - ); + const data = { + base: `${da.progressPercent.toFixed(2)}%`, + change: da.difficultyChange, + progress: da.progressPercent, + remainingBlocks: da.remainingBlocks, + colorAdjustments, + colorPreviousAdjustments, + newDifficultyHeight: da.nextRetargetHeight, + remainingTime: da.remainingTime, + previousRetarget: da.previousRetarget, + blocksUntilHalving, + timeUntilHalving, + }; + return data; + }) + ); } } diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html index 007916cfd..ecb8163c1 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html @@ -1,5 +1,5 @@ -
+
@@ -26,7 +26,7 @@ - +
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 c03325cfe..db5f06b57 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -8,6 +8,7 @@ import { feeLevels, mempoolFeeColors } from 'src/app/app.constants'; import { specialBlocks } from 'src/app/app.constants'; import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; import { Location } from '@angular/common'; +import { DifficultyAdjustment } from 'src/app/interfaces/node-api.interface'; @Component({ selector: 'app-mempool-blocks', @@ -20,7 +21,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { mempoolBlocks: MempoolBlock[] = []; mempoolEmptyBlocks: MempoolBlock[] = this.mountEmptyBlocks(); mempoolBlocks$: Observable; - timeAvg$: Observable; + difficultyAdjustments$: Observable; loadingBlocks$: Observable; blocksSubscription: Subscription; @@ -123,40 +124,11 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy { }) ); - this.timeAvg$ = timer(0, 1000) + this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$ .pipe( - switchMap(() => combineLatest([ - this.stateService.blocks$.pipe(map(([block]) => block)), - this.stateService.lastDifficultyAdjustment$ - ])), - map(([block, DATime]) => { + map((da) => { this.now = new Date().getTime(); - const now = new Date().getTime() / 1000; - const diff = now - DATime; - const blocksInEpoch = block.height % 2016; - let difficultyChange = 0; - if (blocksInEpoch > 0) { - difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100; - } - const timeAvgDiff = difficultyChange * 0.1; - - let timeAvgMins = 10; - if (timeAvgDiff > 0 ){ - timeAvgMins -= Math.abs(timeAvgDiff); - } else { - timeAvgMins += Math.abs(timeAvgDiff); - } - - // testnet difficulty is set to 1 after 20 minutes of no blockSize - // therefore the time between blocks will always be below 20 minutes (1200s) - if (this.stateService.network === 'testnet' && now - block.timestamp + timeAvgMins * 60 > 1200) { - this.timeOffset = -Math.min(now - block.timestamp, 1200) * 1000; - timeAvgMins = 20; - } else { - this.timeOffset = 0; - } - - return timeAvgMins * 60 * 1000; + return da; }) ); diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 4788b5657..c76fab3aa 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -66,29 +66,8 @@ export class TransactionComponent implements OnInit, OnDestroy { this.timeAvg$ = timer(0, 1000) .pipe( - switchMap(() => combineLatest([ - this.stateService.blocks$.pipe(map(([block]) => block)), - this.stateService.lastDifficultyAdjustment$ - ])), - map(([block, DATime]) => { - this.now = new Date().getTime(); - const now = new Date().getTime() / 1000; - const diff = now - DATime; - const blocksInEpoch = block.height % 2016; - let difficultyChange = 0; - if (blocksInEpoch > 0) { - difficultyChange = (600 / (diff / blocksInEpoch ) - 1) * 100; - } - const timeAvgDiff = difficultyChange * 0.1; - - let timeAvgMins = 10; - if (timeAvgDiff > 0 ){ - timeAvgMins -= Math.abs(timeAvgDiff); - } else { - timeAvgMins += Math.abs(timeAvgDiff); - } - return timeAvgMins * 60 * 1000; - }) + switchMap(() => this.stateService.difficultyAdjustment$), + map((da) => da.timeAvg) ); this.fetchCpfpSubscription = this.fetchCpfp$ diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index faa78560a..8caba7496 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -26,12 +26,15 @@ export interface CpfpInfo { } export interface DifficultyAdjustment { + progressPercent: number; difficultyChange: number; estimatedRetargetDate: number; - previousRetarget: number; - progressPercent: number; remainingBlocks: number; remainingTime: number; + previousRetarget: number; + nextRetargetHeight: number; + timeAvg: number; + timeOffset: number; } export interface AddressInformation { @@ -111,4 +114,3 @@ export interface BlockExtension { export interface BlockExtended extends Block { extras?: BlockExtension; } - diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index b0ae27f73..f55b62bf4 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -1,6 +1,6 @@ import { ILoadingIndicators } from '../services/state.service'; import { Transaction } from './electrs.interface'; -import { BlockExtended } from './node-api.interface'; +import { BlockExtended, DifficultyAdjustment } from './node-api.interface'; export interface WebsocketResponse { block?: BlockExtended; @@ -10,7 +10,6 @@ export interface WebsocketResponse { historicalDate?: string; mempoolInfo?: MempoolInfo; vBytesPerSecond?: number; - lastDifficultyAdjustment?: number; previousRetarget?: number; action?: string; data?: string[]; @@ -21,6 +20,7 @@ export interface WebsocketResponse { transactions?: TransactionStripped[]; loadingIndicators?: ILoadingIndicators; backendInfo?: IBackendInfo; + da?: DifficultyAdjustment; 'track-tx'?: string; 'track-address'?: string; 'track-asset'?: string; diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index da228a833..cf510c449 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs, ITranslators, PoolsStats, PoolStat, BlockExtended, BlockExtension } from '../interfaces/node-api.interface'; +import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolsStats, PoolStat, BlockExtended } from '../interfaces/node-api.interface'; import { Observable } from 'rxjs'; import { StateService } from './state.service'; import { WebsocketResponse } from '../interfaces/websocket.interface'; @@ -105,10 +105,6 @@ export class ApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/cpfp/' + txid); } - getDifficultyAdjustment$(): Observable { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/difficulty-adjustment'); - } - validateAddress$(address: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/validate-address/' + address); } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 5e181063b..85b7674e1 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -2,7 +2,7 @@ import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs'; import { Transaction } from '../interfaces/electrs.interface'; import { IBackendInfo, MempoolBlock, MempoolInfo, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface'; -import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; +import { BlockExtended, DifficultyAdjustment, OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; import { map, shareReplay } from 'rxjs/operators'; @@ -82,11 +82,11 @@ export class StateService { mempoolBlocks$ = new ReplaySubject(1); txReplaced$ = new Subject(); utxoSpent$ = new Subject(); + difficultyAdjustment$ = new ReplaySubject(1); mempoolTransactions$ = new Subject(); blockTransactions$ = new Subject(); isLoadingWebSocket$ = new ReplaySubject(1); vbytesPerSecond$ = new ReplaySubject(1); - lastDifficultyAdjustment$ = new ReplaySubject(1); previousRetarget$ = new ReplaySubject(1); backendInfo$ = new ReplaySubject(1); loadingIndicators$ = new ReplaySubject(1); diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 1eb9a56e6..464b4dc1a 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -259,6 +259,10 @@ export class WebsocketService { this.stateService.utxoSpent$.next(response.utxoSpent); } + if (response.da) { + this.stateService.difficultyAdjustment$.next(response.da); + } + if (response.backendInfo) { this.stateService.backendInfo$.next(response.backendInfo); @@ -301,10 +305,6 @@ export class WebsocketService { this.stateService.vbytesPerSecond$.next(response.vBytesPerSecond); } - if (response.lastDifficultyAdjustment !== undefined) { - this.stateService.lastDifficultyAdjustment$.next(response.lastDifficultyAdjustment); - } - if (response.previousRetarget !== undefined) { this.stateService.previousRetarget$.next(response.previousRetarget); }