Skip to main content
Version: next

Advanced Storage Concepts

The Odra Framework provides advanced storage interaction capabilities that extend beyond the basic storage interaction. This document will focus on the Mapping and Sequence modules, which are key components of the advanced storage interaction in Odra.

Recap and Basic Concepts​

Before we delve into the advanced features, let's recap some basic storage concepts in Odra. In the realm of basic storage interaction, Odra provides several types for interacting with contract storage, including Var, Mapping, and List. These types enable contracts to store and retrieve data in a structured manner. The Var type is used to store a single value, while the List and Mapping types store collections of values.

Var: A Var in Odra is a fundamental building block used for storing single values. Each Var is uniquely identified by its name in the contract.

Mapping: Mapping in Odra serves as a key-value storage system. It stores an association of unique keys to values, and the value can be retrieved using the key.

List: Built on top of the Var and Mapping building blocks, List in Odra allows storing an ordered collection of values that can be iterated over.

If you need a refresher on these topics, please refer to our guide on basic storage in Odra.

Advanced Storage Concepts​

Sequence​

The Sequence in Odra is a basic module that stores a single value in the storage that can be read or incremented. Internally, holds a Var which keeps track of the current value.

simplified
pub struct Sequence<T>
where
T: Num + One + Zero + Default + Copy + ToBytes + FromBytes + CLTyped
{
value: Var<T>
}

The Sequence module provides functions get_current_value and next_value to get the current value and advance the sequence respectively.

caution

next_value() returns 0 the first time it is called on a fresh Sequence, then 1, 2, and so on - it is a zero-based counter, not a pre-increment starting at 1.

Advanced Mapping​

In Odra, a Mapping is a key-value storage system where the key is associated with a value. In previous examples, the value of the Mapping typically comprised a standard serializable type (such as number, string, or bool) or a custom type marked with the #[odra::odra_type] attribute.

However, there are more advanced scenarios where the value of the Mapping represents a module itself. This approach is beneficial when managing a collection of modules, each maintaining its unique state.

Let's consider the following example:

examples/src/features/storage/mapping.rs
use odra::casper_types::U256;
use odra::prelude::*;
use crate::owned_token::OwnedToken;

#[odra::module]
pub struct Mappings {
strings: Mapping<(String, u32, String), String>,
tokens: Mapping<String, OwnedToken>
}

#[odra::module]
impl Mappings {

...

pub fn total_supply(&mut self, token_name: String) -> U256 {
self.tokens.module(&token_name).total_supply()
}

pub fn get_string_api(
&self,
key1: String,
key2: u32,
key3: String
) -> String {
let opt_string = self.strings.get(&(key1, key2, key3));
opt_string.unwrap_or_revert(&self.env())
}
}

As you can see, a Mapping key can consist of a tuple of values, not limited to a single value.

note

Accessing Odra modules differs from accessing regular values such as strings or numbers.

Firstly, within a Mapping, you don't encapsulate the module with Submodule.

Secondly, rather than utilizing the Mapping::get() function, call Mapping::module(), which returns SubModule<T> and sets the appropriate namespace for nested modules.

Mapping with a List value​

Besides plain values and modules, a Mapping value can be a List. This gives every key its own growable, iterable collection - the natural way to model "many items per owner", such as a schedule of tasks per address, or the token ids held by an account.

Reach for Mapping::list(), which mirrors Mapping::module(): it folds the key into the storage namespace and instantiates a List against it.

examples/src/features/storage/list_in_mapping.rs
use odra::prelude::*;

#[odra::module]
pub struct ScheduleContract {
schedules: Mapping<Address, List<u32>>
}

#[odra::module]
impl ScheduleContract {
/// Adds a task id to the owner's schedule.
pub fn add_task(&mut self, owner: &Address, id: u32) {
self.schedules.list(owner).push(id);
}

/// Returns the number of tasks scheduled for the given address.
pub fn task_count(&self, owner: &Address) -> u32 {
self.schedules.list(owner).len()
}

/// Returns the n-th task id scheduled for the given address.
pub fn task_at(&self, owner: &Address, index: u32) -> Option<u32> {
self.schedules.list(owner).get(index)
}
}

The value returned by list() is a fully functional List, so the whole List API is available on it - push, pop, get, replace, len, is_empty and iter.

Each key gets its own, independently stored List. Pushing to alice's list leaves bob's list untouched, and a key that has never been written to simply reads back as an empty list - len() returns 0 and get() returns None, with no initialisation step required.

note

Note the difference from Mapping::module(): module() returns SubModule<T>, whereas list() returns a bare List<T>. That is because a List is a storage component rather than a module, so there is nothing to wrap - you never declare SubModule<List<T>> either.

caution

A List can only be a mapping value, never a key. Every Mapping accessor requires the key type to implement ToBytes, and List deliberately implements no serialization, so Mapping<List<u32>, u32> fails to compile with the trait bound List<u32>: ToBytes is not satisfied.

AdvancedStorage Contract​

The given code snippet showcases the AdvancedStorage contract that incorporates these storage concepts.

Token here is an ordinary module - any #[odra::module] type will do. For this example it only needs to expose mint and balance_of:

modules.rs
use odra::casper_types::U512;
use odra::prelude::*;

#[odra::module]
pub struct Token {
balances: Mapping<Address, U512>,
}

#[odra::module]
impl Token {
pub fn mint(&mut self, amount: U512, to: Address) {
self.balances.add(&to, amount);
}

pub fn balance_of(&self, account: &Address) -> U512 {
self.balances.get_or_default(account)
}
}
use odra::casper_types::U512;
use odra::prelude::*;
use crate::modules::Token;

#[odra::module]
pub struct AdvancedStorage {
counter: Sequence<u32>,
tokens: Mapping<(String, String), Token>,
}

#[odra::module]
impl AdvancedStorage {
pub fn current_value(&self) -> u32 {
self.counter.get_current_value()
}

pub fn increment_and_get(&mut self) -> u32 {
self.counter.next_value()
}

pub fn balance_of(&mut self, token_name: String, creator: String, address: Address) -> U512 {
let token = self.tokens.module(&(token_name, creator));
token.balance_of(&address)
}

pub fn mint(&self, token_name: String, creator: String, amount: U512, to: Address) {
let mut token = self.tokens.module(&(token_name, creator));
token.mint(amount, to);
}
}

Conclusion​

Advanced storage features in Odra offer robust options for managing contract state. Two key takeaways from this document are:

  1. Odra offers a Sequence module, enabling contracts to store and increment a single value.
  2. Mappings support composite keys expressed as tuples, and can store modules (module()) or lists (list()) as values.

Understanding these concepts can help developers design and implement more efficient and flexible smart contracts.