Show exact amounts and address titles in transaction detail
All checks were successful
check / check (push) Successful in 5s

- Display full-precision amount (no 4-decimal truncation) in the
  transaction detail view, with native quantity (wei/base units) below
- Both amount and native quantity are click-copyable
- Show wallet/address title above from/to when the address is ours
- Update README Display Consistency to document the exception
This commit is contained in:
Jeffrey Paul 2026-02-27 16:09:44 +07:00
parent b9250dab2e
commit d67023e80d
4 changed files with 71 additions and 17 deletions

View File

@ -118,12 +118,20 @@ click it.
#### Display Consistency
The same data must be formatted identically everywhere it appears. Token and ETH
amounts are always displayed with exactly 4 decimal places (e.g. "1.0500 ETH",
"17.1900 USDT") — in balance lists, transaction lists, transaction details, send
confirmations, and any other context. Timestamps include both an ISO datetime
and a humanized relative age wherever shown. If a formatting rule applies in one
place, it applies in every place. Users should never see the same value rendered
differently on two screens.
amounts are displayed with exactly 4 decimal places (e.g. "1.0500 ETH", "17.1900
USDT") in balance lists, transaction lists, send confirmations, and approval
screens. Timestamps include both an ISO datetime and a humanized relative age
wherever shown. If a formatting rule applies in one place, it applies in every
place. Users should never see the same value rendered differently on two
screens.
**Exception — Transaction Detail view:** The transaction detail screen is the
authoritative record of a specific transaction and shows the exact, untruncated
amount with all meaningful decimal places (e.g. "0.00498824598498216 ETH"). It
also shows the native quantity (e.g. "4988245984982160 wei") below it. Both are
click-copyable. Truncating to 4 decimals in summary views is acceptable for
scannability, but the detail view must never discard precision — it is the one
place the user goes to verify exact figures.
#### Language & Labeling

View File

@ -843,7 +843,11 @@
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">Amount</div>
<div id="tx-detail-value" class="text-xs font-bold"></div>
<div id="tx-detail-value" class="text-xs"></div>
</div>
<div class="mb-4 hidden">
<div class="text-xs text-muted mb-1">Native quantity</div>
<div id="tx-detail-native" class="text-xs"></div>
</div>
<div class="mb-4">
<div class="text-xs text-muted mb-1">From</div>

View File

@ -6,6 +6,7 @@ const {
showView,
showFlash,
addressDotHtml,
addressTitle,
escapeHtml,
} = require("./helpers");
const { state } = require("../../shared/state");
@ -67,15 +68,18 @@ function blockieHtml(address) {
return `<img src="${src}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
}
function txAddressHtml(address, ensName) {
function txAddressHtml(address, ensName, title) {
const blockie = blockieHtml(address);
const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${address}`;
const extLink = `<a href="${link}" target="_blank" rel="noopener" class="inline-flex items-center">${EXT_ICON}</a>`;
let html = `<div class="mb-1">${blockie}</div>`;
if (title) {
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
}
if (ensName) {
html +=
`<div class="flex items-center">${dot}` +
`<div class="flex items-center">${title ? "" : dot}` +
copyableHtml(ensName, "") +
extLink +
`</div>` +
@ -84,7 +88,7 @@ function txAddressHtml(address, ensName) {
`</div>`;
} else {
html +=
`<div class="flex items-center">${dot}` +
`<div class="flex items-center">${title ? "" : dot}` +
copyableHtml(address, "break-all") +
extLink +
`</div>`;
@ -105,6 +109,9 @@ function show(tx) {
from: tx.from,
to: tx.to,
value: tx.value,
exactValue: tx.exactValue || tx.value,
rawAmount: tx.rawAmount || "",
rawUnit: tx.rawUnit || "",
symbol: tx.symbol,
timestamp: tx.timestamp,
isError: tx.isError,
@ -120,11 +127,33 @@ function render() {
const tx = state.viewData.tx;
if (!tx) return;
$("tx-detail-hash").innerHTML = txHashHtml(tx.hash);
$("tx-detail-from").innerHTML = txAddressHtml(tx.from, tx.fromEns);
$("tx-detail-to").innerHTML = txAddressHtml(tx.to, tx.toEns);
$("tx-detail-value").textContent = tx.value
? tx.value + " " + tx.symbol
const fromTitle = addressTitle(tx.from, state.wallets);
const toTitle = addressTitle(tx.to, state.wallets);
$("tx-detail-from").innerHTML = txAddressHtml(
tx.from,
tx.fromEns,
fromTitle,
);
$("tx-detail-to").innerHTML = txAddressHtml(tx.to, tx.toEns, toTitle);
// Exact amount (full precision, copyable)
const exactStr = tx.exactValue
? tx.exactValue + " " + tx.symbol
: tx.directionLabel + " " + tx.symbol;
$("tx-detail-value").innerHTML = copyableHtml(exactStr, "font-bold");
// Native quantity (raw integer, copyable)
const nativeEl = $("tx-detail-native");
if (tx.rawAmount && tx.rawUnit) {
const nativeStr = tx.rawAmount + " " + tx.rawUnit;
nativeEl.innerHTML = copyableHtml(nativeStr, "");
nativeEl.parentElement.classList.remove("hidden");
} else {
nativeEl.innerHTML = "";
nativeEl.parentElement.classList.add("hidden");
}
$("tx-detail-time").textContent =
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success";

View File

@ -27,6 +27,9 @@ function parseTx(tx, addrLower) {
// For contract calls, produce a meaningful label instead of "0.0000 ETH"
let symbol = "ETH";
let value = formatTxValue(formatEther(rawWei));
let exactValue = formatEther(rawWei);
let rawAmount = rawWei;
let rawUnit = "wei";
let direction = from.toLowerCase() === addrLower ? "sent" : "received";
let directionLabel = direction === "sent" ? "Sent" : "Received";
if (toIsContract && method && method !== "transfer") {
@ -38,6 +41,9 @@ function parseTx(tx, addrLower) {
direction = "contract";
directionLabel = label;
value = "";
exactValue = "";
rawAmount = "";
rawUnit = "";
}
return {
@ -47,6 +53,9 @@ function parseTx(tx, addrLower) {
from: from,
to: to,
value: value,
exactValue: exactValue,
rawAmount: rawAmount,
rawUnit: rawUnit,
valueGwei: Math.floor(Number(BigInt(rawWei) / BigInt(1000000000))),
symbol: symbol,
direction: direction,
@ -63,17 +72,21 @@ function parseTokenTransfer(tt, addrLower) {
const from = tt.from?.hash || "";
const to = tt.to?.hash || "";
const decimals = parseInt(tt.total?.decimals || "18", 10);
const rawValue = tt.total?.value || "0";
const rawVal = tt.total?.value || "0";
const direction = from.toLowerCase() === addrLower ? "sent" : "received";
const sym = tt.token?.symbol || "?";
return {
hash: tt.transaction_hash,
blockNumber: tt.block_number,
timestamp: Math.floor(new Date(tt.timestamp).getTime() / 1000),
from: from,
to: to,
value: formatTxValue(formatUnits(rawValue, decimals)),
value: formatTxValue(formatUnits(rawVal, decimals)),
exactValue: formatUnits(rawVal, decimals),
rawAmount: rawVal,
rawUnit: sym + " base units (10^-" + decimals + ")",
valueGwei: null,
symbol: tt.token?.symbol || "?",
symbol: sym,
direction: direction,
directionLabel: direction === "sent" ? "Sent" : "Received",
isError: false,