1 Commits
Author SHA1 Message Date
sneak 50326ad933 fix: the address screen shows a token just added at its fetched balance, not 0.0000 (closes #516)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
A balance refresh that returns now draws the balances on AddressDetail or
AddressToken again when that screen is up, not only the wallet list; their
transaction lists are left alone. A token added on AddToken reads `quantity
unknown` instead of 0.0000 until a refresh that started with it tracked has
fetched every address's token balances. To tell an address whose token
balances the explorer did not return, the refresh takes them off its copy of
the wallets before fetching.

Model: opus-5-5
2026-10-21 02:43:13 +00:00
9 changed files with 41 additions and 319 deletions

No files matched your search

+12 -25
View File
@@ -1396,9 +1396,8 @@ lists, the send-screen token selector, the address total and the remove-address
warning (`isBelowOneMillionth()` in `src/shared/amountDisplay.js`). The Send and warning (`isBelowOneMillionth()` in `src/shared/amountDisplay.js`). The Send and
confirmation screens show it when its token is the one being sent. Tracked confirmation screens show it when its token is the one being sent. Tracked
tokens with a zero balance are listed as well while "Show tracked tokens with tokens with a zero balance are listed as well while "Show tracked tokens with
zero balance" is on; a token just added on AddToken or SettingsAddToken whose zero balance" is on; a token just added on AddToken whose balance no refresh has
balance no refresh has fetched yet is listed there as `quantity unknown` (see fetched yet is listed there as `quantity unknown` (see AddToken).
AddToken).
#### Stored state and its version #### Stored state and its version
@@ -2370,9 +2369,7 @@ view would leave a wallet one click from deletion.
add path add path
- **Transitions**: - **Transitions**:
- Any of the add paths shown, on success → adds the token and shows an - Any of the add paths shown, on success → adds the token and shows an
"Added SYMBOL" flash message (no screen change). Until its balance is "Added SYMBOL" flash message (no screen change)
fetched, its balance line reads `quantity unknown`, as after "Add" on
AddToken
- A duplicate, a scam-listed address, or a failed contract lookup → flash - A duplicate, a scam-listed address, or a failed contract lookup → flash
message, no screen change message, no screen change
- "Add" by contract address, then a network switch in Settings before the - "Add" by contract address, then a network switch in Settings before the
@@ -3280,25 +3277,15 @@ Currently supported:
of a token other than `Token In`; one of `Token In` after an amount that is of a token other than `Token In`; one of `Token In` after an amount that is
not a quantity (a V4 open delta, `ALREADY_PAID`, which keep their wording on not a quantity (a V4 open delta, `ALREADY_PAID`, which keep their wording on
the `Amount` line, or no stated amount, as of a V4 exact-out step); and one the `Amount` line, or no stated amount, as of a V4 exact-out step); and one
whose amount the decoder does not know. When there is no `Amount` line, each whose amount the decoder does not know. When the step that sets the input side
take the total counts gets them too, so that every amount the decoder reads is states no amount, the user pays it some of its input token, and there is no
shown: the amount the step that sets the input side states, when it is a `Amount` line, that step gets them too whenever another take's lines give a
quantity and the user pays that step (a V2 or V3 step whose `payerIsUser` is figure, or a `PERMIT2_TRANSFER_FROM` of its token gives one, on the lines
true, a V4 step the user pays some of its input token, or a `WRAP_ETH`, whose below or, into the router, on its `Paid to` line, so that figure cannot read
ETH comes from the transaction's value), and each later take of `Token In` as all that leaves. When it states no amount and the user pays it none of its
added to it. A step that sets the input side and is paid from the router takes input token, as when the router pays it, that step takes none of that token
nothing from the user's wallet and gets none. A `PERMIT2_PERMIT` the decoder from the user's wallet and never gets them, with a permit or without. They
cannot read, then V3 exact-ins of 600 USDT and 400 DAI paid by the user, give come in step order, the step that sets the input side first when it has them:
lines for both amounts. When the step that sets the input side states no
amount, the user pays it some of its input token, and there is no `Amount`
line, that step gets them too whenever another take's lines give a figure, or
a `PERMIT2_TRANSFER_FROM` of its token gives one, on the lines below or, into
the router, on its `Paid to` line, so that figure cannot read as all that
leaves. When it states no amount and the user pays it none of its input token,
as when the router pays it, that step takes none of that token from the user's
wallet and never gets them, with a permit or without. They come in step order,
except that the step that sets the input side comes first when it has them,
followed, when there is no `Amount` line, by the other takes the total counts:
`Moved from the sender`, its amount, worded as the `Amount` line words one, or `Moved from the sender`, its amount, worded as the `Amount` line words one, or
`Unknown (could not be decoded)` when the decoder does not know it; `Unknown (could not be decoded)` when the decoder does not know it;
`Token moved`, giving the token's address as the `Token In` line does; and `Token moved`, giving the token's address as the `Token In` line does; and
+3 -2
View File
@@ -4,7 +4,7 @@ const {
showFlash, showFlash,
escapeHtml, escapeHtml,
goBack, goBack,
trackToken, tokensNotFetched,
} = require("./helpers"); } = require("./helpers");
const { getTopTokens } = require("../../shared/tokenList"); const { getTopTokens } = require("../../shared/tokenList");
const { state, saveState, currentNetwork } = require("../../shared/state"); const { state, saveState, currentNetwork } = require("../../shared/state");
@@ -79,12 +79,13 @@ function init(ctx) {
return; return;
} }
log.infof("Adding token", info.symbol, contractAddr); log.infof("Adding token", info.symbol, contractAddr);
trackToken({ state.trackedTokens.push({
address: contractAddr, address: contractAddr,
symbol: info.symbol, symbol: info.symbol,
decimals: info.decimals, decimals: info.decimals,
name: info.name, name: info.name,
}); });
tokensNotFetched.add(contractAddr.toLowerCase());
await saveState(); await saveState();
ctx.doRefreshAndRender(); ctx.doRefreshAndRender();
// Pop the stack (back to address detail) and re-render it // Pop the stack (back to address detail) and re-render it
+4 -12
View File
@@ -380,22 +380,15 @@ function balanceLine(symbol, amount, price, tokenId) {
); );
} }
// Contract addresses, lowercased, of the tokens the user added that no balance // Contract addresses, lowercased, of the tokens added on AddToken that no
// refresh has fetched yet. An address with no balance row for one of them has // balance refresh has fetched yet. An address with no balance row for one of
// no known balance of it, so its line says the quantity is unknown rather than // them has no known balance of it, so its line says the quantity is unknown
// 0.0000. trackToken() adds a token here, and doRefreshAndRender() in // rather than 0.0000. AddToken adds a token here, and doRefreshAndRender() in
// src/popup/index.js takes it off once a refresh that started with it tracked // src/popup/index.js takes it off once a refresh that started with it tracked
// has fetched every address's token balances // has fetched every address's token balances
// (https://git.eeqj.de/sneak/AutistMask/issues/516). // (https://git.eeqj.de/sneak/AutistMask/issues/516).
const tokensNotFetched = new Set(); const tokensNotFetched = new Set();
// Tracks `token` on the active network, as every add path on AddToken and
// SettingsAddToken does. No refresh has fetched its balance yet.
function trackToken(token) {
state.trackedTokens.push(token);
tokensNotFetched.add(token.address.toLowerCase());
}
function balanceLinesForAddress(addr, trackedTokens, showZero) { function balanceLinesForAddress(addr, trackedTokens, showZero) {
let html = balanceLine( let html = balanceLine(
nativeCurrency(), nativeCurrency(),
@@ -804,7 +797,6 @@ module.exports = {
balanceLine, balanceLine,
balanceLinesForAddress, balanceLinesForAddress,
tokensNotFetched, tokensNotFetched,
trackToken,
addressHoldsFunds, addressHoldsFunds,
keyMaterialName, keyMaterialName,
unknownableAmount, unknownableAmount,
+4 -11
View File
@@ -1,11 +1,4 @@
const { const { $, showView, showFlash, escapeHtml, goBack } = require("./helpers");
$,
showView,
showFlash,
escapeHtml,
goBack,
trackToken,
} = require("./helpers");
const { getTopTokens } = require("../../shared/tokenList"); const { getTopTokens } = require("../../shared/tokenList");
const { state, saveState, currentNetwork } = require("../../shared/state"); const { state, saveState, currentNetwork } = require("../../shared/state");
const { lookupTokenInfo } = require("../../shared/balances"); const { lookupTokenInfo } = require("../../shared/balances");
@@ -50,7 +43,7 @@ function renderTop10() {
decimals: parseInt(btn.dataset.decimals, 10), decimals: parseInt(btn.dataset.decimals, 10),
name: btn.dataset.name || btn.dataset.symbol, name: btn.dataset.name || btn.dataset.symbol,
}; };
trackToken(token); state.trackedTokens.push(token);
await saveState(); await saveState();
showFlash("Added " + token.symbol); showFlash("Added " + token.symbol);
renderTop10(); renderTop10();
@@ -117,7 +110,7 @@ function init(_ctx) {
decimals: parseInt(opt.dataset.decimals, 10), decimals: parseInt(opt.dataset.decimals, 10),
name: opt.dataset.name || opt.dataset.symbol, name: opt.dataset.name || opt.dataset.symbol,
}; };
trackToken(token); state.trackedTokens.push(token);
await saveState(); await saveState();
showFlash("Added " + token.symbol); showFlash("Added " + token.symbol);
renderTop10(); renderTop10();
@@ -163,7 +156,7 @@ function init(_ctx) {
return; return;
} }
log.infof("Adding token", info.symbol, addr); log.infof("Adding token", info.symbol, addr);
trackToken({ state.trackedTokens.push({
address: addr, address: addr,
symbol: info.symbol, symbol: info.symbol,
decimals: info.decimals, decimals: info.decimals,
+4 -25
View File
@@ -1234,11 +1234,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
let inputToken = null; let inputToken = null;
let inputAmount = null; let inputAmount = null;
let inputEstablished = false; let inputEstablished = false;
// Whether the user pays the step that set the input side: a V2 or V3 // Whether the user pays the V4 step that set the input side any of
// swap step with `payerIsUser` true, a V4 step the user pays some of // its input currency (see decodeV4Swap()). Only a V4 step can set the
// its input currency (see decodeV4Swap()), or a WRAP_ETH, whose ETH // input side with no amount, the one case in which this is read.
// comes from the transaction's value. A step paid from the router
// takes nothing from the sender.
let inputPaidByUser = false; let inputPaidByUser = false;
// The last PERMIT2_PERMIT this decoder read before a step set the // The last PERMIT2_PERMIT this decoder read before a step set the
// input side, or null (see permitStands after the loop). // input side, or null (see permitStands after the loop).
@@ -1624,7 +1622,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (s) { if (s) {
if (!inputEstablished) { if (!inputEstablished) {
setInputOnce(s.tokenIn, s.amountIn); setInputOnce(s.tokenIn, s.amountIn);
inputPaidByUser = s.payerIsUser;
} else if (s.payerIsUser) { } else if (s.payerIsUser) {
addUserPaid(s.tokenIn, s.amountIn); addUserPaid(s.tokenIn, s.amountIn);
} }
@@ -1648,7 +1645,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (s) { if (s) {
if (!inputEstablished) { if (!inputEstablished) {
setInputOnce(s.tokenIn, s.amountIn); setInputOnce(s.tokenIn, s.amountIn);
inputPaidByUser = s.payerIsUser;
} else if (s.payerIsUser) { } else if (s.payerIsUser) {
addUserPaid(s.tokenIn, s.amountIn); addUserPaid(s.tokenIn, s.amountIn);
} }
@@ -1680,7 +1676,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
if (s) { if (s) {
if (!inputEstablished) { if (!inputEstablished) {
setInputOnce(s.tokenIn, s.amountInMax); setInputOnce(s.tokenIn, s.amountInMax);
inputPaidByUser = s.payerIsUser;
} else if (s.payerIsUser) { } else if (s.payerIsUser) {
addUserPaid(s.tokenIn, s.amountInMax); addUserPaid(s.tokenIn, s.amountInMax);
} }
@@ -1776,13 +1771,10 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
} else if (command === "WRAP_ETH") { } else if (command === "WRAP_ETH") {
const w = decodeWrapEth(inputs[i]); const w = decodeWrapEth(inputs[i]);
if (w) { if (w) {
if (!inputEstablished) {
setInputOnce( setInputOnce(
"0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000",
w.amount, w.amount,
); );
inputPaidByUser = true;
}
addPayee(w.recipient); addPayee(w.recipient);
// CONTRACT_BALANCE wraps all the router's ETH. // CONTRACT_BALANCE wraps all the router's ETH.
if (w.amount === CONTRACT_BALANCE) { if (w.amount === CONTRACT_BALANCE) {
@@ -2049,11 +2041,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// (see spentUnread()), or a token it does not know. // (see spentUnread()), or a token it does not know.
const spentFromSender = []; const spentFromSender = [];
let total = null; let total = null;
// The takes from the sender that `total` adds up, each a quantity, as
// { token, amount }. When there is no Amount line, each gets lines of
// its own (see below). The amount of a step that set the input side
// and is paid from the router counts in `total` but is not among them.
const inTotal = [];
// Whether the step that set the input side counts as a take from the // Whether the step that set the input side counts as a take from the
// sender, which may get lines of its own. The amount it stated counts // sender, which may get lines of its own. The amount it stated counts
// whoever pays it. Stating none, it counts only when the user pays it // whoever pays it. Stating none, it counts only when the user pays it
@@ -2066,9 +2053,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
let firstTakeUnstated = null; let firstTakeUnstated = null;
if (!permitStands || sameAddress(inputToken, permit.token)) { if (!permitStands || sameAddress(inputToken, permit.token)) {
total = inputAmount; total = inputAmount;
if (typeof inputAmount === "bigint" && inputPaidByUser) {
inTotal.push({ token: inputToken, amount: inputAmount });
}
if (hasFirstTake && !present(inputAmount)) { if (hasFirstTake && !present(inputAmount)) {
firstTakeUnstated = { token: inputToken, amount: null }; firstTakeUnstated = { token: inputToken, amount: null };
} }
@@ -2087,7 +2071,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
typeof t.amount === "bigint" typeof t.amount === "bigint"
) { ) {
total += t.amount; total += t.amount;
inTotal.push(t);
continue; continue;
} }
spentFromSender.push(t); spentFromSender.push(t);
@@ -2219,9 +2202,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
// A null token in `spentFromSender`, a step this decoder did not read // A null token in `spentFromSender`, a step this decoder did not read
// or a token it does not know, and one in `permits`, a permit or // or a token it does not know, and one in `permits`, a permit or
// batch it could not read, leave no Amount figure, which may then be // batch it could not read, leave no Amount figure, which may then be
// too low. With no Amount line, each take the total adds up gets // too low.
// lines of its own, first, so that every amount this decoder read is
// shown.
if ( if (
present(inputAmount) && present(inputAmount) &&
spentFromSender.every((s) => present(s.token)) && spentFromSender.every((s) => present(s.token)) &&
@@ -2233,8 +2214,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
value: amount.display, value: amount.display,
rawValue: amount.raw, rawValue: amount.raw,
}); });
} else {
spentFromSender.unshift(...inTotal);
} }
// What steps take from the sender beyond the Amount line, in the shape // What steps take from the sender beyond the Amount line, in the shape
+4 -49
View File
@@ -1,7 +1,6 @@
// After "Add" on AddToken, AddressDetail lists the new token, and its line reads // After "Add" on AddToken, AddressDetail lists the new token, and its line reads
// `quantity unknown`, not 0.0000, until a balance refresh that asked the // `quantity unknown`, not 0.0000, until a balance refresh that asked the
// explorer about the token has returned for every address; so does a token // explorer about the token has returned for every address. A refresh that
// added on SettingsAddToken, wherever it is listed. A refresh that
// returns draws the balances on AddressDetail or AddressToken again, so the // returns draws the balances on AddressDetail or AddressToken again, so the
// fetched balance shows without the screen being reopened // fetched balance shows without the screen being reopened
// (https://git.eeqj.de/sneak/AutistMask/issues/516). Driven through the real // (https://git.eeqj.de/sneak/AutistMask/issues/516). Driven through the real
@@ -79,28 +78,16 @@ async function addNewToken(page) {
await page.click("btn-add-token-confirm"); await page.click("btn-add-token-confirm");
} }
// NEW_TOKEN's row in the balance list in element `id`, AddressDetail's unless // NEW_TOKEN's row in AddressDetail's balance list, as markup.
// another is named, as markup. function newTokenRow(page) {
function newTokenRow(page, id = "address-balances") {
return page return page
.node(id) .node("address-balances")
.innerHTML.split("<div ") .innerHTML.split("<div ")
.find((row) => .find((row) =>
row.includes('data-token="' + NEW_TOKEN.toLowerCase() + '"'), row.includes('data-token="' + NEW_TOKEN.toLowerCase() + '"'),
); );
} }
// AddressDetail for the one address opened from Home, as its `[info]` button
// does.
async function openAddress(page) {
const { state } = require("../src/shared/state");
state.selectedWallet = 0;
state.selectedAddress = 0;
require("../src/popup/views/helpers").pushCurrentView();
require("../src/popup/views/addressDetail").show();
await page.settle();
}
// AddressToken for `token` opened from AddressDetail, as tapping the token's // AddressToken for `token` opened from AddressDetail, as tapping the token's
// row there does. // row there does.
async function openToken(page, token) { async function openToken(page, token) {
@@ -194,38 +181,6 @@ test("AddressToken for the new token reads quantity unknown until the refresh re
); );
}); });
test("a token added by contract address on SettingsAddToken reads quantity unknown until the refresh returns", async () => {
const page = await bootPopup(unversionedValidProfile());
expect(page.visibleViews()).toEqual(["main"]);
const finish = holdNextRefresh(page);
const { lookupTokenInfo } = require("../src/shared/balances");
lookupTokenInfo.mockResolvedValueOnce({
symbol: "NEW",
decimals: 18,
name: "New Token",
});
await page.click("btn-settings");
await page.click("btn-settings-add-token");
page.node("settings-addtoken-address").value = NEW_TOKEN;
await page.click("btn-settings-addtoken-manual");
await page.click("btn-settings-addtoken-back");
await page.click("btn-settings-back");
expect(page.visibleViews()).toEqual(["main"]);
const onHome = newTokenRow(page, "wallet-list");
expect(onHome).toContain("quantity unknown");
expect(onHome).not.toContain("0.0000");
await openAddress(page);
const onAddress = newTokenRow(page);
expect(onAddress).toContain("quantity unknown");
expect(onAddress).not.toContain("0.0000");
await finish([MILLION]);
expect(newTokenRow(page)).toContain("1000000.0000");
});
test("a refresh that returns while AddressToken is up draws its balance again", async () => { test("a refresh that returns while AddressToken is up draws its balance again", async () => {
const page = await bootPopup( const page = await bootPopup(
unversionedValidProfile({ unversionedValidProfile({
+1 -1
View File
@@ -88,7 +88,7 @@ describe.each([
showFlash: (msg) => flashes.push(msg), showFlash: (msg) => flashes.push(msg),
escapeHtml: (s) => s, escapeHtml: (s) => s,
goBack: () => {}, goBack: () => {},
trackToken: () => {}, tokensNotFetched: new Set(),
})); }));
require("../src/popup/views/" + view).init({ require("../src/popup/views/" + view).init({
+4 -178
View File
@@ -1447,20 +1447,6 @@ describe("uniswap decoder", () => {
USDT_ADDR, USDT_ADDR,
USDC_ADDR, USDC_ADDR,
]); ]);
// The lines of its 600 USDT when there is no Amount line.
// https://git.eeqj.de/sneak/AutistMask/issues/659 — on next at
// 5f2b606 no line gave the 600 USDT then.
const USDT_600_GROUP = [
["Moved from the sender", "600.0000 USDT"],
["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", "The swap step that spends it"],
];
// The same for a step's 400 USDT.
const USDT_400_GROUP = [
["Moved from the sender", "400.0000 USDT"],
["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", "The swap step that spends it"],
];
// V4 SETTLE of `amount` of `currency`, and TAKE of `amount` of // V4 SETTLE of `amount` of `currency`, and TAKE of `amount` of
// `currency` to `recipient`. // `currency` to `recipient`.
const settle = (currency, amount, payerIsUser) => const settle = (currency, amount, payerIsUser) =>
@@ -1687,7 +1673,6 @@ describe("uniswap decoder", () => {
encodeV4SettleAll(USDT_ADDR, USDT_400), encodeV4SettleAll(USDT_ADDR, USDT_400),
], ],
lines: [ lines: [
...USDT_600_GROUP,
["Moved from the sender", UNDECODED], ["Moved from the sender", UNDECODED],
["Token moved", "USDT (" + USDT_ADDR + ")"], ["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", "The swap step that spends it"], ["Moved to", "The swap step that spends it"],
@@ -1708,7 +1693,6 @@ describe("uniswap decoder", () => {
encodeV4TakeAll(USDC_ADDR, 0n), encodeV4TakeAll(USDC_ADDR, 0n),
], ],
lines: [ lines: [
...USDT_600_GROUP,
["Moved from the sender", UNDECODED], ["Moved from the sender", UNDECODED],
["Token moved", "USDT (" + USDT_ADDR + ")"], ["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", "The swap step that spends it"], ["Moved to", "The swap step that spends it"],
@@ -1840,8 +1824,6 @@ describe("uniswap decoder", () => {
// A step that may take from the sender what the decoder did not read // A step that may take from the sender what the decoder did not read
// leaves no Amount figure, which may be too low, and one line says so. // leaves no Amount figure, which may be too low, and one line says so.
// The amount of the step that set the input side then gets lines of
// its own.
const UNREAD_SWAP = V4_SWAP_ACTION_SHAPES[0].newer; const UNREAD_SWAP = V4_SWAP_ACTION_SHAPES[0].newer;
test.each([ test.each([
{ {
@@ -1889,7 +1871,6 @@ describe("uniswap decoder", () => {
[V3_600, encodeV4Swap(new Uint8Array(actions), params)], [V3_600, encodeV4Swap(new Uint8Array(actions), params)],
); );
expect(linesBetweenTokens(result)).toEqual([ expect(linesBetweenTokens(result)).toEqual([
...USDT_600_GROUP,
["Moved from the sender", UNDECODED], ["Moved from the sender", UNDECODED],
]); ]);
}); });
@@ -1912,9 +1893,6 @@ describe("uniswap decoder", () => {
], ],
), ),
], ],
// Nothing read says the user pays this step, so its 400 USDT
// gets no lines of its own.
first: [],
}, },
{ {
kind: "a V3 step of 600 USDT, then a V4 step whose input cannot be read", kind: "a V3 step of 600 USDT, then a V4 step whose input cannot be read",
@@ -1985,27 +1963,9 @@ describe("uniswap decoder", () => {
), ),
], ],
}, },
// Each take the total would add up gets lines of its own. ])("$kind", ({ commands, inputs }) => {
{
kind: "a V3 step of 600 USDT and a V2 step of 400 USDT the user pays for, then a PERMIT2_TRANSFER_FROM_BATCH",
commands: [0x00, 0x08, 0x0d],
inputs: [
V3_600,
encodeV2SwapExactIn(USER_ADDR, USDT_400, 390000000n, [
USDT_ADDR,
USDC_ADDR,
]),
coder.encode(
["tuple(address,address,uint160,address)[]"],
[[[USER_ADDR, OTHER_ADDR, USDT_400, USDT_ADDR]]],
),
],
first: [...USDT_600_GROUP, ...USDT_400_GROUP],
},
])("$kind", ({ commands, inputs, first = USDT_600_GROUP }) => {
const result = decodeSteps(commands, inputs); const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual([ expect(linesBetweenTokens(result)).toEqual([
...first,
["Moved from the sender", UNDECODED], ["Moved from the sender", UNDECODED],
]); ]);
}); });
@@ -2024,7 +1984,6 @@ describe("uniswap decoder", () => {
commands: [0x00, 0x08, 0x21], commands: [0x00, 0x08, 0x21],
inputs: [V3_600, V2_DAI_400, SUB_PLAN], inputs: [V3_600, V2_DAI_400, SUB_PLAN],
lines: [ lines: [
...USDT_600_GROUP,
["Moved from the sender", "400.0000 DAI"], ["Moved from the sender", "400.0000 DAI"],
["Token moved", "DAI (" + DAI_ADDR + ")"], ["Token moved", "DAI (" + DAI_ADDR + ")"],
["Moved to", "The swap step that spends it"], ["Moved to", "The swap step that spends it"],
@@ -2036,7 +1995,6 @@ describe("uniswap decoder", () => {
commands: [0x21, 0x00, 0x08], commands: [0x21, 0x00, 0x08],
inputs: [SUB_PLAN, V3_600, V2_DAI_400], inputs: [SUB_PLAN, V3_600, V2_DAI_400],
lines: [ lines: [
...USDT_600_GROUP,
["Moved from the sender", UNDECODED], ["Moved from the sender", UNDECODED],
["Moved from the sender", "400.0000 DAI"], ["Moved from the sender", "400.0000 DAI"],
["Token moved", "DAI (" + DAI_ADDR + ")"], ["Token moved", "DAI (" + DAI_ADDR + ")"],
@@ -2055,7 +2013,6 @@ describe("uniswap decoder", () => {
SUB_PLAN, SUB_PLAN,
], ],
lines: [ lines: [
...USDT_600_GROUP,
["Moved from the sender", UNDECODED], ["Moved from the sender", UNDECODED],
["Moved from the sender", "400.0000 DAI"], ["Moved from the sender", "400.0000 DAI"],
["Token moved", "DAI (" + DAI_ADDR + ")"], ["Token moved", "DAI (" + DAI_ADDR + ")"],
@@ -2333,103 +2290,6 @@ describe("uniswap decoder", () => {
expect(linesBetweenTokens(result)).toEqual(lines); expect(linesBetweenTokens(result)).toEqual(lines);
}); });
// A step that sets the input side and is paid from the router takes
// nothing from the sender, so with no Amount line its amount gets no
// lines of its own: in each route below the sender's 600 USDT moves
// into the router first, and such lines would say it left twice.
const INTO_ROUTER_600 = permit2TransferFrom(
USDT_ADDR,
ROUTER_ADDR,
USDT_600,
);
const V3_600_FROM_ROUTER = coder.encode(
["address", "uint256", "uint256", "bytes", "bool"],
[
USER_ADDR,
USDT_600,
590000000n,
encodeV3Path([USDT_ADDR, USDC_ADDR]),
false,
],
);
const TRANSFER_FROM_BATCH = coder.encode(
["tuple(address,address,uint160,address)[]"],
[[[USER_ADDR, OTHER_ADDR, USDT_400, USDT_ADDR]]],
);
test.each([
{
kind: "a PERMIT2_TRANSFER_FROM of 600 USDT into the router, a V3 exact-in of 600 USDT paid from the router, then a PERMIT2_TRANSFER_FROM_BATCH",
commands: [0x02, 0x00, 0x0d],
inputs: [
INTO_ROUTER_600,
V3_600_FROM_ROUTER,
TRANSFER_FROM_BATCH,
],
lines: [["Moved from the sender", UNDECODED]],
},
{
kind: "a PERMIT2_PERMIT that cannot be read, a PERMIT2_TRANSFER_FROM of 600 USDT into the router, then a V3 exact-in of 600 USDT paid from the router",
commands: [0x0a, 0x02, 0x00],
inputs: [
dirtyWord(
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
0,
),
INTO_ROUTER_600,
V3_600_FROM_ROUTER,
],
lines: [["Permit2 allowance", UNDECODED]],
},
{
kind: "a PERMIT2_TRANSFER_FROM of 600 USDT into the router, a V4 step swapping 600 USDT whose SETTLE the router pays, then a PERMIT2_TRANSFER_FROM_BATCH",
commands: [0x02, 0x10, 0x0d],
inputs: [
INTO_ROUTER_600,
encodeV4Swap(
new Uint8Array([
V4_SWAP_EXACT_IN_SINGLE_ID,
V4_SETTLE_ID,
V4_TAKE_ALL_ID,
]),
[
encodeV4ExactInSingle(
USDT_ADDR,
USDC_ADDR,
USDT_600,
590000000n,
),
settle(USDT_ADDR, USDT_600, false),
encodeV4TakeAll(USDC_ADDR, 0n),
],
),
TRANSFER_FROM_BATCH,
],
lines: [["Moved from the sender", UNDECODED]],
},
{
kind: "a PERMIT2_TRANSFER_FROM of 600 USDT into the router, a V2 exact-in of CONTRACT_BALANCE paid from the router, then an EXECUTE_SUB_PLAN",
commands: [0x02, 0x08, 0x21],
inputs: [
INTO_ROUTER_600,
coder.encode(
["address", "uint256", "uint256", "address[]", "bool"],
[
USER_ADDR,
2n ** 255n, // CONTRACT_BALANCE
590000000n,
[USDT_ADDR, USDC_ADDR],
false,
],
),
SUB_PLAN,
],
lines: [["Moved from the sender", UNDECODED]],
},
])("$kind", ({ commands, inputs, lines }) => {
const result = decodeSteps(commands, inputs);
expect(linesBetweenTokens(result)).toEqual(lines);
});
// A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax` // A V3_SWAP_EXACT_OUT the user pays for, of at most `amountInMax`
// USDT for USDC. // USDT for USDC.
const v3ExactOut = (amountInMax) => const v3ExactOut = (amountInMax) =>
@@ -2775,35 +2635,10 @@ describe("uniswap decoder", () => {
[input, V3_600], [input, V3_600],
); );
expect(linesBetweenTokens(result)).toEqual([ expect(linesBetweenTokens(result)).toEqual([
...USDT_600_GROUP,
["Permit2 allowance", UNDECODED], ["Permit2 allowance", UNDECODED],
]); ]);
}, },
); );
// https://git.eeqj.de/sneak/AutistMask/issues/659 — on next at
// 5f2b606 only the 400 DAI was shown.
test("a PERMIT2_PERMIT that cannot be read, then a V3 exact-in of 600 USDT and a V2 exact-in of 400 DAI, both paid by the user, shows both amounts", () => {
const result = decodeSteps(
[0x0a, 0x00, 0x08],
[
dirtyWord(
encodePermit2(USDT_ADDR, USDT_5000, ROUTER_ADDR),
0,
),
V3_600,
encodeV2SwapExactIn(USER_ADDR, DAI_400, 390000000n, [
DAI_ADDR,
USDC_ADDR,
]),
],
);
expect(linesBetweenTokens(result)).toEqual([
...USDT_600_GROUP,
...DAI_GROUP,
["Permit2 allowance", UNDECODED],
]);
});
}); });
// A V3_SWAP_EXACT_OUT whose path is shorter than one token cannot be // A V3_SWAP_EXACT_OUT whose path is shorter than one token cannot be
@@ -2815,12 +2650,7 @@ describe("uniswap decoder", () => {
[USER_ADDR, 390000000n, USDT_400, "0x", true], [USER_ADDR, 390000000n, USDT_400, "0x", true],
); );
const result = decodeSteps([0x00, 0x01], [V3_600, unreadable]); const result = decodeSteps([0x00, 0x01], [V3_600, unreadable]);
// With a V3 exact-out step, the 600 USDT reads as a maximum, as
// the Amount line would.
expect(linesBetweenTokens(result)).toEqual([ expect(linesBetweenTokens(result)).toEqual([
["Moved from the sender", "Up to 600.0000 USDT"],
["Token moved", "USDT (" + USDT_ADDR + ")"],
["Moved to", "The swap step that spends it"],
["Moved from the sender", UNDECODED], ["Moved from the sender", UNDECODED],
]); ]);
expect(detail(result, "Paid to").value).toBe(UNDECODED); expect(detail(result, "Paid to").value).toBe(UNDECODED);
@@ -2871,9 +2701,6 @@ describe("uniswap decoder", () => {
], ],
); );
expect(linesBetweenTokens(result)).toEqual([ expect(linesBetweenTokens(result)).toEqual([
["Moved from the sender", "Up to 0.5000 WETH"],
["Token moved", "WETH (" + WETH_ADDR + ")"],
["Moved to", "The swap step that spends it"],
["Moved from the sender", UNDECODED], ["Moved from the sender", UNDECODED],
...permitLines("1.0000 WETH", WETH_ADDR, "WETH", ROUTER_ADDR), ...permitLines("1.0000 WETH", WETH_ADDR, "WETH", ROUTER_ADDR),
]); ]);
@@ -3029,15 +2856,14 @@ describe("uniswap decoder", () => {
); );
// The V4 exact-out route above the other way round, where the later // The V4 exact-out route above the other way round, where the later
// take's unknown amount leaves no Amount figure and the 400 USDT gets // take's unknown amount leaves no figure at all, and other ways the
// lines of its own, and other ways the step that sets the input side // step that sets the input side states no amount.
// states no amount.
test.each([ test.each([
{ {
kind: "a V2 exact-out of at most 400 USDT, then a V4 exact-out step of at most 600 USDT", kind: "a V2 exact-out of at most 400 USDT, then a V4 exact-out step of at most 600 USDT",
commands: [0x09, 0x10], commands: [0x09, 0x10],
inputs: [V2_EXACT_OUT_400, V4_EXACT_OUT_600], inputs: [V2_EXACT_OUT_400, V4_EXACT_OUT_600],
lines: [...UP_TO_400_USDT, ...UNKNOWN_USDT], lines: UNKNOWN_USDT,
}, },
{ {
kind: "a PERMIT2_PERMIT of 1 USDT to another spender, a V4 exact-out step of at most 600 USDT, then a V2 exact-out of at most 400 USDT", kind: "a PERMIT2_PERMIT of 1 USDT to another spender, a V4 exact-out step of at most 600 USDT, then a V2 exact-out of at most 400 USDT",
+5 -16
View File
@@ -1439,17 +1439,8 @@ describe("any token left in the router is not received", () => {
"Token In", "Token In",
// A later step whose SETTLE or SETTLE_ALL the decoder cannot read // A later step whose SETTLE or SETTLE_ALL the decoder cannot read
// may take anything from the sender: one line says so, in place // may take anything from the sender: one line says so, in place
// of the Amount line (https://git.eeqj.de/sneak/AutistMask/issues/625), // of the Amount line (https://git.eeqj.de/sneak/AutistMask/issues/625).
// after the lines the WRAP_ETH's 1 ETH then gets unread ? "Moved from the sender" : "Amount",
// (https://git.eeqj.de/sneak/AutistMask/issues/659).
...(unread
? [
"Moved from the sender",
"Token moved",
"Moved to",
"Moved from the sender",
]
: ["Amount"]),
// A later swap step the user pays for spends `spent` of a token // A later swap step the user pays for spends `spent` of a token
// other than Token In. // other than Token In.
...(spent ...(spent
@@ -1466,11 +1457,9 @@ describe("any token left in the router is not received", () => {
expect(detail(result, "Moved from the sender").value).toBe(spent); expect(detail(result, "Moved from the sender").value).toBe(spent);
} }
if (unread) { if (unread) {
expect( expect(detail(result, "Moved from the sender").value).toBe(
result.details UNDECODED,
.filter((d) => d.label === "Moved from the sender") );
.map((d) => d.value),
).toEqual(["1.0000 ETH", UNDECODED]);
} }
expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]); expect(paidTo(result)).toEqual([STAYS_IN_ROUTER]);
expect(detail(result, "Token moved into the router")).toEqual(left); expect(detail(result, "Token moved into the router")).toEqual(left);