@exodus/errors 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [1.2.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@1.1.1...@exodus/errors@1.2.0) (2025-02-12)
7
+
8
+ ### Features
9
+
10
+ - feat(errors): any error to SafeError conversion capability (#9918)
11
+
12
+ ## [1.1.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@1.1.0...@exodus/errors@1.1.1) (2024-12-20)
13
+
14
+ ### License
15
+
16
+ - license: re-license under MIT license (#10599)
17
+
6
18
  ## [1.1.0](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@1.0.1...@exodus/errors@1.1.0) (2024-08-19)
7
19
 
8
20
  ### Features
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2024 Exodus Movement, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/lib/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default as sanitizeErrorMessage } from './sanitize.js';
2
2
  export { default as parseStackTrace } from './stack.js';
3
+ export * from './safe-error.js';
package/lib/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default as sanitizeErrorMessage } from './sanitize.js';
2
2
  export { default as parseStackTrace } from './stack.js';
3
+ export * from './safe-error.js';
@@ -0,0 +1,77 @@
1
+ import type { Frame } from './types.js';
2
+ type ReadonlySetValues<S> = S extends ReadonlySet<infer T> ? T : never;
3
+ declare const SAFE_NAMES_SET: ReadonlySet<"Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse">;
4
+ declare const SAFE_HINTS_SET: ReadonlySet<"broadcastTx" | "otherErr:broadcastTx" | "retry:broadcastTx" | "getNftArguments" | "ethCall" | "ethCall:executionReverted" | "failed to parse error">;
5
+ type SafeName = ReadonlySetValues<typeof SAFE_NAMES_SET>;
6
+ type SafeHint = ReadonlySetValues<typeof SAFE_HINTS_SET>;
7
+ type SafeCode = string & {
8
+ __branded_type: 'SafeCodeOutcome';
9
+ };
10
+ type UnknownError = Error & {
11
+ hint?: unknown;
12
+ code?: unknown;
13
+ };
14
+ declare const FACTORY_SYMBOL: unique symbol;
15
+ export declare class SafeError {
16
+ #private;
17
+ static readonly hints: {
18
+ readonly __proto__: null;
19
+ readonly broadcastTx: {
20
+ readonly general: "broadcastTx";
21
+ readonly other: "otherErr:broadcastTx";
22
+ readonly retry: "retry:broadcastTx";
23
+ };
24
+ readonly getNftArguments: {
25
+ readonly general: "getNftArguments";
26
+ };
27
+ readonly ethCall: {
28
+ readonly general: "ethCall";
29
+ readonly executionReverted: "ethCall:executionReverted";
30
+ };
31
+ readonly safeError: {
32
+ readonly failedToParse: "failed to parse error";
33
+ };
34
+ };
35
+ static from<T extends UnknownError>(err: T): SafeError;
36
+ get name(): "Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse";
37
+ get code(): SafeCode | undefined;
38
+ get hint(): "broadcastTx" | "otherErr:broadcastTx" | "retry:broadcastTx" | "getNftArguments" | "ethCall" | "ethCall:executionReverted" | "failed to parse error" | undefined;
39
+ get stackFrames(): {
40
+ function?: string | null;
41
+ method?: string | null;
42
+ file?: string | null;
43
+ line?: number | null;
44
+ column?: number | null;
45
+ async?: boolean | null;
46
+ toplevel?: boolean | null;
47
+ in_app?: boolean | null;
48
+ __proto__: null;
49
+ }[] | undefined;
50
+ get timestamp(): number;
51
+ toJSON(): {
52
+ __proto__: null;
53
+ name: "Error" | "AssertionError" | "TypeError" | "RangeError" | "UnknownError" | "SafeErrorFailedToParse";
54
+ code: SafeCode | undefined;
55
+ hint: "broadcastTx" | "otherErr:broadcastTx" | "retry:broadcastTx" | "getNftArguments" | "ethCall" | "ethCall:executionReverted" | "failed to parse error" | undefined;
56
+ stackFrames: {
57
+ function?: string | null;
58
+ method?: string | null;
59
+ file?: string | null;
60
+ line?: number | null;
61
+ column?: number | null;
62
+ async?: boolean | null;
63
+ toplevel?: boolean | null;
64
+ in_app?: boolean | null;
65
+ __proto__: null;
66
+ }[] | undefined;
67
+ timestamp: number;
68
+ };
69
+ constructor({ name, code, hint, stack, initSymbol, }: {
70
+ name: SafeName;
71
+ code?: SafeCode;
72
+ hint?: SafeHint;
73
+ stack?: Frame[];
74
+ initSymbol: typeof FACTORY_SYMBOL;
75
+ });
76
+ }
77
+ export {};
@@ -0,0 +1,140 @@
1
+ import assert from 'minimalistic-assert';
2
+ import parseStackTraceNatively from './stack.js';
3
+ function makeReadonlySet(values) {
4
+ return new Set(values);
5
+ }
6
+ const SAFE_CODES_SET = makeReadonlySet(['EPIPE']);
7
+ const SAFE_NAMES_SET = makeReadonlySet([
8
+ 'Error',
9
+ 'AssertionError',
10
+ 'TypeError',
11
+ 'RangeError',
12
+ 'UnknownError',
13
+ 'SafeErrorFailedToParse',
14
+ ]);
15
+ const safeHints = {
16
+ __proto__: null,
17
+ broadcastTx: {
18
+ general: 'broadcastTx',
19
+ other: 'otherErr:broadcastTx',
20
+ retry: 'retry:broadcastTx',
21
+ },
22
+ getNftArguments: {
23
+ general: 'getNftArguments',
24
+ },
25
+ ethCall: {
26
+ general: 'ethCall',
27
+ executionReverted: 'ethCall:executionReverted',
28
+ },
29
+ safeError: {
30
+ failedToParse: 'failed to parse error',
31
+ },
32
+ };
33
+ const SAFE_HINTS_SET = makeReadonlySet(
34
+ // @ts-expect-error doesn't like __proto__: null
35
+ Object.values(safeHints).flatMap((safeHintCategory) => Object.values(safeHintCategory)));
36
+ function getSafeHint(value) {
37
+ if (typeof value !== 'string') {
38
+ return undefined;
39
+ }
40
+ if (value === '') {
41
+ return undefined;
42
+ }
43
+ if (isSafeHint(value)) {
44
+ return value;
45
+ }
46
+ return [...SAFE_HINTS_SET].find((sh) => value.includes(sh));
47
+ }
48
+ function isSafeHint(value) {
49
+ return SAFE_HINTS_SET.has(value);
50
+ }
51
+ function isSafeName(value) {
52
+ return SAFE_NAMES_SET.has(value);
53
+ }
54
+ function isExodusErrorCode(code) {
55
+ return /^EXOD-\d{1,4}$/u.test(code);
56
+ }
57
+ function isSafeCode(value) {
58
+ return SAFE_CODES_SET.has(value) || isExodusErrorCode(value);
59
+ }
60
+ const FACTORY_SYMBOL = Symbol('SafeError');
61
+ export class SafeError {
62
+ static hints = safeHints;
63
+ static from(err) {
64
+ let safeName;
65
+ let safeCode;
66
+ let safeHint;
67
+ try {
68
+ const { name, message, hint, code } = err;
69
+ safeName = isSafeName(name) ? name : 'UnknownError';
70
+ const safeCodeCandidate = `${code}`;
71
+ safeCode = isSafeCode(safeCodeCandidate) ? safeCodeCandidate : undefined;
72
+ safeHint = getSafeHint(hint) || getSafeHint(message);
73
+ const safeStack = parseStackTraceNatively(err);
74
+ return new SafeError({
75
+ name: safeName,
76
+ code: safeCode,
77
+ hint: safeHint,
78
+ stack: safeStack,
79
+ initSymbol: FACTORY_SYMBOL,
80
+ });
81
+ }
82
+ catch {
83
+ try {
84
+ return safeName
85
+ ? new SafeError({
86
+ name: safeName,
87
+ code: safeCode,
88
+ hint: safeHint,
89
+ initSymbol: FACTORY_SYMBOL,
90
+ })
91
+ : FAILED_TO_PARSE_ERROR;
92
+ }
93
+ catch {
94
+ return FAILED_TO_PARSE_ERROR;
95
+ }
96
+ }
97
+ }
98
+ #name;
99
+ #code;
100
+ #hint;
101
+ #stackFrames;
102
+ #timestamp;
103
+ get name() {
104
+ return this.#name;
105
+ }
106
+ get code() {
107
+ return this.#code;
108
+ }
109
+ get hint() {
110
+ return this.#hint;
111
+ }
112
+ get stackFrames() {
113
+ return this.#stackFrames?.map((frame) => ({ __proto__: null, ...frame }));
114
+ }
115
+ get timestamp() {
116
+ return this.#timestamp;
117
+ }
118
+ toJSON() {
119
+ return {
120
+ __proto__: null,
121
+ name: this.name,
122
+ code: this.code,
123
+ hint: this.hint,
124
+ stackFrames: this.stackFrames,
125
+ timestamp: this.timestamp,
126
+ };
127
+ }
128
+ constructor({ name, code, hint, stack, initSymbol, }) {
129
+ assert(initSymbol === FACTORY_SYMBOL, 'SafeError: use SafeError.from()');
130
+ this.#name = name;
131
+ this.#code = code;
132
+ this.#hint = hint;
133
+ this.#stackFrames = stack?.map((frame) => ({ ...frame }));
134
+ this.#timestamp = Date.now();
135
+ }
136
+ }
137
+ const FAILED_TO_PARSE_ERROR = new SafeError({
138
+ initSymbol: FACTORY_SYMBOL,
139
+ name: 'SafeErrorFailedToParse',
140
+ });
package/lib/sanitize.js CHANGED
@@ -1,13 +1,13 @@
1
1
  const sanitizeErrorMessage = (message) => message
2
2
  // 12 word phrase
3
- .replaceAll(/(?:[A-Za-z]{3,20}\s+){11}[A-Za-z]{3,20}/g, '****')
3
+ .replaceAll(/(?:[A-Za-z]{3,20}\s+){11}[A-Za-z]{3,20}/gu, '****')
4
4
  // hex
5
- .replaceAll(/(?:0x)?[\dA-Fa-f]{20,}/g, '****')
5
+ .replaceAll(/(?:0x)?[\dA-Fa-f]{20,}/gu, '****')
6
6
  // base58
7
7
  // https://stackoverflow.com/a/33060399
8
- .replaceAll(/[1-9A-HJ-NP-Za-km-z]{50,}/g, '****')
8
+ .replaceAll(/[1-9A-HJ-NP-Za-km-z]{50,}/gu, '****')
9
9
  // base64
10
10
  // adapted from https://stackoverflow.com/a/5885097
11
11
  // added `{3,}` to match at least 12 characters
12
- .replaceAll(/(?:[\d+/A-Za-z]{4}){3,}(?:[\d+/A-Za-z]{2}==|[\d+/A-Za-z]{3}=|[\d+/A-Za-z]{4})/g, '****');
12
+ .replaceAll(/(?:[\d+/A-Za-z]{4}){3,}(?:[\d+/A-Za-z]{2}==|[\d+/A-Za-z]{3}=|[\d+/A-Za-z]{4})/gu, '****');
13
13
  export default sanitizeErrorMessage;
package/lib/stack.js CHANGED
@@ -16,7 +16,7 @@ export default function parseStackTraceNatively(err) {
16
16
  }));
17
17
  };
18
18
  try {
19
- // eslint-disable-next-line no-unused-expressions
19
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
20
20
  err.stack; // trigger prepareStackTrace
21
21
  }
22
22
  finally {
package/lib/types.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  export type Frame = {
2
- function?: string;
3
- method?: string;
4
- file?: string;
5
- line?: number;
6
- column?: number;
7
- async?: boolean;
8
- toplevel?: boolean;
9
- in_app?: boolean;
2
+ function?: string | null;
3
+ method?: string | null;
4
+ file?: string | null;
5
+ line?: number | null;
6
+ column?: number | null;
7
+ async?: boolean | null;
8
+ toplevel?: boolean | null;
9
+ in_app?: boolean | null;
10
10
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exodus/errors",
3
3
  "type": "module",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "description": "Utilities for error handling in client code, such as sanitization",
6
6
  "author": "Exodus Movement, Inc.",
7
7
  "repository": {
@@ -9,7 +9,7 @@
9
9
  "url": "git+https://github.com/ExodusMovement/exodus-hydra.git"
10
10
  },
11
11
  "homepage": "https://github.com/ExodusMovement/exodus-hydra/tree/master/libraries/errors",
12
- "license": "UNLICENSED",
12
+ "license": "MIT",
13
13
  "bugs": {
14
14
  "url": "https://github.com/ExodusMovement/exodus-hydra/issues?q=is%3Aissue+is%3Aopen+label%3Aerrors"
15
15
  },
@@ -27,15 +27,22 @@
27
27
  "build": "run -T tsc --build tsconfig.build.json",
28
28
  "prepublishOnly": "yarn run -T build --scope @exodus/errors",
29
29
  "clean": "run -T tsc --build --clean",
30
- "lint": "run -T eslint . --ignore-path ../../.gitignore",
30
+ "lint": "run -T eslint .",
31
31
  "lint:fix": "yarn lint --fix",
32
32
  "test": "yarn run test:v8 && yarn run test:hermes",
33
33
  "test:v8": "run -T exodus-test --esbuild --jest src/__tests__/v8/*.test.ts",
34
- "test:hermes": "run -T exodus-test --engine hermes:bundle --esbuild --jest src/__tests__/hermes/*.test.ts"
34
+ "test:hermes": "EXODUS_TEST_COVERAGE=0 run -T exodus-test --engine hermes:bundle --esbuild --jest src/__tests__/hermes/*.test.ts"
35
+ },
36
+ "dependencies": {
37
+ "minimalistic-assert": "^1.0.1"
35
38
  },
36
39
  "devDependencies": {
37
40
  "@exodus/errors-fixture": "^1.0.0",
41
+ "@types/minimalistic-assert": "^1.0.1",
38
42
  "hermes-engine-cli": "^0.12.0"
39
43
  },
40
- "gitHead": "0f5db1a33a6a8dd5fee393f6d8204504af5c2c6e"
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "gitHead": "3f98f500b3b5289ff3e38a8c8385c164b4ee8a5e"
41
48
  }