Skip to main content

Add your description here

Project description

eth-contract

EVM contract abstraction and ABI utilities for Python.

The core design principle: decouple contract definitions from Web3 instances. Build calldata as pure functions, compose contract calls freely, and only bind to a provider at the moment of execution. This makes it easy to work with multiple chains/providers and to compose calls with multicall or other routers.

Installation

pip install eth-contract

Patterns

1. Pythonic ABI Definitions

Stop writing unreadable JSON ABI files or relying on the Solidity compiler to get an ABI. Define your contract interface directly in Python.

Human-Readable ABIs

Pass Solidity-style signature strings to Contract.from_abi. The library parses them into a full JSON ABI at runtime:

from eth_contract import Contract

ERC20 = Contract.from_abi([
    "function transfer(address to, uint256 amount) returns (bool)",
    "function balanceOf(address owner) view returns (uint256)",
    "function approve(address spender, uint256 amount) returns (bool)",
    "event Transfer(address indexed from, address indexed to, uint256 amount)",
    "event Approval(address indexed owner, address indexed spender, uint256 amount)",
])

Structs are supported too — define them inline and reference them from functions and events:

Router = Contract.from_abi([
    """struct ExactInputSingleParams {
      address tokenIn;
      address tokenOut;
      uint24 fee;
      address recipient;
      uint256 amountIn;
      uint256 amountOutMinimum;
      uint160 sqrtPriceLimitX96;
    }""",
    "function exactInputSingle(ExactInputSingleParams params) payable returns (uint256 amountOut)",
])

Type-Annotated ABI Structs

For richer Python integration, define structs as typed Python classes using ABIStruct. The class behaves like a NamedTuple and provides encode() / decode() / human_readable_abi() for free.

Supported annotation forms:

Python annotation Solidity ABI type
Annotated[T, 'solidity_type'] explicit type (always works)
bool bool
int uint256
str string
bytes bytes
list[bool|int|str|bytes] bool[] / uint256[] / …
SomeStruct (ABIStruct subclass) tuple (nested struct)
list[SomeStruct] tuple[] (dynamic array of structs)
Annotated[list[SomeStruct], 'SomeStruct[N]'] tuple[N] (fixed-size array of structs)
from typing import Annotated
from eth_contract import ABIStruct, Contract

class SwapParams(ABIStruct):
    token_in:  Annotated[str, 'address']
    token_out: Annotated[str, 'address']
    fee:       Annotated[int, 'uint24']   # explicit when default doesn't fit
    recipient: Annotated[str, 'address']
    amount_in: int                         # default: int → uint256
    amount_out_minimum: int

# Generate the human-readable ABI fragment automatically
print(SwapParams.human_readable_abi())
# ['struct SwapParams { address token_in; address token_out; uint24 fee; ... }']

# Build the contract using the generated struct definition
Router = Contract.from_abi(
    SwapParams.human_readable_abi() + [
        "function exactInputSingle(SwapParams params) payable returns (uint256 amountOut)",
    ]
)

ABIStruct supports nesting — use another ABIStruct subclass directly as a field type, or wrap it in list[...] for arrays of structs:

class Inner(ABIStruct):
    x: bool               # default mapping: bool → bool
    y: Annotated[bytes, 'bytes32']

class Outer(ABIStruct):
    value: int            # default mapping: int → uint256
    inner: Inner          # single nested struct
    inners: list[Inner]   # dynamic array of structs  → tuple[]
    static_inners: Annotated[list[Inner], 'Inner[3]']  # fixed-size → tuple[3]

tx = Outer(
    value=42,
    inner=Inner(x=True, y=b'\x01' * 32),
    inners=(Inner(x=False, y=b'\x02' * 32),),
    static_inners=(Inner(x=True, y=b'\x03' * 32),) * 3,
)
decoded = Outer.decode(tx.encode())
assert decoded == tx

print(Outer.human_readable_abi())
# ['struct Inner { bool x; bytes32 y; }',
#  'struct Outer { uint256 value; Inner inner; Inner[] inners; Inner[3] static_inners; }']

2. Web3-Agnostic Calldata Building

Building calldata is a pure function — no Web3 instance required. Bind an address or transaction parameters to a contract with contract(to=..., ...), then call functions to produce encoded calldata. The actual Web3 provider is only provided at the point of execution (.call() or .transact()).

from eth_contract import Contract

ERC20 = Contract.from_abi([
    "function transfer(address to, uint256 amount) returns (bool)",
    "function balanceOf(address owner) view returns (uint256)",
])

token = ERC20(to="0xTokenAddress...")

# Build calldata without any Web3 instance
calldata = token.fns.transfer("0xRecipient...", 10**18).data
# HexBytes('0xa9059cbb...')

# Execute only when you have a provider
balance = await token.fns.balanceOf("0xUser...").call(w3)
receipt = await token.fns.transfer("0xRecipient...", 10**18).transact(w3, account)

Because calldata building is decoupled from execution, the same ContractFunction object can be passed to multicall or any other batching mechanism:

from eth_contract.multicall3 import multicall
from eth_contract import ERC20

USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
users = ["0xUser1...", "0xUser2..."]

# Build all calls without a provider
calls = [(USDC, ERC20.fns.balanceOf(user)) for user in users]
calls += [(WETH, ERC20.fns.balanceOf(user)) for user in users]

# Execute all calls in a single RPC round-trip
results = await multicall(w3, calls)

Contracts can be rebound to different addresses or parameters on the fly:

# Base contract definition (no address bound)
ERC20 = Contract.from_abi(["function balanceOf(address) view returns (uint256)"])

# Bind to a specific token address
usdc = ERC20(to="0xUSDC...")
weth = ERC20(to="0xWETH...")

# Both share the same ABI, different addresses
usdc_balance = await usdc.fns.balanceOf(user).call(w3)
weth_balance = await weth.fns.balanceOf(user).call(w3)

3. Built-In Utility ABIs

No need to copy-paste ABIs for common contracts. eth-contract ships ready-to-use instances:

from eth_contract import ERC20
from eth_contract.multicall3 import MULTICALL3, multicall
from eth_contract.weth import WETH
from eth_contract.entrypoint import ENTRYPOINT07, ENTRYPOINT08

ERC20

from eth_contract import ERC20

token = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"  # USDC

balance = await ERC20.fns.balanceOf(user).call(w3, to=token)
receipt = await ERC20.fns.transfer(recipient, amount).transact(w3, account, to=token)

Multicall3

Batch many read calls into one RPC request:

from eth_contract import ERC20
from eth_contract.multicall3 import multicall

tokens = ["0xUSDC...", "0xWETH...", "0xDAI..."]
calls = [(token, ERC20.fns.balanceOf(user)) for token in tokens]
balances = await multicall(w3, calls)
# [usdc_balance, weth_balance, dai_balance]

WETH

from eth_contract.weth import WETH

weth_address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
receipt = await WETH.fns.deposit().transact(w3, account, to=weth_address, value=10**18)

Deterministic Deployment (CREATE2 / CREATE3)

from eth_contract.create2 import create2_deploy, create2_address
from eth_contract.create3 import create3_deploy, create3_address
from eth_contract.utils import get_initcode

initcode = get_initcode(artifact)              # from a compiled artifact dict
salt = 0

address = create2_address(initcode, salt=salt) # compute address before deploying
address = await create2_deploy(w3, account, initcode, salt=salt)

CLI tools for deployment:

python -m eth_contract.create2 artifact.json [ctor_args...] --salt 0 --rpc-url http://...
python -m eth_contract.create3 artifact.json [ctor_args...] --salt 0 --rpc-url http://...
python -m eth_contract.contract abi.json      # list all signatures in an ABI file

Utility Helpers

from eth_contract.utils import send_transaction, send_transactions, transfer, balance_of

# Single transaction
receipt = await send_transaction(w3, account, to=addr, data=calldata)

# Batch with automatic nonce management
receipts = await send_transactions(w3, [tx1, tx2, tx3], account=account)

# ERC20 or native transfer (pass ZERO_ADDRESS for native ETH)
await transfer(w3, token_address, sender, receiver, amount)
await transfer(w3, ZERO_ADDRESS, sender, receiver, amount)  # native ETH

# Balance query
bal = await balance_of(w3, token_address, address)
bal = await balance_of(w3, ZERO_ADDRESS, address)  # native ETH

Built-In Contracts Summary

Contract Import
ERC20 from eth_contract import ERC20
Multicall3 from eth_contract.multicall3 import MULTICALL3, multicall
WETH from eth_contract.weth import WETH
ERC-4337 EntryPoint v0.7 from eth_contract.entrypoint import ENTRYPOINT07
ERC-4337 EntryPoint v0.8 from eth_contract.entrypoint import ENTRYPOINT08
CREATE2 factory from eth_contract.create2 import create2_deploy
CreateX (CREATE3) from eth_contract.create3 import create3_deploy

Please open an issue if you want to see more ABIs included.


Project Setup

  1. Install nix
  2. Install direnv and nix-direnv
  3. uv sync --frozen to install all dependencies. (Without --frozen, uv version after v0.6.15 modifies the uv.lock)
  4. pytest to run all tests

If you are able to run all tests, you are ready to go!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

eth_contract-0.4.1.tar.gz (129.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

eth_contract-0.4.1-py3-none-any.whl (108.5 kB view details)

Uploaded Python 3

File details

Details for the file eth_contract-0.4.1.tar.gz.

File metadata

  • Download URL: eth_contract-0.4.1.tar.gz
  • Upload date:
  • Size: 129.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.6 {"installer":{"name":"uv","version":"0.10.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for eth_contract-0.4.1.tar.gz
Algorithm Hash digest
SHA256 157522506f254bbfd1f9546187ce9c7852378f84beb0fbffee086a7b21f5f03f
MD5 2153d658a842a95565023993d0e16aef
BLAKE2b-256 f6d43d3988b0cb9eee80c6bc205f4b56a4e6e6306b9685f67a3312a352d58926

See more details on using hashes here.

File details

Details for the file eth_contract-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: eth_contract-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 108.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.6 {"installer":{"name":"uv","version":"0.10.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for eth_contract-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 58fa898285f4bd12520b50e1cbacbee43efa1b1ad94702f481ad0481ee8146a6
MD5 ce6ede6bf5b99bc6f35694808eba3d72
BLAKE2b-256 6c01811d91abed4d75822399890e87284fbb54d19d7b8c7086e609f813544af8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page