Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd53737403 | ||
|
|
c45cf628a9 | ||
|
|
69ce8fd6d6 |
@@ -1473,20 +1473,21 @@ tracked.
|
|||||||
|
|
||||||
A tracked token is shown when its balance is nonzero, and any other ERC-20 when
|
A tracked token is shown when its balance is nonzero, and any other ERC-20 when
|
||||||
its balance is nonzero and it is in the bundled known-token list or has 1,000 or
|
its balance is nonzero and it is in the bundled known-token list or has 1,000 or
|
||||||
more holders. The balance list always drops a token that claims a bundled-list
|
more holders. A token claiming a symbol from the bundled list from any other
|
||||||
symbol from another contract address, whatever the settings. The list also drops
|
contract address is always dropped, tracked or not, and so is any token claiming
|
||||||
every token that claims the native asset's symbol: `"ETH"`, or any network's
|
a symbol that belongs to the native asset and therefore has no legitimate
|
||||||
`nativeCurrency`, such as `"SepoliaETH"`. That filter is unconditional — the
|
contract at all (`"ETH"`, and every network's `nativeCurrency`, such as
|
||||||
"Hide tokens with fewer than 1,000 holders" setting governs the transaction
|
`"SepoliaETH"`, on every network). That filter is unconditional — the "Hide
|
||||||
history and the send-screen token selector, not this list. A token's holder
|
tokens with fewer than 1,000 holders" setting governs the transaction history
|
||||||
count is unknown when the explorer reports none, or reports anything other than
|
and the send-screen token selector, not this list. A token's holder count is
|
||||||
a whole number written in digits alone, such as `1,000` or `1e3`
|
unknown when the explorer reports none, or reports anything other than a whole
|
||||||
(`parseHoldersCount()` in `src/shared/holders.js`). This list does not take an
|
number written in digits alone, such as `1,000` or `1e3` (`parseHoldersCount()`
|
||||||
unknown count as 1,000 or more, so such a token is shown only when it is on the
|
in `src/shared/holders.js`). This list does not take an unknown count as 1,000
|
||||||
bundled list or tracked. Every nonzero holding of a token shown is stored
|
or more, so such a token is shown only when it is on the bundled list or
|
||||||
exactly, however small, but a holding below 0.000001 is left out of the balance
|
tracked. Every nonzero holding of a token shown is stored exactly, however
|
||||||
lists, the send-screen token selector, the address total and the remove-address
|
small, but a holding below 0.000001 is left out of the balance lists, the
|
||||||
warning (`isBelowOneMillionth()` in `src/shared/amountDisplay.js`). The Send and
|
send-screen token selector, the address total and the remove-address warning
|
||||||
|
(`isBelowOneMillionth()` in `src/shared/amountDisplay.js`). The Send and
|
||||||
confirmation screens show it when its token is the one being sent. Tracked
|
confirmation screens show it when its token is the one being sent. Tracked
|
||||||
tokens with a zero balance are listed as well while "Show tracked tokens with
|
tokens with a zero balance are listed as well while "Show tracked tokens with
|
||||||
zero balance" is on.
|
zero balance" is on.
|
||||||
@@ -2238,8 +2239,6 @@ view would leave a wallet one click from deletion.
|
|||||||
**AddressDetail**
|
**AddressDetail**
|
||||||
- "Add" with a token already tracked, a scam-listed address, or a failed
|
- "Add" with a token already tracked, a scam-listed address, or a failed
|
||||||
contract lookup → flash message, no screen change
|
contract lookup → flash message, no screen change
|
||||||
- "Add" with a token that the spoof rule refuses → nothing is tracked; "Not
|
|
||||||
added: this token fakes a known symbol." flash message, no screen change
|
|
||||||
- "Add", then the contract lookup answers while another screen shows, or
|
- "Add", then the contract lookup answers while another screen shows, or
|
||||||
after AddToken was opened again → tracks the token, no screen change
|
after AddToken was opened again → tracks the token, no screen change
|
||||||
- "Add", then a network switch in Settings before the contract lookup
|
- "Add", then a network switch in Settings before the contract lookup
|
||||||
@@ -2291,8 +2290,6 @@ view would leave a wallet one click from deletion.
|
|||||||
- Either "Save" stores its URL only if the input still shows that URL when
|
- Either "Save" stores its URL only if the input still shows that URL when
|
||||||
the endpoint answers. Otherwise a flash message says that the URL was not
|
the endpoint answers. Otherwise a flash message says that the URL was not
|
||||||
saved
|
saved
|
||||||
- If the user taps the same "Save" again before the endpoint answers, the
|
|
||||||
earlier tap stores nothing and shows no flash message
|
|
||||||
- Etherscan:
|
- Etherscan:
|
||||||
- "Check recipients on Etherscan" checkbox, unticked until the user
|
- "Check recipients on Etherscan" checkbox, unticked until the user
|
||||||
ticks it
|
ticks it
|
||||||
@@ -2550,9 +2547,6 @@ view would leave a wallet one click from deletion.
|
|||||||
"Added SYMBOL" flash message (no screen change)
|
"Added SYMBOL" flash message (no screen change)
|
||||||
- A duplicate, a scam-listed address, or a failed contract lookup → flash
|
- A duplicate, a scam-listed address, or a failed contract lookup → flash
|
||||||
message, no screen change
|
message, no screen change
|
||||||
- "Add" by contract address with a token that the spoof rule refuses →
|
|
||||||
nothing is tracked; "Not added: this token fakes a known symbol." flash
|
|
||||||
message, no screen change
|
|
||||||
- "Add" by contract address, then a network switch in Settings before the
|
- "Add" by contract address, then a network switch in Settings before the
|
||||||
lookup answers → nothing is tracked; "The active network changed before
|
lookup answers → nothing is tracked; "The active network changed before
|
||||||
the lookup finished, so the token was not added." flash message, no screen
|
the lookup finished, so the token was not added." flash message, no screen
|
||||||
|
|||||||
+4
-2
@@ -380,8 +380,10 @@ but comes from an unrecognized contract, it is identified as a spoof and hidden.
|
|||||||
In your transaction history this is the "Hide fake tokens impersonating a known
|
In your transaction history this is the "Hide fake tokens impersonating a known
|
||||||
symbol" setting, which you can switch off; doing so also stops new entries being
|
symbol" setting, which you can switch off; doing so also stops new entries being
|
||||||
added to the fraud contract blocklist below, since detecting a spoof is what
|
added to the fraud contract blocklist below, since detecting a spoof is what
|
||||||
fills it. The send token list always applies the check. Your balance list always
|
fills it. The send token list always applies the check. Your balances apply it
|
||||||
applies the check too.
|
too, with one exception: a token claiming the symbol "ETH" is not filtered
|
||||||
|
there, so a fake "ETH" token can still show up in your balance list even though
|
||||||
|
it is hidden from your transaction history and from the send token list.
|
||||||
|
|
||||||
**Low-holder token filtering.** Tokens with fewer than 1,000 holders are hidden
|
**Low-holder token filtering.** Tokens with fewer than 1,000 holders are hidden
|
||||||
from transaction history and the send token list, and are left out of your
|
from transaction history and the send token list, and are left out of your
|
||||||
|
|||||||
+216
-75
@@ -1,87 +1,228 @@
|
|||||||
# Releasing AutistMask
|
# Releasing AutistMask
|
||||||
|
|
||||||
This is the procedure that turns a green `main` into a tagged, packaged release.
|
A release is a tagged commit on the `main` branch and the two archives that
|
||||||
It gathers into one place what is otherwise spread across the `Makefile` and
|
`make package` writes from that commit. This file is the procedure, in order.
|
||||||
three `README.md` sections, so the person cutting a release does not have to
|
|
||||||
reconstruct the order from them.
|
|
||||||
|
|
||||||
There is one version, declared in three files (`package.json`,
|
Only the owner ([@sneak](https://sneak.berlin)):
|
||||||
`manifest/chrome.json`, `manifest/firefox.json`), and `make package` builds and
|
|
||||||
packages but publishes nothing. `make build` and `make package` can be run by
|
- decides whether and when a release is tagged;
|
||||||
anyone; tagging, signing, packing a CRX and any upload need credentials only the
|
- creates, moves or deletes a tag;
|
||||||
owner ([@sneak](https://sneak.berlin)) holds and are marked **owner-only**
|
- decides where the archives are published.
|
||||||
below. Releases are tagged from `main` (see the Workflow section of `TODO.md`),
|
|
||||||
so the "release commit" throughout is the `main` commit the milestone PR merged.
|
Anyone can run steps 1 to 4. No script in this repository uploads or publishes
|
||||||
|
the archives. None signs them, and none packs a CRX (Chrome's packaged extension
|
||||||
|
file).
|
||||||
|
|
||||||
|
## Terms
|
||||||
|
|
||||||
|
- The tracker is the repository's Gitea site (Gitea is the software that hosts
|
||||||
|
the repository): https://git.eeqj.de/sneak/AutistMask. The tracker's Actions
|
||||||
|
page, https://git.eeqj.de/sneak/AutistMask/actions, lists the workflow runs.
|
||||||
|
- A workflow is a file under `.gitea/workflows/` that lists jobs. The tracker
|
||||||
|
runs every workflow on each push. Each job runs one script on the tracker's
|
||||||
|
runner (the machine that runs jobs).
|
||||||
|
- The release commit is a commit on `main`. Releases are tagged from `main` only
|
||||||
|
(`TODO.md`, Workflow).
|
||||||
|
- A clean checkout is a working tree (the checked-out files) at the release
|
||||||
|
commit where `git status --porcelain` prints nothing. The build stamps the
|
||||||
|
commit hash (the commit's id) into the extension and appends `-dirty` when the
|
||||||
|
tree has local changes.
|
||||||
|
- A tag is a name that git attaches to one commit. Release tags read `vX.Y.Z`,
|
||||||
|
such as `v0.1.0`.
|
||||||
|
- A digest is the sha256 hash of a file (a fixed-length fingerprint of its
|
||||||
|
bytes), written as 64 hexadecimal characters.
|
||||||
|
|
||||||
|
## What you need
|
||||||
|
|
||||||
|
- A clean checkout of the release commit.
|
||||||
|
- `make`.
|
||||||
|
- Docker (a tool that runs programs in containers), for `script/cibuild` in
|
||||||
|
step 1. `make package` needs no Docker.
|
||||||
|
- Node and yarn. Both come from the system when present. Otherwise
|
||||||
|
`script/bootstrap` installs Node through a pinned (fixed-version) install of
|
||||||
|
nvm, a Node version manager. `script/bootstrap` installs yarn through corepack
|
||||||
|
(Node's package manager installer). When no corepack is found,
|
||||||
|
`script/bootstrap` installs yarn through `npm` (Node's package manager).
|
||||||
|
|
||||||
## Procedure
|
## Procedure
|
||||||
|
|
||||||
1. **Confirm `main` is green in CI.** The `check` workflow
|
### 1. Confirm the release commit is green
|
||||||
(`.gitea/workflows/check.yml`) runs `script/cibuild`, which runs
|
|
||||||
`script/check` and then builds the image uncached, so a green `check` run is
|
|
||||||
a green `make check`. Find the run for the exact release commit on the
|
|
||||||
tracker's Actions view. _Check:_ that commit's `check` run succeeded; running
|
|
||||||
`make check` on a clean checkout of the commit reproduces it and exits 0.
|
|
||||||
|
|
||||||
2. **Confirm the version matches the intended tag.** `package.json`,
|
The `check` workflow (`.gitea/workflows/check.yml`) runs `script/cibuild` on
|
||||||
`manifest/chrome.json` and `manifest/firefox.json` must all declare the same
|
every push. `script/cibuild` runs:
|
||||||
`X.Y.Z`. `make build` fails when they disagree, but nothing checks that they
|
|
||||||
equal the tag you mean to create — that is this manual step. _Check:_ all
|
|
||||||
three files read the same `X.Y.Z`, and it is the version you intend to tag
|
|
||||||
`vX.Y.Z`.
|
|
||||||
|
|
||||||
3. **Build and package from a clean checkout of that commit.** From a fresh
|
- `script/bootstrap`, which installs the dependencies;
|
||||||
clone, or a working tree with no local modifications (`git status` clean),
|
- `script/check`, which is what `make check` runs;
|
||||||
checked out at the release commit: run `make setup`, then `make package`.
|
- the Docker image build (an image is Docker's packaged form of a program).
|
||||||
`make package` runs `make build` first, so the archives can only be made from
|
|
||||||
a `dist/` verified against that build's own receipt as a release (not debug)
|
|
||||||
build. It writes three files into `release/`:
|
|
||||||
`autistmask-chrome-<version>.zip`, `autistmask-firefox-<version>.xpi`, and
|
|
||||||
`SHA256SUMS`. _Check:_ those three files exist and `<version>` in the archive
|
|
||||||
names is the version confirmed in step 2. The Firefox `.xpi` is **unsigned**
|
|
||||||
(see step 6 and "Installing on Firefox" in `README.md`).
|
|
||||||
|
|
||||||
4. **Verify `SHA256SUMS`.** The archives are deterministic — sorted entries,
|
`script/check` runs:
|
||||||
fixed timestamps, fixed compression — so a second `make package` from another
|
|
||||||
clean checkout of the same commit produces byte-identical files. Verify the
|
|
||||||
recorded digests against the files with `sha256sum -c SHA256SUMS`, run from
|
|
||||||
`release/`. To confirm reproducibility, run `make package` again on a
|
|
||||||
separate clean checkout and compare the digests. _Check:_ `sha256sum -c`
|
|
||||||
reports `OK` for every file, and an independent build's digests match.
|
|
||||||
|
|
||||||
5. **Tag the release commit.** _(owner-only)_ Create an annotated tag `vX.Y.Z`
|
- `script/test`: the tests, in Docker;
|
||||||
on the release commit and push it: `git tag -a vX.Y.Z` (with a message), then
|
- `script/lint`, which runs three checks in Docker:
|
||||||
`git push origin vX.Y.Z`. _Check:_ `git tag` lists `vX.Y.Z`, and
|
- ESLint, a code checker;
|
||||||
`git rev-parse vX.Y.Z^{commit}` resolves to the release commit.
|
- `prettier --check .`, the formatting check;
|
||||||
|
- `script/check-censored`, which fails when the name that `RULES.md` bars
|
||||||
|
appears outside its allowed places;
|
||||||
|
- `script/fmt-check`: `prettier --check .` again, on the host.
|
||||||
|
|
||||||
6. **Distribute per browser.** _(owner-only; pending the owner's choice on
|
The `e2e` workflow (`.gitea/workflows/e2e.yml`) has two jobs. One runs
|
||||||
https://git.eeqj.de/sneak/AutistMask/issues/386)_ How 1.0.0 is distributed on
|
`script/test-e2e`, the browser tests in Chrome. The other runs
|
||||||
each browser is not yet decided; it is the open question on that issue, and
|
`script/test-e2e-firefox`, the browser tests in Firefox. A red `e2e` job does
|
||||||
the concrete steps cannot be written until the owner records a choice there.
|
not fail the `check` run.
|
||||||
These steps need credentials only the owner holds. The options under
|
|
||||||
consideration are:
|
|
||||||
- **Firefox** — the packaged `.xpi` is unsigned, and release Firefox and ESR
|
|
||||||
refuse an unsigned add-on:
|
|
||||||
- (a) AMO self-distribution signing (unlisted): submit the `.xpi` to AMO
|
|
||||||
with the owner's credentials; AMO returns a signed `.xpi` installable
|
|
||||||
on every Firefox, with nothing listed publicly.
|
|
||||||
- (b) AMO listed: as (a), plus a public AMO listing and review.
|
|
||||||
- (c) Ship the unsigned `.xpi` and state that Firefox support means
|
|
||||||
Developer Edition, Nightly, or an Unbranded build with
|
|
||||||
`xpinstall.signatures.required` set to `false`.
|
|
||||||
- **Chrome** — the repo packs no CRX and publishes nothing; the extension id
|
|
||||||
is fixed by the `key` in `manifest/chrome.json`:
|
|
||||||
- (a) Chrome Web Store (unlisted): upload the `.zip` with the owner's
|
|
||||||
developer account; the store delivers installs and updates.
|
|
||||||
- (b) Self-hosted CRX signed with the private key the owner holds
|
|
||||||
(`chrome --pack-extension=dist/chrome --pack-extension-key=<path to the .pem>`),
|
|
||||||
installable only via enterprise policy on Windows and macOS, so
|
|
||||||
realistically Linux-only.
|
|
||||||
- (c) "Load unpacked" from `dist/chrome/` only, as today.
|
|
||||||
|
|
||||||
Once the owner decides, the chosen steps — including which credentials they
|
Run `script/cibuild` on the clean checkout. `script/cibuild` runs the same steps
|
||||||
need and who holds them — are written into this section and `README.md`'s
|
as the `check` run, and must exit 0.
|
||||||
installation sections are updated to match, which is part of the definition
|
|
||||||
of done of https://git.eeqj.de/sneak/AutistMask/issues/386. _Check:_ for a
|
Check: on the Actions page, the `check` run for the release commit succeeded,
|
||||||
store or AMO route, the artifact installs from the store or AMO on a clean
|
and both `e2e` jobs for that commit succeeded. `script/cibuild` exited 0 on the
|
||||||
browser profile; for the CRX or unpacked route, the documented load succeeds
|
clean checkout.
|
||||||
and Chrome reports the extension id `gipbhkogfopeahplcjhipkgpcimdpkip`.
|
|
||||||
|
### 2. Confirm the version
|
||||||
|
|
||||||
|
Three files declare the version:
|
||||||
|
|
||||||
|
- `package.json`;
|
||||||
|
- `manifest/chrome.json`;
|
||||||
|
- `manifest/firefox.json`.
|
||||||
|
|
||||||
|
`script/lib/version.js` fails the build when the three disagree.
|
||||||
|
`tests/version.test.js` fails when the version is not three numbers separated by
|
||||||
|
dots. Nothing compares the version with a tag. That comparison is this step.
|
||||||
|
|
||||||
|
Check: all three files read the same `X.Y.Z`, and `vX.Y.Z` is the tag that the
|
||||||
|
owner creates in step 5.
|
||||||
|
|
||||||
|
### 3. Build and package
|
||||||
|
|
||||||
|
On the clean checkout, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make setup
|
||||||
|
make package
|
||||||
|
```
|
||||||
|
|
||||||
|
`make setup` installs two things:
|
||||||
|
|
||||||
|
- the dependencies, from the lockfile `yarn.lock`, which pins each one's
|
||||||
|
version;
|
||||||
|
- the git pre-commit hook, a script that git runs before each commit. Here the
|
||||||
|
hook runs `script/check`.
|
||||||
|
|
||||||
|
`make package` runs `make build`, then `script/lib/package.js`.
|
||||||
|
|
||||||
|
`make build`:
|
||||||
|
|
||||||
|
- runs `build.js`. `build.js` writes `dist/chrome/`, `dist/firefox/` and a build
|
||||||
|
receipt. The receipt lists every file that `build.js` wrote, with each file's
|
||||||
|
digest. The receipt lives outside the repository and is deleted when the build
|
||||||
|
ends;
|
||||||
|
- runs `script/verify-build --expect release` against the receipt. The check
|
||||||
|
fails when:
|
||||||
|
- `dist/` differs from the receipt;
|
||||||
|
- a bundle (one JavaScript file built from many source files) was compiled
|
||||||
|
with the build-time constant `DEBUG` on;
|
||||||
|
- any file carries the test recovery phrase, which a debug build gives every
|
||||||
|
new wallet;
|
||||||
|
- runs `script/check-censored --require-dist` against the built files;
|
||||||
|
- removes `dist/` when any of those steps fails.
|
||||||
|
|
||||||
|
`script/lib/package.js`:
|
||||||
|
|
||||||
|
- empties `release/`;
|
||||||
|
- refuses a symbolic link (a file that points at another file) under `dist/`;
|
||||||
|
- checks that the archive is self-contained: every file that the manifest or the
|
||||||
|
popup's HTML page names is inside the browser's directory (`dist/chrome/` or
|
||||||
|
`dist/firefox/`). The manifest is `manifest.json`, the file that describes the
|
||||||
|
extension to the browser. The popup is the window that the toolbar button
|
||||||
|
opens;
|
||||||
|
- checks that the `version` in each `manifest.json` is the version from step 2;
|
||||||
|
- writes one archive per browser;
|
||||||
|
- reads each archive back;
|
||||||
|
- compares every member (a file inside the archive) with `dist/` byte for byte;
|
||||||
|
- writes `SHA256SUMS`, with one line per archive:
|
||||||
|
- the digest;
|
||||||
|
- two spaces;
|
||||||
|
- the file name.
|
||||||
|
|
||||||
|
`make package` writes three files:
|
||||||
|
|
||||||
|
```
|
||||||
|
release/autistmask-chrome-<version>.zip
|
||||||
|
release/autistmask-firefox-<version>.xpi
|
||||||
|
release/SHA256SUMS
|
||||||
|
```
|
||||||
|
|
||||||
|
The `.xpi` is Firefox's add-on package file (an add-on is Firefox's word for an
|
||||||
|
extension). The `.xpi` is not signed.
|
||||||
|
|
||||||
|
`make package` prints a line that starts `Not shipped` and names `styles.css`.
|
||||||
|
This is expected: each archive holds its own copy of the stylesheet.
|
||||||
|
|
||||||
|
Check: `release/` holds those three files, and `<version>` in their names is the
|
||||||
|
version from step 2.
|
||||||
|
|
||||||
|
### 4. Verify the digests
|
||||||
|
|
||||||
|
From `release/`, run `sha256sum -c SHA256SUMS`.
|
||||||
|
|
||||||
|
The archives are deterministic for a given `dist/`: the same `dist/` always
|
||||||
|
gives the same bytes. `script/lib/zip.js`:
|
||||||
|
|
||||||
|
- sorts the members;
|
||||||
|
- sets every member's timestamp (its recorded date) to 1980-01-01;
|
||||||
|
- compresses each member the same way every time.
|
||||||
|
|
||||||
|
`dist/` itself carries the build date. `build.js` compiles the build date, a UTC
|
||||||
|
(universal time) day, into the extension. So two builds of the same commit on
|
||||||
|
the same UTC day give byte-identical archives. Builds on different days differ.
|
||||||
|
|
||||||
|
Check: `sha256sum -c` prints `OK` for both archives. On the same UTC day, a
|
||||||
|
second `make package` from another clean checkout of the same commit gives the
|
||||||
|
same `SHA256SUMS`.
|
||||||
|
|
||||||
|
### 5. Tag the release commit
|
||||||
|
|
||||||
|
Owner only. Only the owner decides whether and when a release is tagged. Only
|
||||||
|
the owner creates, moves or deletes a tag. On the release commit, the owner
|
||||||
|
runs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a vX.Y.Z -m "AutistMask X.Y.Z"
|
||||||
|
git push origin vX.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
`-a` makes an annotated tag: a tag that carries a message and records who made
|
||||||
|
the tag and when. `v0.1.0` is one.
|
||||||
|
|
||||||
|
Once the tag exists, `git describe --tags` on the release commit prints
|
||||||
|
`vX.Y.Z`. `script/cibuild` and `script/docker` pass that value to the Docker
|
||||||
|
image build as `VERSION`.
|
||||||
|
|
||||||
|
Check: `git rev-parse vX.Y.Z^{commit}` prints the release commit's hash, and the
|
||||||
|
tag is listed at https://git.eeqj.de/sneak/AutistMask/tags.
|
||||||
|
|
||||||
|
### 6. Publish the archives
|
||||||
|
|
||||||
|
Owner only. The owner decides where the archives are published. No script in
|
||||||
|
this repository uploads or publishes them.
|
||||||
|
|
||||||
|
Whoever receives an archive installs it as follows.
|
||||||
|
|
||||||
|
Chrome loads the extension from a directory:
|
||||||
|
|
||||||
|
- unzip the archive into a new, empty directory, for example
|
||||||
|
`unzip autistmask-chrome-0.1.0.zip -d autistmask-chrome`;
|
||||||
|
- open `chrome://extensions/` and turn on "Developer mode";
|
||||||
|
- click "Load unpacked" and pick that directory.
|
||||||
|
|
||||||
|
Chrome then reports the extension id `gipbhkogfopeahplcjhipkgpcimdpkip`. The id
|
||||||
|
is the fixed string that identifies the extension to Chrome; the `key` in
|
||||||
|
`manifest/chrome.json` sets the id.
|
||||||
|
|
||||||
|
Release builds of Firefox refuse to install an unsigned add-on. Desktop Firefox
|
||||||
|
115 or newer, in any edition, loads the `.xpi` as a temporary add-on from the
|
||||||
|
`about:debugging` page. Firefox unloads a temporary add-on when Firefox exits.
|
||||||
|
The section "Installing on Firefox" in `README.md` has the details.
|
||||||
|
|
||||||
|
Check: each archive installs on a fresh browser profile (a new, empty set of
|
||||||
|
browser data) by the route above.
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const { getTopTokens } = require("../../shared/tokenList");
|
|||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { lookupTokenInfo } = require("../../shared/balances");
|
const { lookupTokenInfo } = require("../../shared/balances");
|
||||||
const { isScamAddress } = require("../../shared/scamlist");
|
const { isScamAddress } = require("../../shared/scamlist");
|
||||||
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
|
||||||
const { log } = require("../../shared/log");
|
const { log } = require("../../shared/log");
|
||||||
|
|
||||||
// Counts the times AddToken has opened. A contract lookup started before
|
// Counts the times AddToken has opened. A contract lookup started before
|
||||||
@@ -78,15 +77,6 @@ function init(ctx) {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The balance list never shows a token that the spoof rule
|
|
||||||
// refuses, so such a token is not tracked.
|
|
||||||
if (isSpoofedSymbol(info.symbol, contractAddr)) {
|
|
||||||
showFlash("Not added: this token fakes a known symbol.");
|
|
||||||
if (addTokenScreenOpenings !== opening) return;
|
|
||||||
infoEl.textContent = "";
|
|
||||||
infoEl.style.visibility = "hidden";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.infof("Adding token", info.symbol, contractAddr);
|
log.infof("Adding token", info.symbol, contractAddr);
|
||||||
state.trackedTokens.push({
|
state.trackedTokens.push({
|
||||||
address: contractAddr,
|
address: contractAddr,
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ const { networkByChainId } = require("../../shared/networks");
|
|||||||
const { formatUsd, getPrice } = require("../../shared/prices");
|
const { formatUsd, getPrice } = require("../../shared/prices");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { displaySymbol } = require("../../shared/symbolDisplay");
|
const { displaySymbol } = require("../../shared/symbolDisplay");
|
||||||
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
|
||||||
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
||||||
const { markViewRendered } = require("../viewRouter");
|
const { markViewRendered } = require("../viewRouter");
|
||||||
|
|
||||||
@@ -448,9 +447,6 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
|||||||
if (showZero && trackedTokens) {
|
if (showZero && trackedTokens) {
|
||||||
for (const t of trackedTokens) {
|
for (const t of trackedTokens) {
|
||||||
if (seen.has(t.address.toLowerCase())) continue;
|
if (seen.has(t.address.toLowerCase())) continue;
|
||||||
// A token that the spoof rule refuses has no stored balance, and
|
|
||||||
// gets no zero row either.
|
|
||||||
if (isSpoofedSymbol(t.symbol, t.address)) continue;
|
|
||||||
html += balanceLine(
|
html += balanceLine(
|
||||||
t.symbol,
|
t.symbol,
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -50,11 +50,6 @@ let versionClickTimer = null;
|
|||||||
// not the one in `state`.
|
// not the one in `state`.
|
||||||
let shownNetwork = null;
|
let shownNetwork = null;
|
||||||
|
|
||||||
// Each endpoint "Save" counts its taps here, under the field that it stores.
|
|
||||||
// Once the user taps the same "Save" again, the earlier tap shows no flash
|
|
||||||
// message. The earlier tap stores its URL only if its write had already begun.
|
|
||||||
const saveTaps = { rpcUrl: 0, blockscoutUrl: 0 };
|
|
||||||
|
|
||||||
// One row per site origin, however many addresses it appears under, each with
|
// One row per site origin, however many addresses it appears under, each with
|
||||||
// an [x] that hands it to onRemove.
|
// an [x] that hands it to onRemove.
|
||||||
function renderSiteList(containerId, origins, onRemove) {
|
function renderSiteList(containerId, origins, onRemove) {
|
||||||
@@ -352,16 +347,10 @@ function showNewSettings() {
|
|||||||
// shows `url`, nothing is saved. The flash message then says that the URL was
|
// shows `url`, nothing is saved. The flash message then says that the URL was
|
||||||
// changed during the check. The save compares the input and sets `url` in
|
// changed during the check. The save compares the input and sets `url` in
|
||||||
// `state` in one step. So a redraw after the comparison shows `url`.
|
// `state` in one step. So a redraw after the comparison shows `url`.
|
||||||
//
|
async function saveEndpoint(field, input, url, networkId) {
|
||||||
// `tap` is the number of the "Save" tap that checked `url`. If the user taps
|
|
||||||
// the same "Save" again before this save holds the lock, nothing is stored. If
|
|
||||||
// the user taps the same "Save" again before the write ends, no flash message
|
|
||||||
// is shown.
|
|
||||||
async function saveEndpoint(field, input, url, networkId, tap) {
|
|
||||||
let refused = false;
|
let refused = false;
|
||||||
let inputChanged = false;
|
let inputChanged = false;
|
||||||
await saveState((stored) => {
|
await saveState((stored) => {
|
||||||
if (saveTaps[field] !== tap) return false;
|
|
||||||
refused =
|
refused =
|
||||||
state.networkId !== networkId || stored.networkId !== networkId;
|
state.networkId !== networkId || stored.networkId !== networkId;
|
||||||
inputChanged = $(input).value.trim() !== url;
|
inputChanged = $(input).value.trim() !== url;
|
||||||
@@ -369,7 +358,6 @@ async function saveEndpoint(field, input, url, networkId, tap) {
|
|||||||
state[field] = url;
|
state[field] = url;
|
||||||
stored[field] = url;
|
stored[field] = url;
|
||||||
});
|
});
|
||||||
if (saveTaps[field] !== tap) return;
|
|
||||||
if (refused) {
|
if (refused) {
|
||||||
showFlash("The URL was not saved because the network changed.");
|
showFlash("The URL was not saved because the network changed.");
|
||||||
} else if (inputChanged) {
|
} else if (inputChanged) {
|
||||||
@@ -387,8 +375,6 @@ function init(_ctx) {
|
|||||||
showPhrase.init();
|
showPhrase.init();
|
||||||
|
|
||||||
$("btn-save-rpc").addEventListener("click", async () => {
|
$("btn-save-rpc").addEventListener("click", async () => {
|
||||||
saveTaps.rpcUrl++;
|
|
||||||
const tap = saveTaps.rpcUrl;
|
|
||||||
const url = $("settings-rpc").value.trim();
|
const url = $("settings-rpc").value.trim();
|
||||||
if (!url) {
|
if (!url) {
|
||||||
showFlash("Please enter an RPC URL.");
|
showFlash("Please enter an RPC URL.");
|
||||||
@@ -409,8 +395,6 @@ function init(_ctx) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const json = await resp.json();
|
const json = await resp.json();
|
||||||
// The user has tapped this "Save" again since the check began.
|
|
||||||
if (tap !== saveTaps.rpcUrl) return;
|
|
||||||
if (json.error) {
|
if (json.error) {
|
||||||
log.errorf("RPC validation error:", json.error);
|
log.errorf("RPC validation error:", json.error);
|
||||||
showFlash("Endpoint returned an error.");
|
showFlash("Endpoint returned an error.");
|
||||||
@@ -424,7 +408,6 @@ function init(_ctx) {
|
|||||||
// Cancelled by the popup closing, not failed: see pageClosed in
|
// Cancelled by the popup closing, not failed: see pageClosed in
|
||||||
// src/popup/index.js.
|
// src/popup/index.js.
|
||||||
if (ctx.pageClosed.aborted) return;
|
if (ctx.pageClosed.aborted) return;
|
||||||
if (tap !== saveTaps.rpcUrl) return;
|
|
||||||
// Not the error's message: fetch puts the whole URL, password and
|
// Not the error's message: fetch puts the whole URL, password and
|
||||||
// key included, in the message of the error it throws for a URL
|
// key included, in the message of the error it throws for a URL
|
||||||
// with a user name and password or one it cannot parse.
|
// with a user name and password or one it cannot parse.
|
||||||
@@ -432,12 +415,10 @@ function init(_ctx) {
|
|||||||
showFlash("Could not reach endpoint.");
|
showFlash("Could not reach endpoint.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await saveEndpoint("rpcUrl", "settings-rpc", url, net.id, tap);
|
await saveEndpoint("rpcUrl", "settings-rpc", url, net.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("btn-save-blockscout").addEventListener("click", async () => {
|
$("btn-save-blockscout").addEventListener("click", async () => {
|
||||||
saveTaps.blockscoutUrl++;
|
|
||||||
const tap = saveTaps.blockscoutUrl;
|
|
||||||
const url = $("settings-blockscout").value.trim();
|
const url = $("settings-blockscout").value.trim();
|
||||||
if (!url) {
|
if (!url) {
|
||||||
showFlash("Please enter a Blockscout API URL.");
|
showFlash("Please enter a Blockscout API URL.");
|
||||||
@@ -447,26 +428,18 @@ function init(_ctx) {
|
|||||||
showFlash("Testing endpoint...");
|
showFlash("Testing endpoint...");
|
||||||
try {
|
try {
|
||||||
const resp = await debugFetch(url + "/stats");
|
const resp = await debugFetch(url + "/stats");
|
||||||
if (tap !== saveTaps.blockscoutUrl) return;
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
showFlash("Endpoint returned HTTP " + resp.status + ".");
|
showFlash("Endpoint returned HTTP " + resp.status + ".");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (ctx.pageClosed.aborted) return;
|
if (ctx.pageClosed.aborted) return;
|
||||||
if (tap !== saveTaps.blockscoutUrl) return;
|
|
||||||
// Not the error's message, as for the RPC check above.
|
// Not the error's message, as for the RPC check above.
|
||||||
log.errorf("Blockscout validation failed:", urlOrigin(url));
|
log.errorf("Blockscout validation failed:", urlOrigin(url));
|
||||||
showFlash("Could not reach endpoint.");
|
showFlash("Could not reach endpoint.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await saveEndpoint(
|
await saveEndpoint("blockscoutUrl", "settings-blockscout", url, net.id);
|
||||||
"blockscoutUrl",
|
|
||||||
"settings-blockscout",
|
|
||||||
url,
|
|
||||||
net.id,
|
|
||||||
tap,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Each change is made inside the save, holding the lock. So
|
// Each change is made inside the save, holding the lock. So
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ const { getTopTokens } = require("../../shared/tokenList");
|
|||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { lookupTokenInfo } = require("../../shared/balances");
|
const { lookupTokenInfo } = require("../../shared/balances");
|
||||||
const { isScamAddress } = require("../../shared/scamlist");
|
const { isScamAddress } = require("../../shared/scamlist");
|
||||||
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
|
||||||
const { log } = require("../../shared/log");
|
const { log } = require("../../shared/log");
|
||||||
|
|
||||||
let ctx;
|
let ctx;
|
||||||
@@ -156,16 +155,6 @@ function init(_ctx) {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The balance list never shows a token that the spoof rule
|
|
||||||
// refuses, so such a token is not tracked. The rule never refuses
|
|
||||||
// a bundled token, so the "Top tokens:" buttons and the top 100
|
|
||||||
// dropdown need no such check.
|
|
||||||
if (isSpoofedSymbol(info.symbol, addr)) {
|
|
||||||
showFlash("Not added: this token fakes a known symbol.");
|
|
||||||
infoEl.textContent = "";
|
|
||||||
infoEl.style.visibility = "hidden";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.infof("Adding token", info.symbol, addr);
|
log.infof("Adding token", info.symbol, addr);
|
||||||
state.trackedTokens.push({
|
state.trackedTokens.push({
|
||||||
address: addr,
|
address: addr,
|
||||||
|
|||||||
@@ -167,272 +167,3 @@ test.each([
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// This answer passes both endpoint checks on Ethereum Mainnet.
|
|
||||||
const PASSING_ANSWER = {
|
|
||||||
ok: true,
|
|
||||||
json: async () => ({ result: NETWORKS.mainnet.chainId }),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Both endpoint checks read this answer as an error. The RPC check reads the
|
|
||||||
// answer's `error`. The Blockscout check reads the answer's `ok`.
|
|
||||||
const ERROR_ANSWER = {
|
|
||||||
ok: false,
|
|
||||||
status: 500,
|
|
||||||
json: async () => ({ error: { code: -32603, message: "Internal error" } }),
|
|
||||||
};
|
|
||||||
|
|
||||||
// The user types a URL in Settings and taps "Save". Before the endpoint answers,
|
|
||||||
// the user types a second URL and taps "Save" again. The second check passes
|
|
||||||
// first. The second URL is stored, and the flash message reads "Saved.". The
|
|
||||||
// first check then ends in one of three ways:
|
|
||||||
//
|
|
||||||
// - It passes.
|
|
||||||
// - It fails, because fetch cannot reach the endpoint.
|
|
||||||
// - It gets an answer that holds an error.
|
|
||||||
//
|
|
||||||
// None of the three changes the stored URL or the flash message
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/698).
|
|
||||||
test.each([
|
|
||||||
{
|
|
||||||
button: "btn-save-rpc",
|
|
||||||
input: "settings-rpc",
|
|
||||||
field: "rpcUrl",
|
|
||||||
first: "passes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
button: "btn-save-rpc",
|
|
||||||
input: "settings-rpc",
|
|
||||||
field: "rpcUrl",
|
|
||||||
first: "fails",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
button: "btn-save-rpc",
|
|
||||||
input: "settings-rpc",
|
|
||||||
field: "rpcUrl",
|
|
||||||
first: "answers with an error",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
button: "btn-save-blockscout",
|
|
||||||
input: "settings-blockscout",
|
|
||||||
field: "blockscoutUrl",
|
|
||||||
first: "passes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
button: "btn-save-blockscout",
|
|
||||||
input: "settings-blockscout",
|
|
||||||
field: "blockscoutUrl",
|
|
||||||
first: "fails",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
button: "btn-save-blockscout",
|
|
||||||
input: "settings-blockscout",
|
|
||||||
field: "blockscoutUrl",
|
|
||||||
first: "answers with an error",
|
|
||||||
},
|
|
||||||
])(
|
|
||||||
"$button keeps the second tap's result when the first check $first last",
|
|
||||||
async ({ button, input, field, first }) => {
|
|
||||||
const page = await bootPopup(unversionedValidProfile());
|
|
||||||
await page.click("btn-settings");
|
|
||||||
const checks = [];
|
|
||||||
globalThis.fetch.mockImplementation(
|
|
||||||
() =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
checks.push({ resolve, reject });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const secondUrl = "https://second.example.invalid";
|
|
||||||
|
|
||||||
page.node(input).value = "https://first.example.invalid";
|
|
||||||
const firstTap = page.node(button).listeners.click.map((fn) => fn());
|
|
||||||
await page.settle();
|
|
||||||
page.node(input).value = secondUrl;
|
|
||||||
const secondTap = page.node(button).listeners.click.map((fn) => fn());
|
|
||||||
await page.settle();
|
|
||||||
checks[1].resolve(PASSING_ANSWER);
|
|
||||||
await Promise.all(secondTap);
|
|
||||||
await page.settle();
|
|
||||||
expect(page.text("flash-msg")).toBe("Saved.");
|
|
||||||
if (first === "passes") {
|
|
||||||
checks[0].resolve(PASSING_ANSWER);
|
|
||||||
} else if (first === "answers with an error") {
|
|
||||||
checks[0].resolve(ERROR_ANSWER);
|
|
||||||
} else {
|
|
||||||
// fetch rejects when it cannot reach the endpoint.
|
|
||||||
checks[0].reject(new TypeError("Failed to fetch"));
|
|
||||||
}
|
|
||||||
await Promise.all(firstTap);
|
|
||||||
await page.settle();
|
|
||||||
|
|
||||||
expect(checks).toHaveLength(2);
|
|
||||||
expect(page.storage.read("autistmask")[field]).toBe(secondUrl);
|
|
||||||
expect(page.text("flash-msg")).toBe("Saved.");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// The user types a URL in Settings and taps "Save". Before the endpoint answers,
|
|
||||||
// the user empties the input and taps "Save" again. The flash message asks for
|
|
||||||
// a URL. The first check then passes. The flash message still asks for a URL,
|
|
||||||
// and the stored URL is unchanged.
|
|
||||||
test.each([
|
|
||||||
{
|
|
||||||
button: "btn-save-rpc",
|
|
||||||
input: "settings-rpc",
|
|
||||||
field: "rpcUrl",
|
|
||||||
message: "Please enter an RPC URL.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
button: "btn-save-blockscout",
|
|
||||||
input: "settings-blockscout",
|
|
||||||
field: "blockscoutUrl",
|
|
||||||
message: "Please enter a Blockscout API URL.",
|
|
||||||
},
|
|
||||||
])(
|
|
||||||
"$button keeps the empty-input message when the first check passes last",
|
|
||||||
async ({ button, input, field, message }) => {
|
|
||||||
const page = await bootPopup(unversionedValidProfile());
|
|
||||||
await page.click("btn-settings");
|
|
||||||
const storedUrl = page.value(input);
|
|
||||||
let answer;
|
|
||||||
globalThis.fetch.mockReturnValue(
|
|
||||||
new Promise((resolve) => {
|
|
||||||
answer = resolve;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
page.node(input).value = "https://typed.example.invalid";
|
|
||||||
const firstTap = page.node(button).listeners.click.map((fn) => fn());
|
|
||||||
await page.settle();
|
|
||||||
page.node(input).value = "";
|
|
||||||
await page.click(button);
|
|
||||||
expect(page.text("flash-msg")).toBe(message);
|
|
||||||
answer(PASSING_ANSWER);
|
|
||||||
await Promise.all(firstTap);
|
|
||||||
await page.settle();
|
|
||||||
|
|
||||||
expect(page.storage.read("autistmask")[field]).toBe(storedUrl);
|
|
||||||
expect(page.text("flash-msg")).toBe(message);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// The user types a URL in Settings and taps "Save". Another write holds the
|
|
||||||
// lock. So the save waits for the lock once the check passes. The user then
|
|
||||||
// taps "Save" again on the same URL. When the lock is free, the first save
|
|
||||||
// stores nothing and shows no flash message. The second check then passes, and
|
|
||||||
// the second save stores the URL.
|
|
||||||
test.each([
|
|
||||||
{ button: "btn-save-rpc", input: "settings-rpc", field: "rpcUrl" },
|
|
||||||
{
|
|
||||||
button: "btn-save-blockscout",
|
|
||||||
input: "settings-blockscout",
|
|
||||||
field: "blockscoutUrl",
|
|
||||||
},
|
|
||||||
])(
|
|
||||||
"$button stores nothing for an earlier tap still waiting for the lock",
|
|
||||||
async ({ button, input, field }) => {
|
|
||||||
const page = await bootPopup(unversionedValidProfile());
|
|
||||||
await page.click("btn-settings");
|
|
||||||
const storedUrl = page.value(input);
|
|
||||||
const checks = [];
|
|
||||||
globalThis.fetch.mockImplementation(
|
|
||||||
() =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
checks.push(resolve);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const typedUrl = "https://typed.example.invalid";
|
|
||||||
|
|
||||||
page.node(input).value = typedUrl;
|
|
||||||
const firstTap = page.node(button).listeners.click.map((fn) => fn());
|
|
||||||
await page.settle();
|
|
||||||
let releaseLock;
|
|
||||||
const lockHeld = navigator.locks.request(
|
|
||||||
"autistmask",
|
|
||||||
() =>
|
|
||||||
new Promise((resolve) => {
|
|
||||||
releaseLock = resolve;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
checks[0](PASSING_ANSWER);
|
|
||||||
await page.settle();
|
|
||||||
const secondTap = page.node(button).listeners.click.map((fn) => fn());
|
|
||||||
await page.settle();
|
|
||||||
releaseLock();
|
|
||||||
await lockHeld;
|
|
||||||
await Promise.all(firstTap);
|
|
||||||
await page.settle();
|
|
||||||
|
|
||||||
expect(page.storage.read("autistmask")[field]).toBe(storedUrl);
|
|
||||||
expect(page.text("flash-msg")).toBe("Testing endpoint...");
|
|
||||||
|
|
||||||
checks[1](PASSING_ANSWER);
|
|
||||||
await Promise.all(secondTap);
|
|
||||||
await page.settle();
|
|
||||||
|
|
||||||
expect(checks).toHaveLength(2);
|
|
||||||
expect(page.storage.read("autistmask")[field]).toBe(typedUrl);
|
|
||||||
expect(page.text("flash-msg")).toBe("Saved.");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// The user taps "Save", and the check passes. The save then begins its storage
|
|
||||||
// write, and this test keeps the write waiting. The user taps "Save" again, and
|
|
||||||
// the second check cannot reach the endpoint. When the write ends, the flash
|
|
||||||
// message still reads "Could not reach endpoint.". The write stores the URL
|
|
||||||
// anyway, because the write began before the second tap.
|
|
||||||
test.each([
|
|
||||||
{ button: "btn-save-rpc", input: "settings-rpc", field: "rpcUrl" },
|
|
||||||
{
|
|
||||||
button: "btn-save-blockscout",
|
|
||||||
input: "settings-blockscout",
|
|
||||||
field: "blockscoutUrl",
|
|
||||||
},
|
|
||||||
])(
|
|
||||||
"$button keeps the second tap's message when tapped again during the write",
|
|
||||||
async ({ button, input, field }) => {
|
|
||||||
const page = await bootPopup(unversionedValidProfile());
|
|
||||||
await page.click("btn-settings");
|
|
||||||
const checks = [];
|
|
||||||
globalThis.fetch.mockImplementation(
|
|
||||||
() =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
checks.push({ resolve, reject });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const typedUrl = "https://typed.example.invalid";
|
|
||||||
|
|
||||||
page.node(input).value = typedUrl;
|
|
||||||
const firstTap = page.node(button).listeners.click.map((fn) => fn());
|
|
||||||
await page.settle();
|
|
||||||
const { set } = page.storage.local;
|
|
||||||
const write = set.getMockImplementation();
|
|
||||||
let writeBegan = false;
|
|
||||||
let releaseWrite;
|
|
||||||
const writeHeld = new Promise((resolve) => {
|
|
||||||
releaseWrite = resolve;
|
|
||||||
});
|
|
||||||
set.mockImplementationOnce(async (items) => {
|
|
||||||
writeBegan = true;
|
|
||||||
await writeHeld;
|
|
||||||
await write(items);
|
|
||||||
});
|
|
||||||
checks[0].resolve(PASSING_ANSWER);
|
|
||||||
await page.settle();
|
|
||||||
expect(writeBegan).toBe(true);
|
|
||||||
const secondTap = page.node(button).listeners.click.map((fn) => fn());
|
|
||||||
await page.settle();
|
|
||||||
// fetch rejects when it cannot reach the endpoint.
|
|
||||||
checks[1].reject(new TypeError("Failed to fetch"));
|
|
||||||
await Promise.all(secondTap);
|
|
||||||
await page.settle();
|
|
||||||
expect(page.text("flash-msg")).toBe("Could not reach endpoint.");
|
|
||||||
releaseWrite();
|
|
||||||
await Promise.all(firstTap);
|
|
||||||
await page.settle();
|
|
||||||
|
|
||||||
expect(checks).toHaveLength(2);
|
|
||||||
expect(page.storage.read("autistmask")[field]).toBe(typedUrl);
|
|
||||||
expect(page.text("flash-msg")).toBe("Could not reach endpoint.");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
// A token that the spoof rule refuses never shows in the balance list, even
|
|
||||||
// tracked at zero balance with "Show tracked tokens with zero balance" on, and
|
|
||||||
// the two add-token screens refuse to track one
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/705). Driven through the real
|
|
||||||
// popup over a stored profile.
|
|
||||||
|
|
||||||
const {
|
|
||||||
bootPopup,
|
|
||||||
cleanupPopup,
|
|
||||||
unversionedValidProfile,
|
|
||||||
TOKEN_ADDRESS,
|
|
||||||
} = require("./support/popupBoot");
|
|
||||||
|
|
||||||
// Claims the symbol of the bundled USDT from another contract.
|
|
||||||
const FAKE_USDT = {
|
|
||||||
address: TOKEN_ADDRESS,
|
|
||||||
symbol: "USDT",
|
|
||||||
decimals: 6,
|
|
||||||
name: "Tether USD",
|
|
||||||
};
|
|
||||||
// A symbol the bundled list does not have, so the spoof rule passes it.
|
|
||||||
const TEST_TOKEN = {
|
|
||||||
address: "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
|
|
||||||
symbol: "TST",
|
|
||||||
decimals: 18,
|
|
||||||
name: "Test Token",
|
|
||||||
};
|
|
||||||
const REFUSED_FLASH = "Not added: this token fakes a known symbol.";
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
cleanupPopup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a fake token tracked at zero balance is not listed while zero balances are shown", async () => {
|
|
||||||
const boot = await bootPopup(
|
|
||||||
unversionedValidProfile({
|
|
||||||
currentView: "address",
|
|
||||||
selectedWallet: 0,
|
|
||||||
selectedAddress: 0,
|
|
||||||
trackedTokens: [FAKE_USDT, TEST_TOKEN],
|
|
||||||
showZeroBalanceTokens: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const rows = boot.node("address-balances").innerHTML;
|
|
||||||
expect(rows).toContain(`data-token="${TEST_TOKEN.address.toLowerCase()}"`);
|
|
||||||
expect(rows).not.toContain(
|
|
||||||
`data-token="${FAKE_USDT.address.toLowerCase()}"`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Each screen is opened from AddressDetail by the buttons named, in order.
|
|
||||||
describe.each([
|
|
||||||
[
|
|
||||||
"AddToken",
|
|
||||||
["btn-add-token"],
|
|
||||||
"add-token",
|
|
||||||
"add-token-address",
|
|
||||||
"btn-add-token-confirm",
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"SettingsAddToken",
|
|
||||||
["btn-settings", "btn-settings-add-token"],
|
|
||||||
"settings-addtoken",
|
|
||||||
"settings-addtoken-address",
|
|
||||||
"btn-settings-addtoken-manual",
|
|
||||||
],
|
|
||||||
])("%s", (_, opening, view, field, button) => {
|
|
||||||
test("refuses a fake token by contract address", async () => {
|
|
||||||
const boot = await bootPopup(
|
|
||||||
unversionedValidProfile({
|
|
||||||
currentView: "address",
|
|
||||||
selectedWallet: 0,
|
|
||||||
selectedAddress: 0,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
for (const id of opening) await boot.click(id);
|
|
||||||
expect(boot.visibleViews()).toEqual([view]);
|
|
||||||
const { lookupTokenInfo } = require("../src/shared/balances");
|
|
||||||
lookupTokenInfo.mockResolvedValueOnce({
|
|
||||||
symbol: FAKE_USDT.symbol,
|
|
||||||
decimals: FAKE_USDT.decimals,
|
|
||||||
name: FAKE_USDT.name,
|
|
||||||
});
|
|
||||||
boot.node(field).value = FAKE_USDT.address;
|
|
||||||
|
|
||||||
await boot.click(button);
|
|
||||||
expect(boot.text("flash-msg")).toBe(REFUSED_FLASH);
|
|
||||||
expect(boot.visibleViews()).toEqual([view]);
|
|
||||||
expect(boot.storage.read("autistmask").trackedTokens).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user