diff --git a/.changeset/happy-bottles-collect.md b/.changeset/happy-bottles-collect.md new file mode 100644 index 0000000000..b082f292e8 --- /dev/null +++ b/.changeset/happy-bottles-collect.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added `encodeErrorResult`. diff --git a/site/.vitepress/sidebar.ts b/site/.vitepress/sidebar.ts index 963c85f50b..ad5477584a 100644 --- a/site/.vitepress/sidebar.ts +++ b/site/.vitepress/sidebar.ts @@ -462,7 +462,7 @@ export const sidebar: DefaultTheme.Sidebar = { link: '/docs/contract/encodeDeployData', }, { - text: 'encodeErrorResult 🚧', + text: 'encodeErrorResult', link: '/docs/contract/encodeErrorResult', }, { diff --git a/src/errors/abi.ts b/src/errors/abi.ts index 8002eff994..dcb3fb21fa 100644 --- a/src/errors/abi.ts +++ b/src/errors/abi.ts @@ -76,6 +76,37 @@ export class AbiEncodingLengthMismatchError extends BaseError { } } +export class AbiErrorInputsNotFoundError extends BaseError { + name = 'AbiErrorInputsNotFoundError' + constructor(errorName: string) { + super( + [ + `Arguments (\`args\`) were provided to "${errorName}", but "${errorName}" on the ABI does not contain any parameters (\`inputs\`).`, + 'Cannot encode error result without knowing what the parameter types are.', + 'Make sure you are using the correct ABI and that the inputs exist on it.', + ].join('\n'), + { + docsPath: '/docs/contract/encodeErrorResult', + }, + ) + } +} + +export class AbiErrorNotFoundError extends BaseError { + name = 'AbiErrorNotFoundError' + constructor(errorName: string) { + super( + [ + `Error "${errorName}" not found on ABI.`, + 'Make sure you are using the correct ABI and that the error exists on it.', + ].join('\n'), + { + docsPath: '/docs/contract/encodeErrorResult', + }, + ) + } +} + export class AbiErrorSignatureNotFoundError extends BaseError { name = 'AbiErrorSignatureNotFoundError' constructor(signature: Hex) { diff --git a/src/errors/index.ts b/src/errors/index.ts index 594915eb9f..d395ab5de7 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -4,6 +4,8 @@ export { AbiDecodingDataSizeInvalidError, AbiEncodingArrayLengthMismatchError, AbiEncodingLengthMismatchError, + AbiErrorInputsNotFoundError, + AbiErrorNotFoundError, AbiErrorSignatureNotFoundError, AbiEventNotFoundError, AbiFunctionNotFoundError, diff --git a/src/index.test.ts b/src/index.test.ts index c6c660009d..d00d2f4acb 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -10,6 +10,10 @@ test('exports actions', () => { "AbiDecodingDataSizeInvalidError": [Function], "AbiEncodingArrayLengthMismatchError": [Function], "AbiEncodingLengthMismatchError": [Function], + "AbiErrorInputsNotFoundError": [Function], + "AbiErrorNotFoundError": [Function], + "AbiErrorSignatureNotFoundError": [Function], + "AbiEventNotFoundError": [Function], "AbiFunctionNotFoundError": [Function], "AbiFunctionOutputsNotFoundError": [Function], "AbiFunctionSignatureNotFoundError": [Function], @@ -17,6 +21,7 @@ test('exports actions', () => { "BlockNotFoundError": [Function], "DataLengthTooLongError": [Function], "DataLengthTooShortError": [Function], + "FilterTypeNotSupportedError": [Function], "HttpRequestError": [Function], "InternalRpcError": [Function], "InvalidAbiDecodingTypeError": [Function], @@ -79,6 +84,7 @@ test('exports actions', () => { "encodeAbi": [Function], "encodeBytes": [Function], "encodeDeployData": [Function], + "encodeErrorResult": [Function], "encodeEventTopics": [Function], "encodeFunctionData": [Function], "encodeFunctionResult": [Function], diff --git a/src/index.ts b/src/index.ts index afeecd0e62..014a32e6db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -172,6 +172,10 @@ export { AbiDecodingDataSizeInvalidError, AbiEncodingArrayLengthMismatchError, AbiEncodingLengthMismatchError, + AbiErrorInputsNotFoundError, + AbiErrorNotFoundError, + AbiErrorSignatureNotFoundError, + AbiEventNotFoundError, AbiFunctionNotFoundError, AbiFunctionOutputsNotFoundError, AbiFunctionSignatureNotFoundError, @@ -180,6 +184,7 @@ export { DataLengthTooLongError, DataLengthTooShortError, HttpRequestError, + FilterTypeNotSupportedError, InternalRpcError, InvalidAbiDecodingTypeError, InvalidAbiEncodingTypeError, @@ -281,6 +286,7 @@ export { encodeAbi, encodeBytes, encodeDeployData, + encodeErrorResult, encodeEventTopics, encodeFunctionData, encodeFunctionResult, diff --git a/src/types/index.ts b/src/types/index.ts index e75b4f3bb9..8e01ef44db 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -41,6 +41,7 @@ export type { ExtractArgsFromEventDefinition, ExtractArgsFromFunctionDefinition, ExtractConstructorArgsFromAbi, + ExtractErrorArgsFromAbi, ExtractResultFromAbi, } from './solidity' diff --git a/src/types/solidity.ts b/src/types/solidity.ts index ccfc1c09a9..45d7baf747 100644 --- a/src/types/solidity.ts +++ b/src/types/solidity.ts @@ -9,6 +9,8 @@ import type { AbiParameter, ExtractAbiEvent, AbiEvent, + AbiError, + ExtractAbiError, } from 'abitype' import type { Trim } from './utils' @@ -105,6 +107,31 @@ export type ExtractConstructorArgsFromAbi< /** Arguments to pass contract method */ args: TArgs } +export type ExtractErrorArgsFromAbi< + TAbi extends Abi | readonly unknown[], + TErrorName extends string, + TAbiError extends AbiError = TAbi extends Abi + ? ExtractAbiError + : AbiError, + TArgs = AbiParametersToPrimitiveTypes, + FailedToParseArgs = + | ([TArgs] extends [never] ? true : false) + | (readonly unknown[] extends TArgs ? true : false), +> = true extends FailedToParseArgs + ? { + /** + * Arguments to pass contract method + * + * Use a [const assertion](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) on {@link abi} for type inference. + */ + args?: readonly unknown[] + } + : TArgs extends readonly [] + ? { args?: never } + : { + /** Arguments to pass contract method */ args: TArgs + } + export type ExtractResultFromAbi< TAbi extends Abi | readonly unknown[], TFunctionName extends string, diff --git a/src/utils/abi/encodeAbi.test.ts b/src/utils/abi/encodeAbi.test.ts index de059d363e..11fdc3f582 100644 --- a/src/utils/abi/encodeAbi.test.ts +++ b/src/utils/abi/encodeAbi.test.ts @@ -9,7 +9,7 @@ describe('static', () => { params: [], values: [], }), - ).toBe(undefined) + ).toBe('0x') }) test('uint', () => { diff --git a/src/utils/abi/encodeAbi.ts b/src/utils/abi/encodeAbi.ts index efa85756d3..2769b9a8a5 100644 --- a/src/utils/abi/encodeAbi.ts +++ b/src/utils/abi/encodeAbi.ts @@ -32,7 +32,7 @@ export function encodeAbi({ // Prepare the parameters to determine dynamic types to encode. const preparedParams = prepareParams({ params, values }) const data = encodeParams(preparedParams) - if (data.length === 0) return undefined + if (data.length === 0) return '0x' return data } diff --git a/src/utils/abi/encodeDeployData.ts b/src/utils/abi/encodeDeployData.ts index b69bdf9c4c..2f8d326c9c 100644 --- a/src/utils/abi/encodeDeployData.ts +++ b/src/utils/abi/encodeDeployData.ts @@ -1,4 +1,4 @@ -import { Abi, AbiParameterToPrimitiveType } from 'abitype' +import { Abi } from 'abitype' import { AbiConstructorNotFoundError, diff --git a/src/utils/abi/encodeErrorResult.test.ts b/src/utils/abi/encodeErrorResult.test.ts new file mode 100644 index 0000000000..b25743cad0 --- /dev/null +++ b/src/utils/abi/encodeErrorResult.test.ts @@ -0,0 +1,197 @@ +import { expect, test } from 'vitest' + +import { encodeErrorResult } from './encodeErrorResult' + +test('revert SoldOutError()', () => { + expect( + encodeErrorResult({ + abi: [ + { + inputs: [], + name: 'SoldOutError', + type: 'error', + }, + ], + errorName: 'SoldOutError', + }), + ).toEqual('0x7f6df6bb') + expect( + encodeErrorResult({ + abi: [ + // @ts-expect-error + { + name: 'SoldOutError', + type: 'error', + }, + ], + errorName: 'SoldOutError', + }), + ).toEqual('0x7f6df6bb') +}) + +test('revert AccessDeniedError(string)', () => { + expect( + encodeErrorResult({ + abi: [ + { + inputs: [ + { + internalType: 'string', + name: 'a', + type: 'string', + }, + ], + name: 'AccessDeniedError', + type: 'error', + }, + ] as const, + errorName: 'AccessDeniedError', + args: ['you do not have access ser'], + }), + ).toEqual( + '0x83aa206e0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a796f7520646f206e6f7420686176652061636365737320736572000000000000', + ) +}) + +test('revert AccessDeniedError((uint256,bool,address,uint256))', () => { + expect( + encodeErrorResult({ + abi: [ + { + inputs: [ + { + components: [ + { + internalType: 'uint256', + name: 'weight', + type: 'uint256', + }, + { + internalType: 'bool', + name: 'voted', + type: 'bool', + }, + { + internalType: 'address', + name: 'delegate', + type: 'address', + }, + { + internalType: 'uint256', + name: 'vote', + type: 'uint256', + }, + ], + internalType: 'struct Ballot.Voter', + name: 'voter', + type: 'tuple', + }, + ], + name: 'AccessDeniedError', + type: 'error', + }, + ], + errorName: 'AccessDeniedError', + args: [ + { + delegate: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + vote: 41n, + voted: true, + weight: 69420n, + }, + ], + }), + ).toEqual( + '0x0a1895610000000000000000000000000000000000000000000000000000000000010f2c0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac0000000000000000000000000000000000000000000000000000000000000029', + ) +}) + +test("errors: error doesn't exist", () => { + expect(() => + encodeErrorResult({ + abi: [ + { + inputs: [], + name: 'SoldOutError', + type: 'error', + }, + ], + errorName: 'AccessDeniedError', + args: [ + { + delegate: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + vote: 41n, + voted: true, + weight: 69420n, + }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Error \\"AccessDeniedError\\" not found on ABI. + Make sure you are using the correct ABI and that the error exists on it. + + Docs: https://viem.sh/docs/contract/encodeErrorResult + + Version: viem@1.0.2" + `) +}) + +test('errors: no inputs', () => { + expect(() => + encodeErrorResult({ + abi: [ + // @ts-expect-error + { + name: 'AccessDeniedError', + type: 'error', + }, + ], + errorName: 'AccessDeniedError', + args: [ + { + delegate: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + vote: 41n, + voted: true, + weight: 69420n, + }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Arguments (\`args\`) were provided to \\"AccessDeniedError\\", but \\"AccessDeniedError\\" on the ABI does not contain any parameters (\`inputs\`). + Cannot encode error result without knowing what the parameter types are. + Make sure you are using the correct ABI and that the inputs exist on it. + + Docs: https://viem.sh/docs/contract/encodeErrorResult + + Version: viem@1.0.2" + `) + expect(() => + encodeErrorResult({ + abi: [ + { + // @ts-expect-error + inputs: undefined, + name: 'AccessDeniedError', + type: 'error', + }, + ], + errorName: 'AccessDeniedError', + args: [ + { + delegate: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + vote: 41n, + voted: true, + weight: 69420n, + }, + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Arguments (\`args\`) were provided to \\"AccessDeniedError\\", but \\"AccessDeniedError\\" on the ABI does not contain any parameters (\`inputs\`). + Cannot encode error result without knowing what the parameter types are. + Make sure you are using the correct ABI and that the inputs exist on it. + + Docs: https://viem.sh/docs/contract/encodeErrorResult + + Version: viem@1.0.2" + `) +}) diff --git a/src/utils/abi/encodeErrorResult.ts b/src/utils/abi/encodeErrorResult.ts new file mode 100644 index 0000000000..731fa32273 --- /dev/null +++ b/src/utils/abi/encodeErrorResult.ts @@ -0,0 +1,36 @@ +import { Abi, ExtractAbiErrorNames } from 'abitype' +import { + AbiErrorInputsNotFoundError, + AbiErrorNotFoundError, +} from '../../errors' + +import { ExtractErrorArgsFromAbi, Hex } from '../../types' +import { concatHex } from '../data' +import { getFunctionSignature } from '../hash' +import { encodeAbi } from './encodeAbi' +import { getDefinition } from './getDefinition' + +export function encodeErrorResult< + TAbi extends Abi = Abi, + TErrorName extends ExtractAbiErrorNames = any, +>({ + abi, + errorName, + args, +}: { abi: TAbi; errorName: TErrorName } & ExtractErrorArgsFromAbi< + TAbi, + TErrorName +>) { + const description = abi.find((x) => 'name' in x && x.name === errorName) + if (!description) throw new AbiErrorNotFoundError(errorName) + const definition = getDefinition(description) + const signature = getFunctionSignature(definition) + + let data: Hex = '0x' + if (args && args.length > 0) { + if (!('inputs' in description && description.inputs)) + throw new AbiErrorInputsNotFoundError(errorName) + data = encodeAbi({ params: description.inputs, values: args as any }) + } + return concatHex([signature, data]) +} diff --git a/src/utils/abi/index.test.ts b/src/utils/abi/index.test.ts index 499721a7b2..e32327a74e 100644 --- a/src/utils/abi/index.test.ts +++ b/src/utils/abi/index.test.ts @@ -11,6 +11,7 @@ test('exports utils', () => { "decodeFunctionResult": [Function], "encodeAbi": [Function], "encodeDeployData": [Function], + "encodeErrorResult": [Function], "encodeEventTopics": [Function], "encodeFunctionData": [Function], "encodeFunctionResult": [Function], diff --git a/src/utils/abi/index.ts b/src/utils/abi/index.ts index 6ecedee99d..54fc1ee215 100644 --- a/src/utils/abi/index.ts +++ b/src/utils/abi/index.ts @@ -10,6 +10,8 @@ export { encodeAbi } from './encodeAbi' export { encodeDeployData } from './encodeDeployData' +export { encodeErrorResult } from './encodeErrorResult' + export { encodeEventTopics } from './encodeEventTopics' export { encodeFunctionData } from './encodeFunctionData' diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index a6a86e870e..ceb61ec3d0 100644 --- a/src/utils/index.test.ts +++ b/src/utils/index.test.ts @@ -23,6 +23,7 @@ test('exports utils', () => { "encodeAbi": [Function], "encodeBytes": [Function], "encodeDeployData": [Function], + "encodeErrorResult": [Function], "encodeEventTopics": [Function], "encodeFunctionData": [Function], "encodeFunctionResult": [Function], diff --git a/src/utils/index.ts b/src/utils/index.ts index 5f658e19cd..55a05e869c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,6 +5,7 @@ export { decodeFunctionResult, encodeAbi, encodeDeployData, + encodeErrorResult, encodeEventTopics, encodeFunctionData, encodeFunctionResult,