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
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 15-3,
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 starknet::ContractAddress;
#[starknet::interface]
pub 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);
fn pop_last_registered_address(ref self: TState) -> Option<ContractAddress>;
}
#[starknet::contract]
pub mod AddressList {
use starknet::storage::{
MutableVecTrait, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
addresses: Vec<ContractAddress>,
}
#[abi(embed_v0)]
impl AddressListImpl of super::IAddressList<ContractState> {
fn register_caller(ref self: ContractState) {
let caller = get_caller_address();
self.addresses.push(caller);
}
fn get_n_th_registered_address(
self: @ContractState, index: u64,
) -> Option<ContractAddress> {
self.addresses.get(index).map(|ptr| ptr.read())
}
fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
let mut addresses = array![];
for i in 0..self.addresses.len() {
addresses.append(self.addresses[i].read());
}
addresses
}
fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
self.addresses[index].write(new_address);
}
fn pop_last_registered_address(ref self: ContractState) -> Option<ContractAddress> {
self.addresses.pop()
}
}
}
#[cfg(test)]
mod tests {
use snforge_std::{
ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
stop_cheat_caller_address,
};
use starknet::ContractAddress;
use super::{IAddressListDispatcher, IAddressListDispatcherTrait};
fn deploy_contract() -> IAddressListDispatcher {
let contract = declare("AddressList").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
IAddressListDispatcher { contract_address }
}
#[test]
fn test_get_out_of_bounds_returns_none() {
let dispatcher = deploy_contract();
assert!(dispatcher.get_n_th_registered_address(0).is_none());
}
#[test]
fn test_register_and_get_single() {
let dispatcher = deploy_contract();
let user: ContractAddress = 0x111.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, user);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
let first = dispatcher.get_n_th_registered_address(0).unwrap();
assert_eq!(first, user);
}
#[test]
fn test_register_multiple_and_get_all_in_order() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0xaaa.try_into().unwrap();
let a2: ContractAddress = 0xbbb.try_into().unwrap();
let a3: ContractAddress = 0xccc.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a3);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
let mut all = dispatcher.get_all_addresses();
assert_eq!(all.pop_front().unwrap(), a1);
assert_eq!(all.pop_front().unwrap(), a2);
assert_eq!(all.pop_front().unwrap(), a3);
assert!(all.pop_front().is_none());
}
#[test]
fn test_modify_nth_address() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0x101.try_into().unwrap();
let a2: ContractAddress = 0x202.try_into().unwrap();
let a3: ContractAddress = 0x303.try_into().unwrap();
let new_mid: ContractAddress = 0x404.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a3);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
// Modify the second entry (index 1)
dispatcher.modify_nth_address(1, new_mid);
assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
assert_eq!(dispatcher.get_n_th_registered_address(1).unwrap(), new_mid);
assert_eq!(dispatcher.get_n_th_registered_address(2).unwrap(), a3);
}
#[test]
fn test_pop_empty_returns_none() {
let dispatcher = deploy_contract();
assert!(dispatcher.pop_last_registered_address().is_none());
}
#[test]
fn test_pop_removes_last_in_lifo_order() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0x111.try_into().unwrap();
let a2: ContractAddress = 0x222.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
// First pop returns last pushed (a2)
assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a2);
// Index 1 should now be out of bounds
assert!(dispatcher.get_n_th_registered_address(1).is_none());
// Index 0 remains a1
assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
// Second pop returns a1 and empties the list
assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a1);
assert!(dispatcher.get_n_th_registered_address(0).is_none());
// Further pops return None
assert!(dispatcher.pop_last_registered_address().is_none());
}
}
Declaring a storage Vec in the Storage struct
To add an element to a Vec, you can use the push method to add an element to
the end of the Vec.
use starknet::ContractAddress;
#[starknet::interface]
pub 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);
fn pop_last_registered_address(ref self: TState) -> Option<ContractAddress>;
}
#[starknet::contract]
pub mod AddressList {
use starknet::storage::{
MutableVecTrait, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
addresses: Vec<ContractAddress>,
}
#[abi(embed_v0)]
impl AddressListImpl of super::IAddressList<ContractState> {
fn register_caller(ref self: ContractState) {
let caller = get_caller_address();
self.addresses.push(caller);
}
fn get_n_th_registered_address(
self: @ContractState, index: u64,
) -> Option<ContractAddress> {
self.addresses.get(index).map(|ptr| ptr.read())
}
fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
let mut addresses = array![];
for i in 0..self.addresses.len() {
addresses.append(self.addresses[i].read());
}
addresses
}
fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
self.addresses[index].write(new_address);
}
fn pop_last_registered_address(ref self: ContractState) -> Option<ContractAddress> {
self.addresses.pop()
}
}
}
#[cfg(test)]
mod tests {
use snforge_std::{
ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
stop_cheat_caller_address,
};
use starknet::ContractAddress;
use super::{IAddressListDispatcher, IAddressListDispatcherTrait};
fn deploy_contract() -> IAddressListDispatcher {
let contract = declare("AddressList").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
IAddressListDispatcher { contract_address }
}
#[test]
fn test_get_out_of_bounds_returns_none() {
let dispatcher = deploy_contract();
assert!(dispatcher.get_n_th_registered_address(0).is_none());
}
#[test]
fn test_register_and_get_single() {
let dispatcher = deploy_contract();
let user: ContractAddress = 0x111.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, user);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
let first = dispatcher.get_n_th_registered_address(0).unwrap();
assert_eq!(first, user);
}
#[test]
fn test_register_multiple_and_get_all_in_order() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0xaaa.try_into().unwrap();
let a2: ContractAddress = 0xbbb.try_into().unwrap();
let a3: ContractAddress = 0xccc.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a3);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
let mut all = dispatcher.get_all_addresses();
assert_eq!(all.pop_front().unwrap(), a1);
assert_eq!(all.pop_front().unwrap(), a2);
assert_eq!(all.pop_front().unwrap(), a3);
assert!(all.pop_front().is_none());
}
#[test]
fn test_modify_nth_address() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0x101.try_into().unwrap();
let a2: ContractAddress = 0x202.try_into().unwrap();
let a3: ContractAddress = 0x303.try_into().unwrap();
let new_mid: ContractAddress = 0x404.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a3);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
// Modify the second entry (index 1)
dispatcher.modify_nth_address(1, new_mid);
assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
assert_eq!(dispatcher.get_n_th_registered_address(1).unwrap(), new_mid);
assert_eq!(dispatcher.get_n_th_registered_address(2).unwrap(), a3);
}
#[test]
fn test_pop_empty_returns_none() {
let dispatcher = deploy_contract();
assert!(dispatcher.pop_last_registered_address().is_none());
}
#[test]
fn test_pop_removes_last_in_lifo_order() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0x111.try_into().unwrap();
let a2: ContractAddress = 0x222.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
// First pop returns last pushed (a2)
assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a2);
// Index 1 should now be out of bounds
assert!(dispatcher.get_n_th_registered_address(1).is_none());
// Index 0 remains a1
assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
// Second pop returns a1 and empties the list
assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a1);
assert!(dispatcher.get_n_th_registered_address(0).is_none());
// Further pops return None
assert!(dispatcher.pop_last_registered_address().is_none());
}
}
To retrieve an element, you can use the indexing syntax (vec[index]) or the
at/get methods to obtain a storage pointer to the element at the specified
index, and then call read() to get the value. If the index is out of bounds,
at (and indexing) panics, while get returns None.
use starknet::ContractAddress;
#[starknet::interface]
pub 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);
fn pop_last_registered_address(ref self: TState) -> Option<ContractAddress>;
}
#[starknet::contract]
pub mod AddressList {
use starknet::storage::{
MutableVecTrait, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
addresses: Vec<ContractAddress>,
}
#[abi(embed_v0)]
impl AddressListImpl of super::IAddressList<ContractState> {
fn register_caller(ref self: ContractState) {
let caller = get_caller_address();
self.addresses.push(caller);
}
fn get_n_th_registered_address(
self: @ContractState, index: u64,
) -> Option<ContractAddress> {
self.addresses.get(index).map(|ptr| ptr.read())
}
fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
let mut addresses = array![];
for i in 0..self.addresses.len() {
addresses.append(self.addresses[i].read());
}
addresses
}
fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
self.addresses[index].write(new_address);
}
fn pop_last_registered_address(ref self: ContractState) -> Option<ContractAddress> {
self.addresses.pop()
}
}
}
#[cfg(test)]
mod tests {
use snforge_std::{
ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
stop_cheat_caller_address,
};
use starknet::ContractAddress;
use super::{IAddressListDispatcher, IAddressListDispatcherTrait};
fn deploy_contract() -> IAddressListDispatcher {
let contract = declare("AddressList").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
IAddressListDispatcher { contract_address }
}
#[test]
fn test_get_out_of_bounds_returns_none() {
let dispatcher = deploy_contract();
assert!(dispatcher.get_n_th_registered_address(0).is_none());
}
#[test]
fn test_register_and_get_single() {
let dispatcher = deploy_contract();
let user: ContractAddress = 0x111.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, user);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
let first = dispatcher.get_n_th_registered_address(0).unwrap();
assert_eq!(first, user);
}
#[test]
fn test_register_multiple_and_get_all_in_order() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0xaaa.try_into().unwrap();
let a2: ContractAddress = 0xbbb.try_into().unwrap();
let a3: ContractAddress = 0xccc.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a3);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
let mut all = dispatcher.get_all_addresses();
assert_eq!(all.pop_front().unwrap(), a1);
assert_eq!(all.pop_front().unwrap(), a2);
assert_eq!(all.pop_front().unwrap(), a3);
assert!(all.pop_front().is_none());
}
#[test]
fn test_modify_nth_address() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0x101.try_into().unwrap();
let a2: ContractAddress = 0x202.try_into().unwrap();
let a3: ContractAddress = 0x303.try_into().unwrap();
let new_mid: ContractAddress = 0x404.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a3);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
// Modify the second entry (index 1)
dispatcher.modify_nth_address(1, new_mid);
assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
assert_eq!(dispatcher.get_n_th_registered_address(1).unwrap(), new_mid);
assert_eq!(dispatcher.get_n_th_registered_address(2).unwrap(), a3);
}
#[test]
fn test_pop_empty_returns_none() {
let dispatcher = deploy_contract();
assert!(dispatcher.pop_last_registered_address().is_none());
}
#[test]
fn test_pop_removes_last_in_lifo_order() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0x111.try_into().unwrap();
let a2: ContractAddress = 0x222.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
// First pop returns last pushed (a2)
assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a2);
// Index 1 should now be out of bounds
assert!(dispatcher.get_n_th_registered_address(1).is_none());
// Index 0 remains a1
assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
// Second pop returns a1 and empties the list
assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a1);
assert!(dispatcher.get_n_th_registered_address(0).is_none());
// Further pops return None
assert!(dispatcher.pop_last_registered_address().is_none());
}
}
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 starknet::ContractAddress;
#[starknet::interface]
pub 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);
fn pop_last_registered_address(ref self: TState) -> Option<ContractAddress>;
}
#[starknet::contract]
pub mod AddressList {
use starknet::storage::{
MutableVecTrait, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
addresses: Vec<ContractAddress>,
}
#[abi(embed_v0)]
impl AddressListImpl of super::IAddressList<ContractState> {
fn register_caller(ref self: ContractState) {
let caller = get_caller_address();
self.addresses.push(caller);
}
fn get_n_th_registered_address(
self: @ContractState, index: u64,
) -> Option<ContractAddress> {
self.addresses.get(index).map(|ptr| ptr.read())
}
fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
let mut addresses = array![];
for i in 0..self.addresses.len() {
addresses.append(self.addresses[i].read());
}
addresses
}
fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
self.addresses[index].write(new_address);
}
fn pop_last_registered_address(ref self: ContractState) -> Option<ContractAddress> {
self.addresses.pop()
}
}
}
#[cfg(test)]
mod tests {
use snforge_std::{
ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
stop_cheat_caller_address,
};
use starknet::ContractAddress;
use super::{IAddressListDispatcher, IAddressListDispatcherTrait};
fn deploy_contract() -> IAddressListDispatcher {
let contract = declare("AddressList").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
IAddressListDispatcher { contract_address }
}
#[test]
fn test_get_out_of_bounds_returns_none() {
let dispatcher = deploy_contract();
assert!(dispatcher.get_n_th_registered_address(0).is_none());
}
#[test]
fn test_register_and_get_single() {
let dispatcher = deploy_contract();
let user: ContractAddress = 0x111.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, user);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
let first = dispatcher.get_n_th_registered_address(0).unwrap();
assert_eq!(first, user);
}
#[test]
fn test_register_multiple_and_get_all_in_order() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0xaaa.try_into().unwrap();
let a2: ContractAddress = 0xbbb.try_into().unwrap();
let a3: ContractAddress = 0xccc.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a3);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
let mut all = dispatcher.get_all_addresses();
assert_eq!(all.pop_front().unwrap(), a1);
assert_eq!(all.pop_front().unwrap(), a2);
assert_eq!(all.pop_front().unwrap(), a3);
assert!(all.pop_front().is_none());
}
#[test]
fn test_modify_nth_address() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0x101.try_into().unwrap();
let a2: ContractAddress = 0x202.try_into().unwrap();
let a3: ContractAddress = 0x303.try_into().unwrap();
let new_mid: ContractAddress = 0x404.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a3);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
// Modify the second entry (index 1)
dispatcher.modify_nth_address(1, new_mid);
assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
assert_eq!(dispatcher.get_n_th_registered_address(1).unwrap(), new_mid);
assert_eq!(dispatcher.get_n_th_registered_address(2).unwrap(), a3);
}
#[test]
fn test_pop_empty_returns_none() {
let dispatcher = deploy_contract();
assert!(dispatcher.pop_last_registered_address().is_none());
}
#[test]
fn test_pop_removes_last_in_lifo_order() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0x111.try_into().unwrap();
let a2: ContractAddress = 0x222.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
// First pop returns last pushed (a2)
assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a2);
// Index 1 should now be out of bounds
assert!(dispatcher.get_n_th_registered_address(1).is_none());
// Index 0 remains a1
assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
// Second pop returns a1 and empties the list
assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a1);
assert!(dispatcher.get_n_th_registered_address(0).is_none());
// Further pops return None
assert!(dispatcher.pop_last_registered_address().is_none());
}
}
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.
You can also remove the last element of a storage Vec using the pop method.
It returns Some(value) if the vector is non-empty and None otherwise, and
updates the stored length accordingly.
use starknet::ContractAddress;
#[starknet::interface]
pub 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);
fn pop_last_registered_address(ref self: TState) -> Option<ContractAddress>;
}
#[starknet::contract]
pub mod AddressList {
use starknet::storage::{
MutableVecTrait, StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait,
};
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
addresses: Vec<ContractAddress>,
}
#[abi(embed_v0)]
impl AddressListImpl of super::IAddressList<ContractState> {
fn register_caller(ref self: ContractState) {
let caller = get_caller_address();
self.addresses.push(caller);
}
fn get_n_th_registered_address(
self: @ContractState, index: u64,
) -> Option<ContractAddress> {
self.addresses.get(index).map(|ptr| ptr.read())
}
fn get_all_addresses(self: @ContractState) -> Array<ContractAddress> {
let mut addresses = array![];
for i in 0..self.addresses.len() {
addresses.append(self.addresses[i].read());
}
addresses
}
fn modify_nth_address(ref self: ContractState, index: u64, new_address: ContractAddress) {
self.addresses[index].write(new_address);
}
fn pop_last_registered_address(ref self: ContractState) -> Option<ContractAddress> {
self.addresses.pop()
}
}
}
#[cfg(test)]
mod tests {
use snforge_std::{
ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address,
stop_cheat_caller_address,
};
use starknet::ContractAddress;
use super::{IAddressListDispatcher, IAddressListDispatcherTrait};
fn deploy_contract() -> IAddressListDispatcher {
let contract = declare("AddressList").unwrap().contract_class();
let (contract_address, _) = contract.deploy(@array![]).unwrap();
IAddressListDispatcher { contract_address }
}
#[test]
fn test_get_out_of_bounds_returns_none() {
let dispatcher = deploy_contract();
assert!(dispatcher.get_n_th_registered_address(0).is_none());
}
#[test]
fn test_register_and_get_single() {
let dispatcher = deploy_contract();
let user: ContractAddress = 0x111.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, user);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
let first = dispatcher.get_n_th_registered_address(0).unwrap();
assert_eq!(first, user);
}
#[test]
fn test_register_multiple_and_get_all_in_order() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0xaaa.try_into().unwrap();
let a2: ContractAddress = 0xbbb.try_into().unwrap();
let a3: ContractAddress = 0xccc.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a3);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
let mut all = dispatcher.get_all_addresses();
assert_eq!(all.pop_front().unwrap(), a1);
assert_eq!(all.pop_front().unwrap(), a2);
assert_eq!(all.pop_front().unwrap(), a3);
assert!(all.pop_front().is_none());
}
#[test]
fn test_modify_nth_address() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0x101.try_into().unwrap();
let a2: ContractAddress = 0x202.try_into().unwrap();
let a3: ContractAddress = 0x303.try_into().unwrap();
let new_mid: ContractAddress = 0x404.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a3);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
// Modify the second entry (index 1)
dispatcher.modify_nth_address(1, new_mid);
assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
assert_eq!(dispatcher.get_n_th_registered_address(1).unwrap(), new_mid);
assert_eq!(dispatcher.get_n_th_registered_address(2).unwrap(), a3);
}
#[test]
fn test_pop_empty_returns_none() {
let dispatcher = deploy_contract();
assert!(dispatcher.pop_last_registered_address().is_none());
}
#[test]
fn test_pop_removes_last_in_lifo_order() {
let dispatcher = deploy_contract();
let a1: ContractAddress = 0x111.try_into().unwrap();
let a2: ContractAddress = 0x222.try_into().unwrap();
start_cheat_caller_address(dispatcher.contract_address, a1);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
start_cheat_caller_address(dispatcher.contract_address, a2);
dispatcher.register_caller();
stop_cheat_caller_address(dispatcher.contract_address);
// First pop returns last pushed (a2)
assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a2);
// Index 1 should now be out of bounds
assert!(dispatcher.get_n_th_registered_address(1).is_none());
// Index 0 remains a1
assert_eq!(dispatcher.get_n_th_registered_address(0).unwrap(), a1);
// Second pop returns a1 and empties the list
assert_eq!(dispatcher.pop_last_registered_address().unwrap(), a1);
assert!(dispatcher.get_n_th_registered_address(0).is_none());
// Further pops return None
assert!(dispatcher.pop_last_registered_address().is_none());
}
}
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
Vecis stored at the base address, computed assn_keccak(variable_name). - The elements of the
Vecare stored in addresses computed ash(base_address, i), whereiis the index of the element in theVecandhis the Pedersen hash function.
Summary
- Use the
Vectype to store collections of values in contract storage - Access Vecs using the
pushmethod to add elements, thepopmethod to remove the last element, and theat/indexing orgetmethods to read elements - The address of a
Vecvariable is computed using thesn_keccakand 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.