Source code for erc7730.convert.calldata.v1.abi
"""
Conversion of a function ABI to an ABI tree.
An ABI tree is a tree representation of the ABI of a function inputs, enriched with some metadata to ease crafting
paths to access values in the serialized calldata.
"""
from abc import ABC, abstractmethod
from typing import Annotated, Literal, assert_never, override
import eth_abi
from eth_abi.grammar import BasicType, TupleType
from pydantic import Field
from erc7730.model.abi import Component, Function, InputOutput
from erc7730.model.base import Model
from erc7730.model.calldata.v1.value import (
CalldataDescriptorTypeFamily,
)
[docs]
class ABINode(Model, ABC):
"""Represents a node in the tree defined by a function ABI."""
@property
@abstractmethod
def is_dynamic(self) -> bool:
raise NotImplementedError()
@property
@abstractmethod
def size(self) -> int:
raise NotImplementedError()
[docs]
class ABILeafNode(ABINode, ABC):
"""Represents a leaf node in the tree defined by a function ABI."""
type_family: CalldataDescriptorTypeFamily = Field(title="Data type family")
type_size: int | None = Field(default=None, title="Data type size (in bytes)")
[docs]
class ABIStruct(ABINode):
"""ABI node representing a function or a tuple."""
type: Literal["struct"] = Field(default="struct", title="ABI tree node type")
components: dict[str, "ABITree"] = Field(title="Struct components")
offsets: dict[str, int] = Field(title="Struct components offsets")
@override
@property
def is_dynamic(self) -> bool:
return any(comp.is_dynamic for comp in self.components.values())
@override
@property
def size(self) -> int:
return 1 if self.is_dynamic else sum(comp.size for comp in self.components.values())
[docs]
class ABIStaticArray(ABINode):
"""ABI node representing an array with static size."""
type: Literal["static_array"] = Field(default="static_array", title="ABI tree node type")
dimension: int = Field(title="Array dimension", ge=0)
component: "ABITree" = Field(title="Array element type")
@override
@property
def is_dynamic(self) -> bool:
return self.component.is_dynamic
@override
@property
def size(self) -> int:
return 1 if self.is_dynamic else self.dimension * self.component.size
[docs]
class ABIDynamicArray(ABINode):
"""ABI node representing an array with dynamic size."""
type: Literal["dynamic_array"] = Field(default="dynamic_array", title="ABI tree node type")
component: "ABITree" = Field(title="Array element type")
@override
@property
def is_dynamic(self) -> bool:
return True
@override
@property
def size(self) -> int:
return 1
[docs]
class ABIStaticLeaf(ABILeafNode):
"""ABI node representing a scalar type with static size."""
type: Literal["static_leaf"] = Field(default="static_leaf", title="ABI tree node type")
@override
@property
def is_dynamic(self) -> bool:
return False
@override
@property
def size(self) -> int:
return 1
[docs]
class ABIDynamicLeaf(ABILeafNode):
"""ABI node representing a scalar type with dynamic size."""
type: Literal["dynamic_leaf"] = Field(default="dynamic_leaf", title="ABI tree node type")
@override
@property
def is_dynamic(self) -> bool:
return True
@override
@property
def size(self) -> int:
return 1
ABITree = Annotated[
ABIStruct | ABIStaticArray | ABIDynamicArray | ABIStaticLeaf | ABIDynamicLeaf, Field(discriminator="type")
]
[docs]
def function_to_abi_tree(function: Function) -> ABITree:
"""
Convert a function ABI to an ABI tree.
An ABI tree is a tree representation of the ABI of a function inputs, enriched with some metadata to ease crafting
paths to access values in the serialized calldata.
@param function: function ABI
@return:
"""
return _struct_component_to_abi_tree(function)
def _component_to_abi_tree(inp: InputOutput | Component) -> ABITree:
"""
Convert an ABI component to an ABI tree node.
@param inp: ABI element (can be a single component, or a function input)
@return: ABI tree
"""
match eth_abi.grammar.parse(inp.type):
case TupleType():
return _struct_component_to_abi_tree(inp)
case BasicType() as tp:
if tp.is_array:
match tp.base:
case "tuple" | "struct":
component = _struct_component_to_abi_tree(inp)
case _:
component = _component_to_abi_tree(inp.model_copy(update={"type": tp.item_type.to_type_str()}))
if len(dimension := tp.arrlist[-1]) == 0:
return ABIDynamicArray(component=component)
else:
return ABIStaticArray(component=component, dimension=dimension[0])
match tp.base:
case "tuple" | "struct":
return _struct_component_to_abi_tree(inp)
case "int":
type_family = CalldataDescriptorTypeFamily.INT
type_size = (tp.sub or 256) // 8
case "uint":
type_family = CalldataDescriptorTypeFamily.UINT
type_size = (tp.sub or 256) // 8
case "address":
type_family = CalldataDescriptorTypeFamily.ADDRESS
type_size = (tp.sub or 160) // 8
case "bool":
type_family = CalldataDescriptorTypeFamily.BOOL
type_size = (tp.sub or 8) // 8
case "bytes":
type_family = CalldataDescriptorTypeFamily.BYTES
type_size = tp.sub // 8 if tp.sub else None
case "string":
type_family = CalldataDescriptorTypeFamily.STRING
type_size = tp.sub // 8 if tp.sub else None
case "fixed" | "ufixed":
raise NotImplementedError("Fixed precision numbers are not supported by v1 of calldata descriptor")
case unknown:
raise Exception(f"Unexpected ABI type: {unknown}")
if tp.is_dynamic:
return ABIDynamicLeaf(type_family=type_family, type_size=type_size)
else:
return ABIStaticLeaf(type_family=type_family, type_size=type_size)
case unknown:
raise Exception(f"Unexpected ABI type: {type(unknown)}")
def _struct_component_to_abi_tree(inp: Function | InputOutput | Component) -> ABITree:
"""
Convert a struct-like ABI component to an ABI tree node (can be the top level function directly).
@param inp: ABI element
@return: ABI tree
"""
# get inputs/components list based on argument type
input_components: list[InputOutput | Component] = []
match inp:
case Function(inputs=inputs):
if inputs is not None:
input_components.extend(inputs)
case InputOutput(components=inp_components):
if inp_components is not None:
input_components.extend(inp_components)
case Component(components=inp_components):
if inp_components is not None:
input_components.extend(inp_components)
case _:
assert_never(inp)
# recurse and compute field offsets
components: dict[str, ABITree] = {}
offsets: dict[str, int] = {}
offset = 0
for _, component in enumerate(input_components):
node = _component_to_abi_tree(component)
components[component.name] = node
offsets[component.name] = offset
offset += node.size
return ABIStruct(components=components, offsets=offsets)