diff --git a/app.js b/app.js index f015e48..9f746a0 100644 --- a/app.js +++ b/app.js @@ -18,6 +18,7 @@ const logger = require('utils/logger.js'); const bitcoind = require('routes/v1/bitcoind/info.js'); const charts = require('routes/v1/bitcoind/charts.js'); const system = require('routes/v1/bitcoind/system.js'); +const widgets = require('routes/v1/bitcoind/widgets.js'); const ping = require('routes/ping.js'); const app = express(); @@ -37,6 +38,7 @@ app.use('/', express.static('./ui/dist')); app.use('/v1/bitcoind/info', bitcoind); app.use('/v1/bitcoind/info', charts); app.use('/v1/bitcoind/system', system); +app.use('/v1/bitcoind/widgets', widgets); app.use('/ping', ping); app.use(errorHandleMiddleware); diff --git a/logic/widgets.js b/logic/widgets.js new file mode 100644 index 0000000..a0ab0e7 --- /dev/null +++ b/logic/widgets.js @@ -0,0 +1,96 @@ +const bitcoind = require('logic/bitcoind.js'); + +async function getBitcoinStatsWidgetData() { + const stats = await bitcoind.nodeStatusSummary(); + // bitcoind.nodeStatusSummary() returns the following format: + // { + // "difficulty": 70343519904866.8, + // "size": 619737765556, + // "mempool": 264944224, + // "connections": 11, + // "networkhashps": 516082722091118500000 + // } + + const [hashRateValue, hashRateUnits] = abbreviateHashRate(stats.networkhashps); + const [blockchainSizeValue, blockchainSizeUnits] = abbreviateSize(stats.size); + const [mempoolValue, mempoolUnits] = abbreviateSize(stats.mempool); + + const widgetData = { + type: 'four-stats', + refresh: '5s', + link: '', + items: [ + {title: 'Connections', text: stats.connections, subtext: 'peers'}, + {title: 'Mempool', text: mempoolValue, subtext: mempoolUnits}, + {title: 'Hashrate', text: hashRateValue, subtext: hashRateUnits}, + {title: 'Blockchain size', text: blockchainSizeValue, subtext: blockchainSizeUnits} + ] + }; + + return widgetData; +} + +async function getBitcoinSyncWidgetData() { + const sync = await bitcoind.getSyncStatus(); + // bitcoind.getSyncStatus() returns the following format: + // { + // "chain": "main", + // "percent": 0.9999993962382976, + // "currentBlock": 828436, + // "headerCount": 828436, + // "pruned": false + // } + + + // sync.percent is `verificationprogress` from the getblockchaininfo RPC, which can't reach 1 when the most recent block is in the past. + // To ensure accurate percentage display during sync: + // - When no headers have been downloaded, we set the progress to 0% (this is because sync.percent = 1 when no headers have been downloaded). + // - When current block matches header count (indicating sync completion), we set the progress to 100%. + // - For other cases, we use the value of `verificationprogress` with 2 decimal places and floor it such that the value is in the range 0% to 99.99%. + // This allows us to show accurate percentage during initial sync by using the `verificationprogress` value, while also + // not relying on prematurely rounding to 100% before the sync is actually complete + let syncPercent = Math.floor(sync.percent * 10000) / 100; + // If we're synced to the tip, always show 100 + if (sync.currentBlock === sync.headerCount) syncPercent = 100; + // If no headers have been downloaded, show 0. sync.percent = 1 when no headers have been downloaded. + if (sync.headerCount === 0) syncPercent = 0; + + + const widgetData = { + type: 'text-with-progress', + refresh: '2s', + link: '', + title: 'Blockchain sync', + text: `${syncPercent}%`, + progressLabel: syncPercent === 100 ? 'Synced' : 'In progress', + progress: syncPercent / 100 + }; + + return widgetData; +} + +// consider breaking out into a utility and using in both frontend and backend +function abbreviateHashRate(n) { + if (n < 1e3) return [Number(n.toFixed(1)), 'H/s']; + if (n >= 1e3 && n < 1e6) return [Number((n / 1e3).toFixed(1)), 'kH/s']; + if (n >= 1e6 && n < 1e9) return [Number((n / 1e6).toFixed(1)), 'MH/s']; + if (n >= 1e9 && n < 1e12) return [Number((n / 1e9).toFixed(1)), 'GH/s']; + if (n >= 1e12 && n < 1e15) return [Number((n / 1e12).toFixed(1)), 'TH/s']; + if (n >= 1e15 && n < 1e18) return [Number((n / 1e15).toFixed(1)), 'PH/s']; + if (n >= 1e18 && n < 1e21) return [Number((n / 1e18).toFixed(1)), 'EH/s']; + if (n >= 1e21) return [Number(+(n / 1e21).toFixed(1)), 'ZH/s']; +} + +function abbreviateSize(n) { + if (n < 1e3) return [Number(n.toFixed(1)), 'Bytes']; + if (n >= 1e3 && n < 1e6) return [Number((n / 1e3).toFixed(1)), 'KB']; + if (n >= 1e6 && n < 1e9) return [Number((n / 1e6).toFixed(1)), 'MB']; + if (n >= 1e9 && n < 1e12) return [Number((n / 1e9).toFixed(1)), 'GB']; + if (n >= 1e12 && n < 1e15) return [Number((n / 1e12).toFixed(1)), 'TB']; + if (n >= 1e15) return [Number(+(n / 1e15).toFixed(1)), 'PB']; +} + +module.exports = { + getBitcoinStatsWidgetData, + getBitcoinSyncWidgetData +}; diff --git a/routes/v1/bitcoind/widgets.js b/routes/v1/bitcoind/widgets.js new file mode 100644 index 0000000..6e653a7 --- /dev/null +++ b/routes/v1/bitcoind/widgets.js @@ -0,0 +1,17 @@ +const express = require('express'); +const router = express.Router(); + +const widgetLogic = require('logic/widgets.js'); +const safeHandler = require('utils/safeHandler'); + +router.get('/stats', safeHandler(async(req, res) => { + const widgetData = await widgetLogic.getBitcoinStatsWidgetData(); + res.json(widgetData); +})); + +router.get('/sync', safeHandler(async(req, res) => { + const widgetData = await widgetLogic.getBitcoinSyncWidgetData(); + res.json(widgetData); +})); + +module.exports = router;