Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/six-pumpkins-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

Added error preservation in `getContractError`.
12 changes: 10 additions & 2 deletions src/errors/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ export type AbiDecodingZeroDataErrorType = AbiDecodingZeroDataError & {
name: 'AbiDecodingZeroDataError'
}
export class AbiDecodingZeroDataError extends BaseError {
constructor() {
constructor({ cause }: { cause?: BaseError | Error | undefined } = {}) {
super('Cannot decode zero data ("0x") with ABI parameters.', {
name: 'AbiDecodingZeroDataError',
cause,
})
}
}
Expand Down Expand Up @@ -210,7 +211,13 @@ export type AbiErrorSignatureNotFoundErrorType =
export class AbiErrorSignatureNotFoundError extends BaseError {
signature: Hex

constructor(signature: Hex, { docsPath }: { docsPath: string }) {
constructor(
signature: Hex,
{
docsPath,
cause,
}: { docsPath: string; cause?: BaseError | Error | undefined },
) {
super(
[
`Encoded error signature "${signature}" not found on ABI.`,
Expand All @@ -220,6 +227,7 @@ export class AbiErrorSignatureNotFoundError extends BaseError {
{
docsPath,
name: 'AbiErrorSignatureNotFoundError',
cause,
},
)
this.signature = signature
Expand Down
15 changes: 12 additions & 3 deletions src/errors/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,19 +179,21 @@ export class ContractFunctionRevertedError extends BaseError {
data,
functionName,
message,
cause: error,
}: {
abi: Abi
data?: Hex | undefined
functionName: string
message?: string | undefined
cause?: BaseError | Error | undefined
}) {
let cause: Error | undefined
let decodedData: DecodeErrorResultReturnType | undefined
let metaMessages: string[] | undefined
let reason: string | undefined
if (data && data !== '0x') {
try {
decodedData = decodeErrorResult({ abi, data })
decodedData = decodeErrorResult({ abi, data, cause: error })
const { abiItem, errorName, args: errorArgs } = decodedData
if (errorName === 'Error') {
reason = (errorArgs as [string])[0]
Expand Down Expand Up @@ -246,7 +248,7 @@ export class ContractFunctionRevertedError extends BaseError {
].join('\n')
: `The contract function "${functionName}" reverted.`,
{
cause,
cause: cause ?? error,
metaMessages,
name: 'ContractFunctionRevertedError',
},
Expand All @@ -264,7 +266,13 @@ export type ContractFunctionZeroDataErrorType =
name: 'ContractFunctionZeroDataError'
}
export class ContractFunctionZeroDataError extends BaseError {
constructor({ functionName }: { functionName: string }) {
constructor({
functionName,
cause,
}: {
functionName: string
cause?: BaseError | Error | undefined
}) {
super(`The contract function "${functionName}" returned no data ("0x").`, {
metaMessages: [
'This could be due to any of the following:',
Expand All @@ -273,6 +281,7 @@ export class ContractFunctionZeroDataError extends BaseError {
' - The address is not a contract.',
],
name: 'ContractFunctionZeroDataError',
cause,
})
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/utils/abi/decodeErrorResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AbiErrorSignatureNotFoundError,
type AbiErrorSignatureNotFoundErrorType,
} from '../../errors/abi.js'
import type { BaseError } from '../../errors/base.js'
import type { ErrorType } from '../../errors/utils.js'
import type {
AbiItem,
Expand All @@ -28,7 +29,7 @@ import { type FormatAbiItemErrorType, formatAbiItem } from './formatAbiItem.js'

export type DecodeErrorResultParameters<
abi extends Abi | readonly unknown[] = Abi,
> = { abi?: abi | undefined; data: Hex }
> = { abi?: abi | undefined; data: Hex; cause?: BaseError | Error | undefined }

export type DecodeErrorResultReturnType<
abi extends Abi | readonly unknown[] = Abi,
Expand Down Expand Up @@ -65,10 +66,10 @@ export type DecodeErrorResultErrorType =
export function decodeErrorResult<const abi extends Abi | readonly unknown[]>(
parameters: DecodeErrorResultParameters<abi>,
): DecodeErrorResultReturnType<abi> {
const { abi, data } = parameters as DecodeErrorResultParameters
const { abi, data, cause } = parameters as DecodeErrorResultParameters

const signature = slice(data, 0, 4)
if (signature === '0x') throw new AbiDecodingZeroDataError()
if (signature === '0x') throw new AbiDecodingZeroDataError({ cause })

const abi_ = [...(abi || []), solidityError, solidityPanic]
const abiItem = abi_.find(
Expand All @@ -78,6 +79,7 @@ export function decodeErrorResult<const abi extends Abi | readonly unknown[]>(
if (!abiItem)
throw new AbiErrorSignatureNotFoundError(signature, {
docsPath: '/docs/contract/decodeErrorResult',
cause,
})
return {
abiItem,
Expand Down
105 changes: 103 additions & 2 deletions src/utils/errors/getContractError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ import { describe, expect, test } from 'vitest'

import { baycContractConfig } from '~test/abis.js'
import { accounts } from '~test/constants.js'
import { AbiDecodingZeroDataError } from '../../errors/abi.js'
import {
AbiDecodingZeroDataError,
AbiErrorSignatureNotFoundError,
} from '../../errors/abi.js'
import { BaseError } from '../../errors/base.js'
import { RawContractError } from '../../errors/contract.js'
import {
ContractFunctionExecutionError,
ContractFunctionRevertedError,
ContractFunctionZeroDataError,
RawContractError,
} from '../../errors/contract.js'
import { RpcRequestError } from '../../errors/request.js'

import { getContractError } from './getContractError.js'
Expand Down Expand Up @@ -67,12 +75,14 @@ describe('getContractError', () => {
args: (1)
sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

Details: execution reverted: Sale must be active to mint Ape
Version: viem@x.y.z]
`)
expect(error.cause).toMatchInlineSnapshot(`
[ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason:
Sale must be active to mint Ape

Details: execution reverted: Sale must be active to mint Ape
Version: viem@x.y.z]
`)
})
Expand Down Expand Up @@ -102,12 +112,14 @@ describe('getContractError', () => {
args: (1)
sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

Details: execution reverted: Sale must be active to mint Ape
Version: viem@x.y.z]
`)
expect(error.cause).toMatchInlineSnapshot(`
[ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason:
Sale must be active to mint Ape

Details: execution reverted: Sale must be active to mint Ape
Version: viem@x.y.z]
`)
})
Expand Down Expand Up @@ -171,12 +183,14 @@ describe('getContractError', () => {
args: (1)
sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

Details: ah no
Version: viem@x.y.z]
`)
expect(error.cause).toMatchInlineSnapshot(`
[ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason:
ah no

Details: ah no
Version: viem@x.y.z]
`)
})
Expand Down Expand Up @@ -240,12 +254,14 @@ describe('getContractError', () => {
args: (1)
sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

Details: ah no
Version: viem@x.y.z]
`)
expect(error.cause).toMatchInlineSnapshot(`
[ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason:
ah no

Details: ah no
Version: viem@x.y.z]
`)
})
Expand Down Expand Up @@ -275,12 +291,14 @@ describe('getContractError', () => {
args: (1)
sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266

Details: execution reverted: Sale must be active to mint Ape
Version: viem@x.y.z]
`)
expect(error.cause).toMatchInlineSnapshot(`
[ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason:
Sale must be active to mint Ape

Details: execution reverted: Sale must be active to mint Ape
Version: viem@x.y.z]
`)
})
Expand Down Expand Up @@ -360,6 +378,89 @@ describe('getContractError', () => {
`)
})

describe('preserves error cause', () => {
test('preserves the cause when receiving an AbiDecodingZeroDataError', () => {
const originalError = new AbiDecodingZeroDataError()
const error = getContractError(originalError, {
abi: baycContractConfig.abi,
functionName: 'mintApe',
args: [1n],
sender: accounts[0].address,
})
expect(error).toBeInstanceOf(ContractFunctionExecutionError)
expect(error.cause).toBeInstanceOf(ContractFunctionZeroDataError)
expect(error.cause.cause).toBe(originalError)
})

describe('when wrapping it in a ContractFunctionRevertedError error', () => {
test('preserves the cause when the return data decoding succeeds', () => {
const originalError = new RawContractError({
message: 'execution reverted: Sale must be active to mint Ape',
data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001f53616c65206d7573742062652061637469766520746f206d696e742041706500',
})
const error = getContractError(originalError, {
abi: baycContractConfig.abi,
functionName: 'mintApe',
args: [1n],
sender: accounts[0].address,
})
expect(error).toBeInstanceOf(ContractFunctionExecutionError)
expect(error.cause).toBeInstanceOf(ContractFunctionRevertedError)
expect(error.cause.cause).toBe(originalError)
})

test('preserves the cause when decoding the return data throws an AbiErrorSignatureNotFoundError error', () => {
// Data with unknown error signature (not in ABI) + padding
const originalError = new RawContractError({
message: 'execution reverted',
data: '0xdeadbeef0000000000000000000000000000000000000000000000000000000000000000',
})
const error = getContractError(originalError, {
abi: baycContractConfig.abi,
functionName: 'mintApe',
args: [1n],
sender: accounts[0].address,
})
expect(error).toBeInstanceOf(ContractFunctionExecutionError)
expect(error.cause).toBeInstanceOf(ContractFunctionRevertedError)
expect(error.cause.cause).toBeInstanceOf(AbiErrorSignatureNotFoundError)
expect(
(error.cause.cause as AbiErrorSignatureNotFoundError).cause,
).toBe(originalError)
})

test('looses the cause when decoding the return data throws an other error (known limitation)', () => {
// Valid Error(string) selector but malformed/truncated params so decodeAbiParameters throws
const originalError = new RawContractError({
message: 'execution reverted',
data: '0x08c379a0000000000000000000000000000000000000000000000000000000000000ffff',
})
const error = getContractError(originalError, {
abi: baycContractConfig.abi,
functionName: 'mintApe',
args: [1n],
sender: accounts[0].address,
})
expect(error).toBeInstanceOf(ContractFunctionExecutionError)
expect(error.cause).toBeInstanceOf(ContractFunctionRevertedError)
// The cause is the decode error, not the original error — known limitation
expect(error.cause.cause).not.toBe(originalError)
})
})

test('preserves the cause as the direct error.cause on every other case', () => {
const originalError = new BaseError('some unknown error')
const error = getContractError(originalError, {
abi: baycContractConfig.abi,
functionName: 'mintApe',
args: [1n],
sender: accounts[0].address,
})
expect(error).toBeInstanceOf(ContractFunctionExecutionError)
expect(error.cause).toBe(originalError)
})
})

test('zero data', () => {
const error = getContractError(new AbiDecodingZeroDataError(), {
abi: baycContractConfig.abi,
Expand Down
3 changes: 2 additions & 1 deletion src/utils/errors/getContractError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function getContractError<err extends ErrorType<string>>(

const cause = (() => {
if (err instanceof AbiDecodingZeroDataError)
return new ContractFunctionZeroDataError({ functionName })
return new ContractFunctionZeroDataError({ functionName, cause: err })
if (
([EXECUTION_REVERTED_ERROR_CODE, InternalRpcError.code].includes(code) &&
(data || details || message || shortMessage)) ||
Expand All @@ -73,6 +73,7 @@ export function getContractError<err extends ErrorType<string>>(
error instanceof RpcRequestError
? details
: (shortMessage ?? message),
cause: err,
})
}
return err
Expand Down
Loading