Author SHA1 Message Date
sneak fe1e84b071 fix: a confirmation screen drawn again keeps the network it was reviewed for (closes #586)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
ConfirmTx set the transaction's chain id to the active network's every
time it drew the screen, so a popup reopened onto a saved confirmation
after a network switch in Settings described the reviewed transfer as
the new network's and sent it there.

show() now sets the chain id only on a transaction Send hands over. One
drawn again from the saved screen keeps its own, so Sign & Send refuses
it while another network is active. The screen labels its amounts in
that network's native currency and asks that network's RPC endpoint for
the fee and the recipient checks.

Model: opus-5-5
2026-10-19 02:55:38 +00: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
14 changed files with 589 additions and 99 deletions
+22 -12
View File
@@ -1013,10 +1013,12 @@ rendered differently on two screens.
The native token's label is a network's `nativeCurrency` in
`src/shared/networks.js`: `ETH` on mainnet, `SepoliaETH` on Sepolia. The
wallet's balances and the Send and confirmation screens, which send on the
active network, use the active network's. A transaction's figures use the one of
the network its chain id names, whichever network is active: the value and fee
on the approval screen, the amount on the wait, success and error screens, the
wallet's balances and the Send screen, which sends on the active network, use
the active network's. The confirmation screen uses the one of the network its
transaction was reviewed on, the network active when Send showed it, even after
the network is switched in Settings. A transaction's figures use the one of the
network its chain id names, whichever network is active: the value and fee on
the approval screen, the amount on the wait, success and error screens, the
transaction history and the transaction detail screen, and the refusal of a fee
above 1 ETH. A chain id that names no network reads `ETH`. Wherever this
document shows ETH as the label of a native balance, value or fee, in a "Native
@@ -1781,12 +1783,16 @@ view would leave a wallet one click from deletion.
the contract's own answer is read at signing time only to be compared with
it, and a disagreement is a refusal rather than a preference for either
value (`src/shared/transferAmount.js`)
- "Sign & Send" after the active network was switched in Settings while this
screen was open → nothing is signed; "The active network changed after
- "Sign & Send" after the active network was switched in Settings since this
transaction was reviewed, with this screen open or before the popup
reopened onto it → nothing is signed; "The active network changed after
this transaction was reviewed, so it was not sent. Please go back and
review it again." in the errors box, no screen change. The transaction
carries the chain id of the network the screen was shown for, so it cannot
be signed for another
carries the chain id of the network active when Send showed this screen,
and keeps it when the screen is drawn again from the saved transaction, so
it cannot be signed for another. The screen describes it on that network:
its amounts are in that network's native currency, and the fee estimate
and the recipient checks ask that network's RPC endpoint
- "Sign & Send" (wrong password) → "Wrong password." on the password error
line, no screen change
- "Sign & Send" signs as the From address above, not as whichever address is
@@ -2880,10 +2886,14 @@ Currently supported:
or recipient, and amount.
- **Uniswap Universal Router**: `execute()` calls — shows swap direction (e.g.
"Swap USDT → ETH"), token addresses, amounts, execution steps, and deadline.
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
The swap summary is shown only for a transaction sent to one of the Universal
Router addresses Uniswap publishes for the network: on TxApproval the network
the transaction is for, on TransactionDetail the active network. The same
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)`.
### Non-Goals Forever
+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.
function decodeCalldata(data, toAddress) {
function decodeCalldata(data, toAddress, chainId) {
if (!data || data === "0x" || data.length < 10) return null;
// 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
const routerResult = uniswap.decode(data, toAddress, decimalsSources);
const routerResult = uniswap.decode(
data,
toAddress,
chainId,
decimalsSources,
);
if (routerResult) return routerResult;
return null;
@@ -269,8 +275,14 @@ function showTxApproval(details) {
chainId: approvedTx.chainId,
};
// If this is an ERC-20 call, try to extract the real recipient and amount
const decoded = decodeCalldata(approvedTx.data, toAddr || "");
// If this is an ERC-20 call, try to extract the real recipient and amount.
// 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) {
// 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
+46 -19
View File
@@ -11,7 +11,6 @@ const {
addressTitle,
escapeHtml,
displaySymbol,
nativeCurrency,
renderAddressHtml,
blockieHtml,
attachCopyHandlers,
@@ -20,6 +19,10 @@ const {
formatFee,
} = require("./helpers");
const { state, currentNetwork } = require("../../shared/state");
const {
networkByChainId,
nativeCurrencyByChainId,
} = require("../../shared/networks");
const { getSignerForAddress } = require("../../shared/wallet");
const { walletDefect } = require("../../shared/walletDefects");
const { decryptWithPassword } = require("../../shared/vault");
@@ -87,7 +90,9 @@ function valueWithUsd(text, usdAmount) {
// once its amount is re-derived from the fee estimate.
function renderAmount(txInfo) {
const isErc20 = txInfo.token !== "ETH";
const rawSymbol = isErc20 ? txInfo.tokenSymbol || "?" : nativeCurrency();
const rawSymbol = isErc20
? txInfo.tokenSymbol || "?"
: nativeCurrencyByChainId(txInfo.chainId);
const price = isErc20 ? getPrice(rawSymbol) : getPrice("ETH");
const amountUsd = price ? parseFloat(txInfo.amount) * price : null;
$("confirm-amount").textContent = valueWithUsd(
@@ -101,16 +106,21 @@ function show(txInfo) {
feeStatus = FEE_PENDING;
feeWei = null;
maxSendFees = null;
// The network this screen is drawn for, and the only one it sends on:
// Settings can switch the active network while the screen is open. The
// wait, success and error screens label the amount by it too.
txInfo.chainId = currentNetwork().chainId;
// The network this screen is drawn for, and the only one it sends on: the
// one active when Send handed the transaction over. Drawn again from the
// saved transaction, as when the popup reopens onto this screen, it keeps
// that network, so Sign & Send refuses once Settings has switched to
// another. This screen and the wait, success and error screens label
// amounts by it.
if (!txInfo.chainId) txInfo.chainId = currentNetwork().chainId;
const isErc20 = txInfo.token !== "ETH";
// The raw symbol is the price-table key; the capped one is what the
// screen says. Truncating before the lookup would silently drop the
// price of any token whose symbol is long enough to be capped.
const rawSymbol = isErc20 ? txInfo.tokenSymbol || "?" : nativeCurrency();
const rawSymbol = isErc20
? txInfo.tokenSymbol || "?"
: nativeCurrencyByChainId(txInfo.chainId);
const symbol = displaySymbol(rawSymbol);
// Transaction type
@@ -212,9 +222,9 @@ function show(txInfo) {
$("confirm-gas-error").classList.toggle("hidden", !isErc20);
$("confirm-gas-error").textContent =
"You do not have enough " +
nativeCurrency() +
nativeCurrencyByChainId(txInfo.chainId) +
" to pay the network fee for this transfer. Please add " +
nativeCurrency() +
nativeCurrencyByChainId(txInfo.chainId) +
" to this address and try again.";
// Both shown later, by checkRecipientHistory(). The first sentence says
@@ -222,12 +232,12 @@ function show(txInfo) {
$("confirm-recipient-warning").textContent =
"WARNING: The recipient address has never sent a transaction and" +
" holds no " +
nativeCurrency() +
nativeCurrencyByChainId(txInfo.chainId) +
". This may indicate a fresh or unused address. Double-check the" +
" address before sending.";
$("confirm-contract-warning").textContent =
"WARNING: The recipient is a smart contract. Sending " +
nativeCurrency() +
nativeCurrencyByChainId(txInfo.chainId) +
" or tokens directly to a contract may result in permanent loss of" +
" funds.";
@@ -280,7 +290,7 @@ function renderValidation(txInfo) {
const isErc20 = txInfo.token !== "ETH";
const symbol = isErc20
? displaySymbol(txInfo.tokenSymbol || "?")
: nativeCurrency();
: nativeCurrencyByChainId(txInfo.chainId);
const { canSend, codes } = validateTransfer({
isErc20,
@@ -369,9 +379,23 @@ function setVisible(id, visible) {
$(id).style.visibility = visible ? "visible" : "hidden";
}
// The node of the network this transaction was reviewed for, which need not be
// the active one: the popup can reopen onto this screen after Settings switched
// the network. Its RPC URL is the one Settings has for that network: the
// active network's is state.rpcUrl, any other's the one remembered for it (see
// applyChainSwitchFields()).
function reviewedNetworkProvider(txInfo) {
const net = networkByChainId(txInfo.chainId);
if (net.id === state.networkId) {
return getProvider(state.rpcUrl, net.id);
}
const remembered = state.networkEndpoints[net.id] || {};
return getProvider(remembered.rpcUrl || net.defaultRpcUrl, net.id);
}
async function estimateGas(txInfo) {
try {
const provider = getProvider(state.rpcUrl, state.networkId);
const provider = reviewedNetworkProvider(txInfo);
const feeData = await provider.getFeeData();
let gasLimit;
@@ -419,12 +443,13 @@ async function estimateGas(txInfo) {
// does, so the same fee reads the same on both.
if (estimateWei !== null && estimateWei < gasCostWei) {
$("confirm-fee-amount").textContent =
"~" + formatFee(estimateWei, nativeCurrency());
"~" +
formatFee(estimateWei, nativeCurrencyByChainId(txInfo.chainId));
$("confirm-fee-reserve").textContent =
"up to " +
truncateAmountNeverZero(formatEther(gasCostWei)) +
" " +
nativeCurrency() +
nativeCurrencyByChainId(txInfo.chainId) +
" reserved";
setVisible("confirm-fee-reserve", true);
} else {
@@ -433,7 +458,7 @@ async function estimateGas(txInfo) {
// cost is not below the reserve. Show the reserve alone.
$("confirm-fee-amount").textContent = formatFee(
gasCostWei,
nativeCurrency(),
nativeCurrencyByChainId(txInfo.chainId),
);
setVisible("confirm-fee-reserve", false);
}
@@ -523,7 +548,7 @@ function showSendError(message) {
async function checkRecipientHistory(txInfo) {
try {
const provider = getProvider(state.rpcUrl, state.networkId);
const provider = reviewedNetworkProvider(txInfo);
const asyncWarnings = await getFullWarnings(txInfo.to, provider, {
fromAddress: txInfo.from,
});
@@ -564,8 +589,10 @@ function init(_ctx) {
return;
}
// Back from Settings only unhides this screen, still drawn for the
// network that was active when it was shown.
// The screen describes the network the transaction was reviewed for,
// which Settings may have switched away from since: with this screen
// open, where Back only unhides it, or before the popup reopened onto
// it.
if (currentNetwork().chainId !== pendingTx.chainId) {
showSendError(
"The active network changed after this transaction was" +
+7 -2
View File
@@ -21,7 +21,7 @@ const {
displaySymbol,
goBack,
} = require("./helpers");
const { state } = require("../../shared/state");
const { state, currentNetwork } = require("../../shared/state");
const { nativeCurrencyByChainId } = require("../../shared/networks");
const { formatEther, formatUnits } = require("ethers");
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
// explorer reports it, not the history entry's `to`: for a token
// 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) {
// Render decoded calldata matching approval view style
actionEl.textContent = decoded.name;
+45 -4
View File
@@ -8,6 +8,7 @@ const {
resolveTokenSymbol,
unknownDecimalsAmount,
} = require("./approvalAmount");
const { NETWORKS } = require("./networks");
const coder = AbiCoder.defaultAbiCoder();
@@ -15,6 +16,41 @@ const ROUTER_IFACE = new Interface([
"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)
const COMMAND_NAMES = {
0x00: "V3 Swap (Exact In)",
@@ -120,7 +156,7 @@ const NO_MINIMUM = "None (no minimum guaranteed)";
const MAX_UINT160 = BigInt("0xffffffffffffffffffffffffffffffffffffffff");
// 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 = [
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"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
// the approval UI, or null if the calldata is not a recognised execute().
function decode(data, toAddress, sources) {
// the approval UI, or null if the calldata is not a recognised execute() or
// `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 {
const parsed = ROUTER_IFACE.parseTransaction({ data });
if (!parsed) return null;
+5 -1
View File
@@ -68,7 +68,11 @@ function swapData(amountIn, amountOutMin) {
}
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);
}
+84 -2
View File
@@ -7,25 +7,36 @@
// reviewed on Sepolia went out as 1 ETH on mainnet once mainnet was chosen in
// Settings in between.
//
// The same holds when the popup was closed after the switch and reopened, and
// Back drew the screen again from the transaction it saved
// (https://git.eeqj.de/sneak/AutistMask/issues/586): the screen was drawn for
// the new network, and Sign & Send sent the reviewed transfer there.
//
// Driven through the real confirmation screen, signer and chain switch, with
// only the node and the DOM stubbed. The stub node is on the network its
// provider was built for, so a broadcast records the network it went to.
"use strict";
// The signed transactions the stub node was handed, with its network's chain id.
const mockNode = { broadcast: [] };
// The nodes asked for the fee, by network name and RPC URL, in order. And the
// signed transactions the stub node was handed, with its network's chain id.
const mockNode = { feeAsked: [], broadcast: [] };
jest.mock("ethers", () => {
const actual = jest.requireActual("ethers");
class StubProvider {
constructor(url, network) {
this.url = url;
this.network = network;
}
async getNetwork() {
return this.network;
}
async getFeeData() {
mockNode.feeAsked.push({
network: this.network.name,
url: this.url,
});
return {
maxFeePerGas: 20000000000n,
maxPriorityFeePerGas: 1000000000n,
@@ -128,6 +139,16 @@ const { encryptWithPassword } = require("../src/shared/vault");
const { state, loadState } = require("../src/shared/state");
const { onChainSwitch } = require("../src/shared/chainSwitch");
const { NETWORKS } = require("../src/shared/networks");
const {
goBack,
pushCurrentView,
setBackRenderer,
showView,
} = require("../src/popup/views/helpers");
const {
makeBackRenderer,
resetRenderedViews,
} = require("../src/popup/viewRouter");
const txStatus = require("../src/popup/views/txStatus");
const confirmTx = require("../src/popup/views/confirmTx");
@@ -150,6 +171,7 @@ beforeAll(async () => {
beforeEach(() => {
elements.clear();
mockNode.feeAsked = [];
mockNode.broadcast = [];
txStatus.showWait.mockClear();
txStatus.showError.mockClear();
@@ -187,6 +209,12 @@ async function review(networkId) {
tokenDecimals: null,
max: false,
});
await settle();
}
// show() estimates the fee and checks the recipient without awaiting either;
// this lets both finish.
async function settle() {
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
}
@@ -238,6 +266,60 @@ describe.each([
},
);
describe.each([
["sepolia", "mainnet", "SepoliaETH"],
["mainnet", "sepolia", "ETH"],
])(
"a transfer reviewed on %s, with %s chosen in Settings, the popup reopened and Back pressed",
(reviewedOn, switchedTo, symbol) => {
test("is described on the network it was reviewed for, and neither signed nor sent", async () => {
await review(reviewedOn);
// The user's own node for that network, set in Settings.
const ownNode = "https://" + reviewedOn + ".node.example.invalid";
state.rpcUrl = ownNode;
// The Settings gear, then its network select.
pushCurrentView();
showView("settings");
await onChainSwitch(switchedTo);
// The reopened popup loads what was saved onto a blank page, and
// Back draws the confirmation screen again from it.
await loadState();
elements.clear();
resetRenderedViews();
setBackRenderer(makeBackRenderer(state, { confirmTx }));
confirmTx.init({});
mockNode.feeAsked = [];
goBack();
await settle();
expect(state.currentView).toBe("confirm-tx");
expect(el("confirm-type").textContent).toBe(
"Native " + symbol + " transfer",
);
expect(el("confirm-balance").textContent).toBe("5.0000 " + symbol);
expect(el("confirm-fee-amount").textContent).toBe(
"~0.0002 " + symbol,
);
expect(mockNode.feeAsked).toEqual([
{ network: reviewedOn, url: ownNode },
]);
expect(el("btn-confirm-send").disabled).toBe(false);
await signAndSend();
expect(signTransaction).not.toHaveBeenCalled();
expect(mockNode.broadcast).toEqual([]);
expect(txStatus.showWait).not.toHaveBeenCalled();
expect(txStatus.showError).not.toHaveBeenCalled();
expect(el("confirm-errors").innerHTML).toContain(
"The active network changed after this transaction was" +
" reviewed, so it was not sent.",
);
});
},
);
// The check in the click handler tells the user; the send itself carries the
// chain id the screen was drawn for, so a signer on another network's node
// cannot sign it even if the network changes after that check.
+1 -1
View File
@@ -136,7 +136,7 @@ function erc20AmountLine(data, tokenAddress) {
}
function swapAmountLine(data) {
return decodeCalldata(data, ROUTER).details.find(
return decodeCalldata(data, ROUTER, "0x1").details.find(
(d) => d.label === "Amount",
).value;
}
+47 -44
View File
@@ -1,6 +1,7 @@
const { AbiCoder, Interface, solidityPacked } = require("ethers");
const uniswap = require("../src/shared/uniswap");
// A Universal Router on Ethereum Mainnet, chain id 0x1.
const ROUTER_ADDR = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
@@ -315,13 +316,13 @@ function detail(result, label) {
describe("uniswap decoder", () => {
test("returns null for non-execute calldata", () => {
expect(uniswap.decode("0x", ROUTER_ADDR)).toBeNull();
expect(uniswap.decode("0xdeadbeef", ROUTER_ADDR)).toBeNull();
expect(uniswap.decode(null, ROUTER_ADDR)).toBeNull();
expect(uniswap.decode("0x", ROUTER_ADDR, "0x1")).toBeNull();
expect(uniswap.decode("0xdeadbeef", ROUTER_ADDR, "0x1")).toBeNull();
expect(uniswap.decode(null, ROUTER_ADDR, "0x1")).toBeNull();
});
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.name).toBe("Swap USDT \u2192 ETH");
@@ -361,6 +362,7 @@ describe("uniswap decoder", () => {
const result = uniswap.decode(
v4SwapCalldata(id, deployed),
ROUTER_ADDR,
"0x1",
);
expect(detail(result, "Steps").value).toBe("V4 Swap");
},
@@ -376,6 +378,7 @@ describe("uniswap decoder", () => {
const result = uniswap.decode(
v4SwapCalldata(id, newer),
ROUTER_ADDR,
"0x1",
);
expect(detail(result, "Amount")).toBeUndefined();
expect(detail(result, "Min. received")).toBeUndefined();
@@ -406,7 +409,7 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)",
);
@@ -427,7 +430,7 @@ describe("uniswap decoder", () => {
[encodeV4Swap(new Uint8Array([id]), [newer])],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)",
);
@@ -457,7 +460,7 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Amount").value).toBe("2.0000 USDT");
expect(detail(result, "Token Out").value).toBe(
@@ -485,7 +488,7 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Token In").value).toBe(
"Unknown (could not be decoded)",
@@ -507,7 +510,7 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Token Out").value).toBe(
"Unknown (could not be decoded)",
);
@@ -527,7 +530,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(result.name).toBe("Swap USDT \u2192 WETH");
@@ -554,7 +557,7 @@ describe("uniswap decoder", () => {
1767225600n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap USDC → WETH");
expect(detail(result, "Token In").address).toBe(USDC_ADDR);
expect(detail(result, "Token Out").address).toBe(WETH_ADDR);
@@ -586,7 +589,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap ETH → USDC");
const amount = detail(result, "Amount");
@@ -615,7 +618,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap USDC → ETH");
expect(detail(result, "Amount").value).toBe("Up to 1500.0000 USDC");
expect(detail(result, "Token Out").value).toBe("ETH");
@@ -658,7 +661,7 @@ describe("uniswap decoder", () => {
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, "Min. received").value).toBe(minReceived);
},
@@ -679,7 +682,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
expect(detail(result, "Min. received").value).toBe("1.9000 USDC");
@@ -712,7 +715,7 @@ describe("uniswap decoder", () => {
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, "Min. received").value).toBe("0.4000 ETH");
});
@@ -731,7 +734,7 @@ describe("uniswap decoder", () => {
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, "Min. received").value).toBe("0.0005 ETH");
});
@@ -756,7 +759,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Token Out").value).toBe(
"Unknown (not named in the calldata)",
);
@@ -779,7 +782,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(result.name).toBe("Swap USDT \u2192 WETH");
});
@@ -791,7 +794,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
const tokenIn = result.details.find((d) => d.label === "Token In");
@@ -814,7 +817,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
// UNWRAP_WETH means output is native ETH
expect(result.name).toBe("Swap USDT \u2192 ETH");
@@ -830,7 +833,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
const minOut = result.details.find((d) => d.label === "Min. received");
@@ -850,7 +853,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
// 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", () => {
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");
});
@@ -880,7 +883,7 @@ describe("uniswap decoder", () => {
2n ** 256n - 1n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(result.name).toBe("Swap USDT \u2192 WETH");
expect(detail(result, "Deadline").value).toBe(
@@ -895,7 +898,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
const amount = result.details.find((d) => d.label === "Amount");
@@ -970,7 +973,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
// Before decodeV4Swap fix: name would be "Swap USDT → ETH" or "Uniswap Swap"
// After fix: correctly identifies both tokens from V4 sub-actions
@@ -1004,7 +1007,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
// The amount and the token it is counted in come from the same hop.
@@ -1044,7 +1047,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toBe(
@@ -1089,7 +1092,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDC");
@@ -1118,7 +1121,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDC");
@@ -1155,7 +1158,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(detail(result, "Token In").value).toContain("WETH");
@@ -1348,7 +1351,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
@@ -1400,7 +1403,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Steps").value).toBe(
"V4 Swap (could not be decoded)",
);
@@ -1459,7 +1462,7 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Token In").address).toBe(
USDT_ADDR.toLowerCase(),
@@ -1515,7 +1518,7 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Uniswap Swap");
expect(detail(result, "Token In").value).toBe(
"Unknown (not named in the calldata)",
@@ -1538,7 +1541,7 @@ describe("uniswap decoder", () => {
],
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(detail(result, "Token Out").value).toBe(
"Unknown (not named in the calldata)",
);
@@ -1572,7 +1575,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result.name).toBe("Swap USDT → USDC");
expect(detail(result, "Token In").address).toBe(USDT_ADDR);
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
@@ -1596,7 +1599,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(detail(result, "Token In").value).toContain("USDT");
@@ -1627,7 +1630,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("WETH");
@@ -1648,7 +1651,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDC");
@@ -1667,7 +1670,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(detail(result, "Token Out").value).toContain("USDT");
@@ -1687,7 +1690,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
expect(result.name).toBe("Uniswap Swap");
@@ -1707,7 +1710,7 @@ describe("uniswap decoder", () => {
9999999999n,
);
const result = uniswap.decode(data, ROUTER_ADDR);
const result = uniswap.decode(data, ROUTER_ADDR, "0x1");
expect(result).not.toBeNull();
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) {
const decoded = uniswap.decode(data, ROUTER, sources || {});
const decoded = uniswap.decode(data, ROUTER, "0x1", sources || {});
return decoded.details.find((d) => d.label === label);
}
+10 -4
View File
@@ -79,7 +79,7 @@ function balanceCheck(token, minBalance) {
}
function detail(data, label) {
const decoded = uniswap.decode(data, ROUTER, {});
const decoded = uniswap.decode(data, ROUTER, "0x1", {});
expect(decoded).not.toBeNull();
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", () => {
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", () => {
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", () => {
@@ -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, "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", () => {
+5 -3
View File
@@ -105,7 +105,7 @@ function v3ExactIn(tokenIn, tokenOut, amountIn, amountOutMin) {
}
function detail(data, label) {
const decoded = uniswap.decode(data, ROUTER, {});
const decoded = uniswap.decode(data, ROUTER, "0x1", {});
expect(decoded).not.toBeNull();
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", () => {
const decoded = uniswap.decode(data(), ROUTER, {});
const decoded = uniswap.decode(data(), ROUTER, "0x1", {});
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", () => {
expect(detail(data(), "Token Out").value).toBe("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
// sources the screen supplies are part of what is under test.
function swapDetail(data, label) {
const decoded = decodeCalldata(data, ROUTER);
const decoded = decodeCalldata(data, ROUTER, "0x1");
return decoded.details.find((d) => d.label === label);
}