-
Notifications
You must be signed in to change notification settings - Fork 2
feat: move errors helpers into shared pkg #241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
taran-a
wants to merge
6
commits into
main
Choose a base branch
from
feat/move-errors-into-shared-pkg
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
617b0a7
feat: move withCatchAndThrowSnapErrorHandler into shared lib
taran-a dd0961b
chore: use shared withCatchAndThrowSnapError in tron snap
taran-a 92411be
chore: use shared withCatchAndThrowSnapError in solana snap
taran-a 52cdc8b
chore: use shared withCatchAndThrowSnapError in stellar snap
taran-a 2652df8
chore: fix lint errors
taran-a fc77f9f
feat: add optional logError override
taran-a File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
219 changes: 219 additions & 0 deletions
219
packages/snap-networks-utils/src/utils/errors/errors.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| import { | ||
| ChainDisconnectedError, | ||
| DisconnectedError, | ||
| InternalError, | ||
| InvalidInputError, | ||
| InvalidParamsError, | ||
| InvalidRequestError, | ||
| LimitExceededError, | ||
| MethodNotFoundError, | ||
| MethodNotSupportedError, | ||
| ParseError, | ||
| ResourceNotFoundError, | ||
| ResourceUnavailableError, | ||
| SnapError, | ||
| TransactionRejected, | ||
| UnauthorizedError, | ||
| UnsupportedMethodError, | ||
| UserRejectedRequestError, | ||
| } from '@metamask/snaps-sdk'; | ||
|
|
||
| import { mockLogger } from '../logger/__mocks__/Logger'; | ||
| import { createWithCatchAndThrowSnapError, normalizeError } from './errors'; | ||
| import type { CreateWithCatchAndThrowSnapErrorOptions } from './errors'; | ||
| import { isSnapRpcError } from './snapRpcError'; | ||
|
|
||
| type SetupTestResult = { | ||
| trackError: jest.Mock; | ||
| withCatchAndThrowSnapError: ReturnType< | ||
| typeof createWithCatchAndThrowSnapError | ||
| >; | ||
| createBoundWithCatchAndThrowSnapError: ( | ||
| options?: Omit<CreateWithCatchAndThrowSnapErrorOptions, 'logError'>, | ||
| ) => ReturnType<typeof createWithCatchAndThrowSnapError>; | ||
| }; | ||
|
|
||
| const setupTest = (): SetupTestResult => { | ||
| jest.clearAllMocks(); | ||
|
|
||
| const trackError = jest.fn(); | ||
| const withCatchAndThrowSnapError = createWithCatchAndThrowSnapError({ | ||
| logError: mockLogger.error.bind(mockLogger), | ||
| trackError, | ||
| }); | ||
|
|
||
| return { | ||
| trackError, | ||
| withCatchAndThrowSnapError, | ||
| createBoundWithCatchAndThrowSnapError: ( | ||
| options: Omit<CreateWithCatchAndThrowSnapErrorOptions, 'logError'> = { | ||
| trackError, | ||
| }, | ||
| ): ReturnType<typeof createWithCatchAndThrowSnapError> => | ||
| createWithCatchAndThrowSnapError({ | ||
| ...options, | ||
| logError: mockLogger.error.bind(mockLogger), | ||
| }), | ||
| }; | ||
| }; | ||
|
|
||
| describe('errors', () => { | ||
| describe('isSnapRpcError', () => { | ||
| it.each([ | ||
| new SnapError('Test error'), | ||
| new MethodNotFoundError(), | ||
| new UserRejectedRequestError(), | ||
| new MethodNotSupportedError(), | ||
| new ParseError(), | ||
| new ResourceNotFoundError(), | ||
| new ResourceUnavailableError(), | ||
| new TransactionRejected(), | ||
| new ChainDisconnectedError(), | ||
| new DisconnectedError(), | ||
| new UnauthorizedError(), | ||
| new UnsupportedMethodError(), | ||
| new InternalError(), | ||
| new InvalidInputError(), | ||
| new InvalidParamsError(), | ||
| new InvalidRequestError(), | ||
| new LimitExceededError(), | ||
| ])('returns true for Snap RPC errors', (error) => { | ||
| expect(isSnapRpcError(error)).toBe(true); | ||
| }); | ||
|
|
||
| it('returns false for generic errors', () => { | ||
| expect(isSnapRpcError(new Error('Unexpected error'))).toBe(false); | ||
| }); | ||
|
|
||
| it('returns false for non-error values', () => { | ||
| expect(isSnapRpcError('string')).toBe(false); | ||
| expect(isSnapRpcError(null)).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('normalizeError', () => { | ||
| it('preserves Snap RPC errors without wrapping', () => { | ||
| const originalError = new UserRejectedRequestError(); | ||
|
|
||
| expect(normalizeError(originalError)).toBe(originalError); | ||
| }); | ||
|
|
||
| it('wraps generic errors in SnapError', () => { | ||
| const originalError = new Error('Test error'); | ||
|
|
||
| const normalized = normalizeError(originalError); | ||
|
|
||
| expect(normalized).toBeInstanceOf(SnapError); | ||
| expect(normalized.message).toBe('Test error'); | ||
| }); | ||
|
|
||
| it('wraps non-Error values in SnapError', () => { | ||
| const normalized = normalizeError('string error'); | ||
|
|
||
| expect(normalized).toBeInstanceOf(SnapError); | ||
| expect(normalized.message).toBe('string error'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('createWithCatchAndThrowSnapError', () => { | ||
| it('returns the result when the function succeeds', async () => { | ||
| const { withCatchAndThrowSnapError } = setupTest(); | ||
| const mockFn = jest.fn().mockResolvedValue('success'); | ||
|
|
||
| const result = await withCatchAndThrowSnapError(mockFn); | ||
|
|
||
| expect(result).toBe('success'); | ||
| expect(mockFn).toHaveBeenCalledTimes(1); | ||
| expect(mockLogger.error).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('tracks, logs, and re-throws errors as SnapError', async () => { | ||
| const { trackError, withCatchAndThrowSnapError } = setupTest(); | ||
| const originalError = new Error('Test error'); | ||
| const mockFn = jest.fn().mockRejectedValue(originalError); | ||
|
|
||
| await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( | ||
| SnapError, | ||
| ); | ||
|
|
||
| expect(trackError).toHaveBeenCalledWith(originalError); | ||
| expect(mockLogger.error).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('preserves Snap RPC errors without wrapping', async () => { | ||
| const { trackError, withCatchAndThrowSnapError } = setupTest(); | ||
| const originalError = new UserRejectedRequestError(); | ||
| const mockFn = jest.fn().mockRejectedValue(originalError); | ||
|
|
||
| await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( | ||
| UserRejectedRequestError, | ||
| ); | ||
|
|
||
| expect(trackError).toHaveBeenCalledWith(originalError); | ||
| }); | ||
|
|
||
| it('handles non-Error objects and converts them to SnapError', async () => { | ||
| const { withCatchAndThrowSnapError } = setupTest(); | ||
| const mockFn = jest.fn().mockRejectedValue('string error'); | ||
|
|
||
| await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( | ||
| SnapError, | ||
| ); | ||
|
|
||
| expect(mockLogger.error).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('handles null errors', async () => { | ||
| const { withCatchAndThrowSnapError } = setupTest(); | ||
| const mockFn = jest.fn().mockRejectedValue(null); | ||
|
|
||
| await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( | ||
| SnapError, | ||
| ); | ||
|
|
||
| expect(mockLogger.error).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('preserves the original error message in the SnapError', async () => { | ||
| const { withCatchAndThrowSnapError } = setupTest(); | ||
| const originalError = new Error('Custom error message'); | ||
| const mockFn = jest.fn().mockRejectedValue(originalError); | ||
|
|
||
| await expect(withCatchAndThrowSnapError(mockFn)).rejects.toThrow( | ||
| 'Custom error message', | ||
| ); | ||
| }); | ||
|
|
||
| it('uses a custom normalizeErrorFn when provided', async () => { | ||
| const { trackError, createBoundWithCatchAndThrowSnapError } = setupTest(); | ||
| const customError = new MethodNotFoundError(); | ||
| const normalizeErrorFn = jest.fn().mockReturnValue(customError); | ||
| const bound = createBoundWithCatchAndThrowSnapError({ | ||
| trackError, | ||
| normalizeErrorFn, | ||
| }); | ||
| const mockFn = jest.fn().mockRejectedValue(new Error('Test error')); | ||
|
|
||
| await expect(bound(mockFn)).rejects.toThrow(MethodNotFoundError); | ||
|
|
||
| expect(normalizeErrorFn).toHaveBeenCalledWith( | ||
| expect.objectContaining({ message: 'Test error' }), | ||
| ); | ||
| }); | ||
|
|
||
| it('uses a custom logError when provided', async () => { | ||
| const { trackError, withCatchAndThrowSnapError } = setupTest(); | ||
| const customLogError = jest.fn(); | ||
| const originalError = new Error('Test error'); | ||
| const mockFn = jest.fn().mockRejectedValue(originalError); | ||
|
|
||
| await expect( | ||
| withCatchAndThrowSnapError(mockFn, customLogError), | ||
| ).rejects.toThrow(SnapError); | ||
|
|
||
| expect(customLogError).toHaveBeenCalledTimes(1); | ||
| expect(mockLogger.error).not.toHaveBeenCalled(); | ||
| expect(trackError).toHaveBeenCalledWith(originalError); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { SnapError, getErrorMessage } from '@metamask/snaps-sdk'; | ||
|
|
||
| import type { Logger } from '../logger/Logger'; | ||
| import { isSnapRpcError } from './snapRpcError'; | ||
| import type { SnapRpcError } from './snapRpcError'; | ||
|
|
||
| /** | ||
| * Sends an error to the snap's tracking transport (e.g. `snap_trackError`). | ||
| * Whether to invoke this for a given error in a given context is the caller's decision. | ||
| */ | ||
| export type TrackErrorFn = (error: unknown) => Promise<string | undefined>; | ||
|
|
||
| /** | ||
| * Converts a caught value into an error suitable for Snap RPC responses. | ||
| */ | ||
| export type NormalizeErrorFn = (error: unknown) => SnapRpcError; | ||
|
|
||
| /** | ||
| * Normalizes an unknown caught value into a Snap RPC error. | ||
| * | ||
| * Preserves existing Snap RPC errors; otherwise wraps the value in {@link SnapError}. | ||
| * | ||
| * @param error - The caught value. | ||
| * @returns A Snap RPC error. | ||
| */ | ||
| export function normalizeError(error: unknown): SnapRpcError { | ||
| return isSnapRpcError(error) | ||
| ? error | ||
| : new SnapError(error instanceof Error ? error : getErrorMessage(error)); | ||
| } | ||
|
|
||
| export type LogErrorFn = Logger['error']; | ||
|
|
||
| export type CreateWithCatchAndThrowSnapErrorOptions = { | ||
| logError: LogErrorFn; | ||
| trackError: TrackErrorFn; | ||
| normalizeErrorFn?: NormalizeErrorFn; | ||
| }; | ||
|
|
||
| /** | ||
| * Creates a handler-boundary error wrapper wired with logger, tracking, and optional error normalization. | ||
| * | ||
| * @param options - Logger, error-tracking transport, and optional custom normalizer. | ||
| * @param options.logError - Logger method used to record the normalized error. | ||
| * @param options.trackError - Snap-specific Sentry transport. | ||
| * @param options.normalizeErrorFn - Optional error normalizer; defaults to {@link normalizeError}. | ||
| * @returns A function that catches errors, tracks them, logs, and rethrows as Snap RPC errors. | ||
| */ | ||
| export function createWithCatchAndThrowSnapError({ | ||
| logError, | ||
| trackError, | ||
| normalizeErrorFn = normalizeError, | ||
| }: CreateWithCatchAndThrowSnapErrorOptions): <ResponseT>( | ||
| fn: () => Promise<ResponseT>, | ||
| logErrorOverride?: LogErrorFn, | ||
| ) => Promise<ResponseT> { | ||
| return <ResponseT>( | ||
| fn: () => Promise<ResponseT>, | ||
| logErrorOverride?: LogErrorFn, | ||
| ): Promise<ResponseT> => | ||
| withCatchAndThrowSnapErrorHandler( | ||
| logErrorOverride ?? logError, | ||
| trackError, | ||
| normalizeErrorFn, | ||
| fn, | ||
| ); | ||
| } | ||
|
|
||
| async function withCatchAndThrowSnapErrorHandler<ResponseT>( | ||
| logError: LogErrorFn, | ||
| trackError: TrackErrorFn, | ||
| normalizeErrorFn: NormalizeErrorFn, | ||
| fn: () => Promise<ResponseT>, | ||
| ): Promise<ResponseT> { | ||
| try { | ||
| return await fn(); | ||
| } catch (unknownError) { | ||
| await trackError(unknownError); | ||
|
|
||
| const error = normalizeErrorFn(unknownError); | ||
|
|
||
| logError( | ||
| { error }, | ||
| `[SnapError] ${JSON.stringify(error.toJSON(), null, 2)}`, | ||
| ); | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/only-throw-error -- Snap RPC errors are the handler boundary surface | ||
| throw error; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| export { createWithCatchAndThrowSnapError, normalizeError } from './errors'; | ||
| export { isSnapRpcError } from './snapRpcError'; | ||
| export type { | ||
| CreateWithCatchAndThrowSnapErrorOptions, | ||
| LogErrorFn, | ||
| NormalizeErrorFn, | ||
| TrackErrorFn, | ||
| } from './errors'; | ||
| export type { SnapRpcError } from './snapRpcError'; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it may have duplicate track error
when snap it self already track the error from deeper level?