Skip to content

Commit cd5b925

Browse files
committed
fix: handle errors on the edge
1 parent fefb2dd commit cd5b925

3 files changed

Lines changed: 123 additions & 189 deletions

File tree

packages/snap/snap.manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"url": "https://github.com/MetaMask/snap-solana-wallet.git"
88
},
99
"source": {
10-
"shasum": "5dayxdLk26hWbBj7hAl3wzDaPIPWYYsnsvQcIKuo8KA=",
10+
"shasum": "SNeUb5hAzMEExl6/sODXFY5l7FrC7cEFb2j2lL8PhGM=",
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",
Lines changed: 16 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,21 @@
1-
import {
2-
MethodNotFoundError,
3-
UserRejectedRequestError,
4-
MethodNotSupportedError,
5-
ParseError,
6-
ResourceNotFoundError,
7-
ResourceUnavailableError,
8-
TransactionRejected,
9-
ChainDisconnectedError,
10-
DisconnectedError,
11-
UnauthorizedError,
12-
UnsupportedMethodError,
13-
InternalError,
14-
InvalidInputError,
15-
InvalidParamsError,
16-
InvalidRequestError,
17-
LimitExceededError,
18-
SnapError,
19-
} from '@metamask/snaps-sdk';
1+
import { SnapError } from '@metamask/snaps-sdk';
202

21-
export class CustomError extends Error {
22-
name!: string;
3+
import logger from './logger';
234

24-
constructor(message?: string) {
25-
super(message);
5+
export const handle = async <ResponseT>(
6+
scope: string,
7+
fn: () => Promise<ResponseT>,
8+
): Promise<ResponseT> => {
9+
try {
10+
return await fn();
11+
} catch (errorInstance: any) {
12+
const error = new SnapError(errorInstance as Error);
2613

27-
// set error name as constructor name, make it not enumerable to keep native Error behavior
28-
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new.target#new.target_in_constructors
29-
// see https://github.com/adriengibrat/ts-custom-error/issues/30
30-
Object.defineProperty(this, 'name', {
31-
value: new.target.name,
32-
enumerable: false,
33-
configurable: true,
34-
});
14+
logger.error(
15+
{ error },
16+
`[${scope}] Error occurred: ${JSON.stringify(error.toJSON(), null, 2)}`,
17+
);
3518

36-
// fix the extended error prototype chain
37-
// because typescript __extends implementation can't
38-
// see https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
39-
Object.setPrototypeOf(this, new.target.prototype);
40-
// remove constructor from stack trace
41-
Error.captureStackTrace(this, this.constructor);
19+
throw error;
4220
}
43-
}
44-
45-
/**
46-
* Compacts an error to a specific error instance.
47-
*
48-
* @param error - The error instance to be compacted.
49-
* @param ErrCtor - The error constructor for the desired error instance.
50-
* @returns The compacted error instance.
51-
*/
52-
export function compactError<ErrorInstance extends Error>(
53-
error: ErrorInstance,
54-
ErrCtor: new (message?: string) => ErrorInstance,
55-
): ErrorInstance {
56-
if (error instanceof ErrCtor) {
57-
return error;
58-
}
59-
return new ErrCtor(error.message);
60-
}
61-
62-
/**
63-
* Determines if the given error is a Snap RPC error.
64-
*
65-
* @param error - The error instance to be checked.
66-
* @returns A boolean indicating whether the error is a Snap RPC error.
67-
*/
68-
export function isSnapRpcError(error: Error): boolean {
69-
const errors = [
70-
SnapError,
71-
MethodNotFoundError,
72-
UserRejectedRequestError,
73-
MethodNotSupportedError,
74-
MethodNotFoundError,
75-
ParseError,
76-
ResourceNotFoundError,
77-
ResourceUnavailableError,
78-
TransactionRejected,
79-
ChainDisconnectedError,
80-
DisconnectedError,
81-
UnauthorizedError,
82-
UnsupportedMethodError,
83-
InternalError,
84-
InvalidInputError,
85-
InvalidParamsError,
86-
InvalidRequestError,
87-
LimitExceededError,
88-
];
89-
return errors.some((errType) => error instanceof errType);
90-
}
21+
};

packages/snap/src/index.ts

Lines changed: 106 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import type {
1414
} from '@metamask/snaps-sdk';
1515
import {
1616
MethodNotFoundError,
17-
SnapError,
1817
type OnRpcRequestHandler,
1918
} from '@metamask/snaps-sdk';
2019
import { assert, enums } from '@metamask/superstruct';
@@ -29,7 +28,7 @@ import { CronjobMethod } from './core/handlers/onCronjob/cronjobs/CronjobMethod'
2928
import { onProtocolRequest as onProtocolRequestHandler } from './core/handlers/onProtocolRequest/onProtocolRequest';
3029
import { handlers as onRpcRequestHandlers } from './core/handlers/onRpcRequest';
3130
import { RpcRequestMethod } from './core/handlers/onRpcRequest/types';
32-
import { isSnapRpcError } from './core/utils/errors';
31+
import { handle } from './core/utils/errors';
3332
import { getClientStatus } from './core/utils/interface';
3433
import logger from './core/utils/logger';
3534
import { validateOrigin } from './core/validation/validators';
@@ -64,35 +63,27 @@ export const onRpcRequest: OnRpcRequestHandler = async ({
6463
origin,
6564
request,
6665
}) => {
67-
try {
68-
logger.log('[🔄 onRpcRequest]', request.method, request);
66+
logger.log('[🔄 onRpcRequest]', request.method, request);
6967

70-
const { method } = request;
68+
const { method } = request;
7169

72-
validateOrigin(origin, method);
70+
validateOrigin(origin, method);
7371

74-
const handler = onRpcRequestHandlers[method as RpcRequestMethod];
72+
const handler = onRpcRequestHandlers[method as RpcRequestMethod];
7573

76-
if (!handler) {
77-
throw new MethodNotFoundError(
78-
`RpcRequest method ${method} not found. Available methods: ${Object.values(
79-
RpcRequestMethod,
80-
).toString()}`,
81-
) as unknown as Error;
82-
}
74+
if (!handler) {
75+
throw new MethodNotFoundError(
76+
`RpcRequest method ${method} not found. Available methods: ${Object.values(
77+
RpcRequestMethod,
78+
).toString()}`,
79+
) as unknown as Error;
80+
}
8381

84-
return handler({ origin, request });
85-
} catch (error: any) {
86-
let snapError = error;
82+
const result = await handle('onRpcRequest', async () =>
83+
handler({ origin, request }),
84+
);
8785

88-
if (!isSnapRpcError(error)) {
89-
snapError = new SnapError(error);
90-
}
91-
logger.error(
92-
`onRpcRequest error: ${JSON.stringify(snapError.toJSON(), null, 2)}`,
93-
);
94-
throw snapError;
95-
}
86+
return result ?? null;
9687
};
9788

9889
/**
@@ -109,42 +100,25 @@ export const onKeyringRequest: OnKeyringRequestHandler = async ({
109100
origin,
110101
request,
111102
}): Promise<Json> => {
112-
try {
113-
logger.log('[🔑 onKeyringRequest]', request.method, request);
114-
115-
validateOrigin(origin, request.method);
116-
117-
// This is a temporal fix to prevent the swap/bridge functionality breaking
118-
// TODO: Remove this once changes in bridge-status-controller are in place
119-
if (
120-
request.method === KeyringRpcMethod.SubmitRequest &&
121-
request.params &&
122-
!('origin' in request.params)
123-
) {
124-
(request.params as Record<string, Json>).origin = 'https://metamask.io';
125-
}
126-
127-
return (await handleKeyringRequest(
128-
keyring,
129-
request,
130-
)) as unknown as Promise<Json>;
131-
} catch (error: any) {
132-
let snapError = error;
133-
134-
if (!isSnapRpcError(error)) {
135-
snapError = new SnapError(error);
136-
}
103+
logger.log('[🔑 onKeyringRequest]', request.method, request);
104+
105+
validateOrigin(origin, request.method);
106+
107+
// This is a temporal fix to prevent the swap/bridge functionality breaking
108+
// TODO: Remove this once changes in bridge-status-controller are in place
109+
if (
110+
request.method === KeyringRpcMethod.SubmitRequest &&
111+
request.params &&
112+
!('origin' in request.params)
113+
) {
114+
(request.params as Record<string, Json>).origin = 'https://metamask.io';
115+
}
137116

138-
logger.error(
139-
`onKeyringRequest - ${request.method} - Error: ${JSON.stringify(
140-
snapError.toJSON(),
141-
null,
142-
2,
143-
)}`,
144-
);
117+
const result = await handle('onKeyringRequest', async () =>
118+
handleKeyringRequest(keyring, request),
119+
);
145120

146-
throw snapError;
147-
}
121+
return result ?? null;
148122
};
149123

150124
/**
@@ -183,7 +157,9 @@ export const onUserInput: OnUserInputHandler = async ({
183157
return;
184158
}
185159

186-
await handler({ id, event, context, snapContext });
160+
await handle('onUserInput', async () =>
161+
handler({ id, event, context, snapContext }),
162+
);
187163
};
188164

189165
/**
@@ -207,59 +183,86 @@ export const onCronjob: OnCronjobHandler = async ({ request }) => {
207183
]),
208184
);
209185

210-
/**
211-
* Don't run cronjobs if client is locked or inactive
212-
* - We dont want to call cronjobs if the client is locked
213-
* - We Dont want to call cronjobs if the client is inactive (except if we havent run a cronjob in the last 15 minutes)
214-
*/
215-
const { locked, active } =
216-
(await getClientStatus()) as GetClientStatusResult & {
217-
active: boolean | undefined; // FIXME: Remove this once the snap SDK is updated
218-
};
186+
const result = await handle('onCronjob', async () => {
187+
/**
188+
* Don't run cronjobs if client is locked or inactive
189+
* - We dont want to call cronjobs if the client is locked
190+
* - We Dont want to call cronjobs if the client is inactive (except if we havent run a cronjob in the last 15 minutes)
191+
*/
192+
const { locked, active } =
193+
(await getClientStatus()) as GetClientStatusResult & {
194+
active: boolean | undefined; // FIXME: Remove this once the snap SDK is updated
195+
};
219196

220-
logger.log('[🔑 onCronjob] Client status', { locked, active });
221-
222-
if (locked) {
223-
return Promise.resolve();
224-
}
197+
logger.log('[🔑 onCronjob] Client status', { locked, active });
225198

226-
// explicit check for non-undefined active
227-
// to make sure the cronjob is executed if `active` is undefined
228-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
229-
if (active === false) {
230-
const lastCronjobRun = await state.getKey<number>('lastCronjobRun');
231-
const THIRTY_MINUTES = 30 * 60 * 1000; // 30 minutes in milliseconds
232-
233-
logger.log('[🔑 onCronjob] Last cronjob run', { lastCronjobRun });
234-
235-
// Only skip if we've run a cronjob in the last 30 minutes
236-
if (lastCronjobRun && Date.now() - lastCronjobRun < THIRTY_MINUTES) {
237-
logger.log(
238-
'[🔑 onCronjob] Skipping cronjob because it has been run in the last 30 minutes',
239-
);
199+
if (locked) {
240200
return Promise.resolve();
241201
}
242-
// if `lastCronjobRun` is undefined, we can run the cronjob
243-
}
244202

245-
await state.setKey('lastCronjobRun', Date.now());
203+
// explicit check for non-undefined active
204+
// to make sure the cronjob is executed if `active` is undefined
205+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
206+
if (active === false) {
207+
const lastCronjobRun = await state.getKey<number>('lastCronjobRun');
208+
const THIRTY_MINUTES = 30 * 60 * 1000; // 30 minutes in milliseconds
209+
210+
logger.log('[🔑 onCronjob] Last cronjob run', { lastCronjobRun });
211+
212+
// Only skip if we've run a cronjob in the last 30 minutes
213+
if (lastCronjobRun && Date.now() - lastCronjobRun < THIRTY_MINUTES) {
214+
logger.log(
215+
'[🔑 onCronjob] Skipping cronjob because it has been run in the last 30 minutes',
216+
);
217+
return Promise.resolve();
218+
}
219+
// if `lastCronjobRun` is undefined, we can run the cronjob
220+
}
221+
222+
await state.setKey('lastCronjobRun', Date.now());
223+
224+
logger.log('[🔑 onCronjob] Running cronjob', { method });
246225

247-
logger.log('[🔑 onCronjob] Running cronjob', { method });
226+
const handler = onCronjobHandlers[method];
227+
return handler({ request });
228+
});
248229

249-
const handler = onCronjobHandlers[method];
250-
return handler({ request });
230+
return result ?? null;
251231
};
252232

253-
export const onAssetsLookup: OnAssetsLookupHandler = onAssetsLookupHandler;
233+
export const onAssetsLookup: OnAssetsLookupHandler = async (params) => {
234+
const result = await handle('onAssetsLookup', async () =>
235+
onAssetsLookupHandler(params),
236+
);
237+
return result ?? null;
238+
};
254239

255-
export const onAssetsConversion: OnAssetsConversionHandler =
256-
onAssetsConversionHandler;
240+
export const onAssetsConversion: OnAssetsConversionHandler = async (params) => {
241+
const result = await handle('onAssetsConversion', async () =>
242+
onAssetsConversionHandler(params),
243+
);
244+
return result ?? null;
245+
};
257246

258-
export const onProtocolRequest: OnProtocolRequestHandler =
259-
onProtocolRequestHandler;
247+
export const onProtocolRequest: OnProtocolRequestHandler = async (params) => {
248+
const result = await handle('onProtocolRequest', async () =>
249+
onProtocolRequestHandler(params),
250+
);
251+
return result ?? null;
252+
};
260253

261-
export const onAssetHistoricalPrice: OnAssetHistoricalPriceHandler =
262-
onAssetHistoricalPriceHandler;
254+
export const onAssetHistoricalPrice: OnAssetHistoricalPriceHandler = async (
255+
params,
256+
) => {
257+
const result = await handle('onAssetHistoricalPrice', async () =>
258+
onAssetHistoricalPriceHandler(params),
259+
);
260+
return result ?? null;
261+
};
263262

264-
export const onClientRequest: OnClientRequestHandler = async ({ request }) =>
265-
clientRequestHandler.handle(request);
263+
export const onClientRequest: OnClientRequestHandler = async ({ request }) => {
264+
const result = await handle('onClientRequest', async () =>
265+
clientRequestHandler.handle(request),
266+
);
267+
return result ?? null;
268+
};

0 commit comments

Comments
 (0)