Compare commits
4
Commits
2a918bf6bf
...
3b94a17d35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b94a17d35 | ||
|
|
2da790fbe9 | ||
|
|
9ac7df0128 | ||
|
|
99292b9188 |
@@ -45,6 +45,52 @@ but the review is broader than any of them.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-09-21: The test recovery phrase no longer survives in a release bundle,
|
||||
and the committed-key guard matches by content
|
||||
([#351](https://git.eeqj.de/sneak/AutistMask/issues/351)). `DEBUG_MNEMONIC` in
|
||||
`src/shared/constants.js` is now behind the `__BUILD_DEBUG__` define, so a
|
||||
release build folds the phrase to `null` and no emitted bundle carries it; it
|
||||
used to survive as dead text because `module.exports` keeps the const alive.
|
||||
`script/verify-build` now fails a release build if the phrase appears in any
|
||||
emitted file, so the fold cannot silently regress. `tests/extensionId.test.js`
|
||||
scans the content of every tracked file for a PEM private-key header instead
|
||||
of matching filename extensions alone.
|
||||
- 2026-09-21: A transaction response is honoured only for a transaction
|
||||
approval, and the three remaining approval-settlement paths are pinned
|
||||
([#262](https://git.eeqj.de/sneak/AutistMask/issues/262)). The liveness fix
|
||||
the issue asks for — settle `4001` on release when the window it would be
|
||||
retried in is gone — already landed with
|
||||
[#271](https://git.eeqj.de/sneak/AutistMask/issues/271); this closes the rest.
|
||||
`AUTISTMASK_TX_RESPONSE` now refuses any approval that is not a transaction
|
||||
approval, so a reject no longer retires a sign or connection approval and a
|
||||
signed artifact never runs the broadcast path against one, which before only
|
||||
failed closed by throwing deeper in. Tests pin the site-connection port's
|
||||
approve, reject and disconnect paths against a transaction approval
|
||||
broadcasting behind them: each is declined and the dApp still receives its
|
||||
broadcast result.
|
||||
|
||||
- 2026-09-21: Adding a second wallet no longer accepts a different password with
|
||||
nothing saying it is a separate one
|
||||
([#374](https://git.eeqj.de/sneak/AutistMask/issues/374)). Each wallet has its
|
||||
own encrypted secret, so per-wallet passwords are by design; the add-wallet
|
||||
screen said only "Choose a password". A note now appears on that screen when
|
||||
the profile already holds a wallet, stating that each wallet has its own
|
||||
password and this one need not match any already in use. It is shown only
|
||||
then, since the first wallet has no other password to differ from, and it
|
||||
stays consistent with the no-reset reality of
|
||||
[#312](https://git.eeqj.de/sneak/AutistMask/issues/312) by promising no
|
||||
recovery or reset.
|
||||
|
||||
- 2026-09-21: The dApp approval and transaction-status screens resolve a token's
|
||||
symbol from the bundled list, then the tokens the user tracks, then the block
|
||||
explorer's report — the same sources and precedence the amount line already
|
||||
used for the token's scale
|
||||
([#323](https://git.eeqj.de/sneak/AutistMask/issues/323), folding in
|
||||
[#354](https://git.eeqj.de/sneak/AutistMask/issues/354)). A token the user
|
||||
added by hand, or holds a balance of, is now named rather than labelled
|
||||
`Unknown token`, and a non-bundled ERC-20 is no longer carried onto the wait
|
||||
screen as `ETH`. A tracked or explorer-reported name stays subject to the
|
||||
spoof rule, so resolving a symbol is not a new way to wear a known ticker.
|
||||
- 2026-08-30: An address no longer wraps, or is shortened to fit, in any of the
|
||||
common views ([#380](https://git.eeqj.de/sneak/AutistMask/issues/380)). The
|
||||
wallet list was the reported case: the address shared one row with the
|
||||
|
||||
@@ -37,6 +37,10 @@ DISCARD_DIST="$ROOT/script/discard-dist-on-failure"
|
||||
MARKER_ON="autistmask-build-debug=on"
|
||||
MARKER_OFF="autistmask-build-debug=off"
|
||||
|
||||
# The same test recovery phrase verify-build searches release bundles for. Held
|
||||
# here too, the way the markers above are, so a case can plant it in a bundle.
|
||||
TEST_MNEMONIC="cube evolve unfold result inch risk jealous skill hotel bulb night wreck"
|
||||
|
||||
RECEIPT_HEADER="autistmask-build-receipt v1"
|
||||
|
||||
NEWLINE='
|
||||
@@ -516,6 +520,24 @@ c_debug_build() {
|
||||
write_receipt
|
||||
}
|
||||
|
||||
# A release bundle that still carries the test recovery phrase — the regression
|
||||
# verify-build guards against, and the reason DEBUG_MNEMONIC is behind the
|
||||
# __BUILD_DEBUG__ define in src/shared/constants.js. The receipt is regenerated
|
||||
# so the phrase is caught as bundle content, not incidentally as a stale digest.
|
||||
c_release_bundle_with_mnemonic() {
|
||||
printf '/* %s */\n' "$TEST_MNEMONIC" >>dist/chrome/src/popup/index.js
|
||||
write_receipt
|
||||
}
|
||||
|
||||
# The same phrase in a debug build is expected: make build-debug ships it on
|
||||
# purpose, so the phrase check must stay quiet under --expect debug.
|
||||
c_debug_bundle_with_mnemonic() {
|
||||
write_bundle dist/chrome/src/popup/index.js "$MARKER_ON"
|
||||
write_bundle dist/firefox/src/popup/index.js "$MARKER_ON"
|
||||
printf '/* %s */\n' "$TEST_MNEMONIC" >>dist/chrome/src/popup/index.js
|
||||
write_receipt
|
||||
}
|
||||
|
||||
c_no_dist() { rm -rf dist; }
|
||||
|
||||
# --- dist discard -----------------------------------------------------------
|
||||
@@ -753,6 +775,13 @@ run_cases() {
|
||||
check_case "debug bundles under --expect debug pass" \
|
||||
no debug 0 "2 bundle(s) $MARKER_ON" c_debug_build
|
||||
|
||||
check_case "release bundle carrying the test recovery phrase fails" \
|
||||
no release 1 \
|
||||
"carries the BIP-39 test recovery phrase" c_release_bundle_with_mnemonic
|
||||
|
||||
check_case "debug bundle carrying the test recovery phrase passes" \
|
||||
no debug 0 "2 bundle(s) $MARKER_ON" c_debug_bundle_with_mnemonic
|
||||
|
||||
check_case "no --expect argument" \
|
||||
no no-expect 1 "no --expect argument." c_control
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
# fallback branch; the property only exists in the emitted output, so it has to
|
||||
# be asserted against the emitted output.
|
||||
#
|
||||
# The DEBUG half also checks the phrase directly: a release build must not carry
|
||||
# the test recovery phrase in any emitted file. The phrase is behind the
|
||||
# __BUILD_DEBUG__ define in src/shared/constants.js and folds away in a release
|
||||
# build, but the marker only proves DEBUG compiled off, not that the fold
|
||||
# removed the string; the phrase grep is the assertion that it did.
|
||||
#
|
||||
# Which mode to expect is an ARGUMENT (--expect release|debug) and is never
|
||||
# taken from this script's environment. It used to be read from
|
||||
# AUTISTMASK_DEBUG here, which meant an operator with AUTISTMASK_DEBUG=1
|
||||
@@ -67,6 +73,15 @@ TAB=' '
|
||||
MARKER_ON="autistmask-build-debug=on"
|
||||
MARKER_OFF="autistmask-build-debug=off"
|
||||
|
||||
# The 12-word BIP-39 test recovery phrase from src/shared/constants.js. It is
|
||||
# behind the __BUILD_DEBUG__ define there, so a release build folds it out of
|
||||
# every bundle; this is the assertion that it stayed out. The phrase is a
|
||||
# publicly committed test value rather than a secret, but a BIP-39 phrase in a
|
||||
# distributed wallet artifact is exactly the string a scanner or auditor has to
|
||||
# stop and reason about, so a release build must not ship it. A debug build
|
||||
# ships it on purpose, so this is checked only when release is expected.
|
||||
TEST_MNEMONIC="cube evolve unfold result inch risk jealous skill hotel bulb night wreck"
|
||||
|
||||
RECEIPT_HEADER="autistmask-build-receipt v1"
|
||||
|
||||
# Set by the arguments.
|
||||
@@ -283,6 +298,18 @@ check_entry() {
|
||||
receipt records $ENTRY_HASH and the file on disk is $SHA. Something wrote
|
||||
to dist/ after the build, so this artifact is not the one that was built."
|
||||
|
||||
# No emitted file of a release build may carry the test recovery phrase.
|
||||
# Checked on every file, not only the audited bundles, so a copy that
|
||||
# reached some other emitted file fails here too. A debug build ships the
|
||||
# phrase deliberately, so this runs only when release was expected.
|
||||
if [ "$EXPECT" = "$MARKER_OFF" ] && has_marker "$TEST_MNEMONIC" "$ENTRY_PATH"; then
|
||||
fail "$ENTRY_PATH carries the BIP-39 test recovery phrase, which a
|
||||
release build must fold out. The __BUILD_DEBUG__ define in build.js is what
|
||||
drops it from src/shared/constants.js; check that DEBUG_MNEMONIC is still
|
||||
behind that flag. A recovery phrase in a distributed bundle is exactly the
|
||||
string an auditor or scanner has to stop on, so this is a hard failure."
|
||||
fi
|
||||
|
||||
if [ "$ENTRY_FLAG" = A ]; then
|
||||
read_marker "$ENTRY_PATH"
|
||||
[ "$MARKER" = "$EXPECT" ] ||
|
||||
|
||||
@@ -1344,6 +1344,15 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
const approval = pendingApprovals[msg.id];
|
||||
if (!approval) return false;
|
||||
|
||||
// This message signs and broadcasts a transaction, so it is honoured
|
||||
// only for a transaction approval. A sign or connection approval
|
||||
// carries no approvedTx, and reaching the broadcast path with one used
|
||||
// to fail closed by throwing deeper in; refusing here keeps a future
|
||||
// refactor from turning that incidental throw into a live path, and
|
||||
// keeps a reject on this message from retiring an approval of another
|
||||
// kind.
|
||||
if (approval.type !== "tx") return false;
|
||||
|
||||
// A reject arriving while an attempt holds the approval is refused,
|
||||
// not honoured: the attempt is on its way to broadcasting the
|
||||
// transaction, and resolving 4001 here would tell the page the request
|
||||
|
||||
@@ -152,6 +152,21 @@
|
||||
|
||||
<!-- Shared password fields -->
|
||||
<div class="mb-2" id="add-wallet-password-section">
|
||||
<!-- Shown only when the profile already holds a wallet:
|
||||
each wallet has its own password (its own
|
||||
encryptedSecret), so a second wallet does not reuse
|
||||
the first one's. addWallet.js toggles this on screen
|
||||
entry from state.wallets.length, so it is constant
|
||||
while the screen is up and moves nothing. -->
|
||||
<p
|
||||
class="text-xs mb-2 border border-border border-dashed p-2 hidden"
|
||||
id="add-wallet-separate-password-note"
|
||||
>
|
||||
You already have a wallet. Each wallet has its own
|
||||
password: the one you choose here is only for this new
|
||||
wallet, and it need not match any password you already
|
||||
use.
|
||||
</p>
|
||||
<label class="block mb-1">Choose a password</label>
|
||||
<!-- The hint is swapped in place when the import tab
|
||||
changes, and it sits directly above the password
|
||||
|
||||
@@ -100,9 +100,24 @@ function clear() {
|
||||
$("add-wallet-phrase-warning").style.visibility = "hidden";
|
||||
}
|
||||
|
||||
// Each wallet has its own password (its own encryptedSecret), so adding a
|
||||
// second wallet does not reuse the first one's. The note that says so is
|
||||
// only meaningful once a wallet exists — on the first wallet there is no
|
||||
// other password to be separate from — so it is shown only then. This is
|
||||
// decided on entry and stays put while the screen is up, so it does not
|
||||
// move the password fields the way a per-tab hint would.
|
||||
function updateSeparatePasswordNote() {
|
||||
const hasExistingWallet = state.wallets.length > 0;
|
||||
$("add-wallet-separate-password-note").classList.toggle(
|
||||
"hidden",
|
||||
!hasExistingWallet,
|
||||
);
|
||||
}
|
||||
|
||||
function show() {
|
||||
clear();
|
||||
switchMode("mnemonic");
|
||||
updateSeparatePasswordNote();
|
||||
showView("add-wallet");
|
||||
}
|
||||
|
||||
|
||||
+32
-21
@@ -20,9 +20,9 @@ const {
|
||||
} = require("ethers");
|
||||
const { getPrice, formatUsd } = require("../../shared/prices");
|
||||
const { ERC20_ABI } = require("../../shared/constants");
|
||||
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
|
||||
const {
|
||||
resolveTokenDecimals,
|
||||
resolveTokenSymbol,
|
||||
unknownDecimalsAmount,
|
||||
} = require("../../shared/approvalAmount");
|
||||
// Four decimals, with the nonzero floor these screens hold: every amount this
|
||||
@@ -63,9 +63,15 @@ function tokenAmountText(rawAmount, decimals, symbol) {
|
||||
};
|
||||
}
|
||||
|
||||
// The symbol shown for a token line, resolved from the bundled list, the
|
||||
// tokens the user tracks, and the explorer's report — the same chain the
|
||||
// amount line's scale comes from. Null when no source names one, so the token
|
||||
// lines keep saying `Unknown token` for a token nothing knows.
|
||||
function tokenLabel(address) {
|
||||
const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
|
||||
return t ? t.symbol : null;
|
||||
return resolveTokenSymbol(address, {
|
||||
trackedTokens: state.trackedTokens,
|
||||
wallets: state.wallets,
|
||||
});
|
||||
}
|
||||
|
||||
// Try to decode calldata using known ABIs.
|
||||
@@ -85,8 +91,7 @@ function decodeCalldata(data, toAddress) {
|
||||
try {
|
||||
const parsed = erc20Iface.parseTransaction({ data });
|
||||
if (parsed) {
|
||||
const token = TOKEN_BY_ADDRESS.get(toAddress.toLowerCase());
|
||||
const tokenSymbol = token ? token.symbol : null;
|
||||
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
|
||||
@@ -242,8 +247,11 @@ function showTxApproval(details) {
|
||||
const approvedTx = details.approvedTx;
|
||||
|
||||
const toAddr = approvedTx.to;
|
||||
const token = toAddr ? TOKEN_BY_ADDRESS.get(toAddr.toLowerCase()) : null;
|
||||
const ethValue = formatEther(approvedTx.value || "0");
|
||||
const sources = {
|
||||
trackedTokens: state.trackedTokens,
|
||||
wallets: state.wallets,
|
||||
};
|
||||
|
||||
// Build txInfo for status screens
|
||||
pendingTxDetails = {
|
||||
@@ -251,14 +259,17 @@ function showTxApproval(details) {
|
||||
to: toAddr || "",
|
||||
amount: formatTxValue(ethValue),
|
||||
token: "ETH",
|
||||
tokenSymbol: token ? token.symbol : null,
|
||||
tokenSymbol: null,
|
||||
};
|
||||
|
||||
// If this is an ERC-20 call, try to extract the real recipient and amount
|
||||
const decoded = decodeCalldata(approvedTx.data, toAddr || "");
|
||||
if (decoded && decoded.details) {
|
||||
let decodedTokenAddr = null;
|
||||
let decodedTokenSymbol = null;
|
||||
// The asset the status summary is counted in: an ERC-20 call's Token
|
||||
// contract, or a swap's input token. Its symbol is resolved from the
|
||||
// same sources as the approval screen, so a non-bundled token the
|
||||
// wallet knows is not carried onto the wait and success screens as ETH.
|
||||
let assetAddr = null;
|
||||
for (const d of decoded.details) {
|
||||
if (d.label === "Recipient" && d.address) {
|
||||
pendingTxDetails.to = d.address;
|
||||
@@ -266,20 +277,20 @@ function showTxApproval(details) {
|
||||
if (d.label === "Amount") {
|
||||
pendingTxDetails.amount = d.rawValue || d.value;
|
||||
}
|
||||
if (d.label === "Token In" && d.isToken && d.address) {
|
||||
const t = TOKEN_BY_ADDRESS.get(d.address.toLowerCase());
|
||||
if (t) {
|
||||
decodedTokenAddr = d.address;
|
||||
decodedTokenSymbol = t.symbol;
|
||||
if (
|
||||
(d.label === "Token" || d.label === "Token In") &&
|
||||
d.isToken &&
|
||||
d.address
|
||||
) {
|
||||
assetAddr = d.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (token) {
|
||||
pendingTxDetails.token = toAddr;
|
||||
pendingTxDetails.tokenSymbol = token.symbol;
|
||||
} else if (decodedTokenAddr) {
|
||||
pendingTxDetails.token = decodedTokenAddr;
|
||||
pendingTxDetails.tokenSymbol = decodedTokenSymbol;
|
||||
if (assetAddr) {
|
||||
pendingTxDetails.token = assetAddr;
|
||||
pendingTxDetails.tokenSymbol = resolveTokenSymbol(
|
||||
assetAddr,
|
||||
sources,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const {
|
||||
displaySymbol,
|
||||
clearViewStack,
|
||||
} = require("./helpers");
|
||||
const { TOKEN_BY_ADDRESS } = require("../../shared/tokenList");
|
||||
const { resolveTokenSymbol } = require("../../shared/approvalAmount");
|
||||
const { state } = require("../../shared/state");
|
||||
const { getProvider } = require("../../shared/balances");
|
||||
const { log } = require("../../shared/log");
|
||||
@@ -232,9 +232,15 @@ function showSuccess(txInfo, txHash, blockNumber) {
|
||||
ctx.doRefreshAndRender();
|
||||
}
|
||||
|
||||
// The symbol shown for a decoded token line, resolved from the bundled list,
|
||||
// the tokens the user tracks, and the explorer's report — the same chain the
|
||||
// approval screen uses. Null when no source names one, so the line keeps
|
||||
// saying `Unknown token`.
|
||||
function tokenLabel(address) {
|
||||
const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
|
||||
return t ? t.symbol : null;
|
||||
return resolveTokenSymbol(address, {
|
||||
trackedTokens: state.trackedTokens,
|
||||
wallets: state.wallets,
|
||||
});
|
||||
}
|
||||
|
||||
function decodedDetailsHtml(decoded) {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
// enumerated rather than coerced.
|
||||
const { toDecimals } = require("./transferAmount");
|
||||
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||
const { isSpoofedSymbol } = require("./symbolSpoof");
|
||||
|
||||
// Every decimals the explorer reported for this contract, across all the
|
||||
// addresses whose balances have been fetched. They describe one contract, so
|
||||
@@ -74,6 +75,59 @@ function resolveTokenDecimals(tokenAddress, sources) {
|
||||
return explorerDecimals(lower, sources && sources.wallets);
|
||||
}
|
||||
|
||||
// Every symbol the explorer reported for this contract, across the addresses
|
||||
// whose balances have been fetched. The counterpart to explorerDecimals(): one
|
||||
// contract, so the reports should agree, and a set that does not agree is a
|
||||
// name this screen has no way to choose between.
|
||||
function explorerSymbol(lower, wallets) {
|
||||
let found = null;
|
||||
for (const wallet of wallets || []) {
|
||||
for (const addr of wallet.addresses || []) {
|
||||
for (const tb of addr.tokenBalances || []) {
|
||||
if ((tb.address || "").toLowerCase() !== lower) continue;
|
||||
if (!tb.symbol) continue;
|
||||
if (found !== null && found !== tb.symbol) return null;
|
||||
found = tb.symbol;
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// The symbol to label a token with, or null when no source the wallet trusts
|
||||
// names one — in which case the screen keeps saying `Unknown token` rather than
|
||||
// guessing. The bundled list, then the tokens the user tracks, then what the
|
||||
// explorer reported: the same sources and the same precedence
|
||||
// resolveTokenDecimals() uses, so a token's name and its scale are drawn from
|
||||
// the same place and the two can no longer disagree about which sources they
|
||||
// trust. `sources` is { trackedTokens, wallets }, shaped as on `state`.
|
||||
//
|
||||
// A tracked or explorer-reported symbol is attacker-influenced text, so it is
|
||||
// held to the spoof rule (symbolSpoof.js): a candidate that wears a bundled or
|
||||
// native ticker from a contract not entitled to it is refused and the next
|
||||
// source tried, so resolving a symbol never becomes a new way to claim a known
|
||||
// ticker. The bundled list is the wallet's own data and is trusted as it is.
|
||||
function resolveTokenSymbol(tokenAddress, sources) {
|
||||
const lower = (tokenAddress || "").toLowerCase();
|
||||
if (!lower) return null;
|
||||
|
||||
const bundled = TOKEN_BY_ADDRESS.get(lower);
|
||||
if (bundled && bundled.symbol) return bundled.symbol;
|
||||
|
||||
const tracked = ((sources && sources.trackedTokens) || []).find(
|
||||
(t) => (t.address || "").toLowerCase() === lower,
|
||||
);
|
||||
const candidates = [];
|
||||
if (tracked && tracked.symbol) candidates.push(tracked.symbol);
|
||||
const reported = explorerSymbol(lower, sources && sources.wallets);
|
||||
if (reported) candidates.push(reported);
|
||||
|
||||
for (const symbol of candidates) {
|
||||
if (!isSpoofedSymbol(symbol, tokenAddress)) return symbol;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// What the amount line reads when the scale is unknown. The base units are
|
||||
// exact and the caveat is part of the same string, so the number on the screen
|
||||
// cannot be mistaken for a token quantity, and it can never read as zero for a
|
||||
@@ -84,5 +138,6 @@ function unknownDecimalsAmount(rawAmount) {
|
||||
|
||||
module.exports = {
|
||||
resolveTokenDecimals,
|
||||
resolveTokenSymbol,
|
||||
unknownDecimalsAmount,
|
||||
};
|
||||
|
||||
@@ -22,8 +22,15 @@ const BUILD_DEBUG_MARKER = DEBUG
|
||||
? "autistmask-build-debug=on"
|
||||
: "autistmask-build-debug=off";
|
||||
|
||||
const DEBUG_MNEMONIC =
|
||||
"cube evolve unfold result inch risk jealous skill hotel bulb night wreck";
|
||||
// Behind DEBUG for the same reason BUILD_DEBUG_MARKER is above: in a release
|
||||
// build __BUILD_DEBUG__ is a compile-time false, esbuild drops this branch, and
|
||||
// the phrase never reaches a distributed bundle. The literal used to survive as
|
||||
// dead text because module.exports keeps this const live even though wallet.js's
|
||||
// only use of it is folded away; making the value itself fold to null removes
|
||||
// it. script/verify-build fails a release build if the phrase appears anyway.
|
||||
const DEBUG_MNEMONIC = DEBUG
|
||||
? "cube evolve unfold result inch risk jealous skill hotel bulb night wreck"
|
||||
: null;
|
||||
|
||||
const ETHEREUM_MAINNET_CHAIN_ID = "0x1";
|
||||
const ETHEREUM_SEPOLIA_CHAIN_ID = "0xaa36a7";
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// swap details. Designed to be extended with other DEX decoders later.
|
||||
|
||||
const { Interface, AbiCoder, getBytes, formatUnits } = require("ethers");
|
||||
const { TOKEN_BY_ADDRESS } = require("./tokenList");
|
||||
const { truncateAmountNeverZero } = require("./amountDisplay");
|
||||
const {
|
||||
resolveTokenDecimals,
|
||||
resolveTokenSymbol,
|
||||
unknownDecimalsAmount,
|
||||
} = require("./approvalAmount");
|
||||
|
||||
@@ -123,9 +123,8 @@ function tokenInfo(address, sources) {
|
||||
if (address === "0x0000000000000000000000000000000000000000") {
|
||||
return { symbol: "ETH", decimals: 18, address: null };
|
||||
}
|
||||
const t = TOKEN_BY_ADDRESS.get(address.toLowerCase());
|
||||
return {
|
||||
symbol: t ? t.symbol : null,
|
||||
symbol: resolveTokenSymbol(address, sources),
|
||||
decimals: resolveTokenDecimals(address, sources),
|
||||
address,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Adding a second wallet accepts a password different from the first one's
|
||||
// with nothing on screen saying the two are separate — each wallet has its
|
||||
// own encryptedSecret, so per-wallet passwords are by design, but the add
|
||||
// screen said only "Choose a password"
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/374).
|
||||
//
|
||||
// The fix is copy: a note on the password screen that says each wallet has
|
||||
// its own password and this one need not match. It is only meaningful once
|
||||
// a wallet exists — on the very first wallet there is no other password to
|
||||
// be separate from — so it is shown then and hidden otherwise. These boot
|
||||
// the real popup and reach the add-wallet screen through the same button a
|
||||
// user presses, so the note's visibility is decided by the real show().
|
||||
|
||||
const {
|
||||
bootPopup,
|
||||
cleanupPopup,
|
||||
unversionedValidProfile,
|
||||
POPUP_HTML,
|
||||
} = require("./support/popupBoot");
|
||||
|
||||
const NOTE = "add-wallet-separate-password-note";
|
||||
|
||||
afterEach(() => {
|
||||
cleanupPopup();
|
||||
});
|
||||
|
||||
describe("second-wallet password note", () => {
|
||||
test("hidden while onboarding the first wallet", async () => {
|
||||
const page = await bootPopup(undefined);
|
||||
expect(page.pageErrors).toEqual([]);
|
||||
await page.click("btn-welcome-add");
|
||||
expect(page.visibleViews()).toContain("add-wallet");
|
||||
expect(page.hidden(NOTE)).toBe(true);
|
||||
});
|
||||
|
||||
test("shown when a wallet already exists", async () => {
|
||||
const page = await bootPopup(unversionedValidProfile());
|
||||
expect(page.pageErrors).toEqual([]);
|
||||
await page.click("btn-main-add-wallet");
|
||||
expect(page.visibleViews()).toContain("add-wallet");
|
||||
expect(page.hidden(NOTE)).toBe(false);
|
||||
});
|
||||
|
||||
// The copy states the two facts the definition of done asks for — each
|
||||
// wallet has its own password, and this one need not match — and stays
|
||||
// consistent with the no-password-reset reality of
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/312 by not promising any
|
||||
// recovery or reset here.
|
||||
test("the note says the password is per-wallet and need not match", () => {
|
||||
const note = /id="add-wallet-separate-password-note"[^>]*>([^]*?)<\/p>/
|
||||
.exec(POPUP_HTML)[1]
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
expect(note).toContain("its own");
|
||||
expect(note).toContain("need not match");
|
||||
expect(note).not.toMatch(/recover|reset/i);
|
||||
});
|
||||
});
|
||||
@@ -143,16 +143,18 @@ describe("decodeCalldata amount", () => {
|
||||
state.trackedTokens = [
|
||||
{ address: NOVEL_TOKEN, symbol: "NOVEL", decimals: 6 },
|
||||
];
|
||||
// The tracked entry supplies both: the scale (5000.0000) and, since
|
||||
// issue #323, the symbol that the scale is counted in.
|
||||
expect(
|
||||
amountLine(transferData(FIVE_THOUSAND_AT_SIX), NOVEL_TOKEN),
|
||||
).toBe("5000.0000");
|
||||
).toBe("5000.0000 NOVEL");
|
||||
});
|
||||
|
||||
test("transfer priced off the explorer's decimals shows the true quantity", () => {
|
||||
state.wallets = walletsHolding(NOVEL_TOKEN, "6");
|
||||
expect(
|
||||
amountLine(transferData(FIVE_THOUSAND_AT_SIX), NOVEL_TOKEN),
|
||||
).toBe("5000.0000");
|
||||
).toBe("5000.0000 NOVEL");
|
||||
});
|
||||
|
||||
test("transfer of an unknown-decimals token shows base units, not a number", () => {
|
||||
@@ -172,7 +174,7 @@ describe("decodeCalldata amount", () => {
|
||||
{ address: NOVEL_TOKEN, symbol: "NOVEL", decimals: 6 },
|
||||
];
|
||||
expect(amountLine(approveData(FIVE_THOUSAND_AT_SIX), NOVEL_TOKEN)).toBe(
|
||||
"5000.0000",
|
||||
"5000.0000 NOVEL",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1541,6 +1541,149 @@ describe("a claimed approval outlives every other retirement path", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// The approval popup connects a port named for its approval whatever the
|
||||
// approval's kind, so a decision or a disconnect on that port can reach a
|
||||
// transaction approval. Both must be declined: the port decides only
|
||||
// site-connection approvals, and settling a transaction approval it does not
|
||||
// own — while an attempt is broadcasting behind it — is the round-3 fund-loss
|
||||
// bug, where the page is told the request was rejected as the transaction goes
|
||||
// out. These three paths route through settleApproval() and, before this
|
||||
// suite, were exercised only against site approvals.
|
||||
describe("the site-connection port never retires a transaction approval", () => {
|
||||
async function txMidBroadcast() {
|
||||
const bg = loadBackground();
|
||||
const pending = bg.requestTx();
|
||||
await settle();
|
||||
const id = pending.id();
|
||||
|
||||
const inFlight = deferred();
|
||||
bg.broadcastTransaction.mockReturnValue(inFlight.promise);
|
||||
const first = bg.send(
|
||||
{
|
||||
type: "AUTISTMASK_TX_RESPONSE",
|
||||
id,
|
||||
approved: true,
|
||||
rawSignedTx: await signedAtNonce(7),
|
||||
},
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
await settle();
|
||||
expect(bg.broadcastTransaction).toHaveBeenCalledTimes(1);
|
||||
return { bg, pending, id, inFlight, first };
|
||||
}
|
||||
|
||||
test("an approve on the port does not settle it", async () => {
|
||||
const { bg, pending, id, inFlight, first } = await txMidBroadcast();
|
||||
|
||||
bg.connectApproval(id).decide(true, false);
|
||||
await settle();
|
||||
expect(pending.result()).toBeNull();
|
||||
|
||||
inFlight.resolve({ hash: "0xfeed" });
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({ result: "0xfeed" });
|
||||
expect(first.sendResponse).toHaveBeenCalledWith({ txHash: "0xfeed" });
|
||||
});
|
||||
|
||||
test("a reject on the port does not settle it", async () => {
|
||||
const { bg, pending, id, inFlight, first } = await txMidBroadcast();
|
||||
|
||||
bg.connectApproval(id).decide(false, false);
|
||||
await settle();
|
||||
expect(pending.result()).toBeNull();
|
||||
|
||||
inFlight.resolve({ hash: "0xfeed" });
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({ result: "0xfeed" });
|
||||
expect(first.sendResponse).toHaveBeenCalledWith({ txHash: "0xfeed" });
|
||||
});
|
||||
|
||||
test("a port disconnect does not settle it", async () => {
|
||||
const { bg, pending, id, inFlight, first } = await txMidBroadcast();
|
||||
|
||||
bg.connectApproval(id).disconnect();
|
||||
await settle();
|
||||
expect(pending.result()).toBeNull();
|
||||
|
||||
inFlight.resolve({ hash: "0xfeed" });
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({ result: "0xfeed" });
|
||||
expect(first.sendResponse).toHaveBeenCalledWith({ txHash: "0xfeed" });
|
||||
});
|
||||
});
|
||||
|
||||
// AUTISTMASK_TX_RESPONSE signs and broadcasts a transaction, so it is honoured
|
||||
// only for a transaction approval. A reject shaped as this message used to
|
||||
// retire a sign or connection approval outright, and an approve carrying a
|
||||
// signed artifact used to run the broadcast path against an approval that names
|
||||
// no transaction, failing closed only by throwing deeper in.
|
||||
describe("a transaction response is honoured only for a transaction approval", () => {
|
||||
test("a reject does not retire a sign approval", async () => {
|
||||
const bg = loadBackground();
|
||||
const pending = bg.requestSign();
|
||||
await settle();
|
||||
const id = pending.id();
|
||||
|
||||
bg.send(
|
||||
{ type: "AUTISTMASK_TX_RESPONSE", id, approved: false },
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
await settle();
|
||||
expect(pending.result()).toBeNull();
|
||||
|
||||
// Still live: its own reject settles it.
|
||||
bg.send(
|
||||
{ type: "AUTISTMASK_SIGN_RESPONSE", id, approved: false },
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({
|
||||
error: { code: 4001, message: "User rejected the request." },
|
||||
});
|
||||
});
|
||||
|
||||
test("a reject does not retire a connection approval", async () => {
|
||||
const bg = loadBackground({ actionPopup: true });
|
||||
const pending = bg.requestSite();
|
||||
await settle();
|
||||
const id = pending.id();
|
||||
|
||||
bg.send(
|
||||
{ type: "AUTISTMASK_TX_RESPONSE", id, approved: false },
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
await settle();
|
||||
expect(pending.result()).toBeNull();
|
||||
|
||||
// Still live: the port that owns it connects the site.
|
||||
const port = bg.connectApproval(id);
|
||||
port.decide(true, false);
|
||||
port.disconnect();
|
||||
await settle();
|
||||
expect(pending.result()).toEqual({ result: [signer.address] });
|
||||
});
|
||||
|
||||
test("an approve carrying a signed transaction never broadcasts against a sign approval", async () => {
|
||||
const bg = loadBackground();
|
||||
const pending = bg.requestSign();
|
||||
await settle();
|
||||
const id = pending.id();
|
||||
|
||||
bg.send(
|
||||
{
|
||||
type: "AUTISTMASK_TX_RESPONSE",
|
||||
id,
|
||||
approved: true,
|
||||
rawSignedTx: await signedAtNonce(7),
|
||||
},
|
||||
{ url: bg.fromPopup.url },
|
||||
);
|
||||
await settle();
|
||||
expect(bg.broadcastTransaction).not.toHaveBeenCalled();
|
||||
expect(pending.result()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// A handler that throws must still answer. `sendResponse` is the only thing
|
||||
// that settles the page's window.ethereum.request() promise, so a throw that
|
||||
// escapes a handler leaves that promise pending forever — no error, no
|
||||
|
||||
@@ -89,19 +89,28 @@ describe("chrome extension identity", () => {
|
||||
|
||||
// The private half is a credential. It has never been in this repo and no
|
||||
// target generates one into the working tree; this fails loudly if that
|
||||
// ever changes, because a committed .pem is a key anyone can sign a CRX
|
||||
// with under this extension's id.
|
||||
// ever changes, because a committed private key is one anyone can sign a
|
||||
// CRX with under this extension's id.
|
||||
//
|
||||
// Matched by CONTENT, not by filename: a key committed as notes.txt or with
|
||||
// no extension carries the same risk as one named key.pem, and a
|
||||
// filename-only check waves it through. The PEM header a private key opens
|
||||
// with is the signature searched for. The pattern does not trip on its own
|
||||
// source: the bracket-expression characters between the two anchors are not
|
||||
// in the character class, so this file is not a match for it.
|
||||
const PRIVATE_KEY_HEADER = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/;
|
||||
test("no private key is committed anywhere in the tree", () => {
|
||||
const root = path.join(__dirname, "..");
|
||||
const tracked = require("child_process")
|
||||
.execSync("git ls-files", {
|
||||
cwd: path.join(__dirname, ".."),
|
||||
encoding: "utf8",
|
||||
})
|
||||
.execSync("git ls-files", { cwd: root, encoding: "utf8" })
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
expect(tracked.filter((f) => /\.(pem|key|p12|pfx)$/i.test(f))).toEqual(
|
||||
[],
|
||||
const offenders = tracked.filter((f) =>
|
||||
PRIVATE_KEY_HEADER.test(
|
||||
fs.readFileSync(path.join(root, f), "latin1"),
|
||||
),
|
||||
);
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -253,8 +253,9 @@ describe("the ERC-20 approval line reaches its refusal", () => {
|
||||
|
||||
test("a scale the explorer did report still formats", async () => {
|
||||
await fetchOnto([row({ decimals: "6" })]);
|
||||
// The same explorer entry now also names the token (issue #323).
|
||||
expect(erc20AmountLine(transferData(THOUSAND_AT_SIX), NOVEL)).toBe(
|
||||
"1000.0000",
|
||||
"1000.0000 NOVEL",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -273,7 +274,7 @@ describe("the swap approval line reaches its refusal", () => {
|
||||
await fetchOnto([row({ decimals: "6" })]);
|
||||
expect(
|
||||
swapAmountLine(swapData(NOVEL, THOUSAND_AT_SIX, WETH, HALF_WETH)),
|
||||
).toBe("1000.0000");
|
||||
).toBe("1000.0000 NOVEL");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// The symbol the dApp approval and status screens label a token with.
|
||||
//
|
||||
// Issue #323: the approval screen labelled anything outside the bundled list
|
||||
// `Unknown token`, even a token the user tracks or holds a balance of, while
|
||||
// the amount line already read that token's *scale* from those same sources
|
||||
// (issue #306). The name and the scale disagreed about which sources they
|
||||
// trust. resolveTokenSymbol() closes that gap: it draws the symbol from the
|
||||
// bundled list, then the tracked tokens, then the explorer's report — the
|
||||
// precedence resolveTokenDecimals() uses — and returns null, not a guess,
|
||||
// when nothing names it, so the screens keep saying `Unknown token`.
|
||||
//
|
||||
// A tracked or explorer-reported symbol is attacker-influenced text, so it
|
||||
// stays subject to the spoof rule (src/shared/symbolSpoof.js): resolving a
|
||||
// symbol must not become a new way for a stray contract to wear a bundled or
|
||||
// native ticker.
|
||||
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
};
|
||||
|
||||
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/popup/views/approval");
|
||||
|
||||
const iface = new Interface(ERC20_ABI);
|
||||
|
||||
// Outside the bundled list, as the great majority of ERC-20s are.
|
||||
const NOVEL_TOKEN = "0xE2E0000000000000000000000000000000000E2e";
|
||||
// In the bundled list: USDC at 6 decimals, DAI at 18.
|
||||
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
||||
const RECIPIENT = "0xC0FfEE0000000000000000000000000000c0fFEe";
|
||||
const FIVE_THOUSAND_AT_SIX = 5000000000n;
|
||||
|
||||
function transferData(amount) {
|
||||
return iface.encodeFunctionData("transfer", [RECIPIENT, amount]);
|
||||
}
|
||||
|
||||
// A wallet whose block-explorer balance for `token` reports `symbol`, shaped
|
||||
// as balances.js writes it onto state.
|
||||
function walletsReporting(token, symbol) {
|
||||
return [
|
||||
{
|
||||
name: "Wallet 1",
|
||||
addresses: [
|
||||
{
|
||||
address: "0x" + "a".repeat(40),
|
||||
balance: "1.0",
|
||||
tokenBalances: [
|
||||
{
|
||||
address: token,
|
||||
symbol,
|
||||
decimals: 6,
|
||||
balance: "5000.0",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
state.trackedTokens = [];
|
||||
state.wallets = [];
|
||||
});
|
||||
|
||||
describe("resolveTokenSymbol", () => {
|
||||
test("reads the bundled list", () => {
|
||||
expect(resolveTokenSymbol(USDC, state)).toBe("USDC");
|
||||
});
|
||||
|
||||
test("prefers the bundled list over a tracked entry", () => {
|
||||
state.trackedTokens = [{ address: USDC, symbol: "NOTUSDC" }];
|
||||
expect(resolveTokenSymbol(USDC, state)).toBe("USDC");
|
||||
});
|
||||
|
||||
test("reads a token the user tracks", () => {
|
||||
state.trackedTokens = [{ address: NOVEL_TOKEN, symbol: "NOVEL" }];
|
||||
expect(resolveTokenSymbol(NOVEL_TOKEN, state)).toBe("NOVEL");
|
||||
});
|
||||
|
||||
test("reads the symbol the explorer reported", () => {
|
||||
state.wallets = walletsReporting(NOVEL_TOKEN, "NOVEL");
|
||||
expect(resolveTokenSymbol(NOVEL_TOKEN, state)).toBe("NOVEL");
|
||||
});
|
||||
|
||||
test("is null when no source names the token", () => {
|
||||
expect(resolveTokenSymbol(NOVEL_TOKEN, state)).toBeNull();
|
||||
});
|
||||
|
||||
test("refuses a name the explorer's own entries disagree about", () => {
|
||||
const wallets = walletsReporting(NOVEL_TOKEN, "NOVEL");
|
||||
wallets[0].addresses.push({
|
||||
address: "0x" + "b".repeat(40),
|
||||
balance: "0.0",
|
||||
tokenBalances: [{ address: NOVEL_TOKEN, symbol: "OTHER" }],
|
||||
});
|
||||
state.wallets = wallets;
|
||||
expect(resolveTokenSymbol(NOVEL_TOKEN, state)).toBeNull();
|
||||
});
|
||||
|
||||
test("rejects a tracked entry claiming a bundled ticker it is not", () => {
|
||||
// NOVEL_TOKEN is not the real USDC contract, so it may not wear USDC.
|
||||
state.trackedTokens = [{ address: NOVEL_TOKEN, symbol: "USDC" }];
|
||||
expect(resolveTokenSymbol(NOVEL_TOKEN, state)).toBeNull();
|
||||
});
|
||||
|
||||
test("rejects an explorer entry claiming the native ETH ticker", () => {
|
||||
state.wallets = walletsReporting(NOVEL_TOKEN, "ETH");
|
||||
expect(resolveTokenSymbol(NOVEL_TOKEN, state)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("decodeCalldata symbol", () => {
|
||||
test("a tracked token is named, not called Unknown", () => {
|
||||
state.trackedTokens = [
|
||||
{ address: NOVEL_TOKEN, symbol: "NOVEL", decimals: 6 },
|
||||
];
|
||||
const decoded = decodeCalldata(
|
||||
transferData(FIVE_THOUSAND_AT_SIX),
|
||||
NOVEL_TOKEN,
|
||||
);
|
||||
expect(decoded.description).toBe("Transfer NOVEL");
|
||||
const amount = decoded.details.find((d) => d.label === "Amount");
|
||||
expect(amount.value).toBe("5000.0000 NOVEL");
|
||||
});
|
||||
|
||||
test("a token nothing knows keeps a symbol-less label", () => {
|
||||
const decoded = decodeCalldata(
|
||||
transferData(FIVE_THOUSAND_AT_SIX),
|
||||
NOVEL_TOKEN,
|
||||
);
|
||||
expect(decoded.description).toBe("Transfer ERC-20 token");
|
||||
const token = decoded.details.find((d) => d.label === "Token");
|
||||
// The Token line carries the address and is flagged for the screen's
|
||||
// symbol lookup, which resolves to nothing here — so `Unknown token`.
|
||||
expect(token.isToken).toBe(true);
|
||||
expect(token.address).toBe(NOVEL_TOKEN);
|
||||
expect(resolveTokenSymbol(token.address, state)).toBeNull();
|
||||
});
|
||||
|
||||
test("a tracked token spoofing a bundled ticker is not named by it", () => {
|
||||
state.trackedTokens = [
|
||||
{ address: NOVEL_TOKEN, symbol: "USDC", decimals: 6 },
|
||||
];
|
||||
const decoded = decodeCalldata(
|
||||
transferData(FIVE_THOUSAND_AT_SIX),
|
||||
NOVEL_TOKEN,
|
||||
);
|
||||
expect(decoded.description).toBe("Transfer ERC-20 token");
|
||||
const amount = decoded.details.find((d) => d.label === "Amount");
|
||||
expect(amount.value).not.toMatch(/USDC/);
|
||||
});
|
||||
});
|
||||
@@ -78,15 +78,20 @@ describe("a swap to a token absent from the bundled list", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("names the address when the scale is known but the symbol is not", () => {
|
||||
test("names the tracked symbol alongside the address (issue #323)", () => {
|
||||
// The tracked entry supplies both halves now: the scale, and the
|
||||
// symbol the output line is named by. Before #323 the symbol was read
|
||||
// from the bundled list alone, so this line fell back to the address.
|
||||
const sources = {
|
||||
trackedTokens: [
|
||||
{ address: NOVEL_OUT, symbol: "NOVEL", decimals: 6 },
|
||||
],
|
||||
};
|
||||
expect(detail(data(), "Token Out", sources).value).toBe(NOVEL_OUT);
|
||||
expect(detail(data(), "Token Out", sources).value).toBe(
|
||||
"NOVEL (" + NOVEL_OUT + ")",
|
||||
);
|
||||
expect(detail(data(), "Min. received", sources).value).toBe(
|
||||
"1000.0000",
|
||||
"1000.0000 NOVEL",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,12 +98,13 @@ describe("a swap of a token outside the bundled list", () => {
|
||||
state.trackedTokens = [
|
||||
{ address: NOVEL, symbol: "NOVEL", decimals: 6 },
|
||||
];
|
||||
expect(swapDetail(data(), "Amount").value).toBe("1000.0000");
|
||||
// The tracked entry names the token as well as scaling it (issue #323).
|
||||
expect(swapDetail(data(), "Amount").value).toBe("1000.0000 NOVEL");
|
||||
});
|
||||
|
||||
test("shows the true quantity from the explorer's decimals", () => {
|
||||
state.wallets = walletsHolding(NOVEL, "6");
|
||||
expect(swapDetail(data(), "Amount").value).toBe("1000.0000");
|
||||
expect(swapDetail(data(), "Amount").value).toBe("1000.0000 NOVEL");
|
||||
});
|
||||
|
||||
test("refuses to format when nothing knows the scale", () => {
|
||||
@@ -140,7 +141,7 @@ describe("the Min. received line takes the same rule", () => {
|
||||
{ address: NOVEL_OUT, symbol: "NOVEL", decimals: 6 },
|
||||
];
|
||||
const data = swapData(WETH, HALF_WETH, NOVEL_OUT, THOUSAND_AT_SIX);
|
||||
expect(swapDetail(data, "Min. received").value).toBe("1000.0000");
|
||||
expect(swapDetail(data, "Min. received").value).toBe("1000.0000 NOVEL");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,14 @@ describe("generateMnemonic in a release build", () => {
|
||||
expect(constants.DEBUG).toBe(false);
|
||||
});
|
||||
|
||||
test("the test phrase folds away when DEBUG is false", () => {
|
||||
// The release bundle is what must not carry the phrase; here, with the
|
||||
// define absent, DEBUG_MNEMONIC is the null branch the bundler keeps,
|
||||
// and the literal only exists in the branch it drops.
|
||||
const { constants } = loadWallet();
|
||||
expect(constants.DEBUG_MNEMONIC).toBeNull();
|
||||
});
|
||||
|
||||
test("returns fresh, valid 12-word phrases that are not the test phrase", () => {
|
||||
const { constants, wallet } = loadWallet();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user