Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8741f491e9 |
+3
-3
@@ -9,9 +9,9 @@ WORKDIR /app
|
||||
ENV AUTISTMASK_LINT_NATIVE=1
|
||||
|
||||
# script/test's default 30s bound is the host figure, against a suite that
|
||||
# takes 23-29s there with three jest workers. In here the same suite starts on
|
||||
# a cold jest cache and shares the runner with the rest of the build, so 30s
|
||||
# is too tight — it killed a healthy suite at 30.6s on a cold CI cache. 180s
|
||||
# runs in about 8s there. In here the same suite starts on a cold jest cache
|
||||
# and shares the runner with the rest of the build, so 30s is marginal rather
|
||||
# than a bound — it killed a healthy suite at 30.6s on a cold CI cache. 180s
|
||||
# still catches a hang in three minutes and cannot be tripped by a suite that
|
||||
# is merely running on contended hardware.
|
||||
ENV AUTISTMASK_TEST_TIMEOUT=180
|
||||
|
||||
@@ -969,25 +969,16 @@ read:
|
||||
- `Unlimited`: on the ERC-20 `Amount` line, an `approve` of the `uint256`
|
||||
maximum, an unbounded allowance. On the swap's `Amount` line, any amount at or
|
||||
above the `uint160` maximum, whichever step set the line: a `PERMIT2_PERMIT`
|
||||
amount at that maximum, which is an unbounded permit, or a V2 or V3 exact-in,
|
||||
V2 exact-out or `WRAP_ETH` amount that large, which is not an allowance. The
|
||||
router's whole-balance value, `CONTRACT_BALANCE` (`2^255`), is one such
|
||||
amount.
|
||||
- `Up to <amount>`: the swap's `Amount` line, when the transaction has a V2
|
||||
exact-out step, whichever step set the line, including the `WRAP_ETH` of a
|
||||
swap paid in ETH and a `PERMIT2_PERMIT`. The swap spends at most that figure,
|
||||
not necessarily all of it; the wait, success and error screens show it with
|
||||
the same words. `Unlimited` and `All available (V4 open delta)` keep their
|
||||
wording. When a V2 exact-out step sets `Min. received`, that line shows its
|
||||
`amountOut`, the exact amount it buys.
|
||||
amount at that maximum, which is an unbounded permit, or a V2 or V3 exact-in
|
||||
or `WRAP_ETH` amount that large, which is not an allowance. The router's
|
||||
whole-balance value, `CONTRACT_BALANCE` (`2^255`), is one such amount.
|
||||
- `All available (V4 open delta)`: the swap's `Amount` line, when the amount it
|
||||
shows is a V4 exact-in `amountIn` of zero. V4 reads that zero as "use the
|
||||
whole open delta", so the calldata states no quantity. The line shows the
|
||||
amount of one step that names an input token or amount: the last
|
||||
`PERMIT2_PERMIT` step if there is one, otherwise the first V2 or V3 exact-in,
|
||||
V2 exact-out, `WRAP_ETH` or V4 swap step. A V2 exact-out step gives its
|
||||
`amountInMax`, and a V4 swap step the `amountIn` of its first readable
|
||||
exact-in action.
|
||||
`WRAP_ETH` or V4 swap step, a V4 swap step giving the `amountIn` of its first
|
||||
readable exact-in action.
|
||||
- `None (no minimum guaranteed)`: the swap's `Min. received` line, when the
|
||||
minimum it shows is zero, whether a V2, V3 or V4 swap's minimum or a
|
||||
`BALANCE_CHECK_ERC20` step's `minBalance`. Before
|
||||
@@ -996,14 +987,9 @@ read:
|
||||
|
||||
The swap's `Token In` and `Token Out` lines name a currency, not an amount; each
|
||||
reads `Unknown (not named in the calldata)` when the decoder found no token for
|
||||
that side. An `UNWRAP_WETH` step makes `Token Out` ETH only when the output side
|
||||
is WETH, on mainnet or Sepolia, or when no step set the output side; a WETH
|
||||
`Min. received` figure then reads in ETH. Otherwise `Token Out` and
|
||||
`Min. received` keep the output side's own token and figure, whether the swap
|
||||
was paid in ETH or in a token: a V2 exact-out swap that buys USDC and then
|
||||
unwraps the WETH it did not spend shows USDC. The token permission warning on
|
||||
the signature screen has its own amount wording, including `Unknown`; the
|
||||
SignApproval section below describes it.
|
||||
that side. The token permission warning on the signature screen has its own
|
||||
amount wording, including `Unknown`; the SignApproval section below describes
|
||||
it.
|
||||
|
||||
#### Partial USD totals
|
||||
|
||||
|
||||
@@ -57,42 +57,6 @@ but the review is broader than any of them.
|
||||
of asking the user to go back and try again, which cannot help. For any other
|
||||
cause it is unchanged.
|
||||
|
||||
- 2026-10-04: A Uniswap V2 exact-out swap (Universal Router command `0x09`) is
|
||||
decoded on the approval screen
|
||||
([#283](https://git.eeqj.de/sneak/AutistMask/issues/283)). `decode()` in
|
||||
`src/shared/uniswap.js` had no arm for it, so the screen named the step and
|
||||
showed no token or amount. The input side is the path's first token with
|
||||
`amountInMax`, the output side the last token with `amountOut`. When the
|
||||
transaction has such a step, the `Amount` figure reads `Up to <amount>`,
|
||||
whichever step set it, there and on the wait, success and error screens,
|
||||
except where it reads `Unlimited` (an unbounded `PERMIT2_PERMIT`, or any
|
||||
amount at or above the `uint160` maximum) or `All available (V4 open delta)`.
|
||||
In every swap, `UNWRAP_WETH` makes `Token Out` ETH only when the output side
|
||||
is WETH, on mainnet or Sepolia, or when no step set the output side; otherwise
|
||||
`Token Out` and `Min. received` keep the output side's own token and figure.
|
||||
V3 exact-out (`0x01`) is still not decoded.
|
||||
|
||||
- 2026-10-04: `make test` runs jest in three worker processes
|
||||
([#426](https://git.eeqj.de/sneak/AutistMask/issues/426)). The `test` and
|
||||
`test:verbose` scripts in `package.json` ran `jest --forceExit`, which starts
|
||||
one worker per CPU core: about 47 processes and 7-8 GiB per run on the shared
|
||||
48-core build host. They now pass `--maxWorkers=3`, and the suite takes 23-29s
|
||||
there: inside the 30-second cap in `script/test`, which is unchanged, but not
|
||||
by much, because `tests/persistedFieldContract.test.js` alone takes most of it
|
||||
([#428](https://git.eeqj.de/sneak/AutistMask/issues/428)). One or two workers
|
||||
went past the cap. `make check`, the pre-commit hook and `script/cibuild` all
|
||||
run the suite through these scripts.
|
||||
|
||||
- 2026-10-04: The error container on each dApp approval screen keeps its height
|
||||
when an error appears
|
||||
([#297](https://git.eeqj.de/sneak/AutistMask/issues/297)). `#approve-tx-error`
|
||||
and `#approve-sign-error` reserved 20px, but their border and padding took
|
||||
10px of it, so a one-line error grew them to 26px and pushed the buttons below
|
||||
down 6px. They now reserve 30px. A new test in `tests/e2e/run.js` shows each
|
||||
of the six password error containers on its own screen, empty and then with an
|
||||
error, and fails if one changes height or the element below it moves. Some of
|
||||
the longer messages these two containers can show still take two lines.
|
||||
|
||||
- 2026-10-04: A transaction with no `to` says "This transaction creates a new
|
||||
contract. It has no recipient." on its recipient line and in its transaction
|
||||
history row ([#250](https://git.eeqj.de/sneak/AutistMask/issues/250)). The
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "jest --forceExit --maxWorkers=3",
|
||||
"test:verbose": "jest --forceExit --maxWorkers=3 --verbose",
|
||||
"test": "jest --forceExit",
|
||||
"test:verbose": "jest --forceExit --verbose",
|
||||
"build": "node build.js",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fmt": "prettier --write .",
|
||||
|
||||
+7
-10
@@ -1,16 +1,13 @@
|
||||
#!/bin/sh
|
||||
# script/test: run the test suite.
|
||||
#
|
||||
# jest runs three worker processes (package.json), not one per CPU core: on a
|
||||
# many-core shared host one per core took gigabytes of RAM per run.
|
||||
#
|
||||
# The timeout bounds a hung suite; it is not a performance budget. On the busy
|
||||
# shared build host the suite takes 23-29s with three workers, so
|
||||
# REPO_POLICIES' 30s cap is tight there, not comfortable. Inside the image the
|
||||
# same suite also pays a cold jest cache and shares the runner with the rest of
|
||||
# the build, which is not what that budget describes, so the Dockerfile raises
|
||||
# the bound through AUTISTMASK_TEST_TIMEOUT. A cap a healthy suite can trip on a
|
||||
# cold cache produces a red that means nothing, and teaches "just run it again".
|
||||
# The timeout bounds a hung suite; it is not a performance budget. On a
|
||||
# developer host the suite finishes in about 8s and REPO_POLICIES' 30s cap is
|
||||
# the bound. Inside the image the same suite also pays a cold jest cache and
|
||||
# shares the runner with the rest of the build, which is not what that budget
|
||||
# describes, so the Dockerfile raises the bound through
|
||||
# AUTISTMASK_TEST_TIMEOUT. A cap a healthy suite can trip on a cold cache
|
||||
# produces a red that means nothing, and teaches "just run it again".
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
@@ -1631,7 +1631,7 @@
|
||||
</div>
|
||||
<div
|
||||
id="approve-tx-error"
|
||||
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem]"
|
||||
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem]"
|
||||
style="visibility: hidden"
|
||||
></div>
|
||||
<div class="flex justify-between">
|
||||
@@ -1708,7 +1708,7 @@
|
||||
</div>
|
||||
<div
|
||||
id="approve-sign-error"
|
||||
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem]"
|
||||
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.25rem]"
|
||||
style="visibility: hidden"
|
||||
></div>
|
||||
<div class="flex justify-between">
|
||||
|
||||
+8
-39
@@ -100,13 +100,6 @@ const NO_MINIMUM = "None (no minimum guaranteed)";
|
||||
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
|
||||
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.
|
||||
const WETH_ADDRESSES = [
|
||||
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
||||
"0xfff9976782d46cc05630d1f6ebab18b2324d6b14",
|
||||
];
|
||||
|
||||
// `decimals` is null when nothing knows this token's scale. It is not
|
||||
// defaulted to 18: the swap lines land on the same approval screen as the
|
||||
// ERC-20 line, and a scale guessed there is what showed a 1,000 USDT swap as
|
||||
@@ -205,6 +198,11 @@ function decodeV2SwapExactIn(input) {
|
||||
// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
|
||||
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
|
||||
// address[] path, bool payerIsUser)
|
||||
//
|
||||
// Nothing calls this: decode() has no 0x09 arm, so a V2 exact-out swap gets
|
||||
// its command name and no token or amount detail. Kept for the fix, which is
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/283.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function decodeV2SwapExactOut(input) {
|
||||
try {
|
||||
const d = coder.decode(
|
||||
@@ -449,7 +447,6 @@ function decode(data, toAddress, sources) {
|
||||
let outputToken = null;
|
||||
let minOutput = null;
|
||||
let hasUnwrapWeth = false;
|
||||
let hasV2ExactOut = false;
|
||||
const commandNames = [];
|
||||
|
||||
// THE INVARIANT: an amount and the token it is counted in always come
|
||||
@@ -524,16 +521,6 @@ function decode(data, toAddress, sources) {
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdId === 0x09) {
|
||||
// Buys exactly amountOut and spends at most amountInMax.
|
||||
hasV2ExactOut = true;
|
||||
const s = decodeV2SwapExactOut(inputs[i]);
|
||||
if (s) {
|
||||
setInputOnce(s.tokenIn, s.amountInMax);
|
||||
setOutput(s.tokenOut, s.amountOut);
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdId === 0x0b) {
|
||||
const w = decodeWrapEth(inputs[i]);
|
||||
if (w) {
|
||||
@@ -572,17 +559,10 @@ function decode(data, toAddress, sources) {
|
||||
|
||||
// Resolve token info. A null token on either side means the calldata
|
||||
// named no currency for it; tokenInfo() refuses rather than calling it
|
||||
// ETH. UNWRAP_WETH turns WETH into ETH, so it makes the output ETH
|
||||
// when the output side is WETH, or when no step set the output side.
|
||||
// Any other output keeps its own token and figure: a swap that buys
|
||||
// USDC and then unwraps the WETH it did not spend receives USDC.
|
||||
const outputIsWeth =
|
||||
present(outputToken) &&
|
||||
WETH_ADDRESSES.includes(outputToken.toLowerCase());
|
||||
const outputUnset = !present(outputToken) && !present(minOutput);
|
||||
// ETH. UNWRAP_WETH is the one output that is ETH without a currency to
|
||||
// decode, and it is answered here rather than left to that rule.
|
||||
const inInfo = tokenInfo(inputToken, sources);
|
||||
const outInfo =
|
||||
hasUnwrapWeth && (outputIsWeth || outputUnset)
|
||||
const outInfo = hasUnwrapWeth
|
||||
? { symbol: "ETH", decimals: 18, address: null }
|
||||
: tokenInfo(outputToken, sources);
|
||||
|
||||
@@ -633,17 +613,6 @@ function decode(data, toAddress, sources) {
|
||||
amount = { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
||||
} else if (inputAmount >= MAX_UINT160) {
|
||||
amount = { raw: "Unlimited", display: "Unlimited" };
|
||||
} else if (hasV2ExactOut) {
|
||||
// A V2 exact-out swap spends at most this figure, whichever
|
||||
// step set the line (its amountInMax, the WRAP_ETH of a swap
|
||||
// paid in ETH, a permit), so it is said to be a maximum, in
|
||||
// `raw` too: the wait, success and error screens show `raw` as
|
||||
// the transaction's amount.
|
||||
const most = amountText(inputAmount, inInfo);
|
||||
amount = {
|
||||
raw: "Up to " + most.raw,
|
||||
display: "Up to " + most.display,
|
||||
};
|
||||
} else {
|
||||
amount = amountText(inputAmount, inInfo);
|
||||
}
|
||||
|
||||
@@ -1449,81 +1449,6 @@ test("an over-long flash message keeps to one line (#252)", async (env) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --------------------------------------- password error containers (#297)
|
||||
|
||||
// Every screen that asks for a password reserves room for one line of error.
|
||||
// The two on the dApp approval screens also have a border and padding, which
|
||||
// that reserved height has to cover too.
|
||||
const PASSWORD_ERROR_CONTAINERS = [
|
||||
"approve-tx-error",
|
||||
"approve-sign-error",
|
||||
"export-privkey-flash",
|
||||
"show-phrase-flash",
|
||||
"delete-wallet-flash",
|
||||
"confirm-tx-password-error",
|
||||
];
|
||||
|
||||
// Shows only the screen holding the container, then measures the container
|
||||
// and the element below it empty and again filled the way showError() in
|
||||
// src/popup/views/helpers.js fills it. Runs in the page.
|
||||
function measurePasswordError(id) {
|
||||
const container = document.getElementById(id);
|
||||
const screen = container.closest(".view");
|
||||
for (const view of document.querySelectorAll(".view")) {
|
||||
view.classList.toggle("hidden", view !== screen);
|
||||
}
|
||||
const below = container.nextElementSibling;
|
||||
const measure = () => ({
|
||||
height: container.getBoundingClientRect().height,
|
||||
belowTop: below.getBoundingClientRect().top + window.scrollY,
|
||||
belowHeight: below.getBoundingClientRect().height,
|
||||
});
|
||||
const empty = measure();
|
||||
container.textContent = "Please enter your password.";
|
||||
container.style.visibility = "visible";
|
||||
const filled = measure();
|
||||
container.textContent = "";
|
||||
container.style.visibility = "hidden";
|
||||
return { empty, filled };
|
||||
}
|
||||
|
||||
test("a password error moves nothing on any screen (#297)", async (env) => {
|
||||
const page = await openPopup(env.ctx, env.popupUrl);
|
||||
try {
|
||||
await page.setViewportSize(POPUP_VIEWPORT);
|
||||
for (const id of PASSWORD_ERROR_CONTAINERS) {
|
||||
const { empty, filled } = await page.evaluate(
|
||||
measurePasswordError,
|
||||
id,
|
||||
);
|
||||
assert(
|
||||
empty.belowHeight > 0,
|
||||
"nothing is shown below #" + id + ", so nothing was measured",
|
||||
);
|
||||
assert(
|
||||
filled.height === empty.height,
|
||||
"#" +
|
||||
id +
|
||||
" is " +
|
||||
empty.height +
|
||||
"px empty and " +
|
||||
filled.height +
|
||||
"px with an error",
|
||||
);
|
||||
assert(
|
||||
filled.belowTop === empty.belowTop,
|
||||
"the element below #" +
|
||||
id +
|
||||
" moved " +
|
||||
(filled.belowTop - empty.belowTop) +
|
||||
"px when the error appeared",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
|
||||
// --------------------------------------------- confirmation screen (#238)
|
||||
//
|
||||
// The screen that decides what gets signed. The arithmetic underneath it
|
||||
|
||||
@@ -5,8 +5,6 @@ const ROUTER_ADDR = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
||||
const USDT_ADDR = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
||||
const WETH_ADDR = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
|
||||
const USDC_ADDR = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
||||
const DAI_ADDR = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
|
||||
const SEPOLIA_WETH_ADDR = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14";
|
||||
const USER_ADDR = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
||||
|
||||
// AutistMask's first-ever swap, 2026-02-27.
|
||||
@@ -77,14 +75,6 @@ function encodeV2SwapExactIn(recipient, amountIn, amountOutMin, pathAddrs) {
|
||||
);
|
||||
}
|
||||
|
||||
// Helper: encode a V2_SWAP_EXACT_OUT input (command 0x09)
|
||||
function encodeV2SwapExactOut(recipient, amountOut, amountInMax, pathAddrs) {
|
||||
return coder.encode(
|
||||
["address", "uint256", "uint256", "address[]", "bool"],
|
||||
[recipient, amountOut, amountInMax, pathAddrs, true],
|
||||
);
|
||||
}
|
||||
|
||||
// Helper: encode a V3_SWAP_EXACT_IN input (command 0x00)
|
||||
function encodeV3SwapExactIn(recipient, amountIn, amountOutMin, pathTokens) {
|
||||
// V3 path: token(20) + fee(3) + token(20) ...
|
||||
@@ -233,233 +223,6 @@ describe("uniswap decoder", () => {
|
||||
expect(minOut.value).toContain("WETH");
|
||||
});
|
||||
|
||||
// Buy exactly 0.5 WETH for at most 1,500 USDC, paid by the user, sent to
|
||||
// the caller (the router's MSG_SENDER recipient, address(1)).
|
||||
test("decodes V2_SWAP_EXACT_OUT, stating the input amount as a maximum", () => {
|
||||
const data = buildExecute(
|
||||
"0x09", // V2_SWAP_EXACT_OUT
|
||||
[
|
||||
encodeV2SwapExactOut(
|
||||
"0x0000000000000000000000000000000000000001",
|
||||
500000000000000000n, // amountOut: 0.5 WETH
|
||||
1500000000n, // amountInMax: 1,500 USDC (6 decimals)
|
||||
[USDC_ADDR, WETH_ADDR],
|
||||
),
|
||||
],
|
||||
1767225600n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
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);
|
||||
|
||||
// The wait, success and error screens show rawValue as the amount, so
|
||||
// it says "Up to" as well.
|
||||
const amount = detail(result, "Amount");
|
||||
expect(amount.value).toBe("Up to 1500.0000 USDC");
|
||||
expect(amount.rawValue).toBe("Up to 1500.0000");
|
||||
|
||||
expect(detail(result, "Min. received").value).toBe("0.5000 WETH");
|
||||
});
|
||||
|
||||
// Buy exactly 1,500 USDC for at most 0.5 ETH: WRAP_ETH of the maximum, the
|
||||
// swap, then UNWRAP_WETH of 0, which returns the ETH the swap did not spend.
|
||||
test("a V2 exact-out swap paid in ETH shows the token it buys and a maximum", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8", "uint8"], [0x0b, 0x09, 0x0c]),
|
||||
[
|
||||
encodeWrapEth(ROUTER_ADDR, 500000000000000000n),
|
||||
encodeV2SwapExactOut(
|
||||
USER_ADDR,
|
||||
1500000000n, // amountOut: 1,500 USDC
|
||||
500000000000000000n, // amountInMax: 0.5 WETH
|
||||
[WETH_ADDR, USDC_ADDR],
|
||||
),
|
||||
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH same encoding
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(result.name).toBe("Swap ETH → USDC");
|
||||
|
||||
const amount = detail(result, "Amount");
|
||||
expect(amount.value).toBe("Up to 0.5000 ETH");
|
||||
expect(amount.rawValue).toBe("Up to 0.5000");
|
||||
|
||||
expect(detail(result, "Token Out").address).toBe(USDC_ADDR);
|
||||
expect(detail(result, "Min. received").value).toBe("1500.0000 USDC");
|
||||
});
|
||||
|
||||
// Buy exactly 0.5 ETH for at most 1,500 USDC under a permit: the swap buys
|
||||
// WETH and UNWRAP_WETH turns it into ETH.
|
||||
test("a V2 exact-out swap that buys ETH shows ETH and the permit as a maximum", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x09, 0x0c]),
|
||||
[
|
||||
encodePermit2(USDC_ADDR, 1500000000n, ROUTER_ADDR),
|
||||
encodeV2SwapExactOut(
|
||||
ROUTER_ADDR,
|
||||
500000000000000000n, // amountOut: 0.5 WETH
|
||||
1500000000n, // amountInMax: 1,500 USDC
|
||||
[USDC_ADDR, WETH_ADDR],
|
||||
),
|
||||
encodeWrapEth(USER_ADDR, 500000000000000000n), // UNWRAP_WETH
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
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");
|
||||
expect(detail(result, "Min. received").value).toBe("0.5000 ETH");
|
||||
});
|
||||
|
||||
// Paid in a token, an UNWRAP_WETH of 0 after a swap that buys something
|
||||
// other than WETH leaves the output side as it is: Token Out and Min.
|
||||
// received are the token bought and its figure, not ETH.
|
||||
test.each([
|
||||
{
|
||||
paidIn: "WETH",
|
||||
tokenIn: WETH_ADDR,
|
||||
amountInMax: 500000000000000000n, // 0.5 WETH
|
||||
tokenOut: USDC_ADDR,
|
||||
amountOut: 1300000000n, // 1,300 USDC
|
||||
minReceived: "1300.0000 USDC",
|
||||
},
|
||||
{
|
||||
paidIn: "USDC",
|
||||
tokenIn: USDC_ADDR,
|
||||
amountInMax: 8000000n, // 8 USDC
|
||||
tokenOut: DAI_ADDR,
|
||||
amountOut: 7000000000000000000n, // 7 DAI
|
||||
minReceived: "7.0000 DAI",
|
||||
},
|
||||
])(
|
||||
"a V2 exact-out swap paid in $paidIn, then UNWRAP_WETH, shows the token it buys",
|
||||
({ tokenIn, amountInMax, tokenOut, amountOut, minReceived }) => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8", "uint8"], [0x0a, 0x09, 0x0c]),
|
||||
[
|
||||
encodePermit2(tokenIn, amountInMax, ROUTER_ADDR),
|
||||
encodeV2SwapExactOut(USER_ADDR, amountOut, amountInMax, [
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
]),
|
||||
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(detail(result, "Token Out").address).toBe(tokenOut);
|
||||
expect(detail(result, "Min. received").value).toBe(minReceived);
|
||||
},
|
||||
);
|
||||
|
||||
// An exact-in swap is held to the same rule: buying USDC, then UNWRAP_WETH,
|
||||
// receives USDC.
|
||||
test("an exact-in swap to a token other than WETH, then UNWRAP_WETH, shows that token", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
||||
[
|
||||
encodeV2SwapExactIn(USER_ADDR, 2000000n, 1900000n, [
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
]),
|
||||
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
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");
|
||||
});
|
||||
|
||||
// Paid in ETH, with an exact-out step, the last swap step buys WETH, so
|
||||
// UNWRAP_WETH makes the output ETH.
|
||||
test("an ETH-paid swap whose last step buys WETH, then UNWRAP_WETH, shows ETH", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(
|
||||
["uint8", "uint8", "uint8", "uint8"],
|
||||
[0x0b, 0x09, 0x08, 0x0c],
|
||||
),
|
||||
[
|
||||
encodeWrapEth(ROUTER_ADDR, 500000000000000000n),
|
||||
encodeV2SwapExactOut(
|
||||
ROUTER_ADDR,
|
||||
1500000000n, // amountOut: 1,500 USDC
|
||||
500000000000000000n, // amountInMax: 0.5 WETH
|
||||
[WETH_ADDR, USDC_ADDR],
|
||||
),
|
||||
encodeV2SwapExactIn(
|
||||
ROUTER_ADDR,
|
||||
1500000000n, // amountIn: 1,500 USDC
|
||||
400000000000000000n, // amountOutMin: 0.4 WETH
|
||||
[USDC_ADDR, WETH_ADDR],
|
||||
),
|
||||
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||
expect(detail(result, "Min. received").value).toBe("0.4000 ETH");
|
||||
});
|
||||
|
||||
// Sepolia's WETH is a different contract; UNWRAP_WETH makes it ETH too.
|
||||
test("a swap to Sepolia WETH, then UNWRAP_WETH, shows ETH", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x08, 0x0c]),
|
||||
[
|
||||
encodeV2SwapExactIn(USER_ADDR, 1000000n, 500000000000000n, [
|
||||
USDC_ADDR,
|
||||
SEPOLIA_WETH_ADDR,
|
||||
]),
|
||||
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(detail(result, "Token Out").value).toBe("ETH");
|
||||
expect(detail(result, "Min. received").value).toBe("0.0005 ETH");
|
||||
});
|
||||
|
||||
// A step that states a Min. received figure but names no output token has
|
||||
// set the output side, so UNWRAP_WETH does not make it ETH: nothing says
|
||||
// the figure is counted in WETH.
|
||||
test("a step with a minimum but no output token, then UNWRAP_WETH, names no token", () => {
|
||||
const data = buildExecute(
|
||||
solidityPacked(["uint8", "uint8"], [0x10, 0x0c]),
|
||||
[
|
||||
encodeV4Swap(new Uint8Array([V4_SWAP_EXACT_IN]), [
|
||||
encodeV4ExactIn(
|
||||
USDC_ADDR,
|
||||
[], // no path: this step names no output currency
|
||||
1000000000n, // 1,000 USDC
|
||||
400000000000000000n, // amountOutMin
|
||||
),
|
||||
]),
|
||||
encodeWrapEth(USER_ADDR, 0n), // UNWRAP_WETH
|
||||
],
|
||||
9999999999n,
|
||||
);
|
||||
|
||||
const result = uniswap.decode(data, ROUTER_ADDR);
|
||||
expect(detail(result, "Token Out").value).toBe(
|
||||
"Unknown (not named in the calldata)",
|
||||
);
|
||||
expect(detail(result, "Min. received").value).toBe(
|
||||
"400000000000000000 base units (decimals unknown)",
|
||||
);
|
||||
});
|
||||
|
||||
test("decodes V3_SWAP_EXACT_IN with known tokens", () => {
|
||||
const data = buildExecute(
|
||||
"0x00", // V3_SWAP_EXACT_IN
|
||||
|
||||
Reference in New Issue
Block a user