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.0.tar.gz (122.2 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.0-py3-none-any.whl (106.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: eth_contract-0.4.0.tar.gz
  • Upload date:
  • Size: 122.2 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.0.tar.gz
Algorithm Hash digest
SHA256 96313b19c20b59d0089b794f97e6abc9bb61d064f5f8e6debe4a9a3b18ba86d2
MD5 e8630f45953e4702e504141a0cd25aff
BLAKE2b-256 5fd961cb0d8d62c98e82319eac0e133022f02c85dda3bebc46b3a47ef7fb6068

See more details on using hashes here.

File details

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

File metadata

  • Download URL: eth_contract-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 106.3 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 98cc1feffd9592c53e01b784acc39afbef3d5ed55ce9a438ffa7bef0fa9092f3
MD5 a46b51143e84a0a2d148622e83e9b9cd
BLAKE2b-256 1bc287f0c2a6c0a17a542d789257efb4a2a02235f56e27a79d651deca7725087

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