diff --git a/.changeset/abi-error-decode-overloads.md b/.changeset/abi-error-decode-overloads.md new file mode 100644 index 00000000..74dfcfd8 --- /dev/null +++ b/.changeset/abi-error-decode-overloads.md @@ -0,0 +1,5 @@ +--- +"ox": patch +--- + +Fixed `AbiError.decode` to resolve overloaded errors by the selector in `data` instead of always decoding with the first matching overload. diff --git a/src/core/AbiError.ts b/src/core/AbiError.ts index 26226ba4..3d54f438 100644 --- a/src/core/AbiError.ts +++ b/src/core/AbiError.ts @@ -210,14 +210,17 @@ export function decode( } return parameters as [AbiError, Hex.Hex, decode.Options | undefined] })() + const { overloads } = abiError as AbiError + if (Hex.size(data) < 4) throw new AbiItem.InvalidSelectorSizeError({ data }) - if (abiError.inputs?.length === 0) return undefined - const values = AbiParameters.decode( - abiError.inputs, - Hex.slice(data, 4), - options, - ) + const item = overloads + ? (fromAbi([abiError, ...overloads], data as never) as AbiError) + : abiError + + if (item.inputs?.length === 0) return undefined + + const values = AbiParameters.decode(item.inputs, Hex.slice(data, 4), options) if (values && Object.keys(values).length === 1) { if (Array.isArray(values)) return values[0] return Object.values(values)[0] @@ -239,22 +242,30 @@ export declare namespace decode { abiError extends AbiError = AbiError, as extends 'Object' | 'Array' = 'Array', > = IsNarrowable extends true - ? abiError['inputs'] extends readonly [] - ? undefined - : abiError['inputs'] extends readonly [ - infer type extends abitype.AbiParameter, - ] - ? abitype.AbiParameterToPrimitiveType - : AbiParameters.decode.ReturnType< - abiError['inputs'], - as - > extends infer types - ? types extends readonly [] - ? undefined - : types extends readonly [infer type] - ? type - : types - : never + ? // distribute over the error and its overloads + | abiError + | (abiError['overloads'] extends readonly AbiError[] + ? abiError['overloads'][number] + : never) extends infer item extends AbiError + ? item extends AbiError + ? item['inputs'] extends readonly [] + ? undefined + : item['inputs'] extends readonly [ + infer type extends abitype.AbiParameter, + ] + ? abitype.AbiParameterToPrimitiveType + : AbiParameters.decode.ReturnType< + item['inputs'], + as + > extends infer types + ? types extends readonly [] + ? undefined + : types extends readonly [infer type] + ? type + : types + : never + : never + : never : unknown | readonly unknown[] | undefined type ErrorType = diff --git a/src/core/_test/AbiError.test-d.ts b/src/core/_test/AbiError.test-d.ts index 3c51e6d8..35b8f95c 100644 --- a/src/core/_test/AbiError.test-d.ts +++ b/src/core/_test/AbiError.test-d.ts @@ -1,4 +1,4 @@ -import { AbiError } from 'ox' +import { Abi, AbiError } from 'ox' import { describe, expectTypeOf, test } from 'vitest' describe('AbiError.decode', () => { @@ -36,6 +36,16 @@ describe('AbiError.decode', () => { expectTypeOf(decoded).toEqualTypeOf() }) + test('behavior: with overloads', () => { + const abi = Abi.from([ + 'error Example()', + 'error Example(uint256 amount)', + 'error Example(string reason)', + ]) + const decoded = AbiError.decode(abi, 'Example', '0x') + expectTypeOf(decoded).toEqualTypeOf() + }) + test('not narrowable', () => { const abiError = {} as AbiError.AbiError const decoded = AbiError.decode(abiError, '0x') diff --git a/src/core/_test/AbiError.test.ts b/src/core/_test/AbiError.test.ts index b99c3285..6f51e4a5 100644 --- a/src/core/_test/AbiError.test.ts +++ b/src/core/_test/AbiError.test.ts @@ -64,6 +64,37 @@ describe('decode', () => { expect(decoded).toEqual(1) }) + test('behavior: with overloads, resolved by name', () => { + const abi = Abi.from([ + 'error Example()', + 'error Example(uint256 amount)', + 'error Example(string reason)', + ]) + expect( + AbiError.decode( + abi, + 'Example', + AbiError.encode(AbiError.from('error Example(uint256 amount)'), [5n]), + ), + ).toEqual(5n) + expect( + AbiError.decode( + abi, + 'Example', + AbiError.encode(AbiError.from('error Example(string reason)'), [ + 'denied', + ]), + ), + ).toEqual('denied') + expect( + AbiError.decode( + abi, + 'Example', + AbiError.encode(AbiError.from('error Example()')), + ), + ).toEqual(undefined) + }) + test('error: invalid data', () => { const error = AbiError.from('error Example(uint256 a)') expect(() =>