Author SHA1 Message Date
clawbot cb58e9d5f7 fix: the Settings network help text says switching restores the last endpoints used (closes #513)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
The text under Network said switching resets the RPC and Blockscout
endpoints to their defaults. Switching actually restores the endpoints
last used on that network, or that network's defaults if it has none,
as README's Settings section says. The text now says that, in README's
words.

Model: opus-5-5
2026-10-19 05:28:21 +02:00
clawbot 8b6c2b4df9 fix: the swap summary is shown only for a transaction sent to a Universal Router (closes #520)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
The Universal Router decoder recognised execute() by its selector alone, so
the same calldata sent to any address was described as a swap through the
router. It now keeps the router deployments Uniswap publishes for Ethereum
Mainnet and Sepolia, and decodes only a transaction sent to one of them on
its network, ignoring address case. TxApproval passes the transaction's
chain id, TransactionDetail the active network's.

Model: opus-5-5
2026-10-19 04:40:55 +02:00
clawbot 85fa864db4 test: no timer a popup test started fires after cleanupPopup() (closes #578)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
tests/support/popupBoot.js recorded every interval the popup sets but left
timeouts alone, so the two-second timer that clears a flash message could fire
after cleanupPopup() had removed the page, throw "document is not defined",
and fail whichever test file the jest worker had moved on to. Timeouts still
run for real during a popup test; the helper now records each one until it
fires or is cleared, and cleanupPopup() clears the ones still pending. The
fake timers tests/firstWalletRefresh.test.js used to hold that timer are
removed. tests/popupBoot.test.js shows a flash message, cleans up, waits past
the flash duration, and fails if anything throws.

Model: opus-5-5
2026-10-19 04:06:24 +02:00
16 changed files with 518 additions and 77 deletions
+8 -4
View File
@@ -2880,10 +2880,14 @@ Currently supported:
or recipient, and amount. or recipient, and amount.
- **Uniswap Universal Router**: `execute()` calls — shows swap direction (e.g. - **Uniswap Universal Router**: `execute()` calls — shows swap direction (e.g.
"Swap USDT → ETH"), token addresses, amounts, execution steps, and deadline. "Swap USDT → ETH"), token addresses, amounts, execution steps, and deadline.
Decodes Permit2, V2/V3/V4 swaps, wrap/unwrap, and balance checks. A V4 swap The swap summary is shown only for a transaction sent to one of the Universal
action is read only in the parameter shape deployed on mainnet and Sepolia, Router addresses Uniswap publishes for the network: on TxApproval the network
not in the newer one on `v4-periphery` `main`, which adds a `minHopPriceX36` the transaction is for, on TransactionDetail the active network. The same
field. An action in any other shape is not read, and its step is listed as calldata sent to any other address is shown as raw data. Decodes Permit2,
V2/V3/V4 swaps, wrap/unwrap, and balance checks. A V4 swap action is read only
in the parameter shape deployed on mainnet and Sepolia, not in the newer one
on `v4-periphery` `main`, which adds a `minHopPriceX36` field. An action in
any other shape is not read, and its step is listed as
`V4 Swap (could not be decoded)`. `V4 Swap (could not be decoded)`.
### Non-Goals Forever ### Non-Goals Forever
+3 -3
View File
@@ -924,9 +924,9 @@
<div class="bg-well p-3 mx-1 mb-3"> <div class="bg-well p-3 mx-1 mb-3">
<h3 class="font-bold mb-1">Network</h3> <h3 class="font-bold mb-1">Network</h3>
<p class="text-xs text-muted mb-1"> <p class="text-xs text-muted mb-1">
Select the Ethereum network. Switching networks will Select the Ethereum network. Switching restores the RPC
update the RPC and Blockscout endpoints to their and Blockscout endpoints last used on that network, or
defaults. that network's defaults if it has none.
</p> </p>
<div class="text-xs flex items-center gap-1"> <div class="text-xs flex items-center gap-1">
<select <select
+17 -5
View File
@@ -79,9 +79,10 @@ function tokenAmountText(rawAmount, decimals, symbol) {
}; };
} }
// Try to decode calldata using known ABIs. // Try to decode calldata sent to `toAddress` on the network with chain id
// `chainId`, using known ABIs.
// Returns { name, description, details } or null. // Returns { name, description, details } or null.
function decodeCalldata(data, toAddress) { function decodeCalldata(data, toAddress, chainId) {
if (!data || data === "0x" || data.length < 10) return null; if (!data || data === "0x" || data.length < 10) return null;
// Where a token's scale is looked for, for every decoder below: the ERC-20 // Where a token's scale is looked for, for every decoder below: the ERC-20
@@ -185,7 +186,12 @@ function decodeCalldata(data, toAddress) {
} }
// Try Uniswap Universal Router // Try Uniswap Universal Router
const routerResult = uniswap.decode(data, toAddress, decimalsSources); const routerResult = uniswap.decode(
data,
toAddress,
chainId,
decimalsSources,
);
if (routerResult) return routerResult; if (routerResult) return routerResult;
return null; return null;
@@ -269,8 +275,14 @@ function showTxApproval(details) {
chainId: approvedTx.chainId, chainId: approvedTx.chainId,
}; };
// If this is an ERC-20 call, try to extract the real recipient and amount // If this is an ERC-20 call, try to extract the real recipient and amount.
const decoded = decodeCalldata(approvedTx.data, toAddr || ""); // Decoded for the network the transaction is for, which the Network line
// names.
const decoded = decodeCalldata(
approvedTx.data,
toAddr || "",
approvedTx.chainId,
);
if (decoded && decoded.details) { if (decoded && decoded.details) {
// The asset the status summary is counted in: an ERC-20 call's Token // The asset the status summary is counted in: an ERC-20 call's Token
// contract, or a swap's input token. Its symbol is resolved from the // contract, or a swap's input token. Its symbol is resolved from the
+7 -2
View File
@@ -21,7 +21,7 @@ const {
displaySymbol, displaySymbol,
goBack, goBack,
} = require("./helpers"); } = require("./helpers");
const { state } = require("../../shared/state"); const { state, currentNetwork } = require("../../shared/state");
const { nativeCurrencyByChainId } = require("../../shared/networks"); const { nativeCurrencyByChainId } = require("../../shared/networks");
const { formatEther, formatUnits } = require("ethers"); const { formatEther, formatUnits } = require("ethers");
const { log, debugFetch } = require("../../shared/log"); const { log, debugFetch } = require("../../shared/log");
@@ -316,7 +316,12 @@ async function loadFullTxDetails(txHash, chainId) {
// Decoded against the address the transaction was sent to, as the // Decoded against the address the transaction was sent to, as the
// explorer reports it, not the history entry's `to`: for a token // explorer reports it, not the history entry's `to`: for a token
// transfer that is the transfer's recipient, not the token contract. // transfer that is the transfer's recipient, not the token contract.
const decoded = decodeCalldata(inputData, txData.to?.hash || ""); // On the active network, the one whose explorer was just asked.
const decoded = decodeCalldata(
inputData,
txData.to?.hash || "",
currentNetwork().chainId,
);
if (decoded) { if (decoded) {
// Render decoded calldata matching approval view style // Render decoded calldata matching approval view style
actionEl.textContent = decoded.name; actionEl.textContent = decoded.name;
+45 -4
View File
@@ -8,6 +8,7 @@ const {
resolveTokenSymbol, resolveTokenSymbol,
unknownDecimalsAmount, unknownDecimalsAmount,
} = require("./approvalAmount"); } = require("./approvalAmount");
const { NETWORKS } = require("./networks");
const coder = AbiCoder.defaultAbiCoder(); const coder = AbiCoder.defaultAbiCoder();
@@ -15,6 +16,41 @@ const ROUTER_IFACE = new Interface([
"function execute(bytes commands, bytes[] inputs, uint256 deadline)", "function execute(bytes commands, bytes[] inputs, uint256 deadline)",
]); ]);
// The Universal Router deployments on each network, by chain id, copied from
// the deployment files Uniswap publishes, each address named by its key there.
// The files also list `UnsupportedProtocol`, which is not a router.
const ROUTER_ADDRESSES = new Map([
[
NETWORKS.mainnet.chainId,
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/mainnet.json
[
"0xEf1c6E67703c7BD7107eed8303Fbe6EC2554BF6B", // UniversalRouterV1
"0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", // UniversalRouterV1_2_V2Support
"0x66a9893cc07d91d95644aedd05d03f95e1dba8af", // UniversalRouterV2
"0x4C82D1fBFe28C977cBB58D8C7FF8FCF9F70a2cCA", // UniversalRouterV2_1_1
"0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85", // UniversalRouterV2_1_2
],
],
[
NETWORKS.sepolia.chainId,
// https://github.com/Uniswap/universal-router/blob/543e1a19d6e21e31ced2512eec5792b50f13a0ba/deploy-addresses/sepolia.json
[
"0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD", // UniversalRouterV1_2_V2Support
"0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b", // UniversalRouterV2
"0x7DfD4F31be6814D2906BDE155c3e1B146EAc1468", // UniversalRouterV2_1_1
"0x7E4f6c5e954Da5c61B3423D81E2277431Ac043f3", // UniversalRouterV2_1_2
],
],
]);
// Whether `address` is a Universal Router deployment on the network with this
// chain id. Address case is ignored.
function isRouterAddress(address, chainId) {
const routers = ROUTER_ADDRESSES.get(chainId) || [];
const wanted = (address || "").toLowerCase();
return routers.some((router) => router.toLowerCase() === wanted);
}
// Universal Router command IDs (lower 5 bits of each command byte) // Universal Router command IDs (lower 5 bits of each command byte)
const COMMAND_NAMES = { const COMMAND_NAMES = {
0x00: "V3 Swap (Exact In)", 0x00: "V3 Swap (Exact In)",
@@ -120,7 +156,7 @@ const NO_MINIMUM = "None (no minimum guaranteed)";
const MAX_UINT160 = BigInt("0xffffffffffffffffffffffffffffffffffffffff"); const MAX_UINT160 = BigInt("0xffffffffffffffffffffffffffffffffffffffff");
// WETH, the token UNWRAP_WETH turns into ETH: on mainnet, then on Sepolia. // WETH, the token UNWRAP_WETH turns into ETH: on mainnet, then on Sepolia.
// decode() is not told the network, so it takes either. // Either one counts on either network.
const WETH_ADDRESSES = [ const WETH_ADDRESSES = [
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"0xfff9976782d46cc05630d1f6ebab18b2324d6b14", "0xfff9976782d46cc05630d1f6ebab18b2324d6b14",
@@ -559,10 +595,15 @@ function decodeV4Swap(input) {
} }
} }
// Try to decode a Universal Router execute() call. // Try to decode a Universal Router execute() call sent to `toAddress` on the
// network with chain id `chainId`.
// Returns { name, description, details } matching the format used by // Returns { name, description, details } matching the format used by
// the approval UI, or null if the calldata is not a recognised execute(). // the approval UI, or null if the calldata is not a recognised execute() or
function decode(data, toAddress, sources) { // `toAddress` is not a Universal Router on that network: the same calldata
// sent to any other contract is not a swap through the router, whatever that
// contract does with it.
function decode(data, toAddress, chainId, sources) {
if (!isRouterAddress(toAddress, chainId)) return null;
try { try {
const parsed = ROUTER_IFACE.parseTransaction({ data }); const parsed = ROUTER_IFACE.parseTransaction({ data });
if (!parsed) return null; if (!parsed) return null;
+5 -1
View File
@@ -68,7 +68,11 @@ function swapData(amountIn, amountOutMin) {
} }
function swapDetail(amountIn, amountOutMin, label) { function swapDetail(amountIn, amountOutMin, label) {
const decoded = uniswap.decode(swapData(amountIn, amountOutMin), ROUTER); const decoded = uniswap.decode(
swapData(amountIn, amountOutMin),
ROUTER,
"0x1",
);
return decoded.details.find((d) => d.label === label); return decoded.details.find((d) => d.label === label);
} }
+1 -1
View File
@@ -136,7 +136,7 @@ function erc20AmountLine(data, tokenAddress) {
} }
function swapAmountLine(data) { function swapAmountLine(data) {
return decodeCalldata(data, ROUTER).details.find( return decodeCalldata(data, ROUTER, "0x1").details.find(
(d) => d.label === "Amount", (d) => d.label === "Amount",
).value; ).value;
} }
-4
View File
@@ -61,16 +61,12 @@ async function importOn(page, { tab, field, secret }) {
await page.click("btn-add-wallet-confirm"); await page.click("btn-add-wallet-confirm");
} }
// Fake timers hold the timer of the flash message an import ends with, which
// would otherwise fire after cleanupPopup() has removed the page and throw.
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers();
chainBalance = "0.0"; chainBalance = "0.0";
}); });
afterEach(() => { afterEach(() => {
cleanupPopup(); cleanupPopup();
jest.useRealTimers();
}); });
describe("a popup that opened on Welcome", () => { describe("a popup that opened on Welcome", () => {
+42
View File
@@ -0,0 +1,42 @@
// A timer the popup started and that fires after cleanupPopup() has removed
// the page throws "document is not defined", and the throw fails whatever test
// the jest worker is running by then, in this file or another
// (https://git.eeqj.de/sneak/AutistMask/issues/578). The flash message's
// timer is the one that did it. A throw from a timer during a test fails that
// test, so each test here passes only if nothing throws.
const {
bootPopup,
cleanupPopup,
unversionedValidProfile,
} = require("./support/popupBoot");
const FLASH_MS = 10;
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
afterEach(() => {
cleanupPopup();
});
test("a flash message clears while the page is up", async () => {
const page = await bootPopup(unversionedValidProfile());
const { showFlash } = require("../src/popup/views/helpers");
showFlash("Copied.", FLASH_MS);
expect(page.text("flash-msg")).toBe("Copied.");
await wait(FLASH_MS * 5);
expect(page.text("flash-msg")).toBe("");
});
test("a flash message's timer does not fire after cleanupPopup()", async () => {
await bootPopup(unversionedValidProfile());
const { showFlash } = require("../src/popup/views/helpers");
showFlash("Copied.", FLASH_MS);
cleanupPopup();
await wait(FLASH_MS * 5);
});
+28
View File
@@ -43,6 +43,12 @@ jest.doMock("ethereum-blockies-base64", () => makeBlockie);
// Taken before any boot replaces them; see bootPopup(). // Taken before any boot replaces them; see bootPopup().
const realSetInterval = globalThis.setInterval; const realSetInterval = globalThis.setInterval;
const realClearInterval = globalThis.clearInterval; const realClearInterval = globalThis.clearInterval;
const realSetTimeout = globalThis.setTimeout;
const realClearTimeout = globalThis.clearTimeout;
// Every timeout set since the last cleanupPopup() that has neither fired nor
// been cleared. cleanupPopup() clears them all.
const pendingTimeouts = new Set();
const POPUP_HTML = fs.readFileSync( const POPUP_HTML = fs.readFileSync(
path.join(__dirname, "..", "..", "src", "popup", "index.html"), path.join(__dirname, "..", "..", "src", "popup", "index.html"),
@@ -342,6 +348,24 @@ async function bootPopup(stored, options) {
globalThis.clearInterval = (id) => { globalThis.clearInterval = (id) => {
intervals.delete(id); intervals.delete(id);
}; };
// Timeouts, such as the one that clears a flash message, run for real, but
// each is recorded until it fires or is cleared, so cleanupPopup() can
// clear the ones still pending. One that fired after the page was removed
// would throw "document is not defined" into whatever test the worker runs
// by then. jest.useFakeTimers() in a test does not reach them. Put back by
// cleanupPopup().
globalThis.setTimeout = (fn, ms, ...args) => {
const timer = realSetTimeout(() => {
pendingTimeouts.delete(timer);
fn(...args);
}, ms);
pendingTimeouts.add(timer);
return timer;
};
globalThis.clearTimeout = (timer) => {
pendingTimeouts.delete(timer);
realClearTimeout(timer);
};
require("../../src/popup/index"); require("../../src/popup/index");
@@ -407,6 +431,10 @@ function cleanupPopup() {
delete globalThis.window; delete globalThis.window;
globalThis.setInterval = realSetInterval; globalThis.setInterval = realSetInterval;
globalThis.clearInterval = realClearInterval; globalThis.clearInterval = realClearInterval;
for (const timer of pendingTimeouts) realClearTimeout(timer);
pendingTimeouts.clear();
globalThis.setTimeout = realSetTimeout;
globalThis.clearTimeout = realClearTimeout;
} }
module.exports = { module.exports = {
+47 -44
View File
@@ -1,6 +1,7 @@
const { AbiCoder, Interface, solidityPacked } = require("ethers"); const { AbiCoder, Interface, solidityPacked } = require("ethers");
const uniswap = require("../src/shared/uniswap"); const uniswap = require("../src/shared/uniswap");
// A Universal Router on Ethereum Mainnet, chain id 0x1.
const ROUTER_ADDR = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af"; const ROUTER_ADDR = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
@@ -315,13 +316,13 @@ function detail(result, label) {
describe("uniswap decoder", () => { describe("uniswap decoder", () => {
test("returns null for non-execute calldata", () => { test("returns null for non-execute calldata", () => {
expect(uniswap.decode("0x", ROUTER_ADDR)).toBeNull(); expect(uniswap.decode("0x", ROUTER_ADDR, "0x1")).toBeNull();
expect(uniswap.decode("0xdeadbeef", ROUTER_ADDR)).toBeNull(); expect(uniswap.decode("0xdeadbeef", ROUTER_ADDR, "0x1")).toBeNull();
expect(uniswap.decode(null, ROUTER_ADDR)).toBeNull(); expect(uniswap.decode(null, ROUTER_ADDR, "0x1")).toBeNull();
}); });
test("decodes first-ever AutistMask swap (PERMIT2_PERMIT + V4_SWAP)", () => { test("decodes first-ever AutistMask swap (PERMIT2_PERMIT + V4_SWAP)", () => {
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR); const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(result.name).toBe("Swap USDT \u2192 ETH"); expect(result.name).toBe("Swap USDT \u2192 ETH");
@@ -361,6 +362,7 @@ describe("uniswap decoder", () => {
const result = uniswap.decode( const result = uniswap.decode(
v4SwapCalldata(id, deployed), v4SwapCalldata(id, deployed),
ROUTER_ADDR, ROUTER_ADDR,
"0x1",
); );
expect(detail(result, "Steps").value).toBe("V4 Swap"); expect(detail(result, "Steps").value).toBe("V4 Swap");
}, },
@@ -376,6 +378,7 @@ describe("uniswap decoder", () => {
const result = uniswap.decode( const result = uniswap.decode(
v4SwapCalldata(id, newer), v4SwapCalldata(id, newer),
ROUTER_ADDR, ROUTER_ADDR,
"0x1",
); );
expect(detail(result, "Amount")).toBeUndefined(); expect(detail(result, "Amount")).toBeUndefined();
expect(detail(result, "Min. received")).toBeUndefined(); expect(detail(result, "Min. received")).toBeUndefined();
@@ -406,7 +409,7 @@ describe("uniswap decoder", () => {
], ],
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Steps").value).toBe( expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)", "V4 Swap (could not be decoded)",
); );
@@ -427,7 +430,7 @@ describe("uniswap decoder", () => {
[encodeV4Swap(new Uint8Array([id]), [newer])], [encodeV4Swap(new Uint8Array([id]), [newer])],
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Steps").value).toBe( expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)", "V4 Swap (could not be decoded)",
); );
@@ -457,7 +460,7 @@ describe("uniswap decoder", () => {
], ],
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Uniswap Swap"); expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Amount").value).toBe("2.0000 USDT"); expect(detail(result, "Amount").value).toBe("2.0000 USDT");
expect(detail(result, "Token Out").value).toBe( expect(detail(result, "Token Out").value).toBe(
@@ -485,7 +488,7 @@ describe("uniswap decoder", () => {
], ],
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Uniswap Swap"); expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Token In").value).toBe( expect(detail(result, "Token In").value).toBe(
"Unknown (could not be decoded)", "Unknown (could not be decoded)",
@@ -507,7 +510,7 @@ describe("uniswap decoder", () => {
], ],
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Token Out").value).toBe( expect(detail(result, "Token Out").value).toBe(
"Unknown (could not be decoded)", "Unknown (could not be decoded)",
); );
@@ -527,7 +530,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(result.name).toBe("Swap USDT \u2192 WETH"); expect(result.name).toBe("Swap USDT \u2192 WETH");
@@ -554,7 +557,7 @@ describe("uniswap decoder", () => {
1767225600n, 1767225600n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap USDC → WETH"); expect(result.name).toBe("Swap USDC → WETH");
expect(detail(result, "Token In").address).toBe(USDC_ADDR); expect(detail(result, "Token In").address).toBe(USDC_ADDR);
expect(detail(result, "Token Out").address).toBe(WETH_ADDR); expect(detail(result, "Token Out").address).toBe(WETH_ADDR);
@@ -586,7 +589,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap ETH → USDC"); expect(result.name).toBe("Swap ETH → USDC");
const amount = detail(result, "Amount"); const amount = detail(result, "Amount");
@@ -615,7 +618,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap USDC → ETH"); expect(result.name).toBe("Swap USDC → ETH");
expect(detail(result, "Amount").value).toBe("Up to 1500.0000 USDC"); expect(detail(result, "Amount").value).toBe("Up to 1500.0000 USDC");
expect(detail(result, "Token Out").value).toBe("ETH"); expect(detail(result, "Token Out").value).toBe("ETH");
@@ -658,7 +661,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Token Out").address).toBe(tokenOut); expect(detail(result, "Token Out").address).toBe(tokenOut);
expect(detail(result, "Min. received").value).toBe(minReceived); expect(detail(result, "Min. received").value).toBe(minReceived);
}, },
@@ -679,7 +682,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap USDT → USDC"); expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token Out").address).toBe(USDC_ADDR); expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
expect(detail(result, "Min. received").value).toBe("1.9000 USDC"); expect(detail(result, "Min. received").value).toBe("1.9000 USDC");
@@ -712,7 +715,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Token Out").value).toBe("ETH"); expect(detail(result, "Token Out").value).toBe("ETH");
expect(detail(result, "Min. received").value).toBe("0.4000 ETH"); expect(detail(result, "Min. received").value).toBe("0.4000 ETH");
}); });
@@ -731,7 +734,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Token Out").value).toBe("ETH"); expect(detail(result, "Token Out").value).toBe("ETH");
expect(detail(result, "Min. received").value).toBe("0.0005 ETH"); expect(detail(result, "Min. received").value).toBe("0.0005 ETH");
}); });
@@ -756,7 +759,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Token Out").value).toBe( expect(detail(result, "Token Out").value).toBe(
"Unknown (not named in the calldata)", "Unknown (not named in the calldata)",
); );
@@ -779,7 +782,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(result.name).toBe("Swap USDT \u2192 WETH"); expect(result.name).toBe("Swap USDT \u2192 WETH");
}); });
@@ -791,7 +794,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
const tokenIn = result.details.find((d) => d.label === "Token In"); const tokenIn = result.details.find((d) => d.label === "Token In");
@@ -814,7 +817,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
// UNWRAP_WETH means output is native ETH // UNWRAP_WETH means output is native ETH
expect(result.name).toBe("Swap USDT \u2192 ETH"); expect(result.name).toBe("Swap USDT \u2192 ETH");
@@ -830,7 +833,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
const minOut = result.details.find((d) => d.label === "Min. received"); const minOut = result.details.find((d) => d.label === "Min. received");
@@ -850,7 +853,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
// The V4 swap action's parameters are empty, so that step is said to // The V4 swap action's parameters are empty, so that step is said to
@@ -862,7 +865,7 @@ describe("uniswap decoder", () => {
}); });
test("shows the deadline as a UTC date and time", () => { test("shows the deadline as a UTC date and time", () => {
const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR); const result = uniswap.decode(FIRST_SWAP_CALLDATA, ROUTER_ADDR, "0x1");
expect(detail(result, "Deadline").value).toBe("2026-02-27 08:25:51"); expect(detail(result, "Deadline").value).toBe("2026-02-27 08:25:51");
}); });
@@ -880,7 +883,7 @@ describe("uniswap decoder", () => {
2n ** 256n - 1n, 2n ** 256n - 1n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(result.name).toBe("Swap USDT \u2192 WETH"); expect(result.name).toBe("Swap USDT \u2192 WETH");
expect(detail(result, "Deadline").value).toBe( expect(detail(result, "Deadline").value).toBe(
@@ -895,7 +898,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
const amount = result.details.find((d) => d.label === "Amount"); const amount = result.details.find((d) => d.label === "Amount");
@@ -970,7 +973,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
// Before decodeV4Swap fix: name would be "Swap USDT → ETH" or "Uniswap Swap" // Before decodeV4Swap fix: name would be "Swap USDT → ETH" or "Uniswap Swap"
// After fix: correctly identifies both tokens from V4 sub-actions // After fix: correctly identifies both tokens from V4 sub-actions
@@ -1004,7 +1007,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
// The amount and the token it is counted in come from the same hop. // The amount and the token it is counted in come from the same hop.
@@ -1044,7 +1047,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toBe( expect(detail(result, "Token Out").value).toBe(
@@ -1089,7 +1092,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDC"); expect(detail(result, "Token Out").value).toContain("USDC");
@@ -1118,7 +1121,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDC"); expect(detail(result, "Token Out").value).toContain("USDC");
@@ -1155,7 +1158,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(detail(result, "Token In").value).toContain("WETH"); expect(detail(result, "Token In").value).toContain("WETH");
@@ -1348,7 +1351,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap USDT → USDC"); expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token In").address).toBe(USDT_ADDR); expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Token Out").address).toBe(USDC_ADDR); expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
@@ -1400,7 +1403,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Steps").value).toBe( expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)", "V4 Swap (could not be decoded)",
); );
@@ -1459,7 +1462,7 @@ describe("uniswap decoder", () => {
], ],
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Uniswap Swap"); expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Token In").address).toBe( expect(detail(result, "Token In").address).toBe(
USDT_ADDR.toLowerCase(), USDT_ADDR.toLowerCase(),
@@ -1515,7 +1518,7 @@ describe("uniswap decoder", () => {
], ],
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Uniswap Swap"); expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Token In").value).toBe( expect(detail(result, "Token In").value).toBe(
"Unknown (not named in the calldata)", "Unknown (not named in the calldata)",
@@ -1538,7 +1541,7 @@ describe("uniswap decoder", () => {
], ],
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Token Out").value).toBe( expect(detail(result, "Token Out").value).toBe(
"Unknown (not named in the calldata)", "Unknown (not named in the calldata)",
); );
@@ -1572,7 +1575,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap USDT → USDC"); expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token In").address).toBe(USDT_ADDR); expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Token Out").address).toBe(USDC_ADDR); expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
@@ -1596,7 +1599,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(detail(result, "Token In").value).toContain("USDT"); expect(detail(result, "Token In").value).toContain("USDT");
@@ -1627,7 +1630,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("WETH"); expect(detail(result, "Token Out").value).toContain("WETH");
@@ -1648,7 +1651,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDC"); expect(detail(result, "Token Out").value).toContain("USDC");
@@ -1667,7 +1670,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDT"); expect(detail(result, "Token Out").value).toContain("USDT");
@@ -1687,7 +1690,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(result.name).toBe("Uniswap Swap"); expect(result.name).toBe("Uniswap Swap");
@@ -1707,7 +1710,7 @@ describe("uniswap decoder", () => {
9999999999n, 9999999999n,
); );
const result = uniswap.decode(data, ROUTER_ADDR); const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull(); expect(result).not.toBeNull();
expect(result.name).toBe("Uniswap Swap"); expect(result.name).toBe("Uniswap Swap");
+298
View File
@@ -0,0 +1,298 @@
// Which transactions are shown as a swap through the Uniswap Universal Router
// (https://git.eeqj.de/sneak/AutistMask/issues/520).
//
// The decoder used to recognise the router's execute() call by its selector
// alone, so the same calldata sent to an ordinary address was shown as a swap
// through the router, with that address as its Protocol. It is now decoded
// only when the transaction is sent to a Universal Router deployment on the
// network it is for; anywhere else it is left to the raw data.
//
// The transaction detail screen and the approval screen are driven against a
// minimal DOM stub in the shape tests/tokenTransferDetail.test.js uses, the
// approval screen opened the way tests/swapStatusAmount.test.js opens it.
const { AbiCoder, Interface, getAddress, solidityPacked } = require("ethers");
jest.mock("../src/shared/log", () => ({
log: {
debugf: () => {},
infof: () => {},
warnf: () => {},
errorf: () => {},
},
debugFetch: async (url) => ({
ok: true,
json: async () => mockExplorerAnswer(url),
}),
setRuntimeDebug: () => {},
isDebug: () => false,
}));
globalThis.chrome = {
storage: { local: { get: async () => ({}), set: async () => {} } },
};
const { state } = require("../src/shared/state");
const approval = require("../src/popup/views/approval");
const { decodeCalldata } = approval;
const transactionDetail = require("../src/popup/views/transactionDetail");
const MAINNET = "0x1";
const SEPOLIA = "0xaa36a7";
// From Uniswap's deployment files: one router on Ethereum Mainnet only, one on
// Sepolia only, and one at the same address on both.
const MAINNET_ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
const SEPOLIA_ROUTER = "0x3a9d48ab9751398bbfa63ad67599bb04e4bdf98b";
const ROUTER_ON_BOTH = "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD";
// The address with no code from the issue.
const ORDINARY = "0x0b0050Da68435b3EcBC236c40F16dB2070bCD79A";
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
const SEPOLIA_WETH = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
const TOKEN = "0xE2E0000000000000000000000000000000000E2e";
const HUNDREDTH_ETH = 10000000000000000n;
const coder = AbiCoder.defaultAbiCoder();
const routerIface = new Interface([
"function execute(bytes commands, bytes[] inputs, uint256 deadline)",
]);
// The calldata from the issue: wrap 0.01 ETH, then a V3 swap of it into a
// token.
const SWAP_DATA = routerIface.encodeFunctionData("execute", [
solidityPacked(["uint8", "uint8"], [0x0b, 0x00]),
[
coder.encode(["address", "uint256"], [RECIPIENT, HUNDREDTH_ETH]),
coder.encode(
["address", "uint256", "uint256", "bytes", "bool"],
[
RECIPIENT,
HUNDREDTH_ETH,
1n,
solidityPacked(
["address", "uint24", "address"],
[SEPOLIA_WETH, 3000, TOKEN],
),
false,
],
),
],
9999999999n,
]);
beforeEach(() => {
state.trackedTokens = [];
state.wallets = [];
});
describe("execute() calldata on the approval screen's decoder", () => {
test("sent to a router on the transaction's network, it is a swap", () => {
const onMainnet = decodeCalldata(SWAP_DATA, MAINNET_ROUTER, MAINNET);
expect(onMainnet.description).toBe("Swap via Uniswap Universal Router");
expect(onMainnet.details[0]).toMatchObject({
label: "Protocol",
address: MAINNET_ROUTER,
});
expect(
decodeCalldata(SWAP_DATA, SEPOLIA_ROUTER, SEPOLIA).description,
).toBe("Swap via Uniswap Universal Router");
});
test("sent to an ordinary address, it is not decoded", () => {
expect(decodeCalldata(SWAP_DATA, ORDINARY, MAINNET)).toBeNull();
expect(decodeCalldata(SWAP_DATA, ORDINARY, SEPOLIA)).toBeNull();
});
test("sent to a router of the other network only, it is not decoded", () => {
expect(decodeCalldata(SWAP_DATA, SEPOLIA_ROUTER, MAINNET)).toBeNull();
expect(decodeCalldata(SWAP_DATA, MAINNET_ROUTER, SEPOLIA)).toBeNull();
});
test("a router deployed at the same address on both networks is one on each", () => {
expect(
decodeCalldata(SWAP_DATA, ROUTER_ON_BOTH, MAINNET),
).not.toBeNull();
expect(
decodeCalldata(SWAP_DATA, ROUTER_ON_BOTH, SEPOLIA),
).not.toBeNull();
});
test("the router address matches in any letter case", () => {
const upper = "0x" + MAINNET_ROUTER.slice(2).toUpperCase();
for (const address of [getAddress(MAINNET_ROUTER), upper]) {
expect(decodeCalldata(SWAP_DATA, address, MAINNET)).not.toBeNull();
}
});
});
// The transaction the explorer reports: SWAP_DATA, sent to `mockSentTo`.
const TX_HASH = "0x" + "5".repeat(64);
let mockSentTo = null;
function mockExplorerAnswer(url) {
if (url.endsWith("/transactions/" + TX_HASH)) {
return {
to: { hash: mockSentTo },
raw_input: SWAP_DATA,
method: "execute",
};
}
return {};
}
function makeElement(id) {
const classes = new Set();
const el = {
id,
textContent: "",
value: "",
innerHTML: "",
style: {},
dataset: {},
classList: {
add: (...names) => names.forEach((n) => classes.add(n)),
remove: (...names) => names.forEach((n) => classes.delete(n)),
contains: (n) => classes.has(n),
toggle: () => false,
},
addEventListener: () => {},
querySelectorAll: () => [],
appendChild: () => {},
};
// Views reach for .parentElement to hide whole sections.
Object.defineProperty(el, "parentElement", {
get: () => node(id + "-parent"),
});
return el;
}
function makeDocument() {
const els = new Map();
return {
getElementById(id) {
// The debug banner is created on demand by helpers.js; absent
// is the state a non-debug popup is in.
if (id === "debug-banner") return null;
if (!els.has(id)) els.set(id, makeElement(id));
return els.get(id);
},
createElement: () => makeElement("created"),
body: { prepend: () => {} },
};
}
function node(id) {
return globalThis.document.getElementById(id);
}
// Open the transaction detail screen with Sepolia active, for the
// transaction the explorer reports sent to `sentTo`, and wait for its
// decoded details.
async function showDetailSentTo(sentTo) {
mockSentTo = sentTo;
globalThis.document = makeDocument();
globalThis.window = { location: { search: "" } };
state.networkId = "sepolia";
state.blockscoutUrl = "https://explorer.example/api/v2";
state.viewData = {};
state.viewStack = [];
state.currentView = null;
transactionDetail.show({
hash: TX_HASH,
from: RECIPIENT,
to: sentTo,
value: "",
symbol: "SepoliaETH",
timestamp: 1790000000,
isError: false,
direction: "contract",
directionLabel: "Swap",
method: "execute",
chainId: SEPOLIA,
});
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
}
describe("execute() calldata on the transaction detail screen", () => {
test("sent to a router on the active network, it is a swap", async () => {
await showDetailSentTo(SEPOLIA_ROUTER);
expect(node("tx-detail-calldata-details").innerHTML).toContain(
"Swap via Uniswap Universal Router",
);
});
test("sent to an ordinary address, it is not decoded", async () => {
await showDetailSentTo(ORDINARY);
expect(node("tx-detail-calldata-action").textContent).toBe("execute");
expect(node("tx-detail-calldata-details").innerHTML).toBe("");
expect(node("tx-detail-rawdata").innerHTML).toContain(SWAP_DATA);
});
test("sent to a router of the other network only, it is not decoded", async () => {
await showDetailSentTo(MAINNET_ROUTER);
expect(node("tx-detail-calldata-action").textContent).toBe("execute");
expect(node("tx-detail-calldata-details").innerHTML).toBe("");
expect(node("tx-detail-rawdata").innerHTML).toContain(SWAP_DATA);
});
});
// Open the approval screen with Ethereum Mainnet active, for a site's Sepolia
// transaction that sends SWAP_DATA to `sentTo`.
async function showApprovalSentTo(sentTo) {
globalThis.document = makeDocument();
globalThis.window = { location: { search: "" }, close: () => {} };
globalThis.chrome.runtime = {
connect: () => ({ postMessage: () => {} }),
sendMessage: (_msg, reply) =>
reply({
type: "tx",
origin: "https://dapp.example",
isPhishingDomain: false,
approvedFrom: RECIPIENT,
approvedTx: {
type: 2,
from: RECIPIENT,
chainId: SEPOLIA,
nonce: "0x7",
gasLimit: "0x30d40",
maxPriorityFeePerGas: "0x3b9aca00",
maxFeePerGas: "0x77359400",
to: sentTo,
value: "0x" + HUNDREDTH_ETH.toString(16),
data: SWAP_DATA,
accessList: [],
},
}),
};
state.networkId = "mainnet";
state.currentView = null;
await approval.show(1);
}
// The router is on Sepolia's list only, so the swap is shown only if the
// screen decodes for the transaction's network: on Ethereum Mainnet, the
// active network, that address is not a router.
describe("execute() calldata on the approval screen, for a Sepolia transaction while Ethereum Mainnet is active", () => {
test("sent to a router on Sepolia, it is a swap", async () => {
await showApprovalSentTo(SEPOLIA_ROUTER);
expect(node("approve-tx-decoded").classList.contains("hidden")).toBe(
false,
);
expect(node("approve-tx-decoded-details").innerHTML).toContain(
"Swap via Uniswap Universal Router",
);
});
test("sent to an ordinary address, it is shown as raw data", async () => {
await showApprovalSentTo(ORDINARY);
expect(node("approve-tx-decoded").classList.contains("hidden")).toBe(
true,
);
expect(node("approve-tx-decoded-details").innerHTML).toBe("");
expect(node("approve-tx-data").textContent).toBe(SWAP_DATA);
expect(
node("approve-tx-data-section").classList.contains("hidden"),
).toBe(false);
});
});
+1 -1
View File
@@ -53,7 +53,7 @@ function unwrapData() {
} }
function detail(data, label, sources) { function detail(data, label, sources) {
const decoded = uniswap.decode(data, ROUTER, sources || {}); const decoded = uniswap.decode(data, ROUTER, "0x1", sources || {});
return decoded.details.find((d) => d.label === label); return decoded.details.find((d) => d.label === label);
} }
+10 -4
View File
@@ -79,7 +79,7 @@ function balanceCheck(token, minBalance) {
} }
function detail(data, label) { function detail(data, label) {
const decoded = uniswap.decode(data, ROUTER, {}); const decoded = uniswap.decode(data, ROUTER, "0x1", {});
expect(decoded).not.toBeNull(); expect(decoded).not.toBeNull();
return decoded.details.find((d) => d.label === label); return decoded.details.find((d) => d.label === label);
} }
@@ -113,7 +113,9 @@ describe("an execute() whose input currency never decoded", () => {
}); });
test("does not name ETH in the swap title either", () => { test("does not name ETH in the swap title either", () => {
expect(uniswap.decode(data(), ROUTER, {}).name).toBe("Uniswap Swap"); expect(uniswap.decode(data(), ROUTER, "0x1", {}).name).toBe(
"Uniswap Swap",
);
}); });
}); });
@@ -123,7 +125,9 @@ describe("an execute() that names only an output token", () => {
test("refuses the input rather than defaulting it to ETH", () => { test("refuses the input rather than defaulting it to ETH", () => {
expect(detail(data(), "Token In").value).toBe(REFUSAL); expect(detail(data(), "Token In").value).toBe(REFUSAL);
expect(uniswap.decode(data(), ROUTER, {}).name).toBe("Uniswap Swap"); expect(uniswap.decode(data(), ROUTER, "0x1", {}).name).toBe(
"Uniswap Swap",
);
}); });
test("still states the output it did establish", () => { test("still states the output it did establish", () => {
@@ -170,7 +174,9 @@ describe("native ETH in, which V4 spells as the zero address", () => {
expect(detail(data, "Token In").value).toBe("ETH (native)"); expect(detail(data, "Token In").value).toBe("ETH (native)");
expect(detail(data, "Amount").value).toBe("1.0000 ETH"); expect(detail(data, "Amount").value).toBe("1.0000 ETH");
expect(uniswap.decode(data, ROUTER, {}).name).toBe("Swap ETH → USDT"); expect(uniswap.decode(data, ROUTER, "0x1", {}).name).toBe(
"Swap ETH → USDT",
);
}); });
test("WRAP_ETH is still ETH at 18 decimals", () => { test("WRAP_ETH is still ETH at 18 decimals", () => {
+5 -3
View File
@@ -105,7 +105,7 @@ function v3ExactIn(tokenIn, tokenOut, amountIn, amountOutMin) {
} }
function detail(data, label) { function detail(data, label) {
const decoded = uniswap.decode(data, ROUTER, {}); const decoded = uniswap.decode(data, ROUTER, "0x1", {});
expect(decoded).not.toBeNull(); expect(decoded).not.toBeNull();
return decoded.details.find((d) => d.label === label); return decoded.details.find((d) => d.label === label);
} }
@@ -140,7 +140,7 @@ describe("a V4 step that states a Min. received but no output currency", () => {
}); });
test("does not name ETH in the swap title either", () => { test("does not name ETH in the swap title either", () => {
const decoded = uniswap.decode(data(), ROUTER, {}); const decoded = uniswap.decode(data(), ROUTER, "0x1", {});
expect(decoded.name).toBe("Uniswap Swap"); expect(decoded.name).toBe("Uniswap Swap");
}); });
}); });
@@ -206,6 +206,8 @@ describe("native ETH out, which V4 spells as the zero address", () => {
test("is still named ETH and still scaled at 18 decimals", () => { test("is still named ETH and still scaled at 18 decimals", () => {
expect(detail(data(), "Token Out").value).toBe("ETH"); expect(detail(data(), "Token Out").value).toBe("ETH");
expect(detail(data(), "Min. received").value).toBe("0.5000 ETH"); expect(detail(data(), "Min. received").value).toBe("0.5000 ETH");
expect(uniswap.decode(data(), ROUTER, {}).name).toBe("Swap USDT → ETH"); expect(uniswap.decode(data(), ROUTER, "0x1", {}).name).toBe(
"Swap USDT → ETH",
);
}); });
}); });
+1 -1
View File
@@ -82,7 +82,7 @@ function walletsHolding(token, decimals) {
// decodeCalldata() rather than uniswap.decode() directly, because the scale // decodeCalldata() rather than uniswap.decode() directly, because the scale
// sources the screen supplies are part of what is under test. // sources the screen supplies are part of what is under test.
function swapDetail(data, label) { function swapDetail(data, label) {
const decoded = decodeCalldata(data, ROUTER); const decoded = decodeCalldata(data, ROUTER, "0x1");
return decoded.details.find((d) => d.label === label); return decoded.details.find((d) => d.label === label);
} }