Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1727a4af07 |
@@ -1494,6 +1494,15 @@ such as an edit typed into Settings and not yet saved. Home is the one screen
|
|||||||
"Back" always re-renders, so the wallet list reflects anything that changed
|
"Back" always re-renders, so the wallet list reflects anything that changed
|
||||||
while the user was away from it.
|
while the user was away from it.
|
||||||
|
|
||||||
|
A network switch in Settings leaves every screen beneath Settings showing the
|
||||||
|
previous network, so after it "Back" behaves as in a reopened popup: every
|
||||||
|
screen on the stack counts as not yet rendered, and the stack is cut at the
|
||||||
|
first screen that cannot be rendered from saved state, such as Send or AddToken.
|
||||||
|
"Back" then renders the screen it lands on for the new network, and anything
|
||||||
|
typed there and not saved, which belonged to the previous network, is dropped.
|
||||||
|
Without a wallet the stack is not cut, because it starts at Welcome, which shows
|
||||||
|
nothing from a network.
|
||||||
|
|
||||||
Every screen that holds secret material in the page registers a cleanup with
|
Every screen that holds secret material in the page registers a cleanup with
|
||||||
`onViewLeave()` (`src/popup/views/helpers.js`), which `showView()` runs on every
|
`onViewLeave()` (`src/popup/views/helpers.js`), which `showView()` runs on every
|
||||||
exit from that screen rather than only on its "Back" button, so nothing secret
|
exit from that screen rather than only on its "Back" button, so nothing secret
|
||||||
@@ -1924,7 +1933,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
|
||||||
@@ -1941,16 +1951,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
|
||||||
@@ -1995,7 +2008,8 @@ view would leave a wallet one click from deletion.
|
|||||||
- `[x]` on a denied site → forgets the refusal, in place; it connects
|
- `[x]` on a denied site → forgets the refusal, in place; it connects
|
||||||
nothing and tells the background nothing
|
nothing and tells the background nothing
|
||||||
- Ten clicks on the version → reveals the Debug well (no screen change)
|
- Ten clicks on the version → reveals the Debug well (no screen change)
|
||||||
- "Back" (or Settings gear again) → previous screen (Home)
|
- "Back" (or Settings gear again) → previous screen (Home); after a network
|
||||||
|
switch, the stack is cut first (see the top of Screen Map)
|
||||||
|
|
||||||
#### ShowRecoveryPhrase (`show-phrase`)
|
#### ShowRecoveryPhrase (`show-phrase`)
|
||||||
|
|
||||||
@@ -2174,7 +2188,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
|
||||||
@@ -2183,8 +2198,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
|
||||||
@@ -2402,8 +2421,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"
|
||||||
|
|||||||
+31
-28
@@ -838,7 +838,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
|
||||||
>
|
>
|
||||||
@@ -1321,36 +1321,39 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<!-- top 10 quick-pick buttons -->
|
<div id="settings-addtoken-common">
|
||||||
<div class="mb-3">
|
<!-- top 10 quick-pick buttons -->
|
||||||
<label class="block mb-1 text-xs text-muted"
|
<div class="mb-3">
|
||||||
>Top tokens:</label
|
<label class="block mb-1 text-xs text-muted"
|
||||||
>
|
>Top tokens:</label
|
||||||
<div
|
>
|
||||||
id="settings-addtoken-top10"
|
<div
|
||||||
class="flex flex-wrap gap-1"
|
id="settings-addtoken-top10"
|
||||||
></div>
|
class="flex flex-wrap gap-1"
|
||||||
</div>
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- top 100 dropdown -->
|
<!-- top 100 dropdown -->
|
||||||
<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"
|
||||||
>Or pick from top 100:</label
|
>Or pick from top 100:</label
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
id="settings-addtoken-select"
|
id="settings-addtoken-select"
|
||||||
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
class="border border-border p-1 w-full font-mono text-sm bg-bg text-fg"
|
||||||
>
|
>
|
||||||
<option value="">-- select --</option>
|
<option value="">-- select --</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
id="btn-settings-addtoken-select"
|
id="btn-settings-addtoken-select"
|
||||||
class="border border-border px-2 py-1 mt-1 hover:bg-fg hover:text-bg cursor-pointer"
|
class="border border-border px-2 py-1 mt-1 hover:bg-fg hover:text-bg cursor-pointer"
|
||||||
>
|
>
|
||||||
Add selected
|
Add selected
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- manual contract address -->
|
<!-- manual contract address -->
|
||||||
|
|||||||
@@ -37,9 +37,11 @@ function markViewRendered(view) {
|
|||||||
if (view) renderedViews.add(view);
|
if (view) renderedViews.add(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin a fresh page-load scope. The popup gets one by being loaded; the
|
// Begin a fresh page-load scope. The popup gets one by being loaded, and
|
||||||
|
// asks for one when the user switches the network, because every screen
|
||||||
|
// then on the page shows the previous network (views/settings.js); the
|
||||||
// unit tests, which simulate several page loads against one module
|
// unit tests, which simulate several page loads against one module
|
||||||
// instance, ask for one.
|
// instance, ask for one too.
|
||||||
function resetRenderedViews() {
|
function resetRenderedViews() {
|
||||||
renderedViews.clear();
|
renderedViews.clear();
|
||||||
}
|
}
|
||||||
@@ -213,9 +215,10 @@ function renderView(view, state, views) {
|
|||||||
// in the two cases where the view is known to be on the page already:
|
// in the two cases where the view is known to be on the page already:
|
||||||
//
|
//
|
||||||
// - It is not one the popup renders from persisted state. The restored
|
// - It is not one the popup renders from persisted state. The restored
|
||||||
// stack is filtered against RESTORABLE_VIEWS, so such a view can only
|
// stack is filtered against RESTORABLE_VIEWS, and a network switch cuts
|
||||||
// be on the stack from this page load, where forward navigation
|
// it the same way (views/settings.js), so such a view can only be on the
|
||||||
// rendered it on the way in.
|
// stack from this page load, where forward navigation rendered it on the
|
||||||
|
// way in.
|
||||||
// - This page load has rendered it. Re-rendering would re-fetch and
|
// - This page load has rendered it. Re-rendering would re-fetch and
|
||||||
// clobber what it holds; Home is rendered anyway, see above.
|
// clobber what it holds; Home is rendered anyway, see above.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const {
|
|||||||
} = require("../dustThreshold");
|
} = require("../dustThreshold");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { onChainSwitch } = require("../../shared/chainSwitch");
|
const { onChainSwitch } = require("../../shared/chainSwitch");
|
||||||
|
const { restorableStack } = require("../../shared/persistedState");
|
||||||
|
const { resetRenderedViews, markViewRendered } = require("../viewRouter");
|
||||||
const {
|
const {
|
||||||
log,
|
log,
|
||||||
debugFetch,
|
debugFetch,
|
||||||
@@ -323,8 +325,21 @@ function init(ctx) {
|
|||||||
// Open pages are told by the background, as after a site's approved
|
// Open pages are told by the background, as after a site's approved
|
||||||
// switch request.
|
// switch request.
|
||||||
notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId });
|
notify({ type: "AUTISTMASK_NETWORK_CHANGED", chainId: net.chainId });
|
||||||
|
// The screens beneath Settings show the previous network, so Back
|
||||||
|
// finds them as it does in a reopened popup: each is rendered again
|
||||||
|
// when Back lands on it, and the stack is cut at the first one that
|
||||||
|
// cannot be rendered from saved state, such as Send or AddToken.
|
||||||
|
// Without a wallet the stack starts at Welcome, which that cut would
|
||||||
|
// drop, and Welcome shows nothing from a network.
|
||||||
|
if (state.hasWallet) {
|
||||||
|
state.viewStack = restorableStack(state.viewStack, "settings");
|
||||||
|
}
|
||||||
|
resetRenderedViews();
|
||||||
|
// Settings itself is brought up to date for the new network here.
|
||||||
|
markViewRendered("settings");
|
||||||
$("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,189 @@
|
|||||||
|
// 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), including on
|
||||||
|
// the screen "Back" lands on after a switch made from the Settings gear. 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",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fake timers hold the timer of the flash message a switch ends with, which
|
||||||
|
// would otherwise fire after cleanupPopup() has removed the page and throw.
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupPopup();
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The popup reopened onto SettingsAddToken, on `networkId`.
|
||||||
|
function bootOnAddToken(networkId, extra) {
|
||||||
|
return bootPopup(
|
||||||
|
unversionedValidProfile({
|
||||||
|
networkId,
|
||||||
|
currentView: "settings-addtoken",
|
||||||
|
...extra,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The popup reopened onto AddressDetail on Ethereum Mainnet, with DAI tracked
|
||||||
|
// there and TST tracked on Sepolia.
|
||||||
|
function bootOnAddress() {
|
||||||
|
return bootPopup(
|
||||||
|
unversionedValidProfile({
|
||||||
|
currentView: "address",
|
||||||
|
selectedWallet: 0,
|
||||||
|
selectedAddress: 0,
|
||||||
|
trackedTokens: [MAINNET_TOKEN],
|
||||||
|
networkTrackedTokens: { sepolia: [SEPOLIA_TOKEN] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Settings gear, a switch to Sepolia, and Back.
|
||||||
|
async function gearSwitchBack(boot) {
|
||||||
|
await boot.click("btn-settings");
|
||||||
|
expect(boot.visibleViews()).toEqual(["settings"]);
|
||||||
|
await chooseNetwork(boot, "sepolia");
|
||||||
|
await boot.click("btn-settings-back");
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Back after a switch from the Settings gear", () => {
|
||||||
|
test("SettingsAddToken no longer offers the common tokens", async () => {
|
||||||
|
const boot = await bootOnAddToken("mainnet");
|
||||||
|
expect(boot.hidden("settings-addtoken-common")).toBe(false);
|
||||||
|
|
||||||
|
await gearSwitchBack(boot);
|
||||||
|
expect(boot.visibleViews()).toEqual(["settings-addtoken"]);
|
||||||
|
expect(boot.hidden("settings-addtoken-common")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("AddressDetail lists the new network's tracked tokens", async () => {
|
||||||
|
const boot = await bootOnAddress();
|
||||||
|
expect(boot.node("address-balances").innerHTML).toContain("DAI");
|
||||||
|
|
||||||
|
await gearSwitchBack(boot);
|
||||||
|
expect(boot.visibleViews()).toEqual(["address"]);
|
||||||
|
const balances = boot.node("address-balances").innerHTML;
|
||||||
|
expect(balances).toContain("TST");
|
||||||
|
expect(balances).not.toContain("DAI");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Neither can be rendered from saved state, so neither is landed on.
|
||||||
|
test.each([
|
||||||
|
["btn-add-token", "add-token"],
|
||||||
|
["btn-send", "send"],
|
||||||
|
])("from over %s, it lands on AddressDetail", async (button, view) => {
|
||||||
|
const boot = await bootOnAddress();
|
||||||
|
await boot.click(button);
|
||||||
|
expect(boot.visibleViews()).toEqual([view]);
|
||||||
|
|
||||||
|
await gearSwitchBack(boot);
|
||||||
|
expect(boot.visibleViews()).toEqual(["address"]);
|
||||||
|
expect(boot.node("address-balances").innerHTML).not.toContain("DAI");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("without a wallet, it lands on Welcome", async () => {
|
||||||
|
const boot = await bootPopup(undefined);
|
||||||
|
expect(boot.visibleViews()).toEqual(["welcome"]);
|
||||||
|
|
||||||
|
await gearSwitchBack(boot);
|
||||||
|
expect(boot.visibleViews()).toEqual(["welcome"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a later Back onto Settings keeps what was typed there", async () => {
|
||||||
|
const boot = await bootOnAddress();
|
||||||
|
await boot.click("btn-settings");
|
||||||
|
await chooseNetwork(boot, "sepolia");
|
||||||
|
boot.node("settings-rpc").value = "https://typed.example";
|
||||||
|
|
||||||
|
await boot.click("btn-settings-add-token");
|
||||||
|
expect(boot.visibleViews()).toEqual(["settings-addtoken"]);
|
||||||
|
await boot.click("btn-settings-addtoken-back");
|
||||||
|
expect(boot.visibleViews()).toEqual(["settings"]);
|
||||||
|
expect(boot.value("settings-rpc")).toBe("https://typed.example");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user