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
9 changes: 7 additions & 2 deletions client/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search

// Single TX
, tx$ = reply('tx').merge(goTx$.mapTo(null)).startWith(null)
, txBlock$ = reply('tx-block').merge(goTx$.mapTo(null)).startWith(null)

// Currently collapsed tx/block ("details")
, openTx$ = togTx$.startWith(null).scan((prev, txid) => prev == txid ? null : txid)
Expand Down Expand Up @@ -348,7 +349,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$, txAnalysis$, openTx$
, tx$, txBlock$, txAnalysis$, openTx$
, goAddr$, addr$, addrTxs$, addrQR$
, assetMap$, assetList$, goAssetList$, goAsset$, asset$, assetTxs$, unblinded$
, isReady$, loading$, page$, view$, title$
Expand Down Expand Up @@ -376,6 +377,10 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search
// fetch single tx (including confirmation status)
, goTx$.map(txid => ({ category: 'tx', method: 'GET', path: `/tx/${txid}` }))

// fetch the block containing a confirmed tx
, 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}` }))
Comment thread
FedOken marked this conversation as resolved.

// fetch address and its txs
, goAddr$.flatMap(d => [{ category: 'address', method: 'GET', path: `/address/${d.addr}` }
, d.last_txids.length
Expand Down Expand Up @@ -499,7 +504,7 @@ export default function main({ DOM, HTTP, route, storage, scanner: scan$, search

dbg({ goBlocks$, goBlock$, goTx$, goAddr$, togTx$, page$, lang$, vdom$, moreBlocks$
, openTx$, openBlock$, updateQuery$
, state$, view$, block$, blockTxs$, blocks$, tx$, txAnalysis$, spends$, addr$
, state$, view$, block$, blockTxs$, blocks$, tx$, txBlock$, txAnalysis$, spends$, addr$
, tipHeight$, error$, loading$
, goSearch$, searchResult$, copy$, store$, navto$, scanning$, scan$
, assetMap$, goAssetList$, assetList$
Expand Down
88 changes: 88 additions & 0 deletions client/src/components/block-grid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { maxBlockWeight } from "../const";

const GRID_LENGTH = 15;

const drawBlockGrid = (canvas, blockWeight) => {
if (
typeof HTMLCanvasElement === "undefined" ||
!(canvas instanceof HTMLCanvasElement)
) {
return;
}

const ctx = canvas.getContext("2d");
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const width = rect.width;
const height = rect.height;
const gap = 2;
const cellWidth = (width - gap * (GRID_LENGTH - 1)) / GRID_LENGTH;
const cellHeight = (height - gap * (GRID_LENGTH - 1)) / GRID_LENGTH;
const fillRatio = Number.isFinite(blockWeight)
? Math.min(Math.max(blockWeight / maxBlockWeight, 0), 1)
: 0;
const filledCells = Math.round(fillRatio * GRID_LENGTH * GRID_LENGTH);
const styles = window.getComputedStyle(canvas);
const emptyColor =
styles.getPropertyValue("--block-grid-empty-color").trim() || "#141414";
const filledColor = styles
.getPropertyValue("--block-grid-filled-color")
.trim() || "#FA8A00";

canvas.width = Math.max(1, Math.round(width * dpr));
canvas.height = Math.max(1, Math.round(height * dpr));
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, width, height);

for (let index = 0; index < GRID_LENGTH * GRID_LENGTH; index += 1) {
const row = Math.floor(index / GRID_LENGTH);
const column = index % GRID_LENGTH;

ctx.fillStyle = index < filledCells ? filledColor : emptyColor;
ctx.fillRect(
column * (cellWidth + gap),
row * (cellHeight + gap),
cellWidth,
cellHeight,
);
}
};

const drawGrid = (vnode, blockWeight) => {
const draw = () => drawBlockGrid(vnode.elm, blockWeight);

if (typeof window !== "undefined" && window.requestAnimationFrame) {
window.requestAnimationFrame(draw);
return;
}

draw();
};

export const BlockGrid = ({ blockWeight } = {}) => {
const hasBlockWeight = Number.isFinite(blockWeight);
const percentage = hasBlockWeight
? Math.min(
Math.max(
Math.round((blockWeight / maxBlockWeight) * 10_000) / 100,
0,
),
100,
)
: 0;

return (
<div className="block-details-card-grid">
<canvas
aria-label={
hasBlockWeight
? `Block is ${percentage}% full`
: "Block utilization unavailable"
}
role="img"
hook-insert={(vnode) => drawGrid(vnode, blockWeight)}
hook-postpatch={(_, vnode) => drawGrid(vnode, blockWeight)}
></canvas>
</div>
);
};
17 changes: 14 additions & 3 deletions client/src/components/elapsed-time.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const UPDATE_INTERVAL_MS = 60 * 1000;
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) => {
const fromDate =
Expand All @@ -7,10 +10,18 @@ const formatElapsedTime = (timestamp) => {
0,
Math.floor((new Date() - fromDate) / UPDATE_INTERVAL_MS),
);
const years = Math.floor(diffMinutes / MINUTES_PER_YEAR);
const minutesAfterYears = diffMinutes % MINUTES_PER_YEAR;
const months = Math.floor(minutesAfterYears / MINUTES_PER_MONTH);
const minutesAfterMonths = Math.floor(
minutesAfterYears - months * MINUTES_PER_MONTH,
);
const units = [
["d", Math.floor(diffMinutes / (24 * 60))],
["h", Math.floor((diffMinutes % (24 * 60)) / 60)],
["m", diffMinutes % 60],
["y", years],
["mo", months],
["d", Math.floor(minutesAfterMonths / MINUTES_PER_DAY)],
["h", Math.floor((minutesAfterMonths % MINUTES_PER_DAY) / 60)],
["m", minutesAfterMonths % 60],
];
const parts = units
.filter(([_, value]) => value > 0)
Expand Down
16 changes: 16 additions & 0 deletions client/src/components/icons.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions client/src/components/info-stat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const InfoStat = ({ title, value } = {}) => (
<div className="info-stat">
<div className="info-stat-title">{title}</div>
<div className="info-stat-value">{value}</div>
</div>
);
1 change: 1 addition & 0 deletions client/src/const.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const difficultyPeriod = 2016
export const maxMempoolTxs = 50
export const satoshisPerBitcoin = 100000000
export const averageNativeSegwitTransactionSize = 140
export const maxBlockWeight = 4000000

export const nativeAssetId = process.env.NATIVE_ASSET_ID || '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'
export const nativeAssetLabel = process.env.NATIVE_ASSET_LABEL || 'BTC'
Expand Down
92 changes: 92 additions & 0 deletions client/src/views/block-details-card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { BlockGrid } from "../components/block-grid";
import { InfoStat } from "../components/info-stat";
import { ElapsedTime } from "../components/elapsed-time";
import {
formatTime,
formatVMB,
getBlockPercentageUsed,
} from "./util";

const formatInteger = (value) =>
Number.isFinite(value) ? value.toLocaleString() : "N/A";

const BlockDetailsCard = ({ className, block, confirmed }) => {
const percentage = block
? Math.min(Math.max(getBlockPercentageUsed(block.weight), 0), 100)
: 0;

return (
<span className={className}>
<div className="block-details-card-summary">
<BlockGrid blockWeight={block && block.weight} />

<div className="block-details-card-content">
<div className="block-details-card-header">
<p className="block-number">
{block ? (
<a href={`block/${block.id}`}>
#{block.height.toLocaleString()}
</a>
) : (
"-"
)}
</p>

<p
className="block-details-card-timestamp"
title={block ? formatTime(block.timestamp) : ""}
>
{block ? (
<span>
<ElapsedTime timestamp={block.timestamp} /> ago
</span>
) : (
"Loading block..."
)}
</p>

{confirmed ? (
<div className="confirmation-status-badge success">
<p>Confirmed</p>
</div>
) : null}
</div>

<div className="block-details-card-stats">
<InfoStat
title="TRANSACTIONS"
value={block ? formatInteger(block.tx_count) : "N/A"}
/>
<InfoStat
title="SIZE"
value={block ? formatVMB(block.size, "MB") : "N/A"}
/>
</div>

<div className="block-details-card-progress">
<div className="block-details-card-progress-header">
<p>BLOCK FILLING</p>
<p className="usage-number">
{block ? `${percentage}%` : "N/A"}
</p>
</div>

<div className="block-details-card-usage-bar">
<div
className="block-details-card-usage-bar-fill"
style={{
width: `${percentage}%`,
backgroundSize: percentage
? `${10_000 / percentage}% 100%`
: "100% 100%",
}}
></div>
</div>
</div>
</div>
</div>
</span>
);
};

export default BlockDetailsCard;
24 changes: 14 additions & 10 deletions client/src/views/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import layout from './layout'
import { txBox } from './tx'
import { updateQuery } from '../util'
import { formatTime, formatHex, formatNumber } from './util'
import { blockTxsPerPage as perPage } from '../const'
import loader from '../components/loading'
import { MinusIcon, PlusIcon } from '../components/icons'


const staticRoot = process.env.STATIC_ROOT || ''
Expand Down Expand Up @@ -122,8 +122,17 @@ export default ({ t, block: b, blockStatus: status, blockTxs, openTx, spends, op
</div>

<div className="transactions">
<h3 className="font-h3">{txsShownText(b.tx_count, goBlock.start_index, blockTxs && blockTxs.length, t)}</h3>
{ blockTxs ? blockTxs.map(tx => txBox( { ...tx, status: txsStatus }, { openTx, tipHeight, t, spends }))
{ blockTxs ? blockTxs.map((tx, index) => txBox(
{ ...tx, status: txsStatus },
{
openTx,
tipHeight,
t,
spends,
listingIndex: +goBlock.start_index + index + 1,
listingTotal: b.tx_count
}
))
: loader() }
</div>

Expand All @@ -137,11 +146,6 @@ export default ({ t, block: b, blockStatus: status, blockTxs, openTx, spends, op
]
, { t, page, activeTab: 'recentBlocks', ...S })

const txsShownText = (total, start, shown, t) =>
(total > perPage && shown > 0)
? t`${ start > 0 ? `${start}-${+start+shown}` : shown} of ${total} Transactions`
: t`${total} Transactions`

const pagingNav = (block, { nextBlockTxs, prevBlockTxs, t }) =>
process.browser

Expand Down Expand Up @@ -171,6 +175,6 @@ const btnDetails = (blockhash, isOpen, query, t) => process.browser

const btnDetailsContent = (isOpen, t) =>
<div role="button" tabindex="0">
<div>{t`Details`}</div>
<div className={isOpen?'minus':'plus'}></div>
<p>{t`Details`}</p>
<div>{isOpen ? <MinusIcon /> : <PlusIcon />}</div>
</div>
18 changes: 6 additions & 12 deletions client/src/views/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "./util";
import loader from "../components/loading";
import { BlockIcon, ClockIcon, CopyIcon } from "../components/icons";
import { InfoStat } from "../components/info-stat";

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

Expand Down Expand Up @@ -67,18 +68,11 @@ export const blks = (blocks, viewMore, { t, ...S }) => (
</p>
</div>
<div className="block-card-body">
<div className="block-stat">
<div className="block-stat-title">TRANSACTIONS</div>
<div className="block-stat-value">
{formatNumber(b.tx_count).toLocaleString()}
</div>
</div>
<div className="block-stat">
<div className="block-stat-title">SIZE</div>
<div className="block-stat-value">
{formatVMB(b.size, "MB")}
</div>
</div>
<InfoStat
title="TRANSACTIONS"
value={formatNumber(b.tx_count).toLocaleString()}
/>
<InfoStat title="SIZE" value={formatVMB(b.size, "MB")} />
</div>

<div className="block-usage">
Expand Down
4 changes: 2 additions & 2 deletions client/src/views/transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const transactions = (txs, viewMore, { t, ...S }) => (
) : !txs.length ? (
<p>{t`No recent transactions`}</p>
) : (
<div className="transaction-table">
<div className="latest-transactions-table">
<div className="table-header">
<div className="table-header-icon-container">
<TxArrowsIcon />
Expand All @@ -38,7 +38,7 @@ export const transactions = (txs, viewMore, { t, ...S }) => (
</div>
</div>

<div className="transaction-table-body">
<div className="latest-transactions-table-body">
{txs.map((txOverview) => {
const feerate = txOverview.fee / txOverview.vsize;
const feeClass = feeRateClass(feerate, S.feeEst);
Expand Down
Loading