diff --git a/.changeset/nice-sheep-brake.md b/.changeset/nice-sheep-brake.md new file mode 100644 index 0000000000..3e54084f52 --- /dev/null +++ b/.changeset/nice-sheep-brake.md @@ -0,0 +1,7 @@ +--- +"viem": patch +--- + +- **Breaking**: Renamed `humanMessage` to `shortMessage` in `BaseError`. +- Added `multicall`. +- Support overloaded contract functions. \ No newline at end of file diff --git a/rome.json b/rome.json index 403654a35e..6b72f41f8b 100644 --- a/rome.json +++ b/rome.json @@ -1,6 +1,16 @@ { "files": { - "ignore": ["cache", "coverage", "node_modules", "dist", "generated", "CHANGELOG.md", "pnpm-lock.yaml", ".next"] + "ignore": [ + "**/types/multicall.ts", + "cache", + "coverage", + "node_modules", + "dist", + "generated", + "CHANGELOG.md", + "pnpm-lock.yaml", + ".next" + ] }, "formatter": { "enabled": true, diff --git a/site/docs/contract/watchContractEvent.md b/site/docs/contract/watchContractEvents.md similarity index 100% rename from site/docs/contract/watchContractEvent.md rename to site/docs/contract/watchContractEvents.md diff --git a/src/_test/abis.ts b/src/_test/abis.ts index 5a62d834d8..fd9e9b0067 100644 --- a/src/_test/abis.ts +++ b/src/_test/abis.ts @@ -1128,6 +1128,13 @@ export const wagmiContractConfig = { stateMutability: 'view', type: 'function', }, + { + inputs: [], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [{ internalType: 'uint256', name: 'tokenId', type: 'uint256' }], name: 'mint', diff --git a/src/actions/public/multicall.test.ts b/src/actions/public/multicall.test.ts new file mode 100644 index 0000000000..5bc2da19e4 --- /dev/null +++ b/src/actions/public/multicall.test.ts @@ -0,0 +1,454 @@ +/** + * TODO: Heaps more test cases :D + * - Complex calldata types + * - Complex return types (tuple/structs) + * - Calls against blocks + * - Reverts (custom errors/Error(string)/Panic(uint256)) + */ + +import { describe, expect, test } from 'vitest' +import { + accounts, + initialBlockNumber, + publicClient, + usdcContractConfig, + vitalikAddress, +} from '../../_test' +import { baycContractConfig, wagmiContractConfig } from '../../_test/abis' + +import { multicall } from './multicall' + +test('default', async () => { + expect( + await multicall(publicClient, { + blockNumber: initialBlockNumber, + contracts: [ + { + ...usdcContractConfig, + functionName: 'totalSupply', + }, + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...baycContractConfig, + functionName: 'totalSupply', + }, + ], + multicallAddress: '0xca11bde05977b3631167028862be2a173976ca11', + }), + ).toMatchInlineSnapshot(` + [ + { + "result": 41119586940119550n, + "status": "success", + }, + { + "result": 231481998602n, + "status": "success", + }, + { + "result": 10000n, + "status": "success", + }, + ] + `) +}) + +describe('errors', async () => { + describe('allowFailure is truthy', async () => { + test('function not found', async () => { + expect( + await multicall(publicClient, { + blockNumber: initialBlockNumber, + contracts: [ + { + ...usdcContractConfig, + // @ts-expect-error + functionName: 'lol', + }, + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...baycContractConfig, + functionName: 'totalSupply', + }, + ], + multicallAddress: '0xca11bde05977b3631167028862be2a173976ca11', + }), + ).toMatchInlineSnapshot(` + [ + { + "error": [ContractFunctionExecutionError: The contract function "lol" returned no data ("0x"). + + This could be due to any of the following: + - The contract does not have the function "lol", + - The parameters passed to the contract function may be invalid, or + - The address is not a contract. + + Contract: 0x0000000000000000000000000000000000000000 + + Docs: https://viem.sh/docs/contract/multicall + Version: viem@1.0.2], + "result": undefined, + "status": "failure", + }, + { + "result": 231481998602n, + "status": "success", + }, + { + "result": 10000n, + "status": "success", + }, + ] + `) + }) + + test('invalid params', async () => { + expect( + await multicall(publicClient, { + blockNumber: initialBlockNumber, + // @ts-expect-error + contracts: [ + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress, 1n], + }, + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...baycContractConfig, + functionName: 'totalSupply', + }, + ] as const, + multicallAddress: '0xca11bde05977b3631167028862be2a173976ca11', + }), + ).toMatchInlineSnapshot(` + [ + { + "error": [ContractFunctionExecutionError: The contract function "balanceOf" returned no data ("0x"). + + This could be due to any of the following: + - The contract does not have the function "balanceOf", + - The parameters passed to the contract function may be invalid, or + - The address is not a contract. + + Contract: 0x0000000000000000000000000000000000000000 + Function: balanceOf(address account) + Arguments: (0xd8da6bf26964af9d7eed9e03e53415d37aa96045) + + Docs: https://viem.sh/docs/contract/multicall + Version: viem@1.0.2], + "result": undefined, + "status": "failure", + }, + { + "result": 231481998602n, + "status": "success", + }, + { + "result": 10000n, + "status": "success", + }, + ] + `) + }) + + test('invalid contract address', async () => { + expect( + await multicall(publicClient, { + blockNumber: initialBlockNumber, + contracts: [ + { + ...usdcContractConfig, + address: '0x0000000000000000000000000000000000000000', + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...baycContractConfig, + functionName: 'totalSupply', + }, + ] as const, + multicallAddress: '0xca11bde05977b3631167028862be2a173976ca11', + }), + ).toMatchInlineSnapshot(` + [ + { + "error": [ContractFunctionExecutionError: The contract function "balanceOf" returned no data ("0x"). + + This could be due to any of the following: + - The contract does not have the function "balanceOf", + - The parameters passed to the contract function may be invalid, or + - The address is not a contract. + + Contract: 0x0000000000000000000000000000000000000000 + Function: balanceOf(address account) + Arguments: (0xd8da6bf26964af9d7eed9e03e53415d37aa96045) + + Docs: https://viem.sh/docs/contract/multicall + Version: viem@1.0.2], + "result": undefined, + "status": "failure", + }, + { + "result": 231481998602n, + "status": "success", + }, + { + "result": 10000n, + "status": "success", + }, + ] + `) + }) + + test('contract revert', async () => { + expect( + await multicall(publicClient, { + blockNumber: initialBlockNumber, + contracts: [ + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...wagmiContractConfig, + functionName: 'transferFrom', + args: [vitalikAddress, accounts[0].address, 1n], + }, + { + ...baycContractConfig, + functionName: 'totalSupply', + }, + { + ...baycContractConfig, + functionName: 'tokenOfOwnerByIndex', + args: [vitalikAddress, 1n], + }, + ] as const, + multicallAddress: '0xca11bde05977b3631167028862be2a173976ca11', + }), + ).toMatchInlineSnapshot(` + [ + { + "result": 231481998602n, + "status": "success", + }, + { + "result": 231481998602n, + "status": "success", + }, + { + "error": [ContractFunctionExecutionError: The contract function "transferFrom" reverted with the following reason: + ERC721: transfer caller is not owner nor approved + + Contract: 0x0000000000000000000000000000000000000000 + Function: transferFrom(address from, address to, uint256 tokenId) + Arguments: (0xd8da6bf26964af9d7eed9e03e53415d37aa96045, 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266, 1) + + Docs: https://viem.sh/docs/contract/multicall + Version: viem@1.0.2], + "result": undefined, + "status": "failure", + }, + { + "result": 10000n, + "status": "success", + }, + { + "error": [ContractFunctionExecutionError: The contract function "tokenOfOwnerByIndex" reverted with the following reason: + EnumerableSet: index out of bounds + + Contract: 0x0000000000000000000000000000000000000000 + Function: tokenOfOwnerByIndex(address owner, uint256 index) + Arguments: (0xd8da6bf26964af9d7eed9e03e53415d37aa96045, 1) + + Docs: https://viem.sh/docs/contract/multicall + Version: viem@1.0.2], + "result": undefined, + "status": "failure", + }, + ] + `) + }) + }) + + describe('allowFailure is falsy', async () => { + test('function not found', async () => { + await expect(() => + multicall(publicClient, { + allowFailure: false, + contracts: [ + { + ...usdcContractConfig, + // @ts-expect-error + functionName: 'lol', + }, + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...baycContractConfig, + functionName: 'totalSupply', + }, + ], + multicallAddress: '0xca11bde05977b3631167028862be2a173976ca11', + }), + ).rejects.toMatchInlineSnapshot(` + [ContractFunctionExecutionError: Function "lol" not found on ABI. + Make sure you are using the correct ABI and that the function exists on it. + + Contract: 0x0000000000000000000000000000000000000000 + + Docs: https://viem.sh/docs/contract/encodeFunctionData + Version: viem@1.0.2] + `) + }) + + test('invalid params', async () => { + await expect(() => + multicall(publicClient, { + allowFailure: false, + // @ts-expect-error + contracts: [ + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress, 1n], + }, + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...baycContractConfig, + functionName: 'totalSupply', + }, + ] as const, + multicallAddress: '0xca11bde05977b3631167028862be2a173976ca11', + }), + ).rejects.toMatchInlineSnapshot(` + [ContractFunctionExecutionError: ABI encoding params/values length mismatch. + Expected length (params): 1 + Given length (values): 2 + + Contract: 0x0000000000000000000000000000000000000000 + Function: balanceOf(address account) + Arguments: (0xd8da6bf26964af9d7eed9e03e53415d37aa96045) + + Docs: https://viem.sh/docs/contract/multicall + Version: viem@1.0.2] + `) + }) + + test('invalid contract address', async () => { + await expect(() => + multicall(publicClient, { + allowFailure: false, + contracts: [ + { + ...usdcContractConfig, + address: '0x0000000000000000000000000000000000000000', + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...baycContractConfig, + functionName: 'totalSupply', + }, + ] as const, + multicallAddress: '0xca11bde05977b3631167028862be2a173976ca11', + }), + ).rejects.toMatchInlineSnapshot(` + [ContractFunctionExecutionError: The contract function "balanceOf" returned no data ("0x"). + + This could be due to any of the following: + - The contract does not have the function "balanceOf", + - The parameters passed to the contract function may be invalid, or + - The address is not a contract. + + Contract: 0x0000000000000000000000000000000000000000 + Function: balanceOf(address account) + Arguments: (0xd8da6bf26964af9d7eed9e03e53415d37aa96045) + + Docs: https://viem.sh/docs/contract/multicall + Version: viem@1.0.2] + `) + }) + + test('contract revert', async () => { + await expect(() => + multicall(publicClient, { + allowFailure: false, + contracts: [ + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...usdcContractConfig, + functionName: 'balanceOf', + args: [vitalikAddress], + }, + { + ...wagmiContractConfig, + functionName: 'transferFrom', + args: [vitalikAddress, accounts[0].address, 1n], + }, + { + ...baycContractConfig, + functionName: 'totalSupply', + }, + { + ...baycContractConfig, + functionName: 'tokenOfOwnerByIndex', + args: [vitalikAddress, 1n], + }, + ] as const, + multicallAddress: '0xca11bde05977b3631167028862be2a173976ca11', + }), + ).rejects.toMatchInlineSnapshot(` + [ContractFunctionExecutionError: The contract function "transferFrom" reverted with the following reason: + ERC721: transfer caller is not owner nor approved + + Contract: 0x0000000000000000000000000000000000000000 + Function: transferFrom(address from, address to, uint256 tokenId) + Arguments: (0xd8da6bf26964af9d7eed9e03e53415d37aa96045, 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266, 1) + + Docs: https://viem.sh/docs/contract/multicall + Version: viem@1.0.2] + `) + }) + }) +}) diff --git a/src/actions/public/multicall.ts b/src/actions/public/multicall.ts new file mode 100644 index 0000000000..668c59a63f --- /dev/null +++ b/src/actions/public/multicall.ts @@ -0,0 +1,89 @@ +import { PublicClient } from '../../clients' +import { multicall3Abi } from '../../constants' +import { BaseError, RawContractError } from '../../errors' +import { Address, ContractConfig, Hex, MulticallContracts } from '../../types' +import { MulticallResults } from '../../types/multicall' +import { + decodeFunctionResult, + encodeFunctionData, + EncodeFunctionDataArgs, + getContractError, +} from '../../utils' +import { CallArgs } from './call' +import { readContract } from './readContract' + +export type MulticallArgs< + TContracts extends ContractConfig[], + TAllowFailure extends boolean = true, +> = Pick & { + allowFailure?: TAllowFailure + contracts: readonly [...MulticallContracts] + multicallAddress: Address +} + +export async function multicall< + TContracts extends ContractConfig[], + TAllowFailure extends boolean = true, +>( + client: PublicClient, + args: MulticallArgs, +): Promise> { + const { allowFailure = true, contracts, multicallAddress } = args + + const calls = contracts.map(({ abi, address, args, functionName }) => { + try { + const callData = encodeFunctionData({ + abi, + args, + functionName, + } as unknown as EncodeFunctionDataArgs) + return { + allowFailure: true, + callData, + target: address, + } + } catch (err) { + const error = getContractError(err as BaseError, { + abi, + address, + args, + docsPath: '/docs/contract/multicall', + functionName, + }) + if (!allowFailure) throw error + return { + allowFailure: true, + callData: '0x' as Hex, + target: address, + } + } + }) + const results = await readContract(client, { + abi: multicall3Abi, + address: multicallAddress, + args: [calls], + functionName: 'aggregate3', + }) + return results.map(({ returnData, success }, i) => { + const { abi, address, functionName, args } = contracts[i] + try { + if (!success) throw new RawContractError({ data: returnData }) + const result = decodeFunctionResult({ + abi, + data: returnData, + functionName: functionName, + }) + return { result, status: 'success' } + } catch (err) { + const error = getContractError(err as BaseError, { + abi, + address, + args, + docsPath: '/docs/contract/multicall', + functionName, + }) + if (!allowFailure) throw error + return { error, result: undefined, status: 'failure' } + } + }) as MulticallResults +} diff --git a/src/actions/public/readContract.test.ts b/src/actions/public/readContract.test.ts index 5c5e00fd38..b6f7972df1 100644 --- a/src/actions/public/readContract.test.ts +++ b/src/actions/public/readContract.test.ts @@ -3,20 +3,21 @@ * - Complex calldata types * - Complex return types (tuple/structs) * - Calls against blocks + * - Reverts (custom errors/Error(string)/Panic(uint256)) */ import { describe, expect, test } from 'vitest' import { accounts, + initialBlockNumber, publicClient, testClient, + vitalikAddress, wagmiContractConfig, walletClient, } from '../../_test' import { baycContractConfig } from '../../_test/abis' -import { encodeFunctionData } from '../../utils' import { mine } from '../test' -import { sendTransaction } from '../wallet' import { deployContract } from './deployContract' import { getTransactionReceipt } from './getTransactionReceipt' @@ -86,10 +87,73 @@ describe('wagmi', () => { expect( await readContract(publicClient, { ...wagmiContractConfig, + blockNumber: initialBlockNumber, functionName: 'totalSupply', }), ).toEqual(558n) }) + + test('overloaded function', async () => { + expect( + await readContract(publicClient, { + ...wagmiContractConfig, + abi: [ + { + inputs: [{ type: 'uint256', name: 'x' }], + name: 'balanceOf', + outputs: [{ type: 'address', name: 'x' }], + stateMutability: 'pure', + type: 'function', + }, + ...wagmiContractConfig.abi, + ], + functionName: 'balanceOf', + args: ['0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC'], + }), + ).toEqual(3n) + }) +}) + +describe('bayc', () => { + test('revert', async () => { + await expect(() => + readContract(publicClient, { + ...baycContractConfig, + functionName: 'tokenOfOwnerByIndex', + args: [vitalikAddress, 5n], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "The contract function \\"tokenOfOwnerByIndex\\" reverted with the following reason: + EnumerableSet: index out of bounds + + Contract: 0x0000000000000000000000000000000000000000 + Function: tokenOfOwnerByIndex(address owner, uint256 index) + Arguments: (0xd8da6bf26964af9d7eed9e03e53415d37aa96045, 5) + + Docs: https://viem.sh/docs/contract/readContract + Version: viem@1.0.2" + `) + }) + + test('revert', async () => { + await expect(() => + readContract(publicClient, { + ...baycContractConfig, + functionName: 'ownerOf', + args: [420213123123n], + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "The contract function \\"ownerOf\\" reverted with the following reason: + ERC721: owner query for nonexistent token + + Contract: 0x0000000000000000000000000000000000000000 + Function: ownerOf(uint256 tokenId) + Arguments: (420213123123) + + Docs: https://viem.sh/docs/contract/readContract + Version: viem@1.0.2" + `) + }) }) test('fake contract address', async () => { @@ -100,15 +164,17 @@ test('fake contract address', async () => { functionName: 'totalSupply', }), ).rejects.toThrowErrorMatchingInlineSnapshot(` - "The contract method \\"totalSupply\\" returned no data (\\"0x\\"). This could be due to any of the following: + "The contract function \\"totalSupply\\" returned no data (\\"0x\\"). + + This could be due to any of the following: - The contract does not have the function \\"totalSupply\\", - The parameters passed to the contract function may be invalid, or - The address is not a contract. - Contract: 0x0000000000000000000000000000000000000000 - Function: totalSupply() - > \\"0x\\" + Contract: 0x0000000000000000000000000000000000000000 + Function: totalSupply() + Docs: https://viem.sh/docs/contract/readContract Version: viem@1.0.2" `) }) diff --git a/src/actions/public/readContract.ts b/src/actions/public/readContract.ts index 292f69198f..f943340486 100644 --- a/src/actions/public/readContract.ts +++ b/src/actions/public/readContract.ts @@ -1,11 +1,10 @@ import { Abi } from 'abitype' import type { PublicClient } from '../../clients' +import { BaseError } from '../../errors' import type { - Address, - ExtractArgsFromAbi, + ContractConfig, ExtractResultFromAbi, - ExtractFunctionNameFromAbi, Formatter, } from '../../types' import { @@ -13,6 +12,7 @@ import { decodeFunctionResult, encodeFunctionData, getContractError, + DecodeFunctionResultArgs, } from '../../utils' import { call, CallArgs, FormattedCall } from './call' @@ -36,11 +36,8 @@ export type ReadContractArgs< | 'to' | 'data' | 'value' -> & { - address: Address - abi: TAbi - functionName: ExtractFunctionNameFromAbi -} & ExtractArgsFromAbi +> & + ContractConfig export type ReadContractResponse< TAbi extends Abi | readonly unknown[] = Abi, @@ -73,14 +70,16 @@ export async function readContract< } as unknown as CallArgs) return decodeFunctionResult({ abi, + args, functionName, data: data || '0x', - }) + } as DecodeFunctionResultArgs) } catch (err) { - throw getContractError(err, { + throw getContractError(err as BaseError, { abi, address, args, + docsPath: '/docs/contract/readContract', functionName, }) } diff --git a/src/actions/public/simulateContract.test.ts b/src/actions/public/simulateContract.test.ts index 4b4a5ee53e..181b2734b2 100644 --- a/src/actions/public/simulateContract.test.ts +++ b/src/actions/public/simulateContract.test.ts @@ -6,6 +6,7 @@ * - Calls against blocks * - Custom chain types * - Custom nonce + * - More reverts (custom errors/Error(string)/Panic(uint256)) */ import { describe, expect, test } from 'vitest' @@ -53,6 +54,18 @@ describe('wagmi', () => { ).toEqual(undefined) }) + test('overloaded function', async () => { + expect( + ( + await simulateContract(publicClient, { + ...wagmiContractConfig, + from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + functionName: 'mint', + }) + ).result, + ).toEqual(undefined) + }) + test('revert', async () => { await expect(() => simulateContract(publicClient, { @@ -62,14 +75,15 @@ describe('wagmi', () => { from: accounts[0].address, }), ).rejects.toThrowErrorMatchingInlineSnapshot(` - "ERC721: approval to current owner - - Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + "The contract function \\"approve\\" reverted with the following reason: + ERC721: approval to current owner + Contract: 0x0000000000000000000000000000000000000000 Function: approve(address to, uint256 tokenId) Arguments: (0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC, 420) + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 - Details: execution reverted: ERC721: approval to current owner + Docs: https://viem.sh/docs/contract/simulateContract Version: viem@1.0.2" `) await expect(() => @@ -80,14 +94,15 @@ describe('wagmi', () => { from: accounts[0].address, }), ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Token ID is taken - - Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + "The contract function \\"mint\\" reverted with the following reason: + Token ID is taken + Contract: 0x0000000000000000000000000000000000000000 Function: mint(uint256 tokenId) Arguments: (1) + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 - Details: execution reverted: Token ID is taken + Docs: https://viem.sh/docs/contract/simulateContract Version: viem@1.0.2" `) await expect(() => @@ -102,14 +117,15 @@ describe('wagmi', () => { ], }), ).rejects.toThrowErrorMatchingInlineSnapshot(` - "ERC721: transfer caller is not owner nor approved - - Sender: 0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC + "The contract function \\"safeTransferFrom\\" reverted with the following reason: + ERC721: transfer caller is not owner nor approved + Contract: 0x0000000000000000000000000000000000000000 Function: safeTransferFrom(address from, address to, uint256 tokenId) Arguments: (0x1a1E021A302C237453D3D45c7B82B19cEEB7E2e6, 0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC, 1) + Sender: 0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC - Details: execution reverted: ERC721: transfer caller is not owner nor approved + Docs: https://viem.sh/docs/contract/simulateContract Version: viem@1.0.2" `) }) @@ -178,14 +194,15 @@ describe('BAYC', () => { args: [1n], }), ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Sale must be active to mint Ape - - Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + "The contract function \\"mintApe\\" reverted with the following reason: + Sale must be active to mint Ape + Contract: 0x0000000000000000000000000000000000000000 Function: mintApe(uint256 numberOfTokens) Arguments: (1) + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 - Details: execution reverted: Sale must be active to mint Ape + Docs: https://viem.sh/docs/contract/simulateContract Version: viem@1.0.2" `) }) @@ -210,15 +227,18 @@ test('fake contract address', async () => { args: [], }), ).rejects.toThrowErrorMatchingInlineSnapshot(` - "The contract method \\"mint\\" returned no data (\\"0x\\"). This could be due to any of the following: + "The contract function \\"mint\\" returned no data (\\"0x\\"). + + This could be due to any of the following: - The contract does not have the function \\"mint\\", - The parameters passed to the contract function may be invalid, or - The address is not a contract. - Contract: 0x0000000000000000000000000000000000000000 - Function: mint() - > \\"0x\\" + Contract: 0x0000000000000000000000000000000000000000 + Function: mint() + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + Docs: https://viem.sh/docs/contract/simulateContract Version: viem@1.0.2" `) }) diff --git a/src/actions/public/simulateContract.ts b/src/actions/public/simulateContract.ts index c929d1d325..97569e797d 100644 --- a/src/actions/public/simulateContract.ts +++ b/src/actions/public/simulateContract.ts @@ -1,9 +1,11 @@ import { Abi } from 'abitype' import type { PublicClient } from '../../clients' +import { BaseError } from '../../errors' import type { Address, Chain, + ContractConfig, ExtractArgsFromAbi, ExtractResultFromAbi, ExtractFunctionNameFromAbi, @@ -14,6 +16,7 @@ import { decodeFunctionResult, encodeFunctionData, getContractError, + DecodeFunctionResultArgs, } from '../../utils' import { WriteContractArgs } from '../wallet' import { call, CallArgs } from './call' @@ -22,16 +25,10 @@ export type SimulateContractArgs< TChain extends Chain = Chain, TAbi extends Abi | readonly unknown[] = Abi, TFunctionName extends string = any, -> = Omit, 'to' | 'data' | 'value'> & { - address: Address - abi: TAbi - functionName: ExtractFunctionNameFromAbi< - TAbi, - TFunctionName, - 'payable' | 'nonpayable' - > - value?: GetValue['value']> -} & ExtractArgsFromAbi +> = Omit, 'to' | 'data' | 'value'> & + ContractConfig & { + value?: GetValue['value']> + } export type SimulateContractResponse< TChain extends Chain = Chain, @@ -73,9 +70,10 @@ export async function simulateContract< } as unknown as CallArgs) const result = decodeFunctionResult({ abi, + args, functionName, data: data || '0x', - }) + } as DecodeFunctionResultArgs) return { result, request: { @@ -87,10 +85,11 @@ export async function simulateContract< }, } as unknown as SimulateContractResponse } catch (err) { - throw getContractError(err, { + throw getContractError(err as BaseError, { abi, address, args, + docsPath: '/docs/contract/simulateContract', functionName, sender: callRequest.from, }) diff --git a/src/actions/public/watchContractEvent.test.ts b/src/actions/public/watchContractEvent.test.ts index d10e21c6b9..0fd0f938cb 100644 --- a/src/actions/public/watchContractEvent.test.ts +++ b/src/actions/public/watchContractEvent.test.ts @@ -32,41 +32,45 @@ afterAll(async () => { }) }) -test('default', async () => { - let logs: OnLogsResponse[] = [] - - const unwatch = watchContractEvent(publicClient, { - ...usdcContractConfig, - onLogs: (logs_) => logs.push(logs_), - }) +test( + 'default', + async () => { + let logs: OnLogsResponse[] = [] + + const unwatch = watchContractEvent(publicClient, { + ...usdcContractConfig, + onLogs: (logs_) => logs.push(logs_), + }) - await writeContract(walletClient, { - ...usdcContractConfig, - from: vitalikAddress, - functionName: 'transfer', - args: [vitalikAddress, 1n], - }) - await writeContract(walletClient, { - ...usdcContractConfig, - from: vitalikAddress, - functionName: 'transfer', - args: [vitalikAddress, 1n], - }) - await wait(1000) - await writeContract(walletClient, { - ...usdcContractConfig, - from: vitalikAddress, - functionName: 'approve', - args: [vitalikAddress, 1n], - }) + await writeContract(walletClient, { + ...usdcContractConfig, + from: vitalikAddress, + functionName: 'transfer', + args: [vitalikAddress, 1n], + }) + await writeContract(walletClient, { + ...usdcContractConfig, + from: vitalikAddress, + functionName: 'transfer', + args: [vitalikAddress, 1n], + }) + await wait(1000) + await writeContract(walletClient, { + ...usdcContractConfig, + from: vitalikAddress, + functionName: 'approve', + args: [vitalikAddress, 1n], + }) - await wait(2000) - unwatch() + await wait(2000) + unwatch() - expect(logs.length).toBe(2) - expect(logs[0].length).toBe(2) - expect(logs[1].length).toBe(1) -}) + expect(logs.length).toBe(2) + expect(logs[0].length).toBe(2) + expect(logs[1].length).toBe(1) + }, + { retry: 3 }, +) test('args: batch', async () => { let logs: OnLogsResponse[] = [] diff --git a/src/actions/public/watchEvent.test.ts b/src/actions/public/watchEvent.test.ts index 056b6513eb..8008925f1b 100644 --- a/src/actions/public/watchEvent.test.ts +++ b/src/actions/public/watchEvent.test.ts @@ -9,7 +9,7 @@ import { vitalikAddress, walletClient, } from '../../_test' -import { impersonateAccount, stopImpersonatingAccount } from '../test' +import { impersonateAccount, mine, stopImpersonatingAccount } from '../test' import { sendTransaction } from '../wallet' import * as createEventFilter from './createEventFilter' import * as getFilterChanges from './getFilterChanges' @@ -19,6 +19,7 @@ beforeAll(async () => { await impersonateAccount(testClient, { address: vitalikAddress, }) + await mine(testClient, { blocks: 1 }) }) afterAll(async () => { @@ -27,37 +28,41 @@ afterAll(async () => { }) }) -test('default', async () => { - let logs: OnLogsResponse[] = [] +test( + 'default', + async () => { + let logs: OnLogsResponse[] = [] - const unwatch = watchEvent(publicClient, { - onLogs: (logs_) => logs.push(logs_), - }) + const unwatch = watchEvent(publicClient, { + onLogs: (logs_) => logs.push(logs_), + }) - await wait(1000) - await sendTransaction(walletClient, { - from: vitalikAddress, - to: usdcContractConfig.address, - data: transfer1Data(accounts[0].address), - }) - await sendTransaction(walletClient, { - from: vitalikAddress, - to: usdcContractConfig.address, - data: transfer1Data(accounts[0].address), - }) - await wait(1000) - await sendTransaction(walletClient, { - from: vitalikAddress, - to: usdcContractConfig.address, - data: transfer1Data(accounts[1].address), - }) - await wait(2000) - unwatch() + await wait(1000) + await sendTransaction(walletClient, { + from: vitalikAddress, + to: usdcContractConfig.address, + data: transfer1Data(accounts[0].address), + }) + await sendTransaction(walletClient, { + from: vitalikAddress, + to: usdcContractConfig.address, + data: transfer1Data(accounts[0].address), + }) + await wait(1000) + await sendTransaction(walletClient, { + from: vitalikAddress, + to: usdcContractConfig.address, + data: transfer1Data(accounts[1].address), + }) + await wait(2000) + unwatch() - expect(logs.length).toBe(2) - expect(logs[0].length).toBe(2) - expect(logs[1].length).toBe(1) -}) + expect(logs.length).toBe(2) + expect(logs[0].length).toBe(2) + expect(logs[1].length).toBe(1) + }, + { retry: 3 }, +) test('args: batch', async () => { let logs: OnLogsResponse[] = [] diff --git a/src/actions/wallet/signMessage.test.ts b/src/actions/wallet/signMessage.test.ts index 75824d2d00..0005f929bc 100644 --- a/src/actions/wallet/signMessage.test.ts +++ b/src/actions/wallet/signMessage.test.ts @@ -35,7 +35,6 @@ test('hex', async () => { "data (\\"deadbeaf\\") must be a hex value. Encode it first to a hex with the \`encodeHex\` util. Docs: https://viem.sh/TODO - Version: viem@1.0.2" `, ) diff --git a/src/actions/wallet/switchChain.test.ts b/src/actions/wallet/switchChain.test.ts index 2b9731d141..ad14f5a7cd 100644 --- a/src/actions/wallet/switchChain.test.ts +++ b/src/actions/wallet/switchChain.test.ts @@ -16,7 +16,6 @@ test('unsupported chain', async () => { [UnknownRpcError: An unknown RPC error occurred. Details: Unrecognized chain. - Version: viem@1.0.2 - Internal Error: {"code":-4902,"details":"Unrecognized chain."}] + Version: viem@1.0.2] `) }) diff --git a/src/actions/wallet/watchAsset.test.ts b/src/actions/wallet/watchAsset.test.ts index ad20888772..5bd8e3944f 100644 --- a/src/actions/wallet/watchAsset.test.ts +++ b/src/actions/wallet/watchAsset.test.ts @@ -35,7 +35,6 @@ test('errors: unsupported type', async () => { Double check you have provided the correct parameters. Details: Token type ERC721 not supported. - Version: viem@1.0.2 - Internal Error: {\\"code\\":-32602,\\"details\\":\\"Token type ERC721 not supported.\\"}" + Version: viem@1.0.2" `) }) diff --git a/src/actions/wallet/writeContract.test.ts b/src/actions/wallet/writeContract.test.ts index b339058f3d..60086a9466 100644 --- a/src/actions/wallet/writeContract.test.ts +++ b/src/actions/wallet/writeContract.test.ts @@ -12,6 +12,16 @@ import { mine } from '../test' import { writeContract } from './writeContract' test('default', async () => { + expect( + await writeContract(walletClient, { + ...wagmiContractConfig, + from: accounts[0].address, + functionName: 'mint', + }), + ).toBeDefined() +}) + +test('overloaded function', async () => { expect( await writeContract(walletClient, { ...wagmiContractConfig, @@ -27,7 +37,26 @@ test('w/ simulateContract', async () => { ...wagmiContractConfig, from: accounts[0].address, functionName: 'mint', - args: [69420n], + }) + expect(await writeContract(walletClient, request)).toBeDefined() + + await mine(testClient, { blocks: 1 }) + + expect( + await simulateContract(publicClient, { + ...wagmiContractConfig, + from: accounts[0].address, + functionName: 'mint', + }), + ).toBeDefined() +}) + +test('w/ simulateContract (overloaded)', async () => { + const { request } = await simulateContract(publicClient, { + ...wagmiContractConfig, + from: accounts[0].address, + functionName: 'mint', + args: [69421n], }) expect(await writeContract(walletClient, request)).toBeDefined() @@ -38,17 +67,18 @@ test('w/ simulateContract', async () => { ...wagmiContractConfig, from: accounts[0].address, functionName: 'mint', - args: [69420n], + args: [69421n], }), ).rejects.toThrowErrorMatchingInlineSnapshot(` - "Token ID is taken - - Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + "The contract function \\"mint\\" reverted with the following reason: + Token ID is taken + Contract: 0x0000000000000000000000000000000000000000 Function: mint(uint256 tokenId) - Arguments: (69420) + Arguments: (69421) + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 - Details: execution reverted: Token ID is taken + Docs: https://viem.sh/docs/contract/simulateContract Version: viem@1.0.2" `) }) diff --git a/src/actions/wallet/writeContract.ts b/src/actions/wallet/writeContract.ts index 2c133d9181..0670083750 100644 --- a/src/actions/wallet/writeContract.ts +++ b/src/actions/wallet/writeContract.ts @@ -1,13 +1,7 @@ import { Abi } from 'abitype' import type { WalletClient } from '../../clients' -import type { - Address, - Chain, - ExtractArgsFromAbi, - ExtractFunctionNameFromAbi, - GetValue, -} from '../../types' +import type { Chain, ContractConfig, GetValue } from '../../types' import { EncodeFunctionDataArgs, encodeFunctionData } from '../../utils' import { sendTransaction, @@ -20,11 +14,8 @@ export type WriteContractArgs< TAbi extends Abi | readonly unknown[] = Abi, TFunctionName extends string = any, > = Omit, 'to' | 'data' | 'value'> & { - address: Address - abi: TAbi - functionName: ExtractFunctionNameFromAbi value?: GetValue['value']> -} & ExtractArgsFromAbi +} & ContractConfig export type WriteContractResponse = SendTransactionResponse diff --git a/src/clients/transports/http.test.ts b/src/clients/transports/http.test.ts index 0e166f4a8a..ab174b05b7 100644 --- a/src/clients/transports/http.test.ts +++ b/src/clients/transports/http.test.ts @@ -103,7 +103,6 @@ test('no url', () => { "No URL was provided to the Transport. Please provide a valid RPC URL to the Transport. Docs: https://viem.sh/docs/clients/intro - Version: viem@1.0.2" `, ) diff --git a/src/clients/transports/webSocket.test.ts b/src/clients/transports/webSocket.test.ts index 28d7eb0b49..40fb0b27fb 100644 --- a/src/clients/transports/webSocket.test.ts +++ b/src/clients/transports/webSocket.test.ts @@ -158,7 +158,6 @@ test('no url', () => { "No URL was provided to the Transport. Please provide a valid RPC URL to the Transport. Docs: https://viem.sh/docs/clients/intro - Version: viem@1.0.2" `) }) diff --git a/src/constants/abis.test.ts b/src/constants/abis.test.ts new file mode 100644 index 0000000000..946f2bf20e --- /dev/null +++ b/src/constants/abis.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from 'vitest' + +import * as abis from './index' + +test('exports abis', () => { + expect(abis).toMatchInlineSnapshot(` + { + "multicall3Abi": [ + { + "inputs": [ + { + "components": [ + { + "name": "target", + "type": "address", + }, + { + "name": "allowFailure", + "type": "bool", + }, + { + "name": "callData", + "type": "bytes", + }, + ], + "name": "calls", + "type": "tuple[]", + }, + ], + "name": "aggregate3", + "outputs": [ + { + "components": [ + { + "name": "success", + "type": "bool", + }, + { + "name": "returnData", + "type": "bytes", + }, + ], + "name": "returnData", + "type": "tuple[]", + }, + ], + "stateMutability": "view", + "type": "function", + }, + ], + } + `) +}) diff --git a/src/constants/abis.ts b/src/constants/abis.ts new file mode 100644 index 0000000000..4e27fe123b --- /dev/null +++ b/src/constants/abis.ts @@ -0,0 +1,556 @@ +/* [Multicall3](https://github.com/mds1/multicall) */ +export const multicall3Abi = [ + { + inputs: [ + { + components: [ + { + name: 'target', + type: 'address', + }, + { + name: 'allowFailure', + type: 'bool', + }, + { + name: 'callData', + type: 'bytes', + }, + ], + name: 'calls', + type: 'tuple[]', + }, + ], + name: 'aggregate3', + outputs: [ + { + components: [ + { + name: 'success', + type: 'bool', + }, + { + name: 'returnData', + type: 'bytes', + }, + ], + name: 'returnData', + type: 'tuple[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const + +/** + * [ERC-4626 Tokenized Vaults Standard](https://ethereum.org/en/developers/docs/standards/tokens/erc-4626) + */ +export const erc4626ABI = [ + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'owner', + type: 'address', + }, + { + indexed: true, + name: 'spender', + type: 'address', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'sender', + type: 'address', + }, + { + indexed: true, + name: 'receiver', + type: 'address', + }, + { + indexed: false, + name: 'assets', + type: 'uint256', + }, + { + indexed: false, + name: 'shares', + type: 'uint256', + }, + ], + name: 'Deposit', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: true, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'sender', + type: 'address', + }, + { + indexed: true, + name: 'receiver', + type: 'address', + }, + { + indexed: true, + name: 'owner', + type: 'address', + }, + { + indexed: false, + name: 'assets', + type: 'uint256', + }, + { + indexed: false, + name: 'shares', + type: 'uint256', + }, + ], + name: 'Withdraw', + type: 'event', + }, + { + inputs: [ + { + name: 'owner', + type: 'address', + }, + { + name: 'spender', + type: 'address', + }, + ], + name: 'allowance', + outputs: [ + { + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'spender', + type: 'address', + }, + { + name: 'amount', + type: 'uint256', + }, + ], + name: 'approve', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'asset', + outputs: [ + { + name: 'assetTokenAddress', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'account', + type: 'address', + }, + ], + name: 'balanceOf', + outputs: [ + { + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'shares', + type: 'uint256', + }, + ], + name: 'convertToAssets', + outputs: [ + { + name: 'assets', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'assets', + type: 'uint256', + }, + ], + name: 'convertToShares', + outputs: [ + { + name: 'shares', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'assets', + type: 'uint256', + }, + { + name: 'receiver', + type: 'address', + }, + ], + name: 'deposit', + outputs: [ + { + name: 'shares', + type: 'uint256', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + name: 'caller', + type: 'address', + }, + ], + name: 'maxDeposit', + outputs: [ + { + name: 'maxAssets', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'caller', + type: 'address', + }, + ], + name: 'maxMint', + outputs: [ + { + name: 'maxShares', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'owner', + type: 'address', + }, + ], + name: 'maxRedeem', + outputs: [ + { + name: 'maxShares', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'owner', + type: 'address', + }, + ], + name: 'maxWithdraw', + outputs: [ + { + name: 'maxAssets', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'shares', + type: 'uint256', + }, + { + name: 'receiver', + type: 'address', + }, + ], + name: 'mint', + outputs: [ + { + name: 'assets', + type: 'uint256', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + name: 'assets', + type: 'uint256', + }, + ], + name: 'previewDeposit', + outputs: [ + { + name: 'shares', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'shares', + type: 'uint256', + }, + ], + name: 'previewMint', + outputs: [ + { + name: 'assets', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'shares', + type: 'uint256', + }, + ], + name: 'previewRedeem', + outputs: [ + { + name: 'assets', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'assets', + type: 'uint256', + }, + ], + name: 'previewWithdraw', + outputs: [ + { + name: 'shares', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'shares', + type: 'uint256', + }, + { + name: 'receiver', + type: 'address', + }, + { + name: 'owner', + type: 'address', + }, + ], + name: 'redeem', + outputs: [ + { + name: 'assets', + type: 'uint256', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'totalAssets', + outputs: [ + { + name: 'totalManagedAssets', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'totalSupply', + outputs: [ + { + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'to', + type: 'address', + }, + { + name: 'amount', + type: 'uint256', + }, + ], + name: 'transfer', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + name: 'from', + type: 'address', + }, + { + name: 'to', + type: 'address', + }, + { + name: 'amount', + type: 'uint256', + }, + ], + name: 'transferFrom', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + name: 'assets', + type: 'uint256', + }, + { + name: 'receiver', + type: 'address', + }, + { + name: 'owner', + type: 'address', + }, + ], + name: 'withdraw', + outputs: [ + { + name: 'shares', + type: 'uint256', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const diff --git a/src/constants/index.test.ts b/src/constants/index.test.ts new file mode 100644 index 0000000000..e74b36772a --- /dev/null +++ b/src/constants/index.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from 'vitest' + +import * as clients from './index' + +test('exports clients', () => { + expect(clients).toMatchInlineSnapshot(` + { + "multicall3Abi": [ + { + "inputs": [ + { + "components": [ + { + "name": "target", + "type": "address", + }, + { + "name": "allowFailure", + "type": "bool", + }, + { + "name": "callData", + "type": "bytes", + }, + ], + "name": "calls", + "type": "tuple[]", + }, + ], + "name": "aggregate3", + "outputs": [ + { + "components": [ + { + "name": "success", + "type": "bool", + }, + { + "name": "returnData", + "type": "bytes", + }, + ], + "name": "returnData", + "type": "tuple[]", + }, + ], + "stateMutability": "view", + "type": "function", + }, + ], + } + `) +}) diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000000..2c3c02e53f --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1 @@ +export { multicall3Abi } from './abis' diff --git a/src/errors/abi.test.ts b/src/errors/abi.test.ts index 92e8ae7280..aa5d82bad0 100644 --- a/src/errors/abi.test.ts +++ b/src/errors/abi.test.ts @@ -25,7 +25,6 @@ test('InvalidAbiDecodingTypeError', () => { Please provide a valid ABI type. Docs: https://viem.sh/lol - Version: viem@1.0.2] `) }) @@ -69,7 +68,6 @@ test('InvalidAbiEncodingTypeError', () => { Please provide a valid ABI type. Docs: https://viem.sh/lol - Version: viem@1.0.2] `) }) diff --git a/src/errors/base.test.ts b/src/errors/base.test.ts index 48a9ac0d57..5ecea5d0e4 100644 --- a/src/errors/base.test.ts +++ b/src/errors/base.test.ts @@ -17,6 +17,13 @@ test('BaseError', () => { Details: details Version: viem@1.0.2] `) + + expect(new BaseError('', { details: 'details' })).toMatchInlineSnapshot(` + [ViemError: An error occurred. + + Details: details + Version: viem@1.0.2] + `) }) test('BaseError (w/ docsPath)', () => { @@ -29,6 +36,43 @@ test('BaseError (w/ docsPath)', () => { [ViemError: An error occurred. Docs: https://viem.sh/lol + Details: details + Version: viem@1.0.2] + `) + expect( + new BaseError('An error occurred.', { + cause: new BaseError('error', { docsPath: '/docs' }), + }), + ).toMatchInlineSnapshot(` + [ViemError: An error occurred. + + Docs: https://viem.sh/docs + Version: viem@1.0.2] + `) + expect( + new BaseError('An error occurred.', { + cause: new BaseError('error'), + docsPath: '/lol', + }), + ).toMatchInlineSnapshot(` + [ViemError: An error occurred. + + Docs: https://viem.sh/lol + Version: viem@1.0.2] + `) +}) + +test('BaseError (w/ metaMessages)', () => { + expect( + new BaseError('An error occurred.', { + details: 'details', + metaMessages: ['Reason: idk', 'Cause: lol'], + }), + ).toMatchInlineSnapshot(` + [ViemError: An error occurred. + + Reason: idk + Cause: lol Details: details Version: viem@1.0.2] @@ -48,7 +92,6 @@ test('inherited BaseError', () => { [ViemError: An internal error occurred. Docs: https://viem.sh/lol - Details: details Version: viem@1.0.2] `) @@ -65,7 +108,6 @@ test('inherited Error', () => { [ViemError: An internal error occurred. Docs: https://viem.sh/lol - Details: details Version: viem@1.0.2] `) diff --git a/src/errors/base.ts b/src/errors/base.ts index 75ee879cd0..fdf7cd6997 100644 --- a/src/errors/base.ts +++ b/src/errors/base.ts @@ -1,11 +1,13 @@ // @ts-ignore import pkg from '../../package.json' -import { stringify } from '../utils/stringify' /* c8 ignore next */ const version = process.env.TEST ? '1.0.2' : pkg.version -type BaseErrorArgs = { docsPath?: string } & ( +type BaseErrorArgs = { + docsPath?: string + metaMessages?: string[] +} & ( | { cause?: never details?: string @@ -17,13 +19,14 @@ type BaseErrorArgs = { docsPath?: string } & ( ) export class BaseError extends Error { - humanMessage: string details: string docsPath?: string + metaMessages?: string[] + shortMessage: string name = 'ViemError' - constructor(humanMessage: string, args: BaseErrorArgs = {}) { + constructor(shortMessage: string, args: BaseErrorArgs = {}) { const details = args.cause instanceof BaseError ? args.cause.details @@ -35,16 +38,12 @@ export class BaseError extends Error { ? args.cause.docsPath || args.docsPath : args.docsPath const message = [ - humanMessage, - ...(docsPath ? ['', `Docs: https://viem.sh${docsPath}`] : []), + shortMessage || 'An error occurred.', '', + ...(args.metaMessages ? [...args.metaMessages, ''] : []), + ...(docsPath ? [`Docs: https://viem.sh${docsPath}`] : []), ...(details ? [`Details: ${details}`] : []), `Version: viem@${version}`, - ...(args.cause && - !(args.cause instanceof BaseError) && - Object.keys(args.cause).length > 0 - ? [`Internal Error: ${stringify(args.cause)}`] - : []), ].join('\n') super(message) @@ -52,6 +51,7 @@ export class BaseError extends Error { if (args.cause) this.cause = args.cause this.details = details this.docsPath = docsPath - this.humanMessage = humanMessage + this.metaMessages = args.metaMessages + this.shortMessage = shortMessage } } diff --git a/src/errors/block.test.ts b/src/errors/block.test.ts index 5be1b44481..ab1ebd50f6 100644 --- a/src/errors/block.test.ts +++ b/src/errors/block.test.ts @@ -5,17 +5,17 @@ test('BlockNotFoundError', () => { expect( new BlockNotFoundError({ blockNumber: 69420n }), ).toMatchInlineSnapshot(` - [BlockNotFoundError: Block at number "69420" could not be found. + [BlockNotFoundError: Block at number "69420" could not be found. - Version: viem@1.0.2] - `) + Version: viem@1.0.2] + `) expect( new BlockNotFoundError({ blockHash: '0x69420' }), ).toMatchInlineSnapshot(` - [BlockNotFoundError: Block at hash "0x69420" could not be found. + [BlockNotFoundError: Block at hash "0x69420" could not be found. - Version: viem@1.0.2] - `) + Version: viem@1.0.2] + `) expect(new BlockNotFoundError({})).toMatchInlineSnapshot(` [BlockNotFoundError: Block could not be found. diff --git a/src/errors/contract.ts b/src/errors/contract.ts index 98c544965f..62f823de78 100644 --- a/src/errors/contract.ts +++ b/src/errors/contract.ts @@ -1,25 +1,26 @@ import { Abi } from 'abitype' -import { Address } from '../types' +import { Address, Hex } from '../types' +import { DecodeErrorResultResponse, decodeErrorResult } from '../utils' import { BaseError } from './base' -export class ContractMethodExecutionError extends BaseError { +export class ContractFunctionExecutionError extends BaseError { abi?: Abi args?: unknown[] + cause: BaseError contractAddress?: Address formattedArgs?: string functionName?: string - reason?: string sender?: Address - name = 'ContractMethodExecutionError' + name = 'ContractFunctionExecutionError' constructor( - message?: string, + cause: BaseError, { abi, args, - cause, contractAddress, + docsPath, formattedArgs, functionName, functionWithParams, @@ -27,99 +28,104 @@ export class ContractMethodExecutionError extends BaseError { }: { abi?: Abi args?: any - cause?: Error contractAddress?: Address + docsPath?: string formattedArgs?: string functionName?: string functionWithParams?: string sender?: Address - } = {}, + }, ) { super( - [ - message, - ' ', - sender && `Sender: ${sender}`, - contractAddress && - `Contract: ${ - /* c8 ignore start */ - process.env.TEST - ? '0x0000000000000000000000000000000000000000' - : contractAddress - /* c8 ignore end */ - }`, - functionWithParams && `Function: ${functionWithParams}`, - formattedArgs && - `Arguments: ${[...Array(functionName?.length ?? 0).keys()] - .map(() => ' ') - .join('')}${formattedArgs}`, - ] - .filter(Boolean) - .join('\n'), + cause.shortMessage || + `An unknown error occurred while executing the contract function "${functionName}".`, { cause, + docsPath, + metaMessages: [ + ...(cause.metaMessages ? [...cause.metaMessages, ' '] : []), + contractAddress && + `Contract: ${ + /* c8 ignore start */ + process.env.TEST + ? '0x0000000000000000000000000000000000000000' + : contractAddress + /* c8 ignore end */ + }`, + functionWithParams && `Function: ${functionWithParams}`, + formattedArgs && + formattedArgs !== '()' && + `Arguments: ${[...Array(functionName?.length ?? 0).keys()] + .map(() => ' ') + .join('')}${formattedArgs}`, + sender && `Sender: ${sender}`, + ].filter(Boolean) as string[], }, ) - if (message) this.reason = message this.abi = abi this.args = args + this.cause = cause this.contractAddress = contractAddress this.functionName = functionName this.sender = sender } } -export class ContractMethodZeroDataError extends BaseError { - abi?: Abi - args?: unknown[] - contractAddress?: Address - functionName?: string - functionWithParams?: string +export class ContractFunctionRevertedError extends BaseError { + name = 'ContractFunctionRevertedError' - name = 'ContractMethodZeroDataError' + data?: DecodeErrorResultResponse + reason?: string constructor({ abi, - args, - cause, - contractAddress, + data, functionName, - functionWithParams, - }: { - abi?: Abi - args?: any - cause?: Error - contractAddress?: Address - functionName?: string - functionWithParams?: string - } = {}) { + message, + }: { abi: Abi; data?: Hex; functionName: string; message?: string }) { + let decodedData: DecodeErrorResultResponse | undefined = undefined + let reason + if (data) { + decodedData = decodeErrorResult({ abi, data }) + const { errorName, args: errorArgs } = decodedData + if (errorName === 'Error') reason = (errorArgs as string[])[0] + // TODO: Support Panic(uint256) & custom errors. + } else if (message) reason = message + super( [ - `The contract method "${functionName}" returned no data ("0x"). This could be due to any of the following:`, + `The contract function "${functionName}" reverted with the following reason:`, + reason, + ].join('\n'), + ) + + this.reason = reason + this.data = decodedData + } +} + +export class ContractFunctionZeroDataError extends BaseError { + name = 'ContractFunctionZeroDataError' + constructor({ functionName }: { functionName: string }) { + super(`The contract function "${functionName}" returned no data ("0x").`, { + metaMessages: [ + 'This could be due to any of the following:', `- The contract does not have the function "${functionName}",`, '- The parameters passed to the contract function may be invalid, or', '- The address is not a contract.', - ' ', - contractAddress && - `Contract: ${ - /* c8 ignore start */ - process.env.TEST - ? '0x0000000000000000000000000000000000000000' - : contractAddress - /* c8 ignore end */ - }`, - functionWithParams && `Function: ${functionWithParams}`, - functionWithParams && ` > "0x"`, - ] - .filter(Boolean) - .join('\n'), - { - cause, - }, - ) - this.abi = abi - this.args = args - this.contractAddress = contractAddress - this.functionName = functionName + ], + }) + } +} + +export class RawContractError extends BaseError { + code = 3 + name = 'RawContractError' + + data?: Hex + + constructor({ data, message }: { data?: Hex; message?: string }) { + super(message || '') + this.data = data } } diff --git a/src/errors/index.ts b/src/errors/index.ts index a9881d62ec..4da0347374 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -25,8 +25,8 @@ export { BaseError } from './base' export { BlockNotFoundError } from './block' export { - ContractMethodExecutionError, - ContractMethodZeroDataError, + ContractFunctionExecutionError, + RawContractError, } from './contract' export { SizeExceedsPaddingSizeError } from './data' diff --git a/src/errors/request.test.ts b/src/errors/request.test.ts index 9a0b59ea82..7e5427482e 100644 --- a/src/errors/request.test.ts +++ b/src/errors/request.test.ts @@ -27,7 +27,7 @@ test('RequestError', () => { error: { code: 1337, message: 'error details' }, }), { - humanMessage: 'An internal error was received.', + shortMessage: 'An internal error was received.', }, ), ).toMatchInlineSnapshot(` @@ -46,7 +46,7 @@ test('RpcRequestError', () => { url: 'https://viem.sh', error: { code: 1337, message: 'error details' }, }), - { humanMessage: 'An internal error was received.' }, + { shortMessage: 'An internal error was received.' }, ), ).toMatchInlineSnapshot(` [RpcError: An internal error was received. @@ -65,7 +65,7 @@ test('RpcRequestError', () => { error: { code: 1337, message: 'error details' }, }), { - humanMessage: 'An internal error was received.', + shortMessage: 'An internal error was received.', docsPath: '/lol', }, ), @@ -73,7 +73,6 @@ test('RpcRequestError', () => { [RpcError: An internal error was received. Docs: https://viem.sh/lol - Details: error details Version: viem@1.0.2] `) diff --git a/src/errors/request.ts b/src/errors/request.ts index 5f3d719969..3632ac75ab 100644 --- a/src/errors/request.ts +++ b/src/errors/request.ts @@ -4,9 +4,9 @@ import { RpcError } from './rpc' export class RequestError extends BaseError { constructor( err: Error, - { docsPath, humanMessage }: { docsPath?: string; humanMessage: string }, + { docsPath, shortMessage }: { docsPath?: string; shortMessage: string }, ) { - super(humanMessage, { + super(shortMessage, { cause: err, docsPath, }) @@ -19,9 +19,9 @@ export class RpcRequestError extends RequestError { constructor( err: RpcError, - { docsPath, humanMessage }: { docsPath?: string; humanMessage: string }, + { docsPath, shortMessage }: { docsPath?: string; shortMessage: string }, ) { - super(err, { docsPath, humanMessage }) + super(err, { docsPath, shortMessage }) this.code = err.code this.name = err.name } @@ -33,7 +33,7 @@ export class ParseRpcError extends RpcRequestError { constructor(err: RpcError) { super(err, { - humanMessage: + shortMessage: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.', }) } @@ -44,7 +44,7 @@ export class InvalidRequestRpcError extends RpcRequestError { code = -32600 constructor(err: RpcError) { - super(err, { humanMessage: 'JSON is not a valid request object.' }) + super(err, { shortMessage: 'JSON is not a valid request object.' }) } } @@ -54,7 +54,7 @@ export class MethodNotFoundRpcError extends RpcRequestError { constructor(err: RpcError) { super(err, { - humanMessage: 'The method does not exist / is not available.', + shortMessage: 'The method does not exist / is not available.', }) } } @@ -65,7 +65,7 @@ export class InvalidParamsRpcError extends RpcRequestError { constructor(err: RpcError) { super(err, { - humanMessage: [ + shortMessage: [ 'Invalid parameters were provided to the RPC method.', 'Double check you have provided the correct parameters.', ].join('\n'), @@ -78,7 +78,7 @@ export class InternalRpcError extends RpcRequestError { code = -32603 constructor(err: RpcError) { - super(err, { humanMessage: 'An internal error was received.' }) + super(err, { shortMessage: 'An internal error was received.' }) } } @@ -88,7 +88,7 @@ export class InvalidInputRpcError extends RpcRequestError { constructor(err: RpcError) { super(err, { - humanMessage: [ + shortMessage: [ 'Missing or invalid parameters.', 'Double check you have provided the correct parameters.', ].join('\n'), @@ -101,7 +101,7 @@ export class ResourceNotFoundRpcError extends RpcRequestError { code = -32001 constructor(err: RpcError) { - super(err, { humanMessage: 'Requested resource not found.' }) + super(err, { shortMessage: 'Requested resource not found.' }) } } @@ -110,7 +110,7 @@ export class ResourceUnavailableRpcError extends RpcRequestError { code = -32002 constructor(err: RpcError) { - super(err, { humanMessage: 'Requested resource not available.' }) + super(err, { shortMessage: 'Requested resource not available.' }) } } @@ -119,7 +119,7 @@ export class TransactionRejectedRpcError extends RpcRequestError { code = -32003 constructor(err: RpcError) { - super(err, { humanMessage: 'Transaction creation failed.' }) + super(err, { shortMessage: 'Transaction creation failed.' }) } } @@ -128,7 +128,7 @@ export class MethodNotSupportedRpcError extends RpcRequestError { code = -32004 constructor(err: RpcError) { - super(err, { humanMessage: 'Method is not implemented.' }) + super(err, { shortMessage: 'Method is not implemented.' }) } } @@ -137,7 +137,7 @@ export class LimitExceededRpcError extends RpcRequestError { code = -32005 constructor(err: RpcError) { - super(err, { humanMessage: 'Request exceeds defined limit.' }) + super(err, { shortMessage: 'Request exceeds defined limit.' }) } } @@ -147,7 +147,7 @@ export class JsonRpcVersionUnsupportedError extends RpcRequestError { constructor(err: RpcError) { super(err, { - humanMessage: 'Version of JSON-RPC protocol is not supported.', + shortMessage: 'Version of JSON-RPC protocol is not supported.', }) } } @@ -157,7 +157,7 @@ export class UnknownRpcError extends RequestError { constructor(err: Error) { super(err, { - humanMessage: 'An unknown RPC error occurred.', + shortMessage: 'An unknown RPC error occurred.', }) } } diff --git a/src/errors/rpc.test.ts b/src/errors/rpc.test.ts index 82f64a2982..ac8d1a413f 100644 --- a/src/errors/rpc.test.ts +++ b/src/errors/rpc.test.ts @@ -21,8 +21,7 @@ test('RpcError', () => { Request body: {"foo":"bar"} Details: Error - Version: viem@1.0.2 - Internal Error: {"code":420,"message":"Error"}] + Version: viem@1.0.2] `) }) diff --git a/src/errors/transaction.test.ts b/src/errors/transaction.test.ts index ae12ea9ebe..c9573e31e4 100644 --- a/src/errors/transaction.test.ts +++ b/src/errors/transaction.test.ts @@ -19,40 +19,40 @@ describe('TransactionNotFoundError', () => { expect( new TransactionNotFoundError({ blockHash: '0x123', index: 420 }), ).toMatchInlineSnapshot(` - [TransactionNotFoundError: Transaction at block hash "0x123" at index "420" could not be found. + [TransactionNotFoundError: Transaction at block hash "0x123" at index "420" could not be found. - Version: viem@1.0.2] - `) + Version: viem@1.0.2] + `) }) test('blockTag', async () => { expect( new TransactionNotFoundError({ blockTag: 'latest', index: 420 }), ).toMatchInlineSnapshot(` - [TransactionNotFoundError: Transaction at block time "latest" at index "420" could not be found. + [TransactionNotFoundError: Transaction at block time "latest" at index "420" could not be found. - Version: viem@1.0.2] - `) + Version: viem@1.0.2] + `) }) test('blockNumber', async () => { expect( new TransactionNotFoundError({ blockNumber: 42069n, index: 420 }), ).toMatchInlineSnapshot(` - [TransactionNotFoundError: Transaction at block number "42069" at index "420" could not be found. + [TransactionNotFoundError: Transaction at block number "42069" at index "420" could not be found. - Version: viem@1.0.2] - `) + Version: viem@1.0.2] + `) }) test('hash', async () => { expect( new TransactionNotFoundError({ hash: '0x123' }), ).toMatchInlineSnapshot(` - [TransactionNotFoundError: Transaction with hash "0x123" could not be found. + [TransactionNotFoundError: Transaction with hash "0x123" could not be found. - Version: viem@1.0.2] - `) + Version: viem@1.0.2] + `) }) }) diff --git a/src/errors/transport.test.ts b/src/errors/transport.test.ts index fa98f0609b..eb4f078011 100644 --- a/src/errors/transport.test.ts +++ b/src/errors/transport.test.ts @@ -6,7 +6,6 @@ test('UrlRequiredError', () => { [ViemError: No URL was provided to the Transport. Please provide a valid RPC URL to the Transport. Docs: https://viem.sh/docs/clients/intro - Version: viem@1.0.2] `) }) diff --git a/src/index.test.ts b/src/index.test.ts index d8309603b4..3b11a21551 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -151,6 +151,49 @@ test('exports actions', () => { "isHex": [Function], "keccak256": [Function], "mine": [Function], + "multicall3Abi": [ + { + "inputs": [ + { + "components": [ + { + "name": "target", + "type": "address", + }, + { + "name": "allowFailure", + "type": "bool", + }, + { + "name": "callData", + "type": "bytes", + }, + ], + "name": "calls", + "type": "tuple[]", + }, + ], + "name": "aggregate3", + "outputs": [ + { + "components": [ + { + "name": "success", + "type": "bool", + }, + { + "name": "returnData", + "type": "bytes", + }, + ], + "name": "returnData", + "type": "tuple[]", + }, + ], + "stateMutability": "view", + "type": "function", + }, + ], "numberToBytes": [Function], "numberToHex": [Function], "pad": [Function], diff --git a/src/index.ts b/src/index.ts index f6fecb1f28..62b7d2a2ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -191,6 +191,8 @@ export { webSocket, } from './clients' +export { multicall3Abi } from './constants' + export { AbiConstructorNotFoundError, AbiConstructorParamsNotFoundError, @@ -254,6 +256,8 @@ export type { BlockNumber, BlockTag, ByteArray, + Chain, + ContractConfig, Hex, FeeHistory, FeeValues, @@ -300,6 +304,7 @@ export type { GetContractAddressOptions, GetCreateAddressOptions, GetCreate2AddressOptions, + GetAbiItemArgs, EncodeRlpResponse, FormattedBlock, FormattedTransaction, diff --git a/src/types/contract.ts b/src/types/contract.ts index 7076268a31..a3ba814141 100644 --- a/src/types/contract.ts +++ b/src/types/contract.ts @@ -8,15 +8,16 @@ import type { AbiParameter, AbiParameterToPrimitiveType, AbiParametersToPrimitiveTypes, + AbiStateMutability, ExtractAbiFunction, ExtractAbiEvent, ExtractAbiEventNames, - ExtractAbiError, - AbiStateMutability, ExtractAbiFunctionNames, + ExtractAbiError, + ExtractAbiErrorNames, } from 'abitype' -import { TransactionRequest } from './transaction' - +import type { Address } from './misc' +import type { TransactionRequest } from './transaction' import type { Trim } from './utils' ////////////////////////////////////////////////////////////////////// @@ -191,6 +192,23 @@ export type ExtractFunctionNameFromAbi< : never : TFunctionName +type ExtractNames = + | ExtractAbiFunctionNames + | ExtractAbiEventNames + | ExtractAbiErrorNames + +export type ExtractNameFromAbi< + TAbi extends Abi | readonly unknown[] = Abi, + TName extends string = string, +> = TAbi extends Abi + ? ExtractNames extends infer AbiNames + ? + | AbiNames + | (TName extends AbiNames ? TName : never) + | (Abi extends TAbi ? string : never) + : never + : TName + export type ExtractResultFromAbi< TAbi extends Abi | readonly unknown[] = Abi, TFunctionName extends string = string, @@ -329,7 +347,24 @@ export type ExtractArgsFromFunctionDefinition = ExtractArgsFromDefinition< > ////////////////////////////////////////////////////////////////////// -// Call Args +// Args + +export type ContractConfig< + TAbi extends Abi | readonly unknown[] = Abi, + TFunctionName extends string = string, + TAbiStateMutability extends AbiStateMutability = AbiStateMutability, +> = { + /** Contract ABI */ + abi: TAbi + /** Contract address */ + address: Address + /** Function to invoke on the contract */ + functionName: ExtractFunctionNameFromAbi< + TAbi, + TFunctionName, + TAbiStateMutability + > +} & ExtractArgsFromAbi export type GetValue< TAbi extends Abi | readonly unknown[], diff --git a/src/types/index.ts b/src/types/index.ts index 866f28a6cd..d39e2fbae1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -11,6 +11,7 @@ export type { Chain } from './chain' export type { AbiItem, AbiEventParametersToPrimitiveTypes, + ContractConfig, EventDefinition, ExtractArgsFromAbi, ExtractArgsFromEventDefinition, @@ -20,6 +21,7 @@ export type { ExtractEventArgsFromAbi, ExtractEventNameFromAbi, ExtractFunctionNameFromAbi, + ExtractNameFromAbi, ExtractResultFromAbi, GetValue, } from './contract' @@ -40,6 +42,8 @@ export type { Log } from './log' export type { Address, ByteArray, Hex, Hash, LogTopic } from './misc' +export type { MulticallContracts } from './multicall' + export type { Index, Quantity, diff --git a/src/types/multicall.ts b/src/types/multicall.ts new file mode 100644 index 0000000000..82a5e762ef --- /dev/null +++ b/src/types/multicall.ts @@ -0,0 +1,82 @@ +import { Abi } from 'abitype'; +import { ContractConfig, ExtractResultFromAbi } from './contract'; + +type MAXIMUM_DEPTH = 20 + +export type Contract< + TAbi extends Abi | readonly unknown[] = Abi | readonly unknown[], + TFunctionName extends string = string, +> = { abi: TAbi; functionName: TFunctionName } + +export type MulticallContracts< + TContracts extends Contract[], + TProperties extends Record = object, + Result extends any[] = [], + Depth extends ReadonlyArray = [], +> = Depth['length'] extends MAXIMUM_DEPTH + ? (ContractConfig & TProperties)[] + : TContracts extends [] + ? [] + : TContracts extends [infer Head extends Contract] + ? [ + ...Result, + ContractConfig & + TProperties, + ] + : TContracts extends [ + infer Head extends Contract, + ...infer Tail extends Contract[], + ] + ? MulticallContracts< + [...Tail], + TProperties, + [ + ...Result, + ContractConfig & + TProperties, + ], + [...Depth, 1] + > + : unknown[] extends TContracts + ? TContracts + : // If `TContracts` is *some* array but we couldn't assign `unknown[]` to it, then it must hold some known/homogenous type! + // use this to infer the param types in the case of Array.map() argument + TContracts extends ContractConfig[] + ? (ContractConfig & TProperties)[] + : (ContractConfig & TProperties)[] + + +export type MulticallResult = TAllowFailure extends true ? ({ + error?: undefined + result: Result + status: 'success' +} | { + error: Error + result?: undefined + status: 'error' +}) : Result + +export type MulticallResults< + TContracts extends Contract[], + TAllowFailure extends boolean = true, + Result extends any[] = [], + Depth extends ReadonlyArray = [], +> = Depth['length'] extends MAXIMUM_DEPTH + ? MulticallResult[] + : TContracts extends [] + ? [] + : TContracts extends [infer Head extends Contract] + ? [...Result, MulticallResult, TAllowFailure>] + : TContracts extends [ + infer Head extends Contract, + ...infer Tail extends Contract[], + ] + ? MulticallResults< + [...Tail], + TAllowFailure, + [...Result, MulticallResult, TAllowFailure>], + [...Depth, 1] + > + : TContracts extends ContractConfig[] + ? MulticallResult, TAllowFailure>[] + : MulticallResult[] \ No newline at end of file diff --git a/src/utils/abi/decodeAbi.test.ts b/src/utils/abi/decodeAbi.test.ts index 4e44ff9ed9..016787c102 100644 --- a/src/utils/abi/decodeAbi.test.ts +++ b/src/utils/abi/decodeAbi.test.ts @@ -1582,7 +1582,6 @@ test('invalid type', () => { Please provide a valid ABI type. Docs: https://viem.sh/docs/contract/decodeAbi - Version: viem@1.0.2" `) }) diff --git a/src/utils/abi/decodeDeployData.test.ts b/src/utils/abi/decodeDeployData.test.ts index d7bab94fd0..701060bd8a 100644 --- a/src/utils/abi/decodeDeployData.test.ts +++ b/src/utils/abi/decodeDeployData.test.ts @@ -80,7 +80,6 @@ test('error: constructor not found', () => { Make sure you are using the correct ABI and that the constructor exists on it. Docs: https://viem.sh/docs/contract/decodeDeployData - Version: viem@1.0.2" `) }) @@ -105,7 +104,6 @@ test('error: no inputs', () => { Make sure you are using the correct ABI, and that the \`inputs\` attribute on the constructor exists. Docs: https://viem.sh/docs/contract/decodeDeployData - Version: viem@1.0.2" `, ) @@ -128,7 +126,6 @@ test('error: no inputs', () => { Make sure you are using the correct ABI, and that the \`inputs\` attribute on the constructor exists. Docs: https://viem.sh/docs/contract/decodeDeployData - Version: viem@1.0.2" `, ) @@ -152,7 +149,6 @@ test('error: no inputs', () => { Make sure you are using the correct ABI, and that the \`inputs\` attribute on the constructor exists. Docs: https://viem.sh/docs/contract/decodeDeployData - Version: viem@1.0.2" `, ) diff --git a/src/utils/abi/decodeErrorResult.test.ts b/src/utils/abi/decodeErrorResult.test.ts index 204b0c6899..d88b85d48d 100644 --- a/src/utils/abi/decodeErrorResult.test.ts +++ b/src/utils/abi/decodeErrorResult.test.ts @@ -106,6 +106,38 @@ test('revert AccessDeniedError((uint256,bool,address,uint256))', () => { }) }) +test('Error(string)', () => { + expect( + decodeErrorResult({ + data: '0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000047465737400000000000000000000000000000000000000000000000000000000', + }), + ).toEqual({ + errorName: 'Error', + args: ['test'], + }) +}) + +test.todo('Panic(uint256)') + +test('zero data', () => { + expect(() => + decodeErrorResult({ + abi: [ + { + inputs: [], + name: 'SoldOutError', + type: 'error', + }, + ], + data: '0x', + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Cannot decode zero data (\\"0x\\") with ABI parameters. + + Version: viem@1.0.2" + `) +}) + test("errors: error doesn't exist", () => { expect(() => decodeErrorResult({ @@ -124,7 +156,6 @@ test("errors: error doesn't exist", () => { You can look up the signature \\"0xa3741467\\" here: https://sig.eth.samczsun.com/. Docs: https://viem.sh/docs/contract/decodeErrorResult - Version: viem@1.0.2" `) }) diff --git a/src/utils/abi/decodeErrorResult.ts b/src/utils/abi/decodeErrorResult.ts index d2ae3fde6a..d94b55cb25 100644 --- a/src/utils/abi/decodeErrorResult.ts +++ b/src/utils/abi/decodeErrorResult.ts @@ -1,17 +1,56 @@ -import { Abi } from 'abitype' -import { AbiErrorSignatureNotFoundError } from '../../errors' +import { Abi, AbiError } from 'abitype' +import { + AbiDecodingZeroDataError, + AbiErrorSignatureNotFoundError, +} from '../../errors' import { Hex } from '../../types' import { slice } from '../data' import { getFunctionSignature } from '../hash' import { decodeAbi } from './decodeAbi' import { formatAbiItem } from './formatAbiItem' -export type DecodeErrorResultArgs = { abi: Abi; data: Hex } +const solidityError: AbiError = { + inputs: [ + { + internalType: 'string', + name: 'message', + type: 'string', + }, + ], + name: 'Error', + type: 'error', +} +const solidityPanic: AbiError = { + inputs: [ + { + internalType: 'uint256', + name: 'reason', + type: 'uint256', + }, + ], + name: 'Panic', + type: 'error', +} + +export type DecodeErrorResultArgs = { abi?: Abi; data: Hex } -export function decodeErrorResult({ abi, data }: DecodeErrorResultArgs) { +export type DecodeErrorResultResponse = { + errorName: string + args?: readonly unknown[] +} + +export function decodeErrorResult({ + abi, + data, +}: DecodeErrorResultArgs): DecodeErrorResultResponse { const signature = slice(data, 0, 4) - const description = abi.find( - (x) => signature === getFunctionSignature(formatAbiItem(x)), + if (signature === '0x') throw new AbiDecodingZeroDataError() + + const abi_ = [...(abi || []), solidityError, solidityPanic] + const description = abi_.find( + (x) => + x.type === 'error' && + signature === getFunctionSignature(formatAbiItem(x)), ) if (!description) throw new AbiErrorSignatureNotFoundError(signature, { diff --git a/src/utils/abi/decodeFunctionData.test.ts b/src/utils/abi/decodeFunctionData.test.ts index 41f836f2b6..c13cb0086a 100644 --- a/src/utils/abi/decodeFunctionData.test.ts +++ b/src/utils/abi/decodeFunctionData.test.ts @@ -134,7 +134,6 @@ test("errors: function doesn't exist", () => { You can look up the signature \\"0xa3741467\\" here: https://sig.eth.samczsun.com/. Docs: https://viem.sh/docs/contract/decodeFunctionData - Version: viem@1.0.2" `) }) diff --git a/src/utils/abi/decodeFunctionResult.test.ts b/src/utils/abi/decodeFunctionResult.test.ts index 93864179db..df087e15c6 100644 --- a/src/utils/abi/decodeFunctionResult.test.ts +++ b/src/utils/abi/decodeFunctionResult.test.ts @@ -200,6 +200,79 @@ test('returns (Bar, string)', () => { ]) }) +test('overloads', () => { + expect( + decodeFunctionResult({ + abi: [ + { + inputs: [{ internalType: 'uint256', name: 'x', type: 'uint256' }], + name: 'foo', + outputs: [ + { + internalType: 'uint256', + name: 'x', + type: 'uint256', + }, + ], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'foo', + outputs: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + stateMutability: 'pure', + type: 'function', + }, + ] as const, + functionName: 'foo', + data: '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', + }), + ).toEqual('0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC') + + expect( + decodeFunctionResult({ + abi: [ + { + inputs: [{ internalType: 'uint256', name: 'x', type: 'uint256' }], + name: 'foo', + outputs: [ + { + internalType: 'uint256', + name: 'x', + type: 'uint256', + }, + ], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [], + name: 'foo', + outputs: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + stateMutability: 'pure', + type: 'function', + }, + ] as const, + functionName: 'foo', + data: '0x0000000000000000000000000000000000000000000000000000000000000069', + args: [10n], + }), + ).toEqual(105n) +}) + test("error: function doesn't exist", () => { expect(() => decodeFunctionResult({ @@ -228,7 +301,6 @@ test("error: function doesn't exist", () => { Make sure you are using the correct ABI and that the function exists on it. Docs: https://viem.sh/docs/contract/decodeFunctionResult - Version: viem@1.0.2" `, ) @@ -255,7 +327,6 @@ test("error: function doesn't exist", () => { Make sure you are using the correct ABI and that the function exists on it. Docs: https://viem.sh/docs/contract/decodeFunctionResult - Version: viem@1.0.2" `, ) diff --git a/src/utils/abi/decodeFunctionResult.ts b/src/utils/abi/decodeFunctionResult.ts index 7abc27694c..bd4c9d040d 100644 --- a/src/utils/abi/decodeFunctionResult.ts +++ b/src/utils/abi/decodeFunctionResult.ts @@ -1,15 +1,16 @@ -import { Abi, ExtractAbiFunctionNames } from 'abitype' +import { Abi } from 'abitype' import { AbiFunctionNotFoundError, AbiFunctionOutputsNotFoundError, } from '../../errors' - import { + ExtractArgsFromAbi, ExtractFunctionNameFromAbi, ExtractResultFromAbi, Hex, } from '../../types' import { decodeAbi } from './decodeAbi' +import { getAbiItem, GetAbiItemArgs } from './getAbiItem' const docsPath = '/docs/contract/decodeFunctionResult' @@ -20,7 +21,7 @@ export type DecodeFunctionResultArgs< abi: TAbi functionName: ExtractFunctionNameFromAbi data: Hex -} +} & Partial> export type DecodeFunctionResultResponse< TAbi extends Abi | readonly unknown[] = Abi, @@ -32,15 +33,18 @@ export function decodeFunctionResult< TFunctionName extends string = any, >({ abi, + args, functionName, data, }: DecodeFunctionResultArgs): DecodeFunctionResultResponse< TAbi, TFunctionName > { - const description = (abi as Abi).find( - (x) => 'name' in x && x.name === functionName, - ) + const description = getAbiItem({ + abi, + args, + name: functionName, + } as GetAbiItemArgs) if (!description) throw new AbiFunctionNotFoundError(functionName, { docsPath }) if (!('outputs' in description)) diff --git a/src/utils/abi/encodeAbi.test.ts b/src/utils/abi/encodeAbi.test.ts index d2f2601f00..f9e66c65f0 100644 --- a/src/utils/abi/encodeAbi.test.ts +++ b/src/utils/abi/encodeAbi.test.ts @@ -1390,7 +1390,6 @@ test('invalid type', () => { Please provide a valid ABI type. Docs: https://viem.sh/docs/contract/encodeAbi - Version: viem@1.0.2" `) }) diff --git a/src/utils/abi/encodeDeployData.test.ts b/src/utils/abi/encodeDeployData.test.ts index bd1aa4e2d6..46ae8d9af4 100644 --- a/src/utils/abi/encodeDeployData.test.ts +++ b/src/utils/abi/encodeDeployData.test.ts @@ -74,7 +74,6 @@ test('error: constructor not found', () => { Make sure you are using the correct ABI and that the constructor exists on it. Docs: https://viem.sh/docs/contract/encodeDeployData - Version: viem@1.0.2" `) }) @@ -99,7 +98,6 @@ test('error: no inputs', () => { Make sure you are using the correct ABI, and that the \`inputs\` attribute on the constructor exists. Docs: https://viem.sh/docs/contract/encodeDeployData - Version: viem@1.0.2" `, ) @@ -124,7 +122,6 @@ test('error: no inputs', () => { Make sure you are using the correct ABI, and that the \`inputs\` attribute on the constructor exists. Docs: https://viem.sh/docs/contract/encodeDeployData - Version: viem@1.0.2" `, ) diff --git a/src/utils/abi/encodeErrorResult.test.ts b/src/utils/abi/encodeErrorResult.test.ts index b25743cad0..4e543f21a5 100644 --- a/src/utils/abi/encodeErrorResult.test.ts +++ b/src/utils/abi/encodeErrorResult.test.ts @@ -131,7 +131,6 @@ test("errors: error doesn't exist", () => { 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" `) }) @@ -162,7 +161,6 @@ test('errors: no inputs', () => { 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(() => @@ -191,7 +189,6 @@ test('errors: no inputs', () => { 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 index a9583627bf..8d47309dc8 100644 --- a/src/utils/abi/encodeErrorResult.ts +++ b/src/utils/abi/encodeErrorResult.ts @@ -9,7 +9,7 @@ import { concatHex } from '../data' import { getFunctionSignature } from '../hash' import { encodeAbi } from './encodeAbi' import { formatAbiItem } from './formatAbiItem' -import { getAbiItem } from './getAbiItem' +import { getAbiItem, GetAbiItemArgs } from './getAbiItem' const docsPath = '/docs/contract/encodeErrorResult' @@ -25,7 +25,11 @@ export function encodeErrorResult< TAbi extends Abi = Abi, TErrorName extends ExtractAbiErrorNames = any, >({ abi, errorName, args }: EncodeErrorResultArgs) { - const description = getAbiItem({ abi, name: errorName }) + const description = getAbiItem({ + abi, + args, + name: errorName, + } as GetAbiItemArgs) if (!description) throw new AbiErrorNotFoundError(errorName, { docsPath }) const definition = formatAbiItem(description) const signature = getFunctionSignature(definition) diff --git a/src/utils/abi/encodeEventTopics.test.ts b/src/utils/abi/encodeEventTopics.test.ts index 2df717306e..78714c7d05 100644 --- a/src/utils/abi/encodeEventTopics.test.ts +++ b/src/utils/abi/encodeEventTopics.test.ts @@ -320,7 +320,6 @@ test("errors: event doesn't exist", () => { Make sure you are using the correct ABI and that the event exists on it. Docs: https://viem.sh/docs/contract/encodeEventTopics - Version: viem@1.0.2" `) }) diff --git a/src/utils/abi/encodeEventTopics.ts b/src/utils/abi/encodeEventTopics.ts index 46e437a1ad..6c630bf718 100644 --- a/src/utils/abi/encodeEventTopics.ts +++ b/src/utils/abi/encodeEventTopics.ts @@ -14,7 +14,7 @@ import { encodeBytes } from '../encoding' import { keccak256, getEventSignature } from '../hash' import { encodeAbi } from './encodeAbi' import { formatAbiItem } from './formatAbiItem' -import { getAbiItem } from './getAbiItem' +import { getAbiItem, GetAbiItemArgs } from './getAbiItem' export type EncodeEventTopicsArgs< TAbi extends Abi = Abi, @@ -28,7 +28,7 @@ export function encodeEventTopics< TAbi extends Abi = Abi, TEventName extends ExtractAbiEventNames = any, >({ abi, eventName, args }: EncodeEventTopicsArgs) { - const abiItem = getAbiItem({ abi, name: eventName }) + const abiItem = getAbiItem({ abi, args, name: eventName } as GetAbiItemArgs) if (!abiItem) throw new AbiEventNotFoundError(eventName, { docsPath: '/docs/contract/encodeEventTopics', diff --git a/src/utils/abi/encodeFunctionData.test.ts b/src/utils/abi/encodeFunctionData.test.ts index 0130073376..a859cdaef9 100644 --- a/src/utils/abi/encodeFunctionData.test.ts +++ b/src/utils/abi/encodeFunctionData.test.ts @@ -134,7 +134,6 @@ test("errors: function doesn't exist", () => { Make sure you are using the correct ABI and that the function exists on it. Docs: https://viem.sh/docs/contract/encodeFunctionData - Version: viem@1.0.2" `) }) diff --git a/src/utils/abi/encodeFunctionData.ts b/src/utils/abi/encodeFunctionData.ts index df4f0d628f..878b289c56 100644 --- a/src/utils/abi/encodeFunctionData.ts +++ b/src/utils/abi/encodeFunctionData.ts @@ -1,12 +1,15 @@ import { Abi, ExtractAbiFunctionNames } from 'abitype' -import { AbiFunctionNotFoundError } from '../../errors' +import { + AbiEncodingLengthMismatchError, + AbiFunctionNotFoundError, +} from '../../errors' import { ExtractArgsFromAbi, ExtractFunctionNameFromAbi } from '../../types' import { concatHex } from '../data' import { getFunctionSignature } from '../hash' import { encodeAbi } from './encodeAbi' import { formatAbiItem } from './formatAbiItem' -import { getAbiItem } from './getAbiItem' +import { getAbiItem, GetAbiItemArgs } from './getAbiItem' export type EncodeFunctionDataArgs< TAbi extends Abi = Abi, @@ -20,7 +23,11 @@ export function encodeFunctionData< TAbi extends Abi = Abi, TFunctionName extends string = any, >({ abi, args, functionName }: EncodeFunctionDataArgs) { - const description = getAbiItem({ abi, name: functionName }) + const description = getAbiItem({ + abi, + args, + name: functionName, + } as GetAbiItemArgs) if (!description) throw new AbiFunctionNotFoundError(functionName, { docsPath: '/docs/contract/encodeFunctionData', diff --git a/src/utils/abi/encodeFunctionResult.test.ts b/src/utils/abi/encodeFunctionResult.test.ts index b38fd097c8..c876b7d7bb 100644 --- a/src/utils/abi/encodeFunctionResult.test.ts +++ b/src/utils/abi/encodeFunctionResult.test.ts @@ -249,7 +249,6 @@ test("error: function doesn't exist", () => { Make sure you are using the correct ABI and that the function exists on it. Docs: https://viem.sh/docs/contract/encodeFunctionResult - Version: viem@1.0.2" `, ) @@ -277,7 +276,6 @@ test("error: function doesn't exist", () => { Make sure you are using the correct ABI and that the function exists on it. Docs: https://viem.sh/docs/contract/encodeFunctionResult - Version: viem@1.0.2" `, ) diff --git a/src/utils/abi/getAbiItem.test.ts b/src/utils/abi/getAbiItem.test.ts new file mode 100644 index 0000000000..8d59cce4a0 --- /dev/null +++ b/src/utils/abi/getAbiItem.test.ts @@ -0,0 +1,549 @@ +import { AbiParameter } from 'abitype' +import { describe, expect, test } from 'vitest' +import { wagmiContractConfig } from '../../_test' +import { encodeBytes } from '../encoding' +import { getAbiItem, isArgOfType } from './getAbiItem' + +test('default', () => { + expect( + getAbiItem({ + abi: wagmiContractConfig.abi, + name: 'balanceOf', + args: ['0x0000000000000000000000000000000000000000'], + }), + ).toMatchInlineSnapshot(` + { + "inputs": [ + { + "name": "owner", + "type": "address", + }, + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + } + `) +}) + +test('no matching name', () => { + expect( + getAbiItem({ + abi: [], + // @ts-expect-error + name: 'balanceOf', + args: ['0x0000000000000000000000000000000000000000'], + }), + ).toMatchInlineSnapshot('undefined') +}) + +test('overloads: no inputs', () => { + expect( + getAbiItem({ + abi: [ + // @ts-expect-error + { + name: 'balanceOf', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ name: 'x', type: 'uint256' }], + name: 'balanceOf', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + ], + name: 'balanceOf', + args: ['0x0000000000000000000000000000000000000000'], + }), + ).toMatchInlineSnapshot(` + { + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + } + `) +}) + +test('overloads: undefined inputs', () => { + expect( + getAbiItem({ + abi: [ + { + // @ts-expect-error + inputs: undefined, + name: 'balanceOf', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'balanceOf', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + ], + name: 'balanceOf', + args: ['0x0000000000000000000000000000000000000000'], + }), + ).toMatchInlineSnapshot(` + { + "inputs": undefined, + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + } + `) +}) + +test('overloads: no args', () => { + expect( + getAbiItem({ + abi: [ + { + inputs: [{ name: '', type: 'uint256' }], + name: 'balanceOf', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'balanceOf', + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + ], + name: 'balanceOf', + }), + ).toMatchInlineSnapshot(` + { + "inputs": [], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + } + `) +}) + +test('overload: different lengths', () => { + expect( + getAbiItem({ + abi: wagmiContractConfig.abi, + name: 'safeTransferFrom', + args: [ + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + 420n, + ], + }), + ).toMatchInlineSnapshot(` + { + "inputs": [ + { + "name": "from", + "type": "address", + }, + { + "name": "to", + "type": "address", + }, + { + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + } + `) + + expect( + getAbiItem({ + abi: wagmiContractConfig.abi, + name: 'safeTransferFrom', + args: [ + '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', + 420n, + '0x0000000000000000000000000000000000000000', + ], + }), + ).toMatchInlineSnapshot(` + { + "inputs": [ + { + "name": "from", + "type": "address", + }, + { + "name": "to", + "type": "address", + }, + { + "name": "tokenId", + "type": "uint256", + }, + { + "name": "_data", + "type": "bytes", + }, + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + } + `) +}) + +test('overload: different types', () => { + const abi = [ + { + inputs: [], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ name: 'tokenId', type: 'uint256' }], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ name: 'tokenId', type: 'string' }], + name: 'mint', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + ] as const + + expect( + getAbiItem({ + abi, + name: 'mint', + }), + ).toMatchInlineSnapshot(` + { + "inputs": [], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + } + `) + + expect( + getAbiItem({ + abi, + name: 'mint', + args: [420n], + }), + ).toMatchInlineSnapshot(` + { + "inputs": [ + { + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + } + `) + + expect( + getAbiItem({ + abi, + name: 'mint', + args: ['foo'], + }), + ).toMatchInlineSnapshot(` + { + "inputs": [ + { + "name": "tokenId", + "type": "string", + }, + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + } + `) +}) + +test('overloads: tuple', () => { + expect( + getAbiItem({ + abi: [ + { + inputs: [ + { name: 'foo', type: 'uint256' }, + { + name: 'bar', + type: 'tuple', + components: [ + { name: 'a', type: 'string' }, + { + name: 'b', + type: 'tuple', + components: [ + { name: 'merp', type: 'string' }, + { name: 'meep', type: 'string' }, + ], + }, + { name: 'c', type: 'uint256' }, + ], + }, + ], + name: 'foo', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { name: 'foo', type: 'uint256' }, + { + name: 'bar', + type: 'tuple', + components: [ + { name: 'a', type: 'string' }, + { + name: 'b', + type: 'tuple', + components: [ + { name: 'merp', type: 'string' }, + { name: 'meep', type: 'string' }, + ], + }, + { name: 'c', type: 'address' }, + ], + }, + ], + name: 'foo', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + ] as const, + name: 'foo', + args: [ + 420n, + { + a: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + b: { merp: 'test', meep: 'test' }, + c: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + }, + ], + }), + ).toMatchInlineSnapshot(` + { + "inputs": [ + { + "name": "foo", + "type": "uint256", + }, + { + "components": [ + { + "name": "a", + "type": "string", + }, + { + "components": [ + { + "name": "merp", + "type": "string", + }, + { + "name": "meep", + "type": "string", + }, + ], + "name": "b", + "type": "tuple", + }, + { + "name": "c", + "type": "address", + }, + ], + "name": "bar", + "type": "tuple", + }, + ], + "name": "foo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + } + `) +}) + +describe.each([ + // array + { arg: ['foo'], abiParameter: { type: 'string[]' }, expected: true }, + { arg: ['foo'], abiParameter: { type: 'string[1]' }, expected: true }, + { arg: [['foo']], abiParameter: { type: 'string[][]' }, expected: true }, + { arg: [['foo']], abiParameter: { type: 'string[][1]' }, expected: true }, + { + arg: [1n], + abiParameter: { type: 'uint256[]' }, + expected: true, + }, + { + arg: [{ foo: 1n, bar: [{ baz: 1n }] }], + abiParameter: { + type: 'tuple[]', + components: [ + { name: 'foo', type: 'uint256' }, + { + name: 'bar', + type: 'tuple[]', + components: [{ name: 'baz', type: 'uint256' }], + }, + ], + }, + expected: true, + }, + { arg: ['foo'], abiParameter: { type: 'string[test]' }, expected: false }, + { arg: [1], abiParameter: { type: 'uint69[]' }, expected: false }, + // address + { + arg: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + abiParameter: { type: 'address' }, + expected: true, + }, + { + arg: 'A0Cf798816D4b9b9866b5330EEa46a18382f251e', + abiParameter: { type: 'address' }, + expected: true, + }, + { arg: 'test', abiParameter: { type: 'address' }, expected: false }, + // bool + { arg: true, abiParameter: { type: 'bool' }, expected: true }, + { arg: false, abiParameter: { type: 'bool' }, expected: true }, + { arg: 'true', abiParameter: { type: 'bool' }, expected: false }, + // bytes + { arg: 'foo', abiParameter: { type: 'bytes' }, expected: true }, + { arg: 'foo', abiParameter: { type: 'bytes32' }, expected: true }, + { arg: encodeBytes('foo'), abiParameter: { type: 'bytes' }, expected: true }, + { arg: 1, abiParameter: { type: 'bytes32' }, expected: false }, + // function + { arg: 'foo', abiParameter: { type: 'function' }, expected: true }, + { arg: 1, abiParameter: { type: 'function' }, expected: false }, + // int + { arg: 1, abiParameter: { type: 'int' }, expected: true }, + { arg: 1n, abiParameter: { type: 'int' }, expected: true }, + { arg: 1n, abiParameter: { type: 'int' }, expected: true }, + { arg: 1, abiParameter: { type: 'uint' }, expected: true }, + { arg: 1n, abiParameter: { type: 'uint' }, expected: true }, + { arg: 1n, abiParameter: { type: 'uint' }, expected: true }, + { arg: 1, abiParameter: { type: 'int256' }, expected: true }, + { arg: 1, abiParameter: { type: 'uint256' }, expected: true }, + { arg: 1, abiParameter: { type: 'int69' }, expected: false }, + { arg: 1, abiParameter: { type: 'uint69' }, expected: false }, + // string + { arg: 'foo', abiParameter: { type: 'string' }, expected: true }, + { arg: 1, abiParameter: { type: 'string' }, expected: false }, + // tuple + { + arg: { bar: 1, baz: 'test' }, + abiParameter: { + name: 'foo', + type: 'tuple', + components: [ + { name: 'bar', type: 'uint256' }, + { name: 'baz', type: 'string' }, + ], + }, + expected: true, + }, + { + arg: [1, 'test'], + abiParameter: { + name: 'foo', + type: 'tuple', + components: [ + { name: 'bar', type: 'uint256' }, + { name: 'baz', type: 'string' }, + ], + }, + expected: true, + }, + { + arg: { bar: ['test'] }, + abiParameter: { + name: 'foo', + type: 'tuple', + components: [ + { + name: 'bar', + type: 'tuple', + components: [{ name: 'baz', type: 'string' }], + }, + ], + }, + expected: true, + }, + { + arg: {}, + abiParameter: { + name: 'foo', + type: 'tuple', + components: [ + { name: 'bar', type: 'uint256' }, + { name: 'baz', type: 'uint256' }, + ], + }, + expected: false, + }, +] as { arg: unknown; abiParameter: AbiParameter; expected: boolean }[])( + 'isArgOfType($arg, $abiParameter)', + ({ arg, abiParameter, expected }) => { + test(`isArgOfType: returns ${expected}`, () => { + expect(isArgOfType(arg, abiParameter)).toEqual(expected) + }) + }, +) diff --git a/src/utils/abi/getAbiItem.ts b/src/utils/abi/getAbiItem.ts index 8c41662734..fcfa52b621 100644 --- a/src/utils/abi/getAbiItem.ts +++ b/src/utils/abi/getAbiItem.ts @@ -1,5 +1,95 @@ -import { Abi } from 'abitype' +import type { Abi, AbiParameter, Address } from 'abitype' +import type { ExtractArgsFromAbi, ExtractNameFromAbi } from '../../types' +import { isAddress } from '../address' -export function getAbiItem({ abi, name }: { abi: Abi; name: string }) { - return abi.find((x) => 'name' in x && x.name === name) +export type GetAbiItemArgs< + TAbi extends Abi = Abi, + TFunctionName extends string = any, +> = { + abi: TAbi + name: ExtractNameFromAbi +} & Partial> + +export function getAbiItem< + TAbi extends Abi = Abi, + TFunctionName extends string = any, +>({ abi, args = [], name }: GetAbiItemArgs) { + const abiItems = abi.filter((x) => 'name' in x && x.name === name) + + if (abiItems.length === 0) return undefined + if (abiItems.length === 1) return abiItems[0] + + for (const abiItem of abiItems) { + if (!('inputs' in abiItem)) continue + if (!args || args.length === 0) { + if (!abiItem.inputs || abiItem.inputs.length === 0) return abiItem + continue + } + if (!abiItem.inputs) continue + if (abiItem.inputs.length === 0) continue + const matched = (args as readonly unknown[]).every((arg, index) => { + const abiParameter = 'inputs' in abiItem && abiItem.inputs![index] + if (!abiParameter) return false + return isArgOfType(arg, abiParameter as AbiParameter) + }) + if (matched) return abiItem + } + return abiItems[0] +} + +export function isArgOfType(arg: unknown, abiParameter: AbiParameter): boolean { + const argType = typeof arg + const abiParameterType = abiParameter.type + switch (abiParameterType) { + case 'address': + return isAddress(arg as Address) + case 'bool': + return argType === 'boolean' + case 'function': + return argType === 'string' + case 'string': + return argType === 'string' + default: { + if (abiParameterType === 'tuple' && 'components' in abiParameter) + return Object.values(abiParameter.components).every( + (component, index) => { + return isArgOfType( + Object.values(arg as unknown[] | Record)[index], + component as AbiParameter, + ) + }, + ) + + // `(u)int`: (un)signed integer type of `M` bits, `0 < M <= 256`, `M % 8 == 0` + // https://regexr.com/6v8hp + if ( + /^u?int(8|16|24|32|40|48|56|64|72|80|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256)?$/.test( + abiParameterType, + ) + ) + return argType === 'number' || argType === 'bigint' + + // `bytes`: binary type of `M` bytes, `0 < M <= 32` + // https://regexr.com/6va55 + if (/^bytes([1-9]|1[0-9]|2[0-9]|3[0-2])?$/.test(abiParameterType)) + return argType === 'string' || arg instanceof Uint8Array + + // fixed-length (`[M]`) and dynamic (`[]`) arrays + // https://regexr.com/6va6i + if (/[a-z]+[1-9]{0,3}(\[[0-9]{0,}\])+$/.test(abiParameterType)) { + return ( + Array.isArray(arg) && + arg.every((x: unknown) => + isArgOfType(x, { + ...abiParameter, + // Pop off `[]` or `[M]` from end of type + type: abiParameterType.replace(/(\[[0-9]{0,}\])$/, ''), + } as AbiParameter), + ) + ) + } + + return false + } + } } diff --git a/src/utils/abi/index.ts b/src/utils/abi/index.ts index b179934499..ec5f5b4e29 100644 --- a/src/utils/abi/index.ts +++ b/src/utils/abi/index.ts @@ -1,7 +1,10 @@ export type { DecodeAbiArgs } from './decodeAbi' export { decodeAbi } from './decodeAbi' -export type { DecodeErrorResultArgs } from './decodeErrorResult' +export type { + DecodeErrorResultArgs, + DecodeErrorResultResponse, +} from './decodeErrorResult' export { decodeErrorResult } from './decodeErrorResult' export type { DecodeFunctionDataArgs } from './decodeFunctionData' @@ -35,4 +38,5 @@ export { formatAbiItemWithArgs } from './formatAbiItemWithArgs' export { formatAbiItem } from './formatAbiItem' +export type { GetAbiItemArgs } from './getAbiItem' export { getAbiItem } from './getAbiItem' diff --git a/src/utils/address/getAddress.test.ts b/src/utils/address/getAddress.test.ts index dfc16bc247..0164a007c2 100644 --- a/src/utils/address/getAddress.test.ts +++ b/src/utils/address/getAddress.test.ts @@ -31,16 +31,16 @@ describe('errors', () => { expect(() => getAddress('0xa5cc3c03994db5b0d9a5eEdD10Cabab0813678az'), ).toThrowErrorMatchingInlineSnapshot(` - "Address \\"0xa5cc3c03994db5b0d9a5eEdD10Cabab0813678az\\" is invalid. + "Address \\"0xa5cc3c03994db5b0d9a5eEdD10Cabab0813678az\\" is invalid. - Version: viem@1.0.2" - `) + Version: viem@1.0.2" + `) expect(() => getAddress('0xa5cc3c03994db5b0d9a5eEdD10Cabab0813678aff'), ).toThrowErrorMatchingInlineSnapshot(` - "Address \\"0xa5cc3c03994db5b0d9a5eEdD10Cabab0813678aff\\" is invalid. + "Address \\"0xa5cc3c03994db5b0d9a5eEdD10Cabab0813678aff\\" is invalid. - Version: viem@1.0.2" - `) + Version: viem@1.0.2" + `) }) }) diff --git a/src/utils/contract/getContractError.test.ts b/src/utils/contract/getContractError.test.ts index b952bdd130..06717ba1c0 100644 --- a/src/utils/contract/getContractError.test.ts +++ b/src/utils/contract/getContractError.test.ts @@ -2,96 +2,281 @@ import { describe, expect, test } from 'vitest' import { accounts } from '../../_test' import { baycContractConfig } from '../../_test/abis' -import { BaseError } from '../../errors' +import { + AbiDecodingZeroDataError, + BaseError, + RawContractError, +} from '../../errors' import { getContractError } from './getContractError' describe('getContractError', () => { test('default', () => { - expect( - getContractError( - new BaseError('An RPC error occurred', { - cause: { - code: 3, - message: 'execution reverted: Sale must be active to mint Ape', - data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001f53616c65206d7573742062652061637469766520746f206d696e742041706500', - } as unknown as Error, - }), - { - abi: baycContractConfig.abi, - functionName: 'mintApe', - args: [1n], - sender: accounts[0].address, - }, - ), - ).toMatchInlineSnapshot(` - [ContractMethodExecutionError: Sale must be active to mint Ape - + const error = getContractError( + new RawContractError({ + message: 'execution reverted: Sale must be active to mint Ape', + data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001f53616c65206d7573742062652061637469766520746f206d696e742041706500', + }), + { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }, + ) + expect(error).toMatchInlineSnapshot(` + [ContractFunctionExecutionError: The contract function "mintApe" reverted with the following reason: + Sale must be active to mint Ape + + Function: mintApe(uint256 numberOfTokens) + Arguments: (1) Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + + Version: viem@1.0.2] + `) + expect(error.cause).toMatchInlineSnapshot(` + [ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason: + Sale must be active to mint Ape + + Version: viem@1.0.2] + `) + }) + + test('default: rpc', () => { + const error = getContractError( + new BaseError('An RPC error occurred', { + cause: { + code: 3, + message: 'execution reverted: Sale must be active to mint Ape', + data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001f53616c65206d7573742062652061637469766520746f206d696e742041706500', + } as unknown as Error, + }), + { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }, + ) + expect(error).toMatchInlineSnapshot(` + [ContractFunctionExecutionError: The contract function "mintApe" reverted with the following reason: + Sale must be active to mint Ape + Function: mintApe(uint256 numberOfTokens) Arguments: (1) + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + + Version: viem@1.0.2] + `) + 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@1.0.2] `) }) - test('default', () => { - expect( - getContractError( - new BaseError('An RPC error occurred', { - cause: { - code: 3, - message: 'execution reverted: Sale must be active to mint Ape', - data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001f53616c65206d7573742062652061637469766520746f206d696e742041706500', - } as unknown as Error, - }), - { - abi: baycContractConfig.abi, - functionName: 'foo', - args: [1n], - sender: accounts[0].address, - }, - ), - ).toMatchInlineSnapshot(` - [ContractMethodExecutionError: Sale must be active to mint Ape - + test('no data', () => { + const error = getContractError( + new BaseError('An RPC error occurred', { + cause: { + code: 3, + message: 'ah no', + } as unknown as Error, + }), + { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }, + ) + expect(error).toMatchInlineSnapshot(` + [ContractFunctionExecutionError: The contract function "mintApe" reverted with the following reason: + ah no + + Function: mintApe(uint256 numberOfTokens) + Arguments: (1) Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 - Details: execution reverted: Sale must be active to mint Ape + Version: viem@1.0.2] + `) + expect(error.cause).toMatchInlineSnapshot(` + [ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason: + ah no + Version: viem@1.0.2] `) }) - test('unknown error', () => { - expect( - getContractError( - new BaseError('An RPC error occurred', { - cause: new Error('rarararar i am an error lmaoaoo'), - }), - { - abi: baycContractConfig.abi, - functionName: 'foo', - args: [1n], - sender: accounts[0].address, - }, - ), - ).toMatchInlineSnapshot(` - [ViemError: An RPC error occurred + test('no message', () => { + const error = getContractError( + new BaseError('An RPC error occurred', { + cause: { + code: 3, + data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001f53616c65206d7573742062652061637469766520746f206d696e742041706500', + } as unknown as Error, + }), + { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }, + ) + expect(error).toMatchInlineSnapshot(` + [ContractFunctionExecutionError: The contract function "mintApe" reverted with the following reason: + Sale must be active to mint Ape + + Function: mintApe(uint256 numberOfTokens) + Arguments: (1) + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + + Version: viem@1.0.2] + `) + expect(error.cause).toMatchInlineSnapshot(` + [ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason: + Sale must be active to mint Ape - Details: rarararar i am an error lmaoaoo Version: viem@1.0.2] `) - expect( - getContractError(new BaseError('An RPC error occurred'), { + }) + + test('unknown function', () => { + const error = getContractError( + new BaseError('An RPC error occurred', { + cause: { + code: 3, + message: 'execution reverted: Sale must be active to mint Ape', + data: '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001f53616c65206d7573742062652061637469766520746f206d696e742041706500', + } as unknown as Error, + }), + { abi: baycContractConfig.abi, - functionName: 'foo', + functionName: 'mintApe', args: [1n], sender: accounts[0].address, + }, + ) + expect(error).toMatchInlineSnapshot(` + [ContractFunctionExecutionError: The contract function "mintApe" reverted with the following reason: + Sale must be active to mint Ape + + Function: mintApe(uint256 numberOfTokens) + Arguments: (1) + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + + Version: viem@1.0.2] + `) + expect(error.cause).toMatchInlineSnapshot(` + [ContractFunctionRevertedError: The contract function "mintApe" reverted with the following reason: + Sale must be active to mint Ape + + Version: viem@1.0.2] + `) + }) + + test('unknown error', () => { + const error = getContractError( + new BaseError('An RPC error occurred', { + cause: new Error('rarararar i am an error lmaoaoo'), }), - ).toMatchInlineSnapshot(` + { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }, + ) + expect(error).toMatchInlineSnapshot(` + [ContractFunctionExecutionError: An RPC error occurred + + Function: mintApe(uint256 numberOfTokens) + Arguments: (1) + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + + Details: rarararar i am an error lmaoaoo + Version: viem@1.0.2] + `) + expect(error.cause).toMatchInlineSnapshot(` + [ViemError: An RPC error occurred + + Details: rarararar i am an error lmaoaoo + Version: viem@1.0.2] + `) + + const error2 = getContractError(new BaseError('An RPC error occurred'), { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }) + expect(error2).toMatchInlineSnapshot(` + [ContractFunctionExecutionError: An RPC error occurred + + Function: mintApe(uint256 numberOfTokens) + Arguments: (1) + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + + Version: viem@1.0.2] + `) + expect(error2.cause).toMatchInlineSnapshot(` [ViemError: An RPC error occurred Version: viem@1.0.2] `) + + const error3 = getContractError(new BaseError(''), { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }) + expect(error3).toMatchInlineSnapshot(` + [ContractFunctionExecutionError: An unknown error occurred while executing the contract function "mintApe". + + Function: mintApe(uint256 numberOfTokens) + Arguments: (1) + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + + Version: viem@1.0.2] + `) + expect(error3.cause).toMatchInlineSnapshot(` + [ViemError: An error occurred. + + Version: viem@1.0.2] + `) + }) + + test('zero data', () => { + const error = getContractError(new AbiDecodingZeroDataError(), { + abi: baycContractConfig.abi, + functionName: 'mintApe', + args: [1n], + sender: accounts[0].address, + }) + expect(error).toMatchInlineSnapshot(` + [ContractFunctionExecutionError: The contract function "mintApe" returned no data ("0x"). + + This could be due to any of the following: + - The contract does not have the function "mintApe", + - The parameters passed to the contract function may be invalid, or + - The address is not a contract. + + Function: mintApe(uint256 numberOfTokens) + Arguments: (1) + Sender: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 + + Version: viem@1.0.2] + `) + expect(error.cause).toMatchInlineSnapshot(` + [ContractFunctionZeroDataError: The contract function "mintApe" returned no data ("0x"). + + This could be due to any of the following: + - The contract does not have the function "mintApe", + - The parameters passed to the contract function may be invalid, or + - The address is not a contract. + + Version: viem@1.0.2] + `) }) }) diff --git a/src/utils/contract/getContractError.ts b/src/utils/contract/getContractError.ts index d05038035c..58b749ecc2 100644 --- a/src/utils/contract/getContractError.ts +++ b/src/utils/contract/getContractError.ts @@ -1,32 +1,42 @@ import { Abi } from 'abitype' import { AbiDecodingZeroDataError, - ContractMethodExecutionError, - ContractMethodZeroDataError, + BaseError, + ContractFunctionExecutionError, + RawContractError, } from '../../errors' +import { + ContractFunctionRevertedError, + ContractFunctionZeroDataError, +} from '../../errors/contract' import { Address } from '../../types' import { formatAbiItemWithArgs, formatAbiItem, getAbiItem } from '../abi' +const EXECUTION_REVERTED_ERROR_CODE = 3 + export function getContractError( - err: unknown, + err: BaseError, { abi, address, args, + docsPath, functionName, sender, }: { abi: Abi args: any address?: Address + docsPath?: string functionName: string sender?: Address }, ) { - const { code, message } = - ((err as Error).cause as { code?: number; message?: string }) || {} + const { code, data, message } = ( + err instanceof RawContractError ? err : err.cause || {} + ) as RawContractError - const abiItem = getAbiItem({ abi, name: functionName }) + const abiItem = getAbiItem({ abi, args, name: functionName }) const formattedArgs = abiItem ? formatAbiItemWithArgs({ abiItem, @@ -39,28 +49,26 @@ export function getContractError( ? formatAbiItem(abiItem, { includeName: true }) : undefined - if (err instanceof AbiDecodingZeroDataError) { - return new ContractMethodZeroDataError({ - abi, - args, - cause: err as Error, - contractAddress: address, - functionName, - functionWithParams, - }) - } - if (code === 3 || message?.includes('execution reverted')) { - const message_ = message?.replace('execution reverted: ', '') - return new ContractMethodExecutionError(message_, { + let cause = err + if (err instanceof AbiDecodingZeroDataError || data === '0x') { + cause = new ContractFunctionZeroDataError({ functionName }) + } else if (code === EXECUTION_REVERTED_ERROR_CODE && (data || message)) { + cause = new ContractFunctionRevertedError({ abi, - args, - cause: err as Error, - contractAddress: address, - formattedArgs, + data, functionName, - functionWithParams, - sender, + message, }) } - return err + + return new ContractFunctionExecutionError(cause, { + abi, + args, + contractAddress: address, + docsPath, + formattedArgs, + functionName, + functionWithParams, + sender, + }) } diff --git a/src/utils/index.ts b/src/utils/index.ts index 31645bdd09..d684595262 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,7 @@ export type { DecodeAbiArgs, DecodeErrorResultArgs, + DecodeErrorResultResponse, DecodeFunctionDataArgs, DecodeFunctionResultArgs, DecodeFunctionResultResponse, @@ -10,6 +11,7 @@ export type { EncodeEventTopicsArgs, EncodeFunctionDataArgs, EncodeFunctionResultArgs, + GetAbiItemArgs, } from './abi' export { decodeAbi, diff --git a/src/utils/rpc.test.ts b/src/utils/rpc.test.ts index 9b85710f6d..d283db4173 100644 --- a/src/utils/rpc.test.ts +++ b/src/utils/rpc.test.ts @@ -70,8 +70,7 @@ describe('http', () => { Request body: {\\"method\\":\\"eth_wagmi\\"} Details: Method not found - Version: viem@1.0.2 - Internal Error: {\\"code\\":-32601,\\"message\\":\\"Method not found\\"}" + Version: viem@1.0.2" `) }) @@ -706,8 +705,7 @@ describe('webSocket (subscription)', () => { Request body: {"method":"eth_subscribe","params":["fakeHeadz"]} Details: data did not match any variant of untagged enum EthRpcCall - Version: viem@1.0.2 - Internal Error: {"code":-32602,"message":"data did not match any variant of untagged enum EthRpcCall"}] + Version: viem@1.0.2] `) }) }) @@ -961,8 +959,7 @@ describe('webSocketAsync', () => { Request body: {\\"method\\":\\"wagmi_lol\\"} Details: data did not match any variant of untagged enum EthRpcCall - Version: viem@1.0.2 - Internal Error: {\\"code\\":-32602,\\"message\\":\\"data did not match any variant of untagged enum EthRpcCall\\"}" + Version: viem@1.0.2" `, ) })