From bd26cdd5ff01199d44aded6055ab2953d5f8fc0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Mas=C5=82owski?= Date: Tue, 18 Feb 2025 18:52:46 +0100 Subject: [PATCH] refactor: improve error handling of extension tab api interactions --- .../src/components/Layout/MainLayout.tsx | 4 +- .../features/dapp/components/ConfirmData.tsx | 23 ++++++--- .../ConfirmTransaction.tsx | 30 +++++++---- .../components/confirm-transaction/hooks.ts | 3 +- .../components/confirm-transaction/utils.ts | 5 +- .../lace-migration-client.extension.ts | 11 +++- .../create-lace-migration-open-listener.ts | 6 ++- .../src/lib/scripts/background/cip30.ts | 5 +- .../lib/scripts/background/requestAccess.ts | 32 +++++++++--- .../background/services/utilityServices.ts | 32 +++++++++--- .../background/session/is-lace-tab-active.ts | 12 +++-- .../src/lib/scripts/background/util.ts | 31 +++++++----- .../scripts/trezor/trezor-usb-permissions.ts | 50 +++++++++++++------ .../catch-and-brand-extension-api-error.ts | 23 +++++++++ .../src/utils/senderToDappInfo.ts | 6 ++- .../TopUpWallet/TopUpWalletButton.tsx | 6 +-- .../TopUpWallet/TopUpWalletDialog.tsx | 11 ++-- .../src/views/browser-view/routes/index.tsx | 16 ++++-- .../ui/app/pages/dapp-connector/signData.tsx | 28 +++++++---- .../ui/app/pages/dapp-connector/signTx.tsx | 31 ++++++++---- 20 files changed, 260 insertions(+), 105 deletions(-) create mode 100644 apps/browser-extension-wallet/src/utils/catch-and-brand-extension-api-error.ts diff --git a/apps/browser-extension-wallet/src/components/Layout/MainLayout.tsx b/apps/browser-extension-wallet/src/components/Layout/MainLayout.tsx index d81ce835bb..8ba985a6b4 100644 --- a/apps/browser-extension-wallet/src/components/Layout/MainLayout.tsx +++ b/apps/browser-extension-wallet/src/components/Layout/MainLayout.tsx @@ -52,7 +52,7 @@ export const MainLayout = ({ getAboutExtensionData(); }, [getAboutExtensionData]); - const onUpdateAknowledge = useCallback(async () => { + const onUpdateAcknowledge = useCallback(async () => { const data = { version, acknowledged: true, reason }; await storage.local.set({ [ABOUT_EXTENSION_KEY]: data @@ -72,7 +72,7 @@ export const MainLayout = ({ diff --git a/apps/browser-extension-wallet/src/features/dapp/components/ConfirmData.tsx b/apps/browser-extension-wallet/src/features/dapp/components/ConfirmData.tsx index 6ccd1dd225..7a1e0953a8 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/ConfirmData.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/ConfirmData.tsx @@ -56,16 +56,27 @@ export const DappConfirmData = (): React.ReactElement => { dataToSign: string; }>(); - const cancelTransaction = useCallback(async () => { - await req.reject('User rejected to sign'); - window.close(); - }, [req]); + const cancelTransaction = useCallback( + async (reason = 'User rejected to sign') => { + await req.reject(reason); + window.close(); + }, + [req] + ); useOnUnload(cancelTransaction); useEffect(() => { const subscription = signingCoordinator.signDataRequest$.pipe(take(1)).subscribe(async (r) => { - setDappInfo(await senderToDappInfo(r.signContext.sender)); + try { + setDappInfo(await senderToDappInfo(r.signContext.sender)); + } catch (error) { + logger.error(error); + void cancelTransaction('Could not get DApp info'); + redirectToSignFailure(); + return; + } + setSignDataRequest(r); }); @@ -86,7 +97,7 @@ export const DappConfirmData = (): React.ReactElement => { subscription.unsubscribe(); api.shutdown(); }; - }, [setSignDataRequest]); + }, [cancelTransaction, redirectToSignFailure, setSignDataRequest]); useEffect(() => { if (!req) return; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx index b82adf8e16..0f8125879c 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx @@ -19,6 +19,8 @@ import { runtime } from 'webextension-polyfill'; import { Skeleton } from 'antd'; import { DappTransactionContainer } from './DappTransactionContainer'; import { useTxWitnessRequest } from '@providers/TxWitnessRequestProvider'; +import { useRedirection } from '@hooks'; +import { dAppRoutePaths } from '@routes'; export const ConfirmTransaction = (): React.ReactElement => { const { t } = useTranslation(); @@ -28,6 +30,7 @@ export const ConfirmTransaction = (): React.ReactElement => { signTxRequest: { request: req, set: setSignTxRequest } } = useViewsFlowContext(); const { walletType, isHardwareWallet, walletInfo, inMemoryWallet } = useWalletStore(); + const redirectToDappTxSignFailure = useRedirection(dAppRoutePaths.dappTxSignFailure); const analytics = useAnalyticsContext(); const [confirmTransactionError] = useState(false); const disallowSignTx = useDisallowSignTx(req); @@ -49,11 +52,26 @@ export const ConfirmTransaction = (): React.ReactElement => { const txWitnessRequest = useTxWitnessRequest(); + const cancelTransaction = useCallback(() => { + disallowSignTx(true); + }, [disallowSignTx]); + + useOnUnload(cancelTransaction); + useEffect(() => { (async () => { - if (!txWitnessRequest) return (): (() => void) => void 0; + const emptyFn = (): void => void 0; + if (!txWitnessRequest) return emptyFn; + + try { + setDappInfo(await senderToDappInfo(txWitnessRequest.signContext.sender)); + } catch (error) { + logger.error(error); + void disallowSignTx(true, 'Could not get DApp info'); + redirectToDappTxSignFailure(); + return emptyFn; + } - setDappInfo(await senderToDappInfo(txWitnessRequest.signContext.sender)); setSignTxRequest(txWitnessRequest); const api = exposeApi>( @@ -73,7 +91,7 @@ export const ConfirmTransaction = (): React.ReactElement => { api.shutdown(); }; })(); - }, [setSignTxRequest, setDappInfo, txWitnessRequest]); + }, [setSignTxRequest, setDappInfo, txWitnessRequest, redirectToDappTxSignFailure, disallowSignTx]); const onCancelTransaction = () => { analytics.sendEventToPostHog(PostHogAction.SendTransactionSummaryCancelClick, { @@ -82,12 +100,6 @@ export const ConfirmTransaction = (): React.ReactElement => { disallowSignTx(true); }; - const cancelTransaction = useCallback(() => { - disallowSignTx(true); - }, [disallowSignTx]); - - useOnUnload(cancelTransaction); - return ( {req && walletInfo && inMemoryWallet ? : } diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts index 694be40fb8..0ad2b6e2e2 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts @@ -151,7 +151,8 @@ export const useCreateMintedAssetList = ({ export const useDisallowSignTx = ( req: TransactionWitnessRequest -): ((close?: boolean) => Promise) => useCallback(async (close) => await disallowSignTx(req, close), [req]); +): ((close?: boolean, reason?: string) => Promise) => + useCallback(async (close, reason) => await disallowSignTx(req, close, reason), [req]); export const useAllowSignTx = ( req: TransactionWitnessRequest diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/utils.ts b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/utils.ts index 519b4613d9..c08a384b2f 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/utils.ts +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/utils.ts @@ -27,10 +27,11 @@ export const readyToSign = (): void => { export const disallowSignTx = async ( req: TransactionWitnessRequest, - close = false + close = false, + reason = 'User declined to sign' ): Promise => { try { - await req?.reject('User declined to sign'); + await req?.reject(reason); } finally { close && window.close(); } diff --git a/apps/browser-extension-wallet/src/features/nami-migration/migration-tool/cross-extension-messaging/lace-migration-client.extension.ts b/apps/browser-extension-wallet/src/features/nami-migration/migration-tool/cross-extension-messaging/lace-migration-client.extension.ts index 3bc1769303..fc07da39f3 100644 --- a/apps/browser-extension-wallet/src/features/nami-migration/migration-tool/cross-extension-messaging/lace-migration-client.extension.ts +++ b/apps/browser-extension-wallet/src/features/nami-migration/migration-tool/cross-extension-messaging/lace-migration-client.extension.ts @@ -7,6 +7,7 @@ import { NAMI_EXTENSION_ID } from './lace/environment'; import { createLaceMigrationOpenListener } from './lace/create-lace-migration-open-listener'; import { LACE_EXTENSION_ID } from './nami/environment'; import { logger } from '@lace/common'; +import { catchAndBrandExtensionApiError } from '@utils/catch-and-brand-extension-api-error'; type CheckMigrationStatus = () => Promise; @@ -42,6 +43,14 @@ export const handleNamiRequests = (): void => { runtime.onMessageExternal.addListener(createLaceMigrationPingListener(NAMI_EXTENSION_ID)); logger.debug('[NAMI MIGRATION] createLaceMigrationOpenListener'); runtime.onMessageExternal.addListener( - createLaceMigrationOpenListener(NAMI_EXTENSION_ID, LACE_EXTENSION_ID, tabs.create) + createLaceMigrationOpenListener(NAMI_EXTENSION_ID, LACE_EXTENSION_ID, ({ url }) => + catchAndBrandExtensionApiError( + tabs.create({ url }), + `[NAMI MIGRATION] laceMigrationOpenListener failed to create tab with url ${url}`, + { + reThrow: false + } + ) + ) ); }; diff --git a/apps/browser-extension-wallet/src/features/nami-migration/migration-tool/cross-extension-messaging/lace/create-lace-migration-open-listener.ts b/apps/browser-extension-wallet/src/features/nami-migration/migration-tool/cross-extension-messaging/lace/create-lace-migration-open-listener.ts index 967e3adc73..f4bcd6a7a6 100644 --- a/apps/browser-extension-wallet/src/features/nami-migration/migration-tool/cross-extension-messaging/lace/create-lace-migration-open-listener.ts +++ b/apps/browser-extension-wallet/src/features/nami-migration/migration-tool/cross-extension-messaging/lace/create-lace-migration-open-listener.ts @@ -8,7 +8,11 @@ export const createLaceMigrationOpenListener = logger.debug('[NAMI MIGRATION] createLaceMigrationOpenListener', message, sender); if (message === NamiMessages.open && sender.id === namiExtensionId) { // First close all open lace tabs - await closeAllLaceOrNamiTabs(); + try { + await closeAllLaceOrNamiTabs(); + } catch (error) { + logger.error('[NAMI MIGRATION] createLaceMigrationOpenListener: failed to close all windows', error); + } createTab({ url: `chrome-extension://${laceExtensionId}/app.html` }); } }; diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/cip30.ts b/apps/browser-extension-wallet/src/lib/scripts/background/cip30.ts index 395b72624b..94d87b34e3 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/background/cip30.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/background/cip30.ts @@ -47,7 +47,7 @@ export const confirmationCallback: walletCip30.CallbackConfirmation = { return cancelOnTabClose(tab); } catch (error) { logger.error(error); - return Promise.reject(new ApiError(APIErrorCode.InternalError, 'Unable to sign transaction')); + throw new ApiError(APIErrorCode.InternalError, 'Unable to sign transaction'); } }, DEBOUNCE_THROTTLE, @@ -62,8 +62,7 @@ export const confirmationCallback: walletCip30.CallbackConfirmation = { return cancelOnTabClose(tab); } catch (error) { logger.error(error); - // eslint-disable-next-line unicorn/no-useless-undefined - return Promise.reject(new ApiError(APIErrorCode.InternalError, 'Unable to sign data')); + throw new ApiError(APIErrorCode.InternalError, 'Unable to sign data'); } }, DEBOUNCE_THROTTLE, diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/requestAccess.ts b/apps/browser-extension-wallet/src/lib/scripts/background/requestAccess.ts index bd10398783..090d377a82 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/background/requestAccess.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/background/requestAccess.ts @@ -10,15 +10,31 @@ import { AUTHORIZED_DAPPS_KEY } from '../types'; import { Wallet } from '@lace/cardano'; import { BehaviorSubject } from 'rxjs'; import { senderToDappInfo } from '@src/utils/senderToDappInfo'; +import { logger } from '@lace/common'; const DEBOUNCE_THROTTLE = 500; export const dappInfo$ = new BehaviorSubject(undefined); export const requestAccess: RequestAccess = async (sender: Runtime.MessageSender) => { - const { logo, name, url } = await senderToDappInfo(sender); + let dappInfo: Wallet.DappInfo; + try { + dappInfo = await senderToDappInfo(sender); + } catch (error) { + logger.error('Failed to get info of a DApp requesting access', error); + return false; + } + + const { logo, name, url } = dappInfo; dappInfo$.next({ logo, name, url }); - await ensureUiIsOpenAndLoaded('#/dapp/connect'); + + try { + await ensureUiIsOpenAndLoaded('#/dapp/connect'); + } catch (error) { + logger.error('Failed to ensure DApp connection UI is loaded', error); + return false; + } + const isAllowed = await userPromptService.allowOrigin(url); if (isAllowed === 'deny') return Promise.reject(); if (isAllowed === 'allow') { @@ -31,14 +47,16 @@ export const requestAccess: RequestAccess = async (sender: Runtime.MessageSender authorizedDappsList.next([{ logo, name, url }]); } } else { - tabs.onRemoved.addListener((t) => { - if (t === sender.tab.id) { + const onRemovedHandler = (tabId: number) => { + if (tabId === sender.tab.id) { authenticator.revokeAccess(sender); - tabs.onRemoved.removeListener(this); + tabs.onRemoved.removeListener(onRemovedHandler); } - }); + }; + tabs.onRemoved.addListener(onRemovedHandler); } - return Promise.resolve(true); + + return true; }; export const requestAccessDebounced = pDebounce(requestAccess, DEBOUNCE_THROTTLE, { before: true }); diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/services/utilityServices.ts b/apps/browser-extension-wallet/src/lib/scripts/background/services/utilityServices.ts index 2f48a8358f..9d608fbf41 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/background/services/utilityServices.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/background/services/utilityServices.ts @@ -29,6 +29,7 @@ import { laceFeaturesApiProperties, LACE_FEATURES_CHANNEL } from '../injectUtil' import { getErrorMessage } from '@src/utils/get-error-message'; import { logger } from '@lace/common'; import { POPUP_WINDOW_NAMI_TITLE } from '@utils/constants'; +import { catchAndBrandExtensionApiError } from '@utils/catch-and-brand-extension-api-error'; export const requestMessage$ = new Subject(); export const backendFailures$ = new BehaviorSubject(0); @@ -103,17 +104,26 @@ const handleOpenBrowser = async (data: OpenBrowserData) => { break; } const params = data.urlSearchParams ? `?${data.urlSearchParams}` : ''; - await tabs.create({ url: `app.html#${path}${params}` }).catch((error) => logger.error(error)); + const url = `app.html#${path}${params}`; + await catchAndBrandExtensionApiError(tabs.create({ url }), `Failed to open expanded view with url: ${url}`).catch( + (error) => logger.error(error) + ); }; const handleOpenNamiBrowser = async (data: OpenNamiBrowserData) => { - await tabs.create({ url: `popup.html#${data.path}` }).catch((error) => logger.error(error)); + const url = `popup.html#${data.path}`; + await catchAndBrandExtensionApiError( + tabs.create({ url }), + `Failed to open nami mode extended with url: ${url}` + ).catch((error) => logger.error(error)); }; const enrichWithTabsDataIfMissing = (browserWindows: Windows.Window[]) => { const promises = browserWindows.map(async (w) => ({ ...w, - tabs: w.tabs || (await tabs.query({ windowId: w.id })) + tabs: + w.tabs || + (await catchAndBrandExtensionApiError(tabs.query({ windowId: w.id }), 'Failed to query tabs of a window')) })); return Promise.all(promises); }; @@ -129,7 +139,8 @@ const doesWindowHaveOtherTabs = (browserWindow: WindowWithTabsNotOptional) => const closeAllTabsAndOpenPopup = async () => { try { - const allWindows = await enrichWithTabsDataIfMissing(await windows.getAll()); + const allWindowsRaw = await catchAndBrandExtensionApiError(windows.getAll(), 'Failed to query all browser windows'); + const allWindows = await enrichWithTabsDataIfMissing(allWindowsRaw); if (allWindows.length === 0) return; const windowsWith3rdPartyTabs = allWindows.filter((w) => doesWindowHaveOtherTabs(w)); @@ -141,14 +152,19 @@ const closeAllTabsAndOpenPopup = async () => { const noSingleWindowWith3rdPartyTabsOpen = !nextFocusedWindow; if (noSingleWindowWith3rdPartyTabsOpen) { nextFocusedWindow = allWindows[0]; - await tabs.create({ active: true, windowId: nextFocusedWindow.id }); + await catchAndBrandExtensionApiError( + tabs.create({ active: true, windowId: nextFocusedWindow.id }), + 'Failed to open empty tab to prevent window from closing' + ); } - await windows.update(nextFocusedWindow.id, { focused: true }); + await catchAndBrandExtensionApiError( + windows.update(nextFocusedWindow.id, { focused: true }), + 'Failed to focus window' + ); await closeAllLaceOrNamiTabs(); - await action.openPopup(); + await catchAndBrandExtensionApiError(action.openPopup(), 'Failed to open popup'); } catch (error) { - // unable to programatically open the popup again logger.error(error); } }; diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/session/is-lace-tab-active.ts b/apps/browser-extension-wallet/src/lib/scripts/background/session/is-lace-tab-active.ts index cacdfe5413..bca0397d01 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/background/session/is-lace-tab-active.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/background/session/is-lace-tab-active.ts @@ -1,6 +1,7 @@ import { LACE_EXTENSION_ID } from '@src/features/nami-migration/migration-tool/cross-extension-messaging/nami/environment'; import { distinctUntilChanged, from, fromEventPattern, map, merge, share, startWith, switchMap } from 'rxjs'; import { Tabs, tabs, windows } from 'webextension-polyfill'; +import { catchAndBrandExtensionApiError } from '@utils/catch-and-brand-extension-api-error'; type WindowId = number; @@ -21,10 +22,13 @@ const tabActivated$ = fromEventPattern( export const isLaceTabActive$ = merge(windowRemoved$, tabUpdated$, tabActivated$).pipe( switchMap(() => from( - tabs.query({ - active: true, - url: `chrome-extension://${LACE_EXTENSION_ID}/*` - }) + catchAndBrandExtensionApiError( + tabs.query({ + active: true, + url: `chrome-extension://${LACE_EXTENSION_ID}/*` + }), + 'Failed to query for currently active lace tab' + ) ) ), map((activeLaceTabs) => activeLaceTabs.length > 0), diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/util.ts b/apps/browser-extension-wallet/src/lib/scripts/background/util.ts index 3f7c2b78f4..ba8bf7d441 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/background/util.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/background/util.ts @@ -12,6 +12,7 @@ import { WalletType } from '@cardano-sdk/web-extension'; import { getBackgroundStorage } from './storage'; +import { catchAndBrandExtensionApiError } from '@utils/catch-and-brand-extension-api-error'; const { blake2b } = Wallet.Crypto; const DAPP_CONNECTOR_REGEX = new RegExp(/dappconnector/i); @@ -47,13 +48,6 @@ const calculatePopupWindowPositionAndSize = ( ...popup }); -const createTab = async (url: string, active = false) => - tabs.create({ - url: runtime.getURL(url), - active, - pinned: true - }); - const createWindow = ( tabId: number, windowSize: WindowSizeAndPositionProps, @@ -74,7 +68,14 @@ const createWindow = ( */ export const launchCip30Popup = async (url: string): Promise => { const currentWindow = await windows.getCurrent(); - const tab = await createTab(`../dappConnector.html${url}`, false); + const tab = await catchAndBrandExtensionApiError( + tabs.create({ + url: runtime.getURL(`../dappConnector.html${url}`), + active: false, + pinned: true + }), + 'Failed to launch cip30 popup' + ); const { namiMigration } = await getBackgroundStorage(); const windowSize = namiMigration?.mode === 'nami' ? POPUP_WINDOW_NAMI : POPUP_WINDOW; @@ -125,12 +126,18 @@ export const getActiveWallet = async ({ }; export const closeAllLaceOrNamiTabs = async (shouldRemoveTab?: (url: string) => boolean): Promise => { - const openTabs = await tabs.query({ title: 'Lace' }); - const namiTabs = await tabs.query({ title: POPUP_WINDOW_NAMI_TITLE }); - openTabs.push(...namiTabs); + const openTabs = [ + ...(await catchAndBrandExtensionApiError(tabs.query({ title: 'Lace' }), 'Failed to query lace tabs for closing')), + ...(await catchAndBrandExtensionApiError( + tabs.query({ title: POPUP_WINDOW_NAMI_TITLE }), + 'Failed to query nami mode tabs for closing' + )) + ]; // Close all previously opened lace dapp connector windows for (const tab of openTabs) { - if (!shouldRemoveTab || shouldRemoveTab(tab.url)) await tabs.remove(tab.id); + if (!shouldRemoveTab || shouldRemoveTab(tab.url)) { + await catchAndBrandExtensionApiError(tabs.remove(tab.id), `Failed to close tab with url ${tab.url}`); + } } }; diff --git a/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-usb-permissions.ts b/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-usb-permissions.ts index c4aa97fb19..3e3d1bdf0b 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-usb-permissions.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/trezor/trezor-usb-permissions.ts @@ -1,27 +1,47 @@ import { runtime, tabs } from 'webextension-polyfill'; import { AllowedOrigins } from './types'; +import { catchAndBrandExtensionApiError } from '@utils/catch-and-brand-extension-api-error'; +import { logger } from '@lace/common'; + +const contextualMessage = (msg: string) => `[trezor-usb-permissions] ${msg}`; /* Handling messages from usb permissions iframe */ const switchToPopupTab = async (event?: BeforeUnloadEvent) => { window.removeEventListener('beforeunload', switchToPopupTab); - if (!event) { - // triggered from 'usb-permissions-close' message - // close current tab - const currentTabs = await tabs.query({ - currentWindow: true, - active: true - }); + try { + if (!event) { + // triggered from 'usb-permissions-close' message + // close current tab + const currentTabs = await catchAndBrandExtensionApiError( + tabs.query({ + currentWindow: true, + active: true + }), + contextualMessage('Failed to query for current tab when switching to popup') + ); + if (currentTabs.length < 0) return; + await catchAndBrandExtensionApiError( + tabs.remove(currentTabs[0].id), + contextualMessage('Failed to remove current tab when switching to popup') + ); + } + + // find tab by popup pattern and switch to it + const currentTabs = await catchAndBrandExtensionApiError( + tabs.query({ + url: `${AllowedOrigins.TREZOR_CONNECT_POPUP_BASE_URL}/popup.html` + }), + contextualMessage('Failed to query TREZOR_CONNECT_POPUP tab') + ); if (currentTabs.length < 0) return; - await tabs.remove(currentTabs[0].id); + void catchAndBrandExtensionApiError( + tabs.update(currentTabs[0].id, { active: true }), + contextualMessage('Failed to switch to the TREZOR_CONNECT_POPUP tab') + ); + } catch (error) { + logger.error(error); } - - // find tab by popup pattern and switch to it - const currentTabs = await tabs.query({ - url: `${AllowedOrigins.TREZOR_CONNECT_POPUP_BASE_URL}/popup.html` - }); - if (currentTabs.length < 0) return; - tabs.update(currentTabs[0].id, { active: true }); }; window.addEventListener('message', async (event) => { diff --git a/apps/browser-extension-wallet/src/utils/catch-and-brand-extension-api-error.ts b/apps/browser-extension-wallet/src/utils/catch-and-brand-extension-api-error.ts new file mode 100644 index 0000000000..e5eb9d5ed3 --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/catch-and-brand-extension-api-error.ts @@ -0,0 +1,23 @@ +import { logger } from '@lace/common'; + +class ExtensionApiError extends Error {} + +type BrandExtensionApiErrorOptions = { + reThrow?: boolean; +}; + +export const catchAndBrandExtensionApiError = async ( + promise: Promise, + errorMessage: string, + { reThrow = true }: BrandExtensionApiErrorOptions = {} + // eslint-disable-next-line consistent-return +): Promise => { + try { + return await promise; + } catch (error) { + logger.error(`Extension API error, ${errorMessage} due to:`, error); + if (reThrow) { + throw new ExtensionApiError(errorMessage); + } + } +}; diff --git a/apps/browser-extension-wallet/src/utils/senderToDappInfo.ts b/apps/browser-extension-wallet/src/utils/senderToDappInfo.ts index 2ac5c51f56..ece34ddcff 100644 --- a/apps/browser-extension-wallet/src/utils/senderToDappInfo.ts +++ b/apps/browser-extension-wallet/src/utils/senderToDappInfo.ts @@ -3,12 +3,16 @@ import { Wallet } from '@lace/cardano'; import { getRandomIcon } from '@lace/common'; import uniqueId from 'lodash/uniqueId'; import { tabs, Runtime } from 'webextension-polyfill'; +import { catchAndBrandExtensionApiError } from '@utils/catch-and-brand-extension-api-error'; export const senderToDappInfo = async (sender: Runtime.MessageSender): Promise => { if (!sender.tab?.id) throw new Error('Unknown sender tab id'); // Tab info might've changed. It used to fail e2e tests when using data from 'sender.tab'. // It would be better if SDK waited for tab to load before emitting events with sender. - const tab = await tabs.get(sender.tab?.id); + const tab = await catchAndBrandExtensionApiError( + tabs.get(sender.tab?.id), + `Failed to get tab data of a DApp with url ${sender.url}` + ); return { url: senderOrigin(sender), logo: tab.favIconUrl || getRandomIcon({ id: uniqueId(), size: 40 }), diff --git a/apps/browser-extension-wallet/src/views/browser-view/components/TopUpWallet/TopUpWalletButton.tsx b/apps/browser-extension-wallet/src/views/browser-view/components/TopUpWallet/TopUpWalletButton.tsx index 3d3c9090e6..39a0358b12 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/components/TopUpWallet/TopUpWalletButton.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/components/TopUpWallet/TopUpWalletButton.tsx @@ -1,10 +1,9 @@ -import { tabs } from 'webextension-polyfill'; import { AdaComponentTransparent, Button } from '@input-output-hk/lace-ui-toolkit'; import React, { useRef, useState } from 'react'; import { TopUpWalletDialog } from './TopUpWalletDialog'; import { useTranslation } from 'react-i18next'; import { BANXA_LACE_URL } from './config'; -import { useAnalyticsContext } from '@providers'; +import { useAnalyticsContext, useExternalLinkOpener } from '@providers'; import { PostHogAction } from '@lace/common'; export const TopUpWalletButton = (): React.ReactElement => { @@ -12,6 +11,7 @@ export const TopUpWalletButton = (): React.ReactElement => { const [open, setOpen] = useState(false); const { t } = useTranslation(); const analytics = useAnalyticsContext(); + const openExternalLink = useExternalLinkOpener(); return ( <> @@ -35,7 +35,7 @@ export const TopUpWalletButton = (): React.ReactElement => { triggerRef={dialogTriggerReference} onConfirm={() => { analytics.sendEventToPostHog(PostHogAction.TokenBuyAdaDisclaimerContinueClick); - tabs.create({ url: BANXA_LACE_URL }); + openExternalLink(BANXA_LACE_URL); setOpen(false); }} /> diff --git a/apps/browser-extension-wallet/src/views/browser-view/components/TopUpWallet/TopUpWalletDialog.tsx b/apps/browser-extension-wallet/src/views/browser-view/components/TopUpWallet/TopUpWalletDialog.tsx index 4a65ce7698..269f31eefc 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/components/TopUpWallet/TopUpWalletDialog.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/components/TopUpWallet/TopUpWalletDialog.tsx @@ -1,9 +1,9 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { Box, Dialog, Text, TextLink } from '@input-output-hk/lace-ui-toolkit'; import styles from './TopUpWallet.module.scss'; import { useTranslation } from 'react-i18next'; -import { tabs } from 'webextension-polyfill'; import { BANXA_HOMEPAGE_URL } from './config'; +import { useExternalLinkOpener } from '@providers'; interface TopUpWalletDialogProps { open: boolean; @@ -18,9 +18,10 @@ export const TopUpWalletDialog = ({ triggerRef }: TopUpWalletDialogProps): React.ReactElement => { const { t } = useTranslation(); - const handleOpenTabBanxaHomepage = useCallback(() => { - tabs.create({ url: BANXA_HOMEPAGE_URL }); - }, []); + const openExternalLink = useExternalLinkOpener(); + const handleOpenTabBanxaHomepage = () => { + openExternalLink(BANXA_HOMEPAGE_URL); + }; return ( diff --git a/apps/browser-extension-wallet/src/views/browser-view/routes/index.tsx b/apps/browser-extension-wallet/src/views/browser-view/routes/index.tsx index a684c9c434..c2c615c3e8 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/routes/index.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/routes/index.tsx @@ -42,6 +42,7 @@ import { Crash } from '@components/Crash'; import { useIsPosthogClientInitialized } from '@providers/PostHogClientProvider/useIsPosthogClientInitialized'; import { logger } from '@lace/common'; import { VotingLayout } from '../features/voting-beta'; +import { catchAndBrandExtensionApiError } from '@utils/catch-and-brand-extension-api-error'; export const defaultRoutes: RouteMap = [ { @@ -96,13 +97,18 @@ const { CHAIN, GOV_TOOLS_URLS } = config(); * @param {number} currentTabId - Tab not to discard (freeze) */ const discardStaleTabs = async (currentTabId: number) => { - const allTabs = await tabs.query({ title: 'Lace' }); - const namiTabs = await tabs.query({ title: POPUP_WINDOW_NAMI_TITLE }); - allTabs.push(...namiTabs); + const allTabs = [ + ...(await catchAndBrandExtensionApiError(tabs.query({ title: 'Lace' }), 'Failed to query for stale lace tabs')), + ...(await catchAndBrandExtensionApiError( + tabs.query({ title: POPUP_WINDOW_NAMI_TITLE }), + 'Failed to query for stale nami mode tabs' + )) + ]; const isLaceOrigin = allTabs.find((tab) => tab.id === currentTabId); if (!isLaceOrigin) return; - allTabs.forEach(async (tab) => { - if (currentTabId !== tab.id) await tabs.discard(tab.id); + allTabs.forEach((tab) => { + if (currentTabId === tab.id) return; + void catchAndBrandExtensionApiError(tabs.discard(tab.id), 'Failed to discard stale tab'); }); }; diff --git a/packages/nami/src/ui/app/pages/dapp-connector/signData.tsx b/packages/nami/src/ui/app/pages/dapp-connector/signData.tsx index f75bf2b317..da0b4832da 100644 --- a/packages/nami/src/ui/app/pages/dapp-connector/signData.tsx +++ b/packages/nami/src/ui/app/pages/dapp-connector/signData.tsx @@ -24,6 +24,7 @@ import { DappConnector, useDappOutsideHandles, } from '../../../../features/dapp-outside-handles-provider'; +import { logger } from '@lace/common'; interface Props { dappConnector: DappConnector; @@ -87,24 +88,29 @@ export const SignData = ({ dappConnector, account }: Readonly) => { } }; + const cancelTransaction = useCallback(async () => { + await request?.reject(() => void 0); + window.close(); + }, [request]); + const loadData = async () => { - const { dappInfo, request } = await dappConnector.getSignDataRequest(); - getPayload(request.data.payload); - getAddress(request.data.address); - setDappInfo(dappInfo); - setRequest(request); - setIsLoading(false); + try { + const { dappInfo, request } = await dappConnector.getSignDataRequest(); + getPayload(request.data.payload); + getAddress(request.data.address); + setDappInfo(dappInfo); + setRequest(request); + setIsLoading(false); + } catch (error) { + logger.error('Failed to get SignData request data', error); + void cancelTransaction(); + } }; React.useEffect(() => { loadData(); }, []); - const cancelTransaction = useCallback(async () => { - await request?.reject(() => void 0); - window.close(); - }, [request]); - useOnUnload(cancelTransaction); return ( diff --git a/packages/nami/src/ui/app/pages/dapp-connector/signTx.tsx b/packages/nami/src/ui/app/pages/dapp-connector/signTx.tsx index a711ec2afe..5bafef0b80 100644 --- a/packages/nami/src/ui/app/pages/dapp-connector/signTx.tsx +++ b/packages/nami/src/ui/app/pages/dapp-connector/signTx.tsx @@ -51,6 +51,7 @@ import type { DappConnector } from '../../../../features/dapp-outside-handles-pr import type { Asset as NamiAsset } from '../../../../types/assets'; import type { AssetsModalRef } from '../../components/assetsModal'; import type { Cardano } from '@cardano-sdk/core'; +import { logger } from '@lace/common'; interface Props { dappConnector: DappConnector; @@ -231,10 +232,29 @@ export const SignTx = ({ } }; + const cancelTransaction = useCallback(async () => { + await request?.reject(() => void 0); + window.close(); + }, [request]); + + useOnUnload(cancelTransaction); + const getInfo = async () => { if (!txWitnessRequest) return; - const { dappInfo, request } = - await dappConnector.getSignTxRequest(txWitnessRequest); + + let signTxRequestData: Awaited< + ReturnType + >; + try { + signTxRequestData = + await dappConnector.getSignTxRequest(txWitnessRequest); + } catch (error) { + logger.error('Failed to get SignTx request data', error); + void cancelTransaction(); + return; + } + + const { dappInfo, request } = signTxRequestData; setRequest(request); setDappInfo(dappInfo); @@ -276,13 +296,6 @@ export const SignTx = ({ getInfo(); }, [txWitnessRequest]); - const cancelTransaction = useCallback(async () => { - await request?.reject(() => void 0); - window.close(); - }, [request]); - - useOnUnload(cancelTransaction); - return ( <> {isLoading.loading ? (