Skip to content

Commit f0d7440

Browse files
authored
Support serialization of ByteArrays (#1344)
* Support serialization of ByteArrays * Rename `BYTES_SIZE_IN_BYTES_31` -> `BYTES_31_SIZE`
1 parent 5b54a9c commit f0d7440

File tree

10 files changed

+189
-3
lines changed

10 files changed

+189
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ cython_debug/
147147

148148
# Compiled contracts
149149
/starknet_py/tests/e2e/mock/contracts_compiled/*
150+
/starknet_py/tests/e2e/mock/*/Scarb.lock
150151

151152
# Allow precompiled contracts
152153
!/starknet_py/tests/e2e/mock/contracts_compiled/precompiled/

starknet_py/abi/v2/parser_transformer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
actual_type: type_unit
2424
| type_bool
2525
| type_felt
26+
| type_bytes
2627
| type_uint
2728
| type_contract_address
2829
| type_class_hash
@@ -36,6 +37,7 @@
3637
3738
type_unit: "()"
3839
type_felt: "core::felt252"
40+
type_bytes: "core::bytes_31::bytes31"
3941
type_bool: "core::bool"
4042
type_uint: "core::integer::u" INT
4143
type_contract_address: "core::starknet::contract_address::ContractAddress"
@@ -89,6 +91,12 @@ def type_felt(self, _value: List[Any]) -> FeltType:
8991
"""
9092
return FeltType()
9193

94+
def type_bytes(self, _value: List[Any]) -> FeltType:
95+
"""
96+
Felt does not contain any additional arguments, so `_value` is just an empty list.
97+
"""
98+
return FeltType()
99+
92100
def type_bool(self, _value: List[Any]) -> BoolType:
93101
"""
94102
Bool does not contain any additional arguments, so `_value` is just an empty list.

starknet_py/serialization/data_serializers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .array_serializer import ArraySerializer
22
from .bool_serializer import BoolSerializer
3+
from .byte_array_serializer import ByteArraySerializer
34
from .cairo_data_serializer import CairoDataSerializer
45
from .felt_serializer import FeltSerializer
56
from .named_tuple_serializer import NamedTupleSerializer
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from dataclasses import dataclass
2+
from typing import Generator
3+
4+
from starknet_py.cairo.felt import decode_shortstring, encode_shortstring
5+
from starknet_py.serialization._context import (
6+
DeserializationContext,
7+
SerializationContext,
8+
)
9+
from starknet_py.serialization.data_serializers._common import (
10+
deserialize_to_list,
11+
serialize_from_list,
12+
)
13+
from starknet_py.serialization.data_serializers.cairo_data_serializer import (
14+
CairoDataSerializer,
15+
)
16+
from starknet_py.serialization.data_serializers.felt_serializer import FeltSerializer
17+
18+
BYTES_31_SIZE = 31
19+
20+
21+
@dataclass
22+
class ByteArraySerializer(CairoDataSerializer[str, str]):
23+
"""
24+
Serializer for ByteArrays. Serializes to and deserializes from str values.
25+
26+
Examples:
27+
"" => [0,0,0]
28+
"hello" => [0,448378203247,5]
29+
"""
30+
31+
def deserialize_with_context(self, context: DeserializationContext) -> str:
32+
with context.push_entity("data_array_len"):
33+
[size] = context.reader.read(1)
34+
35+
data = deserialize_to_list([FeltSerializer()] * size, context)
36+
37+
with context.push_entity("pending_word"):
38+
[pending_word] = context.reader.read(1)
39+
40+
with context.push_entity("pending_word_len"):
41+
[pending_word_len] = context.reader.read(1)
42+
43+
pending_word = decode_shortstring(pending_word)
44+
context.ensure_valid_value(
45+
len(pending_word) == pending_word_len,
46+
f"Invalid length {pending_word_len} for pending word {pending_word}",
47+
)
48+
49+
data_joined = "".join(map(decode_shortstring, data))
50+
return data_joined + pending_word
51+
52+
def serialize_with_context(
53+
self, context: SerializationContext, value: str
54+
) -> Generator[int, None, None]:
55+
context.ensure_valid_type(value, isinstance(value, str), "str")
56+
data = [
57+
value[i : i + BYTES_31_SIZE] for i in range(0, len(value), BYTES_31_SIZE)
58+
]
59+
pending_word = (
60+
"" if len(data) == 0 or len(data[-1]) == BYTES_31_SIZE else data.pop(-1)
61+
)
62+
63+
yield len(data)
64+
yield from serialize_from_list([FeltSerializer()] * len(data), context, data)
65+
yield encode_shortstring(pending_word)
66+
yield len(pending_word)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import pytest
2+
3+
from starknet_py.serialization.data_serializers.byte_array_serializer import (
4+
ByteArraySerializer,
5+
)
6+
7+
byte_array_serializer = ByteArraySerializer()
8+
9+
10+
@pytest.mark.parametrize(
11+
"value, serialized_value",
12+
[
13+
("", [0, 0, 0]),
14+
("hello", [0, 0x68656C6C6F, 5]),
15+
(
16+
"Long string, more than 31 characters.",
17+
[
18+
1,
19+
0x4C6F6E6720737472696E672C206D6F7265207468616E203331206368617261,
20+
0x63746572732E,
21+
6,
22+
],
23+
),
24+
],
25+
)
26+
def test_values(value, serialized_value):
27+
serialized = byte_array_serializer.serialize(value)
28+
deserialized = byte_array_serializer.deserialize(serialized_value)
29+
30+
assert deserialized == value
31+
assert serialized == serialized_value

starknet_py/serialization/factory.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
UintType,
2121
UnitType,
2222
)
23-
from starknet_py.serialization.data_serializers import BoolSerializer
23+
from starknet_py.serialization.data_serializers import (
24+
BoolSerializer,
25+
ByteArraySerializer,
26+
)
2427
from starknet_py.serialization.data_serializers.array_serializer import ArraySerializer
2528
from starknet_py.serialization.data_serializers.cairo_data_serializer import (
2629
CairoDataSerializer,
@@ -55,6 +58,14 @@
5558
)
5659

5760
_uint256_type = StructType("Uint256", OrderedDict(low=FeltType(), high=FeltType()))
61+
_byte_array_type = StructType(
62+
"core::byte_array::ByteArray",
63+
OrderedDict(
64+
data=ArrayType(FeltType()),
65+
pending_word=FeltType(),
66+
pending_word_len=UintType(bits=32),
67+
),
68+
)
5869

5970

6071
def serializer_for_type(cairo_type: CairoType) -> CairoDataSerializer:
@@ -64,7 +75,7 @@ def serializer_for_type(cairo_type: CairoType) -> CairoDataSerializer:
6475
:param cairo_type: CairoType.
6576
:return: CairoDataSerializer.
6677
"""
67-
# pylint: disable=too-many-return-statements
78+
# pylint: disable=too-many-return-statements, too-many-branches
6879
if isinstance(cairo_type, FeltType):
6980
return FeltSerializer()
7081

@@ -76,6 +87,9 @@ def serializer_for_type(cairo_type: CairoType) -> CairoDataSerializer:
7687
if cairo_type == _uint256_type:
7788
return Uint256Serializer()
7889

90+
if cairo_type == _byte_array_type:
91+
return ByteArraySerializer()
92+
7993
return StructSerializer(
8094
OrderedDict(
8195
(name, serializer_for_type(member_type))

starknet_py/tests/e2e/contract_interaction/v1_interaction_test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,24 @@ async def test_from_address_on_v1_contract(account, cairo1_erc20_class_hash: int
174174
assert erc20_from_address.account == erc20.account
175175
assert erc20_from_address.functions.keys() == erc20.functions.keys()
176176
assert erc20_from_address.data == erc20.data
177+
178+
179+
@pytest.mark.skipif(
180+
"--contract_dir=v1" in sys.argv,
181+
reason="Contract exists only in v2 directory",
182+
)
183+
@pytest.mark.asyncio
184+
async def test_invoke_contract_with_bytearray(string_contract):
185+
(initial_string,) = await string_contract.functions["get_string"].call()
186+
assert initial_string == "Hello"
187+
188+
value_to_set = """
189+
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
190+
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
191+
"""
192+
193+
await string_contract.functions["set_string"].invoke_v3(
194+
new_string=value_to_set, auto_estimate=True
195+
)
196+
(new_string,) = await string_contract.functions["get_string"].call()
197+
assert new_string == value_to_set

starknet_py/tests/e2e/fixtures/contracts_v1.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
import pytest_asyncio
66

77
from starknet_py.common import create_casm_class
8+
from starknet_py.contract import Contract
89
from starknet_py.hash.casm_class_hash import compute_casm_class_hash
910
from starknet_py.net.account.base_account import BaseAccount
1011
from starknet_py.net.models import DeclareV2
1112
from starknet_py.tests.e2e.fixtures.constants import MAX_FEE
1213
from starknet_py.tests.e2e.fixtures.contracts import deploy_v1_contract
13-
from starknet_py.tests.e2e.fixtures.misc import load_contract
14+
from starknet_py.tests.e2e.fixtures.misc import ContractVersion, load_contract
1415

1516

1617
async def declare_cairo1_contract(
@@ -130,3 +131,23 @@ async def cairo1_hello_starknet_deploy(
130131
contract_name="HelloStarknet",
131132
class_hash=cairo1_hello_starknet_class_hash,
132133
)
134+
135+
136+
@pytest_asyncio.fixture(scope="package", name="string_contract_class_hash")
137+
async def declare_string_contract(account: BaseAccount) -> int:
138+
contract = load_contract("MyString", version=ContractVersion.V2)
139+
class_hash, _ = await declare_cairo1_contract(
140+
account, contract["sierra"], contract["casm"]
141+
)
142+
return class_hash
143+
144+
145+
@pytest_asyncio.fixture(scope="package", name="string_contract")
146+
async def deploy_string_contract(
147+
account: BaseAccount, string_contract_class_hash
148+
) -> Contract:
149+
return await deploy_v1_contract(
150+
account=account,
151+
contract_name="MyString",
152+
class_hash=string_contract_class_hash,
153+
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod hello_starknet;
44
mod hello2;
55
mod minimal_contract;
66
mod new_syntax_test_contract;
7+
mod string;
78
mod test_contract;
89
mod test_enum;
910
mod test_option;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#[starknet::contract]
2+
mod MyString {
3+
#[storage]
4+
struct Storage {
5+
string: ByteArray,
6+
}
7+
8+
#[constructor]
9+
fn constructor(ref self: ContractState) {
10+
self.string.write("Hello");
11+
}
12+
13+
#[external(v0)]
14+
fn set_string(ref self: ContractState, new_string: ByteArray) {
15+
self.string.write(new_string);
16+
}
17+
18+
#[external(v0)]
19+
fn get_string(self: @ContractState) -> ByteArray {
20+
self.string.read()
21+
}
22+
}

0 commit comments

Comments
 (0)