This commit was merged in pull request #381.
This commit is contained in:
+20
-29
@@ -213,10 +213,7 @@
|
||||
</div>
|
||||
|
||||
<!-- active address display -->
|
||||
<div
|
||||
id="active-address-display"
|
||||
class="text-xs break-all mb-3"
|
||||
></div>
|
||||
<div id="active-address-display" class="text-xs mb-3"></div>
|
||||
|
||||
<!-- quick actions for active address -->
|
||||
<div class="flex gap-2 mb-2">
|
||||
@@ -292,7 +289,7 @@
|
||||
class="font-bold mb-1 hidden flex items-center"
|
||||
></div>
|
||||
<div
|
||||
class="text-xs mb-1 cursor-pointer break-all"
|
||||
class="text-xs mb-1 cursor-pointer"
|
||||
title="Click to copy"
|
||||
id="address-line"
|
||||
>
|
||||
@@ -380,14 +377,14 @@
|
||||
></div>
|
||||
<h2 class="font-bold mb-1">Export Private Key</h2>
|
||||
<p class="text-xs mb-1" id="export-privkey-title"></p>
|
||||
<p class="text-xs mb-3">
|
||||
<div class="text-xs mb-3">
|
||||
<span id="export-privkey-dot"></span>
|
||||
<span
|
||||
id="export-privkey-address"
|
||||
class="cursor-pointer"
|
||||
title="Click to copy"
|
||||
></span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs mb-3 text-muted">
|
||||
Warning: anyone with this private key can access and
|
||||
transfer all funds from this address. Never share it.
|
||||
@@ -440,7 +437,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-xs mb-1 cursor-pointer break-all"
|
||||
class="text-xs mb-1 cursor-pointer"
|
||||
title="Click to copy"
|
||||
id="address-token-line"
|
||||
>
|
||||
@@ -573,19 +570,16 @@
|
||||
<!-- ERC-20 token contract (hidden for ETH) -->
|
||||
<div id="confirm-token-section" class="mb-3 hidden">
|
||||
<div class="text-xs text-muted mb-1">Token contract</div>
|
||||
<div
|
||||
id="confirm-token-contract"
|
||||
class="text-xs break-all"
|
||||
></div>
|
||||
<div id="confirm-token-contract" class="text-xs"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">From</div>
|
||||
<div id="confirm-from" class="text-xs break-all"></div>
|
||||
<div id="confirm-from" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="confirm-to" class="text-xs break-all"></div>
|
||||
<div id="confirm-to" class="text-xs"></div>
|
||||
<div
|
||||
id="confirm-to-ens"
|
||||
class="text-xs text-muted hidden"
|
||||
@@ -728,7 +722,7 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="wait-tx-to" class="text-xs break-all"></div>
|
||||
<div id="wait-tx-to" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Transaction hash</div>
|
||||
@@ -747,7 +741,7 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="success-tx-to" class="text-xs break-all"></div>
|
||||
<div id="success-tx-to" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Block</div>
|
||||
@@ -774,7 +768,7 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="error-tx-to" class="text-xs break-all"></div>
|
||||
<div id="error-tx-to" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div
|
||||
@@ -811,9 +805,9 @@
|
||||
<canvas id="receive-qr"></canvas>
|
||||
</div>
|
||||
<div
|
||||
class="border border-border p-2 break-all mb-3 text-xs cursor-pointer"
|
||||
class="border border-border p-2 mb-3 text-xs cursor-pointer"
|
||||
>
|
||||
<span id="receive-address-block" class="select-all"></span>
|
||||
<div id="receive-address-block" class="select-all"></div>
|
||||
<span id="receive-etherscan-link"></span>
|
||||
</div>
|
||||
<button
|
||||
@@ -1239,7 +1233,7 @@
|
||||
</p>
|
||||
<div
|
||||
id="delete-address-value"
|
||||
class="text-xs mb-2 break-all min-h-[1rem]"
|
||||
class="text-xs mb-2 min-h-[1rem]"
|
||||
></div>
|
||||
<div
|
||||
class="text-xs mb-2 border border-border border-dashed p-2"
|
||||
@@ -1429,14 +1423,11 @@
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="text-xs text-muted mb-1">From</div>
|
||||
<div
|
||||
id="tx-detail-from"
|
||||
class="text-xs break-all"
|
||||
></div>
|
||||
<div id="tx-detail-from" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="text-xs text-muted mb-1">To</div>
|
||||
<div id="tx-detail-to" class="text-xs break-all"></div>
|
||||
<div id="tx-detail-to" class="text-xs"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1473,7 +1464,7 @@
|
||||
</div>
|
||||
<div
|
||||
id="tx-detail-token-contract"
|
||||
class="text-xs break-all"
|
||||
class="text-xs"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1567,11 +1558,11 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">From</div>
|
||||
<div id="approve-tx-from" class="text-xs break-all"></div>
|
||||
<div id="approve-tx-from" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Contract</div>
|
||||
<div id="approve-tx-to" class="text-xs break-all"></div>
|
||||
<div id="approve-tx-to" class="text-xs"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">Value</div>
|
||||
@@ -1673,7 +1664,7 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-xs text-muted mb-1">From</div>
|
||||
<div id="approve-sign-from" class="text-xs break-all"></div>
|
||||
<div id="approve-sign-from" class="text-xs"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
|
||||
@@ -44,3 +44,23 @@ body {
|
||||
background-color 225ms ease-out,
|
||||
color 225ms ease-out;
|
||||
}
|
||||
|
||||
/* An address is one atomic string, so it gets a row of its own and never
|
||||
* breaks across lines. A wrapped address reads as two shorter strings, and
|
||||
* two shorter strings are exactly what an address-poisoning attack needs
|
||||
* the user to compare instead of the whole thing. Every view that shows an
|
||||
* address puts it in one of these, alone: the colour dot, the wallet title,
|
||||
* the ENS name and the explorer link all live on their own line above, so
|
||||
* nothing competes with the 42 characters for width.
|
||||
*
|
||||
* overflow-x is the escape hatch, not the mechanism. The row is wide enough
|
||||
* for a full address at every nesting depth the popup uses; if that ever
|
||||
* stops being true — a font with wider glyphs, a browser zoom — the row
|
||||
* scrolls and the user can still reach the last character, rather than the
|
||||
* tail being clipped away by #app's overflow-x-hidden with nothing to say
|
||||
* it happened. tests/e2e asserts the scroll is never actually needed. */
|
||||
.am-address {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ const {
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
truncateMiddle,
|
||||
renderAddressHtml,
|
||||
attachCopyHandlers,
|
||||
goBack,
|
||||
@@ -229,10 +228,12 @@ function renderTransactions(txs) {
|
||||
const amountStr = tx.value
|
||||
? escapeHtml(tx.value + " " + sym)
|
||||
: escapeHtml(sym);
|
||||
const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
|
||||
const displayAddr =
|
||||
title || ensName || truncateMiddle(counterparty, maxAddr);
|
||||
const addrStr = escapeHtml(displayAddr);
|
||||
// The counterparty used to be squeezed in beside the amount and
|
||||
// truncated to whatever was left over. It gets its own row now and
|
||||
// is shown whole; the title or ENS name, where there is one, names
|
||||
// it on the line above rather than replacing it.
|
||||
const nameStr = escapeHtml(title || ensName || "");
|
||||
const addrStr = escapeHtml(counterparty);
|
||||
const dot = addressDotHtml(counterparty);
|
||||
const err = tx.isError ? " (failed)" : "";
|
||||
const opacity = tx.isError ? " opacity:0.5;" : "";
|
||||
@@ -240,7 +241,8 @@ function renderTransactions(txs) {
|
||||
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
|
||||
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${nameStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="am-address">${addrStr}</div>`;
|
||||
html += `</div>`;
|
||||
i++;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ const {
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
truncateMiddle,
|
||||
balanceLine,
|
||||
unknownableAmount,
|
||||
renderAddressHtml,
|
||||
@@ -305,10 +304,12 @@ function renderTransactions(txs) {
|
||||
const amountStr = tx.value
|
||||
? escapeHtml(tx.value + " " + sym)
|
||||
: escapeHtml(sym);
|
||||
const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
|
||||
const displayAddr =
|
||||
title || ensName || truncateMiddle(counterparty, maxAddr);
|
||||
const addrStr = escapeHtml(displayAddr);
|
||||
// The counterparty used to be squeezed in beside the amount and
|
||||
// truncated to whatever was left over. It gets its own row now and
|
||||
// is shown whole; the title or ENS name, where there is one, names
|
||||
// it on the line above rather than replacing it.
|
||||
const nameStr = escapeHtml(title || ensName || "");
|
||||
const addrStr = escapeHtml(counterparty);
|
||||
const dot = addressDotHtml(counterparty);
|
||||
const err = tx.isError ? " (failed)" : "";
|
||||
const opacity = tx.isError ? " opacity:0.5;" : "";
|
||||
@@ -316,7 +317,8 @@ function renderTransactions(txs) {
|
||||
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
|
||||
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${nameStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="am-address">${addrStr}</div>`;
|
||||
html += `</div>`;
|
||||
i++;
|
||||
}
|
||||
|
||||
+33
-15
@@ -331,6 +331,12 @@ function addressHoldsFunds(addr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The fewest characters of an address any caller may ask to display. The
|
||||
// 10-character cap inside truncateMiddle() is the other half of the same
|
||||
// guarantee; this is the half that used to be spelled out at each call
|
||||
// site, and is now enforced once in renderAddressHtml().
|
||||
const ADDRESS_MIN_DISPLAY_LEN = 32;
|
||||
|
||||
// Truncate the middle of a string, replacing removed characters with "…".
|
||||
// Safety: refuses to truncate more than 10 characters, which is the maximum
|
||||
// that still prevents address spoofing attacks (see Display Consistency in
|
||||
@@ -518,17 +524,29 @@ function attachCopyHandlers(container) {
|
||||
|
||||
// Unified address rendering.
|
||||
//
|
||||
// Produces consistent HTML for any Ethereum address:
|
||||
// • Color dot
|
||||
// • Optional title (e.g. "Wallet 1 — Address 2") shown bold above address
|
||||
// • Optional ENS name shown bold above address
|
||||
// • Full address (or truncated via maxLen) with dashed-underline click-to-copy
|
||||
// • Etherscan external link icon
|
||||
// Two stacked rows, in this order:
|
||||
// 1. Identity strip — colour dot, optional title (e.g. "Wallet 1 —
|
||||
// Address 2") and the explorer link icon. Optional ENS name below it.
|
||||
// 2. The address itself, alone on a full-width row that never wraps
|
||||
// (see .am-address in styles/main.css).
|
||||
//
|
||||
// The split is the point. Everything used to sit on one line: dot, address
|
||||
// and link together, with `break-all` to let the address fold when the line
|
||||
// ran out. In the wallet list, where the row also carried [info] and [x],
|
||||
// it ran out every time — the bug in #380 — and a folded address is a
|
||||
// spoofing hazard, not a cosmetic one. Nothing shares the address's row
|
||||
// now, so all 42 characters fit at every nesting depth the popup uses and
|
||||
// nothing has to be dropped or folded to make room.
|
||||
//
|
||||
// Options object:
|
||||
// title — wallet title string (from addressTitle)
|
||||
// ensName — ENS name string
|
||||
// maxLen — if set, truncate address display (min 32 chars enforced)
|
||||
// maxLen — if set, truncate address display. Floored at 32 characters
|
||||
// here rather than by the caller: no view passes it any more
|
||||
// (every address row is wide enough for all 42 characters),
|
||||
// so a floor that lived in the callers would have gone away
|
||||
// with them, and the "at least 32 characters" guarantee has
|
||||
// to survive having no current callers to be a guarantee.
|
||||
// noLink — if true, omit etherscan link
|
||||
//
|
||||
// After inserting the returned HTML into the DOM, call
|
||||
@@ -536,22 +554,22 @@ function attachCopyHandlers(container) {
|
||||
function renderAddressHtml(address, opts) {
|
||||
const { title, ensName, maxLen, noLink } = opts || {};
|
||||
const dot = addressDotHtml(address);
|
||||
const displayAddr = maxLen ? truncateMiddle(address, maxLen) : address;
|
||||
const displayAddr = maxLen
|
||||
? truncateMiddle(address, Math.max(ADDRESS_MIN_DISPLAY_LEN, maxLen))
|
||||
: address;
|
||||
const link = etherscanAddressUrl(address);
|
||||
const extLink = noLink ? "" : etherscanLinkHtml(link);
|
||||
|
||||
let html = "";
|
||||
html += `<div class="flex items-center">${dot}`;
|
||||
if (title) {
|
||||
html += `<div class="flex items-center font-bold">${dot}${escapeHtml(title)}</div>`;
|
||||
html += `<span class="font-bold">${escapeHtml(title)}</span>`;
|
||||
}
|
||||
html += `${extLink}</div>`;
|
||||
if (ensName) {
|
||||
html += `<div class="flex items-center font-bold">${title ? "" : dot}${escapeHtml(ensName)}</div>`;
|
||||
}
|
||||
if (title || ensName) {
|
||||
html += `<div class="flex items-center">${copyableHtml(displayAddr, "break-all")}${extLink}</div>`;
|
||||
} else {
|
||||
html += `<div class="flex items-center">${dot}${copyableHtml(displayAddr, "break-all")}${extLink}</div>`;
|
||||
html += `<div class="font-bold">${escapeHtml(ensName)}</div>`;
|
||||
}
|
||||
html += `<div class="am-address">${copyableHtml(displayAddr)}</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
+19
-11
@@ -9,7 +9,6 @@ const {
|
||||
addressTitle,
|
||||
escapeHtml,
|
||||
displaySymbol,
|
||||
truncateMiddle,
|
||||
renderAddressHtml,
|
||||
attachCopyHandlers,
|
||||
pushCurrentView,
|
||||
@@ -117,10 +116,13 @@ function renderHomeTxList(ctx) {
|
||||
const amountStr = tx.value
|
||||
? escapeHtml(tx.value + " " + sym)
|
||||
: escapeHtml(sym);
|
||||
// The counterparty used to be squeezed in beside the amount and
|
||||
// truncated to whatever was left over. It gets its own row now and
|
||||
// is shown whole; the title, when it is one of our own addresses,
|
||||
// names it on the line above rather than replacing it.
|
||||
const title = addressTitle(counterparty, state.wallets);
|
||||
const maxAddr = Math.max(32, 36 - Math.max(0, amountStr.length - 10));
|
||||
const displayAddr = title || truncateMiddle(counterparty, maxAddr);
|
||||
const addrStr = escapeHtml(displayAddr);
|
||||
const titleStr = title ? escapeHtml(title) : "";
|
||||
const addrStr = escapeHtml(counterparty);
|
||||
const dot = addressDotHtml(counterparty);
|
||||
const err = tx.isError ? " (failed)" : "";
|
||||
const opacity = tx.isError ? " opacity:0.5;" : "";
|
||||
@@ -128,7 +130,8 @@ function renderHomeTxList(ctx) {
|
||||
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
|
||||
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${addrStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="flex justify-between"><span class="flex items-center">${dot}${titleStr}</span><span>${amountStr}</span></div>`;
|
||||
html += `<div class="am-address">${addrStr}</div>`;
|
||||
html += `</div>`;
|
||||
i++;
|
||||
}
|
||||
@@ -252,17 +255,22 @@ function walletListHtml() {
|
||||
: "";
|
||||
const dot = addressDotHtml(addr.address);
|
||||
const titleBold = isActive ? "font-bold" : "";
|
||||
html += `<div class="text-xs ${titleBold}">Address ${ai + 1}</div>`;
|
||||
// [info] and [x] ride on the "Address N" line, which was empty
|
||||
// to its right, so the address below gets the row to itself.
|
||||
// They used to sit beside the address and take about a third of
|
||||
// the width off it, which is what made a 42-character address
|
||||
// fold onto a second line here and nowhere else (#380).
|
||||
html += `<div class="flex text-xs items-center justify-between">`;
|
||||
html += `<span class="flex items-center ${titleBold}">${dot}Address ${ai + 1}</span>`;
|
||||
html += `<span class="flex-shrink-0 ml-1">${infoBtn}${removeBtn}</span>`;
|
||||
html += `</div>`;
|
||||
if (addr.ensName) {
|
||||
// An ENS reverse record is whatever the name owner set it
|
||||
// to; renderAddressHtml() escapes its own copy of this and
|
||||
// this list was the one that did not.
|
||||
html += `<div class="text-xs font-bold flex items-center">${dot}${escapeHtml(addr.ensName)}</div>`;
|
||||
html += `<div class="text-xs font-bold">${escapeHtml(addr.ensName)}</div>`;
|
||||
}
|
||||
html += `<div class="flex text-xs items-center justify-between">`;
|
||||
html += `<span class="flex items-center break-all">${addr.ensName ? "" : dot}${escapeHtml(addr.address)}</span>`;
|
||||
html += `<span class="flex-shrink-0 ml-1">${infoBtn}${removeBtn}</span>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="am-address text-xs">${escapeHtml(addr.address)}</div>`;
|
||||
const addrTotal = formatAddressTotal(getAddressValue(addr));
|
||||
html += `<div class="text-xs text-muted text-right min-h-[1rem]">${addrTotal || " "}</div>`;
|
||||
html += balanceLinesForAddress(
|
||||
|
||||
@@ -137,10 +137,16 @@ function render() {
|
||||
if (tx.contractAddress) {
|
||||
const dot = addressDotHtml(tx.contractAddress);
|
||||
const link = explorerUrl("token", tx.contractAddress);
|
||||
// Hand-rolled rather than renderAddressHtml() because the
|
||||
// link goes to the explorer's /token/ page, not /address/.
|
||||
// Same two-row shape though: dot and link on the strip, the
|
||||
// contract address alone on the row below it.
|
||||
tokenContractEl.innerHTML =
|
||||
`<div class="flex items-center">${dot}` +
|
||||
copyableHtml(tx.contractAddress, "break-all") +
|
||||
etherscanLinkHtml(link) +
|
||||
`</div>` +
|
||||
`<div class="am-address">` +
|
||||
copyableHtml(tx.contractAddress) +
|
||||
`</div>`;
|
||||
tokenContractSection.classList.remove("hidden");
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user