Storing Collections with Vectors
The Vec
type provides a way to store collections of values in the contract's storage. In this section, we will explore how to declare, add elements to and retrieve elements from a Vec
, as well as how the storage addresses for Vec
variables are computed.
The Vec
type is provided by the Cairo core library, inside the core::starknet::storage
module. Its associated methods are defined in the VecTrait
and MutableVecTrait
traits that you will also need to import for read and write operations on the Vec
type.
The
Array<T>
type is a memory type and cannot be directly stored in contract storage. For storage, use theVec<T>
type, which is a [phantom type][phantom types] designed specifically for contract storage. However,Vec<T>
has limitations: it can't be instantiated as a regular variable, used as a function parameter, or included as a member in regular structs. To work with the full contents of aVec<T>
, you'll need to copy its elements to and from a memoryArray<T>
.
Declaring and Using Storage Vectors
To declare a Storage Vector, use the Vec
type enclosed in angle brackets <>
, specifying the type of elements it will store. In Listing 14-4, we create a simple contract that registers all the addresses that call it and stores them in a Vec
. We can then retrieve the n
-th registered address, or all registered addresses.
use core::starknet::ContractAddress;
#[starknet::interface]
trait IAddressList<TState> {
fn register_caller(ref self: TState);
fn get_n_th_registered_address(self: @TState, index: u64) -> Option<ContractAddress>;
fn get_all_addresses(self: @TState) -> Array<ContractAddress>;
fn modify_nth_address(ref self: TState, index: u64, new_address: ContractAddress);
}
#[starknet::contract]
mod AddressList {
use starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, MutableVecTrait,
};
use core::starknet::{get_caller_address, ContractAddress};
#[storage]
struct Storage {
addresses: Vec<ContractAddress>,
}
impl AddressListImpl of super::IAddressList<ContractState> {
fn register_caller(ref self: ContractState) {
let caller = get_caller_address();
self.addresses.append().write(caller);
}
fn get_n_th_registered_address(
self: @ContractState, index: u64,
) -> Option<ContractAddress> {
if let Option::Some(storage_ptr) = self.addresses.get(index) {
return Option::Some(storage_ptr.read());
}
return Option::None;
}
fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
let mut addresses = array![];
for i in 0..self.addresses.len() {
addresses.append(self.addresses.at(i).read());
};
addresses
}
fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
let mut storage_ptr = self.addresses.at(index);
storage_ptr.write(new_address);
}
}
}
To add an element to a Vec
, you use the append
method to get a storage pointer to the next available slot, and then call the write
function on it with the value to add.
use core::starknet::ContractAddress;
#[starknet::interface]
trait IAddressList<TState> {
fn register_caller(ref self: TState);
fn get_n_th_registered_address(self: @TState, index: u64) -> Option<ContractAddress>;
fn get_all_addresses(self: @TState) -> Array<ContractAddress>;
fn modify_nth_address(ref self: TState, index: u64, new_address: ContractAddress);
}
#[starknet::contract]
mod AddressList {
use starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, MutableVecTrait,
};
use core::starknet::{get_caller_address, ContractAddress};
#[storage]
struct Storage {
addresses: Vec<ContractAddress>,
}
impl AddressListImpl of super::IAddressList<ContractState> {
fn register_caller(ref self: ContractState) {
let caller = get_caller_address();
self.addresses.append().write(caller);
}
fn get_n_th_registered_address(
self: @ContractState, index: u64,
) -> Option<ContractAddress> {
if let Option::Some(storage_ptr) = self.addresses.get(index) {
return Option::Some(storage_ptr.read());
}
return Option::None;
}
fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
let mut addresses = array![];
for i in 0..self.addresses.len() {
addresses.append(self.addresses.at(i).read());
};
addresses
}
fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
let mut storage_ptr = self.addresses.at(index);
storage_ptr.write(new_address);
}
}
}
To retrieve an element, you can use the at
or get
methods to get a storage pointer to the element at the specified index, and then call the read
method to get the value. If the index is out of bounds, the at
method panics, while the get
method returns None
.
use core::starknet::ContractAddress;
#[starknet::interface]
trait IAddressList<TState> {
fn register_caller(ref self: TState);
fn get_n_th_registered_address(self: @TState, index: u64) -> Option<ContractAddress>;
fn get_all_addresses(self: @TState) -> Array<ContractAddress>;
fn modify_nth_address(ref self: TState, index: u64, new_address: ContractAddress);
}
#[starknet::contract]
mod AddressList {
use starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, MutableVecTrait,
};
use core::starknet::{get_caller_address, ContractAddress};
#[storage]
struct Storage {
addresses: Vec<ContractAddress>,
}
impl AddressListImpl of super::IAddressList<ContractState> {
fn register_caller(ref self: ContractState) {
let caller = get_caller_address();
self.addresses.append().write(caller);
}
fn get_n_th_registered_address(
self: @ContractState, index: u64,
) -> Option<ContractAddress> {
if let Option::Some(storage_ptr) = self.addresses.get(index) {
return Option::Some(storage_ptr.read());
}
return Option::None;
}
fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
let mut addresses = array![];
for i in 0..self.addresses.len() {
addresses.append(self.addresses.at(i).read());
};
addresses
}
fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
let mut storage_ptr = self.addresses.at(index);
storage_ptr.write(new_address);
}
}
}
If you want to retrieve all the elements of the Vec, you can iterate over the indices of the storage Vec
, read the value at each index, and append it to a memory Array<T>
.
Similarly, you can't store an Array<T>
in storage: you would need to iterate over the elements of the array and append them to a storage Vec<T>
.
At this point, you should be familiar with the concept of storage pointers and storage paths introduced in the "Contract Storage" section and how they are used to access storage variables through a pointer-based model. Thus how would you modify the address stored at a specific index of a Vec
?
use core::starknet::ContractAddress;
#[starknet::interface]
trait IAddressList<TState> {
fn register_caller(ref self: TState);
fn get_n_th_registered_address(self: @TState, index: u64) -> Option<ContractAddress>;
fn get_all_addresses(self: @TState) -> Array<ContractAddress>;
fn modify_nth_address(ref self: TState, index: u64, new_address: ContractAddress);
}
#[starknet::contract]
mod AddressList {
use starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait, MutableVecTrait,
};
use core::starknet::{get_caller_address, ContractAddress};
#[storage]
struct Storage {
addresses: Vec<ContractAddress>,
}
impl AddressListImpl of super::IAddressList<ContractState> {
fn register_caller(ref self: ContractState) {
let caller = get_caller_address();
self.addresses.append().write(caller);
}
fn get_n_th_registered_address(
self: @ContractState, index: u64,
) -> Option<ContractAddress> {
if let Option::Some(storage_ptr) = self.addresses.get(index) {
return Option::Some(storage_ptr.read());
}
return Option::None;
}
fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
let mut addresses = array![];
for i in 0..self.addresses.len() {
addresses.append(self.addresses.at(i).read());
};
addresses
}
fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
let mut storage_ptr = self.addresses.at(index);
storage_ptr.write(new_address);
}
}
}
The answer is fairly simple: get a mutable pointer to the storage pointer at the desired index, and use the write
method to modify the value at that index.
Storage Address Computation for Vecs
The address in storage of a variable stored in a Vec
is computed according to the following rules:
- The length of the
Vec
is stored at the base address, computed assn_keccak(variable_name)
. - The elements of the
Vec
are stored in addresses computed ash(base_address, i)
, wherei
is the index of the element in theVec
andh
is the Pedersen hash function.
Summary
- Use the
Vec
type to store collections of values in contract storage - Access Vecs using the
append
method to add elements, and theat
orget
methods to read elements - The address of a
Vec
variable is computed using thesn_keccak
and the Pedersen hash functions
This wraps up our tour of the Contract Storage! In the next section, we'll start looking at the different kind of functions defined in a contract. You already know most of them, as we used them in the previous chapters, but we'll explain them in more detail.