Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f46515d87 | ||
|
|
bcdffa56a4 |
No files matched your search
@@ -905,8 +905,8 @@ src/
|
|||||||
prices.js — ETH/USD and token/USD from Chainlink price feeds
|
prices.js — ETH/USD and token/USD from Chainlink price feeds
|
||||||
scamlist.js — known fraud contract addresses
|
scamlist.js — known fraud contract addresses
|
||||||
state.js — persisted state (extension storage)
|
state.js — persisted state (extension storage)
|
||||||
storedSettings.js — "Use Blockscout" and "Check recipients on Etherscan",
|
storedSettings.js — "Use Blockscout", "Check recipients on Etherscan" and
|
||||||
read from storage for the requests those settings gate
|
the active network, read from storage
|
||||||
symbolSpoof.js — the known-symbol spoof rule, shared by all surfaces
|
symbolSpoof.js — the known-symbol spoof rule, shared by all surfaces
|
||||||
tokenList.js — top ERC-20 tokens by market cap, and their Chainlink
|
tokenList.js — top ERC-20 tokens by market cap, and their Chainlink
|
||||||
price feeds (hardcoded)
|
price feeds (hardcoded)
|
||||||
|
|||||||
@@ -334,6 +334,11 @@ function showNewSettings() {
|
|||||||
// src/popup/index.js) between setting the URL and writing it. If the active
|
// src/popup/index.js) between setting the URL and writing it. If the active
|
||||||
// network is no longer `networkId`, on this page or in the stored record,
|
// network is no longer `networkId`, on this page or in the stored record,
|
||||||
// nothing is saved. The flash message then says that the network changed.
|
// nothing is saved. The flash message then says that the network changed.
|
||||||
|
//
|
||||||
|
// The save writes only the fields that differ from what this page last saved.
|
||||||
|
// Another page may have stored a different URL since then. So `url` is set in
|
||||||
|
// the stored record too. "Saved." then means that the stored record holds
|
||||||
|
// `url`.
|
||||||
async function saveEndpoint(field, url, networkId) {
|
async function saveEndpoint(field, url, networkId) {
|
||||||
let refused = false;
|
let refused = false;
|
||||||
await saveState((stored) => {
|
await saveState((stored) => {
|
||||||
@@ -341,6 +346,7 @@ async function saveEndpoint(field, url, networkId) {
|
|||||||
state.networkId !== networkId || stored.networkId !== networkId;
|
state.networkId !== networkId || stored.networkId !== networkId;
|
||||||
if (refused) return false;
|
if (refused) return false;
|
||||||
state[field] = url;
|
state[field] = url;
|
||||||
|
stored[field] = url;
|
||||||
});
|
});
|
||||||
if (refused) {
|
if (refused) {
|
||||||
showFlash("The URL was not saved because the network changed.");
|
showFlash("The URL was not saved because the network changed.");
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const { resolveEnsNames } = require("../src/shared/ens");
|
|||||||
const { state, loadState } = require("../src/shared/state");
|
const { state, loadState } = require("../src/shared/state");
|
||||||
const { onChainSwitch } = require("../src/shared/chainSwitch");
|
const { onChainSwitch } = require("../src/shared/chainSwitch");
|
||||||
const { applyChainSwitchFields } = require("../src/shared/chainSwitchFields");
|
const { applyChainSwitchFields } = require("../src/shared/chainSwitchFields");
|
||||||
|
const { storedNetworkIs } = require("../src/shared/storedSettings");
|
||||||
const home = require("../src/popup/views/home");
|
const home = require("../src/popup/views/home");
|
||||||
const addressDetail = require("../src/popup/views/addressDetail");
|
const addressDetail = require("../src/popup/views/addressDetail");
|
||||||
const addressToken = require("../src/popup/views/addressToken");
|
const addressToken = require("../src/popup/views/addressToken");
|
||||||
@@ -196,7 +197,7 @@ function node(id) {
|
|||||||
return globalThis.document.getElementById(id);
|
return globalThis.document.getElementById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lets every pending promise settle.
|
// Lets every promise that is not held open settle.
|
||||||
function settle() {
|
function settle() {
|
||||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
}
|
}
|
||||||
@@ -242,7 +243,11 @@ const ADDRESS_TOKEN = {
|
|||||||
open: () => addressToken.show(),
|
open: () => addressToken.show(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// The two ways a network switch reaches the page.
|
// Three ways a test switches the network:
|
||||||
|
//
|
||||||
|
// - made in Settings: the page and storage have both switched;
|
||||||
|
// - saved elsewhere: storage has switched and the page has not;
|
||||||
|
// - not yet stored: the page has switched and storage has not.
|
||||||
const SWITCHES = [
|
const SWITCHES = [
|
||||||
{
|
{
|
||||||
how: "made in Settings",
|
how: "made in Settings",
|
||||||
@@ -258,6 +263,14 @@ const SWITCHES = [
|
|||||||
storage.write("autistmask", record);
|
storage.write("autistmask", record);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// The switch's own save has changed the page and has not yet written
|
||||||
|
// storage.
|
||||||
|
how: "not yet stored",
|
||||||
|
to: async (networkId) => {
|
||||||
|
applyChainSwitchFields(state, networkId);
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// The list still shows `before`, and neither the page nor storage holds a
|
// The list still shows `before`, and neither the page nor storage holds a
|
||||||
@@ -287,6 +300,7 @@ describe.each([HOME, ADDRESS_DETAIL, ADDRESS_TOKEN])("$name", (screen) => {
|
|||||||
async ({ to }) => {
|
async ({ to }) => {
|
||||||
const land = holdHistory();
|
const land = holdHistory();
|
||||||
screen.open();
|
screen.open();
|
||||||
|
await settle();
|
||||||
const before = node(screen.listId).innerHTML;
|
const before = node(screen.listId).innerHTML;
|
||||||
await to("sepolia");
|
await to("sepolia");
|
||||||
land();
|
land();
|
||||||
@@ -298,20 +312,35 @@ describe.each([HOME, ADDRESS_DETAIL, ADDRESS_TOKEN])("$name", (screen) => {
|
|||||||
|
|
||||||
// The answer is the transfer of ETH alone, which adds no fraud contract.
|
// The answer is the transfer of ETH alone, which adds no fraud contract.
|
||||||
// Nothing is saved, so only the check before the save can drop the answer.
|
// Nothing is saved, so only the check before the save can drop the answer.
|
||||||
test("an answer that lands once the page has switched and storage has not adds nothing", async () => {
|
test.each(SWITCHES)(
|
||||||
const land = holdHistory(history().slice(0, 1));
|
"an answer without a fraud contract that lands after a switch $how adds nothing",
|
||||||
screen.open();
|
async ({ to }) => {
|
||||||
await settle();
|
const land = holdHistory(history().slice(0, 1));
|
||||||
const before = node(screen.listId).innerHTML;
|
screen.open();
|
||||||
// The switch's own save has changed the page and has not yet written
|
await settle();
|
||||||
// storage.
|
const before = node(screen.listId).innerHTML;
|
||||||
applyChainSwitchFields(state, "sepolia");
|
await to("sepolia");
|
||||||
land();
|
land();
|
||||||
await settle();
|
await settle();
|
||||||
|
|
||||||
expectNothingAdded(screen, before);
|
expectNothingAdded(screen, before);
|
||||||
expect(storage.read("autistmask").networkId).toBe("mainnet");
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
|
test.each(SWITCHES)(
|
||||||
|
"a request that fails after a switch $how shows no message",
|
||||||
|
async ({ to }) => {
|
||||||
|
const fail = holdFailure();
|
||||||
|
screen.open();
|
||||||
|
await settle();
|
||||||
|
const before = node(screen.listId).innerHTML;
|
||||||
|
await to("sepolia");
|
||||||
|
fail();
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expect(node(screen.listId).innerHTML).toBe(before);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// The user picks Sepolia in Settings while another write holds the lock.
|
// The user picks Sepolia in Settings while another write holds the lock.
|
||||||
// The answer lands, and its save waits behind the switch.
|
// The answer lands, and its save waits behind the switch.
|
||||||
@@ -348,6 +377,22 @@ describe.each([HOME, ADDRESS_DETAIL, ADDRESS_TOKEN])("$name", (screen) => {
|
|||||||
expectNothingAdded(screen, before);
|
expectNothingAdded(screen, before);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The answer lands and its save waits for the lock. The page then switches,
|
||||||
|
// and storage has not.
|
||||||
|
test("an answer whose save waits while the page switches adds nothing", async () => {
|
||||||
|
const land = holdHistory();
|
||||||
|
screen.open();
|
||||||
|
const before = node(screen.listId).innerHTML;
|
||||||
|
const releaseLock = holdLock();
|
||||||
|
land();
|
||||||
|
await settle();
|
||||||
|
applyChainSwitchFields(state, "sepolia");
|
||||||
|
await releaseLock();
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expectNothingAdded(screen, before);
|
||||||
|
});
|
||||||
|
|
||||||
test("a request that fails after a switch leaves the new network's rows", async () => {
|
test("a request that fails after a switch leaves the new network's rows", async () => {
|
||||||
const fail = holdFailure();
|
const fail = holdFailure();
|
||||||
screen.open();
|
screen.open();
|
||||||
@@ -378,6 +423,19 @@ describe.each([ADDRESS_DETAIL, ADDRESS_TOKEN])("$name", (screen) => {
|
|||||||
expectNothingAdded(screen, before);
|
expectNothingAdded(screen, before);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a request that fails once the screen shows another address shows no message", async () => {
|
||||||
|
const fail = holdFailure();
|
||||||
|
screen.open();
|
||||||
|
state.selectedAddress = 1;
|
||||||
|
screen.open();
|
||||||
|
await settle();
|
||||||
|
const before = node(screen.listId).innerHTML;
|
||||||
|
fail();
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expect(node(screen.listId).innerHTML).toBe(before);
|
||||||
|
});
|
||||||
|
|
||||||
test("an answer whose name lookup lands after a switch adds nothing", async () => {
|
test("an answer whose name lookup lands after a switch adds nothing", async () => {
|
||||||
let landNames;
|
let landNames;
|
||||||
resolveEnsNames.mockImplementationOnce(
|
resolveEnsNames.mockImplementationOnce(
|
||||||
@@ -410,3 +468,26 @@ test("AddressToken: an answer that lands once the screen shows another token add
|
|||||||
|
|
||||||
expectNothingAdded(ADDRESS_TOKEN, before);
|
expectNothingAdded(ADDRESS_TOKEN, before);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("AddressToken: a request that fails once the screen shows another token shows no message", async () => {
|
||||||
|
const fail = holdFailure();
|
||||||
|
addressToken.show();
|
||||||
|
state.selectedToken = TOKEN;
|
||||||
|
addressToken.show();
|
||||||
|
await settle();
|
||||||
|
const before = node(ADDRESS_TOKEN.listId).innerHTML;
|
||||||
|
fail();
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expect(node(ADDRESS_TOKEN.listId).innerHTML).toBe(before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("storedNetworkIs(): a failed read counts as no", async () => {
|
||||||
|
globalThis.chrome.storage.local = {
|
||||||
|
get: async () => {
|
||||||
|
throw new Error("storage is down");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(storedNetworkIs("mainnet")).resolves.toBe(false);
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Another page stores a different RPC or Blockscout URL for the same network.
|
||||||
|
// Then the user taps "Save" in Settings on this page, on the URL that this page
|
||||||
|
// loaded. The URL on screen is stored, and the flash message reads "Saved."
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/688). Driven through the real
|
||||||
|
// popup over a stored profile on Ethereum Mainnet.
|
||||||
|
|
||||||
|
const {
|
||||||
|
bootPopup,
|
||||||
|
cleanupPopup,
|
||||||
|
unversionedValidProfile,
|
||||||
|
} = require("./support/popupBoot");
|
||||||
|
const { NETWORKS } = require("../src/shared/networks");
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupPopup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{ button: "btn-save-rpc", input: "settings-rpc", field: "rpcUrl" },
|
||||||
|
{
|
||||||
|
button: "btn-save-blockscout",
|
||||||
|
input: "settings-blockscout",
|
||||||
|
field: "blockscoutUrl",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"$button stores the URL on screen after another page stored a different one",
|
||||||
|
async ({ button, input, field }) => {
|
||||||
|
const page = await bootPopup(unversionedValidProfile());
|
||||||
|
await page.click("btn-settings");
|
||||||
|
const url = page.value(input);
|
||||||
|
// This write sends this page no storage change event.
|
||||||
|
page.storage.write("autistmask", {
|
||||||
|
...page.storage.read("autistmask"),
|
||||||
|
[field]: "https://other-page.example.invalid",
|
||||||
|
});
|
||||||
|
// This answer passes both endpoint checks on Ethereum Mainnet. The RPC
|
||||||
|
// check reads Ethereum Mainnet's chain id from it. The Blockscout check
|
||||||
|
// reads `ok`.
|
||||||
|
globalThis.fetch.mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ result: NETWORKS.mainnet.chainId }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.click(button);
|
||||||
|
|
||||||
|
expect(page.storage.read("autistmask")[field]).toBe(url);
|
||||||
|
expect(page.text("flash-msg")).toBe("Saved.");
|
||||||
|
},
|
||||||
|
);
|
||||||
Reference in new issue
Block a user