Compare commits
1
Commits
next
..
2c5364f7bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c5364f7bf |
No files matched your search
@@ -264,8 +264,6 @@ The shared modules, each in one line:
|
||||
messaging, windows and the lock.
|
||||
- `buildInfo.js`: the version, license, author, commit and date compiled into
|
||||
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.
|
||||
- `chainSwitchFields.js`: the fields a network switch changes.
|
||||
- `constants.js`: `DEBUG`, the default endpoints, the ERC-20 ABI and the burn
|
||||
@@ -1189,20 +1187,17 @@ These helpers write one kind of value on every screen that shows it:
|
||||
times;
|
||||
- `renderAddressHtml()` in the same file: most addresses.
|
||||
|
||||
Most screens show token and ETH amounts with four decimal places. History rows
|
||||
cut amounts to four decimals and never round up (`truncateAmount()` in
|
||||
`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
|
||||
history does, and extend an amount below 0.0001 to its first nonzero digit
|
||||
(`truncateAmountNeverZero()`). So a nonzero amount never reads as zero there.
|
||||
Those screens are:
|
||||
Every screen shows token and ETH amounts the same way, through
|
||||
`truncateAmountNeverZero()` in `src/shared/amountDisplay.js`:
|
||||
|
||||
- TxApproval;
|
||||
- SignApproval's Permit warning;
|
||||
- ConfirmTx, and Send's current balance;
|
||||
- WaitTx, SuccessTx and ErrorTx.
|
||||
- an amount is cut to four decimal places, never rounded, so 0.99996 reads
|
||||
`0.9999`;
|
||||
- an amount below 0.0001 is shown to its first nonzero digit, so 0.00004 reads
|
||||
`0.00004`.
|
||||
|
||||
TransactionDetail shows the exact amount and its base units.
|
||||
So an amount never reads as more than it is, and a nonzero amount never reads as
|
||||
zero. TransactionDetail is the one exception: it shows the exact amount and its
|
||||
base units.
|
||||
|
||||
An amount to send is valid when it is a number above zero, written as digits
|
||||
with at most one decimal point. Nothing but zeros may follow the token's last
|
||||
@@ -1381,7 +1376,7 @@ approval screens show a red warning.
|
||||
### Transaction decoding
|
||||
|
||||
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
|
||||
is shown in full. No contract gets any treatment beyond a legible confirmation
|
||||
screen.
|
||||
@@ -1647,10 +1642,11 @@ The development dependencies ship nowhere:
|
||||
|
||||
### Crypto policy
|
||||
|
||||
In the extension, hashing, key derivation, signing and encryption go through
|
||||
`ethers` or `libsodium-wrappers-sumo`. A new recovery phrase takes its random
|
||||
bytes from `ethers`. `build.js` and `script/lib/package.js` also hash files with
|
||||
`ethers`. No primitive is implemented in this repository.
|
||||
Hashing, key derivation, signing and encryption go through `ethers` or
|
||||
`libsodium-wrappers-sumo`. No primitive is implemented in this repository. A new
|
||||
recovery phrase draws its random bytes from the browser's
|
||||
`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
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const crypto = require("crypto");
|
||||
const { execSync } = require("child_process");
|
||||
const esbuild = require("esbuild");
|
||||
const { sha256 } = require("ethers");
|
||||
const { resolveVersion } = require("./script/lib/version");
|
||||
const {
|
||||
BACKGROUND_ENTRY_PREFIX,
|
||||
@@ -314,8 +314,10 @@ function copyIcons(distDir) {
|
||||
}
|
||||
|
||||
function sha256File(absPath) {
|
||||
// ethers prefixes the hex with 0x; the receipt holds bare hex.
|
||||
return sha256(fs.readFileSync(absPath)).slice(2);
|
||||
return crypto
|
||||
.createHash("sha256")
|
||||
.update(fs.readFileSync(absPath))
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
// Write the receipt for the files this build emitted. Deliberately records no
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ethers = require("ethers");
|
||||
|
||||
const { readZip, writeZip } = require("./zip");
|
||||
const { resolveVersion } = require("./version");
|
||||
@@ -60,9 +60,8 @@ function fail(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// Lowercase hex without the 0x prefix ethers adds, as sha256sum prints it.
|
||||
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
|
||||
@@ -292,9 +291,4 @@ if (require.main === module) {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkSelfContained,
|
||||
htmlReferences,
|
||||
manifestReferences,
|
||||
sha256,
|
||||
};
|
||||
module.exports = { checkSelfContained, htmlReferences, manifestReferences };
|
||||
@@ -159,10 +159,10 @@ function renderBalance() {
|
||||
const addr =
|
||||
state.wallets[state.selectedWallet].addresses[state.selectedAddress];
|
||||
const tokenId = state.selectedToken;
|
||||
let amount, price;
|
||||
let balance, price;
|
||||
if (tokenId === "ETH") {
|
||||
// null when no refresh has read the balance.
|
||||
amount = unknownableAmount(addr.balance);
|
||||
balance = addr.balance;
|
||||
price = getPrice("ETH");
|
||||
} else {
|
||||
const id = tokenId.toLowerCase();
|
||||
@@ -175,12 +175,13 @@ function renderBalance() {
|
||||
// null when the scale is unknown, or when no refresh has read the
|
||||
// token's balance: no quantity to show, and none to price.
|
||||
// balanceLine() states that rather than printing 0.0000.
|
||||
if (tb) amount = unknownableAmount(tb.balance);
|
||||
else amount = tracked && tracked.balanceNotFetched ? null : 0;
|
||||
if (tb) balance = tb.balance;
|
||||
else balance = tracked && tracked.balanceNotFetched ? null : "0";
|
||||
price = getPrice(tokenId);
|
||||
}
|
||||
|
||||
// USD total for this token only
|
||||
const amount = unknownableAmount(balance);
|
||||
const usdVal = price && amount !== null ? amount * price : null;
|
||||
const usdStr = formatUsd(usdVal);
|
||||
$("address-token-usd-total").innerHTML = escapeHtml(usdStr) || " ";
|
||||
@@ -188,7 +189,7 @@ function renderBalance() {
|
||||
// Single token balance line (no tokenId — not clickable here)
|
||||
$("address-token-balance").innerHTML = balanceLine(
|
||||
currentSymbol,
|
||||
amount,
|
||||
balance,
|
||||
price,
|
||||
);
|
||||
}
|
||||
|
||||
+144
-12
@@ -12,13 +12,9 @@ const {
|
||||
formatFee,
|
||||
tokenLabel,
|
||||
blockscoutOffTokenNoteHtml,
|
||||
} = require("./helpers");
|
||||
const {
|
||||
state,
|
||||
saveState,
|
||||
currentNetwork,
|
||||
tokenSourcesByChainId,
|
||||
} = require("../../shared/state");
|
||||
} = require("./helpers");
|
||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||
const {
|
||||
networkById,
|
||||
networkByChainId,
|
||||
@@ -30,24 +26,21 @@ const {
|
||||
getAddress,
|
||||
getBigInt,
|
||||
getBytes,
|
||||
Interface,
|
||||
MaxUint256,
|
||||
toUtf8String,
|
||||
} = require("ethers");
|
||||
const { getPrice, formatUsd } = require("../../shared/prices");
|
||||
const { ERC20_ABI } = require("../../shared/constants");
|
||||
const { INVISIBLE_CHARACTERS } = require("../../shared/symbolSpoof");
|
||||
const {
|
||||
resolveTokenDecimals,
|
||||
resolveTokenSymbol,
|
||||
unknownDecimalsAmount,
|
||||
} = require("../../shared/approvalAmount");
|
||||
const {
|
||||
decodeCalldata,
|
||||
tokenAmountText,
|
||||
} = require("../../shared/calldataDecoder");
|
||||
// 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
|
||||
// it carries forward to the wait/success/error screens goes through it. The
|
||||
// ERC-20 line gets it from tokenAmountText().
|
||||
// it carries forward to the wait/success/error screens goes through it.
|
||||
const {
|
||||
truncateAmountNeverZero: formatTxValue,
|
||||
} = require("../../shared/amountDisplay");
|
||||
@@ -61,8 +54,11 @@ const {
|
||||
typedDataChainRefusal,
|
||||
} = require("../../shared/approvalVerify");
|
||||
const txStatus = require("./txStatus");
|
||||
const uniswap = require("../../shared/uniswap");
|
||||
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
|
||||
// network when none is given. A chain id that names no network has no
|
||||
// explorer, so the address is drawn without a link.
|
||||
@@ -74,6 +70,141 @@ function approvalAddressHtml(address, 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) {
|
||||
const el = $(elementId);
|
||||
if (!el) return;
|
||||
@@ -1167,6 +1298,7 @@ function init(_ctx) {
|
||||
module.exports = {
|
||||
init,
|
||||
show,
|
||||
decodeCalldata,
|
||||
formatTypedDataHtml,
|
||||
typedDataRefusal,
|
||||
};
|
||||
+40
-17
@@ -31,12 +31,7 @@ const { escapeHtml } = require("../../shared/html");
|
||||
const { isDebug } = require("../../shared/log");
|
||||
const { networkByChainId } = require("../../shared/networks");
|
||||
const { formatUsd, getPrice } = require("../../shared/prices");
|
||||
const {
|
||||
state,
|
||||
saveState,
|
||||
currentNetwork,
|
||||
tokenSourcesByChainId,
|
||||
} = require("../../shared/state");
|
||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||
const { displaySymbol } = require("../../shared/symbolDisplay");
|
||||
const { isSpoofedSymbol } = require("../../shared/symbolSpoof");
|
||||
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
||||
@@ -310,6 +305,32 @@ function 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 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
|
||||
@@ -373,13 +394,16 @@ function formatFee(wei, symbol, chainId) {
|
||||
// the row that issue #307 was reported against: every screen that lists a
|
||||
// holding renders through here.
|
||||
//
|
||||
// `amount` is null for a holding whose scale nothing knows
|
||||
// `balance` is the stored balance, a decimal string. It is null for a holding
|
||||
// whose scale nothing knows
|
||||
// (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
|
||||
// derive from one, and printing 0.0000 for a real holding is the failure this
|
||||
// whole rule exists to prevent, so the row says so instead.
|
||||
function balanceLine(symbol, amount, price, tokenId) {
|
||||
const qty = amount === null ? "quantity unknown" : amount.toFixed(4);
|
||||
function balanceLine(symbol, balance, price, tokenId) {
|
||||
const amount = unknownableAmount(balance);
|
||||
const qty =
|
||||
amount === null ? "quantity unknown" : truncateAmountNeverZero(balance);
|
||||
const usd =
|
||||
price && amount !== null
|
||||
? escapeHtml(formatUsd(amount * price)) || " "
|
||||
@@ -394,7 +418,7 @@ function balanceLine(symbol, amount, price, tokenId) {
|
||||
`<div class="flex text-xs${clickClass}"${tokenAttr}>` +
|
||||
`<span class="flex justify-between w-[42ch] max-w-full">` +
|
||||
`<span>${escapeHtml(displaySymbol(symbol))}</span>` +
|
||||
`<span>${qty}</span>` +
|
||||
`<span>${escapeHtml(qty)}</span>` +
|
||||
`</span>` +
|
||||
`<span class="text-right text-muted flex-1">${usd}</span>` +
|
||||
`</div>`
|
||||
@@ -420,7 +444,7 @@ function markTrackedTokensNotFetched() {
|
||||
function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
||||
let html = balanceLine(
|
||||
nativeCurrency(),
|
||||
unknownableAmount(addr.balance),
|
||||
addr.balance,
|
||||
getPrice("ETH"),
|
||||
"ETH",
|
||||
);
|
||||
@@ -438,7 +462,7 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
||||
if (bal === 0 && !showZero) continue;
|
||||
html += balanceLine(
|
||||
t.symbol,
|
||||
bal,
|
||||
t.balance,
|
||||
getPrice(t.address),
|
||||
t.address.toLowerCase(),
|
||||
);
|
||||
@@ -456,7 +480,7 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
||||
}
|
||||
html += balanceLine(
|
||||
t.symbol,
|
||||
t.balanceNotFetched ? null : 0,
|
||||
t.balanceNotFetched ? null : "0",
|
||||
getPrice(t.address),
|
||||
t.address.toLowerCase(),
|
||||
);
|
||||
@@ -467,10 +491,8 @@ function balanceLinesForAddress(addr, trackedTokens, showZero) {
|
||||
|
||||
// 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
|
||||
// under the remove-address warning leaves out too. Deliberately unrounded —
|
||||
// the rendered lines round to four decimals, so a dust balance displays as
|
||||
// 0.0000 while still being real money at a real address. Callers that warn
|
||||
// about holdings must ask this, not the rendered figure.
|
||||
// under the remove-address warning leaves out too. Callers that warn about
|
||||
// holdings must ask this, not the rendered lines.
|
||||
function addressHoldsFunds(addr) {
|
||||
if (!addr) return false;
|
||||
// A null or absent ETH balance reads as unknown, so the address may hold
|
||||
@@ -845,6 +867,7 @@ module.exports = {
|
||||
keyMaterialName,
|
||||
unknownableAmount,
|
||||
nativeCurrency,
|
||||
tokenSourcesByChainId,
|
||||
blockscoutOffTokenNoteHtml,
|
||||
tokenLabel,
|
||||
formatFee,
|
||||
|
||||
@@ -31,6 +31,7 @@ const {
|
||||
resetSendValidation,
|
||||
} = require("./send");
|
||||
const { deriveAddressFromXpub } = require("../../shared/wallet");
|
||||
const { truncateAmountNeverZero } = require("../../shared/amountDisplay");
|
||||
const { canRemoveAddress } = require("../../shared/walletDelete");
|
||||
const {
|
||||
walletDefect,
|
||||
@@ -81,7 +82,8 @@ function renderTotalValue() {
|
||||
if (ethBal === null) {
|
||||
el.textContent = "unknown (" + nativeCurrency() + ")";
|
||||
} else {
|
||||
const ethStr = ethBal.toFixed(4) + " " + nativeCurrency();
|
||||
const ethStr =
|
||||
truncateAmountNeverZero(addr.balance) + " " + nativeCurrency();
|
||||
const ethUsd = ethPrice
|
||||
? " (" + formatUsd(ethBal * ethPrice) + ")"
|
||||
: "";
|
||||
|
||||
@@ -10,13 +10,9 @@ const {
|
||||
renderAddressHtml,
|
||||
attachCopyHandlers,
|
||||
goBack,
|
||||
} = require("./helpers");
|
||||
const {
|
||||
state,
|
||||
currentAddress,
|
||||
currentNetwork,
|
||||
tokenSourcesByChainId,
|
||||
} = require("../../shared/state");
|
||||
} = require("./helpers");
|
||||
const { state, currentAddress, currentNetwork } = require("../../shared/state");
|
||||
let ctx;
|
||||
const { getProvider } = require("../../shared/balances");
|
||||
const { resolveTokenDecimals } = require("../../shared/approvalAmount");
|
||||
|
||||
@@ -27,7 +27,7 @@ const { nativeCurrencyByChainId } = require("../../shared/networks");
|
||||
const { formatEther, formatUnits } = require("ethers");
|
||||
const { log, debugFetch } = require("../../shared/log");
|
||||
const { storedSettingIsOn } = require("../../shared/storedSettings");
|
||||
const { decodeCalldata } = require("../../shared/calldataDecoder");
|
||||
const { decodeCalldata } = require("./approval");
|
||||
|
||||
/**
|
||||
* Determine a human-readable transaction type string from tx fields.
|
||||
|
||||
+10
-19
@@ -1,15 +1,6 @@
|
||||
// The 4-decimal amount rule from README.md's Amounts section, and
|
||||
// the one exception to it, in one place. Three call sites had grown their own
|
||||
// copy of the truncation — the history and balance lists
|
||||
// (`src/shared/transactions.js`), the approval screen's ERC-20 amount line
|
||||
// (`src/shared/calldataDecoder.js`) and its Uniswap swap detail lines
|
||||
// (`src/shared/uniswap.js`) — and a fix applied to one of them left the other
|
||||
// two showing a different number for the same value.
|
||||
//
|
||||
// The two truncation functions below are the two policies, not two
|
||||
// implementations of one: summary lists truncate, and the screens that state
|
||||
// what is being authorized truncate with a floor. Keeping them adjacent is the
|
||||
// point, so a change to the rule cannot reach one screen and miss another.
|
||||
// The amount rule from README.md's Amounts section. Every screen that shows a
|
||||
// token or ETH amount, except TransactionDetail, shows it through
|
||||
// truncateAmountNeverZero(), so one value reads the same on every screen.
|
||||
|
||||
// Truncate to exactly four decimal places. Truncation, never rounding: an
|
||||
// amount must never be displayed as larger than it is, so 0.99999 stays
|
||||
@@ -20,12 +11,13 @@ function truncateAmount(val) {
|
||||
return parts[0] + "." + (parts[1] + "0000").slice(0, 4);
|
||||
}
|
||||
|
||||
// The same rule, plus the invariant the approval and confirmation screens
|
||||
// hold: a nonzero amount never renders as zero. Truncating to four decimals
|
||||
// does exactly that to an amount below 0.0001 — one base unit of an 18-decimal
|
||||
// token, 500 of an 8-decimal one — and a real transfer or allowance then reads
|
||||
// as "nothing is being moved" on the screen whose whole job is to say what is
|
||||
// being authorized.
|
||||
// Four decimal places, truncated, and a nonzero amount never renders as zero.
|
||||
// Truncating alone would show an amount below 0.0001 as 0.0000, and a real
|
||||
// holding, transfer or allowance would then read as nothing.
|
||||
//
|
||||
// `val` is the exact decimal string: a stored balance, or what formatEther()
|
||||
// or formatUnits() returns. Never a JavaScript number, whose own rounding
|
||||
// would happen before the truncation.
|
||||
//
|
||||
// When the truncated string carries no significant digit and the value does,
|
||||
// the amount is extended to its first significant digit instead. It stays in
|
||||
@@ -54,7 +46,6 @@ function isBelowOneMillionth(balance) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
truncateAmount,
|
||||
truncateAmountNeverZero,
|
||||
isBelowOneMillionth,
|
||||
};
|
||||
@@ -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
|
||||
// guarantee.
|
||||
|
||||
const { networkById, networkByChainId } = require("./networks");
|
||||
const { networkById } = require("./networks");
|
||||
const { copyChainSwitchFields } = require("./chainSwitchFields");
|
||||
const {
|
||||
DEFAULT_STATE,
|
||||
@@ -125,32 +125,6 @@ function currentNetwork() {
|
||||
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
|
||||
// loadState() finished, or when its last successful saveState() began.
|
||||
// takeStoredNetwork(), takeStoredTrackedTokens() and
|
||||
@@ -846,6 +820,5 @@ module.exports = {
|
||||
loadState,
|
||||
currentAddress,
|
||||
currentNetwork,
|
||||
tokenSourcesByChainId,
|
||||
StateNotLoadedError,
|
||||
};
|
||||
@@ -16,10 +16,7 @@ const { nativeCurrencyByChainId } = require("./networks");
|
||||
// a scale is either reported or it is unknown, and "unknown" must mean the
|
||||
// same thing here as it does on the screens that refuse to format one.
|
||||
const { toDecimals } = require("./transferAmount");
|
||||
// The plain 4-decimal rule. The history and balance lists deliberately keep
|
||||
// truncation without the approval screens' nonzero floor: the transaction
|
||||
// detail view is the authoritative record and already shows exact precision.
|
||||
const { truncateAmount: formatTxValue } = require("./amountDisplay");
|
||||
const { truncateAmountNeverZero } = require("./amountDisplay");
|
||||
const { routerRelease } = require("./uniswap");
|
||||
const { storedSettingIsOn } = require("./storedSettings");
|
||||
|
||||
@@ -40,7 +37,7 @@ function parseTx(tx, addrLower, chainId) {
|
||||
|
||||
// For contract calls, produce a meaningful label instead of "0.0000 ETH"
|
||||
let symbol = nativeCurrencyByChainId(chainId);
|
||||
let value = formatTxValue(formatEther(rawWei));
|
||||
let value = truncateAmountNeverZero(formatEther(rawWei));
|
||||
let exactValue = formatEther(rawWei);
|
||||
let rawAmount = rawWei;
|
||||
let rawUnit = "wei";
|
||||
@@ -122,7 +119,9 @@ function parseTokenTransfer(tt, addrLower, chainId) {
|
||||
// The exact figure is not lost — it is the base-unit line below, which is
|
||||
// the one number that needs no scale to be true.
|
||||
const formatted =
|
||||
decimals === null ? "" : formatTxValue(formatUnits(rawVal, decimals));
|
||||
decimals === null
|
||||
? ""
|
||||
: truncateAmountNeverZero(formatUnits(rawVal, decimals));
|
||||
const exact = decimals === null ? "" : formatUnits(rawVal, decimals);
|
||||
return {
|
||||
hash: tt.transaction_hash,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Wallet operations: mnemonic generation, HD derivation, signing.
|
||||
// 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");
|
||||
|
||||
function generateMnemonic() {
|
||||
@@ -10,7 +10,10 @@ function generateMnemonic() {
|
||||
// settings toggle drives, which would let a user of a release build turn
|
||||
// the hardcoded, publicly known test phrase back on for real wallets.
|
||||
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.
|
||||
|
||||
@@ -23,7 +23,7 @@ const {
|
||||
resolveTokenDecimals,
|
||||
unknownDecimalsAmount,
|
||||
} = require("../src/shared/approvalAmount");
|
||||
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
||||
const { decodeCalldata } = require("../src/popup/views/approval");
|
||||
|
||||
const iface = new Interface(ERC20_ABI);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// asserted on `rawValue` alongside the displayed line.
|
||||
//
|
||||
// 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`.
|
||||
|
||||
globalThis.chrome = {
|
||||
@@ -23,12 +23,9 @@ globalThis.chrome = {
|
||||
const { AbiCoder, Interface } = require("ethers");
|
||||
const { ERC20_ABI } = require("../src/shared/constants");
|
||||
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 {
|
||||
truncateAmount,
|
||||
truncateAmountNeverZero,
|
||||
} = require("../src/shared/amountDisplay");
|
||||
const { truncateAmountNeverZero } = require("../src/shared/amountDisplay");
|
||||
|
||||
const iface = new Interface(ERC20_ABI);
|
||||
|
||||
@@ -184,12 +181,7 @@ describe("the four-decimal rule is otherwise unchanged", () => {
|
||||
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");
|
||||
test("one base unit of an 18-decimal token reads as itself", () => {
|
||||
expect(truncateAmountNeverZero("0.000000000000000001")).toBe(
|
||||
"0.000000000000000001",
|
||||
);
|
||||
|
||||
@@ -41,13 +41,13 @@ describe("balanceLine", () => {
|
||||
// this payload before its id attribute, so an assertion about the
|
||||
// rest of the payload would pass on the cap and say nothing about
|
||||
// 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).toContain("<iframe");
|
||||
});
|
||||
|
||||
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).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
|
||||
// way out of the attribute and into a new one.
|
||||
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).toContain('data-token="" onclick="alert(1)"');
|
||||
});
|
||||
|
||||
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>1.5000</span>");
|
||||
expect(html).toContain('data-token="0xabc"');
|
||||
@@ -69,7 +69,7 @@ describe("balanceLine", () => {
|
||||
|
||||
// formatUsd() writes a value under a cent as "< $0.01".
|
||||
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).not.toContain("< $0.01");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// The balance lists and Home's ETH line show a balance as Send does (README.md,
|
||||
// Amounts): cut to four decimal places, never rounded up, and a nonzero amount
|
||||
// never shown as zero
|
||||
// (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("shows 0.00004 as 0.00004, not 0.0000", () => {
|
||||
expect(quantity(balanceLine("USDC", "0.00004", null, USDC))).toBe(
|
||||
"0.00004",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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.00004", async () => {
|
||||
const page = await bootHolding("0.99996", "0.00004");
|
||||
|
||||
expect(quantity(homeRow(page, USDC.toLowerCase()))).toBe("0.00004");
|
||||
});
|
||||
});
|
||||
@@ -4,8 +4,8 @@
|
||||
// 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
|
||||
// obvious routes — and what counts as holding something, which is any
|
||||
// ERC-20 as well as ETH, at any size, including a balance that rounds to
|
||||
// zero at the four decimals the balance lines render. The DOM behaviour
|
||||
// ERC-20 as well as ETH, at any size, including a balance below the four
|
||||
// decimals the balance lines render. The DOM behaviour
|
||||
// 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.
|
||||
@@ -102,7 +102,7 @@ describe("whether an address holds anything", () => {
|
||||
expect(addressHoldsFunds(TOKEN_ONLY)).toBe(true);
|
||||
});
|
||||
|
||||
// 0.00001 ETH renders as "0.0000" at four decimals. It is still money.
|
||||
// 0.00001 ETH is below four decimals. It is still money.
|
||||
test("an ETH balance below the displayed precision counts", () => {
|
||||
expect(addressHoldsFunds(DUST)).toBe(true);
|
||||
});
|
||||
@@ -141,13 +141,12 @@ describe("the balance warning on the removal confirmation", () => {
|
||||
expect(html).toContain("2500.0000");
|
||||
});
|
||||
|
||||
// The rendered line says 0.0000 for this address — that is the display
|
||||
// format, shared with Home and AddressDetail — and the warning is shown
|
||||
// all the same, because the balance is not zero.
|
||||
test("an ETH balance that renders as 0.0000 is warned about", () => {
|
||||
// The rendered line shows the balance to its first nonzero digit, as Home
|
||||
// and AddressDetail do (README.md, Amounts).
|
||||
test("an ETH balance below 0.0001 is warned about, and reads as itself", () => {
|
||||
const html = balanceWarningHtml(DUST);
|
||||
expect(html).toContain("This address holds a balance.");
|
||||
expect(html).toContain("<span>0.0000</span>");
|
||||
expect(html).toContain("<span>0.00001</span>");
|
||||
});
|
||||
|
||||
test("an ETH balance no refresh has read is warned about, and reads as unknown", () => {
|
||||
|
||||
@@ -47,7 +47,7 @@ const { fetchTokenBalances } = require("../src/shared/balances");
|
||||
const { debugFetch } = require("../src/shared/log");
|
||||
const { state } = require("../src/shared/state");
|
||||
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 HOLDER = "0x" + "a".repeat(40);
|
||||
|
||||
+2
-13
@@ -1,5 +1,4 @@
|
||||
// The release archives: the zip container, the self-containment rule, and the
|
||||
// digest format in release/SHA256SUMS.
|
||||
// The release archives: the zip container, and the self-containment rule.
|
||||
//
|
||||
// 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:
|
||||
@@ -12,14 +11,12 @@
|
||||
// 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
|
||||
// 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
|
||||
// release/SHA256SUMS.
|
||||
// artifact is made of.
|
||||
|
||||
const {
|
||||
checkSelfContained,
|
||||
htmlReferences,
|
||||
manifestReferences,
|
||||
sha256,
|
||||
} = require("../script/lib/package");
|
||||
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", () => {
|
||||
const files = [
|
||||
{ name: "manifest.json", data: Buffer.from('{"a":1}') },
|
||||
|
||||
@@ -22,7 +22,7 @@ const { Interface } = require("ethers");
|
||||
const { ERC20_ABI } = require("../src/shared/constants");
|
||||
const { state } = require("../src/shared/state");
|
||||
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);
|
||||
|
||||
|
||||
@@ -1361,6 +1361,31 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
||||
expect(txs[0].holders).toBe(3500000);
|
||||
});
|
||||
|
||||
// README.md, Amounts: a nonzero amount never reads as zero.
|
||||
test("a token transfer below 0.0001 shows its first nonzero digit", 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.00004");
|
||||
});
|
||||
|
||||
test("two transfers of the same token in one transaction collapse to one entry", async () => {
|
||||
const hash = "0x" + "3".repeat(64);
|
||||
const leg = (value) => ({
|
||||
|
||||
@@ -47,7 +47,7 @@ globalThis.chrome = {
|
||||
const { state } = require("../src/shared/state");
|
||||
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
||||
const approval = require("../src/popup/views/approval");
|
||||
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
||||
const { decodeCalldata } = approval;
|
||||
const transactionDetail = require("../src/popup/views/transactionDetail");
|
||||
|
||||
const MAINNET = "0x1";
|
||||
|
||||
@@ -18,7 +18,7 @@ globalThis.chrome = {
|
||||
const { AbiCoder, Interface } = require("ethers");
|
||||
const { state } = require("../src/shared/state");
|
||||
const { unknownDecimalsAmount } = require("../src/shared/approvalAmount");
|
||||
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
||||
const { decodeCalldata } = require("../src/popup/views/approval");
|
||||
|
||||
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
||||
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||
|
||||
@@ -96,7 +96,7 @@ afterEach(() => {
|
||||
describe("balanceLine", () => {
|
||||
test("an unknown quantity and a zero one render differently", () => {
|
||||
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(quantities(unknown)).toEqual(["NOVEL", "quantity unknown"]);
|
||||
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", () => {
|
||||
prices[NOVEL] = 3;
|
||||
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
|
||||
// claim of "nothing here" the quantity cell just refused to make.
|
||||
expect(unknown).toContain(
|
||||
|
||||
Reference in new issue
Block a user