Show exact amounts and address titles in transaction detail
All checks were successful
check / check (push) Successful in 5s
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:
20
README.md
20
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user