Skip to content

Commit

Permalink
🪚 OmniGraph™ Standalone error parser for EVM (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
janjakubnanista authored Dec 12, 2023
1 parent c9c1bdb commit 24eb448
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 28 deletions.
82 changes: 55 additions & 27 deletions packages/utils-evm/src/errors/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { OmniContract, OmniContractFactory } from '@/omnigraph/types'
import type { OmniError } from '@layerzerolabs/utils'
import { ContractError, CustomError, UnknownError, PanicError, RevertError } from './errors'
import { BigNumberishBigintSchema } from '../schema'
import type { Contract } from '@ethersproject/contracts'
import type { OmniContractErrorParser } from './types'

/**
* Creates an asynchronous error parser for EVM contract errors.
Expand All @@ -15,32 +17,58 @@ import { BigNumberishBigintSchema } from '../schema'
*/
export const createErrorParser =
(contractFactory: OmniContractFactory) =>
async ({ error, point }: OmniError<unknown>): Promise<OmniError<ContractError>> => {
try {
// If the error already is a ContractError, we'll continue
if (error instanceof ContractError) return { error, point }

// If the error is unknown we'll try to decode basic errors
const candidates = getErrorDataCandidates(error)
const [basicError] = candidates.flatMap(basicDecoder)
if (basicError != null) return { point, error: basicError }

// Then we'll try to decode custom errors
const contract = await contractFactory(point)
const contractDecoder = createContractDecoder(contract)
const [customError] = candidates.flatMap(contractDecoder)
if (customError != null) return { point, error: customError }

// If none of the decoding works, we'll send a generic error back
return { point, error: new UnknownError(`Unknown error: ${toStringSafe(error)}`) }
} catch {
// If we fail, we send an unknown error back
return {
point,
error: new UnknownError(`Unexpected error: ${toStringSafe(error)}`),
}
}
async ({ error, point }: OmniError<unknown>): Promise<OmniError<ContractError>> => ({
point,
error: createContractErrorParser(await contractFactory(point))(error),
})

/**
* Creates an error parser based on a specific `OmniContract`
*
* This call will never fail and will always return an instance of `ContractError`
*
* @param {OmniContract | null | undefined} contract
* @returns {ContractError}
*/
export const createContractErrorParser =
(contract: OmniContract | null | undefined): OmniContractErrorParser =>
(error) =>
// First we'll try to decode a contract error if we have a contract
(contract ? parseContractError(error, contract.contract) : null) ??
// Then we'll try decoding a generic one
parseGenericError(error) ??
// The we throw a generic one
new UnknownError(`Unknown error: ${toStringSafe(error)}`)

export const parseContractError = (error: unknown, contract: Contract): ContractError | undefined => {
// If the error already is a ContractError, we'll continue
if (error instanceof ContractError) return error

try {
// If the error is unknown we'll try to decode basic errors
const candidates = getErrorDataCandidates(error)

const contractDecoder = createContractDecoder(contract)
return candidates.flatMap(contractDecoder).at(0)
} catch {
return undefined
}
}

export const parseGenericError = (error: unknown): ContractError | undefined => {
// If the error already is a ContractError, we'll continue
if (error instanceof ContractError) return error

try {
// If the error is unknown we'll try to decode basic errors
const candidates = getErrorDataCandidates(error)

// And return the first candidate
return candidates.flatMap(basicDecoder).at(0)
} catch {
return undefined
}
}

// If a contract reverts using revert, the error data will be prefixed with this beauty
const REVERT_ERROR_PREFIX = '0x08c379a0'
Expand Down Expand Up @@ -91,12 +119,12 @@ const basicDecoder = (data: string): ContractError[] => {
/**
* Contract decoder uses the contract ABIs to decode error revert string
*
* @param contract `OmniContract`
* @param contract `Contract`
*
* @returns `(data: string) => ContractError[]` Custom error decoder
*/
const createContractDecoder =
({ contract }: OmniContract) =>
(contract: Contract) =>
(data: string): ContractError[] => {
try {
const errorDescription = contract.interface.parseError(data)
Expand Down
3 changes: 3 additions & 0 deletions packages/utils-evm/src/errors/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { ContractError } from './errors'

export type OmniContractErrorParser = (error: unknown) => ContractError
12 changes: 11 additions & 1 deletion packages/utils-evm/src/omnigraph/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { OmniPoint, OmniTransaction } from '@layerzerolabs/utils'
import { IOmniSDK, OmniContract } from './types'
import { omniContractToPoint } from './coordinates'
import { createContractErrorParser } from '@/errors/parser'
import { OmniContractErrorParser } from '@/errors/types'
import { ContractError } from '..'

/**
* Base class for all EVM SDKs, providing some common functionality
* to reduce the boilerplate
*/
export abstract class OmniSDK implements IOmniSDK {
constructor(public readonly contract: OmniContract) {}
constructor(
public readonly contract: OmniContract,
protected readonly errorParser: OmniContractErrorParser = createContractErrorParser(contract)
) {}

get point(): OmniPoint {
return omniContractToPoint(this.contract)
Expand All @@ -19,4 +25,8 @@ export abstract class OmniSDK implements IOmniSDK {
data,
}
}

protected parseError(error: unknown): ContractError {
return this.errorParser(error)
}
}

0 comments on commit 24eb448

Please sign in to comment.