From 3573e862b60187bbe3d35ee2fe9e597f84ecfae1 Mon Sep 17 00:00:00 2001 From: Randall Naar Date: Wed, 12 Aug 2026 13:31:47 -0400 Subject: [PATCH] Added fee market components. --- client/src/const.js | 12 ++- client/src/lib/block-template.js | 8 +- client/src/lib/fees.js | 34 +++++++-- client/src/lib/market.js | 9 +++ client/src/lib/pending-block-details.js | 19 ----- client/src/views/fee-market.js | 53 +++++++++++++ client/src/views/home.js | 3 + client/src/views/overview.js | 55 ++++---------- .../src/views/pending-block-details-card.js | 18 ++--- client/src/views/util.js | 14 ++++ lang/strings.txt | 7 ++ test/fee-market.test.js | 75 +++++++++++++++++++ test/fees.test.js | 20 ++++- test/format-number.test.js | 20 ++++- test/market.test.js | 20 +++++ test/overview.test.js | 36 +++++++++ test/pending-block-details.test.js | 12 --- www/style.css | 33 ++++++++ 18 files changed, 353 insertions(+), 95 deletions(-) create mode 100644 client/src/lib/market.js create mode 100644 client/src/views/fee-market.js create mode 100644 test/fee-market.test.js create mode 100644 test/market.test.js create mode 100644 test/overview.test.js diff --git a/client/src/const.js b/client/src/const.js index fe1d2ade..8f75dfe5 100644 --- a/client/src/const.js +++ b/client/src/const.js @@ -4,10 +4,20 @@ export const blocksPerPage = 10 export const difficultyPeriod = 2016 export const maxMempoolTxs = 50 export const satoshisPerBitcoin = 100000000 -export const averageNativeSegwitTransactionSize = 140 +export const typicalNativeSegwitTransactionVsize = 140 +// One native-SegWit input, two confidential outputs, and an explicit fee +// output. After ELIP-200 normalizes confidential values, nonces, and proofs, +// its discount weight is 228 base-equivalent bytes * 4 plus 117 witness bytes +// (using a 108-byte P2WPKH witness), rounded up from 1029 WU. +export const typicalDiscountedConfidentialTransactionVsize = 258 export const maxBlockWeight = 4000000 export const blockGridLoadingDelayMs = 100 export const blockGridTransactionSelectEvent = 'block-grid-transaction-select' +export const feeEstimateTargets = { + low: 12, + average: 3, + high: 1, +} const configuredTargetBlockIntervalSeconds = Number(process.env.TARGET_BLOCK_INTERVAL_SECONDS) export const targetBlockIntervalSeconds = configuredTargetBlockIntervalSeconds > 0 diff --git a/client/src/lib/block-template.js b/client/src/lib/block-template.js index 55e12c66..d51cab3f 100644 --- a/client/src/lib/block-template.js +++ b/client/src/lib/block-template.js @@ -1,4 +1,4 @@ -import { maxBlockWeight } from "../const"; +import { feeEstimateTargets, maxBlockWeight } from "../const"; import { feeRateClass } from "./fees"; import { clamp } from "./math"; @@ -47,7 +47,11 @@ const summarizeFeeBucket = (transactions) => { }; const summarizeFeeBuckets = (transactions, feeEst) => { - if (!feeEst || feeEst[3] == null || feeEst[12] == null) { + if ( + !feeEst || + feeEst[feeEstimateTargets.average] == null || + feeEst[feeEstimateTargets.low] == null + ) { return { low: null, medium: null, high: null }; } diff --git a/client/src/lib/fees.js b/client/src/lib/fees.js index ba41c689..c8da6203 100644 --- a/client/src/lib/fees.js +++ b/client/src/lib/fees.js @@ -1,11 +1,22 @@ +import { + feeEstimateTargets, + satoshisPerBitcoin, + typicalDiscountedConfidentialTransactionVsize, + typicalNativeSegwitTransactionVsize, +} from '../const' + const MAX_BLOCK_VSIZE = 1000000 + , typicalTransactionVsize = process.env.IS_ELEMENTS + ? typicalDiscountedConfidentialTransactionVsize + : typicalNativeSegwitTransactionVsize export const getFeeTierBoundaries = feeEst => { - const low = feeEst && feeEst[12] - , high = feeEst && feeEst[3] + const lowMaximum = feeEst && feeEst[feeEstimateTargets.low] + , mediumMaximum = feeEst && feeEst[feeEstimateTargets.average] - return Number.isFinite(low) && Number.isFinite(high) && low >= 0 && high >= 0 - ? { low, high: Math.max(low, high) } + return Number.isFinite(lowMaximum) && Number.isFinite(mediumMaximum) + && lowMaximum >= 0 && mediumMaximum >= 0 + ? { lowMaximum, mediumMaximum: Math.max(lowMaximum, mediumMaximum) } : null } @@ -13,9 +24,9 @@ export const feeRateClass = (feerate, feeEst) => { const boundaries = getFeeTierBoundaries(feeEst) if (!Number.isFinite(feerate) || !boundaries) return "" - return feerate <= boundaries.low + return feerate <= boundaries.lowMaximum ? "success" - : feerate <= boundaries.high + : feerate <= boundaries.mediumMaximum ? "warning" : "danger" } @@ -38,6 +49,17 @@ export function getConfEstimate(fee_estimates, feerate) { return target_est ? target_est[0] : -1 } +// Estimate the USD cost of a typical transaction at a given fee-rate. Elements +// uses the ELIP-200 discount virtual size rather than serialized virtual size. +export function estimateTypicalTransactionFeeUsd(bitcoinPrice, feerate) { + return Number.isFinite(bitcoinPrice) && bitcoinPrice >= 0 + && Number.isFinite(feerate) && feerate >= 0 + ? (bitcoinPrice / satoshisPerBitcoin) * + feerate * + typicalTransactionVsize + : null +} + // Squash the fee histogram into fixed feerate ranges export function squashFeeHistogram(histogram) { let i = 0 diff --git a/client/src/lib/market.js b/client/src/lib/market.js new file mode 100644 index 00000000..60155e13 --- /dev/null +++ b/client/src/lib/market.js @@ -0,0 +1,9 @@ +export const getBitcoinPrices = marketChart => + (Array.isArray(marketChart?.prices) ? marketChart.prices : []) + .map(price => Array.isArray(price) ? price[1] : null) + .filter(Number.isFinite) + +export const getLatestBitcoinPrice = marketChart => { + const prices = getBitcoinPrices(marketChart) + return prices.length ? prices[prices.length - 1] : null +} diff --git a/client/src/lib/pending-block-details.js b/client/src/lib/pending-block-details.js index 8b2c8c60..b93a5bab 100644 --- a/client/src/lib/pending-block-details.js +++ b/client/src/lib/pending-block-details.js @@ -47,9 +47,6 @@ const formatTrimmedDecimal = (value) => export const formatPanelPercentage = (value, fallback = "N/A") => Number.isFinite(value) ? `${formatTrimmedDecimal(value)}%` : fallback; -export const formatFeeRate = (value, fallback = "N/A") => - Number.isFinite(value) ? `${value.toFixed(2)} sat/vB` : fallback; - export const formatFeeBoundary = (value, fallback = "N/A") => Number.isFinite(value) ? value.toFixed(2) : fallback; @@ -66,22 +63,6 @@ export const formatMegabytes = (value, fallback = "N/A") => export const formatCount = (value, fallback = "N/A") => Number.isFinite(value) ? value.toLocaleString() : fallback; -export const getLatestBitcoinPrice = (marketChart) => { - const prices = ((marketChart && marketChart.prices) || []) - .map((price) => price && price[1]) - .filter(Number.isFinite); - - return prices.length ? prices[prices.length - 1] : null; -}; - -export const formatUsd = (value, fallback = "N/A") => - Number.isFinite(value) - ? `$${value.toLocaleString("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })} USD` - : fallback; - export const formatTransactionDelta = (delta) => delta > 0 ? `+ ${formatCount(delta)}` diff --git a/client/src/views/fee-market.js b/client/src/views/fee-market.js new file mode 100644 index 00000000..580ecb05 --- /dev/null +++ b/client/src/views/fee-market.js @@ -0,0 +1,53 @@ +import { InfoCard } from "../components/info-card"; +import { feeEstimateTargets } from "../const"; +import { estimateTypicalTransactionFeeUsd } from "../lib/fees"; +import { getLatestBitcoinPrice } from "../lib/market"; +import { formatFeeRate, formatUsd } from "./util"; + +const getFeeMarketLevels = (t) => [ + { + key: "low", + title: t`Low`, + tooltip: t`A lower-priority fee rate estimated to confirm within ${feeEstimateTargets.low} blocks.`, + }, + { + key: "average", + title: t`Average`, + tooltip: t`A balanced fee rate estimated to confirm within ${feeEstimateTargets.average} blocks.`, + }, + { + key: "high", + title: t`High`, + tooltip: t`A higher-priority fee rate estimated to confirm in the next block.`, + }, +]; + +export const feeMarket = ({ feeEst, bitcoinMarketChart, t } = {}) => { + const bitcoinPrice = getLatestBitcoinPrice(bitcoinMarketChart); + const unavailable = t`N/A`; + + return ( +
+

{t`Fee Market`}

+
+ {getFeeMarketLevels(t).map(({ key, title, tooltip }) => { + const feerate = feeEst && feeEst[feeEstimateTargets[key]]; + const feeUsd = estimateTypicalTransactionFeeUsd( + bitcoinPrice, + feerate, + ); + + return ( + + ); + })} +
+
+ ); +}; diff --git a/client/src/views/home.js b/client/src/views/home.js index 01124cf4..df7d1f61 100644 --- a/client/src/views/home.js +++ b/client/src/views/home.js @@ -3,6 +3,7 @@ import { blks } from "./blocks"; import { transactions } from "./transactions"; import { pegInfo } from "./peg-info"; import { overview } from "./overview"; +import { feeMarket } from "./fee-market"; import difficultyAdjustment from "./difficulty-adjustment"; import { isBitcoinNetwork } from "../lib/network"; import { showPegData } from "../const"; @@ -19,12 +20,14 @@ export const dashBoard = ({ t, blocks, dashboardState, loading, ...S }) => {
{overview({ blocks: dashblocks, t, ...S })} {blks(dashblocks, true, { t, ...S })} + {isBitcoinNetwork ? feeMarket({ t, ...S }) : ""}
{transactions(dashTxs, true, { t, ...S })} {showPegData ? pegInfo(peg.asset, peg.txs, { t, ...S, error: peg.error }) : ""}
+ {!isBitcoinNetwork ? feeMarket({ t, ...S }) : ""} {isBitcoinNetwork ? difficultyAdjustment({ blocks: dashblocks, ...S }) : ""} diff --git a/client/src/views/overview.js b/client/src/views/overview.js index 1b0e5976..61408c27 100644 --- a/client/src/views/overview.js +++ b/client/src/views/overview.js @@ -1,47 +1,20 @@ -import { - averageNativeSegwitTransactionSize, - satoshisPerBitcoin, -} from "../const"; +import { feeEstimateTargets } from "../const"; import { ElapsedTime } from "../components/elapsed-time"; import { InfoCard } from "../components/info-card"; import { MempoolCongestion } from "../components/mempool-congestion"; import { ReferenceLineChart } from "../components/reference-line-chart"; +import { estimateTypicalTransactionFeeUsd } from "../lib/fees"; +import { + getBitcoinPrices, + getLatestBitcoinPrice, +} from "../lib/market"; +import { formatFeeRate, formatUsd } from "./util"; const staticRoot = process.env.STATIC_ROOT || ""; -const getBitcoinPrices = (marketChart) => - ((marketChart && marketChart.prices) || []) - .map((price) => price && price[1]) - .filter(Number.isFinite); - const getChartPrices = (marketChart) => getBitcoinPrices(marketChart).slice(-24); -const formatUsd = (value) => - Number.isFinite(value) - ? `$${value.toLocaleString("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })} USD` - : ""; - -const formatRecommendedFee = (feeEst) => - feeEst && Number.isFinite(feeEst[3]) ? `${feeEst[3].toFixed(1)} sat/vB` : ""; - -const estimateNativeSegwitFeeUsd = (bitcoinPrice, feeEst) => - Number.isFinite(bitcoinPrice) && feeEst && Number.isFinite(feeEst[3]) - ? formatUsd( - (bitcoinPrice / satoshisPerBitcoin) * - feeEst[3] * - averageNativeSegwitTransactionSize, - ) - : ""; - -const getLatestPrice = (marketChart) => { - const prices = getBitcoinPrices(marketChart); - return prices.length ? prices[prices.length - 1] : null; -}; - export const overview = ({ blocks, feeEst, @@ -51,10 +24,12 @@ export const overview = ({ } = {}) => { const latestBlock = blocks && blocks[0]; const chartPrices = getChartPrices(bitcoinMarketChart); - const currentBitcoinPrice = getLatestPrice(bitcoinMarketChart); - const recommendedFeeUsd = estimateNativeSegwitFeeUsd( + const currentBitcoinPrice = getLatestBitcoinPrice(bitcoinMarketChart); + const recommendedFeerate = + feeEst && feeEst[feeEstimateTargets.average]; + const recommendedFeeUsd = estimateTypicalTransactionFeeUsd( currentBitcoinPrice, - feeEst, + recommendedFeerate, ); return (
@@ -78,8 +53,8 @@ export const overview = ({

{t`Low`} ( - {`≤${formatFeeBoundary(feeTierBoundaries.low)} sat/vB`} + {`≤${formatFeeBoundary(feeTierBoundaries.lowMaximum)} sat/vB`} )

@@ -524,7 +522,7 @@ const PendingBlockDetailsCard = ({

{t`Medium`} ( - {`>${formatFeeBoundary(feeTierBoundaries.low)}–≤${formatFeeBoundary(feeTierBoundaries.high)} sat/vB`} + {`>${formatFeeBoundary(feeTierBoundaries.lowMaximum)}–≤${formatFeeBoundary(feeTierBoundaries.mediumMaximum)} sat/vB`} )

@@ -532,7 +530,7 @@ const PendingBlockDetailsCard = ({

{t`High`} ( - {`>${formatFeeBoundary(feeTierBoundaries.high)} sat/vB`} + {`>${formatFeeBoundary(feeTierBoundaries.mediumMaximum)} sat/vB`} )

diff --git a/client/src/views/util.js b/client/src/views/util.js index b6ca0902..d9dd5b0a 100644 --- a/client/src/views/util.js +++ b/client/src/views/util.js @@ -24,6 +24,20 @@ export const formatTime = (unix, with_tz = true) => { export const formatSat = (sats, label=nativeAssetLabel) => `${formatNumber(sat2btc(sats), NATIVE_PRECISION)} ${label}` +const formatTrimmedDecimal = value => + value.toFixed(2).replace(/\.?0+$/, '') + +export const formatFeeRate = (feerate, fallback='N/A') => + Number.isFinite(feerate) ? `${formatTrimmedDecimal(feerate)} sat/vB` : fallback + +export const formatUsd = (value, fallback='N/A') => + Number.isFinite(value) + ? `$${value.toLocaleString('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} USD` + : fallback + export const formatAssetAmount = (value, precision=0, t) => {formatNumber(precision > 0 ? moveDec(value, -precision) : value, precision)} diff --git a/lang/strings.txt b/lang/strings.txt index 40e1b8d6..21f57f14 100644 --- a/lang/strings.txt +++ b/lang/strings.txt @@ -1,3 +1,6 @@ +A balanced fee rate estimated to confirm within %s blocks. +A higher-priority fee rate estimated to confirm in the next block. +A lower-priority fee rate estimated to confirm within %s blocks. Address Address reuse Address: %s @@ -10,6 +13,7 @@ Asset ID Asset name Asset: %s Assets vs Liabilities +Average Bits BLOCK Block Challenge @@ -55,9 +59,11 @@ Esplora is currently unavailable, please try again later. ETA Estimated time until the peg transaction is confirmed. Fee +Fee Market Federation BTC Holdings Go Height +High How full the block is. In block In best chain @@ -81,6 +87,7 @@ Linked domain Loading block... Loading... Load more +Low Lock time ltr Mempool diff --git a/test/fee-market.test.js b/test/fee-market.test.js new file mode 100644 index 00000000..d5de8f2d --- /dev/null +++ b/test/fee-market.test.js @@ -0,0 +1,75 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const render = require("snabbdom-to-html"); + +const { feeMarket } = require("../client/src/views/fee-market"); + +const t = (parts, ...values) => parts.reduce( + (result, part, index) => + result + part + (index < values.length ? values[index] : ""), + "", +); + +test("renders low, average, and high fee estimates with dollar footers", () => { + const html = render(feeMarket({ + feeEst: { + 1: 12, + 3: 6, + 12: 2, + }, + bitcoinMarketChart: { + prices: [[1, 40_000], [2, 50_000], [3, null]], + }, + t, + })); + + const expectedUsd = process.env.IS_ELEMENTS + ? ["$0.26 USD", "$0.77 USD", "$1.55 USD"] + : ["$0.14 USD", "$0.42 USD", "$0.84 USD"]; + + assert.match(html, /Low[\s\S]*2 sat\/vB/); + assert.match(html, /Average[\s\S]*6 sat\/vB/); + assert.match(html, /High[\s\S]*12 sat\/vB/); + expectedUsd.forEach((value) => assert(html.includes(value), value)); + assert.match(html, /within 12 blocks/); + assert.match(html, /within 3 blocks/); + assert.match(html, /in the next block/); + assert.doesNotMatch(html, /\[object Object\]/); +}); + +test("keeps fee market panels visible while estimates are unavailable", () => { + const html = render(feeMarket({ t })); + + assert.match(html, /Fee Market/); + assert.match(html, /Low/); + assert.match(html, /Average/); + assert.match(html, /High/); + assert.match(html, /N\/A/); + assert.doesNotMatch(html, /sat\/vB/); + assert.doesNotMatch(html, /USD/); +}); + +test("passes fee market copy through localization", () => { + const localizedStrings = new Set(); + const localizedT = (parts, ...values) => { + localizedStrings.add(parts.join("%s")); + return parts.reduce( + (result, part, index) => + result + part + (index < values.length ? values[index] : ""), + "", + ); + }; + + render(feeMarket({ t: localizedT })); + + [ + "A balanced fee rate estimated to confirm within %s blocks.", + "A higher-priority fee rate estimated to confirm in the next block.", + "A lower-priority fee rate estimated to confirm within %s blocks.", + "Average", + "Fee Market", + "High", + "Low", + "N/A", + ].forEach((value) => assert(localizedStrings.has(value), value)); +}); diff --git a/test/fees.test.js b/test/fees.test.js index 9a74331d..c82ab803 100644 --- a/test/fees.test.js +++ b/test/fees.test.js @@ -3,6 +3,7 @@ const assert = require("node:assert/strict"); const { feeRateClass, + estimateTypicalTransactionFeeUsd, feerateCutoff, getConfEstimate, getFeeTierBoundaries, @@ -10,12 +11,12 @@ const { test("validates and applies fee tier boundaries", () => { assert.deepEqual(getFeeTierBoundaries({ 3: 10, 12: 2 }), { - low: 2, - high: 10, + lowMaximum: 2, + mediumMaximum: 10, }); assert.deepEqual(getFeeTierBoundaries({ 3: 1, 12: 2 }), { - low: 2, - high: 2, + lowMaximum: 2, + mediumMaximum: 2, }); assert.equal(getFeeTierBoundaries(), null); assert.equal(getFeeTierBoundaries({ 3: 10, 12: -1 }), null); @@ -55,3 +56,14 @@ test("selects a confirmation target by numeric order", () => { assert.equal(getConfEstimate(estimates, 6), "10"); assert.equal(getConfEstimate(estimates, 0.5), -1); }); + +test("estimates a typical transaction fee in dollars", () => { + const expectedFee = process.env.IS_ELEMENTS ? 1.548 : 0.84; + + assert.equal(estimateTypicalTransactionFeeUsd(50_000, 12), expectedFee); + assert.equal(estimateTypicalTransactionFeeUsd(50_000, 0), 0); + assert.equal(estimateTypicalTransactionFeeUsd(null, 12), null); + assert.equal(estimateTypicalTransactionFeeUsd(50_000, undefined), null); + assert.equal(estimateTypicalTransactionFeeUsd(-50_000, 12), null); + assert.equal(estimateTypicalTransactionFeeUsd(50_000, -12), null); +}); diff --git a/test/format-number.test.js b/test/format-number.test.js index 4dda28f5..78804b23 100644 --- a/test/format-number.test.js +++ b/test/format-number.test.js @@ -1,7 +1,11 @@ const test = require("node:test"); const assert = require("node:assert/strict"); -const { formatNumber } = require("../client/src/views/util"); +const { + formatFeeRate, + formatNumber, + formatUsd, +} = require("../client/src/views/util"); test("separates whole-number digits into groups of three", () => { assert.equal(formatNumber(999), "999"); @@ -17,3 +21,17 @@ test("preserves exact decimal strings and requested precision", () => { ); assert.equal(formatNumber("1234.5", 3), "1,234.500"); }); + +test("formats fee rates with up to two decimal places and a fallback", () => { + assert.equal(formatFeeRate(2), "2 sat/vB"); + assert.equal(formatFeeRate(2.5), "2.5 sat/vB"); + assert.equal(formatFeeRate(2.555), "2.56 sat/vB"); + assert.equal(formatFeeRate(null), "N/A"); + assert.equal(formatFeeRate(undefined, "-"), "-"); +}); + +test("formats dollar values with a fallback", () => { + assert.equal(formatUsd(1_234.5), "$1,234.50 USD"); + assert.equal(formatUsd(null), "N/A"); + assert.equal(formatUsd(undefined, "-"), "-"); +}); diff --git a/test/market.test.js b/test/market.test.js new file mode 100644 index 00000000..e11ec1c4 --- /dev/null +++ b/test/market.test.js @@ -0,0 +1,20 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); + +const { + getBitcoinPrices, + getLatestBitcoinPrice, +} = require("../client/src/lib/market"); + +test("selects finite market prices and the latest available value", () => { + const marketChart = { + prices: [[1, 100], null, [2, NaN], [3, 125]], + }; + + assert.deepEqual(getBitcoinPrices(marketChart), [100, 125]); + assert.equal(getLatestBitcoinPrice(marketChart), 125); + assert.deepEqual(getBitcoinPrices(null), []); + assert.equal(getLatestBitcoinPrice(null), null); + assert.deepEqual(getBitcoinPrices({ prices: {} }), []); + assert.deepEqual(getBitcoinPrices({ prices: "invalid" }), []); +}); diff --git a/test/overview.test.js b/test/overview.test.js new file mode 100644 index 00000000..8a39b279 --- /dev/null +++ b/test/overview.test.js @@ -0,0 +1,36 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const render = require("snabbdom-to-html"); + +const { overview } = require("../client/src/views/overview"); + +const t = (parts, ...values) => parts.reduce( + (result, part, index) => + result + part + (index < values.length ? values[index] : ""), + "", +); + +test("formats the recommended fee rate and dollar estimate", () => { + const html = render(overview({ + blocks: [], + feeEst: { 3: 6 }, + mempool: { vsize: 0 }, + bitcoinMarketChart: { prices: [[1, 50_000]] }, + t, + })); + + const expectedFeeUsd = process.env.IS_ELEMENTS ? "$0.77 USD" : "$0.42 USD"; + + assert.match( + html, + /Recommended Fee[\s\S]*6 sat\/vB/, + ); + assert(html.includes(expectedFeeUsd), expectedFeeUsd); + assert.match(html, /\$50,000\.00 USD/); +}); + +test("shows unavailable overview fee and price values explicitly", () => { + const html = render(overview({ blocks: [], mempool: null, t })); + + assert.match(html, /Recommended Fee[\s\S]*N\/A[\s\S]*N\/A/); +}); diff --git a/test/pending-block-details.test.js b/test/pending-block-details.test.js index 2d9bdfcf..d6596145 100644 --- a/test/pending-block-details.test.js +++ b/test/pending-block-details.test.js @@ -5,14 +5,11 @@ const { formatCount, formatEstimatedBlockTime, formatFeeBoundary, - formatFeeRate, formatMegabytes, formatPanelPercentage, formatPercentage, formatTransactionDelta, - formatUsd, formatWeight, - getLatestBitcoinPrice, } = require("../client/src/lib/pending-block-details"); const { targetBlockIntervalSeconds, @@ -62,22 +59,13 @@ test("counts down to and measures time past the expected block interval", () => test("formats pending-block metric values and fallbacks", () => { assert.equal(formatPercentage(12.345), "12.35%"); assert.equal(formatPanelPercentage(12.3), "12.3%"); - assert.equal(formatFeeRate(2.5), "2.50 sat/vB"); assert.equal(formatFeeBoundary(2.5), "2.50"); assert.equal(formatWeight(1_250_000), "1.25 MWU"); assert.equal(formatMegabytes(2_500_000), "2.5 MB"); assert.equal(formatCount(12_345), "12,345"); - assert.equal(formatUsd(1_234.5), "$1,234.50 USD"); assert.equal(formatPercentage(null, "-"), "-"); }); -test("selects the latest finite market price", () => { - assert.equal(getLatestBitcoinPrice({ - prices: [[1, 100], null, [2, NaN], [3, 125]], - }), 125); - assert.equal(getLatestBitcoinPrice(null), null); -}); - test("formats pending transaction changes", () => { assert.equal(formatTransactionDelta(12), "+ 12"); assert.equal(formatTransactionDelta(-12), "− 12"); diff --git a/www/style.css b/www/style.css index 0f75ff53..9da6f397 100644 --- a/www/style.css +++ b/www/style.css @@ -3946,6 +3946,31 @@ a.back-link img{ gap: 12px; } +.fee-market { + display: flex; + flex-direction: column; + gap: 12px; +} + +.fee-market-body { + display: flex; + gap: var(--page-gap); +} + +.fee-market .info-card-container { + flex: 1; + width: auto; + min-width: 0; +} + +.fee-market-low .info-card-value { + color: var(--success-color); +} + +.fee-market-high .info-card-value { + color: var(--danger-color); +} + .info-stats-row { display: flex; gap: 12px; @@ -5665,6 +5690,14 @@ a.back-link img{ .dashboard-transaction-section { flex-direction: column; } + + .fee-market-body { + flex-wrap: wrap; + } + + .fee-market .info-card-container { + flex-basis: 260px; + } } @media only screen and (max-width: 1328px) and (prefers-reduced-motion: reduce) {