2 Commits
Author SHA1 Message Date
sneak f8f20155ab fix: the Chrome layout tests and popup comments use the popup's real 396x600 size (closes #556)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
POPUP_VIEWPORT in tests/e2e/run.js said 360x600, while body in
src/popup/styles/main.css is 396px wide and the background opens an
approval window at 396x600. It is now 396x600, defined once near the top
of the file, and the hostile token symbol's frame takes its size from it.

assertAddressesFit() now sets the page it measures to that size first.
Every page it measured, the popup's own screens and the two approval
prompts, was 1280x720, where a page never scrolls sideways, so its
sideways-scroll check could not fail. README's list of what the Chrome
suite leaves out says what sizing the page does not cover.

Comments in src/popup/index.html and tests/dustThreshold.test.js that
said 360x600 now say 396x600.

Model: opus-5-5
2026-10-20 11:02:07 +00:00
clawbot 070ad82338 fix: a swap's unknown first take shows beside a PERMIT2_TRANSFER_FROM, only when the user pays it (closes #634)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
decode() now records whether the user pays the V4 step that sets the
input side any of its input token, and decides once, after the loop,
whether that step counts as a take from the sender. A stated amount
counts whoever pays it; with none stated, the step counts only when the
user pays it. Paid from the router, it gets no Moved from the sender
lines, with a PERMIT2_PERMIT of another token or without. When it counts
and states no amount, its Unknown (could not be decoded) line also shows
beside a PERMIT2_TRANSFER_FROM of its token, whose figure is on that
move's own lines or, into the router, on its Paid to line. README
Transaction Decoding says the same.

Model: opus-5-5
2026-10-20 12:37:01 +02:00
3 changed files with 182 additions and 13 deletions
+12 -6
View File
@@ -3187,7 +3187,8 @@ Currently supported:
step whose input it cannot read, in whole or in part, and a step it fails on.
In any other case the line gives the total, as with no permit. When the step
that sets the input side spends a token other than the permit's, or one the
decoder does not know, its amount gets lines of its own and the total has no
decoder does not know, its amount gets lines of its own, unless it states no
amount and the user pays it none of its input token, and the total has no
figure to start from: unless the permit's amount stands there is no `Amount`
line, and each take of the permit's token gets lines of its own. Each take the
total does not count gives three lines after the `Amount` line (after
@@ -3195,11 +3196,16 @@ Currently supported:
`Token In` after an amount that is not a quantity (a V4 open delta,
`ALREADY_PAID`, which keep their wording on the `Amount` line, or no stated
amount, as of a V4 exact-out step); and one whose amount the decoder does not
know. When the step that sets the input side states no amount and there is no
`Amount` line, that step gets them too whenever another take's lines give a
figure, so that figure cannot read as all that leaves. They come in step
order, the step that sets the input side first when it has them:
`Moved from the sender`, its amount, worded as the `Amount` line words one, or
know. When the step that sets the input side states no amount, the user pays
it some of its input token, and there is no `Amount` line, that step gets them
too whenever another take's lines give a figure, or a `PERMIT2_TRANSFER_FROM`
of its token gives one, on the lines below or, into the router, on its
`Paid to` line, so that figure cannot read as all that leaves. When it states
no amount and the user pays it none of its input token, as when the router
pays it, that step takes none of that token from the user's wallet and never
gets them, with a permit or without. They come in step order, the step that
sets the input side first when it has them: `Moved from the sender`, its
amount, worded as the `Amount` line words one, or
`Unknown (could not be decoded)` when the decoder does not know it;
`Token moved`, giving the token's address as the `Token In` line does; and
`Moved to`, which reads `The swap step that spends it`. A token the decoder
+23 -7
View File
@@ -1204,6 +1204,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
let inputToken = null;
let inputAmount = null;
let inputEstablished = false;
// Whether the user pays the V4 step that set the input side any of
// its input currency (see decodeV4Swap()). Only a V4 step can set the
// input side with no amount, the one case in which this is read.
let inputPaidByUser = false;
// The last PERMIT2_PERMIT this decoder read, or null (see
// permitStands after the loop).
let permit = null;
@@ -1756,6 +1760,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// this decoder could not read may take anything: the
// router still runs it.
const setInputHere = inputEstablished && !inputSetBefore;
if (setInputHere) inputPaidByUser = paidIn !== undefined;
if (v4) {
for (const p of v4.paidByUser) {
if (setInputHere && p === paidIn) continue;
@@ -1844,15 +1849,22 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// (see spentUnread()), or a token it does not know.
const spentFromSender = [];
let total = null;
// Whether the step that set the input side counts as a take from the
// sender, which may get lines of its own. The amount it stated counts
// whoever pays it. Stating none, it counts only when the user pays it
// some of its input token: paid from the router, it takes nothing
// from the sender.
const hasFirstTake =
inputEstablished && (present(inputAmount) || inputPaidByUser);
// The take of the step that set the input side, when the total
// starts from it and it stated no amount (see below), or null.
let firstTakeUnstated = null;
if (permit === null || sameAddress(inputToken, permit.token)) {
total = inputAmount;
if (inputEstablished && !present(inputAmount)) {
if (hasFirstTake && !present(inputAmount)) {
firstTakeUnstated = { token: inputToken, amount: null };
}
} else if (inputEstablished) {
} else if (hasFirstTake) {
// The step that set the input side spent a token other than the
// permit's, or one not known, so the total starts with no figure.
spentFromSender.push({ token: inputToken, amount: inputAmount });
@@ -1873,14 +1885,18 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (ofTokenIn && !present(t.amount)) total = null;
}
inputAmount = permitStands ? permit.amount : total;
// With no Amount figure, a later take's own figure could read as all
// that leaves, so the step that set the input side, when it stated
// no amount, then gets lines of its own too, first, saying its amount
// is not known.
// With no Amount figure, a later take's own figure, or that of a
// PERMIT2_TRANSFER_FROM of the same token, into the router or not,
// could read as all that leaves, so the step that set the input side,
// when the user pays it and it stated no amount, then gets lines of
// its own too, first, saying its amount is not known.
if (
!present(inputAmount) &&
firstTakeUnstated !== null &&
spentFromSender.some((s) => present(s.amount))
(spentFromSender.some((s) => present(s.amount)) ||
[...movedFromSender, ...movedIntoRouter].some((m) =>
sameAddress(m.token, firstTakeUnstated.token),
))
) {
spentFromSender.unshift(firstTakeUnstated);
}
+147
View File
@@ -2318,6 +2318,24 @@ describe("uniswap decoder", () => {
encodeV4TakeAll(WETH_ADDR, 0n),
],
);
// The same step with its SETTLE of OPEN_DELTA paid from the router.
const V4_EXACT_OUT_600_FROM_ROUTER = encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_OUT_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactOutSingle(
USDT_ADDR,
WETH_ADDR,
ONE_WETH,
USDT_600,
),
settle(USDT_ADDR, 0n, false),
encodeV4TakeAll(WETH_ADDR, 0n),
],
);
test.each([
{
kind: "a V4 exact-out step with a SETTLE of OPEN_DELTA by the user",
@@ -2325,6 +2343,13 @@ describe("uniswap decoder", () => {
first: V4_EXACT_OUT_600,
lines: [...UNKNOWN_USDT, ...UP_TO_400_USDT],
},
// Paid from the router, the step takes nothing from the wallet.
{
kind: "a V4 exact-out step with a SETTLE of OPEN_DELTA from the router",
commands: [0x10, 0x09],
first: V4_EXACT_OUT_600_FROM_ROUTER,
lines: UP_TO_400_USDT,
},
// Settled by the user, the step's amount would be what that
// SETTLE pays, which the decoder cannot state here.
{
@@ -2370,6 +2395,36 @@ describe("uniswap decoder", () => {
},
);
// https://git.eeqj.de/sneak/AutistMask/issues/634 — after a
// PERMIT2_PERMIT of another token, the V4 exact-out step paid from
// the router still takes nothing from the wallet, so it gets no lines
// of its own. On next it got lines saying its amount is not known.
const ONE_DAI_PERMIT = encodePermit2(DAI_ADDR, 10n ** 18n, ROUTER_ADDR);
test.each([
{
kind: "a PERMIT2_PERMIT of 1 DAI, then a V4 exact-out step of at most 600 USDT paid from the router",
commands: [0x0a, 0x10],
inputs: [ONE_DAI_PERMIT, V4_EXACT_OUT_600_FROM_ROUTER],
lines: [["Amount", "1.0000 DAI"]],
},
{
kind: "a PERMIT2_PERMIT of 1 DAI, a V4 exact-out step of at most 600 USDT paid from the router, then a V2 exact-out of at most 400 USDT the user pays for",
commands: [0x0a, 0x10, 0x09],
inputs: [
ONE_DAI_PERMIT,
V4_EXACT_OUT_600_FROM_ROUTER,
V2_EXACT_OUT_400,
],
lines: [["Amount", "Up to 1.0000 DAI"], ...UP_TO_400_USDT],
},
])(
"$kind, gives the V4 step no lines of its own",
({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
},
);
// The V4 exact-out route above the other way round, where the later
// take's unknown amount leaves no figure at all, and other ways the
// step that sets the input side states no amount.
@@ -2423,6 +2478,98 @@ describe("uniswap decoder", () => {
},
);
// https://git.eeqj.de/sneak/AutistMask/issues/634 — a
// PERMIT2_TRANSFER_FROM of the token the V4 exact-out step spends,
// before or after it, shows its own figure, so the step's amount is
// shown as not known beside it, and the 400 USDT cannot read as all
// that leaves. On next only the 400 USDT was shown. Into the router,
// the 400 USDT is on its Paid to line.
const transfer = (token, recipient, amount) =>
coder.encode(
["address", "address", "uint160"],
[token, recipient, amount],
);
const MOVED_400_USDT = [
["Moved from the sender", "400.0000 USDT"],
["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", OTHER_ADDR],
];
test.each([
{
kind: "a V4 exact-out step of at most 600 USDT, then a PERMIT2_TRANSFER_FROM of 400 USDT to another address",
commands: [0x10, 0x02],
inputs: [
V4_EXACT_OUT_600,
transfer(USDT_ADDR, OTHER_ADDR, USDT_400),
],
lines: [...UNKNOWN_USDT, ...MOVED_400_USDT],
},
{
kind: "a PERMIT2_TRANSFER_FROM of 400 USDT to another address, then a V4 exact-out step of at most 600 USDT",
commands: [0x02, 0x10],
inputs: [
transfer(USDT_ADDR, OTHER_ADDR, USDT_400),
V4_EXACT_OUT_600,
],
lines: [...UNKNOWN_USDT, ...MOVED_400_USDT],
},
{
kind: "a V4 exact-out step of at most 600 USDT, then a PERMIT2_TRANSFER_FROM of 400 USDT into the router",
commands: [0x10, 0x02],
inputs: [
V4_EXACT_OUT_600,
transfer(USDT_ADDR, ROUTER_ADDR, USDT_400),
],
lines: UNKNOWN_USDT,
},
{
kind: "a PERMIT2_TRANSFER_FROM of 400 USDT into the router, then a V4 exact-out step of at most 600 USDT",
commands: [0x02, 0x10],
inputs: [
transfer(USDT_ADDR, ROUTER_ADDR, USDT_400),
V4_EXACT_OUT_600,
],
lines: UNKNOWN_USDT,
},
])(
"$kind, paid by the user, shows that the step's amount is not known",
({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
},
);
// Paid from the router, the V4 exact-out step takes nothing from the
// wallet, so the moves give all that leaves.
test("a PERMIT2_TRANSFER_FROM of 400 USDT into the router, one of 10 USDT to another address, then a V4 exact-out step paid from the router, shows only the moves", () => {
const result = decodeSteps(
[0x02, 0x02, 0x10],
[
transfer(USDT_ADDR, ROUTER_ADDR, USDT_400),
transfer(USDT_ADDR, OTHER_ADDR, 10000000n),
V4_EXACT_OUT_600_FROM_ROUTER,
],
);
expect(linesBetweenTokens(result)).toEqual([
["Moved from the sender", "10.0000 USDT"],
["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", OTHER_ADDR],
]);
});
// A move of another token says nothing of how much USDT leaves.
test("a V4 exact-out step of at most 600 USDT, then a PERMIT2_TRANSFER_FROM of 400 DAI, shows only the DAI moved", () => {
const result = decodeSteps(
[0x10, 0x02],
[V4_EXACT_OUT_600, transfer(DAI_ADDR, OTHER_ADDR, DAI_400)],
);
expect(linesBetweenTokens(result)).toEqual([
["Moved from the sender", "400.0000 DAI"],
["Token moved", "DAI (" + DAI_ADDR + ")"],
["Moved to", OTHER_ADDR],
]);
});
// A V2 exact-out step makes every amount the swap steps spend a maximum,
// the total included.
test("a split with a V2 exact-out step the user pays for shows the total as a maximum", () => {