Static Python bindings for Ethereum smart contracts.
Apache License 2.0
9
stars
2
forks
source link
readme
Type-safe Python bindings for Ethereum smart contracts!
Used by
## Features
Static Python bindings for ethereum smart contracts.
- Parses JSON ABIs to create typesafe web3.py contract instances.
- Functions have typesafe function parameters and return values.
- Functions have transparent exceptions that contains decoded error messages and more.
- Smart Contract internal types are exposed as dataclasses.
- Contract event interfaces are exposed as typesafe dataclasses.
- Helper functions to deploy a given contract.
- Helper functions for `get_logs_typed` and `process_receipt_typed` to return typesafe dataclass events.
- Helper functions for signing, transacting, and waiting for transaction receipts.
This project is a work-in-progress. All code is provided as is and without guarantee.
## Install
```bash
pip install --upgrade pypechain
```
For development install instructions, see toplevel [INSTALL.md](https://github.com/delvtech/pypechain/blob/main/INSTALL.md)
## Packages 📦
| Package Name | Version | Description |
| ------------ | ---------------------------------------------------------------------------------------- | -------------------------------------------------- |
| pypechain | [![](https://img.shields.io/pypi/v/pypechain.svg)](<(https://pypi.org/pypi/pypechain/)>) | Codegen python interfaces for web3.py contracts. |
| autoflake | [![](https://img.shields.io/pypi/v/autoflake.svg)](<(https://pypi.org/pypi/autoflake/)>) | Removes unused imports and unused variables |
| black | [![](https://img.shields.io/pypi/v/black.svg)](<(https://pypi.org/pypi/black/)>) | The uncompromising code formatter. |
| isort | [![](https://img.shields.io/pypi/v/isort.svg)](<(https://pypi.org/pypi/isort/)>) | A Python utility / library to sort Python imports. |
| jinja2 | [![](https://img.shields.io/pypi/v/jinja2.svg)](<(https://pypi.org/pypi/jinja2/)>) | A very fast and expressive template engine. |
| web3 | [![](https://img.shields.io/pypi/v/web3.svg)](<(https://pypi.org/pypi/web3/)>) | web3.py |
## Usage
Pypechain is primarily to be used via the CLI:
```
❯❯ pypechain -h
usage: pypechain [-h] [--output-dir OUTPUT_DIR] [--line-length LINE_LENGTH] abi_file_path
Generates class files for a given abi.
positional arguments:
abi_file_path Path to the abi JSON file or directory containing multiple JSON files.
options:
-h, --help show this help message and exit
--output-dir OUTPUT_DIR
Path to the directory where files will be generated. Defaults to pypechain_types.
--line-length LINE_LENGTH
Optional argument for the output file's maximum line length. Defaults to 80.
```
However, you can also run the `main` script directly from Python:
```python
from pypechain import pypechain
abi_dir = "some/abi/dir"
output_dir = "some/output/dir"
pypechain(abi_dir, output_dir)
```
## Examples
Pypechain generates a Python module from compiled Solidity code in ABI format.
This enables access to typesafe contract, struct, and event objects, which greatly improves
the developer experience.
### Accessing contract balances
Using web3:
```python
from web3 import Web3
web3 = Web3()
base_token_address = "0xSomeAddress"
user_address = "0xUserAddress"
# Contract construction takes an ABI filepath string
base_token_contract = web3.eth.contract(
abi=base_contract_abi, address=web3.to_checksum_address(base_token_address)
)
# Arbitrary function arguments and names forces one to examine the ABI JSON to know the values & types
# Additionally, the types are not specified as Python types in the ABI
fn_args = [user_address]
fn_kwargs = {}
# HARD TO DISCOVER ALL FUNCTIONS AND THEIR ARGUMENTS
contract_function = base_token_contract.get_function_by_name("balanceOf")(*fn_args, **fn_kwargs)
# The function call also takes arbitrary args and kwargs
call_args = []
call_kwargs = {}
# UNTYPED RETURN VALUE!!
return_values: dict[str, Any] = contract_function.call(*call_args, **call_kwargs)
```
Using Pypechain generated objects:
```python
from web3 import Web3
from pypechain_types import ERC20MintableContract
web3 = Web3()
base_token_address = "0xSomeAddress"
user_address = "0xUserAddress"
# Contracts include a factory function to initialize with your given web3 provider
base_token_contract: ERC20MintableContract = ERC20MintableContract.deploy(w3=web3, signer=user_address)
# balanceOf is a class function, enabling IDE tab-completion, intuitive inspection, typed inputs and typed outputs
user_base_balance: int = base_token_contract.functions.balanceOf(user_address).call()
```
### Understanding contracts
Solidity files can be difficult to read for native Python programmers that have little exposure to smart contract code.
```typescript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ReturnTypes {
struct SimpleStruct {
uint intVal;
string strVal;
}
struct InnerStruct {
bool boolVal;
}
struct NestedStruct {
uint intVal;
string strVal;
InnerStruct innerStruct;
}
function mixStructsAndPrimitives() public pure returns (SimpleStruct memory simpleStruct, NestedStruct memory, uint, string memory name, bool YesOrNo) {
simpleStruct = SimpleStruct({
intVal: 1,
strVal: "You are number 1"
});
NestedStruct memory nestedStruct = NestedStruct({
intVal: 2,
strVal: "You are number 2",
innerStruct: InnerStruct({boolVal: true})
});
return (simpleStruct, nestedStruct, 1, "ReturnTypesContract", false);
}
}
```
Running pypechain on the compiled ABI from this contract produces code that is more intuitive for Python programmers.
```python
... # imports
@dataclass
class SimpleStruct:
"""SimpleStruct struct."""
intVal: int
strVal: str
@dataclass
class InnerStruct:
"""InnerStruct struct."""
boolVal: bool
@dataclass
class NestedStruct:
"""NestedStruct struct."""
intVal: int
strVal: str
innerStruct: InnerStruct
class ReturnTypesMixStructsAndPrimitivesContractFunction(ContractFunction):
"""ContractFunction for the mixStructsAndPrimitives method."""
class ReturnValues(NamedTuple):
"""The return named tuple for MixStructsAndPrimitives."""
simpleStruct: SimpleStruct
arg2: NestedStruct
arg3: int
name: str
YesOrNo: bool
def __call__(self) -> ReturnTypesMixStructsAndPrimitivesContractFunction:
clone = super().__call__()
self.kwargs = clone.kwargs
self.args = clone.args
return self
def call(
self,
transaction: TxParams | None = None,
block_identifier: BlockIdentifier = "latest",
state_override: CallOverride | None = None,
ccip_read_enabled: bool | None = None,
) -> ReturnValues:
"""returns ReturnValues."""
# Define the expected return types from the smart contract call
return_types = [SimpleStruct, NestedStruct, int, str, bool]
# Call the function
raw_values = super().call(transaction, block_identifier, state_override, ccip_read_enabled)
return self.ReturnValues(*rename_returned_types(return_types, raw_values))
class ReturnTypesContractFunctions(ContractFunctions):
"""ContractFunctions for the ReturnTypes contract."""
mixStructsAndPrimitives: ReturnTypesMixStructsAndPrimitivesContractFunction
def __init__(
self,
abi: ABI,
w3: "Web3",
address: ChecksumAddress | None = None,
decode_tuples: bool | None = False,
) -> None:
super().__init__(abi, w3, address, decode_tuples)
self.mixStructsAndPrimitives = ReturnTypesMixStructsAndPrimitivesContractFunction.factory(
"mixStructsAndPrimitives",
w3=w3,
contract_abi=abi,
address=address,
decode_tuples=decode_tuples,
function_identifier="mixStructsAndPrimitives",
)
class ReturnTypesContract(Contract):
"""A web3.py Contract class for the ReturnTypes contract."""
abi: ABI = returntypes_abi
bytecode: bytes = HexBytes(returntypes_bytecode)
def __init__(self, address: ChecksumAddress | None = None) -> None:
try:
# Initialize parent Contract class
super().__init__(address=address)
self.functions = ReturnTypesContractFunctions(returntypes_abi, self.w3, address)
except FallbackNotFound:
print("Fallback function not found. Continuing...")
functions: ReturnTypesContractFunctions
@classmethod
def deploy(cls, w3: Web3, signer: ChecksumAddress) -> Self:
"""Deploys and instance of the contract.
Parameters
----------
w3 : Web3
A web3 instance.
signer : ChecksumAddress
The address to deploy the contract from.
Returns
-------
Self
A deployed instance of the contract.
"""
deployer = cls.factory(w3=w3)
tx_hash = deployer.constructor().transact({"from": signer})
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
deployed_contract = deployer(address=tx_receipt.contractAddress) # type: ignore
return deployed_contract
@classmethod
def factory(cls, w3: Web3, class_name: str | None = None, **kwargs: Any) -> Type[Self]:
contract = super().factory(w3, class_name, **kwargs)
contract.functions = ReturnTypesContractFunctions(returntypes_abi, w3, None)
return contract
```
## Tests
We use pytest in our pypechain tests. Pytest, when ran locally, automatically compiles Solidity
using [Foundry](https://book.getfoundry.sh/getting-started/installation), as well as running
pypechain on the output abis.
If you run into issues during pytest, run `make clean; make build-test` to rebuild all solidity
and pypechain types in tests.
We also use [`pytest-snapshot`](https://pypi.org/project/pytest-snapshot/) for some tests to ensure
rendered files are as expected. If any changes are made to rendering that results in failures in snapshots,
run `pytest --snapshot-update`, ensure the generated files in `snapshots/` are as expected, and commit the new
snapshots in `snapshots/` as part of the update.
TODO also add in tests compiled via solc. See `conftest.py` for more information.