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
85 changes: 75 additions & 10 deletions client/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ import * as views from './views'

const apiBase = (process.env.API_URL || '/api').replace(/\/+$/, '')
, bitcoinMarketChartUrl = process.env.BITCOIN_MARKET_CHART_URL || 'https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=1&interval=hourly'
, blockTemplatePollIntervalMs = 30000
// Wait one electrs cache window after a new tip before requesting the next
// template. If a refresh is still in progress, electrs holds the request
// until fresh data is ready, so no additional client-side jitter is needed.
, blockTemplatePollAfterNewBlockMs = 15000
, setBase = ({ path, ...r }) => ({ ...r, url: path.includes('://') || path.startsWith('./') ? path : apiBase + path })

const reservedPaths = [ 'mempool', 'assets', 'search' ]
Expand Down Expand Up @@ -73,6 +78,32 @@ const trackNewEntries = (items$, getId, getNewIds=defaultNewIds) => {
.scan((current, mod) => mod(current), {})
}

export const scheduleBlockTemplatePolls = (start$, newBlock$, scheduler) =>
O.merge(
start$.mapTo(0),
newBlock$.mapTo(blockTemplatePollAfterNewBlockMs)
)
.switchMap(delay => O.timer(
delay,
blockTemplatePollIntervalMs,
scheduler
))

export const scheduleDashboardBlockTemplatePolls = (
view$,
newBlock$,
scheduler
) => scheduleBlockTemplatePolls(
view$
.distinctUntilChanged()
.filter(view => view == 'dashBoard'),
newBlock$,
scheduler
)
.withLatestFrom(view$, (pollIndex, view) => ({ pollIndex, view }))
.filter(({ view }) => view == 'dashBoard')
.map(({ pollIndex }) => pollIndex)

const trackPendingBlockTemplateUpdate = (previous, template) => {
if (!template || !Array.isArray(template.transactions)) {
return { template: null, key: null, transactionCount: null, delta: null }
Expand Down Expand Up @@ -283,6 +314,11 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search
, tx$ = reply('tx').merge(goTx$.mapTo(null)).startWith(null)
, txBlock$ = reply('tx-block').merge(goTx$.mapTo(null)).startWith(null)

// Predecessor metadata for confirmed block interval calculations
, previousBlock$ = reply('previous-block')
.merge(O.merge(goBlock$, goTx$).mapTo(null))
.startWith(null)

// Currently collapsed tx/block ("details")
, openTx$ = togTx$.startWith(null).scan((prev, txid) => prev == txid ? null : txid)
, openBlock$ = togBlock$.startWith(null).scan((prev, blockhash) => prev == blockhash ? null : blockhash)
Expand Down Expand Up @@ -422,6 +458,18 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search
.distinctUntilChanged((a, b) => a.height == b.height)
: O.empty()

, dashboardNewBlock$ = latestBlock$.skip(1)
.withLatestFrom(view$)
.filter(([ _, view ]) => view == 'dashBoard')

// In the browser, wait for the ready dashboard view. Liquid remains in the
// loading view until its asset map arrives, so starting from goHome$ would
// discard the initial template request.
, blockTemplatePoll$ = !process.browser
? goHome$
: scheduleDashboardBlockTemplatePolls(view$, dashboardNewBlock$)
.filter(pollIndex => pollIndex == 0 || document.hasFocus())

, dashboardEpochStartHeight$ = dashboardLatestBlock$
.map(block => block.height - (block.height % difficultyPeriod))
.distinctUntilChanged()
Expand Down Expand Up @@ -451,7 +499,7 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search
, newBlockEntries$, newTxEntries$
, goBlock$, block$, blockStatus$, blockTxs$, nextBlockTxs$, prevBlockTxs$, openBlock$
, mempool$, mempoolRecent$, feeEst$, bitcoinMarketChart$
, tx$, txBlock$, txAnalysis$, openTx$
, tx$, txBlock$, previousBlock$, txAnalysis$, openTx$
, goAddr$, addr$, addrTxs$, addrQR$
, assetMap$, assetList$, goAssetList$, goAsset$, asset$, assetTxs$, unblinded$
, isReady$, loading$, page$, view$, title$
Expand Down Expand Up @@ -483,6 +531,11 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search
, tx$.filter(tx => tx && tx.status && tx.status.confirmed && tx.status.block_hash)
.map(tx => ({ category: 'tx-block', method: 'GET', path: `/block/${tx.status.block_hash}` }))

// fetch the predecessor needed to calculate a confirmed block's interval
, O.merge(block$, txBlock$)
.filter(block => block && block.previousblockhash)
.map(block => ({ category: 'previous-block', method: 'GET', path: `/block/${block.previousblockhash}`, bg: true }))

// fetch address and its txs
, goAddr$.flatMap(d => [{ category: 'address', method: 'GET', path: `/address/${d.addr}` }
, d.last_txids.length
Expand Down Expand Up @@ -563,14 +616,9 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search
, { category: 'dashboard-peg-chain-txs', method: 'GET', path: `/asset/${nativeAssetId}/txs/chain`, bg: true }
, { category: 'dashboard-peg-mempool-txs', method: 'GET', path: `/asset/${nativeAssetId}/txs/mempool`, bg: true }]))

// refresh the pending block template while the dashboard remains open
, O.merge(
O.merge(goHome$, tickWhileViewing(30000, 'dashBoard', view$))
.throttleTime(1000)
, latestBlock$.skip(1)
.withLatestFrom(view$)
.filter(([ _, view ]) => view == 'dashBoard')
)
// Refresh the pending block template while the dashboard remains open. A new
// tip resets the cadence and waits for electrs' block cache to refresh.
, blockTemplatePoll$
.mapTo({ category: 'block-template', method: 'GET', path: '/block-template', bg: true })

, goHome$.flatMap(_ => [{ category: 'blocks', method: 'GET', path: '/blocks' }
Expand Down Expand Up @@ -670,7 +718,22 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search
})

on('.table-copy-button', 'click', { preventDefault: true }).subscribe(e => e.stopPropagation())
on('.tooltip', 'click', { preventDefault: true }).subscribe(e => e.stopPropagation())

const closeOpenTooltips = except => {
document.querySelectorAll('.tooltip.tooltip-open').forEach(tooltip => {
if (tooltip != except) tooltip.classList.remove('tooltip-open')
})
}
on('.tooltip', 'click', { preventDefault: true }).subscribe(e => {
e.stopPropagation()
const tooltip = e.ownerTarget
, shouldOpen = !tooltip.classList.contains('tooltip-open')
closeOpenTooltips(tooltip)
tooltip.classList.toggle('tooltip-open', shouldOpen)
if (!shouldOpen) tooltip.blur()
})
on('.tooltip', 'blur').subscribe(({ ownerTarget: tooltip }) =>
tooltip.classList.remove('tooltip-open'))
on('[data-scroll-top]', 'click').subscribe(_ => window.scrollTo(0, 0))

const keepTooltipInViewport = ({ ownerTarget: tooltip }) => {
Expand Down Expand Up @@ -709,13 +772,15 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search
if (activeTooltip && activeTooltip.classList.contains('tooltip') && !e.target.closest('.tooltip')) {
activeTooltip.blur()
}
if (!e.target.closest('.tooltip')) closeOpenTooltips()
if (e.target.closest('.main-nav-container')) return
closeNetworkMenus()
})

document.addEventListener('keydown', e => {
if (e.key == 'Escape') {
closeNetworkMenus()
closeOpenTooltips()
document.activeElement && document.activeElement.classList.contains('tooltip') && document.activeElement.blur()
}

Expand Down
44 changes: 32 additions & 12 deletions client/src/components/block-details-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { InfoStat } from "./info-stat";
import { MinusIcon, PlusIcon } from "./icons";
import { MetricBar } from "./metric-bar";
import { StatusBadge } from "./status-badge";
import { ElapsedTime } from "./elapsed-time";
import { ElapsedTime, formatDuration } from "./elapsed-time";
import { Tooltip } from "./tooltip";
import { maxBlockWeight } from "../const";
import {
Expand Down Expand Up @@ -42,16 +42,34 @@ const formatVirtualSize = (weight) => {
return formatScaledValue(virtualSize, 1_000_000, "vMB");
};

const ExpandedBlockDetails = ({ block, t }) => {
const formatBlockInterval = (block, previousBlock) => {
if (
!block ||
!previousBlock ||
previousBlock.id !== block.previousblockhash ||
!Number.isFinite(block.timestamp) ||
!Number.isFinite(previousBlock.timestamp)
) {
return "N/A";
}

return formatDuration(
(block.timestamp - previousBlock.timestamp) * 1000,
true,
);
};

const ExpandedBlockDetails = ({ block, previousBlock, t }) => {
const weightPercentage = getBlockPercentageUsed(block.weight);
const blockInterval = formatBlockInterval(block, previousBlock);

return (
<div id="expanded-block-details" className="expanded-block-details">
<InfoCard
className="block-detail-time-panel"
title={t`Time Since Last Block`}
tooltip={t`Time elapsed since this block was mined.`}
value={<ElapsedTime timestamp={block.timestamp} compact />}
tooltip={t`Time elapsed between this block and the previous block.`}
value={blockInterval}
footer={t`Block #${block.height.toLocaleString()}`}
/>
<InfoCard
Expand Down Expand Up @@ -128,13 +146,15 @@ const BlockDetailsCard = ({
className,
block,
detailsOpen = false,
previousBlock,
statusText,
statusVariant = "success",
t,
}) => {
const percentage = block
? Math.min(Math.max(getBlockPercentageUsed(block.weight), 0), 100)
: 0;
const blockInterval = formatBlockInterval(block, previousBlock);

return (
<div
Expand Down Expand Up @@ -196,13 +216,7 @@ const BlockDetailsCard = ({
<div className="block-details-card-stats">
<InfoStat
title={t`Time Since Last Block`}
value={
block ? (
<ElapsedTime timestamp={block.timestamp} compact />
) : (
"N/A"
)
}
value={blockInterval}
/>
<InfoStat
title={t`Transactions`}
Expand Down Expand Up @@ -243,7 +257,13 @@ const BlockDetailsCard = ({
</div>
</div>

{detailsOpen && block ? <ExpandedBlockDetails block={block} t={t} /> : null}
{detailsOpen && block ? (
<ExpandedBlockDetails
block={block}
previousBlock={previousBlock}
t={t}
/>
) : null}
</div>
);
};
Expand Down
18 changes: 13 additions & 5 deletions client/src/components/elapsed-time.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ const MINUTES_PER_DAY = 24 * 60;
const MINUTES_PER_YEAR = 365 * MINUTES_PER_DAY;
const MINUTES_PER_MONTH = MINUTES_PER_YEAR / 12;

const formatElapsedTime = (timestamp, compact) => {
const fromDate =
timestamp < 1e12 ? new Date(timestamp * 1000) : new Date(timestamp);
export const formatDuration = (durationMilliseconds, compact = false) => {
const diffMinutes = Math.max(
0,
Math.floor((new Date() - fromDate) / UPDATE_INTERVAL_MS),
Math.floor(durationMilliseconds / UPDATE_INTERVAL_MS),
);
const years = Math.floor(diffMinutes / MINUTES_PER_YEAR);
const minutesAfterYears = diffMinutes % MINUTES_PER_YEAR;
Expand Down Expand Up @@ -39,7 +37,17 @@ const formatElapsedTime = (timestamp, compact) => {

if (compact) return parts.length ? parts.join(" ") : "< 1m";

return parts.length ? `${parts.join(" ")} AGO` : "< 1 MINUTE AGO";
return parts.length ? parts.join(" ") : "< 1 MINUTE";
};

const formatElapsedTime = (timestamp, compact) => {
const fromDate =
timestamp < 1e12 ? new Date(timestamp * 1000) : new Date(timestamp);
const duration = formatDuration(new Date() - fromDate, compact);

if (compact) return duration;

return `${duration} AGO`;
};

const updateElapsedTime = (element) => {
Expand Down
38 changes: 23 additions & 15 deletions client/src/views/asset-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,31 @@ export default ({ assetList, goAssetList, loading, t, ...S }) => {
<div className={`asset-list-body ${loading ? "asset-list-body-loading" : ""}`}>
{assets.map((asset) => (
<a href={`asset/${asset.asset_id}`}>
<div className="assets-table-link-row">
<div className="assets-table-row">
<div className="asset-list-name" data-label={t`Name`}>
<CurrencyDollarIcon className="currency-dollar" />
{asset.name}
</div>
<div className="asset-list-ticker" data-label={t`Ticker`}>
{asset.ticker || <em>None</em>}
</div>
<div className="asset-list-total-supply" data-label={t`Total Supply`}>
{getSupply(asset, t)}
</div>
<div className="asset-list-issuer-domain" data-label={t`Issuer domain`}>
{asset.entity.domain}
<div className="assets-table-link-row">
<div className="assets-table-row">
<div className="asset-list-name" data-label={t`Name`}>
<span className="asset-list-field-value">
<CurrencyDollarIcon className="currency-dollar" />
{asset.name}
</span>
</div>
<div className="asset-list-ticker" data-label={t`Ticker`}>
<span className="asset-list-field-value">
{asset.ticker || <em>None</em>}
</span>
</div>
<div className="asset-list-total-supply" data-label={t`Total Supply`}>
<span className="asset-list-field-value">
{getSupply(asset, t)}
</span>
</div>
<div className="asset-list-issuer-domain" data-label={t`Issuer domain`}>
<span className="asset-list-field-value">
{asset.entity.domain}
</span>
</div>
</div>
</div>
</div>
</a>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion client/src/views/asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export default ({ t, asset, assetTxs, goAsset, openTx, spends, tipHeight, loadin
{detailField(
'ISSUER PUBKEY',
contract.issuer_pubkey,
'mono',
null,
t,
shortenValue(contract.issuer_pubkey)
)}
Expand Down
2 changes: 2 additions & 0 deletions client/src/views/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default ({
tipHeight,
loading,
page,
previousBlock,
txsStatus = makeStatus(b),
...S
}) =>
Expand Down Expand Up @@ -60,6 +61,7 @@ export default ({
</div>
<BlockDetailsCard
block={b}
previousBlock={previousBlock}
t={t}
detailsOpen={S.openBlock === b.id}
statusText={
Expand Down
2 changes: 1 addition & 1 deletion client/src/views/footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const resourceLinks = [
['Bitcoin Education', 'https://help.blockstream.com/education/'],
['Glossary', 'https://help.blockstream.com/education/glossary/'],
['Local', 'https://blockstream.com/local/'],
['Brand Assets', 'https://design.blockstream.com/styleguide/branding/overview/']
['Brand Assets', 'https://design.blockstream.com/styleguide/branding/overview.html']
]

const socialLinks = [
Expand Down
4 changes: 2 additions & 2 deletions client/src/views/nav-toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ export default () =>
<ul className="font-p3">
<li><a href={`${siteRoot}/`} rel="external">Bitcoin</a></li>
<li><a href={`${siteRoot}/liquid/`} rel="external">Liquid Network</a></li>
<li><a href={`${siteRoot}/testnet/`} rel="external" target="_blank">Bitcoin Testnet</a></li>
<li><a href={`${siteRoot}/liquidtestnet/`} rel="external" target="_blank">Liquid Testnet</a></li>
<li><a href={`${siteRoot}/testnet/`} rel="external">Bitcoin Testnet</a></li>
<li><a href={`${siteRoot}/liquidtestnet/`} rel="external">Liquid Testnet</a></li>
</ul>
<h4 className="menu-title font-h5">Developer Tools</h4>
<ul className="font-p3">
Expand Down
7 changes: 6 additions & 1 deletion client/src/views/overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 { isBitcoinNetwork } from "../lib/network";
import {
getBitcoinPrices,
getLatestBitcoinPrice,
Expand Down Expand Up @@ -37,7 +38,11 @@ export const overview = ({
<div className="overview-body">
<InfoCard
title={t`Time since last block`}
tooltip={t`Elapsed time since the last block confirmed. Bitcoin targets one every ~10 minutes.`}
tooltip={
isBitcoinNetwork
? t`Elapsed time since the last block confirmed. Bitcoin targets one every ~10 minutes.`
: t`Elapsed time since the last block confirmed. Liquid targets one every ~1 minute.`
}
value={
latestBlock ? (
<ElapsedTime timestamp={latestBlock.timestamp} compact />
Expand Down
Loading