Skip to content

Commit

Permalink
feat: improve handling of errors (#191)
Browse files Browse the repository at this point in the history
  • Loading branch information
DNR500 authored Jul 18, 2024
1 parent b4f2daa commit 3087baa
Show file tree
Hide file tree
Showing 40 changed files with 1,746 additions and 1,077 deletions.
27 changes: 15 additions & 12 deletions src/core/EVM/EVMStepExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@ import { publicActions } from 'viem'
import { config } from '../../config.js'
import { getStepTransaction } from '../../services/api.js'
import {
LiFiErrorCode,
TransactionError,
ValidationError,
getTransactionFailedMessage,
isZeroAddress,
parseError,
} from '../../utils/index.js'
import { ValidationError, TransactionError } from '../../errors/errors.js'
import { LiFiErrorCode } from '../../errors/constants.js'
import { parseEVMErrors } from './parseEVMErrors.js'
import { BaseStepExecutor } from '../BaseStepExecutor.js'
import { checkBalance } from '../checkBalance.js'
import { getSubstatusMessage } from '../processMessages.js'
Expand Down Expand Up @@ -87,9 +86,13 @@ export class EVMStepExecutor extends BaseStepExecutor {
},
})
this.statusManager.updateExecution(step, 'FAILED')
throw new TransactionError(
LiFiErrorCode.WalletChangedDuringExecution,
errorMessage
throw await parseEVMErrors(
new TransactionError(
LiFiErrorCode.WalletChangedDuringExecution,
errorMessage
),
step,
process
)
}
return updatedWalletClient
Expand Down Expand Up @@ -397,20 +400,21 @@ export class EVMStepExecutor extends BaseStepExecutor {
process = this.statusManager.updateProcess(step, process.type, 'DONE')
}
} catch (e: any) {
const error = await parseError(e, step, process)
const error = await parseEVMErrors(e, step, process)
process = this.statusManager.updateProcess(
step,
process.type,
'FAILED',
{
error: {
message: error.message,
htmlMessage: error.htmlMessage,
message: error.cause.message,
htmlMessage: error.cause.htmlMessage,
code: error.code,
},
}
)
this.statusManager.updateExecution(step, 'FAILED')

throw error
}
}
Expand Down Expand Up @@ -479,8 +483,7 @@ export class EVMStepExecutor extends BaseStepExecutor {
},
})
this.statusManager.updateExecution(step, 'FAILED')
console.warn(e)
throw e
throw await parseEVMErrors(e as Error, step, process)
}

// DONE
Expand Down
8 changes: 4 additions & 4 deletions src/core/EVM/checkAllowance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Chain, LiFiStep, Process, ProcessType } from '@lifi/types'
import type { Address, Hash, WalletClient } from 'viem'
import { maxUint256 } from 'viem'
import { parseError } from '../../utils/parseError.js'
import { parseEVMErrors } from './parseEVMErrors.js'
import type { StatusManager } from '../StatusManager.js'
import type { ExecutionOptions } from '../types.js'
import { getAllowance } from './getAllowance.js'
Expand Down Expand Up @@ -100,15 +100,15 @@ export const checkAllowance = async (
}
}
} catch (e: any) {
const error = await parseError(e, step, allowanceProcess)
const error = await parseEVMErrors(e, step, allowanceProcess)
allowanceProcess = statusManager.updateProcess(
step,
allowanceProcess.type,
'FAILED',
{
error: {
message: error.message,
htmlMessage: error.htmlMessage,
message: error.cause.message,
htmlMessage: error.cause.htmlMessage,
code: error.code,
},
}
Expand Down
3 changes: 2 additions & 1 deletion src/core/EVM/multisig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ExtendedChain, LiFiStep, ProcessType } from '@lifi/types'
import type { Hash } from 'viem'
import { LiFiErrorCode, TransactionError } from '../../utils/errors.js'
import { LiFiErrorCode } from '../../errors/constants.js'
import { TransactionError } from '../../errors/errors.js'
import type { StatusManager } from '../StatusManager.js'
import type { MultisigConfig, MultisigTxDetails } from './types.js'

Expand Down
66 changes: 66 additions & 0 deletions src/core/EVM/parseEVMErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { type LiFiStep, type Process } from '@lifi/types'
import { TransactionError, UnknownError } from '../../errors/errors.js'
import { SDKError } from '../../errors/SDKError.js'
import { ErrorMessage, LiFiErrorCode } from '../../errors/constants.js'
import { BaseError } from '../../utils/index.js'
import { fetchTxErrorDetails } from '../../helpers.js'

export const parseEVMErrors = async (
e: Error,
step?: LiFiStep,
process?: Process
): Promise<SDKError> => {
if (e instanceof SDKError) {
e.step = e.step ?? step
e.process = e.process ?? process
return e
}

const baseError = await handleSpecificErrors(e, step, process)

return new SDKError(baseError, step, process)
}

const handleSpecificErrors = async (
e: any,
step?: LiFiStep,
process?: Process
) => {
if (e.cause?.name === 'UserRejectedRequestError') {
return new TransactionError(
LiFiErrorCode.SignatureRejected,
e.message,
undefined,
e
)
}

if (
step &&
process?.txHash &&
e.code === LiFiErrorCode.TransactionFailed &&
e.message === ErrorMessage.TransactionReverted
) {
const response = await fetchTxErrorDetails(
process.txHash,
step.action.fromChainId
)

const errorMessage = response?.error_message

if (errorMessage?.toLowerCase().includes('out of gas')) {
return new TransactionError(
LiFiErrorCode.GasLimitError,
ErrorMessage.GasLimitLow,
undefined,
e
)
}
}

if (e instanceof BaseError) {
return e
}

return new UnknownError(e.message || ErrorMessage.UnknownError, undefined, e)
}
215 changes: 215 additions & 0 deletions src/core/EVM/parseEVMErrors.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { setupTestEnvironment } from '../../../tests/setup.js'
import { parseEVMErrors } from './parseEVMErrors.js'
import {
ErrorName,
BaseError,
LiFiErrorCode,
SDKError,
TransactionError,
ErrorMessage,
} from '../../utils/index.js'
import { buildStepObject } from '../../../tests/fixtures.js'
import type { LiFiStep, Process } from '@lifi/types'
import * as helpers from '../../helpers.js'

beforeAll(setupTestEnvironment)

describe('parseEVMStepErrors', () => {
describe('when a SDKError is passed', async () => {
it('should return the original error', async () => {
const error = new SDKError(
new BaseError(
ErrorName.UnknownError,
LiFiErrorCode.InternalError,
'there was an error'
)
)

const parsedError = await parseEVMErrors(error)

expect(parsedError).toBe(error)

expect(parsedError.step).toBeUndefined()
expect(parsedError.process).toBeUndefined()
})
})

describe('when step and process is passed', () => {
it('should return the original error with step and process added', async () => {
const error = new SDKError(
new BaseError(
ErrorName.UnknownError,
LiFiErrorCode.InternalError,
'there was an error'
)
)

const step = buildStepObject({ includingExecution: true })
const process = step.execution!.process[0]

const parsedError = await parseEVMErrors(error, step, process)

expect(parsedError).toBe(error)

expect(parsedError.step).toBe(step)
expect(parsedError.process).toBe(process)
})
})

describe('when the SDKError already has a step and process', () => {
it('should return the original error with teh existing step and process specified', async () => {
const expectedStep = buildStepObject({ includingExecution: true })
const expectedProcess = expectedStep.execution!.process[0]

const error = new SDKError(
new BaseError(
ErrorName.UnknownError,
LiFiErrorCode.InternalError,
'there was an error'
),
expectedStep,
expectedProcess
)

const step = buildStepObject({ includingExecution: true })
const process = step.execution!.process[0]

const parsedError = await parseEVMErrors(error, step, process)

expect(parsedError).toBe(error)

expect(parsedError.step).toBe(expectedStep)
expect(parsedError.process).toBe(expectedProcess)
})
})

describe('when a BaseError is passed', () => {
it('should return the BaseError as the cause on a SDKError', async () => {
const error = new BaseError(
ErrorName.BalanceError,
LiFiErrorCode.BalanceError,
'there was an error'
)

const parsedError = await parseEVMErrors(error)

expect(parsedError).toBeInstanceOf(SDKError)
expect(parsedError.step).toBeUndefined()
expect(parsedError.process).toBeUndefined()
expect(parsedError.cause).toBe(error)
})

describe('when step and process is passed', () => {
it('should return the SDKError with step and process added', async () => {
const error = new BaseError(
ErrorName.BalanceError,
LiFiErrorCode.BalanceError,
'there was an error'
)

const step = buildStepObject({ includingExecution: true })
const process = step.execution!.process[0]

const parsedError = await parseEVMErrors(error, step, process)

expect(parsedError).toBeInstanceOf(SDKError)
expect(parsedError.step).toBe(step)
expect(parsedError.process).toBe(process)
expect(parsedError.cause).toBe(error)
})
})
})

describe('when a generic Error is passed', () => {
it('should return the Error as he cause on a BaseError which is wrapped in an SDKError', async () => {
const error = new Error('Somethings fishy')

const parsedError = await parseEVMErrors(error)
expect(parsedError).toBeInstanceOf(SDKError)
expect(parsedError.step).toBeUndefined()
expect(parsedError.process).toBeUndefined()

const baseError = parsedError.cause
expect(baseError).toBeInstanceOf(BaseError)

const causeError = baseError.cause
expect(causeError).toBe(error)
})

describe('when step and process is passed', () => {
it('should return an SDKError with step and process added', async () => {
const error = new Error('Somethings fishy')

const step = buildStepObject({ includingExecution: true })
const process = step.execution?.process[0]

const parsedError = await parseEVMErrors(error, step, process)
expect(parsedError).toBeInstanceOf(SDKError)
expect(parsedError.step).toBe(step)
expect(parsedError.process).toBe(process)
})
})
})

describe('when specific Errors are passed', () => {
describe('when the error is the viem UserRejectedRequestError error', () => {
it('should return the BaseError with the SignatureRejected code as the cause on a SDKError', async () => {
const mockViemError = new Error()
const UserRejectedRequestError = new Error()
UserRejectedRequestError.name = 'UserRejectedRequestError'
mockViemError.cause = UserRejectedRequestError

const parsedError = await parseEVMErrors(mockViemError)

expect(parsedError).toBeInstanceOf(SDKError)

const baseError = parsedError.cause
expect(baseError).toBeInstanceOf(TransactionError)
expect(baseError.code).toEqual(LiFiErrorCode.SignatureRejected)

expect(baseError.cause?.cause).toBe(UserRejectedRequestError)
})
})
})

describe('when the error is a Transaction reverted error caused by low gas', () => {
it('should return the TransactionError with the GasLimitError code and GasLimitLow message', async () => {
vi.spyOn(helpers, 'fetchTxErrorDetails').mockResolvedValue({
error_message: 'out of gas',
})

const mockTransactionError = new TransactionError(
LiFiErrorCode.TransactionFailed,
ErrorMessage.TransactionReverted
)

const mockStep = {
action: {
fromChainId: 10,
},
} as LiFiStep

const mockProcess = {
txHash:
'0x5c73f72a72a75d8b716ed42cd620042f53b958f028d0c9ad772908b7791c017b',
} as Process

const parsedError = await parseEVMErrors(
mockTransactionError,
mockStep,
mockProcess
)

expect(parsedError).toBeInstanceOf(SDKError)

const baseError = parsedError.cause
expect(baseError).toBeInstanceOf(TransactionError)
expect(baseError.code).toEqual(LiFiErrorCode.GasLimitError)
expect(baseError.message).toEqual(ErrorMessage.GasLimitLow)
expect(baseError.cause).toBe(mockTransactionError)

vi.clearAllMocks()
})
})
})
3 changes: 2 additions & 1 deletion src/core/EVM/switchChain.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { WalletClient } from 'viem'
import { LiFiErrorCode, ProviderError } from '../../utils/errors.js'
import { LiFiErrorCode } from '../../errors/constants.js'
import { ProviderError } from '../../errors/errors.js'
import type { StatusManager } from '../StatusManager.js'
import type { LiFiStepExtended, SwitchChainHook } from '../types.js'

Expand Down
Loading

0 comments on commit 3087baa

Please sign in to comment.