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,
|
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
|
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
|
this page changed since its last save. It merges the fields below entry by
|
||||||
merged entry by entry, so two pages changing different things do not overwrite
|
entry, so two pages changing different things do not overwrite each other:
|
||||||
each other. The background's `updateState()` reads the record, applies one
|
|
||||||
change and writes it back.
|
- 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
|
Every write stamps `schemaVersion` with `STATE_SCHEMA_VERSION` from
|
||||||
`src/shared/stateSchema.js`, currently `1`. The version is raised only when the
|
`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
|
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
|
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
|
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
|
`Moved to` line names that recipient, or reads `The sender`. A `WRAP_ETH`
|
||||||
sender pays a V4 step can get these three lines when the router may hold ETH
|
after the step that sets `Token In` takes the amount it states from the
|
||||||
from an earlier step. When the decoder knows the amount, its
|
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.
|
`Moved from the sender` line reads `Up to` and the amount.
|
||||||
- `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance
|
- `Permit2 allowance`, `Allowance token` and `Allowance spender`: each allowance
|
||||||
that a `PERMIT2_PERMIT` or `PERMIT2_PERMIT_BATCH` grants, unless the permit's
|
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);
|
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
|
// 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
|
// 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
|
// 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) {
|
function networkTrackedTokensWithChange(stored, page) {
|
||||||
return {
|
return {
|
||||||
...stored.networkTrackedTokens,
|
...stored.networkTrackedTokens,
|
||||||
[page.networkId]: mergeListByIdentity(
|
[page.networkId]: mergeTrackedTokens(
|
||||||
baseline.trackedTokens,
|
baseline.trackedTokens,
|
||||||
page.trackedTokens,
|
page.trackedTokens,
|
||||||
stored.networkTrackedTokens[page.networkId],
|
stored.networkTrackedTokens[page.networkId] || [],
|
||||||
(token) => token.address.toLowerCase(),
|
|
||||||
(base, ours) => ours,
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -461,23 +489,27 @@ function networkTrackedTokensWithChange(stored, page) {
|
|||||||
// comment at mergeEndpointEntry() for why the collision this closes is
|
// comment at mergeEndpointEntry() for why the collision this closes is
|
||||||
// milder than the other two.
|
// 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:
|
// Every other persisted field stays a whole-field diff:
|
||||||
// `fraudContracts`/`viewStack` are arrays of scalars with no per-element
|
// `fraudContracts`/`viewStack` are arrays of scalars with no per-element
|
||||||
// identity to merge by; a save diffs `trackedTokens` whole, and a switch in
|
// identity to merge by; `networkTrackedTokens` is diffed whole like `networkId`,
|
||||||
// Settings first merges that list by contract address
|
// which a network switch moves together with it; `tokenHolderCache` is a map
|
||||||
// (takeStoredTrackedTokens()); `networkTrackedTokens` is diffed whole like
|
// shaped like the ones above, but nothing in src/ ever writes an entry into it —
|
||||||
// `trackedTokens` and `networkId`, the two fields a network switch moves
|
// it is only ever reset wholesale to `{}` (applyChainSwitchFields()) — so there
|
||||||
// together with it; `tokenHolderCache` is a map shaped like the ones above,
|
// is no in-place mutation for a whole-field diff to collide with; `viewData` is
|
||||||
// but nothing in src/ ever writes an entry into it — it is only ever reset
|
// this page's own UI scratch space, not data another page has any reason to
|
||||||
// wholesale to `{}` (applyChainSwitchFields()) — so there is no in-place
|
// share membership of.
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// This does not make two pages that both change the SAME leaf concurrently
|
// 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
|
// safe: last write wins on that one leaf, same as before. What it removes
|
||||||
@@ -564,6 +596,20 @@ async function saveStateOnce(beforeWrite) {
|
|||||||
fresh,
|
fresh,
|
||||||
current,
|
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);
|
merged.hasWallet = Boolean(merged.wallets && merged.wallets.length > 0);
|
||||||
// Stamped on every write, never merged or diffed: the record that goes to
|
// 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`
|
// `baseline` takes the stored list, so the save stores the list in `state`
|
||||||
// whenever the two differ.
|
// whenever the two differ.
|
||||||
function takeStoredTrackedTokens(stored) {
|
function takeStoredTrackedTokens(stored) {
|
||||||
rawState.trackedTokens = mergeListByIdentity(
|
rawState.trackedTokens = mergeTrackedTokens(
|
||||||
baseline.trackedTokens,
|
baseline.trackedTokens,
|
||||||
rawState.trackedTokens,
|
rawState.trackedTokens,
|
||||||
stored.trackedTokens,
|
stored.trackedTokens,
|
||||||
(token) => token.address.toLowerCase(),
|
|
||||||
(base, ours) => ours,
|
|
||||||
);
|
);
|
||||||
baseline.trackedTokens = structuredClone(stored.trackedTokens);
|
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
|
// 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
|
// (see permitStands). A take with `atMost` set may take less than
|
||||||
// `amount`: it never joins the Amount line, and its line reads
|
// `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 = [];
|
const userPaid = [];
|
||||||
// A swap step the user pays for (`payerIsUser` true) takes `amount`
|
// 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
|
// 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
|
// 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
|
// 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
|
// cannot read; a V2 or V3 swap step, PERMIT2_TRANSFER_FROM or WRAP_ETH
|
||||||
// input it cannot read; a PERMIT2_TRANSFER_FROM_BATCH; an
|
// 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
|
// 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
|
// 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
|
// 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)) {
|
if (!isRouter(w.recipient)) {
|
||||||
wrapEthRecipient = 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);
|
addPayee(w.recipient);
|
||||||
// CONTRACT_BALANCE wraps all the router's ETH.
|
// CONTRACT_BALANCE wraps all the router's ETH.
|
||||||
@@ -1931,6 +1952,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
} else {
|
} else {
|
||||||
payeeUnread = true;
|
payeeUnread = true;
|
||||||
stepsUnread.push(i);
|
stepsUnread.push(i);
|
||||||
|
spentUnread();
|
||||||
}
|
}
|
||||||
} else if (command === "V4_SWAP") {
|
} else if (command === "V4_SWAP") {
|
||||||
const v4 = decodeV4Swap(inputs[i]);
|
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,
|
// 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
|
// being of another token, coming after an amount that is not a
|
||||||
// quantity (a V4 open delta, ALREADY_PAID, or none stated), or having
|
// 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
|
// `atMost` or a `recipient` set, gets lines of its own. One whose
|
||||||
// also leaves the total unknown, with no figure.
|
// 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
|
// 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
|
// 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
|
// { token, amount, atMost, recipient }. A null token is a step this
|
||||||
// decoder did not read (see spentUnread()), or a token it does not
|
// 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
|
// 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 = [];
|
const spentFromSender = [];
|
||||||
let total = null;
|
let total = null;
|
||||||
// The takes from the sender that `total` adds up, each a quantity, in
|
// 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 &&
|
ofTokenIn &&
|
||||||
typeof total === "bigint" &&
|
typeof total === "bigint" &&
|
||||||
typeof t.amount === "bigint" &&
|
typeof t.amount === "bigint" &&
|
||||||
!t.atMost
|
!t.atMost &&
|
||||||
|
!present(t.recipient)
|
||||||
) {
|
) {
|
||||||
total += t.amount;
|
total += t.amount;
|
||||||
inTotal.push(t);
|
inTotal.push(t);
|
||||||
|
|||||||
@@ -117,6 +117,17 @@ async function chooseNetwork(boot, networkId) {
|
|||||||
await boot.settle();
|
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.
|
// The Settings gear, a switch to Sepolia, and Back.
|
||||||
async function gearSwitchBack(boot) {
|
async function gearSwitchBack(boot) {
|
||||||
await boot.click("btn-settings");
|
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([]);
|
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", () => {
|
describe("Back after a switch from the Settings gear", () => {
|
||||||
test("SettingsAddToken no longer offers the common tokens", async () => {
|
test("SettingsAddToken no longer offers the common tokens", async () => {
|
||||||
const boot = await bootOnAddToken("mainnet");
|
const boot = await bootOnAddToken("mainnet");
|
||||||
|
|||||||
@@ -2593,6 +2593,9 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
lines: [
|
lines: [
|
||||||
["Amount", "1.0000 ETH"],
|
["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"),
|
...ethLines("Up to 1.0000 ETH"),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -2622,6 +2625,7 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
lines: [
|
lines: [
|
||||||
["Amount", "600.0000 USDT"],
|
["Amount", "600.0000 USDT"],
|
||||||
|
...wrapLines("The swap step that spends it"),
|
||||||
...ethLines("Up to 1.0000 ETH"),
|
...ethLines("Up to 1.0000 ETH"),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -2753,6 +2757,172 @@ describe("uniswap decoder", () => {
|
|||||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
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`
|
// 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) =>
|
||||||
|
|||||||
@@ -370,7 +370,8 @@ const PAID_ELSEWHERE = [
|
|||||||
"Steps",
|
"Steps",
|
||||||
"Deadline",
|
"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) =>
|
const withMove = (lines) =>
|
||||||
lines.flatMap((l) =>
|
lines.flatMap((l) =>
|
||||||
l === "Amount"
|
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
|
// 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", () => {
|
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(
|
const result = decode(
|
||||||
execute(
|
execute(
|
||||||
@@ -996,7 +997,10 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(result.name).toBe("Swap USDT → ETH");
|
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", () => {
|
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", () => {
|
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(
|
const result = decode(
|
||||||
execute(
|
execute(
|
||||||
@@ -1108,7 +1114,11 @@ describe("what reaches the UNWRAP_WETH of the swap's WETH", () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(result.name).toBe("Swap USDT → 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
|
// 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"),
|
left: tokenLine(WETH, "WETH"),
|
||||||
|
spent: "1.0000 ETH",
|
||||||
},
|
},
|
||||||
// A SETTLE of OPEN_DELTA from the router pays only what the step
|
// A SETTLE of OPEN_DELTA from the router pays only what the step
|
||||||
// still owes of its currency when it runs.
|
// 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",
|
"Moved from the sender",
|
||||||
]
|
]
|
||||||
: ["Amount"]),
|
: ["Amount"]),
|
||||||
// A later swap step the user pays for spends `spent` of a token
|
// A later swap step or WRAP_ETH the user pays for spends `spent`
|
||||||
// other than Token In.
|
// of a token other than Token In.
|
||||||
...(spent
|
...(spent
|
||||||
? ["Moved from the sender", "Token moved", "Moved to"]
|
? ["Moved from the sender", "Token moved", "Moved to"]
|
||||||
: []),
|
: []),
|
||||||
|
|||||||
Reference in New Issue
Block a user