Skip to content

Commit bd989a3

Browse files
authored
feat: implement first release
1 parent b2041f3 commit bd989a3

17 files changed

Lines changed: 2340 additions & 1 deletion

.eslintrc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"extends": [
3+
"eslint:recommended",
4+
"plugin:@typescript-eslint/recommended",
5+
"prettier"
6+
],
7+
"parser": "@typescript-eslint/parser",
8+
"plugins": ["@typescript-eslint"],
9+
"root": true,
10+
"env": {
11+
"node": true
12+
}
13+
}

.github/workflows/test.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Test
2+
on:
3+
pull_request:
4+
types: [opened, reopened, synchronize]
5+
6+
jobs:
7+
run-tests:
8+
name: Run tests
9+
runs-on: ubuntu-latest
10+
strategy:
11+
matrix:
12+
node-version: [14.x]
13+
14+
steps:
15+
- name: Cancel previous runs
16+
uses: styfle/cancel-workflow-action@0.5.0
17+
with:
18+
access_token: ${{ github.token }}
19+
20+
- name: Checkout repository
21+
uses: actions/checkout@v2
22+
23+
- name: Use Node.js ${{ matrix.node-version }}
24+
uses: actions/setup-node@v2
25+
with:
26+
registry-url: https://registry.npmjs.org
27+
28+
- name: Restore yarn cache if available
29+
uses: actions/cache@v2
30+
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
31+
with:
32+
path: |
33+
node_modules
34+
*/*/node_modules
35+
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
36+
37+
- name: Install dependencies
38+
run: |
39+
yarn install --frozen-lockfile
40+
41+
- name: Run TypeScript type checker
42+
run: |
43+
yarn ts:check
44+
env:
45+
CI: true
46+
47+
- name: Run formatting check
48+
run: |
49+
yarn format:check
50+
env:
51+
CI: true
52+
53+
- name: Run linting check
54+
run: |
55+
yarn lint
56+
env:
57+
CI: true
58+
59+
- name: Run tests
60+
run: |
61+
yarn test:ci
62+
env:
63+
CI: true
64+
65+
- name: Upload coverage
66+
uses: codecov/codecov-action@v3

.gitignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?
25+
26+
# Vitest
27+
coverage

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist
2+
node_modules

.prettierrc

Whitespace-only changes.

README.md

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,77 @@
11
# ethers-error-parser
2-
Parse Ethers.js errors with ease
2+
3+
![Tests](https://github.com/enzoferey/ethers-error-parser/actions/workflows/test.yml/badge.svg)
4+
[![npm version](https://badge.fury.io/js/@enzoferey%2Fethers-error-parser.svg)](https://badge.fury.io/js/@enzoferey%2Fethers-error-parser)
5+
[![codecov.io Code Coverage](https://img.shields.io/codecov/c/github/enzoferey/ethers-error-parser.svg?maxAge=2592000)](https://codecov.io/github/enzoferey/ethers-error-parser)
6+
7+
Parse Ethers.js errors with ease 💅🏻
8+
9+
## Highlights
10+
11+
- Zero dependencies 🧹
12+
- Lightweight (637 bytes gzipped) 📦
13+
- Simple to use ⚡️
14+
- Work in progress 🚧
15+
16+
## Why
17+
18+
[Ethers.js](https://github.com/ethers-io/ethers.js/) is well known for its cryptic error messages. Whenever a transaction fails you will get an error message that combines plain text and JSON stringified string like this:
19+
20+
```
21+
Error: cannot estimate gas; transaction may fail or may require manual gas limit (error={"code":-32603,"message":"execution reverted: Code has already claimed","data":{"originalError":{"code":3,"data":"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000018436f64652068617320616c726561647920636c61696d65640000000000000000","message":"execution reverted: Code has already claimed"}}}, method="estimateGas", transaction={"from":"0xC16f5C62b29704F7aBECb27A3cb7E12a91383261","to":"0xb21FFFd62BD2f4aBd2a1dC34A2302Fda364977a0","data":"0xd2c34d3f0000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000063132333435360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004153801a64156372ec7cd1d91868dd35ed68972dfa8b347c59db14bc49b753ed576fbe8a2bc00b6d0ba5dc8429c01748ae55e87dffa9547aafeb844aa40bb6c3e31b00000000000000000000000000000000000000000000000000000000000000","accessList":null}, code=UNPREDICTABLE_GAS_LIMIT, version=providers/5.5.1)
22+
```
23+
24+
Your users deserve to get amazing feedback on failing transactions. Thanks to `ethers-error-parser` you can do that with ease.
25+
26+
## Work in progress
27+
28+
This package is a work in progress. Although it is not yet complete and many Ethers errors could be handled more elegantly, it is safe to use in production as it is. Some insights is better than no insights.
29+
30+
This package is being used in different production projects and it is in constant evolution based on the needs of these projects. If you find some error that is not handled yet or that does not provide a great context, please open an issue or pull request 🙏
31+
32+
## Getting started
33+
34+
1. Install the package
35+
36+
```sh
37+
yarn install @enzoferey/ethers-error-parser
38+
```
39+
40+
2. Use it
41+
42+
```ts
43+
import { getParsedEthersError } from "@enzoferey/ethers-error-parser";
44+
45+
try {
46+
const transaction = await someContract.someMethod();
47+
await transaction.wait();
48+
} catch (error) {
49+
const parsedEthersError = getParsedEthersError(error);
50+
// parsedError.errorCode - contains a well defined error code (see full list below)
51+
// parsedError.context - contains a context based string providing additional information
52+
// profit ! 💅🏻
53+
}
54+
```
55+
56+
## Return value
57+
58+
When using `getParsedEthersError` you will get back an object containing a well known `errorCode` property and an optional `context` property with additional information. The TypeScript type definition looks like the following:
59+
60+
```ts
61+
interface ReturnValue {
62+
errorCode: string;
63+
context?: string;
64+
}
65+
```
66+
67+
Here is the complete list of returned objects:
68+
69+
| `errorCode` | `context` |
70+
| ---------------------------- | -------------------------------------------------------------------------- |
71+
| `TRANSACTION_RUN_OUT_OF_GAS` | The transaction gas limit as a string. |
72+
| `TRANSACTION_UNDERPRICED` | `undefined` |
73+
| `REJECTED_TRANSACTION` | The reason why the transaction rejected. |
74+
| `EXECUTION_REVERTED` | The reason why the transaction reverted. |
75+
| `UNKNOWN_ERROR` | Some code or description of the error if available. `undefined` otherwise. |
76+
77+
If you find some error that is not handled yet or that does not provide a great context, please open an issue or pull request 🙏
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import type { BigNumber } from "../types";
4+
import { ERROR_CODES, ETHERS_ERROR_CODES } from "../constants";
5+
import { getParsedEthersError } from "../getParsedEthersError";
6+
7+
function getTestBigNumber(value: string): BigNumber {
8+
return {
9+
gte: (other) => {
10+
return parseFloat(value) >= parseFloat(other.toString());
11+
},
12+
toString: () => {
13+
return value;
14+
},
15+
};
16+
}
17+
18+
describe("getParsedEthersError", () => {
19+
it("should handle transaction underpriced at the top level", () => {
20+
const result = getParsedEthersError({
21+
code: ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC,
22+
message: `RPC '{"value":{"data":{"code":${ETHERS_ERROR_CODES.TRANSACTION_UNDERPRICED}}}}'`,
23+
});
24+
25+
expect(result).toEqual({
26+
errorCode: ERROR_CODES.TRANSACTION_UNDERPRICED,
27+
context: undefined,
28+
});
29+
});
30+
it("should handle transaction underpriced at the nested level", () => {
31+
const result = getParsedEthersError({
32+
error: {
33+
code: ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC,
34+
message: `RPC '{"value":{"data":{"code":${ETHERS_ERROR_CODES.TRANSACTION_UNDERPRICED}}}}'`,
35+
},
36+
});
37+
38+
expect(result).toEqual({
39+
errorCode: ERROR_CODES.TRANSACTION_UNDERPRICED,
40+
context: undefined,
41+
});
42+
});
43+
it("should handle transaction underpriced that does not provide the right JSON details", () => {
44+
const result1 = getParsedEthersError({
45+
code: ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC,
46+
message: `RPC '{"value":{"data":{"code":"SOME NON EXPECTED CODE"}}}'`,
47+
});
48+
49+
expect(result1).toEqual({
50+
errorCode: ERROR_CODES.UNKNOWN_ERROR,
51+
context:
52+
ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC.toString(),
53+
});
54+
55+
const result2 = getParsedEthersError({
56+
code: ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC,
57+
message: `RPC '{}'`,
58+
});
59+
60+
expect(result2).toEqual({
61+
errorCode: ERROR_CODES.UNKNOWN_ERROR,
62+
context:
63+
ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC.toString(),
64+
});
65+
});
66+
it("should handle transaction underpriced that does not provide a valid JSON details", () => {
67+
const result = getParsedEthersError({
68+
code: ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC,
69+
message: `RPC 'not-a-json'`,
70+
});
71+
72+
expect(result).toEqual({
73+
errorCode: ERROR_CODES.UNKNOWN_ERROR,
74+
context:
75+
ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC.toString(),
76+
});
77+
});
78+
it("should handle transaction rejected at the top level", () => {
79+
const message = "User rejected transaction";
80+
81+
const result = getParsedEthersError({
82+
code: ETHERS_ERROR_CODES.REJECTED_TRANSACTION,
83+
message,
84+
});
85+
86+
expect(result).toEqual({
87+
errorCode: ERROR_CODES.REJECTED_TRANSACTION,
88+
context: message,
89+
});
90+
});
91+
it("should handle transaction rejected at the nested level", () => {
92+
const message = "User rejected transaction";
93+
94+
const result = getParsedEthersError({
95+
error: {
96+
code: ETHERS_ERROR_CODES.REJECTED_TRANSACTION,
97+
message,
98+
},
99+
});
100+
101+
expect(result).toEqual({
102+
errorCode: ERROR_CODES.REJECTED_TRANSACTION,
103+
context: message,
104+
});
105+
});
106+
it("should handle execution reverted at the top level", () => {
107+
const reason = "Some reason";
108+
109+
const result = getParsedEthersError({
110+
code: ETHERS_ERROR_CODES.REQUIRE_TRANSACTION,
111+
message: `execution reverted: ${reason}`,
112+
});
113+
114+
expect(result).toEqual({
115+
errorCode: ERROR_CODES.EXECUTION_REVERTED,
116+
context: reason,
117+
});
118+
});
119+
it("should handle execution reverted at the nested level", () => {
120+
const reason = "Some reason";
121+
122+
const result = getParsedEthersError({
123+
error: {
124+
code: ETHERS_ERROR_CODES.REQUIRE_TRANSACTION,
125+
message: `execution reverted: ${reason}`,
126+
},
127+
});
128+
129+
expect(result).toEqual({
130+
errorCode: ERROR_CODES.EXECUTION_REVERTED,
131+
context: reason,
132+
});
133+
});
134+
it("should handle transaction run out of gas errors", () => {
135+
const gasLimit = getTestBigNumber("100");
136+
const gasUsed = getTestBigNumber("100");
137+
138+
const result = getParsedEthersError({
139+
transaction: {
140+
gasLimit,
141+
},
142+
receipt: {
143+
gasUsed,
144+
},
145+
});
146+
147+
expect(result).toEqual({
148+
errorCode: ERROR_CODES.TRANSACTION_RUN_OUT_OF_GAS,
149+
context: gasLimit.toString(),
150+
});
151+
});
152+
it("should handle unknown errors with a nested level error message", () => {
153+
const message = "Some internal Ethers error message";
154+
155+
const result = getParsedEthersError({
156+
error: {
157+
message,
158+
},
159+
});
160+
161+
expect(result).toEqual({
162+
errorCode: ERROR_CODES.UNKNOWN_ERROR,
163+
context: message,
164+
});
165+
});
166+
it("should handle unknown errors with a top level error code", () => {
167+
const code = "SOME INTERNAL ETHERS CODE";
168+
169+
const result = getParsedEthersError({ code });
170+
171+
expect(result).toEqual({
172+
errorCode: ERROR_CODES.UNKNOWN_ERROR,
173+
context: code,
174+
});
175+
});
176+
it("should handle totally unknown errors", () => {
177+
const result = getParsedEthersError({});
178+
179+
expect(result).toEqual({
180+
errorCode: ERROR_CODES.UNKNOWN_ERROR,
181+
context: undefined,
182+
});
183+
});
184+
});

lib/constants.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const ERROR_CODES = {
2+
TRANSACTION_RUN_OUT_OF_GAS: "TRANSACTION_RUN_OUT_OF_GAS",
3+
TRANSACTION_UNDERPRICED: "TRANSACTION_UNDERPRICED",
4+
REJECTED_TRANSACTION: "REJECTED_TRANSACTION",
5+
EXECUTION_REVERTED: "EXECUTION_REVERTED",
6+
UNKNOWN_ERROR: "UNKNOWN_ERROR",
7+
} as const;
8+
9+
export const ETHERS_ERROR_CODES = {
10+
REJECTED_TRANSACTION: 4001,
11+
REQUIRE_TRANSACTION: -32603,
12+
ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC: -32603,
13+
TRANSACTION_UNDERPRICED: -32000,
14+
};

0 commit comments

Comments
 (0)