This commit is contained in:
lz_db
2025-11-16 12:31:03 +08:00
commit 0fab423a18
1451 changed files with 743213 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
# PayloadSerializer and FunctionSerializationAdapter would mostly be used by users
from .data_serializers import (
ArraySerializer,
CairoDataSerializer,
FeltSerializer,
NamedTupleSerializer,
PayloadSerializer,
StructSerializer,
TupleSerializer,
Uint256Serializer,
)
from .errors import (
CairoSerializerException,
InvalidTypeException,
InvalidValueException,
)
from .factory import (
serializer_for_event,
serializer_for_function,
serializer_for_payload,
serializer_for_type,
)
from .function_serialization_adapter import FunctionSerializationAdapter
from .tuple_dataclass import TupleDataclass

View File

@@ -0,0 +1,40 @@
from typing import List
from ..cairo.felt import CairoData
class OutOfBoundsError(Exception):
def __init__(self, position: int, requested_size: int, remaining_size: int):
super().__init__(
f"Requested {requested_size} elements, {remaining_size} available."
)
self.position = position
self.requested_size = requested_size
self.remaining_len = remaining_size
class CalldataReader:
_data: List[int]
_position: int
def __init__(self, data: List[int]):
self._data = data
self._position = 0
@property
def remaining_len(self) -> int:
return len(self._data) - self._position
def read(self, size: int) -> CairoData:
if size < 1:
raise ValueError("size must be greater than 0")
if size > self.remaining_len:
raise OutOfBoundsError(
position=self._position,
requested_size=size,
remaining_size=self.remaining_len,
)
data = self._data[self._position : self._position + size]
self._position += size
return data

View File

@@ -0,0 +1,142 @@
from __future__ import annotations
from abc import ABC
from contextlib import contextmanager
from typing import Any, Generator, Iterator, List
from ._calldata_reader import (
CairoData,
CalldataReader,
OutOfBoundsError,
)
from .errors import InvalidTypeException, InvalidValueException
class Context(ABC):
"""
Holds information about context when (de)serializing data. This is needed to inform what and where went
wrong during processing. Every separate (de)serialization should have its own context.
"""
_namespace_stack: List[str]
def __init__(self):
self._namespace_stack = []
@property
def current_entity(self):
"""
Name of currently processed entity.
:return: transformed path.
"""
return ".".join(self._namespace_stack)
@contextmanager
def push_entity(self, name: str) -> Generator:
"""
Manager used for maintaining information about names of (de)serialized types. Wraps some errors with
custom errors, adding information about the context.
:param name: name of (de)serialized entity.
"""
# This ensures the name will be popped if everything is ok. In case an exception is raised we want the stack to
# be filled to wrap the error at the end.
self._namespace_stack.append(name)
yield
self._namespace_stack.pop()
def ensure_valid_value(self, valid: bool, text: str):
if not valid:
raise InvalidValueException(f"{self._error_prefix}: {text}.")
def ensure_valid_type(self, value: Any, valid: bool, expected_type: str):
if not valid:
raise InvalidTypeException(
f"{self._error_prefix}: expected {expected_type}, "
f"received '{value}' of type '{type(value)}'."
)
@contextmanager
def _wrap_errors(self):
try:
yield
except OutOfBoundsError as err:
action_name = (
f"deserialize '{self.current_entity}'"
if self._namespace_stack
else "deserialize"
)
# This way we can precisely inform user what's wrong when reading calldata.
raise InvalidValueException(
f"Not enough data to {action_name}. "
f"Can't read {err.requested_size} values at position {err.position}, {err.remaining_len} available."
) from err
# Those two are based on ValueError and TypeError, we have to catch them early
except (InvalidValueException, InvalidTypeException) as err:
raise err
except ValueError as err:
raise InvalidValueException(f"{self._error_prefix}: {err}") from err
except TypeError as err:
raise InvalidTypeException(f"{self._error_prefix}: {err}") from err
@property
def _error_prefix(self):
if not self._namespace_stack:
return "Error"
return f"Error at path '{self.current_entity}'"
class SerializationContext(Context):
"""
Context used during serialization.
"""
# Type is iterator, because ContextManager doesn't work with pyright :|
# https://github.com/microsoft/pyright/issues/476
@classmethod
@contextmanager
def create(cls) -> Iterator[SerializationContext]:
context = cls()
with context._wrap_errors():
yield context
class DeserializationContext(Context):
"""
Context used during deserialization.
"""
reader: CalldataReader
def __init__(self, calldata: CairoData):
"""
Don't use default constructor. Use DeserializationContext.create context manager.
"""
super().__init__()
self._namespace_stack = []
self.reader = CalldataReader(calldata)
@classmethod
@contextmanager
def create(cls, data: CairoData) -> Iterator[DeserializationContext]:
context = cls(data)
with context._wrap_errors():
yield context
context._ensure_all_values_read(len(data))
def _ensure_all_values_read(self, total_len: int):
values_not_used = self.reader.remaining_len
if values_not_used != 0:
# We want to output up to 3 values. It there is more they will be truncated like "0x1,0x1,0x1..."
max_values_to_show = 3
values_to_show = min(values_not_used, max_values_to_show)
example = ",".join(hex(v) for v in self.reader.read(values_to_show))
suffix = "..." if values_not_used > max_values_to_show else ""
raise InvalidValueException(
f"Last {values_not_used} values '{example}{suffix}' out of total {total_len} "
"values were not used during deserialization."
)

View File

@@ -0,0 +1,10 @@
from .array_serializer import ArraySerializer
from .bool_serializer import BoolSerializer
from .byte_array_serializer import ByteArraySerializer
from .cairo_data_serializer import CairoDataSerializer
from .felt_serializer import FeltSerializer
from .named_tuple_serializer import NamedTupleSerializer
from .payload_serializer import PayloadSerializer
from .struct_serializer import StructSerializer
from .tuple_serializer import TupleSerializer
from .uint256_serializer import Uint256Serializer

View File

@@ -0,0 +1,82 @@
# We have to use parametrised type from typing
from collections import OrderedDict as _OrderedDict
from typing import Dict, Generator, List, OrderedDict
from .._context import (
DeserializationContext,
SerializationContext,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
# The actual serialization logic is very similar among all serializers: they either serialize data based on
# position or their name. Having this logic reused adds indirection, but makes sure proper logic is used everywhere.
def deserialize_to_list(
deserializers: List[CairoDataSerializer], context: DeserializationContext
) -> List:
"""
Deserializes data from context to list. This logic is used in every sequential type (arrays and tuples).
"""
result = []
for index, serializer in enumerate(deserializers):
with context.push_entity(f"[{index}]"):
result.append(serializer.deserialize_with_context(context))
return result
def deserialize_to_dict(
deserializers: OrderedDict[str, CairoDataSerializer],
context: DeserializationContext,
) -> OrderedDict:
"""
Deserializes data from context to dictionary. This logic is used in every type with named fields (structs,
named tuples and payloads).
"""
result = _OrderedDict()
for key, serializer in deserializers.items():
with context.push_entity(key):
result[key] = serializer.deserialize_with_context(context)
return result
def serialize_from_list(
serializers: List[CairoDataSerializer], context: SerializationContext, values: List
) -> Generator[int, None, None]:
"""
Serializes data from list. This logic is used in every sequential type (arrays and tuples).
"""
context.ensure_valid_value(
len(serializers) == len(values),
f"expected {len(serializers)} elements, {len(values)} provided",
)
for index, (serializer, value) in enumerate(zip(serializers, values)):
with context.push_entity(f"[{index}]"):
yield from serializer.serialize_with_context(context, value)
def serialize_from_dict(
serializers: OrderedDict[str, CairoDataSerializer],
context: SerializationContext,
values: Dict,
) -> Generator[int, None, None]:
"""
Serializes data from dict. This logic is used in every type with named fields (structs, named tuples and payloads).
"""
excessive_keys = set(values.keys()).difference(serializers.keys())
context.ensure_valid_value(
not excessive_keys,
f"unexpected keys '{','.join(excessive_keys)}' were provided",
)
for name, serializer in serializers.items():
with context.push_entity(name):
context.ensure_valid_value(name in values, f"key '{name}' is missing")
yield from serializer.serialize_with_context(context, values[name])

View File

@@ -0,0 +1,43 @@
from dataclasses import dataclass
from typing import Generator, Iterable, List
from .._context import (
DeserializationContext,
SerializationContext,
)
from ..data_serializers._common import (
deserialize_to_list,
serialize_from_list,
)
from ..data_serializers.cairo_data_serializer import (
CairoDataSerializer,
)
@dataclass
class ArraySerializer(CairoDataSerializer[Iterable, List]):
"""
Serializer for arrays. In abi they are represented as a pointer to a type.
Can serialize any iterable and prepends its length to resulting list.
Deserializes data to a list.
Examples:
[1,2,3] => [3,1,2,3]
[] => [0]
"""
inner_serializer: CairoDataSerializer
def deserialize_with_context(self, context: DeserializationContext) -> List:
with context.push_entity("len"):
[size] = context.reader.read(1)
return deserialize_to_list([self.inner_serializer] * size, context)
def serialize_with_context(
self, context: SerializationContext, value: List
) -> Generator[int, None, None]:
yield len(value)
yield from serialize_from_list(
[self.inner_serializer] * len(value), context, value
)

View File

@@ -0,0 +1,37 @@
from dataclasses import dataclass
from typing import Generator
from .._context import (
Context,
DeserializationContext,
SerializationContext,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
@dataclass
class BoolSerializer(CairoDataSerializer[bool, int]):
"""
Serializer for boolean.
"""
def deserialize_with_context(self, context: DeserializationContext) -> bool:
[val] = context.reader.read(1)
self._ensure_bool(context, val)
return bool(val)
def serialize_with_context(
self, context: SerializationContext, value: bool
) -> Generator[int, None, None]:
context.ensure_valid_type(value, isinstance(value, bool), "bool")
self._ensure_bool(context, value)
yield int(value)
@staticmethod
def _ensure_bool(context: Context, value: int):
context.ensure_valid_value(
value in [0, 1],
f"invalid value '{value}' - must be in [0, 2) range",
)

View File

@@ -0,0 +1,66 @@
from dataclasses import dataclass
from typing import Generator
from ...cairo.felt import decode_shortstring, encode_shortstring
from .._context import (
DeserializationContext,
SerializationContext,
)
from ._common import (
deserialize_to_list,
serialize_from_list,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
from .felt_serializer import FeltSerializer
BYTES_31_SIZE = 31
@dataclass
class ByteArraySerializer(CairoDataSerializer[str, str]):
"""
Serializer for ByteArrays. Serializes to and deserializes from str values.
Examples:
"" => [0,0,0]
"hello" => [0,448378203247,5]
"""
def deserialize_with_context(self, context: DeserializationContext) -> str:
with context.push_entity("data_array_len"):
[size] = context.reader.read(1)
data = deserialize_to_list([FeltSerializer()] * size, context)
with context.push_entity("pending_word"):
[pending_word] = context.reader.read(1)
with context.push_entity("pending_word_len"):
[pending_word_len] = context.reader.read(1)
pending_word = decode_shortstring(pending_word)
context.ensure_valid_value(
len(pending_word) == pending_word_len,
f"Invalid length {pending_word_len} for pending word {pending_word}",
)
data_joined = "".join(map(decode_shortstring, data))
return data_joined + pending_word
def serialize_with_context(
self, context: SerializationContext, value: str
) -> Generator[int, None, None]:
context.ensure_valid_type(value, isinstance(value, str), "str")
data = [
value[i : i + BYTES_31_SIZE] for i in range(0, len(value), BYTES_31_SIZE)
]
pending_word = (
"" if len(data) == 0 or len(data[-1]) == BYTES_31_SIZE else data.pop(-1)
)
yield len(data)
yield from serialize_from_list([FeltSerializer()] * len(data), context, data)
yield encode_shortstring(pending_word)
yield len(pending_word)

View File

@@ -0,0 +1,71 @@
from abc import ABC, abstractmethod
from typing import Generator, Generic, List, TypeVar
from .._calldata_reader import CairoData
from .._context import (
DeserializationContext,
SerializationContext,
)
# Python type that is accepted by a serializer
# pylint: disable=invalid-name
SerializationType = TypeVar("SerializationType")
# Python type that will be returned from a serializer. Often same as SerializationType.
# pylint: disable=invalid-name
DeserializationType = TypeVar("DeserializationType")
class CairoDataSerializer(ABC, Generic[SerializationType, DeserializationType]):
"""
Base class for serializing/deserializing data to/from calldata.
"""
def deserialize(self, data: List[int]) -> DeserializationType:
"""
Transform calldata into python value.
:param data: calldata to deserialize.
:return: defined DeserializationType.
"""
with DeserializationContext.create(data) as context:
return self.deserialize_with_context(context)
def serialize(self, data: SerializationType) -> CairoData:
"""
Transform python data into calldata.
:param data: data to serialize.
:return: calldata.
"""
with SerializationContext.create() as context:
serialized_data = list(self.serialize_with_context(context, data))
return self.remove_units_from_serialized_data(serialized_data)
@abstractmethod
def deserialize_with_context(
self, context: DeserializationContext
) -> DeserializationType:
"""
Transform calldata into python value.
:param context: context of this deserialization.
:return: defined DeserializationType.
"""
@abstractmethod
def serialize_with_context(
self, context: SerializationContext, value: SerializationType
) -> Generator[int, None, None]:
"""
Transform python value into calldata.
:param context: context of this serialization.
:param value: python value to serialize.
:return: defined SerializationType.
"""
@staticmethod
def remove_units_from_serialized_data(serialized_data: List) -> List:
return [x for x in serialized_data if x is not None]

View File

@@ -0,0 +1,71 @@
from dataclasses import dataclass
from typing import Dict, Generator, OrderedDict, Tuple, Union
from .._context import (
DeserializationContext,
SerializationContext,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
from ..tuple_dataclass import TupleDataclass
@dataclass
class EnumSerializer(CairoDataSerializer[Union[Dict, TupleDataclass], TupleDataclass]):
"""
Serializer of enums.
Can serialize a dictionary and TupleDataclass.
Deserializes data to a TupleDataclass.
Example:
enum MyEnum {
a: u128,
b: u128
}
{"a": 1} => [0, 1]
{"b": 100} => [1, 100]
TupleDataclass(variant='a', value=100) => [0, 100]
"""
serializers: OrderedDict[str, CairoDataSerializer]
def deserialize_with_context(
self, context: DeserializationContext
) -> TupleDataclass:
[variant_index] = context.reader.read(1)
variant_name, serializer = self._get_variant(variant_index)
with context.push_entity("enum.variant: " + variant_name):
result_dict = {
"variant": variant_name,
"value": serializer.deserialize_with_context(context),
}
return TupleDataclass.from_dict(result_dict)
def serialize_with_context(
self, context: SerializationContext, value: Union[Dict, TupleDataclass]
) -> Generator[int, None, None]:
if isinstance(value, Dict):
items = list(value.items())
if len(items) != 1:
raise ValueError(
"Can serialize only one enum variant, got: " + str(len(items))
)
variant_name, variant_value = items[0]
else:
variant_name, variant_value = value
yield self._get_variant_index(variant_name)
yield from self.serializers[variant_name].serialize_with_context(
context, variant_value
)
def _get_variant(self, variant_index: int) -> Tuple[str, CairoDataSerializer]:
return list(self.serializers.items())[variant_index]
def _get_variant_index(self, variant_name: str) -> int:
return list(self.serializers.keys()).index(variant_name)

View File

@@ -0,0 +1,50 @@
import warnings
from dataclasses import dataclass
from typing import Generator
from ...cairo.felt import encode_shortstring, is_in_felt_range
from ...constants import FIELD_PRIME
from .._context import (
Context,
DeserializationContext,
SerializationContext,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
@dataclass
class FeltSerializer(CairoDataSerializer[int, int]):
"""
Serializer for field element. At the time of writing it is the only existing numeric type.
"""
def deserialize_with_context(self, context: DeserializationContext) -> int:
[val] = context.reader.read(1)
self._ensure_felt(context, val)
return val
def serialize_with_context(
self, context: SerializationContext, value: int
) -> Generator[int, None, None]:
if isinstance(value, str):
warnings.warn(
"Serializing shortstrings in FeltSerializer is deprecated. "
"Use starknet_py.cairo.felt.encode_shortstring instead.",
category=DeprecationWarning,
)
value = encode_shortstring(value)
yield value
return
context.ensure_valid_type(value, isinstance(value, int), "int")
self._ensure_felt(context, value)
yield value
@staticmethod
def _ensure_felt(context: Context, value: int):
context.ensure_valid_value(
is_in_felt_range(value),
f"invalid value '{value}' - must be in [0, {FIELD_PRIME}) range",
)

View File

@@ -0,0 +1,58 @@
from dataclasses import dataclass
from typing import Dict, Generator, NamedTuple, OrderedDict, Union
from .._context import (
DeserializationContext,
SerializationContext,
)
from ._common import (
deserialize_to_dict,
serialize_from_dict,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
from ..tuple_dataclass import TupleDataclass
@dataclass
class NamedTupleSerializer(
CairoDataSerializer[Union[Dict, NamedTuple, TupleDataclass], TupleDataclass]
):
"""
Serializer for tuples with named fields.
Can serialize a dictionary, a named tuple and TupleDataclass.
Deserializes data to a TupleDataclass.
Example:
{"a": 1, "b": 2} => [1,2]
"""
serializers: OrderedDict[str, CairoDataSerializer]
def deserialize_with_context(
self, context: DeserializationContext
) -> TupleDataclass:
as_dictionary = deserialize_to_dict(self.serializers, context)
return TupleDataclass.from_dict(as_dictionary)
def serialize_with_context(
self,
context: SerializationContext,
value: Union[Dict, NamedTuple, TupleDataclass],
) -> Generator[int, None, None]:
# We can't use isinstance(value, NamedTuple), because there is no NamedTuple type.
context.ensure_valid_type(
value,
isinstance(value, (dict, TupleDataclass)) or self._is_namedtuple(value),
"dict, NamedTuple or TupleDataclass",
)
# noinspection PyUnresolvedReferences, PyProtectedMember
values: Dict = value if isinstance(value, dict) else value._asdict()
yield from serialize_from_dict(self.serializers, context, values)
@staticmethod
def _is_namedtuple(value) -> bool:
return isinstance(value, tuple) and hasattr(value, "_fields")

View File

@@ -0,0 +1,43 @@
from dataclasses import dataclass
from typing import Any, Generator, Optional
from .._context import (
DeserializationContext,
SerializationContext,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
@dataclass
class OptionSerializer(CairoDataSerializer[Optional[Any], Optional[Any]]):
"""
Serializer for Option type.
Can serialize None and common CairoTypes.
Deserializes data to None or CairoType.
Example:
None => [1]
{"option1": 123, "option2": None} => [0, 123, 1]
"""
serializer: CairoDataSerializer
def deserialize_with_context(
self, context: DeserializationContext
) -> Optional[Any]:
(is_none,) = context.reader.read(1)
if is_none == 1:
return None
return self.serializer.deserialize_with_context(context)
def serialize_with_context(
self, context: SerializationContext, value: Optional[Any]
) -> Generator[int, None, None]:
if value is None:
yield 1
else:
yield 0
yield from self.serializer.serialize_with_context(context, value)

View File

@@ -0,0 +1,40 @@
from dataclasses import dataclass, field
from typing import Dict, Generator, List, Tuple
from .._context import (
DeserializationContext,
SerializationContext,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
@dataclass
class OutputSerializer(CairoDataSerializer[List, Tuple]):
"""
Serializer for function output.
Can't serialize anything.
Deserializes data to a Tuple.
Example:
[1, 1, 1] => (340282366920938463463374607431768211457)
"""
serializers: List[CairoDataSerializer] = field(init=True)
def deserialize_with_context(self, context: DeserializationContext) -> Tuple:
result = []
for index, serializer in enumerate(self.serializers):
with context.push_entity("output[" + str(index) + "]"):
result.append(serializer.deserialize_with_context(context))
return tuple(result)
def serialize_with_context(
self, context: SerializationContext, value: Dict
) -> Generator[int, None, None]:
raise ValueError(
"Output serializer can't be used to transform python data into calldata."
)

View File

@@ -0,0 +1,72 @@
from collections import OrderedDict as _OrderedDict
from dataclasses import InitVar, dataclass, field
from typing import Dict, Generator, OrderedDict
from .._context import (
DeserializationContext,
SerializationContext,
)
from ._common import (
deserialize_to_dict,
serialize_from_dict,
)
from .array_serializer import ArraySerializer
from .cairo_data_serializer import (
CairoDataSerializer,
)
from .felt_serializer import FeltSerializer
from ..tuple_dataclass import TupleDataclass
SIZE_SUFFIX = "_len"
SIZE_SUFFIX_LEN = len(SIZE_SUFFIX)
@dataclass
class PayloadSerializer(CairoDataSerializer[Dict, TupleDataclass]):
"""
Serializer for payloads like function arguments/function outputs/events.
Can serialize a dictionary.
Deserializes data to a TupleDataclass.
Example:
{"a": 1, "b": 2} => [1,2]
"""
# Value present only in constructor.
# We don't want to mutate the serializers received in constructor.
input_serializers: InitVar[OrderedDict[str, CairoDataSerializer]]
serializers: OrderedDict[str, CairoDataSerializer] = field(init=False)
def __post_init__(self, input_serializers):
"""
ABI adds ARG_len for every argument ARG that is an array. We parse length as a part of ArraySerializer, so we
need to remove those lengths from args.
"""
self.serializers = _OrderedDict(
(key, serializer)
for key, serializer in input_serializers.items()
if not self._is_len_arg(key, input_serializers)
)
def deserialize_with_context(
self, context: DeserializationContext
) -> TupleDataclass:
as_dictionary = deserialize_to_dict(self.serializers, context)
return TupleDataclass.from_dict(as_dictionary)
def serialize_with_context(
self, context: SerializationContext, value: Dict
) -> Generator[int, None, None]:
yield from serialize_from_dict(self.serializers, context, value)
@staticmethod
def _is_len_arg(arg_name: str, serializers: Dict[str, CairoDataSerializer]) -> bool:
return (
arg_name.endswith(SIZE_SUFFIX)
and isinstance(serializers[arg_name], FeltSerializer)
# There is an ArraySerializer under key that is arg_name without the size suffix
and isinstance(
serializers.get(arg_name[:-SIZE_SUFFIX_LEN]), ArraySerializer
)
)

View File

@@ -0,0 +1,36 @@
from dataclasses import dataclass
from typing import Dict, Generator, OrderedDict
from .._context import (
DeserializationContext,
SerializationContext,
)
from ._common import (
deserialize_to_dict,
serialize_from_dict,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
@dataclass
class StructSerializer(CairoDataSerializer[Dict, Dict]):
"""
Serializer of custom structures.
Can serialize a dictionary.
Deserializes data to a dictionary.
Example:
{"a": 1, "b": 2} => [1,2]
"""
serializers: OrderedDict[str, CairoDataSerializer]
def deserialize_with_context(self, context: DeserializationContext) -> Dict:
return deserialize_to_dict(self.serializers, context)
def serialize_with_context(
self, context: SerializationContext, value: Dict
) -> Generator[int, None, None]:
yield from serialize_from_dict(self.serializers, context, value)

View File

@@ -0,0 +1,36 @@
from dataclasses import dataclass
from typing import Generator, Iterable, List, Tuple
from .._context import (
DeserializationContext,
SerializationContext,
)
from ._common import (
deserialize_to_list,
serialize_from_list,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
@dataclass
class TupleSerializer(CairoDataSerializer[Iterable, Tuple]):
"""
Serializer for tuples without named fields.
Can serialize any iterable.
Deserializes data to a python tuple.
Example:
(1,2,(3,4)) => [1,2,3,4]
"""
serializers: List[CairoDataSerializer]
def deserialize_with_context(self, context: DeserializationContext) -> Tuple:
return tuple(deserialize_to_list(self.serializers, context))
def serialize_with_context(
self, context: SerializationContext, value: Iterable
) -> Generator[int, None, None]:
yield from serialize_from_list(self.serializers, context, [*value])

View File

@@ -0,0 +1,76 @@
from dataclasses import dataclass
from typing import Generator, TypedDict, Union
from ...cairo.felt import uint256_range_check
from .._context import (
Context,
DeserializationContext,
SerializationContext,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
U128_UPPER_BOUND = 2**128
class Uint256Dict(TypedDict):
low: int
high: int
@dataclass
class Uint256Serializer(CairoDataSerializer[Union[int, Uint256Dict], int]):
"""
Serializer of Uint256. In Cairo it is represented by structure {low: Uint128, high: Uint128}.
Can serialize an int.
Deserializes data to an int.
Examples:
0 => [0,0]
1 => [1,0]
2**128 => [0,1]
3 + 2**128 => [3,1]
"""
def deserialize_with_context(self, context: DeserializationContext) -> int:
[low, high] = context.reader.read(2)
# Checking if resulting value is in [0, 2**256) range is not enough. Uint256 should be made of two uint128.
with context.push_entity("low"):
self._ensure_valid_uint128(low, context)
with context.push_entity("high"):
self._ensure_valid_uint128(high, context)
return (high << 128) + low
def serialize_with_context(
self, context: SerializationContext, value: Union[int, Uint256Dict]
) -> Generator[int, None, None]:
context.ensure_valid_type(value, isinstance(value, (int, dict)), "int or dict")
if isinstance(value, int):
yield from self._serialize_from_int(value)
else:
yield from self._serialize_from_dict(context, value)
@staticmethod
def _serialize_from_int(value: int) -> Generator[int, None, None]:
uint256_range_check(value)
result = (value % 2**128, value // 2**128)
yield from result
def _serialize_from_dict(
self, context: SerializationContext, value: Uint256Dict
) -> Generator[int, None, None]:
with context.push_entity("low"):
self._ensure_valid_uint128(value["low"], context)
yield value["low"]
with context.push_entity("high"):
self._ensure_valid_uint128(value["high"], context)
yield value["high"]
@staticmethod
def _ensure_valid_uint128(value: int, context: Context):
context.ensure_valid_value(
0 <= value < U128_UPPER_BOUND, "expected value in range [0;2**128)"
)

View File

@@ -0,0 +1,100 @@
from dataclasses import dataclass
from typing import Generator, TypedDict, Union
from ...cairo.felt import uint256_range_check
from .._context import (
Context,
DeserializationContext,
SerializationContext,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
class Uint256Dict(TypedDict):
low: int
high: int
@dataclass
class UintSerializer(CairoDataSerializer[Union[int, Uint256Dict], int]):
"""
Serializer of uint. In Cairo there are few uints (u8, ..., u128 and u256).
u256 is represented by structure {low: u128, high: u128}.
Can serialize an int and dict.
Deserializes data to an int.
Examples:
if bits < 256:
0 => [0]
1 => [1]
2**128-1 => [2**128-1]
else:
0 => [0,0]
1 => [1,0]
2**128 => [0,1]
3 + 2**128 => [3,1]
"""
bits: int
def deserialize_with_context(self, context: DeserializationContext) -> int:
if self.bits < 256:
(uint,) = context.reader.read(1)
with context.push_entity("uint" + str(self.bits)):
self._ensure_valid_uint(uint, context, self.bits)
return uint
[low, high] = context.reader.read(2)
# Checking if resulting value is in [0, 2**256) range is not enough. Uint256 should be made of two uint128.
with context.push_entity("low"):
self._ensure_valid_uint(low, context, bits=128)
with context.push_entity("high"):
self._ensure_valid_uint(high, context, bits=128)
return (high << 128) + low
def serialize_with_context(
self, context: SerializationContext, value: Union[int, Uint256Dict]
) -> Generator[int, None, None]:
context.ensure_valid_type(value, isinstance(value, (int, dict)), "int or dict")
if isinstance(value, int):
yield from self._serialize_from_int(value, context, self.bits)
else:
yield from self._serialize_from_dict(context, value)
@staticmethod
def _serialize_from_int(
value: int, context: SerializationContext, bits: int
) -> Generator[int, None, None]:
if bits < 256:
UintSerializer._ensure_valid_uint(value, context, bits)
yield value
else:
uint256_range_check(value)
result = (value % 2**128, value >> 128)
yield from result
def _serialize_from_dict(
self, context: SerializationContext, value: Uint256Dict
) -> Generator[int, None, None]:
with context.push_entity("low"):
self._ensure_valid_uint(value["low"], context, bits=128)
yield value["low"]
with context.push_entity("high"):
self._ensure_valid_uint(value["high"], context, bits=128)
yield value["high"]
@staticmethod
def _ensure_valid_uint(value: int, context: Context, bits: int):
"""
Ensures that value is a valid uint on `bits` bits.
"""
context.ensure_valid_value(
0 <= value < 2**bits, "expected value in range [0;2**" + str(bits) + ")"
)

View File

@@ -0,0 +1,32 @@
from dataclasses import dataclass
from typing import Any, Generator, Optional
from .._context import (
DeserializationContext,
SerializationContext,
)
from .cairo_data_serializer import (
CairoDataSerializer,
)
@dataclass
class UnitSerializer(CairoDataSerializer[None, None]):
"""
Serializer for unit type.
Can only serialize None.
Deserializes data to None.
Example:
[] => None
"""
def deserialize_with_context(self, context: DeserializationContext) -> None:
return None
def serialize_with_context(
self, context: SerializationContext, value: Optional[Any]
) -> Generator[None, None, None]:
if value is not None:
raise ValueError("Can only serialize `None`.")
yield None

View File

@@ -0,0 +1,10 @@
class CairoSerializerException(Exception):
"""Exception thrown by CairoSerializer."""
class InvalidTypeException(CairoSerializerException, TypeError):
"""Exception thrown when invalid type was provided."""
class InvalidValueException(CairoSerializerException, ValueError):
"""Exception thrown when invalid value was provided."""

View File

@@ -0,0 +1,229 @@
from __future__ import annotations
from collections import OrderedDict
from typing import Dict, List, Union
from ..abi.v0 import Abi as AbiV0
from ..abi.v1 import Abi as AbiV1
from ..abi.v2 import Abi as AbiV2
from ..cairo.data_types import (
ArrayType,
BoolType,
CairoType,
EnumType,
EventType,
FeltType,
NamedTupleType,
OptionType,
StructType,
TupleType,
UintType,
UnitType,
)
from .data_serializers import (
BoolSerializer,
ByteArraySerializer,
)
from .data_serializers.array_serializer import ArraySerializer
from .data_serializers.cairo_data_serializer import (
CairoDataSerializer,
)
from .data_serializers.enum_serializer import EnumSerializer
from .data_serializers.felt_serializer import FeltSerializer
from .data_serializers.named_tuple_serializer import (
NamedTupleSerializer,
)
from .data_serializers.option_serializer import (
OptionSerializer,
)
from .data_serializers.output_serializer import (
OutputSerializer,
)
from .data_serializers.payload_serializer import (
PayloadSerializer,
)
from .data_serializers.struct_serializer import (
StructSerializer,
)
from .data_serializers.tuple_serializer import TupleSerializer
from .data_serializers.uint256_serializer import (
Uint256Serializer,
)
from .data_serializers.uint_serializer import UintSerializer
from .data_serializers.unit_serializer import UnitSerializer
from .errors import InvalidTypeException
from .function_serialization_adapter import (
FunctionSerializationAdapter,
FunctionSerializationAdapterV1,
)
_uint256_type = StructType("Uint256", OrderedDict(low=FeltType(), high=FeltType()))
_byte_array_type = StructType(
"core::byte_array::ByteArray",
OrderedDict(
data=ArrayType(FeltType()),
pending_word=FeltType(),
pending_word_len=UintType(bits=32),
),
)
def serializer_for_type(cairo_type: CairoType) -> CairoDataSerializer:
"""
Create a serializer for cairo type.
:param cairo_type: CairoType.
:return: CairoDataSerializer.
"""
# pylint: disable=too-many-return-statements, too-many-branches
if isinstance(cairo_type, FeltType):
return FeltSerializer()
if isinstance(cairo_type, BoolType):
return BoolSerializer()
if isinstance(cairo_type, StructType):
# Special case: Uint256 is represented as struct
if cairo_type == _uint256_type:
return Uint256Serializer()
if cairo_type == _byte_array_type:
return ByteArraySerializer()
return StructSerializer(
OrderedDict(
(name, serializer_for_type(member_type))
for name, member_type in cairo_type.types.items()
)
)
if isinstance(cairo_type, ArrayType):
return ArraySerializer(serializer_for_type(cairo_type.inner_type))
if isinstance(cairo_type, TupleType):
return TupleSerializer(
[serializer_for_type(member) for member in cairo_type.types]
)
if isinstance(cairo_type, NamedTupleType):
return NamedTupleSerializer(
OrderedDict(
(name, serializer_for_type(member_type))
for name, member_type in cairo_type.types.items()
)
)
if isinstance(cairo_type, UintType):
return UintSerializer(bits=cairo_type.bits)
if isinstance(cairo_type, OptionType):
return OptionSerializer(serializer_for_type(cairo_type.type))
if isinstance(cairo_type, UnitType):
return UnitSerializer()
if isinstance(cairo_type, EnumType):
return EnumSerializer(
OrderedDict(
(name, serializer_for_type(variant_type))
for name, variant_type in cairo_type.variants.items()
)
)
if isinstance(cairo_type, EventType):
return serializer_for_payload(cairo_type.types)
raise InvalidTypeException(f"Received unknown Cairo type '{cairo_type}'.")
# We don't want to require users to use OrderedDict. Regular python requires order since python 3.7.
def serializer_for_payload(payload: Dict[str, CairoType]) -> PayloadSerializer:
"""
Create PayloadSerializer for types listed in a dictionary. Please note that the order of fields in the dict is
very important. Make sure the keys are provided in the right order.
:param payload: dictionary with cairo types.
:return: PayloadSerializer that can be used to (de)serialize events/function calls.
"""
return PayloadSerializer(
OrderedDict(
(name, serializer_for_type(cairo_type))
for name, cairo_type in payload.items()
)
)
def serializer_for_outputs(payload: List[CairoType]) -> OutputSerializer:
"""
Create OutputSerializer for types in list. Please note that the order of fields in the list is
very important. Make sure the types are provided in the right order.
:param payload: list with cairo types.
:return: OutputSerializer that can be used to deserialize function outputs.
"""
return OutputSerializer(
serializers=[serializer_for_type(cairo_type) for cairo_type in payload]
)
EventV0 = AbiV0.Event
EventV1 = AbiV1.Event
EventV2 = EventType
def serializer_for_event(event: EventV0 | EventV1 | EventV2) -> PayloadSerializer:
"""
Create serializer for an event.
:param event: parsed event.
:return: PayloadSerializer that can be used to (de)serialize events.
"""
if isinstance(event, EventV0):
return serializer_for_payload(event.data)
if isinstance(event, EventV1):
return serializer_for_payload(event.inputs)
return serializer_for_payload(event.types)
def serializer_for_function(
abi_function: AbiV0.Function,
) -> FunctionSerializationAdapter:
"""
Create FunctionSerializationAdapter for serializing function inputs and deserializing function outputs.
:param abi_function: parsed function's abi.
:return: FunctionSerializationAdapter.
"""
return FunctionSerializationAdapter(
inputs_serializer=serializer_for_payload(abi_function.inputs),
outputs_deserializer=serializer_for_payload(abi_function.outputs),
)
def serializer_for_function_v1(
abi_function: Union[AbiV1.Function, AbiV2.Function],
) -> FunctionSerializationAdapter:
"""
Create FunctionSerializationAdapter for serializing function inputs and deserializing function outputs.
:param abi_function: parsed function's abi.
:return: FunctionSerializationAdapter.
"""
return FunctionSerializationAdapterV1(
inputs_serializer=serializer_for_payload(abi_function.inputs),
outputs_deserializer=serializer_for_outputs(abi_function.outputs),
)
def serializer_for_constructor_v2(
abi_function: AbiV2.Constructor,
) -> FunctionSerializationAdapter:
"""
Create FunctionSerializationAdapter for serializing constructor inputs.
:param abi_function: parsed constructor's abi.
:return: FunctionSerializationAdapter.
"""
return FunctionSerializationAdapterV1(
inputs_serializer=serializer_for_payload(abi_function.inputs),
outputs_deserializer=serializer_for_outputs([]),
)

View File

@@ -0,0 +1,110 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, List, Set, Tuple
from ..cairo.felt import CairoData
from .data_serializers.output_serializer import (
OutputSerializer,
)
from .data_serializers.payload_serializer import (
PayloadSerializer,
)
from .errors import InvalidTypeException
from .tuple_dataclass import TupleDataclass
@dataclass
class FunctionSerializationAdapter:
"""
Class serializing ``*args`` and ``**kwargs`` by adapting them to function inputs.
"""
inputs_serializer: PayloadSerializer
outputs_deserializer: PayloadSerializer
expected_args: Tuple[str] = field(init=False)
def __post_init__(self):
self.expected_args = tuple(
self.inputs_serializer.serializers.keys()
) # pyright: ignore
def serialize(self, *args, **kwargs) -> CairoData:
"""
Method using args and kwargs to match members and serialize them separately.
:return: Members serialized separately in SerializedPayload.
"""
named_arguments = self._merge_arguments(args, kwargs)
return self.inputs_serializer.serialize(named_arguments)
def deserialize(self, data: List[int]) -> TupleDataclass:
"""
Deserializes data into TupleDataclass containing python representations.
:return: cairo data.
"""
return self.outputs_deserializer.deserialize(data)
def _merge_arguments(self, args: Tuple, kwargs: Dict) -> Dict:
"""
Merges positional and keyed arguments.
"""
# After this line we know that len(args) <= len(self.expected_args)
self._ensure_no_unnecessary_positional_args(args)
named_arguments = dict(kwargs)
for arg, input_name in zip(args, self.expected_args):
if input_name in kwargs:
raise InvalidTypeException(
f"Both positional and named argument provided for '{input_name}'."
)
named_arguments[input_name] = arg
expected_args = set(self.expected_args)
provided_args = set(named_arguments.keys())
# named_arguments might have unnecessary arguments coming from kwargs (we ensure that
# len(args) <= len(self.expected_args) above)
self._ensure_no_unnecessary_args(expected_args, provided_args)
# there might be some argument missing (not provided)
self._ensure_no_missing_args(expected_args, provided_args)
return named_arguments
def _ensure_no_unnecessary_positional_args(self, args: Tuple):
if len(args) > len(self.expected_args):
raise InvalidTypeException(
f"Provided {len(args)} positional arguments, {len(self.expected_args)} possible."
)
@staticmethod
def _ensure_no_unnecessary_args(expected_args: Set[str], provided_args: Set[str]):
excessive_arguments = provided_args - expected_args
if excessive_arguments:
raise InvalidTypeException(
f"Unnecessary named arguments provided: '{', '.join(excessive_arguments)}'."
)
@staticmethod
def _ensure_no_missing_args(expected_args: Set[str], provided_args: Set[str]):
missing_arguments = expected_args - provided_args
if missing_arguments:
raise InvalidTypeException(
f"Missing arguments: '{', '.join(missing_arguments)}'."
)
@dataclass
class FunctionSerializationAdapterV1(FunctionSerializationAdapter):
outputs_deserializer: OutputSerializer
def deserialize(self, data: List[int]) -> Tuple:
"""
Deserializes data into TupleDataclass containing python representations.
:return: cairo data.
"""
return self.outputs_deserializer.deserialize(data)

View File

@@ -0,0 +1,59 @@
from __future__ import annotations
from dataclasses import dataclass, fields, make_dataclass
from typing import Dict, Optional, Tuple
@dataclass(frozen=True, eq=False)
class TupleDataclass:
"""
Dataclass that behaves like a tuple at the same time. Used when data has defined order and names.
For instance in case of named tuples or function responses.
"""
# getattr is called when attribute is not found in object. For instance when using object.unknown_attribute.
# This way pyright will know that there might be some arguments it doesn't know about and will stop complaining
# about some fields that don't exist statically.
def __getattr__(self, item):
# This should always fail - only attributes that don't exist end up in here.
# We use __getattribute__ to get the native error.
return super().__getattribute__(item)
def __getitem__(self, item: int):
field = fields(self)[item]
return getattr(self, field.name)
def __iter__(self):
return (getattr(self, field.name) for field in fields(self))
def as_tuple(self) -> Tuple:
"""
Creates a regular tuple from TupleDataclass.
"""
return tuple(self)
def as_dict(self) -> Dict:
"""
Creates a regular dict from TupleDataclass.
"""
return {field.name: getattr(self, field.name) for field in fields(self)}
# Added for backward compatibility with previous implementation based on NamedTuple
def _asdict(self):
return self.as_dict()
def __eq__(self, other):
if isinstance(other, TupleDataclass):
return self.as_tuple() == other.as_tuple()
return self.as_tuple() == other
@staticmethod
def from_dict(data: Dict, *, name: Optional[str] = None) -> TupleDataclass:
result_class = make_dataclass(
name or "TupleDataclass",
fields=[(key, type(value)) for key, value in data.items()],
bases=(TupleDataclass,),
frozen=True,
eq=False,
)
return result_class(**data)