Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion client/src/const.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions client/src/lib/block-template.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { maxBlockWeight } from "../const";
import { feeEstimateTargets, maxBlockWeight } from "../const";
import { feeRateClass } from "./fees";
import { clamp } from "./math";

Expand Down Expand Up @@ -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 };
}

Expand Down
34 changes: 28 additions & 6 deletions client/src/lib/fees.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
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
}

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"
}
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions client/src/lib/market.js
Original file line number Diff line number Diff line change
@@ -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)
Comment thread
Randy808 marked this conversation as resolved.

export const getLatestBitcoinPrice = marketChart => {
const prices = getBitcoinPrices(marketChart)
return prices.length ? prices[prices.length - 1] : null
}
19 changes: 0 additions & 19 deletions client/src/lib/pending-block-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)}`
Expand Down
53 changes: 53 additions & 0 deletions client/src/views/fee-market.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="fee-market">
<p className="section-title">{t`Fee Market`}</p>
<div className="fee-market-body">
{getFeeMarketLevels(t).map(({ key, title, tooltip }) => {
const feerate = feeEst && feeEst[feeEstimateTargets[key]];
const feeUsd = estimateTypicalTransactionFeeUsd(
bitcoinPrice,
feerate,
);

return (
<InfoCard
className={`fee-market-${key}`}
title={title}
tooltip={tooltip}
value={formatFeeRate(feerate, unavailable)}
footer={formatUsd(feeUsd, unavailable)}
Comment thread
Randy808 marked this conversation as resolved.
/>
);
})}
</div>
</div>
);
};
3 changes: 3 additions & 0 deletions client/src/views/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -19,12 +20,14 @@ export const dashBoard = ({ t, blocks, dashboardState, loading, ...S }) => {
<div className="home-page" key="dashBoard">
{overview({ blocks: dashblocks, t, ...S })}
{blks(dashblocks, true, { t, ...S })}
{isBitcoinNetwork ? feeMarket({ t, ...S }) : ""}
<div className="dashboard-transaction-section">
{transactions(dashTxs, true, { t, ...S })}
{showPegData
? pegInfo(peg.asset, peg.txs, { t, ...S, error: peg.error })
: ""}
</div>
{!isBitcoinNetwork ? feeMarket({ t, ...S }) : ""}
{isBitcoinNetwork
? difficultyAdjustment({ blocks: dashblocks, ...S })
: ""}
Expand Down
55 changes: 15 additions & 40 deletions client/src/views/overview.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 (
<div className="overview">
Expand All @@ -78,8 +53,8 @@ export const overview = ({
<InfoCard
title={t`Recommended Fee`}
tooltip={t`Suggested rate (sat/vB) to confirm in the next block or two.`}
value={formatRecommendedFee(feeEst)}
footer={recommendedFeeUsd}
value={formatFeeRate(recommendedFeerate, t`N/A`)}
footer={formatUsd(recommendedFeeUsd, t`N/A`)}
/>

<InfoCard
Expand All @@ -96,7 +71,7 @@ export const overview = ({
<InfoCard
title={t`Bitcoin`}
iconSrc={`${staticRoot}img/icons/Bitcoin-menu-logo.svg`}
headerValue={formatUsd(currentBitcoinPrice)}
headerValue={formatUsd(currentBitcoinPrice, t`N/A`)}
body={
<ReferenceLineChart
className="overview-bitcoin-price-chart"
Expand Down
18 changes: 8 additions & 10 deletions client/src/views/pending-block-details-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,14 @@ import { getFeeTierBoundaries } from "../lib/fees";
import {
formatCount,
formatFeeBoundary,
formatFeeRate,
formatMegabytes,
formatPanelPercentage,
formatPercentage,
formatTransactionDelta,
formatUsd,
formatWeight,
getLatestBitcoinPrice,
} from "../lib/pending-block-details";
import { formatSat, formatVMB } from "./util";
import { getLatestBitcoinPrice } from "../lib/market";
import { formatFeeRate, formatSat, formatUsd, formatVMB } from "./util";

// Fee estimates are inclusive upper bounds, while the grid expects inclusive
// lower thresholds. Move just beyond a boundary so an equal rate stays in the
Expand All @@ -55,14 +53,14 @@ const getBlockGridConfig = (
color: rgbColor("--success-color-rgb"),
},
medium: {
threshold: exclusiveFeeTierMinimum(boundaries.low),
threshold: exclusiveFeeTierMinimum(boundaries.lowMaximum),
color: rgbColor("--warning-color-rgb"),
},
high: {
threshold: exclusiveFeeTierMinimum(
Math.max(
boundaries.high,
exclusiveFeeTierMinimum(boundaries.low),
boundaries.mediumMaximum,
exclusiveFeeTierMinimum(boundaries.lowMaximum),
),
),
color: rgbColor("--danger-color-rgb"),
Expand Down Expand Up @@ -516,23 +514,23 @@ const PendingBlockDetailsCard = ({
<div className="pending-block-color-reference success"></div>
<p className="pending-block-legend-label">
{t`Low`} (
{`≤${formatFeeBoundary(feeTierBoundaries.low)} sat/vB`}
{`≤${formatFeeBoundary(feeTierBoundaries.lowMaximum)} sat/vB`}
)
</p>
</div>
<div className="pending-block-legend-item">
<div className="pending-block-color-reference warning"></div>
<p className="pending-block-legend-label">
{t`Medium`} (
{`>${formatFeeBoundary(feeTierBoundaries.low)}–≤${formatFeeBoundary(feeTierBoundaries.high)} sat/vB`}
{`>${formatFeeBoundary(feeTierBoundaries.lowMaximum)}–≤${formatFeeBoundary(feeTierBoundaries.mediumMaximum)} sat/vB`}
)
</p>
</div>
<div className="pending-block-legend-item">
<div className="pending-block-color-reference danger"></div>
<p className="pending-block-legend-label">
{t`High`} (
{`>${formatFeeBoundary(feeTierBoundaries.high)} sat/vB`}
{`>${formatFeeBoundary(feeTierBoundaries.mediumMaximum)} sat/vB`}
)
</p>
</div>
Expand Down
14 changes: 14 additions & 0 deletions client/src/views/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
<span>
{formatNumber(precision > 0 ? moveDec(value, -precision) : value, precision)}
Expand Down
Loading