From 3e288a3d51bf54f4560559d26141ad6fbaca3455 Mon Sep 17 00:00:00 2001 From: Caesar Mukama Date: Tue, 28 Apr 2026 12:41:17 +0300 Subject: [PATCH 1/4] fix: processBlockData parses HISTORICAL_BLOCKSIZES flat-array shape The mempool worker's getWrkExtData returns a flat array of block records [{ts, blockSize, blockReward, blockTotalFees}, ...], but processBlockData only handled nested .data/.blocks wrappers - it iterated entry's own field names, Number('ts') returned NaN, and produced an empty daily map. The SubsidyFee page then rendered "No data available". Detect the flat shape (entry has .ts/.timestamp/.time) and process it directly. Also surface blockSize on getSubsidyFees and getRevenueSummary log entries (and totalBlockSize/avgBlockSize on the summary) - needed for SubsidyFee's "Avg Fees in Sats/vByte" chart to render after this fix. --- tests/unit/handlers/finance.handlers.test.js | 6 ++++-- tests/unit/handlers/finance.utils.test.js | 19 ++++++++++++++++++- .../lib/server/handlers/finance.handlers.js | 15 +++++++++++---- workers/lib/server/handlers/finance.utils.js | 15 +++++++++------ 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/tests/unit/handlers/finance.handlers.test.js b/tests/unit/handlers/finance.handlers.test.js index 0127a15..7a7a6a7 100644 --- a/tests/unit/handlers/finance.handlers.test.js +++ b/tests/unit/handlers/finance.handlers.test.js @@ -569,16 +569,18 @@ test('getSubsidyFees - empty ork results', async (t) => { test('calculateSubsidyFeesSummary - calculates from log entries', (t) => { const log = [ - { blockReward: 6.25, blockTotalFees: 0.5 }, - { blockReward: 6.25, blockTotalFees: 0.3 } + { blockReward: 6.25, blockTotalFees: 0.5, blockSize: 1500000 }, + { blockReward: 6.25, blockTotalFees: 0.3, blockSize: 1300000 } ] const summary = calculateSubsidyFeesSummary(log) t.is(summary.totalBlockReward, 12.5, 'should sum block rewards') t.is(summary.totalBlockTotalFees, 0.8, 'should sum block fees') + t.is(summary.totalBlockSize, 2800000, 'should sum block sizes') t.ok(summary.avgBlockReward !== null, 'should calculate avg block reward') t.is(summary.avgBlockReward, 6.25, 'should calculate correct avg block reward') t.ok(summary.avgBlockTotalFees !== null, 'should calculate avg block fees') + t.is(summary.avgBlockSize, 1400000, 'should calculate correct avg block size') t.pass() }) diff --git a/tests/unit/handlers/finance.utils.test.js b/tests/unit/handlers/finance.utils.test.js index b82f793..65a0adb 100644 --- a/tests/unit/handlers/finance.utils.test.js +++ b/tests/unit/handlers/finance.utils.test.js @@ -205,7 +205,8 @@ test('processBlockData - array items', (t) => { blocks: [{ ts: 1700006400000, blockReward: 6.25, - blockTotalFees: 0.5 + blockTotalFees: 0.5, + blockSize: 1500000 }] }] ] @@ -213,6 +214,22 @@ test('processBlockData - array items', (t) => { const key = Object.keys(daily)[0] t.is(daily[key].blockReward, 6.25, 'should extract blockReward') t.is(daily[key].blockTotalFees, 0.5, 'should extract blockTotalFees') + t.is(daily[key].blockSize, 1500000, 'should extract blockSize') + t.pass() +}) + +test('processBlockData - flat per-ork items (production shape)', (t) => { + const results = [ + [ + { ts: 1700006400000, blockSize: 1500000, blockHash: 'abc', blockReward: 6.25, blockTotalFees: 0.5 }, + { ts: 1700006400000, blockSize: 1200000, blockHash: 'def', blockReward: 6.25, blockTotalFees: 0.3 } + ] + ] + const daily = processBlockData(results) + const key = Object.keys(daily)[0] + t.is(daily[key].blockReward, 12.5, 'should sum blockReward across same-day items') + t.is(daily[key].blockTotalFees, 0.8, 'should sum blockTotalFees across same-day items') + t.is(daily[key].blockSize, 2700000, 'should sum blockSize across same-day items') t.pass() }) diff --git a/workers/lib/server/handlers/finance.handlers.js b/workers/lib/server/handlers/finance.handlers.js index 444e90d..7cb3457 100644 --- a/workers/lib/server/handlers/finance.handlers.js +++ b/workers/lib/server/handlers/finance.handlers.js @@ -628,7 +628,8 @@ async function getSubsidyFees (ctx, req) { log.push({ ts, blockReward: block.blockReward, - blockTotalFees: block.blockTotalFees + blockTotalFees: block.blockTotalFees, + blockSize: block.blockSize }) } @@ -643,22 +644,27 @@ function calculateSubsidyFeesSummary (log) { return { totalBlockReward: 0, totalBlockTotalFees: 0, + totalBlockSize: 0, avgBlockReward: null, - avgBlockTotalFees: null + avgBlockTotalFees: null, + avgBlockSize: null } } const totals = log.reduce((acc, entry) => { acc.blockReward += entry.blockReward || 0 acc.blockTotalFees += entry.blockTotalFees || 0 + acc.blockSize += entry.blockSize || 0 return acc - }, { blockReward: 0, blockTotalFees: 0 }) + }, { blockReward: 0, blockTotalFees: 0, blockSize: 0 }) return { totalBlockReward: totals.blockReward, totalBlockTotalFees: totals.blockTotalFees, + totalBlockSize: totals.blockSize, avgBlockReward: safeDiv(totals.blockReward, log.length), - avgBlockTotalFees: safeDiv(totals.blockTotalFees, log.length) + avgBlockTotalFees: safeDiv(totals.blockTotalFees, log.length), + avgBlockSize: safeDiv(totals.blockSize, log.length) } } @@ -879,6 +885,7 @@ async function getRevenueSummary (ctx, req) { hashRevenueUSDPerPHsPerDay: safeDiv(revenueUSD, hashratePhs), blockReward: block.blockReward || 0, blockTotalFees: block.blockTotalFees || 0, + blockSize: block.blockSize || 0, curtailmentMWh, curtailmentRate, operationalIssuesRate, diff --git a/workers/lib/server/handlers/finance.utils.js b/workers/lib/server/handlers/finance.utils.js index 8b6746c..853c8e5 100644 --- a/workers/lib/server/handlers/finance.utils.js +++ b/workers/lib/server/handlers/finance.utils.js @@ -94,25 +94,28 @@ function processBlockData (results) { if (!Array.isArray(data)) continue for (const entry of data) { if (!entry) continue - const items = entry.data || entry.blocks || entry + const rawTs = entry.ts || entry.timestamp || entry.time + const items = rawTs ? [entry] : (entry.data || entry.blocks || entry) if (Array.isArray(items)) { for (const item of items) { if (!item) continue - const rawTs = item.ts || item.timestamp || item.time - const ts = getStartOfDay(normalizeTimestampMs(rawTs)) + const itemTs = item.ts || item.timestamp || item.time + const ts = getStartOfDay(normalizeTimestampMs(itemTs)) if (!ts) continue - if (!daily[ts]) daily[ts] = { blockReward: 0, blockTotalFees: 0 } + if (!daily[ts]) daily[ts] = { blockReward: 0, blockTotalFees: 0, blockSize: 0 } daily[ts].blockReward += (item.blockReward || item.block_reward || item.subsidy || 0) daily[ts].blockTotalFees += (item.blockTotalFees || item.block_total_fees || item.totalFees || item.total_fees || 0) + daily[ts].blockSize += (item.blockSize || item.block_size || item.size || 0) } - } else if (typeof items === 'object' && !Array.isArray(items)) { + } else if (typeof items === 'object') { for (const [key, val] of Object.entries(items)) { const ts = getStartOfDay(Number(key)) if (!ts) continue - if (!daily[ts]) daily[ts] = { blockReward: 0, blockTotalFees: 0 } + if (!daily[ts]) daily[ts] = { blockReward: 0, blockTotalFees: 0, blockSize: 0 } if (typeof val === 'object') { daily[ts].blockReward += (val.blockReward || val.block_reward || val.subsidy || 0) daily[ts].blockTotalFees += (val.blockTotalFees || val.block_total_fees || val.totalFees || val.total_fees || 0) + daily[ts].blockSize += (val.blockSize || val.block_size || val.size || 0) } } } From 8a2194deea5808585066d6a7b5c5f96bc3cc1e53 Mon Sep 17 00:00:00 2001 From: Caesar Mukama Date: Tue, 28 Apr 2026 13:17:57 +0300 Subject: [PATCH 2/4] fix: cost-summary propagates per-day btcPrice Two compounding bugs in getCostSummary caused btcPrice=0 for every log entry, leaving ProductionCostPriceChart's overlay line at 0: 1. Wrong RPC key. The handler queried mempool with key: 'prices', which the worker doesn't recognize and so returns the live snapshot ({currentPrice, blockHeight, ...}). Switched to 'HISTORICAL_PRICES', which returns [{ts, priceUSD}, ...]. 2. processEbitdaPrices couldn't parse the production shape. Same class as the processBlockData fix (Bug C): the mempool worker returns a flat per-ORK array of records, not a wrapper. Detect when entry has .ts/.timestamp/.time and process it as the item directly; also accept priceUSD (the actual upstream field name) alongside price. The processEbitdaPrices fix incidentally makes getRevenueSummary and getHashRevenue use real per-day prices instead of falling back to currentBtcPrice. getEbitda still uses the wrong key: 'prices' (line 344) and is left for a follow-up. --- tests/unit/handlers/finance.handlers.test.js | 13 +++++++++++++ workers/lib/server/handlers/finance.handlers.js | 14 ++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/tests/unit/handlers/finance.handlers.test.js b/tests/unit/handlers/finance.handlers.test.js index 7a7a6a7..77fe314 100644 --- a/tests/unit/handlers/finance.handlers.test.js +++ b/tests/unit/handlers/finance.handlers.test.js @@ -371,6 +371,19 @@ test('processEbitdaPrices - processes valid data', (t) => { t.pass() }) +test('processEbitdaPrices - flat per-ork items with priceUSD (production shape)', (t) => { + const results = [ + [ + { ts: 1700006400000, priceUSD: 40000 }, + { ts: 1700092800000, priceUSD: 41500 } + ] + ] + const daily = processEbitdaPrices(results) + t.is(daily[1700006400000], 40000, 'should extract priceUSD for first day') + t.is(daily[1700092800000], 41500, 'should extract priceUSD for second day') + t.pass() +}) + test('calculateEbitdaSummary - calculates from log entries', (t) => { const log = [ { revenueBTC: 0.5, revenueUSD: 20000, totalCostsUSD: 5000, ebitdaSelling: 15000, ebitdaHodl: 15000 }, diff --git a/workers/lib/server/handlers/finance.handlers.js b/workers/lib/server/handlers/finance.handlers.js index 7cb3457..8de27ce 100644 --- a/workers/lib/server/handlers/finance.handlers.js +++ b/workers/lib/server/handlers/finance.handlers.js @@ -449,19 +449,21 @@ function processEbitdaPrices (results) { if (!Array.isArray(data)) continue for (const entry of data) { if (!entry) continue - const items = entry.data || entry.prices || entry + const rawTs = entry.ts || entry.timestamp || entry.time + const items = rawTs ? [entry] : (entry.data || entry.prices || entry) if (Array.isArray(items)) { for (const item of items) { const ts = getStartOfDay(item.ts || item.timestamp || item.time) - if (ts && item.price) { - daily[ts] = item.price + const price = item.priceUSD || item.price + if (ts && price) { + daily[ts] = price } } - } else if (typeof items === 'object' && !Array.isArray(items)) { + } else if (typeof items === 'object') { for (const [key, val] of Object.entries(items)) { const ts = getStartOfDay(Number(key)) if (ts) { - daily[ts] = typeof val === 'object' ? (val.USD || val.price || 0) : Number(val) || 0 + daily[ts] = typeof val === 'object' ? (val.USD || val.priceUSD || val.price || 0) : Number(val) || 0 } } } @@ -518,7 +520,7 @@ async function getCostSummary (ctx, req) { (cb) => ctx.dataProxy.requestData(RPC_METHODS.GET_WRK_EXT_DATA, { type: WORKER_TYPES.MEMPOOL, - query: { key: 'prices', start, end } + query: { key: 'HISTORICAL_PRICES', start, end } }).then(r => cb(null, r)).catch(cb), (cb) => ctx.dataProxy.requestData(RPC_METHODS.TAIL_LOG_RANGE_AGGR, { From 5f0172a9a1b34a61e61d21e6d9140bcd15685971 Mon Sep 17 00:00:00 2001 From: Caesar Mukama Date: Tue, 28 Apr 2026 13:24:08 +0300 Subject: [PATCH 3/4] fix: processTailLogData drills into .val for nested TAIL_LOG_RANGE_AGGR The TAIL_LOG_RANGE_AGGR RPC returns per-day items as {ts, val: {site_power_w, hashrate_mhs_5m_sum_aggr}}, but processTailLogData read item[AGGR_FIELDS.SITE_POWER] directly without drilling into .val, so powerW and hashrateMhs were 0 for every entry. RevenueSummary then showed 0 for Avg Power, Avg Hashrate, and Hashrate Capacity Factor; EBITDA's per-day power/hashrate were similarly zeroed. Mirror the .val/.flat fallback pattern that processConsumptionData already uses (workers/lib/server/handlers/finance.handlers.js:172). --- tests/unit/handlers/finance.handlers.test.js | 28 +++++++++++++++++++ .../lib/server/handlers/finance.handlers.js | 5 ++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/tests/unit/handlers/finance.handlers.test.js b/tests/unit/handlers/finance.handlers.test.js index 77fe314..d26b56f 100644 --- a/tests/unit/handlers/finance.handlers.test.js +++ b/tests/unit/handlers/finance.handlers.test.js @@ -355,6 +355,34 @@ test('processTailLogData - processes power and hashrate', (t) => { t.pass() }) +test('processTailLogData - drills into .val (production shape)', (t) => { + const results = [ + [ + { + type: 'powermeter', + data: [ + { ts: 1700006400000, val: { site_power_w: 5000 } }, + { ts: 1700092800000, val: { site_power_w: 6000 } } + ] + }, + { + type: 'miner', + data: [ + { ts: 1700006400000, val: { hashrate_mhs_5m_sum_aggr: 100000 } }, + { ts: 1700092800000, val: { hashrate_mhs_5m_sum_aggr: 120000 } } + ] + } + ] + ] + + const daily = processTailLogData(results) + t.is(daily[1700006400000].powerW, 5000, 'extracts powerW from .val on day 1') + t.is(daily[1700006400000].hashrateMhs, 100000, 'extracts hashrateMhs from .val on day 1') + t.is(daily[1700092800000].powerW, 6000, 'extracts powerW from .val on day 2') + t.is(daily[1700092800000].hashrateMhs, 120000, 'extracts hashrateMhs from .val on day 2') + t.pass() +}) + test('processTailLogData - handles error results', (t) => { const results = [{ error: 'timeout' }] const daily = processTailLogData(results) diff --git a/workers/lib/server/handlers/finance.handlers.js b/workers/lib/server/handlers/finance.handlers.js index 8de27ce..fa78f7d 100644 --- a/workers/lib/server/handlers/finance.handlers.js +++ b/workers/lib/server/handlers/finance.handlers.js @@ -432,8 +432,9 @@ function processTailLogData (results) { for (const item of items) { const ts = getStartOfDay(item.ts || item.timestamp) if (!daily[ts]) daily[ts] = { powerW: 0, hashrateMhs: 0 } - daily[ts].powerW += (item[AGGR_FIELDS.SITE_POWER] || 0) - daily[ts].hashrateMhs += (item[AGGR_FIELDS.HASHRATE_SUM] || 0) + const val = item.val || item + daily[ts].powerW += (val[AGGR_FIELDS.SITE_POWER] || 0) + daily[ts].hashrateMhs += (val[AGGR_FIELDS.HASHRATE_SUM] || 0) } } } From d130147ddeceaa0cead63528614b704f20cc0bd3 Mon Sep 17 00:00:00 2001 From: Caesar Mukama Date: Tue, 28 Apr 2026 13:27:52 +0300 Subject: [PATCH 4/4] fix: hash-revenue hashrate and network-hashrate populate from real RPC Two bugs in getHashRevenue caused hashrateMhs and networkHashrateMhs to be 0 for every log entry, leaving HashBalance entirely zeroed: 1. processHashrateData read item[AGGR_FIELDS.HASHRATE_SUM] directly, but TAIL_LOG_RANGE_AGGR wraps measurements as {ts, val: {hashrate_mhs_5m_sum_aggr}}. Same .val drilling fix as processTailLogData. 2. processNetworkHashrateData expected entries wrapped under .data, but the HISTORICAL_HASHRATE RPC returns a flat per-ORK array of {ts, avgHashrateMHs} records. Same flat-shape detection as processBlockData (Bug C). --- tests/unit/handlers/finance.handlers.test.js | 31 +++++++++++++++++++ .../lib/server/handlers/finance.handlers.js | 12 ++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/tests/unit/handlers/finance.handlers.test.js b/tests/unit/handlers/finance.handlers.test.js index d26b56f..134ccd9 100644 --- a/tests/unit/handlers/finance.handlers.test.js +++ b/tests/unit/handlers/finance.handlers.test.js @@ -1033,6 +1033,24 @@ test('processHashrateData - processes array data', (t) => { t.pass() }) +test('processHashrateData - drills into .val (production shape)', (t) => { + const results = [ + [ + { + type: 'miner', + data: [ + { ts: 1700006400000, val: { hashrate_mhs_5m_sum_aggr: 500000 } }, + { ts: 1700092800000, val: { hashrate_mhs_5m_sum_aggr: 600000 } } + ] + } + ] + ] + const daily = processHashrateData(results) + t.is(daily[1700006400000], 500000, 'extracts hashrate from .val on day 1') + t.is(daily[1700092800000], 600000, 'extracts hashrate from .val on day 2') + t.pass() +}) + test('processHashrateData - handles error results', (t) => { const results = [{ error: 'timeout' }] const daily = processHashrateData(results) @@ -1053,6 +1071,19 @@ test('processNetworkHashrateData - processes array data', (t) => { t.pass() }) +test('processNetworkHashrateData - flat per-ork items (production shape)', (t) => { + const results = [ + [ + { ts: 1700006400000, avgHashrateMHs: 1019725948656278 }, + { ts: 1700092800000, avgHashrateMHs: 1029591824888537 } + ] + ] + const daily = processNetworkHashrateData(results) + t.is(daily[1700006400000], 1019725948656278, 'extracts avgHashrateMHs day 1') + t.is(daily[1700092800000], 1029591824888537, 'extracts avgHashrateMHs day 2') + t.pass() +}) + test('processNetworkHashrateData - processes object-keyed data', (t) => { const results = [ [{ data: { 1700006400000: { avgHashrateMHs: 500000000000000 } } }] diff --git a/workers/lib/server/handlers/finance.handlers.js b/workers/lib/server/handlers/finance.handlers.js index fa78f7d..5767a91 100644 --- a/workers/lib/server/handlers/finance.handlers.js +++ b/workers/lib/server/handlers/finance.handlers.js @@ -1097,7 +1097,8 @@ function processHashrateData (results) { const ts = getStartOfDay(item.ts || item.timestamp) if (!ts) continue if (!daily[ts]) daily[ts] = 0 - daily[ts] += (item[AGGR_FIELDS.HASHRATE_SUM] || 0) + const val = item.val || item + daily[ts] += (val[AGGR_FIELDS.HASHRATE_SUM] || 0) } } } @@ -1113,18 +1114,19 @@ function processNetworkHashrateData (results) { if (!Array.isArray(data)) continue for (const entry of data) { if (!entry) continue - const items = entry.data || entry + const rawTs = entry.ts || entry.timestamp || entry.time + const items = rawTs ? [entry] : (entry.data || entry) if (Array.isArray(items)) { for (const item of items) { if (!item) continue - const rawTs = item.ts || item.timestamp || item.time - const ts = getStartOfDay(normalizeTimestampMs(rawTs)) + const itemTs = item.ts || item.timestamp || item.time + const ts = getStartOfDay(normalizeTimestampMs(itemTs)) if (!ts) continue if (item.avgHashrateMHs) { daily[ts] = item.avgHashrateMHs } } - } else if (typeof items === 'object' && !Array.isArray(items)) { + } else if (typeof items === 'object') { for (const [key, val] of Object.entries(items)) { const ts = getStartOfDay(Number(key)) if (!ts) continue