add
This commit is contained in:
24
ccxt/static_dependencies/starknet/serialization/__init__.py
Normal file
24
ccxt/static_dependencies/starknet/serialization/__init__.py
Normal 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
|
||||
@@ -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
|
||||
142
ccxt/static_dependencies/starknet/serialization/_context.py
Normal file
142
ccxt/static_dependencies/starknet/serialization/_context.py
Normal 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."
|
||||
)
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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
|
||||
)
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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."
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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])
|
||||
@@ -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)"
|
||||
)
|
||||
@@ -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) + ")"
|
||||
)
|
||||
@@ -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
|
||||
10
ccxt/static_dependencies/starknet/serialization/errors.py
Normal file
10
ccxt/static_dependencies/starknet/serialization/errors.py
Normal 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."""
|
||||
229
ccxt/static_dependencies/starknet/serialization/factory.py
Normal file
229
ccxt/static_dependencies/starknet/serialization/factory.py
Normal 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([]),
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user