fix: tracked tokens are kept per network, and bundled picks are offered on mainnet only (closes #523)
A network switch now puts the outgoing network's tracked tokens away in a new persisted field, networkTrackedTokens, and brings back the incoming network's list, or an empty one, the way endpoints are already kept per network. trackedTokens stays the active network's list, so its readers are unchanged, and the background's switch keeps the lists too because it goes through the same function. The two add-token screens show their common-token picks, which come from the bundled list of mainnet contracts, on Ethereum Mainnet only; adding by contract address works on every network. Settings redraws its tracked-token list after a switch. Model: opus-5-5
This commit is contained in:
@@ -1898,7 +1898,8 @@ view would leave a wallet one click from deletion.
|
|||||||
- Contract address input
|
- Contract address input
|
||||||
- Status line ("Looking up token...", cleared or replaced on failure)
|
- Status line ("Looking up token...", cleared or replaced on failure)
|
||||||
- Common token quick-pick buttons (top 25 by market cap), which fill the
|
- Common token quick-pick buttons (top 25 by market cap), which fill the
|
||||||
contract address input
|
contract address input. Shown on Ethereum Mainnet only, because the
|
||||||
|
bundled token list holds mainnet contracts
|
||||||
- "Add" button
|
- "Add" button
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- "Add" (valid contract) → tracks the token, pops the stack, and re-renders
|
- "Add" (valid contract) → tracks the token, pops the stack, and re-renders
|
||||||
@@ -1915,16 +1916,19 @@ view would leave a wallet one click from deletion.
|
|||||||
- Wallets: one row per wallet with its name (tap to rename inline), a
|
- Wallets: one row per wallet with its name (tap to rename inline), a
|
||||||
`[recovery phrase]` button on HD wallets only, and an `[x]` delete button,
|
`[recovery phrase]` button on HD wallets only, and an `[x]` delete button,
|
||||||
plus a "+ Add wallet" button
|
plus a "+ Add wallet" button
|
||||||
- Tracked Tokens: one row per tracked token with an `[x]` remove button,
|
- Tracked Tokens: one row per token tracked on the active network with an
|
||||||
plus a "+ Add token" button
|
`[x]` remove button, plus a "+ Add token" button
|
||||||
- Display: "Show tracked tokens with zero balance" checkbox, "UTC
|
- Display: "Show tracked tokens with zero balance" checkbox, "UTC
|
||||||
Timestamps" checkbox, and a Theme selector (System / Light / Dark)
|
Timestamps" checkbox, and a Theme selector (System / Light / Dark)
|
||||||
- Network: network selector (Ethereum Mainnet / Sepolia Testnet). The
|
- Network: network selector (Ethereum Mainnet / Sepolia Testnet). The
|
||||||
network changes only here, or when the user approves a site's request on
|
network changes only here, or when the user approves a site's request on
|
||||||
**NetworkApproval**; either way, switching restores the RPC and Blockscout
|
**NetworkApproval**; either way, switching restores the RPC and Blockscout
|
||||||
endpoints last used on that network, or that network's defaults if it has
|
endpoints last used on that network, or that network's defaults if it has
|
||||||
none, and sends `chainChanged` with the new chain id to every open tab.
|
none, and the tokens tracked on that network, and sends `chainChanged`
|
||||||
Choosing the network already active changes nothing and sends nothing
|
with the new chain id to every open tab. A token is tracked only on the
|
||||||
|
network it was added on, because a contract address names a token on one
|
||||||
|
network only. Choosing the network already active changes nothing and
|
||||||
|
sends nothing
|
||||||
- Ethereum RPC: endpoint URL input + "Save" button (validated against
|
- Ethereum RPC: endpoint URL input + "Save" button (validated against
|
||||||
`eth_chainId` before being saved)
|
`eth_chainId` before being saved)
|
||||||
- Blockscout API: endpoint URL input + "Save" button (validated against
|
- Blockscout API: endpoint URL input + "Save" button (validated against
|
||||||
@@ -2148,7 +2152,8 @@ view would leave a wallet one click from deletion.
|
|||||||
#### SettingsAddToken (`settings-addtoken`)
|
#### SettingsAddToken (`settings-addtoken`)
|
||||||
|
|
||||||
- **When**: User tapped "+ Add token" in Settings. Tokens added here are tracked
|
- **When**: User tapped "+ Add token" in Settings. Tokens added here are tracked
|
||||||
across every address, unlike AddToken which is reached from one address.
|
across every address on the active network, unlike AddToken which is reached
|
||||||
|
from one address.
|
||||||
- **Elements**:
|
- **Elements**:
|
||||||
- "Back" button, "Add Token" heading
|
- "Back" button, "Add Token" heading
|
||||||
- Instruction text
|
- Instruction text
|
||||||
@@ -2157,8 +2162,12 @@ view would leave a wallet one click from deletion.
|
|||||||
- "Or pick from top 100:" dropdown (already-tracked tokens are disabled) +
|
- "Or pick from top 100:" dropdown (already-tracked tokens are disabled) +
|
||||||
"Add selected" button
|
"Add selected" button
|
||||||
- "Or enter contract address:" input, a status line, and an "Add" button
|
- "Or enter contract address:" input, a status line, and an "Add" button
|
||||||
|
- The two lists from the bundled token list, "Top tokens:" and "Or pick from
|
||||||
|
top 100:", are shown on Ethereum Mainnet only, because that list holds
|
||||||
|
mainnet contracts. On any other network the contract address is the only
|
||||||
|
add path
|
||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- Any of the three add paths, on success → adds the token and shows an
|
- Any of the add paths shown, on success → adds the token and shows an
|
||||||
"Added SYMBOL" flash message (no screen change)
|
"Added SYMBOL" flash message (no screen change)
|
||||||
- A duplicate, a scam-listed address, or a failed contract lookup → flash
|
- A duplicate, a scam-listed address, or a failed contract lookup → flash
|
||||||
message, no screen change
|
message, no screen change
|
||||||
@@ -2376,8 +2385,9 @@ view would leave a wallet one click from deletion.
|
|||||||
- **Transitions**:
|
- **Transitions**:
|
||||||
- "Switch" → closes popup; the background switches the network as
|
- "Switch" → closes popup; the background switches the network as
|
||||||
**Settings** does, restoring the RPC and Blockscout endpoints last used on
|
**Settings** does, restoring the RPC and Blockscout endpoints last used on
|
||||||
that network (or that network's defaults if it has none), sends
|
that network (or that network's defaults if it has none) and the tokens
|
||||||
`chainChanged` to every open tab, and answers the site with success
|
tracked on that network, sends `chainChanged` to every open tab, and
|
||||||
|
answers the site with success
|
||||||
- "Reject" → closes popup; the site is answered with EIP-1193 code 4001 and
|
- "Reject" → closes popup; the site is answered with EIP-1193 code 4001 and
|
||||||
nothing changes
|
nothing changes
|
||||||
- Popup window closed without answering → the same as "Reject"
|
- Popup window closed without answering → the same as "Reject"
|
||||||
|
|||||||
@@ -843,7 +843,7 @@
|
|||||||
id="add-token-info"
|
id="add-token-info"
|
||||||
class="text-xs text-muted mb-2 min-h-[1.25rem] invisible"
|
class="text-xs text-muted mb-2 min-h-[1.25rem] invisible"
|
||||||
></div>
|
></div>
|
||||||
<div class="mb-2">
|
<div id="add-token-common" class="mb-2">
|
||||||
<label class="block mb-1 text-xs text-muted"
|
<label class="block mb-1 text-xs text-muted"
|
||||||
>Or pick a common token:</label
|
>Or pick a common token:</label
|
||||||
>
|
>
|
||||||
@@ -1326,8 +1326,10 @@
|
|||||||
<h2 class="font-bold mb-2">Add Token</h2>
|
<h2 class="font-bold mb-2">Add Token</h2>
|
||||||
<p class="text-xs text-muted mb-3">
|
<p class="text-xs text-muted mb-3">
|
||||||
Pick a common token or enter a contract address manually.
|
Pick a common token or enter a contract address manually.
|
||||||
|
Common tokens are offered on Ethereum Mainnet only.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div id="settings-addtoken-common">
|
||||||
<!-- top 10 quick-pick buttons -->
|
<!-- top 10 quick-pick buttons -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="block mb-1 text-xs text-muted"
|
<label class="block mb-1 text-xs text-muted"
|
||||||
@@ -1357,6 +1359,7 @@
|
|||||||
Add selected
|
Add selected
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- manual contract address -->
|
<!-- manual contract address -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ function show() {
|
|||||||
$("add-token-address").value = "";
|
$("add-token-address").value = "";
|
||||||
$("add-token-info").textContent = "";
|
$("add-token-info").textContent = "";
|
||||||
$("add-token-info").style.visibility = "hidden";
|
$("add-token-info").style.visibility = "hidden";
|
||||||
|
// The bundled list holds Ethereum Mainnet contracts, which name no token
|
||||||
|
// on any other network.
|
||||||
|
$("add-token-common").classList.toggle(
|
||||||
|
"hidden",
|
||||||
|
state.networkId !== "mainnet",
|
||||||
|
);
|
||||||
const list = $("common-token-list");
|
const list = $("common-token-list");
|
||||||
list.innerHTML = getTopTokens(25)
|
list.innerHTML = getTopTokens(25)
|
||||||
.map(
|
.map(
|
||||||
|
|||||||
@@ -325,6 +325,7 @@ function init(ctx) {
|
|||||||
notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId });
|
notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId });
|
||||||
$("settings-rpc").value = state.rpcUrl;
|
$("settings-rpc").value = state.rpcUrl;
|
||||||
$("settings-blockscout").value = state.blockscoutUrl;
|
$("settings-blockscout").value = state.blockscoutUrl;
|
||||||
|
renderTrackedTokens();
|
||||||
showFlash("Switched to " + net.name + ".");
|
showFlash("Switched to " + net.name + ".");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ function show() {
|
|||||||
$("settings-addtoken-address").value = "";
|
$("settings-addtoken-address").value = "";
|
||||||
$("settings-addtoken-info").textContent = "";
|
$("settings-addtoken-info").textContent = "";
|
||||||
$("settings-addtoken-info").style.visibility = "hidden";
|
$("settings-addtoken-info").style.visibility = "hidden";
|
||||||
|
// The bundled list holds Ethereum Mainnet contracts, which name no token
|
||||||
|
// on any other network.
|
||||||
|
$("settings-addtoken-common").classList.toggle(
|
||||||
|
"hidden",
|
||||||
|
state.networkId !== "mainnet",
|
||||||
|
);
|
||||||
renderTop10();
|
renderTop10();
|
||||||
renderDropdown();
|
renderDropdown();
|
||||||
showView("settings-addtoken");
|
showView("settings-addtoken");
|
||||||
|
|||||||
@@ -38,6 +38,16 @@ function applyChainSwitchFields(s, newNetworkId) {
|
|||||||
blockscoutUrl: s.blockscoutUrl,
|
blockscoutUrl: s.blockscoutUrl,
|
||||||
};
|
};
|
||||||
const remembered = s.networkEndpoints[net.id] || {};
|
const remembered = s.networkEndpoints[net.id] || {};
|
||||||
|
|
||||||
|
// Tracked tokens are kept per network the same way, because a contract
|
||||||
|
// address names a token on one network only. s.trackedTokens stays the
|
||||||
|
// active network's list, so nothing that reads it changes: the outgoing
|
||||||
|
// list is put away under its network, and the incoming network's list, or
|
||||||
|
// an empty one, takes its place.
|
||||||
|
if (!s.networkTrackedTokens) s.networkTrackedTokens = {};
|
||||||
|
s.networkTrackedTokens[s.networkId] = s.trackedTokens;
|
||||||
|
s.trackedTokens = s.networkTrackedTokens[net.id] || [];
|
||||||
|
|
||||||
s.networkId = net.id;
|
s.networkId = net.id;
|
||||||
s.rpcUrl = remembered.rpcUrl || net.defaultRpcUrl;
|
s.rpcUrl = remembered.rpcUrl || net.defaultRpcUrl;
|
||||||
s.blockscoutUrl = remembered.blockscoutUrl || net.defaultBlockscoutUrl;
|
s.blockscoutUrl = remembered.blockscoutUrl || net.defaultBlockscoutUrl;
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ const DEFAULT_STATE = {
|
|||||||
hasWallet: false,
|
hasWallet: false,
|
||||||
wallets: [],
|
wallets: [],
|
||||||
trackedTokens: [],
|
trackedTokens: [],
|
||||||
|
// Tracked tokens remembered per network: { [networkId]: [token, ...] }.
|
||||||
|
// trackedTokens above is the active network's list; this is what the
|
||||||
|
// others are restored from when the active network changes. See
|
||||||
|
// applyChainSwitchFields().
|
||||||
|
networkTrackedTokens: {},
|
||||||
networkId: "mainnet",
|
networkId: "mainnet",
|
||||||
rpcUrl: DEFAULT_RPC_URL,
|
rpcUrl: DEFAULT_RPC_URL,
|
||||||
blockscoutUrl: DEFAULT_BLOCKSCOUT_URL,
|
blockscoutUrl: DEFAULT_BLOCKSCOUT_URL,
|
||||||
@@ -289,6 +294,20 @@ function normalizePersisted(saved) {
|
|||||||
// profile whose wallets are perfectly fine. An empty list is a legitimate
|
// profile whose wallets are perfectly fine. An empty list is a legitimate
|
||||||
// value and survives.
|
// value and survives.
|
||||||
out.trackedTokens = structuredClone(tokenRefs(saved.trackedTokens));
|
out.trackedTokens = structuredClone(tokenRefs(saved.trackedTokens));
|
||||||
|
// Each remembered list is floored like trackedTokens, because
|
||||||
|
// applyChainSwitchFields() puts it straight back onto trackedTokens when
|
||||||
|
// its network becomes the active one. A key that is not a network in
|
||||||
|
// networks.js is dropped, which also keeps an own "__proto__" key from
|
||||||
|
// reaching the plain assignment below.
|
||||||
|
out.networkTrackedTokens = {};
|
||||||
|
if (isRecord(saved.networkTrackedTokens)) {
|
||||||
|
for (const netId of Object.keys(saved.networkTrackedTokens)) {
|
||||||
|
if (!isKnownNetworkId(netId)) continue;
|
||||||
|
out.networkTrackedTokens[netId] = structuredClone(
|
||||||
|
tokenRefs(saved.networkTrackedTokens[netId]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// The loud refusal for an unknown id is assertStateUsable(); this is the
|
// The loud refusal for an unknown id is assertStateUsable(); this is the
|
||||||
// floor under it. networkId is an object KEY into networkEndpoints below,
|
// floor under it. networkId is an object KEY into networkEndpoints below,
|
||||||
// so a value that is not a network in networks.js must never get that far
|
// so a value that is not a network in networks.js must never get that far
|
||||||
|
|||||||
+12
-8
@@ -42,9 +42,11 @@ const { log } = require("./log");
|
|||||||
// and writes THIS object, never the proxy: the guard is for callers.
|
// and writes THIS object, never the proxy: the guard is for callers.
|
||||||
const rawState = {
|
const rawState = {
|
||||||
...DEFAULT_STATE,
|
...DEFAULT_STATE,
|
||||||
// Its own object, not the one DEFAULT_STATE holds: applyChainSwitchFields()
|
// Their own objects, not the ones DEFAULT_STATE holds:
|
||||||
// mutates this map in place, and a spread copies the reference.
|
// applyChainSwitchFields() mutates these maps in place, and a spread copies
|
||||||
|
// the reference.
|
||||||
networkEndpoints: {},
|
networkEndpoints: {},
|
||||||
|
networkTrackedTokens: {},
|
||||||
currentView: null,
|
currentView: null,
|
||||||
selectedWallet: null,
|
selectedWallet: null,
|
||||||
selectedAddress: null,
|
selectedAddress: null,
|
||||||
@@ -438,12 +440,14 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
|||||||
//
|
//
|
||||||
// Every other persisted field stays a whole-field diff:
|
// Every other persisted field stays a whole-field diff:
|
||||||
// `trackedTokens`/`fraudContracts`/`viewStack` are arrays of scalars with no
|
// `trackedTokens`/`fraudContracts`/`viewStack` are arrays of scalars with no
|
||||||
// per-element identity to merge by; `tokenHolderCache` is a map shaped like
|
// per-element identity to merge by; `networkTrackedTokens` is diffed whole like
|
||||||
// the ones above, but nothing in src/ ever writes an entry into it — it is
|
// `trackedTokens` and `networkId`, the two fields a network switch moves
|
||||||
// only ever reset wholesale to `{}` (applyChainSwitchFields()) — so there is
|
// together with it; `tokenHolderCache` is a map shaped like the ones above,
|
||||||
// no in-place mutation for a whole-field diff to collide with; `viewData` is
|
// but nothing in src/ ever writes an entry into it — it is only ever reset
|
||||||
// this page's own UI scratch space, not data another page has any reason to
|
// wholesale to `{}` (applyChainSwitchFields()) — so there is no in-place
|
||||||
// share membership of.
|
// mutation for a whole-field diff to collide with; `viewData` is this page's
|
||||||
|
// own UI scratch space, not data another page has any reason to share
|
||||||
|
// membership of.
|
||||||
//
|
//
|
||||||
// This does not make two pages that both change the SAME leaf concurrently
|
// This does not make two pages that both change the SAME leaf concurrently
|
||||||
// safe: last write wins on that one leaf, same as before. What it removes
|
// safe: last write wins on that one leaf, same as before. What it removes
|
||||||
|
|||||||
@@ -204,17 +204,21 @@ describe("a chain switch on a worker that never loaded state", () => {
|
|||||||
expect(after.rpcUrl).toBe(SEPOLIA.defaultRpcUrl);
|
expect(after.rpcUrl).toBe(SEPOLIA.defaultRpcUrl);
|
||||||
|
|
||||||
// And it took nothing else with it. Without the load these come back
|
// And it took nothing else with it. Without the load these come back
|
||||||
// as [], {}, [] and "system" from DEFAULT_STATE — every wallet in the
|
// as [], {}, {} and "system" from DEFAULT_STATE — every wallet in the
|
||||||
// extension gone, encrypted secrets included.
|
// extension gone, encrypted secrets included.
|
||||||
expect(after.wallets).toEqual(walletFixture());
|
expect(after.wallets).toEqual(walletFixture());
|
||||||
expect(after.hasWallet).toBe(true);
|
expect(after.hasWallet).toBe(true);
|
||||||
expect(after.activeAddress).toBe(ADDRESS);
|
expect(after.activeAddress).toBe(ADDRESS);
|
||||||
expect(after.allowedSites).toEqual({ [ADDRESS]: [CONNECTED_ORIGIN] });
|
expect(after.allowedSites).toEqual({ [ADDRESS]: [CONNECTED_ORIGIN] });
|
||||||
expect(after.trackedTokens).toEqual([
|
expect(after.networkTrackedTokens).toEqual({
|
||||||
{ address: TOKEN, symbol: "DAI", decimals: 18 },
|
mainnet: [{ address: TOKEN, symbol: "DAI", decimals: 18 }],
|
||||||
]);
|
});
|
||||||
expect(after.theme).toBe("dark");
|
expect(after.theme).toBe("dark");
|
||||||
|
|
||||||
|
// The mainnet token is tracked on mainnet only: Sepolia starts with
|
||||||
|
// none of its own.
|
||||||
|
expect(after.trackedTokens).toEqual([]);
|
||||||
|
|
||||||
// The user's mainnet endpoint is remembered rather than replaced by
|
// The user's mainnet endpoint is remembered rather than replaced by
|
||||||
// the public default, so switching back returns it.
|
// the public default, so switching back returns it.
|
||||||
expect(after.networkEndpoints.mainnet).toEqual({
|
expect(after.networkEndpoints.mainnet).toEqual({
|
||||||
@@ -226,6 +230,9 @@ describe("a chain switch on a worker that never loaded state", () => {
|
|||||||
expect(bg.persisted().rpcUrl).toBe(CUSTOM_RPC);
|
expect(bg.persisted().rpcUrl).toBe(CUSTOM_RPC);
|
||||||
expect(bg.persisted().blockscoutUrl).toBe(CUSTOM_BLOCKSCOUT);
|
expect(bg.persisted().blockscoutUrl).toBe(CUSTOM_BLOCKSCOUT);
|
||||||
expect(bg.persisted().wallets).toEqual(walletFixture());
|
expect(bg.persisted().wallets).toEqual(walletFixture());
|
||||||
|
expect(bg.persisted().trackedTokens).toEqual([
|
||||||
|
{ address: TOKEN, symbol: "DAI", decimals: 18 },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("compares the requested chain against the stored one, not the default", async () => {
|
test("compares the requested chain against the stored one, not the default", async () => {
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ const {
|
|||||||
normalizePersisted,
|
normalizePersisted,
|
||||||
} = require("../src/shared/persistedState");
|
} = require("../src/shared/persistedState");
|
||||||
const { stateProblem } = require("../src/shared/stateSchema");
|
const { stateProblem } = require("../src/shared/stateSchema");
|
||||||
|
const { isKnownNetworkId } = require("../src/shared/networks");
|
||||||
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
|
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
|
||||||
const {
|
const {
|
||||||
bootPopup,
|
bootPopup,
|
||||||
@@ -193,6 +194,29 @@ const CONTRACT = [
|
|||||||
floorOnly: [[1, 2], [null], [{}], [[TOKEN_ADDRESS]]],
|
floorOnly: [[1, 2], [null], [{}], [[TOKEN_ADDRESS]]],
|
||||||
holds: (v) => everyEntry(v, (t) => isRecord(t) && isText(t.address)),
|
holds: (v) => everyEntry(v, (t) => isRecord(t) && isText(t.address)),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: "networkTrackedTokens",
|
||||||
|
kind: KIND.ENTRIES,
|
||||||
|
hostile: [42, "notarecord", { sepolia: "notalist" }],
|
||||||
|
floorOnly: [
|
||||||
|
[1, 2],
|
||||||
|
{ sepolia: [1, 2] },
|
||||||
|
{ mainnet: [null, {}, [TOKEN_ADDRESS]] },
|
||||||
|
{ notanetwork: [{ address: TOKEN_ADDRESS }] },
|
||||||
|
JSON.parse('{"__proto__":[{"address":"0xabc"}]}'),
|
||||||
|
],
|
||||||
|
// Each list is put straight back onto trackedTokens by
|
||||||
|
// applyChainSwitchFields() when its network becomes the active one, so
|
||||||
|
// it is floored the same way; a key that names no network is dropped.
|
||||||
|
holds: (v) =>
|
||||||
|
isRecord(v) &&
|
||||||
|
Object.getPrototypeOf(v) === Object.prototype &&
|
||||||
|
Object.keys(v).every(
|
||||||
|
(id) =>
|
||||||
|
isKnownNetworkId(id) &&
|
||||||
|
everyEntry(v[id], (t) => isRecord(t) && isText(t.address)),
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: "allowedSites",
|
field: "allowedSites",
|
||||||
kind: KIND.ENTRIES,
|
kind: KIND.ENTRIES,
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// Tracked tokens are kept per network, and the two add-token screens offer the
|
||||||
|
// bundled token list, which holds Ethereum Mainnet contracts, on Ethereum
|
||||||
|
// Mainnet only (https://git.eeqj.de/sneak/AutistMask/issues/523). Driven
|
||||||
|
// through the real popup over a stored profile; the same switch made by the
|
||||||
|
// background is in tests/coldWorkerChainSwitch.test.js.
|
||||||
|
|
||||||
|
const {
|
||||||
|
bootPopup,
|
||||||
|
cleanupPopup,
|
||||||
|
unversionedValidProfile,
|
||||||
|
TOKEN_ADDRESS,
|
||||||
|
} = require("./support/popupBoot");
|
||||||
|
|
||||||
|
const SEPOLIA_TOKEN = {
|
||||||
|
address: TOKEN_ADDRESS,
|
||||||
|
symbol: "TST",
|
||||||
|
decimals: 18,
|
||||||
|
name: "Test Token",
|
||||||
|
};
|
||||||
|
const MAINNET_TOKEN = {
|
||||||
|
address: "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||||
|
symbol: "DAI",
|
||||||
|
decimals: 18,
|
||||||
|
name: "Dai Stablecoin",
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupPopup();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The popup reopened onto SettingsAddToken, on `networkId`.
|
||||||
|
function bootOnAddToken(networkId, extra) {
|
||||||
|
return bootPopup(
|
||||||
|
unversionedValidProfile({
|
||||||
|
networkId,
|
||||||
|
currentView: "settings-addtoken",
|
||||||
|
...extra,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Picks `networkId` in the Settings network selector, as the user does.
|
||||||
|
async function chooseNetwork(boot, networkId) {
|
||||||
|
const select = boot.node("settings-network");
|
||||||
|
select.value = networkId;
|
||||||
|
for (const fn of select.listeners.change) await fn();
|
||||||
|
await boot.settle();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
["mainnet", false],
|
||||||
|
["sepolia", true],
|
||||||
|
])("on %s", (networkId, hidden) => {
|
||||||
|
test(`the common tokens are hidden: ${hidden}`, async () => {
|
||||||
|
const boot = await bootOnAddToken(networkId);
|
||||||
|
expect(boot.pageErrors).toEqual([]);
|
||||||
|
expect(boot.visibleViews()).toEqual(["settings-addtoken"]);
|
||||||
|
expect(boot.hidden("settings-addtoken-common")).toBe(hidden);
|
||||||
|
|
||||||
|
require("../src/popup/views/addToken").show();
|
||||||
|
expect(boot.visibleViews()).toEqual(["add-token"]);
|
||||||
|
expect(boot.hidden("add-token-common")).toBe(hidden);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a token added by contract address on Sepolia is tracked on Sepolia only", async () => {
|
||||||
|
const boot = await bootOnAddToken("sepolia", {
|
||||||
|
networkTrackedTokens: { mainnet: [MAINNET_TOKEN] },
|
||||||
|
});
|
||||||
|
const stored = () => boot.storage.read("autistmask");
|
||||||
|
const { lookupTokenInfo } = require("../src/shared/balances");
|
||||||
|
lookupTokenInfo.mockResolvedValueOnce({
|
||||||
|
symbol: SEPOLIA_TOKEN.symbol,
|
||||||
|
decimals: SEPOLIA_TOKEN.decimals,
|
||||||
|
name: SEPOLIA_TOKEN.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
boot.node("settings-addtoken-address").value = TOKEN_ADDRESS;
|
||||||
|
await boot.click("btn-settings-addtoken-manual");
|
||||||
|
|
||||||
|
expect(lookupTokenInfo).toHaveBeenCalledWith(
|
||||||
|
TOKEN_ADDRESS,
|
||||||
|
expect.any(String),
|
||||||
|
"sepolia",
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
expect(stored().trackedTokens).toEqual([SEPOLIA_TOKEN]);
|
||||||
|
|
||||||
|
await chooseNetwork(boot, "mainnet");
|
||||||
|
expect(stored().networkId).toBe("mainnet");
|
||||||
|
expect(stored().trackedTokens).toEqual([MAINNET_TOKEN]);
|
||||||
|
const onMainnet = boot.node("settings-tracked-tokens").innerHTML;
|
||||||
|
expect(onMainnet).toContain("DAI");
|
||||||
|
expect(onMainnet).not.toContain("TST");
|
||||||
|
|
||||||
|
await chooseNetwork(boot, "sepolia");
|
||||||
|
expect(stored().networkId).toBe("sepolia");
|
||||||
|
expect(stored().trackedTokens).toEqual([SEPOLIA_TOKEN]);
|
||||||
|
const onSepolia = boot.node("settings-tracked-tokens").innerHTML;
|
||||||
|
expect(onSepolia).toContain("TST");
|
||||||
|
expect(onSepolia).not.toContain("DAI");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user