fix: a saved wait in another shape reaches SuccessTx when its receipt arrives (closes #734)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s

A saved wait whose decoded record is in another shape threw when its
receipt arrived. WaitTx then stayed on screen with the counter stopped.
decodedDetailsHtml() now shows only the lines whose label and value are
text. SuccessTx shows the amount and recipient when no line can be read.

WaitTx and ErrorTx now show each allowance's Allowance token line. Long
amounts use break-words, so words stay whole. ErrorTx sets up copying
when it has no hash, so the token's address can be copied.

Model: opus-5-5
This commit was merged in pull request #739.
This commit is contained in:
2026-11-03 10:14:05 +01:00
parent 49713e1cdb
commit 448afdb5ce
5 changed files with 222 additions and 44 deletions
+54 -30
View File
@@ -73,25 +73,53 @@ function toAddressHtml(address, chainId) {
return renderAddressHtml(address, { title, chainId });
}
// The amount of each `Moved from the sender` and `Permit2 allowance` line of a
// decoded swap, for the wait and error screens to show under the Amount figure.
// That figure can leave these amounts out, so alone it can understate what can
// leave the wallet. `decoded` can come back from storage in any shape, so this
// shows only the lines whose value is text.
function movedOrAllowedHtml(decoded) {
// A decoded record can come back from storage in any shape, so these screens
// show only the lines whose label and value are text. An address that is not
// text would throw in toAddressHtml().
function isReadableLine(d) {
return (
d &&
typeof d.label === "string" &&
typeof d.value === "string" &&
(!d.address || typeof d.address === "string")
);
}
// What a decoded line with an address shows: the address, under the token's
// symbol when the line names a token.
function decodedAddressHtml(d, chainId) {
if (!d.isToken) return toAddressHtml(d.address, chainId);
const sym = tokenLabel(d.address, chainId) || "Unknown token";
return (
`<div class="font-bold">${escapeHtml(sym)}</div>` +
blockscoutOffTokenNoteHtml(d.address, chainId) +
toAddressHtml(d.address, chainId)
);
}
// Each `Moved from the sender`, `Permit2 allowance` and `Allowance token` line
// of a decoded swap, for the wait and error screens to show under the Amount
// figure. That figure can leave these amounts out, so alone it can understate
// what can leave the wallet. The token line says which token an allowance
// covers, which `Unlimited` alone does not.
function movedOrAllowedHtml(decoded, chainId) {
if (!decoded || !Array.isArray(decoded.details)) return "";
let html = "";
for (const d of decoded.details) {
if (
!d ||
!isReadableLine(d) ||
(d.label !== "Moved from the sender" &&
d.label !== "Permit2 allowance") ||
typeof d.value !== "string"
d.label !== "Permit2 allowance" &&
d.label !== "Allowance token")
)
continue;
html += `<div class="mt-2">`;
html += `<div class="text-xs text-muted mb-1">${escapeHtml(d.label)}</div>`;
html += `<div class="font-bold break-all">${escapeHtml(d.value)}</div>`;
if (d.address) {
html += `<div class="text-xs">${decodedAddressHtml(d, chainId)}</div>`;
} else {
html += `<div class="font-bold break-words">${escapeHtml(d.value)}</div>`;
}
html += `</div>`;
}
return html;
@@ -135,7 +163,7 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
txInfo.amount + (symbol ? " " + symbol : "");
$("wait-tx-token-note").innerHTML =
blockscoutOffTokenNoteHtml(txInfo.token, txInfo.chainId) +
movedOrAllowedHtml(txInfo.decoded);
movedOrAllowedHtml(txInfo.decoded, txInfo.chainId);
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to, txInfo.chainId);
$("wait-tx-hash").innerHTML = txHashHtml(txHash, txInfo.chainId);
attachCopyHandlers("view-wait-tx");
@@ -246,7 +274,7 @@ function showWait(txInfo, txHash) {
// to name a network. txInfo.token and txInfo.tokenSymbol are not checked here.
// blockscoutOffTokenNoteHtml() shows no line for a token that is not text.
// Everything else only compares and coalesces them, and tokenSymbol is null for
// ETH. txInfo.decoded is not checked either: see movedOrAllowedHtml().
// ETH. txInfo.decoded is not checked either: see isReadableLine().
function restoreWait() {
const d = state.viewData;
if (!d || !d.pendingWait) return false;
@@ -293,29 +321,25 @@ function showSuccess(txInfo, txHash, blockNumber) {
ctx.doRefreshAndRender();
}
// The decoded action, or "" when no line of it can be read.
function decodedDetailsHtml(decoded, chainId) {
if (!decoded || !decoded.details) return "";
if (!decoded || !Array.isArray(decoded.details)) return "";
const lines = decoded.details.filter(isReadableLine);
if (lines.length === 0) return "";
let html = `<div class="border border-border border-dashed p-2 mb-3">`;
if (decoded.name) {
if (decoded.name && typeof decoded.name === "string") {
html += `<div class="mb-2"><div class="text-xs text-muted mb-1">Action</div>`;
html += `<div class="font-bold">${escapeHtml(decoded.name)}</div></div>`;
}
if (decoded.description) {
if (decoded.description && typeof decoded.description === "string") {
html += `<div class="mb-2"><div class="text-xs text-muted mb-1">Description</div>`;
html += `<div>${escapeHtml(decoded.description)}</div></div>`;
}
for (const d of decoded.details) {
for (const d of lines) {
html += `<div class="mb-2">`;
html += `<div class="text-xs text-muted mb-1">${escapeHtml(d.label)}</div>`;
if (d.address) {
if (d.isToken) {
const sym = tokenLabel(d.address, chainId) || "Unknown token";
html += `<div class="font-bold">${escapeHtml(sym)}</div>`;
html += blockscoutOffTokenNoteHtml(d.address, chainId);
html += toAddressHtml(d.address, chainId);
} else {
html += toAddressHtml(d.address, chainId);
}
html += decodedAddressHtml(d, chainId);
} else {
html += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
}
@@ -329,13 +353,13 @@ function renderSuccess() {
const d = state.viewData;
if (!d || !d.hash) return;
const hasDecoded = d.decoded && d.decoded.details;
const decodedHtml = decodedDetailsHtml(d.decoded, d.chainId);
// When decoded details are present, the Amount and To are already
// shown inside the decoded well — hide the top-level duplicates.
const summarySection = $("success-tx-summary").parentElement;
const toSection = $("success-tx-to").parentElement;
if (hasDecoded) {
if (decodedHtml) {
summarySection.classList.add("hidden");
toSection.classList.add("hidden");
} else {
@@ -351,8 +375,8 @@ function renderSuccess() {
// Show decoded calldata details if present
const decodedEl = $("success-tx-decoded");
if (decodedEl && hasDecoded) {
decodedEl.innerHTML = decodedDetailsHtml(d.decoded, d.chainId);
if (decodedEl && decodedHtml) {
decodedEl.innerHTML = decodedHtml;
decodedEl.classList.remove("hidden");
} else if (decodedEl) {
decodedEl.classList.add("hidden");
@@ -392,18 +416,18 @@ function renderError() {
d.amount + (d.symbol ? " " + d.symbol : "");
$("error-tx-token-note").innerHTML =
blockscoutOffTokenNoteHtml(d.token, d.chainId) +
movedOrAllowedHtml(d.decoded);
movedOrAllowedHtml(d.decoded, d.chainId);
$("error-tx-to").innerHTML = toAddressHtml(d.to, d.chainId);
$("error-tx-message").textContent = d.message;
if (d.hash) {
$("error-tx-hash").innerHTML = txHashHtml(d.hash, d.chainId);
$("error-tx-hash-section").classList.remove("hidden");
attachCopyHandlers("view-error-tx");
} else {
$("error-tx-hash-section").classList.add("hidden");
}
attachCopyHandlers("view-error-tx");
showView("error-tx");
}