Tutorial#

Async support#

While the examples and tests use trio, pons is anyio-based and supports all the corresponding backends.

Sessions#

All calls to the provider in pons happen within a session. It translates to the usage of a single session in the backend HTTP request library, so the details are implementation-dependent, but in general it means that multiple requests will happen faster. For example, in a session an SSL handshake only happens once, and it is a somewhat slow process.

Correspondingly, all the main functionality of the library is concentrated in the ClientSession class.

Signers#

Any operation that entails writing information into the blockchain takes a Signer object. For now, only signers created from eth_account.Account are supported, but one can define their own class backed by, say, a hardware signer, using the abstract Signer class.

Amounts and addresses#

Native currency amounts and network addresses are typed in pons. All methods expect and return only Amount and Address objects — no integers or strings allowed.

In an application using pons one can superclass these classes to distinguish between different types of currencies, or addresses from different networks. Note though that all the arithmetic and comparison functions require strict type equality and raise an exception if it is not the case, to protect from accidental usage of addresses/amounts from wrong domains.

Contract ABI#

Contract ABI can be declared in two different ways in pons. The first one can be used when you have a JSON ABI definition, for example installed as a JS package, or obtained from compiling a contract.

from pons import ContractABI

cabi = ContractABI.from_json(json_abi)
print(cabi)

This will show a brief summary of the ABI in a C-like code.

{
    constructor(uint256 _v1, uint256 _v2) nonpayable
    fallback() nonpayable
    receive() payable
    function getState(uint256 _x) view returns (uint256)
    function testStructs((uint256,uint256) inner_in, ((uint256,uint256),uint256) outer_in) view returns ((uint256,uint256) inner_out, ((uint256,uint256),uint256) outer_out)
    function v1() view returns (uint256)
    function v2() view returns (uint256)
    function setState(uint256 _v1) nonpayable
}

Alternatively, one can define only the methods they need directly in Python code:

from pons import ContractABI, abi, Constructor, Method, Mutability

inner_struct = abi.struct(inner1=abi.uint(256), inner2=abi.uint(256))
outer_struct = abi.struct(inner=inner_struct, outer1=abi.uint(256))
cabi = ContractABI(
    constructor=Constructor(inputs=dict(_v1=abi.uint(256), _v2=abi.uint(256))),
    methods=[
        Method(
            name='setState',
            mutability=Mutability.NONPAYABLE,
            inputs=dict(_v1=abi.uint(256)))
        Method(
            name='getState',
            mutability=Mutability.VIEW,
            inputs=dict(_x=abi.uint(256)),
            outputs=abi.uint(256)),
        Method(
            name='testStructs',
            mutability=Mutability.VIEW,
            inputs=dict(inner_in=inner_struct, outer_in=outer_struct),
            outputs=dict(inner_out=inner_struct, outer_out=outer_struct),
            )
        ]
    )

print(cabi)
{
    constructor(uint256 _v1, uint256 _v2) nonpayable
    function getState(uint256 _x) view returns (uint256)
    function testStructs((uint256,uint256) inner_in, ((uint256,uint256),uint256) outer_in) view returns ((uint256,uint256) inner_out, ((uint256,uint256),uint256) outer_out)
    function setState(uint256 _v1) nonpayable
}

Contract methods#

All the enumerated methods have corresponding objects that can be accessed via ContractABI fields (see the API reference for details). For example,

print(cabi.method.getState)
function getState(uint256 _x) view returns (uint256)

With a specific method object one can create a contract call by, naturally, calling the object. The arguments are processed the same as in Python functions, so one can either use positional arguments, keyword ones (if the parameter names are present in the contract ABI), or mix the two.

call = cabi.method.getState(1)
call = cabi.method.getState(_x=1)

Note that the arguments are checked and encoded on call creation, so any inconsistency will result in a raised exception:

call = cabi.method.getState(1, 2)
Traceback (most recent call last):
...
TypeError: too many positional arguments
call = cabi.method.getState("a")
Traceback (most recent call last):
...
TypeError: `uint256` must correspond to an integer, got str

Deploying contracts#

In order to deploy a contract one needs its ABI and bytecode. At the moment pons does not expose the compiler interface, so it has to come from a third party library, for example py-solcx. With that, create a CompiledContract object and use deploy():

compiled_contract = CompiledContract(cabi, bytecode)
deployed_contract = await session.deploy(signer, compiled_contract.constructor(arg1, arg2))

This will result in a DeployedContract object encapsulating the contract address and its ABI and allowing one to interact with the contract.

Alternatively, a DeployedContract object can be created with a known address if the contract is already deployed:

deployed_contract = DeployedContract(cabi, Address.from_hex("0x<contract_address>"))

Interacting with deployed contracts#

A DeployedContract object wraps all ABI method objects into “bound” state, similarly to how Python methods are bound to class instances. It means that all the method calls created from this object have the contract address inside them, so that it does not need to be provided every time.

For example, to call a non-mutating contract method via eth_call():

call = deployed_contract.method.getState(1)
result = await session.eth_call(call)

Note that when the ContractABI object is created from the JSON ABI, even if the method returns a single value, it is still represented as a list of one element in the JSON, so the result will be a list too. If the ABI is declared programmatically, one can provide a single output value instead of the list, and then pons will unpack that list.

Naturally, a mutating call requires a signer to be provided:

call = deployed_contract.method.setState(1)
await session.transact(signer, call)