fix: Blockscout and Etherscan lookups are off until the user turns them on (closes #668)
Out of the box the wallet sent the user's addresses to Blockscout and each recipient to Etherscan. Two settings, "Use Blockscout" and "Check recipients on Etherscan", now gate those requests. Both are off in a new install and offered unticked on Welcome and in Settings. While one is off, the screens that relied on it say so in a plain line. Each gated request reads its setting from storage before it asks and when the answer lands, and drops a late answer. An open popup takes another page's change and draws its screens again. While "Use Blockscout" is off, no address keeps an untracked token, and no screen names a token from Blockscout's report. ENS lookups ask only the RPC endpoint. Model: opus-5-5 (implementation); fable-5-1 (prose)
This commit is contained in:
@@ -41,6 +41,8 @@ jest.mock("../src/shared/balances", () => ({
|
||||
refreshBalances: jest.fn(async () => {
|
||||
mockBalanceRefreshAt.push(Date.now());
|
||||
}),
|
||||
dropUntrackedTokens: jest.requireActual("../src/shared/balances")
|
||||
.dropUntrackedTokens,
|
||||
getProvider: jest.fn(() => ({})),
|
||||
}));
|
||||
|
||||
|
||||
@@ -84,6 +84,9 @@ function walletsHolding(token, decimals) {
|
||||
beforeEach(() => {
|
||||
state.trackedTokens = [];
|
||||
state.wallets = [];
|
||||
// The approval screen reads the explorer's report only while "Use
|
||||
// Blockscout" is on.
|
||||
state.useBlockscout = true;
|
||||
});
|
||||
|
||||
describe("resolveTokenDecimals", () => {
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
// While "Use Blockscout" is off, the approval screens, and the screens that
|
||||
// follow TxApproval, take a token's name and scale from the bundled list and
|
||||
// the tracked tokens alone (https://git.eeqj.de/sneak/AutistMask/issues/668).
|
||||
// A token missing from both gets a line under it. The line says Blockscout's
|
||||
// report of the user's balances is not used to name the token. While the
|
||||
// setting is on, Blockscout's report of the user's holding is a source too, as
|
||||
// before.
|
||||
//
|
||||
// Booted through the real popup entry point, as the approval window or as a
|
||||
// popup reopened onto SuccessTx, WaitTx or ErrorTx. The vault is mocked so that
|
||||
// no password has to be hashed: the stored secret decrypts to the test key
|
||||
// below.
|
||||
|
||||
jest.mock("../src/shared/vault", () => ({
|
||||
decryptWithPassword: jest.fn(),
|
||||
encryptWithPassword: jest.fn(),
|
||||
}));
|
||||
|
||||
const { Interface, Wallet } = require("ethers");
|
||||
const {
|
||||
bootPopup,
|
||||
cleanupPopup,
|
||||
unversionedValidProfile,
|
||||
} = require("./support/popupBoot");
|
||||
|
||||
// A published test key, never used for real funds.
|
||||
const SIGNER_KEY =
|
||||
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
|
||||
const signer = new Wallet(SIGNER_KEY);
|
||||
const ORIGIN = "https://dapp.example";
|
||||
const RECIPIENT = "0x2222222222222222222222222222222222222222";
|
||||
const TX_HASH = "0x" + "ab".repeat(32);
|
||||
// Outside the bundled list, and not tracked.
|
||||
const TOKEN = "0xE2E0000000000000000000000000000000000E2e";
|
||||
// In the bundled list, at 6 decimals.
|
||||
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
|
||||
|
||||
const LINE =
|
||||
'Blockscout\'s report of your balances is not used to name this token, because "Use Blockscout" is off in Settings.';
|
||||
|
||||
// TOKEN as Blockscout reports the user's holding of it.
|
||||
const TOKEN_ROW = {
|
||||
address: TOKEN,
|
||||
name: "Novel",
|
||||
symbol: "NOVEL",
|
||||
decimals: "6",
|
||||
balance: "5000.0",
|
||||
holders: 5000,
|
||||
};
|
||||
|
||||
// A transfer of 5,000 of a 6-decimal token to RECIPIENT.
|
||||
const TRANSFER_DATA = new Interface([
|
||||
"function transfer(address to, uint256 amount)",
|
||||
]).encodeFunctionData("transfer", [RECIPIENT, 5000000000n]);
|
||||
|
||||
// An EIP-2612 permit letting RECIPIENT take 5,000 of TOKEN.
|
||||
const PERMIT = {
|
||||
types: {
|
||||
EIP712Domain: [
|
||||
{ name: "name", type: "string" },
|
||||
{ name: "chainId", type: "uint256" },
|
||||
{ name: "verifyingContract", type: "address" },
|
||||
],
|
||||
Permit: [
|
||||
{ name: "owner", type: "address" },
|
||||
{ name: "spender", type: "address" },
|
||||
{ name: "value", type: "uint256" },
|
||||
{ name: "nonce", type: "uint256" },
|
||||
{ name: "deadline", type: "uint256" },
|
||||
],
|
||||
},
|
||||
primaryType: "Permit",
|
||||
domain: { name: "Novel", chainId: 1, verifyingContract: TOKEN },
|
||||
message: {
|
||||
owner: signer.address,
|
||||
spender: RECIPIENT,
|
||||
value: "5000000000",
|
||||
nonce: "0",
|
||||
deadline: "1790000000",
|
||||
},
|
||||
};
|
||||
|
||||
// The stored profile: the user's one address holds `tokenBalances`.
|
||||
function profile(useBlockscout, tokenBalances, extra) {
|
||||
return unversionedValidProfile({
|
||||
useBlockscout,
|
||||
wallets: [
|
||||
{
|
||||
type: "key",
|
||||
name: "Wallet 1",
|
||||
encryptedSecret: "encrypted-key",
|
||||
addresses: [
|
||||
{
|
||||
address: signer.address,
|
||||
balance: "1.0",
|
||||
tokenBalances,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
activeAddress: signer.address,
|
||||
allowedSites: { [signer.address]: [ORIGIN] },
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
// The approval window for the site's request: a transfer sent to `token`, or
|
||||
// with `signParams`, a signature. The background answers the signed
|
||||
// transaction with `txResponse`.
|
||||
function bootApproval(stored, { token = TOKEN, signParams, txResponse } = {}) {
|
||||
return bootPopup(stored, {
|
||||
search: "?approval=approval-1",
|
||||
runtime: {
|
||||
connect: () => ({ postMessage: () => {} }),
|
||||
sendMessage: (msg, reply) => {
|
||||
if (msg.type === "AUTISTMASK_GET_APPROVAL") {
|
||||
reply({
|
||||
type: signParams ? "sign" : "tx",
|
||||
origin: ORIGIN,
|
||||
isPhishingDomain: false,
|
||||
approvedFrom: signer.address,
|
||||
signParams,
|
||||
approvedTx: {
|
||||
type: 2,
|
||||
from: signer.address,
|
||||
chainId: "0x1",
|
||||
nonce: "0x7",
|
||||
gasLimit: "0x30d40",
|
||||
maxPriorityFeePerGas: "0x3b9aca00",
|
||||
maxFeePerGas: "0x77359400",
|
||||
to: token,
|
||||
value: "0x0",
|
||||
data: TRANSFER_DATA,
|
||||
accessList: [],
|
||||
},
|
||||
});
|
||||
} else if (msg.type === "AUTISTMASK_TX_RESPONSE") {
|
||||
reply(txResponse);
|
||||
} else {
|
||||
reply(undefined);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// The page logs its failed RPC reads; the stub node in popupBoot.js
|
||||
// answers none.
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupPopup();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("TxApproval", () => {
|
||||
test("has the line under a token it cannot name while Use Blockscout is off", async () => {
|
||||
const page = await bootApproval(profile(false, []));
|
||||
const details = page.node("approve-tx-decoded-details").innerHTML;
|
||||
expect(details).toContain("Unknown token");
|
||||
expect(details).toContain(LINE);
|
||||
expect(details).toContain("5000000000 base units (decimals unknown)");
|
||||
cleanupPopup();
|
||||
|
||||
// The bundled list names USDC, so there is nothing to say.
|
||||
const bundled = await bootApproval(profile(false, []), {
|
||||
token: USDC,
|
||||
});
|
||||
expect(
|
||||
bundled.node("approve-tx-decoded-details").innerHTML,
|
||||
).not.toContain(LINE);
|
||||
cleanupPopup();
|
||||
|
||||
const on = await bootApproval(profile(true, [TOKEN_ROW]));
|
||||
const named = on.node("approve-tx-decoded-details").innerHTML;
|
||||
expect(named).toContain("5000.0000 NOVEL");
|
||||
expect(named).not.toContain(LINE);
|
||||
});
|
||||
|
||||
test("names nothing from a Blockscout entry still in the balances while Use Blockscout is off", async () => {
|
||||
const page = await bootApproval(profile(false, [TOKEN_ROW]));
|
||||
const details = page.node("approve-tx-decoded-details").innerHTML;
|
||||
expect(details).not.toContain("NOVEL");
|
||||
expect(details).toContain("Unknown token");
|
||||
expect(details).toContain(LINE);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["WaitTx", { txHash: TX_HASH }, "wait-tx"],
|
||||
[
|
||||
"ErrorTx",
|
||||
{ error: "The transaction could not be sent.", retryable: false },
|
||||
"error-tx",
|
||||
],
|
||||
])(
|
||||
"%s after it says so under the amount while Use Blockscout is off",
|
||||
async (_name, txResponse, view) => {
|
||||
const page = await bootApproval(profile(false, []), {
|
||||
txResponse,
|
||||
});
|
||||
const { decryptWithPassword } = require("../src/shared/vault");
|
||||
decryptWithPassword.mockResolvedValue(SIGNER_KEY);
|
||||
page.node("approve-tx-password").value = "any password";
|
||||
await page.click("btn-approve-tx");
|
||||
expect(page.visibleViews()).toEqual([view]);
|
||||
expect(page.node(view + "-token-note").innerHTML).toBe(
|
||||
`<div class="text-xs text-muted">${LINE}</div>`,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("SuccessTx says so under the decoded token while Use Blockscout is off", async () => {
|
||||
const page = await bootPopup(
|
||||
profile(false, [], {
|
||||
currentView: "success-tx",
|
||||
viewStack: ["main"],
|
||||
viewData: {
|
||||
amount: "5000000000 base units (decimals unknown)",
|
||||
symbol: "?",
|
||||
to: RECIPIENT,
|
||||
hash: TX_HASH,
|
||||
blockNumber: 21000000,
|
||||
chainId: "0x1",
|
||||
decoded: {
|
||||
name: "Token Transfer",
|
||||
description: "Transfer ERC-20 token",
|
||||
details: [
|
||||
{
|
||||
label: "Token",
|
||||
value: TOKEN,
|
||||
address: TOKEN,
|
||||
isToken: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(page.visibleViews()).toEqual(["success-tx"]);
|
||||
const decoded = page.node("success-tx-decoded").innerHTML;
|
||||
expect(decoded).toContain("Unknown token");
|
||||
expect(decoded).toContain(LINE);
|
||||
});
|
||||
|
||||
test.each([
|
||||
[
|
||||
"wait-tx",
|
||||
{
|
||||
pendingWait: {
|
||||
hash: TX_HASH,
|
||||
txInfo: {
|
||||
to: RECIPIENT,
|
||||
amount: "1",
|
||||
token: 42,
|
||||
chainId: "0x1",
|
||||
},
|
||||
broadcastTime: Date.now(),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"error-tx",
|
||||
{
|
||||
amount: "1",
|
||||
symbol: "?",
|
||||
token: 42,
|
||||
to: RECIPIENT,
|
||||
hash: TX_HASH,
|
||||
message: "The transaction failed.",
|
||||
chainId: "0x1",
|
||||
},
|
||||
],
|
||||
])(
|
||||
"a popup reopened onto %s shows no line for a stored token that is not text",
|
||||
async (view, viewData) => {
|
||||
const page = await bootPopup(
|
||||
profile(false, [], {
|
||||
currentView: view,
|
||||
viewStack: ["main"],
|
||||
viewData,
|
||||
}),
|
||||
);
|
||||
expect(page.pageErrors).toEqual([]);
|
||||
expect(page.visibleViews()).toEqual([view]);
|
||||
expect(page.node(view + "-token-note").innerHTML).toBe("");
|
||||
},
|
||||
);
|
||||
|
||||
test("SignApproval says so under a permit's token while Use Blockscout is off", async () => {
|
||||
const signParams = {
|
||||
method: "eth_signTypedData_v4",
|
||||
typedData: JSON.stringify(PERMIT),
|
||||
from: signer.address,
|
||||
};
|
||||
const page = await bootApproval(profile(false, []), { signParams });
|
||||
const message = page.node("approve-sign-message").innerHTML;
|
||||
expect(message).toContain("Unknown token");
|
||||
expect(message).toContain(LINE);
|
||||
cleanupPopup();
|
||||
|
||||
const on = await bootApproval(profile(true, [TOKEN_ROW]), { signParams });
|
||||
const named = on.node("approve-sign-message").innerHTML;
|
||||
expect(named).toContain("5000.0000 NOVEL");
|
||||
expect(named).not.toContain(LINE);
|
||||
});
|
||||
@@ -142,6 +142,8 @@ function loadWorker(networkId, opts) {
|
||||
},
|
||||
refreshBalances:
|
||||
options.refreshBalances || jest.fn(async () => undefined),
|
||||
dropUntrackedTokens: jest.requireActual("../src/shared/balances")
|
||||
.dropUntrackedTokens,
|
||||
}));
|
||||
jest.doMock("../src/shared/phishingDomains", () => ({
|
||||
isPhishingDomain: () => false,
|
||||
@@ -443,3 +445,54 @@ describe("a balance refresh under a network switch", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a balance refresh under a change of Use Blockscout", () => {
|
||||
// Turning "Use Blockscout" off drops the tokens the user has not tracked
|
||||
// (src/popup/views/settings.js). Blockscout may have answered a refresh
|
||||
// just before. That refresh still saves its balances when it returns, but
|
||||
// not those tokens. The same refresh in the popup is in
|
||||
// tests/blockscoutEtherscanSettings.test.js.
|
||||
test("a refresh that returns after Use Blockscout was turned off saves its balances without the untracked token", async () => {
|
||||
const roundTrip = deferred();
|
||||
const started = deferred();
|
||||
|
||||
const bg = loadWorker("mainnet", {
|
||||
refreshBalances: async (wallets) => {
|
||||
started.resolve();
|
||||
await roundTrip.promise;
|
||||
wallets[0].addresses[0].balance = "7.0";
|
||||
wallets[0].addresses[0].tokenBalances = [
|
||||
{
|
||||
address: "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
name: "Test Token",
|
||||
symbol: "TST",
|
||||
decimals: 18,
|
||||
balance: "5.0",
|
||||
holders: 50000,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
global.chrome.storage.write("autistmask", {
|
||||
...bg.persisted(),
|
||||
useBlockscout: true,
|
||||
});
|
||||
|
||||
const refresh = bg.fireBalanceAlarm();
|
||||
await started.promise;
|
||||
|
||||
// The user unticks "Use Blockscout" in Settings while the round trip
|
||||
// is out.
|
||||
global.chrome.storage.write("autistmask", {
|
||||
...bg.persisted(),
|
||||
useBlockscout: false,
|
||||
});
|
||||
|
||||
roundTrip.resolve();
|
||||
await refresh;
|
||||
|
||||
const after = bg.persisted();
|
||||
expect(after.wallets[0].addresses[0].balance).toBe("7.0");
|
||||
expect(after.wallets[0].addresses[0].tokenBalances).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,12 @@ const TRACKED = {
|
||||
decimals: 6,
|
||||
};
|
||||
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
globalThis.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
};
|
||||
|
||||
const realFetch = globalThis.fetch;
|
||||
let logged;
|
||||
|
||||
|
||||
@@ -0,0 +1,848 @@
|
||||
// Blockscout and Etherscan are sent none of the user's data until the user
|
||||
// turns on a setting for each
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/668). A new profile has "Use
|
||||
// Blockscout" and "Check recipients on Etherscan" both off. While a setting is
|
||||
// off, no request sends its service the user's data.
|
||||
//
|
||||
// One request still reaches Blockscout while "Use Blockscout" is off: the check
|
||||
// behind "Save" beside the Blockscout URL in Settings. That check asks the URL
|
||||
// only for `/stats` and sends none of the user's data.
|
||||
//
|
||||
// Before any other request to Blockscout or Etherscan, the wallet reads that
|
||||
// service's setting from storage, not the page's copy. The wallet reads the
|
||||
// setting again when the answer lands. So a change saved by another page
|
||||
// applies at once. An open page also takes such a change and draws its screens
|
||||
// again.
|
||||
//
|
||||
// Only Blockscout reports the tokens the user has not tracked. While "Use
|
||||
// Blockscout" is off, no address keeps them and no screen lists them.
|
||||
//
|
||||
// The functions that make the requests are driven directly, against a fetch()
|
||||
// that records every URL it is asked for. The screens are driven through the
|
||||
// real popup over a stored profile (tests/support/popupBoot.js). The fetch()
|
||||
// stub in popupBoot.js records the same way.
|
||||
|
||||
"use strict";
|
||||
|
||||
const { FetchRequest } = require("ethers");
|
||||
const {
|
||||
bootPopup,
|
||||
cleanupPopup,
|
||||
unversionedValidProfile,
|
||||
ADDRESS,
|
||||
TOKEN_ADDRESS,
|
||||
} = require("./support/popupBoot");
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
const {
|
||||
DEFAULT_STATE,
|
||||
normalizePersisted,
|
||||
} = require("../src/shared/persistedState");
|
||||
const {
|
||||
fetchTokenBalances,
|
||||
refreshBalances,
|
||||
} = require("../src/shared/balances");
|
||||
const { fetchRecentTransactions } = require("../src/shared/transactions");
|
||||
const { getFullWarnings } = require("../src/shared/addressWarnings");
|
||||
const { checkEtherscanLabel } = require("../src/shared/etherscanLabels");
|
||||
const { HISTORY_OFF_TEXT, escapeHtml } = require("../src/popup/views/helpers");
|
||||
|
||||
const RPC_URL = "https://rpc.example.invalid";
|
||||
const BLOCKSCOUT = "https://eth.blockscout.com/api/v2";
|
||||
const RECIPIENT = "0x2222222222222222222222222222222222222222";
|
||||
const TX_HASH = "0x" + "ab".repeat(32);
|
||||
const ZERO_WORD = "0x" + "0".repeat(64);
|
||||
|
||||
// USDC, from the bundled list, as Blockscout reports a holding of it.
|
||||
const USDC_ROW = {
|
||||
value: "5000000",
|
||||
token: {
|
||||
type: "ERC-20",
|
||||
address_hash: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||
symbol: "USDC",
|
||||
name: "USD Coin",
|
||||
decimals: "6",
|
||||
holders_count: "50000",
|
||||
},
|
||||
};
|
||||
|
||||
// A transfer of 1 ETH from RECIPIENT to ADDRESS, as Blockscout lists it in
|
||||
// ADDRESS's history.
|
||||
const HISTORY_ITEM = {
|
||||
hash: TX_HASH,
|
||||
block_number: 21000000,
|
||||
timestamp: "2026-02-27T12:00:00.000Z",
|
||||
from: { hash: RECIPIENT },
|
||||
to: { hash: ADDRESS, is_contract: false },
|
||||
value: "1000000000000000000",
|
||||
method: null,
|
||||
status: "ok",
|
||||
};
|
||||
|
||||
// Etherscan's page for RECIPIENT, which labels it a phishing address.
|
||||
const PHISHING_PAGE =
|
||||
"<html><head><title>Fake_Phishing1 | Address: " +
|
||||
RECIPIENT +
|
||||
" | Etherscan</title></head><body></body></html>";
|
||||
|
||||
const realFetch = globalThis.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
cleanupPopup();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("a new profile", () => {
|
||||
test("has both settings off", () => {
|
||||
expect(DEFAULT_STATE.useBlockscout).toBe(false);
|
||||
expect(DEFAULT_STATE.checkRecipientsOnEtherscan).toBe(false);
|
||||
const loaded = normalizePersisted(undefined);
|
||||
expect(loaded.useBlockscout).toBe(false);
|
||||
expect(loaded.checkRecipientsOnEtherscan).toBe(false);
|
||||
});
|
||||
|
||||
test("keeps either setting the user turned on", () => {
|
||||
const loaded = normalizePersisted({
|
||||
useBlockscout: true,
|
||||
checkRecipientsOnEtherscan: true,
|
||||
});
|
||||
expect(loaded.useBlockscout).toBe(true);
|
||||
expect(loaded.checkRecipientsOnEtherscan).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("where each request is made", () => {
|
||||
// Every URL that fetch() was asked for, in order.
|
||||
let asked;
|
||||
|
||||
// What Blockscout and Etherscan answer: USDC among ADDRESS's tokens,
|
||||
// HISTORY_ITEM in its history, and PHISHING_PAGE for RECIPIENT.
|
||||
function answer(url) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => {
|
||||
if (url.endsWith("/token-balances")) return [USDC_ROW];
|
||||
if (url.endsWith("/transactions")) {
|
||||
return { items: [HISTORY_ITEM] };
|
||||
}
|
||||
return { items: [] };
|
||||
},
|
||||
text: async () => PHISHING_PAGE,
|
||||
};
|
||||
}
|
||||
|
||||
async function recordAndAnswer(url) {
|
||||
asked.push(String(url));
|
||||
return answer(String(url));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
asked = [];
|
||||
globalThis.fetch = jest.fn(recordAndAnswer);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = realFetch;
|
||||
delete globalThis.chrome;
|
||||
});
|
||||
|
||||
// Storage holds `profile` as the stored profile, or nothing when it is
|
||||
// undefined.
|
||||
function storing(profile) {
|
||||
globalThis.chrome = {
|
||||
storage: makeStorageStub(
|
||||
profile === undefined ? {} : { autistmask: profile },
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
test("Blockscout's token balances are asked for only while Use Blockscout is on", async () => {
|
||||
storing({ useBlockscout: false });
|
||||
expect(await fetchTokenBalances(ADDRESS, BLOCKSCOUT, [])).toEqual([]);
|
||||
expect(asked).toEqual([]);
|
||||
|
||||
storing({ useBlockscout: true });
|
||||
const rows = await fetchTokenBalances(ADDRESS, BLOCKSCOUT, []);
|
||||
expect(rows.map((row) => row.symbol)).toEqual(["USDC"]);
|
||||
expect(asked).toEqual([
|
||||
BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances",
|
||||
]);
|
||||
});
|
||||
|
||||
test("the transaction history is asked for only while Use Blockscout is on", async () => {
|
||||
storing({ useBlockscout: false });
|
||||
expect(
|
||||
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1"),
|
||||
).toEqual([]);
|
||||
expect(asked).toEqual([]);
|
||||
|
||||
storing({ useBlockscout: true });
|
||||
await fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1");
|
||||
expect(asked.sort()).toEqual([
|
||||
BLOCKSCOUT +
|
||||
"/addresses/" +
|
||||
ADDRESS +
|
||||
"/token-transfers?type=ERC-20",
|
||||
BLOCKSCOUT + "/addresses/" + ADDRESS + "/transactions",
|
||||
]);
|
||||
});
|
||||
|
||||
test("the Etherscan label check is made only while the stored profile has Check recipients on Etherscan on", async () => {
|
||||
const provider = {
|
||||
getCode: async () => "0x",
|
||||
getTransactionCount: async () => 1,
|
||||
getBalance: async () => 0n,
|
||||
};
|
||||
const checkWith = async (profile) => {
|
||||
storing(profile);
|
||||
await getFullWarnings(RECIPIENT, provider, { chainId: "0x1" });
|
||||
};
|
||||
|
||||
await checkWith(undefined);
|
||||
await checkWith({ checkRecipientsOnEtherscan: false });
|
||||
// Only a stored true is on, as when the profile is loaded.
|
||||
await checkWith({ checkRecipientsOnEtherscan: "true" });
|
||||
await checkWith({ checkRecipientsOnEtherscan: 42 });
|
||||
expect(asked).toEqual([]);
|
||||
|
||||
await checkWith({ checkRecipientsOnEtherscan: true });
|
||||
expect(asked).toEqual(["https://etherscan.io/address/" + RECIPIENT]);
|
||||
});
|
||||
|
||||
// Asks with `setting` on in storage. Once every request is out, storage
|
||||
// holds `after` for the setting, as when the user changes it here or on
|
||||
// another page. Only then does each request get its answer.
|
||||
async function answeredAfter(setting, after, ask) {
|
||||
storing({ [setting]: true });
|
||||
let release;
|
||||
const held = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
let requested;
|
||||
const out = new Promise((resolve) => {
|
||||
requested = resolve;
|
||||
});
|
||||
globalThis.fetch = jest.fn(async (url) => {
|
||||
requested();
|
||||
await held;
|
||||
return recordAndAnswer(url);
|
||||
});
|
||||
const result = ask();
|
||||
await out;
|
||||
globalThis.chrome.storage.write("autistmask", { [setting]: after });
|
||||
release();
|
||||
return result;
|
||||
}
|
||||
|
||||
// One row per request, giving:
|
||||
//
|
||||
// - its name
|
||||
// - the setting that gates it
|
||||
// - how to make it
|
||||
// - its answer while that setting is off
|
||||
test.each([
|
||||
[
|
||||
"Blockscout's token balances",
|
||||
"useBlockscout",
|
||||
() => fetchTokenBalances(ADDRESS, BLOCKSCOUT, []),
|
||||
[],
|
||||
],
|
||||
[
|
||||
"the transaction history",
|
||||
"useBlockscout",
|
||||
() => fetchRecentTransactions(ADDRESS, BLOCKSCOUT, "0x1"),
|
||||
[],
|
||||
],
|
||||
[
|
||||
"the Etherscan label check",
|
||||
"checkRecipientsOnEtherscan",
|
||||
() => checkEtherscanLabel(RECIPIENT, "0x1"),
|
||||
null,
|
||||
],
|
||||
])(
|
||||
"%s keeps an answer that lands while its setting is on, and drops one that lands after it went off",
|
||||
async (_what, setting, ask, dropped) => {
|
||||
expect(await answeredAfter(setting, true, ask)).not.toEqual(
|
||||
dropped,
|
||||
);
|
||||
expect(await answeredAfter(setting, false, ask)).toEqual(dropped);
|
||||
},
|
||||
);
|
||||
|
||||
describe("a balance refresh", () => {
|
||||
// The RPC endpoint answers every balance with zero. The ENS reverse
|
||||
// lookup makes the only other calls, and it finds no resolver.
|
||||
beforeEach(() => {
|
||||
FetchRequest.registerGetUrl(async (req) => {
|
||||
const body = JSON.parse(Buffer.from(req.body).toString("utf8"));
|
||||
const reply = (r) => ({
|
||||
jsonrpc: "2.0",
|
||||
id: r.id,
|
||||
result: r.method === "eth_getBalance" ? "0x0" : ZERO_WORD,
|
||||
});
|
||||
const answer = Array.isArray(body)
|
||||
? body.map(reply)
|
||||
: reply(body);
|
||||
return {
|
||||
statusCode: 200,
|
||||
statusMessage: "OK",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: Buffer.from(JSON.stringify(answer)),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
|
||||
});
|
||||
|
||||
// An address refreshed while "Use Blockscout" was on. It holds the USDC
|
||||
// that Blockscout reported then.
|
||||
function addressWithUsdc() {
|
||||
return {
|
||||
address: ADDRESS,
|
||||
tokenBalances: [
|
||||
{
|
||||
address: USDC_ROW.token.address_hash,
|
||||
name: "USD Coin",
|
||||
symbol: "USDC",
|
||||
decimals: 6,
|
||||
balance: "5.0",
|
||||
holders: 50000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function refresh(addr, useBlockscout) {
|
||||
storing({ useBlockscout });
|
||||
await refreshBalances(
|
||||
[{ addresses: [addr] }],
|
||||
RPC_URL,
|
||||
BLOCKSCOUT,
|
||||
[],
|
||||
"mainnet",
|
||||
);
|
||||
}
|
||||
|
||||
test("asks Blockscout nothing while Use Blockscout is off, and then lists no token the user has not tracked", async () => {
|
||||
const addr = addressWithUsdc();
|
||||
await refresh(addr, false);
|
||||
expect(asked).toEqual([]);
|
||||
expect(addr.tokenBalances).toEqual([]);
|
||||
});
|
||||
|
||||
test("asks Blockscout for the token balances while Use Blockscout is on", async () => {
|
||||
const addr = addressWithUsdc();
|
||||
await refresh(addr, true);
|
||||
expect(asked).toEqual([
|
||||
BLOCKSCOUT + "/addresses/" + ADDRESS + "/token-balances",
|
||||
]);
|
||||
expect(addr.tokenBalances.map((row) => row.symbol)).toEqual([
|
||||
"USDC",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("the popup", () => {
|
||||
// Every URL the booted page handed to fetch().
|
||||
function fetched() {
|
||||
return globalThis.fetch.mock.calls.map((call) => String(call[0]));
|
||||
}
|
||||
|
||||
function blockscoutOrEtherscan(url) {
|
||||
return url.includes("blockscout") || url.includes("etherscan");
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// The screens below log their failed RPC reads; the stub node in
|
||||
// popupBoot.js answers none.
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
test("Home says why there is no history and only tracked tokens while Use Blockscout is off", async () => {
|
||||
const page = await bootPopup(unversionedValidProfile());
|
||||
expect(page.node("home-tx-list").innerHTML).toContain(
|
||||
escapeHtml(HISTORY_OFF_TEXT),
|
||||
);
|
||||
expect(page.hidden("home-tokens-note")).toBe(false);
|
||||
cleanupPopup();
|
||||
|
||||
const on = await bootPopup(
|
||||
unversionedValidProfile({ useBlockscout: true }),
|
||||
);
|
||||
expect(on.node("home-tx-list").innerHTML).toContain(
|
||||
"No transactions found.",
|
||||
);
|
||||
expect(on.hidden("home-tokens-note")).toBe(true);
|
||||
});
|
||||
|
||||
// The transaction screen, as a reopened popup draws it.
|
||||
function onTransactionScreen(extra) {
|
||||
return unversionedValidProfile({
|
||||
currentView: "transaction",
|
||||
viewStack: ["main"],
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
viewData: {
|
||||
tx: {
|
||||
hash: TX_HASH,
|
||||
from: ADDRESS,
|
||||
to: RECIPIENT,
|
||||
value: "1.0000",
|
||||
exactValue: "1.0",
|
||||
symbol: "ETH",
|
||||
timestamp: 1790000000,
|
||||
isError: false,
|
||||
direction: "sent",
|
||||
directionLabel: "Sent",
|
||||
contractAddress: null,
|
||||
chainId: "0x1",
|
||||
},
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
test("TransactionDetail asks Blockscout for the transaction only while Use Blockscout is on", async () => {
|
||||
const page = await bootPopup(onTransactionScreen());
|
||||
expect(page.visibleViews()).toEqual(["transaction"]);
|
||||
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([]);
|
||||
expect(page.hidden("tx-detail-blockscout-note")).toBe(false);
|
||||
cleanupPopup();
|
||||
|
||||
const on = await bootPopup(
|
||||
onTransactionScreen({ useBlockscout: true }),
|
||||
);
|
||||
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([
|
||||
BLOCKSCOUT + "/transactions/" + TX_HASH,
|
||||
]);
|
||||
expect(on.hidden("tx-detail-blockscout-note")).toBe(true);
|
||||
});
|
||||
|
||||
// Another page turns "Use Blockscout" off while TransactionDetail's request
|
||||
// to Blockscout is out. This page draws TransactionDetail again with its
|
||||
// line, and only then does Blockscout answer.
|
||||
test("TransactionDetail shows nothing from an answer that lands after another page turned Use Blockscout off", async () => {
|
||||
const page = await bootPopup(
|
||||
onTransactionScreen({ useBlockscout: true }),
|
||||
);
|
||||
let release;
|
||||
const held = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
globalThis.fetch = jest.fn(async () => {
|
||||
await held;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
block_number: 21000000,
|
||||
fee: { value: "21000000000000" },
|
||||
raw_input: "0x12345678",
|
||||
method: "doSomething",
|
||||
}),
|
||||
};
|
||||
});
|
||||
require("../src/popup/views/transactionDetail").render();
|
||||
await page.settle();
|
||||
expect(fetched()).toEqual([BLOCKSCOUT + "/transactions/" + TX_HASH]);
|
||||
|
||||
await anotherPageAnnounces(page, { useBlockscout: false });
|
||||
release();
|
||||
await page.settle();
|
||||
|
||||
expect(page.visibleViews()).toEqual(["transaction"]);
|
||||
expect(page.hidden("tx-detail-blockscout-note")).toBe(false);
|
||||
for (const id of [
|
||||
"tx-detail-block-section",
|
||||
"tx-detail-network-section",
|
||||
"tx-detail-calldata-section",
|
||||
"tx-detail-rawdata-section",
|
||||
]) {
|
||||
expect(page.hidden(id)).toBe(true);
|
||||
}
|
||||
expect(fetched()).toHaveLength(1);
|
||||
});
|
||||
|
||||
// The confirmation screen for a send of 0.1 ETH to RECIPIENT, as a
|
||||
// reopened popup draws it.
|
||||
function onConfirmScreen(extra) {
|
||||
return unversionedValidProfile({
|
||||
currentView: "confirm-tx",
|
||||
viewStack: ["main"],
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
viewData: {
|
||||
pendingTx: {
|
||||
token: "ETH",
|
||||
from: ADDRESS,
|
||||
to: RECIPIENT,
|
||||
amount: "0.1",
|
||||
chainId: "0x1",
|
||||
},
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
test("ConfirmTx asks Etherscan about the recipient only while Check recipients on Etherscan is on", async () => {
|
||||
const page = await bootPopup(onConfirmScreen());
|
||||
expect(page.visibleViews()).toEqual(["confirm-tx"]);
|
||||
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([]);
|
||||
expect(page.hidden("confirm-etherscan-note")).toBe(false);
|
||||
expect(page.hidden("confirm-etherscan-warning")).toBe(true);
|
||||
cleanupPopup();
|
||||
|
||||
const on = await bootPopup(
|
||||
onConfirmScreen({ checkRecipientsOnEtherscan: true }),
|
||||
);
|
||||
expect(fetched().filter(blockscoutOrEtherscan)).toEqual([
|
||||
"https://etherscan.io/address/" + RECIPIENT,
|
||||
]);
|
||||
expect(on.hidden("confirm-etherscan-note")).toBe(true);
|
||||
expect(on.hidden("confirm-etherscan-warning")).toBe(false);
|
||||
});
|
||||
|
||||
// Ticks or unticks a checkbox on the page, as the user does.
|
||||
async function setBox(page, id, checked) {
|
||||
const box = page.node(id);
|
||||
box.checked = checked;
|
||||
for (const fn of box.listeners.change) await fn();
|
||||
await page.settle();
|
||||
}
|
||||
|
||||
test("Welcome offers both settings unticked, and ticking one there stores it", async () => {
|
||||
const page = await bootPopup(undefined);
|
||||
expect(page.visibleViews()).toEqual(["welcome"]);
|
||||
expect(page.node("welcome-use-blockscout").checked).toBe(false);
|
||||
expect(page.node("welcome-check-recipients-on-etherscan").checked).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
await setBox(page, "welcome-use-blockscout", true);
|
||||
expect(page.storage.read("autistmask").useBlockscout).toBe(true);
|
||||
await setBox(page, "welcome-check-recipients-on-etherscan", true);
|
||||
expect(page.storage.read("autistmask").checkRecipientsOnEtherscan).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("Settings shows both settings unticked, and ticking one there stores it", async () => {
|
||||
const page = await bootPopup(unversionedValidProfile());
|
||||
await page.click("btn-settings");
|
||||
expect(page.node("settings-use-blockscout").checked).toBe(false);
|
||||
expect(
|
||||
page.node("settings-check-recipients-on-etherscan").checked,
|
||||
).toBe(false);
|
||||
|
||||
await setBox(page, "settings-use-blockscout", true);
|
||||
expect(page.storage.read("autistmask").useBlockscout).toBe(true);
|
||||
await setBox(page, "settings-check-recipients-on-etherscan", true);
|
||||
expect(page.storage.read("autistmask").checkRecipientsOnEtherscan).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// A token the user tracks, and an untracked one that Blockscout reported.
|
||||
// Both are written in lower case, because the balance rows' and the
|
||||
// dropdown's token ids are compared in lower case below.
|
||||
const TRACKED = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
const UNTRACKED = TOKEN_ADDRESS.toLowerCase();
|
||||
const SECOND_ADDRESS = "0x3333333333333333333333333333333333333333";
|
||||
|
||||
function tokenRow(address, symbol) {
|
||||
return {
|
||||
address,
|
||||
name: symbol,
|
||||
symbol,
|
||||
decimals: 18,
|
||||
balance: "5.0",
|
||||
holders: 50000,
|
||||
};
|
||||
}
|
||||
|
||||
// AddressDetail of the first of two addresses, as a reopened popup draws
|
||||
// it. Both addresses hold the tracked token. While "Use Blockscout" is on,
|
||||
// both hold the untracked one too.
|
||||
function onAddressDetail(useBlockscout) {
|
||||
const profile = unversionedValidProfile({
|
||||
useBlockscout,
|
||||
currentView: "address",
|
||||
viewStack: ["main"],
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
trackedTokens: [
|
||||
{ address: TRACKED, name: "TRK", symbol: "TRK", decimals: 18 },
|
||||
],
|
||||
});
|
||||
const wallet = profile.wallets[0];
|
||||
wallet.nextIndex = 2;
|
||||
wallet.addresses.push({ address: SECOND_ADDRESS, balance: "0.0" });
|
||||
for (const addr of wallet.addresses) {
|
||||
addr.tokenBalances = useBlockscout
|
||||
? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")]
|
||||
: [tokenRow(TRACKED, "TRK")];
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
// The token ids of the options created after the first `from` elements.
|
||||
// The page stub keeps no children, so this reads the options for Send's
|
||||
// dropdown from the elements the page created.
|
||||
function optionsSince(page, from) {
|
||||
return page.document.created
|
||||
.slice(from)
|
||||
.filter((el) => el.tagName === "OPTION")
|
||||
.map((el) => el.value.toLowerCase());
|
||||
}
|
||||
|
||||
test("unticking Use Blockscout from AddressDetail's gear takes the untracked token off AddressDetail, Send's dropdown and Home on Back", async () => {
|
||||
const page = await bootPopup(onAddressDetail(true));
|
||||
expect(page.visibleViews()).toEqual(["address"]);
|
||||
expect(page.node("address-balances").innerHTML).toContain(UNTRACKED);
|
||||
|
||||
await page.click("btn-settings");
|
||||
await setBox(page, "settings-use-blockscout", false);
|
||||
const from = page.document.created.length;
|
||||
await page.click("btn-settings-back");
|
||||
|
||||
expect(page.visibleViews()).toEqual(["address"]);
|
||||
const balances = page.node("address-balances").innerHTML;
|
||||
expect(balances).toContain(TRACKED);
|
||||
expect(balances).not.toContain(UNTRACKED);
|
||||
expect(page.hidden("address-tokens-note")).toBe(false);
|
||||
expect(optionsSince(page, from)).toEqual([TRACKED]);
|
||||
|
||||
await page.click("btn-send");
|
||||
expect(page.visibleViews()).toEqual(["send"]);
|
||||
expect(page.hidden("send-token-dropdown")).toBe(false);
|
||||
expect(page.hidden("send-tokens-note")).toBe(false);
|
||||
|
||||
await page.click("btn-send-back");
|
||||
await page.click("btn-address-back");
|
||||
expect(page.visibleViews()).toEqual(["main"]);
|
||||
const home = page.node("wallet-list").innerHTML;
|
||||
expect(home).toContain(TRACKED);
|
||||
expect(home).not.toContain(UNTRACKED);
|
||||
|
||||
const stored = page.storage.read("autistmask");
|
||||
for (const addr of stored.wallets[0].addresses) {
|
||||
expect(
|
||||
addr.tokenBalances.map((row) => row.address.toLowerCase()),
|
||||
).toEqual([TRACKED]);
|
||||
}
|
||||
});
|
||||
|
||||
// Send's dropdown was filled under the old setting, and Back cannot draw
|
||||
// Send again. So Back lands on the screen under it.
|
||||
test.each([
|
||||
["unticking", true],
|
||||
["ticking", false],
|
||||
])(
|
||||
"%s Use Blockscout from Send's gear lands Back on AddressDetail",
|
||||
async (_how, before) => {
|
||||
const page = await bootPopup(onAddressDetail(before));
|
||||
await page.click("btn-send");
|
||||
expect(page.visibleViews()).toEqual(["send"]);
|
||||
|
||||
await page.click("btn-settings");
|
||||
await setBox(page, "settings-use-blockscout", !before);
|
||||
await page.click("btn-settings-back");
|
||||
expect(page.visibleViews()).toEqual(["address"]);
|
||||
expect(page.hidden("address-tokens-note")).toBe(!before);
|
||||
},
|
||||
);
|
||||
|
||||
test("Send says under its token dropdown that only tracked tokens are listed while Use Blockscout is off", async () => {
|
||||
const off = await bootPopup(onAddressDetail(false));
|
||||
await off.click("btn-send");
|
||||
expect(off.hidden("send-token-dropdown")).toBe(false);
|
||||
expect(off.hidden("send-tokens-note")).toBe(false);
|
||||
cleanupPopup();
|
||||
|
||||
const on = await bootPopup(onAddressDetail(true));
|
||||
await on.click("btn-send");
|
||||
expect(on.hidden("send-token-dropdown")).toBe(false);
|
||||
expect(on.hidden("send-tokens-note")).toBe(true);
|
||||
});
|
||||
|
||||
test("Send opened from AddressToken shows neither the dropdown nor its line", async () => {
|
||||
const page = await bootPopup({
|
||||
...onAddressDetail(false),
|
||||
currentView: "address-token",
|
||||
viewStack: ["main", "address"],
|
||||
selectedToken: "ETH",
|
||||
});
|
||||
expect(page.visibleViews()).toEqual(["address-token"]);
|
||||
await page.click("btn-address-token-send");
|
||||
expect(page.visibleViews()).toEqual(["send"]);
|
||||
expect(page.hidden("send-token-dropdown")).toBe(true);
|
||||
expect(page.hidden("send-token-static")).toBe(false);
|
||||
});
|
||||
|
||||
test("DeleteAddress says only tracked tokens are listed while Use Blockscout is off", async () => {
|
||||
for (const useBlockscout of [false, true]) {
|
||||
const page = await bootPopup(onAddressDetail(useBlockscout));
|
||||
require("../src/popup/views/deleteAddress").show(0, 1);
|
||||
expect(page.visibleViews()).toEqual(["delete-address-confirm"]);
|
||||
expect(page.hidden("delete-address-tokens-note")).toBe(
|
||||
useBlockscout,
|
||||
);
|
||||
cleanupPopup();
|
||||
}
|
||||
});
|
||||
|
||||
// The token ids of the first address's balances in the stored profile.
|
||||
function storedTokens(page) {
|
||||
return page.storage
|
||||
.read("autistmask")
|
||||
.wallets[0].addresses[0].tokenBalances.map((row) =>
|
||||
row.address.toLowerCase(),
|
||||
);
|
||||
}
|
||||
|
||||
// Turning the setting off dropped the untracked token. A refresh that
|
||||
// started while the setting was on still saves its balances when it
|
||||
// returns, but not that token. The same refresh in the background is in
|
||||
// tests/backgroundStateIsolation.test.js.
|
||||
test("a refresh that returns after Use Blockscout was turned off saves its balances without the untracked token", async () => {
|
||||
const page = await bootPopup(onAddressDetail(true));
|
||||
const { refreshBalances: held } = require("../src/shared/balances");
|
||||
let answer;
|
||||
held.mockImplementationOnce(async (wallets) => {
|
||||
await new Promise((resolve) => {
|
||||
answer = resolve;
|
||||
});
|
||||
wallets[0].addresses[0].balance = "7.0";
|
||||
wallets[0].addresses[0].tokenBalances = [
|
||||
tokenRow(TRACKED, "TRK"),
|
||||
tokenRow(UNTRACKED, "TST"),
|
||||
];
|
||||
});
|
||||
const ticking = page.tick();
|
||||
await page.settle();
|
||||
|
||||
await page.click("btn-settings");
|
||||
await setBox(page, "settings-use-blockscout", false);
|
||||
answer();
|
||||
await ticking;
|
||||
|
||||
expect(
|
||||
page.storage.read("autistmask").wallets[0].addresses[0].balance,
|
||||
).toBe("7.0");
|
||||
expect(storedTokens(page)).toEqual([TRACKED]);
|
||||
await page.click("btn-settings-back");
|
||||
expect(page.node("address-balances").innerHTML).not.toContain(
|
||||
UNTRACKED,
|
||||
);
|
||||
});
|
||||
|
||||
// Another page saves `change` over the stored profile. The browser is not
|
||||
// made to tell this page, so this page sees the change only by reading the
|
||||
// stored profile.
|
||||
function anotherPageSaves(page, change) {
|
||||
page.storage.write("autistmask", {
|
||||
...page.storage.read("autistmask"),
|
||||
...change,
|
||||
});
|
||||
}
|
||||
|
||||
test.each([
|
||||
["off", true, false],
|
||||
["on", false, true],
|
||||
])(
|
||||
"after another page turns Use Blockscout %s, this page takes the change and its next refresh is saved",
|
||||
async (_how, before, after) => {
|
||||
const page = await bootPopup(onAddressDetail(before));
|
||||
// The refresh answers as it does under the stored setting.
|
||||
const { refreshBalances: held } = require("../src/shared/balances");
|
||||
held.mockImplementationOnce(async (wallets) => {
|
||||
const addr = wallets[0].addresses[0];
|
||||
addr.balance = "7.0";
|
||||
addr.tokenBalances = after
|
||||
? [tokenRow(TRACKED, "TRK"), tokenRow(UNTRACKED, "TST")]
|
||||
: [tokenRow(TRACKED, "TRK")];
|
||||
});
|
||||
|
||||
anotherPageSaves(page, { useBlockscout: after });
|
||||
await page.tick();
|
||||
|
||||
expect(
|
||||
page.storage.read("autistmask").wallets[0].addresses[0].balance,
|
||||
).toBe("7.0");
|
||||
expect(storedTokens(page)).toEqual(
|
||||
after ? [TRACKED, UNTRACKED] : [TRACKED],
|
||||
);
|
||||
expect(page.hidden("address-tokens-note")).toBe(after);
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
["off", true, false],
|
||||
["on", false, true],
|
||||
])(
|
||||
"after another page turns Check recipients on Etherscan %s, this page's next Etherscan check follows the stored setting",
|
||||
async (_how, before, after) => {
|
||||
const page = await bootPopup(
|
||||
onConfirmScreen({ checkRecipientsOnEtherscan: before }),
|
||||
);
|
||||
anotherPageSaves(page, { checkRecipientsOnEtherscan: after });
|
||||
const from = fetched().length;
|
||||
require("../src/popup/views/confirmTx").show({
|
||||
token: "ETH",
|
||||
from: ADDRESS,
|
||||
to: RECIPIENT,
|
||||
amount: "0.1",
|
||||
chainId: "0x1",
|
||||
});
|
||||
await page.settle();
|
||||
|
||||
// The page also takes the change and draws ConfirmTx again, which
|
||||
// checks once more. So the test counts each URL once, however often
|
||||
// it was asked for.
|
||||
const asked = new Set(
|
||||
fetched().slice(from).filter(blockscoutOrEtherscan),
|
||||
);
|
||||
expect([...asked]).toEqual(
|
||||
after ? ["https://etherscan.io/address/" + RECIPIENT] : [],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Another page's save of the whole profile with `change`, which the
|
||||
// browser announces to this page as it announces any write.
|
||||
async function anotherPageAnnounces(page, change) {
|
||||
await page.storage.local.set({
|
||||
autistmask: { ...page.storage.read("autistmask"), ...change },
|
||||
});
|
||||
await page.settle();
|
||||
}
|
||||
|
||||
test("a page open on AddressDetail draws it again when another page turns Use Blockscout off", async () => {
|
||||
const page = await bootPopup(onAddressDetail(true));
|
||||
expect(page.hidden("address-tokens-note")).toBe(true);
|
||||
|
||||
await anotherPageAnnounces(page, { useBlockscout: false });
|
||||
|
||||
expect(page.visibleViews()).toEqual(["address"]);
|
||||
expect(page.hidden("address-tokens-note")).toBe(false);
|
||||
expect(page.node("address-balances").innerHTML).not.toContain(
|
||||
UNTRACKED,
|
||||
);
|
||||
expect(storedTokens(page)).toEqual([TRACKED]);
|
||||
});
|
||||
|
||||
test("a page open on ConfirmTx draws it again when another page turns Check recipients on Etherscan off", async () => {
|
||||
const page = await bootPopup(
|
||||
onConfirmScreen({ checkRecipientsOnEtherscan: true }),
|
||||
);
|
||||
expect(page.hidden("confirm-etherscan-note")).toBe(true);
|
||||
|
||||
await anotherPageAnnounces(page, { checkRecipientsOnEtherscan: false });
|
||||
|
||||
expect(page.visibleViews()).toEqual(["confirm-tx"]);
|
||||
expect(page.hidden("confirm-etherscan-note")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -73,6 +73,8 @@ function loadColdWorker(networkId, opts) {
|
||||
jest.doMock("../src/shared/balances", () => ({
|
||||
getProvider: () => ({}),
|
||||
refreshBalances: options.refreshBalances || jest.fn(async () => {}),
|
||||
dropUntrackedTokens: jest.requireActual("../src/shared/balances")
|
||||
.dropUntrackedTokens,
|
||||
}));
|
||||
jest.doMock("../src/shared/phishingDomains", () => ({
|
||||
isPhishingDomain: () => false,
|
||||
|
||||
@@ -108,7 +108,10 @@ global.fetch = jest.fn(() => {
|
||||
});
|
||||
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
runtime: { sendMessage() {} },
|
||||
};
|
||||
|
||||
// A stub DOM: every id resolves to a recording element.
|
||||
const elements = new Map();
|
||||
@@ -243,6 +246,10 @@ async function openSend(token, tokenUnits = null) {
|
||||
];
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on. The
|
||||
// screens save this page's copy of "Use Blockscout" over the stored one. So
|
||||
// the page's copy is turned on here too.
|
||||
state.useBlockscout = true;
|
||||
await refreshBalances(
|
||||
state.wallets,
|
||||
"https://rpc.example.invalid",
|
||||
|
||||
@@ -50,8 +50,14 @@ function explorerPage(title) {
|
||||
// The explorer labels no address.
|
||||
global.fetch = jest.fn(async () => explorerPage("Etherscan"));
|
||||
|
||||
// The Etherscan label check is made, so its box can be shown.
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({
|
||||
autistmask: { checkRecipientsOnEtherscan: true },
|
||||
}),
|
||||
runtime: { sendMessage() {} },
|
||||
};
|
||||
|
||||
// A stub DOM: every id resolves to a recording element.
|
||||
const elements = new Map();
|
||||
|
||||
@@ -142,13 +142,19 @@ jest.mock("../src/popup/views/txStatus", () => ({
|
||||
}));
|
||||
|
||||
// The Etherscan label check is its only caller here: the explorer pages it
|
||||
// asks for are recorded, and nothing is fetched.
|
||||
// asks for are recorded, and nothing is fetched. The stored profile has the
|
||||
// check turned on.
|
||||
global.fetch = jest.fn(() => {
|
||||
throw new Error("tests must not perform network requests");
|
||||
});
|
||||
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({
|
||||
autistmask: { checkRecipientsOnEtherscan: true },
|
||||
}),
|
||||
runtime: { sendMessage() {} },
|
||||
};
|
||||
|
||||
// A stub DOM: every id resolves to a recording element.
|
||||
const elements = new Map();
|
||||
|
||||
@@ -297,6 +297,8 @@ describe.each([
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
state.selectedToken = "ETH";
|
||||
// The history is Blockscout's.
|
||||
state.useBlockscout = true;
|
||||
});
|
||||
|
||||
test("a contract creation's row says so, with no colour dot and no address line", async () => {
|
||||
|
||||
+23
-2
@@ -653,6 +653,12 @@ function traceEnabled(raw) {
|
||||
* confirmed receipt instead of null, so a wait screen resolves.
|
||||
* @param {boolean} [opts.failReceiptLookup] answer eth_getTransactionReceipt
|
||||
* with an error, so every receipt lookup fails; read at request time.
|
||||
* @param {string} [opts.etherscanLabel] the label Etherscan's page gives the
|
||||
* address asked about, such as "Fake_Phishing1"; none when unset. Read at
|
||||
* request time.
|
||||
* @param {object[]} [opts.requests] a list for the requests this handler
|
||||
* sees. Each is appended in order as { method, url, fromWorker }, whether it
|
||||
* came from a popup page or the background worker.
|
||||
* @returns {Promise<{waitForServiceWorkerTraffic: (ms: number) =>
|
||||
* Promise<string|null>}>}
|
||||
*/
|
||||
@@ -691,6 +697,13 @@ async function installNetworkStubs(ctx, opts) {
|
||||
const origin = fromWorker ? "[sw] " : "[page] ";
|
||||
console.log("# routed " + origin + req.method() + " " + req.url());
|
||||
}
|
||||
if (Array.isArray(opts.requests)) {
|
||||
opts.requests.push({
|
||||
method: req.method(),
|
||||
url: req.url(),
|
||||
fromWorker,
|
||||
});
|
||||
}
|
||||
|
||||
// JSON-RPC endpoint (any host): a POST with a JSON-RPC body.
|
||||
if (req.method() === "POST") {
|
||||
@@ -762,12 +775,20 @@ async function installNetworkStubs(ctx, opts) {
|
||||
return route.fulfill({ status: 204, body: "" });
|
||||
}
|
||||
|
||||
// Best-effort Etherscan address labels: served as an empty page.
|
||||
// Best-effort Etherscan address labels: served as an empty page, or
|
||||
// as a page labelling the address with opts.etherscanLabel.
|
||||
if (url.hostname.endsWith("etherscan.io")) {
|
||||
const title = opts.etherscanLabel
|
||||
? "<title>" +
|
||||
opts.etherscanLabel +
|
||||
" | Address: " +
|
||||
p.split("/").pop() +
|
||||
" | Etherscan</title>"
|
||||
: "";
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: "text/html",
|
||||
body: "<html><body></body></html>",
|
||||
body: "<html><head>" + title + "</head><body></body></html>",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -269,6 +269,70 @@ test("the exact confirmation phrase erases the record and reloads into Welcome (
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------ Blockscout and Etherscan are sent no user data (#668)
|
||||
//
|
||||
// A fresh install sends Blockscout and Etherscan none of the user's data until
|
||||
// the user turns on "Use Blockscout" or "Check recipients on Etherscan". One
|
||||
// request still reaches Blockscout while "Use Blockscout" is off: pressing
|
||||
// "Save" beside the Blockscout URL asks that URL for `/stats`. That request
|
||||
// carries no user data.
|
||||
//
|
||||
// The route handler in tests/e2e/network.js records in env.routeOpts.requests
|
||||
// every request the extension makes, from its pages and its background worker.
|
||||
// Recording starts when launch() in tests/e2e/harness.js installs that handler,
|
||||
// before the first popup page opens. The background worker also makes requests
|
||||
// while it starts up. Those come before the handler is installed, so they are
|
||||
// not recorded.
|
||||
//
|
||||
// Before either setting is changed, these tests drive:
|
||||
//
|
||||
// - Home, AddressDetail, AddressToken and ConfirmTx
|
||||
// - the background's own balance refresh
|
||||
//
|
||||
// Then they turn each setting on and watch its requests start. "Use Blockscout"
|
||||
// is also turned off once and on again, to see an untracked token leave every
|
||||
// list. TransactionDetail, which also asks Blockscout, is left to the unit
|
||||
// tests in tests/blockscoutEtherscanSettings.test.js.
|
||||
|
||||
// The line shown in place of the three transaction histories while "Use
|
||||
// Blockscout" is off. Written out here rather than imported, so the assertion
|
||||
// does not compare the popup with itself.
|
||||
const HISTORY_OFF =
|
||||
'No transaction history is shown, because "Use Blockscout" is off in Settings.';
|
||||
|
||||
// The requests made so far to Blockscout or Etherscan.
|
||||
function explorerRequests(env) {
|
||||
return env.routeOpts.requests.filter(({ url }) => {
|
||||
const host = new URL(url).hostname;
|
||||
return host.endsWith("blockscout.com") || host.endsWith("etherscan.io");
|
||||
});
|
||||
}
|
||||
|
||||
// Waits until the list `listId` shows the line saying "Use Blockscout" is off.
|
||||
async function waitForHistoryOff(page, listId) {
|
||||
await page.waitForFunction(
|
||||
([id, want]) => document.getElementById(id).textContent.trim() === want,
|
||||
[listId, HISTORY_OFF],
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
}
|
||||
|
||||
test("Welcome offers Use Blockscout and Check recipients on Etherscan, both unticked (#668)", async (env) => {
|
||||
await visible(env.page, "#view-welcome");
|
||||
for (const id of [
|
||||
"welcome-use-blockscout",
|
||||
"welcome-check-recipients-on-etherscan",
|
||||
]) {
|
||||
const st = await controlState(env.page, id);
|
||||
assert(st !== null, "Welcome has no #" + id);
|
||||
assert(
|
||||
st.tag === "input" && st.type === "checkbox",
|
||||
"#" + id + " is a <" + st.tag + " type=" + st.type + ">",
|
||||
);
|
||||
assert(st.checked === false, "#" + id + " is ticked on a new install");
|
||||
}
|
||||
});
|
||||
|
||||
// The address scan that follows creating the wallet is held, so the popup
|
||||
// reloads while it is in flight. As for the reloads mid-refresh further down,
|
||||
// the assertion for that is the harness's own: a console.error from the page
|
||||
@@ -296,6 +360,261 @@ test("wallet creation through the UI reaches the main view, and reloading mid-sc
|
||||
assert(addrCount > 0, "no addresses rendered in the wallet list");
|
||||
});
|
||||
|
||||
test("with neither setting changed, Home, AddressDetail, AddressToken and ConfirmTx say what is off (#668)", async (env) => {
|
||||
// Funded, so Send leads to the confirmation screen.
|
||||
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||
try {
|
||||
await visible(env.page, "#view-main");
|
||||
await waitForHistoryOff(env.page, "home-tx-list");
|
||||
await visible(env.page, "#home-tokens-note");
|
||||
await env.page.waitForFunction(
|
||||
(want) =>
|
||||
document
|
||||
.getElementById("wallet-list")
|
||||
.textContent.includes(want),
|
||||
quantity(FUNDED_ETH_WEI),
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
|
||||
await openAddressDetail(env.page);
|
||||
await waitForHistoryOff(env.page, "tx-list");
|
||||
await visible(env.page, "#address-tokens-note");
|
||||
|
||||
await env.page.locator('#address-balances [data-token="ETH"]').click();
|
||||
await visible(env.page, "#view-address-token");
|
||||
await waitForHistoryOff(env.page, "address-token-tx-list");
|
||||
await env.page.click("#btn-address-token-back");
|
||||
await visible(env.page, "#view-address");
|
||||
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: COMFORTABLE_AMOUNT,
|
||||
});
|
||||
await waitForEstimate(env.page);
|
||||
await visible(env.page, "#confirm-etherscan-note");
|
||||
assert(
|
||||
!(await env.page.isVisible("#confirm-etherscan-warning")),
|
||||
"ConfirmTx keeps the Etherscan warning's place while no Etherscan check is made",
|
||||
);
|
||||
await backToAddress(env.page);
|
||||
} finally {
|
||||
env.routeOpts.ethBalanceWei = null;
|
||||
}
|
||||
});
|
||||
|
||||
// The background refreshes balances on its alarm, once a minute. It skips a
|
||||
// refresh when the stored record says one ran in the last 30 seconds. The open
|
||||
// popup's own refresh runs every 10. So the popup is closed first. The test then
|
||||
// has the background worker set the stored time of the last refresh to 0, so
|
||||
// its next alarm runs a refresh.
|
||||
test("with neither setting changed, nothing has reached Blockscout or Etherscan, the background's refresh included (#668)", async (env) => {
|
||||
await waitForPersisted(
|
||||
env.page,
|
||||
"currentView",
|
||||
"address",
|
||||
"before the popup is closed",
|
||||
);
|
||||
await env.page.close();
|
||||
const [worker] = env.ctx.serviceWorkers();
|
||||
await worker.evaluate(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
chrome.storage.local.get("autistmask", (r) => {
|
||||
const record = r.autistmask;
|
||||
record.lastBalanceRefresh = 0;
|
||||
chrome.storage.local.set({ autistmask: record }, resolve);
|
||||
});
|
||||
}),
|
||||
);
|
||||
const before = env.routeOpts.requests.length;
|
||||
const deadline = Date.now() + 75000;
|
||||
while (
|
||||
!env.routeOpts.requests
|
||||
.slice(before)
|
||||
.some((r) => r.fromWorker && r.method === "POST")
|
||||
) {
|
||||
assert(
|
||||
Date.now() < deadline,
|
||||
"the background's balance refresh never asked the RPC endpoint",
|
||||
);
|
||||
await sleep(250);
|
||||
}
|
||||
// The refresh's RPC reads and any Blockscout request start together.
|
||||
await sleep(3000);
|
||||
|
||||
const seen = explorerRequests(env);
|
||||
assert(
|
||||
seen.length === 0,
|
||||
"a fresh install asked Blockscout or Etherscan: " +
|
||||
JSON.stringify(seen),
|
||||
);
|
||||
// The watch saw the popup's own traffic as well as the worker's.
|
||||
assert(
|
||||
env.routeOpts.requests.some((r) => !r.fromWorker),
|
||||
"no request from the popup was recorded",
|
||||
);
|
||||
// The popup reopens on the address screen the test above left it on.
|
||||
env.page = await openPopup(env.ctx, env.popupUrl);
|
||||
await visible(env.page, "#view-address");
|
||||
});
|
||||
|
||||
// Ticked from AddressDetail's gear, so Back lands on the screen that said the
|
||||
// setting is off.
|
||||
test("turning on Use Blockscout from AddressDetail's gear asks Blockscout for that screen's history on Back, then for the tokens (#668)", async (env) => {
|
||||
await openAddressDetail(env.page);
|
||||
await waitForHistoryOff(env.page, "tx-list");
|
||||
await openSettings(env.page);
|
||||
await env.page.check("#settings-use-blockscout");
|
||||
await waitForPersisted(env.page, "useBlockscout", true, "once ticked");
|
||||
const historyAsked = env.page.waitForRequest(
|
||||
(r) =>
|
||||
r.url().includes("blockscout.com") &&
|
||||
r.url().endsWith("/transactions"),
|
||||
);
|
||||
const tokensAsked = env.page.waitForRequest(
|
||||
(r) => r.url().endsWith("/token-balances"),
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
await env.page.click("#btn-settings-back");
|
||||
await visible(env.page, "#view-address");
|
||||
await historyAsked;
|
||||
await env.page.waitForFunction(
|
||||
() =>
|
||||
document
|
||||
.getElementById("tx-list")
|
||||
.textContent.includes("No transactions found."),
|
||||
null,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
assert(
|
||||
!(await env.page.isVisible("#address-tokens-note")),
|
||||
"AddressDetail still says only tracked tokens are listed",
|
||||
);
|
||||
|
||||
await goHome(env.page);
|
||||
await tokensAsked;
|
||||
await env.page.waitForFunction(
|
||||
() =>
|
||||
document
|
||||
.getElementById("home-tx-list")
|
||||
.textContent.includes("No transactions found."),
|
||||
null,
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
assert(
|
||||
!(await env.page.isVisible("#home-tokens-note")),
|
||||
"Home still says only tracked tokens are listed",
|
||||
);
|
||||
});
|
||||
|
||||
// Unticked from AddressDetail's gear while the address holds the stub token,
|
||||
// which the user has not tracked and only Blockscout reports. Ticked again at
|
||||
// the end, so the rest of the run has Blockscout as before.
|
||||
test("turning off Use Blockscout from AddressDetail's gear takes an untracked token off AddressDetail, Send and Home on Back (#668)", async (env) => {
|
||||
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||
env.routeOpts.seedTokenBalance = true;
|
||||
const tokenRow = '[data-token="' + STUB_TOKEN.address + '"]';
|
||||
try {
|
||||
await settleOnMain(env, { ethWei: FUNDED_ETH_WEI, expectToken: true });
|
||||
await openAddressDetail(env.page);
|
||||
await visible(env.page, "#address-balances " + tokenRow);
|
||||
|
||||
await openSettings(env.page);
|
||||
await env.page.uncheck("#settings-use-blockscout");
|
||||
await waitForPersisted(
|
||||
env.page,
|
||||
"useBlockscout",
|
||||
false,
|
||||
"once unticked",
|
||||
);
|
||||
await env.page.click("#btn-settings-back");
|
||||
await visible(env.page, "#view-address");
|
||||
await visible(env.page, "#address-tokens-note");
|
||||
assert(
|
||||
(await env.page
|
||||
.locator("#address-balances " + tokenRow)
|
||||
.count()) === 0,
|
||||
"AddressDetail still lists the untracked token",
|
||||
);
|
||||
|
||||
await env.page.click("#btn-send");
|
||||
await visible(env.page, "#view-send");
|
||||
await visible(env.page, "#send-tokens-note");
|
||||
const offered = await env.page.$$eval("#send-token option", (opts) =>
|
||||
opts.map((opt) => opt.value.toLowerCase()),
|
||||
);
|
||||
assert(
|
||||
!offered.includes(STUB_TOKEN.address),
|
||||
"Send's dropdown still offers the untracked token",
|
||||
);
|
||||
await env.page.click("#btn-send-back");
|
||||
await visible(env.page, "#view-address");
|
||||
|
||||
await goHome(env.page);
|
||||
await visible(env.page, "#home-tokens-note");
|
||||
assert(
|
||||
(await env.page.locator("#wallet-list " + tokenRow).count()) === 0,
|
||||
"Home still lists the untracked token",
|
||||
);
|
||||
} finally {
|
||||
await openSettings(env.page);
|
||||
await env.page.check("#settings-use-blockscout");
|
||||
await waitForPersisted(env.page, "useBlockscout", true, "ticked again");
|
||||
await env.page.click("#btn-settings-back");
|
||||
env.routeOpts.ethBalanceWei = null;
|
||||
env.routeOpts.seedTokenBalance = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Ticked from ConfirmTx's gear, so Back lands on the screen that said no
|
||||
// Etherscan check was made. Left on for the rest of the run, so the later
|
||||
// confirmation screens are driven with the check made.
|
||||
test("turning on Check recipients on Etherscan from ConfirmTx's gear has ConfirmTx ask Etherscan on Back and show its label (#668)", async (env) => {
|
||||
env.routeOpts.ethBalanceWei = toHexWei(FUNDED_ETH_WEI);
|
||||
env.routeOpts.etherscanLabel = "Fake_Phishing668";
|
||||
try {
|
||||
await settleOnMain(env, { ethWei: FUNDED_ETH_WEI });
|
||||
await goToConfirm(env.page, {
|
||||
token: "ETH",
|
||||
balance: FUNDED_ETH_TEXT + " ETH",
|
||||
amount: COMFORTABLE_AMOUNT,
|
||||
});
|
||||
await visible(env.page, "#confirm-etherscan-note");
|
||||
|
||||
await openSettings(env.page);
|
||||
await env.page.check("#settings-check-recipients-on-etherscan");
|
||||
await waitForPersisted(
|
||||
env.page,
|
||||
"checkRecipientsOnEtherscan",
|
||||
true,
|
||||
"once ticked",
|
||||
);
|
||||
const before = env.routeOpts.requests.length;
|
||||
await env.page.click("#btn-settings-back");
|
||||
await visible(env.page, "#view-confirm-tx");
|
||||
await visible(env.page, "#confirm-etherscan-warning", 30000);
|
||||
assert(
|
||||
!(await env.page.isVisible("#confirm-etherscan-note")),
|
||||
"ConfirmTx says no Etherscan check was made, with the check on",
|
||||
);
|
||||
const asked = env.routeOpts.requests
|
||||
.slice(before)
|
||||
.map((r) => r.url.toLowerCase())
|
||||
.filter((url) => url.includes("etherscan.io"));
|
||||
assert(
|
||||
asked.length === 1 &&
|
||||
asked[0] ===
|
||||
"https://etherscan.io/address/" + STUB_COUNTERPARTY,
|
||||
"ConfirmTx asked Etherscan for " + JSON.stringify(asked),
|
||||
);
|
||||
await backToAddress(env.page);
|
||||
} finally {
|
||||
env.routeOpts.ethBalanceWei = null;
|
||||
env.routeOpts.etherscanLabel = null;
|
||||
}
|
||||
});
|
||||
|
||||
test("add token screen opens from address detail (#150)", async (env) => {
|
||||
await openAddressDetail(env.page);
|
||||
await env.page.click("#btn-add-token");
|
||||
@@ -5258,10 +5577,17 @@ async function main() {
|
||||
seedReceipt: false,
|
||||
// Whether eth_getTransactionReceipt fails instead of answering (#315).
|
||||
failReceiptLookup: false,
|
||||
// The label Etherscan's page gives an address, when it gives one
|
||||
// (#668).
|
||||
etherscanLabel: null,
|
||||
// Every raw signed transaction handed to eth_sendRawTransaction, in
|
||||
// order. The dApp transaction round trip asserts against these bytes
|
||||
// rather than against anything the extension reported about them.
|
||||
broadcastTransactions: [],
|
||||
// Every request the extension makes, in order, from when launch()
|
||||
// installs the route handler, before the first popup page opens
|
||||
// (#668).
|
||||
requests: [],
|
||||
};
|
||||
|
||||
let session;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// ENS lookups ask nothing but the RPC endpoint. A resolver can answer that a
|
||||
// name is resolved off-chain, naming a web address to fetch the answer from.
|
||||
// ethers fetches that address unless the provider has `disableCcipRead` set.
|
||||
// That request would carry the name to a server other than the RPC endpoint
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/668). With the flag set, such a
|
||||
// name does not resolve.
|
||||
//
|
||||
// The test builds its provider with getProvider() and runs it against a fake
|
||||
// JSON-RPC node. The fake node answers through ethers' own request hook. The
|
||||
// hook also records the address of every request ethers makes, the off-chain
|
||||
// fetch included.
|
||||
|
||||
const { FetchRequest, Interface } = require("ethers");
|
||||
const { getProvider } = require("../src/shared/balances");
|
||||
|
||||
const RPC_URL = "https://rpc.example.invalid";
|
||||
const RESOLVER = "0x5555555555555555555555555555555555555555";
|
||||
const USER = "0x1111111111111111111111111111111111111111";
|
||||
const NAME = "offchain.eth";
|
||||
|
||||
// The ENS registry's resolver(), and the resolver's functions ethers calls:
|
||||
//
|
||||
// - the registry names RESOLVER for every name
|
||||
// - the resolver says it resolves names through resolve()
|
||||
// - it gives NAME as the reverse record of any address
|
||||
// - it answers resolve() with the OffchainLookup error, which says the name
|
||||
// is resolved off-chain
|
||||
const ens = new Interface([
|
||||
"function resolver(bytes32) view returns (address)",
|
||||
"function supportsInterface(bytes4) view returns (bool)",
|
||||
"function name(bytes32) view returns (string)",
|
||||
"function resolve(bytes, bytes) view returns (bytes)",
|
||||
"error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData)",
|
||||
]);
|
||||
|
||||
let asked;
|
||||
|
||||
// One JSON-RPC answer. Anything but a call to one of the functions above is
|
||||
// refused.
|
||||
function reply(req) {
|
||||
const envelope = { jsonrpc: "2.0", id: req.id };
|
||||
const call =
|
||||
req.method === "eth_call" ? ens.parseTransaction(req.params[0]) : null;
|
||||
if (call === null) {
|
||||
return {
|
||||
...envelope,
|
||||
error: { code: -32601, message: "method not found" },
|
||||
};
|
||||
}
|
||||
if (call.name === "resolve") {
|
||||
const offchain = ens.encodeErrorResult("OffchainLookup", [
|
||||
RESOLVER,
|
||||
["https://gateway.example.invalid/{sender}/{data}.json"],
|
||||
"0x1234",
|
||||
"0x12345678",
|
||||
"0x",
|
||||
]);
|
||||
return {
|
||||
...envelope,
|
||||
error: { code: 3, message: "execution reverted", data: offchain },
|
||||
};
|
||||
}
|
||||
const answers = {
|
||||
resolver: [RESOLVER],
|
||||
supportsInterface: [true],
|
||||
name: [NAME],
|
||||
};
|
||||
return {
|
||||
...envelope,
|
||||
result: ens.encodeFunctionResult(call.name, answers[call.name]),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
asked = [];
|
||||
FetchRequest.registerGetUrl(async (req) => {
|
||||
asked.push(req.url);
|
||||
// Any other address is the off-chain fetch, answered as not found.
|
||||
if (req.url !== RPC_URL) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
statusMessage: "Not Found",
|
||||
headers: {},
|
||||
body: null,
|
||||
};
|
||||
}
|
||||
const body = JSON.parse(Buffer.from(req.body).toString("utf8"));
|
||||
const answer = Array.isArray(body) ? body.map(reply) : reply(body);
|
||||
return {
|
||||
statusCode: 200,
|
||||
statusMessage: "OK",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: Buffer.from(JSON.stringify(answer)),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
|
||||
});
|
||||
|
||||
test("a name typed into Send that is resolved off-chain does not resolve, and only the RPC endpoint is asked", async () => {
|
||||
const provider = getProvider(RPC_URL, "mainnet");
|
||||
const [outcome] = await Promise.allSettled([provider.resolveName(NAME)]);
|
||||
expect(asked.filter((url) => url !== RPC_URL)).toEqual([]);
|
||||
expect(outcome).toEqual({ status: "fulfilled", value: null });
|
||||
});
|
||||
|
||||
test("the reverse lookup of a user's address whose name is resolved off-chain finds no name, and only the RPC endpoint is asked", async () => {
|
||||
const provider = getProvider(RPC_URL, "mainnet");
|
||||
const [outcome] = await Promise.allSettled([provider.lookupAddress(USER)]);
|
||||
expect(asked.filter((url) => url !== RPC_URL)).toEqual([]);
|
||||
expect(outcome).toEqual({ status: "fulfilled", value: null });
|
||||
});
|
||||
@@ -35,8 +35,11 @@ global.fetch = jest.fn(() => {
|
||||
throw new Error("tests must not perform network requests");
|
||||
});
|
||||
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = { storage: makeStorageStub() };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
};
|
||||
|
||||
const { AbiCoder, Interface } = require("ethers");
|
||||
const { ERC20_ABI } = require("../src/shared/constants");
|
||||
@@ -165,6 +168,9 @@ beforeEach(() => {
|
||||
debugFetch.mockReset();
|
||||
state.trackedTokens = [];
|
||||
state.wallets = [];
|
||||
// The approval screens read the explorer's report only while the page's
|
||||
// "Use Blockscout" is on.
|
||||
state.useBlockscout = true;
|
||||
});
|
||||
|
||||
describe("what fetchTokenBalances stores for an absent scale", () => {
|
||||
|
||||
@@ -21,7 +21,11 @@ jest.mock("../src/shared/log", () => ({
|
||||
global.fetch = jest.fn(() => {
|
||||
throw new Error("tests must not perform network requests");
|
||||
});
|
||||
global.chrome = { storage: { local: {} } };
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
};
|
||||
|
||||
const {
|
||||
LOW_HOLDER_THRESHOLD,
|
||||
|
||||
@@ -436,6 +436,13 @@ describe.each([
|
||||
});
|
||||
|
||||
test("the transaction detail screen's type line and fee", async () => {
|
||||
// The fee is Blockscout's, asked for only while the stored "Use
|
||||
// Blockscout" is on.
|
||||
state.useBlockscout = true;
|
||||
global.chrome.storage.write("autistmask", {
|
||||
...global.chrome.storage.read("autistmask"),
|
||||
useBlockscout: true,
|
||||
});
|
||||
debugFetch.mockImplementationOnce(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
|
||||
@@ -286,8 +286,12 @@ describe("a malformed fraudContracts", () => {
|
||||
function stubSendDocument() {
|
||||
const select = { innerHTML: "", children: [] };
|
||||
select.appendChild = (child) => select.children.push(child);
|
||||
const elements = {
|
||||
"send-token": select,
|
||||
"send-tokens-note": { classList: { toggle: () => {} } },
|
||||
};
|
||||
globalThis.document = {
|
||||
getElementById: (id) => (id === "send-token" ? select : null),
|
||||
getElementById: (id) => elements[id] || null,
|
||||
createElement: () => ({ value: "", textContent: "" }),
|
||||
};
|
||||
return select;
|
||||
|
||||
@@ -629,6 +629,14 @@ const CONTRACT = [
|
||||
// the falsy answer is also the DEFAULT_STATE default.
|
||||
falsy: [0],
|
||||
})),
|
||||
// Each lets a request reach another service, so anything but true is off.
|
||||
...["useBlockscout", "checkRecipientsOnEtherscan"].map((field) => ({
|
||||
field,
|
||||
kind: KIND.SCALAR,
|
||||
hostile: [42, "notabool", { a: 1 }],
|
||||
floorOnly: [0, null, "true"],
|
||||
holds: (v) => v === false,
|
||||
})),
|
||||
];
|
||||
|
||||
function siteMapHolds(v) {
|
||||
|
||||
@@ -44,7 +44,9 @@ test("a flash message's timer does not fire after cleanupPopup()", async () => {
|
||||
|
||||
// A boot onto a screen that sends a request no mocked module answers: that
|
||||
// request reaches the boot's fetch() stub, never the internet
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/559).
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/559). TransactionDetail asks
|
||||
// Blockscout only while "Use Blockscout" is on. ConfirmTx asks Etherscan only
|
||||
// while "Check recipients on Etherscan" is on. So the profile turns both on.
|
||||
function restoringOnto(view, viewData) {
|
||||
return unversionedValidProfile({
|
||||
currentView: view,
|
||||
@@ -52,6 +54,8 @@ function restoringOnto(view, viewData) {
|
||||
selectedAddress: 0,
|
||||
viewStack: ["main"],
|
||||
viewData,
|
||||
useBlockscout: true,
|
||||
checkRecipientsOnEtherscan: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ let approvalDetails = null;
|
||||
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = {
|
||||
storage: makeStorageStub(),
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
runtime: {
|
||||
connect: () => ({
|
||||
postMessage() {},
|
||||
@@ -220,6 +220,10 @@ async function refreshWith(balanceWei, tokenItems = []) {
|
||||
state.wallets = [{ name: "Wallet 1", addresses: [{ address: HOLDER }] }];
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on. The
|
||||
// screens save this page's copy of "Use Blockscout" over the stored one. So
|
||||
// the page's copy is turned on here too.
|
||||
state.useBlockscout = true;
|
||||
await refreshBalances(
|
||||
state.wallets,
|
||||
"https://rpc.example.invalid",
|
||||
|
||||
@@ -100,7 +100,10 @@ global.fetch = jest.fn(() => {
|
||||
});
|
||||
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
runtime: { sendMessage() {} },
|
||||
};
|
||||
|
||||
// A stub DOM: every id resolves to a recording element.
|
||||
const elements = new Map();
|
||||
@@ -213,6 +216,10 @@ async function refreshWith(balanceWei, tokenItems = []) {
|
||||
];
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on. The
|
||||
// screens save this page's copy of "Use Blockscout" over the stored one. So
|
||||
// the page's copy is turned on here too.
|
||||
state.useBlockscout = true;
|
||||
await refreshBalances(
|
||||
state.wallets,
|
||||
"https://rpc.example.invalid",
|
||||
|
||||
@@ -27,8 +27,12 @@ let select;
|
||||
function installStubDocument() {
|
||||
select = { innerHTML: "", children: [] };
|
||||
select.appendChild = (child) => select.children.push(child);
|
||||
const elements = {
|
||||
"send-token": select,
|
||||
"send-tokens-note": { classList: { toggle: () => {} } },
|
||||
};
|
||||
globalThis.document = {
|
||||
getElementById: (id) => (id === "send-token" ? select : null),
|
||||
getElementById: (id) => elements[id] || null,
|
||||
createElement: () => ({ value: "", textContent: "" }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -523,13 +523,16 @@ describe("a garbage value in a field the gate does not check", () => {
|
||||
});
|
||||
|
||||
test("a malformed tokenBalances entry is dropped, and the wallet and its address survive", async () => {
|
||||
const env = await bootPopup(
|
||||
profileWithTokenBalances([
|
||||
null,
|
||||
42,
|
||||
{ address: TOKEN_ADDRESS, symbol: "AAA", balance: "2.0" },
|
||||
]),
|
||||
);
|
||||
// "Use Blockscout" is on. Otherwise the boot's balance refresh would
|
||||
// drop the well-formed entry too, since the user has not tracked its
|
||||
// token.
|
||||
const profile = profileWithTokenBalances([
|
||||
null,
|
||||
42,
|
||||
{ address: TOKEN_ADDRESS, symbol: "AAA", balance: "2.0" },
|
||||
]);
|
||||
profile.useBlockscout = true;
|
||||
const env = await bootPopup(profile);
|
||||
|
||||
const stored = env.storage.read("autistmask");
|
||||
expect(stored.wallets[0].encryptedSecret).toBe("encrypted-secret-1");
|
||||
|
||||
@@ -283,6 +283,8 @@ async function bootPopup(stored, options) {
|
||||
jest.doMock("../../src/shared/balances", () => ({
|
||||
fetchTokenBalances: jest.fn(async () => []),
|
||||
refreshBalances: jest.fn(async () => {}),
|
||||
dropUntrackedTokens: jest.requireActual("../../src/shared/balances")
|
||||
.dropUntrackedTokens,
|
||||
lookupTokenInfo: jest.fn(async () => null),
|
||||
getProvider: () => ({}),
|
||||
scanForAddresses: jest.fn(async () => ({
|
||||
|
||||
@@ -52,8 +52,14 @@ jest.mock("../src/shared/log", () => ({
|
||||
global.fetch = jest.fn(() => {
|
||||
throw new Error("tests must not perform network requests");
|
||||
});
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
global.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
storage: {
|
||||
local: {
|
||||
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||
set: async () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { isSpoofedSymbol } = require("../src/shared/symbolSpoof");
|
||||
@@ -485,8 +491,12 @@ describe("surface 2: the Send token selector", () => {
|
||||
function render(tokenBalances) {
|
||||
select = { innerHTML: "", children: [] };
|
||||
select.appendChild = (child) => select.children.push(child);
|
||||
const elements = {
|
||||
"send-token": select,
|
||||
"send-tokens-note": { classList: { toggle: () => {} } },
|
||||
};
|
||||
globalThis.document = {
|
||||
getElementById: (id) => (id === "send-token" ? select : null),
|
||||
getElementById: (id) => elements[id] || null,
|
||||
createElement: () => ({ value: "", textContent: "" }),
|
||||
};
|
||||
renderSendTokenSelect({
|
||||
|
||||
@@ -134,6 +134,8 @@ beforeEach(() => {
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
state.selectedToken = "ETH";
|
||||
// The history is Blockscout's.
|
||||
state.useBlockscout = true;
|
||||
});
|
||||
|
||||
describe.each([
|
||||
|
||||
@@ -38,8 +38,14 @@ jest.mock("../src/shared/log", () => ({
|
||||
isDebug: () => false,
|
||||
}));
|
||||
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
storage: {
|
||||
local: {
|
||||
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||
set: async () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { state } = require("../src/shared/state");
|
||||
@@ -158,6 +164,7 @@ beforeEach(() => {
|
||||
globalThis.document = makeDocument();
|
||||
globalThis.window = { location: { search: "" } };
|
||||
state.blockscoutUrl = EXPLORER;
|
||||
state.useBlockscout = true;
|
||||
state.networkEndpoints = {};
|
||||
state.wallets = [
|
||||
{
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
|
||||
const { AbiCoder, FetchRequest } = require("ethers");
|
||||
const { refreshBalances } = require("../src/shared/balances");
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
globalThis.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
};
|
||||
|
||||
const RPC_URL = "https://rpc.example.invalid";
|
||||
const EXPLORER_URL = "https://explorer.example.invalid/api/v2";
|
||||
|
||||
@@ -287,6 +287,7 @@ describe("Back after a switch from the Settings gear", () => {
|
||||
currentView: "confirm-tx",
|
||||
selectedWallet: 0,
|
||||
selectedAddress: 0,
|
||||
checkRecipientsOnEtherscan: true,
|
||||
viewData: {
|
||||
pendingTx: {
|
||||
token: "ETH",
|
||||
|
||||
@@ -125,6 +125,8 @@ beforeEach(() => {
|
||||
state.selectedWallet = 0;
|
||||
state.selectedAddress = 0;
|
||||
state.selectedToken = "ETH";
|
||||
// The history is Blockscout's.
|
||||
state.useBlockscout = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1215,6 +1215,8 @@ function respondWith(txItems, tokenTransferItems) {
|
||||
describe("fetchRecentTransactions merge and dedup", () => {
|
||||
beforeEach(() => {
|
||||
debugFetch.mockReset();
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
global.chrome.storage.write("autistmask", { useBlockscout: true });
|
||||
});
|
||||
|
||||
test("queries only the two Blockscout endpoints for the address", async () => {
|
||||
|
||||
@@ -42,8 +42,14 @@ jest.mock("../src/shared/log", () => ({
|
||||
isDebug: () => false,
|
||||
}));
|
||||
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
storage: {
|
||||
local: {
|
||||
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||
set: async () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const uniswap = require("../src/shared/uniswap");
|
||||
@@ -3456,6 +3462,7 @@ async function showDetail(data) {
|
||||
globalThis.window = { location: { search: "" } };
|
||||
state.networkId = "mainnet";
|
||||
state.blockscoutUrl = "https://explorer.example/api/v2";
|
||||
state.useBlockscout = true;
|
||||
state.viewData = {};
|
||||
state.viewStack = [];
|
||||
state.currentView = null;
|
||||
|
||||
@@ -34,8 +34,14 @@ jest.mock("../src/shared/log", () => ({
|
||||
isDebug: () => false,
|
||||
}));
|
||||
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
globalThis.chrome = {
|
||||
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||
storage: {
|
||||
local: {
|
||||
get: async () => ({ autistmask: { useBlockscout: true } }),
|
||||
set: async () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { state } = require("../src/shared/state");
|
||||
@@ -208,6 +214,7 @@ async function showDetail(row, activeNetworkId) {
|
||||
globalThis.window = { location: { search: "" } };
|
||||
state.networkId = activeNetworkId;
|
||||
state.blockscoutUrl = "https://explorer.example/api/v2";
|
||||
state.useBlockscout = true;
|
||||
state.viewData = {};
|
||||
state.viewStack = [];
|
||||
state.currentView = null;
|
||||
|
||||
@@ -90,6 +90,9 @@ function swapDetail(data, label) {
|
||||
beforeEach(() => {
|
||||
state.trackedTokens = [];
|
||||
state.wallets = [];
|
||||
// The approval screen reads the explorer's report only while "Use
|
||||
// Blockscout" is on.
|
||||
state.useBlockscout = true;
|
||||
});
|
||||
|
||||
describe("a swap of a token outside the bundled list", () => {
|
||||
|
||||
@@ -72,8 +72,12 @@ global.fetch = jest.fn(() => {
|
||||
throw new Error("tests must not perform network requests");
|
||||
});
|
||||
|
||||
// Blockscout is asked only while the stored "Use Blockscout" is on.
|
||||
const { makeStorageStub } = require("./support/storageStub");
|
||||
global.chrome = { storage: makeStorageStub(), runtime: { sendMessage() {} } };
|
||||
global.chrome = {
|
||||
storage: makeStorageStub({ autistmask: { useBlockscout: true } }),
|
||||
runtime: { sendMessage() {} },
|
||||
};
|
||||
|
||||
// A stub DOM. Every id in index.html that these two views touch resolves to a
|
||||
// fresh recording element; nothing here depends on layout, only on what the
|
||||
@@ -268,6 +272,9 @@ beforeEach(() => {
|
||||
state.fraudContracts = [];
|
||||
state.hideLowHolderTokens = false;
|
||||
state.currentView = null;
|
||||
// The screens save this page's copy of "Use Blockscout" over the stored
|
||||
// one. So the page's copy is turned on here too.
|
||||
state.useBlockscout = true;
|
||||
});
|
||||
|
||||
describe("the Send screen resolves the scale rather than reading the stored one", () => {
|
||||
|
||||
Reference in New Issue
Block a user