From 0616dcc56b68bb4ae43a1bcc35111b345d1b0453 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 b2953a5122100d4337b624b92556019718278650 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 1b1bdaabccd143076818b4db1670a347866fd105 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 - .../solana-wallet-snap/snap.manifest.json | 18 +- .../src/clients/snap/SnapClient.test.ts | 1 - 8 files changed, 14 insertions(+), 383 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/solana-wallet-snap/snap.manifest.json b/packages/solana-wallet-snap/snap.manifest.json index ff45fdc1..00dc32b0 100644 --- a/packages/solana-wallet-snap/snap.manifest.json +++ b/packages/solana-wallet-snap/snap.manifest.json @@ -16,7 +16,9 @@ "registry": "https://registry.npmjs.org/" } }, - "locales": ["locales/en.json"] + "locales": [ + "locales/en.json" + ] }, "initialConnections": { "https://portfolio.metamask.io": {} @@ -27,9 +29,13 @@ "snaps": false }, "endowment:keyring": { - "allowedOrigins": ["https://portfolio.metamask.io"], + "allowedOrigins": [ + "https://portfolio.metamask.io" + ], "capabilities": { - "scopes": ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"], + "scopes": [ + "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + ], "privateKey": { "exportFormats": [ { @@ -46,7 +52,11 @@ }, "snap_getBip32Entropy": [ { - "path": ["m", "44'", "501'"], + "path": [ + "m", + "44'", + "501'" + ], "curve": "ed25519" } ], 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 f0bd18adbed7ed2fc42327d9f5e012bbf17d910b 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 f2699595b01820bc21ce7614735d08a8030c4919 Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Tue, 1 Sep 2026 20:57:37 +0200 Subject: [PATCH 5/6] feat: use shared trackError in bitcoin --- .../src/handlers/HandlerMiddleware.test.ts | 46 +++---- .../src/handlers/HandlerMiddleware.ts | 23 +--- packages/bitcoin-wallet-snap/src/index.ts | 3 +- .../src/infra/SnapClientAdapter.test.ts | 33 ++++- .../src/infra/SnapClientAdapter.ts | 12 +- .../src/infra/getSnapProvider.ts | 11 ++ .../src/utils/errors.test.ts | 117 ++++++++++++++++++ .../bitcoin-wallet-snap/src/utils/errors.ts | 33 +++++ .../bitcoin-wallet-snap/src/utils/logger.ts | 7 ++ .../solana-wallet-snap/snap.manifest.json | 20 +-- 10 files changed, 225 insertions(+), 80 deletions(-) create mode 100644 packages/bitcoin-wallet-snap/src/infra/getSnapProvider.ts create mode 100644 packages/bitcoin-wallet-snap/src/utils/errors.test.ts create mode 100644 packages/bitcoin-wallet-snap/src/utils/errors.ts create mode 100644 packages/bitcoin-wallet-snap/src/utils/logger.ts diff --git a/packages/bitcoin-wallet-snap/src/handlers/HandlerMiddleware.test.ts b/packages/bitcoin-wallet-snap/src/handlers/HandlerMiddleware.test.ts index 8ca6d59d..76553a0b 100644 --- a/packages/bitcoin-wallet-snap/src/handlers/HandlerMiddleware.test.ts +++ b/packages/bitcoin-wallet-snap/src/handlers/HandlerMiddleware.test.ts @@ -1,9 +1,14 @@ import type { GetPreferencesResult } from '@metamask/snaps-sdk'; import { mock } from 'jest-mock-extended'; -import { BaseError, ExternalServiceError, UserActionError } from '../entities'; +import { BaseError, ExternalServiceError } from '../entities'; import type { Logger, SnapClient, Translator } from '../entities'; -import { HandlerMiddleware, shouldTrackError } from './HandlerMiddleware'; +import { trackError } from '../utils/errors'; +import { HandlerMiddleware } from './HandlerMiddleware'; + +jest.mock('../utils/errors', () => ({ + trackError: jest.fn(), +})); describe('HandlerMiddleware', () => { const mockLogger = mock(); @@ -13,6 +18,7 @@ describe('HandlerMiddleware', () => { const mockTranslator = mock({ load: jest.fn(), }); + const mockTrackError = jest.mocked(trackError); const middleware = new HandlerMiddleware( mockLogger, @@ -22,33 +28,13 @@ describe('HandlerMiddleware', () => { beforeEach(() => { jest.clearAllMocks(); + mockTrackError.mockResolvedValue(undefined); mockSnapClient.getPreferences.mockResolvedValue({ locale: 'en', } as GetPreferencesResult); mockTranslator.load.mockResolvedValue({}); }); - describe('shouldTrackError', () => { - it('returns false for canceled confirmation errors', () => { - expect( - shouldTrackError( - new UserActionError('User canceled the confirmation'), - mockLogger, - ), - ).toBe(false); - }); - - it('returns true for other errors', () => { - expect(shouldTrackError(new Error('boom'), mockLogger)).toBe(true); - expect( - shouldTrackError( - new UserActionError('Another user action'), - mockLogger, - ), - ).toBe(true); - }); - }); - describe('handle', () => { it('executes the function successfully', async () => { const mockFn = jest.fn().mockResolvedValue('success'); @@ -66,6 +52,7 @@ describe('HandlerMiddleware', () => { expect(mockSnapClient.getPreferences).toHaveBeenCalled(); expect(mockTranslator.load).toHaveBeenCalledWith('en'); expect(mockLogger.error).toHaveBeenCalledWith(error); + expect(mockTrackError).toHaveBeenCalledWith(error); }); it('tracks an unexpected Error before rethrowing it as a SnapError', async () => { @@ -73,16 +60,16 @@ describe('HandlerMiddleware', () => { const mockFn = jest.fn().mockRejectedValue(error); await expect(middleware.handle(mockFn)).rejects.toThrow('tracked boom'); - expect(mockSnapClient.emitTrackingError).toHaveBeenCalledWith(error); + expect(mockTrackError).toHaveBeenCalledWith(error); }); - it('continues to throw a SnapError when emitTrackingError fails', async () => { + it('continues to throw a SnapError when trackError is invoked', async () => { const error = new Error('boom after tracking failure'); const mockFn = jest.fn().mockRejectedValue(error); await expect(middleware.handle(mockFn)).rejects.toThrow(error); - expect(mockSnapClient.emitTrackingError).toHaveBeenCalledWith(error); + expect(mockTrackError).toHaveBeenCalledWith(error); expect(mockSnapClient.getPreferences).toHaveBeenCalled(); }); @@ -91,6 +78,7 @@ describe('HandlerMiddleware', () => { await expect(middleware.handle(mockFn)).rejects.toThrow('string failure'); expect(mockLogger.error).toHaveBeenCalledWith('string failure'); + expect(mockTrackError).toHaveBeenCalledWith('string failure'); }); it('wraps a thrown plain object by stringifying it', async () => { @@ -101,6 +89,7 @@ describe('HandlerMiddleware', () => { '[object Object]', ); expect(mockLogger.error).toHaveBeenCalledWith(thrown); + expect(mockTrackError).toHaveBeenCalledWith(thrown); }); it('uses the message property if it exists on a thrown plain object', async () => { @@ -111,6 +100,7 @@ describe('HandlerMiddleware', () => { 'InsufficientFunds', ); expect(mockLogger.error).toHaveBeenCalledWith(thrown); + expect(mockTrackError).toHaveBeenCalledWith(thrown); }); it('handles error successfully if instance of BaseError', async () => { @@ -124,7 +114,7 @@ describe('HandlerMiddleware', () => { expect(mockSnapClient.getPreferences).toHaveBeenCalled(); expect(mockTranslator.load).toHaveBeenCalledWith('en'); expect(mockLogger.error).toHaveBeenCalledWith(error, error.data); - expect(mockSnapClient.emitTrackingError).toHaveBeenCalledWith(error); + expect(mockTrackError).toHaveBeenCalledWith(error); }); it('includes the concrete external service failure in the returned error message', async () => { @@ -139,7 +129,7 @@ describe('HandlerMiddleware', () => { await expect(middleware.handle(mockFn)).rejects.toThrow( 'Connection error: Failed to synchronize account', ); - expect(mockSnapClient.emitTrackingError).toHaveBeenCalledWith(error); + expect(mockTrackError).toHaveBeenCalledWith(error); }); }); }); diff --git a/packages/bitcoin-wallet-snap/src/handlers/HandlerMiddleware.ts b/packages/bitcoin-wallet-snap/src/handlers/HandlerMiddleware.ts index 99faf8df..2b7a7c55 100644 --- a/packages/bitcoin-wallet-snap/src/handlers/HandlerMiddleware.ts +++ b/packages/bitcoin-wallet-snap/src/handlers/HandlerMiddleware.ts @@ -26,24 +26,7 @@ import { WalletError, AssertionError, } from '../entities'; - -/** - * Determines whether an error should be reported through `snap_trackError`. - * - * @param error - The error to evaluate. - * @param logger - logger for error - * @returns `true` when the error should be tracked. - */ -export function shouldTrackError(error: unknown, logger: Logger): boolean { - try { - return !( - (error as UserActionError)?.message === 'User canceled the confirmation' - ); - } catch { - logger.error(error, 'Failed to determine if error should be tracked'); - return false; - } -} +import { trackError } from '../utils/errors'; export class HandlerMiddleware { readonly #logger: Logger; @@ -62,9 +45,7 @@ export class HandlerMiddleware { try { return await fn(); } catch (error) { - if (shouldTrackError(error, this.#logger)) { - await this.#snapClient.emitTrackingError(error as Error); - } + await trackError(error); const { locale } = await this.#snapClient.getPreferences(); const messages = await this.#translator.load(locale); diff --git a/packages/bitcoin-wallet-snap/src/index.ts b/packages/bitcoin-wallet-snap/src/index.ts index 92076b77..c0e6036c 100644 --- a/packages/bitcoin-wallet-snap/src/index.ts +++ b/packages/bitcoin-wallet-snap/src/index.ts @@ -1,5 +1,4 @@ import { handleKeyringRequest } from '@metamask/keyring-snap-sdk/v2'; -import { Logger } from '@metamask/snap-networks-utils'; import type { OnAssetsConversionHandler, OnAssetsLookupHandler, @@ -37,9 +36,9 @@ import { ConfirmationUseCases, SendFlowUseCases, } from './use-cases'; +import logger from './utils/logger'; // Infra layer -const logger = new Logger({ level: Config.logLevel }); const snapClient = new SnapClientAdapter(logger, Config.encrypt); const chainClient = new EsploraClientAdapter(Config.chain); const assetRatesClient = new PriceApiClientAdapter(Config.priceApi); diff --git a/packages/bitcoin-wallet-snap/src/infra/SnapClientAdapter.test.ts b/packages/bitcoin-wallet-snap/src/infra/SnapClientAdapter.test.ts index a4498f87..95802e8b 100644 --- a/packages/bitcoin-wallet-snap/src/infra/SnapClientAdapter.test.ts +++ b/packages/bitcoin-wallet-snap/src/infra/SnapClientAdapter.test.ts @@ -4,6 +4,7 @@ import { mock } from 'jest-mock-extended'; import type { BitcoinAccount, Logger } from '../entities'; import { TrackingSnapEvent } from '../entities'; +import logger from '../utils/logger'; import { SnapClientAdapter } from './SnapClientAdapter'; jest.mock('@metamask/bitcoindevkit', () => ({ @@ -16,6 +17,13 @@ jest.mock('@metamask/bitcoindevkit', () => ({ }, })); +jest.mock('../utils/logger', () => ({ + __esModule: true, + default: { + error: jest.fn(), + }, +})); + const setupTest = () => { const mockLogger = mock(); const mockRequest = jest.fn(); @@ -27,10 +35,19 @@ const setupTest = () => { writable: true, }); - return { snapClient, mockLogger, mockRequest }; + return { + snapClient, + mockLogger, + mockRequest, + mockTrackingLogger: jest.mocked(logger), + }; }; describe('SnapClientAdapter', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('emitTrackingEvent', () => { it("doesn't throw and logs when event tracking fails", async () => { const { snapClient, mockLogger, mockRequest } = setupTest(); @@ -79,7 +96,7 @@ describe('SnapClientAdapter', () => { describe('emitTrackingError', () => { it('sends the tracking error payload to the snap client', async () => { - const { snapClient, mockRequest } = setupTest(); + const { snapClient, mockRequest, mockTrackingLogger } = setupTest(); const error = new Error('boom'); mockRequest.mockResolvedValue(undefined); @@ -90,19 +107,25 @@ describe('SnapClientAdapter', () => { method: 'snap_trackError', params: { error: getJsonError(error) }, }); + expect(mockTrackingLogger.error).not.toHaveBeenCalled(); }); it("doesn't break execution when error tracking fails", async () => { - const { snapClient, mockLogger, mockRequest } = setupTest(); + const { snapClient, mockRequest, mockTrackingLogger } = setupTest(); const error = new Error('boom'); const trackingError = new Error('track failed'); mockRequest.mockRejectedValue(trackingError); expect(await snapClient.emitTrackingError(error)).toBeUndefined(); - expect(mockLogger.error).toHaveBeenCalledWith( + + expect(mockRequest).toHaveBeenCalledWith({ + method: 'snap_trackError', + params: { error: getJsonError(error) }, + }); + expect(mockTrackingLogger.error).toHaveBeenCalledWith( + { error: trackingError }, 'Failed to track error', - trackingError, ); }); }); diff --git a/packages/bitcoin-wallet-snap/src/infra/SnapClientAdapter.ts b/packages/bitcoin-wallet-snap/src/infra/SnapClientAdapter.ts index d6efbe8a..955f1b57 100644 --- a/packages/bitcoin-wallet-snap/src/infra/SnapClientAdapter.ts +++ b/packages/bitcoin-wallet-snap/src/infra/SnapClientAdapter.ts @@ -12,7 +12,7 @@ import type { GetPreferencesResult, Json, } from '@metamask/snaps-sdk'; -import { DialogType, getJsonError } from '@metamask/snaps-sdk'; +import { DialogType } from '@metamask/snaps-sdk'; import type { BitcoinAccount, Logger, SnapClient } from '../entities'; import { @@ -27,6 +27,7 @@ import { networkToScope, } from '../handlers'; import { mapToTransaction } from '../handlers/mappings'; +import { trackError } from '../utils/errors'; export class SnapClientAdapter implements SnapClient { readonly #encrypt: boolean; @@ -286,14 +287,7 @@ export class SnapClientAdapter implements SnapClient { } async emitTrackingError(error: Error): Promise { - try { - await snap.request({ - method: 'snap_trackError', - params: { error: getJsonError(error) }, - }); - } catch (trackingError) { - this.#logger.error('Failed to track error', trackingError); - } + await trackError(error); } async startTrace(name: string): Promise { diff --git a/packages/bitcoin-wallet-snap/src/infra/getSnapProvider.ts b/packages/bitcoin-wallet-snap/src/infra/getSnapProvider.ts new file mode 100644 index 00000000..fffb00f1 --- /dev/null +++ b/packages/bitcoin-wallet-snap/src/infra/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/bitcoin-wallet-snap/src/utils/errors.test.ts b/packages/bitcoin-wallet-snap/src/utils/errors.test.ts new file mode 100644 index 00000000..5cc8b5e4 --- /dev/null +++ b/packages/bitcoin-wallet-snap/src/utils/errors.test.ts @@ -0,0 +1,117 @@ +import { UserRejectedRequestError } from '@metamask/snaps-sdk'; + +import { UserActionError } from '../entities'; +import { shouldTrackError, trackError } from './errors'; +import logger from './logger'; + +jest.mock('./logger', () => ({ + error: jest.fn(), +})); + +type SetupTestResult = { + mockSnapRequest: jest.Mock; + mockLogger: jest.Mocked; +}; + +const setupTest = (): SetupTestResult => { + jest.clearAllMocks(); + + const mockSnapRequest = jest.fn(); + Object.defineProperty(globalThis, 'snap', { + configurable: true, + value: { request: mockSnapRequest }, + writable: true, + }); + + return { mockSnapRequest, mockLogger: jest.mocked(logger) }; +}; + +describe('errors', () => { + describe('shouldTrackError', () => { + it('returns false for canceled confirmation errors', () => { + expect( + shouldTrackError(new UserActionError('User canceled the confirmation')), + ).toBe(false); + }); + + it('returns false for UserRejectedRequestError', () => { + expect(shouldTrackError(new UserRejectedRequestError())).toBe(false); + }); + + it('returns true for other errors', () => { + expect(shouldTrackError(new Error('boom'))).toBe(true); + expect(shouldTrackError(new UserActionError('Another user action'))).toBe( + true, + ); + }); + + it('returns false and logs when error inspection fails', () => { + const { mockLogger } = setupTest(); + const brokenError = { + get message(): string { + throw new Error('broken getter'); + }, + }; + + expect(shouldTrackError(brokenError)).toBe(false); + expect(mockLogger.error).toHaveBeenCalledWith( + expect.objectContaining({ message: 'broken getter' }), + 'Failed to determine if error should be tracked', + ); + }); + }); + + describe('trackError', () => { + it('does not track canceled confirmation UserActionError', async () => { + const { mockSnapRequest } = setupTest(); + + expect( + await trackError(new UserActionError('User canceled the confirmation')), + ).toBeUndefined(); + + expect(mockSnapRequest).not.toHaveBeenCalled(); + }); + + it('does not throw if error tracking fails', async () => { + const { mockLogger, mockSnapRequest } = setupTest(); + + const originalError = new Error('Test error'); + const trackingError = new Error('Tracking failed'); + mockSnapRequest.mockRejectedValue(trackingError); + + expect(await trackError(originalError)).toBeUndefined(); + + expect(mockSnapRequest).toHaveBeenCalledWith({ + method: 'snap_trackError', + params: { + error: expect.objectContaining({ + message: originalError.message, + }), + }, + }); + expect(mockLogger.error).toHaveBeenCalledWith( + { error: trackingError }, + 'Failed to track error', + ); + }); + + it('tracks errors', async () => { + const { mockLogger, mockSnapRequest } = setupTest(); + + const originalError = new Error('Test error'); + mockSnapRequest.mockResolvedValue('tracked-error-id'); + + expect(await trackError(originalError)).toBe('tracked-error-id'); + + expect(mockSnapRequest).toHaveBeenCalledWith({ + method: 'snap_trackError', + params: { + error: expect.objectContaining({ + message: originalError.message, + }), + }, + }); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/bitcoin-wallet-snap/src/utils/errors.ts b/packages/bitcoin-wallet-snap/src/utils/errors.ts new file mode 100644 index 00000000..329c9af5 --- /dev/null +++ b/packages/bitcoin-wallet-snap/src/utils/errors.ts @@ -0,0 +1,33 @@ +import { createTrackError } from '@metamask/snap-networks-utils'; +import { UserRejectedRequestError } from '@metamask/snaps-sdk'; + +import { UserActionError } from '../entities'; +import { getSnapProvider } from '../infra/getSnapProvider'; +import logger from './logger'; + +/** + * 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 { + try { + if (error instanceof UserRejectedRequestError) { + return false; + } + + return !( + (error as UserActionError)?.message === 'User canceled the confirmation' + ); + } catch (checkError) { + logger.error(checkError, 'Failed to determine if error should be tracked'); + return false; + } +} + +export const trackError = createTrackError({ + getSnapProvider, + logError: logger.error.bind(logger), + shouldTrack: shouldTrackError, +}); diff --git a/packages/bitcoin-wallet-snap/src/utils/logger.ts b/packages/bitcoin-wallet-snap/src/utils/logger.ts new file mode 100644 index 00000000..de72f67b --- /dev/null +++ b/packages/bitcoin-wallet-snap/src/utils/logger.ts @@ -0,0 +1,7 @@ +import { Logger } from '@metamask/snap-networks-utils'; + +import { Config } from '../config'; + +const logger = new Logger({ level: Config.logLevel }); + +export default logger; diff --git a/packages/solana-wallet-snap/snap.manifest.json b/packages/solana-wallet-snap/snap.manifest.json index 00dc32b0..a9821a00 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": "f67A1dy0bXl9jvoMuZ4heLBBs+m6bwaU20/WT30COeU=", + "shasum": "yB5xB25BZQ5HXseP96yOLBh6PHI5cfetfYCz594URnY=", "location": { "npm": { "filePath": "dist/bundle.js", @@ -16,9 +16,7 @@ "registry": "https://registry.npmjs.org/" } }, - "locales": [ - "locales/en.json" - ] + "locales": ["locales/en.json"] }, "initialConnections": { "https://portfolio.metamask.io": {} @@ -29,13 +27,9 @@ "snaps": false }, "endowment:keyring": { - "allowedOrigins": [ - "https://portfolio.metamask.io" - ], + "allowedOrigins": ["https://portfolio.metamask.io"], "capabilities": { - "scopes": [ - "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" - ], + "scopes": ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"], "privateKey": { "exportFormats": [ { @@ -52,11 +46,7 @@ }, "snap_getBip32Entropy": [ { - "path": [ - "m", - "44'", - "501'" - ], + "path": ["m", "44'", "501'"], "curve": "ed25519" } ], From 594d156438552073ae977383ef2cebb105968c85 Mon Sep 17 00:00:00 2001 From: Andrew Taran Date: Wed, 2 Sep 2026 15:47:05 +0200 Subject: [PATCH 6/6] chore: update jsdoc --- packages/bitcoin-wallet-snap/src/utils/errors.ts | 11 +++++++++++ packages/solana-wallet-snap/snap.manifest.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/bitcoin-wallet-snap/src/utils/errors.ts b/packages/bitcoin-wallet-snap/src/utils/errors.ts index 329c9af5..82c4d14f 100644 --- a/packages/bitcoin-wallet-snap/src/utils/errors.ts +++ b/packages/bitcoin-wallet-snap/src/utils/errors.ts @@ -26,6 +26,17 @@ export function shouldTrackError(error: unknown): boolean { } } +/** + * Tracks an error in MetaMask via Sentry (`snap_trackError`). + * + * Skips errors that {@link shouldTrackError} filters out. RPC failures are + * caught and logged but never rethrown, so this is safe to call from + * already-failing error-handling paths without 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 const trackError = createTrackError({ getSnapProvider, logError: logger.error.bind(logger), diff --git a/packages/solana-wallet-snap/snap.manifest.json b/packages/solana-wallet-snap/snap.manifest.json index a9821a00..ff45fdc1 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": "yB5xB25BZQ5HXseP96yOLBh6PHI5cfetfYCz594URnY=", + "shasum": "f67A1dy0bXl9jvoMuZ4heLBBs+m6bwaU20/WT30COeU=", "location": { "npm": { "filePath": "dist/bundle.js",