A stored allowedSites whose value was not a list rendered a working popup and then made every subsequent save fail silently, so the user operated a wallet that persisted nothing -- worse than a blank popup, which is at least visibly broken. fraudContracts and selectedToken had the same shape: a container floored by truthiness or not at all, while its entries were dereferenced. Entries are now floored as well as containers, following the idiom #311 established, and a failed save raises a persistent banner instead of vanishing into a swallowed rejection. The per-field justifications that used to live in a hand-written header are replaced by a contract test that drives each field's hostile and falsy values through a real popup boot, so a claim about a field answers to the code rather than to prose. Its guarantee is stated narrowly and deliberately: no structural dereference on the code paths a wholly-corrupted profile takes, which is not every path a stored record takes. The paths it does not drive are named where the claim is made, and are tracked in #379.
252 lines
7.7 KiB
JavaScript
252 lines
7.7 KiB
JavaScript
// AutistMask popup entry point.
|
|
// Loads state, initializes views, triggers first render.
|
|
|
|
const {
|
|
state,
|
|
saveState,
|
|
onSaveFailure,
|
|
loadState,
|
|
} = require("../shared/state");
|
|
const { StateUnusableError } = require("../shared/stateSchema");
|
|
const { log, setRuntimeDebug } = require("../shared/log");
|
|
const { refreshPrices } = require("../shared/prices");
|
|
const { refreshBalances } = require("../shared/balances");
|
|
const {
|
|
$,
|
|
showView,
|
|
updateDebugBanner,
|
|
setBackRenderer,
|
|
showSaveFailureBanner,
|
|
pushCurrentView,
|
|
goBack,
|
|
} = require("./views/helpers");
|
|
const { applyTheme } = require("./theme");
|
|
// Renders a view the popup lands on without having navigated to it forward:
|
|
// on restore here, and on Back. Only the views that can be fully re-rendered
|
|
// from persisted state (RESTORABLE_VIEWS, src/shared/restorableViews.js) go
|
|
// through it; anything else falls back to the nearest restorable parent.
|
|
const { renderView, makeBackRenderer } = require("./viewRouter");
|
|
|
|
const home = require("./views/home");
|
|
const welcome = require("./views/welcome");
|
|
const addWallet = require("./views/addWallet");
|
|
const addressDetail = require("./views/addressDetail");
|
|
const addressToken = require("./views/addressToken");
|
|
const send = require("./views/send");
|
|
const confirmTx = require("./views/confirmTx");
|
|
const txStatus = require("./views/txStatus");
|
|
const transactionDetail = require("./views/transactionDetail");
|
|
const receive = require("./views/receive");
|
|
const addToken = require("./views/addToken");
|
|
const settings = require("./views/settings");
|
|
const settingsAddToken = require("./views/settingsAddToken");
|
|
const deleteAddress = require("./views/deleteAddress");
|
|
const approval = require("./views/approval");
|
|
const stateRecovery = require("./views/stateRecovery");
|
|
|
|
function renderWalletList() {
|
|
home.render(ctx);
|
|
}
|
|
|
|
let refreshInFlight = false;
|
|
|
|
async function doRefreshAndRender() {
|
|
if (refreshInFlight) return;
|
|
refreshInFlight = true;
|
|
try {
|
|
await Promise.all([
|
|
refreshPrices(),
|
|
refreshBalances(
|
|
state.wallets,
|
|
state.rpcUrl,
|
|
state.blockscoutUrl,
|
|
state.trackedTokens,
|
|
state.networkId,
|
|
),
|
|
]);
|
|
state.lastBalanceRefresh = Date.now();
|
|
await saveState();
|
|
renderWalletList();
|
|
} catch (e) {
|
|
// Every call site fires this and walks away — the boot below, the ten
|
|
// second interval, and eight views through ctx — so it must never
|
|
// reject: an unhandled rejection is not a report of anything. The save
|
|
// inside it reports its own failure through onSaveFailure() (see
|
|
// src/shared/state.js); what is left here is a failed network round
|
|
// trip, which the next tick retries.
|
|
log.errorf("popup: background refresh failed:", e);
|
|
} finally {
|
|
refreshInFlight = false;
|
|
}
|
|
}
|
|
|
|
const ctx = {
|
|
renderWalletList,
|
|
doRefreshAndRender,
|
|
showAddWalletView: () => {
|
|
pushCurrentView();
|
|
addWallet.show();
|
|
},
|
|
showAddressDetail: () => {
|
|
pushCurrentView();
|
|
addressDetail.show();
|
|
},
|
|
showAddressToken: () => {
|
|
pushCurrentView();
|
|
addressToken.show();
|
|
},
|
|
showAddTokenView: () => {
|
|
pushCurrentView();
|
|
addToken.show();
|
|
},
|
|
showConfirmTx: (txInfo) => {
|
|
pushCurrentView();
|
|
confirmTx.show(txInfo);
|
|
},
|
|
showReceive: () => {
|
|
pushCurrentView();
|
|
receive.show();
|
|
},
|
|
showTransactionDetail: (tx) => {
|
|
pushCurrentView();
|
|
transactionDetail.show(tx);
|
|
},
|
|
showSettingsView: () => {
|
|
pushCurrentView();
|
|
settings.show();
|
|
},
|
|
showSettingsAddTokenView: () => {
|
|
pushCurrentView();
|
|
settingsAddToken.show();
|
|
},
|
|
showDeleteAddress: (walletIdx, addrIdx) => {
|
|
pushCurrentView();
|
|
deleteAddress.show(walletIdx, addrIdx);
|
|
},
|
|
};
|
|
|
|
// The view modules the router renders through, keyed as it expects them.
|
|
const viewModules = {
|
|
main: { show: () => fallbackView() },
|
|
addressDetail,
|
|
addressToken,
|
|
receive,
|
|
settings,
|
|
settingsAddToken,
|
|
confirmTx,
|
|
transactionDetail,
|
|
txStatus,
|
|
};
|
|
|
|
function restoreView() {
|
|
if (!renderView(state.currentView, state, viewModules)) {
|
|
fallbackView();
|
|
}
|
|
}
|
|
|
|
function fallbackView() {
|
|
renderWalletList();
|
|
showView("main");
|
|
}
|
|
|
|
async function init() {
|
|
// First, before anything can save: showView() saves on every navigation
|
|
// without awaiting, so a save that fails from here on has somewhere to be
|
|
// reported rather than being swallowed by the save queue
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/362). Registered ahead of
|
|
// the approval-window branch below too, since that window saves as well.
|
|
onSaveFailure(showSaveFailureBanner);
|
|
try {
|
|
await loadState();
|
|
} catch (e) {
|
|
// A profile this build cannot read is the one failure that must not
|
|
// fall through to the rest of init(). It used to: the load "succeeded"
|
|
// on a record nothing had validated, and the first dereference below
|
|
// threw, leaving a popup with no view, no message and no control on
|
|
// it, and no way out of the wallet from inside the product
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/311). Now the load
|
|
// refuses, and this is the screen that says so.
|
|
if (e instanceof StateUnusableError) {
|
|
stateRecovery.show(e);
|
|
return;
|
|
}
|
|
throw e;
|
|
}
|
|
applyTheme(state.theme);
|
|
|
|
// Sync runtime debug flag from persisted state before first render
|
|
setRuntimeDebug(state.debugMode);
|
|
|
|
// Create the debug/testnet banner if needed (uses runtime debug state)
|
|
updateDebugBanner();
|
|
|
|
// Auto-default active address
|
|
if (
|
|
state.activeAddress === null &&
|
|
state.wallets.length > 0 &&
|
|
state.wallets[0].addresses.length > 0
|
|
) {
|
|
state.activeAddress = state.wallets[0].addresses[0].address;
|
|
await saveState();
|
|
}
|
|
|
|
// Always init approval and txStatus — they may run in the approval popup window
|
|
approval.init(ctx);
|
|
txStatus.init(ctx);
|
|
|
|
// Check for approval mode
|
|
const params = new URLSearchParams(window.location.search);
|
|
const approvalId = params.get("approval");
|
|
if (approvalId) {
|
|
// Deliberately not awaited, and deliberately not .catch()ed. show()
|
|
// is async, so a throw past its first await surfaces as an unhandled
|
|
// rejection rather than an uncaught error — measured as still failing
|
|
// the run on both harnesses (Playwright `pageerror`, and the Firefox
|
|
// driver's console-service drain), so nothing is lost by leaving it
|
|
// on that path.
|
|
approval.show(approvalId);
|
|
showView("approve-site");
|
|
return;
|
|
}
|
|
|
|
$("btn-settings").addEventListener("click", () => {
|
|
if (
|
|
!document
|
|
.getElementById("view-settings")
|
|
.classList.contains("hidden")
|
|
) {
|
|
goBack();
|
|
return;
|
|
}
|
|
pushCurrentView();
|
|
settings.show();
|
|
});
|
|
|
|
setBackRenderer(makeBackRenderer(state, viewModules));
|
|
|
|
welcome.init(ctx);
|
|
addWallet.init(ctx);
|
|
home.init(ctx);
|
|
addressDetail.init(ctx);
|
|
addressToken.init(ctx);
|
|
send.init(ctx);
|
|
confirmTx.init(ctx);
|
|
transactionDetail.init(ctx);
|
|
receive.init(ctx);
|
|
addToken.init(ctx);
|
|
settings.init(ctx);
|
|
settingsAddToken.init(ctx);
|
|
deleteAddress.init(ctx);
|
|
|
|
if (!state.hasWallet) {
|
|
showView("welcome");
|
|
} else {
|
|
renderWalletList();
|
|
restoreView();
|
|
doRefreshAndRender();
|
|
setInterval(doRefreshAndRender, 10000);
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", init);
|