Skip to content

Contract Function Types#

When building a contract function, you can utilize a variety of pythonic concepts to define your signature to be as easy to manipulate as possible. Firstly, if you need to define a solidity struct you can utilize the eth_rpc.types.Struct class, and define your model inheriting from this class. For example, if you had a smart contract with a function like:

Note

This is not a realistic function signature, but it showcases multiple features of the ContractFunc class

contract ExampleContract {
    enum Status {
        SUCCESS,
        FAILURE
    }

    struct Account {
        uint256 accountId;
        uint256 balance;
        bool isActive;
    }

    struct Transaction {
        uint256 recipientId;
        uint256 amount;
    }

    struct DepositResult {
        int256 balanceUpdate;
        Status status;
    }

    function depositFunds(Account memory account, Transaction[] memory transactions, uint256 deadline, bytes[] memory additionalArgs) external returns(bool success, DepositResult memory) {
        ...
    }
}

So this method, depositFunds, has a few characteristics. It takes multiple structs, one as a direct argument and the other as a list of structs. It also returns multiple values, one with an Enum field. Now we can translate this directly to python types.

Basic Imports#

from enum import IntEnum  # Its best to use IntEnum, since your enum fields need to be indexed from 0
from typing import Annotated  # This is used to attach additional information to fields

# this is used to wrap the Input/Output class of the ContractFunc
from pydantic import BaseModel

# these are the main types used in building a Contract
from eth_rpc import ContractFunc, ProtocolBase, Struct
from eth_rpc.types import METHOD, primitives

Struct Definitions#

Then we can define the internal types. The Status Enum and the Account and Transaction Structs. You need to define your fields as primitives/Structs, with the exception of bool and bytes. Likewise if there are no args or return type you can provide None to indicate this. Make sure to use the Struct type when referring to an onchain Struct or tuple, this helps the encoder/decoder to understand the onchain structure of the function.

class Status(IntEnum):
    SUCCESS = 0
    FAILURE = 1


class Account(Struct):
    account_id: Annotated[
        primitives.uint256,
        Name("accountId"),
    ]
    balance: primitives.uint256
    is_active: Annotated[
        bool,
        Name("isActive"),
    ]

class Transaction(Struct):
    recipient_id: Annotated[
        primitives.uint256,
        Name("recipientId"),
    ]
    amount: primitives.uint256


class DepositResult(Struct):
    balance_updated: Annotated[
        primitives.int256,
        Name("balanceUpdate"),
    ]
    status: Status

Defining the Input and Output types#

Then we can define the objects for the Request and Response types. Notice these are not Structs. That is because the encoder for a contract will encode a Struct argument differently from a list of parameters. This is by design, so ensure you are only using Structs to abstract actual struct types.

For functions that take a single argument, you can provide the raw type. For example, you could define a function balance: ContractFunc[primitives.address, primitives.uint256] for a function that takes an address as an argument and returns a uint256.

class DepositRequest(BaseModel):
    account: Account
    txs: list[Transaction]
    deadline: primitives.uint256
    additional_args: Annotated[
        list[bytes],
        Name("additionalArgs"),
    ]


class DepositResponse(BaseModel):
    success: bool
    result: DepositResult

Building the Contract#

With our types defined, we can now define the function depositFunds as a python function with type hints. The METHOD is important because it indicates to the type checker this function does not require initialization. If you change from CamelCase to snake_case, make sure to annotate the function with its name onchain, as this is used to calculate the function selector.

class ExampleContract(ProtocolBase):
    deposit_funds: Annotated[
        ContractFunc[
            DepositRequest,
            DepositResponse,
        ],
        Name("depositFunc"),
     ] = METHOD

contract = ExampleContract(address=contract_address)

# this response object is now parsed back into a DepositResponse
response: DepositResponse = await contract.deposit_funds(
    DepositRequest(
        account=Account(account_id=1, balance=100, is_active=True),
        deadline=primitives.uint256(12345),
        additional_args=[b'123', b'456'],
    )
).get()