In the first part of this series, we learnt how to implement StorageAccess
for a custom data type so we can use it in contract storage. The struct itself was pretty simple, containing only primitive types. Now what if we want to store a more complex type, itself containing a custom struct?
1
2
3
4
5
6
7
8
9
10
11
12
#[derive(Drop, Serde)]
struct Pos2D {
x: u16,
y: u16
}
#[derive(Drop, Serde)]
struct Trip {
origin: Pos2D,
destination: Pos2D,
tag: felt252
}
A naive implementation of the StorageAccess trait for Trip
could go through all of the embedded structs and call read/write one by one on every primitive type. Obviously though, we want to reuse what we’ve built already, i.e. we want to call StorageAccess::read
and StorageAccess::write
for origin
and destination
when dealing with an instance of Trip
. Let’s see how we can achieve that, dealing with write
first:
1
2
3
4
5
6
7
8
9
10
11
12
13
fn write(address_domain: u32, base: StorageBaseAddress, value: Trip) -> SyscallResult::<()> {
StorageAccess::write(address_domain, base, value.origin)?;
let destination_base = storage_base_address_from_felt252(
storage_address_from_base_and_offset(base, 2_u8).into()
);
StorageAccess::write(address_domain, destination_base, value.destination)?;
let tag_base = storage_base_address_from_felt252(
storage_address_from_base_and_offset(base, 4_u8).into()
);
StorageAccess::write(address_domain, tag_base, value.tag)
}
To write origin
and destination
, we reuse the Pos2D StorageAccess
implementation by calling StorageAccess::write
. Similarly for tag
, which is a native type (felt252), hence comes with StorageAccess
out of the box.
It’s worth noticing two things. One, the compiler is smart enough to infer the types, we don’t have to do StorageAccess::<Pos2D>::write
. Two, we have to use a different base address for each member when calling write
. To understand why, let’s check how an instance of Trip
looks in Starknet’s storage using the above implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌───────────────────────────────────────────────────────────────────────────────────┐
│ │
│ trip │
│ │
├─────────────────────────────────┬─────────────────────────────────┬───────────────┤
│ │ │ │
│ origin │ destination │ tag │
│ │ │ │
├────────────────┬────────────────┼────────────────┬────────────────┼───────────────┤
│ │ │ │ │ │
│ x │ y │ x │ y │ │
│ │ │ │ │ │
├────────────────┼────────────────┼────────────────┼────────────────┼───────────────┤
│ ^ │ │ ^ │ │ ^ │
│ trip base │ │ trip base + 2 │ │ trip base + 4 │
│ origin base │ │ dest. base │ │ tag base │
└────────────────┴────────────────┴────────────────┴────────────────┴───────────────┘
It’s one continuous space, origin
occupying two slots, destination
next two and the last slot belonging to tag
. The base address of origin
and trip
are the same, but we have to calculate it using storage_address_from_base_and_offset
for the other members. Sadly, there’s no “size_of” function in Cairo (yet?), so we have to specify the offsets manually. So base address for destination
is trip base plus and offset of 2 (2 being the slot size necessary to store origin
). Base address for tag
is trip base plus 4 (2 for origin
+ 2 for destination
). In other words, it functions as an array.
Given this knowledge, we can deal with read
as well:
1
2
3
4
5
6
7
8
9
10
11
fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult::<Trip> {
let origin = StorageAccess::read(address_domain, base)?;
let destination_base = storage_base_address_from_felt252(storage_address_from_base_and_offset(base, 2_u8).into());
let destination = StorageAccess::read(address_domain, destination_base)?;
let tag_base = storage_base_address_from_felt252(storage_address_from_base_and_offset(base, 4_u8).into());
let tag = StorageAccess::read(address_domain, tag_base)?;
Result::Ok(Trip { origin, destination, tag })
}
In the same fashion as above, we use the appropriate base addresses for each member of the Trip
struct.
That’s it. We now have a full implementation of StorageAccess
for our slightly more complex Trip
data type. As usual, I’m attaching a full contract with a simple test (please use the lateset Cairo version (HEAD) as this won’t compile with 1.0.0-alpha.6).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
use starknet::StorageAccess;
use starknet::StorageBaseAddress;
use starknet::SyscallResult;
use starknet::storage_access;
use starknet::storage_read_syscall;
use starknet::storage_write_syscall;
use starknet::storage_base_address_from_felt252;
use starknet::storage_address_from_base_and_offset;
use traits::Into;
use traits::TryInto;
use option::OptionTrait;
#[derive(Drop, Serde)]
struct Pos2D {
x: u16,
y: u16
}
#[derive(Drop, Serde)]
struct Trip {
origin: Pos2D,
destination: Pos2D,
tag: felt252
}
impl Pos2DStorageAccess of StorageAccess::<Pos2D> {
fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult::<Pos2D> {
Result::Ok(
Pos2D {
x: storage_read_syscall(
address_domain,
storage_address_from_base_and_offset(base, 0_u8)
)?.try_into().unwrap(),
y: storage_read_syscall(
address_domain,
storage_address_from_base_and_offset(base, 1_u8)
)?.try_into().unwrap(),
}
)
}
fn write(address_domain: u32, base: StorageBaseAddress, value: Pos2D) -> SyscallResult::<()> {
storage_write_syscall(
address_domain,
storage_address_from_base_and_offset(base, 0_u8),
value.x.into()
)?;
storage_write_syscall(
address_domain,
storage_address_from_base_and_offset(base, 1_u8),
value.y.into()
)
}
}
impl TripStorageAccess of StorageAccess::<Trip> {
fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult::<Trip> {
let origin = StorageAccess::read(address_domain, base)?;
let destination_base = storage_base_address_from_felt252(storage_address_from_base_and_offset(base, 2_u8).into());
let destination = StorageAccess::read(address_domain, destination_base)?;
let tag_base = storage_base_address_from_felt252(storage_address_from_base_and_offset(base, 4_u8).into());
let tag = StorageAccess::read(address_domain, tag_base)?;
Result::Ok(Trip { origin, destination, tag })
}
fn write(address_domain: u32, base: StorageBaseAddress, value: Trip) -> SyscallResult::<()> {
StorageAccess::write(address_domain, base, value.origin)?;
let destination_base = storage_base_address_from_felt252(storage_address_from_base_and_offset(base, 2_u8).into());
StorageAccess::write(address_domain, destination_base, value.destination)?;
let tag_base = storage_base_address_from_felt252(storage_address_from_base_and_offset(base, 4_u8).into());
StorageAccess::write(address_domain, tag_base, value.tag)
}
}
#[contract]
mod Travel {
use super::Pos2D;
use super::Trip;
struct Storage {
trip: Trip
}
#[view]
fn get_trip() -> Trip {
trip::read()
}
#[external]
fn set_trip(ox: u16, oy: u16, dx: u16, dy: u16, tag: felt252) {
let origin = Pos2D { x: ox, y: oy };
let destination = Pos2D { x: dx, y: dy };
trip::write( Trip { origin, destination, tag });
}
}
#[test]
#[available_gas(1000000)]
fn test_trip_storage() {
let trip = Travel::get_trip();
assert(trip.origin.x == 0_u16, 'init origin x');
assert(trip.origin.y == 0_u16, 'init origin y');
assert(trip.destination.x == 0_u16, 'init destination x');
assert(trip.destination.y == 0_u16, 'init destination y');
assert(trip.tag == 0, 'init tag');
let ox = 12_u16;
let oy = 5_u16;
let dx = 24_u16;
let dy = 10_u16;
let tag = 'business';
Travel::set_trip(ox, oy, dx, dy, tag);
let trip = Travel::get_trip();
assert(trip.origin.x == ox, 'set origin x');
assert(trip.origin.y == oy, 'set origin y');
assert(trip.destination.x == dx, 'set destination x');
assert(trip.destination.y == dy, 'set destination y');
assert(trip.tag == tag, 'set tag');
}