multicall.py

repository·master·Indexed 43 days ago

https://github.com/banteg/multicall.py

A Python interface for MakerDAO's multicall and a port of multicall.js. It allows developers to batch multiple Ethereum contract calls into a single request to reduce RPC overhead and improve efficiency. Version 0.16.0 provides abstractions like the Multicall class for aggregating calls, the Call class for individual contract interactions, and the Signature class for handling seth-style function signatures.

Tokens
4.4K
Snippets
14
Records
21
Agent score
53%

What's inside multicall.py

  1. How Multicall handles batching and errors

    master

    The Multicall implementation uses an internal NotSoBrightBatcher to manage large volumes of calls.

    Batching Logic:

    • Calls are split into chunks based on batcher.step (defaulting to 10,000).
    • If a batch fails due to size or connectivity issues, the rebatch mechanism automatically reduces the step size (splitting the failed batch in half) and retries.

    Error Handling:

    • The library automatically retries on specific recoverable errors, such as:
      • aiohttp.ClientResponseError with "request entity too large" or "connection reset by peer".
      • requests.HTTPError with "request entity too large", "payload too large", "time-out", or "520 server error".
      • ValueError containing "out of gas" (unless it's a single-call batch).
      • TimeoutError.
    • If an error is not in the recoverable list, it is raised immediately.
  2. Configure multicall via Environment Variables

    master

    You can tune the library behavior using the following environment variables:

    VariableDescriptionDefault
    GAS_LIMITSets the overridable default gas limit for Multicall to prevent out of gas errors.50,000,000
    MULTICALL_DEBUGIf set, sets the logging level for all library loggers to logging.DEBUG.N/A
    AIOHTTP_TIMEOUTSets the aiohttp timeout period in seconds for async calls to the node.30
  3. Use Multicall to batch multiple blockchain calls

    master

    The Multicall class allows you to batch multiple Call objects into a single request. You can define Call objects with specific return handlers (functions) to process the raw data immediately upon execution. When Multicall is called, it returns a dictionary where keys are the names provided in the returns argument and values are the processed results.

    from multicall import Call, Multicall
    
    # Example setup
    MKR_TOKEN = '0xaaf64bfcc32d0f15873a02163e7e500671a4ffcd'
    MKR_WHALE = '0xdb33dfd3d61308c33c63209845dad3e6bfb2c674'
    MKR_FISH = '0x2dfcedcb401557354d0cf174876ab17bfd6f4efd'
    
    def from_wei(value):
        return value / 1e18
    
    multi = Multicall([
        Call(MKR_TOKEN, ['balanceOf(address)(uint256)', MKR_WHALE], [('whale', from_wei)]),
        Call(MKR_TOKEN, ['balanceOf(address)(uint256)', MKR_FISH], [('fish', from_wei)]),
        Call(MKR_TOKEN, 'totalSupply()(uint256)', [('supply', from_wei)]),
    ])
    
    # Executes the batch and returns a dict of processed values
    results = multi()  # {'whale': 566437.0921992733, 'fish': 7005.0, 'supply': 1000003.1220798912}
  4. Create a Call

    master

    The Call(target, function, returns) class represents a single contract call.

    Parameters:

    • target: The to address for the eth_call.
    • function: Either a seth-style signature string method(input,types)(output,types) or a list containing [signature, *args].
    • returns: A list of tuples (name, handler) for processing return values.
      • If omitted, the call returns a tuple.
      • If provided, the call returns a dictionary.
      • To skip processing a specific value, pass None as the handler.

    Usage Patterns:

    • Predefined args: Call(... )() executes the call with arguments provided during initialization.
    • Dynamic args: Call(...)(args) allows you to reuse a prepared call object with different arguments.
    • Decoding: Use decode_output(output) to decode raw output and apply the returns handlers.
  5. Create a Multicall

    master

    The Multicall(calls) class aggregates a list of Call objects.

    Parameters:

    • calls: A list of Call objects that have already had their arguments prepared.

    Usage:

    • Call the instance Multicall(...)() to execute the batch and retrieve the results.
  6. Create a seth-style Signature

    master

    The Signature(signature) class handles seth-style function signatures in the format function_name(input,types)(output,types). It supports complex types like structs, provided they are broken down into basic parts (e.g., (address,bytes)[]).

    Use the following methods for data manipulation:

    • encode_data(args): Encodes input arguments into calldata.
    • decode_data(output): Decodes the output data.
  7. Use Call, Multicall, and Signature from the multicall package

    master

    The multicall package provides a structured way to define and execute multiple blockchain calls using Signature, Call, and Multicall abstractions.

    • Signature: Defines the cryptographic signature or identity used for the calls.
    • Call: Represents a single target function call, including the target address, the function selector, and the expected return types.
    • Multicall: Orchestrates a collection of Call objects into a single batch execution.
    from multicall import Signature, Call, Multicall
    
    # Example workflow (conceptual):
    signature = Signature("...")
    calls = [
        Call(target="0x...", function="0x...", returns=["uint256"]),
        Call(target="0x...", function="0x...", returns=["bool"])
    ]
    multicall = Multicall(calls)
    # Execute using the multicall instance...
  8. Configure logging with setup_logger()

    master

    The setup_logger function initializes a logging.Logger instance for a given name. If the environment variable MULTICALL_DEBUG is set to a truthy value, the logger is automatically configured to output to a StreamHandler (stdout/stderr) and its level is set to logging.DEBUG.

    import os
    from multicall.loggers import setup_logger
    
    # To enable debug logging, ensure the environment variable is set
    os.environ["MULTICALL_DEBUG"] = "True"
    
    logger = setup_logger("my_app")
    logger.debug("This will be visible if MULTICALL_DEBUG is set")
  9. Use the Multicall class to aggregate blockchain calls

    master

    The Multicall class aggregates multiple Call objects into a single batch request to a Multicall contract (Multicall2 or Multicall3, depending on the chain ID). This reduces the number of RPC calls needed to fetch data from multiple addresses or contracts.

    To use it, initialize Multicall with a list of Call objects. You can then await the instance directly to execute the batch.

    Key Parameters:

    • calls: A list of Call objects to execute.
    • block_id: An optional integer representing the block number to query.
    • require_success: If True (default), uses the aggregate method. If False, uses tryBlockAndAggregate to allow individual calls within the batch to fail without reverting the whole batch.
    • gas_limit: The gas limit for the multicall execution.
    • _w3: An instance of Web3 (defaults to the imported w3).
    • origin: An optional AnyAddress to use for state overrides if supported by the chain.
    from multicall.multicall import Multicall
    from multicall.call import Call
    
    # Define your calls
    calls = [
        Call(target="0x...", function="balanceOf(address)", params=["0x..."]),
        # ... more calls
    ]
    
    # Initialize and execute
    multicall = Multicall(calls=calls, require_success=True)
    results = await multicall
  10. Execute a Call synchronously or asynchronously

    master

    Once a Call object is instantiated, you can execute it using several methods:

    1. Synchronous execution: Use call() (via __call__). This method includes automatic retries using eth_retry.
    2. Asynchronous execution: Use await call.coroutine() for non-blocking execution. This also supports automatic retries and respects semaphore limits for concurrency control.

    Both methods allow you to override the Web3 instance or the block_id at runtime.

    # Synchronous
    result = call()
    
    # Asynchronous
    result = await call.coroutine(block_id=12345)
  11. Add common blockchain metadata calls to a Multicall

    master

    The Multicall class provides helper methods to easily add common blockchain state queries to your batch. These methods append a new Call to the existing self.calls list using the appropriate Multicall contract signature.

    Available helper methods:

    • add_base_fee(return_signature=('base_fee', None))
    • add_block_hash(block_number, return_signature=('block_hash', None))
    • add_block_number(return_signature=('block_number', None))
    • add_chain_id(return_signature=('chain_id', None))
    • add_block_coinbase(return_signature=('coinbase', None))
    • add_block_difficulty(return_signature=('difficulty', None))
    • add_block_gas_limit(return_signature=('gas_limit', None))
    • add_block_timestamp(return_signature=('timestamp', None))
    • add_eth_balance(address, return_signature=('eth_balance', None))
    • add_last_block_hash(return_signature=('last_block_hash', None))
    calls = []
    m = Multicall(calls=calls)
    
    # Add metadata calls to the batch
    m.add_chain_id()
    m.add_block_number()
    m.add_eth_balance("0x...")
    
    # Execute the combined batch
    results = await m