fix: a swap split across steps the user pays for shows every amount it spends (closes #625)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run

The swap's Amount line gave only the first swap step's amount. It now
gives the total the steps take of Token In: the amount of the step that
sets the input side plus each later take of it by a step the user pays.
A take of another token, one after an amount that is not a quantity, or
one of unknown amount gets lines of its own. A step that may take what
the decoder did not read, such as a V3_SWAP_EXACT_OUT or an
EXECUTE_SUB_PLAN, gets one line saying so and leaves no figure. A
PERMIT2_PERMIT names Token In; its amount stands only in the plain shape
of Uniswap's own routes.

Model: opus-5-5
This commit is contained in:
2026-10-20 04:15:32 +00:00
committed by sneak
parent ca9cd438d9
commit cad2d262ce
4 changed files with 1364 additions and 130 deletions
+353 -82
View File
@@ -335,7 +335,8 @@ const UNNAMED_CURRENCY = "Unknown (not named in the calldata)";
// commands this decoder does not read. That action names both of its
// currencies, and those commands may name one, so saying the calldata did not
// name it would be false. The Paid to line uses it too, when a step may pay an
// address this decoder did not read.
// address this decoder did not read, and so do the lines for what a step
// takes from the sender, when this decoder does not know it.
const UNDECODED_CURRENCY = "Unknown (could not be decoded)";
// The Paid to line for each token paid to the router, the swap's output or
@@ -356,6 +357,11 @@ function present(value) {
return value !== null && value !== undefined;
}
// Whether `a` and `b` are both known and the same address.
function sameAddress(a, b) {
return present(a) && present(b) && a.toLowerCase() === b.toLowerCase();
}
// Uniswap V4 spells "use the whole open delta" as an amount of zero:
// v4-periphery `src/libraries/ActionConstants.sol` declares
// `uint128 internal constant OPEN_DELTA = 0` ("used to signal that an action
@@ -566,6 +572,7 @@ function decodeV2SwapExactIn(input) {
// Decode V2_SWAP_EXACT_OUT (command 0x09) input bytes.
// ABI: (address recipient, uint256 amountOut, uint256 amountInMax,
// address[] path, bool payerIsUser)
// With `payerIsUser` false the router pays the input from its own balance.
function decodeV2SwapExactOut(input) {
try {
const d = coder.decode(
@@ -578,6 +585,7 @@ function decodeV2SwapExactOut(input) {
amountInMax: d[2],
tokenIn: d[3][0],
tokenOut: d[3][d[3].length - 1],
payerIsUser: d[4],
};
} catch {
return null;
@@ -901,10 +909,15 @@ function totalAmount(amounts) {
// and its recipient as written, MSG_SENDER for a TAKE_ALL. TAKE_PORTION takes
// a share of the currency, so it does not name the output side as TAKE does.
// `recipientUnread` is set when the step has a payout this decoder did not
// read. `settledFromRouter` is every SETTLE that the router pays from its own
// balance (`payerIsUser` false), as its currency and the amount it pays: a
// quantity, CONTRACT_BALANCE (all the router holds), or null when this
// decoder cannot tell (see below).
// read, and `settleUnread` when it has a SETTLE or SETTLE_ALL this decoder
// did not read. `settledFromRouter` is every SETTLE that the router pays from
// its own balance (`payerIsUser` false), as its currency and the amount it
// pays: a quantity, CONTRACT_BALANCE (all the router holds), or null when
// this decoder cannot tell (see below). `paidByUser` is what the user pays
// the step, once per currency, as that currency and the amount: the total of
// every SETTLE paid by the user (`payerIsUser` true) and every SETTLE_ALL,
// which V4Router always has the user pay, of that currency, or null when
// this decoder cannot tell what one of them pays.
function decodeV4Swap(input) {
try {
const d = coder.decode(["bytes", "bytes[]"], input);
@@ -914,16 +927,18 @@ function decodeV4Swap(input) {
let settleToken = null;
let takeToken = null;
const takes = [];
// Every SETTLE paid from the router, as its currency, its amount as
// written and its index among the step's actions.
const routerSettles = [];
// The currency of every SETTLE and SETTLE_ALL.
const settledCurrencies = [];
// Every SETTLE and SETTLE_ALL, as its currency, its amount as
// written, whether the user pays it, and its index among the step's
// actions. A SETTLE_ALL pays what a SETTLE of OPEN_DELTA (zero) paid
// by the user pays, all the step still owes of its currency, so it
// is kept as one; its maxAmount only caps that.
const settles = [];
// The index of the step's last swap action, read or not, or -1.
let lastSwapAction = -1;
// Set when an action of the step could not be read.
let actionUnread = false;
let recipientUnread = false;
let settleUnread = false;
// The step's swap actions that could be read, as decodeV4SwapAction()
// reads them.
const swaps = [];
@@ -942,19 +957,22 @@ function decodeV4Swap(input) {
params[i],
);
settleToken = s[0];
settledCurrencies.push(s[0]);
if (!s[2]) {
routerSettles.push({
currency: s[0],
amount: s[1],
index: i,
});
}
settles.push({
currency: s[0],
amount: s[1],
payerIsUser: s[2],
index: i,
});
} else if (actionId === V4_SETTLE_ALL) {
// SETTLE_ALL: (address currency, uint256 maxAmount)
const s = coder.decode(["address", "uint256"], params[i]);
settleToken = s[0];
settledCurrencies.push(s[0]);
settles.push({
currency: s[0],
amount: 0n,
payerIsUser: true,
index: i,
});
} else if (actionId === V4_TAKE) {
// TAKE: (address currency, address recipient, uint256 amount)
const t = coder.decode(
@@ -996,7 +1014,8 @@ function decodeV4Swap(input) {
} catch {
// Skip sub-actions we can't decode. A TAKE, TAKE_ALL or
// TAKE_PORTION among them is a payout this decoder did not
// read.
// read, and a SETTLE or SETTLE_ALL may take from the user
// what it did not read: the router still runs both.
actionUnread = true;
if (
actionId === V4_TAKE ||
@@ -1005,25 +1024,29 @@ function decodeV4Swap(input) {
) {
recipientUnread = true;
}
if (actionId === V4_SETTLE || actionId === V4_SETTLE_ALL) {
settleUnread = true;
}
}
}
// What each SETTLE paid from the router pays. A quantity or
// CONTRACT_BALANCE pays as written. A SETTLE of OPEN_DELTA (zero)
// pays whatever the step still owes of its currency when it runs.
// That is what the step's swap actions state they spend of it only
// in the plain shape of Uniswap's own routes, so only then is it
// given that amount: every action in the step was read; every swap
// action names the currency it spends and the one it buys (an
// exact-in action with an empty path swaps nothing); the SETTLE is
// the step's only SETTLE or SETTLE_ALL of its currency and comes
// after every swap action; no swap action buys that currency; and
// the swap actions that spend it state a quantity together (see
// totalAmount()). Otherwise it is given null.
const settledFromRouter = routerSettles.map((s) => {
if (s.amount !== 0n) {
return { currency: s.currency, amount: s.amount };
}
// What a SETTLE or SETTLE_ALL pays. A SETTLE of any amount but
// OPEN_DELTA (zero) pays as written: a quantity, or, for
// CONTRACT_BALANCE, as much as the router holds. A SETTLE of
// OPEN_DELTA, and a SETTLE_ALL, pay whatever the step still owes of
// the currency when they run. That is what the step's swap actions
// state they spend of it only in the plain shape of Uniswap's own
// routes, so only then is it given that amount: every action in the
// step was read; every swap action names the currency it spends and
// the one it buys (an exact-in action with an empty path swaps
// nothing); it is the step's only SETTLE or SETTLE_ALL of its
// currency and comes after every swap action; no swap action buys
// that currency and nothing in the step takes it (a TAKE of a stated
// amount adds to what the step owes); and the swap actions that spend
// it state a quantity together (see totalAmount()). Otherwise it is
// given null.
const pays = (s) => {
if (s.amount !== 0n) return s.amount;
const stated = totalAmount(
swaps
.filter((w) => w.tokenIn === s.currency)
@@ -1032,16 +1055,30 @@ function decodeV4Swap(input) {
const paysWhatSwapsSpend =
!actionUnread &&
swaps.every((w) => present(w.tokenIn) && present(w.tokenOut)) &&
settledCurrencies.filter((c) => c === s.currency).length ===
1 &&
settles.filter((c) => c.currency === s.currency).length === 1 &&
s.index > lastSwapAction &&
!swaps.some((w) => w.tokenOut === s.currency) &&
!takes.some((t) => t.currency === s.currency) &&
typeof stated === "bigint";
return {
currency: s.currency,
amount: paysWhatSwapsSpend ? stated : null,
};
});
return paysWhatSwapsSpend ? stated : null;
};
const settledFromRouter = settles
.filter((s) => !s.payerIsUser)
.map((s) => ({ currency: s.currency, amount: pays(s) }));
const paidByUser = [];
for (const s of settles.filter((c) => c.payerIsUser)) {
const amount = pays(s);
const paid = paidByUser.find((p) => p.currency === s.currency);
if (!paid) {
paidByUser.push({ currency: s.currency, amount });
} else {
paid.amount =
typeof paid.amount === "bigint" &&
typeof amount === "bigint"
? paid.amount + amount
: null;
}
}
// Rule 3.
if (swapUndecoded) {
@@ -1052,7 +1089,9 @@ function decodeV4Swap(input) {
amountOutMin: null,
takes,
settledFromRouter,
paidByUser,
recipientUnread,
settleUnread,
swapUndecoded,
hasSwapAction: true,
};
@@ -1094,7 +1133,9 @@ function decodeV4Swap(input) {
amountOutMin,
takes,
settledFromRouter,
paidByUser,
recipientUnread,
settleUnread,
swapUndecoded,
hasSwapAction: swaps.length > 0,
};
@@ -1125,6 +1166,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
let inputToken = null;
let inputAmount = null;
let inputEstablished = false;
// The last PERMIT2_PERMIT this decoder read, or null (see
// permitStands after the loop).
let permit = null;
// The index of each step this decoder did not read in full: a
// command it does not read, a step whose input it cannot read, in
// whole or in part, and one it fails on (see permitStands).
const stepsUnread = [];
let outputToken = null;
let minOutput = null;
let outputEstablished = false;
@@ -1135,8 +1183,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
const commandNames = [];
// THE INVARIANT: an amount and the token it is counted in always come
// from the same hop. A figure is never rendered against a token that
// did not supply it.
// from the same hop, or from hops that all spend that token. A figure
// is never rendered against a token that did not supply it.
//
// Both sides are therefore set as a PAIR — never field by field, and
// never on truthiness. An address is never falsy once set but an
@@ -1152,7 +1200,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// states neither half: the output side always, and the input side
// when no earlier hop set it. A half the establishing hop did not
// state stays null and the line says so, rather than being filled in
// from a different hop.
// from a different hop. A PERMIT2_PERMIT sets no side here: after the
// loop, the last one read names Token In, and the Amount line is
// formed there, in one place (see permitStands).
const setInput = (token, amount) => {
inputToken = present(token) ? token : null;
inputAmount = present(amount) ? amount : null;
@@ -1170,6 +1220,29 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
outputEstablished = true;
};
// What each step but the one that sets the input side takes from the
// sender, as { token, amount }, in step order. After the loop each
// joins the Amount line or gets lines of its own (see permitStands).
const userPaid = [];
// A swap step the user pays for (`payerIsUser` true) takes `amount`
// of `token` from the sender. A V2 exact-in of ALREADY_PAID takes
// nothing.
const addUserPaid = (token, amount) => {
if (amount !== ALREADY_PAID) userPaid.push({ token, amount });
};
// A step may take from the sender what this decoder did not read: a
// V4 step, other than the one that sets the input side, whose input
// or one of whose SETTLEs or SETTLE_ALLs it cannot read; a V2 or V3
// exact-in or V2 exact-out step or PERMIT2_TRANSFER_FROM whose input
// it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; a V3_SWAP_EXACT_OUT;
// an EXECUTE_SUB_PLAN; and a step it fails on. It is recorded with a
// null token and amount: one line says so, there is no Amount line,
// whose figure may then be too low, and the amount of a permit after
// it does not stand (see permitStands).
const spentUnread = () => {
userPaid.push({ token: null, amount: null });
};
// Who the steps pay, in whichever step and of whatever token.
// MSG_SENDER is the sender. The router itself, as ADDRESS_THIS or by
// its own address, holds what it is paid until a later step pays it
@@ -1300,20 +1373,40 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// EXECUTE_SUB_PLAN runs a further list of commands held in
// its input, which this decoder does not read. The step
// says so, and after this loop neither side is named.
// Those commands may pay any address.
// Those commands may pay any address and take from the
// sender.
hasSubPlan = true;
commandNames[i] += " (its steps are not shown)";
payeeUnread = true;
stepsUnread.push(i);
spentUnread();
} else if (command === "PERMIT2_PERMIT") {
const p = decodePermit2(inputs[i]);
// A permit states both halves itself, so it may replace an
// input side an earlier hop established without breaking
// the invariant.
if (p) setInput(p.token, p.amount);
if (p) {
// Whether a step before it took its token, or may
// have: the step that set the input side or one in
// `userPaid`, when its token is the permit's or not
// known.
const mayBeItsToken = (token) =>
!present(token) || sameAddress(token, p.token);
const tookBefore =
(inputEstablished && mayBeItsToken(inputToken)) ||
userPaid.some((t) => mayBeItsToken(t.token));
permit = {
...p,
step: i,
allowedToFail,
beforeEveryTake: !tookBefore,
};
} else {
stepsUnread.push(i);
}
} else if (command === "PERMIT2_TRANSFER_FROM") {
const p = decodePermit2TransferFrom(inputs[i]);
if (!p) {
payeeUnread = true;
stepsUnread.push(i);
spentUnread();
} else if (
commandTypeMask === 0x1f &&
[MSG_SENDER, ADDRESS_THIS].includes(
@@ -1351,6 +1444,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// owner may be any address. It sets no side, and the
// step says so.
commandNames[i] += " (could not be decoded)";
stepsUnread.push(i);
} else if (b.owner.toLowerCase() !== sender) {
// The router reads the owner as written (see
// decodeBalanceCheck()), so address(1) and address(2)
@@ -1375,7 +1469,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else if (command === "V3_SWAP_EXACT_IN") {
const s = decodeV3SwapExactIn(inputs[i]);
if (s) {
setInputOnce(s.tokenIn, s.amountIn);
if (!inputEstablished) {
setInputOnce(s.tokenIn, s.amountIn);
} else if (s.payerIsUser) {
addUserPaid(s.tokenIn, s.amountIn);
}
// Always update output: in multi-step swaps (V3 → V4),
// the last swap step determines the final output token
// and minimum received amount.
@@ -1388,11 +1486,25 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}
} else {
payeeUnread = true;
stepsUnread.push(i);
spentUnread();
}
} else if (command === "V3_SWAP_EXACT_OUT") {
// This decoder does not read it
// (https://git.eeqj.de/sneak/AutistMask/issues/601). It
// may pay any address and take from the sender, so
// wherever it stands it leaves no Amount figure.
payeeUnread = true;
stepsUnread.push(i);
spentUnread();
} else if (command === "V2_SWAP_EXACT_IN") {
const s = decodeV2SwapExactIn(inputs[i]);
if (s) {
setInputOnce(s.tokenIn, s.amountIn);
if (!inputEstablished) {
setInputOnce(s.tokenIn, s.amountIn);
} else if (s.payerIsUser) {
addUserPaid(s.tokenIn, s.amountIn);
}
setOutput(s.tokenOut, s.amountOutMin);
if (!s.payerIsUser) {
spendFromRouter(s.tokenIn, s.amountIn);
@@ -1402,20 +1514,34 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}
} else {
payeeUnread = true;
stepsUnread.push(i);
spentUnread();
}
} else if (command === "V2_SWAP_EXACT_OUT") {
// Buys exactly amountOut and spends at most amountInMax.
hasV2ExactOut = true;
const s = decodeV2SwapExactOut(inputs[i]);
if (s) {
setInputOnce(s.tokenIn, s.amountInMax);
if (!inputEstablished) {
setInputOnce(s.tokenIn, s.amountInMax);
} else if (s.payerIsUser) {
addUserPaid(s.tokenIn, s.amountInMax);
}
setOutput(s.tokenOut, s.amountOut);
if (!isHop(i, s.tokenOut, s.recipient)) {
addPayee(s.recipient, s.tokenOut);
}
} else {
payeeUnread = true;
stepsUnread.push(i);
spentUnread();
}
} else if (command === "PERMIT2_TRANSFER_FROM_BATCH") {
// It moves the sender's tokens to any addresses, and this
// decoder does not read it.
payeeUnread = true;
stepsUnread.push(i);
spentUnread();
} else if (command === "SWEEP") {
const s = decodeSweep(inputs[i]);
if (s) {
@@ -1425,6 +1551,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}
} else {
payeeUnread = true;
stepsUnread.push(i);
}
} else if (
command === "TRANSFER" ||
@@ -1461,6 +1588,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}
} else {
payeeUnread = true;
stepsUnread.push(i);
}
} else if (command === "ACROSS_V4_DEPOSIT_V3") {
const a = decodeAcrossV4DepositV3(inputs[i]);
@@ -1484,6 +1612,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}
} else {
payeeUnread = true;
stepsUnread.push(i);
}
} else if (command === "WRAP_ETH") {
const w = decodeWrapEth(inputs[i]);
@@ -1514,9 +1643,11 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}
} else {
payeeUnread = true;
stepsUnread.push(i);
}
} else if (command === "V4_SWAP") {
const v4 = decodeV4Swap(inputs[i]);
const inputSetBefore = inputEstablished;
if (v4) {
// Each SETTLE paid from the router spends the amount
// decodeV4Swap() says it pays.
@@ -1532,6 +1663,14 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (!v4 || v4.recipientUnread) {
payeeUnread = true;
}
if (
!v4 ||
v4.recipientUnread ||
v4.settleUnread ||
v4.swapUndecoded
) {
stepsUnread.push(i);
}
if (v4 && v4.swapUndecoded) {
// A swap action of this step went unread, so the step
// is marked.
@@ -1562,6 +1701,23 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
setInputOnce(v4.tokenIn, v4.amountIn);
setOutput(v4.tokenOut, v4.amountOutMin);
}
// Every V4 step but the one that sets the input side,
// with a swap action or without, takes from the sender
// what the user pays it (an amount an unread action may
// change is null). One whose input, or one of whose
// SETTLEs or SETTLE_ALLs, this decoder could not read may
// take anything: the router still runs it.
const setInputHere = inputEstablished && !inputSetBefore;
if (!setInputHere) {
if (v4) {
for (const p of v4.paidByUser) {
addUserPaid(p.currency, p.amount);
}
}
if (!v4 || v4.settleUnread) {
spentUnread();
}
}
} else if (command === "UNWRAP_WETH") {
hasUnwrapWeth = true;
const u = decodeUnwrapWeth(inputs[i]);
@@ -1582,26 +1738,109 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
}
} else {
payeeUnread = true;
stepsUnread.push(i);
}
} else {
// Any other step may pay an address this decoder does not
// read: V3_SWAP_EXACT_OUT, PERMIT2_TRANSFER_FROM_BATCH and
// the NFT commands among them, and a type the release
// names no command for.
// read: PERMIT2_PERMIT_BATCH and the NFT commands among
// them, and a type the release names no command for.
payeeUnread = true;
stepsUnread.push(i);
}
} catch {
// Skip commands we can't decode. One may pay any address.
// Skip commands we can't decode. One may pay any address and
// take from the sender.
payeeUnread = true;
stepsUnread.push(i);
spentUnread();
}
if (allowedToFail) {
commandNames[i] += " (allowed to fail)";
}
}
// The Amount line, and the lines for what the steps take beyond it.
//
// The total is what the steps take of Token In: the amount of the
// step that set the input side, with each later take of that token
// added to it while both are quantities. A take that cannot be added,
// being of another token or coming after an amount that is not a
// quantity (a V4 open delta, ALREADY_PAID, or none stated), gets
// lines of its own. One whose amount is not known also leaves the
// total unknown, with no figure.
//
// The last PERMIT2_PERMIT read names Token In. Its amount stands as
// the Amount figure in place of the total only in the plain shape of
// Uniswap's own routes: it is the only PERMIT2_PERMIT, its spender is
// the router, the router may not let it fail, no step before it took
// its token or one not known, and this decoder read every step after
// it in full (see stepsUnread). Permit2 then lets the router take no
// more of the token than that amount, so the permit covers every take
// of it. A later step this decoder did not read may raise what Permit2
// lets the router take, as a PERMIT2_PERMIT_BATCH can. A step that
// may take from the sender what this decoder did not read leaves no
// figure, permit or not (see spentUnread()), and an EXECUTE_SUB_PLAN
// step leaves both sides unknown (below).
const stepCommands = Array.from(
commandsBytes,
(b) => commands[b & commandTypeMask],
);
const permitStands =
permit !== null &&
stepCommands.filter((c) => c === "PERMIT2_PERMIT").length === 1 &&
sameAddress(permit.spender, toAddress) &&
!permit.allowedToFail &&
permit.beforeEveryTake &&
!stepsUnread.some((step) => step > permit.step);
// What the steps take that the Amount line does not give, as
// { token, amount }. A null token is a step this decoder did not read
// (see spentUnread()), or a token it does not know.
const spentFromSender = [];
let total = null;
// The take of the step that set the input side, when the total
// starts from it and it stated no amount (see below), or null.
let firstTakeUnstated = null;
if (permit === null || sameAddress(inputToken, permit.token)) {
total = inputAmount;
if (inputEstablished && !present(inputAmount)) {
firstTakeUnstated = { token: inputToken, amount: null };
}
} else if (inputEstablished) {
// The step that set the input side spent a token other than the
// permit's, or one not known, so the total starts with no figure.
spentFromSender.push({ token: inputToken, amount: inputAmount });
}
if (permit !== null) inputToken = permit.token;
for (const t of userPaid) {
const ofTokenIn = sameAddress(t.token, inputToken);
if (ofTokenIn && permitStands) continue;
if (
ofTokenIn &&
typeof total === "bigint" &&
typeof t.amount === "bigint"
) {
total += t.amount;
continue;
}
spentFromSender.push(t);
if (ofTokenIn && !present(t.amount)) total = null;
}
inputAmount = permitStands ? permit.amount : total;
// With no Amount figure, a later take's own figure could read as all
// that leaves, so the step that set the input side, when it stated
// no amount, then gets lines of its own too, first, saying its amount
// is not known.
if (
!present(inputAmount) &&
firstTakeUnstated !== null &&
spentFromSender.some((s) => present(s.amount))
) {
spentFromSender.unshift(firstTakeUnstated);
}
// The commands of an EXECUTE_SUB_PLAN step may spend the user's tokens
// or pay the user, before or after any other step, and a permit among
// them would replace the input side. What the other steps state is
// them would name Token In. What the other steps state is
// then not the whole swap, so both sides are set unknown, with no
// figure, wherever the step stands.
if (hasSubPlan) {
@@ -1672,37 +1911,49 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
});
}
if (present(inputAmount)) {
// Three amounts need no scale to describe and are named rather
// than formatted: V4's open delta and V2's already-paid zero,
// neither of which is a quantity at all (see OPEN_DELTA and
// ALREADY_PAID), and an unbounded permit. Those two tests come
// first — the sentinels are not bigints and cannot be compared
// with one.
let amount;
if (inputAmount === OPEN_DELTA) {
amount = { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
} else if (inputAmount === ALREADY_PAID) {
amount = {
// The text of an amount the swap steps spend, for the Amount line and
// the lines for other tokens below it. Three amounts need no scale to
// describe and are named rather than formatted: V4's open delta and
// V2's already-paid zero, neither of which is a quantity at all (see
// OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two
// tests come first — the sentinels are not bigints and cannot be
// compared with one.
const spentText = (amount, info) => {
if (amount === OPEN_DELTA) {
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
}
if (amount === ALREADY_PAID) {
return {
raw: ALREADY_PAID_AMOUNT,
display: ALREADY_PAID_AMOUNT,
};
} else if (inputAmount >= MAX_UINT160) {
amount = { raw: "Unlimited", display: "Unlimited" };
} else if (hasV2ExactOut) {
}
if (amount >= MAX_UINT160) {
return { raw: "Unlimited", display: "Unlimited" };
}
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 = {
// steps gave it (an amountInMax, the WRAP_ETH of a swap paid
// in ETH, a permit, or several steps' total), 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(amount, info);
return {
raw: "Up to " + most.raw,
display: "Up to " + most.display,
};
} else {
amount = amountText(inputAmount, inInfo);
}
return amountText(amount, info);
};
// A null token in `spentFromSender`, a step this decoder did not read
// or a token it does not know, leaves no Amount figure, which may
// then be too low.
if (
present(inputAmount) &&
spentFromSender.every((s) => present(s.token))
) {
const amount = spentText(inputAmount, inInfo);
details.push({
label: "Amount",
value: amount.display,
@@ -1710,6 +1961,26 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
});
}
// What steps take from the sender beyond the Amount line, in the shape
// of the lines below for a PERMIT2_TRANSFER_FROM. An amount this
// decoder does not know is said so, and a step it did not read gets
// that one line alone.
for (const s of spentFromSender) {
details.push({
label: "Moved from the sender",
value: present(s.amount)
? spentText(s.amount, tokenInfo(s.token, sources)).display
: UNDECODED_CURRENCY,
});
if (present(s.token)) {
details.push(tokenMovedLine("Token moved", s.token, sources));
details.push({
label: "Moved to",
value: "The swap step that spends it",
});
}
}
// What each PERMIT2_TRANSFER_FROM moves: its amount, its token and
// where it goes, an address on a line of its own.
for (const m of movedFromSender) {