diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 380ef1dae..bf14f99f3 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -860,14 +860,6 @@ }, "@typescript-eslint/no-shadow": { "count": 1 - }, - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "packages/solana-wallet-snap/src/core/utils/errors.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 1 } }, "packages/solana-wallet-snap/src/core/utils/formatCrypto.test.ts": { diff --git a/packages/snap-networks-utils/CHANGELOG.md b/packages/snap-networks-utils/CHANGELOG.md index 80837ab97..1e17788f6 100644 --- a/packages/snap-networks-utils/CHANGELOG.md +++ b/packages/snap-networks-utils/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add helpers `serialize`, `deserialize`, and `Serializable` for round-tripping `BigNumber`, `bigint`, `Uint8Array`, and `undefined` through snap state ([#197](https://github.com/MetaMask/internal-snaps/pull/197)) +- Add shared snap error utilities to the main package entry point ([#241](https://github.com/MetaMask/internal-snaps/pull/241)) + - `createWithCatchAndThrowSnapError` for handler-boundary error catching, logging, and Snap RPC normalization + - `normalizeError` for converting caught values into Snap RPC errors, with optional custom normalizers via `createWithCatchAndThrowSnapError`'s `normalizeErrorFn` option + - `isSnapRpcError` type guard and `SnapRpcError` union type - Add `InFlightCoalescer`, exported from a new `./dedupe` entry point, which coalesces concurrent async operations by key so callers share one in-flight run ([#149](https://github.com/MetaMask/internal-snaps/pull/149)) - Add shared async batching utilities. ([#211](https://github.com/MetaMask/internal-snaps/pull/211)) - Add origin permission helpers ([#193](https://github.com/MetaMask/internal-snaps/pull/193)) diff --git a/packages/snap-networks-utils/jest.config.js b/packages/snap-networks-utils/jest.config.js index ca0841333..f198bc8f9 100644 --- a/packages/snap-networks-utils/jest.config.js +++ b/packages/snap-networks-utils/jest.config.js @@ -14,6 +14,11 @@ module.exports = merge(baseConfig, { // The display name when running multiple projects displayName, + coveragePathIgnorePatterns: [ + ...(baseConfig.coveragePathIgnorePatterns ?? []), + '.*/__mocks__/', + ], + // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { diff --git a/packages/snap-networks-utils/src/index.ts b/packages/snap-networks-utils/src/index.ts index 3406f3960..f075b9746 100644 --- a/packages/snap-networks-utils/src/index.ts +++ b/packages/snap-networks-utils/src/index.ts @@ -41,3 +41,15 @@ export { } from './utils/originPermissions/createOriginPermissions'; export type { CreateOriginPermissionsParams } from './utils/originPermissions/createOriginPermissions'; export { validateOrigin } from './utils/originPermissions/validateOrigin'; +export { + createWithCatchAndThrowSnapError, + isSnapRpcError, + normalizeError, +} from './utils/errors'; +export type { + CreateWithCatchAndThrowSnapErrorOptions, + LogErrorFn, + NormalizeErrorFn, + SnapRpcError, + TrackErrorFn, +} from './utils/errors'; diff --git a/packages/snap-networks-utils/src/utils/errors/errors.test.ts b/packages/snap-networks-utils/src/utils/errors/errors.test.ts new file mode 100644 index 000000000..0f4a42c6c --- /dev/null +++ b/packages/snap-networks-utils/src/utils/errors/errors.test.ts @@ -0,0 +1,219 @@ +import { + ChainDisconnectedError, + DisconnectedError, + InternalError, + InvalidInputError, + InvalidParamsError, + InvalidRequestError, + LimitExceededError, + MethodNotFoundError, + MethodNotSupportedError, + ParseError, + ResourceNotFoundError, + ResourceUnavailableError, + SnapError, + TransactionRejected, + UnauthorizedError, + UnsupportedMethodError, + UserRejectedRequestError, +} from '@metamask/snaps-sdk'; + +import { mockLogger } from '../logger/__mocks__/Logger'; +import { createWithCatchAndThrowSnapError, normalizeError } from './errors'; +import type { CreateWithCatchAndThrowSnapErrorOptions } from './errors'; +import { isSnapRpcError } from './snapRpcError'; + +type SetupTestResult = { + trackError: jest.Mock; + withCatchAndThrowSnapError: ReturnType< + typeof createWithCatchAndThrowSnapError + >; + createBoundWithCatchAndThrowSnapError: ( + options?: Omit, + ) => ReturnType; +}; + +const setupTest = (): SetupTestResult => { + jest.clearAllMocks(); + + const trackError = jest.fn(); + const withCatchAndThrowSnapError = createWithCatchAndThrowSnapError({ + logError: mockLogger.error.bind(mockLogger), + trackError, + }); + + return { + trackError, + withCatchAndThrowSnapError, + createBoundWithCatchAndThrowSnapError: ( + options: Omit = { + trackError, + }, + ): ReturnType => + createWithCatchAndThrowSnapError({ + ...options, + logError: mockLogger.error.bind(mockLogger), + }), + }; +}; + +describe('errors', () => { + describe('isSnapRpcError', () => { + it.each([ + new SnapError('Test error'), + new MethodNotFoundError(), + new UserRejectedRequestError(), + new MethodNotSupportedError(), + new ParseError(), + new ResourceNotFoundError(), + new ResourceUnavailableError(), + new TransactionRejected(), + new ChainDisconnectedError(), + new DisconnectedError(), + new UnauthorizedError(), + new UnsupportedMethodError(), + new InternalError(), + new InvalidInputError(), + new InvalidParamsError(), + new InvalidRequestError(), + new LimitExceededError(), + ])('returns true for Snap RPC errors', (error) => { + expect(isSnapRpcError(error)).toBe(true); + }); + + it('returns false for generic errors', () => { + expect(isSnapRpcError(new Error('Unexpected error'))).toBe(false); + }); + + it('returns false for non-error values', () => { + expect(isSnapRpcError('string')).toBe(false); + expect(isSnapRpcError(null)).toBe(false); + }); + }); + + describe('normalizeError', () => { + it('preserves Snap RPC errors without wrapping', () => { + const originalError = new UserRejectedRequestError(); + + expect(normalizeError(originalError)).toBe(originalError); + }); + + it('wraps generic errors in SnapError', () => { + const originalError = new Error('Test error'); + + const normalized = normalizeError(originalError); + + expect(normalized).toBeInstanceOf(SnapError); + expect(normalized.message).toBe('Test error'); + }); + + it('wraps non-Error values in SnapError', () => { + const normalized = normalizeError('string error'); + + expect(normalized).toBeInstanceOf(SnapError); + expect(normalized.message).toBe('string error'); + }); + }); + + describe('createWithCatchAndThrowSnapError', () => { + it('returns the result when the function succeeds', async () => { + const { withCatchAndThrowSnapError } = setupTest(); + const mockFn = jest.fn().mockResolvedValue('success'); + + const result = await withCatchAndThrowSnapError(mockFn); + + expect(result).toBe('success'); + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + it('tracks, logs, and re-throws errors as SnapError', async () => { + const { trackError, withCatchAndThrowSnapError } = setupTest(); + const originalError = new Error('Test error'); + const mockFn = jest.fn().mockRejectedValue(originalError); + + await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( + SnapError, + ); + + expect(trackError).toHaveBeenCalledWith(originalError); + expect(mockLogger.error).toHaveBeenCalledTimes(1); + }); + + it('preserves Snap RPC errors without wrapping', async () => { + const { trackError, withCatchAndThrowSnapError } = setupTest(); + const originalError = new UserRejectedRequestError(); + const mockFn = jest.fn().mockRejectedValue(originalError); + + await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( + UserRejectedRequestError, + ); + + expect(trackError).toHaveBeenCalledWith(originalError); + }); + + it('handles non-Error objects and converts them to SnapError', async () => { + const { withCatchAndThrowSnapError } = setupTest(); + const mockFn = jest.fn().mockRejectedValue('string error'); + + await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( + SnapError, + ); + + expect(mockLogger.error).toHaveBeenCalledTimes(1); + }); + + it('handles null errors', async () => { + const { withCatchAndThrowSnapError } = setupTest(); + const mockFn = jest.fn().mockRejectedValue(null); + + await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( + SnapError, + ); + + expect(mockLogger.error).toHaveBeenCalledTimes(1); + }); + + it('preserves the original error message in the SnapError', async () => { + const { withCatchAndThrowSnapError } = setupTest(); + const originalError = new Error('Custom error message'); + const mockFn = jest.fn().mockRejectedValue(originalError); + + await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( + 'Custom error message', + ); + }); + + it('uses a custom normalizeErrorFn when provided', async () => { + const { trackError, createBoundWithCatchAndThrowSnapError } = setupTest(); + const customError = new MethodNotFoundError(); + const normalizeErrorFn = jest.fn().mockReturnValue(customError); + const bound = createBoundWithCatchAndThrowSnapError({ + trackError, + normalizeErrorFn, + }); + const mockFn = jest.fn().mockRejectedValue(new Error('Test error')); + + await expect(bound(mockFn)).rejects.toThrow(MethodNotFoundError); + + expect(normalizeErrorFn).toHaveBeenCalledWith( + expect.objectContaining({ message: 'Test error' }), + ); + }); + + it('uses a custom logError when provided', async () => { + const { trackError, withCatchAndThrowSnapError } = setupTest(); + const customLogError = jest.fn(); + const originalError = new Error('Test error'); + const mockFn = jest.fn().mockRejectedValue(originalError); + + await expect( + withCatchAndThrowSnapError(mockFn, customLogError), + ).rejects.toThrow(SnapError); + + expect(customLogError).toHaveBeenCalledTimes(1); + expect(mockLogger.error).not.toHaveBeenCalled(); + expect(trackError).toHaveBeenCalledWith(originalError); + }); + }); +}); diff --git a/packages/snap-networks-utils/src/utils/errors/errors.ts b/packages/snap-networks-utils/src/utils/errors/errors.ts new file mode 100644 index 000000000..7a32c0373 --- /dev/null +++ b/packages/snap-networks-utils/src/utils/errors/errors.ts @@ -0,0 +1,90 @@ +import { SnapError, getErrorMessage } from '@metamask/snaps-sdk'; + +import type { Logger } from '../logger/Logger'; +import { isSnapRpcError } from './snapRpcError'; +import type { SnapRpcError } from './snapRpcError'; + +/** + * Sends an error to the snap's tracking transport (e.g. `snap_trackError`). + * Whether to invoke this for a given error in a given context is the caller's decision. + */ +export type TrackErrorFn = (error: unknown) => Promise; + +/** + * Converts a caught value into an error suitable for Snap RPC responses. + */ +export type NormalizeErrorFn = (error: unknown) => SnapRpcError; + +/** + * Normalizes an unknown caught value into a Snap RPC error. + * + * Preserves existing Snap RPC errors; otherwise wraps the value in {@link SnapError}. + * + * @param error - The caught value. + * @returns A Snap RPC error. + */ +export function normalizeError(error: unknown): SnapRpcError { + return isSnapRpcError(error) + ? error + : new SnapError(error instanceof Error ? error : getErrorMessage(error)); +} + +export type LogErrorFn = Logger['error']; + +export type CreateWithCatchAndThrowSnapErrorOptions = { + logError: LogErrorFn; + trackError: TrackErrorFn; + normalizeErrorFn?: NormalizeErrorFn; +}; + +/** + * Creates a handler-boundary error wrapper wired with logger, tracking, and optional error normalization. + * + * @param options - Logger, error-tracking transport, and optional custom normalizer. + * @param options.logError - Logger method used to record the normalized error. + * @param options.trackError - Snap-specific Sentry transport. + * @param options.normalizeErrorFn - Optional error normalizer; defaults to {@link normalizeError}. + * @returns A function that catches errors, tracks them, logs, and rethrows as Snap RPC errors. + */ +export function createWithCatchAndThrowSnapError({ + logError, + trackError, + normalizeErrorFn = normalizeError, +}: CreateWithCatchAndThrowSnapErrorOptions): ( + fn: () => Promise, + logErrorOverride?: LogErrorFn, +) => Promise { + return ( + fn: () => Promise, + logErrorOverride?: LogErrorFn, + ): Promise => + withCatchAndThrowSnapErrorHandler( + logErrorOverride ?? logError, + trackError, + normalizeErrorFn, + fn, + ); +} + +async function withCatchAndThrowSnapErrorHandler( + logError: LogErrorFn, + trackError: TrackErrorFn, + normalizeErrorFn: NormalizeErrorFn, + fn: () => Promise, +): Promise { + try { + return await fn(); + } catch (unknownError) { + await trackError(unknownError); + + const error = normalizeErrorFn(unknownError); + + logError( + { error }, + `[SnapError] ${JSON.stringify(error.toJSON(), null, 2)}`, + ); + + // eslint-disable-next-line @typescript-eslint/only-throw-error -- Snap RPC errors are the handler boundary surface + throw error; + } +} diff --git a/packages/snap-networks-utils/src/utils/errors/index.ts b/packages/snap-networks-utils/src/utils/errors/index.ts new file mode 100644 index 000000000..582480c9b --- /dev/null +++ b/packages/snap-networks-utils/src/utils/errors/index.ts @@ -0,0 +1,9 @@ +export { createWithCatchAndThrowSnapError, normalizeError } from './errors'; +export { isSnapRpcError } from './snapRpcError'; +export type { + CreateWithCatchAndThrowSnapErrorOptions, + LogErrorFn, + NormalizeErrorFn, + TrackErrorFn, +} from './errors'; +export type { SnapRpcError } from './snapRpcError'; diff --git a/packages/snap-networks-utils/src/utils/errors/snapRpcError.ts b/packages/snap-networks-utils/src/utils/errors/snapRpcError.ts new file mode 100644 index 000000000..72b321d67 --- /dev/null +++ b/packages/snap-networks-utils/src/utils/errors/snapRpcError.ts @@ -0,0 +1,69 @@ +import { + MethodNotFoundError, + ParseError, + ResourceNotFoundError, + ResourceUnavailableError, + ChainDisconnectedError, + TransactionRejected, + DisconnectedError, + InternalError, + UnauthorizedError, + UnsupportedMethodError, + InvalidInputError, + InvalidParamsError, + InvalidRequestError, + LimitExceededError, + SnapError, + MethodNotSupportedError, + UserRejectedRequestError, +} from '@metamask/snaps-sdk'; + +/** Union of Snap RPC error instance types (for type narrowing). */ +export type SnapRpcError = + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType + | InstanceType; + +const SNAP_RPC_ERROR_TYPES = [ + SnapError, + MethodNotFoundError, + UserRejectedRequestError, + MethodNotSupportedError, + ParseError, + ResourceNotFoundError, + ResourceUnavailableError, + TransactionRejected, + ChainDisconnectedError, + DisconnectedError, + UnauthorizedError, + UnsupportedMethodError, + InternalError, + InvalidInputError, + InvalidParamsError, + InvalidRequestError, + LimitExceededError, +] as const; + +/** + * Determines if the given error is a Snap RPC error. + * + * @param error - The error instance to be checked. + * @returns A boolean indicating whether the error is a Snap RPC error. + */ +export function isSnapRpcError(error: Error | unknown): error is SnapRpcError { + return SNAP_RPC_ERROR_TYPES.some((errType) => error instanceof errType); +} diff --git a/packages/snap-networks-utils/src/utils/logger/__mocks__/Logger.ts b/packages/snap-networks-utils/src/utils/logger/__mocks__/Logger.ts new file mode 100644 index 000000000..567d97372 --- /dev/null +++ b/packages/snap-networks-utils/src/utils/logger/__mocks__/Logger.ts @@ -0,0 +1,26 @@ +import type { Logger } from '../Logger'; + +export const mockLogger = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + withPrefix: (prefix: string): Logger => createPrefixedLogger([prefix]), +} as unknown as jest.Mocked; + +function createPrefixedLogger(prefixes: string[]): Logger { + return new Proxy(mockLogger, { + get(target, property: keyof Logger): unknown { + if (property === 'withPrefix') { + return (prefix: string) => createPrefixedLogger([...prefixes, prefix]); + } + + const method = target[property]; + return typeof method === 'function' + ? (...args: unknown[]): unknown => method(...prefixes, ...args) + : method; + }, + }); +} diff --git a/packages/solana-wallet-snap/snap.manifest.json b/packages/solana-wallet-snap/snap.manifest.json index 8a5f96c45..6e0428938 100644 --- a/packages/solana-wallet-snap/snap.manifest.json +++ b/packages/solana-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "V3iOdFzSye/NKphktjgWLlyZ6npSWHr+dgR0tkVtJvU=", + "shasum": "yxd4HvExG2dvJVH5uajNfTzGfaybRnywIRZk8orqZqo=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/solana-wallet-snap/src/core/utils/errors.test.ts b/packages/solana-wallet-snap/src/core/utils/errors.test.ts index 5de61b203..af5267209 100644 --- a/packages/solana-wallet-snap/src/core/utils/errors.test.ts +++ b/packages/solana-wallet-snap/src/core/utils/errors.test.ts @@ -4,7 +4,6 @@ import { SnapError, UserRejectedRequestError } from '@metamask/snaps-sdk'; import { trackError, withCatchAndThrowSnapError } from './errors'; import logger from './logger'; -// Mock the logger to avoid actual console output during tests jest.mock('./logger', () => ({ error: jest.fn(), warn: jest.fn(), @@ -23,6 +22,14 @@ const setupTest = () => { describe('errors', () => { describe('trackError', () => { + it('does not track UserRejectedRequestError', async () => { + const { mockSnapRequest } = setupTest(); + + expect(await trackError(new UserRejectedRequestError())).toBeUndefined(); + + expect(mockSnapRequest).not.toHaveBeenCalled(); + }); + it('does not throw if error tracking fails', async () => { const { mockLogger, mockSnapRequest } = setupTest(); @@ -68,7 +75,7 @@ describe('errors', () => { }); }); - describe('handle', () => { + describe('withCatchAndThrowSnapError', () => { it('returns the result when the function succeeds', async () => { const { mockLogger } = setupTest(); @@ -81,184 +88,18 @@ describe('errors', () => { expect(mockLogger.error).not.toHaveBeenCalled(); }); - it('handles and re-throws errors as SnapError', async () => { - const { mockLogger } = setupTest(); - - const originalError = new Error('Test error'); - const mockFn = jest.fn().mockRejectedValue(originalError); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, - ); - - expect(mockFn).toHaveBeenCalledTimes(1); - expect(mockLogger.error).toHaveBeenCalledTimes(1); - }); - - it('logs errors with the correct scope and error details', async () => { - const { mockLogger } = setupTest(); - - const originalError = new Error('Test error'); - const mockFn = jest.fn().mockRejectedValue(originalError); - - try { - await withCatchAndThrowSnapError(mockFn); - } catch (error) { - // Expected to throw - } - - expect(mockLogger.error).toHaveBeenCalledWith( - { error: expect.any(SnapError) }, - expect.stringContaining(`[SnapError]`), - ); - - expect(mockLogger.error).toHaveBeenCalledTimes(1); - const logCall = mockLogger.error.mock.calls[0]; - const loggedError = (logCall?.[0] as { error?: Error } | undefined) - ?.error; - expect(loggedError).toBeInstanceOf(SnapError); - }); - - it('handles non-Error objects and converts them to SnapError', async () => { - const { mockLogger } = setupTest(); - - const nonErrorValue = 'string error'; - const mockFn = jest.fn().mockRejectedValue(nonErrorValue); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, - ); - - expect(mockLogger.error).toHaveBeenCalledTimes(1); - const logCall = mockLogger.error.mock.calls[0]; - const loggedError = (logCall?.[0] as { error?: Error } | undefined) - ?.error; - expect(loggedError).toBeInstanceOf(SnapError); - }); - - it('handles null and undefined errors', async () => { - const { mockLogger } = setupTest(); - - const mockFn = jest.fn().mockRejectedValue(null); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, - ); - - expect(mockLogger.error).toHaveBeenCalledTimes(1); - }); - - it('preserves the original error message in the SnapError', async () => { - const originalError = new Error('Custom error message'); - const mockFn = jest.fn().mockRejectedValue(originalError); - - let caughtError: unknown; - try { - await withCatchAndThrowSnapError(mockFn); - } catch (error) { - caughtError = error; - } - - expect(caughtError).toBeInstanceOf(SnapError); - const snapError = caughtError as SnapError; - expect(snapError.message).toBe('Custom error message'); - }); - - it('handles async functions that return different types', async () => { - const { mockLogger } = setupTest(); - - const testCases = [ - { value: 42, type: 'number' }, - { value: { key: 'value' }, type: 'object' }, - { value: [1, 2, 3], type: 'array' }, - { value: true, type: 'boolean' }, - { value: null, type: 'null' }, - ]; - - for (const testCase of testCases) { - const mockFn = jest.fn().mockResolvedValue(testCase.value); - - const result = await withCatchAndThrowSnapError(mockFn); - - expect(result).toBe(testCase.value); - expect(mockLogger.error).not.toHaveBeenCalled(); - } - }); - - it('handles functions that throw different error types', async () => { - const { mockLogger } = setupTest(); - - const errorTypes = [ - new TypeError('Type error'), - new ReferenceError('Reference error'), - new RangeError('Range error'), - new SyntaxError('Syntax error'), - ]; - - for (const errorType of errorTypes) { - const mockFn = jest.fn().mockRejectedValue(errorType); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, - ); - } - - expect(mockLogger.error).toHaveBeenCalledTimes(errorTypes.length); - const logCalls = mockLogger.error.mock.calls; - expect(logCalls).toHaveLength(errorTypes.length); - - for (let i = 0; i < errorTypes.length; i++) { - const logCall = logCalls[i]; - const loggedError = (logCall?.[0] as { error?: Error } | undefined) - ?.error; - expect(loggedError).toBeInstanceOf(SnapError); - expect(loggedError?.message).toBe(errorTypes[i]?.message); - } - }); - - it('includes error stack trace in the logged error', async () => { - const { mockLogger } = setupTest(); + it('tracks and re-throws errors as SnapError', async () => { + const { mockLogger, mockSnapRequest } = setupTest(); + mockSnapRequest.mockResolvedValue('tracked-error-id'); const originalError = new Error('Test error'); - originalError.stack = 'Error: Test error\n at test.js:1:1'; const mockFn = jest.fn().mockRejectedValue(originalError); - try { - await withCatchAndThrowSnapError(mockFn); - } catch (error) { - // Expected to throw - } - - expect(mockLogger.error).toHaveBeenCalledWith( - { error: expect.any(SnapError) }, - expect.stringContaining('[SnapError]'), - ); - }); - - it('handles functions that throw promises', async () => { - const { mockLogger } = setupTest(); - - const rejectedPromise = Promise.reject(new Error('Promise error')); - const mockFn = jest.fn().mockImplementation(async () => rejectedPromise); - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( SnapError, ); - expect(mockLogger.error).toHaveBeenCalledTimes(1); - }); - - it('does not track errors when shouldTrackError returns false', async () => { - const { mockLogger, mockSnapRequest } = setupTest(); - - const originalError = new UserRejectedRequestError(); - const mockFn = jest.fn().mockRejectedValue(originalError); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - UserRejectedRequestError, - ); - - expect(mockSnapRequest).not.toHaveBeenCalled(); + expect(mockSnapRequest).toHaveBeenCalledTimes(1); expect(mockLogger.error).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/solana-wallet-snap/src/core/utils/errors.ts b/packages/solana-wallet-snap/src/core/utils/errors.ts index c9810aed7..b4c1bb94c 100644 --- a/packages/solana-wallet-snap/src/core/utils/errors.ts +++ b/packages/solana-wallet-snap/src/core/utils/errors.ts @@ -1,59 +1,30 @@ import { - MethodNotFoundError, - ParseError, - ResourceNotFoundError, - ResourceUnavailableError, - ChainDisconnectedError, - TransactionRejected, - DisconnectedError, - InternalError, - UnauthorizedError, - UnsupportedMethodError, - InvalidInputError, - InvalidParamsError, - InvalidRequestError, - LimitExceededError, - SnapError, - MethodNotSupportedError, - UserRejectedRequestError, - getJsonError, -} from '@metamask/snaps-sdk'; + createWithCatchAndThrowSnapError, + isSnapRpcError, +} from '@metamask/snap-networks-utils'; +import { UserRejectedRequestError, getJsonError } from '@metamask/snaps-sdk'; import logger from './logger'; +export { isSnapRpcError }; +export type { SnapRpcError } from '@metamask/snap-networks-utils'; + /** - * Determines if the given error is a Snap RPC error. + * Reports an error to MetaMask via Sentry (`snap_trackError`). + * + * Skips user rejections. Callers decide whether to invoke this for a given + * error in a given context. * - * @param error - The error instance to be checked. - * @returns A boolean indicating whether the error is a Snap RPC error. + * @param error - The error to report. + * @returns The Sentry event ID on success, or `undefined` on failure or if the error is skipped. */ -export function isSnapRpcError(error: Error): boolean { - const errors = [ - SnapError, - MethodNotFoundError, - UserRejectedRequestError, - MethodNotSupportedError, - MethodNotFoundError, - ParseError, - ResourceNotFoundError, - ResourceUnavailableError, - TransactionRejected, - ChainDisconnectedError, - DisconnectedError, - UnauthorizedError, - UnsupportedMethodError, - InternalError, - InvalidInputError, - InvalidParamsError, - InvalidRequestError, - LimitExceededError, - ]; - return errors.some((errType) => error instanceof errType); -} - export const trackError = async ( error: unknown, ): Promise => { + if (error instanceof UserRejectedRequestError) { + return undefined; + } + try { return await snap.request({ method: 'snap_trackError', @@ -67,29 +38,7 @@ export const trackError = async ( } }; -const shouldTrackError = (error: Error): boolean => { - return !(error instanceof UserRejectedRequestError); -}; - -export const withCatchAndThrowSnapError = async ( - fn: () => Promise, -): Promise => { - try { - return await fn(); - } catch (errorInstance: any) { - if (shouldTrackError(errorInstance)) { - await trackError(errorInstance); - } - - const error = isSnapRpcError(errorInstance) - ? errorInstance - : new SnapError(errorInstance); - - logger.error( - { error }, - `[SnapError] ${JSON.stringify(error.toJSON(), null, 2)}`, - ); - - throw error; - } -}; +export const withCatchAndThrowSnapError = createWithCatchAndThrowSnapError({ + logError: logger.error.bind(logger), + trackError, +}); diff --git a/packages/stellar-wallet-snap/snap.manifest.json b/packages/stellar-wallet-snap/snap.manifest.json index d8bce9f7e..51bc71cdc 100644 --- a/packages/stellar-wallet-snap/snap.manifest.json +++ b/packages/stellar-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "mTg3/L83Phw10/v+VhquQ5alsyAdbGVhEHTaNTzT1xA=", + "shasum": "QMvShnvrFA65DF39Qs4iC0ZYywBT31wqdBMy/uvBm60=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/stellar-wallet-snap/src/handlers/clientRequest/clientRequest.ts b/packages/stellar-wallet-snap/src/handlers/clientRequest/clientRequest.ts index f03c766ec..fb0be93e3 100644 --- a/packages/stellar-wallet-snap/src/handlers/clientRequest/clientRequest.ts +++ b/packages/stellar-wallet-snap/src/handlers/clientRequest/clientRequest.ts @@ -38,7 +38,7 @@ export class ClientRequestHandler { const result = (await withCatchAndThrowSnapError(async () => { return this.#handleClientRequest(request); - }, this.#logger)) ?? null; + }, this.#logger.error.bind(this.#logger))) ?? null; return result; } diff --git a/packages/stellar-wallet-snap/src/handlers/clientRequest/confirmSend.test.ts b/packages/stellar-wallet-snap/src/handlers/clientRequest/confirmSend.test.ts index b29e69394..08bf5b910 100644 --- a/packages/stellar-wallet-snap/src/handlers/clientRequest/confirmSend.test.ts +++ b/packages/stellar-wallet-snap/src/handlers/clientRequest/confirmSend.test.ts @@ -49,7 +49,6 @@ import { WalletService } from '../../services/wallet'; import { getTestWallet } from '../../services/wallet/__mocks__/wallet.fixtures'; import { ConfirmationInterfaceKey } from '../../ui/confirmation/api'; import { ConfirmationUXController } from '../../ui/confirmation/controller'; -import * as errorUtils from '../../utils/errors'; import { logger } from '../../utils/logger'; import * as snapUtils from '../../utils/snap'; import { AccountResolver } from '../accountResolver'; @@ -543,30 +542,30 @@ describe('ConfirmSendHandler', () => { 'Invalid transfer function arguments', ); createValidatedSendTransaction.mockRejectedValueOnce(xdrParseError); - const trackErrorIfNeededSpy = jest - .spyOn(errorUtils, 'trackErrorIfNeeded') + const trackErrorSpy = jest + .spyOn(snapUtils, 'trackError') .mockResolvedValue(undefined); expect(await handler.handle(baseRequest())).toStrictEqual({ valid: false, errors: [{ code: MultiChainSendErrorCodes.Invalid }], }); - expect(trackErrorIfNeededSpy).toHaveBeenCalledWith(xdrParseError); + expect(trackErrorSpy).toHaveBeenCalledWith(xdrParseError); }); it('returns invalid for unexpected errors from createValidatedSendTransaction', async () => { const { handler, createValidatedSendTransaction } = setup(); const unexpectedError = new Error('unexpected'); createValidatedSendTransaction.mockRejectedValueOnce(unexpectedError); - const trackErrorIfNeededSpy = jest - .spyOn(errorUtils, 'trackErrorIfNeeded') + const trackErrorSpy = jest + .spyOn(snapUtils, 'trackError') .mockResolvedValue(undefined); expect(await handler.handle(baseRequest())).toStrictEqual({ valid: false, errors: [{ code: MultiChainSendErrorCodes.Invalid }], }); - expect(trackErrorIfNeededSpy).toHaveBeenCalledWith(unexpectedError); + expect(trackErrorSpy).toHaveBeenCalledWith(unexpectedError); }); it('does not track expected validation errors from createValidatedSendTransaction', async () => { @@ -574,15 +573,15 @@ describe('ConfirmSendHandler', () => { createValidatedSendTransaction.mockRejectedValueOnce( new TransactionValidationException('x'), ); - const trackErrorIfNeededSpy = jest - .spyOn(errorUtils, 'trackErrorIfNeeded') + const trackErrorSpy = jest + .spyOn(snapUtils, 'trackError') .mockResolvedValue(undefined); expect(await handler.handle(baseRequest())).toStrictEqual({ valid: false, errors: [{ code: MultiChainSendErrorCodes.Invalid }], }); - expect(trackErrorIfNeededSpy).not.toHaveBeenCalled(); + expect(trackErrorSpy).not.toHaveBeenCalled(); }); it('continues successfully when saving pending transaction fails', async () => { diff --git a/packages/stellar-wallet-snap/src/handlers/clientRequest/confirmSend.ts b/packages/stellar-wallet-snap/src/handlers/clientRequest/confirmSend.ts index ac7506462..620cfd60a 100644 --- a/packages/stellar-wallet-snap/src/handlers/clientRequest/confirmSend.ts +++ b/packages/stellar-wallet-snap/src/handlers/clientRequest/confirmSend.ts @@ -30,7 +30,7 @@ import { hasDecimals, isSlip44Id, toSmallestUnit, - trackErrorIfNeeded, + trackError, trackTransactionAdded, trackTransactionApproved, trackTransactionRejected, @@ -245,7 +245,7 @@ export class ConfirmSendHandler extends BaseClientRequestHandler< } // Unexpected errors are swallowed into `{ valid: false }`, so track them for debugging. - await trackErrorIfNeeded(error); + await trackError(error); this.#logger.warn( 'Failed to confirm send transaction due to unexpected issue', diff --git a/packages/stellar-wallet-snap/src/handlers/clientRequest/signAndSendTransaction.ts b/packages/stellar-wallet-snap/src/handlers/clientRequest/signAndSendTransaction.ts index e3f4358be..a86eda50b 100644 --- a/packages/stellar-wallet-snap/src/handlers/clientRequest/signAndSendTransaction.ts +++ b/packages/stellar-wallet-snap/src/handlers/clientRequest/signAndSendTransaction.ts @@ -27,7 +27,7 @@ import { isSlip44Id, parseClassicAssetCodeIssuer, removeTrailingZeros, - trackErrorIfNeeded, + trackError, } from '../../utils'; import { trackTransactionSubmitted } from '../../utils/snap'; import type { @@ -252,7 +252,7 @@ export class SignAndSendTransactionHandler extends BaseClientRequestHandler< }); } } catch (error) { - await trackErrorIfNeeded(error); + await trackError(error); this.logger.warn('Failed to map a transaction for swap and bridge send', { error, }); diff --git a/packages/stellar-wallet-snap/src/handlers/cronjob/trackTransaction.ts b/packages/stellar-wallet-snap/src/handlers/cronjob/trackTransaction.ts index 2b56a1b8c..cf0ddbee6 100644 --- a/packages/stellar-wallet-snap/src/handlers/cronjob/trackTransaction.ts +++ b/packages/stellar-wallet-snap/src/handlers/cronjob/trackTransaction.ts @@ -11,7 +11,7 @@ import { import type { NetworkService } from '../../services/network'; import type { SynchronizeService } from '../../services/sync/SynchronizeService'; import { isCompletedTransactionStatus } from '../../services/transaction/utils'; -import { trackErrorIfNeeded } from '../../utils'; +import { trackError } from '../../utils'; import { Duration, scheduleBackgroundEvent, @@ -129,7 +129,7 @@ export class TrackTransactionHandler extends CronjobBaseHandler { const setupTest = () => { jest.mocked(emitSnapKeyringEvent).mockResolvedValue(undefined); - jest.spyOn(errorUtils, 'trackErrorIfNeeded').mockResolvedValue(undefined); + jest.spyOn(snapUtils, 'trackError').mockResolvedValue(undefined); }; const buildActivatedAccountPair = ( @@ -974,7 +974,7 @@ describe('OnChainAccountSynchronizeService', () => { mockSep41Assets, ); - expect(errorUtils.trackErrorIfNeeded).toHaveBeenCalledWith( + expect(snapUtils.trackError).toHaveBeenCalledWith( expect.objectContaining({ message: 'sep41 fetch temporarily unavailable', }), @@ -1144,7 +1144,7 @@ describe('OnChainAccountSynchronizeService', () => { ); expect(saveManySpy).toHaveBeenCalled(); - expect(errorUtils.trackErrorIfNeeded).toHaveBeenCalledWith(emitError); + expect(snapUtils.trackError).toHaveBeenCalledWith(emitError); expect(logger.warn).toHaveBeenCalledWith( '[💼 OnChainAccountSynchronizeService]', 'Failed to emit keyring events after synchronize', diff --git a/packages/stellar-wallet-snap/src/services/on-chain-account/OnChainAccountSynchronizeService.ts b/packages/stellar-wallet-snap/src/services/on-chain-account/OnChainAccountSynchronizeService.ts index d438fa584..6598055c2 100644 --- a/packages/stellar-wallet-snap/src/services/on-chain-account/OnChainAccountSynchronizeService.ts +++ b/packages/stellar-wallet-snap/src/services/on-chain-account/OnChainAccountSynchronizeService.ts @@ -15,7 +15,7 @@ import { isClassicAssetId, isSep41Id, isSlip44Id, - trackErrorIfNeeded, + trackError, } from '../../utils'; import type { StellarAssetMetadata } from '../asset-metadata'; import type { NetworkService } from '../network'; @@ -109,7 +109,7 @@ export class OnChainAccountSynchronizeService { sep41Assets, }); } catch (error: unknown) { - await trackErrorIfNeeded(error); + await trackError(error); this.#logger.warn( 'SEP-41 token balance step failed; merge will reuse last-saved SEP-41 asset entries where needed', { error }, @@ -534,7 +534,7 @@ export class OnChainAccountSynchronizeService { ); } } catch (error: unknown) { - await trackErrorIfNeeded(error); + await trackError(error); this.#logger.warn('Failed to emit keyring events after synchronize', { error, diff --git a/packages/stellar-wallet-snap/src/services/price/PriceService.ts b/packages/stellar-wallet-snap/src/services/price/PriceService.ts index 585f6e1b2..efb1bde9d 100644 --- a/packages/stellar-wallet-snap/src/services/price/PriceService.ts +++ b/packages/stellar-wallet-snap/src/services/price/PriceService.ts @@ -10,7 +10,7 @@ import { BigNumber } from 'bignumber.js'; import { pick } from 'lodash'; import { AppConfig } from '../../config'; -import { getFiatTicker, isFiat, trackErrorIfNeeded } from '../../utils'; +import { getFiatTicker, isFiat, trackError } from '../../utils'; import type { ICache } from '../cache'; import { useCache } from '../cache'; import { GET_HISTORICAL_PRICES_RESPONSE_NULL_OBJECT } from './api'; @@ -121,7 +121,7 @@ export class PriceService { : await this.#cache.mget(uniqueAssetTypes.map(toCacheKey)); } catch (error) { this.#logger.warn('Error fetching cached spot prices', error); - await trackErrorIfNeeded(error); + await trackError(error); } const cachedSpotPricesByAssetId: Partial = {}; @@ -167,7 +167,7 @@ export class PriceService { ); } catch (error) { this.#logger.warn('Error caching spot prices', error); - await trackErrorIfNeeded(error); + await trackError(error); } return { @@ -280,7 +280,7 @@ export class PriceService { response, }; } catch (error) { - await trackErrorIfNeeded(error); + await trackError(error); // Gracefully handle individual errors to avoid breaking the entire operation this.#logger.warn( `Error fetching historical prices for ${from} to ${to} with time period ${timePeriod}. Returning null object.`, diff --git a/packages/stellar-wallet-snap/src/services/sync/SynchronizeService.ts b/packages/stellar-wallet-snap/src/services/sync/SynchronizeService.ts index 66d274e90..5d959b1b3 100644 --- a/packages/stellar-wallet-snap/src/services/sync/SynchronizeService.ts +++ b/packages/stellar-wallet-snap/src/services/sync/SynchronizeService.ts @@ -4,11 +4,7 @@ import { E_ALREADY_LOCKED, Mutex, tryAcquire } from 'async-mutex'; import type { KnownCaip2ChainId } from '../../api'; import { AppConfig } from '../../config'; import { BackgroundEventMethod } from '../../handlers/cronjob/api'; -import { - Duration, - scheduleBackgroundEvent, - trackErrorIfNeeded, -} from '../../utils'; +import { Duration, scheduleBackgroundEvent, trackError } from '../../utils'; import type { StellarKeyringAccount } from '../account'; import type { AssetMetadataService, @@ -137,7 +133,7 @@ export class SynchronizeService { for (const [index, result] of results.entries()) { if (result.status === 'rejected') { - await trackErrorIfNeeded(result.reason); + await trackError(result.reason); const taskName = tasks[index]?.name ?? 'synchronize'; this.#logger.warn(`Failed to ${taskName}`, { @@ -159,7 +155,7 @@ export class SynchronizeService { } this.#logger.error('Synchronize failed', { error }); - await trackErrorIfNeeded(error); + await trackError(error); } } @@ -209,7 +205,7 @@ export class SynchronizeService { try { await this.#assetMetadataService.synchronize(scope); } catch (error: unknown) { - await trackErrorIfNeeded(error); + await trackError(error); this.#logger.warn('Failed to synchronize assets', { error, @@ -229,7 +225,7 @@ export class SynchronizeService { try { return await this.#assetMetadataService.fetchSep41AssetsOrSyncOnce(scope); } catch (error: unknown) { - await trackErrorIfNeeded(error); + await trackError(error); this.#logger.warn('Failed to load SEP-41 assets', { error, @@ -269,7 +265,7 @@ export class SynchronizeService { // Only capture the error if it is unexpected. // AccountNotActivatedException is expected when the account is not activated yet. if (!(error instanceof AccountNotActivatedException)) { - await trackErrorIfNeeded(error); + await trackError(error); this.#logger.warn('Failed to load account for sync', { error, diff --git a/packages/stellar-wallet-snap/src/services/transaction-scan/TransactionScanService.ts b/packages/stellar-wallet-snap/src/services/transaction-scan/TransactionScanService.ts index c45ad85cc..375424ab3 100644 --- a/packages/stellar-wallet-snap/src/services/transaction-scan/TransactionScanService.ts +++ b/packages/stellar-wallet-snap/src/services/transaction-scan/TransactionScanService.ts @@ -6,7 +6,7 @@ import { xlmIcon } from '../../ui/images'; import { toCaip19ClassicAssetId, toCaip19Sep41AssetId, - trackErrorIfNeeded, + trackError, } from '../../utils'; import { toDisplayBalance } from '../../utils/currency'; import { getIconUrl } from '../asset-metadata/utils'; @@ -87,7 +87,7 @@ export class TransactionScanService { reason: error, }); - await trackErrorIfNeeded(error); + await trackError(error); return null; } diff --git a/packages/stellar-wallet-snap/src/services/transaction/TransactionService.ts b/packages/stellar-wallet-snap/src/services/transaction/TransactionService.ts index 0cb75d3f2..bb9383ee1 100644 --- a/packages/stellar-wallet-snap/src/services/transaction/TransactionService.ts +++ b/packages/stellar-wallet-snap/src/services/transaction/TransactionService.ts @@ -8,7 +8,7 @@ import type { KnownCaip19Slip44Id, KnownCaip2ChainId, } from '../../api'; -import { isSep41Id, isSlip44Id, trackErrorIfNeeded } from '../../utils'; +import { isSep41Id, isSlip44Id, trackError } from '../../utils'; import type { AccountService } from '../account'; import type { StellarAssetMetadata } from '../asset-metadata'; import type { NetworkService } from '../network'; @@ -492,7 +492,7 @@ export class TransactionService { try { return await this.savePendingKeyringTransaction(request); } catch (error: unknown) { - await trackErrorIfNeeded(error); + await trackError(error); this.#logger.warn('Failed to save pending transaction', { error }); return null; diff --git a/packages/stellar-wallet-snap/src/services/transaction/TransactionSynchronizeService.ts b/packages/stellar-wallet-snap/src/services/transaction/TransactionSynchronizeService.ts index 258bde51e..3a185e4fa 100644 --- a/packages/stellar-wallet-snap/src/services/transaction/TransactionSynchronizeService.ts +++ b/packages/stellar-wallet-snap/src/services/transaction/TransactionSynchronizeService.ts @@ -18,7 +18,7 @@ import { isSameStr, isSep41Id, pushToRecordArray, - trackErrorIfNeeded, + trackError, } from '../../utils'; import type { AccountService, @@ -278,7 +278,7 @@ export class TransactionSynchronizeService { fetchResult.reason, ); - await trackErrorIfNeeded(fetchResult.reason); + await trackError(fetchResult.reason); continue; } @@ -357,7 +357,7 @@ export class TransactionSynchronizeService { error: fetchResult.reason, }); - await trackErrorIfNeeded(fetchResult.reason); + await trackError(fetchResult.reason); continue; } diff --git a/packages/stellar-wallet-snap/src/services/transaction/simulation/simulators.ts b/packages/stellar-wallet-snap/src/services/transaction/simulation/simulators.ts index 4f3a41f03..49ee6e21e 100644 --- a/packages/stellar-wallet-snap/src/services/transaction/simulation/simulators.ts +++ b/packages/stellar-wallet-snap/src/services/transaction/simulation/simulators.ts @@ -632,7 +632,7 @@ export class InvokeHostFunctionOPSimulator implements OperationSimulator { // Let XdrParseException propagate as-is: it signals an internal XDR/ScVal // parsing failure, not a user-facing validation outcome like insufficient - // balance. confirmSend tracks these via trackErrorIfNeeded; onAmountInput returns + // balance. confirmSend tracks these via trackError; onAmountInput returns // invalid without tracking to reduce Sentry noise during amount entry. const parsed = parseSep41TransferInvoke(op, scope); diff --git a/packages/stellar-wallet-snap/src/utils/errors.test.ts b/packages/stellar-wallet-snap/src/utils/errors.test.ts index 59ad5f8e0..5cd8e2eee 100644 --- a/packages/stellar-wallet-snap/src/utils/errors.test.ts +++ b/packages/stellar-wallet-snap/src/utils/errors.test.ts @@ -1,43 +1,9 @@ -import { - ChainDisconnectedError, - DisconnectedError, - InternalError, - InvalidInputError, - InvalidParamsError, - InvalidRequestError, - LimitExceededError, - MethodNotFoundError, - MethodNotSupportedError, - ParseError, - ResourceNotFoundError, - ResourceUnavailableError, - SnapError, - TransactionRejected, - UnauthorizedError, - UnsupportedMethodError, - UserRejectedRequestError, -} from '@metamask/snaps-sdk'; - import { InvalidHttpRequestParamsException, - isSnapRpcError, rethrowIfInstanceElseThrow, - trackErrorIfNeeded, - withCatchAndThrowSnapError, } from './errors'; -import { logger } from './logger'; -import * as snapUtils from './snap'; - -jest.mock('./logger'); -jest.mock('./snap'); describe('errors', () => { - const mockLogger = logger as jest.Mocked; - - beforeEach(() => { - jest.clearAllMocks(); - }); - describe('rethrowIfInstanceElseThrow', () => { class SampleDomainError extends Error { constructor(message: string) { @@ -128,223 +94,11 @@ describe('errors', () => { }); }); - describe('withCatchAndThrowSnapError', () => { - it('returns the result when the function succeeds', async () => { - const mockFn = jest.fn().mockResolvedValue('success'); - - const result = await withCatchAndThrowSnapError(mockFn); - - expect(result).toBe('success'); - expect(mockFn).toHaveBeenCalledTimes(1); - expect(mockLogger.error).not.toHaveBeenCalled(); - }); - - it('handles and re-throws errors as SnapError', async () => { - const originalError = new Error('Test error'); - const mockFn = jest.fn().mockRejectedValue(originalError); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, - ); - - expect(mockFn).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledTimes(1); - }); - - it('logs errors with the correct scope and error details', async () => { - const originalError = new Error('Test error'); - const mockFn = jest.fn().mockRejectedValue(originalError); - - try { - await withCatchAndThrowSnapError(mockFn); - } catch { - // Expected to throw - } - - expect(mockLogger.error).toHaveBeenCalledWith( - { error: expect.any(SnapError) }, - expect.stringContaining(`[SnapError]`), - ); - - expect(mockLogger.error).toHaveBeenCalledTimes(1); - const logCall = mockLogger.error.mock.calls[0]; - const loggedError = logCall?.[0] as { error: SnapError }; - expect(loggedError.error).toBeInstanceOf(SnapError); - }); - - it('handles non-Error objects and converts them to SnapError', async () => { - const nonErrorValue = 'string error'; - const mockFn = jest.fn().mockRejectedValue(nonErrorValue); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, - ); - - expect(mockLogger.error).toHaveBeenCalledTimes(1); - const logCall = mockLogger.error.mock.calls[0]; - const loggedError = logCall?.[0] as { error: SnapError }; - expect(loggedError.error).toBeInstanceOf(SnapError); - }); - - it('handles null and undefined errors', async () => { - const mockFn = jest.fn().mockRejectedValue(null); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, + describe('InvalidHttpRequestParamsException', () => { + it('can be constructed with a message', () => { + expect(new InvalidHttpRequestParamsException('bad params').message).toBe( + 'bad params', ); - - expect(mockLogger.error).toHaveBeenCalledTimes(1); - }); - - it('preserves the original error message in the SnapError', async () => { - const originalError = new Error('Custom error message'); - const mockFn = jest.fn().mockRejectedValue(originalError); - - let caughtError: unknown; - try { - await withCatchAndThrowSnapError(mockFn); - } catch (error) { - caughtError = error; - } - - expect(caughtError).toBeInstanceOf(SnapError); - const snapError = caughtError as SnapError; - expect(snapError.message).toBe('Custom error message'); - }); - - it('handles async functions that return different types', async () => { - const testCases = [ - { value: 42, type: 'number' }, - { value: { key: 'value' }, type: 'object' }, - { value: [1, 2, 3], type: 'array' }, - { value: true, type: 'boolean' }, - { value: null, type: 'null' }, - ]; - - for (const testCase of testCases) { - const mockFn = jest.fn().mockResolvedValue(testCase.value); - - const result = await withCatchAndThrowSnapError(mockFn); - - expect(result).toBe(testCase.value); - expect(mockLogger.error).not.toHaveBeenCalled(); - } - }); - - it('handles functions that throw different error types', async () => { - const errorTypes = [ - new TypeError('Type error'), - new ReferenceError('Reference error'), - new RangeError('Range error'), - new SyntaxError('Syntax error'), - ]; - - for (const errorType of errorTypes) { - const mockFn = jest.fn().mockRejectedValue(errorType); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, - ); - } - - expect(mockLogger.error).toHaveBeenCalledTimes(errorTypes.length); - const logCalls = mockLogger.error.mock.calls; - expect(logCalls).toHaveLength(errorTypes.length); - - for (let i = 0; i < errorTypes.length; i++) { - const logCall = logCalls[i]; - const loggedError = logCall?.[0] as { error: SnapError }; - expect(loggedError.error).toBeInstanceOf(SnapError); - expect(loggedError.error.message).toBe(errorTypes[i]?.message); - } - }); - - it('includes error stack trace in the logged error', async () => { - const originalError = new Error('Test error'); - originalError.stack = 'Error: Test error\n at test.js:1:1'; - const mockFn = jest.fn().mockRejectedValue(originalError); - - try { - await withCatchAndThrowSnapError(mockFn); - } catch { - // Expected to throw - } - - expect(mockLogger.error).toHaveBeenCalledWith( - { error: expect.any(SnapError) }, - expect.stringContaining('[SnapError]'), - ); - }); - - it('handles functions that throw promises', async () => { - const rejectedPromise = Promise.reject(new Error('Promise error')); - const mockFn = jest.fn().mockImplementation(async () => rejectedPromise); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, - ); - - expect(mockLogger.error).toHaveBeenCalledTimes(1); - }); - }); - - describe('isSnapRpcError', () => { - it.each([ - new SnapError('Test error'), - new MethodNotFoundError(), - new UserRejectedRequestError(), - new MethodNotSupportedError(), - new ParseError(), - new ResourceNotFoundError(), - new ResourceUnavailableError(), - new TransactionRejected(), - new ChainDisconnectedError(), - new DisconnectedError(), - new UnauthorizedError(), - new UnsupportedMethodError(), - new InternalError(), - new InvalidInputError(), - new InvalidParamsError(), - new InvalidRequestError(), - new LimitExceededError(), - ])('return true if the error is $error', (error) => { - expect(isSnapRpcError(error)).toBe(true); - }); - }); - - describe('trackErrorIfNeeded', () => { - let trackErrorSpy: jest.SpiedFunction; - - beforeEach(() => { - trackErrorSpy = jest - .spyOn(snapUtils, 'trackError') - .mockResolvedValue(undefined); - }); - - afterEach(() => { - trackErrorSpy.mockRestore(); - }); - - it('does not call trackError for UserRejectedRequestError', async () => { - await trackErrorIfNeeded(new UserRejectedRequestError()); - - expect(trackErrorSpy).not.toHaveBeenCalled(); - }); - - it('calls trackError for unexpected errors', async () => { - const error = new Error('unexpected'); - - await trackErrorIfNeeded(error); - - expect(trackErrorSpy).toHaveBeenCalledWith(error); - }); - - it('calls trackError for InvalidHttpRequestParamsException', async () => { - const error = new InvalidHttpRequestParamsException('bad params'); - - await trackErrorIfNeeded(error); - - expect(trackErrorSpy).toHaveBeenCalledWith(error); }); }); }); diff --git a/packages/stellar-wallet-snap/src/utils/errors.ts b/packages/stellar-wallet-snap/src/utils/errors.ts index bf37588dd..622340f70 100644 --- a/packages/stellar-wallet-snap/src/utils/errors.ts +++ b/packages/stellar-wallet-snap/src/utils/errors.ts @@ -1,29 +1,13 @@ -import type { Logger } from '@metamask/snap-networks-utils'; -import { - ChainDisconnectedError, - DisconnectedError, - InternalError, - InvalidInputError, - InvalidParamsError, - InvalidRequestError, - LimitExceededError, - MethodNotFoundError, - MethodNotSupportedError, - ParseError, - ResourceNotFoundError, - ResourceUnavailableError, - SnapError, - TransactionRejected, - UnauthorizedError, - UnsupportedMethodError, - UserRejectedRequestError, -} from '@metamask/snaps-sdk'; +import { createWithCatchAndThrowSnapError } from '@metamask/snap-networks-utils'; import type { Struct } from '@metamask/superstruct'; import { assert, enums, object, type } from '@metamask/superstruct'; -import { logger as defaultLogger } from './logger'; +import { logger } from './logger'; import { trackError } from './snap'; +export { isSnapRpcError } from '@metamask/snap-networks-utils'; +export type { SnapRpcError } from '@metamask/snap-networks-utils'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- must accept arbitrary `Error` subclass ctor signatures export type AnyErrorConstructor = abstract new (...args: any[]) => Error; @@ -49,55 +33,6 @@ export function rethrowIfInstanceElseThrow( throw fallback; } -/** Union of Snap RPC error instance types (for type narrowing). */ -export type SnapRpcError = - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType - | InstanceType; - -/** - * Determines if the given error is a Snap RPC error. - * - * @param error - The error instance to be checked. - * @returns A boolean indicating whether the error is a Snap RPC error. - */ -export function isSnapRpcError(error: Error): error is SnapRpcError { - const errors = [ - SnapError, - MethodNotFoundError, - UserRejectedRequestError, - MethodNotSupportedError, - ParseError, - ResourceNotFoundError, - ResourceUnavailableError, - TransactionRejected, - ChainDisconnectedError, - DisconnectedError, - UnauthorizedError, - UnsupportedMethodError, - InternalError, - InvalidInputError, - InvalidParamsError, - InvalidRequestError, - LimitExceededError, - ]; - return errors.some((errType) => error instanceof errType); -} - export type StellarSnapExceptionOptions = { cause?: unknown; data?: Record; @@ -317,58 +252,7 @@ export function isStellarSnapException( return error instanceof StellarSnapException; } -/** - * A utility function that catches errors and throws them as SnapError. - * - * @param fn - The function to catch errors from. - * @param logger - The logger to use for logging errors. Defaults to the default logger. - * @returns The result of the function. - */ -export const withCatchAndThrowSnapError = async ( - fn: () => Promise, - logger: Logger = defaultLogger, -): Promise => { - try { - return await fn(); - } catch (errorInstance: unknown) { - await trackErrorIfNeeded(errorInstance); - - let error: SnapRpcError; - - if (errorInstance instanceof Error) { - if (isStellarSnapException(errorInstance)) { - error = new SnapError(errorInstance); - } else if (isSnapRpcError(errorInstance)) { - error = errorInstance; - } else { - error = new SnapError(errorInstance); - } - } else { - error = new SnapError(errorInstance as string | Error); - } - - logger.error( - { error }, - `[SnapError] ${JSON.stringify(error.toJSON(), null, 2)}`, - ); - - // eslint-disable-next-line @typescript-eslint/only-throw-error - throw error; - } -}; - -/** - * Sends `error` to Sentry when it represents an unexpected failure. - * - * Skips tracking for explicit user rejections. - * Callers should prefer this over {@link trackError} in swallow paths; - * - * @param error - Value from a `catch` clause. - */ -export async function trackErrorIfNeeded(error: unknown): Promise { - if (error instanceof UserRejectedRequestError) { - return; - } - - await trackError(error); -} +export const withCatchAndThrowSnapError = createWithCatchAndThrowSnapError({ + logError: logger.error.bind(logger), + trackError, +}); diff --git a/packages/stellar-wallet-snap/src/utils/snap.ts b/packages/stellar-wallet-snap/src/utils/snap.ts index d60fb7cf9..22f1d477f 100644 --- a/packages/stellar-wallet-snap/src/utils/snap.ts +++ b/packages/stellar-wallet-snap/src/utils/snap.ts @@ -2,7 +2,7 @@ import type { JsonSLIP10Node } from '@metamask/key-tree'; import type { EntropySourceId } from '@metamask/keyring-api'; import { deserialize, serialize } from '@metamask/snap-networks-utils'; import type { Serializable } from '@metamask/snap-networks-utils'; -import { getJsonError } from '@metamask/snaps-sdk'; +import { getJsonError, UserRejectedRequestError } from '@metamask/snaps-sdk'; import type { ComponentOrElement, DialogResult, @@ -640,11 +640,15 @@ export async function trackSecurityScanCompleted(properties: { * of masking the original failure. * * @param error - The error to report to Sentry. - * @returns The Sentry event ID on success, or `undefined` on failure. + * @returns The Sentry event ID on success, or `undefined` on failure or if the error is skipped. */ export async function trackError( error: Error | unknown, ): Promise { + if (error instanceof UserRejectedRequestError) { + return undefined; + } + try { let errorToTrack = error; diff --git a/packages/tron-wallet-snap/snap.manifest.json b/packages/tron-wallet-snap/snap.manifest.json index 8ed1ab610..b6dcad973 100644 --- a/packages/tron-wallet-snap/snap.manifest.json +++ b/packages/tron-wallet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "source": { - "shasum": "QnhXo3QTBC3XgD/9FKIEjyoSnVdVtZ3uGansykUe9uc=", + "shasum": "JeCbAFBIfqDsc+ysNskueF8qdvLOtmMdOmtZASZetr4=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/tron-wallet-snap/src/clients/snap/SnapClient.test.ts b/packages/tron-wallet-snap/src/clients/snap/SnapClient.test.ts index c2d68ce4c..0706c5ca7 100644 --- a/packages/tron-wallet-snap/src/clients/snap/SnapClient.test.ts +++ b/packages/tron-wallet-snap/src/clients/snap/SnapClient.test.ts @@ -1,4 +1,5 @@ import type { Logger } from '@metamask/snap-networks-utils'; +import { UserRejectedRequestError } from '@metamask/snaps-sdk'; import { mockLogger } from '../../utils/mockLogger'; import { SnapClient } from './SnapClient'; @@ -117,6 +118,24 @@ describe('SnapClient', () => { }); describe('trackError', () => { + it('does not call snap_trackError for user rejections', async () => { + await withSnapClient( + async ({ + snapClient, + mockSnapRequest: mockRequest, + mockLogger: logger, + }) => { + const result = await snapClient.trackError( + new UserRejectedRequestError(), + ); + + expect(result).toBeUndefined(); + expect(mockRequest).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + }, + ); + }); + it('returns the Sentry event ID and forwards the serialized error', async () => { await withSnapClient( async ({ diff --git a/packages/tron-wallet-snap/src/clients/snap/SnapClient.ts b/packages/tron-wallet-snap/src/clients/snap/SnapClient.ts index 6f56639df..16f671ada 100644 --- a/packages/tron-wallet-snap/src/clients/snap/SnapClient.ts +++ b/packages/tron-wallet-snap/src/clients/snap/SnapClient.ts @@ -1,7 +1,7 @@ import type { JsonSLIP10Node } from '@metamask/key-tree'; import type { EntropySourceId } from '@metamask/keyring-api'; import type { Logger } from '@metamask/snap-networks-utils'; -import { getJsonError } from '@metamask/snaps-sdk'; +import { getJsonError, UserRejectedRequestError } from '@metamask/snaps-sdk'; import type { DialogResult, EntropySource, @@ -255,9 +255,13 @@ export class SnapClient { * of masking the original failure. * * @param error - The error to report to Sentry. - * @returns The Sentry event ID on success, or `undefined` on failure. + * @returns The Sentry event ID on success, or `undefined` on failure or if the error is skipped. */ async trackError(error: Error): Promise { + if (error instanceof UserRejectedRequestError) { + return undefined; + } + try { return await snap.request({ method: 'snap_trackError', diff --git a/packages/tron-wallet-snap/src/utils/errors.test.ts b/packages/tron-wallet-snap/src/utils/errors.test.ts index ea9d33ffe..bed6cfc5d 100644 --- a/packages/tron-wallet-snap/src/utils/errors.test.ts +++ b/packages/tron-wallet-snap/src/utils/errors.test.ts @@ -1,6 +1,6 @@ import { SnapError, UserRejectedRequestError } from '@metamask/snaps-sdk'; -import { shouldTrackError, withCatchAndThrowSnapError } from './errors'; +import { withCatchAndThrowSnapError } from './errors'; import { mockLogger } from './mockLogger'; jest.mock('../clients/snap/SnapClient', () => { @@ -26,7 +26,7 @@ describe('errors', () => { jest.clearAllMocks(); }); - describe('handle', () => { + describe('withCatchAndThrowSnapError', () => { it('returns the result when the function succeeds', async () => { const mockFn = jest.fn().mockResolvedValue('success'); @@ -37,7 +37,7 @@ describe('errors', () => { expect(mockLogger.error).not.toHaveBeenCalled(); }); - it('handles and re-throws errors as SnapError', async () => { + it('tracks, logs, and re-throws errors as SnapError', async () => { const originalError = new Error('Test error'); const mockFn = jest.fn().mockRejectedValue(originalError); @@ -45,186 +45,23 @@ describe('errors', () => { SnapError, ); + expect(trackError).toHaveBeenCalledWith(originalError); expect(mockFn).toHaveBeenCalledTimes(1); expect(mockLogger.error).toHaveBeenCalledTimes(1); }); - it('tracks unhandled errors', async () => { - const originalError = new Error('Test error'); - const mockFn = jest.fn().mockRejectedValue(originalError); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - originalError, - ); - - expect(trackError).toHaveBeenCalledTimes(1); - expect(trackError).toHaveBeenCalledWith( - expect.objectContaining({ message: originalError.message }), - ); - expect(mockLogger.error).toHaveBeenCalledTimes(1); - }); - - it('skips tracking user rejected request errors', async () => { - const originalError = new UserRejectedRequestError(); - const mockFn = jest.fn().mockRejectedValue(originalError); + it('delegates tracking to SnapClient for user rejections', async () => { + const mockFn = jest + .fn() + .mockRejectedValue(new UserRejectedRequestError()); await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( UserRejectedRequestError, ); - expect(trackError).not.toHaveBeenCalled(); - expect(mockLogger.error).toHaveBeenCalledTimes(1); - }); - - it('logs errors with the correct scope and error details', async () => { - const originalError = new Error('Test error'); - const mockFn = jest.fn().mockRejectedValue(originalError); - - try { - await withCatchAndThrowSnapError(mockFn); - } catch { - // Expected to throw - } - - expect(mockLogger.error).toHaveBeenCalledWith( - { error: expect.any(SnapError) }, - expect.stringContaining(`[SnapError]`), - ); - - expect(mockLogger.error).toHaveBeenCalledTimes(1); - const logCall = mockLogger.error.mock.calls[0]; - const loggedError = (logCall?.[0] as { error?: unknown } | undefined) - ?.error; - expect(loggedError).toBeInstanceOf(SnapError); - }); - - it('handles non-Error objects and converts them to SnapError', async () => { - const nonErrorValue = 'string error'; - const mockFn = jest.fn().mockRejectedValue(nonErrorValue); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, - ); - - expect(mockLogger.error).toHaveBeenCalledTimes(1); - const logCall = mockLogger.error.mock.calls[0]; - const loggedError = (logCall?.[0] as { error?: unknown } | undefined) - ?.error; - expect(loggedError).toBeInstanceOf(SnapError); - }); - - it('handles null and undefined errors', async () => { - const mockFn = jest.fn().mockRejectedValue(null); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, - ); - - expect(mockLogger.error).toHaveBeenCalledTimes(1); - }); - - it('preserves the original error message in the SnapError', async () => { - const originalError = new Error('Custom error message'); - const mockFn = jest.fn().mockRejectedValue(originalError); - - let caughtError: unknown; - try { - await withCatchAndThrowSnapError(mockFn); - } catch (error) { - caughtError = error; - } - - expect(caughtError).toBeInstanceOf(SnapError); - const snapError = caughtError as SnapError; - expect(snapError.message).toBe('Custom error message'); - }); - - it('handles async functions that return different types', async () => { - const testCases = [ - { value: 42, type: 'number' }, - { value: { key: 'value' }, type: 'object' }, - { value: [1, 2, 3], type: 'array' }, - { value: true, type: 'boolean' }, - { value: null, type: 'null' }, - ]; - - for (const testCase of testCases) { - const mockFn = jest.fn().mockResolvedValue(testCase.value); - - const result = await withCatchAndThrowSnapError(mockFn); - - expect(result).toBe(testCase.value); - expect(mockLogger.error).not.toHaveBeenCalled(); - } - }); - - it('handles functions that throw different error types', async () => { - const errorTypes = [ - new TypeError('Type error'), - new ReferenceError('Reference error'), - new RangeError('Range error'), - new SyntaxError('Syntax error'), - ]; - - for (const errorType of errorTypes) { - const mockFn = jest.fn().mockRejectedValue(errorType); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, - ); - } - - expect(mockLogger.error).toHaveBeenCalledTimes(errorTypes.length); - const logCalls = mockLogger.error.mock.calls; - expect(logCalls).toHaveLength(errorTypes.length); - - for (let i = 0; i < errorTypes.length; i++) { - const logCall = logCalls[i]; - const loggedError = (logCall?.[0] as { error?: unknown } | undefined) - ?.error; - expect(loggedError).toBeInstanceOf(SnapError); - expect((loggedError as Error | undefined)?.message).toBe( - errorTypes[i]?.message, - ); - } - }); - - it('includes error stack trace in the logged error', async () => { - const originalError = new Error('Test error'); - originalError.stack = 'Error: Test error\n at test.js:1:1'; - const mockFn = jest.fn().mockRejectedValue(originalError); - - try { - await withCatchAndThrowSnapError(mockFn); - } catch { - // Expected to throw - } - - expect(mockLogger.error).toHaveBeenCalledWith( - { error: expect.any(SnapError) }, - expect.stringContaining('[SnapError]'), - ); - }); - - it('handles functions that throw promises', async () => { - const rejectedPromise = Promise.reject(new Error('Promise error')); - const mockFn = jest.fn().mockImplementation(async () => rejectedPromise); - - await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( - SnapError, + expect(trackError).toHaveBeenCalledWith( + expect.any(UserRejectedRequestError), ); - - expect(mockLogger.error).toHaveBeenCalledTimes(1); - }); - }); - - describe('shouldTrackError', () => { - it('returns false for user rejected request errors', () => { - expect(shouldTrackError(new UserRejectedRequestError())).toBe(false); - }); - - it('returns true for other errors', () => { - expect(shouldTrackError(new Error('Unexpected error'))).toBe(true); }); }); }); diff --git a/packages/tron-wallet-snap/src/utils/errors.ts b/packages/tron-wallet-snap/src/utils/errors.ts index f9a031503..d2d33e01e 100644 --- a/packages/tron-wallet-snap/src/utils/errors.ts +++ b/packages/tron-wallet-snap/src/utils/errors.ts @@ -1,44 +1,13 @@ -import { SnapError, UserRejectedRequestError } from '@metamask/snaps-sdk'; +import { createWithCatchAndThrowSnapError } from '@metamask/snap-networks-utils'; import { SnapClient } from '../clients/snap/SnapClient'; import logger from './logger'; -import { isSnapRpcError } from './sensitiveErrors'; - -const snapClient = new SnapClient({ logger }); export { isSnapRpcError, sanitizeSensitiveError } from './sensitiveErrors'; -/** - * Determines whether an error should be reported through `snap_trackError`. - * - * @param error - The error to evaluate. - * @returns `true` when the error should be tracked. - */ -export function shouldTrackError(error: unknown): boolean { - return !(error instanceof UserRejectedRequestError); -} - -export const withCatchAndThrowSnapError = async ( - fn: () => Promise, -): Promise => { - try { - return await fn(); - // TODO: Replace `any` with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (errorInstance: any) { - if (shouldTrackError(errorInstance)) { - await snapClient.trackError(errorInstance); - } - - const error = isSnapRpcError(errorInstance) - ? errorInstance - : new SnapError(errorInstance); - - logger.error( - { error }, - `[SnapError] ${JSON.stringify(error.toJSON(), null, 2)}`, - ); +const snapClient = new SnapClient({ logger }); - throw error; - } -}; +export const withCatchAndThrowSnapError = createWithCatchAndThrowSnapError({ + logError: logger.error.bind(logger), + trackError: (error) => snapClient.trackError(error as Error), +}); diff --git a/packages/tron-wallet-snap/src/utils/sensitiveErrors.ts b/packages/tron-wallet-snap/src/utils/sensitiveErrors.ts index 804eebbc2..939dbaf9b 100644 --- a/packages/tron-wallet-snap/src/utils/sensitiveErrors.ts +++ b/packages/tron-wallet-snap/src/utils/sensitiveErrors.ts @@ -1,22 +1,6 @@ -import { - ChainDisconnectedError, - DisconnectedError, - InternalError, - InvalidInputError, - InvalidParamsError, - InvalidRequestError, - LimitExceededError, - MethodNotFoundError, - MethodNotSupportedError, - ParseError, - ResourceNotFoundError, - ResourceUnavailableError, - SnapError, - TransactionRejected, - UnauthorizedError, - UnsupportedMethodError, - UserRejectedRequestError, -} from '@metamask/snaps-sdk'; +import { isSnapRpcError } from '@metamask/snap-networks-utils'; + +export { isSnapRpcError }; /** * Sanitizes error messages that may contain sensitive cryptographic information. @@ -53,40 +37,11 @@ export function sanitizeSensitiveError(error: any): Error { 'Key derivation failed. Please check your connection and try again.', ); if (isSnapRpcError(error)) { - return error.constructor ? new error.constructor() : sanitizedError; + const RpcErrorConstructor = error.constructor as new () => Error; + return RpcErrorConstructor ? new RpcErrorConstructor() : sanitizedError; } return sanitizedError; } return error; } - -/** - * Determines if the given error is a Snap RPC error. - * - * @param error - The error instance to be checked. - * @returns A boolean indicating whether the error is a Snap RPC error. - */ -export function isSnapRpcError(error: Error): boolean { - const errors = [ - SnapError, - MethodNotFoundError, - UserRejectedRequestError, - MethodNotSupportedError, - MethodNotFoundError, - ParseError, - ResourceNotFoundError, - ResourceUnavailableError, - TransactionRejected, - ChainDisconnectedError, - DisconnectedError, - UnauthorizedError, - UnsupportedMethodError, - InternalError, - InvalidInputError, - InvalidParamsError, - InvalidRequestError, - LimitExceededError, - ]; - return errors.some((errType) => error instanceof errType); -}