viem
Version:
288 lines (266 loc) • 8.86 kB
text/typescript
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>
}