Skip to content

Commit

Permalink
refactor: error handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
superical committed Jan 24, 2024
1 parent 5bd53df commit b581451
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 52 deletions.
4 changes: 2 additions & 2 deletions src/common/enums.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export enum ErrorType {
UserRejectError = 'UserRejectError',
EmptyError = 'EmptyError',
RevertError = 'RevertError',
PanicError = 'PanicError',
UnknownError = 'UnknownError',
CustomError = 'CustomError',
UserRejectError = 'UserRejectError',
RpcError = 'RpcError',
UnknownError = 'UnknownError',
}
60 changes: 24 additions & 36 deletions src/error-decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,26 @@ import {
ErrorHandler,
PanicErrorHandler,
RevertErrorHandler,
RpcErrorHandler,
UserRejectionHandler,
} from './errors/handlers'
import { rpcErrorResult, unknownErrorResult, userRejectErrorResult } from './errors/results'
import { unknownErrorResult } from './errors/results'

export class ErrorDecoder {
private readonly errorHandlers: {
predicate: (data: string) => boolean
handler: ErrorHandler['handle']
}[] = []
private readonly errorHandlers: ErrorHandler[] = []

private constructor(
handlers: ErrorHandler[],
public readonly errorInterface: Interface | undefined,
) {
this.errorHandlers = handlers.map((handler) => ({
predicate: handler.predicate,
handler: handler.handle,
handle: handler.handle,
}))
}

private async getContractOrTransactionError(error: Error): Promise<Error> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errorReceipt = (error as any).receipt as TransactionReceipt

if (!errorReceipt) return error
Expand Down Expand Up @@ -56,12 +56,12 @@ export class ErrorDecoder {
}
}

private getDataFromError(error: Error): string {
private getDataFromError(error: Error): string | undefined {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errorData = (error as any).data ?? (error as any).error?.data

if (errorData === undefined) {
throw error
return undefined
}

let returnData = typeof errorData === 'string' ? errorData : errorData.data
Expand All @@ -71,7 +71,7 @@ export class ErrorDecoder {
}

if (returnData === undefined || typeof returnData !== 'string') {
throw error
return undefined
}

return returnData
Expand All @@ -82,39 +82,25 @@ export class ErrorDecoder {
return unknownErrorResult({
data: undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reason: (error as any).message ?? 'Unexpected error',
reason: (error as any).message ?? 'Invalid error',
})
}

try {
const targetError = await this.getContractOrTransactionError(error)
const returnData = this.getDataFromError(targetError)
const targetError = await this.getContractOrTransactionError(error)
const returnData = this.getDataFromError(targetError)

for (const { predicate, handler } of this.errorHandlers) {
if (predicate(returnData)) {
return handler(returnData, this.errorInterface)
}
for (const { predicate, handle } of this.errorHandlers) {
if (predicate(returnData, targetError)) {
return handle(returnData, { errorInterface: this.errorInterface, error: targetError })
}

return unknownErrorResult({
data: returnData,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reason: (error as any).message ?? 'Unrecognised error',
})
} catch (e) {
if (error.message) {
if (error.message.includes('rejected transaction')) {
return userRejectErrorResult({ data: null, reason: error.message })
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rpcError = error as any
if (rpcError.code !== undefined) {
return rpcErrorResult({ data: null, name: rpcError.code, reason: error.message })
}
return unknownErrorResult({ data: null, reason: error.message })
}
return unknownErrorResult({ data: null })
}

return unknownErrorResult({
data: returnData,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reason: (targetError as any)?.message ?? 'Unexpected error',
name: targetError?.name,
})
}

public static create(
Expand Down Expand Up @@ -142,6 +128,8 @@ export class ErrorDecoder {
new RevertErrorHandler(),
new PanicErrorHandler(),
new CustomErrorHandler(),
new UserRejectionHandler(),
new RpcErrorHandler(),
...(additionalErrorHandlers ?? []),
]
return new ErrorDecoder(handlers, errorInterface)
Expand Down
50 changes: 44 additions & 6 deletions src/errors/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ import {
emptyErrorResult,
panicErrorResult,
revertErrorResult,
rpcErrorResult,
unknownErrorResult,
userRejectErrorResult,
} from './results'

type ErrorHandlerErrorInfo = { errorInterface: Interface; error: Error }

export interface ErrorHandler {
predicate: (data: string) => boolean
handle: (data: string, errorInterface?: Interface) => DecodedError
predicate: (data: string | undefined, error: Error) => boolean
handle: (data: string | undefined, errorInfo: ErrorHandlerErrorInfo) => DecodedError
}

export class EmptyErrorHandler implements ErrorHandler {
Expand All @@ -27,7 +31,7 @@ export class EmptyErrorHandler implements ErrorHandler {

export class RevertErrorHandler implements ErrorHandler {
public predicate(data: string): boolean {
return data.startsWith(ERROR_STRING_PREFIX)
return data?.startsWith(ERROR_STRING_PREFIX)
}

public handle(data: string): DecodedError {
Expand All @@ -46,7 +50,7 @@ export class RevertErrorHandler implements ErrorHandler {

export class PanicErrorHandler implements ErrorHandler {
public predicate(data: string): boolean {
return data.startsWith(PANIC_CODE_PREFIX)
return data?.startsWith(PANIC_CODE_PREFIX)
}

public handle(data: string): DecodedError {
Expand All @@ -66,11 +70,14 @@ export class PanicErrorHandler implements ErrorHandler {
export class CustomErrorHandler implements ErrorHandler {
public predicate(data: string): boolean {
return (
data !== '0x' && !data.startsWith(ERROR_STRING_PREFIX) && !data.startsWith(PANIC_CODE_PREFIX)
data &&
data !== '0x' &&
!data?.startsWith(ERROR_STRING_PREFIX) &&
!data?.startsWith(PANIC_CODE_PREFIX)
)
}

public handle(data: string, errorInterface?: Interface): DecodedError {
public handle(data: string, { errorInterface }: ErrorHandlerErrorInfo): DecodedError {
let result: Parameters<typeof customErrorResult>[0] = { data }
if (errorInterface) {
const customError = errorInterface.parseError(data)
Expand All @@ -84,3 +91,34 @@ export class CustomErrorHandler implements ErrorHandler {
return customErrorResult(result)
}
}

export class UserRejectionHandler implements ErrorHandler {
public predicate(data: string, error: Error): boolean {
return !data && error?.message?.includes('rejected transaction')
}

public handle(_data: string, { error }: ErrorHandlerErrorInfo): DecodedError {
return userRejectErrorResult({
data: null,
reason: error.message ?? 'The transaction was rejected',
})
}
}

export class RpcErrorHandler implements ErrorHandler {
public predicate(data: string, error: Error): boolean {
return (
!data &&
error.message &&
!error?.message?.includes('rejected transaction') &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error as any).code !== undefined
)
}

public handle(_data: string, { error }: ErrorHandlerErrorInfo): DecodedError {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rpcError = error as any
return rpcErrorResult({ data: null, name: rpcError.code, reason: rpcError.message })
}
}
18 changes: 12 additions & 6 deletions src/errors/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@ type ErrorResultFormatterParam = {

type ErrorResultFormatter = (params: ErrorResultFormatterParam) => DecodedError

const formatReason = (
reason: string | undefined | null,
defaultReason: string | null,
): string | null => (reason && reason.trim() !== '' ? reason : defaultReason)

const baseErrorResult: (
params: ErrorResultFormatterParam & { type: ErrorType },
) => DecodedError = ({ type, data, reason, fragment, args, selector, name }) => {
let res: DecodedError = {
type,
reason: reason ?? null,
reason: formatReason(reason, null),
data: data ?? null,
fragment: null,
args: args ?? new Result(),
Expand All @@ -45,7 +50,7 @@ export const emptyErrorResult: ErrorResultFormatter = ({ data }) =>
export const userRejectErrorResult: ErrorResultFormatter = ({ data = null, reason }) =>
baseErrorResult({
type: ErrorType.UserRejectError,
reason: reason ?? 'User has rejected the transaction',
reason: formatReason(reason, 'User has rejected the transaction'),
data,
})

Expand All @@ -59,11 +64,12 @@ export const revertErrorResult: ErrorResultFormatter = ({ data, reason, fragment
})
}

export const unknownErrorResult: ErrorResultFormatter = ({ data, reason }) => {
export const unknownErrorResult: ErrorResultFormatter = ({ data, reason, name }) => {
return baseErrorResult({
type: ErrorType.UnknownError,
reason: reason ?? 'Unknown error',
reason: formatReason(reason, 'Unknown error'),
data,
name
})
}

Expand All @@ -79,7 +85,7 @@ export const customErrorResult: ErrorResultFormatter = ({ data, reason, fragment
const selector = data.slice(0, 10)
return baseErrorResult({
type: ErrorType.CustomError,
reason: reason ?? `No ABI for custom error ${selector}`,
reason: formatReason(reason, `No ABI for custom error ${selector}`),
data,
fragment,
args,
Expand All @@ -91,7 +97,7 @@ export const customErrorResult: ErrorResultFormatter = ({ data, reason, fragment
export const rpcErrorResult: ErrorResultFormatter = ({ reason, name }) =>
baseErrorResult({
type: ErrorType.RpcError,
reason: reason ?? 'Error from JSON RPC provider',
reason: formatReason(reason, 'Error from JSON RPC provider'),
data: null,
name: name?.toString() ?? null,
})
4 changes: 2 additions & 2 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ describe('ErrorDecoder', () => {
expect(decodedError.reason).to.equal(fakeErrorMsg)
})

it('should return error message as Unexpected error', async () => {
expect(decodedError.reason).to.equal('Unexpected error')
it('should return error message as Invalid error', async () => {
expect(decodedError.reason).to.equal('Invalid error')
})

it('should return null data', async () => {
Expand Down

0 comments on commit b581451

Please sign in to comment.