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:
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 #### Display Consistency
The same data must be formatted identically everywhere it appears. Token and ETH 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", amounts are displayed with exactly 4 decimal places (e.g. "1.0500 ETH", "17.1900
"17.1900 USDT") in balance lists, transaction lists, transaction details, send USDT") in balance lists, transaction lists, send confirmations, and approval
confirmations, and any other context. Timestamps include both an ISO datetime screens. Timestamps include both an ISO datetime and a humanized relative age
and a humanized relative age wherever shown. If a formatting rule applies in one wherever shown. If a formatting rule applies in one place, it applies in every
place, it applies in every place. Users should never see the same value rendered place. Users should never see the same value rendered differently on two
differently on two screens. 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 #### Language & Labeling

View File

@@ -843,7 +843,11 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<div class="text-xs text-muted mb-1">Amount</div> <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>
<div class="mb-4"> <div class="mb-4">
<div class="text-xs text-muted mb-1">From</div> <div class="text-xs text-muted mb-1">From</div>

View File

@@ -6,6 +6,7 @@ const {
showView, showView,
showFlash, showFlash,
addressDotHtml, addressDotHtml,
addressTitle,
escapeHtml, escapeHtml,
} = require("./helpers"); } = require("./helpers");
const { state } = require("../../shared/state"); 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">`; 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 blockie = blockieHtml(address);
const dot = addressDotHtml(address); const dot = addressDotHtml(address);
const link = `https://etherscan.io/address/${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>`; 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>`; let html = `<div class="mb-1">${blockie}</div>`;
if (title) {
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
}
if (ensName) { if (ensName) {
html += html +=
`<div class="flex items-center">${dot}` + `<div class="flex items-center">${title ? "" : dot}` +
copyableHtml(ensName, "") + copyableHtml(ensName, "") +
extLink + extLink +
`</div>` + `</div>` +
@@ -84,7 +88,7 @@ function txAddressHtml(address, ensName) {
`</div>`; `</div>`;
} else { } else {
html += html +=
`<div class="flex items-center">${dot}` + `<div class="flex items-center">${title ? "" : dot}` +
copyableHtml(address, "break-all") + copyableHtml(address, "break-all") +
extLink + extLink +
`</div>`; `</div>`;
@@ -105,6 +109,9 @@ function show(tx) {
from: tx.from, from: tx.from,
to: tx.to, to: tx.to,
value: tx.value, value: tx.value,
exactValue: tx.exactValue || tx.value,
rawAmount: tx.rawAmount || "",
rawUnit: tx.rawUnit || "",
symbol: tx.symbol, symbol: tx.symbol,
timestamp: tx.timestamp, timestamp: tx.timestamp,
isError: tx.isError, isError: tx.isError,
@@ -120,11 +127,33 @@ function render() {
const tx = state.viewData.tx; const tx = state.viewData.tx;
if (!tx) return; if (!tx) return;
$("tx-detail-hash").innerHTML = txHashHtml(tx.hash); $("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); const fromTitle = addressTitle(tx.from, state.wallets);
$("tx-detail-value").textContent = tx.value const toTitle = addressTitle(tx.to, state.wallets);
? tx.value + " " + tx.symbol $("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.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 = $("tx-detail-time").textContent =
isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")"; isoDate(tx.timestamp) + " (" + timeAgo(tx.timestamp) + ")";
$("tx-detail-status").textContent = tx.isError ? "Failed" : "Success"; $("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" // For contract calls, produce a meaningful label instead of "0.0000 ETH"
let symbol = "ETH"; let symbol = "ETH";
let value = formatTxValue(formatEther(rawWei)); let value = formatTxValue(formatEther(rawWei));
let exactValue = formatEther(rawWei);
let rawAmount = rawWei;
let rawUnit = "wei";
let direction = from.toLowerCase() === addrLower ? "sent" : "received"; let direction = from.toLowerCase() === addrLower ? "sent" : "received";
let directionLabel = direction === "sent" ? "Sent" : "Received"; let directionLabel = direction === "sent" ? "Sent" : "Received";
if (toIsContract && method && method !== "transfer") { if (toIsContract && method && method !== "transfer") {
@@ -38,6 +41,9 @@ function parseTx(tx, addrLower) {
direction = "contract"; direction = "contract";
directionLabel = label; directionLabel = label;
value = ""; value = "";
exactValue = "";
rawAmount = "";
rawUnit = "";
} }
return { return {
@@ -47,6 +53,9 @@ function parseTx(tx, addrLower) {
from: from, from: from,
to: to, to: to,
value: value, value: value,
exactValue: exactValue,
rawAmount: rawAmount,
rawUnit: rawUnit,
valueGwei: Math.floor(Number(BigInt(rawWei) / BigInt(1000000000))), valueGwei: Math.floor(Number(BigInt(rawWei) / BigInt(1000000000))),
symbol: symbol, symbol: symbol,
direction: direction, direction: direction,
@@ -63,17 +72,21 @@ function parseTokenTransfer(tt, addrLower) {
const from = tt.from?.hash || ""; const from = tt.from?.hash || "";
const to = tt.to?.hash || ""; const to = tt.to?.hash || "";
const decimals = parseInt(tt.total?.decimals || "18", 10); 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 direction = from.toLowerCase() === addrLower ? "sent" : "received";
const sym = tt.token?.symbol || "?";
return { return {
hash: tt.transaction_hash, hash: tt.transaction_hash,
blockNumber: tt.block_number, blockNumber: tt.block_number,
timestamp: Math.floor(new Date(tt.timestamp).getTime() / 1000), timestamp: Math.floor(new Date(tt.timestamp).getTime() / 1000),
from: from, from: from,
to: to, 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, valueGwei: null,
symbol: tt.token?.symbol || "?", symbol: sym,
direction: direction, direction: direction,
directionLabel: direction === "sent" ? "Sent" : "Received", directionLabel: direction === "sent" ? "Sent" : "Received",
isError: false, isError: false,