Skip to content

Commit 020a0a9

Browse files
authored
Add Uint64 and Uint128 classes (#1247)
* Add classes for `u64` and `u128` types * Add regex validation to `Felt` class * Use serliazation logic to get broadcasted tx v3 common properties * Add tests for `DAModeField` * Add `NumberAsHex` class * Use `NumberAsHex` instead of `Felt` for some fields to align with RPC spec
1 parent 881f5ae commit 020a0a9

File tree

4 files changed

+245
-55
lines changed

4 files changed

+245
-55
lines changed

starknet_py/net/full_node_client.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
PendingBlockStateUpdateSchema,
6969
PendingStarknetBlockSchema,
7070
PendingStarknetBlockWithTxHashesSchema,
71-
ResourceBoundsMappingSchema,
7271
SentTransactionSchema,
7372
SierraContractClassSchema,
7473
SimulatedTransactionSchema,
@@ -78,6 +77,7 @@
7877
TransactionReceiptSchema,
7978
TransactionStatusResponseSchema,
8079
TransactionTraceSchema,
80+
TransactionV3Schema,
8181
TypesOfTransactionsSchema,
8282
)
8383
from starknet_py.net.schemas.utils import _extract_tx_version
@@ -955,16 +955,7 @@ def _create_broadcasted_txn_common_properties(transaction: AccountTransaction) -
955955
def _create_broadcasted_txn_v3_common_properties(
956956
transaction: Union[DeclareV3, InvokeV3, DeployAccountV3]
957957
) -> dict:
958-
resource_bonds = cast(
959-
Dict, ResourceBoundsMappingSchema().dump(obj=transaction.resource_bounds)
958+
return cast(
959+
Dict,
960+
TransactionV3Schema(exclude=["version", "signature"]).dump(obj=transaction),
960961
)
961-
962-
broadcasted_txn_v3_common_properties = {
963-
"resource_bounds": resource_bonds,
964-
"tip": _to_rpc_felt(transaction.tip),
965-
"paymaster_data": [_to_rpc_felt(data) for data in transaction.paymaster_data],
966-
"nonce_data_availability_mode": transaction.nonce_data_availability_mode.name,
967-
"fee_data_availability_mode": transaction.fee_data_availability_mode.name,
968-
}
969-
970-
return broadcasted_txn_v3_common_properties

starknet_py/net/schemas/common.py

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
import sys
23
from typing import Any, Mapping, Optional, Union
34

45
from marshmallow import Schema, ValidationError, fields, post_load
@@ -25,13 +26,30 @@ def _pascal_to_screaming_upper(checked_string: str) -> str:
2526
return re.sub(r"(?<!^)(?=[A-Z])", "_", checked_string).upper()
2627

2728

28-
class Felt(fields.Field):
29+
class NumberAsHex(fields.Field):
2930
"""
30-
Field that serializes int to felt (hex encoded string)
31+
This field performs the following operations:
32+
33+
- Serializes integers into hexadecimal strings
34+
- Deserializes hexadecimal strings into integers
35+
36+
If a valid hexadecimal string is provided during serialization, it is returned as is.
37+
Similarly, when a valid integer is provided during deserialization, it remains unchanged.
3138
"""
3239

40+
MAX_VALUE = sys.maxsize
41+
REGEX_PATTERN = r"^0x[a-fA-F0-9]+$"
42+
3343
def _serialize(self, value: Any, attr: str, obj: Any, **kwargs):
34-
return hex(value)
44+
if self._is_int_and_in_range(value):
45+
return hex(value)
46+
47+
if self._is_str_and_valid_pattern(value):
48+
return value
49+
50+
raise ValidationError(
51+
f"Invalid value provided for {self.__class__.__name__}: {value}"
52+
)
3553

3654
def _deserialize(
3755
self,
@@ -40,16 +58,51 @@ def _deserialize(
4058
data: Union[Mapping[str, Any], None],
4159
**kwargs,
4260
):
43-
if isinstance(value, int):
61+
if self._is_int_and_in_range(value):
4462
return value
4563

46-
if not isinstance(value, str) or not value.startswith("0x"):
47-
raise ValidationError(f"Invalid value provided for felt: {value}.")
48-
49-
try:
64+
if self._is_str_and_valid_pattern(value):
5065
return int(value, 16)
51-
except ValueError as error:
52-
raise ValidationError("Invalid felt.") from error
66+
67+
raise ValidationError(
68+
f"Invalid value provided for {self.__class__.__name__}: {value}"
69+
)
70+
71+
def _is_int_and_in_range(self, value: Any) -> bool:
72+
return isinstance(value, int) and 0 <= value < self.MAX_VALUE
73+
74+
def _is_str_and_valid_pattern(self, value: Any) -> bool:
75+
return (
76+
isinstance(value, str)
77+
and re.fullmatch(self.REGEX_PATTERN, value) is not None
78+
)
79+
80+
81+
class Felt(NumberAsHex):
82+
"""
83+
Field used to serialize and deserialize felt type.
84+
"""
85+
86+
MAX_VALUE = 2**252
87+
REGEX_PATTERN = r"^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,62})$"
88+
89+
90+
class Uint64(NumberAsHex):
91+
"""
92+
Field used to serialize and deserialize RPC u64 type.
93+
"""
94+
95+
MAX_VALUE = 2**64
96+
REGEX_PATTERN = r"^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,15})$"
97+
98+
99+
class Uint128(NumberAsHex):
100+
"""
101+
Field used to serialize and deserialize RPC u128 type.
102+
"""
103+
104+
MAX_VALUE = 2**128
105+
REGEX_PATTERN = r"^0x(0|[a-fA-F1-9]{1}[a-fA-F0-9]{0,31})$"
53106

54107

55108
class NonPrefixedHex(fields.Field):

starknet_py/net/schemas/common_test.py

Lines changed: 169 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,170 @@
1+
from typing import Optional, Type, Union
2+
13
import pytest
24
from marshmallow import Schema, ValidationError
35

4-
from starknet_py.net.client_models import BlockStatus, TransactionStatus
5-
from starknet_py.net.schemas.common import NonPrefixedHex
6-
from starknet_py.net.schemas.rpc import BlockStatusField, Felt, StatusField
6+
from starknet_py.net.client_models import BlockStatus, DAMode, Hash, TransactionStatus
7+
from starknet_py.net.schemas.common import (
8+
BlockStatusField,
9+
DAModeField,
10+
Felt,
11+
NonPrefixedHex,
12+
StatusField,
13+
Uint64,
14+
Uint128,
15+
)
716

817

9-
def test_serialize_felt():
10-
class SchemaWithFelt(Schema):
11-
value1 = Felt(data_key="value1")
18+
class SchemaWithUint64(Schema):
19+
value = Uint64(data_key="value")
1220

13-
data = {"value1": 2137}
1421

15-
serialized = SchemaWithFelt().dumps(data)
16-
assert '"value1": "0x859"' in serialized
22+
class SchemaWithUint128(Schema):
23+
value = Uint128(data_key="value")
24+
25+
26+
class SchemaWithFelt(Schema):
27+
value = Felt(data_key="value")
1728

1829

19-
def test_serialize_felt_throws_on_none():
20-
class SchemaWithFelt(Schema):
21-
value1 = Felt(data_key="value1")
30+
class SchemaWithDAModeField(Schema):
31+
value = DAModeField(data_key="value")
2232

23-
data = {"value1": None}
24-
with pytest.raises(TypeError):
33+
34+
def test_serialize_felt():
35+
data = {"value": 2137}
36+
37+
serialized = SchemaWithFelt().dumps(data)
38+
assert '"value": "0x859"' in serialized
39+
40+
41+
@pytest.mark.parametrize(
42+
"data",
43+
[
44+
{"value": None},
45+
{"value": 2**252},
46+
],
47+
)
48+
def test_serialize_felt_throws_on_invalid_data(data):
49+
with pytest.raises(ValidationError, match="Invalid value provided for Felt"):
2550
SchemaWithFelt().dumps(data)
2651

2752

2853
def test_deserialize_felt():
29-
class SchemaWithFelt(Schema):
30-
value1 = Felt(data_key="value1")
31-
32-
data = {"value1": "0x859"}
54+
data = {"value": "0x859"}
3355

3456
deserialized = SchemaWithFelt().load(data)
3557
assert isinstance(deserialized, dict)
36-
assert deserialized["value1"] == 2137
58+
assert deserialized["value"] == 2137
3759

3860

3961
def test_deserialize_felt_throws_on_invalid_data():
40-
class SchemaWithFelt(Schema):
41-
value1 = Felt(data_key="value1")
42-
43-
data = {"value1": "2137"}
62+
data = {"value": "2137"}
4463

45-
with pytest.raises(ValidationError, match="Invalid value provided for felt"):
64+
with pytest.raises(ValidationError, match="Invalid value provided for Felt"):
4665
SchemaWithFelt().load(data)
4766

48-
data = {"value1": "0xwww"}
49-
with pytest.raises(ValidationError, match="Invalid felt."):
67+
data = {"value": "0xwww"}
68+
with pytest.raises(ValidationError, match="Invalid value provided for Felt"):
5069
SchemaWithFelt().load(data)
5170

5271

72+
@pytest.mark.parametrize(
73+
"data, expected_serialized",
74+
(
75+
({"value": 0}, "0x0"),
76+
({"value": "0x100"}, "0x100"),
77+
({"value": 2**32}, "0x100000000"),
78+
),
79+
)
80+
def test_serialize_uint64(data, expected_serialized):
81+
serialized = SchemaWithUint64().dumps(data)
82+
assert f'"value": "{expected_serialized}"' in serialized
83+
84+
85+
@pytest.mark.parametrize(
86+
"data",
87+
[{"value": -1}, {"value": 2**64}, {"value": None}],
88+
)
89+
def test_serialize_uint64_throws_on_invalid_data(data):
90+
with pytest.raises(
91+
ValidationError,
92+
match=get_uint_error_message(Uint64, data["value"]),
93+
):
94+
SchemaWithUint64().dumps(data)
95+
96+
97+
@pytest.mark.parametrize(
98+
"data",
99+
[{"value": "0x100000000"}, {"value": 2**32}],
100+
)
101+
def test_deserialize_uint64(data):
102+
deserialized = SchemaWithUint64().load(data)
103+
assert isinstance(deserialized, dict)
104+
assert deserialized["value"] == 2**32
105+
106+
107+
@pytest.mark.parametrize(
108+
"data",
109+
[
110+
{"value": -1},
111+
{"value": "1000"},
112+
{"value": 2**64},
113+
{"value": "0xwrong"},
114+
{"value": ""},
115+
],
116+
)
117+
def test_deserialize_uint64_throws_on_invalid_data(data):
118+
with pytest.raises(
119+
ValidationError,
120+
match=get_uint_error_message(Uint64, data["value"]),
121+
):
122+
SchemaWithUint64().load(data)
123+
124+
125+
def test_serialize_uint128():
126+
data = {"value": 2**64}
127+
serialized = SchemaWithUint128().dumps(data)
128+
assert '"value": "0x10000000000000000"' in serialized
129+
130+
131+
def test_serialize_uint128_throws_on_invalid_data():
132+
data = {"value": 2**128}
133+
with pytest.raises(
134+
ValidationError,
135+
match=get_uint_error_message(Uint128, data["value"]),
136+
):
137+
SchemaWithUint128().dumps(data)
138+
139+
140+
@pytest.mark.parametrize(
141+
"data",
142+
[{"value": "0x10000000000000000"}, {"value": 2**64}],
143+
)
144+
def test_deserialize_uint128(data):
145+
deserialized = SchemaWithUint128().load(data)
146+
assert isinstance(deserialized, dict)
147+
assert deserialized["value"] == 2**64
148+
149+
150+
@pytest.mark.parametrize(
151+
"data",
152+
[
153+
{"value": -1},
154+
{"value": "1000"},
155+
{"value": 2**128},
156+
{"value": "0xwrong"},
157+
{"value": ""},
158+
],
159+
)
160+
def test_deserialize_uint128_throws_on_invalid_data(data):
161+
with pytest.raises(
162+
ValidationError,
163+
match=get_uint_error_message(Uint128, data["value"]),
164+
):
165+
SchemaWithUint128().load(data)
166+
167+
53168
def test_serialize_hex():
54169
class SchemaWithHex(Schema):
55170
value1 = NonPrefixedHex(data_key="value1")
@@ -134,3 +249,31 @@ class SchemaWithBlockStatusField(Schema):
134249

135250
with pytest.raises(ValidationError, match="Invalid value for BlockStatus provided"):
136251
SchemaWithBlockStatusField().load(data)
252+
253+
254+
@pytest.mark.parametrize(
255+
"data",
256+
[{"value": DAMode.L1}, {"value": DAMode.L2}],
257+
)
258+
def test_serialize_damode_field(data):
259+
serialized = SchemaWithDAModeField().dumps(data)
260+
assert f'"value": "{data["value"].name}"' in serialized
261+
262+
263+
@pytest.mark.parametrize(
264+
"data, expected_deserialized",
265+
(
266+
({"value": DAMode.L1.name}, DAMode.L1),
267+
({"value": DAMode.L2.name}, DAMode.L2),
268+
),
269+
)
270+
def test_deserialize_damode_field(data, expected_deserialized):
271+
deserialized = SchemaWithDAModeField().load(data)
272+
assert isinstance(deserialized, dict)
273+
assert deserialized["value"] == expected_deserialized
274+
275+
276+
def get_uint_error_message(
277+
class_type: Union[Type[Uint64], Type[Uint128]], value: Optional[Hash]
278+
) -> str:
279+
return f"Invalid value provided for {class_type.__name__}: {str(value)}"

0 commit comments

Comments
 (0)