Skip to content
Open
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ Browser polling uses three shared cadence tiers defined in `client/src/const.js`
- **Slow (10 minutes):** the Bitcoin market chart, the Liquid high-value-assets
panel, and a block-list safety poll used to detect same-height reorganizations.

On Bitcoin and Liquid mainnet, the market chart also supplies the USD values
shown beside BTC amounts, so it polls on every page that shows them. Other
networks poll it only on the dashboard.

View-scoped polls run only while their view is active and the browser window is
focused. Their first tick occurs after a full interval because route entry
already requests the initial data. This avoids sending duplicate requests when
Expand Down
44 changes: 38 additions & 6 deletions client/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {setAdapt} from '@cycle/run/lib/adapt';
import { getMempoolDepth, getConfEstimate, calcSegwitFeeGains } from './lib/fees'
import { summarizeBlockTemplate } from './lib/block-template'
import { getPriceFeedApiBase } from './lib/high-value-assets'
import { isBitcoinNetwork } from './lib/network'
import { getLatestBitcoinPrice } from './lib/market'
import { isBitcoinNetwork, isBitcoinOrLiquidMainnet } from './lib/network'
import getPrivacyAnalysis from './lib/privacy-analysis'
import {
highValueAssetDefinitions,
Expand Down Expand Up @@ -158,6 +159,21 @@ export const parseTipHeight = text => {

const isUnconfirmedTx = tx => !!tx && !tx.status.confirmed

const isBitcoinUsdValuePage = pathname =>
pathname == '/' || pathname == '/tx/recent' || pathname == '/mempool'
|| /^\/(tx|block)\/[a-f0-9]{64}\/?$/i.test(pathname)
|| /^\/address\/[^/]+\/?$/.test(pathname)
|| (!!process.env.IS_ELEMENTS && /^\/asset\/[a-f0-9]{64}\/?$/i.test(pathname))

// Keep a trigger only when `ms` has passed since the last kept trigger.
const skipWhileFresh = (ms, now, trigger$) =>
trigger$
.scan(({ sentAt }) => {
const at = now()
return at - sentAt >= ms ? { sentAt: at, send: true } : { sentAt, send: false }
}, { sentAt: -Infinity })
.filter(({ send }) => send)

export const subsequentTipHeights = tipHeight$ =>
tipHeight$.distinctUntilChanged().skip(1)

Expand Down Expand Up @@ -248,6 +264,10 @@ export default function main(

/// User actions
, page$ = route()
, bitcoinUsdValuePage$ = page$
.map(({ pathname }) => pathname.replace(/\/$/, '') || '/')
.map(pathname => isBitcoinOrLiquidMainnet && isBitcoinUsdValuePage(pathname) ? pathname : null)
.distinctUntilChanged()
, goHome$ = route('/')
, goAPILanding$= route('/explorer-api')
, goBlocks$ = route('/blocks/recent').map(loc => ({ start_height: loc.query.start != null ? +loc.query.start : null }))
Expand Down Expand Up @@ -503,6 +523,7 @@ export default function main(

// Bitcoin price chart data
, bitcoinMarketChart$ = reply('bitcoin-market-chart').startWith(null)
, bitcoinPrice$ = bitcoinMarketChart$.map(getLatestBitcoinPrice).distinctUntilChanged().shareReplay(1)

// Transaction analysis
, txAnalysis$ = tx$.filter(Boolean)
Expand Down Expand Up @@ -615,7 +636,7 @@ export default function main(
, dashboardEpochStartBlock$, dashboardPreviousDifficultyBlock$
, newBlockEntries$, newTxEntries$
, goBlock$, block$, blockStatus$, blockTxs$, nextBlockTxs$, prevBlockTxs$, openBlock$
, mempool$, mempoolRecent$, feeEst$, bitcoinMarketChart$
, mempool$, mempoolRecent$, feeEst$, bitcoinMarketChart$, bitcoinPrice$
, tx$, txBlock$, previousBlock$, txAnalysis$, openTx$
, goAddr$, addr$, addrTxs$, addrQR$
, assetMap$, assetList$, goAssetList$, goAsset$, asset$, assetTxs$, unblinded$
Expand Down Expand Up @@ -743,8 +764,20 @@ export default function main(
)
.mapTo( { category: 'recent', method: 'GET', path: '/mempool/recent', bg: true })

// the market chart contains hourly samples and uses the slow cadence
, pollViewing(pollIntervalsMs.slow, 'dashBoard', view$)
// One shared feed supplies the chart and all mainnet BTC/LBTC amounts. Its
// timer spans all price pages, and page entry reuses a price requested
// within the slow interval. Other networks keep dashboard-only fetching.
, (isBitcoinOrLiquidMainnet
? skipWhileFresh(
pollIntervalsMs.slow,
() => pollingScheduler ? pollingScheduler.now() : Date.now(),
O.merge(
bitcoinUsdValuePage$.filter(Boolean),
pollActive(pollIntervalsMs.slow, bitcoinUsdValuePage$.map(page => page ? true : null))
)
)
: O.merge(goHome$, pollViewing(pollIntervalsMs.slow, 'dashBoard', view$))
)
.mapTo({ category: 'bitcoin-market-chart', method: 'GET', path: bitcoinMarketChartUrl, bg: true })

// Refresh the pending block template while the dashboard remains open. A new
Expand All @@ -755,8 +788,7 @@ export default function main(
, goHome$.flatMap(_ => [{ category: 'blocks', method: 'GET', path: '/blocks' }
, { category: 'recent', method: 'GET', path: '/mempool/recent' }
, { category: 'fee-est', method: 'GET', path: '/fee-estimates' }
, { category: 'mempool', method: 'GET', path: '/mempool' }
, { category: 'bitcoin-market-chart', method: 'GET', path: bitcoinMarketChartUrl, bg: true }])
, { category: 'mempool', method: 'GET', path: '/mempool' }])

// fetch asset stats and USD prices only while viewing the Liquid dashboard
, !showHighValueAssets ? O.empty() :
Expand Down
9 changes: 9 additions & 0 deletions client/src/components/amount-with-usd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// An empty secondary line keeps its height so rows do not grow when a price arrives.
export const AmountWithUsd = ({ primary, secondary }) => (
<span className="amount-with-usd">
<span className="amount-with-usd-primary">{primary}</span>
{secondary == null
? <span className="amount-with-usd-secondary" aria-hidden="true">{"\u00a0"}</span>
: <span className="amount-with-usd-secondary">{secondary}</span>}
</span>
);
3 changes: 3 additions & 0 deletions client/src/lib/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ const activeNetwork = process.env.MENU_ACTIVE || "";
export const isBitcoinNetwork = activeNetwork
.toLowerCase()
.startsWith("bitcoin");

export const isBitcoinOrLiquidMainnet = ["bitcoin", "liquid"]
.includes(activeNetwork.toLowerCase());
36 changes: 31 additions & 5 deletions client/src/views/addr.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { last } from "../util";
import layout from "./layout";
import { txBox } from "./tx";
import { formatSat, formatNumber } from "./util";
import { formatSat, formatSatWithUsd, formatNumber } from "./util";
import { addrTxsPerPage as perPage, maxMempoolTxs } from "../const";
import loader from "../components/loading";
import { AddressHamburgerIcon, CopyIcon } from "../components/icons";
import { InfoStat } from "../components/info-stat";
import { Tooltip } from "../components/tooltip";
import { isBitcoinOrLiquidMainnet } from "../lib/network";

const staticRoot = process.env.STATIC_ROOT || "";

Expand Down Expand Up @@ -113,6 +114,7 @@ export default ({
chain_stats.funded_txo_count,
chain_stats.funded_txo_sum,
t,
S,
)}
/>
<InfoStat
Expand All @@ -121,11 +123,12 @@ export default ({
chain_stats.spent_txo_count,
chain_stats.spent_txo_sum,
t,
S,
)}
/>
<InfoStat
title={t`Confirmed unspent`}
value={fmtTxos(chain_utxo_count, chain_utxo_sum, t)}
value={fmtTxos(chain_utxo_count, chain_utxo_sum, t, S)}
/>
</div>
</div>
Expand Down Expand Up @@ -155,6 +158,8 @@ export default ({
mempool_stats.funded_txo_count,
mempool_stats.funded_txo_sum,
t,
S,
true,
)}
</div>
</div>
Expand All @@ -168,14 +173,16 @@ export default ({
mempool_stats.spent_txo_count,
mempool_stats.spent_txo_sum,
t,
S,
true,
)}
</div>
</div>

<div className="detail-field">
<p className="detail-field-label">{t`Total unspent`}</p>
<div className="detail-field-content">
{fmtTxos(total_utxo_count, total_utxo_sum, t)}
{fmtTxos(total_utxo_count, total_utxo_sum, t, S, true)}
</div>
</div>
</div>
Expand Down Expand Up @@ -225,13 +232,32 @@ export default ({
);
};

const fmtTxos = (count, sum, t) => {
const fmtTxos = (count, sum, t, S, usdBelowOutputs = false) => {
const outputs =
count > 0 ? t`${formatNumber(count)} outputs` : t`No outputs`;

if (!Number.isFinite(sum)) return outputs;
if (count <= 0) return outputs;

return count > 0 ? `${formatSat(sum)} (${outputs})` : outputs;
if (!isBitcoinOrLiquidMainnet) return `${formatSat(sum)} (${outputs})`;

if (usdBelowOutputs) return (
<span className="address-output-summary address-output-summary-stacked">
{formatSatWithUsd(sum, { t, ...S }, (
<span className="address-output-primary">
<span>{formatSat(sum)}</span>
<span className="address-output-count">({outputs})</span>
</span>
))}
</span>
);

return (
<span className="address-output-summary">
{formatSatWithUsd(sum, { t, ...S })}
<span className="address-output-count">({outputs})</span>
</span>
);
};

const pagingNav = (
Expand Down
10 changes: 5 additions & 5 deletions client/src/views/asset.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { last } from '../util'
import { formatNumber, formatAssetAmount, formatSat } from './util'
import { formatNumber, formatAssetAmount, formatSatWithUsd } from './util'
import layout from './layout'
import { txBox } from './tx'
import { maxMempoolTxs, assetTxsPerPage as perPage, nativeAssetName } from '../const'
Expand Down Expand Up @@ -73,16 +73,16 @@ export default ({ t, asset, assetTxs, goAsset, openTx, spends, tipHeight, loadin
: shortenValue(asset.asset_id)
, stats = is_native_asset
? [
{ title: 'TOTAL SUPPLY', value: formatSat(circulating) },
{ title: 'TOTAL SUPPLY', value: formatSatWithUsd(circulating, { t, ...S }) },
{
title: 'PEGGED IN',
value: formatSat(value(chain_stats.peg_in_amount) + value(mempool_stats.peg_in_amount))
value: formatSatWithUsd(value(chain_stats.peg_in_amount) + value(mempool_stats.peg_in_amount), { t, ...S })
},
{
title: 'PEGGED OUT',
value: formatSat(value(chain_stats.peg_out_amount) + value(mempool_stats.peg_out_amount))
value: formatSatWithUsd(value(chain_stats.peg_out_amount) + value(mempool_stats.peg_out_amount), { t, ...S })
},
{ title: 'BURNED', value: formatSat(total_burned) },
{ title: 'BURNED', value: formatSatWithUsd(total_burned, { t, ...S }) },
{ title: 'TRANSACTIONS', value: formatNumber(total_txs) }
]
: [
Expand Down
1 change: 1 addition & 0 deletions client/src/views/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export default ({
tipHeight,
t,
spends,
...S,
listingIndex: +goBlock.start_index + index + 1,
listingTotal: b.tx_count,
},
Expand Down
1 change: 1 addition & 0 deletions client/src/views/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const blks = (blocks, viewMore, { t, ...S }) => {
{viewMore ? (
<PendingBlockDetailsCard
bitcoinMarketChart={S.bitcoinMarketChart}
bitcoinPrice={S.bitcoinPrice}
block={blocks[0]}
blockTemplate={
blockTemplateState && blockTemplateState.template
Expand Down
4 changes: 1 addition & 3 deletions client/src/views/fee-market.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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) => [
Expand All @@ -22,8 +21,7 @@ const getFeeMarketLevels = (t) => [
},
];

export const feeMarket = ({ feeEst, bitcoinMarketChart, t } = {}) => {
const bitcoinPrice = getLatestBitcoinPrice(bitcoinMarketChart);
export const feeMarket = ({ feeEst, bitcoinPrice, t } = {}) => {
const unavailable = t`N/A`;

return (
Expand Down
3 changes: 2 additions & 1 deletion client/src/views/layout.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import navbar from './navbar'
import footer from './footer'
import subnav from './sub-navbar'
import { isBitcoinOrLiquidMainnet } from '../lib/network'

export default (body, opt) => {
const mainKey = opt.view || opt.activeTab || opt.page && opt.page.pathname || 'main'

return <div className="explorer-container">
{ navbar(opt) }
{ subnav(opt.t, opt.isTouch, opt.activeTab, opt.page) }
<main className="explorer-main" key={mainKey}>
<main className={`explorer-main${isBitcoinOrLiquidMainnet ? ' bitcoin-usd-values' : ''}`} key={mainKey}>
{ body }
</main>
{ footer(opt) }
Expand Down
4 changes: 2 additions & 2 deletions client/src/views/mempool.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getMempoolDepth, squashFeeHistogram, feerateCutoff } from '../lib/fees'
import { formatSat, formatVMB } from './util'
import { formatSatWithUsd, formatVMB } from './util'
import layout from './layout'
import search from './search'

Expand All @@ -19,7 +19,7 @@ export default ({ t, mempool, feeEst, ...S }) => mempool && feeEst && layout(
</div>
<div>
<div>{t`Total fees`}</div>
<div>{formatSat(mempool.total_fee)}</div>
<div>{formatSatWithUsd(mempool.total_fee, { t, ...S })}</div>
</div>
<div>
<div>{t`Total size`}</div>
Expand Down
11 changes: 4 additions & 7 deletions client/src/views/overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import { MempoolCongestion } from "../components/mempool-congestion";
import { ReferenceLineChart } from "../components/reference-line-chart";
import { estimateTypicalTransactionFeeUsd } from "../lib/fees";
import { isBitcoinNetwork } from "../lib/network";
import {
getBitcoinPrices,
getLatestBitcoinPrice,
} from "../lib/market";
import { getBitcoinPrices } from "../lib/market";
import { formatFeeRate, formatUsd } from "./util";

const staticRoot = process.env.STATIC_ROOT || "";
Expand Down Expand Up @@ -53,15 +50,15 @@ export const overview = ({
feeEst,
mempool,
bitcoinMarketChart,
bitcoinPrice,
t,
} = {}) => {
const latestBlock = blocks && blocks[0];
const chartPrices = getChartPrices(bitcoinMarketChart);
const currentBitcoinPrice = getLatestBitcoinPrice(bitcoinMarketChart);
const recommendedFeerate =
feeEst && feeEst[feeEstimateTargets.average];
const recommendedFeeUsd = estimateTypicalTransactionFeeUsd(
currentBitcoinPrice,
bitcoinPrice,
recommendedFeerate,
);
return (
Expand Down Expand Up @@ -112,7 +109,7 @@ export const overview = ({
iconSrc={`${staticRoot}img/icons/Bitcoin-menu-logo.svg`}
headerValue={
<span className="overview-bitcoin-price-chart-header-value">
<span>{formatUsd(currentBitcoinPrice, t`N/A`)}</span>
<span>{formatUsd(bitcoinPrice, t`N/A`)}</span>
{getPercentDelta(chartPrices, t`N/A`)}
</span>
}
Expand Down
7 changes: 3 additions & 4 deletions client/src/views/pending-block-details-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ import {
formatTransactionDelta,
formatWeight,
} from "../lib/pending-block-details";
import { getLatestBitcoinPrice } from "../lib/market";
import { formatFeeRate, formatSat, formatUsd, formatVMB } from "./util";
import { formatFeeRate, formatSat, formatSatWithUsd, 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 Down Expand Up @@ -353,6 +352,7 @@ const feeBucketPanel = (

const PendingBlockDetailsCard = ({
bitcoinMarketChart,
bitcoinPrice,
block,
blockTemplate,
detailsOpen,
Expand All @@ -377,7 +377,6 @@ const PendingBlockDetailsCard = ({
? blockTemplate.transactions
: null;
const feeTierBoundaries = getFeeTierBoundaries(feeEst);
const bitcoinPrice = getLatestBitcoinPrice(bitcoinMarketChart);
const weightPercentage = metrics && metrics.weightPercentage;
const weightLimit = metrics && metrics.weightLimit;
return (
Expand Down Expand Up @@ -438,7 +437,7 @@ const PendingBlockDetailsCard = ({
{blockStat(
t`TOTAL FEE COLLECTED`,
metrics && Number.isFinite(metrics.totalFees)
? formatSat(metrics.totalFees)
? formatSatWithUsd(metrics.totalFees, { bitcoinPrice, t })
: templateFallback,
)}
</div>
Expand Down
Loading