fix: decode Uniswap V2 exact-out swaps, input amount shown as a maximum (closes #283)
check / check (push) Failing after 1s
e2e / e2e-chrome (push) Failing after 1s
e2e / e2e-firefox (push) Failing after 1s

decode() had no arm for Universal Router command 0x09, so the approval
screen named the step and showed no token or amount. It now takes the
path's first token with amountInMax as the input side and the last token
with amountOut as the output side, through setInputOnce() and setOutput()
like 0x08. amountInMax is the most the swap may spend, so the Amount line
reads "Up to <amount>", including the value the wait and error screens
show. The eslint-disable comment on decodeV2SwapExactOut() is gone.

Model: opus-5-5
This commit is contained in:
2026-10-04 08:29:40 +00:00
parent 5bf8b5ff1f
commit 96bc83ff51
4 changed files with 87 additions and 13 deletions
+27 -8
View File
@@ -198,11 +198,6 @@ 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(
@@ -443,6 +438,7 @@ function decode(data, toAddress, sources) {
let inputToken = null;
let inputAmount = null;
let inputAmountIsMax = false;
let inputEstablished = false;
let outputToken = null;
let minOutput = null;
@@ -465,15 +461,20 @@ function decode(data, toAddress, sources) {
// output side by the last, because the final leg is what the user
// receives. A half the establishing hop did not state stays null and
// the line says so, rather than being filled in from a different hop.
const setInput = (token, amount) => {
//
// `isMax` comes from that same hop: true when its amount is the most
// the swap may spend (an exact-out swap's amountInMax), not the
// amount spent.
const setInput = (token, amount, isMax = false) => {
inputToken = present(token) ? token : null;
inputAmount = present(amount) ? amount : null;
inputAmountIsMax = isMax;
inputEstablished = true;
};
const setInputOnce = (token, amount) => {
const setInputOnce = (token, amount, isMax = false) => {
if (inputEstablished) return;
if (!present(token) && !present(amount)) return;
setInput(token, amount);
setInput(token, amount, isMax);
};
const setOutput = (token, amount) => {
if (!present(token) && !present(amount)) return;
@@ -521,6 +522,15 @@ function decode(data, toAddress, sources) {
}
}
if (cmdId === 0x09) {
// Buys exactly amountOut and spends at most amountInMax.
const s = decodeV2SwapExactOut(inputs[i]);
if (s) {
setInputOnce(s.tokenIn, s.amountInMax, true);
setOutput(s.tokenOut, s.amountOut);
}
}
if (cmdId === 0x0b) {
const w = decodeWrapEth(inputs[i]);
if (w) {
@@ -613,6 +623,15 @@ 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 (inputAmountIsMax) {
// A maximum is said to be one, in `raw` too: the wait and
// error screens show `raw` as the transaction's amount, and
// there a bare figure would read as the amount spent.
const most = amountText(inputAmount, inInfo);
amount = {
raw: "Up to " + most.raw,
display: "Up to " + most.display,
};
} else {
amount = amountText(inputAmount, inInfo);
}