Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb7c75101f | ||
|
|
6d158bf1b1 |
No files matched your search
@@ -264,6 +264,8 @@ 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
|
||||||
@@ -1187,17 +1189,20 @@ These helpers write one kind of value on every screen that shows it:
|
|||||||
times;
|
times;
|
||||||
- `renderAddressHtml()` in the same file: most addresses.
|
- `renderAddressHtml()` in the same file: most addresses.
|
||||||
|
|
||||||
Every screen shows token and ETH amounts the same way, through
|
Most screens show token and ETH amounts with four decimal places. History rows
|
||||||
`truncateAmountNeverZero()` in `src/shared/amountDisplay.js`:
|
cut amounts to four decimals and never round up (`truncateAmount()` in
|
||||||
|
`src/shared/amountDisplay.js`). The balance lists and Home's ETH balance cut
|
||||||
|
them the same way. 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:
|
||||||
|
|
||||||
- an amount is cut to four decimal places, never rounded, so 0.99996 reads
|
- TxApproval;
|
||||||
`0.9999`;
|
- SignApproval's Permit warning;
|
||||||
- an amount below 0.0001 is shown to its first nonzero digit, so 0.00004 reads
|
- ConfirmTx, and Send's current balance;
|
||||||
`0.00004`.
|
- WaitTx, SuccessTx and ErrorTx.
|
||||||
|
|
||||||
So an amount never reads as more than it is, and a nonzero amount never reads as
|
TransactionDetail shows the exact amount and its base units.
|
||||||
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
|
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
|
with at most one decimal point. Nothing but zeros may follow the token's last
|
||||||
@@ -1376,7 +1381,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/popup/views/approval.js`). SuccessTx repeats
|
(`decodeCalldata()` in `src/shared/calldataDecoder.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.
|
||||||
|
|||||||
+12
-144
@@ -12,9 +12,13 @@ const {
|
|||||||
formatFee,
|
formatFee,
|
||||||
tokenLabel,
|
tokenLabel,
|
||||||
blockscoutOffTokenNoteHtml,
|
blockscoutOffTokenNoteHtml,
|
||||||
tokenSourcesByChainId,
|
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const {
|
||||||
|
state,
|
||||||
|
saveState,
|
||||||
|
currentNetwork,
|
||||||
|
tokenSourcesByChainId,
|
||||||
|
} = require("../../shared/state");
|
||||||
const {
|
const {
|
||||||
networkById,
|
networkById,
|
||||||
networkByChainId,
|
networkByChainId,
|
||||||
@@ -26,21 +30,24 @@ 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.
|
// it carries forward to the wait/success/error screens goes through it. The
|
||||||
|
// ERC-20 line gets it from tokenAmountText().
|
||||||
const {
|
const {
|
||||||
truncateAmountNeverZero: formatTxValue,
|
truncateAmountNeverZero: formatTxValue,
|
||||||
} = require("../../shared/amountDisplay");
|
} = require("../../shared/amountDisplay");
|
||||||
@@ -54,11 +61,8 @@ 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.
|
||||||
@@ -70,141 +74,6 @@ 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;
|
||||||
@@ -1298,7 +1167,6 @@ function init(_ctx) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
show,
|
show,
|
||||||
decodeCalldata,
|
|
||||||
formatTypedDataHtml,
|
formatTypedDataHtml,
|
||||||
typedDataRefusal,
|
typedDataRefusal,
|
||||||
};
|
};
|
||||||
@@ -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 round to four decimals, so a sentence built from a
|
// own: the rendered lines cut to four decimals, so a sentence built from their
|
||||||
// rounded number would report "0.0000 ETH" for an address holding real money.
|
// figure 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
|
||||||
|
|||||||
+12
-32
@@ -18,6 +18,7 @@
|
|||||||
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");
|
||||||
@@ -31,7 +32,12 @@ 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 { state, saveState, currentNetwork } = require("../../shared/state");
|
const {
|
||||||
|
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");
|
||||||
@@ -305,32 +311,6 @@ 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
|
||||||
@@ -402,8 +382,7 @@ function formatFee(wei, symbol, chainId) {
|
|||||||
// whole rule exists to prevent, so the row says so instead.
|
// whole rule exists to prevent, so the row says so instead.
|
||||||
function balanceLine(symbol, balance, price, tokenId) {
|
function balanceLine(symbol, balance, price, tokenId) {
|
||||||
const amount = unknownableAmount(balance);
|
const amount = unknownableAmount(balance);
|
||||||
const qty =
|
const qty = amount === null ? "quantity unknown" : truncateAmount(balance);
|
||||||
amount === null ? "quantity unknown" : truncateAmountNeverZero(balance);
|
|
||||||
const usd =
|
const usd =
|
||||||
price && amount !== null
|
price && amount !== null
|
||||||
? escapeHtml(formatUsd(amount * price)) || " "
|
? escapeHtml(formatUsd(amount * price)) || " "
|
||||||
@@ -491,8 +470,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. Callers that warn about
|
// under the remove-address warning leaves out too. Deliberately exact — the
|
||||||
// holdings must ask this, not the rendered lines.
|
// rendered lines cut 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.
|
||||||
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
|
||||||
@@ -867,7 +848,6 @@ module.exports = {
|
|||||||
keyMaterialName,
|
keyMaterialName,
|
||||||
unknownableAmount,
|
unknownableAmount,
|
||||||
nativeCurrency,
|
nativeCurrency,
|
||||||
tokenSourcesByChainId,
|
|
||||||
blockscoutOffTokenNoteHtml,
|
blockscoutOffTokenNoteHtml,
|
||||||
tokenLabel,
|
tokenLabel,
|
||||||
formatFee,
|
formatFee,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const {
|
|||||||
resetSendValidation,
|
resetSendValidation,
|
||||||
} = require("./send");
|
} = require("./send");
|
||||||
const { deriveAddressFromXpub } = require("../../shared/wallet");
|
const { deriveAddressFromXpub } = require("../../shared/wallet");
|
||||||
const { truncateAmountNeverZero } = require("../../shared/amountDisplay");
|
const { truncateAmount } = require("../../shared/amountDisplay");
|
||||||
const { canRemoveAddress } = require("../../shared/walletDelete");
|
const { canRemoveAddress } = require("../../shared/walletDelete");
|
||||||
const {
|
const {
|
||||||
walletDefect,
|
walletDefect,
|
||||||
@@ -82,8 +82,7 @@ function renderTotalValue() {
|
|||||||
if (ethBal === null) {
|
if (ethBal === null) {
|
||||||
el.textContent = "unknown (" + nativeCurrency() + ")";
|
el.textContent = "unknown (" + nativeCurrency() + ")";
|
||||||
} else {
|
} else {
|
||||||
const ethStr =
|
const ethStr = truncateAmount(addr.balance) + " " + nativeCurrency();
|
||||||
truncateAmountNeverZero(addr.balance) + " " + nativeCurrency();
|
|
||||||
const ethUsd = ethPrice
|
const ethUsd = ethPrice
|
||||||
? " (" + formatUsd(ethBal * ethPrice) + ")"
|
? " (" + formatUsd(ethBal * ethPrice) + ")"
|
||||||
: "";
|
: "";
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ const {
|
|||||||
renderAddressHtml,
|
renderAddressHtml,
|
||||||
attachCopyHandlers,
|
attachCopyHandlers,
|
||||||
goBack,
|
goBack,
|
||||||
tokenSourcesByChainId,
|
|
||||||
} = require("./helpers");
|
} = require("./helpers");
|
||||||
const { state, currentAddress, currentNetwork } = require("../../shared/state");
|
const {
|
||||||
|
state,
|
||||||
|
currentAddress,
|
||||||
|
currentNetwork,
|
||||||
|
tokenSourcesByChainId,
|
||||||
|
} = 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");
|
||||||
|
|||||||
@@ -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("./approval");
|
const { decodeCalldata } = require("../../shared/calldataDecoder");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine a human-readable transaction type string from tx fields.
|
* Determine a human-readable transaction type string from tx fields.
|
||||||
|
|||||||
+19
-10
@@ -1,6 +1,15 @@
|
|||||||
// The amount rule from README.md's Amounts section. Every screen that shows a
|
// The 4-decimal amount rule from README.md's Amounts section, and
|
||||||
// token or ETH amount, except TransactionDetail, shows it through
|
// the one exception to it, in one place. Three call sites had grown their own
|
||||||
// truncateAmountNeverZero(), so one value reads the same on every screen.
|
// 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.
|
||||||
|
|
||||||
// Truncate to exactly four decimal places. Truncation, never rounding: an
|
// Truncate to exactly four decimal places. Truncation, never rounding: an
|
||||||
// amount must never be displayed as larger than it is, so 0.99999 stays
|
// amount must never be displayed as larger than it is, so 0.99999 stays
|
||||||
@@ -11,13 +20,12 @@ function truncateAmount(val) {
|
|||||||
return parts[0] + "." + (parts[1] + "0000").slice(0, 4);
|
return parts[0] + "." + (parts[1] + "0000").slice(0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Four decimal places, truncated, and a nonzero amount never renders as zero.
|
// The same rule, plus the invariant the approval and confirmation screens
|
||||||
// Truncating alone would show an amount below 0.0001 as 0.0000, and a real
|
// hold: a nonzero amount never renders as zero. Truncating to four decimals
|
||||||
// holding, transfer or allowance would then read as nothing.
|
// 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
|
||||||
// `val` is the exact decimal string: a stored balance, or what formatEther()
|
// as "nothing is being moved" on the screen whose whole job is to say what is
|
||||||
// or formatUnits() returns. Never a JavaScript number, whose own rounding
|
// being authorized.
|
||||||
// would happen before the truncation.
|
|
||||||
//
|
//
|
||||||
// When the truncated string carries no significant digit and the value does,
|
// 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
|
// the amount is extended to its first significant digit instead. It stays in
|
||||||
@@ -46,6 +54,7 @@ function isBelowOneMillionth(balance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
truncateAmount,
|
||||||
truncateAmountNeverZero,
|
truncateAmountNeverZero,
|
||||||
isBelowOneMillionth,
|
isBelowOneMillionth,
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
// 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 };
|
||||||
+28
-1
@@ -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 } = require("./networks");
|
const { networkById, networkByChainId } = require("./networks");
|
||||||
const { copyChainSwitchFields } = require("./chainSwitchFields");
|
const { copyChainSwitchFields } = require("./chainSwitchFields");
|
||||||
const {
|
const {
|
||||||
DEFAULT_STATE,
|
DEFAULT_STATE,
|
||||||
@@ -125,6 +125,32 @@ 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
|
||||||
@@ -820,5 +846,6 @@ module.exports = {
|
|||||||
loadState,
|
loadState,
|
||||||
currentAddress,
|
currentAddress,
|
||||||
currentNetwork,
|
currentNetwork,
|
||||||
|
tokenSourcesByChainId,
|
||||||
StateNotLoadedError,
|
StateNotLoadedError,
|
||||||
};
|
};
|
||||||
@@ -16,7 +16,10 @@ const { nativeCurrencyByChainId } = require("./networks");
|
|||||||
// a scale is either reported or it is unknown, and "unknown" must mean the
|
// 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.
|
// same thing here as it does on the screens that refuse to format one.
|
||||||
const { toDecimals } = require("./transferAmount");
|
const { toDecimals } = require("./transferAmount");
|
||||||
const { truncateAmountNeverZero } = require("./amountDisplay");
|
// 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 { routerRelease } = require("./uniswap");
|
const { routerRelease } = require("./uniswap");
|
||||||
const { storedSettingIsOn } = require("./storedSettings");
|
const { storedSettingIsOn } = require("./storedSettings");
|
||||||
|
|
||||||
@@ -37,7 +40,7 @@ function parseTx(tx, addrLower, chainId) {
|
|||||||
|
|
||||||
// For contract calls, produce a meaningful label instead of "0.0000 ETH"
|
// For contract calls, produce a meaningful label instead of "0.0000 ETH"
|
||||||
let symbol = nativeCurrencyByChainId(chainId);
|
let symbol = nativeCurrencyByChainId(chainId);
|
||||||
let value = truncateAmountNeverZero(formatEther(rawWei));
|
let value = formatTxValue(formatEther(rawWei));
|
||||||
let exactValue = formatEther(rawWei);
|
let exactValue = formatEther(rawWei);
|
||||||
let rawAmount = rawWei;
|
let rawAmount = rawWei;
|
||||||
let rawUnit = "wei";
|
let rawUnit = "wei";
|
||||||
@@ -119,9 +122,7 @@ function parseTokenTransfer(tt, addrLower, chainId) {
|
|||||||
// The exact figure is not lost — it is the base-unit line below, which is
|
// 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.
|
// the one number that needs no scale to be true.
|
||||||
const formatted =
|
const formatted =
|
||||||
decimals === null
|
decimals === null ? "" : formatTxValue(formatUnits(rawVal, decimals));
|
||||||
? ""
|
|
||||||
: truncateAmountNeverZero(formatUnits(rawVal, decimals));
|
|
||||||
const exact = decimals === null ? "" : formatUnits(rawVal, decimals);
|
const exact = decimals === null ? "" : formatUnits(rawVal, decimals);
|
||||||
return {
|
return {
|
||||||
hash: tt.transaction_hash,
|
hash: tt.transaction_hash,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const {
|
|||||||
resolveTokenDecimals,
|
resolveTokenDecimals,
|
||||||
unknownDecimalsAmount,
|
unknownDecimalsAmount,
|
||||||
} = require("../src/shared/approvalAmount");
|
} = require("../src/shared/approvalAmount");
|
||||||
const { decodeCalldata } = require("../src/popup/views/approval");
|
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
||||||
|
|
||||||
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/popup/views/approval.js`, and the swap's `Amount` and `Min. received`
|
// `src/shared/calldataDecoder.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,9 +23,8 @@ 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/popup/views/approval");
|
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
||||||
const uniswap = require("../src/shared/uniswap");
|
const uniswap = require("../src/shared/uniswap");
|
||||||
const { truncateAmountNeverZero } = require("../src/shared/amountDisplay");
|
|
||||||
|
|
||||||
const iface = new Interface(ERC20_ABI);
|
const iface = new Interface(ERC20_ABI);
|
||||||
|
|
||||||
@@ -180,10 +179,4 @@ 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");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("one base unit of an 18-decimal token reads as itself", () => {
|
|
||||||
expect(truncateAmountNeverZero("0.000000000000000001")).toBe(
|
|
||||||
"0.000000000000000001",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
// The balance lists and Home's ETH line show a balance as Send does (README.md,
|
// The balance lists and Home's ETH line show a balance as the history rows do
|
||||||
// Amounts): cut to four decimal places, never rounded up, and a nonzero amount
|
// (README.md, Amounts): cut to four decimal places, never rounded up
|
||||||
// never shown as zero
|
|
||||||
// (https://git.eeqj.de/sneak/AutistMask/issues/757).
|
// (https://git.eeqj.de/sneak/AutistMask/issues/757).
|
||||||
//
|
//
|
||||||
// Driven through the real popup over a stored profile on Ethereum Mainnet.
|
// Driven through the real popup over a stored profile on Ethereum Mainnet.
|
||||||
@@ -54,9 +53,9 @@ describe("balanceLine", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shows 0.00004 as 0.00004, not 0.0000", () => {
|
test("cuts 0.00004 to 0.0000", () => {
|
||||||
expect(quantity(balanceLine("USDC", "0.00004", null, USDC))).toBe(
|
expect(quantity(balanceLine("USDC", "0.00004", null, USDC))).toBe(
|
||||||
"0.00004",
|
"0.0000",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -70,9 +69,9 @@ describe("Home", () => {
|
|||||||
expect(quantity(homeRow(page, "ETH"))).toBe("0.9999");
|
expect(quantity(homeRow(page, "ETH"))).toBe("0.9999");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the balance list reads 0.00004 USDC as 0.00004", async () => {
|
test("the balance list reads 0.00004 USDC as 0.0000", async () => {
|
||||||
const page = await bootHolding("0.99996", "0.00004");
|
const page = await bootHolding("0.99996", "0.00004");
|
||||||
|
|
||||||
expect(quantity(homeRow(page, USDC.toLowerCase()))).toBe("0.00004");
|
expect(quantity(homeRow(page, USDC.toLowerCase()))).toBe("0.0000");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -102,7 +102,7 @@ describe("whether an address holds anything", () => {
|
|||||||
expect(addressHoldsFunds(TOKEN_ONLY)).toBe(true);
|
expect(addressHoldsFunds(TOKEN_ONLY)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 0.00001 ETH is below four decimals. It is still money.
|
// 0.00001 ETH renders as "0.0000" at four decimals. It is still money.
|
||||||
test("an ETH balance below the displayed precision counts", () => {
|
test("an ETH balance below the displayed precision counts", () => {
|
||||||
expect(addressHoldsFunds(DUST)).toBe(true);
|
expect(addressHoldsFunds(DUST)).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -141,12 +141,13 @@ describe("the balance warning on the removal confirmation", () => {
|
|||||||
expect(html).toContain("2500.0000");
|
expect(html).toContain("2500.0000");
|
||||||
});
|
});
|
||||||
|
|
||||||
// The rendered line shows the balance to its first nonzero digit, as Home
|
// The rendered line says 0.0000 for this address — that is the display
|
||||||
// and AddressDetail do (README.md, Amounts).
|
// format, shared with Home and AddressDetail — and the warning is shown
|
||||||
test("an ETH balance below 0.0001 is warned about, and reads as itself", () => {
|
// all the same, because the balance is not zero.
|
||||||
|
test("an ETH balance that renders as 0.0000 is warned about", () => {
|
||||||
const html = balanceWarningHtml(DUST);
|
const html = balanceWarningHtml(DUST);
|
||||||
expect(html).toContain("This address holds a balance.");
|
expect(html).toContain("This address holds a balance.");
|
||||||
expect(html).toContain("<span>0.00001</span>");
|
expect(html).toContain("<span>0.0000</span>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("an ETH balance no refresh has read is warned about, and reads as unknown", () => {
|
test("an ETH balance no refresh has read is warned about, and reads as unknown", () => {
|
||||||
|
|||||||
+9
-9
@@ -829,12 +829,11 @@ 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 (four decimals) and
|
// The native amount as the transaction list writes it (cut to four decimals,
|
||||||
// as the detail screen writes it (full precision). Both are rendered here
|
// quantity() below) and as the detail screen writes it (full precision). Both
|
||||||
// from the fixture rather than read off the screen, so the assertions
|
// are rendered here from the fixture rather than read off the screen, so the
|
||||||
// compare against the wei the stub served.
|
// assertions compare against the wei the stub served.
|
||||||
const NATIVE_ROW_TEXT =
|
const NATIVE_ROW_TEXT = quantity(STUB_NATIVE_VALUE_WEI) + " ETH";
|
||||||
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) => {
|
||||||
@@ -2724,10 +2723,11 @@ async function goToConfirm(page, send) {
|
|||||||
await assertAddressesFit(page, "the confirmation screen");
|
await assertAddressesFit(page, "the confirmation screen");
|
||||||
}
|
}
|
||||||
|
|
||||||
// A balance as the main view renders it: balanceLinesForAddress() writes
|
// An amount in wei as the main view's balance lines and the transaction list
|
||||||
// every quantity with four decimal places.
|
// write it: cut to four decimal places, never rounded, as feeEth() does.
|
||||||
function quantity(wei) {
|
function quantity(wei) {
|
||||||
return parseFloat(formatEther(wei)).toFixed(4);
|
const [whole, frac = ""] = formatEther(wei).split(".");
|
||||||
|
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/popup/views/approval");
|
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
||||||
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);
|
||||||
|
|||||||
@@ -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/popup/views/approval");
|
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
||||||
|
|
||||||
const iface = new Interface(ERC20_ABI);
|
const iface = new Interface(ERC20_ABI);
|
||||||
|
|
||||||
|
|||||||
@@ -1361,8 +1361,8 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
expect(txs[0].holders).toBe(3500000);
|
expect(txs[0].holders).toBe(3500000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// README.md, Amounts: a nonzero amount never reads as zero.
|
// README.md, Amounts: a history row cuts to four decimals, with no floor.
|
||||||
test("a token transfer below 0.0001 shows its first nonzero digit", async () => {
|
test("a token transfer below 0.0001 reads 0.0000", async () => {
|
||||||
respondWith(
|
respondWith(
|
||||||
[],
|
[],
|
||||||
[
|
[
|
||||||
@@ -1383,7 +1383,7 @@ describe("fetchRecentTransactions merge and dedup", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
const txs = await fetchRecentTransactions(VICTIM, BLOCKSCOUT, "0x1");
|
||||||
expect(txs[0].value).toBe("0.00004");
|
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 () => {
|
||||||
|
|||||||
@@ -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 } = approval;
|
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
||||||
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/popup/views/approval");
|
const { decodeCalldata } = require("../src/shared/calldataDecoder");
|
||||||
|
|
||||||
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
const ROUTER = "0x66a9893cc07d91d95644aedd05d03f95e1dba8af";
|
||||||
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||||
|
|||||||
Reference in new issue
Block a user