fix: the wait and error screens show a swap's Moved from the sender amounts (closes #722)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

A swap's Amount figure can leave out what its Moved from the sender lines
take, so these screens could read as if less left the wallet than can have
left. Both now show each of those amounts under the figure, using the decoded
details they already receive. The error screen saves those details too, so a
reopened popup keeps the lines. A saved record in another shape shows only the
lines it can read.

Model: opus-5-5
This commit was merged in pull request #732.
This commit is contained in:
2026-11-03 08:12:41 +01:00
parent 21d5e42528
commit f79390cad5
4 changed files with 187 additions and 10 deletions
+72
View File
@@ -340,3 +340,75 @@ describe.each([
expect(node("error-tx-summary").textContent).toBe("1.0000 ETH");
});
});
// https://git.eeqj.de/sneak/AutistMask/issues/722: a WRAP_ETH of 1 ETH into the
// router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH the user
// settles. The Amount line reads 1.0000 ETH, though up to 2 ETH can leave. The
// rest is on the swap's own Moved from the sender line.
describe("a swap with a Moved from the sender line of its own", () => {
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
const TWO_ETH = 2n * ONE_ETH;
const SEND_FAILS = {
error: "The transaction could not be sent.",
retryable: false,
};
const data = routerIface.encodeFunctionData("execute", [
"0x0b0c10",
[
// WRAP_ETH and UNWRAP_WETH have the same layout.
coder.encode(["address", "uint256"], [ADDRESS_THIS, ONE_ETH]),
coder.encode(["address", "uint256"], [ADDRESS_THIS, 0n]),
coder.encode(
["bytes", "bytes[]"],
[
new Uint8Array([SWAP_EXACT_IN_SINGLE, SETTLE, TAKE_ALL]),
[
coder.encode(
[`tuple(${POOL_KEY},bool,uint128,uint128,bytes)`],
[[ETH_USDC_POOL, true, ONE_ETH, 990000n, "0x"]],
),
// The user pays this one, not the router.
coder.encode(
["address", "uint256", "bool"],
[ZERO, 0n, true],
),
takeAllUsdc,
],
],
),
],
9999999999n,
]);
function expectMovedLine(id) {
const html = node(id).innerHTML;
expect(html).toContain("Moved from the sender");
expect(html).toContain("Up to 1.0000 ETH");
}
test("the wait screen shows that line's amount under the Amount figure", async () => {
await approveSwap(data, { txHash: TX_HASH }, TWO_ETH);
expect(node("wait-tx-summary").textContent).toBe("1.0000 ETH");
expectMovedLine("wait-tx-token-note");
});
test("the error screen shows that line's amount under the Amount figure", async () => {
await approveSwap(data, SEND_FAILS, TWO_ETH);
expect(node("error-tx-summary").textContent).toBe("1.0000 ETH");
expectMovedLine("error-tx-token-note");
});
test("a reopened popup's wait screen still shows it", async () => {
await approveSwap(data, { txHash: TX_HASH }, TWO_ETH);
node("wait-tx-token-note").innerHTML = "";
txStatus.restoreWait();
expectMovedLine("wait-tx-token-note");
});
test("a reopened popup's error screen still shows it", async () => {
await approveSwap(data, SEND_FAILS, TWO_ETH);
node("error-tx-token-note").innerHTML = "";
txStatus.renderError();
expectMovedLine("error-tx-token-note");
});
});
+80
View File
@@ -672,6 +672,86 @@ describe("SuccessTx and ErrorTx for a Sepolia transaction with mainnet active",
});
});
describe("the Moved from the sender lines of a swap on WaitTx and ErrorTx", () => {
const moved = (value) => ({ label: "Moved from the sender", value });
const decoded = {
details: [
moved("Up to 1.0000 ETH"),
{ label: "Token moved", value: "ETH" },
moved("<b>400.0000</b> DAI"),
],
};
function expectEveryLine(id) {
const html = getElement(id).innerHTML;
expect(html).toContain("Up to 1.0000 ETH");
expect(html).toContain("&lt;b&gt;400.0000&lt;/b&gt; DAI");
expect(html).not.toContain("Token moved");
}
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");
});
// A saved record comes back from storage, in whatever shape it is in. A
// throw here would leave the popup's restore unfinished.
test("a saved record in another shape shows only the lines it can read", () => {
mockReceiptLookup.mockResolvedValue(null);
function noteOnReopen(saved) {
state.viewData = {
pendingWait: {
hash: TX_HASH,
txInfo: { ...TX_INFO, decoded: saved },
broadcastTime: Date.now(),
},
};
expect(txStatus.restoreWait()).toBe(true);
const waitNote = getElement("wait-tx-token-note").innerHTML;
txStatus.endWait();
state.viewData = {
message: "It failed.",
to: RECIPIENT,
chainId: TX_INFO.chainId,
decoded: saved,
};
txStatus.renderError();
expect(getElement("error-tx-token-note").innerHTML).toBe(waitNote);
return waitNote;
}
for (const saved of [
7,
"text",
{ details: 7 },
{ details: [null] },
{ details: [{ label: "Moved from the sender" }] },
{ details: [moved(7)] },
{ details: [moved({ toString: 1 })] },
]) {
expect(noteOnReopen(saved)).toBe("");
}
expect(
noteOnReopen({ details: [null, moved("Up to 1.0000 ETH")] }),
).toContain("Up to 1.0000 ETH");
});
});
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