diff --git a/.changeset/empty-poets-destroy.md b/.changeset/empty-poets-destroy.md new file mode 100644 index 0000000000..3757c4eceb --- /dev/null +++ b/.changeset/empty-poets-destroy.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added `readContract` diff --git a/site/.vitepress/sidebar.ts b/site/.vitepress/sidebar.ts index 866ff8b19c..868176c336 100644 --- a/site/.vitepress/sidebar.ts +++ b/site/.vitepress/sidebar.ts @@ -413,7 +413,7 @@ export const sidebar: DefaultTheme.Sidebar = { link: '/docs/contract/multicall', }, { - text: 'readContract 🚧', + text: 'readContract', link: '/docs/contract/readContract', }, { diff --git a/src/actions/index.test.ts b/src/actions/index.test.ts index 64035e8187..21760f2205 100644 --- a/src/actions/index.test.ts +++ b/src/actions/index.test.ts @@ -37,6 +37,7 @@ test('exports actions', () => { "increaseTime": [Function], "inspectTxpool": [Function], "mine": [Function], + "readContract": [Function], "removeBlockTimestampInterval": [Function], "requestAccounts": [Function], "requestPermissions": [Function], diff --git a/src/actions/index.ts b/src/actions/index.ts index 12fd4863b3..dc75c159a6 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -20,6 +20,7 @@ export { getTransactionConfirmations, getTransactionCount, getTransactionReceipt, + readContract, simulateContract, uninstallFilter, waitForTransactionReceipt, @@ -69,6 +70,8 @@ export type { OnBlockResponse, OnTransactions, OnTransactionsResponse, + ReadContractArgs, + ReadContractResponse, ReplacementReason, ReplacementResponse, SimulateContractArgs, diff --git a/src/actions/public/index.test.ts b/src/actions/public/index.test.ts index 27fe23aab3..abb7a6a715 100644 --- a/src/actions/public/index.test.ts +++ b/src/actions/public/index.test.ts @@ -26,6 +26,7 @@ test('exports actions', () => { "getTransactionConfirmations": [Function], "getTransactionCount": [Function], "getTransactionReceipt": [Function], + "readContract": [Function], "simulateContract": [Function], "uninstallFilter": [Function], "waitForTransactionReceipt": [Function], diff --git a/src/actions/public/index.ts b/src/actions/public/index.ts index 974de442de..6a22f01536 100644 --- a/src/actions/public/index.ts +++ b/src/actions/public/index.ts @@ -5,7 +5,6 @@ export { simulateContract } from './simulateContract' export type { SimulateContractArgs, SimulateContractResponse, - FormattedSimulateContract, } from './simulateContract' export { createPendingTransactionFilter } from './createPendingTransactionFilter' @@ -91,6 +90,12 @@ export type { GetTransactionReceiptResponse, } from './getTransactionReceipt' +export { readContract } from './readContract' +export type { + ReadContractArgs, + ReadContractResponse, +} from './readContract' + export { uninstallFilter } from './uninstallFilter' export type { UninstallFilterArgs, diff --git a/src/actions/public/readContract.test.ts b/src/actions/public/readContract.test.ts new file mode 100644 index 0000000000..5c5e00fd38 --- /dev/null +++ b/src/actions/public/readContract.test.ts @@ -0,0 +1,128 @@ +/** + * TODO: Heaps more test cases :D + * - Complex calldata types + * - Complex return types (tuple/structs) + * - Calls against blocks + */ + +import { describe, expect, test } from 'vitest' +import { + accounts, + publicClient, + testClient, + 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' +import { readContract } from './readContract' + +describe('wagmi', () => { + test('default', async () => { + expect( + await readContract(publicClient, { + ...wagmiContractConfig, + functionName: 'balanceOf', + args: ['0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC'], + }), + ).toEqual(3n) + expect( + await readContract(publicClient, { + ...wagmiContractConfig, + functionName: 'getApproved', + args: [420n], + }), + ).toEqual('0x0000000000000000000000000000000000000000') + expect( + await readContract(publicClient, { + ...wagmiContractConfig, + functionName: 'isApprovedForAll', + args: [ + '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + '0x0000000000000000000000000000000000000000', + ], + }), + ).toEqual(false) + expect( + await readContract(publicClient, { + ...wagmiContractConfig, + functionName: 'name', + }), + ).toEqual('wagmi') + expect( + await readContract(publicClient, { + ...wagmiContractConfig, + functionName: 'ownerOf', + args: [420n], + }), + ).toEqual('0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC') + expect( + await readContract(publicClient, { + ...wagmiContractConfig, + functionName: 'supportsInterface', + args: ['0x1a452251'], + }), + ).toEqual(false) + expect( + await readContract(publicClient, { + ...wagmiContractConfig, + functionName: 'symbol', + }), + ).toEqual('WAGMI') + expect( + await readContract(publicClient, { + ...wagmiContractConfig, + functionName: 'tokenURI', + args: [420n], + }), + ).toMatchInlineSnapshot( + '"data:application/json;base64,eyJuYW1lIjogIndhZ21pICM0MjAiLCAiaW1hZ2UiOiAiZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCNGJXeHVjejBpYUhSMGNEb3ZMM2QzZHk1M015NXZjbWN2TWpBd01DOXpkbWNpSUhkcFpIUm9QU0l4TURJMElpQm9aV2xuYUhROUlqRXdNalFpSUdacGJHdzlJbTV2Ym1VaVBqeHdZWFJvSUdacGJHdzlJbWh6YkNneE1UY3NJREV3TUNVc0lERXdKU2tpSUdROUlrMHdJREJvTVRBeU5IWXhNREkwU0RCNklpQXZQanhuSUdacGJHdzlJbWh6YkNneU9EZ3NJREV3TUNVc0lEa3dKU2tpUGp4d1lYUm9JR1E5SWswNU1ETWdORE0zTGpWak1DQTVMakV4TXkwM0xqTTRPQ0F4Tmk0MUxURTJMalVnTVRZdU5YTXRNVFl1TlMwM0xqTTROeTB4Tmk0MUxURTJMalVnTnk0ek9EZ3RNVFl1TlNBeE5pNDFMVEUyTGpVZ01UWXVOU0EzTGpNNE55QXhOaTQxSURFMkxqVjZUVFk1T0M0MU1qa2dOVFkyWXpZdU9USXhJREFnTVRJdU5UTXROUzQxT1RZZ01USXVOVE10TVRJdU5YWXROVEJqTUMwMkxqa3dOQ0ExTGpZd09TMHhNaTQxSURFeUxqVXlPUzB4TWk0MWFESTFMakExT1dNMkxqa3lJREFnTVRJdU5USTVJRFV1TlRrMklERXlMalV5T1NBeE1pNDFkalV3WXpBZ05pNDVNRFFnTlM0Mk1Ea2dNVEl1TlNBeE1pNDFNeUF4TWk0MWN6RXlMalV5T1MwMUxqVTVOaUF4TWk0MU1qa3RNVEl1TlhZdE5UQmpNQzAyTGprd05DQTFMall3T1MweE1pNDFJREV5TGpVekxURXlMalZvTWpVdU1EVTVZell1T1RJZ01DQXhNaTQxTWprZ05TNDFPVFlnTVRJdU5USTVJREV5TGpWMk5UQmpNQ0EyTGprd05DQTFMall3T1NBeE1pNDFJREV5TGpVeU9TQXhNaTQxYURNM0xqVTRPV00yTGpreUlEQWdNVEl1TlRJNUxUVXVOVGsySURFeUxqVXlPUzB4TWk0MWRpMDNOV013TFRZdU9UQTBMVFV1TmpBNUxURXlMalV0TVRJdU5USTVMVEV5TGpWekxURXlMalV6SURVdU5UazJMVEV5TGpVeklERXlMalYyTlRZdU1qVmhOaTR5TmpRZ05pNHlOalFnTUNBeElERXRNVEl1TlRJNUlEQldORGM0TGpWak1DMDJMamt3TkMwMUxqWXdPUzB4TWk0MUxURXlMalV6TFRFeUxqVklOams0TGpVeU9XTXROaTQ1TWlBd0xURXlMalV5T1NBMUxqVTVOaTB4TWk0MU1qa2dNVEl1TlhZM05XTXdJRFl1T1RBMElEVXVOakE1SURFeUxqVWdNVEl1TlRJNUlERXlMalY2SWlBdlBqeHdZWFJvSUdROUlrMHhOVGN1TmpVMUlEVTBNV010Tmk0NU16SWdNQzB4TWk0MU5USXROUzQxT1RZdE1USXVOVFV5TFRFeUxqVjJMVFV3WXpBdE5pNDVNRFF0TlM0Mk1Ua3RNVEl1TlMweE1pNDFOVEV0TVRJdU5WTXhNakFnTkRjeExqVTVOaUF4TWpBZ05EYzRMalYyTnpWak1DQTJMamt3TkNBMUxqWXlJREV5TGpVZ01USXVOVFV5SURFeUxqVm9NVFV3TGpZeVl6WXVPVE16SURBZ01USXVOVFV5TFRVdU5UazJJREV5TGpVMU1pMHhNaTQxZGkwMU1HTXdMVFl1T1RBMElEVXVOakU1TFRFeUxqVWdNVEl1TlRVeUxURXlMalZvTVRRMExqTTBOV016TGpRMk5TQXdJRFl1TWpjMklESXVOems0SURZdU1qYzJJRFl1TWpWekxUSXVPREV4SURZdU1qVXROaTR5TnpZZ05pNHlOVWd6TWpBdU9ESTRZeTAyTGprek15QXdMVEV5TGpVMU1pQTFMalU1TmkweE1pNDFOVElnTVRJdU5YWXpOeTQxWXpBZ05pNDVNRFFnTlM0Mk1Ua2dNVEl1TlNBeE1pNDFOVElnTVRJdU5XZ3hOVEF1TmpKak5pNDVNek1nTUNBeE1pNDFOVEl0TlM0MU9UWWdNVEl1TlRVeUxURXlMalYyTFRjMVl6QXROaTQ1TURRdE5TNDJNVGt0TVRJdU5TMHhNaTQxTlRJdE1USXVOVWd5T0RNdU1UY3lZeTAyTGprek1pQXdMVEV5TGpVMU1TQTFMalU1TmkweE1pNDFOVEVnTVRJdU5YWTFNR013SURZdU9UQTBMVFV1TmpFNUlERXlMalV0TVRJdU5UVXlJREV5TGpWb0xUSTFMakV3TTJNdE5pNDVNek1nTUMweE1pNDFOVEl0TlM0MU9UWXRNVEl1TlRVeUxURXlMalYyTFRVd1l6QXROaTQ1TURRdE5TNDJNaTB4TWk0MUxURXlMalUxTWkweE1pNDFjeTB4TWk0MU5USWdOUzQxT1RZdE1USXVOVFV5SURFeUxqVjJOVEJqTUNBMkxqa3dOQzAxTGpZeE9TQXhNaTQxTFRFeUxqVTFNU0F4TWk0MWFDMHlOUzR4TURSNmJUTXdNUzR5TkRJdE5pNHlOV013SURNdU5EVXlMVEl1T0RFeElEWXVNalV0Tmk0eU56WWdOaTR5TlVnek16a3VOalUxWXkwekxqUTJOU0F3TFRZdU1qYzJMVEl1TnprNExUWXVNamMyTFRZdU1qVnpNaTQ0TVRFdE5pNHlOU0EyTGpJM05pMDJMakkxYURFeE1pNDVOalpqTXk0ME5qVWdNQ0EyTGpJM05pQXlMamM1T0NBMkxqSTNOaUEyTGpJMWVrMDBPVGNnTlRVekxqZ3hPR013SURZdU9USTVJRFV1TmpJNElERXlMalUwTmlBeE1pNDFOekVnTVRJdU5UUTJhREV6TW1FMkxqSTRJRFl1TWpnZ01DQXdJREVnTmk0eU9EWWdOaTR5TnpJZ05pNHlPQ0EyTGpJNElEQWdNQ0F4TFRZdU1qZzJJRFl1TWpjemFDMHhNekpqTFRZdU9UUXpJREF0TVRJdU5UY3hJRFV1TmpFMkxURXlMalUzTVNBeE1pNDFORFpCTVRJdU5UWWdNVEl1TlRZZ01DQXdJREFnTlRBNUxqVTNNU0EyTURSb01UVXdMamcxT0dNMkxqazBNeUF3SURFeUxqVTNNUzAxTGpZeE5pQXhNaTQxTnpFdE1USXVOVFExZGkweE1USXVPVEZqTUMwMkxqa3lPQzAxTGpZeU9DMHhNaTQxTkRVdE1USXVOVGN4TFRFeUxqVTBOVWcxTURrdU5UY3hZeTAyTGprME15QXdMVEV5TGpVM01TQTFMall4TnkweE1pNDFOekVnTVRJdU5UUTFkamMxTGpJM00zcHRNemN1TnpFMExUWXlMamN5TjJNdE5pNDVORE1nTUMweE1pNDFOekVnTlM0Mk1UY3RNVEl1TlRjeElERXlMalUwTlhZeU5TNHdPVEZqTUNBMkxqa3lPU0ExTGpZeU9DQXhNaTQxTkRZZ01USXVOVGN4SURFeUxqVTBObWd4TURBdU5UY3lZell1T1RReklEQWdNVEl1TlRjeExUVXVOakUzSURFeUxqVTNNUzB4TWk0MU5EWjJMVEkxTGpBNU1XTXdMVFl1T1RJNExUVXVOakk0TFRFeUxqVTBOUzB4TWk0MU56RXRNVEl1TlRRMVNEVXpOQzQzTVRSNklpQm1hV3hzTFhKMWJHVTlJbVYyWlc1dlpHUWlJQzgrUEM5blBqd3ZjM1puUGc9PSJ9"', + ) + expect( + await readContract(publicClient, { + ...wagmiContractConfig, + functionName: 'totalSupply', + }), + ).toEqual(558n) + }) +}) + +test('fake contract address', async () => { + await expect(() => + readContract(publicClient, { + abi: wagmiContractConfig.abi, + address: '0x0000000000000000000000000000000000000069', + functionName: 'totalSupply', + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + "The contract method \\"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\\" + + Version: viem@1.0.2" + `) +}) + +// Deploy BAYC Contract +async function deployBAYC() { + const hash = await deployContract(walletClient, { + ...baycContractConfig, + args: ['Bored Ape Wagmi Club', 'BAYC', 69420n, 0n], + from: accounts[0].address, + }) + await mine(testClient, { blocks: 1 }) + const { contractAddress } = await getTransactionReceipt(publicClient, { + hash, + }) + return { contractAddress } +} diff --git a/src/actions/public/readContract.ts b/src/actions/public/readContract.ts new file mode 100644 index 0000000000..4af6588a0b --- /dev/null +++ b/src/actions/public/readContract.ts @@ -0,0 +1,87 @@ +import { Abi } from 'abitype' + +import type { Chain, Formatter } from '../../chains' +import type { PublicClient } from '../../clients' +import type { + Address, + ExtractArgsFromAbi, + ExtractResultFromAbi, + ExtractFunctionNameFromAbi, +} from '../../types' +import { + EncodeFunctionDataArgs, + decodeFunctionResult, + encodeFunctionData, + getContractError, +} from '../../utils' +import { call, CallArgs, FormattedCall } from './call' + +export type FormattedReadContract< + TFormatter extends Formatter | undefined = Formatter, +> = FormattedCall + +export type ReadContractArgs< + TAbi extends Abi | readonly unknown[] = Abi, + TFunctionName extends string = any, +> = Omit< + CallArgs, + | 'accessList' + | 'chain' + | 'from' + | 'gas' + | 'gasPrice' + | 'maxFeePerGas' + | 'maxPriorityFeePerGas' + | 'nonce' + | 'to' + | 'data' + | 'value' +> & { + address: Address + abi: TAbi + functionName: ExtractFunctionNameFromAbi +} & ExtractArgsFromAbi + +export type ReadContractResponse< + TAbi extends Abi | readonly unknown[] = Abi, + TFunctionName extends string = string, +> = ExtractResultFromAbi + +export async function readContract< + TAbi extends Abi = Abi, + TFunctionName extends string = any, +>( + client: PublicClient, + { + abi, + address, + args, + functionName, + ...callRequest + }: ReadContractArgs, +): Promise> { + const calldata = encodeFunctionData({ + abi, + args, + functionName, + } as unknown as EncodeFunctionDataArgs) + try { + const { data } = await call(client, { + data: calldata, + to: address, + ...callRequest, + } as unknown as CallArgs) + return decodeFunctionResult({ + abi, + functionName, + data: data || '0x', + }) + } catch (err) { + throw getContractError(err, { + abi, + address, + args, + functionName, + }) + } +} diff --git a/src/actions/public/simulateContract.ts b/src/actions/public/simulateContract.ts index 581e8d9958..6be4bdf436 100644 --- a/src/actions/public/simulateContract.ts +++ b/src/actions/public/simulateContract.ts @@ -16,11 +16,7 @@ import { getContractError, } from '../../utils' import { WriteContractArgs } from '../wallet' -import { call, CallArgs, FormattedCall } from './call' - -export type FormattedSimulateContract< - TFormatter extends Formatter | undefined = Formatter, -> = FormattedCall +import { call, CallArgs } from './call' export type SimulateContractArgs< TChain extends Chain = Chain, diff --git a/src/actions/wallet/writeContract.ts b/src/actions/wallet/writeContract.ts index 8a75315cde..82a14b8210 100644 --- a/src/actions/wallet/writeContract.ts +++ b/src/actions/wallet/writeContract.ts @@ -10,16 +10,11 @@ import type { } from '../../types' import { EncodeFunctionDataArgs, encodeFunctionData } from '../../utils' import { - FormattedTransactionRequest, sendTransaction, SendTransactionArgs, SendTransactionResponse, } from './sendTransaction' -export type FormattedWriteContract< - TFormatter extends Formatter | undefined = Formatter, -> = FormattedTransactionRequest - export type WriteContractArgs< TChain extends Chain = Chain, TAbi extends Abi | readonly unknown[] = Abi, diff --git a/src/index.test.ts b/src/index.test.ts index 7c6f098c21..d982554788 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -156,6 +156,7 @@ test('exports actions', () => { "parseEther": [Function], "parseGwei": [Function], "parseUnit": [Function], + "readContract": [Function], "removeBlockTimestampInterval": [Function], "requestAccounts": [Function], "requestPermissions": [Function], diff --git a/src/index.ts b/src/index.ts index eab338770b..1e82655b67 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,8 @@ export type { OnBlockResponse, OnTransactions, OnTransactionsResponse, + ReadContractArgs, + ReadContractResponse, ResetArgs, RequestPermissionsResponse, RevertArgs, @@ -113,6 +115,7 @@ export { increaseTime, inspectTxpool, mine, + readContract, removeBlockTimestampInterval, reset, requestAccounts,