Skip to content

Commit

Permalink
chore(sdk): Make the "error handler" give more information (#777)
Browse files Browse the repository at this point in the history
  • Loading branch information
Chirag-S-Kotian authored Nov 8, 2024
1 parent 58abb93 commit 6fd6a63
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 25 deletions.
80 changes: 59 additions & 21 deletions sdk/src/utils/errorHandler.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BaseError, ContractFunctionRevertedError, Abi } from "viem";
import { handleError } from "./errorHandler";
import { handleError, extractErrorName } from "./errorHandler";
import { ActionType } from "./constants";

describe("errorHandler", () => {
Expand Down Expand Up @@ -29,50 +29,88 @@ describe("errorHandler", () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe("extractErrorName", () => {
it("should return errorName if it exists in revertError.data", () => {
const errorName = extractErrorName(mockRevertedError);
expect(errorName).toBe("MockErrorName");
});

it("should return the signature if errorName is undefined", () => {
(mockRevertedError.data as Partial<typeof mockRevertedError.data>).errorName = undefined;
mockRevertedError.signature = "myFunction(uint256)" as `0x${string}`;

const errorName = extractErrorName(mockRevertedError);
expect(errorName).toBe("myFunction(uint256)");
});

it("should return 'unknown revert reason' if both errorName and signature are undefined", () => {
(mockRevertedError.data as Partial<typeof mockRevertedError.data>).errorName = undefined;
mockRevertedError.signature = undefined;

const errorName = extractErrorName(mockRevertedError);
expect(errorName).toBe("unknown revert reason");
});
});

describe("handleError", () => {
const actionType: ActionType = ActionType.Transaction;

it("should throw with the revert error name if error is a ContractFunctionRevertedError", () => {
it("should throw 'An unknown error occurred' if no shortMessage is present", () => {
const mockBaseErrorWithoutShortMessage = new BaseError("Base error");
jest.spyOn(mockBaseErrorWithoutShortMessage, "walk").mockImplementation(() => null);

expect(() => handleError(actionType, mockBaseErrorWithoutShortMessage)).toThrow(
`${actionType} failed with Base error`,
);
});

it("should throw with the revert signature if errorName is undefined", () => {
(mockRevertedError.data as Partial<typeof mockRevertedError.data>).errorName = undefined;
mockRevertedError.signature = "myFunction(uint256)" as `0x${string}`;

jest.spyOn(mockBaseError, "walk").mockImplementation((fn: (arg0: unknown) => unknown) => {
return fn(mockRevertedError) ? mockRevertedError : null;
});

expect(() => handleError(actionType, mockBaseError)).toThrow(`${actionType} failed with MockErrorName`);
expect(() => handleError(actionType, mockBaseError)).toThrow(`${actionType} failed with myFunction(uint256)`);
});

it("should throw a generic error message if errorName is undefined", () => {
// Temporarily cast mockRevertedError.data to bypass TypeScript checks for testing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(mockRevertedError.data as any).errorName = undefined; // Simulate errorName being `undefined`
it("should throw 'unknown revert reason' if both errorName and signature are undefined", () => {
(mockRevertedError.data as Partial<typeof mockRevertedError.data>).errorName = undefined;
mockRevertedError.signature = undefined;

jest.spyOn(mockBaseError, "walk").mockImplementation((fn: (arg0: unknown) => unknown) => {
return fn(mockRevertedError) ? mockRevertedError : null;
});

// This should test the code path where `errorName` is `undefined` and fallback to an empty string
expect(() => handleError(actionType, mockBaseError)).toThrow(`${actionType} failed with `);
expect(() => handleError(actionType, mockBaseError)).toThrow(`${actionType} failed with unknown revert reason`);
});

it("should throw a generic error message if it's an instance of BaseError but not ContractFunctionRevertedError", () => {
const mockBaseError = new BaseError("Base error");
it("should throw with shortMessage if error is a BaseError but not ContractFunctionRevertedError", () => {
const shortMessage = "A short message";
const mockBaseErrorWithShortMessage = new BaseError("Base error");
Object.defineProperty(mockBaseErrorWithShortMessage, "shortMessage", {
get: () => shortMessage,
});

jest.spyOn(mockBaseError, "walk").mockImplementation(() => null);
jest.spyOn(mockBaseErrorWithShortMessage, "walk").mockImplementation(() => null);

expect(() => handleError(actionType, mockBaseError)).toThrow("${type} failed");
expect(() => handleError(actionType, mockBaseErrorWithShortMessage)).toThrow(
`${actionType} failed with ${shortMessage}`,
);
});

it("should throw a generic error message if error is not an instance of BaseError", () => {
const genericError = "Something went wrong";
it("should throw with the error message if error is a native JavaScript Error", () => {
const nativeError = new Error("Native error message");

expect(() => handleError(actionType, genericError)).toThrow(`${actionType} failed with ${genericError}`);
expect(() => handleError(actionType, nativeError)).toThrow(`${actionType} failed with Native error message`);
});

it("should throw a generic error message even when there is no errorName in ContractFunctionRevertedError", () => {
jest.spyOn(mockBaseError, "walk").mockImplementation((fn: (arg0: unknown) => unknown) => {
return fn(mockRevertedError) ? mockRevertedError : null;
});
it("should throw 'unknown error' if the error is not an instance of BaseError or Error", () => {
const unknownError = { message: "Some unknown error" };

expect(() => handleError(actionType, mockBaseError)).toThrow(`${actionType} failed with `);
expect(() => handleError(actionType, unknownError)).toThrow(`${actionType} failed with an unknown error`);
});
});
});
21 changes: 17 additions & 4 deletions sdk/src/utils/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import { BaseError, ContractFunctionRevertedError } from "viem";
import { ActionType } from "./constants";

export function extractErrorName(revertError: ContractFunctionRevertedError): string {
if (revertError.data?.errorName) {
return revertError.data.errorName;
}
if (revertError.signature) {
return revertError.signature;
}
return "unknown revert reason";
}

export function handleError(type: ActionType, err: unknown): never {
if (err instanceof BaseError) {
const revertError = err.walk((err) => err instanceof ContractFunctionRevertedError);
if (revertError instanceof ContractFunctionRevertedError) {
const errorName = revertError.data?.errorName ?? "";
const errorName = extractErrorName(revertError);
throw new Error(`${type} failed with ${errorName}`);
} else {
const errorMessage = err.shortMessage ?? "An unknown error occurred";
throw new Error(`${type} failed with ${errorMessage}`);
}
} else if (err instanceof Error) {
throw new Error(`${type} failed with ${err.message}`);
} else {
throw new Error(`${type} failed with ${err}`);
throw new Error(`${type} failed with an unknown error`);
}

throw new Error("${type} failed");
}

0 comments on commit 6fd6a63

Please sign in to comment.