UNPKG

viem

Version:

TypeScript Interface for Ethereum

242 lines • 9.64 kB
import { multicall3Abi } from '../../constants/abis.js'; import { multicall3Bytecode } from '../../constants/contracts.js'; import { AbiDecodingZeroDataError } from '../../errors/abi.js'; import { BaseError } from '../../errors/base.js'; import { RawContractError } from '../../errors/contract.js'; import { decodeFunctionResult, } from '../../utils/abi/decodeFunctionResult.js'; import { encodeFunctionData, } from '../../utils/abi/encodeFunctionData.js'; import { getChainContractAddress, } from '../../utils/chain/getChainContractAddress.js'; import { getContractError, } from '../../utils/errors/getContractError.js'; import { getAction } from '../../utils/getAction.js'; import { createBatchScheduler, } from '../../utils/promise/createBatchScheduler.js'; import { stringify } from '../../utils/stringify.js'; import { readContract } from './readContract.js'; /** * 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(client, parameters) { const { account, authorizationList, allowFailure = true, blockHash, blockNumber, blockOverrides, blockTag, requireCanonical, stateOverride, } = parameters; const contracts = parameters.contracts; const batch = typeof client.batch?.multicall === 'object' ? client.batch.multicall : {}; const batchSize = parameters.batchSize ?? batch.batchSize ?? 1024; const deployless = parameters.deployless ?? batch.deployless ?? false; const multicallAddress = (() => { if (parameters.multicallAddress) return parameters.multicallAddress; if (deployless) return null; if (client.chain) { return getChainContractAddress({ blockNumber, chain: client.chain, contract: 'multicall3', }); } throw new Error('client chain not configured. multicallAddress is required.'); })(); const chunkedCalls = [[]]; 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, { abi, address, args, docsPath: '/docs/contract/multicall', functionName, sender: account, }); if (!allowFailure) throw error; chunkedCalls[currentChunk] = [ ...chunkedCalls[currentChunk], { allowFailure: true, callData: '0x', target: address, }, ]; } } const batching = Boolean(client.batch?.multicall); const batches = batching ? chunkedCalls.flatMap((calls) => calls.map((call) => [call])) : chunkedCalls; const aggregate3Results = await Promise.allSettled(batches.map((calls) => { if (batching) return scheduleMulticall(client, { account, authorizationList, batchSize, blockHash, blockNumber, blockOverrides, blockTag, call: calls[0], multicallAddress, requireCanonical, stateOverride, }).then((result) => [result]); return getAction(client, readContract, 'readContract')({ ...(multicallAddress === null ? { code: multicall3Bytecode } : { address: multicallAddress }), abi: multicall3Abi, account, args: [calls], authorizationList, blockHash, blockNumber, blockOverrides, blockTag, functionName: 'aggregate3', requireCanonical, 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 < batches[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 } = batches[i][j]; // Extract the contract config for this call from the `contracts` argument // for decoding. const { abi, address, functionName, args } = contracts[results.length]; 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, { 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; } async function scheduleMulticall(client, parameters) { const { batchSize, call, multicallAddress, ...rest } = parameters; const { wait = 0 } = typeof client.batch?.multicall === 'object' ? client.batch.multicall : {}; const { schedule } = createBatchScheduler({ id: stringify(['multicall', client.uid, batchSize, multicallAddress, rest]), wait, shouldSplitBatch(calls) { if (batchSize === 0) return false; const size = calls.reduce((size, { callData }) => size + (callData.length - 2) / 2, 0); return size > batchSize; }, fn: (calls) => getAction(client, readContract, 'readContract')({ ...(multicallAddress === null ? { code: multicall3Bytecode } : { address: multicallAddress }), ...rest, abi: multicall3Abi, args: [calls], functionName: 'aggregate3', }), }); const [result] = await schedule(call); return result; } //# sourceMappingURL=multicall.js.map