From e843274605d267af2d9e83a9da14ac3820342742 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 19 Mar 2026 18:06:58 +0100 Subject: [PATCH 01/12] fix(multichain-account-service): do not report TimeoutError --- .../src/MultichainAccountService.test.ts | 26 +++++++ .../src/MultichainAccountService.ts | 31 +++++--- .../src/MultichainAccountWallet.test.ts | 74 ++++++++++++++++++- .../src/MultichainAccountWallet.ts | 73 ++++++++++-------- .../src/providers/SnapAccountProvider.test.ts | 43 +++++++++++ .../src/providers/SnapAccountProvider.ts | 56 +++++++++----- .../src/providers/index.ts | 2 +- .../src/providers/utils.test.ts | 27 ++++++- .../src/providers/utils.ts | 12 ++- 9 files changed, 281 insertions(+), 63 deletions(-) diff --git a/packages/multichain-account-service/src/MultichainAccountService.test.ts b/packages/multichain-account-service/src/MultichainAccountService.test.ts index 47b540dc0ae..51ec2fa8c67 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.test.ts @@ -15,6 +15,7 @@ import type { EthKeyring } from '@metamask/keyring-internal-api'; import type { MultichainAccountServiceOptions } from './MultichainAccountService'; import { MultichainAccountService } from './MultichainAccountService'; import type { Bip44AccountProvider } from './providers'; +import { TimeoutError } from './providers'; import { AccountProviderWrapper } from './providers/AccountProviderWrapper'; import { EVM_ACCOUNT_PROVIDER_NAME, @@ -1142,6 +1143,31 @@ describe('MultichainAccountService', () => { providerError, ); }); + + it('does not capture exception when provider throws a TimeoutError', async () => { + const rootMessenger = getRootMessenger(); + const captureExceptionSpy = jest.spyOn(rootMessenger, 'captureException'); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + const { service, mocks } = await setup({ + rootMessenger, + accounts: [MOCK_HD_ACCOUNT_1], + }); + + mocks.SolAccountProvider.resyncAccounts.mockRejectedValue( + new TimeoutError('Timed out after: 500ms'), + ); + + await service.resyncAccounts(); // Should not throw. + + expect(captureExceptionSpy).not.toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); }); describe('setBasicFunctionality', () => { diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts index 874a9cdf8a0..3b8c837029a 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -13,13 +13,14 @@ import type { InternalAccount } from '@metamask/keyring-internal-api'; import { areUint8ArraysEqual, assert } from '@metamask/utils'; import { traceFallback } from './analytics'; -import { projectLogger as log } from './logger'; +import { ERROR_PREFIX, projectLogger as log, WARNING_PREFIX } from './logger'; import type { MultichainAccountGroup } from './MultichainAccountGroup'; import { MultichainAccountWallet } from './MultichainAccountWallet'; import { EvmAccountProviderConfig, Bip44AccountProvider, EVM_ACCOUNT_PROVIDER_NAME, + isTimeoutError, } from './providers'; import { AccountProviderWrapper, @@ -36,7 +37,7 @@ import type { MultichainAccountServiceConfig, MultichainAccountServiceMessenger, } from './types'; -import { createSentryError } from './utils'; +import { createSentryError, toErrorMessage } from './utils'; export const serviceName = 'MultichainAccountService'; @@ -298,14 +299,24 @@ export class MultichainAccountService { try { await provider.resyncAccounts(accounts); } catch (error) { - const errorMessage = `Unable to re-sync provider "${provider.getName()}"`; - log(errorMessage); - console.error(errorMessage); - - const sentryError = createSentryError(errorMessage, error as Error, { - provider: provider.getName(), - }); - this.#messenger.captureException?.(sentryError); + const errorMessage = `Unable to re-sync provider "${provider.getName()}: ${toErrorMessage(error)}"`; + + if (isTimeoutError(error)) { + log(`${WARNING_PREFIX} ${errorMessage}`); + console.warn(errorMessage, error); + } else { + log(`${ERROR_PREFIX} ${errorMessage}`); + console.error(errorMessage, error); + + const sentryError = createSentryError( + errorMessage, + error as Error, + { + provider: provider.getName(), + }, + ); + this.#messenger.captureException?.(sentryError); + } } }), ); diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.test.ts b/packages/multichain-account-service/src/MultichainAccountWallet.test.ts index ff103d4446e..bf49a5be704 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.test.ts @@ -17,6 +17,7 @@ import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { WalletState } from './MultichainAccountWallet'; import { MultichainAccountWallet } from './MultichainAccountWallet'; +import { TimeoutError } from './providers'; import type { MockAccountProvider, RootMessenger } from './tests'; import { MOCK_HD_ACCOUNT_1, @@ -307,6 +308,28 @@ describe('MultichainAccountWallet', () => { ); }); + it('does not capture exception when a provider times out creating accounts', async () => { + const groupIndex = 1; + const { wallet, providers, messenger } = setup({ + accounts: [[MOCK_HD_ACCOUNT_1]], + }); + const [provider] = providers; + provider.createAccounts.mockRejectedValueOnce( + new TimeoutError('Timed out after: 500ms'), + ); + const captureExceptionSpy = jest.spyOn(messenger, 'captureException'); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + await expect( + wallet.createMultichainAccountGroup(groupIndex), + ).rejects.toThrow('Timed out after: 500ms'); + expect(captureExceptionSpy).not.toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + it('defers non-EVM account creation to alignment after group creation (waitForAllProvidersToFinishCreatingAccounts = false)', async () => { const groupIndex = 1; @@ -601,6 +624,31 @@ describe('MultichainAccountWallet', () => { ); }); + it('does not capture exception when a provider times out creating accounts in batch', async () => { + const { wallet, providers, messenger } = setup({ + accounts: [[]], + }); + + const [evmProvider] = providers; + evmProvider.createAccounts.mockRejectedValueOnce( + new TimeoutError('Timed out after: 500ms'), + ); + + const captureExceptionSpy = jest.spyOn(messenger, 'captureException'); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + await expect( + wallet.createMultichainAccountGroups({ to: 2 }), + ).rejects.toThrow('Timed out after: 500ms'); + + expect(captureExceptionSpy).not.toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + it('creates accounts for all providers synchronously when waitForAllProvidersToFinishCreatingAccounts is true', async () => { const { wallet, providers } = setup({ accounts: [[], []], @@ -1062,8 +1110,11 @@ describe('MultichainAccountWallet', () => { // Thrown provider should have been called once and not rescheduled expect(providers[0].discoverAccounts).toHaveBeenCalledTimes(1); - expect(consoleSpy).toHaveBeenCalledWith(expect.any(Error)); - expect((consoleSpy.mock.calls[0][0] as Error).message).toBe( + expect(consoleSpy).toHaveBeenCalledWith( + expect.any(String), + expect.any(Error), + ); + expect((consoleSpy.mock.calls[0][1] as Error).message).toBe( 'Failed to discover accounts', ); @@ -1089,5 +1140,24 @@ describe('MultichainAccountWallet', () => { providerError, ); }); + + it('does not capture exception when a provider times out during account discovery', async () => { + const { wallet, providers, messenger } = setup({ + accounts: [[], []], + }); + providers[0].discoverAccounts.mockRejectedValueOnce( + new TimeoutError('Timed out after: 500ms'), + ); + const captureExceptionSpy = jest.spyOn(messenger, 'captureException'); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + providers[1].discoverAccounts.mockResolvedValueOnce([]); + await wallet.discoverAccounts(); + expect(captureExceptionSpy).not.toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); }); }); diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index d726874c7d7..60d03faef05 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -28,6 +28,7 @@ import type { GroupState } from './MultichainAccountGroup'; import { MultichainAccountGroup } from './MultichainAccountGroup'; import type { ServiceState, StateKeys } from './MultichainAccountService'; import type { Bip44AccountProvider } from './providers'; +import { isTimeoutError } from './providers'; import { EvmAccountProvider } from './providers/EvmAccountProvider'; import type { MultichainAccountServiceMessenger } from './types'; import { @@ -250,22 +251,28 @@ export class MultichainAccountWallet< ? `from group index ${from} to ${to}` : `for group index ${to}`; - const errorMessage = `Unable to create ${modeDescription} ${rangeDescription} with provider "${provider.getName()}". Error: ${(error as Error).message}`; - this.#log(`${ERROR_PREFIX} ${errorMessage}:`, error); + const errorMessage = `Unable to create ${modeDescription} ${rangeDescription} with provider "${provider.getName()}". Error: ${toErrorMessage(error)}`; + if (isTimeoutError(error)) { + this.#log(`${WARNING_PREFIX} ${errorMessage}`); + console.warn(errorMessage, error); + } else { + this.#log(`${ERROR_PREFIX} ${errorMessage}`); + console.error(errorMessage, error); - const sentryError = createSentryError( - `Unable to create ${modeDescription} with provider "${provider.getName()}"`, - error as Error, - { - range: { - from, - to, + const sentryError = createSentryError( + `Unable to create ${modeDescription} with provider "${provider.getName()}"`, + error as Error, + { + range: { + from, + to, + }, + provider: provider.getName(), + isBatching, }, - provider: provider.getName(), - isBatching, - }, - ); - this.#messenger.captureException?.(sentryError); + ); + this.#messenger.captureException?.(sentryError); + } throw error; } } @@ -751,23 +758,29 @@ export class MultichainAccountWallet< }); } catch (error) { context.stopped = true; - console.error(error); - log( - message( - `failed (with: "${(error as Error).message}")`, - targetGroupIndex, - ), - error, - ); - const sentryError = createSentryError( - 'Unable to discover accounts', - error as Error, - { - provider: providerName, - groupIndex: targetGroupIndex, - }, + + const errorMessage = message( + `failed (with: "${(error as Error).message}")`, + targetGroupIndex, ); - this.#messenger.captureException?.(sentryError); + + if (isTimeoutError(error)) { + log(`${WARNING_PREFIX} ${errorMessage}`); + console.warn(error); + } else { + log(`${ERROR_PREFIX} ${errorMessage}`); + console.error(errorMessage, error); + + const sentryError = createSentryError( + 'Unable to discover accounts', + error as Error, + { + provider: providerName, + groupIndex: targetGroupIndex, + }, + ); + this.#messenger.captureException?.(sentryError); + } break; } diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts index cc00eccf594..7fee8df5859 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts @@ -28,6 +28,7 @@ import { } from './SnapAccountProvider'; import { SolAccountProvider } from './SolAccountProvider'; import { TrxAccountProvider } from './TrxAccountProvider'; +import { TimeoutError } from './utils'; import { traceFallback } from '../analytics'; import type { DeepPartial, RootMessenger } from '../tests'; import { @@ -799,6 +800,28 @@ describe('SnapAccountProvider', () => { ); }); + it('does not capture exception when deleteAccount times out', async () => { + const { provider, messenger, mocks } = setup({ + accounts: mockAccounts, + config: { resyncAccounts: { autoRemoveExtraSnapAccounts: true } }, + }); + + const captureExceptionSpy = jest.spyOn(messenger, 'captureException'); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + mocks.SnapController.handleKeyringRequest.deleteAccount.mockRejectedValue( + new TimeoutError('Timed out after: 500ms'), + ); + + await provider.resyncAccounts([mockAccounts[0]]); + + expect(captureExceptionSpy).not.toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + it('does not delete accounts that exist in both Snap and MetaMask', async () => { const { provider, mocks } = setup({ accounts: mockAccounts }); @@ -909,6 +932,26 @@ describe('SnapAccountProvider', () => { providerError, ); }); + + it('does not capture exception when re-sync times out', async () => { + const { provider, messenger } = setup({ accounts: [mockAccounts[0]] }); + + const captureExceptionSpy = jest.spyOn(messenger, 'captureException'); + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + const createAccountsSpy = jest.spyOn(provider, 'createAccounts'); + createAccountsSpy.mockRejectedValue( + new TimeoutError('Timed out after: 500ms'), + ); + + await provider.resyncAccounts(mockAccounts); + + expect(captureExceptionSpy).not.toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + consoleWarnSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); }); describe('ensureCanUseSnapPlatform', () => { diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts index 58a9e2e4407..73d4d9bedc0 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts @@ -22,11 +22,11 @@ import { HandlerType } from '@metamask/snaps-utils'; import { Semaphore } from 'async-mutex'; import { BaseBip44AccountProvider } from './BaseBip44AccountProvider'; -import { withTimeout } from './utils'; +import { isTimeoutError, withTimeout } from './utils'; import { traceFallback } from '../analytics'; -import { projectLogger as log, WARNING_PREFIX } from '../logger'; +import { ERROR_PREFIX, projectLogger as log, WARNING_PREFIX } from '../logger'; import type { MultichainAccountServiceMessenger } from '../types'; -import { createSentryError } from '../utils'; +import { createSentryError, toErrorMessage } from '../utils'; export type RestrictedSnapKeyring = { createAccount: (options: Record) => Promise; @@ -230,12 +230,22 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { snapAccounts.delete(snapAccountId); } } catch (error) { - const sentryError = createSentryError( - `Unable to delete de-synced Snap account: ${this.snapId}`, - error as Error, - { provider: this.getName(), snapAccountId }, - ); - this.messenger.captureException?.(sentryError); + const errorMessage = `Unable to delete de-synced Snap account: ${this.snapId}: ${toErrorMessage(error)}`; + + if (isTimeoutError(error)) { + log(`${WARNING_PREFIX} ${errorMessage}`); + console.warn(errorMessage, error); + } else { + log(`${ERROR_PREFIX} ${errorMessage}`); + console.error(errorMessage, error); + + const sentryError = createSentryError( + `Unable to delete de-synced Snap account: ${this.snapId}`, + error as Error, + { provider: this.getName(), snapAccountId }, + ); + this.messenger.captureException?.(sentryError); + } } }), ); @@ -269,15 +279,25 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { }); } } catch (error) { - const sentryError = createSentryError( - `Unable to re-sync account: ${groupIndex}`, - error as Error, - { - provider: this.getName(), - groupIndex, - }, - ); - this.messenger.captureException?.(sentryError); + const errorMessage = `Unable to re-sync account: ${groupIndex}: ${toErrorMessage(error)}`; + + if (isTimeoutError(error)) { + log(`${WARNING_PREFIX} ${errorMessage}`); + console.warn(errorMessage, error); + } else { + log(`${ERROR_PREFIX} ${errorMessage}`); + console.error(errorMessage, error); + + const sentryError = createSentryError( + `Unable to re-sync account: ${groupIndex}`, + error as Error, + { + provider: this.getName(), + groupIndex, + }, + ); + this.messenger.captureException?.(sentryError); + } } }), ); diff --git a/packages/multichain-account-service/src/providers/index.ts b/packages/multichain-account-service/src/providers/index.ts index f482c41d922..0504f99fea6 100644 --- a/packages/multichain-account-service/src/providers/index.ts +++ b/packages/multichain-account-service/src/providers/index.ts @@ -3,7 +3,7 @@ export * from './SnapAccountProvider'; export * from './AccountProviderWrapper'; // Errors that can bubble up outside of provider calls. -export { TimeoutError } from './utils'; +export { TimeoutError, isTimeoutError } from './utils'; // Concrete providers: export * from './SolAccountProvider'; diff --git a/packages/multichain-account-service/src/providers/utils.test.ts b/packages/multichain-account-service/src/providers/utils.test.ts index 1108ce96d77..cea24ca7854 100644 --- a/packages/multichain-account-service/src/providers/utils.test.ts +++ b/packages/multichain-account-service/src/providers/utils.test.ts @@ -1,4 +1,4 @@ -import { TimeoutError, withRetry, withTimeout } from './utils'; +import { TimeoutError, isTimeoutError, withRetry, withTimeout } from './utils'; describe('utils', () => { it('retries RPC request up to 3 times if it fails and throws the last error', async () => { @@ -33,4 +33,29 @@ describe('utils', () => { ), ).rejects.toThrow(TimeoutError); }); + + it('includes the timeout duration in the error message', async () => { + await expect( + withTimeout( + new Promise((resolve) => { + setTimeout(() => { + resolve(null); + }, 600); + }), + 500, + ), + ).rejects.toThrow('Timed out after: 500ms'); + }); + + it('isTimeoutError returns true for TimeoutError instances', () => { + expect(isTimeoutError(new TimeoutError('Timed out after: 500ms'))).toBe( + true, + ); + }); + + it('isTimeoutError returns false for non-TimeoutError instances', () => { + expect(isTimeoutError(new Error('some error'))).toBe(false); + expect(isTimeoutError('string')).toBe(false); + expect(isTimeoutError(null)).toBe(false); + }); }); diff --git a/packages/multichain-account-service/src/providers/utils.ts b/packages/multichain-account-service/src/providers/utils.ts index 8671da48b8f..6ead4026886 100644 --- a/packages/multichain-account-service/src/providers/utils.ts +++ b/packages/multichain-account-service/src/providers/utils.ts @@ -6,6 +6,16 @@ export class TimeoutError extends Error { } } +/** + * Check if an error is a `TimeoutError`. + * + * @param error - The error to check. + * @returns `true` if the error is a `TimeoutError`, otherwise `false`. + */ +export function isTimeoutError(error: unknown): error is TimeoutError { + return error instanceof TimeoutError; +} + /** * Execute a function with exponential backoff on transient failures. * @@ -58,7 +68,7 @@ export async function withTimeout( promise, new Promise((_resolve, reject) => { timer = setTimeout( - () => reject(new TimeoutError('Timed out')), + () => reject(new TimeoutError(`Timed out after: ${timeoutMs}ms`)), timeoutMs, ); }), From c344076fdbbb2116cf594398cb5e1355ed785e8d Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 19 Mar 2026 18:13:18 +0100 Subject: [PATCH 02/12] chore: cosmetic --- .../multichain-account-service/src/MultichainAccountWallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index 60d03faef05..f4517436902 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -760,7 +760,7 @@ export class MultichainAccountWallet< context.stopped = true; const errorMessage = message( - `failed (with: "${(error as Error).message}")`, + `failed (with: "${toErrorMessage(error)}")`, targetGroupIndex, ); From c1f1a1f6b1fa5115409c7162e24093531f4c8677 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 19 Mar 2026 18:14:26 +0100 Subject: [PATCH 03/12] chore: changelog --- packages/multichain-account-service/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index d45eac14706..b35b4d4b9a6 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Batch account creation with single `keyring.addAccounts` call. - Fetch all accounts in single `AccountsController:getAccounts` call instead of multiple `getAccount` calls. - Significantly reduces lock acquisitions and API calls for batch operations. +- Do not report `TimeoutError` errors ([#8249](https://github.com/MetaMask/core/pull/8249)) + - All other kind of errors are still reported as usual. ### Removed From d78a71c5d55e79a15c137546a65b28bee6badbe2 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 19 Mar 2026 18:19:49 +0100 Subject: [PATCH 04/12] fix: fix misplaced quote --- .../multichain-account-service/src/MultichainAccountService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts index 3b8c837029a..b9328a25abd 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -299,7 +299,7 @@ export class MultichainAccountService { try { await provider.resyncAccounts(accounts); } catch (error) { - const errorMessage = `Unable to re-sync provider "${provider.getName()}: ${toErrorMessage(error)}"`; + const errorMessage = `Unable to re-sync provider "${provider.getName()}": ${toErrorMessage(error)}"`; if (isTimeoutError(error)) { log(`${WARNING_PREFIX} ${errorMessage}`); From a5a493645526e245662b079bedc97bf27e8ae124 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 19 Mar 2026 18:20:04 +0100 Subject: [PATCH 05/12] fix: fix invalid warn --- .../multichain-account-service/src/MultichainAccountWallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index f4517436902..a609950ab2b 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -766,7 +766,7 @@ export class MultichainAccountWallet< if (isTimeoutError(error)) { log(`${WARNING_PREFIX} ${errorMessage}`); - console.warn(error); + console.warn(errorMessage, error); } else { log(`${ERROR_PREFIX} ${errorMessage}`); console.error(errorMessage, error); From 5a6e6de95326f7bb585b79f1c4e8c8b8c976dbc2 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 19 Mar 2026 18:45:12 +0100 Subject: [PATCH 06/12] fix: use static messages --- .../src/MultichainAccountService.ts | 6 +-- .../src/MultichainAccountWallet.ts | 44 +++++++++++-------- .../src/providers/SnapAccountProvider.ts | 24 ++++++---- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts index b9328a25abd..6c2ca530488 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -299,13 +299,13 @@ export class MultichainAccountService { try { await provider.resyncAccounts(accounts); } catch (error) { - const errorMessage = `Unable to re-sync provider "${provider.getName()}": ${toErrorMessage(error)}"`; + const errorMessage = `Unable to re-sync provider "${provider.getName()}"`; if (isTimeoutError(error)) { - log(`${WARNING_PREFIX} ${errorMessage}`); + log(`${WARNING_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`); console.warn(errorMessage, error); } else { - log(`${ERROR_PREFIX} ${errorMessage}`); + log(`${ERROR_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`); console.error(errorMessage, error); const sentryError = createSentryError( diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index a609950ab2b..ed27aa713be 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -251,26 +251,26 @@ export class MultichainAccountWallet< ? `from group index ${from} to ${to}` : `for group index ${to}`; - const errorMessage = `Unable to create ${modeDescription} ${rangeDescription} with provider "${provider.getName()}". Error: ${toErrorMessage(error)}`; + const sentryMessage = `Unable to create ${modeDescription} with provider "${provider.getName()}"`; + const errorMessage = `Unable to create ${modeDescription} ${rangeDescription} with provider "${provider.getName()}"`; + if (isTimeoutError(error)) { - this.#log(`${WARNING_PREFIX} ${errorMessage}`); + this.#log( + `${WARNING_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, + ); console.warn(errorMessage, error); } else { - this.#log(`${ERROR_PREFIX} ${errorMessage}`); + this.#log(`${ERROR_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`); console.error(errorMessage, error); - const sentryError = createSentryError( - `Unable to create ${modeDescription} with provider "${provider.getName()}"`, - error as Error, - { - range: { - from, - to, - }, - provider: provider.getName(), - isBatching, + const sentryError = createSentryError(sentryMessage, error as Error, { + range: { + from, + to, }, - ); + provider: provider.getName(), + isBatching, + }); this.#messenger.captureException?.(sentryError); } throw error; @@ -759,16 +759,22 @@ export class MultichainAccountWallet< } catch (error) { context.stopped = true; - const errorMessage = message( - `failed (with: "${toErrorMessage(error)}")`, - targetGroupIndex, + log( + message( + `failed (with: "${toErrorMessage(error)}")`, + targetGroupIndex, + ), ); + const errorMessage = `Unable to discover accounts with provider "${providerName}" for group index: ${targetGroupIndex}`; + if (isTimeoutError(error)) { - log(`${WARNING_PREFIX} ${errorMessage}`); + log( + `${WARNING_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, + ); console.warn(errorMessage, error); } else { - log(`${ERROR_PREFIX} ${errorMessage}`); + log(`${ERROR_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`); console.error(errorMessage, error); const sentryError = createSentryError( diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts index 73d4d9bedc0..49e16d43a95 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts @@ -230,17 +230,21 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { snapAccounts.delete(snapAccountId); } } catch (error) { - const errorMessage = `Unable to delete de-synced Snap account: ${this.snapId}: ${toErrorMessage(error)}`; + const errorMessage = `Unable to delete de-synced Snap account: ${this.snapId}`; if (isTimeoutError(error)) { - log(`${WARNING_PREFIX} ${errorMessage}`); + log( + `${WARNING_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, + ); console.warn(errorMessage, error); } else { - log(`${ERROR_PREFIX} ${errorMessage}`); + log( + `${ERROR_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, + ); console.error(errorMessage, error); const sentryError = createSentryError( - `Unable to delete de-synced Snap account: ${this.snapId}`, + errorMessage, error as Error, { provider: this.getName(), snapAccountId }, ); @@ -279,17 +283,21 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { }); } } catch (error) { - const errorMessage = `Unable to re-sync account: ${groupIndex}: ${toErrorMessage(error)}`; + const errorMessage = `Unable to re-sync account: ${groupIndex}`; if (isTimeoutError(error)) { - log(`${WARNING_PREFIX} ${errorMessage}`); + log( + `${WARNING_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, + ); console.warn(errorMessage, error); } else { - log(`${ERROR_PREFIX} ${errorMessage}`); + log( + `${ERROR_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, + ); console.error(errorMessage, error); const sentryError = createSentryError( - `Unable to re-sync account: ${groupIndex}`, + errorMessage, error as Error, { provider: this.getName(), From 7078978d9de761574f5114b48ab5bbe1b2cc4c72 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 19 Mar 2026 21:16:17 +0100 Subject: [PATCH 07/12] refactor: use more static messages --- .../src/MultichainAccountWallet.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index ed27aa713be..395cd2e3d8e 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -251,19 +251,20 @@ export class MultichainAccountWallet< ? `from group index ${from} to ${to}` : `for group index ${to}`; - const sentryMessage = `Unable to create ${modeDescription} with provider "${provider.getName()}"`; - const errorMessage = `Unable to create ${modeDescription} ${rangeDescription} with provider "${provider.getName()}"`; + const errorMessage = `Unable to create ${modeDescription} with provider "${provider.getName()}"`; if (isTimeoutError(error)) { this.#log( - `${WARNING_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, + `${WARNING_PREFIX} ${errorMessage} (${rangeDescription}): ${toErrorMessage(error)}`, ); console.warn(errorMessage, error); } else { - this.#log(`${ERROR_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`); + this.#log( + `${ERROR_PREFIX} ${errorMessage} (${rangeDescription}): ${toErrorMessage(error)}`, + ); console.error(errorMessage, error); - const sentryError = createSentryError(sentryMessage, error as Error, { + const sentryError = createSentryError(errorMessage, error as Error, { range: { from, to, @@ -766,7 +767,7 @@ export class MultichainAccountWallet< ), ); - const errorMessage = `Unable to discover accounts with provider "${providerName}" for group index: ${targetGroupIndex}`; + const errorMessage = `Unable to discover accounts with provider "${providerName}"`; if (isTimeoutError(error)) { log( From dc547e20c7dc2d8d092639006884ec184b7fbe91 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 19 Mar 2026 23:11:21 +0100 Subject: [PATCH 08/12] refactor: fix errorMessage inconsistency --- .../multichain-account-service/src/MultichainAccountWallet.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index 395cd2e3d8e..c2e447a8187 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -779,7 +779,7 @@ export class MultichainAccountWallet< console.error(errorMessage, error); const sentryError = createSentryError( - 'Unable to discover accounts', + errorMessage, error as Error, { provider: providerName, From 2fafc00f4f49698089aba59d7b90073c6c808b51 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 19 Mar 2026 23:16:00 +0100 Subject: [PATCH 09/12] test: fix test --- .../src/MultichainAccountWallet.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.test.ts b/packages/multichain-account-service/src/MultichainAccountWallet.test.ts index bf49a5be704..a2ca44a9b77 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.test.ts @@ -1133,7 +1133,9 @@ describe('MultichainAccountWallet', () => { providers[1].discoverAccounts.mockResolvedValueOnce([]); await wallet.discoverAccounts(); expect(captureExceptionSpy).toHaveBeenCalledWith( - new Error('Unable to discover accounts'), + new Error( + 'Unable to discover accounts with provider "Mocked Provider 0"', + ), ); expect(captureExceptionSpy.mock.lastCall[0]).toHaveProperty( 'cause', From 09ab178d56d2721cf7d205c64988ba3cc95c7dbc Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 20 Mar 2026 10:26:18 +0100 Subject: [PATCH 10/12] refactor: introduce logErrorAs --- .../src/MultichainAccountService.ts | 8 ++++---- .../src/MultichainAccountWallet.ts | 15 +++++--------- .../multichain-account-service/src/logger.ts | 18 +++++++++++++++++ .../src/providers/SnapAccountProvider.ts | 20 ++++++------------- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts index 6c2ca530488..784d2505df7 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -13,7 +13,7 @@ import type { InternalAccount } from '@metamask/keyring-internal-api'; import { areUint8ArraysEqual, assert } from '@metamask/utils'; import { traceFallback } from './analytics'; -import { ERROR_PREFIX, projectLogger as log, WARNING_PREFIX } from './logger'; +import { logErrorAs, projectLogger as log } from './logger'; import type { MultichainAccountGroup } from './MultichainAccountGroup'; import { MultichainAccountWallet } from './MultichainAccountWallet'; import { @@ -37,7 +37,7 @@ import type { MultichainAccountServiceConfig, MultichainAccountServiceMessenger, } from './types'; -import { createSentryError, toErrorMessage } from './utils'; +import { createSentryError } from './utils'; export const serviceName = 'MultichainAccountService'; @@ -302,10 +302,10 @@ export class MultichainAccountService { const errorMessage = `Unable to re-sync provider "${provider.getName()}"`; if (isTimeoutError(error)) { - log(`${WARNING_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`); + logErrorAs('warn', errorMessage, error); console.warn(errorMessage, error); } else { - log(`${ERROR_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`); + logErrorAs('error', errorMessage, error); console.error(errorMessage, error); const sentryError = createSentryError( diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index c2e447a8187..3cacccb410d 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -21,6 +21,7 @@ import type { Logger } from './logger'; import { createModuleLogger, ERROR_PREFIX, + logErrorAs, projectLogger as log, WARNING_PREFIX, } from './logger'; @@ -254,14 +255,10 @@ export class MultichainAccountWallet< const errorMessage = `Unable to create ${modeDescription} with provider "${provider.getName()}"`; if (isTimeoutError(error)) { - this.#log( - `${WARNING_PREFIX} ${errorMessage} (${rangeDescription}): ${toErrorMessage(error)}`, - ); + logErrorAs('warn', `${errorMessage} (${rangeDescription})`, error); console.warn(errorMessage, error); } else { - this.#log( - `${ERROR_PREFIX} ${errorMessage} (${rangeDescription}): ${toErrorMessage(error)}`, - ); + logErrorAs('error', `${errorMessage} (${rangeDescription})`, error); console.error(errorMessage, error); const sentryError = createSentryError(errorMessage, error as Error, { @@ -770,12 +767,10 @@ export class MultichainAccountWallet< const errorMessage = `Unable to discover accounts with provider "${providerName}"`; if (isTimeoutError(error)) { - log( - `${WARNING_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, - ); + logErrorAs('warn', errorMessage, error); console.warn(errorMessage, error); } else { - log(`${ERROR_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`); + logErrorAs('error', errorMessage, error); console.error(errorMessage, error); const sentryError = createSentryError( diff --git a/packages/multichain-account-service/src/logger.ts b/packages/multichain-account-service/src/logger.ts index 03e92506d50..76dc8f6592f 100644 --- a/packages/multichain-account-service/src/logger.ts +++ b/packages/multichain-account-service/src/logger.ts @@ -1,5 +1,7 @@ import { createProjectLogger, createModuleLogger } from '@metamask/utils'; +import { toErrorMessage } from './utils'; + export const projectLogger = createProjectLogger('multichain-account-service'); export { createModuleLogger }; @@ -8,3 +10,19 @@ export const WARNING_PREFIX = 'WARNING --'; export const ERROR_PREFIX = 'ERROR --'; export type Logger = typeof projectLogger; + +/** + * Logs an error with either WARNING or ERROR prefix, appending the error message. + * + * @param level - 'warn' for WARNING prefix, 'error' for ERROR prefix. + * @param message - The static message describing what failed. + * @param error - The caught error. + */ +export function logErrorAs( + level: 'warn' | 'error', + message: string, + error: unknown, +): void { + const prefix = level === 'warn' ? WARNING_PREFIX : ERROR_PREFIX; + projectLogger(`${prefix} ${message}: ${toErrorMessage(error)}`); +} diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts index 49e16d43a95..fdcf20a0d74 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts @@ -24,9 +24,9 @@ import { Semaphore } from 'async-mutex'; import { BaseBip44AccountProvider } from './BaseBip44AccountProvider'; import { isTimeoutError, withTimeout } from './utils'; import { traceFallback } from '../analytics'; -import { ERROR_PREFIX, projectLogger as log, WARNING_PREFIX } from '../logger'; +import { logErrorAs, projectLogger as log, WARNING_PREFIX } from '../logger'; import type { MultichainAccountServiceMessenger } from '../types'; -import { createSentryError, toErrorMessage } from '../utils'; +import { createSentryError } from '../utils'; export type RestrictedSnapKeyring = { createAccount: (options: Record) => Promise; @@ -233,14 +233,10 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { const errorMessage = `Unable to delete de-synced Snap account: ${this.snapId}`; if (isTimeoutError(error)) { - log( - `${WARNING_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, - ); + logErrorAs('warn', errorMessage, error); console.warn(errorMessage, error); } else { - log( - `${ERROR_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, - ); + logErrorAs('error', errorMessage, error); console.error(errorMessage, error); const sentryError = createSentryError( @@ -286,14 +282,10 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { const errorMessage = `Unable to re-sync account: ${groupIndex}`; if (isTimeoutError(error)) { - log( - `${WARNING_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, - ); + logErrorAs('warn', errorMessage, error); console.warn(errorMessage, error); } else { - log( - `${ERROR_PREFIX} ${errorMessage}: ${toErrorMessage(error)}`, - ); + logErrorAs('error', errorMessage, error); console.error(errorMessage, error); const sentryError = createSentryError( From 1194f3888f214450418d481497a9cd58b210521c Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 20 Mar 2026 11:30:15 +0100 Subject: [PATCH 11/12] refactor: use more static messages --- .../src/providers/SnapAccountProvider.test.ts | 2 +- .../src/providers/SnapAccountProvider.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts index 7fee8df5859..05721d34d57 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts @@ -925,7 +925,7 @@ describe('SnapAccountProvider', () => { expect(createAccountsSpy).toHaveBeenCalled(); expect(captureExceptionSpy).toHaveBeenCalledWith( - new Error('Unable to re-sync account: 0'), + new Error('Unable to re-sync accounts'), ); expect(captureExceptionSpy.mock.lastCall[0]).toHaveProperty( 'cause', diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts index fdcf20a0d74..e8e3e6d6e97 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts @@ -279,7 +279,7 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { }); } } catch (error) { - const errorMessage = `Unable to re-sync account: ${groupIndex}`; + const errorMessage = 'Unable to re-sync accounts'; if (isTimeoutError(error)) { logErrorAs('warn', errorMessage, error); From 663d9b7605337a8352de9dfec2ebdb17dc198653 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 20 Mar 2026 13:27:38 +0100 Subject: [PATCH 12/12] refactor: add new reportError helper --- .../src/MultichainAccountService.ts | 31 +++------ .../src/MultichainAccountWallet.ts | 64 ++++++------------- .../multichain-account-service/src/errors.ts | 33 ++++++++++ .../src/providers/SnapAccountProvider.ts | 54 +++++----------- 4 files changed, 77 insertions(+), 105 deletions(-) create mode 100644 packages/multichain-account-service/src/errors.ts diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts index 784d2505df7..5cf9ae3ea00 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -13,14 +13,14 @@ import type { InternalAccount } from '@metamask/keyring-internal-api'; import { areUint8ArraysEqual, assert } from '@metamask/utils'; import { traceFallback } from './analytics'; -import { logErrorAs, projectLogger as log } from './logger'; +import { reportError } from './errors'; +import { projectLogger as log } from './logger'; import type { MultichainAccountGroup } from './MultichainAccountGroup'; import { MultichainAccountWallet } from './MultichainAccountWallet'; import { EvmAccountProviderConfig, Bip44AccountProvider, EVM_ACCOUNT_PROVIDER_NAME, - isTimeoutError, } from './providers'; import { AccountProviderWrapper, @@ -37,7 +37,6 @@ import type { MultichainAccountServiceConfig, MultichainAccountServiceMessenger, } from './types'; -import { createSentryError } from './utils'; export const serviceName = 'MultichainAccountService'; @@ -299,24 +298,14 @@ export class MultichainAccountService { try { await provider.resyncAccounts(accounts); } catch (error) { - const errorMessage = `Unable to re-sync provider "${provider.getName()}"`; - - if (isTimeoutError(error)) { - logErrorAs('warn', errorMessage, error); - console.warn(errorMessage, error); - } else { - logErrorAs('error', errorMessage, error); - console.error(errorMessage, error); - - const sentryError = createSentryError( - errorMessage, - error as Error, - { - provider: provider.getName(), - }, - ); - this.#messenger.captureException?.(sentryError); - } + reportError( + this.#messenger, + `Unable to re-sync provider "${provider.getName()}"`, + error, + { + provider: provider.getName(), + }, + ); } }), ); diff --git a/packages/multichain-account-service/src/MultichainAccountWallet.ts b/packages/multichain-account-service/src/MultichainAccountWallet.ts index 3cacccb410d..eb71ee60b23 100644 --- a/packages/multichain-account-service/src/MultichainAccountWallet.ts +++ b/packages/multichain-account-service/src/MultichainAccountWallet.ts @@ -17,11 +17,11 @@ import type { EntropySourceId, KeyringAccount } from '@metamask/keyring-api'; import { assert } from '@metamask/utils'; import { Mutex } from 'async-mutex'; +import { reportError } from './errors'; import type { Logger } from './logger'; import { createModuleLogger, ERROR_PREFIX, - logErrorAs, projectLogger as log, WARNING_PREFIX, } from './logger'; @@ -29,13 +29,11 @@ import type { GroupState } from './MultichainAccountGroup'; import { MultichainAccountGroup } from './MultichainAccountGroup'; import type { ServiceState, StateKeys } from './MultichainAccountService'; import type { Bip44AccountProvider } from './providers'; -import { isTimeoutError } from './providers'; import { EvmAccountProvider } from './providers/EvmAccountProvider'; import type { MultichainAccountServiceMessenger } from './types'; import { assertGroupIndexIsValid, assertGroupIndexRangeIsValid, - createSentryError, GroupIndexRange, toErrorMessage, } from './utils'; @@ -245,32 +243,16 @@ export class MultichainAccountWallet< }, }); } catch (error) { - const modeDescription = isBatching - ? 'some accounts (batch)' - : 'some accounts'; - const rangeDescription = isBatching - ? `from group index ${from} to ${to}` - : `for group index ${to}`; - - const errorMessage = `Unable to create ${modeDescription} with provider "${provider.getName()}"`; - - if (isTimeoutError(error)) { - logErrorAs('warn', `${errorMessage} (${rangeDescription})`, error); - console.warn(errorMessage, error); - } else { - logErrorAs('error', `${errorMessage} (${rangeDescription})`, error); - console.error(errorMessage, error); - - const sentryError = createSentryError(errorMessage, error as Error, { - range: { - from, - to, - }, + reportError( + this.#messenger, + `Unable to create ${isBatching ? 'some accounts (batch)' : 'some accounts'} with provider "${provider.getName()}"`, + error, + { + range: { from, to }, provider: provider.getName(), isBatching, - }); - this.#messenger.captureException?.(sentryError); - } + }, + ); throw error; } } @@ -764,25 +746,15 @@ export class MultichainAccountWallet< ), ); - const errorMessage = `Unable to discover accounts with provider "${providerName}"`; - - if (isTimeoutError(error)) { - logErrorAs('warn', errorMessage, error); - console.warn(errorMessage, error); - } else { - logErrorAs('error', errorMessage, error); - console.error(errorMessage, error); - - const sentryError = createSentryError( - errorMessage, - error as Error, - { - provider: providerName, - groupIndex: targetGroupIndex, - }, - ); - this.#messenger.captureException?.(sentryError); - } + reportError( + this.#messenger, + `Unable to discover accounts with provider "${providerName}"`, + error, + { + provider: providerName, + groupIndex: targetGroupIndex, + }, + ); break; } diff --git a/packages/multichain-account-service/src/errors.ts b/packages/multichain-account-service/src/errors.ts new file mode 100644 index 00000000000..3a35d6a70b4 --- /dev/null +++ b/packages/multichain-account-service/src/errors.ts @@ -0,0 +1,33 @@ +import { logErrorAs } from './logger'; +import { isTimeoutError } from './providers/utils'; +import { createSentryError } from './utils'; + +/** + * Reports an error by logging it and optionally capturing it in Sentry. + * + * Timeout errors are treated as warnings (not reported to Sentry). All other + * errors are logged as errors and captured via `captureException`. + * + * @param messenger - Object with an optional `captureException` method. + * @param messenger.captureException - Optional method to capture exceptions in Sentry. + * @param message - The static message describing what failed. + * @param error - The caught error. + * @param context - Optional context to attach to the Sentry error. + */ +export function reportError( + messenger: { captureException?: (error: Error) => void }, + message: string, + error: unknown, + context?: Record, +): void { + if (isTimeoutError(error)) { + logErrorAs('warn', message, error); + console.warn(message, error); + } else { + logErrorAs('error', message, error); + console.error(message, error); + + const sentryError = createSentryError(message, error as Error, context); + messenger.captureException?.(sentryError); + } +} diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts index e8e3e6d6e97..87b0fc80fc7 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts @@ -22,11 +22,11 @@ import { HandlerType } from '@metamask/snaps-utils'; import { Semaphore } from 'async-mutex'; import { BaseBip44AccountProvider } from './BaseBip44AccountProvider'; -import { isTimeoutError, withTimeout } from './utils'; +import { withTimeout } from './utils'; import { traceFallback } from '../analytics'; -import { logErrorAs, projectLogger as log, WARNING_PREFIX } from '../logger'; +import { reportError } from '../errors'; +import { projectLogger as log, WARNING_PREFIX } from '../logger'; import type { MultichainAccountServiceMessenger } from '../types'; -import { createSentryError } from '../utils'; export type RestrictedSnapKeyring = { createAccount: (options: Record) => Promise; @@ -230,22 +230,15 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { snapAccounts.delete(snapAccountId); } } catch (error) { - const errorMessage = `Unable to delete de-synced Snap account: ${this.snapId}`; - - if (isTimeoutError(error)) { - logErrorAs('warn', errorMessage, error); - console.warn(errorMessage, error); - } else { - logErrorAs('error', errorMessage, error); - console.error(errorMessage, error); - - const sentryError = createSentryError( - errorMessage, - error as Error, - { provider: this.getName(), snapAccountId }, - ); - this.messenger.captureException?.(sentryError); - } + reportError( + this.messenger, + `Unable to delete de-synced Snap account: ${this.snapId}`, + error, + { + provider: this.getName(), + snapAccountId, + }, + ); } }), ); @@ -279,25 +272,10 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { }); } } catch (error) { - const errorMessage = 'Unable to re-sync accounts'; - - if (isTimeoutError(error)) { - logErrorAs('warn', errorMessage, error); - console.warn(errorMessage, error); - } else { - logErrorAs('error', errorMessage, error); - console.error(errorMessage, error); - - const sentryError = createSentryError( - errorMessage, - error as Error, - { - provider: this.getName(), - groupIndex, - }, - ); - this.messenger.captureException?.(sentryError); - } + reportError(this.messenger, 'Unable to re-sync accounts', error, { + provider: this.getName(), + groupIndex, + }); } }), );