Skip to content

Commit 6ae88c2

Browse files
Fix parsing signed ints in ABI (#1667)
1 parent 42e6cd3 commit 6ae88c2

File tree

7 files changed

+168
-0
lines changed

7 files changed

+168
-0
lines changed

docs/migration_guide.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
Migration guide
22
===============
33

4+
***************************
5+
x.y.z Migration guide
6+
***************************
7+
8+
x.y.z Bugfixes
9+
--------------
10+
11+
1. Fixed parsing ABI that contains signed integers (e.g. ``i128``).
12+
413
***************************
514
0.29.0-rc.0 Migration guide
615
***************************
@@ -481,6 +490,7 @@ This version of starknet.py requires Python 3.9 as a minimum version.
481490
******************************
482491
0.24.1 Migration guide
483492
******************************
493+
484494
This version contains a quick fix to parsing ABI for Cairo v2 contracts. Due to new release of compiler, ``u96`` is now compiled to `BoundedInt` in ABI.
485495

486496
0.24.1 Minor changes

starknet_py/abi/v2/parser_transformer.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
CairoType,
1111
FeltType,
1212
FixedSizeArrayType,
13+
IntType,
1314
NonZeroType,
1415
OptionType,
1516
TupleType,
@@ -27,6 +28,7 @@
2728
| type_bool
2829
| type_felt
2930
| type_bytes
31+
| type_int
3032
| type_uint
3133
| type_bounded_int
3234
| type_contract_address
@@ -45,6 +47,7 @@
4547
type_felt: "core::felt252"
4648
type_bytes: "core::bytes_31::bytes31"
4749
type_bool: "core::bool"
50+
type_int: "core::integer::i" INT
4851
type_uint: "core::integer::u" INT
4952
type_bounded_int: "core::internal::BoundedInt::<" INT "," WS? INT ">" | "core::internal::bounded_int::BoundedInt::<" INT "," WS? INT ">"
5053
type_contract_address: "core::starknet::contract_address::ContractAddress"
@@ -112,6 +115,12 @@ def type_bool(self, _value: List[Any]) -> BoolType:
112115
"""
113116
return BoolType()
114117

118+
def type_int(self, value: List[Token]) -> IntType:
119+
"""
120+
Int type contains information about its size. It is present in the value[0].
121+
"""
122+
return IntType(int(value[0]))
123+
115124
def type_uint(self, value: List[Token]) -> UintType:
116125
"""
117126
Uint type contains information about its size. It is present in the value[0].

starknet_py/cairo/data_types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ class OptionType(CairoType):
9393
type: CairoType #: Typed of element wrapped in the Option.
9494

9595

96+
@dataclass
97+
class IntType(CairoType):
98+
"""
99+
Type representation of Cairo signed integers.
100+
"""
101+
102+
bits: int #: Number of bits in the integer.
103+
104+
96105
@dataclass
97106
class UintType(CairoType):
98107
"""
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from dataclasses import dataclass
2+
from typing import Generator
3+
4+
from starknet_py.constants import FIELD_PRIME
5+
from starknet_py.serialization._context import (
6+
Context,
7+
DeserializationContext,
8+
SerializationContext,
9+
)
10+
from starknet_py.serialization.data_serializers.cairo_data_serializer import (
11+
CairoDataSerializer,
12+
)
13+
14+
15+
@dataclass
16+
class IntSerializer(CairoDataSerializer[int, int]):
17+
"""
18+
Serializer of int. In Cairo there are few ints (i8, ..., i64 and i128).
19+
Can serialize an int.
20+
Deserializes data to an int.
21+
"""
22+
23+
bits: int
24+
25+
def deserialize_with_context(self, context: DeserializationContext) -> int:
26+
(raw,) = context.reader.read(1)
27+
28+
signed_threshold = 1 << (self.bits - 1)
29+
deserialized_val = raw if raw < signed_threshold else raw - FIELD_PRIME
30+
with context.push_entity("int" + str(self.bits)):
31+
self._ensure_valid_int(
32+
deserialized_val,
33+
context,
34+
self.bits,
35+
)
36+
37+
return deserialized_val
38+
39+
def serialize_with_context(
40+
self, context: SerializationContext, value: int
41+
) -> Generator[int, None, None]:
42+
context.ensure_valid_type(value, isinstance(value, int), "int")
43+
yield from self._serialize_from_int(value, context, self.bits)
44+
45+
@staticmethod
46+
def _serialize_from_int(
47+
value: int, context: SerializationContext, bits: int
48+
) -> Generator[int, None, None]:
49+
IntSerializer._ensure_valid_int(value, context, bits)
50+
51+
unsigned = value % FIELD_PRIME
52+
53+
yield unsigned
54+
55+
@staticmethod
56+
def _ensure_valid_int(value: int, context: Context, bits: int):
57+
"""
58+
Ensures that value is a valid int on `bits` bits.
59+
"""
60+
min_val = -(1 << (bits - 1))
61+
max_val = (1 << (bits - 1)) - 1
62+
context.ensure_valid_value(
63+
(min_val <= value <= max_val),
64+
f"expected value in range [{min_val};{max_val}]",
65+
)

starknet_py/serialization/factory.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
EventType,
1515
FeltType,
1616
FixedSizeArrayType,
17+
IntType,
1718
NamedTupleType,
1819
NonZeroType,
1920
OptionType,
@@ -32,6 +33,7 @@
3233
)
3334
from starknet_py.serialization.data_serializers.enum_serializer import EnumSerializer
3435
from starknet_py.serialization.data_serializers.felt_serializer import FeltSerializer
36+
from starknet_py.serialization.data_serializers.int_serializer import IntSerializer
3537
from starknet_py.serialization.data_serializers.named_tuple_serializer import (
3638
NamedTupleSerializer,
3739
)
@@ -118,6 +120,9 @@ def serializer_for_type(cairo_type: CairoType) -> CairoDataSerializer:
118120
)
119121
)
120122

123+
if isinstance(cairo_type, IntType):
124+
return IntSerializer(bits=cairo_type.bits)
125+
121126
if isinstance(cairo_type, UintType):
122127
return UintSerializer(bits=cairo_type.bits)
123128

starknet_py/tests/e2e/mock/contracts_v2/src/abi_types.cairo

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct ExampleStruct {
2020
field_e: NonZero<felt252>,
2121
field_f: NonZero<u8>,
2222
field_g: [u64; 5],
23+
field_h: [i128; 5],
2324
}
2425

2526
#[starknet::interface]
@@ -73,6 +74,7 @@ mod AbiTypes {
7374
field_e: felt_to_nonzero(100),
7475
field_f: u8_to_nonzero(100),
7576
field_g: [1, 2, 3, 4, 5],
77+
field_h: [-2000, -1000, 0, 1000, 2000],
7678
}
7779
}
7880
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import re
2+
3+
import pytest
4+
5+
from starknet_py.constants import FIELD_PRIME
6+
from starknet_py.serialization.data_serializers.int_serializer import IntSerializer
7+
from starknet_py.serialization.errors import InvalidTypeException, InvalidValueException
8+
9+
u128_serializer = IntSerializer(bits=128)
10+
11+
MIN_I128 = -(2**127)
12+
MAX_I128 = 2**127 - 1
13+
14+
15+
def _felt(x: int) -> int:
16+
if abs(x) >= FIELD_PRIME:
17+
raise ValueError("Value is out of field range.")
18+
19+
return x + FIELD_PRIME if x < 0 else x
20+
21+
22+
@pytest.mark.parametrize(
23+
"value, serializer, serialized_value",
24+
[
25+
(0, u128_serializer, [_felt(0)]),
26+
# positive values
27+
(1, u128_serializer, [_felt(1)]),
28+
(1000, u128_serializer, [_felt(1000)]),
29+
# negative values
30+
(-1, u128_serializer, [_felt(-1)]),
31+
(-1000, u128_serializer, [_felt(-1000)]),
32+
# boundaries
33+
(MIN_I128, u128_serializer, [_felt(MIN_I128)]),
34+
(MAX_I128, u128_serializer, [_felt(MAX_I128)]),
35+
],
36+
)
37+
def test_valid_values(value, serializer, serialized_value):
38+
deserialized = serializer.deserialize(serialized_value)
39+
assert deserialized == value
40+
41+
serialized = serializer.serialize(value)
42+
assert serialized == serialized_value
43+
44+
45+
def test_deserialize_invalid_i128_values():
46+
error_message = re.escape(
47+
f"Error at path 'int128': expected value in range [{MIN_I128};{MAX_I128}]"
48+
)
49+
with pytest.raises(InvalidValueException, match=error_message):
50+
u128_serializer.deserialize([MIN_I128 - 1])
51+
with pytest.raises(InvalidValueException, match=error_message):
52+
u128_serializer.deserialize([MAX_I128 + 1])
53+
54+
55+
def test_serialize_invalid_i128_value():
56+
error_message = re.escape(f"Error: expected value in range [{MIN_I128};{MAX_I128}]")
57+
with pytest.raises(InvalidValueException, match=error_message):
58+
u128_serializer.serialize(2**128)
59+
with pytest.raises(InvalidValueException, match=error_message):
60+
u128_serializer.serialize(-(2**128))
61+
62+
63+
def test_invalid_type():
64+
error_message = re.escape(
65+
"Error: expected int, received 'wololoo' of type '<class 'str'>'."
66+
)
67+
with pytest.raises(InvalidTypeException, match=error_message):
68+
u128_serializer.serialize("wololoo") # type: ignore

0 commit comments

Comments
 (0)