Source code for erc7730.generate.schema_tree
from abc import ABC
from typing import assert_never
import eth_abi
from eip712.model.schema import EIP712SchemaField, EIP712Type
from eth_abi.grammar import BasicType, TupleType
from pydantic import Field
from erc7730.common.abi import ABIDataType
from erc7730.model.abi import Component, Function, InputOutput
from erc7730.model.base import Model
from erc7730.model.context import EIP712Schema
[docs]
class SchemaNode(Model, ABC):
"""Represents a node in the tree defined by a function ABI / EIP-712 schema."""
[docs]
class SchemaStruct(SchemaNode):
"""Schema node representing a function or a tuple."""
components: dict[str, "SchemaTree"] = Field(title="Struct components")
[docs]
class SchemaArray(SchemaNode):
"""Schema node representing an array."""
component: "SchemaTree" = Field(title="Array element type")
[docs]
class SchemaLeaf(SchemaNode):
"""Schema node representing a scalar type."""
data_type: ABIDataType = Field(title="Data type")
SchemaTree = SchemaStruct | SchemaArray | SchemaLeaf
[docs]
def eip712_schema_to_tree(schema: EIP712Schema) -> SchemaTree:
"""
Convert an EIP-712 schema to a schema tree.
A schema tree is a tree representation of the EIP-712 schema, enriched with some metadata to ease crafting
paths to access values in the message.
:param schema: EIP-712 schema
:return: Schema tree
"""
if (primary_type_fields := schema.types.get(schema.primaryType)) is None:
raise ValueError("Primary type not found in schema types")
return _eip712_struct_type_to_tree(fields=primary_type_fields, types=schema.types)
def _eip712_struct_type_to_tree(
fields: list[EIP712SchemaField], types: dict[EIP712Type, list[EIP712SchemaField]]
) -> SchemaTree:
"""
Convert a complex EIP-712 type to a schema tree node (can be the primary type directly).
:param fields: type fields
:param types: all schema types
:return: Schema tree
"""
return SchemaStruct(components={field.name: _eip712_field_to_tree(field, types) for field in fields})
def _eip712_field_to_tree(field: EIP712SchemaField, types: dict[EIP712Type, list[EIP712SchemaField]]) -> SchemaTree:
"""
Convert an EIP-712 type to a schema tree node.
:param field: ABI element (can be a single component, or a function input)
:param types: all schema types
:return: Schema tree
"""
match eth_abi.grammar.parse(field.type):
case TupleType():
return _eip712_struct_type_to_tree(types[field.type], types)
case BasicType() as tp:
if tp.is_array:
match tp.base:
case "tuple" | "struct":
component = _eip712_struct_type_to_tree(types[field.type], types)
case _:
component = _eip712_field_to_tree(
field=field.model_copy(update={"type": tp.item_type.to_type_str()}), types=types
)
return SchemaArray(component=component)
match tp.base:
case "tuple" | "struct":
return _eip712_struct_type_to_tree(types[field.type], types)
case base if base in set(ABIDataType):
type_family = ABIDataType(base)
case base_type:
if (base_type_fields := types.get(base_type)) is None:
raise Exception(f"Unexpected EIP-712 type: {base_type}")
return _eip712_struct_type_to_tree(base_type_fields, types)
return SchemaLeaf(data_type=type_family)
case unknown:
raise Exception(f"Unexpected EIP-712 type: {type(unknown)}")
[docs]
def abi_function_to_tree(function: Function) -> SchemaTree:
"""
Convert a function ABI to a schema tree.
A schema 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: Schema tree
"""
return _abi_struct_component_to_tree(function)
def _abi_struct_component_to_tree(inp: Function | InputOutput | Component) -> SchemaTree:
"""
Convert a struct-like ABI component to a schema tree node (can be the top level function directly).
:param inp: ABI element
:return: Schema 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 list() as eip712_fields:
input_components.extend(eip712_fields)
case _:
assert_never(inp)
return SchemaStruct(
components={component.name: _abi_component_to_tree(component) for component in input_components}
)
def _abi_component_to_tree(inp: InputOutput | Component) -> SchemaTree:
"""
Convert an ABI component to a schema tree node.
:param inp: ABI element (can be a single component, or a function input)
:return: Schema tree
"""
match eth_abi.grammar.parse(inp.type):
case TupleType():
return _abi_struct_component_to_tree(inp)
case BasicType() as tp:
if tp.is_array:
match tp.base:
case "tuple" | "struct":
component = _abi_struct_component_to_tree(inp)
case _:
component = _abi_component_to_tree(inp.model_copy(update={"type": tp.item_type.to_type_str()}))
return SchemaArray(component=component)
match tp.base:
case "tuple" | "struct":
return _abi_struct_component_to_tree(inp)
case base if base in set(ABIDataType):
type_family = ABIDataType(base)
case unknown:
raise Exception(f"Unexpected ABI type: {unknown}")
return SchemaLeaf(data_type=type_family)
case unknown:
raise Exception(f"Unexpected ABI type: {type(unknown)}")