Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bccfd61f60 |
No files matched your search
+5
-4
@@ -18,8 +18,9 @@ COPY . .
|
|||||||
RUN yarn run lint
|
RUN yarn run lint
|
||||||
RUN script/check-censored
|
RUN script/check-censored
|
||||||
|
|
||||||
# Test phase, same shape and for the same reason: the jest suite, rerun
|
# Test phase, same shape and for the same reason: the jest suite (its worker
|
||||||
# verbose on failure, then script/test-verify-build.
|
# cap is in package.json), rerun verbose on failure, then
|
||||||
|
# script/test-verify-build.
|
||||||
#
|
#
|
||||||
# node:22-slim (22.x LTS), 2026-02-24
|
# node:22-slim (22.x LTS), 2026-02-24
|
||||||
FROM node@sha256:5373f1906319b3a1f291da5d102f4ce5c77ccbe29eb637f072b6c7b70443fc36 AS test
|
FROM node@sha256:5373f1906319b3a1f291da5d102f4ce5c77ccbe29eb637f072b6c7b70443fc36 AS test
|
||||||
@@ -32,9 +33,9 @@ RUN script/bootstrap
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN yarn run test || \
|
RUN timeout 90 yarn run test || \
|
||||||
{ echo "--- Rerunning with --verbose for details ---"; \
|
{ echo "--- Rerunning with --verbose for details ---"; \
|
||||||
yarn run test:verbose; exit 1; }
|
timeout 90 yarn run test:verbose; exit 1; }
|
||||||
RUN script/test-verify-build
|
RUN script/test-verify-build
|
||||||
|
|
||||||
# Development environment with the extension built, and the last stage: a
|
# Development environment with the extension built, and the last stage: a
|
||||||
|
|||||||
@@ -264,8 +264,6 @@ The shared modules, each in one line:
|
|||||||
messaging, windows and the lock.
|
messaging, windows and the lock.
|
||||||
- `buildInfo.js`: the version, license, author, commit and date compiled into
|
- `buildInfo.js`: the version, license, author, commit and date compiled into
|
||||||
About.
|
About.
|
||||||
- `calldataDecoder.js`: the ERC-20 `approve()` and `transfer()` decoder, which
|
|
||||||
hands other calldata to `uniswap.js`. Popup only.
|
|
||||||
- `chainSwitch.js`: the popup's network switch.
|
- `chainSwitch.js`: the popup's network switch.
|
||||||
- `chainSwitchFields.js`: the fields a network switch changes.
|
- `chainSwitchFields.js`: the fields a network switch changes.
|
||||||
- `constants.js`: `DEBUG`, the default endpoints, the ERC-20 ABI and the burn
|
- `constants.js`: `DEBUG`, the default endpoints, the ERC-20 ABI and the burn
|
||||||
@@ -1191,23 +1189,15 @@ These helpers write one kind of value on every screen that shows it:
|
|||||||
|
|
||||||
Most screens show token and ETH amounts with four decimal places. History rows
|
Most screens show token and ETH amounts with four decimal places. History rows
|
||||||
cut amounts to four decimals and never round up (`truncateAmount()` in
|
cut amounts to four decimals and never round up (`truncateAmount()` in
|
||||||
`src/shared/amountDisplay.js`). These balances are cut the same way:
|
`src/shared/amountDisplay.js`). The balance lists and Home's ETH balance round
|
||||||
|
to four places. The screens that state what is being authorized cut as the
|
||||||
- the balance lists;
|
history does, and extend an amount below 0.0001 to its first nonzero digit
|
||||||
- Home's ETH balance;
|
|
||||||
- Send's current balance;
|
|
||||||
- ConfirmTx's balance line.
|
|
||||||
|
|
||||||
So a balance below 0.0001 reads `0.0000` there.
|
|
||||||
|
|
||||||
The screens that state what is being authorized cut as the history does, and
|
|
||||||
extend an amount below 0.0001 to its first nonzero digit
|
|
||||||
(`truncateAmountNeverZero()`). So a nonzero amount never reads as zero there.
|
(`truncateAmountNeverZero()`). So a nonzero amount never reads as zero there.
|
||||||
Those screens are:
|
Those screens are:
|
||||||
|
|
||||||
- TxApproval;
|
- TxApproval;
|
||||||
- SignApproval's Permit warning;
|
- SignApproval's Permit warning;
|
||||||
- ConfirmTx's amount and network fee;
|
- ConfirmTx, and Send's current balance;
|
||||||
- WaitTx, SuccessTx and ErrorTx.
|
- WaitTx, SuccessTx and ErrorTx.
|
||||||
|
|
||||||
TransactionDetail shows the exact amount and its base units.
|
TransactionDetail shows the exact amount and its base units.
|
||||||
@@ -1389,7 +1379,7 @@ approval screens show a red warning.
|
|||||||
### Transaction decoding
|
### Transaction decoding
|
||||||
|
|
||||||
TxApproval and TransactionDetail try to decode calldata into a summary
|
TxApproval and TransactionDetail try to decode calldata into a summary
|
||||||
(`decodeCalldata()` in `src/shared/calldataDecoder.js`). SuccessTx repeats
|
(`decodeCalldata()` in `src/popup/views/approval.js`). SuccessTx repeats
|
||||||
TxApproval's summary. Decoding is a display aid. When it fails, the raw calldata
|
TxApproval's summary. Decoding is a display aid. When it fails, the raw calldata
|
||||||
is shown in full. No contract gets any treatment beyond a legible confirmation
|
is shown in full. No contract gets any treatment beyond a legible confirmation
|
||||||
screen.
|
screen.
|
||||||
@@ -1655,10 +1645,11 @@ The development dependencies ship nowhere:
|
|||||||
|
|
||||||
### Crypto policy
|
### Crypto policy
|
||||||
|
|
||||||
In the extension, hashing, key derivation, signing and encryption go through
|
Hashing, key derivation, signing and encryption go through `ethers` or
|
||||||
`ethers` or `libsodium-wrappers-sumo`. A new recovery phrase takes its random
|
`libsodium-wrappers-sumo`. No primitive is implemented in this repository. A new
|
||||||
bytes from `ethers`. `build.js` and `script/lib/package.js` also hash files with
|
recovery phrase draws its random bytes from the browser's
|
||||||
`ethers`. No primitive is implemented in this repository.
|
`crypto.getRandomValues()`. The build tooling, `build.js` and `script/lib/`,
|
||||||
|
hashes files with Node's `crypto`; none of it ships in the extension.
|
||||||
|
|
||||||
### Content Security Policy
|
### Content Security Policy
|
||||||
|
|
||||||
@@ -1761,8 +1752,8 @@ The `Makefile` targets are thin shims over `script/`:
|
|||||||
`package.json`.
|
`package.json`.
|
||||||
- `script/setup`: `script/bootstrap`, then the git pre-commit hook.
|
- `script/setup`: `script/bootstrap`, then the git pre-commit hook.
|
||||||
- `script/test`: build the `test` stage of the `Dockerfile` with `--no-cache`.
|
- `script/test`: build the `test` stage of the `Dockerfile` with `--no-cache`.
|
||||||
The stage runs the jest suite, rerun verbose on failure, then
|
The stage runs the jest suite, stopped at 90 seconds and rerun verbose on
|
||||||
`script/test-verify-build`.
|
failure, then `script/test-verify-build`.
|
||||||
- `script/lint`: build the `lint` stage with `--no-cache`. The stage runs
|
- `script/lint`: build the `lint` stage with `--no-cache`. The stage runs
|
||||||
ESLint, the prettier check and `script/check-censored`. Linting runs only in
|
ESLint, the prettier check and `script/check-censored`. Linting runs only in
|
||||||
Docker.
|
Docker.
|
||||||
@@ -1861,10 +1852,9 @@ have left a complete, loadable debug bundle behind. `make build-debug` keeps its
|
|||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
`make test` runs the jest suite in Docker, in one process. The suite must finish
|
`make test` runs the jest suite in Docker. The suite must finish within 60
|
||||||
within 60 seconds. A test that leaves a timer or another open handle running
|
seconds; the test stage stops it at 90. `make check` runs the tests, the lint
|
||||||
fails the run. `make check` runs the tests, the lint stage and the formatting
|
stage and the formatting check.
|
||||||
check.
|
|
||||||
|
|
||||||
Some unit tests pin facts stated elsewhere in this file:
|
Some unit tests pin facts stated elsewhere in this file:
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,10 @@ contradicts either, the originals govern.
|
|||||||
- [ ] All tool invocations via `make` targets, never directly
|
- [ ] All tool invocations via `make` targets, never directly
|
||||||
- [ ] `make check` = `make test` + `make lint` + `make fmt-check`
|
- [ ] `make check` = `make test` + `make lint` + `make fmt-check`
|
||||||
- [ ] `main` always passes `make check`
|
- [ ] `main` always passes `make check`
|
||||||
- [ ] Feature branches for all changes, merge to main when done
|
- [ ] Branch per issue, cut from `next`. One PR per issue, based on `next`,
|
||||||
|
squash-merged into `next` on a passed independent review. `main` is
|
||||||
|
reached only by the per-milestone PR from `next`; releases are tagged from
|
||||||
|
`main`. `next` and `main` must stay green; feature branches may be red.
|
||||||
- [ ] No force-push to `main`
|
- [ ] No force-push to `main`
|
||||||
- [ ] Pre-commit hook runs `make check`
|
- [ ] Pre-commit hook runs `make check`
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const crypto = require("crypto");
|
||||||
const { execSync } = require("child_process");
|
const { execSync } = require("child_process");
|
||||||
const esbuild = require("esbuild");
|
const esbuild = require("esbuild");
|
||||||
const { sha256 } = require("ethers");
|
|
||||||
const { resolveVersion } = require("./script/lib/version");
|
const { resolveVersion } = require("./script/lib/version");
|
||||||
const {
|
const {
|
||||||
BACKGROUND_ENTRY_PREFIX,
|
BACKGROUND_ENTRY_PREFIX,
|
||||||
@@ -314,8 +314,10 @@ function copyIcons(distDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sha256File(absPath) {
|
function sha256File(absPath) {
|
||||||
// ethers prefixes the hex with 0x; the receipt holds bare hex.
|
return crypto
|
||||||
return sha256(fs.readFileSync(absPath)).slice(2);
|
.createHash("sha256")
|
||||||
|
.update(fs.readFileSync(absPath))
|
||||||
|
.digest("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the receipt for the files this build emitted. Deliberately records no
|
// Write the receipt for the files this build emitted. Deliberately records no
|
||||||
|
|||||||
+2
-7
@@ -135,14 +135,9 @@ module.exports = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Unit tests, the helpers they require, and the test files they hand to
|
// Unit tests, and the helpers they require: jest on node.
|
||||||
// jest: jest on node.
|
|
||||||
{
|
{
|
||||||
files: [
|
files: ["tests/**/*.test.js", "tests/support/**/*.js"],
|
||||||
"tests/**/*.test.js",
|
|
||||||
"tests/support/**/*.js",
|
|
||||||
"tests/fixtures/**/*.js",
|
|
||||||
],
|
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
...commonjs,
|
...commonjs,
|
||||||
globals: { ...globals.node, ...globals.jest },
|
globals: { ...globals.node, ...globals.jest },
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node script/lib/jest.js --runInBand",
|
"test": "node script/lib/jest.js --maxWorkers=3",
|
||||||
"test:verbose": "node script/lib/jest.js --runInBand --verbose",
|
"test:verbose": "node script/lib/jest.js --maxWorkers=3 --verbose",
|
||||||
"build": "node build.js",
|
"build": "node build.js",
|
||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fmt": "prettier --write .",
|
"fmt": "prettier --write .",
|
||||||
|
|||||||
+25
-17
@@ -1,8 +1,8 @@
|
|||||||
// Runs jest with the arguments it is given, and fails the run at once when a
|
// Runs jest with the arguments it is given, and fails the run when jest had to
|
||||||
// test left a timer or another open handle running. Such a handle keeps this
|
// force-stop the worker process that ran a test file. Jest itself only prints a
|
||||||
// process alive after the last test, so the run would hang. Jest itself only
|
// warning when that happens, and its run still passes. The warning means a test
|
||||||
// prints a warning one second after the run, and then keeps waiting. The test
|
// left a timer or another open handle running. The test scripts in package.json
|
||||||
// scripts in package.json run jest through this file, in this one process.
|
// run jest through this file.
|
||||||
//
|
//
|
||||||
// Jest runs inside this process and writes to the terminal directly, so its
|
// Jest runs inside this process and writes to the terminal directly, so its
|
||||||
// output looks the same as a plain jest run. This file only checks each write
|
// output looks the same as a plain jest run. This file only checks each write
|
||||||
@@ -10,23 +10,31 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// The start of the warning that jest prints on stderr when the process has not
|
// The start of the warning that jest prints on stderr after it force-stops a
|
||||||
// exited one second after the test run.
|
// worker process.
|
||||||
const OPEN_HANDLE_WARNING =
|
const WORKER_WARNING = "A worker process has failed to exit gracefully";
|
||||||
"Jest did not exit one second after the test run has completed.";
|
|
||||||
|
let forceStopped = false;
|
||||||
|
|
||||||
const writeStderr = process.stderr.write.bind(process.stderr);
|
const writeStderr = process.stderr.write.bind(process.stderr);
|
||||||
process.stderr.write = (chunk, ...rest) => {
|
process.stderr.write = (chunk, ...rest) => {
|
||||||
const written = writeStderr(chunk, ...rest);
|
if (String(chunk).includes(WORKER_WARNING)) {
|
||||||
if (String(chunk).includes(OPEN_HANDLE_WARNING)) {
|
forceStopped = true;
|
||||||
writeStderr(
|
|
||||||
"script/lib/jest.js: failing the run, because a test left a " +
|
|
||||||
"timer or another open handle running.\n",
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
return written;
|
return writeStderr(chunk, ...rest);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Runs after jest has finished, as the process exits.
|
||||||
|
process.on("exit", () => {
|
||||||
|
if (forceStopped) {
|
||||||
|
console.error(
|
||||||
|
"script/lib/jest.js: failing the run, because jest force-stopped " +
|
||||||
|
"a worker process. A test left a timer or another open " +
|
||||||
|
"handle running.",
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Jest's own command-line program. It reads its arguments from process.argv.
|
// Jest's own command-line program. It reads its arguments from process.argv.
|
||||||
require("jest/bin/jest");
|
require("jest/bin/jest");
|
||||||
@@ -25,9 +25,9 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const crypto = require("crypto");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const ethers = require("ethers");
|
|
||||||
|
|
||||||
const { readZip, writeZip } = require("./zip");
|
const { readZip, writeZip } = require("./zip");
|
||||||
const { resolveVersion } = require("./version");
|
const { resolveVersion } = require("./version");
|
||||||
@@ -60,9 +60,8 @@ function fail(message) {
|
|||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lowercase hex without the 0x prefix ethers adds, as sha256sum prints it.
|
|
||||||
function sha256(buf) {
|
function sha256(buf) {
|
||||||
return ethers.sha256(buf).slice(2);
|
return crypto.createHash("sha256").update(buf).digest("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every regular file under dir, as archive-root-relative forward-slashed
|
// Every regular file under dir, as archive-root-relative forward-slashed
|
||||||
@@ -292,9 +291,4 @@ if (require.main === module) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = { checkSelfContained, htmlReferences, manifestReferences };
|
||||||
checkSelfContained,
|
|
||||||
htmlReferences,
|
|
||||||
manifestReferences,
|
|
||||||
sha256,
|
|
||||||
};
|
|
||||||
@@ -159,10 +159,10 @@ function renderBalance() {
|
|||||||
const addr =
|
const addr =
|
||||||
state.wallets[state.selectedWallet].addresses[state.selectedAddress];
|
state.wallets[state.selectedWallet].addresses[state.selectedAddress];
|
||||||
const tokenId = state.selectedToken;
|
const tokenId = state.selectedToken;
|
||||||
let balance, price;
|
let amount, price;
|
||||||
if (tokenId === "ETH") {
|
if (tokenId === "ETH") {
|
||||||
// null when no refresh has read the balance.
|
// null when no refresh has read the balance.
|
||||||
balance = addr.balance;
|
amount = unknownableAmount(addr.balance);
|
||||||
price = getPrice("ETH");
|
price = getPrice("ETH");
|
||||||
} else {
|
} else {
|
||||||
const id = tokenId.toLowerCase();
|
const id = tokenId.toLowerCase();
|
||||||
@@ -175,13 +175,12 @@ function renderBalance() {
|
|||||||
// null when the scale is unknown, or when no refresh has read the
|
// null when the scale is unknown, or when no refresh has read the
|
||||||
// token's balance: no quantity to show, and none to price.
|
// token's balance: no quantity to show, and none to price.
|
||||||
// balanceLine() states that rather than printing 0.0000.
|
// balanceLine() states that rather than printing 0.0000.
|
||||||
if (tb) balance = tb.balance;
|
if (tb) amount = unknownableAmount(tb.balance);
|
||||||
else balance = tracked && tracked.balanceNotFetched ? null : "0";
|
else amount = tracked && tracked.balanceNotFetched ? null : 0;
|
||||||
price = getPrice(tokenId);
|
price = getPrice(tokenId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// USD total for this token only
|
// USD total for this token only
|
||||||
const amount = unknownableAmount(balance);
|
|
||||||
const usdVal = price && amount !== null ? amount * price : null;
|
const usdVal = price && amount !== null ? amount * price : null;
|
||||||
const usdStr = formatUsd(usdVal);
|
const usdStr = formatUsd(usdVal);
|
||||||
$("address-token-usd-total").innerHTML = escapeHtml(usdStr) || " ";
|
$("address-token-usd-total").innerHTML = escapeHtml(usdStr) || " ";
|
||||||
@@ -189,7 +188,7 @@ function renderBalance() {
|
|||||||
// Single token balance line (no tokenId — not clickable here)
|
// Single token balance line (no tokenId — not clickable here)
|
||||||
$("address-token-balance").innerHTML = balanceLine(
|
$("address-token-balance").innerHTML = balanceLine(
|
||||||
currentSymbol,
|
currentSymbol,
|
||||||
balance,
|
amount,
|
||||||
price,
|
price,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+144
-12
@@ -12,13 +12,9 @@ const {
|
|||||||
formatFee,
|
formatFee,
|
||||||
tokenLabel,
|
tokenLabel,
|
||||||
blockscoutOffTokenNoteHtml,
|
blockscoutOffTokenNoteHtml,
|
||||||
} = require("./helpers");
|
|
||||||
const {
|
|
||||||
state,
|
|
||||||
saveState,
|
|
||||||
currentNetwork,
|
|
||||||
tokenSourcesByChainId,
|
tokenSourcesByChainId,
|
||||||
} = require("../../shared/state");
|
} = require("./helpers");
|
||||||
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const {
|
const {
|
||||||
networkById,
|
networkById,
|
||||||
networkByChainId,
|
networkByChainId,
|
||||||
@@ -30,24 +26,21 @@ const {
|
|||||||
getAddress,
|
getAddress,
|
||||||
getBigInt,
|
getBigInt,
|
||||||
getBytes,
|
getBytes,
|
||||||
|
Interface,
|
||||||
MaxUint256,
|
MaxUint256,
|
||||||
toUtf8String,
|
toUtf8String,
|
||||||
} = require("ethers");
|
} = require("ethers");
|
||||||
const { getPrice, formatUsd } = require("../../shared/prices");
|
const { getPrice, formatUsd } = require("../../shared/prices");
|
||||||
|
const { ERC20_ABI } = require("../../shared/constants");
|
||||||
const { INVISIBLE_CHARACTERS } = require("../../shared/symbolSpoof");
|
const { INVISIBLE_CHARACTERS } = require("../../shared/symbolSpoof");
|
||||||
const {
|
const {
|
||||||
resolveTokenDecimals,
|
resolveTokenDecimals,
|
||||||
resolveTokenSymbol,
|
resolveTokenSymbol,
|
||||||
unknownDecimalsAmount,
|
unknownDecimalsAmount,
|
||||||
} = require("../../shared/approvalAmount");
|
} = require("../../shared/approvalAmount");
|
||||||
const {
|
|
||||||
decodeCalldata,
|
|
||||||
tokenAmountText,
|
|
||||||
} = require("../../shared/calldataDecoder");
|
|
||||||
// Four decimals, with the nonzero floor these screens hold: every amount this
|
// Four decimals, with the nonzero floor these screens hold: every amount this
|
||||||
// view renders — the ERC-20 line, the ETH value, the max fee — and every one
|
// view renders — the ERC-20 line, the ETH value, the max fee — and every one
|
||||||
// it carries forward to the wait/success/error screens goes through it. The
|
// it carries forward to the wait/success/error screens goes through it.
|
||||||
// ERC-20 line gets it from tokenAmountText().
|
|
||||||
const {
|
const {
|
||||||
truncateAmountNeverZero: formatTxValue,
|
truncateAmountNeverZero: formatTxValue,
|
||||||
} = require("../../shared/amountDisplay");
|
} = require("../../shared/amountDisplay");
|
||||||
@@ -61,8 +54,11 @@ const {
|
|||||||
typedDataChainRefusal,
|
typedDataChainRefusal,
|
||||||
} = require("../../shared/approvalVerify");
|
} = require("../../shared/approvalVerify");
|
||||||
const txStatus = require("./txStatus");
|
const txStatus = require("./txStatus");
|
||||||
|
const uniswap = require("../../shared/uniswap");
|
||||||
const { runtimeApi, sendMessage } = require("../../shared/browserApi");
|
const { runtimeApi, sendMessage } = require("../../shared/browserApi");
|
||||||
|
|
||||||
|
const erc20Iface = new Interface(ERC20_ABI);
|
||||||
|
|
||||||
// The link opens the explorer of the network with `chainId`, or of the active
|
// The link opens the explorer of the network with `chainId`, or of the active
|
||||||
// network when none is given. A chain id that names no network has no
|
// network when none is given. A chain id that names no network has no
|
||||||
// explorer, so the address is drawn without a link.
|
// explorer, so the address is drawn without a link.
|
||||||
@@ -74,6 +70,141 @@ function approvalAddressHtml(address, chainId) {
|
|||||||
return renderAddressHtml(address, { title, chainId });
|
return renderAddressHtml(address, { title, chainId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The amount line for a decoded ERC-20 call. With a known scale it is the
|
||||||
|
// token quantity; with `decimals` null it is the base-unit integer with the
|
||||||
|
// unknown scale stated, because formatting it with an assumed scale is what
|
||||||
|
// showed a 5,000-token transfer as `0.0000`. `raw` is what the status screens
|
||||||
|
// carry, `display` is what the approval screen shows.
|
||||||
|
function tokenAmountText(rawAmount, decimals, symbol) {
|
||||||
|
if (decimals === null) {
|
||||||
|
const unknown = unknownDecimalsAmount(rawAmount);
|
||||||
|
return { raw: unknown, display: unknown };
|
||||||
|
}
|
||||||
|
const formatted = formatTxValue(formatUnits(rawAmount, decimals));
|
||||||
|
return {
|
||||||
|
raw: formatted,
|
||||||
|
display: formatted + (symbol ? " " + symbol : ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to decode calldata sent from `fromAddress` to `toAddress` on the network
|
||||||
|
// with chain id `chainId`, using known ABIs.
|
||||||
|
// Returns { name, description, details } or null.
|
||||||
|
function decodeCalldata(data, toAddress, chainId, fromAddress) {
|
||||||
|
if (!data || data === "0x" || data.length < 10) return null;
|
||||||
|
|
||||||
|
// Where a token's name and scale are looked for, for every decoder below:
|
||||||
|
// what the wallet knows of the tokens on the network with `chainId`, which
|
||||||
|
// need not be the active one. The ERC-20 amount line and the swap's Amount
|
||||||
|
// and Min. received lines resolve the scale the same way, and refuse to
|
||||||
|
// format the same way when it is nowhere.
|
||||||
|
const decimalsSources = tokenSourcesByChainId(chainId);
|
||||||
|
|
||||||
|
// Try ERC-20 (approve / transfer)
|
||||||
|
try {
|
||||||
|
const parsed = erc20Iface.parseTransaction({ data });
|
||||||
|
if (parsed) {
|
||||||
|
const tokenSymbol = resolveTokenSymbol(toAddress, decimalsSources);
|
||||||
|
// null when no source knows this token's scale. It is not
|
||||||
|
// defaulted to 18: an amount formatted with a guessed scale is
|
||||||
|
// the wrong number, and for a token with fewer decimals than the
|
||||||
|
// guess it is the wrong number in the direction that reads as
|
||||||
|
// zero. See tokenAmountText().
|
||||||
|
const tokenDecimals = resolveTokenDecimals(
|
||||||
|
toAddress,
|
||||||
|
decimalsSources,
|
||||||
|
);
|
||||||
|
const contractLabel = tokenSymbol
|
||||||
|
? tokenSymbol + " (" + toAddress + ")"
|
||||||
|
: toAddress;
|
||||||
|
|
||||||
|
if (parsed.name === "approve") {
|
||||||
|
const spender = parsed.args[0];
|
||||||
|
const rawAmount = parsed.args[1];
|
||||||
|
const maxUint = BigInt(
|
||||||
|
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
);
|
||||||
|
const isUnlimited = rawAmount === maxUint;
|
||||||
|
// An unbounded allowance needs no scale to describe, so it is
|
||||||
|
// still named rather than refused.
|
||||||
|
const amount = isUnlimited
|
||||||
|
? { raw: "Unlimited", display: "Unlimited" }
|
||||||
|
: tokenAmountText(rawAmount, tokenDecimals, tokenSymbol);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "Token Approval",
|
||||||
|
description: tokenSymbol
|
||||||
|
? "Approve spending of your " + tokenSymbol
|
||||||
|
: "Approve spending of an ERC-20 token",
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
label: "Token",
|
||||||
|
value: contractLabel,
|
||||||
|
address: toAddress,
|
||||||
|
isToken: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Spender",
|
||||||
|
value: spender,
|
||||||
|
address: spender,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Amount",
|
||||||
|
value: amount.display,
|
||||||
|
rawValue: amount.raw,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.name === "transfer") {
|
||||||
|
const to = parsed.args[0];
|
||||||
|
const rawAmount = parsed.args[1];
|
||||||
|
const amount = tokenAmountText(
|
||||||
|
rawAmount,
|
||||||
|
tokenDecimals,
|
||||||
|
tokenSymbol,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "Token Transfer",
|
||||||
|
description: tokenSymbol
|
||||||
|
? "Transfer " + tokenSymbol
|
||||||
|
: "Transfer ERC-20 token",
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
label: "Token",
|
||||||
|
value: contractLabel,
|
||||||
|
address: toAddress,
|
||||||
|
isToken: true,
|
||||||
|
},
|
||||||
|
{ label: "Recipient", value: to, address: to },
|
||||||
|
{
|
||||||
|
label: "Amount",
|
||||||
|
value: amount.display,
|
||||||
|
rawValue: amount.raw,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not ERC-20 — fall through
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Uniswap Universal Router
|
||||||
|
const routerResult = uniswap.decode(
|
||||||
|
data,
|
||||||
|
toAddress,
|
||||||
|
chainId,
|
||||||
|
decimalsSources,
|
||||||
|
fromAddress,
|
||||||
|
);
|
||||||
|
if (routerResult) return routerResult;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function showPhishingWarning(elementId, isPhishing) {
|
function showPhishingWarning(elementId, isPhishing) {
|
||||||
const el = $(elementId);
|
const el = $(elementId);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -1167,6 +1298,7 @@ function init(_ctx) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
show,
|
show,
|
||||||
|
decodeCalldata,
|
||||||
formatTypedDataHtml,
|
formatTypedDataHtml,
|
||||||
typedDataRefusal,
|
typedDataRefusal,
|
||||||
};
|
};
|
||||||
@@ -43,12 +43,9 @@ const {
|
|||||||
transferAmountUnits,
|
transferAmountUnits,
|
||||||
} = require("../../shared/transferAmount");
|
} = require("../../shared/transferAmount");
|
||||||
const { assertWithinCeilings } = require("../../shared/approvalVerify");
|
const { assertWithinCeilings } = require("../../shared/approvalVerify");
|
||||||
// The amount and the fee reserve keep the nonzero floor, as the approval
|
// The amount, the balance line and the fee reserve go through it, as the
|
||||||
// screen's amounts do. The balance line is cut as the balance lists cut it.
|
// approval screen's amounts do.
|
||||||
const {
|
const { truncateAmountNeverZero } = require("../../shared/amountDisplay");
|
||||||
truncateAmount,
|
|
||||||
truncateAmountNeverZero,
|
|
||||||
} = require("../../shared/amountDisplay");
|
|
||||||
const {
|
const {
|
||||||
CODES,
|
CODES,
|
||||||
FEE_PENDING,
|
FEE_PENDING,
|
||||||
@@ -163,7 +160,7 @@ function renderBalance(txInfo) {
|
|||||||
const price = getPrice(txInfo.token, txInfo.chainId);
|
const price = getPrice(txInfo.token, txInfo.chainId);
|
||||||
const balUsd = price ? parseFloat(bal) * price : null;
|
const balUsd = price ? parseFloat(bal) * price : null;
|
||||||
$("confirm-balance").textContent = valueWithUsd(
|
$("confirm-balance").textContent = valueWithUsd(
|
||||||
truncateAmount(bal) + " " + symbol,
|
truncateAmountNeverZero(bal) + " " + symbol,
|
||||||
balUsd,
|
balUsd,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ function recoveryPathText(wallet) {
|
|||||||
// "Holds" means ETH or any ERC-20 the wallet knows about — an address with no
|
// "Holds" means ETH or any ERC-20 the wallet knows about — an address with no
|
||||||
// ETH and a five-figure stablecoin position must not get the blank line on
|
// ETH and a five-figure stablecoin position must not get the blank line on
|
||||||
// the one screen whose job is to warn. The sentence names no figure of its
|
// the one screen whose job is to warn. The sentence names no figure of its
|
||||||
// own: the rendered lines cut to four decimals, so a sentence built from their
|
// own: the rendered lines round to four decimals, so a sentence built from a
|
||||||
// figure would report "0.0000 ETH" for an address holding real money.
|
// rounded number would report "0.0000 ETH" for an address holding real money.
|
||||||
// The lines below it carry the amounts, in the same format as Home and
|
// The lines below it carry the amounts, in the same format as Home and
|
||||||
// AddressDetail, followed by the USD total when there is one to give — no
|
// AddressDetail, followed by the USD total when there is one to give — no
|
||||||
// total line at all on testnet, before the first price fetch or while no
|
// total line at all on testnet, before the first price fetch or while no
|
||||||
|
|||||||
+41
-22
@@ -18,7 +18,6 @@
|
|||||||
const { formatEther } = require("ethers");
|
const { formatEther } = require("ethers");
|
||||||
const makeBlockie = require("ethereum-blockies-base64");
|
const makeBlockie = require("ethereum-blockies-base64");
|
||||||
const {
|
const {
|
||||||
truncateAmount,
|
|
||||||
truncateAmountNeverZero,
|
truncateAmountNeverZero,
|
||||||
isBelowOneMillionth,
|
isBelowOneMillionth,
|
||||||
} = require("../../shared/amountDisplay");
|
} = require("../../shared/amountDisplay");
|
||||||
@@ -32,12 +31,7 @@ const { escapeHtml } = require("../../shared/html");
|
|||||||
const { isDebug } = require("../../shared/log");
|
const { isDebug } = require("../../shared/log");
|
||||||
const { networkByChainId } = require("../../shared/networks");
|
const { networkByChainId } = require("../../shared/networks");
|
||||||
const { formatUsd, getPrice } = require("../../shared/prices");
|
const { formatUsd, getPrice } = require("../../shared/prices");
|
||||||
const {
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
state,
|
|
||||||
saveState,
|
|
||||||
currentNetwork,
|
|
||||||
tokenSourcesByChainId,
|
|
||||||
} = require("../../shared/state");
|
|
||||||
const { displaySymbol } = require("../../shared/symbolDisplay");
|
const { displaySymbol } = require("../../shared/symbolDisplay");
|
||||||
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
||||||
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
||||||
@@ -311,6 +305,32 @@ function nativeCurrency() {
|
|||||||
return currentNetwork().nativeCurrency;
|
return currentNetwork().nativeCurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What the wallet knows of the tokens on the network with this chain id, or
|
||||||
|
// on the active network when no chain id is given, as the `sources` that
|
||||||
|
// resolveTokenSymbol() and resolveTokenDecimals() take. A contract address
|
||||||
|
// names a token on one network only, and every token balance held is the
|
||||||
|
// active network's, so another network gets the tokens tracked on it
|
||||||
|
// (state.networkTrackedTokens, see applyChainSwitchFields()) and no balances.
|
||||||
|
//
|
||||||
|
// The wallets' balances hold what Blockscout reported for each token. So the
|
||||||
|
// wallets are returned only while "Use Blockscout" is on.
|
||||||
|
function tokenSourcesByChainId(chainId) {
|
||||||
|
const chainIdOrActive = chainId || currentNetwork().chainId;
|
||||||
|
const net = networkByChainId(chainIdOrActive);
|
||||||
|
if (net && net.id === state.networkId) {
|
||||||
|
return {
|
||||||
|
chainId: chainIdOrActive,
|
||||||
|
trackedTokens: state.trackedTokens,
|
||||||
|
wallets: state.useBlockscout ? state.wallets : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
chainId: chainIdOrActive,
|
||||||
|
trackedTokens: (net && state.networkTrackedTokens[net.id]) || [],
|
||||||
|
wallets: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// The line shown under a token while "Use Blockscout" is off. The token gets
|
// The line shown under a token while "Use Blockscout" is off. The token gets
|
||||||
// the line only when the bundled list and the tracked tokens lack its name or
|
// the line only when the bundled list and the tracked tokens lack its name or
|
||||||
// decimals. Otherwise this returns "". The token is a contract address on the
|
// decimals. Otherwise this returns "". The token is a contract address on the
|
||||||
@@ -374,15 +394,13 @@ function formatFee(wei, symbol, chainId) {
|
|||||||
// the row that issue #307 was reported against: every screen that lists a
|
// the row that issue #307 was reported against: every screen that lists a
|
||||||
// holding renders through here.
|
// holding renders through here.
|
||||||
//
|
//
|
||||||
// `balance` is the stored balance, a decimal string. It is null for a holding
|
// `amount` is null for a holding whose scale nothing knows
|
||||||
// whose scale nothing knows
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/349), and for a balance no
|
// (https://git.eeqj.de/sneak/AutistMask/issues/349), and for a balance no
|
||||||
// refresh has read. There is no quantity to print for it and no fiat value to
|
// refresh has read. There is no quantity to print for it and no fiat value to
|
||||||
// derive from one. A holding whose amount is unknown is shown as
|
// derive from one, and printing 0.0000 for a real holding is the failure this
|
||||||
// `quantity unknown` rather than as a zero.
|
// whole rule exists to prevent, so the row says so instead.
|
||||||
function balanceLine(symbol, balance, price, tokenId) {
|
function balanceLine(symbol, amount, price, tokenId) {
|
||||||
const amount = unknownableAmount(balance);
|
const qty = amount === null ? "quantity unknown" : amount.toFixed(4);
|
||||||
const qty = amount === null ? "quantity unknown" : truncateAmount(balance);
|
|
||||||
const usd =
|
const usd =
|
||||||
price && amount !== null
|
price && amount !== null
|
||||||
? escapeHtml(formatUsd(amount * price)) || " "
|
? escapeHtml(formatUsd(amount * price)) || " "
|
||||||
@@ -397,7 +415,7 @@ function balanceLine(symbol, balance, price, tokenId) {
|
|||||||
`<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 w-[42ch] max-w-full">` +
|
||||||
`<span>${escapeHtml(displaySymbol(symbol))}</span>` +
|
`<span>${escapeHtml(displaySymbol(symbol))}</span>` +
|
||||||
`<span>${escapeHtml(qty)}</span>` +
|
`<span>${qty}</span>` +
|
||||||
`</span>` +
|
`</span>` +
|
||||||
`<span class="text-right text-muted flex-1">${usd}</span>` +
|
`<span class="text-right text-muted flex-1">${usd}</span>` +
|
||||||
`</div>`
|
`</div>`
|
||||||
@@ -423,7 +441,7 @@ function markTrackedTokensNotFetched() {
|
|||||||
function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
||||||
let html = balanceLine(
|
let html = balanceLine(
|
||||||
nativeCurrency(),
|
nativeCurrency(),
|
||||||
addr.balance,
|
unknownableAmount(addr.balance),
|
||||||
getPrice("ETH"),
|
getPrice("ETH"),
|
||||||
"ETH",
|
"ETH",
|
||||||
);
|
);
|
||||||
@@ -441,7 +459,7 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
|||||||
if (bal === 0 && !showZero) continue;
|
if (bal === 0 && !showZero) continue;
|
||||||
html += balanceLine(
|
html += balanceLine(
|
||||||
t.symbol,
|
t.symbol,
|
||||||
t.balance,
|
bal,
|
||||||
getPrice(t.address),
|
getPrice(t.address),
|
||||||
t.address.toLowerCase(),
|
t.address.toLowerCase(),
|
||||||
);
|
);
|
||||||
@@ -459,7 +477,7 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
|||||||
}
|
}
|
||||||
html += balanceLine(
|
html += balanceLine(
|
||||||
t.symbol,
|
t.symbol,
|
||||||
t.balanceNotFetched ? null : "0",
|
t.balanceNotFetched ? null : 0,
|
||||||
getPrice(t.address),
|
getPrice(t.address),
|
||||||
t.address.toLowerCase(),
|
t.address.toLowerCase(),
|
||||||
);
|
);
|
||||||
@@ -470,10 +488,10 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
|||||||
|
|
||||||
// Whether an address holds anything at all: ETH or any ERC-20 the wallet
|
// Whether an address holds anything at all: ETH or any ERC-20 the wallet
|
||||||
// knows about, except a token holding below 0.000001, which the balance list
|
// knows about, except a token holding below 0.000001, which the balance list
|
||||||
// under the remove-address warning leaves out too. Deliberately exact — the
|
// under the remove-address warning leaves out too. Deliberately unrounded —
|
||||||
// rendered lines cut to four decimals, so a dust balance displays as 0.0000
|
// the rendered lines round to four decimals, so a dust balance displays as
|
||||||
// while still being real money at a real address. Callers that warn about
|
// 0.0000 while still being real money at a real address. Callers that warn
|
||||||
// holdings must ask this, not the rendered figure.
|
// about holdings must ask this, not the rendered figure.
|
||||||
function addressHoldsFunds(addr) {
|
function addressHoldsFunds(addr) {
|
||||||
if (!addr) return false;
|
if (!addr) return false;
|
||||||
// A null or absent ETH balance reads as unknown, so the address may hold
|
// A null or absent ETH balance reads as unknown, so the address may hold
|
||||||
@@ -848,6 +866,7 @@ module.exports = {
|
|||||||
keyMaterialName,
|
keyMaterialName,
|
||||||
unknownableAmount,
|
unknownableAmount,
|
||||||
nativeCurrency,
|
nativeCurrency,
|
||||||
|
tokenSourcesByChainId,
|
||||||
blockscoutOffTokenNoteHtml,
|
blockscoutOffTokenNoteHtml,
|
||||||
tokenLabel,
|
tokenLabel,
|
||||||
formatFee,
|
formatFee,
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ const {
|
|||||||
resetSendValidation,
|
resetSendValidation,
|
||||||
} = require("./send");
|
} = require("./send");
|
||||||
const { deriveAddressFromXpub } = require("../../shared/wallet");
|
const { deriveAddressFromXpub } = require("../../shared/wallet");
|
||||||
const { truncateAmount } = require("../../shared/amountDisplay");
|
|
||||||
const { canRemoveAddress } = require("../../shared/walletDelete");
|
const { canRemoveAddress } = require("../../shared/walletDelete");
|
||||||
const {
|
const {
|
||||||
walletDefect,
|
walletDefect,
|
||||||
@@ -82,7 +81,7 @@ function renderTotalValue() {
|
|||||||
if (ethBal === null) {
|
if (ethBal === null) {
|
||||||
el.textContent = "unknown (" + nativeCurrency() + ")";
|
el.textContent = "unknown (" + nativeCurrency() + ")";
|
||||||
} else {
|
} else {
|
||||||
const ethStr = truncateAmount(addr.balance) + " " + nativeCurrency();
|
const ethStr = ethBal.toFixed(4) + " " + nativeCurrency();
|
||||||
const ethUsd = ethPrice
|
const ethUsd = ethPrice
|
||||||
? " (" + formatUsd(ethBal * ethPrice) + ")"
|
? " (" + formatUsd(ethBal * ethPrice) + ")"
|
||||||
: "";
|
: "";
|
||||||
|
|||||||
+10
-11
@@ -10,13 +10,9 @@ const {
|
|||||||
renderAddressHtml,
|
renderAddressHtml,
|
||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
goBack,
|
goBack,
|
||||||
} = require("./helpers");
|
|
||||||
const {
|
|
||||||
state,
|
|
||||||
currentAddress,
|
|
||||||
currentNetwork,
|
|
||||||
tokenSourcesByChainId,
|
tokenSourcesByChainId,
|
||||||
} = require("../../shared/state");
|
} = require("./helpers");
|
||||||
|
const { state, currentAddress, currentNetwork } = require("../../shared/state");
|
||||||
let ctx;
|
let ctx;
|
||||||
const { getProvider } = require("../../shared/balances");
|
const { getProvider } = require("../../shared/balances");
|
||||||
const { resolveTokenDecimals } = require("../../shared/approvalAmount");
|
const { resolveTokenDecimals } = require("../../shared/approvalAmount");
|
||||||
@@ -24,7 +20,7 @@ const { resolveSymbol } = require("../../shared/tokenList");
|
|||||||
const { isLowHolderCount } = require("../../shared/holders");
|
const { isLowHolderCount } = require("../../shared/holders");
|
||||||
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
||||||
const {
|
const {
|
||||||
truncateAmount,
|
truncateAmountNeverZero,
|
||||||
isBelowOneMillionth,
|
isBelowOneMillionth,
|
||||||
} = require("../../shared/amountDisplay");
|
} = require("../../shared/amountDisplay");
|
||||||
const {
|
const {
|
||||||
@@ -239,7 +235,7 @@ function updateSendBalance() {
|
|||||||
addr.balance == null
|
addr.balance == null
|
||||||
? "Current balance: unknown (" + nativeCurrency() + ")"
|
? "Current balance: unknown (" + nativeCurrency() + ")"
|
||||||
: "Current balance: " +
|
: "Current balance: " +
|
||||||
truncateAmount(addr.balance) +
|
truncateAmountNeverZero(addr.balance) +
|
||||||
" " +
|
" " +
|
||||||
nativeCurrency();
|
nativeCurrency();
|
||||||
} else {
|
} else {
|
||||||
@@ -258,15 +254,18 @@ function updateSendBalance() {
|
|||||||
$("send-balance").textContent =
|
$("send-balance").textContent =
|
||||||
bal == null
|
bal == null
|
||||||
? "Current balance: unknown (" + symbol + ")"
|
? "Current balance: unknown (" + symbol + ")"
|
||||||
: "Current balance: " + truncateAmount(bal) + " " + symbol;
|
: "Current balance: " +
|
||||||
|
truncateAmountNeverZero(bal) +
|
||||||
|
" " +
|
||||||
|
symbol;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill the amount field with the most the selected holding can send: a
|
// Fill the amount field with the most the selected holding can send: a
|
||||||
// token's whole balance (cut to 18 decimal places), or for ETH the exact
|
// token's whole balance (cut to 18 decimal places), or for ETH the exact
|
||||||
// balance minus the fee reserve the confirmation screen checks against, never
|
// balance minus the fee reserve the confirmation screen checks against, never
|
||||||
// the balance the screen shows, which is cut to four decimals. Where there is
|
// the rounded balance the screen shows. Where there is nothing to fill in, a
|
||||||
// nothing to fill in, a flash message says why.
|
// flash message says why.
|
||||||
async function fillMaxAmount() {
|
async function fillMaxAmount() {
|
||||||
const addr = currentAddress();
|
const addr = currentAddress();
|
||||||
if (!addr) return;
|
if (!addr) return;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const { nativeCurrencyByChainId } = require("../../shared/networks");
|
|||||||
const { formatEther, formatUnits } = require("ethers");
|
const { formatEther, formatUnits } = require("ethers");
|
||||||
const { log, debugFetch } = require("../../shared/log");
|
const { log, debugFetch } = require("../../shared/log");
|
||||||
const { storedSettingIsOn } = require("../../shared/storedSettings");
|
const { storedSettingIsOn } = require("../../shared/storedSettings");
|
||||||
const { decodeCalldata } = require("../../shared/calldataDecoder");
|
const { decodeCalldata } = require("./approval");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine a human-readable transaction type string from tx fields.
|
* Determine a human-readable transaction type string from tx fields.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// the one exception to it, in one place. Three call sites had grown their own
|
// the one exception to it, in one place. Three call sites had grown their own
|
||||||
// copy of the truncation — the history and balance lists
|
// copy of the truncation — the history and balance lists
|
||||||
// (`src/shared/transactions.js`), the approval screen's ERC-20 amount line
|
// (`src/shared/transactions.js`), the approval screen's ERC-20 amount line
|
||||||
// (`src/shared/calldataDecoder.js`) and its Uniswap swap detail lines
|
// (`src/popup/views/approval.js`) and its Uniswap swap detail lines
|
||||||
// (`src/shared/uniswap.js`) — and a fix applied to one of them left the other
|
// (`src/shared/uniswap.js`) — and a fix applied to one of them left the other
|
||||||
// two showing a different number for the same value.
|
// two showing a different number for the same value.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
// Decode a transaction's calldata into the summary that TxApproval and
|
|
||||||
// TransactionDetail show: ERC-20 `approve` and `transfer` calls here, and
|
|
||||||
// Uniswap Universal Router calls through uniswap.js. The summary is data; the
|
|
||||||
// views turn its addresses into HTML.
|
|
||||||
|
|
||||||
const { formatUnits, Interface } = require("ethers");
|
|
||||||
const { ERC20_ABI } = require("./constants");
|
|
||||||
const { tokenSourcesByChainId } = require("./state");
|
|
||||||
const {
|
|
||||||
resolveTokenDecimals,
|
|
||||||
resolveTokenSymbol,
|
|
||||||
unknownDecimalsAmount,
|
|
||||||
} = require("./approvalAmount");
|
|
||||||
// Four decimals, with the nonzero floor the approval screens hold.
|
|
||||||
const { truncateAmountNeverZero } = require("./amountDisplay");
|
|
||||||
const uniswap = require("./uniswap");
|
|
||||||
|
|
||||||
const erc20Iface = new Interface(ERC20_ABI);
|
|
||||||
|
|
||||||
// The amount line for a decoded ERC-20 call. With a known scale it is the
|
|
||||||
// token quantity; with `decimals` null it is the base-unit integer with the
|
|
||||||
// unknown scale stated, because formatting it with an assumed scale is what
|
|
||||||
// showed a 5,000-token transfer as `0.0000`. `raw` is what the status screens
|
|
||||||
// carry, `display` is what the approval screen shows.
|
|
||||||
function tokenAmountText(rawAmount, decimals, symbol) {
|
|
||||||
if (decimals === null) {
|
|
||||||
const unknown = unknownDecimalsAmount(rawAmount);
|
|
||||||
return { raw: unknown, display: unknown };
|
|
||||||
}
|
|
||||||
const formatted = truncateAmountNeverZero(formatUnits(rawAmount, decimals));
|
|
||||||
return {
|
|
||||||
raw: formatted,
|
|
||||||
display: formatted + (symbol ? " " + symbol : ""),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to decode calldata sent from `fromAddress` to `toAddress` on the network
|
|
||||||
// with chain id `chainId`, using known ABIs.
|
|
||||||
// Returns { name, description, details } or null.
|
|
||||||
function decodeCalldata(data, toAddress, chainId, fromAddress) {
|
|
||||||
if (!data || data === "0x" || data.length < 10) return null;
|
|
||||||
|
|
||||||
// Where a token's name and scale are looked for, for every decoder below:
|
|
||||||
// what the wallet knows of the tokens on the network with `chainId`, which
|
|
||||||
// need not be the active one. The ERC-20 amount line and the swap's Amount
|
|
||||||
// and Min. received lines resolve the scale the same way, and refuse to
|
|
||||||
// format the same way when it is nowhere.
|
|
||||||
const decimalsSources = tokenSourcesByChainId(chainId);
|
|
||||||
|
|
||||||
// Try ERC-20 (approve / transfer)
|
|
||||||
try {
|
|
||||||
const parsed = erc20Iface.parseTransaction({ data });
|
|
||||||
if (parsed) {
|
|
||||||
const tokenSymbol = resolveTokenSymbol(toAddress, decimalsSources);
|
|
||||||
// null when no source knows this token's scale. It is not
|
|
||||||
// defaulted to 18: an amount formatted with a guessed scale is
|
|
||||||
// the wrong number, and for a token with fewer decimals than the
|
|
||||||
// guess it is the wrong number in the direction that reads as
|
|
||||||
// zero. See tokenAmountText().
|
|
||||||
const tokenDecimals = resolveTokenDecimals(
|
|
||||||
toAddress,
|
|
||||||
decimalsSources,
|
|
||||||
);
|
|
||||||
const contractLabel = tokenSymbol
|
|
||||||
? tokenSymbol + " (" + toAddress + ")"
|
|
||||||
: toAddress;
|
|
||||||
|
|
||||||
if (parsed.name === "approve") {
|
|
||||||
const spender = parsed.args[0];
|
|
||||||
const rawAmount = parsed.args[1];
|
|
||||||
const maxUint = BigInt(
|
|
||||||
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
|
||||||
);
|
|
||||||
const isUnlimited = rawAmount === maxUint;
|
|
||||||
// An unbounded allowance needs no scale to describe, so it is
|
|
||||||
// still named rather than refused.
|
|
||||||
const amount = isUnlimited
|
|
||||||
? { raw: "Unlimited", display: "Unlimited" }
|
|
||||||
: tokenAmountText(rawAmount, tokenDecimals, tokenSymbol);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "Token Approval",
|
|
||||||
description: tokenSymbol
|
|
||||||
? "Approve spending of your " + tokenSymbol
|
|
||||||
: "Approve spending of an ERC-20 token",
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
label: "Token",
|
|
||||||
value: contractLabel,
|
|
||||||
address: toAddress,
|
|
||||||
isToken: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Spender",
|
|
||||||
value: spender,
|
|
||||||
address: spender,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Amount",
|
|
||||||
value: amount.display,
|
|
||||||
rawValue: amount.raw,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsed.name === "transfer") {
|
|
||||||
const to = parsed.args[0];
|
|
||||||
const rawAmount = parsed.args[1];
|
|
||||||
const amount = tokenAmountText(
|
|
||||||
rawAmount,
|
|
||||||
tokenDecimals,
|
|
||||||
tokenSymbol,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: "Token Transfer",
|
|
||||||
description: tokenSymbol
|
|
||||||
? "Transfer " + tokenSymbol
|
|
||||||
: "Transfer ERC-20 token",
|
|
||||||
details: [
|
|
||||||
{
|
|
||||||
label: "Token",
|
|
||||||
value: contractLabel,
|
|
||||||
address: toAddress,
|
|
||||||
isToken: true,
|
|
||||||
},
|
|
||||||
{ label: "Recipient", value: to, address: to },
|
|
||||||
{
|
|
||||||
label: "Amount",
|
|
||||||
value: amount.display,
|
|
||||||
rawValue: amount.raw,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Not ERC-20 — fall through
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try Uniswap Universal Router
|
|
||||||
const routerResult = uniswap.decode(
|
|
||||||
data,
|
|
||||||
toAddress,
|
|
||||||
chainId,
|
|
||||||
decimalsSources,
|
|
||||||
fromAddress,
|
|
||||||
);
|
|
||||||
if (routerResult) return routerResult;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { decodeCalldata, tokenAmountText };
|
|
||||||
+1
-28
@@ -22,7 +22,7 @@
|
|||||||
// same thing in the editor is fast feedback in front of the build, not the
|
// same thing in the editor is fast feedback in front of the build, not the
|
||||||
// guarantee.
|
// guarantee.
|
||||||
|
|
||||||
const { networkById, networkByChainId } = require("./networks");
|
const { networkById } = require("./networks");
|
||||||
const { copyChainSwitchFields } = require("./chainSwitchFields");
|
const { copyChainSwitchFields } = require("./chainSwitchFields");
|
||||||
const {
|
const {
|
||||||
DEFAULT_STATE,
|
DEFAULT_STATE,
|
||||||
@@ -125,32 +125,6 @@ function currentNetwork() {
|
|||||||
return networkById(state.networkId);
|
return networkById(state.networkId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// What the wallet knows of the tokens on the network with this chain id, or
|
|
||||||
// on the active network when no chain id is given, as the `sources` that
|
|
||||||
// resolveTokenSymbol() and resolveTokenDecimals() take. A contract address
|
|
||||||
// names a token on one network only, and every token balance held is the
|
|
||||||
// active network's, so another network gets the tokens tracked on it
|
|
||||||
// (state.networkTrackedTokens, see applyChainSwitchFields()) and no balances.
|
|
||||||
//
|
|
||||||
// The wallets' balances hold what Blockscout reported for each token. So the
|
|
||||||
// wallets are returned only while "Use Blockscout" is on.
|
|
||||||
function tokenSourcesByChainId(chainId) {
|
|
||||||
const chainIdOrActive = chainId || currentNetwork().chainId;
|
|
||||||
const net = networkByChainId(chainIdOrActive);
|
|
||||||
if (net && net.id === state.networkId) {
|
|
||||||
return {
|
|
||||||
chainId: chainIdOrActive,
|
|
||||||
trackedTokens: state.trackedTokens,
|
|
||||||
wallets: state.useBlockscout ? state.wallets : [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
chainId: chainIdOrActive,
|
|
||||||
trackedTokens: (net && state.networkTrackedTokens[net.id]) || [],
|
|
||||||
wallets: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// `baseline` holds the persisted fields as this page held them when its last
|
// `baseline` holds the persisted fields as this page held them when its last
|
||||||
// loadState() finished, or when its last successful saveState() began.
|
// loadState() finished, or when its last successful saveState() began.
|
||||||
// takeStoredNetwork(), takeStoredTrackedTokens() and
|
// takeStoredNetwork(), takeStoredTrackedTokens() and
|
||||||
@@ -846,6 +820,5 @@ module.exports = {
|
|||||||
loadState,
|
loadState,
|
||||||
currentAddress,
|
currentAddress,
|
||||||
currentNetwork,
|
currentNetwork,
|
||||||
tokenSourcesByChainId,
|
|
||||||
StateNotLoadedError,
|
StateNotLoadedError,
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Wallet operations: mnemonic generation, HD derivation, signing.
|
// Wallet operations: mnemonic generation, HD derivation, signing.
|
||||||
// All crypto delegated to ethers.js.
|
// All crypto delegated to ethers.js.
|
||||||
|
|
||||||
const { Mnemonic, HDNodeWallet, Wallet, randomBytes } = require("ethers");
|
const { Mnemonic, HDNodeWallet, Wallet } = require("ethers");
|
||||||
const { DEBUG, DEBUG_MNEMONIC, BIP44_ETH_PATH } = require("./constants");
|
const { DEBUG, DEBUG_MNEMONIC, BIP44_ETH_PATH } = require("./constants");
|
||||||
|
|
||||||
function generateMnemonic() {
|
function generateMnemonic() {
|
||||||
@@ -10,7 +10,10 @@ function generateMnemonic() {
|
|||||||
// settings toggle drives, which would let a user of a release build turn
|
// settings toggle drives, which would let a user of a release build turn
|
||||||
// the hardcoded, publicly known test phrase back on for real wallets.
|
// the hardcoded, publicly known test phrase back on for real wallets.
|
||||||
if (DEBUG) return DEBUG_MNEMONIC;
|
if (DEBUG) return DEBUG_MNEMONIC;
|
||||||
return Mnemonic.fromEntropy(randomBytes(16)).phrase;
|
const m = Mnemonic.fromEntropy(
|
||||||
|
globalThis.crypto.getRandomValues(new Uint8Array(16)),
|
||||||
|
);
|
||||||
|
return m.phrase;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every extended key (xprv or xpub) entering the app goes through this.
|
// Every extended key (xprv or xpub) entering the app goes through this.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const {
|
|||||||
resolveTokenDecimals,
|
resolveTokenDecimals,
|
||||||
unknownDecimalsAmount,
|
unknownDecimalsAmount,
|
||||||
} = require("../src/shared/approvalAmount");
|
} = require("../src/shared/approvalAmount");
|
||||||
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
const { decodeCalldata } = require("../src/popup/views/approval");
|
||||||
|
|
||||||
const iface = new Interface(ERC20_ABI);
|
const iface = new Interface(ERC20_ABI);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
// asserted on `rawValue` alongside the displayed line.
|
// asserted on `rawValue` alongside the displayed line.
|
||||||
//
|
//
|
||||||
// Both amount paths of that screen are covered: the ERC-20 line decoded by
|
// Both amount paths of that screen are covered: the ERC-20 line decoded by
|
||||||
// `src/shared/calldataDecoder.js`, and the swap's `Amount` and `Min. received`
|
// `src/popup/views/approval.js`, and the swap's `Amount` and `Min. received`
|
||||||
// lines decoded by `src/shared/uniswap.js`.
|
// lines decoded by `src/shared/uniswap.js`.
|
||||||
|
|
||||||
globalThis.chrome = {
|
globalThis.chrome = {
|
||||||
@@ -23,8 +23,12 @@ globalThis.chrome = {
|
|||||||
const { AbiCoder, Interface } = require("ethers");
|
const { AbiCoder, Interface } = require("ethers");
|
||||||
const { ERC20_ABI } = require("../src/shared/constants");
|
const { ERC20_ABI } = require("../src/shared/constants");
|
||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
const { decodeCalldata } = require("../src/popup/views/approval");
|
||||||
const uniswap = require("../src/shared/uniswap");
|
const uniswap = require("../src/shared/uniswap");
|
||||||
|
const {
|
||||||
|
truncateAmount,
|
||||||
|
truncateAmountNeverZero,
|
||||||
|
} = require("../src/shared/amountDisplay");
|
||||||
|
|
||||||
const iface = new Interface(ERC20_ABI);
|
const iface = new Interface(ERC20_ABI);
|
||||||
|
|
||||||
@@ -179,4 +183,15 @@ describe("the four-decimal rule is otherwise unchanged", () => {
|
|||||||
test("a genuine zero still renders as zero", () => {
|
test("a genuine zero still renders as zero", () => {
|
||||||
expect(amount(transferData(0n), DAI).rawValue).toBe("0.0000");
|
expect(amount(transferData(0n), DAI).rawValue).toBe("0.0000");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The three truncators now share one module. The floor is a policy of the
|
||||||
|
// approval and confirmation screens only: the history and balance lists
|
||||||
|
// keep plain truncation, because the transaction detail view is the
|
||||||
|
// authoritative record and already shows exact precision.
|
||||||
|
test("the list rule stays unfloored", () => {
|
||||||
|
expect(truncateAmount("0.000000000000000001")).toBe("0.0000");
|
||||||
|
expect(truncateAmountNeverZero("0.000000000000000001")).toBe(
|
||||||
|
"0.000000000000000001",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -41,13 +41,13 @@ describe("balanceLine", () => {
|
|||||||
// this payload before its id attribute, so an assertion about the
|
// this payload before its id attribute, so an assertion about the
|
||||||
// rest of the payload would pass on the cap and say nothing about
|
// rest of the payload would pass on the cap and say nothing about
|
||||||
// the escape.
|
// the escape.
|
||||||
const html = balanceLine(HOSTILE_SYMBOL, "1", null, null);
|
const html = balanceLine(HOSTILE_SYMBOL, 1, null, null);
|
||||||
expect(html).not.toContain("<iframe");
|
expect(html).not.toContain("<iframe");
|
||||||
expect(html).toContain("<iframe");
|
expect(html).toContain("<iframe");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("caps the symbol before rendering it", () => {
|
test("caps the symbol before rendering it", () => {
|
||||||
const html = balanceLine("A".repeat(4096), "1", null, null);
|
const html = balanceLine("A".repeat(4096), 1, null, null);
|
||||||
expect(html).toContain("A".repeat(MAX_SYMBOL_LENGTH - 1) + "…");
|
expect(html).toContain("A".repeat(MAX_SYMBOL_LENGTH - 1) + "…");
|
||||||
expect(html).not.toContain("A".repeat(MAX_SYMBOL_LENGTH + 1));
|
expect(html).not.toContain("A".repeat(MAX_SYMBOL_LENGTH + 1));
|
||||||
});
|
});
|
||||||
@@ -55,13 +55,13 @@ describe("balanceLine", () => {
|
|||||||
// The token id lands inside data-token="...", so a quote in it is a
|
// The token id lands inside data-token="...", so a quote in it is a
|
||||||
// way out of the attribute and into a new one.
|
// way out of the attribute and into a new one.
|
||||||
test("keeps a quote-bearing token id inside its attribute", () => {
|
test("keeps a quote-bearing token id inside its attribute", () => {
|
||||||
const html = balanceLine("TKN", "1", null, '" onclick="alert(1)');
|
const html = balanceLine("TKN", 1, null, '" onclick="alert(1)');
|
||||||
expect(html).not.toContain('onclick="');
|
expect(html).not.toContain('onclick="');
|
||||||
expect(html).toContain('data-token="" onclick="alert(1)"');
|
expect(html).toContain('data-token="" onclick="alert(1)"');
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders an ordinary holding unchanged", () => {
|
test("renders an ordinary holding unchanged", () => {
|
||||||
const html = balanceLine("USDC", "1.5", null, "0xabc");
|
const html = balanceLine("USDC", 1.5, null, "0xabc");
|
||||||
expect(html).toContain("<span>USDC</span>");
|
expect(html).toContain("<span>USDC</span>");
|
||||||
expect(html).toContain("<span>1.5000</span>");
|
expect(html).toContain("<span>1.5000</span>");
|
||||||
expect(html).toContain('data-token="0xabc"');
|
expect(html).toContain('data-token="0xabc"');
|
||||||
@@ -69,7 +69,7 @@ describe("balanceLine", () => {
|
|||||||
|
|
||||||
// formatUsd() writes a value under a cent as "< $0.01".
|
// formatUsd() writes a value under a cent as "< $0.01".
|
||||||
test("escapes the USD value along with the symbol", () => {
|
test("escapes the USD value along with the symbol", () => {
|
||||||
const html = balanceLine("USDC", "0.001", 1, null);
|
const html = balanceLine("USDC", 0.001, 1, null);
|
||||||
expect(html).toContain("< $0.01");
|
expect(html).toContain("< $0.01");
|
||||||
expect(html).not.toContain("< $0.01");
|
expect(html).not.toContain("< $0.01");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
// The balance lists, Home's ETH line and Send's current balance show a balance
|
|
||||||
// as the history rows do (README.md, Amounts): cut to four decimal places,
|
|
||||||
// never rounded up (https://git.eeqj.de/sneak/AutistMask/issues/757).
|
|
||||||
//
|
|
||||||
// Driven through the real popup over a stored profile on Ethereum Mainnet.
|
|
||||||
|
|
||||||
const {
|
|
||||||
ADDRESS,
|
|
||||||
bootPopup,
|
|
||||||
cleanupPopup,
|
|
||||||
unversionedValidProfile,
|
|
||||||
} = require("./support/popupBoot");
|
|
||||||
const { balanceLine } = require("../src/popup/views/helpers");
|
|
||||||
|
|
||||||
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
cleanupPopup();
|
|
||||||
});
|
|
||||||
|
|
||||||
// The quantity cell of a balance row.
|
|
||||||
function quantity(row) {
|
|
||||||
return row.match(/<span>([^<]*)<\/span><\/span>/)[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// The popup on Home over one address holding `eth` ETH and `usdc` USDC.
|
|
||||||
function bootHolding(eth, usdc) {
|
|
||||||
const profile = unversionedValidProfile({
|
|
||||||
trackedTokens: [{ address: USDC, symbol: "USDC", decimals: 6 }],
|
|
||||||
});
|
|
||||||
profile.wallets[0].addresses[0] = {
|
|
||||||
address: ADDRESS,
|
|
||||||
balance: eth,
|
|
||||||
tokenBalances: [
|
|
||||||
{ address: USDC, symbol: "USDC", decimals: 6, balance: usdc },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
return bootPopup(profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The balance row for `token` in Home's wallet list.
|
|
||||||
function homeRow(page, token) {
|
|
||||||
return page
|
|
||||||
.node("wallet-list")
|
|
||||||
.innerHTML.split("<div ")
|
|
||||||
.find((row) => row.includes('data-token="' + token + '"'));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("balanceLine", () => {
|
|
||||||
test("cuts 0.99996 to 0.9999, never rounding it up to 1.0000", () => {
|
|
||||||
expect(quantity(balanceLine("ETH", "0.99996", null, "ETH"))).toBe(
|
|
||||||
"0.9999",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("cuts 0.00004 to 0.0000", () => {
|
|
||||||
expect(quantity(balanceLine("USDC", "0.00004", null, USDC))).toBe(
|
|
||||||
"0.0000",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Home", () => {
|
|
||||||
test("the ETH line and the balance list read 0.99996 ETH as 0.9999", async () => {
|
|
||||||
const page = await bootHolding("0.99996", "0.00004");
|
|
||||||
|
|
||||||
expect(page.visibleViews()).toEqual(["main"]);
|
|
||||||
expect(page.text("total-value")).toBe("0.9999 ETH");
|
|
||||||
expect(quantity(homeRow(page, "ETH"))).toBe("0.9999");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("the balance list reads 0.00004 USDC as 0.0000", async () => {
|
|
||||||
const page = await bootHolding("0.99996", "0.00004");
|
|
||||||
|
|
||||||
expect(quantity(homeRow(page, USDC.toLowerCase()))).toBe("0.0000");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Send", () => {
|
|
||||||
test("the current balance reads 0.00004 USDC as 0.0000", async () => {
|
|
||||||
const page = await bootHolding("0.99996", "0.00004");
|
|
||||||
await page.click("btn-main-send");
|
|
||||||
// USDC chosen in the dropdown, as the user does.
|
|
||||||
const dropdown = page.node("send-token");
|
|
||||||
dropdown.value = USDC;
|
|
||||||
for (const fn of dropdown.listeners.change) await fn();
|
|
||||||
|
|
||||||
expect(page.visibleViews()).toEqual(["send"]);
|
|
||||||
expect(page.text("send-balance")).toBe("Current balance: 0.0000 USDC");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
// the copy is the substance and is tested as such. Two things it must not
|
// the copy is the substance and is tested as such. Two things it must not
|
||||||
// get wrong: what it takes to get the address back — the app refuses both
|
// get wrong: what it takes to get the address back — the app refuses both
|
||||||
// obvious routes — and what counts as holding something, which is any
|
// obvious routes — and what counts as holding something, which is any
|
||||||
// ERC-20 as well as ETH, at any size, including a balance below the four
|
// ERC-20 as well as ETH, at any size, including a balance that rounds to
|
||||||
// decimals the balance lines render. The DOM behaviour
|
// zero at the four decimals the balance lines render. The DOM behaviour
|
||||||
// around them is driven against the real popup by tests/e2e/run.js.
|
// around them is driven against the real popup by tests/e2e/run.js.
|
||||||
|
|
||||||
// helpers.js pulls in state.js, which reads chrome.storage.local at load.
|
// helpers.js pulls in state.js, which reads chrome.storage.local at load.
|
||||||
@@ -165,9 +165,9 @@ describe("the balance warning on the removal confirmation", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// The sentence must not assert an amount, because any amount it could
|
// The sentence must not assert an amount, because any amount it could
|
||||||
// assert has been cut to four decimals: "This address holds 0.0000 ETH." is
|
// assert has been rounded: "This address holds 0.0000 ETH." is what the
|
||||||
// what the cut form produces for an address that holds real money.
|
// rounded form produces for an address that holds real money.
|
||||||
test("the warning sentence asserts no cut amount", () => {
|
test("the warning sentence asserts no rounded amount", () => {
|
||||||
for (const addr of [DUST, ETH_ONLY, TOKEN_ONLY]) {
|
for (const addr of [DUST, ETH_ONLY, TOKEN_ONLY]) {
|
||||||
expect(balanceWarningHtml(addr)).not.toMatch(
|
expect(balanceWarningHtml(addr)).not.toMatch(
|
||||||
/holds [\d.]+ (ETH|USDC)/,
|
/holds [\d.]+ (ETH|USDC)/,
|
||||||
|
|||||||
+9
-9
@@ -829,11 +829,12 @@ test("a common-token quick-pick fills in the contract address (#150)", async (en
|
|||||||
await visible(env.page, "#view-address");
|
await visible(env.page, "#view-address");
|
||||||
});
|
});
|
||||||
|
|
||||||
// The native amount as the transaction list writes it (cut to four decimals,
|
// The native amount as the transaction list writes it (four decimals) and
|
||||||
// quantity() below) and as the detail screen writes it (full precision). Both
|
// as the detail screen writes it (full precision). Both are rendered here
|
||||||
// are rendered here from the fixture rather than read off the screen, so the
|
// from the fixture rather than read off the screen, so the assertions
|
||||||
// assertions compare against the wei the stub served.
|
// compare against the wei the stub served.
|
||||||
const NATIVE_ROW_TEXT = quantity(STUB_NATIVE_VALUE_WEI) + " ETH";
|
const NATIVE_ROW_TEXT =
|
||||||
|
parseFloat(formatEther(STUB_NATIVE_VALUE_WEI)).toFixed(4) + " ETH";
|
||||||
const NATIVE_DETAIL_TEXT = formatEther(STUB_NATIVE_VALUE_WEI) + " ETH";
|
const NATIVE_DETAIL_TEXT = formatEther(STUB_NATIVE_VALUE_WEI) + " ETH";
|
||||||
|
|
||||||
test("the native ETH transaction detail still renders (#151)", async (env) => {
|
test("the native ETH transaction detail still renders (#151)", async (env) => {
|
||||||
@@ -2723,11 +2724,10 @@ async function goToConfirm(page, send) {
|
|||||||
await assertAddressesFit(page, "the confirmation screen");
|
await assertAddressesFit(page, "the confirmation screen");
|
||||||
}
|
}
|
||||||
|
|
||||||
// An amount in wei as the main view's balance lines and the transaction list
|
// A balance as the main view renders it: balanceLinesForAddress() writes
|
||||||
// write it: cut to four decimal places, never rounded, as feeEth() does.
|
// every quantity with four decimal places.
|
||||||
function quantity(wei) {
|
function quantity(wei) {
|
||||||
const [whole, frac = ""] = formatEther(wei).split(".");
|
return parseFloat(formatEther(wei)).toFixed(4);
|
||||||
return whole + "." + (frac + "0000").slice(0, 4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait on the main view until a changed balance fixture has been picked up.
|
// Wait on the main view until a changed balance fixture has been picked up.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const { fetchTokenBalances } = require("../src/shared/balances");
|
|||||||
const { debugFetch } = require("../src/shared/log");
|
const { debugFetch } = require("../src/shared/log");
|
||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
const { unknownDecimalsAmount } = require("../src/shared/approvalAmount");
|
const { unknownDecimalsAmount } = require("../src/shared/approvalAmount");
|
||||||
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
const { decodeCalldata } = require("../src/popup/views/approval");
|
||||||
const { TOKEN_BY_ADDRESS } = require("../src/shared/tokenList");
|
const { TOKEN_BY_ADDRESS } = require("../src/shared/tokenList");
|
||||||
|
|
||||||
const HOLDER = "0x" + "a".repeat(40);
|
const HOLDER = "0x" + "a".repeat(40);
|
||||||
|
|||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
// A test file that starts an interval and never clears it.
|
|
||||||
// tests/jestRunner.test.js runs script/lib/jest.js on this file alone. The
|
|
||||||
// name does not end in .test.js, so the normal run never picks it up.
|
|
||||||
|
|
||||||
test("starts an interval and never clears it", () => {
|
|
||||||
setInterval(() => {}, 1000);
|
|
||||||
});
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// script/lib/jest.js, which package.json's test scripts run, fails a run in
|
|
||||||
// which a test left an open handle, instead of hanging on it.
|
|
||||||
|
|
||||||
const { spawnSync } = require("child_process");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const root = path.join(__dirname, "..");
|
|
||||||
|
|
||||||
test("a test that leaves an interval running fails the run", () => {
|
|
||||||
// The run ends about one second after its last test. Without the check in
|
|
||||||
// script/lib/jest.js it would never end, and the timeout stops it.
|
|
||||||
const result = spawnSync(
|
|
||||||
process.execPath,
|
|
||||||
[
|
|
||||||
"script/lib/jest.js",
|
|
||||||
"--runInBand",
|
|
||||||
"--testMatch",
|
|
||||||
"<rootDir>/tests/fixtures/leavesIntervalRunning.js",
|
|
||||||
],
|
|
||||||
{ cwd: root, encoding: "utf8", timeout: 20000 },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.signal).toBeNull();
|
|
||||||
expect(result.status).toBe(1);
|
|
||||||
expect(result.stderr).toContain("1 passed");
|
|
||||||
expect(result.stderr).toContain(
|
|
||||||
"a test left a timer or another open handle running",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
+2
-13
@@ -1,5 +1,4 @@
|
|||||||
// The release archives: the zip container, the self-containment rule, and the
|
// The release archives: the zip container, and the self-containment rule.
|
||||||
// digest format in release/SHA256SUMS.
|
|
||||||
//
|
//
|
||||||
// script/package produces one archive per browser and script/lib/package.js
|
// script/package produces one archive per browser and script/lib/package.js
|
||||||
// decides what goes in it. The trap that rule exists for is real and specific:
|
// decides what goes in it. The trap that rule exists for is real and specific:
|
||||||
@@ -12,14 +11,12 @@
|
|||||||
// So the packager resolves every reference in the manifest and in every HTML
|
// So the packager resolves every reference in the manifest and in every HTML
|
||||||
// document, and fails on any that leaves the extension root. These are the
|
// document, and fails on any that leaves the extension root. These are the
|
||||||
// cases for that, plus the archive format itself — new code, and the thing the
|
// cases for that, plus the archive format itself — new code, and the thing the
|
||||||
// artifact is made of. One more case checks the digest format in
|
// artifact is made of.
|
||||||
// release/SHA256SUMS.
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
checkSelfContained,
|
checkSelfContained,
|
||||||
htmlReferences,
|
htmlReferences,
|
||||||
manifestReferences,
|
manifestReferences,
|
||||||
sha256,
|
|
||||||
} = require("../script/lib/package");
|
} = require("../script/lib/package");
|
||||||
const { readZip, writeZip } = require("../script/lib/zip");
|
const { readZip, writeZip } = require("../script/lib/zip");
|
||||||
|
|
||||||
@@ -145,14 +142,6 @@ describe("archive self-containment", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// release/SHA256SUMS is checked with `sha256sum -c`, which needs the digest
|
|
||||||
// as bare lowercase hex. The expected value is the FIPS 180-2 digest of "abc".
|
|
||||||
test("sha256 prints the digest as sha256sum does", () => {
|
|
||||||
expect(sha256(Buffer.from("abc"))).toBe(
|
|
||||||
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("the zip container", () => {
|
describe("the zip container", () => {
|
||||||
const files = [
|
const files = [
|
||||||
{ name: "manifest.json", data: Buffer.from('{"a":1}') },
|
{ name: "manifest.json", data: Buffer.from('{"a":1}') },
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const { Interface } = require("ethers");
|
|||||||
const { ERC20_ABI } = require("../src/shared/constants");
|
const { ERC20_ABI } = require("../src/shared/constants");
|
||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
const { resolveTokenSymbol } = require("../src/shared/approvalAmount");
|
const { resolveTokenSymbol } = require("../src/shared/approvalAmount");
|
||||||
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
const { decodeCalldata } = require("../src/popup/views/approval");
|
||||||
|
|
||||||
const iface = new Interface(ERC20_ABI);
|
const iface = new Interface(ERC20_ABI);
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,8 @@
|
|||||||
// balances and the fee were each cut to six decimal places, a rule of their
|
// balances and the fee were each cut to six decimal places, a rule of their
|
||||||
// own, and a token holding cut to zero was dropped, while the approval screen
|
// own, and a token holding cut to zero was dropped, while the approval screen
|
||||||
// showed the same fee through src/shared/amountDisplay.js with the nonzero
|
// showed the same fee through src/shared/amountDisplay.js with the nonzero
|
||||||
// floor. The balances are now stored exactly, and every nonzero token holding
|
// floor. The balances are now stored exactly, every nonzero token holding
|
||||||
// is kept. The fee lines show the fee through that helper with the floor. The
|
// kept, and the screens show them and the fee through that helper.
|
||||||
// balance lines cut a balance to four decimals, as the balance lists do, so a
|
|
||||||
// balance below 0.0001 reads 0.0000.
|
|
||||||
//
|
//
|
||||||
// Driven through the real refreshBalances(), Send screen, confirmation screen
|
// Driven through the real refreshBalances(), Send screen, confirmation screen
|
||||||
// and approval screen, with only the node, the explorer and the DOM stubbed: a
|
// and approval screen, with only the node, the explorer and the DOM stubbed: a
|
||||||
@@ -280,22 +278,21 @@ beforeEach(() => {
|
|||||||
mockNode.feeData = { maxFeePerGas: 1n, gasPrice: 1n };
|
mockNode.feeData = { maxFeePerGas: 1n, gasPrice: 1n };
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("an ETH balance below 0.0001 reads 0.0000", () => {
|
describe("an ETH balance below 0.000001 never renders as zero", () => {
|
||||||
test("on the Send screen", async () => {
|
test("on the Send screen", async () => {
|
||||||
await refreshWith(HALF_MICRO_ETH);
|
await refreshWith(HALF_MICRO_ETH);
|
||||||
state.selectedToken = "ETH";
|
state.selectedToken = "ETH";
|
||||||
send.updateSendBalance();
|
send.updateSendBalance();
|
||||||
expect(text("send-balance")).toBe("Current balance: 0.0000 ETH");
|
expect(text("send-balance")).toBe("Current balance: 0.0000005 ETH");
|
||||||
expect(state.wallets[0].addresses[0].balance).toBe("0.0000005");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("on the confirmation screen", async () => {
|
test("on the confirmation screen", async () => {
|
||||||
await refreshWith(HALF_MICRO_ETH);
|
await refreshWith(HALF_MICRO_ETH);
|
||||||
await confirmSend("0.0000001");
|
await confirmSend("0.0000001");
|
||||||
expect(text("confirm-balance")).toBe("0.0000 ETH");
|
expect(text("confirm-balance")).toBe("0.0000005 ETH");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("while a larger balance is cut to four decimals", async () => {
|
test("while a balance above the floor keeps four decimals", async () => {
|
||||||
await refreshWith(1234567890000000000n);
|
await refreshWith(1234567890000000000n);
|
||||||
await confirmSend("0.1");
|
await confirmSend("0.1");
|
||||||
expect(text("confirm-balance")).toBe("1.2345 ETH");
|
expect(text("confirm-balance")).toBe("1.2345 ETH");
|
||||||
@@ -305,7 +302,7 @@ describe("an ETH balance below 0.0001 reads 0.0000", () => {
|
|||||||
// A token the user tracks stays on the balance list when the explorer's row is
|
// A token the user tracks stays on the balance list when the explorer's row is
|
||||||
// dropped, so a holding of it below 0.000001 reached these screens as zero, and
|
// dropped, so a holding of it below 0.000001 reached these screens as zero, and
|
||||||
// the send was checked against zero.
|
// the send was checked against zero.
|
||||||
describe("a tracked token holding below 0.000001 reads 0.0000", () => {
|
describe("a tracked token holding below 0.000001 never renders as zero", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
state.trackedTokens = [
|
state.trackedTokens = [
|
||||||
{ address: TOKEN, symbol: "TOK", name: "Token", decimals: 18 },
|
{ address: TOKEN, symbol: "TOK", name: "Token", decimals: 18 },
|
||||||
@@ -316,17 +313,13 @@ describe("a tracked token holding below 0.000001 reads 0.0000", () => {
|
|||||||
await refreshWith(10n ** 18n, [tokenRow(HALF_MICRO_ETH)]);
|
await refreshWith(10n ** 18n, [tokenRow(HALF_MICRO_ETH)]);
|
||||||
state.selectedToken = TOKEN;
|
state.selectedToken = TOKEN;
|
||||||
send.updateSendBalance();
|
send.updateSendBalance();
|
||||||
expect(text("send-balance")).toBe("Current balance: 0.0000 TOK");
|
expect(text("send-balance")).toBe("Current balance: 0.0000005 TOK");
|
||||||
const stored = state.wallets[0].addresses[0].tokenBalances.find(
|
|
||||||
(t) => t.address === TOKEN,
|
|
||||||
);
|
|
||||||
expect(stored.balance).toBe("0.0000005");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("on the confirmation screen, which checks the send against it", async () => {
|
test("on the confirmation screen, which checks the send against it", async () => {
|
||||||
await refreshWith(10n ** 18n, [tokenRow(HALF_MICRO_ETH)]);
|
await refreshWith(10n ** 18n, [tokenRow(HALF_MICRO_ETH)]);
|
||||||
await confirmSend("0.0000005", TOKEN);
|
await confirmSend("0.0000005", TOKEN);
|
||||||
expect(text("confirm-balance")).toBe("0.0000 TOK");
|
expect(text("confirm-balance")).toBe("0.0000005 TOK");
|
||||||
expect(errors()).toBe("");
|
expect(errors()).toBe("");
|
||||||
expect(balanceError()).toBe("");
|
expect(balanceError()).toBe("");
|
||||||
await confirmSend("0.0000006", TOKEN);
|
await confirmSend("0.0000006", TOKEN);
|
||||||
@@ -354,9 +347,11 @@ describe("a tracked token holding below 0.000001 reads 0.0000", () => {
|
|||||||
await refreshWith(10n ** 18n, [tokenRow(1n, { decimals: "24" })]);
|
await refreshWith(10n ** 18n, [tokenRow(1n, { decimals: "24" })]);
|
||||||
state.selectedToken = TOKEN;
|
state.selectedToken = TOKEN;
|
||||||
send.updateSendBalance();
|
send.updateSendBalance();
|
||||||
expect(text("send-balance")).toBe("Current balance: 0.0000 TOK");
|
expect(text("send-balance")).toBe(
|
||||||
|
"Current balance: 0.000000000000000000000001 TOK",
|
||||||
|
);
|
||||||
await confirmSend("0.000000000000000001", TOKEN);
|
await confirmSend("0.000000000000000001", TOKEN);
|
||||||
expect(text("confirm-balance")).toBe("0.0000 TOK");
|
expect(text("confirm-balance")).toBe("0.000000000000000000000001 TOK");
|
||||||
expect(balanceError()).toContain(
|
expect(balanceError()).toContain(
|
||||||
"Your TOK balance does not cover this amount.",
|
"Your TOK balance does not cover this amount.",
|
||||||
);
|
);
|
||||||
@@ -368,20 +363,20 @@ describe("a tracked token holding below 0.000001 reads 0.0000", () => {
|
|||||||
// selected while its own screen is open: after sending 2 of a 2.0000003
|
// selected while its own screen is open: after sending 2 of a 2.0000003
|
||||||
// holding, the user is back on that screen, and Send read the missing row as
|
// holding, the user is back on that screen, and Send read the missing row as
|
||||||
// zero.
|
// zero.
|
||||||
describe("an untracked token holding below 0.000001 reads 0.0000", () => {
|
describe("an untracked token holding below 0.000001 never renders as zero", () => {
|
||||||
const row = () => tokenRow(HALF_MICRO_ETH, { holders_count: "50000" });
|
const row = () => tokenRow(HALF_MICRO_ETH, { holders_count: "50000" });
|
||||||
|
|
||||||
test("on the Send screen", async () => {
|
test("on the Send screen", async () => {
|
||||||
await refreshWith(10n ** 18n, [row()]);
|
await refreshWith(10n ** 18n, [row()]);
|
||||||
state.selectedToken = TOKEN;
|
state.selectedToken = TOKEN;
|
||||||
send.updateSendBalance();
|
send.updateSendBalance();
|
||||||
expect(text("send-balance")).toBe("Current balance: 0.0000 TOK");
|
expect(text("send-balance")).toBe("Current balance: 0.0000005 TOK");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("on the confirmation screen, which checks the send against it", async () => {
|
test("on the confirmation screen, which checks the send against it", async () => {
|
||||||
await refreshWith(10n ** 18n, [row()]);
|
await refreshWith(10n ** 18n, [row()]);
|
||||||
await confirmSend("0.0000005", TOKEN);
|
await confirmSend("0.0000005", TOKEN);
|
||||||
expect(text("confirm-balance")).toBe("0.0000 TOK");
|
expect(text("confirm-balance")).toBe("0.0000005 TOK");
|
||||||
expect(errors()).toBe("");
|
expect(errors()).toBe("");
|
||||||
expect(balanceError()).toBe("");
|
expect(balanceError()).toBe("");
|
||||||
await confirmSend("0.0000006", TOKEN);
|
await confirmSend("0.0000006", TOKEN);
|
||||||
|
|||||||
@@ -1361,31 +1361,6 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
expect(txs[0].holders).toBe(3500000);
|
expect(txs[0].holders).toBe(3500000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// README.md, Amounts: a history row cuts to four decimals, with no floor.
|
|
||||||
test("a token transfer below 0.0001 reads 0.0000", async () => {
|
|
||||||
respondWith(
|
|
||||||
[],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
transaction_hash: "0x" + "4".repeat(64),
|
|
||||||
block_number: 21000040,
|
|
||||||
timestamp: TS,
|
|
||||||
from: { hash: ORDINARY_PEER },
|
|
||||||
to: { hash: VICTIM },
|
|
||||||
total: { value: "40", decimals: "6" },
|
|
||||||
token: {
|
|
||||||
symbol: "USDC",
|
|
||||||
address_hash: USDC_CONTRACT,
|
|
||||||
holders_count: "3500000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
|
||||||
expect(txs[0].value).toBe("0.0000");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("two transfers of the same token in one transaction collapse to one entry", async () => {
|
test("two transfers of the same token in one transaction collapse to one entry", async () => {
|
||||||
const hash = "0x" + "3".repeat(64);
|
const hash = "0x" + "3".repeat(64);
|
||||||
const leg = (value) => ({
|
const leg = (value) => ({
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ globalThis.chrome = {
|
|||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
||||||
const approval = require("../src/popup/views/approval");
|
const approval = require("../src/popup/views/approval");
|
||||||
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
const { decodeCalldata } = approval;
|
||||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||||
|
|
||||||
const MAINNET = "0x1";
|
const MAINNET = "0x1";
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ globalThis.chrome = {
|
|||||||
const { AbiCoder, Interface } = require("ethers");
|
const { AbiCoder, Interface } = require("ethers");
|
||||||
const { state } = require("../src/shared/state");
|
const { state } = require("../src/shared/state");
|
||||||
const { unknownDecimalsAmount } = require("../src/shared/approvalAmount");
|
const { unknownDecimalsAmount } = require("../src/shared/approvalAmount");
|
||||||
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
const { decodeCalldata } = require("../src/popup/views/approval");
|
||||||
|
|
||||||
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
||||||
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ afterEach(() => {
|
|||||||
describe("balanceLine", () => {
|
describe("balanceLine", () => {
|
||||||
test("an unknown quantity and a zero one render differently", () => {
|
test("an unknown quantity and a zero one render differently", () => {
|
||||||
const unknown = balanceLine("NOVEL", null, null, NOVEL);
|
const unknown = balanceLine("NOVEL", null, null, NOVEL);
|
||||||
const zero = balanceLine("NOVEL", "0", null, NOVEL);
|
const zero = balanceLine("NOVEL", 0, null, NOVEL);
|
||||||
expect(unknown).not.toBe(zero);
|
expect(unknown).not.toBe(zero);
|
||||||
expect(quantities(unknown)).toEqual(["NOVEL", "quantity unknown"]);
|
expect(quantities(unknown)).toEqual(["NOVEL", "quantity unknown"]);
|
||||||
expect(quantities(zero)).toEqual(["NOVEL", "0.0000"]);
|
expect(quantities(zero)).toEqual(["NOVEL", "0.0000"]);
|
||||||
@@ -105,7 +105,7 @@ describe("balanceLine", () => {
|
|||||||
test("an unknown quantity produces no fiat figure, a zero one does", () => {
|
test("an unknown quantity produces no fiat figure, a zero one does", () => {
|
||||||
prices[NOVEL] = 3;
|
prices[NOVEL] = 3;
|
||||||
const unknown = balanceLine("NOVEL", null, 3, NOVEL);
|
const unknown = balanceLine("NOVEL", null, 3, NOVEL);
|
||||||
const zero = balanceLine("NOVEL", "0", 3, NOVEL);
|
const zero = balanceLine("NOVEL", 0, 3, NOVEL);
|
||||||
// A price times an unknown quantity is not $0.00: that is the same
|
// A price times an unknown quantity is not $0.00: that is the same
|
||||||
// claim of "nothing here" the quantity cell just refused to make.
|
// claim of "nothing here" the quantity cell just refused to make.
|
||||||
expect(unknown).toContain(
|
expect(unknown).toContain(
|
||||||
|
|||||||
Reference in new issue
Block a user