Skip to content

Commit

Permalink
Detect and throw error, and display it to the user, if we cannot get …
Browse files Browse the repository at this point in the history
…vault data when we should be able to
  • Loading branch information
danjm committed Mar 3, 2025
1 parent 33181c4 commit 3fa2d2c
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 13 deletions.
9 changes: 9 additions & 0 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/scripts/app-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ const registerInPageContentScript = async () => {
chrome.runtime.onInstalled.addListener(function (details) {
if (details.reason === 'install') {
chrome.storage.session.set({ isFirstTimeInstall: true });
chrome.storage.local.set({ vaultHasNotYetBeenCreated: true });
} else if (details.reason === 'update') {
chrome.storage.session.set({ isFirstTimeInstall: false });
}
Expand Down
53 changes: 47 additions & 6 deletions app/scripts/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ if (isFirefox) {
browser.runtime.onInstalled.addListener(function (details) {
if (details.reason === 'install') {
browser.storage.session.set({ isFirstTimeInstall: true });
browser.storage.local.set({ vaultHasNotYetBeenCreated: true });
} else if (details.reason === 'update') {
browser.storage.session.set({ isFirstTimeInstall: false });
}
Expand All @@ -155,6 +156,7 @@ if (isFirefox) {
browser.runtime.onInstalled.addListener(function (details) {
if (details.reason === 'install') {
global.sessionStorage.setItem('isFirstTimeInstall', true);
browser.storage.local.set({ vaultHasNotYetBeenCreated: true });
} else if (details.reason === 'update') {
global.sessionStorage.setItem('isFirstTimeInstall', false);
}
Expand Down Expand Up @@ -390,17 +392,37 @@ function overrideContentSecurityPolicyHeader() {
);
}

function stringifyError(error) {
return JSON.stringify({
message: error.message,
name: error.name,
stack: error.stack,
});
}

// These are set after initialization
let connectRemote;
let connectExternalExtension;
let connectExternalCaip;

browser.runtime.onConnect.addListener(async (...args) => {
// Queue up connection attempts here, waiting until after initialization
await isInitialized;
try {
await isInitialized;
connectRemote(...args);
} catch (error) {
const port = args[0];

const _state = await localStore.get();

port.postMessage({
target: 'ui',
error: stringifyError(error),
metamaskState: JSON.stringify(_state),
});
}

// This is set in `setupController`, which is called as part of initialization
connectRemote(...args);
});
browser.runtime.onConnectExternal.addListener(async (...args) => {
// Queue up connection attempts here, waiting until after initialization
Expand Down Expand Up @@ -640,11 +662,29 @@ export async function loadStateFromPersistence() {
firstTimeState = { ...firstTimeState, ...stateOverrides };
}

const { vaultHasNotYetBeenCreated } = await browser.storage.local.get(
'vaultHasNotYetBeenCreated',
);

// read from disk
// first from preferred, async API:
const preMigrationVersionedData =
(await persistenceManager.get()) ||
migrator.generateInitialState(firstTimeState);
let preMigrationVersionedData = await persistenceManager.get();

const vaultDataPresent = Boolean(
preMigrationVersionedData?.data?.KeyringController?.vault,
);

if (vaultHasNotYetBeenCreated === undefined && !vaultDataPresent) {
throw new Error('Data error: storage.local does not contain vault data');
}

if (!preMigrationVersionedData.data && !preMigrationVersionedData.meta) {
const initialState = migrator.generateInitialState(firstTimeState);
preMigrationVersionedData = {
...preMigrationVersionedData,
...initialState,
};
}

// report migration errors to sentry
migrator.on('error', (err) => {
Expand Down Expand Up @@ -1368,8 +1408,8 @@ function setupSentryGetStateGlobal(store) {
}

async function initBackground() {
await onInstall();
try {
await onInstall();
await initialize();
if (process.env.IN_TEST) {
// Send message to offscreen document
Expand All @@ -1385,6 +1425,7 @@ async function initBackground() {
persistenceManager.cleanUpMostRecentRetrievedState();
} catch (error) {
log.error(error);
rejectInitialization(error);
}
}
if (!process.env.SKIP_BACKGROUND_INITIALIZATION) {
Expand Down
11 changes: 11 additions & 0 deletions app/scripts/lib/stores/persistence-manager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import log from 'loglevel';
import browser from 'webextension-polyfill';
import { captureException } from '@sentry/browser';
import { isEmpty } from 'lodash';
import { type MetaMaskStateType, MetaMaskStorageStructure } from './base-store';
Expand Down Expand Up @@ -63,12 +64,15 @@ export class PersistenceManager {

#localStore: ExtensionStore | ReadOnlyNetworkStore;

#vaultReference: string | null;

constructor({
localStore,
}: {
localStore: ExtensionStore | ReadOnlyNetworkStore;
}) {
this.#localStore = localStore;
this.#vaultReference = null;
}

setMetadata(metadata: { version: number }) {
Expand All @@ -84,6 +88,13 @@ export class PersistenceManager {
}
try {
await this.#localStore.set({ data: state, meta: this.#metadata });
const newVaultReference = state.KeyringController?.vault ?? null;
if (newVaultReference !== this.#vaultReference) {
if (newVaultReference && this.#vaultReference === null) {
browser.storage.local.remove('vaultHasNotYetBeenCreated');
}
this.#vaultReference = newVaultReference;
}
if (this.#dataPersistenceFailing) {
this.#dataPersistenceFailing = false;
}
Expand Down
40 changes: 38 additions & 2 deletions app/scripts/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ import {
PLATFORM_FIREFOX,
} from '../../shared/constants/app';
import { isManifestV3 } from '../../shared/modules/mv3.utils';
import { checkForLastErrorAndLog } from '../../shared/modules/browser-runtime.utils';
import {
checkForLastErrorAndLog,
} from '../../shared/modules/browser-runtime.utils';
import { SUPPORT_LINK } from '../../shared/lib/ui-utils';
import { getErrorHtml } from '../../shared/lib/error-utils';
import {
getErrorHtml,
getStateCorruptionErrorHtml,
} from '../../shared/lib/error-utils';
import { endTrace, trace, TraceName } from '../../shared/lib/trace';
import ExtensionPlatform from './platforms/extension';
import { setupMultiplex } from './lib/stream-utils';
Expand All @@ -40,6 +45,11 @@ const PHISHING_WARNING_PAGE_TIMEOUT = 1 * 1000; // 1 Second
const PHISHING_WARNING_SW_STORAGE_KEY = 'phishing-warning-sw-registered';
const METHOD_START_UI_SYNC = 'startUISync';

const STATE_CORRUPTION_ERRORS = [
'Data error: storage.local does not contain vault data',
'Corruption: block checksum mismatch',
];

const container = document.getElementById('app-content');

let extensionPort;
Expand Down Expand Up @@ -99,6 +109,14 @@ async function start() {
const method = message?.data?.method;

if (method !== METHOD_START_UI_SYNC) {
const error = JSON.parse(message.error ?? '');
if (STATE_CORRUPTION_ERRORS.includes(error.message)) {
displayStateCorruptionError(
error,
JSON.parse(message.metamaskState ?? ''),
);
}

return;
}

Expand Down Expand Up @@ -336,6 +354,24 @@ async function displayCriticalError(errorKey, err, metamaskState) {
throw err;
}

async function displayStateCorruptionError(err, metamaskState) {
const html = await getStateCorruptionErrorHtml(SUPPORT_LINK, metamaskState);
container.innerHTML = html;

const button = document.getElementById('critical-error-button');

button?.addEventListener('click', (_) => {
extensionPort.postMessage({
target: 'Background',
data: {
name: 'RESTORE_VAULT_FROM_BACKUP', // the @metamask/object-multiplex channel name
},
});
});
log.error(err.stack);
throw err;
}

/**
* Establishes a connection to the background and a Web3 provider
*
Expand Down
64 changes: 61 additions & 3 deletions shared/lib/error-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const _setupLocale = async (currentLocale) => {
return { currentLocaleMessages, enLocaleMessages };
};

export const setupLocale = memoize(_setupLocale);
export const setupErrorLocale = memoize(_setupLocale);

const getLocaleContext = (currentLocaleMessages, enLocaleMessages) => {
return (key) => {
Expand All @@ -45,10 +45,10 @@ export async function getErrorHtml(errorKey, supportLink, metamaskState) {
let response, preferredLocale;
if (metamaskState?.currentLocale) {
preferredLocale = metamaskState.currentLocale;
response = await setupLocale(metamaskState.currentLocale);
response = await setupErrorLocale(metamaskState.currentLocale);
} else {
preferredLocale = await getFirstPreferredLangCode();
response = await setupLocale(preferredLocale);
response = await setupErrorLocale(preferredLocale);
}

const textDirection = ['ar', 'dv', 'fa', 'he', 'ku'].includes(preferredLocale)
Expand Down Expand Up @@ -96,3 +96,61 @@ export async function getErrorHtml(errorKey, supportLink, metamaskState) {
</div>
`;
}

export async function getStateCorruptionErrorHtml(supportLink, metamaskState) {
let response, preferredLocale;
if (metamaskState?.currentLocale) {
preferredLocale = metamaskState.currentLocale;
response = await setupErrorLocale(metamaskState.currentLocale);
} else {
preferredLocale = await getFirstPreferredLangCode();
response = await setupErrorLocale(preferredLocale);
}

const textDirection = ['ar', 'dv', 'fa', 'he', 'ku'].includes(preferredLocale)
? 'rtl'
: 'auto';

switchDirection(textDirection);
const { currentLocaleMessages, enLocaleMessages } = response;
const t = getLocaleContext(currentLocaleMessages, enLocaleMessages);
const hasBackup =
metamaskState?.data?.PreferencesController?.initializationFlags
?.vaultBackedUp;
/**
* The pattern ${errorKey === 'troubleStarting' ? t('troubleStarting') : ''}
* is neccessary because we we need linter to see the string
* of the locale keys. If we use the variable directly, the linter will not
* see the string and will not be able to check if the locale key exists.
*/
return `
<div class="critical-error__container">
<div class="critical-error">
<div class="critical-error__icon">
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="m443 342l-126-241c-16-32-40-50-65-50-26 0-50 18-66 50l-126 241c-16 30-18 60-5 83 13 23 38 36 71 36l251 0c33 0 58-13 71-36 13-23 11-53-5-83z m-206-145c0-8 6-15 15-15 8 0 14 7 14 15l0 105c0 8-6 15-14 15-9 0-15-7-15-15z m28 182c-1 1-2 2-3 3-1 0-2 1-3 1-1 1-2 1-4 2-1 0-2 0-3 0-2 0-3 0-4 0-2-1-3-1-4-2-1 0-2-1-3-1-1-1-2-2-3-3-4-4-6-9-6-15 0-5 2-11 6-15 1 0 2-1 3-2 1-1 2-2 3-2 1-1 2-1 4-1 2-1 5-1 7 0 2 0 3 0 4 1 1 0 2 1 3 2 1 1 2 2 3 2 4 4 6 10 6 15 0 6-2 11-6 15z"/>
</svg>
</div>
<div>
<p>
${
hasBackup === true
? t('stateCorruptionDetectedWithBackup')
: t('stateCorruptionDetectedNoBackup')
}
</p>
<p class="critical-error__footer">
<span>${t('unexpectedBehavior')}</span>
<a
href=${supportLink}
class="critical-error__link"
target="_blank"
rel="noopener noreferrer">
${t('sendBugReport')}
</a>
</p>
</div>
</div>
</div>
`;
}
4 changes: 2 additions & 2 deletions ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { SENTRY_UI_STATE } from '../app/scripts/constants/sentry-state';
import { ENVIRONMENT_TYPE_POPUP } from '../shared/constants/app';
import { COPY_OPTIONS } from '../shared/constants/copy';
import switchDirection from '../shared/lib/switch-direction';
import { setupLocale } from '../shared/lib/error-utils';
import { setupErrorLocale } from '../shared/lib/error-utils';
import { trace, TraceName } from '../shared/lib/trace';
import { getCurrentChainId } from '../shared/modules/selectors/networks';
import * as actions from './store/actions';
Expand Down Expand Up @@ -101,7 +101,7 @@ export async function setupInitialStore(
metamaskState.featureFlags = {};
}

const { currentLocaleMessages, enLocaleMessages } = await setupLocale(
const { currentLocaleMessages, enLocaleMessages } = await setupErrorLocale(
metamaskState.currentLocale,
);

Expand Down

0 comments on commit 3fa2d2c

Please sign in to comment.