From f4202e8cadbbaa2361fb306afd8e2419c9a9c797 Mon Sep 17 00:00:00 2001 From: Randall Naar Date: Mon, 29 Jun 2026 11:22:46 -0400 Subject: [PATCH] Added components for difficulty adjustment. --- client/src/app.js | 42 ++++ client/src/components/icons.js | 8 + client/src/lib/network.js | 5 + client/src/views/difficulty-adjustment.js | 260 ++++++++++++++++++++++ client/src/views/home.js | 5 + www/style.css | 50 +++++ 6 files changed, 370 insertions(+) create mode 100644 client/src/lib/network.js create mode 100644 client/src/views/difficulty-adjustment.js diff --git a/client/src/app.js b/client/src/app.js index 7196a1c5..8f9e9fbe 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -3,6 +3,7 @@ import { Observable as O } from './rxjs' import {setAdapt} from '@cycle/run/lib/adapt'; import { getMempoolDepth, getConfEstimate, calcSegwitFeeGains } from './lib/fees' +import { isBitcoinNetwork } from './lib/network' import getPrivacyAnalysis from './lib/privacy-analysis' import { nativeAssetId, blockTxsPerPage, blocksPerPage } from './const' import { @@ -32,6 +33,7 @@ const apiBase = (process.env.API_URL || '/api').replace(/\/+$/, '') const reservedPaths = [ 'mempool', 'assets', 'search' ] , NEW_TABLE_ENTRY_MS = 2000 + , DIFFICULTY_PERIOD = 2016 // Make driver source observables rxjs5-compatible via rxjs-compat setAdapt(stream => O.from(stream)) @@ -174,6 +176,24 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search .startWith([]).scan((S, mod) => mod(S)) .share() + , latestBlock$ = blocks$ + .map(blocks => blocks && blocks[0]) + .filter(Boolean) + .distinctUntilChanged((a, b) => a.height == b.height) + + , dashboardEpochStartHeight$ = isBitcoinNetwork + ? latestBlock$ + .map(block => block.height - (block.height % DIFFICULTY_PERIOD)) + .distinctUntilChanged() + : O.empty() + + , dashboardPreviousDifficultyHeight$ = isBitcoinNetwork + ? latestBlock$ + .map(block => block.height - DIFFICULTY_PERIOD) + .filter(height => height >= 0) + .distinctUntilChanged() + : O.empty() + , newBlockEntries$ = trackNewEntries( blocks$, block => block.id, @@ -241,6 +261,14 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search , dashboardState$ = O.combineLatest(blocks$, mempoolRecent$, (blks, txs) => ({ dashblocks: blks.slice(0, 5), dashTxs: txs.slice(0, 5)})) + , dashboardEpochStartBlock$ = reply('dashboard-epoch-start-block', true) + .map(r => ({ ...r.body, requestedHeight: r.request.height })) + .startWith(null) + + , dashboardPreviousDifficultyBlock$ = reply('dashboard-previous-difficulty-block', true) + .map(r => ({ ...r.body, requestedHeight: r.request.height })) + .startWith(null) + // Fee estimates , feeEst$ = reply('fee-est').startWith(null) @@ -314,6 +342,7 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search // App state , state$ = combine({ t$, error$, tipHeight$, spends$ , goBlocks$, blocks$, nextBlocks$, prevBlocks$, dashboardState$ + , dashboardEpochStartBlock$, dashboardPreviousDifficultyBlock$ , newBlockEntries$, newTxEntries$ , goBlock$, block$, blockStatus$, blockTxs$, nextBlockTxs$, prevBlockTxs$, openBlock$ , mempool$, mempoolRecent$, feeEst$, bitcoinMarketChart$ @@ -367,6 +396,19 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search // fetch block by height , goHeight$.map(n => ({ category: 'height', method: 'GET', path: `/block-height/${n}` })) + // fetch dashboard difficulty comparison blocks + , dashboardEpochStartHeight$ + .map(height => ({ category: 'dashboard-epoch-start-height', method: 'GET', path: `/block-height/${height}`, height, bg: true })) + + , reply('dashboard-epoch-start-height', true) + .map(r => ({ category: 'dashboard-epoch-start-block', method: 'GET', path: `/block/${r.text}`, height: r.request.height, bg: true })) + + , dashboardPreviousDifficultyHeight$ + .map(height => ({ category: 'dashboard-previous-difficulty-height', method: 'GET', path: `/block-height/${height}`, height, bg: true })) + + , reply('dashboard-previous-difficulty-height', true) + .map(r => ({ category: 'dashboard-previous-difficulty-block', method: 'GET', path: `/block/${r.text}`, height: r.request.height, bg: true })) + // push tx , pushtx$.map(rawtx => ({ category: 'pushtx', method: 'POST', path: `/tx`, send: rawtx, type: 'text/plain' })) diff --git a/client/src/components/icons.js b/client/src/components/icons.js index f0cf29a5..e57a616a 100644 --- a/client/src/components/icons.js +++ b/client/src/components/icons.js @@ -8,6 +8,14 @@ export const TxArrowsIcon = ({ className } = {}) => +export const ArrowsInSimpleIcon = ({ className } = {}) => + + export const CopyIcon = ({ className } = {}) =>
+

{title}

+

{value}

+
+); + +const statDivider = () => ( +
+); + +export default ({ + blocks, + dashboardEpochStartBlock, + dashboardPreviousDifficultyBlock, +}) => { + const latestBlock = blocks && blocks[0]; + const epochTiming = getEpochTiming(latestBlock, dashboardEpochStartBlock); + const expected = expectedAdjustment(epochTiming); + const previous = previousAdjustment( + latestBlock, + dashboardPreviousDifficultyBlock, + ); + const hashrate = formatHashrate( + latestBlock && latestBlock.difficulty, + epochTiming && epochTiming.averageBlockSeconds, + ); + const estimatedAdjustmentTimestamp = + epochTiming && epochTiming.estimatedAdjustmentTimestamp; + const nextAdjustment = formatTimeUntil(estimatedAdjustmentTimestamp); + const nextAdjustmentFooter = Number.isFinite(estimatedAdjustmentTimestamp) + ? `Next adj. in ${nextAdjustment}` + : "Next adjustment unavailable"; + + return ( +
+
+
+
+ +
+

Difficulty Adjustment

+
+
+ {adjustmentStat("AVERAGE BLOCK TIME", "~10 minutes")} + + {statDivider()} + {adjustmentStat( + "EXPECTED", + formatAdjustment(expected), + adjustmentClass(expected), + )} + + {statDivider()} + {adjustmentStat( + "PREVIOUS", + formatAdjustment(previous), + adjustmentClass(previous), + )} + + {statDivider()} + {adjustmentStat( + "EXPECTED DATE", + formatAdjustmentDate(estimatedAdjustmentTimestamp), + )} +
+
+ +
+ + + +
+
+ ); +}; diff --git a/client/src/views/home.js b/client/src/views/home.js index 678e66c0..7f5c7073 100644 --- a/client/src/views/home.js +++ b/client/src/views/home.js @@ -2,6 +2,8 @@ import layout from "./layout"; import { blks } from "./blocks"; import { transactions } from "./transactions"; import { overview } from "./overview"; +import difficultyAdjustment from "./difficulty-adjustment"; +import { isBitcoinNetwork } from "../lib/network"; const isTouch = process.browser && "ontouchstart" in window; @@ -16,6 +18,9 @@ export const dashBoard = ({ t, blocks, dashboardState, loading, ...S }) => { {overview({ blocks: dashblocks, ...S })} {blks(dashblocks, true, { t, ...S })} {transactions(dashTxs, true, { t, ...S })} + {isBitcoinNetwork + ? difficultyAdjustment({ blocks: dashblocks, ...S }) + : ""} , { ...S, t, activeTab: "dashBoard" }, ); diff --git a/www/style.css b/www/style.css index 5bd7a197..db774978 100644 --- a/www/style.css +++ b/www/style.css @@ -4033,3 +4033,53 @@ a.back-link img{ margin-left: auto; font-weight: 400; } + +.difficulty-adjustment-panel { + margin-top: 24px; + background-color: var(--surface-primary-color); + padding: 24px; + border-radius: 12px; +} + +.difficulty-adjustment-stat-divider { + width: 1px; + height: 30px; + background-color: white; +} + +.difficulty-adjustment-stats { + display: flex; + margin-top: 12px; + justify-content: space-between; +} + +.difficulty-adjustment-stat { + flex: 1; + display: flex; + align-items: center; + flex-direction: column; +} + +.difficulty-adjustment-stat-title { + font-size: 10px; + color: #B5BDC2; +} + +.difficulty-adjustment-stat-value.success { + color: #17C964; +} + +.difficulty-adjustment-stat-value.danger { + color: #DB3B3E; +} + +.difficulty-adjustment-metrics { + display: flex; + flex-wrap: wrap; + margin-top: 24px; + gap: 24px; +} + +.difficulty-adjustment-metric-card { + flex: 1; +}