Compare commits
1
Commits
next
..
d37053305d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d37053305d |
@@ -19,10 +19,13 @@ on: [push]
|
|||||||
# by the host daemon and mounts an empty directory, and the runner image's
|
# by the host daemon and mounts an empty directory, and the runner image's
|
||||||
# node is too old to install this repo's dependencies.
|
# node is too old to install this repo's dependencies.
|
||||||
#
|
#
|
||||||
# These jobs REPORT, they do not gate. No branch requires these two jobs, so
|
# These jobs REPORT, they do not gate. Whether a check blocks a merge is
|
||||||
# a failure is a red mark a reviewer has to account for. The Chrome suite has
|
# Gitea branch protection, which this repo does not configure, so a failure
|
||||||
# failed under load before, and no such report is open. A gate that fails at
|
# here is a red mark a reviewer has to account for rather than a hard
|
||||||
# random teaches people to merge past red.
|
# block. Making e2e-chrome a required check is blocked while reports of the
|
||||||
|
# Chrome suite failing under load are still open; the "In CI" section of
|
||||||
|
# README.md names them. A gate that fails at random teaches people to merge
|
||||||
|
# past red.
|
||||||
#
|
#
|
||||||
# Nothing here may pass vacuously. There is no continue-on-error and no
|
# Nothing here may pass vacuously. There is no continue-on-error and no
|
||||||
# `|| true`. Both scripts exit non-zero when docker is missing, when the
|
# `|| true`. Both scripts exit non-zero when docker is missing, when the
|
||||||
@@ -33,7 +36,8 @@ jobs:
|
|||||||
e2e-chrome:
|
e2e-chrome:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Bounds the image build, a cold cache included, and the three Chrome
|
# Bounds the image build, a cold cache included, and the three Chrome
|
||||||
# programs, so a hung browser frees the shared runner.
|
# programs, so a hung browser frees the shared runner. README.md
|
||||||
|
# "In CI" has the measured times.
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
steps:
|
steps:
|
||||||
# actions/checkout v4.2.2, 2026-02-22
|
# actions/checkout v4.2.2, 2026-02-22
|
||||||
@@ -43,7 +47,8 @@ jobs:
|
|||||||
e2e-firefox:
|
e2e-firefox:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Bounds the image build, a cold cache included, and both Firefox
|
# Bounds the image build, a cold cache included, and both Firefox
|
||||||
# programs, so a hung browser frees the shared runner.
|
# programs, so a hung browser frees the shared runner. README.md
|
||||||
|
# "In CI" has the measured times.
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
# actions/checkout v4.2.2, 2026-02-22
|
# actions/checkout v4.2.2, 2026-02-22
|
||||||
|
|||||||
@@ -471,9 +471,7 @@ When "Back" returns to a screen that this page load has not drawn yet,
|
|||||||
Home is shown instead. "Back" onto a screen already drawn only unhides it, so
|
Home is shown instead. "Back" onto a screen already drawn only unhides it, so
|
||||||
unsaved input in Settings survives. Home is drawn again on every "Back". When a
|
unsaved input in Settings survives. Home is drawn again on every "Back". When a
|
||||||
balance refresh returns while AddressDetail or AddressToken is showing, that
|
balance refresh returns while AddressDetail or AddressToken is showing, that
|
||||||
screen draws its balances again. When a refresh returns while Send is showing,
|
screen draws its balances again.
|
||||||
Send draws its From address and current balance again. Its inputs and dropdown
|
|
||||||
stay as the user left them.
|
|
||||||
|
|
||||||
A network switch, or a change to "Use Blockscout", cuts the stack at the first
|
A network switch, or a change to "Use Blockscout", cuts the stack at the first
|
||||||
screen that cannot be drawn from saved state. The cut also drops AddressToken
|
screen that cannot be drawn from saved state. The cut also drops AddressToken
|
||||||
@@ -581,9 +579,8 @@ Transitions:
|
|||||||
- "+": derives the next address in place.
|
- "+": derives the next address in place.
|
||||||
- `[info]`: AddressDetail.
|
- `[info]`: AddressDetail.
|
||||||
- `[x]`: DeleteAddress.
|
- `[x]`: DeleteAddress.
|
||||||
- "Send": Send, with its dropdown on ETH. Refused with a flash message when the
|
- "Send": Send. Refused with a flash message when the active address has a zero
|
||||||
active address has a zero ETH balance or belongs to a wallet carrying the
|
ETH balance or belongs to a wallet carrying the `walletDefects.js` notice.
|
||||||
`walletDefects.js` notice.
|
|
||||||
- "Receive": Receive.
|
- "Receive": Receive.
|
||||||
- Tap a transaction row: TransactionDetail.
|
- Tap a transaction row: TransactionDetail.
|
||||||
- "Add additional wallet...": AddWallet.
|
- "Add additional wallet...": AddWallet.
|
||||||
@@ -1415,19 +1412,15 @@ 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`. For a `WRAP_ETH`
|
`Moved to` line names that recipient, or reads `The sender`. A `WRAP_ETH`
|
||||||
into the router, `Moved to` reads `The router`. A `WRAP_ETH` after the step
|
after the step that sets `Token In` takes the amount it states from the
|
||||||
that sets `Token In` takes the amount it states from the sender. When it pays
|
sender. When it pays anyone but the router, that amount gets these lines. When
|
||||||
anyone but the router, that amount gets these lines. When it pays the router,
|
it pays the router, that amount joins the `Amount` line if `Token In` is ETH,
|
||||||
that amount joins the `Amount` line if `Token In` is ETH, and gets these lines
|
and gets these lines if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` states
|
||||||
if not. A later `WRAP_ETH` of `CONTRACT_BALANCE` to anyone but the router gets
|
no amount, so it gets no lines. ETH that the sender pays a V4 step or a
|
||||||
these lines. It gets none after an `UNWRAP_WETH` into the router, or while the
|
`WRAP_ETH` of a stated amount can get these three lines when the router may
|
||||||
router holds ETH from a V4 step. When it gets them, its
|
hold ETH from an earlier step. When the decoder knows the amount, its
|
||||||
`Moved from the sender` line reads `All the ETH the transaction carries`. ETH
|
`Moved from the sender` line reads `Up to` and the amount.
|
||||||
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
|
- `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
|
||||||
amount already stands as the `Amount` figure.
|
amount already stands as the `Amount` figure.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Turn a verified dist/ into the two distributable archives.
|
// Turn a verified dist/ into the two distributable archives.
|
||||||
//
|
//
|
||||||
// Invoked by script/package, which runs `make build` first so that dist/ has
|
// Invoked by script/package, which runs `make build` first so that dist/ has
|
||||||
// already been checked against the build's own receipt (see the Builds
|
// already been checked against the build's own receipt (see the Build Receipts
|
||||||
// section of README.md). This program does not build anything and does not
|
// section of README.md). This program does not build anything and does not
|
||||||
// write into dist/: it reads the emitted tree and writes release/.
|
// write into dist/: it reads the emitted tree and writes release/.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ main() {
|
|||||||
|
|
||||||
echo "Running the Firefox e2e suite..."
|
echo "Running the Firefox e2e suite..."
|
||||||
# --shm-size=1g: Firefox needs more than the default 64MB /dev/shm.
|
# --shm-size=1g: Firefox needs more than the default 64MB /dev/shm.
|
||||||
# --network none: the container has no network, and the suite reaches
|
# --network none: the suite stubs nothing, so this is what keeps the
|
||||||
# only the test page and stub node on loopback (README.md, Browser
|
# run offline and deterministic. The extension swallows its own
|
||||||
# tests). The extension swallows its own fetch failures, so the popup
|
# fetch failures, so the popup flows work unchanged; see the
|
||||||
# flows work unchanged. Weaker than the Chrome suite's fixture
|
# network note in README.md. Weaker than the Chrome suite's
|
||||||
# interception, and honestly so — it proves no request escaped, but it
|
# fixture interception, and honestly so — it proves no request
|
||||||
# cannot report which ones were attempted.
|
# escaped, but it cannot report which ones were attempted.
|
||||||
# HOME=/tmp: the image's root home is not a reliable place for the
|
# HOME=/tmp: the image's root home is not a reliable place for the
|
||||||
# browser profile.
|
# browser profile.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -18,11 +18,11 @@
|
|||||||
// updateState(fn) read fresh, apply fn to that fresh record, write it
|
// updateState(fn) read fresh, apply fn to that fresh record, write it
|
||||||
// back — all holding the lock every write of the stored
|
// back — all holding the lock every write of the stored
|
||||||
// record takes, so no other write lands in between, by
|
// record takes, so no other write lands in between, by
|
||||||
// the background or by any page (README, "Stored data").
|
// the background or by any page (README, "Stored state
|
||||||
// The read is one storage round trip ahead of the write
|
// and its version"). The read is one storage round trip
|
||||||
// rather than a page lifetime ahead of it (which is what
|
// ahead of the write rather than a page lifetime ahead of
|
||||||
// made the popup's saveState() need a per-field merge
|
// it (which is what made the popup's saveState() need a
|
||||||
// against a baseline at all).
|
// per-field merge against a baseline at all).
|
||||||
//
|
//
|
||||||
// A handler that must both read and write therefore does its network work
|
// A handler that must both read and write therefore does its network work
|
||||||
// against a snapshot it owns, and applies the RESULT inside updateState().
|
// against a snapshot it owns, and applies the RESULT inside updateState().
|
||||||
@@ -72,7 +72,7 @@ async function updateStateOnce(mutate) {
|
|||||||
// src/shared/browserApi.js) is held from the read to the write: two
|
// src/shared/browserApi.js) is held from the read to the write: two
|
||||||
// background turns, or a turn and a page's save, never interleave
|
// background turns, or a turn and a page's save, never interleave
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/641). Every page's save waits
|
// (https://git.eeqj.de/sneak/AutistMask/issues/641). Every page's save waits
|
||||||
// while it is held (README, "Stored data").
|
// while it is held (README, "Stored state and its version").
|
||||||
//
|
//
|
||||||
// `mutate` must also not call updateState() itself, directly or through
|
// `mutate` must also not call updateState() itself, directly or through
|
||||||
// anything it awaits: the inner turn waits for the lock the outer one holds,
|
// anything it awaits: the inner turn waits for the lock the outer one holds,
|
||||||
|
|||||||
+2
-5
@@ -107,10 +107,8 @@ let refreshTimer = null;
|
|||||||
// tracked token list stays as it is.
|
// tracked token list stays as it is.
|
||||||
//
|
//
|
||||||
// Once the save is done, the wallet list is drawn again. So are the balances
|
// Once the save is done, the wallet list is drawn again. So are the balances
|
||||||
// on AddressDetail or AddressToken when that screen is up. When Send is up, its
|
// on AddressDetail or AddressToken when that screen is up. No other screen is
|
||||||
// From address and current balance are drawn again, and its inputs and dropdown
|
// drawn again, DeleteAddress and its balance lines included.
|
||||||
// stay as the user left them. No other screen is drawn again, DeleteAddress and
|
|
||||||
// its balance lines included.
|
|
||||||
async function doRefreshAndRender() {
|
async function doRefreshAndRender() {
|
||||||
if (refreshInFlight) {
|
if (refreshInFlight) {
|
||||||
refreshAgain = true;
|
refreshAgain = true;
|
||||||
@@ -166,7 +164,6 @@ async function doRefreshAndRender() {
|
|||||||
renderWalletList();
|
renderWalletList();
|
||||||
if (state.currentView === "address") addressDetail.renderBalances();
|
if (state.currentView === "address") addressDetail.renderBalances();
|
||||||
if (state.currentView === "address-token") addressToken.renderBalance();
|
if (state.currentView === "address-token") addressToken.renderBalance();
|
||||||
if (state.currentView === "send") send.updateSendBalance();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No caller waits for this function, so it must never reject. An
|
// No caller waits for this function, so it must never reject. An
|
||||||
// unhandled rejection reports nothing. The save inside it reports its
|
// unhandled rejection reports nothing. The save inside it reports its
|
||||||
|
|||||||
@@ -360,11 +360,6 @@ function init(_ctx) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("btn-address-token-back").addEventListener("click", () => {
|
$("btn-address-token-back").addEventListener("click", () => {
|
||||||
// AddressDetail's show() clears the token, but Back onto a drawn
|
|
||||||
// screen only unhides it. "Send" below may have left the dropdown on
|
|
||||||
// the token as well.
|
|
||||||
state.selectedToken = null;
|
|
||||||
$("send-token").value = "ETH";
|
|
||||||
goBack();
|
goBack();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -319,12 +319,12 @@ function showTxApproval(details) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// A swap the decoder read no amount for, such as one whose V4 swap
|
// A swap the decoder read no amount for, such as one whose V4 swap
|
||||||
// action could not be read or a V4 exact-out swap. The wait, success
|
// action could not be read or a V4 exact-out swap. The wait and error
|
||||||
// and error screens say the amount could not be decoded, with no
|
// screens say the amount could not be decoded, with no currency after
|
||||||
// currency after it, rather than show the transaction's ETH value as
|
// it, rather than show the transaction's ETH value as the amount. A
|
||||||
// the amount. A swap paid in native ETH keeps that value: it is the
|
// swap paid in native ETH keeps that value: it is the ETH the swap
|
||||||
// ETH the swap pays (for an exact-out swap, the most it pays), not a
|
// pays (for an exact-out swap, the most it pays), not a figure that
|
||||||
// figure that had to be read from the calldata.
|
// had to be read from the calldata.
|
||||||
const tokenIn = decoded.details.find((d) => d.label === "Token In");
|
const tokenIn = decoded.details.find((d) => d.label === "Token In");
|
||||||
if (
|
if (
|
||||||
tokenIn &&
|
tokenIn &&
|
||||||
|
|||||||
@@ -83,11 +83,10 @@ function recoveryPathText(wallet) {
|
|||||||
// rounded number would report "0.0000 ETH" for an address holding real money.
|
// rounded number would report "0.0000 ETH" for an address holding real money.
|
||||||
// The lines below it carry the amounts, in the same format as Home and
|
// The lines below it carry the amounts, in the same format as Home and
|
||||||
// AddressDetail, followed by the USD total when there is one to give — no
|
// AddressDetail, followed by the USD total when there is one to give — no
|
||||||
// total line at all on testnet, before the first price fetch or while no
|
// total line at all on testnet or before the first price fetch, and no figure
|
||||||
// refresh has read the ETH balance, and no figure when every holding here is
|
// when every holding here is one with no price, since "$0.00" directly under
|
||||||
// one with no price, since "$0.00" directly under "This address holds a
|
// "This address holds a balance." is a contradiction. A token holding below
|
||||||
// balance." is a contradiction. A token holding below 0.000001 does not count,
|
// 0.000001 does not count, as the lines below leave it out.
|
||||||
// as the lines below leave it out.
|
|
||||||
//
|
//
|
||||||
// While no refresh has read the ETH balance the address may be empty, so the
|
// While no refresh has read the ETH balance the address may be empty, so the
|
||||||
// sentence says "may hold".
|
// sentence says "may hold".
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ function clearFlash() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The flash line reserves exactly one line, and a message that wrapped would
|
// The flash line reserves exactly one line, and a message that wrapped would
|
||||||
// push the screen below it down (README, Appearance). So #flash-msg never
|
// push the screen below it down (README, No Layout Shift). So #flash-msg never
|
||||||
// wraps: text too long for the line is cut with an ellipsis, and the whole
|
// wraps: text too long for the line is cut with an ellipsis, and the whole
|
||||||
// message is also put in the line's title. Write messages to fit, at most 50
|
// message is also put in the line's title. Write messages to fit, at most 50
|
||||||
// characters, so none is cut.
|
// characters, so none is cut.
|
||||||
@@ -528,9 +528,9 @@ const ADDRESS_MIN_DISPLAY_LEN = 32;
|
|||||||
|
|
||||||
// Truncate the middle of a string, replacing removed characters with "…".
|
// Truncate the middle of a string, replacing removed characters with "…".
|
||||||
// Safety: refuses to truncate more than 10 characters, which is the maximum
|
// Safety: refuses to truncate more than 10 characters, which is the maximum
|
||||||
// that still prevents address spoofing attacks (see Addresses in README).
|
// that still prevents address spoofing attacks (see Display Consistency in
|
||||||
// Callers that need to display less should use a different UI approach rather
|
// README). Callers that need to display less should use a different UI
|
||||||
// than silently making addresses insecure.
|
// approach rather than silently making addresses insecure.
|
||||||
function truncateMiddle(str, maxLen) {
|
function truncateMiddle(str, maxLen) {
|
||||||
if (str.length <= maxLen) return str;
|
if (str.length <= maxLen) return str;
|
||||||
const removed = str.length - maxLen + 1; // +1 for the ellipsis char
|
const removed = str.length - maxLen + 1; // +1 for the ellipsis char
|
||||||
@@ -633,9 +633,9 @@ function formatAddressHtml(address, ensName, maxLen, title) {
|
|||||||
return renderAddressHtml(address, { title, ensName, maxLen });
|
return renderAddressHtml(address, { title, ensName, maxLen });
|
||||||
}
|
}
|
||||||
|
|
||||||
// A transaction's time as every screen shows it (README, Amounts): the ISO
|
// A transaction's time as every screen shows it (README, Display
|
||||||
// datetime, in UTC when the UTC Timestamps setting is on, and the relative age.
|
// Consistency): the ISO datetime, in UTC when the UTC Timestamps setting is
|
||||||
// Views import these two; they keep no copies.
|
// on, and the relative age. Views import these two; they keep no copies.
|
||||||
function isoDate(timestamp) {
|
function isoDate(timestamp) {
|
||||||
const d = new Date(timestamp * 1000);
|
const d = new Date(timestamp * 1000);
|
||||||
const pad = (n) => String(n).padStart(2, "0");
|
const pad = (n) => String(n).padStart(2, "0");
|
||||||
|
|||||||
@@ -510,15 +510,11 @@ function init(ctx) {
|
|||||||
showFlash("Cannot send \u2014 zero balance.");
|
showFlash("Cannot send \u2014 zero balance.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Send uses a selected token ahead of its dropdown, and AddressToken
|
|
||||||
// can leave one selected on the way here.
|
|
||||||
state.selectedToken = null;
|
|
||||||
$("send-to").value = "";
|
$("send-to").value = "";
|
||||||
$("send-amount").value = "";
|
$("send-amount").value = "";
|
||||||
$("send-token-dropdown").classList.remove("hidden");
|
$("send-token-dropdown").classList.remove("hidden");
|
||||||
$("send-token-static").classList.add("hidden");
|
$("send-token-static").classList.add("hidden");
|
||||||
renderSendTokenSelect(addr);
|
renderSendTokenSelect(addr);
|
||||||
$("send-token").value = "ETH";
|
|
||||||
updateSendBalance();
|
updateSendBalance();
|
||||||
resetSendValidation();
|
resetSendValidation();
|
||||||
pushCurrentView();
|
pushCurrentView();
|
||||||
@@ -530,9 +526,6 @@ function init(ctx) {
|
|||||||
showFlash("No active address selected.");
|
showFlash("No active address selected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Receive warns about a selected token, and AddressToken can leave one
|
|
||||||
// selected on the way here.
|
|
||||||
state.selectedToken = null;
|
|
||||||
ctx.showReceive();
|
ctx.showReceive();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ async function renderSiteLists() {
|
|||||||
// A network switch, made by the user here or saved by the background once the
|
// A network switch, made by the user here or saved by the background once the
|
||||||
// user approved a site's request (src/popup/index.js), leaves every screen on
|
// user approved a site's request (src/popup/index.js), leaves every screen on
|
||||||
// the page showing the previous network. Two functions leave the page as
|
// the page showing the previous network. Two functions leave the page as
|
||||||
// README's Navigation section describes:
|
// README's Screen Map describes:
|
||||||
//
|
//
|
||||||
// - cutViewStack(), before the switch is saved, so that save keeps the cut for
|
// - cutViewStack(), before the switch is saved, so that save keeps the cut for
|
||||||
// a popup closed straight after
|
// a popup closed straight after
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// The 4-decimal amount rule from README.md's Amounts section, and
|
// The 4-decimal amount rule from README.md's Display Consistency section, and
|
||||||
// the one exception to it, in one place. Three call sites had grown their own
|
// the one exception to it, in one place. Three call sites had grown their own
|
||||||
// copy of the truncation — the history and balance lists
|
// copy of the truncation — the history and balance lists
|
||||||
// (`src/shared/transactions.js`), the approval screen's ERC-20 amount line
|
// (`src/shared/transactions.js`), the approval screen's ERC-20 amount line
|
||||||
|
|||||||
@@ -240,9 +240,10 @@ function onStorageChanged(listener) {
|
|||||||
/**
|
/**
|
||||||
* Run `fn` while holding the browser's lock called `name`
|
* Run `fn` while holding the browser's lock called `name`
|
||||||
* (`navigator.locks`), and settle as `fn` settles. Every page of the extension
|
* (`navigator.locks`), and settle as `fn` settles. Every page of the extension
|
||||||
* and its background share these locks, so while one of them holds `name`
|
* and its background share these locks, except a page in a Firefox private
|
||||||
* another asking for it waits, in the order they asked. The lock is let go when
|
* window (README, "Stored state and its version"), so while one of them holds
|
||||||
* `fn` settles, whether it resolved or rejected.
|
* `name` another asking for it waits, in the order they asked. The lock is let
|
||||||
|
* go when `fn` settles, whether it resolved or rejected.
|
||||||
*
|
*
|
||||||
* Every write of the stored record holds the lock named after its key,
|
* Every write of the stored record holds the lock named after its key,
|
||||||
* "autistmask", from its read to its write: saveState() in
|
* "autistmask", from its read to its write: saveState() in
|
||||||
@@ -265,8 +266,8 @@ function withLock(name, fn) {
|
|||||||
|
|
||||||
// `storage.session`, the browser's session storage: what is kept there lasts
|
// `storage.session`, the browser's session storage: what is kept there lasts
|
||||||
// until the browser closes. It survives the background worker being stopped
|
// until the browser closes. It survives the background worker being stopped
|
||||||
// and started again, which module-level variables do not (README, The
|
// and started again, which module-level variables do not (README, Background
|
||||||
// background worker). The background keeps the connections allowed without
|
// scheduling). The background keeps the connections allowed without
|
||||||
// "Remember" there; see src/background/index.js. Firefox has it from version
|
// "Remember" there; see src/background/index.js. Firefox has it from version
|
||||||
// 115, the minimum manifest/firefox.json declares.
|
// 115, the minimum manifest/firefox.json declares.
|
||||||
//
|
//
|
||||||
|
|||||||
+26
-61
@@ -404,11 +404,6 @@ const OPEN_DELTA = Symbol("v4-open-delta");
|
|||||||
// that nothing is swapped.
|
// that nothing is swapped.
|
||||||
const ALREADY_PAID = Symbol("v2-already-paid");
|
const ALREADY_PAID = Symbol("v2-already-paid");
|
||||||
|
|
||||||
// A later WRAP_ETH of CONTRACT_BALANCE wraps all the ETH the router holds, and
|
|
||||||
// so all the ETH the transaction carries. The calldata states no quantity for
|
|
||||||
// it. The approval screen's Value line gives that ETH.
|
|
||||||
const TRANSACTION_ETH = Symbol("transaction-eth");
|
|
||||||
|
|
||||||
// The router's whole-balance amount, universal-router
|
// The router's whole-balance amount, universal-router
|
||||||
// `ActionConstants.CONTRACT_BALANCE`: a V2 exact-in given it as `amountIn`
|
// `ActionConstants.CONTRACT_BALANCE`: a V2 exact-in given it as `amountIn`
|
||||||
// pays the pair all the router holds of the input token.
|
// pays the pair all the router holds of the input token.
|
||||||
@@ -421,7 +416,6 @@ const CONTRACT_BALANCE = 2n ** 255n;
|
|||||||
const OPEN_DELTA_AMOUNT = "All available (V4 open delta)";
|
const OPEN_DELTA_AMOUNT = "All available (V4 open delta)";
|
||||||
const ALREADY_PAID_AMOUNT =
|
const ALREADY_PAID_AMOUNT =
|
||||||
"Whatever an earlier step sent to the pair (V2 already paid)";
|
"Whatever an earlier step sent to the pair (V2 already paid)";
|
||||||
const TRANSACTION_ETH_AMOUNT = "All the ETH the transaction carries";
|
|
||||||
const NO_MINIMUM = "None (no minimum guaranteed)";
|
const NO_MINIMUM = "None (no minimum guaranteed)";
|
||||||
|
|
||||||
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
|
// Permit2 amounts are uint160; the maximum is Permit2's "unbounded".
|
||||||
@@ -1273,9 +1267,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// ERC-20 token from the router takes none of that token from the
|
// ERC-20 token from the router takes none of that token from the
|
||||||
// sender.
|
// sender.
|
||||||
let inputPaidByUser = false;
|
let inputPaidByUser = false;
|
||||||
// The recipient of a WRAP_ETH that set the input side, as written;
|
// The recipient of a WRAP_ETH that set the input side, as written,
|
||||||
// otherwise null. That WRAP_ETH pays its WETH there (see the Moved to
|
// when it is not the router; otherwise null. That WRAP_ETH pays its
|
||||||
// lines).
|
// WETH there, not to a swap step (see the Moved to lines).
|
||||||
let wrapEthRecipient = null;
|
let wrapEthRecipient = null;
|
||||||
// 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).
|
||||||
@@ -1295,9 +1289,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
let minOutput = null;
|
let minOutput = null;
|
||||||
let outputEstablished = false;
|
let outputEstablished = false;
|
||||||
let hasUnwrapWeth = false;
|
let hasUnwrapWeth = false;
|
||||||
// Set once an UNWRAP_WETH pays ETH to the router, which a later
|
|
||||||
// WRAP_ETH of CONTRACT_BALANCE then wraps.
|
|
||||||
let unwrappedIntoRouter = false;
|
|
||||||
// Set when an UNWRAP_WETH pays out, as ETH, the output side's WETH,
|
// Set when an UNWRAP_WETH pays out, as ETH, the output side's WETH,
|
||||||
// which the router holds; cleared whenever the output side is set
|
// which the router holds; cleared whenever the output side is set
|
||||||
// again, or a later step may pay that ETH out as WETH (see after the
|
// again, or a later step may pay that ETH out as WETH (see after the
|
||||||
@@ -1377,9 +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. A take with a `recipient` is the ETH of a
|
// "Up to" and the amount. A take with a `recipient`, the ETH of a
|
||||||
// later WRAP_ETH. If that recipient is not the router, the take never
|
// later WRAP_ETH that pays anyone but the router, never joins the
|
||||||
// joins the Amount line either: its Moved to line names that recipient.
|
// 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
|
||||||
@@ -1915,7 +1906,9 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
w.amount,
|
w.amount,
|
||||||
);
|
);
|
||||||
inputPaidByUser = true;
|
inputPaidByUser = true;
|
||||||
wrapEthRecipient = w.recipient;
|
if (!isRouter(w.recipient)) {
|
||||||
|
wrapEthRecipient = w.recipient;
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
w.amount > 0n &&
|
w.amount > 0n &&
|
||||||
w.amount !== CONTRACT_BALANCE
|
w.amount !== CONTRACT_BALANCE
|
||||||
@@ -1923,33 +1916,17 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// The ETH of a stated amount comes from the
|
// The ETH of a stated amount comes from the
|
||||||
// transaction's value. ETH an earlier step left in
|
// transaction's value. ETH an earlier step left in
|
||||||
// the router may pay part of it, and the sender
|
// the router may pay part of it, and the sender
|
||||||
// then pays at most that amount.
|
// then pays at most that amount. CONTRACT_BALANCE
|
||||||
|
// states no amount to take.
|
||||||
userPaid.push({
|
userPaid.push({
|
||||||
token: "0x0000000000000000000000000000000000000000",
|
token: "0x0000000000000000000000000000000000000000",
|
||||||
amount: w.amount,
|
amount: w.amount,
|
||||||
atMost: heldByRouter.has(
|
atMost: heldByRouter.has(
|
||||||
"0x0000000000000000000000000000000000000000",
|
"0x0000000000000000000000000000000000000000",
|
||||||
),
|
),
|
||||||
recipient: w.recipient,
|
recipient: isRouter(w.recipient)
|
||||||
});
|
? null
|
||||||
} else if (
|
: w.recipient,
|
||||||
w.amount === CONTRACT_BALANCE &&
|
|
||||||
!isRouter(w.recipient) &&
|
|
||||||
!unwrappedIntoRouter &&
|
|
||||||
!heldByRouter.has(
|
|
||||||
"0x0000000000000000000000000000000000000000",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// It pays the recipient all the ETH the transaction
|
|
||||||
// carries. It gets no lines after an UNWRAP_WETH
|
|
||||||
// into the router, or while the router holds ETH
|
|
||||||
// from a V4 step: Uniswap's routes pay back what
|
|
||||||
// their swap did not spend this way, and carry no
|
|
||||||
// ETH.
|
|
||||||
userPaid.push({
|
|
||||||
token: "0x0000000000000000000000000000000000000000",
|
|
||||||
amount: TRANSACTION_ETH,
|
|
||||||
recipient: w.recipient,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
addPayee(w.recipient);
|
addPayee(w.recipient);
|
||||||
@@ -2192,9 +2169,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
heldByRouter.has(w),
|
heldByRouter.has(w),
|
||||||
);
|
);
|
||||||
WETH_ADDRESSES.forEach((w) => heldByRouter.delete(w));
|
WETH_ADDRESSES.forEach((w) => heldByRouter.delete(w));
|
||||||
if (isRouter(u.recipient)) {
|
|
||||||
unwrappedIntoRouter = true;
|
|
||||||
}
|
|
||||||
if (isRouter(u.recipient) && heldWeth) {
|
if (isRouter(u.recipient) && heldWeth) {
|
||||||
heldByRouter.set(
|
heldByRouter.set(
|
||||||
"0x0000000000000000000000000000000000000000",
|
"0x0000000000000000000000000000000000000000",
|
||||||
@@ -2292,8 +2266,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// What the steps take that the Amount line does not give, as
|
// What the steps take that the Amount line does not give, as
|
||||||
// { 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 has
|
// know. `atMost` is as in `userPaid`. Only the take of a WRAP_ETH that
|
||||||
// a recipient (see wrapEthRecipient and userPaid).
|
// 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
|
||||||
@@ -2341,7 +2315,7 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
typeof total === "bigint" &&
|
typeof total === "bigint" &&
|
||||||
typeof t.amount === "bigint" &&
|
typeof t.amount === "bigint" &&
|
||||||
!t.atMost &&
|
!t.atMost &&
|
||||||
(!present(t.recipient) || isRouter(t.recipient))
|
!present(t.recipient)
|
||||||
) {
|
) {
|
||||||
total += t.amount;
|
total += t.amount;
|
||||||
inTotal.push(t);
|
inTotal.push(t);
|
||||||
@@ -2439,14 +2413,13 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The text of an amount the swap steps spend, for the Amount line and
|
// The text of an amount the swap steps spend, for the Amount line and
|
||||||
// the lines for other tokens below it. Four amounts need no scale to
|
// the lines for other tokens below it. Three amounts need no scale to
|
||||||
// describe and are named rather than formatted: V4's open delta, V2's
|
// describe and are named rather than formatted: V4's open delta and
|
||||||
// already-paid zero and the ETH the transaction carries, none of which
|
// V2's already-paid zero, neither of which is a quantity at all (see
|
||||||
// is a quantity at all (see OPEN_DELTA, ALREADY_PAID and
|
// OPEN_DELTA and ALREADY_PAID), and an unbounded permit. Those two
|
||||||
// TRANSACTION_ETH), and an unbounded permit. Those three tests come
|
// tests come first — the sentinels are not bigints and cannot be
|
||||||
// first — the sentinels are not bigints and cannot be compared with
|
// compared with one. `atMost` marks an amount that is only the most a
|
||||||
// one. `atMost` marks an amount that is only the most a take may be
|
// take may be (see userPaid).
|
||||||
// (see userPaid).
|
|
||||||
const spentText = (amount, info, atMost) => {
|
const spentText = (amount, info, atMost) => {
|
||||||
if (amount === OPEN_DELTA) {
|
if (amount === OPEN_DELTA) {
|
||||||
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
return { raw: OPEN_DELTA_AMOUNT, display: OPEN_DELTA_AMOUNT };
|
||||||
@@ -2457,12 +2430,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
display: ALREADY_PAID_AMOUNT,
|
display: ALREADY_PAID_AMOUNT,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (amount === TRANSACTION_ETH) {
|
|
||||||
return {
|
|
||||||
raw: TRANSACTION_ETH_AMOUNT,
|
|
||||||
display: TRANSACTION_ETH_AMOUNT,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (amount >= MAX_UINT160) {
|
if (amount >= MAX_UINT160) {
|
||||||
return { raw: "Unlimited", display: "Unlimited" };
|
return { raw: "Unlimited", display: "Unlimited" };
|
||||||
}
|
}
|
||||||
@@ -2522,8 +2489,8 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
// of the lines below for a PERMIT2_TRANSFER_FROM. An amount this
|
// of the lines below for a PERMIT2_TRANSFER_FROM. An amount this
|
||||||
// decoder does not know is said so, and a step it did not read gets
|
// decoder does not know is said so, and a step it did not read gets
|
||||||
// that one line alone. A take with no recipient goes to the swap step
|
// that one line alone. A take with no recipient goes to the swap step
|
||||||
// that spends it. A take with a recipient goes to that recipient:
|
// that spends it. A take with a recipient goes to that recipient;
|
||||||
// the router, the sender as `MSG_SENDER`, or an address.
|
// `MSG_SENDER` means the sender.
|
||||||
for (const s of spentFromSender) {
|
for (const s of spentFromSender) {
|
||||||
details.push({
|
details.push({
|
||||||
label: "Moved from the sender",
|
label: "Moved from the sender",
|
||||||
@@ -2547,8 +2514,6 @@ function decode(data, toAddress, chainId, sources, fromAddress) {
|
|||||||
label: "Moved to",
|
label: "Moved to",
|
||||||
value: "The swap step that spends it",
|
value: "The swap step that spends it",
|
||||||
});
|
});
|
||||||
} else if (isRouter(s.recipient)) {
|
|
||||||
details.push({ label: "Moved to", value: "The router" });
|
|
||||||
} else if (s.recipient.toLowerCase() === MSG_SENDER) {
|
} else if (s.recipient.toLowerCase() === MSG_SENDER) {
|
||||||
details.push({ label: "Moved to", value: "The sender" });
|
details.push({ label: "Moved to", value: "The sender" });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,17 +19,12 @@
|
|||||||
// its ETH line reads the same, and Send states it as unknown
|
// its ETH line reads the same, and Send states it as unknown
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/736).
|
// (https://git.eeqj.de/sneak/AutistMask/issues/736).
|
||||||
//
|
//
|
||||||
// A refresh that returns while Send is up draws Send's balance line again, and
|
|
||||||
// leaves what the user entered as it is
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/748).
|
|
||||||
//
|
|
||||||
// Driven through the real popup over a stored profile on Ethereum Mainnet,
|
// Driven through the real popup over a stored profile on Ethereum Mainnet,
|
||||||
// with "Show tracked tokens with zero balance" on, as it is by default.
|
// with "Show tracked tokens with zero balance" on, as it is by default.
|
||||||
|
|
||||||
const { HDNodeWallet, Mnemonic } = require("ethers");
|
const { HDNodeWallet, Mnemonic } = require("ethers");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ADDRESS,
|
|
||||||
bootPopup,
|
bootPopup,
|
||||||
cleanupPopup,
|
cleanupPopup,
|
||||||
unversionedValidProfile,
|
unversionedValidProfile,
|
||||||
@@ -436,62 +431,6 @@ test("Send for the new token reads its balance as unknown before any refresh has
|
|||||||
await finish([MILLION]);
|
await finish([MILLION]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Send for the new token reads the fetched balance once the refresh returns, without being reopened", async () => {
|
|
||||||
const page = await bootOnAddress();
|
|
||||||
const finish = holdNextRefresh(page);
|
|
||||||
await addNewToken(page);
|
|
||||||
await openToken(page, NEW_TOKEN.toLowerCase());
|
|
||||||
await page.click("btn-address-token-send");
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: unknown (NEW)");
|
|
||||||
|
|
||||||
await finish([MILLION]);
|
|
||||||
|
|
||||||
expect(page.visibleViews()).toEqual(["send"]);
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: 1000000.0000 NEW");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a refresh that returns while Send is up draws an unread ETH balance again and leaves the inputs alone", async () => {
|
|
||||||
const page = await bootPopup(profileNotRead());
|
|
||||||
// The dropdown's first option, which a browser selects by itself.
|
|
||||||
page.node("send-token").value = "ETH";
|
|
||||||
await page.click("btn-main-send");
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: unknown (ETH)");
|
|
||||||
const recipient = "0x" + "dd".repeat(20);
|
|
||||||
page.node("send-to").value = recipient;
|
|
||||||
page.node("send-amount").value = "0.5";
|
|
||||||
// Stands for the options on the screen, which a refresh leaves as they are.
|
|
||||||
page.node("send-token").innerHTML = "options on the screen";
|
|
||||||
// Stands for the From address from before the refresh.
|
|
||||||
page.node("send-from").innerHTML = "From address before the refresh";
|
|
||||||
|
|
||||||
await refreshOnce(page, []);
|
|
||||||
|
|
||||||
expect(page.visibleViews()).toEqual(["send"]);
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: 2.0000 ETH");
|
|
||||||
expect(page.node("send-from").innerHTML).toContain(ADDRESS);
|
|
||||||
expect(page.value("send-to")).toBe(recipient);
|
|
||||||
expect(page.value("send-amount")).toBe("0.5");
|
|
||||||
expect(page.value("send-token")).toBe("ETH");
|
|
||||||
expect(page.node("send-token").innerHTML).toBe("options on the screen");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a refresh that returns while Send is up keeps the token picked in the dropdown and draws its new balance", async () => {
|
|
||||||
const page = await bootOnDerivingWallet();
|
|
||||||
const dropdown = page.node("send-token");
|
|
||||||
dropdown.value = "ETH";
|
|
||||||
await page.click("btn-main-send");
|
|
||||||
dropdown.value = NEW_TOKEN;
|
|
||||||
for (const fn of dropdown.listeners.change) await fn();
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: 1000000.0000 NEW");
|
|
||||||
page.node("send-amount").value = "0.5";
|
|
||||||
|
|
||||||
await refreshOnce(page, [{ ...MILLION, balance: "3000000.0" }]);
|
|
||||||
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: 3000000.0000 NEW");
|
|
||||||
expect(page.value("send-token")).toBe(NEW_TOKEN);
|
|
||||||
expect(page.value("send-amount")).toBe("0.5");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a token added by contract address on SettingsAddToken reads quantity unknown until the refresh returns", async () => {
|
test("a token added by contract address on SettingsAddToken reads quantity unknown until the refresh returns", async () => {
|
||||||
const page = await bootPopup(unversionedValidProfile());
|
const page = await bootPopup(unversionedValidProfile());
|
||||||
expect(page.visibleViews()).toEqual(["main"]);
|
expect(page.visibleViews()).toEqual(["main"]);
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
// "Back" from AddressToken leaves no token selected. AddressDetail's show()
|
|
||||||
// would clear it, but Back onto a drawn screen only unhides that screen, and
|
|
||||||
// Send and Receive both read the field
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/751). Driven through the real
|
|
||||||
// popup over a stored profile.
|
|
||||||
|
|
||||||
const {
|
|
||||||
bootPopup,
|
|
||||||
cleanupPopup,
|
|
||||||
unversionedValidProfile,
|
|
||||||
TOKEN_ADDRESS,
|
|
||||||
} = require("./support/popupBoot");
|
|
||||||
|
|
||||||
const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD";
|
|
||||||
const TOKEN = {
|
|
||||||
address: TOKEN_ADDRESS,
|
|
||||||
symbol: "TST",
|
|
||||||
decimals: 18,
|
|
||||||
name: "Test Token",
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
cleanupPopup();
|
|
||||||
});
|
|
||||||
|
|
||||||
// The popup reopened onto AddressDetail, at an address holding 1.5 ETH and
|
|
||||||
// 2.5 TST.
|
|
||||||
function bootOnAddress() {
|
|
||||||
const profile = unversionedValidProfile({
|
|
||||||
currentView: "address",
|
|
||||||
viewStack: ["main"],
|
|
||||||
selectedWallet: 0,
|
|
||||||
selectedAddress: 0,
|
|
||||||
trackedTokens: [TOKEN],
|
|
||||||
});
|
|
||||||
profile.wallets[0].addresses[0].tokenBalances = [
|
|
||||||
{ ...TOKEN, balance: "2.5" },
|
|
||||||
];
|
|
||||||
return bootPopup(profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddressToken opened from AddressDetail, as tapping the token's row does.
|
|
||||||
async function openToken(page) {
|
|
||||||
const { state } = require("../src/shared/state");
|
|
||||||
state.selectedToken = TOKEN_ADDRESS;
|
|
||||||
require("../src/popup/views/helpers").pushCurrentView();
|
|
||||||
require("../src/popup/views/addressToken").show();
|
|
||||||
await page.settle();
|
|
||||||
expect(page.visibleViews()).toEqual(["address-token"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function backFromToken(page) {
|
|
||||||
await page.click("btn-address-token-back");
|
|
||||||
expect(page.visibleViews()).toEqual(["address"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ETH chosen in Send's dropdown, as the user does.
|
|
||||||
async function chooseEth(page) {
|
|
||||||
const dropdown = page.node("send-token");
|
|
||||||
dropdown.value = "ETH";
|
|
||||||
for (const fn of dropdown.listeners.change) await fn();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Review for 1 of the token Send has, to RECIPIENT.
|
|
||||||
async function review(page) {
|
|
||||||
page.node("send-to").value = RECIPIENT;
|
|
||||||
page.node("send-amount").value = "1";
|
|
||||||
await page.click("btn-send-review");
|
|
||||||
expect(page.visibleViews()).toEqual(["confirm-tx"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("after Back from AddressToken", () => {
|
|
||||||
test("no token is selected, on the page or in the stored record", async () => {
|
|
||||||
const page = await bootOnAddress();
|
|
||||||
await openToken(page);
|
|
||||||
await backFromToken(page);
|
|
||||||
|
|
||||||
const { state } = require("../src/shared/state");
|
|
||||||
expect(state.selectedToken).toBeNull();
|
|
||||||
expect(page.storage.read("autistmask").selectedToken).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Send with ETH chosen shows the ETH balance", async () => {
|
|
||||||
const page = await bootOnAddress();
|
|
||||||
await openToken(page);
|
|
||||||
await backFromToken(page);
|
|
||||||
|
|
||||||
await page.click("btn-send");
|
|
||||||
await chooseEth(page);
|
|
||||||
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: 1.5000 ETH");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Send with ETH chosen reviews ETH", async () => {
|
|
||||||
const page = await bootOnAddress();
|
|
||||||
await openToken(page);
|
|
||||||
await backFromToken(page);
|
|
||||||
|
|
||||||
await page.click("btn-send");
|
|
||||||
await chooseEth(page);
|
|
||||||
await review(page);
|
|
||||||
|
|
||||||
expect(page.text("confirm-type")).toBe("Native ETH transfer");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Receive shows no token warning", async () => {
|
|
||||||
const page = await bootOnAddress();
|
|
||||||
await openToken(page);
|
|
||||||
await backFromToken(page);
|
|
||||||
|
|
||||||
await page.click("btn-receive");
|
|
||||||
|
|
||||||
expect(page.visibleViews()).toEqual(["receive"]);
|
|
||||||
expect(page.text("receive-erc20-warning")).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
// AddressToken's Send sets the dropdown to the token, and nothing else
|
|
||||||
// rebuilds it on the way back to AddressDetail.
|
|
||||||
test("Send opens on ETH after AddressToken's Send has been used", async () => {
|
|
||||||
const page = await bootOnAddress();
|
|
||||||
await openToken(page);
|
|
||||||
await page.click("btn-address-token-send");
|
|
||||||
await page.click("btn-send-back");
|
|
||||||
await backFromToken(page);
|
|
||||||
|
|
||||||
await page.click("btn-send");
|
|
||||||
|
|
||||||
expect(page.value("send-token")).toBe("ETH");
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: 1.5000 ETH");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Send opened from AddressToken sends that token", () => {
|
|
||||||
async function expectSendsToken(page) {
|
|
||||||
await page.click("btn-address-token-send");
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: 2.5000 TST");
|
|
||||||
await review(page);
|
|
||||||
expect(page.text("confirm-type")).toBe("ERC-20 token transfer (TST)");
|
|
||||||
}
|
|
||||||
|
|
||||||
test("the first time", async () => {
|
|
||||||
const page = await bootOnAddress();
|
|
||||||
await openToken(page);
|
|
||||||
|
|
||||||
await expectSendsToken(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("after Back from Send", async () => {
|
|
||||||
const page = await bootOnAddress();
|
|
||||||
await openToken(page);
|
|
||||||
await page.click("btn-address-token-send");
|
|
||||||
await page.click("btn-send-back");
|
|
||||||
|
|
||||||
await expectSendsToken(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("when the token is opened again after Back from AddressToken", async () => {
|
|
||||||
const page = await bootOnAddress();
|
|
||||||
await openToken(page);
|
|
||||||
await backFromToken(page);
|
|
||||||
await openToken(page);
|
|
||||||
|
|
||||||
await expectSendsToken(page);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
// The floor of the approval screen's amount line.
|
// The floor of the approval screen's amount line.
|
||||||
//
|
//
|
||||||
// Amounts are truncated to four decimal places for scannability (README.md,
|
// Amounts are truncated to four decimal places for scannability (README.md,
|
||||||
// Amounts). With the token's true scale resolved, that truncation can still
|
// Display Consistency). With the token's true scale resolved, that truncation
|
||||||
// take a real amount below the floor and print it as `0.0000`: one base unit of
|
// can still take a real amount below the floor and print it as `0.0000`: one
|
||||||
// an 18-decimal token, or a few hundred of an 8-decimal one. On the one screen
|
// base unit of an 18-decimal token, or a few hundred of an 8-decimal one. On
|
||||||
// whose job is to state what is being authorized, a nonzero transfer or
|
// the one screen whose job is to state what is being authorized, a nonzero
|
||||||
// allowance then reads as nothing.
|
// transfer or allowance then reads as nothing.
|
||||||
//
|
//
|
||||||
// The invariant asserted here is narrow: a nonzero amount never renders as
|
// The invariant asserted here is narrow: a nonzero amount never renders as
|
||||||
// zero. The four-decimal rule itself is unchanged, and the string the
|
// zero. The four-decimal rule itself is unchanged, and the string the
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ describe("parsing the dust threshold", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("the rejection message", () => {
|
describe("the rejection message", () => {
|
||||||
// README, Wording: error messages are full sentences.
|
// README, Language & Labeling: error messages are full sentences.
|
||||||
test("is a full sentence naming the constraint", () => {
|
test("is a full sentence naming the constraint", () => {
|
||||||
expect(DUST_THRESHOLD_MESSAGE).toMatch(/^[A-Z].*\.$/);
|
expect(DUST_THRESHOLD_MESSAGE).toMatch(/^[A-Z].*\.$/);
|
||||||
expect(DUST_THRESHOLD_MESSAGE).toContain("whole number of gwei");
|
expect(DUST_THRESHOLD_MESSAGE).toContain("whole number of gwei");
|
||||||
@@ -95,9 +95,9 @@ describe("the flash line the message is shown in", () => {
|
|||||||
// NOT and CANNOT assert that the message fits inside it: jest runs on
|
// NOT and CANNOT assert that the message fits inside it: jest runs on
|
||||||
// the node environment here, with no layout engine, so every rendered
|
// the node environment here, with no layout engine, so every rendered
|
||||||
// height is zero. An earlier version of this block claimed to pin the
|
// height is zero. An earlier version of this block claimed to pin the
|
||||||
// reserved space rule in README, Appearance, with this regex, and it
|
// No Layout Shift policy with this regex, and it passed at any message
|
||||||
// passed at any message length, including one that wrapped to two lines
|
// length, including one that wrapped to two lines and pushed the
|
||||||
// and pushed the settings view down 12px.
|
// settings view down 12px.
|
||||||
//
|
//
|
||||||
// The line cuts a message too long for it with an ellipsis (see
|
// The line cuts a message too long for it with an ellipsis (see
|
||||||
// showFlash() in src/popup/views/helpers.js). The assertions that
|
// showFlash() in src/popup/views/helpers.js). The assertions that
|
||||||
|
|||||||
+7
-7
@@ -1280,7 +1280,7 @@ test("the correct password reveals the full phrase, and nothing logs it (#161)",
|
|||||||
);
|
);
|
||||||
assert(!promptShown, "the password prompt is still shown after unlock");
|
assert(!promptShown, "the password prompt is still shown after unlock");
|
||||||
|
|
||||||
// README, ShowRecoveryPhrase: shown whole, and copyable.
|
// Full Identifiers Policy: shown whole, and copyable.
|
||||||
const title = await env.page.getAttribute(
|
const title = await env.page.getAttribute(
|
||||||
"#show-phrase-value",
|
"#show-phrase-value",
|
||||||
"title",
|
"title",
|
||||||
@@ -2255,7 +2255,7 @@ async function waitForFilledFlashLine(page) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// README, Appearance: the rejection message goes into #flash-msg,
|
// README, No Layout Shift: the rejection message goes into #flash-msg,
|
||||||
// whose min-h-[1.25rem] reserves exactly ONE line at text-xs, and which
|
// whose min-h-[1.25rem] reserves exactly ONE line at text-xs, and which
|
||||||
// cuts a message too long for that line with an ellipsis rather than wrap
|
// cuts a message too long for that line with an ellipsis rather than wrap
|
||||||
// it. This shows the real message and measures that nothing moves; the
|
// it. This shows the real message and measures that nothing moves; the
|
||||||
@@ -2603,7 +2603,7 @@ function toHexWei(wei) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A fee in wei as the confirmation screen writes it: truncated to four decimal
|
// A fee in wei as the confirmation screen writes it: truncated to four decimal
|
||||||
// places (README.md, Amounts). Deliberately a second
|
// places (README.md, Display Consistency). Deliberately a second
|
||||||
// implementation rather than an import of src/shared/amountDisplay.js:
|
// implementation rather than an import of src/shared/amountDisplay.js:
|
||||||
// asserting against an independent rendering is stronger than asserting a
|
// asserting against an independent rendering is stronger than asserting a
|
||||||
// function equals itself. The fixture's fees are above 0.0001 ETH, so the
|
// function equals itself. The fixture's fees are above 0.0001 ETH, so the
|
||||||
@@ -3298,7 +3298,7 @@ async function goToTokenConfirm(env) {
|
|||||||
amount: TOKEN_AMOUNT,
|
amount: TOKEN_AMOUNT,
|
||||||
});
|
});
|
||||||
await waitForEstimate(env.page);
|
await waitForEstimate(env.page);
|
||||||
// TOKEN_AMOUNT to four decimal places (README.md, Amounts).
|
// TOKEN_AMOUNT to four decimal places (README.md, Display Consistency).
|
||||||
const shown = await shownAmount(env.page);
|
const shown = await shownAmount(env.page);
|
||||||
assert(
|
assert(
|
||||||
shown === "0.2500 " + STUB_TOKEN.symbol,
|
shown === "0.2500 " + STUB_TOKEN.symbol,
|
||||||
@@ -3453,7 +3453,7 @@ test("a token that lies about decimals() at signing time broadcasts nothing (#30
|
|||||||
|
|
||||||
// ------------------------- a send Sign & Send refuses moves nothing (#583)
|
// ------------------------- a send Sign & Send refuses moves nothing (#583)
|
||||||
//
|
//
|
||||||
// README, Appearance. Sign & Send refuses a send on the confirmation
|
// README, No Layout Shift. Sign & Send refuses a send on the confirmation
|
||||||
// screen when the active network changed since review and when its gas limit
|
// screen when the active network changed since review and when its gas limit
|
||||||
// or fee is over its bound, and each refusal's sentence sits in a box that held
|
// or fee is over its bound, and each refusal's sentence sits in a box that held
|
||||||
// its space from the first paint. This drives the total fee refusal, the longer
|
// its space from the first paint. This drives the total fee refusal, the longer
|
||||||
@@ -5770,8 +5770,8 @@ async function main() {
|
|||||||
// There is no fourth: once the context is closed nothing can record,
|
// There is no fourth: once the context is closed nothing can record,
|
||||||
// because the route handler and the console listeners died with it.
|
// because the route handler and the console listeners died with it.
|
||||||
// Traffic that a test defers past the trailing drain is therefore
|
// Traffic that a test defers past the trailing drain is therefore
|
||||||
// never observed at all — a real limit of this design, and not one
|
// never observed at all — a real limit of this design, stated in the
|
||||||
// any post-teardown hook could close.
|
// README, and not one any post-teardown hook could close.
|
||||||
//
|
//
|
||||||
// Two green-but-vacuous runs on this harness were the same shape: a
|
// Two green-but-vacuous runs on this harness were the same shape: a
|
||||||
// record falling outside somebody's window and being dropped. First
|
// record falling outside somebody's window and being dropped. First
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
// Home's "Send" and "Receive" open with no token selected. Three routes reach
|
|
||||||
// Home with a token still selected, Send reads that token ahead of its dropdown,
|
|
||||||
// and Receive warns about it
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/753). Driven through the real
|
|
||||||
// popup over a stored profile.
|
|
||||||
|
|
||||||
const {
|
|
||||||
bootPopup,
|
|
||||||
cleanupPopup,
|
|
||||||
unversionedValidProfile,
|
|
||||||
TOKEN_ADDRESS,
|
|
||||||
} = require("./support/popupBoot");
|
|
||||||
|
|
||||||
const RECIPIENT = "0x711e691962E5c054b78071C789A0C8CE657377aD";
|
|
||||||
const OTHER_WALLET_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
|
|
||||||
const PRIVATE_KEY = "0x" + "11".repeat(32);
|
|
||||||
const PASSWORD = "correct horse battery staple";
|
|
||||||
const TOKEN = {
|
|
||||||
address: TOKEN_ADDRESS,
|
|
||||||
symbol: "TST",
|
|
||||||
decimals: 18,
|
|
||||||
name: "Test Token",
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
cleanupPopup();
|
|
||||||
});
|
|
||||||
|
|
||||||
// AddressToken opened from AddressDetail, as tapping the token's row does.
|
|
||||||
async function openToken(page) {
|
|
||||||
const { state } = require("../src/shared/state");
|
|
||||||
state.selectedToken = TOKEN_ADDRESS;
|
|
||||||
require("../src/popup/views/helpers").pushCurrentView();
|
|
||||||
require("../src/popup/views/addressToken").show();
|
|
||||||
await page.settle();
|
|
||||||
expect(page.visibleViews()).toEqual(["address-token"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The popup open on AddressToken, at an address holding 1.5 ETH and 2.5 TST.
|
|
||||||
// A second wallet is there for the route that deletes one.
|
|
||||||
async function bootOnToken() {
|
|
||||||
const profile = unversionedValidProfile({
|
|
||||||
currentView: "address",
|
|
||||||
viewStack: ["main"],
|
|
||||||
selectedWallet: 0,
|
|
||||||
selectedAddress: 0,
|
|
||||||
trackedTokens: [TOKEN],
|
|
||||||
});
|
|
||||||
profile.wallets[0].addresses[0].tokenBalances = [
|
|
||||||
{ ...TOKEN, balance: "2.5" },
|
|
||||||
];
|
|
||||||
profile.wallets.push({
|
|
||||||
type: "hd",
|
|
||||||
name: "Wallet 2",
|
|
||||||
xpub: "xpub-wallet-2",
|
|
||||||
encryptedSecret: "encrypted-secret-2",
|
|
||||||
nextIndex: 1,
|
|
||||||
addresses: [
|
|
||||||
{
|
|
||||||
address: OTHER_WALLET_ADDRESS,
|
|
||||||
balance: "0",
|
|
||||||
tokenBalances: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const page = await bootPopup(profile);
|
|
||||||
await openToken(page);
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reopenAfterTokenSend() {
|
|
||||||
const closed = await bootOnToken();
|
|
||||||
await closed.click("btn-address-token-send");
|
|
||||||
cleanupPopup();
|
|
||||||
return bootPopup(undefined, { storage: closed.storage });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addWalletFromTokenSettings() {
|
|
||||||
const page = await bootOnToken();
|
|
||||||
await page.click("btn-settings");
|
|
||||||
await page.click("btn-main-add-wallet");
|
|
||||||
await page.click("tab-privkey");
|
|
||||||
page.node("import-private-key").value = PRIVATE_KEY;
|
|
||||||
page.node("add-wallet-password").value = PASSWORD;
|
|
||||||
page.node("add-wallet-password-confirm").value = PASSWORD;
|
|
||||||
await page.click("btn-add-wallet-confirm");
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deletes through the lost-password screen. It asks for the wallet's name, and
|
|
||||||
// the stored wallets here have no real password.
|
|
||||||
async function deleteWalletFromTokenSettings() {
|
|
||||||
const page = await bootOnToken();
|
|
||||||
await page.click("btn-settings");
|
|
||||||
// What the second wallet's [x] button in Settings does.
|
|
||||||
require("../src/popup/views/helpers").pushCurrentView();
|
|
||||||
require("../src/popup/views/deleteWallet").show(1);
|
|
||||||
await page.click("btn-delete-wallet-lost-password");
|
|
||||||
page.node("delete-wallet-lost-name-input").value = "Wallet 2";
|
|
||||||
await page.click("btn-delete-wallet-lost-confirm");
|
|
||||||
await page.click("btn-settings-back");
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ROUTES = [
|
|
||||||
["reopening the popup after AddressToken's Send", reopenAfterTokenSend],
|
|
||||||
[
|
|
||||||
"the Settings gear on AddressToken, then adding a wallet",
|
|
||||||
addWalletFromTokenSettings,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"the Settings gear on AddressToken, then deleting another wallet, then Back",
|
|
||||||
deleteWalletFromTokenSettings,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
// A route that stopped reaching Home with the token selected would prove nothing
|
|
||||||
// in the tests below, so it fails here.
|
|
||||||
async function arriveOnHome(route) {
|
|
||||||
const page = await route();
|
|
||||||
const { state } = require("../src/shared/state");
|
|
||||||
expect(page.visibleViews()).toEqual(["main"]);
|
|
||||||
expect(state.selectedToken).toBe(TOKEN_ADDRESS);
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ETH chosen in Send's dropdown, as the user does.
|
|
||||||
async function chooseEth(page) {
|
|
||||||
const dropdown = page.node("send-token");
|
|
||||||
dropdown.value = "ETH";
|
|
||||||
for (const fn of dropdown.listeners.change) await fn();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Review for 1 of the token Send has, to RECIPIENT.
|
|
||||||
async function review(page) {
|
|
||||||
page.node("send-to").value = RECIPIENT;
|
|
||||||
page.node("send-amount").value = "1";
|
|
||||||
await page.click("btn-send-review");
|
|
||||||
expect(page.visibleViews()).toEqual(["confirm-tx"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe.each(ROUTES)("after %s", (_name, route) => {
|
|
||||||
test("choosing ETH on Home's Send reviews ETH", async () => {
|
|
||||||
const page = await arriveOnHome(route);
|
|
||||||
|
|
||||||
await page.click("btn-main-send");
|
|
||||||
await chooseEth(page);
|
|
||||||
await review(page);
|
|
||||||
|
|
||||||
expect(page.text("confirm-type")).toBe("Native ETH transfer");
|
|
||||||
expect(page.storage.read("autistmask").selectedToken).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Home's Send starts on ETH", async () => {
|
|
||||||
const page = await arriveOnHome(route);
|
|
||||||
|
|
||||||
await page.click("btn-main-send");
|
|
||||||
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: 1.5000 ETH");
|
|
||||||
expect(page.value("send-token")).toBe("ETH");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Home's Receive shows no token warning", async () => {
|
|
||||||
const page = await arriveOnHome(route);
|
|
||||||
|
|
||||||
await page.click("btn-main-receive");
|
|
||||||
|
|
||||||
expect(page.visibleViews()).toEqual(["receive"]);
|
|
||||||
expect(page.text("receive-erc20-warning")).toBe("");
|
|
||||||
expect(page.storage.read("autistmask").selectedToken).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("AddressToken's Send still sends the token after Home's Send has opened", async () => {
|
|
||||||
const page = await arriveOnHome(deleteWalletFromTokenSettings);
|
|
||||||
await page.click("btn-main-send");
|
|
||||||
await page.click("btn-send-back");
|
|
||||||
await openToken(page);
|
|
||||||
|
|
||||||
await page.click("btn-address-token-send");
|
|
||||||
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: 2.5000 TST");
|
|
||||||
await review(page);
|
|
||||||
expect(page.text("confirm-type")).toBe("ERC-20 token transfer (TST)");
|
|
||||||
});
|
|
||||||
@@ -165,9 +165,9 @@ describe("declared icons", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// README, Installing on Firefox: the extension does not run in private windows.
|
|
||||||
// Firefox gives a page in a private window locks of its own, so that page's
|
// Firefox gives a page in a private window locks of its own, so that page's
|
||||||
// save could land between the background's read and its write.
|
// save could land between the background's read and its write (README,
|
||||||
|
// "Stored state and its version").
|
||||||
describe("private windows", () => {
|
describe("private windows", () => {
|
||||||
test("a page in a firefox private window cannot read or write extension storage", () => {
|
test("a page in a firefox private window cannot read or write extension storage", () => {
|
||||||
expect(readManifest("firefox").incognito).toBe("not_allowed");
|
expect(readManifest("firefox").incognito).toBe("not_allowed");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// browser's own does for the one call the extension makes,
|
// browser's own does for the one call the extension makes,
|
||||||
// `navigator.locks.request(name, callback)`. Every module a test file loads
|
// `navigator.locks.request(name, callback)`. Every module a test file loads
|
||||||
// shares it, as the extension's background and its pages share the browser's
|
// shares it, as the extension's background and its pages share the browser's
|
||||||
// (README, "Stored data").
|
// (README, "Stored state and its version").
|
||||||
//
|
//
|
||||||
// Run before each test file by `setupFiles` in package.json.
|
// Run before each test file by `setupFiles` in package.json.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// A transaction's time is written by isoDate() and timeAgo() in
|
// A transaction's time is written by isoDate() and timeAgo() in
|
||||||
// src/popup/views/helpers.js on every screen that shows one (README, Amounts;
|
// src/popup/views/helpers.js on every screen that shows one (README, Display
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/168). AddressDetail and
|
// Consistency; https://git.eeqj.de/sneak/AutistMask/issues/168). AddressDetail
|
||||||
// AddressToken used to define their own copies, so a fix to the shared pair
|
// and AddressToken used to define their own copies, so a fix to the shared pair
|
||||||
// would not have reached them.
|
// would not have reached them.
|
||||||
//
|
//
|
||||||
// The pair is replaced before the views are loaded, because a view takes it
|
// The pair is replaced before the views are loaded, because a view takes it
|
||||||
|
|||||||
+13
-16
@@ -1,15 +1,15 @@
|
|||||||
// Tests for the address-poisoning defense in src/shared/transactions.js.
|
// Tests for the address-poisoning defense in src/shared/transactions.js.
|
||||||
//
|
//
|
||||||
// README, Scam protection, describes four filters as a core security property
|
// README.md:730-814 describes four filters as a core security property of
|
||||||
// of the wallet: known token symbol verification, the low-holder threshold, the
|
// the wallet: known token symbol verification, the low-holder threshold, the
|
||||||
// fraud contract blocklist, and the dust threshold. A regression in any of
|
// fraud contract blocklist, and the dust threshold. A regression in any of
|
||||||
// them does not crash — it silently stops filtering, and poisoned look-alike
|
// them does not crash — it silently stops filtering, and poisoned look-alike
|
||||||
// addresses reappear in the user's transaction history. These tests pin the
|
// addresses reappear in the user's transaction history. These tests pin the
|
||||||
// behaviour down in both directions: the two attacks below must be filtered,
|
// behaviour down in both directions: the documented attacks must be filtered,
|
||||||
// and legitimate transactions must survive untouched.
|
// and legitimate transactions must survive untouched.
|
||||||
//
|
//
|
||||||
// Fixtures are static local objects modelled on two real attacks.
|
// Fixtures are static local objects modelled on the two real attacks cited in
|
||||||
// Nothing here touches the network: global.fetch is replaced
|
// the README. Nothing here touches the network: global.fetch is replaced
|
||||||
// with a throwing stub and the only fetch path in the module under test
|
// with a throwing stub and the only fetch path in the module under test
|
||||||
// (debugFetch, from src/shared/log) is mocked at the module boundary.
|
// (debugFetch, from src/shared/log) is mocked at the module boundary.
|
||||||
|
|
||||||
@@ -46,12 +46,11 @@ const { debugFetch } = require("../src/shared/log");
|
|||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Addresses and hashes from two real attacks. README, Scam protection, has the
|
// Addresses and hashes from the two attacks documented in README.md:730-814.
|
||||||
// filters that stop them.
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// The fake "Ethereum" token with symbol "ETH" and zero holders. See "Known
|
// The fake "Ethereum" token with symbol "ETH" and zero holders
|
||||||
// symbol spoofing" and "Low-holder tokens" in README, Scam protection.
|
// (README.md:735-750).
|
||||||
const FAKE_ETH_CONTRACT = "0xd05339f9ea5ab9d9f03b9d57f671d2abd1f55c82";
|
const FAKE_ETH_CONTRACT = "0xd05339f9ea5ab9d9f03b9d57f671d2abd1f55c82";
|
||||||
// The fraudulent Transfer event it emitted.
|
// The fraudulent Transfer event it emitted.
|
||||||
const FAKE_ETH_TRANSFER_HASH =
|
const FAKE_ETH_TRANSFER_HASH =
|
||||||
@@ -63,8 +62,7 @@ const LEGIT_RECIPIENT = "0xc3c693ae04bad5f13c45885c1e85a9557798f37e";
|
|||||||
// The scam address the fake token transfer pointed at ("0xC3C0" vs "0xC3c6").
|
// The scam address the fake token transfer pointed at ("0xC3C0" vs "0xC3c6").
|
||||||
const TOKEN_SCAM_LOOKALIKE = "0xc3c0aea127c575b9ffd03bf11c6a878e8979c37f";
|
const TOKEN_SCAM_LOOKALIKE = "0xc3c0aea127c575b9ffd03bf11c6a878e8979c37f";
|
||||||
|
|
||||||
// The second wave: a real 1 gwei native transfer. See "Dust transactions" in
|
// The second wave: a real 1 gwei native transfer (README.md:800-808).
|
||||||
// README, Scam protection.
|
|
||||||
const DUST_TX_HASH =
|
const DUST_TX_HASH =
|
||||||
"0x2708ebddfb9b5fa3f7a89d3ea398ef9fd8771b83ed861ecb7c21cd55d18edc74";
|
"0x2708ebddfb9b5fa3f7a89d3ea398ef9fd8771b83ed861ecb7c21cd55d18edc74";
|
||||||
const DUST_SENDER_LOOKALIKE = "0xc3c6b3b4402bd78a9582ab6b00e747769344f37e";
|
const DUST_SENDER_LOOKALIKE = "0xc3c6b3b4402bd78a9582ab6b00e747769344f37e";
|
||||||
@@ -81,8 +79,7 @@ const NOVEL_SPAM_SYMBOL = "SPAMTKN";
|
|||||||
// An ordinary counterparty for legitimate-transaction fixtures.
|
// An ordinary counterparty for legitimate-transaction fixtures.
|
||||||
const ORDINARY_PEER = "0x5aa0f9f1e0a1d0e0e5c1e7ce3b7dbbe9c19f0a11";
|
const ORDINARY_PEER = "0x5aa0f9f1e0a1d0e0e5c1e7ce3b7dbbe9c19f0a11";
|
||||||
|
|
||||||
// The documented default settings (README, Settings; DEFAULT_STATE in
|
// The documented default settings (README.md:810-814, state.js:24-27).
|
||||||
// src/shared/persistedState.js).
|
|
||||||
const DEFAULT_FILTERS = {
|
const DEFAULT_FILTERS = {
|
||||||
hideSpoofedSymbols: true,
|
hideSpoofedSymbols: true,
|
||||||
hideLowHolderTokens: true,
|
hideLowHolderTokens: true,
|
||||||
@@ -223,7 +220,7 @@ describe("token list assumptions the fixtures rely on", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("the real attacks", () => {
|
describe("the real attacks documented in README.md:730-814", () => {
|
||||||
test('the fake "Ethereum"/"ETH" token transfer is filtered by default', () => {
|
test('the fake "Ethereum"/"ETH" token transfer is filtered by default', () => {
|
||||||
const attack = fakeEthTokenTransfer();
|
const attack = fakeEthTokenTransfer();
|
||||||
const result = filterTransactions([attack], filters());
|
const result = filterTransactions([attack], filters());
|
||||||
@@ -402,8 +399,8 @@ describe("known-symbol spoof verification", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Turning the other three filters off must not turn this one off: each
|
// Turning the other three filters off must not turn this one off: each
|
||||||
// filter is independent, and this is the one that stops the fake "ETH"
|
// filter is independent, and this is the one the README calls out as the
|
||||||
// attack.
|
// defense against the fake "ETH" attack.
|
||||||
test("the check still runs when the other three filters are off", () => {
|
test("the check still runs when the other three filters are off", () => {
|
||||||
const result = filterTransactions(
|
const result = filterTransactions(
|
||||||
[fakeEthTokenTransfer()],
|
[fakeEthTokenTransfer()],
|
||||||
|
|||||||
+21
-356
@@ -2474,11 +2474,10 @@ describe("uniswap decoder", () => {
|
|||||||
// SETTLE of ETH from the router's own ETH, whichever payer it names.
|
// SETTLE of ETH from the router's own ETH, whichever payer it names.
|
||||||
// After an earlier step that may leave ETH in the router, the ETH that
|
// After an earlier step that may leave ETH in the router, the ETH that
|
||||||
// the user pays a V4 step reads as a maximum, on lines of its own.
|
// the user pays a V4 step reads as a maximum, on lines of its own.
|
||||||
// Every row reads at least the ETH that can leave the wallet. The row
|
// Every row reads at least the ETH that can leave the wallet. The two
|
||||||
// marked "reads as on next" reads the same on next. The row with a
|
// rows marked "reads as on next" read the same on next. On next every
|
||||||
// SETTLE_ALL of at most 3 ETH read that ETH as not known on next. On
|
// other row added that ETH to the Amount line, or read it as an exact
|
||||||
// next every other row added that ETH to the Amount line, or read it as
|
// amount.
|
||||||
// an exact amount.
|
|
||||||
//
|
//
|
||||||
// ADDRESS_THIS is the router on every release.
|
// ADDRESS_THIS is the router on every release.
|
||||||
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
|
const ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
|
||||||
@@ -2626,7 +2625,7 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
lines: [
|
lines: [
|
||||||
["Amount", "600.0000 USDT"],
|
["Amount", "600.0000 USDT"],
|
||||||
...wrapLines("The router"),
|
...wrapLines("The swap step that spends it"),
|
||||||
...ethLines("Up to 1.0000 ETH"),
|
...ethLines("Up to 1.0000 ETH"),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -2675,7 +2674,7 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 exact-out paid in ETH with a SETTLE_ALL of at most 3 ETH",
|
kind: "a WRAP_ETH of 1 ETH into the router, an UNWRAP_WETH into the router, then a V4 exact-out paid in ETH with a SETTLE_ALL of at most 3 ETH, reads as on next",
|
||||||
commands: [0x0b, 0x0c, 0x10],
|
commands: [0x0b, 0x0c, 0x10],
|
||||||
inputs: [
|
inputs: [
|
||||||
WRAP_INTO_ROUTER,
|
WRAP_INTO_ROUTER,
|
||||||
@@ -2698,7 +2697,10 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
lines: [...wrapLines("The router"), ...ethLines(UNDECODED)],
|
lines: [
|
||||||
|
...wrapLines("The swap step that spends it"),
|
||||||
|
...ethLines(UNDECODED),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "a WRAP_ETH of 1 ETH into the router, a V3 exact-out of at most 1 WETH paid from the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
|
kind: "a WRAP_ETH of 1 ETH into the router, a V3 exact-out of at most 1 WETH paid from the router, an UNWRAP_WETH into the router, then a V4 swap of 1 ETH that the user settles",
|
||||||
@@ -2759,8 +2761,8 @@ describe("uniswap decoder", () => {
|
|||||||
// does not set the input side takes the amount it states from the
|
// 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
|
// 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
|
// next" state no amount to take from the sender. Uniswap's own routes
|
||||||
// wrap CONTRACT_BALANCE into the router after their first step, as the
|
// wrap CONTRACT_BALANCE after their first step, as the last two rows
|
||||||
// last row does.
|
// do.
|
||||||
const CONTRACT_BALANCE = 2n ** 255n;
|
const CONTRACT_BALANCE = 2n ** 255n;
|
||||||
const WRAP_TO_OTHER = encodeWrapEth(OTHER_ADDR, ONE_ETH);
|
const WRAP_TO_OTHER = encodeWrapEth(OTHER_ADDR, ONE_ETH);
|
||||||
test.each([
|
test.each([
|
||||||
@@ -2785,7 +2787,7 @@ describe("uniswap decoder", () => {
|
|||||||
inputs: [V3_600, WRAP_INTO_ROUTER],
|
inputs: [V3_600, WRAP_INTO_ROUTER],
|
||||||
lines: [
|
lines: [
|
||||||
["Amount", "600.0000 USDT"],
|
["Amount", "600.0000 USDT"],
|
||||||
...wrapLines("The router"),
|
...wrapLines("The swap step that spends it"),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2794,7 +2796,7 @@ describe("uniswap decoder", () => {
|
|||||||
inputs: [V3_600, encodeWrapEth(ROUTER_ADDR, ONE_ETH)],
|
inputs: [V3_600, encodeWrapEth(ROUTER_ADDR, ONE_ETH)],
|
||||||
lines: [
|
lines: [
|
||||||
["Amount", "600.0000 USDT"],
|
["Amount", "600.0000 USDT"],
|
||||||
...wrapLines("The router"),
|
...wrapLines("The swap step that spends it"),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2837,9 +2839,7 @@ describe("uniswap decoder", () => {
|
|||||||
],
|
],
|
||||||
lines: [
|
lines: [
|
||||||
["Amount", "1.0000 ETH"],
|
["Amount", "1.0000 ETH"],
|
||||||
["Moved from the sender", "Up to 1.0000 ETH"],
|
...ethLines("Up to 1.0000 ETH"),
|
||||||
["Token moved", "ETH"],
|
|
||||||
["Moved to", "The router"],
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2877,6 +2877,12 @@ describe("uniswap decoder", () => {
|
|||||||
inputs: [V3_600, encodeWrapEth(OTHER_ADDR, 0n)],
|
inputs: [V3_600, encodeWrapEth(OTHER_ADDR, 0n)],
|
||||||
lines: [["Amount", "600.0000 USDT"]],
|
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",
|
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],
|
commands: [0x10, 0x0b, 0x00],
|
||||||
@@ -2917,347 +2923,6 @@ describe("uniswap decoder", () => {
|
|||||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
expect(linesBetweenTokens(result)).toEqual(lines);
|
||||||
});
|
});
|
||||||
|
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/747 — a WRAP_ETH into the
|
|
||||||
// router pays its WETH to the router, whichever step then takes it. On
|
|
||||||
// next that Moved to line read The swap step that spends it in every
|
|
||||||
// row.
|
|
||||||
const SWAP_STEP = "The swap step that spends it";
|
|
||||||
// Each pays 1 WETH from the router, for USDC, to the user.
|
|
||||||
const V3_WETH_FROM_ROUTER = coder.encode(
|
|
||||||
["address", "uint256", "uint256", "bytes", "bool"],
|
|
||||||
[
|
|
||||||
USER_ADDR,
|
|
||||||
ONE_ETH,
|
|
||||||
1n,
|
|
||||||
encodeV3Path([WETH_ADDR, USDC_ADDR]),
|
|
||||||
false,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
const V2_WETH_FROM_ROUTER = coder.encode(
|
|
||||||
["address", "uint256", "uint256", "address[]", "bool"],
|
|
||||||
[USER_ADDR, ONE_ETH, 1n, [WETH_ADDR, USDC_ADDR], false],
|
|
||||||
);
|
|
||||||
const V4_WETH_FROM_ROUTER = encodeV4Swap(
|
|
||||||
new Uint8Array([
|
|
||||||
V4_SWAP_EXACT_IN_SINGLE_ID,
|
|
||||||
V4_SETTLE_ID,
|
|
||||||
V4_TAKE_ALL_ID,
|
|
||||||
]),
|
|
||||||
[
|
|
||||||
encodeV4ExactInSingle(WETH_ADDR, USDC_ADDR, ONE_ETH, 1n),
|
|
||||||
settle(WETH_ADDR, 0n, false),
|
|
||||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
// SWEEP has the layout of TRANSFER, and its amount is a minimum.
|
|
||||||
const SWEEP_WETH = payOut(WETH_ADDR, 0n);
|
|
||||||
test.each([
|
|
||||||
{
|
|
||||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a SWEEP of that WETH to another address",
|
|
||||||
commands: [0x00, 0x0b, 0x04],
|
|
||||||
inputs: [V3_600, WRAP_INTO_ROUTER, SWEEP_WETH],
|
|
||||||
lines: [
|
|
||||||
["Amount", "600.0000 USDT"],
|
|
||||||
...wrapLines("The router"),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, a SWEEP of that WETH to another address, then a V3 exact-in paid from the router's WETH",
|
|
||||||
commands: [0x00, 0x0b, 0x04, 0x00],
|
|
||||||
inputs: [
|
|
||||||
V3_600,
|
|
||||||
WRAP_INTO_ROUTER,
|
|
||||||
SWEEP_WETH,
|
|
||||||
V3_WETH_FROM_ROUTER,
|
|
||||||
],
|
|
||||||
lines: [
|
|
||||||
["Amount", "600.0000 USDT"],
|
|
||||||
...wrapLines("The router"),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in of WETH that the user pays",
|
|
||||||
commands: [0x00, 0x0b, 0x00],
|
|
||||||
inputs: [
|
|
||||||
V3_600,
|
|
||||||
WRAP_INTO_ROUTER,
|
|
||||||
encodeV3SwapExactIn(USER_ADDR, ONE_ETH, 1n, [
|
|
||||||
WETH_ADDR,
|
|
||||||
USDC_ADDR,
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
lines: [
|
|
||||||
["Amount", "600.0000 USDT"],
|
|
||||||
...wrapLines("The router"),
|
|
||||||
["Moved from the sender", "1.0000 WETH"],
|
|
||||||
["Token moved", "WETH (" + WETH_ADDR + ")"],
|
|
||||||
["Moved to", SWAP_STEP],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, a V3 exact-in paid from the router's WETH, then another WRAP_ETH of 1 ETH into the router",
|
|
||||||
commands: [0x00, 0x0b, 0x00, 0x0b],
|
|
||||||
inputs: [
|
|
||||||
V3_600,
|
|
||||||
WRAP_INTO_ROUTER,
|
|
||||||
V3_WETH_FROM_ROUTER,
|
|
||||||
WRAP_INTO_ROUTER,
|
|
||||||
],
|
|
||||||
lines: [
|
|
||||||
["Amount", "600.0000 USDT"],
|
|
||||||
...wrapLines("The router"),
|
|
||||||
...wrapLines("The router"),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a V3 exact-in of 600 USDT, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in paid from the router's WETH",
|
|
||||||
commands: [0x00, 0x0b, 0x00],
|
|
||||||
inputs: [V3_600, WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER],
|
|
||||||
lines: [
|
|
||||||
["Amount", "600.0000 USDT"],
|
|
||||||
...wrapLines("The router"),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "the same with a V2 exact-in paid from the router's WETH",
|
|
||||||
commands: [0x00, 0x0b, 0x08],
|
|
||||||
inputs: [V3_600, WRAP_INTO_ROUTER, V2_WETH_FROM_ROUTER],
|
|
||||||
lines: [
|
|
||||||
["Amount", "600.0000 USDT"],
|
|
||||||
...wrapLines("The router"),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "the same with a V3 exact-out paid from the router's WETH",
|
|
||||||
commands: [0x00, 0x0b, 0x01],
|
|
||||||
inputs: [
|
|
||||||
V3_600,
|
|
||||||
WRAP_INTO_ROUTER,
|
|
||||||
encodeV3SwapExactOut(
|
|
||||||
USER_ADDR,
|
|
||||||
390000000n,
|
|
||||||
ONE_ETH,
|
|
||||||
[USDC_ADDR, WETH_ADDR],
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
lines: [
|
|
||||||
["Amount", "Up to 600.0000 USDT"],
|
|
||||||
["Moved from the sender", "Up to 1.0000 ETH"],
|
|
||||||
["Token moved", "ETH"],
|
|
||||||
["Moved to", "The router"],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "the same with a V4 swap that the router's WETH settles",
|
|
||||||
commands: [0x00, 0x0b, 0x10],
|
|
||||||
inputs: [V3_600, WRAP_INTO_ROUTER, V4_WETH_FROM_ROUTER],
|
|
||||||
lines: [
|
|
||||||
["Amount", "600.0000 USDT"],
|
|
||||||
...wrapLines("The router"),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a SWEEP of that WETH to another address, then an EXECUTE_SUB_PLAN",
|
|
||||||
commands: [0x0b, 0x04, 0x21],
|
|
||||||
inputs: [WRAP_INTO_ROUTER, SWEEP_WETH, SUB_PLAN],
|
|
||||||
lines: [
|
|
||||||
...wrapLines("The router"),
|
|
||||||
["Moved from the sender", UNDECODED],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a WRAP_ETH of 1 ETH into the router that sets the input side, a V3 exact-in paid from the router's WETH, then an EXECUTE_SUB_PLAN",
|
|
||||||
commands: [0x0b, 0x00, 0x21],
|
|
||||||
inputs: [WRAP_INTO_ROUTER, V3_WETH_FROM_ROUTER, SUB_PLAN],
|
|
||||||
lines: [
|
|
||||||
...wrapLines("The router"),
|
|
||||||
["Moved from the sender", UNDECODED],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a PERMIT2_PERMIT of 5,000 USDT to the router, a WRAP_ETH of 1 ETH into the router, then a V3 exact-in of 600 USDT",
|
|
||||||
commands: [0x0a, 0x0b, 0x00],
|
|
||||||
inputs: [
|
|
||||||
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
|
|
||||||
WRAP_INTO_ROUTER,
|
|
||||||
V3_600,
|
|
||||||
],
|
|
||||||
lines: [
|
|
||||||
["Amount", "5000.0000 USDT"],
|
|
||||||
...wrapLines("The router"),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "the same with a V3 exact-in paid from the router's WETH after the WRAP_ETH",
|
|
||||||
commands: [0x0a, 0x0b, 0x00, 0x00],
|
|
||||||
inputs: [
|
|
||||||
encodePermit2(USDT_ADDR, 5000000000n, ROUTER_ADDR),
|
|
||||||
WRAP_INTO_ROUTER,
|
|
||||||
V3_WETH_FROM_ROUTER,
|
|
||||||
V3_600,
|
|
||||||
],
|
|
||||||
lines: [
|
|
||||||
["Amount", "5000.0000 USDT"],
|
|
||||||
...wrapLines("The router"),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
])("$kind", ({ commands, inputs, lines }) => {
|
|
||||||
const result = decodeSteps(commands, inputs);
|
|
||||||
expect(linesBetweenTokens(result)).toEqual(lines);
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://git.eeqj.de/sneak/AutistMask/issues/747 — a later WRAP_ETH
|
|
||||||
// of CONTRACT_BALANCE wraps all the ETH the router holds. Unless an
|
|
||||||
// earlier step paid ETH to the router, that is the ETH the transaction
|
|
||||||
// carries. On next no line gave it. Into the router it gets no lines.
|
|
||||||
// It gets none after an UNWRAP_WETH into the router, or while the
|
|
||||||
// router holds ETH from a V4 step: Uniswap's own routes pay back what
|
|
||||||
// their swap did not spend this way, and carry no ETH. In the rows
|
|
||||||
// marked "reads as on next" that WRAP_ETH gets no lines.
|
|
||||||
const WRAP_ALL_TO_OTHER = encodeWrapEth(OTHER_ADDR, CONTRACT_BALANCE);
|
|
||||||
const WRAP_ALL_TO_SENDER = encodeWrapEth(MSG_SENDER, CONTRACT_BALANCE);
|
|
||||||
// The lines of such a WRAP_ETH whose Moved to line reads `to`.
|
|
||||||
const allEthLines = (to) => [
|
|
||||||
["Moved from the sender", "All the ETH the transaction carries"],
|
|
||||||
["Token moved", "ETH"],
|
|
||||||
["Moved to", to],
|
|
||||||
];
|
|
||||||
// The first steps of a route that spends WETH on a V4 pool of ETH:
|
|
||||||
// a PERMIT2_TRANSFER_FROM of 1 WETH into the router, an UNWRAP_WETH
|
|
||||||
// into the router, then a V4 exact-out paid from the router's ETH.
|
|
||||||
const UNWRAP_ROUTE = [
|
|
||||||
coder.encode(
|
|
||||||
["address", "address", "uint160"],
|
|
||||||
[WETH_ADDR, ADDRESS_THIS, ONE_ETH],
|
|
||||||
),
|
|
||||||
UNWRAP_INTO_ROUTER,
|
|
||||||
encodeV4Swap(
|
|
||||||
new Uint8Array([
|
|
||||||
V4_SWAP_EXACT_OUT_SINGLE_ID,
|
|
||||||
V4_SETTLE_ID,
|
|
||||||
V4_TAKE_ALL_ID,
|
|
||||||
]),
|
|
||||||
[
|
|
||||||
encodeV4ExactOutSingle(
|
|
||||||
ZERO_ADDR,
|
|
||||||
USDC_ADDR,
|
|
||||||
390000000n,
|
|
||||||
ONE_ETH,
|
|
||||||
),
|
|
||||||
settle(ZERO_ADDR, 0n, false),
|
|
||||||
encodeV4TakeAll(USDC_ADDR, 0n),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
// The lines of that route on next: this decoder cannot tell what the
|
|
||||||
// V4 step's SETTLE of ETH pays.
|
|
||||||
const UNWRAP_ROUTE_LINES = [
|
|
||||||
["Moved from the sender", UNDECODED],
|
|
||||||
["Token moved", "ETH"],
|
|
||||||
["Moved to", SWAP_STEP],
|
|
||||||
];
|
|
||||||
test.each([
|
|
||||||
{
|
|
||||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to another address",
|
|
||||||
commands: [0x00, 0x0b],
|
|
||||||
inputs: [V3_600, WRAP_ALL_TO_OTHER],
|
|
||||||
lines: [
|
|
||||||
["Amount", "600.0000 USDT"],
|
|
||||||
...allEthLines(OTHER_ADDR),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to the sender",
|
|
||||||
commands: [0x00, 0x0b],
|
|
||||||
inputs: [V3_600, WRAP_ALL_TO_SENDER],
|
|
||||||
lines: [
|
|
||||||
["Amount", "600.0000 USDT"],
|
|
||||||
...allEthLines("The sender"),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a V3 exact-out of at most 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE to another address",
|
|
||||||
commands: [0x01, 0x0b],
|
|
||||||
inputs: [
|
|
||||||
encodeV3SwapExactOut(USER_ADDR, 390000000n, USDT_600, [
|
|
||||||
USDC_ADDR,
|
|
||||||
USDT_ADDR,
|
|
||||||
]),
|
|
||||||
WRAP_ALL_TO_OTHER,
|
|
||||||
],
|
|
||||||
lines: [
|
|
||||||
["Amount", "Up to 600.0000 USDT"],
|
|
||||||
...allEthLines(OTHER_ADDR),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a WRAP_ETH of 1 ETH into the router, then a WRAP_ETH of CONTRACT_BALANCE to another address",
|
|
||||||
commands: [0x0b, 0x0b],
|
|
||||||
inputs: [WRAP_INTO_ROUTER, WRAP_ALL_TO_OTHER],
|
|
||||||
lines: [["Amount", "1.0000 ETH"], ...allEthLines(OTHER_ADDR)],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a V3 exact-in of 600 USDT, then a WRAP_ETH of CONTRACT_BALANCE into the router, reads as on next",
|
|
||||||
commands: [0x00, 0x0b],
|
|
||||||
inputs: [V3_600, encodeWrapEth(ADDRESS_THIS, CONTRACT_BALANCE)],
|
|
||||||
lines: [["Amount", "600.0000 USDT"]],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a V3 exact-in of 600 USDT, an UNWRAP_WETH to the sender, then a WRAP_ETH of CONTRACT_BALANCE to another address",
|
|
||||||
commands: [0x00, 0x0c, 0x0b],
|
|
||||||
inputs: [
|
|
||||||
V3_600,
|
|
||||||
encodeWrapEth(MSG_SENDER, 0n),
|
|
||||||
WRAP_ALL_TO_OTHER,
|
|
||||||
],
|
|
||||||
lines: [
|
|
||||||
["Amount", "600.0000 USDT"],
|
|
||||||
...allEthLines(OTHER_ADDR),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "a V4 swap of 600 USDT that takes ETH into the router, then a WRAP_ETH of CONTRACT_BALANCE to the sender, reads as on next",
|
|
||||||
commands: [0x10, 0x0b],
|
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
WRAP_ALL_TO_SENDER,
|
|
||||||
],
|
|
||||||
lines: [["Amount", "600.0000 USDT"]],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "the unwrap route, then a WRAP_ETH of CONTRACT_BALANCE to the sender, reads as on next",
|
|
||||||
commands: [0x02, 0x0c, 0x10, 0x0b],
|
|
||||||
inputs: [...UNWRAP_ROUTE, WRAP_ALL_TO_SENDER],
|
|
||||||
lines: UNWRAP_ROUTE_LINES,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "the unwrap route, then a WRAP_ETH of CONTRACT_BALANCE to another address, reads as on next",
|
|
||||||
commands: [0x02, 0x0c, 0x10, 0x0b],
|
|
||||||
inputs: [...UNWRAP_ROUTE, WRAP_ALL_TO_OTHER],
|
|
||||||
lines: UNWRAP_ROUTE_LINES,
|
|
||||||
},
|
|
||||||
])("$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) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user