From c30c475e53b9fee958770f916b11e932cd7fa6c1 Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Mon, 31 Aug 2026 14:03:20 +0200 Subject: [PATCH 1/6] feat: move withCatchAndThrowSnapErrorHandler into shared lib --- .../src/errors/errors.test.ts | 196 ++++++++++++++++++ .../snap-networks-utils/src/errors/errors.ts | 78 +++++++ .../snap-networks-utils/src/errors/index.ts | 8 + .../src/errors/snapRpcError.ts | 69 ++++++ .../src/logger/__mocks__/Logger.ts | 26 +++ 5 files changed, 377 insertions(+) create mode 100644 packages/snap-networks-utils/src/errors/errors.test.ts create mode 100644 packages/snap-networks-utils/src/errors/errors.ts create mode 100644 packages/snap-networks-utils/src/errors/index.ts create mode 100644 packages/snap-networks-utils/src/errors/snapRpcError.ts create mode 100644 packages/snap-networks-utils/src/logger/__mocks__/Logger.ts diff --git a/packages/snap-networks-utils/src/errors/errors.test.ts b/packages/snap-networks-utils/src/errors/errors.test.ts new file mode 100644 index 00000000..d857b1af --- /dev/null +++ b/packages/snap-networks-utils/src/errors/errors.test.ts @@ -0,0 +1,196 @@ +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'; + +const setupTest = () => { + jest.clearAllMocks(); + + const trackError = jest.fn(); + const withCatchAndThrowSnapError = createWithCatchAndThrowSnapError({ + logError: mockLogger['error'], + trackError, + }); + + return { + mockLogger, + trackError, + withCatchAndThrowSnapError, + createBoundWithCatchAndThrowSnapError: ( + options: Omit = { + trackError, + }, + ) => + createWithCatchAndThrowSnapError({ + ...options, + logError: mockLogger['error'], + }), + }; +}; + +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 { mockLogger, 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 { mockLogger, 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 { mockLogger, 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 { mockLogger, 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' }), + ); + }); + }); +}); diff --git a/packages/snap-networks-utils/src/errors/errors.ts b/packages/snap-networks-utils/src/errors/errors.ts new file mode 100644 index 00000000..233dc32e --- /dev/null +++ b/packages/snap-networks-utils/src/errors/errors.ts @@ -0,0 +1,78 @@ +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 CreateWithCatchAndThrowSnapErrorOptions = { + logError: Logger['error']; + 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. + * @returns A function that catches errors, tracks them, logs, and rethrows as Snap RPC errors. + */ +export function createWithCatchAndThrowSnapError({ + logError, + trackError, + normalizeErrorFn = normalizeError, +}: CreateWithCatchAndThrowSnapErrorOptions) { + return (fn: () => Promise) => + withCatchAndThrowSnapErrorHandler( + logError, + trackError, + normalizeErrorFn, + fn, + ); +} + +async function withCatchAndThrowSnapErrorHandler( + logError: Logger['error'], + 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)}`, + ); + + throw error; + } +} diff --git a/packages/snap-networks-utils/src/errors/index.ts b/packages/snap-networks-utils/src/errors/index.ts new file mode 100644 index 00000000..c111adc0 --- /dev/null +++ b/packages/snap-networks-utils/src/errors/index.ts @@ -0,0 +1,8 @@ +export { createWithCatchAndThrowSnapError, normalizeError } from './errors'; +export { isSnapRpcError } from './snapRpcError'; +export type { + CreateWithCatchAndThrowSnapErrorOptions, + NormalizeErrorFn, + TrackErrorFn, +} from './errors'; +export type { SnapRpcError } from './snapRpcError'; diff --git a/packages/snap-networks-utils/src/errors/snapRpcError.ts b/packages/snap-networks-utils/src/errors/snapRpcError.ts new file mode 100644 index 00000000..72b321d6 --- /dev/null +++ b/packages/snap-networks-utils/src/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/logger/__mocks__/Logger.ts b/packages/snap-networks-utils/src/logger/__mocks__/Logger.ts new file mode 100644 index 00000000..567d9737 --- /dev/null +++ b/packages/snap-networks-utils/src/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; + }, + }); +} From 6d48fafb14784085239674dc6d4a69025da7d32c Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Mon, 31 Aug 2026 15:11:55 +0200 Subject: [PATCH 2/6] chore: use shared withCatchAndThrowSnapError in tron snap --- packages/tron-wallet-snap/src/clients/snap/SnapClient.test.ts | 1 + 1 file changed, 1 insertion(+) 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 0706c5ca..a3174f66 100644 --- a/packages/tron-wallet-snap/src/clients/snap/SnapClient.test.ts +++ b/packages/tron-wallet-snap/src/clients/snap/SnapClient.test.ts @@ -1,3 +1,4 @@ +import { UserRejectedRequestError } from '@metamask/snaps-sdk'; import type { Logger } from '@metamask/snap-networks-utils'; import { UserRejectedRequestError } from '@metamask/snaps-sdk'; From 99f9caff0a8981d99207dc3f1c98e17859da2d1d Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Mon, 31 Aug 2026 15:48:12 +0200 Subject: [PATCH 3/6] chore: fix lint errors --- .../src/errors/errors.test.ts | 196 ------------------ .../snap-networks-utils/src/errors/errors.ts | 78 ------- .../snap-networks-utils/src/errors/index.ts | 8 - .../src/errors/snapRpcError.ts | 69 ------ .../src/logger/__mocks__/Logger.ts | 26 --- .../src/utils/errors/index.ts | 1 - .../src/clients/snap/SnapClient.test.ts | 1 - 7 files changed, 379 deletions(-) delete mode 100644 packages/snap-networks-utils/src/errors/errors.test.ts delete mode 100644 packages/snap-networks-utils/src/errors/errors.ts delete mode 100644 packages/snap-networks-utils/src/errors/index.ts delete mode 100644 packages/snap-networks-utils/src/errors/snapRpcError.ts delete mode 100644 packages/snap-networks-utils/src/logger/__mocks__/Logger.ts diff --git a/packages/snap-networks-utils/src/errors/errors.test.ts b/packages/snap-networks-utils/src/errors/errors.test.ts deleted file mode 100644 index d857b1af..00000000 --- a/packages/snap-networks-utils/src/errors/errors.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -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'; - -const setupTest = () => { - jest.clearAllMocks(); - - const trackError = jest.fn(); - const withCatchAndThrowSnapError = createWithCatchAndThrowSnapError({ - logError: mockLogger['error'], - trackError, - }); - - return { - mockLogger, - trackError, - withCatchAndThrowSnapError, - createBoundWithCatchAndThrowSnapError: ( - options: Omit = { - trackError, - }, - ) => - createWithCatchAndThrowSnapError({ - ...options, - logError: mockLogger['error'], - }), - }; -}; - -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 { mockLogger, 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 { mockLogger, 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 { mockLogger, 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 { mockLogger, 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' }), - ); - }); - }); -}); diff --git a/packages/snap-networks-utils/src/errors/errors.ts b/packages/snap-networks-utils/src/errors/errors.ts deleted file mode 100644 index 233dc32e..00000000 --- a/packages/snap-networks-utils/src/errors/errors.ts +++ /dev/null @@ -1,78 +0,0 @@ -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 CreateWithCatchAndThrowSnapErrorOptions = { - logError: Logger['error']; - 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. - * @returns A function that catches errors, tracks them, logs, and rethrows as Snap RPC errors. - */ -export function createWithCatchAndThrowSnapError({ - logError, - trackError, - normalizeErrorFn = normalizeError, -}: CreateWithCatchAndThrowSnapErrorOptions) { - return (fn: () => Promise) => - withCatchAndThrowSnapErrorHandler( - logError, - trackError, - normalizeErrorFn, - fn, - ); -} - -async function withCatchAndThrowSnapErrorHandler( - logError: Logger['error'], - 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)}`, - ); - - throw error; - } -} diff --git a/packages/snap-networks-utils/src/errors/index.ts b/packages/snap-networks-utils/src/errors/index.ts deleted file mode 100644 index c111adc0..00000000 --- a/packages/snap-networks-utils/src/errors/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { createWithCatchAndThrowSnapError, normalizeError } from './errors'; -export { isSnapRpcError } from './snapRpcError'; -export type { - CreateWithCatchAndThrowSnapErrorOptions, - NormalizeErrorFn, - TrackErrorFn, -} from './errors'; -export type { SnapRpcError } from './snapRpcError'; diff --git a/packages/snap-networks-utils/src/errors/snapRpcError.ts b/packages/snap-networks-utils/src/errors/snapRpcError.ts deleted file mode 100644 index 72b321d6..00000000 --- a/packages/snap-networks-utils/src/errors/snapRpcError.ts +++ /dev/null @@ -1,69 +0,0 @@ -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/logger/__mocks__/Logger.ts b/packages/snap-networks-utils/src/logger/__mocks__/Logger.ts deleted file mode 100644 index 567d9737..00000000 --- a/packages/snap-networks-utils/src/logger/__mocks__/Logger.ts +++ /dev/null @@ -1,26 +0,0 @@ -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/snap-networks-utils/src/utils/errors/index.ts b/packages/snap-networks-utils/src/utils/errors/index.ts index f057effe..4e191caf 100644 --- a/packages/snap-networks-utils/src/utils/errors/index.ts +++ b/packages/snap-networks-utils/src/utils/errors/index.ts @@ -11,7 +11,6 @@ export type { } from './trackError'; export type { CreateWithCatchAndThrowSnapErrorOptions, - LogErrorFn, NormalizeErrorFn, TrackErrorFn, } from './errors'; 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 a3174f66..0706c5ca 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,3 @@ -import { UserRejectedRequestError } from '@metamask/snaps-sdk'; import type { Logger } from '@metamask/snap-networks-utils'; import { UserRejectedRequestError } from '@metamask/snaps-sdk'; From 7ac54db2dadfade0fc6ea5ee2c25462f4b8c2cbd Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Tue, 1 Sep 2026 12:54:17 +0200 Subject: [PATCH 4/6] feat: add optional logError override --- packages/snap-networks-utils/src/utils/errors/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/snap-networks-utils/src/utils/errors/index.ts b/packages/snap-networks-utils/src/utils/errors/index.ts index 4e191caf..f057effe 100644 --- a/packages/snap-networks-utils/src/utils/errors/index.ts +++ b/packages/snap-networks-utils/src/utils/errors/index.ts @@ -11,6 +11,7 @@ export type { } from './trackError'; export type { CreateWithCatchAndThrowSnapErrorOptions, + LogErrorFn, NormalizeErrorFn, TrackErrorFn, } from './errors'; From dfc082f7eae67b3b9157b8e1ed8decd60712ed07 Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Tue, 1 Sep 2026 18:15:33 +0200 Subject: [PATCH 5/6] feat/tron-use-shared-track-error-util --- .../src/clients/snap/SnapClient.test.ts | 22 +++--- .../src/clients/snap/SnapClient.ts | 27 +------- .../src/clients/snap/getSnapProvider.ts | 11 +++ packages/tron-wallet-snap/src/context.ts | 2 +- .../tron-wallet-snap/src/utils/errors.test.ts | 67 +++++++++++++------ packages/tron-wallet-snap/src/utils/errors.ts | 17 ++--- 6 files changed, 83 insertions(+), 63 deletions(-) create mode 100644 packages/tron-wallet-snap/src/clients/snap/getSnapProvider.ts 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 0706c5ca..b41b14ee 100644 --- a/packages/tron-wallet-snap/src/clients/snap/SnapClient.test.ts +++ b/packages/tron-wallet-snap/src/clients/snap/SnapClient.test.ts @@ -4,6 +4,11 @@ import { UserRejectedRequestError } from '@metamask/snaps-sdk'; import { mockLogger } from '../../utils/mockLogger'; import { SnapClient } from './SnapClient'; +jest.mock('../../utils/logger', () => ({ + __esModule: true, + default: jest.requireActual('../../utils/mockLogger').mockLogger, +})); + // Mock the global snap object const mockSnapRequest = jest.fn(); (globalThis as any).snap = { @@ -25,7 +30,7 @@ async function withSnapClient( }) => void | Promise, ) { mockSnapRequest.mockReset(); - const snapClient = new SnapClient({ logger: mockLogger }); + const snapClient = new SnapClient(); await testFn({ snapClient, mockSnapRequest, mockLogger }); } @@ -131,7 +136,7 @@ describe('SnapClient', () => { expect(result).toBeUndefined(); expect(mockRequest).not.toHaveBeenCalled(); - expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); }, ); }); @@ -161,12 +166,12 @@ describe('SnapClient', () => { }), }, }); - expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); }, ); }); - it('swallows RPC failures and logs a warning', async () => { + it('swallows RPC failures and logs an error', async () => { await withSnapClient( async ({ snapClient, @@ -179,11 +184,10 @@ describe('SnapClient', () => { const result = await snapClient.trackError(new Error('x')); expect(result).toBeUndefined(); - expect(logger.warn).toHaveBeenCalledTimes(1); - expect(logger.warn).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ rpcError }), - expect.stringContaining('Failed to track error'), + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith( + { error: rpcError }, + 'Failed to track error', ); }, ); diff --git a/packages/tron-wallet-snap/src/clients/snap/SnapClient.ts b/packages/tron-wallet-snap/src/clients/snap/SnapClient.ts index 16f671ad..eb2a8605 100644 --- a/packages/tron-wallet-snap/src/clients/snap/SnapClient.ts +++ b/packages/tron-wallet-snap/src/clients/snap/SnapClient.ts @@ -1,7 +1,5 @@ import type { JsonSLIP10Node } from '@metamask/key-tree'; import type { EntropySourceId } from '@metamask/keyring-api'; -import type { Logger } from '@metamask/snap-networks-utils'; -import { getJsonError, UserRejectedRequestError } from '@metamask/snaps-sdk'; import type { DialogResult, EntropySource, @@ -13,19 +11,13 @@ import type { import { SecurityEventType, TransactionEventType } from '../../types/analytics'; import type { Preferences } from '../../types/snap'; -import { sanitizeSensitiveError } from '../../utils/sensitiveErrors'; +import { trackError as reportErrorToSentry } from '../../utils/errors'; /** * Client for interacting with the Snap API. * Provides methods for managing interfaces, dialogs, preferences, and background events. */ export class SnapClient { - readonly #logger: Logger; - - constructor({ logger }: { logger: Logger }) { - this.#logger = logger.withPrefix('[📡 SnapClient]'); - } - /** * Retrieves a `SLIP10NodeInterface` object for the specified path and curve. * @@ -258,22 +250,7 @@ export class SnapClient { * @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', - params: { error: getJsonError(sanitizeSensitiveError(error)) }, - }); - } catch (rpcError) { - this.#logger.warn( - { rpcError }, - 'Failed to track error via snap_trackError', - ); - return undefined; - } + return reportErrorToSentry(error); } /** diff --git a/packages/tron-wallet-snap/src/clients/snap/getSnapProvider.ts b/packages/tron-wallet-snap/src/clients/snap/getSnapProvider.ts new file mode 100644 index 00000000..fffb00f1 --- /dev/null +++ b/packages/tron-wallet-snap/src/clients/snap/getSnapProvider.ts @@ -0,0 +1,11 @@ +import type { SnapsProvider } from '@metamask/snaps-sdk'; + +/** + * Returns the Snap provider. + * + * @returns The Snap provider. + */ +export function getSnapProvider(): SnapsProvider { + // snap is a global variable provided by the Snap SDK + return snap; +} diff --git a/packages/tron-wallet-snap/src/context.ts b/packages/tron-wallet-snap/src/context.ts index 700d3510..c400f144 100644 --- a/packages/tron-wallet-snap/src/context.ts +++ b/packages/tron-wallet-snap/src/context.ts @@ -69,7 +69,7 @@ const state = new State({ }, }); -const snapClient = new SnapClient({ logger }); +const snapClient = new SnapClient(); // Repositories - depend on State const accountsRepository = new AccountsRepository(state); diff --git a/packages/tron-wallet-snap/src/utils/errors.test.ts b/packages/tron-wallet-snap/src/utils/errors.test.ts index bed6cfc5..e890ff98 100644 --- a/packages/tron-wallet-snap/src/utils/errors.test.ts +++ b/packages/tron-wallet-snap/src/utils/errors.test.ts @@ -1,33 +1,58 @@ import { SnapError, UserRejectedRequestError } from '@metamask/snaps-sdk'; -import { withCatchAndThrowSnapError } from './errors'; +import { trackError, withCatchAndThrowSnapError } from './errors'; import { mockLogger } from './mockLogger'; -jest.mock('../clients/snap/SnapClient', () => { - const trackError = jest.fn(); - - return { - trackError, - SnapClient: jest.fn().mockImplementation(() => ({ - trackError, - })), - }; -}); - jest.mock('./logger', () => ({ __esModule: true, default: jest.requireActual('./mockLogger').mockLogger, })); -const { trackError } = jest.requireMock('../clients/snap/SnapClient'); +const setupTest = (): { mockSnapRequest: jest.Mock } => { + jest.clearAllMocks(); + + const mockSnapRequest = jest.fn(); + Object.defineProperty(globalThis, 'snap', { + configurable: true, + value: { request: mockSnapRequest }, + writable: true, + }); + + return { mockSnapRequest }; +}; describe('errors', () => { - beforeEach(() => { - jest.clearAllMocks(); + describe('trackError', () => { + it('does not track UserRejectedRequestError', async () => { + const { mockSnapRequest } = setupTest(); + + expect(await trackError(new UserRejectedRequestError())).toBeUndefined(); + + expect(mockSnapRequest).not.toHaveBeenCalled(); + }); + + it('sanitizes errors before tracking', async () => { + const { mockSnapRequest } = setupTest(); + mockSnapRequest.mockResolvedValue('tracked-error-id'); + + await trackError(new Error('Failed to derive private key')); + + expect(mockSnapRequest).toHaveBeenCalledWith({ + method: 'snap_trackError', + params: { + error: expect.objectContaining({ + message: + 'Key derivation failed. Please check your connection and try again.', + }), + }, + }); + }); }); describe('withCatchAndThrowSnapError', () => { it('returns the result when the function succeeds', async () => { + setupTest(); + const mockFn = jest.fn().mockResolvedValue('success'); const result = await withCatchAndThrowSnapError(mockFn); @@ -38,6 +63,9 @@ describe('errors', () => { }); it('tracks, logs, and re-throws errors as SnapError', async () => { + const { mockSnapRequest } = setupTest(); + mockSnapRequest.mockResolvedValue('tracked-error-id'); + const originalError = new Error('Test error'); const mockFn = jest.fn().mockRejectedValue(originalError); @@ -45,12 +73,13 @@ describe('errors', () => { SnapError, ); - expect(trackError).toHaveBeenCalledWith(originalError); + expect(mockSnapRequest).toHaveBeenCalledTimes(1); expect(mockFn).toHaveBeenCalledTimes(1); expect(mockLogger.error).toHaveBeenCalledTimes(1); }); - it('delegates tracking to SnapClient for user rejections', async () => { + it('skips tracking RPC for user rejections', async () => { + const { mockSnapRequest } = setupTest(); const mockFn = jest .fn() .mockRejectedValue(new UserRejectedRequestError()); @@ -59,9 +88,7 @@ describe('errors', () => { UserRejectedRequestError, ); - expect(trackError).toHaveBeenCalledWith( - expect.any(UserRejectedRequestError), - ); + expect(mockSnapRequest).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/tron-wallet-snap/src/utils/errors.ts b/packages/tron-wallet-snap/src/utils/errors.ts index d2d33e01..b8b2a7de 100644 --- a/packages/tron-wallet-snap/src/utils/errors.ts +++ b/packages/tron-wallet-snap/src/utils/errors.ts @@ -1,13 +1,14 @@ -import { createWithCatchAndThrowSnapError } from '@metamask/snap-networks-utils'; +import { createSnapErrorHandling } from '@metamask/snap-networks-utils'; -import { SnapClient } from '../clients/snap/SnapClient'; +import { getSnapProvider } from '../clients/snap/getSnapProvider'; import logger from './logger'; +import { sanitizeSensitiveError } from './sensitiveErrors'; export { isSnapRpcError, sanitizeSensitiveError } from './sensitiveErrors'; -const snapClient = new SnapClient({ logger }); - -export const withCatchAndThrowSnapError = createWithCatchAndThrowSnapError({ - logError: logger.error.bind(logger), - trackError: (error) => snapClient.trackError(error as Error), -}); +export const { trackError, withCatchAndThrowSnapError } = + createSnapErrorHandling({ + getSnapProvider, + logError: logger.error.bind(logger), + prepareError: sanitizeSensitiveError, + }); From 71dfbc36f2dd5bb79ba7b88448170f1d410b77e5 Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Wed, 2 Sep 2026 17:02:03 +0200 Subject: [PATCH 6/6] chore: add links to createTrackError and createWithCatchAndThrowSnapError in createSnapErrorHandling --- packages/snap-networks-utils/src/utils/errors/trackError.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/snap-networks-utils/src/utils/errors/trackError.ts b/packages/snap-networks-utils/src/utils/errors/trackError.ts index e772ca04..55d7d858 100644 --- a/packages/snap-networks-utils/src/utils/errors/trackError.ts +++ b/packages/snap-networks-utils/src/utils/errors/trackError.ts @@ -109,7 +109,8 @@ export function createTrackError({ * @param options.getSnapProvider - Returns the Snap provider used for `snap_trackError`. * @param options.prepareError - Optional error normalizer before Sentry serialization. * @param options.shouldTrack - Optional tracking filter passed to {@link createTrackError}. - * @returns Bound `trackError` and `withCatchAndThrowSnapError` functions. + * @returns Bound {@link createTrackError trackError} and + * {@link createWithCatchAndThrowSnapError withCatchAndThrowSnapError} functions. */ export function createSnapErrorHandling< TProvider extends TrackErrorCapableProvider,