Skip to content
Open
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/afraid-carpets-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ox": patch
---

feat: add parameter context to ABI encoding errors
23 changes: 20 additions & 3 deletions src/core/AbiParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,14 +583,22 @@ export class ZeroDataError extends Errors.BaseError {
*/
export class ArrayLengthMismatchError extends Errors.BaseError {
override readonly name = 'AbiParameters.ArrayLengthMismatchError'
readonly parameter: Parameter | undefined
constructor({
expectedLength,
givenLength,
parameter,
type,
}: { expectedLength: number; givenLength: number; type: string }) {
}: {
expectedLength: number
givenLength: number
parameter?: Parameter | undefined
type: string
}) {
super(
`Array length mismatch for type \`${type}\`. Expected: \`${expectedLength}\`. Given: \`${givenLength}\`.`,
)
this.parameter = parameter
}
}

Expand Down Expand Up @@ -622,15 +630,22 @@ export class ArrayLengthMismatchError extends Errors.BaseError {
*/
export class BytesSizeMismatchError extends Errors.BaseError {
override readonly name = 'AbiParameters.BytesSizeMismatchError'
readonly parameter: Parameter | undefined
constructor({
expectedSize,
parameter,
value,
}: { expectedSize: number; value: Hex.Hex }) {
}: {
expectedSize: number
parameter?: Parameter | undefined
value: Hex.Hex
}) {
super(
`Size of bytes "${value}" (bytes${Hex.size(
value,
)}) does not match expected size (bytes${expectedSize}).`,
)
this.parameter = parameter
}
}

Expand Down Expand Up @@ -691,8 +706,10 @@ export class LengthMismatchError extends Errors.BaseError {
*/
export class InvalidArrayError extends Errors.BaseError {
override readonly name = 'AbiParameters.InvalidArrayError'
constructor(value: unknown) {
readonly parameter: Parameter | undefined
constructor(value: unknown, options?: { parameter?: Parameter | undefined }) {
super(`Value \`${value}\` is not a valid array.`)
this.parameter = options?.parameter
}
}

Expand Down
22 changes: 19 additions & 3 deletions src/core/Address.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Address as abitype_Address } from 'abitype'
import type { AbiParameter, Address as abitype_Address } from 'abitype'
import * as Bytes from './Bytes.js'
import * as Caches from './Caches.js'
import * as Errors from './Errors.js'
Expand Down Expand Up @@ -35,12 +35,13 @@ export function assert(
value: string,
options: assert.Options = {},
): asserts value is Address {
const { strict = true } = options
const { strict = true, parameter } = options

if (!addressRegex.test(value))
throw new InvalidAddressError({
address: value,
cause: new InvalidInputError(),
parameter,
})

if (strict) {
Expand All @@ -49,12 +50,17 @@ export function assert(
throw new InvalidAddressError({
address: value,
cause: new InvalidChecksumError(),
parameter,
})
}
}

export declare namespace assert {
type Options = {
/**
* ABI parameter context for error reporting.
*/
parameter?: AbiParameter | undefined
/**
* Enables strict mode. Whether or not to compare the address against its checksum.
*
Expand Down Expand Up @@ -315,10 +321,20 @@ export class InvalidAddressError<
> extends Errors.BaseError<cause> {
override readonly name = 'Address.InvalidAddressError'

constructor({ address, cause }: { address: string; cause: cause }) {
readonly parameter: AbiParameter | undefined
constructor({
address,
cause,
parameter,
}: {
address: string
cause: cause
parameter?: AbiParameter | undefined
}) {
super(`Address "${address}" is invalid.`, {
cause,
})
this.parameter = parameter
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/core/Hex.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { equalBytes } from '@noble/curves/abstract/utils'
import type { AbiParameter } from 'abitype'
import * as Bytes from './Bytes.js'
import * as Errors from './Errors.js'
import * as internal_bytes from './internal/bytes.js'
Expand Down Expand Up @@ -772,16 +773,19 @@ export declare namespace validate {
*/
export class IntegerOutOfRangeError extends Errors.BaseError {
override readonly name = 'Hex.IntegerOutOfRangeError'
readonly parameter: AbiParameter | undefined

constructor({
max,
min,
parameter,
signed,
size,
value,
}: {
max?: string | undefined
min: string
parameter?: AbiParameter | undefined
signed?: boolean | undefined
size?: number | undefined
value: string
Expand All @@ -791,6 +795,7 @@ export class IntegerOutOfRangeError extends Errors.BaseError {
size ? ` ${size * 8}-bit` : ''
}${signed ? ' signed' : ' unsigned'} integer range ${max ? `(\`${min}\` to \`${max}\`)` : `(above \`${min}\`)`}`,
)
this.parameter = parameter
}
}

Expand Down
108 changes: 108 additions & 0 deletions src/core/_test/AbiParameters.encode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1963,3 +1963,111 @@ test('https://github.com/wevm/viem/issues/1960', () => {
`[BaseError: Invalid boolean value: "true" (type: string). Expected: \`true\` or \`false\`.]`,
)
})

describe('error parameter context', () => {
test('InvalidAddressError includes parameter info', () => {
const parameter = { type: 'address', name: 'recipient' } as const
expect(() => AbiParameters.encode([parameter], ['0xinvalid'])).toThrowError(
expect.objectContaining({ parameter }),
)
})

test('InvalidAddressError includes parameter with checksum validation', () => {
const parameter = { type: 'address', name: 'sender' } as const
expect(() =>
AbiParameters.encode(
[parameter],
['0xa5cC3c03994DB5b0d9A5eEdD10CabaB0813678AC'],
{ checksumAddress: true },
),
).toThrowError(expect.objectContaining({ parameter }))
})

test('IntegerOutOfRangeError includes parameter info', () => {
const parameter = { type: 'uint8', name: 'amount' } as const
expect(() => AbiParameters.encode([parameter], [256])).toThrowError(
expect.objectContaining({ parameter }),
)
})

test('IntegerOutOfRangeError includes parameter for signed int', () => {
const parameter = { type: 'int8', name: 'delta' } as const
expect(() => AbiParameters.encode([parameter], [128])).toThrowError(
expect.objectContaining({ parameter }),
)
})

test('InvalidArrayError includes parameter info', () => {
const parameter = { type: 'uint256[]', name: 'amounts' } as const
expect(() =>
AbiParameters.encode(
[parameter],
/* @ts-expect-error */
[69],
),
).toThrowError(
expect.objectContaining({
parameter: expect.objectContaining({
name: 'amounts',
type: 'uint256',
}),
}),
)
})

test('ArrayLengthMismatchError includes parameter info', () => {
const parameter = { type: 'uint256[3]', name: 'values' } as const
expect(() =>
AbiParameters.encode(
[parameter],
/* @ts-expect-error */
[[69n, 420n]],
),
).toThrowError(
expect.objectContaining({
parameter: expect.objectContaining({ name: 'values', type: 'uint256' }),
}),
)
})

test('BytesSizeMismatchError includes parameter info', () => {
const parameter = { type: 'bytes8', name: 'data' } as const
expect(() => AbiParameters.encode([parameter], ['0x111'])).toThrowError(
expect.objectContaining({ parameter }),
)
})

test('parameter is undefined for direct error construction', () => {
const error = new AbiParameters.InvalidArrayError('test')
expect(error.parameter).toBeUndefined()
})

test('nested tuple with invalid address includes correct parameter', () => {
const parameters = [
{ type: 'address', name: 'from' },
{ type: 'address', name: 'to' },
{ type: 'uint256', name: 'amount' },
] as const
expect(() =>
AbiParameters.encode(parameters, [address.vitalik, '0xinvalid', 100n]),
).toThrowError(
expect.objectContaining({
parameter: expect.objectContaining({ type: 'address', name: 'to' }),
}),
)
})

test('array of addresses with invalid entry includes parameter', () => {
const parameter = { type: 'address[]', name: 'recipients' } as const
expect(() =>
AbiParameters.encode([parameter], [[address.vitalik, '0xinvalid']]),
).toThrowError(
expect.objectContaining({
parameter: expect.objectContaining({
type: 'address',
name: 'recipients',
}),
}),
)
})
})
22 changes: 22 additions & 0 deletions src/core/_test/Address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,29 @@ describe('Address.assert', () => {
test('default', () => {
Address.assert('0x0000000000000000000000000000000000000000')
Address.assert('0xa0cf798816d4b9b9866b5330eea46a18382f251e')
})

test('options: parameter', () => {
const parameter = { type: 'address', name: 'recipient' } as const

expect(() => Address.assert('0xinvalid', { parameter })).toThrowError(
expect.objectContaining({ parameter }),
)

expect(() =>
Address.assert('0xa5cc3c03994db5b0d9a5eEdD10Cabab0813678ac', {
parameter,
}),
).toThrowError(expect.objectContaining({ parameter }))
})

test('parameter is undefined by default', () => {
expect(() => Address.assert('0xinvalid')).toThrowError(
expect.objectContaining({ parameter: undefined }),
)
})

test('error: invalid checksum', () => {
expect(() =>
Address.assert('0xa5cc3c03994db5b0d9a5eEdD10Cabab0813678ac'),
).toThrowErrorMatchingInlineSnapshot(`
Expand Down
Loading