1 Commits
Author SHA1 Message Date
sneak aea693a761 fix: only a permit whose amount is the Amount figure names Token In (closes #370)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
A PERMIT2_PERMIT named Token In whether or not its amount stood as the
Amount figure, so the approval screen could show a permit's token, or an
allowance, where the swap's own input belonged. A permit whose spender
was not the router, or that the router may let fail, had no line naming
its spender.

Now only a permit whose amount stands names Token In. Every other
permit, before the swap steps or after them, leaves Token In and Amount
as they read with no permit and gets three lines of its own: Permit2
allowance, Allowance token and Allowance spender. README Transaction
Decoding, its unknown token scale passage and the TxApproval element
list match.

Model: opus-5-5
2026-10-20 21:19:08 +00:00
3 changed files with 33 additions and 20 deletions
+7 -8
View File
@@ -597,14 +597,12 @@ function decodeV2SwapExactOut(input) {
// token(20) ...), or null when it is not one token followed by whole hops of
// a fee and a token (40 hex digits, then 46 per hop). The router reads only
// whole hops and ignores any bytes after the last one, so the last 20 bytes
// of such a path are not the token it reads there. Both are checksummed, as
// the ABI decoder gives every other address, so a token reads the same on
// every line.
// of such a path are not the token it reads there.
function decodeV3Path(pathHex) {
const hex = pathHex.startsWith("0x") ? pathHex.slice(2) : pathHex;
if (hex.length < 40 || (hex.length - 40) % 46 !== 0) return null;
const first = getAddress("0x" + hex.slice(0, 40));
const last = getAddress("0x" + hex.slice(-40));
const first = "0x" + hex.slice(0, 40);
const last = "0x" + hex.slice(-40);
return { first, last };
}
@@ -2041,9 +2039,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}
// The commands of an EXECUTE_SUB_PLAN step may spend the user's tokens
// or pay the user, before or after any other step. What the other
// steps state is then not the whole swap, so both sides are set
// unknown, with no figure, wherever the step stands.
// or pay the user, before or after any other step, and a permit among
// them would name Token In. What the other steps state is
// then not the whole swap, so both sides are set unknown, with no
// figure, wherever the step stands.
if (hasSubPlan) {
inputToken = null;
inputAmount = null;
+18 -10
View File
@@ -696,8 +696,12 @@ describe("uniswap decoder", () => {
USER_ADDR,
);
expect(result.name).toBe("Swap USDT → WETH");
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Token Out").address).toBe(WETH_ADDR);
expect(detail(result, "Token In").address).toBe(
USDT_ADDR.toLowerCase(),
);
expect(detail(result, "Token Out").address).toBe(
WETH_ADDR.toLowerCase(),
);
const amount = detail(result, "Amount");
expect(amount.value).toBe("Up to 2000.0000 USDT");
@@ -731,7 +735,9 @@ describe("uniswap decoder", () => {
expect(result.name).toBe("Swap ETH → USDC");
expect(detail(result, "Amount").value).toBe("Up to 0.5000 ETH");
expect(detail(result, "Moved from the sender")).toBeUndefined();
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
expect(detail(result, "Token Out").address).toBe(
USDC_ADDR.toLowerCase(),
);
expect(detail(result, "Paid to")).toBeUndefined();
expect(detail(result, "Min. received").value).toBe("1500.0000 USDC");
});
@@ -2367,11 +2373,9 @@ describe("uniswap decoder", () => {
({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(result.name).toBe("Swap USDT → USDC");
// The swap step names Token In, its address checksummed,
// as on a permit's Allowance token line.
expect(detail(result, "Token In").value).toBe(
"USDT (" + USDT_ADDR + ")",
);
expect(
detail(result, "Token In").address.toLowerCase(),
).toBe(USDT_ADDR.toLowerCase());
expect(linesBetweenTokens(result)).toEqual([
["Amount", "600.0000 USDT"],
...lines,
@@ -3280,7 +3284,9 @@ describe("uniswap decoder", () => {
);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1", {}, USER_ADDR);
expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Token In").address).toBe(
USDT_ADDR.toLowerCase(),
);
expect(detail(result, "Amount").value).toBe("1000.0000 USDT");
expect(detail(result, "Token Out").value).toBe(
"Unknown (not named in the calldata)",
@@ -3338,7 +3344,9 @@ describe("uniswap decoder", () => {
"Unknown (not named in the calldata)",
);
expect(detail(result, "Amount")).toBeUndefined();
expect(detail(result, "Token Out").address).toBe(USDT_ADDR);
expect(detail(result, "Token Out").address).toBe(
USDT_ADDR.toLowerCase(),
);
expect(detail(result, "Min. received").value).toBe("990.0000 USDT");
});
+8 -2
View File
@@ -2828,7 +2828,7 @@ describe("the allow-revert flag", () => {
// amount stands as the Amount figure only there, and only without
// the flag. With the flag it stands on no router and gets lines
// of its own, and the swap step names Token In: the same token,
// its address written the same way.
// its address in lowercase.
const permitLines = [
"Permit2 allowance",
"Allowance token",
@@ -2836,9 +2836,15 @@ describe("the allow-revert flag", () => {
];
const swapLines = (r) =>
r.details.filter(
(d) => !["Steps", ...permitLines].includes(d.label),
(d) =>
!["Steps", "Token In", ...permitLines].includes(
d.label,
),
);
expect(swapLines(withFlag)).toEqual(swapLines(plain));
expect(detail(withFlag, "Token In").address.toLowerCase()).toBe(
detail(plain, "Token In").address.toLowerCase(),
);
expect(
labels(withFlag).filter((l) => permitLines.includes(l)),
).toEqual(permitLines);