UNPKG

viem

Version:

TypeScript Interface for Ethereum

288 lines (266 loc) • 8.86 kB
import type { AbiStateMutability, Address, Narrow } from 'abitype' import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import { multicall3Abi } from '../../constants/abis.js' import { AbiDecodingZeroDataError } from '../../errors/abi.js' import { BaseError } from '../../errors/base.js' import { RawContractError } from '../../errors/contract.js' import type { Chain } from '../../types/chain.js' import type { ContractFunctionParameters } from '../../types/contract.js' import type { Hex } from '../../types/misc.js' import type { MulticallContracts, MulticallResults, } from '../../types/multicall.js' import { type DecodeFunctionResultErrorType, decodeFunctionResult, } from '../../utils/abi/decodeFunctionResult.js' import { type EncodeFunctionDataErrorType, encodeFunctionData, } from '../../utils/abi/encodeFunctionData.js' import { type GetChainContractAddressErrorType, getChainContractAddress, } from '../../utils/chain/getChainContractAddress.js' import { type GetContractErrorReturnType, getContractError, } from '../../utils/errors/getContractError.js' import type { ErrorType } from '../../errors/utils.js' import { getAction } from '../../utils/getAction.js' import type { CallParameters } from './call.js' import { type ReadContractErrorType, readContract } from './readContract.js' export type MulticallParameters< contracts extends readonly unknown[] = readonly ContractFunctionParameters[], allowFailure extends boolean = true, options extends { optional?: boolean properties?: Record<string, any> } = {}, > = Pick<CallParameters, 'blockNumber' | 'blockTag' | 'stateOverride'> & { allowFailure?: allowFailure | boolean | undefined batchSize?: number | undefined contracts: MulticallContracts< Narrow<contracts>, { mutability: AbiStateMutability } & options > multicallAddress?: Address | undefined } export type MulticallReturnType< contracts extends readonly unknown[] = readonly ContractFunctionParameters[], allowFailure extends boolean = true, options extends { error?: Error } = { error: Error }, > = MulticallResults< Narrow<contracts>, allowFailure, { mutability: AbiStateMutability } & options > export type MulticallErrorType = | GetChainContractAddressErrorType | ReadContractErrorType | GetContractErrorReturnType< EncodeFunctionDataErrorType | DecodeFunctionResultErrorType > | ErrorType /** * Similar to [`readContract`](https://viem.sh/docs/contract/readContract), but batches up multiple functions on a contract in a single RPC call via the [`multicall3` contract](https://github.com/mds1/multicall). * * - Docs: https://viem.sh/docs/contract/multicall * * @param client - Client to use * @param parameters - {@link MulticallParameters} * @returns An array of results with accompanying status. {@link MulticallReturnType} * * @example * import { createPublicClient, http, parseAbi } from 'viem' * import { mainnet } from 'viem/chains' * import { multicall } from 'viem/contract' * * const client = createPublicClient({ * chain: mainnet, * transport: http(), * }) * const abi = parseAbi([ * 'function balanceOf(address) view returns (uint256)', * 'function totalSupply() view returns (uint256)', * ]) * const results = await multicall(client, { * contracts: [ * { * address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', * abi, * functionName: 'balanceOf', * args: ['0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'], * }, * { * address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', * abi, * functionName: 'totalSupply', * }, * ], * }) * // [{ result: 424122n, status: 'success' }, { result: 1000000n, status: 'success' }] */ export async function multicall< const contracts extends readonly unknown[], chain extends Chain | undefined, allowFailure extends boolean = true, >( client: Client<Transport, chain>, parameters: MulticallParameters<contracts, allowFailure>, ): Promise<MulticallReturnType<contracts, allowFailure>> { const { allowFailure = true, batchSize: batchSize_, blockNumber, blockTag, multicallAddress: multicallAddress_, stateOverride, } = parameters const contracts = parameters.contracts as ContractFunctionParameters[] const batchSize = batchSize_ ?? ((typeof client.batch?.multicall === 'object' && client.batch.multicall.batchSize) || 1_024) let multicallAddress = multicallAddress_ if (!multicallAddress) { if (!client.chain) throw new Error( 'client chain not configured. multicallAddress is required.', ) multicallAddress = getChainContractAddress({ blockNumber, chain: client.chain, contract: 'multicall3', }) } type Aggregate3Calls = { allowFailure: boolean callData: Hex target: Address }[] const chunkedCalls: Aggregate3Calls[] = [[]] let currentChunk = 0 let currentChunkSize = 0 for (let i = 0; i < contracts.length; i++) { const { abi, address, args, functionName } = contracts[i] try { const callData = encodeFunctionData({ abi, args, functionName }) currentChunkSize += (callData.length - 2) / 2 // Check to see if we need to create a new chunk. if ( // Check if batching is enabled. batchSize > 0 && // Check if the current size of the batch exceeds the size limit. currentChunkSize > batchSize && // Check if the current chunk is not already empty. chunkedCalls[currentChunk].length > 0 ) { currentChunk++ currentChunkSize = (callData.length - 2) / 2 chunkedCalls[currentChunk] = [] } chunkedCalls[currentChunk] = [ ...chunkedCalls[currentChunk], { 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 chunkedCalls[currentChunk] = [ ...chunkedCalls[currentChunk], { allowFailure: true, callData: '0x' as Hex, target: address, }, ] } } const aggregate3Results = await Promise.allSettled( chunkedCalls.map((calls) => getAction( client, readContract, 'readContract', )({ abi: multicall3Abi, address: multicallAddress!, args: [calls], blockNumber, blockTag, functionName: 'aggregate3', stateOverride, }), ), ) const results = [] for (let i = 0; i < aggregate3Results.length; i++) { const result = aggregate3Results[i] // If an error occurred in a `readContract` invocation (ie. network error), // then append the failure reason to each contract result. if (result.status === 'rejected') { if (!allowFailure) throw result.reason for (let j = 0; j < chunkedCalls[i].length; j++) { results.push({ status: 'failure', error: result.reason, result: undefined, }) } continue } // If the `readContract` call was successful, then decode the results. const aggregate3Result = result.value for (let j = 0; j < aggregate3Result.length; j++) { // Extract the response from `readContract` const { returnData, success } = aggregate3Result[j] // Extract the request call data from the original call. const { callData } = chunkedCalls[i][j] // Extract the contract config for this call from the `contracts` argument // for decoding. const { abi, address, functionName, args } = contracts[ results.length ] as ContractFunctionParameters try { if (callData === '0x') throw new AbiDecodingZeroDataError() if (!success) throw new RawContractError({ data: returnData }) const result = decodeFunctionResult({ abi, args, data: returnData, functionName, }) results.push(allowFailure ? { result, status: 'success' } : result) } catch (err) { const error = getContractError(err as BaseError, { abi, address, args, docsPath: '/docs/contract/multicall', functionName, }) if (!allowFailure) throw error results.push({ error, result: undefined, status: 'failure' }) } } } if (results.length !== contracts.length) throw new BaseError('multicall results mismatch') return results as MulticallReturnType<contracts, allowFailure> }