@exodus/errors 1.1.1 → 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 +6 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/safe-error.d.ts +77 -0
- package/lib/safe-error.js +140 -0
- package/lib/types.d.ts +8 -8
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
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
|
+
|
|
6
12
|
## [1.1.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/errors@1.1.0...@exodus/errors@1.1.1) (2024-12-20)
|
|
7
13
|
|
|
8
14
|
### License
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -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/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.
|
|
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": {
|
|
@@ -33,12 +33,16 @@
|
|
|
33
33
|
"test:v8": "run -T exodus-test --esbuild --jest src/__tests__/v8/*.test.ts",
|
|
34
34
|
"test:hermes": "EXODUS_TEST_COVERAGE=0 run -T exodus-test --engine hermes:bundle --esbuild --jest src/__tests__/hermes/*.test.ts"
|
|
35
35
|
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"minimalistic-assert": "^1.0.1"
|
|
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
44
|
"publishConfig": {
|
|
41
45
|
"access": "public"
|
|
42
46
|
},
|
|
43
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "3f98f500b3b5289ff3e38a8c8385c164b4ee8a5e"
|
|
44
48
|
}
|