From 68b1e5a5c243d4520f0c9a7020601e09e47f537c Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Mon, 31 Aug 2026 14:03:20 +0200 Subject: [PATCH 1/4] 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 42058179e5c9205c7c9451a59610a9b04204edad Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Mon, 31 Aug 2026 15:11:55 +0200 Subject: [PATCH 2/4] 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 cc87536e206d9c7e97595da488260b98ec253a96 Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Mon, 31 Aug 2026 15:48:12 +0200 Subject: [PATCH 3/4] 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/clients/snap/SnapClient.test.ts | 1 - 6 files changed, 378 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/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 c8cf0eaa7f069b06199f2d02ac71a92377129b68 Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Tue, 1 Sep 2026 17:11:17 +0200 Subject: [PATCH 4/4] feat: use shared trackError in stellar --- .../clientRequest/confirmSend.test.ts | 7 ++-- .../src/handlers/keyring/base.ts | 2 +- .../OnChainAccountSynchronizeService.test.ts | 8 ++-- .../src/utils/__mocks__/snap.ts | 1 - .../stellar-wallet-snap/src/utils/errors.ts | 13 ++++--- .../stellar-wallet-snap/src/utils/snap.ts | 37 +------------------ 6 files changed, 17 insertions(+), 51 deletions(-) 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 08bf5b91..b1dd3435 100644 --- a/packages/stellar-wallet-snap/src/handlers/clientRequest/confirmSend.test.ts +++ b/packages/stellar-wallet-snap/src/handlers/clientRequest/confirmSend.test.ts @@ -49,6 +49,7 @@ 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 errorsUtils from '../../utils/errors'; import { logger } from '../../utils/logger'; import * as snapUtils from '../../utils/snap'; import { AccountResolver } from '../accountResolver'; @@ -543,7 +544,7 @@ describe('ConfirmSendHandler', () => { ); createValidatedSendTransaction.mockRejectedValueOnce(xdrParseError); const trackErrorSpy = jest - .spyOn(snapUtils, 'trackError') + .spyOn(errorsUtils, 'trackError') .mockResolvedValue(undefined); expect(await handler.handle(baseRequest())).toStrictEqual({ @@ -558,7 +559,7 @@ describe('ConfirmSendHandler', () => { const unexpectedError = new Error('unexpected'); createValidatedSendTransaction.mockRejectedValueOnce(unexpectedError); const trackErrorSpy = jest - .spyOn(snapUtils, 'trackError') + .spyOn(errorsUtils, 'trackError') .mockResolvedValue(undefined); expect(await handler.handle(baseRequest())).toStrictEqual({ @@ -574,7 +575,7 @@ describe('ConfirmSendHandler', () => { new TransactionValidationException('x'), ); const trackErrorSpy = jest - .spyOn(snapUtils, 'trackError') + .spyOn(errorsUtils, 'trackError') .mockResolvedValue(undefined); expect(await handler.handle(baseRequest())).toStrictEqual({ diff --git a/packages/stellar-wallet-snap/src/handlers/keyring/base.ts b/packages/stellar-wallet-snap/src/handlers/keyring/base.ts index f701bcbc..6650a5cb 100644 --- a/packages/stellar-wallet-snap/src/handlers/keyring/base.ts +++ b/packages/stellar-wallet-snap/src/handlers/keyring/base.ts @@ -3,8 +3,8 @@ import type { Struct } from '@metamask/superstruct'; import type { Json } from '@metamask/utils'; import type { KnownCaip2ChainId } from '../../api'; +import { trackError } from '../../utils/errors'; import { validateRequest, validateResponse } from '../../utils/requestResponse'; -import { trackError } from '../../utils/snap'; import type { AccountResolver, ResolvedKeyringAndWalletOnly, diff --git a/packages/stellar-wallet-snap/src/services/on-chain-account/OnChainAccountSynchronizeService.test.ts b/packages/stellar-wallet-snap/src/services/on-chain-account/OnChainAccountSynchronizeService.test.ts index 1bfc3592..26f1efa8 100644 --- a/packages/stellar-wallet-snap/src/services/on-chain-account/OnChainAccountSynchronizeService.test.ts +++ b/packages/stellar-wallet-snap/src/services/on-chain-account/OnChainAccountSynchronizeService.test.ts @@ -10,8 +10,8 @@ import type { KnownCaip19Sep41AssetId } from '../../api'; import { KnownCaip2ChainId } from '../../api'; import { NATIVE_ASSET_SYMBOL, STELLAR_DECIMAL_PLACES } from '../../constants'; import { bufferToUint8Array } from '../../utils/buffer'; +import * as errorsUtils from '../../utils/errors'; import { logger } from '../../utils/logger'; -import * as snapUtils from '../../utils/snap'; import type { StellarKeyringAccount } from '../account'; import { generateStellarKeyringAccount } from '../account/__mocks__/account.fixtures'; import { @@ -200,7 +200,7 @@ describe('OnChainAccountSynchronizeService', () => { const setupTest = () => { jest.mocked(emitSnapKeyringEvent).mockResolvedValue(undefined); - jest.spyOn(snapUtils, 'trackError').mockResolvedValue(undefined); + jest.spyOn(errorsUtils, 'trackError').mockResolvedValue(undefined); }; const buildActivatedAccountPair = ( @@ -974,7 +974,7 @@ describe('OnChainAccountSynchronizeService', () => { mockSep41Assets, ); - expect(snapUtils.trackError).toHaveBeenCalledWith( + expect(errorsUtils.trackError).toHaveBeenCalledWith( expect.objectContaining({ message: 'sep41 fetch temporarily unavailable', }), @@ -1144,7 +1144,7 @@ describe('OnChainAccountSynchronizeService', () => { ); expect(saveManySpy).toHaveBeenCalled(); - expect(snapUtils.trackError).toHaveBeenCalledWith(emitError); + expect(errorsUtils.trackError).toHaveBeenCalledWith(emitError); expect(logger.warn).toHaveBeenCalledWith( '[💼 OnChainAccountSynchronizeService]', 'Failed to emit keyring events after synchronize', diff --git a/packages/stellar-wallet-snap/src/utils/__mocks__/snap.ts b/packages/stellar-wallet-snap/src/utils/__mocks__/snap.ts index 77705e26..c4a9be6c 100644 --- a/packages/stellar-wallet-snap/src/utils/__mocks__/snap.ts +++ b/packages/stellar-wallet-snap/src/utils/__mocks__/snap.ts @@ -35,5 +35,4 @@ export const { trackTransactionFinalized, trackSecurityAlertDetected, trackSecurityScanCompleted, - trackError, } = actual; diff --git a/packages/stellar-wallet-snap/src/utils/errors.ts b/packages/stellar-wallet-snap/src/utils/errors.ts index 622340f7..9e878ef0 100644 --- a/packages/stellar-wallet-snap/src/utils/errors.ts +++ b/packages/stellar-wallet-snap/src/utils/errors.ts @@ -1,9 +1,9 @@ -import { createWithCatchAndThrowSnapError } from '@metamask/snap-networks-utils'; +import { createSnapErrorHandling } from '@metamask/snap-networks-utils'; import type { Struct } from '@metamask/superstruct'; import { assert, enums, object, type } from '@metamask/superstruct'; import { logger } from './logger'; -import { trackError } from './snap'; +import { getSnapProvider } from './snap'; export { isSnapRpcError } from '@metamask/snap-networks-utils'; export type { SnapRpcError } from '@metamask/snap-networks-utils'; @@ -252,7 +252,8 @@ export function isStellarSnapException( return error instanceof StellarSnapException; } -export const withCatchAndThrowSnapError = createWithCatchAndThrowSnapError({ - logError: logger.error.bind(logger), - trackError, -}); +export const { trackError, withCatchAndThrowSnapError } = + createSnapErrorHandling({ + getSnapProvider, + logError: logger.error.bind(logger), + }); diff --git a/packages/stellar-wallet-snap/src/utils/snap.ts b/packages/stellar-wallet-snap/src/utils/snap.ts index 22f1d477..8e1e3b01 100644 --- a/packages/stellar-wallet-snap/src/utils/snap.ts +++ b/packages/stellar-wallet-snap/src/utils/snap.ts @@ -1,8 +1,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, UserRejectedRequestError } from '@metamask/snaps-sdk'; +import { deserialize, serialize } from '@metamask/snap-networks-utils'; import type { ComponentOrElement, DialogResult, @@ -14,7 +13,6 @@ import type { SnapsProvider, UpdateInterfaceResult, } from '@metamask/snaps-sdk'; -import { ensureError } from '@metamask/utils'; import { StellarSnapException } from './errors'; import { logger } from './logger'; @@ -631,36 +629,3 @@ export async function trackSecurityScanCompleted(properties: { has_security_alerts: properties.hasSecurityAlerts, }); } - -/** - * Track an error in MetaMask via Sentry (`snap_trackError`). - * - * RPC failures are caught and logged but never rethrown, so this is - * safe to call from already-failing error-handling paths without risk - * of masking the original failure. - * - * @param error - The error to report to Sentry. - * @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; - - if (!(error instanceof Error)) { - errorToTrack = ensureError(error); - } - return await getSnapProvider().request({ - method: 'snap_trackError', - params: { error: getJsonError(errorToTrack) }, - }); - } catch (rpcError) { - logger.warn({ rpcError }, 'Failed to track error via snap_trackError'); - return undefined; - } -}