Compare commits
1
Commits
next
..
7111d8be36
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7111d8be36 |
@@ -3,9 +3,6 @@ on: [push]
|
|||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Bounds script/cibuild, a cold-cache build included, so a hang frees
|
|
||||||
# the shared runner. README.md "In CI" has the measured times.
|
|
||||||
timeout-minutes: 10
|
|
||||||
steps:
|
steps:
|
||||||
# actions/checkout v4.2.2, 2026-02-22
|
# actions/checkout v4.2.2, 2026-02-22
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
|||||||
@@ -35,10 +35,6 @@ on: [push]
|
|||||||
jobs:
|
jobs:
|
||||||
e2e-chrome:
|
e2e-chrome:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Bounds the image build, a cold cache included, and both Chrome
|
|
||||||
# programs, so a hung browser frees the shared runner. README.md
|
|
||||||
# "In CI" has the measured times.
|
|
||||||
timeout-minutes: 20
|
|
||||||
steps:
|
steps:
|
||||||
# actions/checkout v4.2.2, 2026-02-22
|
# actions/checkout v4.2.2, 2026-02-22
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
@@ -46,10 +42,6 @@ jobs:
|
|||||||
|
|
||||||
e2e-firefox:
|
e2e-firefox:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Bounds the image build, a cold cache included, and both Firefox
|
|
||||||
# programs, so a hung browser frees the shared runner. README.md
|
|
||||||
# "In CI" has the measured times.
|
|
||||||
timeout-minutes: 15
|
|
||||||
steps:
|
steps:
|
||||||
# actions/checkout v4.2.2, 2026-02-22
|
# actions/checkout v4.2.2, 2026-02-22
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ node_modules/
|
|||||||
yarn.lock
|
yarn.lock
|
||||||
dist/
|
dist/
|
||||||
release/
|
release/
|
||||||
|
.claude/
|
||||||
|
|||||||
@@ -107,14 +107,6 @@ vendor-blocklist:
|
|||||||
clean:
|
clean:
|
||||||
@rm -rf dist/ release/
|
@rm -rf dist/ release/
|
||||||
|
|
||||||
# Run the build make build runs, without the checks that follow it, then run it
|
|
||||||
# again after every change to a file under src/, manifest/ or icons/, until
|
|
||||||
# interrupted. A failed build is reported, may leave dist/ partly written, and
|
|
||||||
# watching carries on. It writes no build receipt, so nothing can verify what it
|
|
||||||
# leaves in dist/: anything handed on comes from make build. A release build
|
|
||||||
# unless AUTISTMASK_DEBUG=1 is exported. A change anywhere else, package.json
|
|
||||||
# and build.js included, starts no build, and a directory created while it runs
|
|
||||||
# is not watched; restart it after either.
|
|
||||||
dev:
|
dev:
|
||||||
@echo "Building in watch mode..."
|
@echo "Building in watch mode..."
|
||||||
@yarn run build --watch 2>&1
|
@yarn run build --watch 2>&1
|
||||||
|
|||||||
@@ -306,15 +306,7 @@ The Makefile shims to those. It also carries a few targets that have no
|
|||||||
debug build, and keeping its `dist/` on failure (see
|
debug build, and keeping its `dist/` on failure (see
|
||||||
[Debug Builds](#debug-builds))
|
[Debug Builds](#debug-builds))
|
||||||
- `make clean` — remove `dist/` and `release/`
|
- `make clean` — remove `dist/` and `release/`
|
||||||
- `make dev` — run the build `make build` runs, without the checks that follow
|
- `make dev` — build in watch mode
|
||||||
it, then run it again after every change to a file under `src/`, `manifest/`
|
|
||||||
or `icons/`, until interrupted. A failed build is reported, may leave `dist/`
|
|
||||||
partly written, and watching carries on. It writes no build receipt, so
|
|
||||||
nothing can verify what it leaves in `dist/` (see
|
|
||||||
[Build Receipts](#build-receipts)): anything handed on comes from
|
|
||||||
`make build`. A release build unless `AUTISTMASK_DEBUG=1` is exported. A
|
|
||||||
change anywhere else, `package.json` and `build.js` included, starts no build,
|
|
||||||
and a directory created while it runs is not watched; restart it after either
|
|
||||||
|
|
||||||
## End-to-End Tests
|
## End-to-End Tests
|
||||||
|
|
||||||
@@ -384,12 +376,6 @@ reserve while sitting on the same side of the estimate, so swapping the two in
|
|||||||
what [#154](https://git.eeqj.de/sneak/AutistMask/issues/154) was, and it was
|
what [#154](https://git.eeqj.de/sneak/AutistMask/issues/154) was, and it was
|
||||||
previously correct by reading only.
|
previously correct by reading only.
|
||||||
|
|
||||||
It also covers both ways the wait for a sent transaction's receipt ends on the
|
|
||||||
error screen: lookups that still find no receipt 60 seconds after the broadcast,
|
|
||||||
and six lookups in a row that fail. Each must show its own message, and Done
|
|
||||||
must lead back to the address screen. Both wait in real time, about a minute
|
|
||||||
each.
|
|
||||||
|
|
||||||
It also covers the **dApp approval round trips** — the one place where the
|
It also covers the **dApp approval round trips** — the one place where the
|
||||||
content script, the inpage provider, the background worker and the approval
|
content script, the inpage provider, the background worker and the approval
|
||||||
popup all have to work together. A local test page is served by the route
|
popup all have to work together. A local test page is served by the route
|
||||||
@@ -651,20 +637,10 @@ Nothing in either job can pass vacuously. There is no `continue-on-error` and no
|
|||||||
build fails, and when the browser fails to start; the Chrome harness aborts the
|
build fails, and when the browser fails to start; the Chrome harness aborts the
|
||||||
suite outright if its network interception is not in effect.
|
suite outright if its network interception is not in effect.
|
||||||
|
|
||||||
Measured on this repo's runner in the green runs of early October 2026, from a
|
Measured on this repo's runner: `e2e-chrome` about 1m55s cold, almost all of it
|
||||||
warm docker cache to a cold one: `check` 49s to 3m37s, `e2e-chrome` 1m44s to
|
the one-time pull of the pinned ~800MB Playwright layer, and well under a minute
|
||||||
4m48s, and `e2e-firefox` 31s to 4m07s. A cold cache adds three to four minutes
|
once that layer is cached. `e2e-firefox` about 1m05s cold, and it caches its
|
||||||
to each job, spent rebuilding its image: reinstalling dependencies and, for
|
Firefox and geckodriver downloads the same way.
|
||||||
`e2e-firefox`, installing Firefox, geckodriver and their system libraries. Those
|
|
||||||
`e2e-chrome` runs predate the cases that wait in real time for a receipt to end
|
|
||||||
in error. `make test-e2e` now takes 3m51s locally with its image cached, so a
|
|
||||||
cold `e2e-chrome` run comes to about seven minutes.
|
|
||||||
|
|
||||||
Every job has a `timeout-minutes` cap, so a hung build or browser ends the job
|
|
||||||
instead of holding the shared runner: `check` 10 minutes, `e2e-firefox` 15 and
|
|
||||||
`e2e-chrome` 20, each over two and a half times the job's slowest cold run. A
|
|
||||||
job that reaches its cap has hung; read it as a hang, not as a slow run to
|
|
||||||
retry.
|
|
||||||
|
|
||||||
### Element id guard (part of `make check`)
|
### Element id guard (part of `make check`)
|
||||||
|
|
||||||
@@ -830,15 +806,13 @@ discoverable.
|
|||||||
on critical screens and when space is available to allow users to disambiguate
|
on critical screens and when space is available to allow users to disambiguate
|
||||||
addresses visually, as a security feature.
|
addresses visually, as a security feature.
|
||||||
- **Tailwind CSS**: Utility-first CSS via Tailwind. No custom CSS classes for
|
- **Tailwind CSS**: Utility-first CSS via Tailwind. No custom CSS classes for
|
||||||
styling, and no `style="..."` attributes, which the
|
styling. Tailwind is configured with a minimal monochrome palette. This keeps
|
||||||
[Content Security Policy](#content-security-policy) refuses. Tailwind is
|
the styling co-located with the markup and eliminates CSS file management. The
|
||||||
configured with a minimal monochrome palette. This keeps the styling
|
handful of classes in `styles/main.css` are not styling: `.copy-flash-*`
|
||||||
co-located with the markup and eliminates CSS file management. The handful of
|
carries the copy feedback animation, and `.am-address` carries the rule that
|
||||||
classes in `styles/main.css` are not styling: `.copy-flash-*` carries the copy
|
an address never wraps. Both are invariants that hold in every place they
|
||||||
feedback animation, and `.am-address` carries the rule that an address never
|
appear, and spelling either out as repeated utilities is how one of those
|
||||||
wraps. Both are invariants that hold in every place they appear, and spelling
|
places drifts away from the rest.
|
||||||
either out as repeated utilities is how one of those places drifts away from
|
|
||||||
the rest.
|
|
||||||
- **Vanilla JS**: No framework (React, Vue, Svelte, etc.). The popup UI is small
|
- **Vanilla JS**: No framework (React, Vue, Svelte, etc.). The popup UI is small
|
||||||
enough that vanilla JS with simple view switching is sufficient. A framework
|
enough that vanilla JS with simple view switching is sufficient. A framework
|
||||||
would add bundle size, build complexity, and attack surface for no benefit at
|
would add bundle size, build complexity, and attack surface for no benefit at
|
||||||
@@ -2231,7 +2205,7 @@ a bare string in `manifest/firefox.json` (MV2):
|
|||||||
|
|
||||||
```
|
```
|
||||||
default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self';
|
default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self';
|
||||||
style-src 'self'; img-src 'self' data:;
|
style-src 'self' 'unsafe-inline'; img-src 'self' data:;
|
||||||
connect-src 'self' https: http:; frame-src 'none'; form-action 'none';
|
connect-src 'self' https: http:; frame-src 'none'; form-action 'none';
|
||||||
base-uri 'none'
|
base-uri 'none'
|
||||||
```
|
```
|
||||||
@@ -2243,17 +2217,15 @@ wallet's own UI. Escaping is the primary fix for that (see
|
|||||||
`src/shared/html.js`); this is the second line, so an escape that does slip
|
`src/shared/html.js`); this is the second line, so an escape that does slip
|
||||||
cannot reach the network.
|
cannot reach the network.
|
||||||
|
|
||||||
`style-src 'self'` admits the stylesheet and nothing inline: both browsers
|
Four directives are looser than `'self'`, each for a reason that does not
|
||||||
refuse a `style="..."` attribute and a `<style>` block. So the popup's markup,
|
|
||||||
in `src/popup/index.html` and in the HTML the view helpers build, carries
|
|
||||||
Tailwind classes and never a `style` attribute. Script that sets `element.style`
|
|
||||||
is not affected; that is how the views show and hide their error lines. An
|
|
||||||
inline style that slips in anyway is refused with a console error, which fails
|
|
||||||
both end-to-end suites.
|
|
||||||
|
|
||||||
These directives differ from a plain `'self'`, each for a reason that does not
|
|
||||||
generalise:
|
generalise:
|
||||||
|
|
||||||
|
- `style-src 'unsafe-inline'` — `src/popup/index.html` and the view helpers set
|
||||||
|
presentation through `style="..."` attributes, which CSP blocks without this.
|
||||||
|
Chrome enforces `style-src` on attributes, not only on `<style>` blocks, and
|
||||||
|
Firefox has never implemented `style-src-attr`, so there is no narrower
|
||||||
|
spelling that works on both targets. It permits inline **style**; script stays
|
||||||
|
under `script-src`, which does not allow `'unsafe-inline'`.
|
||||||
- `img-src data:` — identicons are generated in the popup by
|
- `img-src data:` — identicons are generated in the popup by
|
||||||
`ethereum-blockies-base64` and assigned to `img.src` as `data:` PNGs.
|
`ethereum-blockies-base64` and assigned to `img.src` as `data:` PNGs.
|
||||||
- `connect-src https: http:` — the RPC endpoint is user-configurable and a local
|
- `connect-src https: http:` — the RPC endpoint is user-configurable and a local
|
||||||
|
|||||||
@@ -45,77 +45,6 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-10-05: `make dev` watches
|
|
||||||
([#332](https://git.eeqj.de/sneak/AutistMask/issues/332)). It used to pass
|
|
||||||
`--watch` to a `build.js` that read no arguments, so it built once and exited.
|
|
||||||
`build.js --watch` now builds, then builds again after every change to a file
|
|
||||||
under `src/`, `manifest/` or `icons/`; any other argument fails. It watches
|
|
||||||
each directory rather than using Node's recursive watch, which on Linux stops
|
|
||||||
seeing a file an editor saves by renaming a new copy over it. It writes no
|
|
||||||
build receipt, so nothing can verify what it builds; `make build` remains the
|
|
||||||
way to produce a `dist/` to hand on. `tests/buildWatch.test.js` covers the
|
|
||||||
watch loop against a temp directory.
|
|
||||||
|
|
||||||
- 2026-10-05: Every CI job has a `timeout-minutes` cap
|
|
||||||
([#294](https://git.eeqj.de/sneak/AutistMask/issues/294)): `check` 10 minutes,
|
|
||||||
`e2e-firefox` 15 and `e2e-chrome` 20, each over two and a half times the job's
|
|
||||||
slowest cold-cache run. A hung build or browser now ends its job instead of
|
|
||||||
holding the shared runner for hours.
|
|
||||||
|
|
||||||
- 2026-10-05: `PROXY_METHODS` in `src/background/index.js` no longer lists
|
|
||||||
`eth_chainId` and `net_version`
|
|
||||||
([#326](https://git.eeqj.de/sneak/AutistMask/issues/326)). `handleRpc` answers
|
|
||||||
both itself before its proxy branch, so the list named two methods that are
|
|
||||||
never sent to the RPC endpoint. No other entry is answered earlier.
|
|
||||||
`tests/proxyMethods.test.js` sends every listed method from a page and fails
|
|
||||||
on any that does not reach the RPC endpoint.
|
|
||||||
|
|
||||||
- 2026-10-05: The popup's Content Security Policy no longer allows inline style
|
|
||||||
([#328](https://git.eeqj.de/sneak/AutistMask/issues/328)): `style-src` is
|
|
||||||
`'self'` in both manifests, pinned in `tests/manifest.test.js`. The 42
|
|
||||||
`style="..."` attributes in `src/popup/index.html` and in the markup the view
|
|
||||||
helpers build are now Tailwind classes, each computing to the value it
|
|
||||||
replaced. The 16 address dot colours are written out as whole classes, because
|
|
||||||
Tailwind builds only the classes it finds in the source. The Settings debug
|
|
||||||
well is shown and hidden with the `hidden` class, since clearing an inline
|
|
||||||
`display` no longer uncovers it. Script that sets `element.style` is
|
|
||||||
unaffected.
|
|
||||||
|
|
||||||
- 2026-10-05: `.prettierignore` no longer lists an AI vendor's tool directory
|
|
||||||
([#363](https://git.eeqj.de/sneak/AutistMask/issues/363)). The directory is
|
|
||||||
not tracked, so the line ignored nothing.
|
|
||||||
|
|
||||||
- 2026-10-05: The Chrome end-to-end suite drives both ways the wait for a
|
|
||||||
transaction's receipt ends on the error screen
|
|
||||||
([#315](https://git.eeqj.de/sneak/AutistMask/issues/315)): lookups that still
|
|
||||||
find no receipt 60 seconds after the broadcast end it with the timeout
|
|
||||||
message, and six lookups that fail in a row end it with the message naming the
|
|
||||||
unreachable network. Done then returns to the address screen. Both cases wait
|
|
||||||
in real time, about a minute each. Playwright's clock would apply to every
|
|
||||||
later test in the run and cannot be removed, and moving the stored broadcast
|
|
||||||
time back can be undone by the save the popup makes every ten seconds.
|
|
||||||
|
|
||||||
- 2026-10-05: The Chrome end-to-end suite covers the last of the
|
|
||||||
[#150](https://git.eeqj.de/sneak/AutistMask/issues/150) and
|
|
||||||
[#151](https://git.eeqj.de/sneak/AutistMask/issues/151) items
|
|
||||||
([#295](https://git.eeqj.de/sneak/AutistMask/issues/295)): a token added on
|
|
||||||
Add Token by its contract address is listed on the address screen;
|
|
||||||
TransactionDetail opened from the token screen leaves that screen on the
|
|
||||||
persisted navigation stack, and Back returns to it; and the token contract row
|
|
||||||
links to the explorer's token page, read off the link rather than followed.
|
|
||||||
The network stub answers `symbol()` and `name()` for the stub token, which
|
|
||||||
adding it reads.
|
|
||||||
|
|
||||||
- 2026-10-05: Each control that leads to a signature or to the private key has a
|
|
||||||
test that it refuses a defective wallet before asking for a password
|
|
||||||
([#254](https://git.eeqj.de/sneak/AutistMask/issues/254)): Send on the main,
|
|
||||||
address and token screens, Export Private Key, and both approval screens, as
|
|
||||||
drawn and as clicked. Send on the confirmation screen refuses it too now,
|
|
||||||
because the popup reopens onto that screen from a saved view. The comments
|
|
||||||
that said the wallet's key cannot be derived now say that
|
|
||||||
`getSignerForAddress` refuses it, and the module comment in
|
|
||||||
`src/shared/walletDefects.js` names both earlier import paths.
|
|
||||||
|
|
||||||
- 2026-10-05: The Chrome end-to-end suite drives the private key export screen
|
- 2026-10-05: The Chrome end-to-end suite drives the private key export screen
|
||||||
as it drives the recovery phrase screen
|
as it drives the recovery phrase screen
|
||||||
([#253](https://git.eeqj.de/sneak/AutistMask/issues/253)): the correct
|
([#253](https://git.eeqj.de/sneak/AutistMask/issues/253)): the correct
|
||||||
|
|||||||
@@ -15,15 +15,6 @@ const DIST_CHROME = path.join(DIST, "chrome");
|
|||||||
const DIST_FIREFOX = path.join(DIST, "firefox");
|
const DIST_FIREFOX = path.join(DIST, "firefox");
|
||||||
const SRC = path.join(__dirname, "src");
|
const SRC = path.join(__dirname, "src");
|
||||||
|
|
||||||
// What `make dev` watches: the directories whose files build() bundles,
|
|
||||||
// compiles or copies. A change anywhere else, package.json and build.js
|
|
||||||
// included, starts no build; restart make dev after one.
|
|
||||||
const WATCHED_DIRS = [
|
|
||||||
SRC,
|
|
||||||
path.join(__dirname, "manifest"),
|
|
||||||
path.join(__dirname, "icons"),
|
|
||||||
];
|
|
||||||
|
|
||||||
// The module whose compiled DEBUG state script/verify-build asserts. Which
|
// The module whose compiled DEBUG state script/verify-build asserts. Which
|
||||||
// bundles contain it is derived from esbuild's own dependency graph rather
|
// bundles contain it is derived from esbuild's own dependency graph rather
|
||||||
// than from a hardcoded list, so it tracks the bundle layout instead of
|
// than from a hardcoded list, so it tracks the bundle layout instead of
|
||||||
@@ -450,10 +441,6 @@ function getBuildInfo() {
|
|||||||
async function build() {
|
async function build() {
|
||||||
console.log("Building AutistMask extension...");
|
console.log("Building AutistMask extension...");
|
||||||
|
|
||||||
// Under make dev this runs once per rebuild in the same process, and each
|
|
||||||
// build accounts only for what it emits itself.
|
|
||||||
emittedFiles.length = 0;
|
|
||||||
|
|
||||||
const receiptPath = receiptTarget();
|
const receiptPath = receiptTarget();
|
||||||
if (!receiptPath) {
|
if (!receiptPath) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -610,94 +597,27 @@ async function build() {
|
|||||||
console.log("Build complete: dist/chrome/ and dist/firefox/");
|
console.log("Build complete: dist/chrome/ and dist/firefox/");
|
||||||
}
|
}
|
||||||
|
|
||||||
// make dev: build, then build again after every change under `dirs`, until
|
|
||||||
// interrupted. A failed build is reported and watching carries on, so a
|
|
||||||
// half-finished edit does not end it. A change that arrives while a build is
|
|
||||||
// running is not lost: it causes one more build as soon as that one finishes.
|
|
||||||
// A directory created after this starts is not watched until a restart.
|
|
||||||
//
|
|
||||||
// make dev sets no AUTISTMASK_BUILD_RECEIPT, so these builds write no receipt
|
|
||||||
// and nothing can verify what they leave in dist/.
|
|
||||||
//
|
|
||||||
// `rebuild` is build() everywhere but tests/buildWatch.test.js, which also
|
|
||||||
// closes the returned watchers.
|
|
||||||
function watch(dirs, rebuild) {
|
|
||||||
let building = false;
|
|
||||||
let changedAgain = false;
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
if (building) {
|
|
||||||
changedAgain = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
building = true;
|
|
||||||
do {
|
|
||||||
// Let the rest of one save's events arrive first, so that one
|
|
||||||
// save is one build.
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
changedAgain = false;
|
|
||||||
try {
|
|
||||||
await rebuild();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(
|
|
||||||
`Build failed: ${err && err.message ? err.message : err}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} while (changedAgain);
|
|
||||||
building = false;
|
|
||||||
console.log("Watching for changes (Ctrl-C to stop)...");
|
|
||||||
}
|
|
||||||
|
|
||||||
// One watcher per directory rather than fs.watch's recursive option: on
|
|
||||||
// Linux, Node's recursive watch watches each file, and stops seeing one
|
|
||||||
// that an editor saves by renaming a new copy over it. A directory's
|
|
||||||
// watcher reports every change to the files in it, however they are saved.
|
|
||||||
const subdirectories = dirs.flatMap((dir) =>
|
|
||||||
fs
|
|
||||||
.readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
||||||
.filter((entry) => entry.isDirectory())
|
|
||||||
.map((entry) => path.join(entry.parentPath, entry.name)),
|
|
||||||
);
|
|
||||||
const watchers = [...dirs, ...subdirectories].map((dir) =>
|
|
||||||
fs.watch(dir, run),
|
|
||||||
);
|
|
||||||
run();
|
|
||||||
return watchers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run only as a program. Required as a module — which is how
|
// Run only as a program. Required as a module — which is how
|
||||||
// tests/buildForbiddenInputs.test.js and tests/buildWatch.test.js reach the
|
// tests/buildForbiddenInputs.test.js reaches the checks below — this file
|
||||||
// functions below — this file builds nothing and writes nothing.
|
// builds nothing and writes nothing.
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const args = process.argv.slice(2);
|
build().catch((err) => {
|
||||||
// An argument this file does not know fails rather than being ignored.
|
console.error(
|
||||||
if (args.length > 1 || (args.length === 1 && args[0] !== "--watch")) {
|
`Build failed: ${err && err.message ? err.message : err}`,
|
||||||
console.error("usage: node build.js [--watch]");
|
);
|
||||||
process.exit(2);
|
process.exit(1);
|
||||||
}
|
});
|
||||||
if (args[0] === "--watch") {
|
|
||||||
watch(WATCHED_DIRS, build);
|
|
||||||
} else {
|
|
||||||
build().catch((err) => {
|
|
||||||
console.error(
|
|
||||||
`Build failed: ${err && err.message ? err.message : err}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exported for tests only: watch() for tests/buildWatch.test.js, the rest for
|
// Exported for tests/buildForbiddenInputs.test.js only. The prohibition these
|
||||||
// tests/buildForbiddenInputs.test.js. The prohibition those enforce is the
|
// three functions enforce is the guarantee behind
|
||||||
// guarantee behind https://git.eeqj.de/sneak/AutistMask/issues/324, and
|
// https://git.eeqj.de/sneak/AutistMask/issues/324, and `make check` does not
|
||||||
// `make check` does not run `make build` — so they are unit tested against
|
// run `make build` — so they are unit tested against synthetic metafiles
|
||||||
// synthetic metafiles rather than being exercised only by CI, where "it ran"
|
// rather than being exercised only by CI, where "it ran" is not "it works".
|
||||||
// is not "it works".
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
importChain,
|
importChain,
|
||||||
newForbiddenRecord,
|
newForbiddenRecord,
|
||||||
recordBundledInputs,
|
recordBundledInputs,
|
||||||
assertNoForbiddenInputs,
|
assertNoForbiddenInputs,
|
||||||
assertForbiddenTableCovered,
|
assertForbiddenTableCovered,
|
||||||
watch,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"permissions": ["storage", "activeTab", "alarms"],
|
"permissions": ["storage", "activeTab", "alarms"],
|
||||||
"host_permissions": ["<all_urls>"],
|
"host_permissions": ["<all_urls>"],
|
||||||
"content_security_policy": {
|
"content_security_policy": {
|
||||||
"extension_pages": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https: http:; frame-src 'none'; form-action 'none'; base-uri 'none'"
|
"extension_pages": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https: http:; frame-src 'none'; form-action 'none'; base-uri 'none'"
|
||||||
},
|
},
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/icon16.png",
|
"16": "icons/icon16.png",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Minimal Ethereum wallet for Firefox",
|
"description": "Minimal Ethereum wallet for Firefox",
|
||||||
"permissions": ["storage", "activeTab", "alarms", "<all_urls>"],
|
"permissions": ["storage", "activeTab", "alarms", "<all_urls>"],
|
||||||
"content_security_policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https: http:; frame-src 'none'; form-action 'none'; base-uri 'none'",
|
"content_security_policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https: http:; frame-src 'none'; form-action 'none'; base-uri 'none'",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/icon16.png",
|
"16": "icons/icon16.png",
|
||||||
"32": "icons/icon32.png",
|
"32": "icons/icon32.png",
|
||||||
|
|||||||
@@ -741,12 +741,11 @@ async function handleConnectionRequest(origin) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods that are safe to proxy directly to the RPC node. A method handleRpc
|
// Methods that are safe to proxy directly to the RPC node
|
||||||
// answers before its proxy branch does not belong here: it would never reach
|
|
||||||
// the node. tests/proxyMethods.test.js sends every one of these.
|
|
||||||
const PROXY_METHODS = [
|
const PROXY_METHODS = [
|
||||||
"eth_blockNumber",
|
"eth_blockNumber",
|
||||||
"eth_call",
|
"eth_call",
|
||||||
|
"eth_chainId",
|
||||||
"eth_estimateGas",
|
"eth_estimateGas",
|
||||||
"eth_gasPrice",
|
"eth_gasPrice",
|
||||||
"eth_getBalance",
|
"eth_getBalance",
|
||||||
@@ -760,6 +759,7 @@ const PROXY_METHODS = [
|
|||||||
"eth_getTransactionReceipt",
|
"eth_getTransactionReceipt",
|
||||||
"eth_maxPriorityFeePerGas",
|
"eth_maxPriorityFeePerGas",
|
||||||
"eth_sendRawTransaction",
|
"eth_sendRawTransaction",
|
||||||
|
"net_version",
|
||||||
"web3_clientVersion",
|
"web3_clientVersion",
|
||||||
"eth_feeHistory",
|
"eth_feeHistory",
|
||||||
"eth_getBlockTransactionCountByHash",
|
"eth_getBlockTransactionCountByHash",
|
||||||
@@ -1802,5 +1802,3 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = { PROXY_METHODS };
|
|
||||||
|
|||||||
+83
-32
@@ -110,7 +110,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="add-wallet-phrase-warning"
|
id="add-wallet-phrase-warning"
|
||||||
class="text-xs mb-2 border border-border border-dashed p-2 invisible"
|
class="text-xs mb-2 border border-border border-dashed p-2"
|
||||||
|
style="visibility: hidden"
|
||||||
>
|
>
|
||||||
Write these words down and keep them safe. Anyone with
|
Write these words down and keep them safe. Anyone with
|
||||||
them can take your funds; if you lose them, your wallet
|
them can take your funds; if you lose them, your wallet
|
||||||
@@ -261,7 +262,10 @@
|
|||||||
|
|
||||||
<!-- recent transactions across all addresses -->
|
<!-- recent transactions across all addresses -->
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold bg-section py-1 px-2 -mx-2">
|
<div
|
||||||
|
class="font-bold bg-section py-1 px-2"
|
||||||
|
style="margin-left: -0.5rem; margin-right: -0.5rem"
|
||||||
|
>
|
||||||
Recent Transactions
|
Recent Transactions
|
||||||
</div>
|
</div>
|
||||||
<div id="home-tx-list">
|
<div id="home-tx-list">
|
||||||
@@ -269,7 +273,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-1 -mx-2"> </div>
|
<div class="py-1" style="margin: 0 -0.5rem"> </div>
|
||||||
|
|
||||||
<div class="text-xs text-muted">
|
<div class="text-xs text-muted">
|
||||||
<span
|
<span
|
||||||
@@ -405,7 +409,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
id="export-privkey-flash"
|
id="export-privkey-flash"
|
||||||
class="text-xs mb-2 min-h-[1.25rem] invisible"
|
class="text-xs mb-2 min-h-[1.25rem]"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<div id="export-privkey-password-section" class="mb-2">
|
<div id="export-privkey-password-section" class="mb-2">
|
||||||
<label class="block mb-1">Password</label>
|
<label class="block mb-1">Password</label>
|
||||||
@@ -537,7 +542,8 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
id="send-to-error"
|
id="send-to-error"
|
||||||
class="text-xs min-h-[1.25rem] text-[#cc0000]"
|
class="text-xs"
|
||||||
|
style="min-height: 1.25rem; color: #cc0000"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@@ -613,7 +619,7 @@
|
|||||||
<div class="text-xs text-muted mb-1">Your balance</div>
|
<div class="text-xs text-muted mb-1">Your balance</div>
|
||||||
<div id="confirm-balance" class="text-xs"></div>
|
<div id="confirm-balance" class="text-xs"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="confirm-fee" class="mb-3 invisible">
|
<div id="confirm-fee" class="mb-3" style="visibility: hidden">
|
||||||
<div class="text-xs text-muted mb-1">Network fee</div>
|
<div class="text-xs text-muted mb-1">Network fee</div>
|
||||||
<div id="confirm-fee-amount" class="text-xs"></div>
|
<div id="confirm-fee-amount" class="text-xs"></div>
|
||||||
<!-- Holds its one line of space from the first paint, so
|
<!-- Holds its one line of space from the first paint, so
|
||||||
@@ -621,13 +627,22 @@
|
|||||||
nothing. The placeholder is never seen. -->
|
nothing. The placeholder is never seen. -->
|
||||||
<div
|
<div
|
||||||
id="confirm-fee-reserve"
|
id="confirm-fee-reserve"
|
||||||
class="text-xs text-muted invisible"
|
class="text-xs text-muted"
|
||||||
|
style="visibility: hidden"
|
||||||
>
|
>
|
||||||
reserve pending
|
reserve pending
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="confirm-warnings" class="mb-2 invisible"></div>
|
<div
|
||||||
<div id="confirm-recipient-warning" class="mb-2 invisible">
|
id="confirm-warnings"
|
||||||
|
class="mb-2"
|
||||||
|
style="visibility: hidden"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
id="confirm-recipient-warning"
|
||||||
|
class="mb-2"
|
||||||
|
style="visibility: hidden"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
|
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
|
||||||
>
|
>
|
||||||
@@ -640,9 +655,14 @@
|
|||||||
in confirmTx.js sets it. -->
|
in confirmTx.js sets it. -->
|
||||||
<div
|
<div
|
||||||
id="confirm-contract-warning"
|
id="confirm-contract-warning"
|
||||||
class="mb-2 border border-red-500 border-dashed p-2 text-xs font-bold text-red-500 invisible"
|
class="mb-2 border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<div id="confirm-burn-warning" class="mb-2 invisible">
|
<div
|
||||||
|
id="confirm-burn-warning"
|
||||||
|
class="mb-2"
|
||||||
|
style="visibility: hidden"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
|
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
|
||||||
>
|
>
|
||||||
@@ -650,7 +670,11 @@
|
|||||||
here are permanently destroyed and cannot be recovered.
|
here are permanently destroyed and cannot be recovered.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="confirm-etherscan-warning" class="mb-2 invisible">
|
<div
|
||||||
|
id="confirm-etherscan-warning"
|
||||||
|
class="mb-2"
|
||||||
|
style="visibility: hidden"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
|
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
|
||||||
>
|
>
|
||||||
@@ -660,11 +684,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="confirm-errors"
|
id="confirm-errors"
|
||||||
class="mb-2 border border-border border-dashed p-2 invisible min-h-[1.25rem]"
|
class="mb-2 border border-border border-dashed p-2"
|
||||||
|
style="visibility: hidden; min-height: 1.25rem"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
id="confirm-amount-fee-error"
|
id="confirm-amount-fee-error"
|
||||||
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
class="mb-2 border border-border border-dashed p-2 text-xs"
|
||||||
|
style="visibility: hidden"
|
||||||
>
|
>
|
||||||
Your balance does not cover this amount plus the network
|
Your balance does not cover this amount plus the network
|
||||||
fee. Please go back and send a smaller amount.
|
fee. Please go back and send a smaller amount.
|
||||||
@@ -673,13 +699,15 @@
|
|||||||
in confirmTx.js sets it. -->
|
in confirmTx.js sets it. -->
|
||||||
<div
|
<div
|
||||||
id="confirm-gas-error"
|
id="confirm-gas-error"
|
||||||
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
class="mb-2 border border-border border-dashed p-2 text-xs"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<!-- Its sentence names why the fee could not be estimated,
|
<!-- Its sentence names why the fee could not be estimated,
|
||||||
so show() in confirmTx.js sets it. -->
|
so show() in confirmTx.js sets it. -->
|
||||||
<div
|
<div
|
||||||
id="confirm-fee-unknown-error"
|
id="confirm-fee-unknown-error"
|
||||||
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
|
class="mb-2 border border-border border-dashed p-2 text-xs"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="block mb-1 text-xs">Password</label>
|
<label class="block mb-1 text-xs">Password</label>
|
||||||
@@ -691,7 +719,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="confirm-tx-password-error"
|
id="confirm-tx-password-error"
|
||||||
class="text-xs mb-2 min-h-[1.25rem] invisible"
|
class="text-xs mb-2 min-h-[1.25rem]"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<button
|
<button
|
||||||
id="btn-confirm-send"
|
id="btn-confirm-send"
|
||||||
@@ -806,7 +835,8 @@
|
|||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
id="receive-erc20-warning"
|
id="receive-erc20-warning"
|
||||||
class="text-xs border border-border border-dashed p-2 mt-3 invisible"
|
class="text-xs border border-border border-dashed p-2 mt-3"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -834,7 +864,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="add-token-info"
|
id="add-token-info"
|
||||||
class="text-xs text-muted mb-2 min-h-[1.25rem] invisible"
|
class="text-xs text-muted mb-2 min-h-[1.25rem]"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="block mb-1 text-xs text-muted"
|
<label class="block mb-1 text-xs text-muted"
|
||||||
@@ -1020,7 +1051,8 @@
|
|||||||
type="text"
|
type="text"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
id="settings-dust-threshold"
|
id="settings-dust-threshold"
|
||||||
class="border border-border p-1 text-xs bg-bg text-fg w-[10ch]"
|
class="border border-border p-1 text-xs bg-bg text-fg"
|
||||||
|
style="width: 10ch"
|
||||||
/>
|
/>
|
||||||
<span class="text-xs text-muted">gwei</span>
|
<span class="text-xs text-muted">gwei</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1097,7 +1129,8 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
id="settings-debug-well"
|
id="settings-debug-well"
|
||||||
class="bg-well p-3 mx-1 mb-3 hidden"
|
class="bg-well p-3 mx-1 mb-3"
|
||||||
|
style="display: none"
|
||||||
>
|
>
|
||||||
<h3 class="font-bold mb-1">Debug</h3>
|
<h3 class="font-bold mb-1">Debug</h3>
|
||||||
<label
|
<label
|
||||||
@@ -1125,7 +1158,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
id="delete-wallet-flash"
|
id="delete-wallet-flash"
|
||||||
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
|
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label class="block mb-1">Password</label>
|
<label class="block mb-1">Password</label>
|
||||||
@@ -1198,7 +1232,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="delete-wallet-lost-flash"
|
id="delete-wallet-lost-flash"
|
||||||
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
|
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<button
|
<button
|
||||||
id="btn-delete-wallet-lost-confirm"
|
id="btn-delete-wallet-lost-confirm"
|
||||||
@@ -1253,7 +1288,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
id="delete-address-flash"
|
id="delete-address-flash"
|
||||||
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
|
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<button
|
<button
|
||||||
id="btn-delete-address-confirm"
|
id="btn-delete-address-confirm"
|
||||||
@@ -1282,7 +1318,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="show-phrase-flash"
|
id="show-phrase-flash"
|
||||||
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
|
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<div id="show-phrase-password-section" class="mb-2">
|
<div id="show-phrase-password-section" class="mb-2">
|
||||||
<label class="block mb-1">Password</label>
|
<label class="block mb-1">Password</label>
|
||||||
@@ -1364,7 +1401,8 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
id="settings-addtoken-info"
|
id="settings-addtoken-info"
|
||||||
class="text-xs text-muted mt-1 min-h-[1.25rem] invisible"
|
class="text-xs text-muted mt-1 min-h-[1.25rem]"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<button
|
<button
|
||||||
id="btn-settings-addtoken-manual"
|
id="btn-settings-addtoken-manual"
|
||||||
@@ -1597,7 +1635,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="approve-tx-error"
|
id="approve-tx-error"
|
||||||
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem] invisible"
|
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem]"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<button
|
<button
|
||||||
@@ -1633,7 +1672,15 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
id="approve-sign-danger-warning"
|
id="approve-sign-danger-warning"
|
||||||
class="mb-3 p-2 text-xs font-bold invisible min-h-[1.25rem] bg-[#fee2e2] text-[#991b1b] border-2 border-[#dc2626] rounded-[6px]"
|
class="mb-3 p-2 text-xs font-bold"
|
||||||
|
style="
|
||||||
|
visibility: hidden;
|
||||||
|
min-height: 1.25rem;
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #991b1b;
|
||||||
|
border: 2px solid #dc2626;
|
||||||
|
border-radius: 6px;
|
||||||
|
"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -1650,7 +1697,8 @@
|
|||||||
<div class="text-xs text-muted mb-1">Message</div>
|
<div class="text-xs text-muted mb-1">Message</div>
|
||||||
<div
|
<div
|
||||||
id="approve-sign-message"
|
id="approve-sign-message"
|
||||||
class="text-xs break-all max-h-48 overflow-y-auto"
|
class="text-xs break-all"
|
||||||
|
style="max-height: 12rem; overflow-y: auto"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1658,7 +1706,8 @@
|
|||||||
<div class="text-xs text-muted mb-1">Raw data</div>
|
<div class="text-xs text-muted mb-1">Raw data</div>
|
||||||
<div
|
<div
|
||||||
id="approve-sign-hex"
|
id="approve-sign-hex"
|
||||||
class="text-xs break-all max-h-24 overflow-y-auto"
|
class="text-xs break-all"
|
||||||
|
style="max-height: 6rem; overflow-y: auto"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1672,7 +1721,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="approve-sign-error"
|
id="approve-sign-error"
|
||||||
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem] invisible"
|
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem]"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<button
|
<button
|
||||||
@@ -1796,7 +1846,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="state-recovery-flash"
|
id="state-recovery-flash"
|
||||||
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
|
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
|
||||||
|
style="visibility: hidden"
|
||||||
></div>
|
></div>
|
||||||
<button
|
<button
|
||||||
id="btn-state-recovery-reset"
|
id="btn-state-recovery-reset"
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ const { walletDefect } = require("../../shared/walletDefects");
|
|||||||
|
|
||||||
// The defect of the wallet the selected address belongs to, or null. Both the
|
// The defect of the wallet the selected address belongs to, or null. Both the
|
||||||
// send and the private-key export path check it before asking for a password,
|
// send and the private-key export path check it before asking for a password,
|
||||||
// so a wallet whose key getSignerForAddress refuses says so instead of failing
|
// so a wallet that cannot derive its keys says so instead of failing after the
|
||||||
// after the user has typed one in.
|
// user has typed one in.
|
||||||
function selectedWalletDefect() {
|
function selectedWalletDefect() {
|
||||||
if (state.selectedWallet === null) return null;
|
if (state.selectedWallet === null) return null;
|
||||||
return walletDefect(state.wallets[state.selectedWallet]);
|
return walletDefect(state.wallets[state.selectedWallet]);
|
||||||
@@ -181,10 +181,10 @@ function renderTransactions(txs) {
|
|||||||
// it on the line above rather than replacing it.
|
// it on the line above rather than replacing it.
|
||||||
const nameStr = escapeHtml(title || ensName || "");
|
const nameStr = escapeHtml(title || ensName || "");
|
||||||
const err = tx.isError ? " (failed)" : "";
|
const err = tx.isError ? " (failed)" : "";
|
||||||
const opacity = tx.isError ? " opacity-50" : "";
|
const opacity = tx.isError ? " opacity:0.5;" : "";
|
||||||
const ago = escapeHtml(timeAgo(tx.timestamp));
|
const ago = escapeHtml(timeAgo(tx.timestamp));
|
||||||
const iso = escapeHtml(isoDate(tx.timestamp));
|
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||||
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
|
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
|
||||||
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||||
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
|
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
@@ -259,10 +259,9 @@ function init(_ctx) {
|
|||||||
$("btn-export-privkey").addEventListener("click", () => {
|
$("btn-export-privkey").addEventListener("click", () => {
|
||||||
moreDropdown.classList.add("hidden");
|
moreDropdown.classList.add("hidden");
|
||||||
moreBtn.classList.remove("bg-fg", "text-bg");
|
moreBtn.classList.remove("bg-fg", "text-bg");
|
||||||
// This address's private key can be derived from the stored key,
|
// There is no private key to export for an address this wallet
|
||||||
// but export goes through getSignerForAddress, which refuses a key
|
// cannot derive. Without this the export screen would take a
|
||||||
// that is not a master key. Without this the export screen would
|
// password and then report it as wrong.
|
||||||
// take a password and then report that refusal as a wrong password.
|
|
||||||
const defect = selectedWalletDefect();
|
const defect = selectedWalletDefect();
|
||||||
if (defect) {
|
if (defect) {
|
||||||
showFlash(defect.shortMessage);
|
showFlash(defect.shortMessage);
|
||||||
|
|||||||
@@ -258,10 +258,10 @@ function renderTransactions(txs) {
|
|||||||
// it on the line above rather than replacing it.
|
// it on the line above rather than replacing it.
|
||||||
const nameStr = escapeHtml(title || ensName || "");
|
const nameStr = escapeHtml(title || ensName || "");
|
||||||
const err = tx.isError ? " (failed)" : "";
|
const err = tx.isError ? " (failed)" : "";
|
||||||
const opacity = tx.isError ? " opacity-50" : "";
|
const opacity = tx.isError ? " opacity:0.5;" : "";
|
||||||
const ago = escapeHtml(timeAgo(tx.timestamp));
|
const ago = escapeHtml(timeAgo(tx.timestamp));
|
||||||
const iso = escapeHtml(isoDate(tx.timestamp));
|
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||||
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
|
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
|
||||||
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||||
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
|
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
|
|||||||
@@ -822,10 +822,9 @@ function setSignButtonBusy(busy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Say so on the approval screen itself, and disable the approve button, when
|
// Say so on the approval screen itself, and disable the approve button, when
|
||||||
// the address the approval was raised for belongs to a wallet whose key
|
// the address the approval was raised for belongs to a wallet whose keys
|
||||||
// getSignerForAddress refuses. Without this the screen would take a password
|
// cannot be derived. Without this the screen would take a password and fail
|
||||||
// and fail after deriving it. Reject stays available; the wallet is not
|
// after deriving it. Reject stays available; the wallet is not touched.
|
||||||
// touched.
|
|
||||||
// Returns true when it gated.
|
// Returns true when it gated.
|
||||||
function gateOnWalletDefect(errorId, buttonId, address) {
|
function gateOnWalletDefect(errorId, buttonId, address) {
|
||||||
const owner = findWalletFor(address);
|
const owner = findWalletFor(address);
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const {
|
|||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, currentNetwork } = require("../../shared/state");
|
const { state, currentNetwork } = require("../../shared/state");
|
||||||
const { getSignerForAddress } = require("../../shared/wallet");
|
const { getSignerForAddress } = require("../../shared/wallet");
|
||||||
const { walletDefect } = require("../../shared/walletDefects");
|
|
||||||
const { decryptWithPassword } = require("../../shared/vault");
|
const { decryptWithPassword } = require("../../shared/vault");
|
||||||
const { formatUsd, getPrice } = require("../../shared/prices");
|
const { formatUsd, getPrice } = require("../../shared/prices");
|
||||||
const { getProvider } = require("../../shared/balances");
|
const { getProvider } = require("../../shared/balances");
|
||||||
@@ -538,15 +537,6 @@ function init(_ctx) {
|
|||||||
onViewLeave("confirm-tx", clearPassword);
|
onViewLeave("confirm-tx", clearPassword);
|
||||||
|
|
||||||
$("btn-confirm-send").addEventListener("click", async () => {
|
$("btn-confirm-send").addEventListener("click", async () => {
|
||||||
const wallet = state.wallets[state.selectedWallet];
|
|
||||||
// Every Send button refuses a defective wallet before this screen,
|
|
||||||
// but the popup also reopens onto it from a saved view.
|
|
||||||
const defect = walletDefect(wallet);
|
|
||||||
if (defect) {
|
|
||||||
showError("confirm-tx-password-error", defect.shortMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const password = $("confirm-tx-password").value;
|
const password = $("confirm-tx-password").value;
|
||||||
if (!password) {
|
if (!password) {
|
||||||
showError(
|
showError(
|
||||||
@@ -556,6 +546,7 @@ function init(_ctx) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wallet = state.wallets[state.selectedWallet];
|
||||||
let decryptedSecret;
|
let decryptedSecret;
|
||||||
hideError("confirm-tx-password-error");
|
hideError("confirm-tx-password-error");
|
||||||
|
|
||||||
|
|||||||
+20
-23
@@ -335,7 +335,7 @@ function balanceLine(symbol, amount, price, tokenId) {
|
|||||||
: "";
|
: "";
|
||||||
return (
|
return (
|
||||||
`<div class="flex text-xs${clickClass}"${tokenAttr}>` +
|
`<div class="flex text-xs${clickClass}"${tokenAttr}>` +
|
||||||
`<span class="flex justify-between w-[42ch] max-w-full">` +
|
`<span class="flex justify-between" style="width:42ch;max-width:100%">` +
|
||||||
`<span>${escapeHtml(displaySymbol(symbol))}</span>` +
|
`<span>${escapeHtml(displaySymbol(symbol))}</span>` +
|
||||||
`<span>${qty}</span>` +
|
`<span>${qty}</span>` +
|
||||||
`</span>` +
|
`</span>` +
|
||||||
@@ -430,26 +430,23 @@ function truncateMiddle(str, maxLen) {
|
|||||||
|
|
||||||
// 16 colors evenly spaced around the hue wheel (22.5° apart),
|
// 16 colors evenly spaced around the hue wheel (22.5° apart),
|
||||||
// all at HSL saturation 70%, lightness 50% for uniform vibrancy.
|
// all at HSL saturation 70%, lightness 50% for uniform vibrancy.
|
||||||
// Each is a whole Tailwind class: Tailwind builds only the classes it finds
|
|
||||||
// written out in the source, so the class name cannot be put together at
|
|
||||||
// runtime.
|
|
||||||
const ADDRESS_COLORS = [
|
const ADDRESS_COLORS = [
|
||||||
"bg-[#d92626]",
|
"#d92626",
|
||||||
"bg-[#d96926]",
|
"#d96926",
|
||||||
"bg-[#d9ac26]",
|
"#d9ac26",
|
||||||
"bg-[#c2d926]",
|
"#c2d926",
|
||||||
"bg-[#80d926]",
|
"#80d926",
|
||||||
"bg-[#3dd926]",
|
"#3dd926",
|
||||||
"bg-[#26d953]",
|
"#26d953",
|
||||||
"bg-[#26d996]",
|
"#26d996",
|
||||||
"bg-[#26d9d9]",
|
"#26d9d9",
|
||||||
"bg-[#2696d9]",
|
"#2696d9",
|
||||||
"bg-[#2653d9]",
|
"#2653d9",
|
||||||
"bg-[#3d26d9]",
|
"#3d26d9",
|
||||||
"bg-[#8026d9]",
|
"#8026d9",
|
||||||
"bg-[#c226d9]",
|
"#c226d9",
|
||||||
"bg-[#d926ac]",
|
"#d926ac",
|
||||||
"bg-[#d92669]",
|
"#d92669",
|
||||||
];
|
];
|
||||||
|
|
||||||
function addressColor(address) {
|
function addressColor(address) {
|
||||||
@@ -459,12 +456,12 @@ function addressColor(address) {
|
|||||||
|
|
||||||
function addressDotHtml(address) {
|
function addressDotHtml(address) {
|
||||||
const color = addressColor(address);
|
const color = addressColor(address);
|
||||||
return `<span class="inline-block w-[8px] h-[8px] rounded-[50%] ${color} mr-[4px] align-middle shrink-0"></span>`;
|
return `<span style="width:8px;height:8px;border-radius:50%;display:inline-block;background:${color};margin-right:4px;vertical-align:middle;flex-shrink:0;"></span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockieHtml(address) {
|
function blockieHtml(address) {
|
||||||
const src = makeBlockie(address);
|
const src = makeBlockie(address);
|
||||||
return `<img src="${escapeHtml(src)}" width="48" height="48" class="inline-block rounded-[50%] [image-rendering:pixelated]">`;
|
return `<img src="${escapeHtml(src)}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up an address across all wallets and return its title
|
// Look up an address across all wallets and return its title
|
||||||
@@ -574,7 +571,7 @@ function timeAgo(timestamp) {
|
|||||||
|
|
||||||
// Shared external-link icon SVG used across all views.
|
// Shared external-link icon SVG used across all views.
|
||||||
const EXT_ICON =
|
const EXT_ICON =
|
||||||
`<span class="inline-block w-[10px] h-[10px] ml-[4px] align-middle">` +
|
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
|
||||||
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
|
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
|
||||||
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
|
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
|
||||||
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
|
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
|
||||||
|
|||||||
@@ -131,10 +131,10 @@ function renderHomeTxList(ctx) {
|
|||||||
const title = addressTitle(counterparty, state.wallets);
|
const title = addressTitle(counterparty, state.wallets);
|
||||||
const titleStr = title ? escapeHtml(title) : "";
|
const titleStr = title ? escapeHtml(title) : "";
|
||||||
const err = tx.isError ? " (failed)" : "";
|
const err = tx.isError ? " (failed)" : "";
|
||||||
const opacity = tx.isError ? " opacity-50" : "";
|
const opacity = tx.isError ? " opacity:0.5;" : "";
|
||||||
const ago = escapeHtml(timeAgo(tx.timestamp));
|
const ago = escapeHtml(timeAgo(tx.timestamp));
|
||||||
const iso = escapeHtml(isoDate(tx.timestamp));
|
const iso = escapeHtml(isoDate(tx.timestamp));
|
||||||
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
|
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
|
||||||
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
|
||||||
html += txCounterpartyHtml(counterparty, titleStr, amountStr);
|
html += txCounterpartyHtml(counterparty, titleStr, amountStr);
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
@@ -241,7 +241,7 @@ function walletListHtml() {
|
|||||||
state.wallets.forEach((wallet, wi) => {
|
state.wallets.forEach((wallet, wi) => {
|
||||||
const defect = walletDefect(wallet);
|
const defect = walletDefect(wallet);
|
||||||
html += `<div>`;
|
html += `<div>`;
|
||||||
html += `<div class="flex justify-between items-center bg-section py-1 px-2 -mx-2">`;
|
html += `<div class="flex justify-between items-center bg-section py-1 px-2" style="margin:0 -0.5rem">`;
|
||||||
html += `<span class="font-bold cursor-pointer wallet-name underline decoration-dashed" data-wallet="${wi}">${escapeHtml(wallet.name)}</span>`;
|
html += `<span class="font-bold cursor-pointer wallet-name underline decoration-dashed" data-wallet="${wi}">${escapeHtml(wallet.name)}</span>`;
|
||||||
// No "+" on a defective wallet: deriving another address from that
|
// No "+" on a defective wallet: deriving another address from that
|
||||||
// xpub would only add one more address the key does not produce
|
// xpub would only add one more address the key does not produce
|
||||||
@@ -255,12 +255,12 @@ function walletListHtml() {
|
|||||||
wallet.addresses.forEach((addr, ai) => {
|
wallet.addresses.forEach((addr, ai) => {
|
||||||
html += `<div class="address-row py-1 border-b border-border-light cursor-pointer hover:bg-hover" data-wallet="${wi}" data-address="${ai}">`;
|
html += `<div class="address-row py-1 border-b border-border-light cursor-pointer hover:bg-hover" data-wallet="${wi}" data-address="${ai}">`;
|
||||||
const isActive = state.activeAddress === addr.address;
|
const isActive = state.activeAddress === addr.address;
|
||||||
const infoBtn = `<span class="btn-addr-info text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg p-0" data-wallet="${wi}" data-address="${ai}">[info]</span>`;
|
const infoBtn = `<span class="btn-addr-info text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg" style="padding:0" data-wallet="${wi}" data-address="${ai}">[info]</span>`;
|
||||||
// Only where a wallet can spare the address: a wallet holding a
|
// Only where a wallet can spare the address: a wallet holding a
|
||||||
// single address has no remove control, because its last address
|
// single address has no remove control, because its last address
|
||||||
// is never removable.
|
// is never removable.
|
||||||
const removeBtn = canRemoveAddress(wallet)
|
const removeBtn = canRemoveAddress(wallet)
|
||||||
? `<span class="btn-remove-address text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg ml-1 p-0" data-wallet="${wi}" data-address="${ai}" title="Remove this address from the wallet">[x]</span>`
|
? `<span class="btn-remove-address text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg ml-1" style="padding:0" data-wallet="${wi}" data-address="${ai}" title="Remove this address from the wallet">[x]</span>`
|
||||||
: "";
|
: "";
|
||||||
const dot = addressDotHtml(addr.address);
|
const dot = addressDotHtml(addr.address);
|
||||||
const titleBold = isActive ? "font-bold" : "";
|
const titleBold = isActive ? "font-bold" : "";
|
||||||
|
|||||||
@@ -213,7 +213,12 @@ function show() {
|
|||||||
versionClickCount = 0;
|
versionClickCount = 0;
|
||||||
|
|
||||||
// Show debug well if debug mode is already enabled
|
// Show debug well if debug mode is already enabled
|
||||||
$("settings-debug-well").classList.toggle("hidden", !state.debugMode);
|
const debugWell = $("settings-debug-well");
|
||||||
|
if (state.debugMode) {
|
||||||
|
debugWell.style.display = "";
|
||||||
|
} else {
|
||||||
|
debugWell.style.display = "none";
|
||||||
|
}
|
||||||
$("settings-debug-mode").checked = state.debugMode;
|
$("settings-debug-mode").checked = state.debugMode;
|
||||||
|
|
||||||
showView("settings");
|
showView("settings");
|
||||||
@@ -429,7 +434,7 @@ function init(ctx) {
|
|||||||
if (versionClickCount >= 10) {
|
if (versionClickCount >= 10) {
|
||||||
versionClickCount = 0;
|
versionClickCount = 0;
|
||||||
clearTimeout(versionClickTimer);
|
clearTimeout(versionClickTimer);
|
||||||
$("settings-debug-well").classList.remove("hidden");
|
$("settings-debug-well").style.display = "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,17 +13,11 @@ const NON_MASTER_XPRV = "non-master-xprv";
|
|||||||
|
|
||||||
// An "xprv" wallet stores the neutered BIP-44 Ethereum node, four levels below
|
// An "xprv" wallet stores the neutered BIP-44 Ethereum node, four levels below
|
||||||
// the key that was imported: the current import path derives the absolute
|
// the key that was imported: the current import path derives the absolute
|
||||||
// m/44'/60'/0'/0 from a depth-0 key, and the path before #210 (57959b7)
|
// m/44'/60'/0'/0 from a depth-0 key, and the pre-#210 path derived the same
|
||||||
// derived the same four levels as a relative path beneath whatever depth it
|
// four levels as a relative path beneath whatever depth it was given. A master
|
||||||
// was given. A master import therefore stores a depth-4 xpub and a depth-d
|
// import therefore stores a depth-4 xpub and a depth-d import stores depth
|
||||||
// import stores depth d + 4, which makes the stored xpub an exact read on the
|
// d + 4, which makes the stored xpub an exact read on the imported key's
|
||||||
// imported key's depth — and it is readable without the password, unlike the
|
// depth — and it is readable without the password, unlike the key itself.
|
||||||
// key itself.
|
|
||||||
//
|
|
||||||
// The first import path (7a7f9c5) does not fit: it stored the imported key's
|
|
||||||
// own xpub with no derivation, so a wallet it wrote is judged wrongly here (a
|
|
||||||
// master import as defective, a depth-4 import as sound). 57959b7 replaced it
|
|
||||||
// in the same push, and no tag contains it.
|
|
||||||
const BIP44_ETH_XPUB_DEPTH = 4;
|
const BIP44_ETH_XPUB_DEPTH = 4;
|
||||||
|
|
||||||
const DEFECTS = {
|
const DEFECTS = {
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
// `make dev` runs `node build.js --watch`
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/332). These drive build.js's
|
|
||||||
// watch() over a temp directory with a stand-in for build(), so nothing here
|
|
||||||
// builds or writes dist/.
|
|
||||||
|
|
||||||
const fs = require("fs");
|
|
||||||
const os = require("os");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const { watch } = require("../build");
|
|
||||||
|
|
||||||
let dir;
|
|
||||||
let watchers = [];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), "autistmask-watch-"));
|
|
||||||
fs.mkdirSync(path.join(dir, "nested"));
|
|
||||||
jest.spyOn(console, "log").mockImplementation(() => {});
|
|
||||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
for (const watcher of watchers) watcher.close();
|
|
||||||
watchers = [];
|
|
||||||
fs.rmSync(dir, { recursive: true, force: true });
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
// Wait until `condition()` holds; fail the test if it has not within 3s.
|
|
||||||
async function until(condition) {
|
|
||||||
const deadline = Date.now() + 3000;
|
|
||||||
while (!condition()) {
|
|
||||||
if (Date.now() > deadline) throw new Error("timed out waiting");
|
|
||||||
await sleep(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the way many editors do: write a new copy, then rename it over the
|
|
||||||
// original, so the file at that path is a different one afterwards.
|
|
||||||
function saveByRename(file, contents) {
|
|
||||||
fs.writeFileSync(`${file}.tmp`, contents);
|
|
||||||
fs.renameSync(`${file}.tmp`, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("builds at start, then once per change to a file in a subdirectory", async () => {
|
|
||||||
const file = path.join(dir, "nested", "a.js");
|
|
||||||
fs.writeFileSync(file, "1");
|
|
||||||
let builds = 0;
|
|
||||||
watchers = watch([dir], async () => {
|
|
||||||
builds++;
|
|
||||||
});
|
|
||||||
await until(() => builds === 1);
|
|
||||||
|
|
||||||
fs.writeFileSync(file, "2");
|
|
||||||
await until(() => builds === 2);
|
|
||||||
await sleep(300);
|
|
||||||
expect(builds).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("keeps seeing a file that is saved by renaming a new copy over it", async () => {
|
|
||||||
const file = path.join(dir, "nested", "a.js");
|
|
||||||
fs.writeFileSync(file, "1");
|
|
||||||
let builds = 0;
|
|
||||||
watchers = watch([dir], async () => {
|
|
||||||
builds++;
|
|
||||||
});
|
|
||||||
await until(() => builds === 1);
|
|
||||||
|
|
||||||
saveByRename(file, "2");
|
|
||||||
await until(() => builds === 2);
|
|
||||||
saveByRename(file, "3");
|
|
||||||
await until(() => builds === 3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a failed build is reported and watching carries on", async () => {
|
|
||||||
let builds = 0;
|
|
||||||
watchers = watch([dir], async () => {
|
|
||||||
builds++;
|
|
||||||
if (builds === 1) throw new Error("unexpected token");
|
|
||||||
});
|
|
||||||
await until(() => console.error.mock.calls.length === 1);
|
|
||||||
expect(console.error).toHaveBeenCalledWith(
|
|
||||||
"Build failed: unexpected token",
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(dir, "a.js"), "1");
|
|
||||||
await until(() => builds === 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a change made while a build runs causes one more build after it", async () => {
|
|
||||||
let builds = 0;
|
|
||||||
watchers = watch([dir], async () => {
|
|
||||||
builds++;
|
|
||||||
if (builds === 1) {
|
|
||||||
fs.writeFileSync(path.join(dir, "a.js"), "1");
|
|
||||||
await sleep(200);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await until(() => builds === 2);
|
|
||||||
await sleep(300);
|
|
||||||
expect(builds).toBe(2);
|
|
||||||
});
|
|
||||||
@@ -301,7 +301,7 @@ describe.each([
|
|||||||
test("a contract creation's row says so, with no colour dot and no address line", async () => {
|
test("a contract creation's row says so, with no colour dot and no address line", async () => {
|
||||||
const html = await rowsFor(historyTx(""));
|
const html = await rowsFor(historyTx(""));
|
||||||
expect(html).toContain(SENTENCE);
|
expect(html).toContain(SENTENCE);
|
||||||
expect(html).not.toContain("bg-[#");
|
expect(html).not.toContain("background:");
|
||||||
expect(html).not.toContain("am-address");
|
expect(html).not.toContain("am-address");
|
||||||
expect(html).not.toContain("undefined");
|
expect(html).not.toContain("undefined");
|
||||||
});
|
});
|
||||||
@@ -309,7 +309,7 @@ describe.each([
|
|||||||
test("a transaction with a recipient shows its colour dot and address", async () => {
|
test("a transaction with a recipient shows its colour dot and address", async () => {
|
||||||
const html = await rowsFor(historyTx(RECIPIENT));
|
const html = await rowsFor(historyTx(RECIPIENT));
|
||||||
expectAddressLine(html);
|
expectAddressLine(html);
|
||||||
expect(html).toContain("bg-[#");
|
expect(html).toContain("background:#");
|
||||||
expect(html).toContain(`<div class="am-address">${RECIPIENT}</div>`);
|
expect(html).toContain(`<div class="am-address">${RECIPIENT}</div>`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+6
-30
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const { AbiCoder, Transaction } = require("ethers");
|
const { Transaction } = require("ethers");
|
||||||
|
|
||||||
// Fictional ERC-20 used to seed the transaction-detail test. The symbol
|
// Fictional ERC-20 used to seed the transaction-detail test. The symbol
|
||||||
// must not collide with any entry in src/shared/tokenList.js, or
|
// must not collide with any entry in src/shared/tokenList.js, or
|
||||||
@@ -244,16 +244,12 @@ function latestBlock() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// keccak("decimals()")[0:4], and the same for symbol() and name().
|
// keccak("decimals()")[0:4].
|
||||||
const SELECTOR_DECIMALS = "0x313ce567";
|
const SELECTOR_DECIMALS = "0x313ce567";
|
||||||
const SELECTOR_SYMBOL = "0x95d89b41";
|
|
||||||
const SELECTOR_NAME = "0x06fdde03";
|
|
||||||
|
|
||||||
// Every eth_call still answers with a zero word except decimals(), symbol()
|
// Every eth_call still answers with a zero word except decimals() on the
|
||||||
// and name() on the stub token. The wallet reads decimals() back at signing
|
// stub token, which the wallet reads back at signing time to compare with
|
||||||
// time to compare with the scale the confirmation screen rendered (issue
|
// the scale the confirmation screen rendered (issue #305).
|
||||||
// #305). Adding the token by its contract address reads all three (issue
|
|
||||||
// #295); symbol() and name() answer what the explorer reports for it.
|
|
||||||
//
|
//
|
||||||
// opts.tokenDecimalsOverride is the lying contract: set it and decimals()
|
// opts.tokenDecimalsOverride is the lying contract: set it and decimals()
|
||||||
// answers something other than the value this same fixture reports through
|
// answers something other than the value this same fixture reports through
|
||||||
@@ -267,17 +263,9 @@ function ethCallResult(req, opts) {
|
|||||||
if (!call || typeof call !== "object") return ZERO_WORD;
|
if (!call || typeof call !== "object") return ZERO_WORD;
|
||||||
const data = String(call.data || call.input || "").toLowerCase();
|
const data = String(call.data || call.input || "").toLowerCase();
|
||||||
const to = String(call.to || "").toLowerCase();
|
const to = String(call.to || "").toLowerCase();
|
||||||
if (to !== STUB_TOKEN.address) return ZERO_WORD;
|
if (data.startsWith(SELECTOR_DECIMALS) && to === STUB_TOKEN.address) {
|
||||||
if (data.startsWith(SELECTOR_DECIMALS)) {
|
|
||||||
return word(opts.tokenDecimalsOverride ?? STUB_TOKEN.decimals);
|
return word(opts.tokenDecimalsOverride ?? STUB_TOKEN.decimals);
|
||||||
}
|
}
|
||||||
const abi = AbiCoder.defaultAbiCoder();
|
|
||||||
if (data.startsWith(SELECTOR_SYMBOL)) {
|
|
||||||
return abi.encode(["string"], [tokenObject(opts).symbol]);
|
|
||||||
}
|
|
||||||
if (data.startsWith(SELECTOR_NAME)) {
|
|
||||||
return abi.encode(["string"], [tokenObject(opts).name]);
|
|
||||||
}
|
|
||||||
return ZERO_WORD;
|
return ZERO_WORD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,16 +448,6 @@ function rpcReply(req, opts, report) {
|
|||||||
return Object.assign(envelope, { result: ethCallResult(req, opts) });
|
return Object.assign(envelope, { result: ethCallResult(req, opts) });
|
||||||
}
|
}
|
||||||
if (req.method === "eth_getTransactionReceipt") {
|
if (req.method === "eth_getTransactionReceipt") {
|
||||||
// A lookup that fails, which the wait screen counts differently from
|
|
||||||
// one that answers "not mined yet" (README.md, WaitTx).
|
|
||||||
if (opts.failReceiptLookup) {
|
|
||||||
return Object.assign(envelope, {
|
|
||||||
error: {
|
|
||||||
code: -32000,
|
|
||||||
message: "e2e fixture: receipt lookup failed",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const hash = Array.isArray(req.params) ? req.params[0] : null;
|
const hash = Array.isArray(req.params) ? req.params[0] : null;
|
||||||
return Object.assign(envelope, {
|
return Object.assign(envelope, {
|
||||||
result: opts.seedReceipt && hash ? transactionReceipt(hash) : null,
|
result: opts.seedReceipt && hash ? transactionReceipt(hash) : null,
|
||||||
@@ -629,8 +607,6 @@ function traceEnabled(raw) {
|
|||||||
* symbol is markup; read at request time.
|
* symbol is markup; read at request time.
|
||||||
* @param {boolean} [opts.seedReceipt] answer eth_getTransactionReceipt with a
|
* @param {boolean} [opts.seedReceipt] answer eth_getTransactionReceipt with a
|
||||||
* confirmed receipt instead of null, so a wait screen resolves.
|
* confirmed receipt instead of null, so a wait screen resolves.
|
||||||
* @param {boolean} [opts.failReceiptLookup] answer eth_getTransactionReceipt
|
|
||||||
* with an error, so every receipt lookup fails; read at request time.
|
|
||||||
* @returns {Promise<{waitForServiceWorkerTraffic: (ms: number) =>
|
* @returns {Promise<{waitForServiceWorkerTraffic: (ms: number) =>
|
||||||
* Promise<string|null>}>}
|
* Promise<string|null>}>}
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-181
@@ -311,7 +311,7 @@ test("transaction detail renders an ERC-20 transfer (#151)", async (env) => {
|
|||||||
"token contract row missing the contract address, got: " +
|
"token contract row missing the contract address, got: " +
|
||||||
JSON.stringify(contractText),
|
JSON.stringify(contractText),
|
||||||
);
|
);
|
||||||
const dots = await contract.locator('span[class*="rounded-[50%]"]').count();
|
const dots = await contract.locator('span[style*="border-radius"]').count();
|
||||||
assert(dots > 0, "token contract row rendered without its colour dot");
|
assert(dots > 0, "token contract row rendered without its colour dot");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -596,106 +596,6 @@ test("tap-to-copy on the transaction detail screen copies the address (#151)", a
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------- the last of the #150 and #151 items (#295)
|
|
||||||
//
|
|
||||||
// Add Token's confirm button, TransactionDetail opened from the token screen
|
|
||||||
// and Back from it, and the explorer link on the token contract row.
|
|
||||||
|
|
||||||
// The stub token stays tracked for the rest of the run: the next test reaches
|
|
||||||
// its token screen through the balance row this one adds.
|
|
||||||
test("a token added by its contract address is listed on the address screen (#150)", async (env) => {
|
|
||||||
await leaveTransactionDetail(env.page);
|
|
||||||
await env.page.click("#btn-add-token");
|
|
||||||
await visible(env.page, "#view-add-token");
|
|
||||||
|
|
||||||
await env.page.fill("#add-token-address", STUB_TOKEN.address);
|
|
||||||
await env.page.click("#btn-add-token-confirm");
|
|
||||||
await visible(env.page, "#view-address");
|
|
||||||
|
|
||||||
// No wait: the confirm renders the balance list before it shows the
|
|
||||||
// screen, and nothing renders the list again while the screen is up.
|
|
||||||
const row = env.page.locator(
|
|
||||||
'#address-balances [data-token="' + STUB_TOKEN.address + '"]',
|
|
||||||
{ hasText: STUB_TOKEN.symbol },
|
|
||||||
);
|
|
||||||
const balances = await env.page.locator("#address-balances").innerText();
|
|
||||||
assert(
|
|
||||||
(await row.count()) === 1,
|
|
||||||
"the balance list has no " +
|
|
||||||
STUB_TOKEN.symbol +
|
|
||||||
" row for the token just added: " +
|
|
||||||
JSON.stringify(balances),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TransactionDetail looks the same from either entry point. Only the
|
|
||||||
// persisted stack says which one opened it, so that is what is asserted: from
|
|
||||||
// the token screen it ends in "address-token", and Back has to land there
|
|
||||||
// rather than on the address screen beneath it.
|
|
||||||
test("transaction detail opened from the token screen goes Back to it (#151)", async (env) => {
|
|
||||||
await goHome(env.page);
|
|
||||||
const base = await persistedViewStack(env.page);
|
|
||||||
|
|
||||||
await env.page.locator("#wallet-list .btn-addr-info").first().click();
|
|
||||||
await visible(env.page, "#view-address");
|
|
||||||
await env.page
|
|
||||||
.locator('#address-balances [data-token="' + STUB_TOKEN.address + '"]')
|
|
||||||
.click();
|
|
||||||
await visible(env.page, "#view-address-token");
|
|
||||||
|
|
||||||
const row = env.page.locator("#address-token-tx-list .tx-row").first();
|
|
||||||
await row.waitFor({ state: "visible", timeout: 30000 });
|
|
||||||
await row.click();
|
|
||||||
await visible(env.page, "#view-transaction");
|
|
||||||
await waitForPersisted(
|
|
||||||
env.page,
|
|
||||||
"viewStack",
|
|
||||||
base.concat("main", "address", "address-token"),
|
|
||||||
"on transaction detail opened from the token screen",
|
|
||||||
);
|
|
||||||
|
|
||||||
// The stack is checked before the screen, so a Back that lands on the
|
|
||||||
// wrong screen fails by saying what the stack holds.
|
|
||||||
await env.page.click("#btn-tx-back");
|
|
||||||
await waitForPersisted(
|
|
||||||
env.page,
|
|
||||||
"viewStack",
|
|
||||||
base.concat("main", "address"),
|
|
||||||
"after Back from transaction detail",
|
|
||||||
);
|
|
||||||
await visible(env.page, "#view-address-token");
|
|
||||||
|
|
||||||
// Onto the address screen, which the next test starts from.
|
|
||||||
await env.page.click("#btn-address-token-back");
|
|
||||||
await visible(env.page, "#view-address");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Read off the anchor rather than followed: where it points is all the popup
|
|
||||||
// decides, and following it would only load the explorer's page. The suite is
|
|
||||||
// on mainnet until the Settings section.
|
|
||||||
test("the token contract row links to the explorer's token page (#151)", async (env) => {
|
|
||||||
await leaveTransactionDetail(env.page);
|
|
||||||
const row = env.page
|
|
||||||
.locator("#tx-list .tx-row")
|
|
||||||
.filter({ hasText: STUB_TOKEN.symbol });
|
|
||||||
await row.waitFor({ state: "visible", timeout: 30000 });
|
|
||||||
await row.click();
|
|
||||||
await visible(env.page, "#view-transaction");
|
|
||||||
await visible(env.page, "#tx-detail-token-contract-section");
|
|
||||||
|
|
||||||
const href = await env.page
|
|
||||||
.locator("#tx-detail-token-contract a")
|
|
||||||
.getAttribute("href");
|
|
||||||
const expected = "https://etherscan.io/token/" + STUB_TOKEN.address;
|
|
||||||
assert(
|
|
||||||
href === expected,
|
|
||||||
"the token contract row links to " +
|
|
||||||
JSON.stringify(href) +
|
|
||||||
", expected " +
|
|
||||||
expected,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// -------------------------------------------- recovery phrase (#161)
|
// -------------------------------------------- recovery phrase (#161)
|
||||||
|
|
||||||
// The gear toggles, so pressing it while Settings is already up leaves it.
|
// The gear toggles, so pressing it while Settings is already up leaves it.
|
||||||
@@ -2799,84 +2699,6 @@ test("a token that lies about decimals() at signing time broadcasts nothing (#30
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------------ the wait for a receipt ending in error (#315)
|
|
||||||
//
|
|
||||||
// README.md (WaitTx) documents two ways the wait ends on the error screen: a
|
|
||||||
// lookup that answers "no receipt" 60 seconds or more after the broadcast, and
|
|
||||||
// six lookups in a row that fail. They are different facts with different
|
|
||||||
// messages, so each is driven to its own.
|
|
||||||
//
|
|
||||||
// Both wait in real time, about a minute each. The wait reads the popup's own
|
|
||||||
// clock and its own ten-second timer. Playwright's clock would move both, but
|
|
||||||
// it is installed on the whole browser context and cannot be removed, so every
|
|
||||||
// later test would run on it. Moving the stored broadcast time back instead can
|
|
||||||
// be undone by the save the popup makes every ten seconds.
|
|
||||||
|
|
||||||
// Send ETH from the address screen and stop on the wait for its receipt.
|
|
||||||
async function sendEthToWait(env) {
|
|
||||||
await goToConfirm(env.page, {
|
|
||||||
token: "ETH",
|
|
||||||
balance: FUNDED_ETH_TEXT + " ETH",
|
|
||||||
amount: COMFORTABLE_AMOUNT,
|
|
||||||
});
|
|
||||||
await waitForEstimate(env.page);
|
|
||||||
await fillPasswordAndSend(env.page);
|
|
||||||
await visible(env.page, "#view-wait-tx", 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("a wait still without a receipt after 60 seconds ends on the timeout message (#315)", async (env) => {
|
|
||||||
try {
|
|
||||||
await sendEthToWait(env);
|
|
||||||
// Lookups run every ten seconds and answer "no receipt", so the one
|
|
||||||
// that ends the wait comes about 60 seconds after the broadcast.
|
|
||||||
await visible(env.page, "#view-error-tx", 90000);
|
|
||||||
const message = (
|
|
||||||
await env.page.locator("#error-tx-message").innerText()
|
|
||||||
).trim();
|
|
||||||
assert(
|
|
||||||
message ===
|
|
||||||
"Transaction was not confirmed within 60 seconds. It may still confirm later — check Etherscan.",
|
|
||||||
"the wait did not end on the timeout message: " +
|
|
||||||
JSON.stringify(message),
|
|
||||||
);
|
|
||||||
await env.page.click("#btn-error-tx-done");
|
|
||||||
await visible(env.page, "#view-address");
|
|
||||||
} finally {
|
|
||||||
await backToAddressAfterSend(env);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("six failed receipt lookups in a row end on the unreachable-network message (#315)", async (env) => {
|
|
||||||
// Each failed lookup is logged through log.errorf, i.e. console.error.
|
|
||||||
// Exactly six are declared: a wait that ended sooner leaves one unmatched,
|
|
||||||
// and one that went on logs a seventh, and either fails this test.
|
|
||||||
for (let i = 1; i <= 6; i++) {
|
|
||||||
env.errors.expect(
|
|
||||||
"failed receipt lookup " + i + " of 6",
|
|
||||||
/poll receipt failed/,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
env.routeOpts.failReceiptLookup = true;
|
|
||||||
try {
|
|
||||||
await sendEthToWait(env);
|
|
||||||
await visible(env.page, "#view-error-tx", 90000);
|
|
||||||
const message = (
|
|
||||||
await env.page.locator("#error-tx-message").innerText()
|
|
||||||
).trim();
|
|
||||||
assert(
|
|
||||||
message ===
|
|
||||||
"The network could not be reached to check this transaction — 6 lookups failed in a row. Check the RPC URL in Settings. The transaction may still have confirmed — check Etherscan.",
|
|
||||||
"the wait did not end on the unreachable-network message: " +
|
|
||||||
JSON.stringify(message),
|
|
||||||
);
|
|
||||||
await env.page.click("#btn-error-tx-done");
|
|
||||||
await visible(env.page, "#view-address");
|
|
||||||
} finally {
|
|
||||||
env.routeOpts.failReceiptLookup = false;
|
|
||||||
await backToAddressAfterSend(env);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ------------------------------------------- hostile token symbol (#307)
|
// ------------------------------------------- hostile token symbol (#307)
|
||||||
//
|
//
|
||||||
// The reproduction from the issue, in the real browser against the real
|
// The reproduction from the issue, in the real browser against the real
|
||||||
@@ -4480,8 +4302,6 @@ async function main() {
|
|||||||
// Whether eth_getTransactionReceipt confirms a transaction rather than
|
// Whether eth_getTransactionReceipt confirms a transaction rather than
|
||||||
// answering "not mined yet".
|
// answering "not mined yet".
|
||||||
seedReceipt: false,
|
seedReceipt: false,
|
||||||
// Whether eth_getTransactionReceipt fails instead of answering (#315).
|
|
||||||
failReceiptLookup: false,
|
|
||||||
// Every raw signed transaction handed to eth_sendRawTransaction, in
|
// Every raw signed transaction handed to eth_sendRawTransaction, in
|
||||||
// order. The dApp transaction round trip asserts against these bytes
|
// order. The dApp transaction round trip asserts against these bytes
|
||||||
// rather than against anything the extension reported about them.
|
// rather than against anything the extension reported about them.
|
||||||
|
|||||||
@@ -21,14 +21,15 @@
|
|||||||
// escaping in src/shared/html.js is the primary fix; default-src is what
|
// escaping in src/shared/html.js is the primary fix; default-src is what
|
||||||
// stops the next escape that slips from reaching the network.
|
// stops the next escape that slips from reaching the network.
|
||||||
//
|
//
|
||||||
// And for #328: style-src is 'self' alone, so the browser refuses every
|
// Every directive below is pinned exactly, because each of the four
|
||||||
// style="..." attribute in the popup's markup, including one an escape lets
|
|
||||||
// through. The popup styles with classes; script setting element.style is
|
|
||||||
// not affected.
|
|
||||||
//
|
|
||||||
// Every directive below is pinned exactly, because each of the three
|
|
||||||
// loosenings is load-bearing and none of them may grow:
|
// loosenings is load-bearing and none of them may grow:
|
||||||
//
|
//
|
||||||
|
// style-src 'unsafe-inline' src/popup/index.html and the view helpers
|
||||||
|
// use style="..." attributes throughout, which
|
||||||
|
// CSP blocks without it. Chrome enforces this
|
||||||
|
// on attributes, not just <style> blocks, and
|
||||||
|
// Firefox has never implemented style-src-attr,
|
||||||
|
// so there is no narrower spelling available.
|
||||||
// img-src data: blockies are data: PNGs assigned to img.src.
|
// img-src data: blockies are data: PNGs assigned to img.src.
|
||||||
// connect-src https: http: the RPC endpoint is user-configurable, and a
|
// connect-src https: http: the RPC endpoint is user-configurable, and a
|
||||||
// local node over http://127.0.0.1 is a
|
// local node over http://127.0.0.1 is a
|
||||||
@@ -57,7 +58,7 @@ const EXPECTED_DIRECTIVES = {
|
|||||||
"default-src": ["'self'"],
|
"default-src": ["'self'"],
|
||||||
"script-src": ["'self'", "'wasm-unsafe-eval'"],
|
"script-src": ["'self'", "'wasm-unsafe-eval'"],
|
||||||
"object-src": ["'self'"],
|
"object-src": ["'self'"],
|
||||||
"style-src": ["'self'"],
|
"style-src": ["'self'", "'unsafe-inline'"],
|
||||||
"img-src": ["'self'", "data:"],
|
"img-src": ["'self'", "data:"],
|
||||||
"connect-src": ["'self'", "http:", "https:"],
|
"connect-src": ["'self'", "http:", "https:"],
|
||||||
"frame-src": ["'none'"],
|
"frame-src": ["'none'"],
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
// Every method in PROXY_METHODS is sent to the RPC node.
|
|
||||||
//
|
|
||||||
// handleRpc answers some methods itself before it reaches its proxy branch. A
|
|
||||||
// method listed in PROXY_METHODS but answered earlier never reaches the node,
|
|
||||||
// so the list would name a method that is not proxied
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/326). Each method is sent from
|
|
||||||
// a page here and must come back with what the node answered.
|
|
||||||
|
|
||||||
const { makeStorageStub } = require("./support/storageStub");
|
|
||||||
|
|
||||||
async function settle() {
|
|
||||||
for (let i = 0; i < 50; i++) await Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
delete global.chrome;
|
|
||||||
delete global.fetch;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("every method in PROXY_METHODS reaches the RPC node", async () => {
|
|
||||||
jest.resetModules();
|
|
||||||
|
|
||||||
jest.doMock("../src/shared/balances", () => ({
|
|
||||||
getProvider: () => ({}),
|
|
||||||
refreshBalances: jest.fn(async () => {}),
|
|
||||||
}));
|
|
||||||
jest.doMock("../src/shared/phishingDomains", () => ({
|
|
||||||
isPhishingDomain: () => false,
|
|
||||||
}));
|
|
||||||
jest.doMock("../src/shared/alarms", () => ({
|
|
||||||
BALANCE_REFRESH_ALARM: "balance",
|
|
||||||
BALANCE_REFRESH_PERIOD_MINUTES: 1,
|
|
||||||
ensureRecurringAlarms: jest.fn(async () => {}),
|
|
||||||
registerAlarmHandlers: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// The node answers each method with a value naming that method.
|
|
||||||
global.fetch = jest.fn(async (url, opts) => ({
|
|
||||||
status: 200,
|
|
||||||
json: async () => ({
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id: 1,
|
|
||||||
result: "node answered " + JSON.parse(opts.body).method,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
let messageListener = null;
|
|
||||||
global.chrome = {
|
|
||||||
storage: makeStorageStub({
|
|
||||||
autistmask: {
|
|
||||||
networkId: "mainnet",
|
|
||||||
wallets: [],
|
|
||||||
allowedSites: {},
|
|
||||||
deniedSites: {},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
runtime: {
|
|
||||||
getURL: (path) => "chrome-extension://autistmask/" + path,
|
|
||||||
onMessage: {
|
|
||||||
addListener: (fn) => {
|
|
||||||
messageListener = fn;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onConnect: { addListener: () => {} },
|
|
||||||
lastError: null,
|
|
||||||
},
|
|
||||||
windows: { onRemoved: { addListener: () => {} } },
|
|
||||||
action: { setPopup: () => {} },
|
|
||||||
};
|
|
||||||
|
|
||||||
const { PROXY_METHODS } = require("../src/background/index");
|
|
||||||
|
|
||||||
const answers = {};
|
|
||||||
const expected = {};
|
|
||||||
for (const method of PROXY_METHODS) {
|
|
||||||
messageListener(
|
|
||||||
{ type: "AUTISTMASK_RPC", method, params: [] },
|
|
||||||
{ origin: "https://dapp.example" },
|
|
||||||
(r) => {
|
|
||||||
answers[method] = r;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await settle();
|
|
||||||
expected[method] = { result: "node answered " + method };
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(PROXY_METHODS.length).toBeGreaterThan(0);
|
|
||||||
expect(answers).toEqual(expected);
|
|
||||||
});
|
|
||||||
+2
-294
@@ -4,16 +4,8 @@
|
|||||||
// already in storage: the import that created it ran before the refusal
|
// already in storage: the import that created it ran before the refusal
|
||||||
// existed. Such a wallet used to sign for the wrong tree and now throws on the
|
// existed. Such a wallet used to sign for the wrong tree and now throws on the
|
||||||
// send screen instead. These tests pin down that it is named and explained in
|
// send screen instead. These tests pin down that it is named and explained in
|
||||||
// the wallet list, that every control leading to a signature or to the private
|
// the wallet list, that nothing on the way there throws, and that a wallet
|
||||||
// key refuses it before asking for a password, that nothing on the way there
|
// imported from a real master key is untouched by any of it.
|
||||||
// throws, and that a wallet imported from a real master key is untouched by
|
|
||||||
// any of it.
|
|
||||||
|
|
||||||
// Mocked so that no password has to be hashed: the controls below are checked
|
|
||||||
// for whether they decrypt at all.
|
|
||||||
jest.mock("../src/shared/vault", () => ({
|
|
||||||
decryptWithPassword: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { HDNodeWallet, Mnemonic } = require("ethers");
|
const { HDNodeWallet, Mnemonic } = require("ethers");
|
||||||
|
|
||||||
@@ -245,290 +237,6 @@ describe("the wallet list", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// A minimal DOM for driving the popup views: any element exists on first
|
|
||||||
// lookup, and click() runs the listeners a view attached to it.
|
|
||||||
function makeElement(id) {
|
|
||||||
const classes = new Set();
|
|
||||||
const el = {
|
|
||||||
id,
|
|
||||||
textContent: "",
|
|
||||||
title: "",
|
|
||||||
value: "",
|
|
||||||
innerHTML: "",
|
|
||||||
disabled: false,
|
|
||||||
style: {},
|
|
||||||
dataset: {},
|
|
||||||
listeners: {},
|
|
||||||
classList: {
|
|
||||||
add: (...names) => names.forEach((n) => classes.add(n)),
|
|
||||||
remove: (...names) => names.forEach((n) => classes.delete(n)),
|
|
||||||
contains: (n) => classes.has(n),
|
|
||||||
toggle: (n, force) => {
|
|
||||||
const on = force === undefined ? !classes.has(n) : force;
|
|
||||||
if (on) classes.add(n);
|
|
||||||
else classes.delete(n);
|
|
||||||
return on;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
addEventListener: (name, fn) => {
|
|
||||||
el.listeners[name] = el.listeners[name] || [];
|
|
||||||
el.listeners[name].push(fn);
|
|
||||||
},
|
|
||||||
querySelectorAll: () => [],
|
|
||||||
appendChild: () => {},
|
|
||||||
};
|
|
||||||
// Views reach for .parentElement to hide whole sections.
|
|
||||||
Object.defineProperty(el, "parentElement", {
|
|
||||||
get: () => node(id + "-parent"),
|
|
||||||
});
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeDocument() {
|
|
||||||
const els = new Map();
|
|
||||||
return {
|
|
||||||
getElementById(id) {
|
|
||||||
// The debug banner is created on demand by helpers.js; absent
|
|
||||||
// is the state a non-debug, non-testnet popup is in.
|
|
||||||
if (id === "debug-banner") return null;
|
|
||||||
if (!els.has(id)) els.set(id, makeElement(id));
|
|
||||||
return els.get(id);
|
|
||||||
},
|
|
||||||
createElement: () => makeElement("created"),
|
|
||||||
addEventListener: () => {},
|
|
||||||
body: { prepend: () => {} },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function node(id) {
|
|
||||||
return globalThis.document.getElementById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function click(id) {
|
|
||||||
return Promise.all((node(id).listeners.click || []).map((fn) => fn()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// getSignerForAddress refuses this wallet's key, but only once the password
|
|
||||||
// has been typed and spent, and the screens report that refusal as a wrong
|
|
||||||
// password or a failed send. So every control that leads to it refuses first.
|
|
||||||
describe("every way to a signature or the private key refuses a defective wallet first", () => {
|
|
||||||
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
|
|
||||||
|
|
||||||
let state;
|
|
||||||
let decryptWithPassword;
|
|
||||||
let home;
|
|
||||||
let addressDetail;
|
|
||||||
let addressToken;
|
|
||||||
let approval;
|
|
||||||
let confirmTx;
|
|
||||||
|
|
||||||
let address;
|
|
||||||
let shortMessage;
|
|
||||||
// What the background answers when the approval window asks which
|
|
||||||
// approval it was opened for, and every message the popup sent it.
|
|
||||||
let approvalDetails;
|
|
||||||
let sent;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
state = require("../src/shared/state").state;
|
|
||||||
decryptWithPassword =
|
|
||||||
require("../src/shared/vault").decryptWithPassword;
|
|
||||||
home = require("../src/popup/views/home");
|
|
||||||
addressDetail = require("../src/popup/views/addressDetail");
|
|
||||||
addressToken = require("../src/popup/views/addressToken");
|
|
||||||
approval = require("../src/popup/views/approval");
|
|
||||||
confirmTx = require("../src/popup/views/confirmTx");
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const broken = brokenXprvWallet();
|
|
||||||
// A balance, so that no Send button's zero-balance refusal can stand
|
|
||||||
// in for the defect check.
|
|
||||||
broken.addresses[0].balance = "1.0000";
|
|
||||||
broken.addresses[0].tokenBalances = [];
|
|
||||||
address = broken.addresses[0].address;
|
|
||||||
shortMessage = walletDefect(broken).shortMessage;
|
|
||||||
|
|
||||||
approvalDetails = null;
|
|
||||||
sent = [];
|
|
||||||
globalThis.document = makeDocument();
|
|
||||||
globalThis.window = { close: () => {} };
|
|
||||||
globalThis.chrome = {
|
|
||||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
|
||||||
runtime: {
|
|
||||||
connect: () => ({ postMessage: () => {} }),
|
|
||||||
sendMessage: (msg, reply) => {
|
|
||||||
sent.push(msg);
|
|
||||||
if (!reply) return;
|
|
||||||
reply(
|
|
||||||
msg.type === "AUTISTMASK_GET_APPROVAL"
|
|
||||||
? approvalDetails
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// What the wallet's stored secret decrypts to: the account-level key
|
|
||||||
// it was imported from.
|
|
||||||
decryptWithPassword.mockReset();
|
|
||||||
decryptWithPassword.mockResolvedValue(accountXprv(VECTOR_PHRASE));
|
|
||||||
|
|
||||||
state.wallets = [broken];
|
|
||||||
state.activeAddress = address;
|
|
||||||
state.selectedWallet = 0;
|
|
||||||
state.selectedAddress = 0;
|
|
||||||
state.selectedToken = "ETH";
|
|
||||||
state.viewStack = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
state.wallets = [];
|
|
||||||
state.activeAddress = null;
|
|
||||||
state.selectedWallet = null;
|
|
||||||
state.selectedAddress = null;
|
|
||||||
state.selectedToken = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Send on the main screen", async () => {
|
|
||||||
state.currentView = "main";
|
|
||||||
home.init({});
|
|
||||||
|
|
||||||
await click("btn-main-send");
|
|
||||||
|
|
||||||
expect(node("flash-msg").textContent).toBe(shortMessage);
|
|
||||||
expect(state.currentView).toBe("main");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Send on the address screen", async () => {
|
|
||||||
state.currentView = "address";
|
|
||||||
addressDetail.init({});
|
|
||||||
|
|
||||||
await click("btn-send");
|
|
||||||
|
|
||||||
expect(node("flash-msg").textContent).toBe(shortMessage);
|
|
||||||
expect(state.currentView).toBe("address");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Export Private Key on the address screen", async () => {
|
|
||||||
state.currentView = "address";
|
|
||||||
addressDetail.init({});
|
|
||||||
|
|
||||||
await click("btn-export-privkey");
|
|
||||||
|
|
||||||
expect(node("flash-msg").textContent).toBe(shortMessage);
|
|
||||||
expect(state.currentView).toBe("address");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Send on a token's screen", async () => {
|
|
||||||
state.currentView = "address-token";
|
|
||||||
addressToken.init({});
|
|
||||||
|
|
||||||
await click("btn-address-token-send");
|
|
||||||
|
|
||||||
expect(node("flash-msg").textContent).toBe(shortMessage);
|
|
||||||
expect(state.currentView).toBe("address-token");
|
|
||||||
});
|
|
||||||
|
|
||||||
// The popup reopens onto this screen from a saved view, so the Send
|
|
||||||
// buttons above are not the only way onto it. The screen is not drawn,
|
|
||||||
// because drawing it starts a fee estimate against the network; with a
|
|
||||||
// decrypt that fails, a handler without the check stops at the password
|
|
||||||
// instead of going on to a transaction that was never set up.
|
|
||||||
test("Send on the confirmation screen", async () => {
|
|
||||||
decryptWithPassword.mockRejectedValue(new Error("wrong password"));
|
|
||||||
state.currentView = "confirm-tx";
|
|
||||||
confirmTx.init({});
|
|
||||||
node("confirm-tx-password").value = "any password";
|
|
||||||
|
|
||||||
await click("btn-confirm-send");
|
|
||||||
|
|
||||||
expect(decryptWithPassword).not.toHaveBeenCalled();
|
|
||||||
expect(node("confirm-tx-password-error").textContent).toBe(
|
|
||||||
shortMessage,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function openTxApproval() {
|
|
||||||
approvalDetails = {
|
|
||||||
type: "tx",
|
|
||||||
origin: "https://dapp.example",
|
|
||||||
isPhishingDomain: false,
|
|
||||||
approvedFrom: address,
|
|
||||||
approvedTx: {
|
|
||||||
from: address,
|
|
||||||
to: RECIPIENT,
|
|
||||||
value: "0x0",
|
|
||||||
data: "0x",
|
|
||||||
chainId: 1,
|
|
||||||
nonce: 0,
|
|
||||||
gasLimit: "21000",
|
|
||||||
maxFeePerGas: "1000000000",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
approval.init({});
|
|
||||||
await approval.show(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openSignApproval() {
|
|
||||||
approvalDetails = {
|
|
||||||
type: "sign",
|
|
||||||
origin: "https://dapp.example",
|
|
||||||
isPhishingDomain: false,
|
|
||||||
approvedFrom: address,
|
|
||||||
// "Hello", as the hex a page sends.
|
|
||||||
signParams: {
|
|
||||||
method: "personal_sign",
|
|
||||||
message: "0x48656c6c6f",
|
|
||||||
from: address,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
approval.init({});
|
|
||||||
await approval.show(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
test("the transaction approval screen says so and disables Approve", async () => {
|
|
||||||
await openTxApproval();
|
|
||||||
|
|
||||||
expect(node("approve-tx-error").textContent).toBe(shortMessage);
|
|
||||||
expect(node("btn-approve-tx").disabled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// The stub runs a disabled button's listener, which a browser would not:
|
|
||||||
// what is asked here is whether the handler refuses on its own.
|
|
||||||
test("Approve on the transaction approval screen does not decrypt", async () => {
|
|
||||||
await openTxApproval();
|
|
||||||
node("approve-tx-password").value = "any password";
|
|
||||||
|
|
||||||
await click("btn-approve-tx");
|
|
||||||
|
|
||||||
expect(decryptWithPassword).not.toHaveBeenCalled();
|
|
||||||
expect(sent.map((msg) => msg.type)).not.toContain(
|
|
||||||
"AUTISTMASK_TX_RESPONSE",
|
|
||||||
);
|
|
||||||
expect(node("approve-tx-error").textContent).toBe(shortMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("the signature approval screen says so and disables Approve", async () => {
|
|
||||||
await openSignApproval();
|
|
||||||
|
|
||||||
expect(node("approve-sign-error").textContent).toBe(shortMessage);
|
|
||||||
expect(node("btn-approve-sign").disabled).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Approve on the signature approval screen does not decrypt", async () => {
|
|
||||||
await openSignApproval();
|
|
||||||
node("approve-sign-password").value = "any password";
|
|
||||||
|
|
||||||
await click("btn-approve-sign");
|
|
||||||
|
|
||||||
expect(decryptWithPassword).not.toHaveBeenCalled();
|
|
||||||
expect(sent.map((msg) => msg.type)).not.toContain(
|
|
||||||
"AUTISTMASK_SIGN_RESPONSE",
|
|
||||||
);
|
|
||||||
expect(node("approve-sign-error").textContent).toBe(shortMessage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("no path throws an unhandled error for a defective wallet", () => {
|
describe("no path throws an unhandled error for a defective wallet", () => {
|
||||||
test("address derivation from the stored xpub still works", () => {
|
test("address derivation from the stored xpub still works", () => {
|
||||||
// The stored xpub is at a non-standard depth but is a valid extended
|
// The stored xpub is at a non-standard depth but is a valid extended
|
||||||
|
|||||||
Reference in New Issue
Block a user