From d360f8ea922863eb116005fefbe5baea363cf9e2 Mon Sep 17 00:00:00 2001 From: Maarten Zuidhoorn Date: Fri, 10 Nov 2023 14:38:21 +0100 Subject: [PATCH] BREAKING: Move `SnapError` to SDK (#1949) This moves `SnapError` and some used utility functions to `snaps-sdk`. ## Breaking changes - `SnapError` was moved from `snaps-utils` to `snaps-sdk`. --- .../packages/bip32/snap.manifest.json | 2 +- .../packages/bip44/snap.manifest.json | 2 +- .../packages/dialogs/snap.manifest.json | 2 +- .../packages/get-entropy/snap.manifest.json | 2 +- .../packages/core-signer/snap.manifest.json | 2 +- .../packages/manage-state/snap.manifest.json | 2 +- .../packages/notifications/snap.manifest.json | 2 +- .../transaction-insights/snap.manifest.json | 2 +- .../src/snaps/SnapController.ts | 3 +- .../lavamoat/browserify/iframe/policy.json | 21 +- .../browserify/node-process/policy.json | 21 +- .../browserify/node-thread/policy.json | 21 +- .../lavamoat/browserify/offscreen/policy.json | 21 +- .../browserify/worker-executor/policy.json | 21 +- .../browserify/worker-pool/policy.json | 21 +- .../src/common/BaseSnapExecutor.ts | 2 +- packages/snaps-sdk/package.json | 1 + packages/snaps-sdk/src/errors.test.ts | 293 ++++++++++++++ packages/snaps-sdk/src/errors.ts | 151 +++++++ packages/snaps-sdk/src/index.ts | 8 + .../snaps-sdk/src/internals/errors.test.ts | 79 ++++ packages/snaps-sdk/src/internals/errors.ts | 85 ++++ packages/snaps-sdk/src/internals/index.ts | 1 + .../src/features/simulation/sagas.test.ts | 2 +- packages/snaps-types/package.json | 1 - packages/snaps-types/src/types.ts | 1 - packages/snaps-utils/coverage.json | 8 +- packages/snaps-utils/src/errors.test.ts | 367 +----------------- packages/snaps-utils/src/errors.ts | 222 +---------- packages/snaps-utils/src/localization.ts | 2 +- packages/snaps-utils/src/manifest/manifest.ts | 2 +- packages/snaps-webpack-plugin/package.json | 1 + packages/snaps-webpack-plugin/src/plugin.ts | 2 +- .../snaps-webpack-plugin/tsconfig.build.json | 3 + packages/snaps-webpack-plugin/tsconfig.json | 3 + yarn.lock | 3 +- 36 files changed, 778 insertions(+), 604 deletions(-) create mode 100644 packages/snaps-sdk/src/errors.test.ts create mode 100644 packages/snaps-sdk/src/errors.ts create mode 100644 packages/snaps-sdk/src/internals/errors.test.ts create mode 100644 packages/snaps-sdk/src/internals/errors.ts diff --git a/packages/examples/packages/bip32/snap.manifest.json b/packages/examples/packages/bip32/snap.manifest.json index a25539c93c..23eff1e664 100644 --- a/packages/examples/packages/bip32/snap.manifest.json +++ b/packages/examples/packages/bip32/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "7z2/4sDVSx8+xUi9dVoD9v/o10rIhzxLfv7G1pfxGFw=", + "shasum": "a6nSBZXDUyWbimLESJxAVZRqTWFy5YXXO/+Si9nhLNY=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/bip44/snap.manifest.json b/packages/examples/packages/bip44/snap.manifest.json index aa71c7b303..300f43caa5 100644 --- a/packages/examples/packages/bip44/snap.manifest.json +++ b/packages/examples/packages/bip44/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "v9l9sN0gm3hxCFfQWRjV1p+5D05NIrFvjRozzVwFUpM=", + "shasum": "kC7naDIca71EEh+FvGppoEgpkdOCNzgSC2noYtZVgKk=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/dialogs/snap.manifest.json b/packages/examples/packages/dialogs/snap.manifest.json index f1a874ab0c..55a9e666ea 100644 --- a/packages/examples/packages/dialogs/snap.manifest.json +++ b/packages/examples/packages/dialogs/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "bG5IXFSsasgMlDWgtj3I5iZY2D16sO8nRBCf61lSJK0=", + "shasum": "p1WpZhW7JUQMqqNVoMPpK3kRu0bN2TLBFMU4IU0ayDA=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/get-entropy/snap.manifest.json b/packages/examples/packages/get-entropy/snap.manifest.json index 5fd2b355c2..e37aa313a7 100644 --- a/packages/examples/packages/get-entropy/snap.manifest.json +++ b/packages/examples/packages/get-entropy/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "uDuEEIVV6EC+OXaBNvU6+4ZKxUQ3E3dDkS0G58YB9N0=", + "shasum": "WcUEK4cnEAv1J4efCUFaXxK4XvzzSIYf9j8qscNOOQ0=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json b/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json index 6e0fd55df3..0002bece21 100644 --- a/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json +++ b/packages/examples/packages/invoke-snap/packages/core-signer/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "lWhar69WgLA0R5em8DwBvk2Yt8ZDf8SODJ5Dad7uHvw=", + "shasum": "mGKJ1s3TMRHRpWEB274UktA7EBsNRbVfWwnms6BE434=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/manage-state/snap.manifest.json b/packages/examples/packages/manage-state/snap.manifest.json index f5f8fdb92e..47118ad73e 100644 --- a/packages/examples/packages/manage-state/snap.manifest.json +++ b/packages/examples/packages/manage-state/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "SzHiX2GC8aoBExEgj5G09/89Lu6O54VcSsL3mWD3jjs=", + "shasum": "zP59BnhJYduugozloTON06CZSbzVAE1h3ktZs+qK94s=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/notifications/snap.manifest.json b/packages/examples/packages/notifications/snap.manifest.json index 86b5254694..adf16b273b 100644 --- a/packages/examples/packages/notifications/snap.manifest.json +++ b/packages/examples/packages/notifications/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "Zkp4KW44svcE/TeX2RccTLWaj5GYnksM+hp1KYP3sUQ=", + "shasum": "uRjhwtFYVPOgRw1VrLKnUlymmVCA0E37WCldywWvRds=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/transaction-insights/snap.manifest.json b/packages/examples/packages/transaction-insights/snap.manifest.json index e1bb90a222..70a8bf5794 100644 --- a/packages/examples/packages/transaction-insights/snap.manifest.json +++ b/packages/examples/packages/transaction-insights/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "A2ry7NMMa+c8vSduxDSDzcKXk8qBIU7JChnrnyy7/nk=", + "shasum": "nA7PTqhnqnS5xJ+V9Pkql2BN4t2wMb3q38esxkTuOus=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snaps-controllers/src/snaps/SnapController.ts b/packages/snaps-controllers/src/snaps/SnapController.ts index 34761d3db6..b632fe0472 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.ts +++ b/packages/snaps-controllers/src/snaps/SnapController.ts @@ -36,7 +36,7 @@ import type { RequestSnapsResult, SnapId, } from '@metamask/snaps-sdk'; -import { AuxiliaryFileEncoding } from '@metamask/snaps-sdk'; +import { AuxiliaryFileEncoding, getErrorMessage } from '@metamask/snaps-sdk'; import { assertUILinksAreSafe } from '@metamask/snaps-ui'; import type { FetchedSnapFiles, @@ -56,7 +56,6 @@ import { DEFAULT_ENDOWMENTS, DEFAULT_REQUESTED_SNAP_VERSION, encodeAuxiliaryFile, - getErrorMessage, HandlerType, isOriginAllowed, logError, diff --git a/packages/snaps-execution-environments/lavamoat/browserify/iframe/policy.json b/packages/snaps-execution-environments/lavamoat/browserify/iframe/policy.json index 0ddfbddc15..e04a3034dd 100644 --- a/packages/snaps-execution-environments/lavamoat/browserify/iframe/policy.json +++ b/packages/snaps-execution-environments/lavamoat/browserify/iframe/policy.json @@ -308,11 +308,29 @@ "eslint>debug>ms": true } }, + "external:../snaps-sdk/src/errors.ts": { + "packages": { + "external:../snaps-sdk/src/internals/index.ts": true + } + }, "external:../snaps-sdk/src/index.ts": { "packages": { + "external:../snaps-sdk/src/errors.ts": true, + "external:../snaps-sdk/src/internals/index.ts": true, "external:../snaps-sdk/src/types/index.ts": true } }, + "external:../snaps-sdk/src/internals/errors.ts": { + "packages": { + "@metamask/utils": true + } + }, + "external:../snaps-sdk/src/internals/index.ts": { + "packages": { + "external:../snaps-sdk/src/internals/errors.ts": true, + "external:../snaps-sdk/src/internals/helpers.ts": true + } + }, "external:../snaps-sdk/src/types/handlers/index.ts": { "packages": { "external:../snaps-sdk/src/types/handlers/cronjob.ts": true, @@ -386,7 +404,8 @@ "external:../snaps-utils/src/errors.ts": { "packages": { "@metamask/rpc-errors": true, - "@metamask/utils": true + "@metamask/utils": true, + "external:../snaps-sdk/src/index.ts": true } }, "external:../snaps-utils/src/handlers.ts": { diff --git a/packages/snaps-execution-environments/lavamoat/browserify/node-process/policy.json b/packages/snaps-execution-environments/lavamoat/browserify/node-process/policy.json index 895f5aa5e1..697bcb2d1e 100644 --- a/packages/snaps-execution-environments/lavamoat/browserify/node-process/policy.json +++ b/packages/snaps-execution-environments/lavamoat/browserify/node-process/policy.json @@ -372,11 +372,29 @@ "util": true } }, + "external:../snaps-sdk/src/errors.ts": { + "packages": { + "external:../snaps-sdk/src/internals/index.ts": true + } + }, "external:../snaps-sdk/src/index.ts": { "packages": { + "external:../snaps-sdk/src/errors.ts": true, + "external:../snaps-sdk/src/internals/index.ts": true, "external:../snaps-sdk/src/types/index.ts": true } }, + "external:../snaps-sdk/src/internals/errors.ts": { + "packages": { + "@metamask/utils": true + } + }, + "external:../snaps-sdk/src/internals/index.ts": { + "packages": { + "external:../snaps-sdk/src/internals/errors.ts": true, + "external:../snaps-sdk/src/internals/helpers.ts": true + } + }, "external:../snaps-sdk/src/types/handlers/index.ts": { "packages": { "external:../snaps-sdk/src/types/handlers/cronjob.ts": true, @@ -450,7 +468,8 @@ "external:../snaps-utils/src/errors.ts": { "packages": { "@metamask/rpc-errors": true, - "@metamask/utils": true + "@metamask/utils": true, + "external:../snaps-sdk/src/index.ts": true } }, "external:../snaps-utils/src/handlers.ts": { diff --git a/packages/snaps-execution-environments/lavamoat/browserify/node-thread/policy.json b/packages/snaps-execution-environments/lavamoat/browserify/node-thread/policy.json index 895f5aa5e1..697bcb2d1e 100644 --- a/packages/snaps-execution-environments/lavamoat/browserify/node-thread/policy.json +++ b/packages/snaps-execution-environments/lavamoat/browserify/node-thread/policy.json @@ -372,11 +372,29 @@ "util": true } }, + "external:../snaps-sdk/src/errors.ts": { + "packages": { + "external:../snaps-sdk/src/internals/index.ts": true + } + }, "external:../snaps-sdk/src/index.ts": { "packages": { + "external:../snaps-sdk/src/errors.ts": true, + "external:../snaps-sdk/src/internals/index.ts": true, "external:../snaps-sdk/src/types/index.ts": true } }, + "external:../snaps-sdk/src/internals/errors.ts": { + "packages": { + "@metamask/utils": true + } + }, + "external:../snaps-sdk/src/internals/index.ts": { + "packages": { + "external:../snaps-sdk/src/internals/errors.ts": true, + "external:../snaps-sdk/src/internals/helpers.ts": true + } + }, "external:../snaps-sdk/src/types/handlers/index.ts": { "packages": { "external:../snaps-sdk/src/types/handlers/cronjob.ts": true, @@ -450,7 +468,8 @@ "external:../snaps-utils/src/errors.ts": { "packages": { "@metamask/rpc-errors": true, - "@metamask/utils": true + "@metamask/utils": true, + "external:../snaps-sdk/src/index.ts": true } }, "external:../snaps-utils/src/handlers.ts": { diff --git a/packages/snaps-execution-environments/lavamoat/browserify/offscreen/policy.json b/packages/snaps-execution-environments/lavamoat/browserify/offscreen/policy.json index d70f1fc702..8feda177b1 100644 --- a/packages/snaps-execution-environments/lavamoat/browserify/offscreen/policy.json +++ b/packages/snaps-execution-environments/lavamoat/browserify/offscreen/policy.json @@ -142,11 +142,29 @@ "eslint>debug>ms": true } }, + "external:../snaps-sdk/src/errors.ts": { + "packages": { + "external:../snaps-sdk/src/internals/index.ts": true + } + }, "external:../snaps-sdk/src/index.ts": { "packages": { + "external:../snaps-sdk/src/errors.ts": true, + "external:../snaps-sdk/src/internals/index.ts": true, "external:../snaps-sdk/src/types/index.ts": true } }, + "external:../snaps-sdk/src/internals/errors.ts": { + "packages": { + "@metamask/utils": true + } + }, + "external:../snaps-sdk/src/internals/index.ts": { + "packages": { + "external:../snaps-sdk/src/internals/errors.ts": true, + "external:../snaps-sdk/src/internals/helpers.ts": true + } + }, "external:../snaps-sdk/src/types/handlers/index.ts": { "packages": { "external:../snaps-sdk/src/types/handlers/cronjob.ts": true, @@ -220,7 +238,8 @@ "external:../snaps-utils/src/errors.ts": { "packages": { "@metamask/rpc-errors": true, - "@metamask/utils": true + "@metamask/utils": true, + "external:../snaps-sdk/src/index.ts": true } }, "external:../snaps-utils/src/handlers.ts": { diff --git a/packages/snaps-execution-environments/lavamoat/browserify/worker-executor/policy.json b/packages/snaps-execution-environments/lavamoat/browserify/worker-executor/policy.json index 0ddfbddc15..e04a3034dd 100644 --- a/packages/snaps-execution-environments/lavamoat/browserify/worker-executor/policy.json +++ b/packages/snaps-execution-environments/lavamoat/browserify/worker-executor/policy.json @@ -308,11 +308,29 @@ "eslint>debug>ms": true } }, + "external:../snaps-sdk/src/errors.ts": { + "packages": { + "external:../snaps-sdk/src/internals/index.ts": true + } + }, "external:../snaps-sdk/src/index.ts": { "packages": { + "external:../snaps-sdk/src/errors.ts": true, + "external:../snaps-sdk/src/internals/index.ts": true, "external:../snaps-sdk/src/types/index.ts": true } }, + "external:../snaps-sdk/src/internals/errors.ts": { + "packages": { + "@metamask/utils": true + } + }, + "external:../snaps-sdk/src/internals/index.ts": { + "packages": { + "external:../snaps-sdk/src/internals/errors.ts": true, + "external:../snaps-sdk/src/internals/helpers.ts": true + } + }, "external:../snaps-sdk/src/types/handlers/index.ts": { "packages": { "external:../snaps-sdk/src/types/handlers/cronjob.ts": true, @@ -386,7 +404,8 @@ "external:../snaps-utils/src/errors.ts": { "packages": { "@metamask/rpc-errors": true, - "@metamask/utils": true + "@metamask/utils": true, + "external:../snaps-sdk/src/index.ts": true } }, "external:../snaps-utils/src/handlers.ts": { diff --git a/packages/snaps-execution-environments/lavamoat/browserify/worker-pool/policy.json b/packages/snaps-execution-environments/lavamoat/browserify/worker-pool/policy.json index d70f1fc702..8feda177b1 100644 --- a/packages/snaps-execution-environments/lavamoat/browserify/worker-pool/policy.json +++ b/packages/snaps-execution-environments/lavamoat/browserify/worker-pool/policy.json @@ -142,11 +142,29 @@ "eslint>debug>ms": true } }, + "external:../snaps-sdk/src/errors.ts": { + "packages": { + "external:../snaps-sdk/src/internals/index.ts": true + } + }, "external:../snaps-sdk/src/index.ts": { "packages": { + "external:../snaps-sdk/src/errors.ts": true, + "external:../snaps-sdk/src/internals/index.ts": true, "external:../snaps-sdk/src/types/index.ts": true } }, + "external:../snaps-sdk/src/internals/errors.ts": { + "packages": { + "@metamask/utils": true + } + }, + "external:../snaps-sdk/src/internals/index.ts": { + "packages": { + "external:../snaps-sdk/src/internals/errors.ts": true, + "external:../snaps-sdk/src/internals/helpers.ts": true + } + }, "external:../snaps-sdk/src/types/handlers/index.ts": { "packages": { "external:../snaps-sdk/src/types/handlers/cronjob.ts": true, @@ -220,7 +238,8 @@ "external:../snaps-utils/src/errors.ts": { "packages": { "@metamask/rpc-errors": true, - "@metamask/utils": true + "@metamask/utils": true, + "external:../snaps-sdk/src/index.ts": true } }, "external:../snaps-utils/src/handlers.ts": { diff --git a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts index 5f6d75fd24..dcd036411e 100644 --- a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts +++ b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.ts @@ -5,6 +5,7 @@ import { StreamProvider } from '@metamask/providers'; import type { RequestArguments } from '@metamask/providers/dist/BaseProvider'; import { errorCodes, rpcErrors, serializeError } from '@metamask/rpc-errors'; import type { SnapsProvider } from '@metamask/snaps-sdk'; +import { getErrorData } from '@metamask/snaps-sdk'; import type { SnapExports, HandlerType, @@ -15,7 +16,6 @@ import { logError, SNAP_EXPORTS, WrappedSnapError, - getErrorData, unwrapError, } from '@metamask/snaps-utils'; import type { diff --git a/packages/snaps-sdk/package.json b/packages/snaps-sdk/package.json index ae9c42ad58..cdbfb3a49d 100644 --- a/packages/snaps-sdk/package.json +++ b/packages/snaps-sdk/package.json @@ -47,6 +47,7 @@ "@metamask/eslint-config-jest": "^12.1.0", "@metamask/eslint-config-nodejs": "^12.1.0", "@metamask/eslint-config-typescript": "^12.1.0", + "@metamask/rpc-errors": "^6.1.0", "@swc/cli": "^0.1.62", "@swc/core": "1.3.78", "@swc/jest": "^0.2.26", diff --git a/packages/snaps-sdk/src/errors.test.ts b/packages/snaps-sdk/src/errors.test.ts new file mode 100644 index 0000000000..3baef080b0 --- /dev/null +++ b/packages/snaps-sdk/src/errors.test.ts @@ -0,0 +1,293 @@ +import { rpcErrors } from '@metamask/rpc-errors'; + +import { SnapError } from './errors'; + +describe('SnapError', () => { + it('creates an error from a message', () => { + const error = new SnapError('foo'); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SnapError); + expect(error.message).toBe('foo'); + expect(error.code).toBe(-32603); + expect(error.data).toStrictEqual({}); + expect(error.stack).toBeDefined(); + expect(error.toJSON()).toStrictEqual({ + code: -31002, + message: 'Snap Error', + data: { + cause: { + code: -32603, + message: 'foo', + stack: error.stack, + data: {}, + }, + }, + }); + }); + + it('creates an error from a message and code', () => { + const error = new SnapError({ + message: 'foo', + code: -32000, + }); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SnapError); + expect(error.message).toBe('foo'); + expect(error.code).toBe(-32000); + expect(error.data).toStrictEqual({}); + expect(error.stack).toBeDefined(); + expect(error.toJSON()).toStrictEqual({ + code: -31002, + message: 'Snap Error', + data: { + cause: { + code: -32000, + message: 'foo', + stack: error.stack, + data: {}, + }, + }, + }); + }); + + it('creates an error from a message and data', () => { + const error = new SnapError('foo', { foo: 'bar' }); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SnapError); + expect(error.message).toBe('foo'); + expect(error.code).toBe(-32603); + expect(error.data).toStrictEqual({ foo: 'bar' }); + expect(error.stack).toBeDefined(); + }); + + it('creates an error from a message, code, and data', () => { + const error = new SnapError( + { + message: 'foo', + code: -32000, + }, + { foo: 'bar' }, + ); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SnapError); + expect(error.message).toBe('foo'); + expect(error.code).toBe(-32000); + expect(error.data).toStrictEqual({ foo: 'bar' }); + expect(error.stack).toBeDefined(); + expect(error.toJSON()).toStrictEqual({ + code: -31002, + message: 'Snap Error', + data: { + cause: { + code: -32000, + message: 'foo', + stack: error.stack, + data: { + foo: 'bar', + }, + }, + }, + }); + }); + + it('creates an error from an error', () => { + const error = new SnapError(new Error('foo')); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SnapError); + expect(error.message).toBe('foo'); + expect(error.code).toBe(-32603); + expect(error.data).toStrictEqual({}); + expect(error.stack).toBeDefined(); + expect(error.toJSON()).toStrictEqual({ + code: -31002, + message: 'Snap Error', + data: { + cause: { + code: -32603, + message: 'foo', + stack: error.stack, + data: {}, + }, + }, + }); + }); + + it('creates an error from an error and data', () => { + const error = new SnapError(new Error('foo'), { foo: 'bar' }); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SnapError); + expect(error.message).toBe('foo'); + expect(error.code).toBe(-32603); + expect(error.data).toStrictEqual({ foo: 'bar' }); + expect(error.stack).toBeDefined(); + expect(error.toJSON()).toStrictEqual({ + code: -31002, + message: 'Snap Error', + data: { + cause: { + code: -32603, + message: 'foo', + stack: error.stack, + data: { + foo: 'bar', + }, + }, + }, + }); + }); + + it('creates an error from a JsonRpcError', () => { + const error = new SnapError(rpcErrors.invalidParams('foo')); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SnapError); + expect(error.message).toBe('foo'); + expect(error.code).toBe(-32602); + expect(error.data).toStrictEqual({}); + expect(error.stack).toBeDefined(); + expect(error.toJSON()).toStrictEqual({ + code: -31002, + message: 'Snap Error', + data: { + cause: { + code: -32602, + message: 'foo', + stack: error.stack, + data: {}, + }, + }, + }); + }); + + it('creates an error from a JsonRpcError and data', () => { + const error = new SnapError(rpcErrors.invalidParams('foo'), { + foo: 'bar', + }); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SnapError); + expect(error.message).toBe('foo'); + expect(error.code).toBe(-32602); + expect(error.data).toStrictEqual({ foo: 'bar' }); + expect(error.stack).toBeDefined(); + expect(error.toJSON()).toStrictEqual({ + code: -31002, + message: 'Snap Error', + data: { + cause: { + code: -32602, + message: 'foo', + stack: error.stack, + data: { + foo: 'bar', + }, + }, + }, + }); + }); + + it('creates an error from a JsonRpcError with a code of 0', () => { + const error = new SnapError({ + message: 'foo', + code: 0, + }); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SnapError); + expect(error.message).toBe('foo'); + expect(error.code).toBe(0); + expect(error.data).toStrictEqual({}); + expect(error.stack).toBeDefined(); + expect(error.toJSON()).toStrictEqual({ + code: -31002, + message: 'Snap Error', + data: { + cause: { + code: 0, + message: 'foo', + stack: error.stack, + data: {}, + }, + }, + }); + }); + + it('creates an error from a JsonRpcError with a code of 0 and data', () => { + const error = new SnapError( + { + message: 'foo', + code: 0, + }, + { foo: 'bar' }, + ); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SnapError); + expect(error.message).toBe('foo'); + expect(error.code).toBe(0); + expect(error.data).toStrictEqual({ foo: 'bar' }); + expect(error.stack).toBeDefined(); + expect(error.toJSON()).toStrictEqual({ + code: -31002, + message: 'Snap Error', + data: { + cause: { + code: 0, + message: 'foo', + stack: error.stack, + data: { + foo: 'bar', + }, + }, + }, + }); + }); + + it('creates an error from a JsonRpcError with a code of 0 and merges the data', () => { + const error = new SnapError( + { + message: 'foo', + code: 0, + data: { + foo: 'baz', + bar: 'qux', + }, + }, + { foo: 'bar' }, + ); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(SnapError); + expect(error.message).toBe('foo'); + expect(error.code).toBe(0); + expect(error.data).toStrictEqual({ foo: 'bar', bar: 'qux' }); + expect(error.stack).toBeDefined(); + expect(error.toJSON()).toStrictEqual({ + code: -31002, + message: 'Snap Error', + data: { + cause: { + code: 0, + message: 'foo', + stack: error.stack, + data: { + foo: 'bar', + bar: 'qux', + }, + }, + }, + }); + }); + + it('serializes an error to JSON', () => { + const error = new SnapError('foo'); + + expect(error.serialize()).toStrictEqual(error.toJSON()); + }); +}); diff --git a/packages/snaps-sdk/src/errors.ts b/packages/snaps-sdk/src/errors.ts new file mode 100644 index 0000000000..93f3c80f31 --- /dev/null +++ b/packages/snaps-sdk/src/errors.ts @@ -0,0 +1,151 @@ +import type { Json, JsonRpcError } from '@metamask/utils'; + +import { + getErrorCode, + getErrorData, + getErrorMessage, + SNAP_ERROR_CODE, + SNAP_ERROR_MESSAGE, +} from './internals'; + +/** + * A generic error which can be thrown by a Snap, without it causing the Snap to + * crash. + */ +export class SnapError extends Error { + readonly #code: number; + + readonly #message: string; + + readonly #data: Record; + + readonly #stack?: string; + + /** + * Create a new `SnapError`. + * + * @param error - The error to create the `SnapError` from. If this is a + * `string`, it will be used as the error message. If this is an `Error`, its + * `message` property will be used as the error message. If this is a + * `JsonRpcError`, its `message` property will be used as the error message + * and its `code` property will be used as the error code. Otherwise, the + * error will be converted to a string and used as the error message. + * @param data - Additional data to include in the error. This will be merged + * with the error data, if any. + */ + constructor( + error: string | Error | JsonRpcError, + data: Record = {}, + ) { + const message = getErrorMessage(error); + super(message); + + this.#message = message; + this.#code = getErrorCode(error); + this.#data = { ...getErrorData(error), ...data }; + this.#stack = super.stack; + } + + /** + * The error name. + * + * @returns The error name. + */ + get name() { + return 'SnapError'; + } + + /** + * The error code. + * + * @returns The error code. + */ + get code() { + return this.#code; + } + + /** + * The error message. + * + * @returns The error message. + */ + // This line is covered, but Jest doesn't pick it up for some reason. + /* istanbul ignore next */ + get message() { + return this.#message; + } + + /** + * Additional data for the error. + * + * @returns Additional data for the error. + */ + get data() { + return this.#data; + } + + /** + * The error stack. + * + * @returns The error stack. + */ + // This line is covered, but Jest doesn't pick it up for some reason. + /* istanbul ignore next */ + get stack() { + return this.#stack; + } + + /** + * Convert the error to a JSON object. + * + * @returns The JSON object. + */ + toJSON(): SerializedSnapError { + return { + code: SNAP_ERROR_CODE, + message: SNAP_ERROR_MESSAGE, + data: { + cause: { + code: this.code, + message: this.message, + stack: this.stack, + data: this.data, + }, + }, + }; + } + + /** + * Serialize the error to a JSON object. This is called by + * `@metamask/rpc-errors` when serializing the error. + * + * @returns The JSON object. + */ + serialize() { + return this.toJSON(); + } +} + +/** + * A serialized {@link SnapError}. It's JSON-serializable, so it can be sent + * over the RPC. The original error is wrapped in the `cause` property. + * + * @property code - The error code. This is always `-31002`. + * @property message - The error message. This is always `'Snap Error'`. + * @property data - The error data. + * @property data.cause - The cause of the error. + * @property data.cause.code - The error code. + * @property data.cause.message - The error message. + * @property data.cause.stack - The error stack. + * @property data.cause.data - Additional data for the error. + * @see SnapError + */ +export type SerializedSnapError = { + code: typeof SNAP_ERROR_CODE; + message: typeof SNAP_ERROR_MESSAGE; + data: { + cause: JsonRpcError & { + data: Record; + }; + }; +}; diff --git a/packages/snaps-sdk/src/index.ts b/packages/snaps-sdk/src/index.ts index 716e2ba44f..f3ce97e87c 100644 --- a/packages/snaps-sdk/src/index.ts +++ b/packages/snaps-sdk/src/index.ts @@ -1,4 +1,12 @@ // Only internals that are used by other Snaps packages should be exported here. export type { EnumToUnion } from './internals'; +export { + getErrorData, + getErrorMessage, + getErrorStack, + SNAP_ERROR_CODE, + SNAP_ERROR_MESSAGE, +} from './internals'; +export * from './errors'; export * from './types'; diff --git a/packages/snaps-sdk/src/internals/errors.test.ts b/packages/snaps-sdk/src/internals/errors.test.ts new file mode 100644 index 0000000000..8e79021502 --- /dev/null +++ b/packages/snaps-sdk/src/internals/errors.test.ts @@ -0,0 +1,79 @@ +import { errorCodes, rpcErrors } from '@metamask/rpc-errors'; + +import { + getErrorCode, + getErrorData, + getErrorMessage, + getErrorStack, +} from './errors'; + +describe('getErrorMessage', () => { + it('returns the error message if the error is an object with a message property', () => { + expect(getErrorMessage(new Error('foo'))).toBe('foo'); + expect(getErrorMessage({ message: 'foo' })).toBe('foo'); + expect(getErrorMessage(rpcErrors.invalidParams('foo'))).toBe('foo'); + }); + + it('returns the error converted to a string if the error does not have a message property', () => { + expect(getErrorMessage('foo')).toBe('foo'); + expect(getErrorMessage(123)).toBe('123'); + expect(getErrorMessage(true)).toBe('true'); + expect(getErrorMessage(null)).toBe('null'); + expect(getErrorMessage(undefined)).toBe('undefined'); + expect(getErrorMessage({ foo: 'bar' })).toBe('[object Object]'); + }); +}); + +describe('getErrorStack', () => { + it('returns the error stack if the error is an object with a stack property', () => { + const error = new Error('foo'); + + expect(getErrorStack(error)).toBe(error.stack); + expect(getErrorStack({ stack: 'foo' })).toBe('foo'); + expect(getErrorStack(rpcErrors.invalidParams('foo'))).toBeDefined(); + }); + + it('returns undefined if the error does not have a stack property', () => { + expect(getErrorStack('foo')).toBeUndefined(); + expect(getErrorStack(123)).toBeUndefined(); + expect(getErrorStack(true)).toBeUndefined(); + expect(getErrorStack(null)).toBeUndefined(); + expect(getErrorStack(undefined)).toBeUndefined(); + expect(getErrorStack({ foo: 'bar' })).toBeUndefined(); + }); +}); + +describe('getErrorCode', () => { + it('returns the error code if the error is an object with a code property', () => { + expect(getErrorCode({ code: 123 })).toBe(123); + expect(getErrorCode(rpcErrors.invalidParams('foo'))).toBe(-32602); + }); + + it('returns `errorCodes.rpc.internal` if the error does not have a code property', () => { + expect(getErrorCode('foo')).toBe(errorCodes.rpc.internal); + expect(getErrorCode(123)).toBe(errorCodes.rpc.internal); + expect(getErrorCode(true)).toBe(errorCodes.rpc.internal); + expect(getErrorCode(null)).toBe(errorCodes.rpc.internal); + expect(getErrorCode(undefined)).toBe(errorCodes.rpc.internal); + expect(getErrorCode({ foo: 'bar' })).toBe(errorCodes.rpc.internal); + }); +}); + +describe('getErrorData', () => { + it('returns the error data if the error is an object with a data property', () => { + expect(getErrorData({ data: { foo: 'bar' } })).toStrictEqual({ + foo: 'bar', + }); + + expect(getErrorData(rpcErrors.invalidParams('foo'))).toStrictEqual({}); + }); + + it('returns an empty object if the error does not have a data property', () => { + expect(getErrorData('foo')).toStrictEqual({}); + expect(getErrorData(123)).toStrictEqual({}); + expect(getErrorData(true)).toStrictEqual({}); + expect(getErrorData(null)).toStrictEqual({}); + expect(getErrorData(undefined)).toStrictEqual({}); + expect(getErrorData({ foo: 'bar' })).toStrictEqual({}); + }); +}); diff --git a/packages/snaps-sdk/src/internals/errors.ts b/packages/snaps-sdk/src/internals/errors.ts new file mode 100644 index 0000000000..96d7fd1272 --- /dev/null +++ b/packages/snaps-sdk/src/internals/errors.ts @@ -0,0 +1,85 @@ +import { hasProperty, isObject, isValidJson } from '@metamask/utils'; + +export const SNAP_ERROR_CODE = -31002; +export const SNAP_ERROR_MESSAGE = 'Snap Error'; + +/** + * Get the error message from an unknown error type. + * + * - If the error is an object with a `message` property, return the message. + * - Otherwise, return the error converted to a string. + * + * @param error - The error to get the message from. + * @returns The error message. + */ +export function getErrorMessage(error: unknown) { + if ( + isObject(error) && + hasProperty(error, 'message') && + typeof error.message === 'string' + ) { + return error.message; + } + + return String(error); +} + +/** + * Get the error stack from an unknown error type. + * + * @param error - The error to get the stack from. + * @returns The error stack, or undefined if the error does not have a valid + * stack. + */ +export function getErrorStack(error: unknown) { + if ( + isObject(error) && + hasProperty(error, 'stack') && + typeof error.stack === 'string' + ) { + return error.stack; + } + + return undefined; +} + +/** + * Get the error code from an unknown error type. + * + * @param error - The error to get the code from. + * @returns The error code, or `-32603` if the error does not have a valid code. + */ +export function getErrorCode(error: unknown) { + if ( + isObject(error) && + hasProperty(error, 'code') && + typeof error.code === 'number' && + Number.isInteger(error.code) + ) { + return error.code; + } + + return -32603; +} + +/** + * Get the error data from an unknown error type. + * + * @param error - The error to get the data from. + * @returns The error data, or an empty object if the error does not have valid + * data. + */ +export function getErrorData(error: unknown) { + if ( + isObject(error) && + hasProperty(error, 'data') && + typeof error.data === 'object' && + error.data !== null && + isValidJson(error.data) && + !Array.isArray(error.data) + ) { + return error.data; + } + + return {}; +} diff --git a/packages/snaps-sdk/src/internals/index.ts b/packages/snaps-sdk/src/internals/index.ts index c5f595cf9d..9a37466793 100644 --- a/packages/snaps-sdk/src/internals/index.ts +++ b/packages/snaps-sdk/src/internals/index.ts @@ -1 +1,2 @@ +export * from './errors'; export * from './helpers'; diff --git a/packages/snaps-simulator/src/features/simulation/sagas.test.ts b/packages/snaps-simulator/src/features/simulation/sagas.test.ts index 0ee8ea7488..facaeb2e6f 100644 --- a/packages/snaps-simulator/src/features/simulation/sagas.test.ts +++ b/packages/snaps-simulator/src/features/simulation/sagas.test.ts @@ -1,9 +1,9 @@ import type { GenericPermissionController } from '@metamask/permission-controller'; import { processSnapPermissions } from '@metamask/snaps-controllers'; +import { SnapError } from '@metamask/snaps-sdk'; import { DEFAULT_ENDOWMENTS, HandlerType, - SnapError, WrappedSnapError, } from '@metamask/snaps-utils'; import { expectSaga } from 'redux-saga-test-plan'; diff --git a/packages/snaps-types/package.json b/packages/snaps-types/package.json index 4706757c10..c5f8ba728b 100644 --- a/packages/snaps-types/package.json +++ b/packages/snaps-types/package.json @@ -35,7 +35,6 @@ "lint:dependencies": "depcheck" }, "dependencies": { - "@metamask/snaps-utils": "workspace:^", "@metamask/utils": "^8.1.0" }, "devDependencies": { diff --git a/packages/snaps-types/src/types.ts b/packages/snaps-types/src/types.ts index 3b2ce0546f..d4050312b7 100644 --- a/packages/snaps-types/src/types.ts +++ b/packages/snaps-types/src/types.ts @@ -1,3 +1,2 @@ // Exported again for convenience. export type { Json, JsonRpcRequest } from '@metamask/utils'; -export { SnapError } from '@metamask/snaps-utils'; diff --git a/packages/snaps-utils/coverage.json b/packages/snaps-utils/coverage.json index 8fc789aa06..e42e426348 100644 --- a/packages/snaps-utils/coverage.json +++ b/packages/snaps-utils/coverage.json @@ -1,6 +1,6 @@ { - "branches": 96.11, - "functions": 98.1, - "lines": 98.48, - "statements": 95.44 + "branches": 95.83, + "functions": 98.99, + "lines": 98.65, + "statements": 95.49 } diff --git a/packages/snaps-utils/src/errors.test.ts b/packages/snaps-utils/src/errors.test.ts index 1f3c7dd79c..f674bda695 100644 --- a/packages/snaps-utils/src/errors.test.ts +++ b/packages/snaps-utils/src/errors.test.ts @@ -1,93 +1,20 @@ import { errorCodes, JsonRpcError, rpcErrors } from '@metamask/rpc-errors'; +import { + SnapError, + SNAP_ERROR_CODE, + SNAP_ERROR_MESSAGE, +} from '@metamask/snaps-sdk'; import { - getErrorCode, - getErrorData, - getErrorMessage, - getErrorStack, isSerializedSnapError, isSnapError, isWrappedSnapError, - SNAP_ERROR_CODE, - SNAP_ERROR_MESSAGE, SNAP_ERROR_WRAPPER_CODE, SNAP_ERROR_WRAPPER_MESSAGE, - SnapError, unwrapError, WrappedSnapError, } from './errors'; -describe('getErrorMessage', () => { - it('returns the error message if the error is an object with a message property', () => { - expect(getErrorMessage(new Error('foo'))).toBe('foo'); - expect(getErrorMessage({ message: 'foo' })).toBe('foo'); - expect(getErrorMessage(rpcErrors.invalidParams('foo'))).toBe('foo'); - }); - - it('returns the error converted to a string if the error does not have a message property', () => { - expect(getErrorMessage('foo')).toBe('foo'); - expect(getErrorMessage(123)).toBe('123'); - expect(getErrorMessage(true)).toBe('true'); - expect(getErrorMessage(null)).toBe('null'); - expect(getErrorMessage(undefined)).toBe('undefined'); - expect(getErrorMessage({ foo: 'bar' })).toBe('[object Object]'); - }); -}); - -describe('getErrorStack', () => { - it('returns the error stack if the error is an object with a stack property', () => { - const error = new Error('foo'); - - expect(getErrorStack(error)).toBe(error.stack); - expect(getErrorStack({ stack: 'foo' })).toBe('foo'); - expect(getErrorStack(rpcErrors.invalidParams('foo'))).toBeDefined(); - }); - - it('returns undefined if the error does not have a stack property', () => { - expect(getErrorStack('foo')).toBeUndefined(); - expect(getErrorStack(123)).toBeUndefined(); - expect(getErrorStack(true)).toBeUndefined(); - expect(getErrorStack(null)).toBeUndefined(); - expect(getErrorStack(undefined)).toBeUndefined(); - expect(getErrorStack({ foo: 'bar' })).toBeUndefined(); - }); -}); - -describe('getErrorCode', () => { - it('returns the error code if the error is an object with a code property', () => { - expect(getErrorCode({ code: 123 })).toBe(123); - expect(getErrorCode(rpcErrors.invalidParams('foo'))).toBe(-32602); - }); - - it('returns `errorCodes.rpc.internal` if the error does not have a code property', () => { - expect(getErrorCode('foo')).toBe(errorCodes.rpc.internal); - expect(getErrorCode(123)).toBe(errorCodes.rpc.internal); - expect(getErrorCode(true)).toBe(errorCodes.rpc.internal); - expect(getErrorCode(null)).toBe(errorCodes.rpc.internal); - expect(getErrorCode(undefined)).toBe(errorCodes.rpc.internal); - expect(getErrorCode({ foo: 'bar' })).toBe(errorCodes.rpc.internal); - }); -}); - -describe('getErrorData', () => { - it('returns the error data if the error is an object with a data property', () => { - expect(getErrorData({ data: { foo: 'bar' } })).toStrictEqual({ - foo: 'bar', - }); - - expect(getErrorData(rpcErrors.invalidParams('foo'))).toStrictEqual({}); - }); - - it('returns an empty object if the error does not have a data property', () => { - expect(getErrorData('foo')).toStrictEqual({}); - expect(getErrorData(123)).toStrictEqual({}); - expect(getErrorData(true)).toStrictEqual({}); - expect(getErrorData(null)).toStrictEqual({}); - expect(getErrorData(undefined)).toStrictEqual({}); - expect(getErrorData({ foo: 'bar' })).toStrictEqual({}); - }); -}); - describe('WrappedSnapError', () => { it('wraps an error', () => { const error = new Error('foo'); @@ -187,290 +114,6 @@ describe('WrappedSnapError', () => { }); }); -describe('SnapError', () => { - it('creates an error from a message', () => { - const error = new SnapError('foo'); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(SnapError); - expect(error.message).toBe('foo'); - expect(error.code).toBe(-32603); - expect(error.data).toStrictEqual({}); - expect(error.stack).toBeDefined(); - expect(error.toJSON()).toStrictEqual({ - code: -31002, - message: 'Snap Error', - data: { - cause: { - code: -32603, - message: 'foo', - stack: error.stack, - data: {}, - }, - }, - }); - }); - - it('creates an error from a message and code', () => { - const error = new SnapError({ - message: 'foo', - code: -32000, - }); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(SnapError); - expect(error.message).toBe('foo'); - expect(error.code).toBe(-32000); - expect(error.data).toStrictEqual({}); - expect(error.stack).toBeDefined(); - expect(error.toJSON()).toStrictEqual({ - code: -31002, - message: 'Snap Error', - data: { - cause: { - code: -32000, - message: 'foo', - stack: error.stack, - data: {}, - }, - }, - }); - }); - - it('creates an error from a message and data', () => { - const error = new SnapError('foo', { foo: 'bar' }); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(SnapError); - expect(error.message).toBe('foo'); - expect(error.code).toBe(-32603); - expect(error.data).toStrictEqual({ foo: 'bar' }); - expect(error.stack).toBeDefined(); - }); - - it('creates an error from a message, code, and data', () => { - const error = new SnapError( - { - message: 'foo', - code: -32000, - }, - { foo: 'bar' }, - ); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(SnapError); - expect(error.message).toBe('foo'); - expect(error.code).toBe(-32000); - expect(error.data).toStrictEqual({ foo: 'bar' }); - expect(error.stack).toBeDefined(); - expect(error.toJSON()).toStrictEqual({ - code: -31002, - message: 'Snap Error', - data: { - cause: { - code: -32000, - message: 'foo', - stack: error.stack, - data: { - foo: 'bar', - }, - }, - }, - }); - }); - - it('creates an error from an error', () => { - const error = new SnapError(new Error('foo')); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(SnapError); - expect(error.message).toBe('foo'); - expect(error.code).toBe(-32603); - expect(error.data).toStrictEqual({}); - expect(error.stack).toBeDefined(); - expect(error.toJSON()).toStrictEqual({ - code: -31002, - message: 'Snap Error', - data: { - cause: { - code: -32603, - message: 'foo', - stack: error.stack, - data: {}, - }, - }, - }); - }); - - it('creates an error from an error and data', () => { - const error = new SnapError(new Error('foo'), { foo: 'bar' }); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(SnapError); - expect(error.message).toBe('foo'); - expect(error.code).toBe(-32603); - expect(error.data).toStrictEqual({ foo: 'bar' }); - expect(error.stack).toBeDefined(); - expect(error.toJSON()).toStrictEqual({ - code: -31002, - message: 'Snap Error', - data: { - cause: { - code: -32603, - message: 'foo', - stack: error.stack, - data: { - foo: 'bar', - }, - }, - }, - }); - }); - - it('creates an error from a JsonRpcError', () => { - const error = new SnapError(rpcErrors.invalidParams('foo')); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(SnapError); - expect(error.message).toBe('foo'); - expect(error.code).toBe(-32602); - expect(error.data).toStrictEqual({}); - expect(error.stack).toBeDefined(); - expect(error.toJSON()).toStrictEqual({ - code: -31002, - message: 'Snap Error', - data: { - cause: { - code: -32602, - message: 'foo', - stack: error.stack, - data: {}, - }, - }, - }); - }); - - it('creates an error from a JsonRpcError and data', () => { - const error = new SnapError(rpcErrors.invalidParams('foo'), { - foo: 'bar', - }); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(SnapError); - expect(error.message).toBe('foo'); - expect(error.code).toBe(-32602); - expect(error.data).toStrictEqual({ foo: 'bar' }); - expect(error.stack).toBeDefined(); - expect(error.toJSON()).toStrictEqual({ - code: -31002, - message: 'Snap Error', - data: { - cause: { - code: -32602, - message: 'foo', - stack: error.stack, - data: { - foo: 'bar', - }, - }, - }, - }); - }); - - it('creates an error from a JsonRpcError with a code of 0', () => { - const error = new SnapError({ - message: 'foo', - code: 0, - }); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(SnapError); - expect(error.message).toBe('foo'); - expect(error.code).toBe(0); - expect(error.data).toStrictEqual({}); - expect(error.stack).toBeDefined(); - expect(error.toJSON()).toStrictEqual({ - code: -31002, - message: 'Snap Error', - data: { - cause: { - code: 0, - message: 'foo', - stack: error.stack, - data: {}, - }, - }, - }); - }); - - it('creates an error from a JsonRpcError with a code of 0 and data', () => { - const error = new SnapError( - { - message: 'foo', - code: 0, - }, - { foo: 'bar' }, - ); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(SnapError); - expect(error.message).toBe('foo'); - expect(error.code).toBe(0); - expect(error.data).toStrictEqual({ foo: 'bar' }); - expect(error.stack).toBeDefined(); - expect(error.toJSON()).toStrictEqual({ - code: -31002, - message: 'Snap Error', - data: { - cause: { - code: 0, - message: 'foo', - stack: error.stack, - data: { - foo: 'bar', - }, - }, - }, - }); - }); - - it('creates an error from a JsonRpcError with a code of 0 and merges the data', () => { - const error = new SnapError( - { - message: 'foo', - code: 0, - data: { - foo: 'baz', - bar: 'qux', - }, - }, - { foo: 'bar' }, - ); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(SnapError); - expect(error.message).toBe('foo'); - expect(error.code).toBe(0); - expect(error.data).toStrictEqual({ foo: 'bar', bar: 'qux' }); - expect(error.stack).toBeDefined(); - expect(error.toJSON()).toStrictEqual({ - code: -31002, - message: 'Snap Error', - data: { - cause: { - code: 0, - message: 'foo', - stack: error.stack, - data: { - foo: 'bar', - bar: 'qux', - }, - }, - }, - }); - }); -}); - describe('isSnapError', () => { it('returns true if the error is a Snap error', () => { const error = new SnapError('foo'); diff --git a/packages/snaps-utils/src/errors.ts b/packages/snaps-utils/src/errors.ts index 2423a97a67..7c647c4e8b 100644 --- a/packages/snaps-utils/src/errors.ts +++ b/packages/snaps-utils/src/errors.ts @@ -4,101 +4,19 @@ import { serializeCause, } from '@metamask/rpc-errors'; import type { DataWithOptionalCause } from '@metamask/rpc-errors'; -import type { Json, JsonRpcError } from '@metamask/utils'; +import type { SerializedSnapError, SnapError } from '@metamask/snaps-sdk'; import { - hasProperty, - isJsonRpcError, - isObject, - isValidJson, -} from '@metamask/utils'; - -/** - * Get the error message from an unknown error type. - * - * - If the error is an object with a `message` property, return the message. - * - Otherwise, return the error converted to a string. - * - * @param error - The error to get the message from. - * @returns The error message. - */ -export function getErrorMessage(error: unknown) { - if ( - isObject(error) && - hasProperty(error, 'message') && - typeof error.message === 'string' - ) { - return error.message; - } - - return String(error); -} - -/** - * Get the error stack from an unknown error type. - * - * @param error - The error to get the stack from. - * @returns The error stack, or undefined if the error does not have a valid - * stack. - */ -export function getErrorStack(error: unknown) { - if ( - isObject(error) && - hasProperty(error, 'stack') && - typeof error.stack === 'string' - ) { - return error.stack; - } - - return undefined; -} - -/** - * Get the error code from an unknown error type. - * - * @param error - The error to get the code from. - * @returns The error code, or `-32603` if the error does not have a valid code. - */ -export function getErrorCode(error: unknown) { - if ( - isObject(error) && - hasProperty(error, 'code') && - typeof error.code === 'number' && - Number.isInteger(error.code) - ) { - return error.code; - } - - return errorCodes.rpc.internal; -} - -/** - * Get the error data from an unknown error type. - * - * @param error - The error to get the data from. - * @returns The error data, or an empty object if the error does not have valid - * data. - */ -export function getErrorData(error: unknown) { - if ( - isObject(error) && - hasProperty(error, 'data') && - typeof error.data === 'object' && - error.data !== null && - isValidJson(error.data) && - !Array.isArray(error.data) - ) { - return error.data; - } - - return {}; -} + getErrorMessage, + getErrorStack, + SNAP_ERROR_CODE, + SNAP_ERROR_MESSAGE, +} from '@metamask/snaps-sdk'; +import type { Json, JsonRpcError } from '@metamask/utils'; +import { isObject, isJsonRpcError } from '@metamask/utils'; export const SNAP_ERROR_WRAPPER_CODE = -31001; export const SNAP_ERROR_WRAPPER_MESSAGE = 'Wrapped Snap Error'; -export const SNAP_ERROR_CODE = -31002; -export const SNAP_ERROR_MESSAGE = 'Snap Error'; - export type SerializedSnapErrorWrapper = { code: typeof SNAP_ERROR_WRAPPER_CODE; message: typeof SNAP_ERROR_WRAPPER_MESSAGE; @@ -107,16 +25,6 @@ export type SerializedSnapErrorWrapper = { }; }; -export type SerializedSnapError = { - code: typeof SNAP_ERROR_CODE; - message: typeof SNAP_ERROR_MESSAGE; - data: { - cause: JsonRpcError & { - data: Record; - }; - }; -}; - export class WrappedSnapError extends Error { readonly #error: unknown; @@ -195,120 +103,6 @@ export class WrappedSnapError extends Error { } } -/** - * A generic error which can be thrown by a Snap, without it causing the Snap to - * crash. - */ -export class SnapError extends Error { - readonly #code: number; - - readonly #message: string; - - readonly #data: Record; - - readonly #stack?: string; - - /** - * Create a new `SnapError`. - * - * @param error - The error to create the `SnapError` from. If this is a - * `string`, it will be used as the error message. If this is an `Error`, its - * `message` property will be used as the error message. If this is a - * `JsonRpcError`, its `message` property will be used as the error message - * and its `code` property will be used as the error code. Otherwise, the - * error will be converted to a string and used as the error message. - * @param data - Additional data to include in the error. This will be merged - * with the error data, if any. - */ - constructor( - error: string | Error | JsonRpcError, - data: Record = {}, - ) { - const message = getErrorMessage(error); - super(message); - - this.#message = message; - this.#code = getErrorCode(error); - this.#data = { ...getErrorData(error), ...data }; - this.#stack = super.stack; - } - - /** - * The error name. - * - * @returns The error name. - */ - get name() { - return 'SnapError'; - } - - /** - * The error code. - * - * @returns The error code. - */ - get code() { - return this.#code; - } - - /** - * The error message. - * - * @returns The error message. - */ - get message() { - return this.#message; - } - - /** - * Additional data for the error. - * - * @returns Additional data for the error. - */ - get data() { - return this.#data; - } - - /** - * The error stack. - * - * @returns The error stack. - */ - get stack() { - return this.#stack; - } - - /** - * Convert the error to a JSON object. - * - * @returns The JSON object. - */ - toJSON(): SerializedSnapError { - return { - code: SNAP_ERROR_CODE, - message: SNAP_ERROR_MESSAGE, - data: { - cause: { - code: this.code, - message: this.message, - stack: this.stack, - data: this.data, - }, - }, - }; - } - - /** - * Serialize the error to a JSON object. This is called by - * `@metamask/rpc-errors` when serializing the error. - * - * @returns The JSON object. - */ - serialize() { - return this.toJSON(); - } -} - /** * Check if an object is a `SnapError`. * diff --git a/packages/snaps-utils/src/localization.ts b/packages/snaps-utils/src/localization.ts index 027efd6667..d5e9fb875c 100644 --- a/packages/snaps-utils/src/localization.ts +++ b/packages/snaps-utils/src/localization.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '@metamask/snaps-sdk'; import type { Infer } from 'superstruct'; import { create, @@ -8,7 +9,6 @@ import { StructError, } from 'superstruct'; -import { getErrorMessage } from './errors'; import { parseJson } from './json'; import type { SnapManifest } from './manifest'; import type { VirtualFile } from './virtual-file'; diff --git a/packages/snaps-utils/src/manifest/manifest.ts b/packages/snaps-utils/src/manifest/manifest.ts index d80a6b1a63..1358d3fc59 100644 --- a/packages/snaps-utils/src/manifest/manifest.ts +++ b/packages/snaps-utils/src/manifest/manifest.ts @@ -1,3 +1,4 @@ +import { getErrorMessage } from '@metamask/snaps-sdk'; import type { Json } from '@metamask/utils'; import { assertExhaustive, assert, isPlainObject } from '@metamask/utils'; import deepEqual from 'fast-deep-equal'; @@ -5,7 +6,6 @@ import { promises as fs } from 'fs'; import pathUtils from 'path'; import { deepClone } from '../deep-clone'; -import { getErrorMessage } from '../errors'; import { readJsonFile } from '../fs'; import { validateNpmSnap } from '../npm'; import { diff --git a/packages/snaps-webpack-plugin/package.json b/packages/snaps-webpack-plugin/package.json index 102c1c56a6..62e9e9992e 100644 --- a/packages/snaps-webpack-plugin/package.json +++ b/packages/snaps-webpack-plugin/package.json @@ -39,6 +39,7 @@ "lint:dependencies": "depcheck" }, "dependencies": { + "@metamask/snaps-sdk": "workspace:^", "@metamask/snaps-utils": "workspace:^", "@metamask/utils": "^8.1.0", "webpack-sources": "^3.2.3" diff --git a/packages/snaps-webpack-plugin/src/plugin.ts b/packages/snaps-webpack-plugin/src/plugin.ts index 6e592a65d3..92c004b499 100644 --- a/packages/snaps-webpack-plugin/src/plugin.ts +++ b/packages/snaps-webpack-plugin/src/plugin.ts @@ -1,8 +1,8 @@ +import { getErrorMessage } from '@metamask/snaps-sdk'; import type { PostProcessOptions, SourceMap } from '@metamask/snaps-utils'; import { checkManifest, evalBundle, - getErrorMessage, postProcessBundle, useTemporaryFile, } from '@metamask/snaps-utils'; diff --git a/packages/snaps-webpack-plugin/tsconfig.build.json b/packages/snaps-webpack-plugin/tsconfig.build.json index 31c878583c..6929150752 100644 --- a/packages/snaps-webpack-plugin/tsconfig.build.json +++ b/packages/snaps-webpack-plugin/tsconfig.build.json @@ -13,6 +13,9 @@ "./src/**/__snapshots__" ], "references": [ + { + "path": "../snaps-sdk/tsconfig.build.json" + }, { "path": "../snaps-utils/tsconfig.build.json" } diff --git a/packages/snaps-webpack-plugin/tsconfig.json b/packages/snaps-webpack-plugin/tsconfig.json index 4a3ba29e4e..b0bf6cc0b3 100644 --- a/packages/snaps-webpack-plugin/tsconfig.json +++ b/packages/snaps-webpack-plugin/tsconfig.json @@ -5,6 +5,9 @@ }, "include": ["./src"], "references": [ + { + "path": "../snaps-sdk" + }, { "path": "../snaps-utils" } diff --git a/yarn.lock b/yarn.lock index 264e58a916..08918490d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5462,6 +5462,7 @@ __metadata: "@metamask/eslint-config-typescript": ^12.1.0 "@metamask/key-tree": ^9.0.0 "@metamask/providers": ^13.0.0 + "@metamask/rpc-errors": ^6.1.0 "@metamask/snaps-ui": "workspace:^" "@metamask/utils": ^8.1.0 "@swc/cli": ^0.1.62 @@ -5604,7 +5605,6 @@ __metadata: "@metamask/eslint-config-jest": ^12.1.0 "@metamask/eslint-config-nodejs": ^12.1.0 "@metamask/eslint-config-typescript": ^12.1.0 - "@metamask/snaps-utils": "workspace:^" "@metamask/utils": ^8.1.0 "@swc/cli": ^0.1.62 "@swc/core": 1.3.78 @@ -5757,6 +5757,7 @@ __metadata: "@metamask/eslint-config-jest": ^12.1.0 "@metamask/eslint-config-nodejs": ^12.1.0 "@metamask/eslint-config-typescript": ^12.1.0 + "@metamask/snaps-sdk": "workspace:^" "@metamask/snaps-utils": "workspace:^" "@metamask/utils": ^8.1.0 "@swc/cli": ^0.1.62