2 Commits
Author SHA1 Message Date
sneak e44d9b3cd8 fix: Home's list drops the previous network's rows after a network switch (closes #718)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
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 08:31:26 +00:00
clawbot 1ea0730a81 fix: the wait and error screens show a swap's Permit2 allowance amounts (closes #733)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
A swap can give another spender an allowance that its Amount figure leaves
out, and these two screens did not show it. Both now show each Permit2
allowance amount under the figure, with the Moved from the sender amounts.
movedFromSenderHtml() is now movedOrAllowedHtml(), as it shows both.

An amount too long for one line now wraps on these screens, rather than losing
its last digits at the screen's edge.

Model: opus-5-5
2026-11-03 09:14:23 +01:00
7 changed files with 199 additions and 13 deletions
+2
View File
@@ -788,6 +788,7 @@ Shown after a broadcast. The screen holds:
- the heading "Transaction Broadcast", with no "Back" button;
- the amount and symbol;
- each `Moved from the sender` amount of a swap;
- each `Permit2 allowance` amount of a swap;
- the recipient;
- the transaction hash, with an explorer link;
- the counter "Waiting for confirmation... Ns".
@@ -827,6 +828,7 @@ The screen holds:
- the heading "Transaction Failed";
- the amount;
- each `Moved from the sender` amount of a swap;
- each `Permit2 allowance` amount of a swap;
- the recipient;
- the error message, in a dashed box;
- the transaction hash, when one exists;
+5 -2
View File
@@ -810,7 +810,7 @@
<h2 class="font-bold mb-2">Transaction Broadcast</h2>
<div class="mb-3">
<div class="text-xs text-muted mb-1">Amount</div>
<div id="wait-tx-summary" class="font-bold"></div>
<div id="wait-tx-summary" class="font-bold break-all"></div>
<div id="wait-tx-token-note"></div>
</div>
<div class="mb-3">
@@ -857,7 +857,10 @@
<h2 class="font-bold mb-2">Transaction Failed</h2>
<div class="mb-3">
<div class="text-xs text-muted mb-1">Amount</div>
<div id="error-tx-summary" class="font-bold"></div>
<div
id="error-tx-summary"
class="font-bold break-all"
></div>
<div id="error-tx-token-note"></div>
</div>
<div class="mb-3">
+2
View File
@@ -268,6 +268,7 @@ async function loadHomeTxs(ctx) {
merged = filtered.transactions;
merged.sort((a, b) => b.blockNumber - a.blockNumber);
homeTxs = merged.slice(0, 25);
listNetworkId = networkId;
renderHomeTxList(ctx);
} catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
@@ -281,6 +282,7 @@ async function loadHomeTxs(ctx) {
) {
return;
}
listNetworkId = networkId;
const list = $("home-tx-list");
if (list) {
list.innerHTML =
+12 -11
View File
@@ -73,24 +73,25 @@ function toAddressHtml(address, chainId) {
return renderAddressHtml(address, { title, chainId });
}
// The amount of each `Moved from the sender` 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 read as less than can have left the
// wallet. `decoded` can come back from storage in any shape, so this shows only
// the lines whose value is text.
function movedFromSenderHtml(decoded) {
// 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) {
if (!decoded || !Array.isArray(decoded.details)) return "";
let html = "";
for (const d of decoded.details) {
if (
!d ||
d.label !== "Moved from the sender" ||
(d.label !== "Moved from the sender" &&
d.label !== "Permit2 allowance") ||
typeof d.value !== "string"
)
continue;
html += `<div class="mt-2">`;
html += `<div class="text-xs text-muted mb-1">${escapeHtml(d.label)}</div>`;
html += `<div class="font-bold">${escapeHtml(d.value)}</div>`;
html += `<div class="font-bold break-all">${escapeHtml(d.value)}</div>`;
html += `</div>`;
}
return html;
@@ -134,7 +135,7 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
txInfo.amount + (symbol ? " " + symbol : "");
$("wait-tx-token-note").innerHTML =
blockscoutOffTokenNoteHtml(txInfo.token, txInfo.chainId) +
movedFromSenderHtml(txInfo.decoded);
movedOrAllowedHtml(txInfo.decoded);
$("wait-tx-to").innerHTML = toAddressHtml(txInfo.to, txInfo.chainId);
$("wait-tx-hash").innerHTML = txHashHtml(txHash, txInfo.chainId);
attachCopyHandlers("view-wait-tx");
@@ -245,7 +246,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 movedFromSenderHtml().
// ETH. txInfo.decoded is not checked either: see movedOrAllowedHtml().
function restoreWait() {
const d = state.viewData;
if (!d || !d.pendingWait) return false;
@@ -391,7 +392,7 @@ function renderError() {
d.amount + (d.symbol ? " " + d.symbol : "");
$("error-tx-token-note").innerHTML =
blockscoutOffTokenNoteHtml(d.token, d.chainId) +
movedFromSenderHtml(d.decoded);
movedOrAllowedHtml(d.decoded);
$("error-tx-to").innerHTML = toAddressHtml(d.to, d.chainId);
$("error-tx-message").textContent = d.message;
+32
View File
@@ -498,6 +498,38 @@ 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.
test("keeps its rows while a redraw on the same network waits for the answer", async () => {
fetchRecentTransactions.mockImplementationOnce(async () => history());
+74
View File
@@ -412,3 +412,77 @@ describe("a swap with a Moved from the sender line of its own", () => {
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,6 +101,9 @@ global.document = {
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
// here used to hand back the live stored object, so an in-memory mutation
// looked like a write that had reached storage. See
@@ -743,6 +746,7 @@ describe("the Moved from the sender lines of a swap on WaitTx and ErrorTx", () =
{ details: [{ label: "Moved from the sender" }] },
{ details: [moved(7)] },
{ details: [moved({ toString: 1 })] },
{ details: [{ label: "Permit2 allowance", value: 7 }] },
]) {
expect(noteOnReopen(saved)).toBe("");
}
@@ -752,6 +756,74 @@ 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", () => {
// The resume feature is wired through RESTORABLE_VIEWS: restoreView()
// refuses any view not in the set, so dropping "wait-tx" from it kills