Compare commits
2
Commits
894d972caa
...
1a3fefde4a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a3fefde4a | ||
|
|
1a4fb0803d |
@@ -397,10 +397,16 @@ is missing, a write fails.
|
||||
|
||||
The popup's `saveState()` (`src/shared/state.js`) reads the stored record,
|
||||
merges this page's changes into it and writes it back. It writes only the fields
|
||||
this page changed since its last save. Wallets, site lists and endpoints are
|
||||
merged entry by entry, so two pages changing different things do not overwrite
|
||||
each other. The background's `updateState()` reads the record, applies one
|
||||
change and writes it back.
|
||||
this page changed since its last save. It merges the fields below entry by
|
||||
entry, so two pages changing different things do not overwrite each other:
|
||||
|
||||
- wallets;
|
||||
- site lists;
|
||||
- endpoints;
|
||||
- tracked tokens.
|
||||
|
||||
The background's `updateState()` reads the record, applies one change and writes
|
||||
it back.
|
||||
|
||||
Every write stamps `schemaVersion` with `STATE_SCHEMA_VERSION` from
|
||||
`src/shared/stateSchema.js`, currently `1`. The version is raised only when the
|
||||
@@ -1359,9 +1365,14 @@ when both tokens are known. The lines are:
|
||||
the sender that the `Amount` line does not include. The same three lines show
|
||||
what a `PERMIT2_TRANSFER_FROM` moves and where. A `WRAP_ETH` can pay its WETH
|
||||
to a recipient other than the router. When its amount gets these lines, its
|
||||
`Moved to` line names that recipient, or reads `The sender`. ETH that the
|
||||
sender pays a V4 step can get these three lines when the router may hold ETH
|
||||
from an earlier step. When the decoder knows the amount, its
|
||||
`Moved to` line names that recipient, or reads `The sender`. A `WRAP_ETH`
|
||||
after the step that sets `Token In` takes the amount it states from the
|
||||
sender. When it pays anyone but the router, that amount gets these lines. When
|
||||
it pays the router, that amount joins the `Amount` line if `Token In` is ETH,
|
||||
and gets these lines if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` states
|
||||
no amount, so it gets no lines. ETH that the sender pays a V4 step or a
|
||||
`WRAP_ETH` of a stated amount can get these three lines when the router may
|
||||
hold ETH from an earlier step. When the decoder knows the amount, its
|
||||
`Moved from the sender` line reads `Up to` and the amount.
|
||||
- `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance
|
||||
that a `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` grants, unless the permit's
|
||||
|
||||
+66
-22
@@ -404,6 +404,36 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
||||
return mergeMapByKey(base, ours, theirs, mergeEndpointEntry);
|
||||
}
|
||||
|
||||
// Merges one network's tracked tokens by contract address:
|
||||
//
|
||||
// - A token another page added or removed since this page's last save stays
|
||||
// added or removed.
|
||||
// - A token that this page and `theirs` both hold keeps this page's record.
|
||||
// - The result holds one record per contract address.
|
||||
//
|
||||
// mergeListByIdentity() keeps records that share an identity, on purpose, for
|
||||
// wallets. A contract address names one token, so this keeps one record for it.
|
||||
// Without that, a token that `ours` and `theirs` each hold twice comes back four
|
||||
// times, and each later merge multiplies it again.
|
||||
//
|
||||
// `theirs` takes this page's records first. mergeListByIdentity() would
|
||||
// otherwise log an error for two different records of one token.
|
||||
function mergeTrackedTokens(base, ours, theirs) {
|
||||
const contract = (token) => token.address.toLowerCase();
|
||||
const oursByContract = new Map(
|
||||
ours.map((token) => [contract(token), token]),
|
||||
);
|
||||
const merged = mergeListByIdentity(
|
||||
base,
|
||||
ours,
|
||||
theirs.map((token) => oursByContract.get(contract(token)) || token),
|
||||
contract,
|
||||
(baseToken, ourToken) => ourToken,
|
||||
);
|
||||
const byContract = new Map(merged.map((token) => [contract(token), token]));
|
||||
return [...byContract.values()];
|
||||
}
|
||||
|
||||
// Returns a copy of `stored.networkTrackedTokens` for when this page has not yet
|
||||
// taken the switch in `stored`. `page` is this page's state. A token that this
|
||||
// page tracked or removed since its last save is tracked or removed in the list
|
||||
@@ -411,12 +441,10 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
||||
function networkTrackedTokensWithChange(stored, page) {
|
||||
return {
|
||||
...stored.networkTrackedTokens,
|
||||
[page.networkId]: mergeListByIdentity(
|
||||
[page.networkId]: mergeTrackedTokens(
|
||||
baseline.trackedTokens,
|
||||
page.trackedTokens,
|
||||
stored.networkTrackedTokens[page.networkId],
|
||||
(token) => token.address.toLowerCase(),
|
||||
(base, ours) => ours,
|
||||
stored.networkTrackedTokens[page.networkId] || [],
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -461,23 +489,27 @@ function networkTrackedTokensWithChange(stored, page) {
|
||||
// comment at mergeEndpointEntry() for why the collision this closes is
|
||||
// milder than the other two.
|
||||
//
|
||||
// `trackedTokens` is merged by contract address (mergeTrackedTokens()) when this
|
||||
// page has changed it, so a token another page added or removed since this
|
||||
// page's last save stays added or removed. A token both lists hold keeps this
|
||||
// page's record. Two saves differ:
|
||||
//
|
||||
// - When the stored record is on another network than this page's, a save keeps
|
||||
// the stored `trackedTokens`. It merges this page's change into the list in
|
||||
// `networkTrackedTokens` for this page's network, by contract address.
|
||||
// - A save that switches networks writes this page's list whole. A switch in
|
||||
// Settings first merges that list by contract address
|
||||
// (takeStoredTrackedTokens()).
|
||||
//
|
||||
// Every other persisted field stays a whole-field diff:
|
||||
// `fraudContracts`/`viewStack` are arrays of scalars with no per-element
|
||||
// identity to merge by; a save diffs `trackedTokens` whole, and a switch in
|
||||
// Settings first merges that list by contract address
|
||||
// (takeStoredTrackedTokens()); `networkTrackedTokens` is diffed whole like
|
||||
// `trackedTokens` and `networkId`, the two fields a network switch moves
|
||||
// together with it; `tokenHolderCache` is a map shaped like the ones above,
|
||||
// but nothing in src/ ever writes an entry into it — it is only ever reset
|
||||
// wholesale to `{}` (applyChainSwitchFields()) — so there is no in-place
|
||||
// mutation for a whole-field diff to collide with; `viewData` is this page's
|
||||
// own UI scratch space, not data another page has any reason to share
|
||||
// membership of.
|
||||
//
|
||||
// One exception: when the stored record is on another network than this
|
||||
// page's, a save keeps the stored `trackedTokens`. It merges this page's change
|
||||
// into the list in `networkTrackedTokens` for this page's network, by contract
|
||||
// address.
|
||||
// identity to merge by; `networkTrackedTokens` is diffed whole like `networkId`,
|
||||
// which a network switch moves together with it; `tokenHolderCache` is a map
|
||||
// shaped like the ones above, but nothing in src/ ever writes an entry into it —
|
||||
// it is only ever reset wholesale to `{}` (applyChainSwitchFields()) — so there
|
||||
// is no in-place mutation for a whole-field diff to collide with; `viewData` is
|
||||
// this page's own UI scratch space, not data another page has any reason to
|
||||
// share membership of.
|
||||
//
|
||||
// This does not make two pages that both change the SAME leaf concurrently
|
||||
// safe: last write wins on that one leaf, same as before. What it removes
|
||||
@@ -564,6 +596,20 @@ async function saveStateOnce(beforeWrite) {
|
||||
fresh,
|
||||
current,
|
||||
);
|
||||
} else if (
|
||||
baseline !== null &&
|
||||
baseline.networkId === current.networkId &&
|
||||
!deepEqual(current.trackedTokens, baseline.trackedTokens)
|
||||
) {
|
||||
// This page's network is the one it last saved on, and the stored
|
||||
// record is on it too, so all three lists are of one network. This
|
||||
// page's change goes into the stored list, and another page's add or
|
||||
// removal stays (https://git.eeqj.de/sneak/AutistMask/issues/717).
|
||||
merged.trackedTokens = mergeTrackedTokens(
|
||||
baseline.trackedTokens,
|
||||
current.trackedTokens,
|
||||
fresh.trackedTokens,
|
||||
);
|
||||
}
|
||||
merged.hasWallet = Boolean(merged.wallets && merged.wallets.length > 0);
|
||||
// Stamped on every write, never merged or diffed: the record that goes to
|
||||
@@ -680,12 +726,10 @@ function takeStoredNetwork(stored) {
|
||||
// `baseline` takes the stored list, so the save stores the list in `state`
|
||||
// whenever the two differ.
|
||||
function takeStoredTrackedTokens(stored) {
|
||||
rawState.trackedTokens = mergeListByIdentity(
|
||||
rawState.trackedTokens = mergeTrackedTokens(
|
||||
baseline.trackedTokens,
|
||||
rawState.trackedTokens,
|
||||
stored.trackedTokens,
|
||||
(token) => token.address.toLowerCase(),
|
||||
(base, ours) => ours,
|
||||
);
|
||||
baseline.trackedTokens = structuredClone(stored.trackedTokens);
|
||||
}
|
||||
|
||||
+30
-7
@@ -1368,7 +1368,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// After the loop each joins the Amount line or gets lines of its own
|
||||
// (see permitStands). A take with `atMost` set may take less than
|
||||
// `amount`: it never joins the Amount line, and its line reads
|
||||
// "Up to" and the amount.
|
||||
// "Up to" and the amount. A take with a `recipient`, the ETH of a
|
||||
// later WRAP_ETH that pays anyone but the router, never joins the
|
||||
// Amount line either: its Moved to line names that recipient.
|
||||
const userPaid = [];
|
||||
// A swap step the user pays for (`payerIsUser` true) takes `amount`
|
||||
// of `token` from the sender, or at most that when `atMost` is set. A
|
||||
@@ -1380,8 +1382,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
};
|
||||
// A step may take from the sender what this decoder did not read: a
|
||||
// V4 step whose input or one of whose SETTLEs or SETTLE_ALLs it
|
||||
// cannot read; a V2 or V3 swap step or PERMIT2_TRANSFER_FROM whose
|
||||
// input it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; an
|
||||
// cannot read; a V2 or V3 swap step, PERMIT2_TRANSFER_FROM or WRAP_ETH
|
||||
// whose input it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; 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
|
||||
@@ -1907,6 +1909,25 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
if (!isRouter(w.recipient)) {
|
||||
wrapEthRecipient = w.recipient;
|
||||
}
|
||||
} else if (
|
||||
w.amount > 0n &&
|
||||
w.amount !== CONTRACT_BALANCE
|
||||
) {
|
||||
// The ETH of a stated amount comes from the
|
||||
// transaction's value. ETH an earlier step left in
|
||||
// the router may pay part of it, and the sender
|
||||
// then pays at most that amount. CONTRACT_BALANCE
|
||||
// states no amount to take.
|
||||
userPaid.push({
|
||||
token: "0x0000000000000000000000000000000000000000",
|
||||
amount: w.amount,
|
||||
atMost: heldByRouter.has(
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
),
|
||||
recipient: isRouter(w.recipient)
|
||||
? null
|
||||
: w.recipient,
|
||||
});
|
||||
}
|
||||
addPayee(w.recipient);
|
||||
// CONTRACT_BALANCE wraps all the router's ETH.
|
||||
@@ -1931,6 +1952,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
} else {
|
||||
payeeUnread = true;
|
||||
stepsUnread.push(i);
|
||||
spentUnread();
|
||||
}
|
||||
} else if (command === "V4_SWAP") {
|
||||
const v4 = decodeV4Swap(inputs[i]);
|
||||
@@ -2203,8 +2225,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// added to it while both are quantities. A take that cannot be added,
|
||||
// being of another token, coming after an amount that is not a
|
||||
// quantity (a V4 open delta, ALREADY_PAID, or none stated), or having
|
||||
// `atMost` set, gets lines of its own. One whose amount is not known
|
||||
// also leaves the total unknown, with no figure.
|
||||
// `atMost` or a `recipient` set, gets lines of its own. One whose
|
||||
// amount is not known also leaves the total unknown, with no figure.
|
||||
//
|
||||
// A PERMIT2_PERMIT names Token In only when its amount stands as the
|
||||
// Amount figure in place of the total, which it does only in the
|
||||
@@ -2245,7 +2267,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
// { token, amount, atMost, recipient }. A null token is a step this
|
||||
// decoder did not read (see spentUnread()), or a token it does not
|
||||
// know. `atMost` is as in `userPaid`. Only the take of a WRAP_ETH that
|
||||
// pays elsewhere has a recipient (see wrapEthRecipient).
|
||||
// pays elsewhere has a recipient (see wrapEthRecipient and userPaid).
|
||||
const spentFromSender = [];
|
||||
let total = null;
|
||||
// The takes from the sender that `total` adds up, each a quantity, in
|
||||
@@ -2292,7 +2314,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
||||
ofTokenIn &&
|
||||
typeof total === "bigint" &&
|
||||
typeof t.amount === "bigint" &&
|
||||
!t.atMost
|
||||
!t.atMost &&
|
||||
!present(t.recipient)
|
||||
) {
|
||||
total += t.amount;
|
||||
inTotal.push(t);
|
||||
|
||||
@@ -117,6 +117,17 @@ async function chooseNetwork(boot, networkId) {
|
||||
await boot.settle();
|
||||
}
|
||||
|
||||
// Adds NEW_TOKEN_ADDRESS by contract address on SettingsAddToken, as the user
|
||||
// does. Returns the record the add tracks.
|
||||
async function addNewToken(boot) {
|
||||
const { lookupTokenInfo } = require("../src/shared/balances");
|
||||
const info = { symbol: "NEW", decimals: 18, name: "New Token" };
|
||||
lookupTokenInfo.mockResolvedValueOnce(info);
|
||||
boot.node("settings-addtoken-address").value = NEW_TOKEN_ADDRESS;
|
||||
await boot.click("btn-settings-addtoken-manual");
|
||||
return { address: NEW_TOKEN_ADDRESS, ...info, balanceNotFetched: true };
|
||||
}
|
||||
|
||||
// The Settings gear, a switch to Sepolia, and Back.
|
||||
async function gearSwitchBack(boot) {
|
||||
await boot.click("btn-settings");
|
||||
@@ -685,6 +696,113 @@ test("a token removed while this page waits to take another page's switch is gon
|
||||
expect(stored.networkTrackedTokens.mainnet).toEqual([]);
|
||||
});
|
||||
|
||||
// Another popup page tracks DAI after this page's last save. This page then
|
||||
// adds a token. Its save keeps DAI.
|
||||
test("a token another page added stays when this page adds a token", async () => {
|
||||
const boot = await bootOnAddToken("mainnet");
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
boot.storage.write("autistmask", {
|
||||
...boot.storage.read("autistmask"),
|
||||
trackedTokens: [MAINNET_TOKEN],
|
||||
});
|
||||
|
||||
const added = await addNewToken(boot);
|
||||
|
||||
expect(boot.storage.read("autistmask").trackedTokens).toEqual([
|
||||
MAINNET_TOKEN,
|
||||
added,
|
||||
]);
|
||||
});
|
||||
|
||||
// Another popup page tracks a new token after this page's last save. This page
|
||||
// then removes DAI, as the `[x]` on a tracked token in Settings does. Its save
|
||||
// keeps the new token.
|
||||
test("a token another page added stays when this page removes a token", async () => {
|
||||
const boot = await bootPopup(
|
||||
unversionedValidProfile({ trackedTokens: [MAINNET_TOKEN] }),
|
||||
);
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
const otherPageToken = {
|
||||
address: NEW_TOKEN_ADDRESS,
|
||||
symbol: "NEW",
|
||||
decimals: 18,
|
||||
name: "New Token",
|
||||
};
|
||||
boot.storage.write("autistmask", {
|
||||
...boot.storage.read("autistmask"),
|
||||
trackedTokens: [MAINNET_TOKEN, otherPageToken],
|
||||
});
|
||||
|
||||
const { state, saveState } = require("../src/shared/state");
|
||||
state.trackedTokens.splice(0, 1);
|
||||
await saveState();
|
||||
|
||||
expect(boot.storage.read("autistmask").trackedTokens).toEqual([
|
||||
otherPageToken,
|
||||
]);
|
||||
});
|
||||
|
||||
// Another popup page stops tracking DAI after this page's last save. This page
|
||||
// still holds DAI and adds a token. Its save does not store DAI again.
|
||||
test("a token another page removed stays removed when this page adds a token", async () => {
|
||||
const boot = await bootOnAddToken("mainnet", {
|
||||
trackedTokens: [MAINNET_TOKEN],
|
||||
});
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
boot.storage.write("autistmask", {
|
||||
...boot.storage.read("autistmask"),
|
||||
trackedTokens: [],
|
||||
});
|
||||
|
||||
const added = await addNewToken(boot);
|
||||
|
||||
expect(boot.storage.read("autistmask").trackedTokens).toEqual([added]);
|
||||
});
|
||||
|
||||
// Another popup page tracks DAI and the new token after this page's last save,
|
||||
// and its refresh has read the new token's balance. This page adds the new token
|
||||
// too, so the two pages hold different records for it. The save stores one
|
||||
// record for the new token, this page's, and keeps DAI.
|
||||
test("a token both pages added is stored once, with this page's record", async () => {
|
||||
const boot = await bootOnAddToken("mainnet");
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
boot.storage.write("autistmask", {
|
||||
...boot.storage.read("autistmask"),
|
||||
trackedTokens: [
|
||||
MAINNET_TOKEN,
|
||||
{
|
||||
address: NEW_TOKEN_ADDRESS,
|
||||
symbol: "NEW",
|
||||
decimals: 18,
|
||||
name: "New Token",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const added = await addNewToken(boot);
|
||||
|
||||
expect(boot.storage.read("autistmask").trackedTokens).toEqual([
|
||||
MAINNET_TOKEN,
|
||||
added,
|
||||
]);
|
||||
});
|
||||
|
||||
// This page and the stored list each hold DAI twice. This page then adds a
|
||||
// token. Its save stores DAI once.
|
||||
test("a token held twice is stored once when this page adds a token", async () => {
|
||||
const boot = await bootOnAddToken("mainnet", {
|
||||
trackedTokens: [MAINNET_TOKEN, MAINNET_TOKEN],
|
||||
});
|
||||
expect(boot.pageErrors).toEqual([]);
|
||||
|
||||
const added = await addNewToken(boot);
|
||||
|
||||
expect(boot.storage.read("autistmask").trackedTokens).toEqual([
|
||||
MAINNET_TOKEN,
|
||||
added,
|
||||
]);
|
||||
});
|
||||
|
||||
describe("Back after a switch from the Settings gear", () => {
|
||||
test("SettingsAddToken no longer offers the common tokens", async () => {
|
||||
const boot = await bootOnAddToken("mainnet");
|
||||
|
||||
@@ -2593,6 +2593,9 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
lines: [
|
||||
["Amount", "1.0000 ETH"],
|
||||
["Moved from the sender", "Up to 1.0000 ETH"],
|
||||
["Token moved", "ETH"],
|
||||
["Moved to", OTHER_ADDR],
|
||||
...ethLines("Up to 1.0000 ETH"),
|
||||
],
|
||||
},
|
||||
@@ -2622,6 +2625,7 @@ describe("uniswap decoder", () => {
|
||||
],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The swap step that spends it"),
|
||||
...ethLines("Up to 1.0000 ETH"),
|
||||
],
|
||||
},
|
||||
@@ -2753,6 +2757,172 @@ describe("uniswap decoder", () => {
|
||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||
});
|
||||
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/721 — a WRAP_ETH that
|
||||
// does not set the input side takes the amount it states from the
|
||||
// sender. On next no line gave that ETH. The rows marked "reads as on
|
||||
// next" state no amount to take from the sender. Uniswap's own routes
|
||||
// wrap CONTRACT_BALANCE after their first step, as the last two rows
|
||||
// do.
|
||||
const CONTRACT_BALANCE = 2n ** 255n;
|
||||
const WRAP_TO_OTHER = encodeWrapEth(OTHER_ADDR, ONE_ETH);
|
||||
test.each([
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of 1 ETH to another address",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, WRAP_TO_OTHER],
|
||||
lines: [["Amount", "600.0000 USDT"], ...wrapLines(OTHER_ADDR)],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of 1 ETH to address(1)",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, encodeWrapEth(MSG_SENDER, ONE_ETH)],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The sender"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of 1 ETH to address(2)",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, WRAP_INTO_ROUTER],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The swap step that spends it"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of 1 ETH to the router by its own address",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, encodeWrapEth(ROUTER_ADDR, ONE_ETH)],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
...wrapLines("The swap step that spends it"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, then a WRAP_ETH of 1 ETH to another address",
|
||||
commands: [0x0b, 0x0b],
|
||||
inputs: [WRAP_INTO_ROUTER, WRAP_TO_OTHER],
|
||||
lines: [["Amount", "1.0000 ETH"], ...wrapLines(OTHER_ADDR)],
|
||||
},
|
||||
{
|
||||
kind: "two WRAP_ETHs of 1 ETH into the router, which the Amount line adds up",
|
||||
commands: [0x0b, 0x0b],
|
||||
inputs: [WRAP_INTO_ROUTER, WRAP_INTO_ROUTER],
|
||||
lines: [["Amount", "2.0000 ETH"]],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT for WETH paid to the router, an UNWRAP_WETH into the router, then a WRAP_ETH of 1 ETH to another address",
|
||||
commands: [0x00, 0x0c, 0x0b],
|
||||
inputs: [
|
||||
encodeV3SwapExactIn(ADDRESS_THIS, USDT_600, 1n, [
|
||||
USDT_ADDR,
|
||||
WETH_ADDR,
|
||||
]),
|
||||
UNWRAP_INTO_ROUTER,
|
||||
WRAP_TO_OTHER,
|
||||
],
|
||||
lines: [
|
||||
["Amount", "600.0000 USDT"],
|
||||
["Moved from the sender", "Up to 1.0000 ETH"],
|
||||
["Token moved", "ETH"],
|
||||
["Moved to", OTHER_ADDR],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a WRAP_ETH of 1 ETH into the router",
|
||||
commands: [0x0b, 0x0c, 0x0b],
|
||||
inputs: [
|
||||
WRAP_INTO_ROUTER,
|
||||
UNWRAP_INTO_ROUTER,
|
||||
WRAP_INTO_ROUTER,
|
||||
],
|
||||
lines: [
|
||||
["Amount", "1.0000 ETH"],
|
||||
...ethLines("Up to 1.0000 ETH"),
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a PERMIT2_PERMIT of 5,000 USDT to the router, a V3 exact-in of 600 USDT, then a WRAP_ETH of 1 ETH to another address",
|
||||
commands: [0x0a, 0x00, 0x0b],
|
||||
inputs: [
|
||||
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
|
||||
V3_600,
|
||||
WRAP_TO_OTHER,
|
||||
],
|
||||
lines: [["Amount", "5000.0000 USDT"], ...wrapLines(OTHER_ADDR)],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH to another address, then an EXECUTE_SUB_PLAN",
|
||||
commands: [0x00, 0x0b, 0x21],
|
||||
inputs: [V3_600, WRAP_TO_OTHER, SUB_PLAN],
|
||||
lines: [
|
||||
...USDT_600_GROUP,
|
||||
...wrapLines(OTHER_ADDR),
|
||||
["Moved from the sender", UNDECODED],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH whose input this decoder cannot read",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, "0x1234"],
|
||||
lines: [
|
||||
...USDT_600_GROUP,
|
||||
["Moved from the sender", UNDECODED],
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of nothing to another address, reads as on next",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, encodeWrapEth(OTHER_ADDR, 0n)],
|
||||
lines: [["Amount", "600.0000 USDT"]],
|
||||
},
|
||||
{
|
||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to the sender, reads as on next",
|
||||
commands: [0x00, 0x0b],
|
||||
inputs: [V3_600, encodeWrapEth(MSG_SENDER, CONTRACT_BALANCE)],
|
||||
lines: [["Amount", "600.0000 USDT"]],
|
||||
},
|
||||
{
|
||||
kind: "a V4 swap of 600 USDT that takes ETH into the router, a WRAP_ETH of CONTRACT_BALANCE into the router, then a V3 exact-in of it, reads as on next",
|
||||
commands: [0x10, 0x0b, 0x00],
|
||||
inputs: [
|
||||
encodeV4Swap(
|
||||
new Uint8Array([
|
||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
||||
V4_SETTLE_ID,
|
||||
V4_TAKE_ID,
|
||||
]),
|
||||
[
|
||||
encodeV4ExactInSingle(
|
||||
USDT_ADDR,
|
||||
ZERO_ADDR,
|
||||
USDT_600,
|
||||
1n,
|
||||
),
|
||||
settle(USDT_ADDR, 0n, true),
|
||||
take(ZERO_ADDR, ADDRESS_THIS, 0n),
|
||||
],
|
||||
),
|
||||
encodeWrapEth(ADDRESS_THIS, CONTRACT_BALANCE),
|
||||
coder.encode(
|
||||
["address", "uint256", "uint256", "bytes", "bool"],
|
||||
[
|
||||
USER_ADDR,
|
||||
CONTRACT_BALANCE,
|
||||
1n,
|
||||
encodeV3Path([WETH_ADDR, USDC_ADDR]),
|
||||
false,
|
||||
],
|
||||
),
|
||||
],
|
||||
lines: [["Amount", "600.0000 USDT"]],
|
||||
},
|
||||
])("$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`
|
||||
// USDT for USDC.
|
||||
const v3ExactOut = (amountInMax) =>
|
||||
|
||||
@@ -370,7 +370,8 @@ const PAID_ELSEWHERE = [
|
||||
"Steps",
|
||||
"Deadline",
|
||||
];
|
||||
// `lines` with the lines of one PERMIT2_TRANSFER_FROM after Amount.
|
||||
// `lines` with the lines of one PERMIT2_TRANSFER_FROM or later WRAP_ETH after
|
||||
// Amount.
|
||||
const withMove = (lines) =>
|
||||
lines.flatMap((l) =>
|
||||
l === "Amount"
|
||||
@@ -985,7 +986,7 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
||||
);
|
||||
|
||||
// The SWEEP pays the user the swap's WETH, so the UNWRAP_WETH unwraps the
|
||||
// WRAP_ETH's.
|
||||
// WRAP_ETH's. The WRAP_ETH's 1 ETH gets lines of its own.
|
||||
test("a SWEEP of the swap's WETH to the user, then a WRAP_ETH into the router, reads as ETH with no minimum", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
@@ -996,7 +997,10 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → ETH");
|
||||
expect(labels(result)).toEqual(NO_MINIMUM_TO_USER);
|
||||
expect(labels(result)).toEqual(withMove(NO_MINIMUM_TO_USER));
|
||||
expect(detail(result, "Moved from the sender").value).toBe(
|
||||
"1.0000 ETH",
|
||||
);
|
||||
});
|
||||
|
||||
test("an UNWRAP_WETH into the router, then a WRAP_ETH of all its ETH to the user, reads as WETH with no minimum", () => {
|
||||
@@ -1097,7 +1101,9 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
||||
},
|
||||
);
|
||||
|
||||
// The user receives 0.5 WETH and the rest of the swap's output as ETH.
|
||||
// The user receives 0.5 WETH and the rest of the swap's output as ETH. The
|
||||
// router holds ETH when the WRAP_ETH runs, so the sender pays at most its
|
||||
// 0.5 ETH.
|
||||
test("an UNWRAP_WETH into the router, then a WRAP_ETH of 0.5 ETH to the user, reads as WETH with no minimum", () => {
|
||||
const result = decode(
|
||||
execute(
|
||||
@@ -1108,7 +1114,11 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
||||
),
|
||||
);
|
||||
expect(result.name).toBe("Swap USDT → WETH");
|
||||
expect(labels(result)).toEqual(NO_MINIMUM_TO_USER);
|
||||
expect(labels(result)).toEqual(withMove(NO_MINIMUM_TO_USER));
|
||||
expect(detail(result, "Moved from the sender").value).toBe(
|
||||
"Up to 0.5000 ETH",
|
||||
);
|
||||
expect(detail(result, "Moved to").value).toBe("The sender");
|
||||
});
|
||||
|
||||
// The UNWRAP_WETH pays the user the swap's WETH as ETH; the WRAP_ETH wraps
|
||||
@@ -1348,6 +1358,7 @@ describe("any token left in the router is not received", () => {
|
||||
),
|
||||
),
|
||||
left: tokenLine(WETH, "WETH"),
|
||||
spent: "1.0000 ETH",
|
||||
},
|
||||
// A SETTLE of OPEN_DELTA from the router pays only what the step
|
||||
// still owes of its currency when it runs.
|
||||
@@ -1516,8 +1527,8 @@ describe("any token left in the router is not received", () => {
|
||||
"Moved from the sender",
|
||||
]
|
||||
: ["Amount"]),
|
||||
// A later swap step the user pays for spends `spent` of a token
|
||||
// other than Token In.
|
||||
// A later swap step or WRAP_ETH the user pays for spends `spent`
|
||||
// of a token other than Token In.
|
||||
...(spent
|
||||
? ["Moved from the sender", "Token moved", "Moved to"]
|
||||
: []),
|
||||
|
||||
Reference in New Issue
Block a user