-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
2,340 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"extends": [ | ||
"eslint:recommended", | ||
"plugin:@typescript-eslint/recommended", | ||
"prettier" | ||
], | ||
"parser": "@typescript-eslint/parser", | ||
"plugins": ["@typescript-eslint"], | ||
"root": true, | ||
"env": { | ||
"node": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
name: Test | ||
on: | ||
pull_request: | ||
types: [opened, reopened, synchronize] | ||
|
||
jobs: | ||
run-tests: | ||
name: Run tests | ||
runs-on: ubuntu-latest | ||
strategy: | ||
matrix: | ||
node-version: [14.x] | ||
|
||
steps: | ||
- name: Cancel previous runs | ||
uses: styfle/cancel-workflow-action@0.5.0 | ||
with: | ||
access_token: ${{ github.token }} | ||
|
||
- name: Checkout repository | ||
uses: actions/checkout@v2 | ||
|
||
- name: Use Node.js ${{ matrix.node-version }} | ||
uses: actions/setup-node@v2 | ||
with: | ||
registry-url: https://registry.npmjs.org | ||
|
||
- name: Restore yarn cache if available | ||
uses: actions/cache@v2 | ||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) | ||
with: | ||
path: | | ||
node_modules | ||
*/*/node_modules | ||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} | ||
|
||
- name: Install dependencies | ||
run: | | ||
yarn install --frozen-lockfile | ||
- name: Run TypeScript type checker | ||
run: | | ||
yarn ts:check | ||
env: | ||
CI: true | ||
|
||
- name: Run formatting check | ||
run: | | ||
yarn format:check | ||
env: | ||
CI: true | ||
|
||
- name: Run linting check | ||
run: | | ||
yarn lint | ||
env: | ||
CI: true | ||
|
||
- name: Run tests | ||
run: | | ||
yarn test:ci | ||
env: | ||
CI: true | ||
|
||
- name: Upload coverage | ||
uses: codecov/codecov-action@v3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
pnpm-debug.log* | ||
lerna-debug.log* | ||
|
||
node_modules | ||
dist | ||
dist-ssr | ||
*.local | ||
|
||
# Editor directories and files | ||
.vscode/* | ||
!.vscode/extensions.json | ||
.idea | ||
.DS_Store | ||
*.suo | ||
*.ntvs* | ||
*.njsproj | ||
*.sln | ||
*.sw? | ||
|
||
# Vitest | ||
coverage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
dist | ||
node_modules |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,77 @@ | ||
# ethers-error-parser | ||
Parse Ethers.js errors with ease | ||
|
||
![Tests](https://github.com/enzoferey/ethers-error-parser/actions/workflows/test.yml/badge.svg) | ||
[![npm version](https://badge.fury.io/js/@enzoferey%2Fethers-error-parser.svg)](https://badge.fury.io/js/@enzoferey%2Fethers-error-parser) | ||
[![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) | ||
|
||
Parse Ethers.js errors with ease 💅🏻 | ||
|
||
## Highlights | ||
|
||
- Zero dependencies 🧹 | ||
- Lightweight (637 bytes gzipped) 📦 | ||
- Simple to use ⚡️ | ||
- Work in progress 🚧 | ||
|
||
## Why | ||
|
||
[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: | ||
|
||
``` | ||
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) | ||
``` | ||
|
||
Your users deserve to get amazing feedback on failing transactions. Thanks to `ethers-error-parser` you can do that with ease. | ||
|
||
## Work in progress | ||
|
||
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. | ||
|
||
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 🙏 | ||
|
||
## Getting started | ||
|
||
1. Install the package | ||
|
||
```sh | ||
yarn install @enzoferey/ethers-error-parser | ||
``` | ||
|
||
2. Use it | ||
|
||
```ts | ||
import { getParsedEthersError } from "@enzoferey/ethers-error-parser"; | ||
|
||
try { | ||
const transaction = await someContract.someMethod(); | ||
await transaction.wait(); | ||
} catch (error) { | ||
const parsedEthersError = getParsedEthersError(error); | ||
// parsedError.errorCode - contains a well defined error code (see full list below) | ||
// parsedError.context - contains a context based string providing additional information | ||
// profit ! 💅🏻 | ||
} | ||
``` | ||
|
||
## Return value | ||
|
||
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: | ||
|
||
```ts | ||
interface ReturnValue { | ||
errorCode: string; | ||
context?: string; | ||
} | ||
``` | ||
|
||
Here is the complete list of returned objects: | ||
|
||
| `errorCode` | `context` | | ||
| ---------------------------- | -------------------------------------------------------------------------- | | ||
| `TRANSACTION_RUN_OUT_OF_GAS` | The transaction gas limit as a string. | | ||
| `TRANSACTION_UNDERPRICED` | `undefined` | | ||
| `REJECTED_TRANSACTION` | The reason why the transaction rejected. | | ||
| `EXECUTION_REVERTED` | The reason why the transaction reverted. | | ||
| `UNKNOWN_ERROR` | Some code or description of the error if available. `undefined` otherwise. | | ||
|
||
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 🙏 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import { describe, expect, it } from "vitest"; | ||
|
||
import type { BigNumber } from "../types"; | ||
import { ERROR_CODES, ETHERS_ERROR_CODES } from "../constants"; | ||
import { getParsedEthersError } from "../getParsedEthersError"; | ||
|
||
function getTestBigNumber(value: string): BigNumber { | ||
return { | ||
gte: (other) => { | ||
return parseFloat(value) >= parseFloat(other.toString()); | ||
}, | ||
toString: () => { | ||
return value; | ||
}, | ||
}; | ||
} | ||
|
||
describe("getParsedEthersError", () => { | ||
it("should handle transaction underpriced at the top level", () => { | ||
const result = getParsedEthersError({ | ||
code: ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC, | ||
message: `RPC '{"value":{"data":{"code":${ETHERS_ERROR_CODES.TRANSACTION_UNDERPRICED}}}}'`, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
errorCode: ERROR_CODES.TRANSACTION_UNDERPRICED, | ||
context: undefined, | ||
}); | ||
}); | ||
it("should handle transaction underpriced at the nested level", () => { | ||
const result = getParsedEthersError({ | ||
error: { | ||
code: ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC, | ||
message: `RPC '{"value":{"data":{"code":${ETHERS_ERROR_CODES.TRANSACTION_UNDERPRICED}}}}'`, | ||
}, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
errorCode: ERROR_CODES.TRANSACTION_UNDERPRICED, | ||
context: undefined, | ||
}); | ||
}); | ||
it("should handle transaction underpriced that does not provide the right JSON details", () => { | ||
const result1 = getParsedEthersError({ | ||
code: ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC, | ||
message: `RPC '{"value":{"data":{"code":"SOME NON EXPECTED CODE"}}}'`, | ||
}); | ||
|
||
expect(result1).toEqual({ | ||
errorCode: ERROR_CODES.UNKNOWN_ERROR, | ||
context: | ||
ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC.toString(), | ||
}); | ||
|
||
const result2 = getParsedEthersError({ | ||
code: ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC, | ||
message: `RPC '{}'`, | ||
}); | ||
|
||
expect(result2).toEqual({ | ||
errorCode: ERROR_CODES.UNKNOWN_ERROR, | ||
context: | ||
ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC.toString(), | ||
}); | ||
}); | ||
it("should handle transaction underpriced that does not provide a valid JSON details", () => { | ||
const result = getParsedEthersError({ | ||
code: ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC, | ||
message: `RPC 'not-a-json'`, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
errorCode: ERROR_CODES.UNKNOWN_ERROR, | ||
context: | ||
ETHERS_ERROR_CODES.ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC.toString(), | ||
}); | ||
}); | ||
it("should handle transaction rejected at the top level", () => { | ||
const message = "User rejected transaction"; | ||
|
||
const result = getParsedEthersError({ | ||
code: ETHERS_ERROR_CODES.REJECTED_TRANSACTION, | ||
message, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
errorCode: ERROR_CODES.REJECTED_TRANSACTION, | ||
context: message, | ||
}); | ||
}); | ||
it("should handle transaction rejected at the nested level", () => { | ||
const message = "User rejected transaction"; | ||
|
||
const result = getParsedEthersError({ | ||
error: { | ||
code: ETHERS_ERROR_CODES.REJECTED_TRANSACTION, | ||
message, | ||
}, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
errorCode: ERROR_CODES.REJECTED_TRANSACTION, | ||
context: message, | ||
}); | ||
}); | ||
it("should handle execution reverted at the top level", () => { | ||
const reason = "Some reason"; | ||
|
||
const result = getParsedEthersError({ | ||
code: ETHERS_ERROR_CODES.REQUIRE_TRANSACTION, | ||
message: `execution reverted: ${reason}`, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
errorCode: ERROR_CODES.EXECUTION_REVERTED, | ||
context: reason, | ||
}); | ||
}); | ||
it("should handle execution reverted at the nested level", () => { | ||
const reason = "Some reason"; | ||
|
||
const result = getParsedEthersError({ | ||
error: { | ||
code: ETHERS_ERROR_CODES.REQUIRE_TRANSACTION, | ||
message: `execution reverted: ${reason}`, | ||
}, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
errorCode: ERROR_CODES.EXECUTION_REVERTED, | ||
context: reason, | ||
}); | ||
}); | ||
it("should handle transaction run out of gas errors", () => { | ||
const gasLimit = getTestBigNumber("100"); | ||
const gasUsed = getTestBigNumber("100"); | ||
|
||
const result = getParsedEthersError({ | ||
transaction: { | ||
gasLimit, | ||
}, | ||
receipt: { | ||
gasUsed, | ||
}, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
errorCode: ERROR_CODES.TRANSACTION_RUN_OUT_OF_GAS, | ||
context: gasLimit.toString(), | ||
}); | ||
}); | ||
it("should handle unknown errors with a nested level error message", () => { | ||
const message = "Some internal Ethers error message"; | ||
|
||
const result = getParsedEthersError({ | ||
error: { | ||
message, | ||
}, | ||
}); | ||
|
||
expect(result).toEqual({ | ||
errorCode: ERROR_CODES.UNKNOWN_ERROR, | ||
context: message, | ||
}); | ||
}); | ||
it("should handle unknown errors with a top level error code", () => { | ||
const code = "SOME INTERNAL ETHERS CODE"; | ||
|
||
const result = getParsedEthersError({ code }); | ||
|
||
expect(result).toEqual({ | ||
errorCode: ERROR_CODES.UNKNOWN_ERROR, | ||
context: code, | ||
}); | ||
}); | ||
it("should handle totally unknown errors", () => { | ||
const result = getParsedEthersError({}); | ||
|
||
expect(result).toEqual({ | ||
errorCode: ERROR_CODES.UNKNOWN_ERROR, | ||
context: undefined, | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
export const ERROR_CODES = { | ||
TRANSACTION_RUN_OUT_OF_GAS: "TRANSACTION_RUN_OUT_OF_GAS", | ||
TRANSACTION_UNDERPRICED: "TRANSACTION_UNDERPRICED", | ||
REJECTED_TRANSACTION: "REJECTED_TRANSACTION", | ||
EXECUTION_REVERTED: "EXECUTION_REVERTED", | ||
UNKNOWN_ERROR: "UNKNOWN_ERROR", | ||
} as const; | ||
|
||
export const ETHERS_ERROR_CODES = { | ||
REJECTED_TRANSACTION: 4001, | ||
REQUIRE_TRANSACTION: -32603, | ||
ERROR_WHILE_FORMATTING_OUTPUT_FROM_RPC: -32603, | ||
TRANSACTION_UNDERPRICED: -32000, | ||
}; |
Oops, something went wrong.