Skip to content

Commit

Permalink
feat: implement first release
Browse files Browse the repository at this point in the history
  • Loading branch information
enzoferey authored Jul 23, 2022
1 parent b2041f3 commit bd989a3
Show file tree
Hide file tree
Showing 17 changed files with 2,340 additions and 1 deletion.
13 changes: 13 additions & 0 deletions .eslintrc
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
}
}
66 changes: 66 additions & 0 deletions .github/workflows/test.yml
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
27 changes: 27 additions & 0 deletions .gitignore
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
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
node_modules
Empty file added .prettierrc
Empty file.
77 changes: 76 additions & 1 deletion README.md
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 🙏
184 changes: 184 additions & 0 deletions lib/__tests__/getParsedEthersError.test.ts
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,
});
});
});
14 changes: 14 additions & 0 deletions lib/constants.ts
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,
};
Loading

0 comments on commit bd989a3

Please sign in to comment.