1 Commits
Author SHA1 Message Date
sneak dd091a6e1d fix: Home's list drops the previous network's rows after a network switch (closes #718)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
A network switch left the previous network's rows or failure message in Home's
list until the new network's answer landed. Home now replaces them with
"Loading..." when it first draws on a new network. A redraw on the same network
keeps its rows.

Home, AddressDetail and AddressToken compared the page's network before reading
storage. A switch made in Settings could change the page during that read, and
the answer was still drawn. They now compare after the read, for an answer and
for a failure.

Model: opus-5-5
2026-11-03 07:48:49 +00:00
7 changed files with 13 additions and 199 deletions
-2
View File
@@ -788,7 +788,6 @@ Shown after a broadcast. The screen holds:
- the heading "Transaction Broadcast", with no "Back" button; - the heading "Transaction Broadcast", with no "Back" button;
- the amount and symbol; - the amount and symbol;
- each `Moved from the sender` amount of a swap; - each `Moved from the sender` amount of a swap;
- each `Permit2 allowance` amount of a swap;
- the recipient; - the recipient;
- the transaction hash, with an explorer link; - the transaction hash, with an explorer link;
- the counter "Waiting for confirmation... Ns". - the counter "Waiting for confirmation... Ns".
@@ -828,7 +827,6 @@ The screen holds:
- the heading "Transaction Failed"; - the heading "Transaction Failed";
- the amount; - the amount;
- each `Moved from the sender` amount of a swap; - each `Moved from the sender` amount of a swap;
- each `Permit2 allowance` amount of a swap;
- the recipient; - the recipient;
- the error message, in a dashed box; - the error message, in a dashed box;
- the transaction hash, when one exists; - the transaction hash, when one exists;
+2 -5
View File
@@ -810,7 +810,7 @@
<h2 class="font-bold mb-2">Transaction Broadcast</h2> <h2 class="font-bold mb-2">Transaction Broadcast</h2>
<div class="mb-3"> <div class="mb-3">
<div class="text-xs text-muted mb-1">Amount</div> <div class="text-xs text-muted mb-1">Amount</div>
<div id="wait-tx-summary" class="font-bold break-all"></div> <div id="wait-tx-summary" class="font-bold"></div>
<div id="wait-tx-token-note"></div> <div id="wait-tx-token-note"></div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
@@ -857,10 +857,7 @@
<h2 class="font-bold mb-2">Transaction Failed</h2> <h2 class="font-bold mb-2">Transaction Failed</h2>
<div class="mb-3"> <div class="mb-3">
<div class="text-xs text-muted mb-1">Amount</div> <div class="text-xs text-muted mb-1">Amount</div>
<div <div id="error-tx-summary" class="font-bold"></div>
id="error-tx-summary"
class="font-bold break-all"
></div>
<div id="error-tx-token-note"></div> <div id="error-tx-token-note"></div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
-2
View File
@@ -268,7 +268,6 @@ async function loadHomeTxs(ctx) {
merged = filtered.transactions; merged = filtered.transactions;
merged.sort((a, b) => b.blockNumber - a.blockNumber); merged.sort((a, b) => b.blockNumber - a.blockNumber);
homeTxs = merged.slice(0, 25); homeTxs = merged.slice(0, 25);
listNetworkId = networkId;
renderHomeTxList(ctx); renderHomeTxList(ctx);
} catch (e) { } catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in // Cancelled by the popup closing, not failed: see pageClosed in
@@ -282,7 +281,6 @@ async function loadHomeTxs(ctx) {
) { ) {
return; return;
} }
listNetworkId = networkId;
const list = $("home-tx-list"); const list = $("home-tx-list");
if (list) { if (list) {
list.innerHTML = list.innerHTML =
+11 -12
View File
@@ -73,25 +73,24 @@ function toAddressHtml(address, chainId) {
return renderAddressHtml(address, { title, chainId }); return renderAddressHtml(address, { title, chainId });
} }
// The amount of each `Moved from the sender` and `Permit2 allowance` line of a // The amount of each `Moved from the sender` line of a decoded swap, for the
// decoded swap, for the wait and error screens to show under the Amount figure. // wait and error screens to show under the Amount figure. That figure can leave
// That figure can leave these amounts out, so alone it can understate what can // these amounts out, so alone it can read as less than can have left the
// leave the wallet. `decoded` can come back from storage in any shape, so this // wallet. `decoded` can come back from storage in any shape, so this shows only
// shows only the lines whose value is text. // the lines whose value is text.
function movedOrAllowedHtml(decoded) { function movedFromSenderHtml(decoded) {
if (!decoded || !Array.isArray(decoded.details)) return ""; if (!decoded || !Array.isArray(decoded.details)) return "";
let html = ""; let html = "";
for (const d of decoded.details) { for (const d of decoded.details) {
if ( if (
!d || !d ||
(d.label !== "Moved from the sender" && d.label !== "Moved from the sender" ||
d.label !== "Permit2 allowance") ||
typeof d.value !== "string" typeof d.value !== "string"
) )
continue; continue;
html += `<div class="mt-2">`; html += `<div class="mt-2">`;
html += `<div class="text-xs text-muted mb-1">${escapeHtml(d.label)}</div>`; html += `<div class="text-xs text-muted mb-1">${escapeHtml(d.label)}</div>`;
html += `<div class="font-bold break-all">${escapeHtml(d.value)}</div>`; html += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
html += `</div>`; html += `</div>`;
} }
return html; return html;
@@ -135,7 +134,7 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
txInfo.amount + (symbol ? " " + symbol : ""); txInfo.amount + (symbol ? " " + symbol : "");
$("wait-tx-token-note").innerHTML = $("wait-tx-token-note").innerHTML =
blockscoutOffTokenNoteHtml(txInfo.token, txInfo.chainId) + blockscoutOffTokenNoteHtml(txInfo.token, txInfo.chainId) +
movedOrAllowedHtml(txInfo.decoded); movedFromSenderHtml(txInfo.decoded);
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to, txInfo.chainId); $("wait-tx-to").innerHTML = toAddressHtml(txInfo.to, txInfo.chainId);
$("wait-tx-hash").innerHTML = txHashHtml(txHash, txInfo.chainId); $("wait-tx-hash").innerHTML = txHashHtml(txHash, txInfo.chainId);
attachCopyHandlers("view-wait-tx"); attachCopyHandlers("view-wait-tx");
@@ -246,7 +245,7 @@ function showWait(txInfo, txHash) {
// to name a network. txInfo.token and txInfo.tokenSymbol are not checked here. // to name a network. txInfo.token and txInfo.tokenSymbol are not checked here.
// blockscoutOffTokenNoteHtml() shows no line for a token that is not text. // blockscoutOffTokenNoteHtml() shows no line for a token that is not text.
// Everything else only compares and coalesces them, and tokenSymbol is null for // 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 movedFromSenderHtml().
function restoreWait() { function restoreWait() {
const d = state.viewData; const d = state.viewData;
if (!d || !d.pendingWait) return false; if (!d || !d.pendingWait) return false;
@@ -392,7 +391,7 @@ function renderError() {
d.amount + (d.symbol ? " " + d.symbol : ""); d.amount + (d.symbol ? " " + d.symbol : "");
$("error-tx-token-note").innerHTML = $("error-tx-token-note").innerHTML =
blockscoutOffTokenNoteHtml(d.token, d.chainId) + blockscoutOffTokenNoteHtml(d.token, d.chainId) +
movedOrAllowedHtml(d.decoded); movedFromSenderHtml(d.decoded);
$("error-tx-to").innerHTML = toAddressHtml(d.to, d.chainId); $("error-tx-to").innerHTML = toAddressHtml(d.to, d.chainId);
$("error-tx-message").textContent = d.message; $("error-tx-message").textContent = d.message;
-32
View File
@@ -498,38 +498,6 @@ describe("Home's list", () => {
}, },
); );
// The user leaves Ethereum Mainnet with a request waiting, comes back, and
// that request lands. The list then holds Ethereum Mainnet's answer, so the
// next draw on Sepolia must replace it.
test.each([
{ what: "rows", hold: holdHistory },
{ what: "failure message", hold: holdFailure },
])(
"shows no $what of an older request that lands after a switch back",
async ({ hold }) => {
const answer = hold();
HOME.open();
await onChainSwitch("sepolia");
holdHistory();
HOME.open();
await onChainSwitch("mainnet");
answer();
await settle();
await onChainSwitch("sepolia");
holdHistory();
HOME.open();
await settle();
const list = node(HOME.listId).innerHTML;
expect(list).toContain("Loading...");
expect(list).not.toContain("tx-row");
expect(list).not.toContain("Failed to load transactions.");
},
);
// Each balance refresh draws Home again, so the rows must not blink. // Each balance refresh draws Home again, so the rows must not blink.
test("keeps its rows while a redraw on the same network waits for the answer", async () => { test("keeps its rows while a redraw on the same network waits for the answer", async () => {
fetchRecentTransactions.mockImplementationOnce(async () => history()); fetchRecentTransactions.mockImplementationOnce(async () => history());
-74
View File
@@ -412,77 +412,3 @@ describe("a swap with a Moved from the sender line of its own", () => {
expectMovedLine("error-tx-token-note"); expectMovedLine("error-tx-token-note");
}); });
}); });
// https://git.eeqj.de/sneak/AutistMask/issues/733: a PERMIT2_PERMIT that gives
// another spender an unlimited USDC allowance, then a V4 swap of 1 ETH. The
// Amount line reads 1.0000 ETH and says nothing of the allowance.
describe("a swap that gives another spender an allowance", () => {
const OTHER_SPENDER = "0x1111111111111111111111111111111111111111";
const MAX_UINT160 = (1n << 160n) - 1n;
const SEND_FAILS = {
error: "The transaction could not be sent.",
retryable: false,
};
const data = routerIface.encodeFunctionData("execute", [
"0x0a10",
[
coder.encode(
[
"tuple(tuple(address,uint160,uint48,uint48),address,uint256)",
"bytes",
],
[
[[USDC, MAX_UINT160, 0, 0], OTHER_SPENDER, 9999999999n],
"0x1234",
],
),
coder.encode(
["bytes", "bytes[]"],
[
new Uint8Array([SWAP_EXACT_IN_SINGLE, SETTLE, TAKE]),
[
coder.encode(
[`tuple(${POOL_KEY},bool,uint128,uint128,bytes)`],
[[ETH_USDC_POOL, true, ONE_ETH, 990000n, "0x"]],
),
settleEth,
takeUsdc,
],
],
),
],
9999999999n,
]);
function expectAllowanceLine(id) {
const html = node(id).innerHTML;
expect(html).toContain("Permit2 allowance");
expect(html).toContain("Unlimited");
}
test("the wait screen shows the allowance under the Amount figure", async () => {
await approveSwap(data, { txHash: TX_HASH }, ONE_ETH);
expect(node("wait-tx-summary").textContent).toBe("1.0000 ETH");
expectAllowanceLine("wait-tx-token-note");
});
test("the error screen shows the allowance under the Amount figure", async () => {
await approveSwap(data, SEND_FAILS, ONE_ETH);
expect(node("error-tx-summary").textContent).toBe("1.0000 ETH");
expectAllowanceLine("error-tx-token-note");
});
test("a reopened popup's wait screen still shows it", async () => {
await approveSwap(data, { txHash: TX_HASH }, ONE_ETH);
node("wait-tx-token-note").innerHTML = "";
txStatus.restoreWait();
expectAllowanceLine("wait-tx-token-note");
});
test("a reopened popup's error screen still shows it", async () => {
await approveSwap(data, SEND_FAILS, ONE_ETH);
node("error-tx-token-note").innerHTML = "";
txStatus.renderError();
expectAllowanceLine("error-tx-token-note");
});
});
-72
View File
@@ -101,9 +101,6 @@ global.document = {
global.window = { location: { search: "" } }; global.window = { location: { search: "" } };
const fs = require("fs");
const path = require("path");
// Clones in both directions, as the real chrome.storage.local does; the stub // Clones in both directions, as the real chrome.storage.local does; the stub
// here used to hand back the live stored object, so an in-memory mutation // here used to hand back the live stored object, so an in-memory mutation
// looked like a write that had reached storage. See // looked like a write that had reached storage. See
@@ -746,7 +743,6 @@ describe("the Moved from the sender lines of a swap on WaitTx and ErrorTx", () =
{ details: [{ label: "Moved from the sender" }] }, { details: [{ label: "Moved from the sender" }] },
{ details: [moved(7)] }, { details: [moved(7)] },
{ details: [moved({ toString: 1 })] }, { details: [moved({ toString: 1 })] },
{ details: [{ label: "Permit2 allowance", value: 7 }] },
]) { ]) {
expect(noteOnReopen(saved)).toBe(""); expect(noteOnReopen(saved)).toBe("");
} }
@@ -756,74 +752,6 @@ describe("the Moved from the sender lines of a swap on WaitTx and ErrorTx", () =
}); });
}); });
describe("the Permit2 allowance lines of a swap on WaitTx and ErrorTx", () => {
const allowance = (value) => ({ label: "Permit2 allowance", value });
const decoded = {
details: [
allowance("Unlimited"),
{ label: "Allowance token", value: "USDC" },
allowance("<b>400.0000</b> DAI"),
],
};
function expectEveryLine(id) {
const html = getElement(id).innerHTML;
expect(html).toContain("Permit2 allowance");
expect(html).toContain("Unlimited");
expect(html).toContain("&lt;b&gt;400.0000&lt;/b&gt; DAI");
expect(html).not.toContain("Allowance token");
}
test("WaitTx shows each line's amount, escaped", () => {
mockReceiptLookup.mockResolvedValue(null);
txStatus.showWait({ ...TX_INFO, decoded }, TX_HASH);
expectEveryLine("wait-tx-token-note");
});
test("ErrorTx shows each line's amount, escaped", () => {
txStatus.showError({ ...TX_INFO, decoded }, TX_HASH, "It failed.");
expectEveryLine("error-tx-token-note");
});
test("a wait that ends in ErrorTx keeps them", async () => {
mockReceiptLookup.mockResolvedValue(null);
txStatus.showWait({ ...TX_INFO, decoded }, TX_HASH);
await jest.advanceTimersByTimeAsync(60000);
expect(state.currentView).toBe("error-tx");
expectEveryLine("error-tx-token-note");
});
});
// An amount of about 47 digits or more is wider than the screen. Without the
// break-all class its last digits are cut off at the edge.
describe("a long amount on WaitTx and ErrorTx", () => {
test("each amount under the Amount figure wraps", () => {
const decoded = {
details: [
{ label: "Moved from the sender", value: "1.0000 ETH" },
{ label: "Permit2 allowance", value: "Unlimited" },
],
};
txStatus.showError({ ...TX_INFO, decoded }, TX_HASH, "It failed.");
const html = getElement("error-tx-token-note").innerHTML;
expect(html).toContain('<div class="font-bold break-all">1.0000 ETH<');
expect(html).toContain('<div class="font-bold break-all">Unlimited<');
});
test("the Amount figure wraps", () => {
const html = fs.readFileSync(
path.join(__dirname, "..", "src", "popup", "index.html"),
"utf8",
);
expect(html).toMatch(
/id="wait-tx-summary"\s+class="font-bold break-all"/,
);
expect(html).toMatch(
/id="error-tx-summary"\s+class="font-bold break-all"/,
);
});
});
describe("wait-tx is a view the popup may reopen onto", () => { describe("wait-tx is a view the popup may reopen onto", () => {
// The resume feature is wired through RESTORABLE_VIEWS: restoreView() // The resume feature is wired through RESTORABLE_VIEWS: restoreView()
// refuses any view not in the set, so dropping "wait-tx" from it kills // refuses any view not in the set, so dropping "wait-tx" from it kills