Five defects, one of which destroyed every wallet, came from src/background reading and writing the module-level state singleton the MV3 worker never populates, which silently served DEFAULT_STATE. Each point fix created the next defect. The background now has its own per-call getState() and a queued read-modify-write updateState(); the singleton is unreachable from it, and an unpopulated read throws instead of serving defaults. The prohibition is enforced by the build, not by review: build.js asserts over esbuild's own metafile that no forbidden module is an input of a background bundle, so every specifier syntax esbuild resolves is covered, and both halves of the table are checked for rot -- a stale key, a stale module, an empty list, or an unlisted entry point under src/background/ all fail the build. The ESLint rule remains as fast local feedback and reads the same shared table. Known bounds are documented where the table lives. Also closes #320: getProvider() now requires a validated network id, so a cold worker no longer prepares a non-mainnet dApp transaction for mainnet and gets refused by the wallet's own verifier. backgroundRefresh() no longer mutates address objects across a network round trip, the broadcast path takes its endpoint and chain id from one snapshot, and eight test storage stubs now structured-clone on get as the real chrome.storage.local does. closes #320
70 lines
3.0 KiB
JavaScript
70 lines
3.0 KiB
JavaScript
// The field mutations a chain switch performs, applied to a state record
|
|
// handed in rather than to the module-level `state` singleton.
|
|
//
|
|
// Split out of chainSwitch.js so the background can perform a chain switch
|
|
// without the singleton being reachable from its bundle at all. The popup
|
|
// still goes through onChainSwitch() (chainSwitch.js), which applies this to
|
|
// the singleton and saves; the background applies it to the detached record of
|
|
// its own read-modify-write (src/background/state.js).
|
|
//
|
|
// Everything here is synchronous and touches nothing but the object it is
|
|
// given: no storage, no caches, no imports beyond the network table. That is
|
|
// what makes it usable on a record that has been read fresh from storage
|
|
// microseconds earlier and is about to be written back.
|
|
|
|
const { networkById } = require("./networks");
|
|
|
|
// Switch `s` to `newNetworkId` and reset every piece of chain-specific state
|
|
// it carries. Returns the network configuration object for the new chain.
|
|
function applyChainSwitchFields(s, newNetworkId) {
|
|
const net = networkById(newNetworkId);
|
|
|
|
// --- core identity ---
|
|
// Endpoints are remembered per network rather than reset to the
|
|
// defaults, because a user who points the wallet at their own node has
|
|
// no way to get that URL back once it is gone: overwriting it moved
|
|
// every address and every transaction onto a third-party endpoint
|
|
// silently and permanently.
|
|
//
|
|
// s.rpcUrl / s.blockscoutUrl stay the live endpoints of the active
|
|
// network, so nothing that reads them changes. The invariant is that for
|
|
// the ACTIVE network those two fields are authoritative and the map entry
|
|
// may be stale (Settings writes the fields directly); for every other
|
|
// network the map is authoritative. Snapshotting the outgoing network
|
|
// here, before the switch, is what reconciles them.
|
|
if (!s.networkEndpoints) s.networkEndpoints = {};
|
|
s.networkEndpoints[s.networkId] = {
|
|
rpcUrl: s.rpcUrl,
|
|
blockscoutUrl: s.blockscoutUrl,
|
|
};
|
|
const remembered = s.networkEndpoints[net.id] || {};
|
|
s.networkId = net.id;
|
|
s.rpcUrl = remembered.rpcUrl || net.defaultRpcUrl;
|
|
s.blockscoutUrl = remembered.blockscoutUrl || net.defaultBlockscoutUrl;
|
|
|
|
// --- balance / refresh state ---
|
|
// Reset last-refresh timestamp so the next polling cycle
|
|
// triggers an immediate balance refresh on the new chain.
|
|
s.lastBalanceRefresh = 0;
|
|
|
|
// Clear per-address balances and token balances so stale data
|
|
// from the previous chain is never displayed while the first
|
|
// refresh on the new chain is in flight.
|
|
for (const wallet of s.wallets || []) {
|
|
for (const addr of wallet.addresses || []) {
|
|
addr.balance = "0";
|
|
addr.tokenBalances = [];
|
|
}
|
|
}
|
|
|
|
// --- chain-specific caches ---
|
|
// Token holder counts and fraud contract lists are
|
|
// chain-specific and must not carry over.
|
|
s.tokenHolderCache = {};
|
|
s.fraudContracts = [];
|
|
|
|
return net;
|
|
}
|
|
|
|
module.exports = { applyChainSwitchFields };
|