Storing User Data with Rust Structs and Enums

Soroban User Data

In Soroban we model contract state with Rust types. For example, the SoroMarket contract defines a Rust struct for each user’s bets and an enum for the market outcome.

Here is a simplified version of the code:

#[derive(Clone, PartialEq, Eq)]
#[contracttype]
pub enum Outcome {
    Undecided,
    Yes,
    No,
}

#[derive(Clone, PartialEq, Eq)]
#[contracttype]
pub struct Bets {
    pub yes: i128,
    pub no:  i128,
}

Here Outcome holds the global state of the contract as a single enum variable and Bets is a custom struct of user data holding each users wager on the two sides of a binary outcome.

This pattern using custom structs and enums to bundle related fields is a common pattern in Soroban development.

Persistent Storage

Soroban has a key value storage API, each entry is addressed by a storage key and holds a value.

It’s idiomatic to define a Rust enum for your keys, with variants that carry parameters. For example:

#[derive(Clone, PartialEq, Eq)]
#[contracttype]
pub enum StorageKey {
    Outcome,            // global data (no payload)
    Bets(Address),      // user-specific data
}

This is directly analogous to using a mapping key in Solidity:

mapping(address => Bets) public bets;
Bets my_bet = bets[user_addr];

Likewise, StorageKey::Bets(user_addr) yields a unique storage key per user.

At runtime you can use these keys with env.storage().persistent() to read or write a users bets:

let mut bets: Bets = env.storage()
    .persistent()
    .get(&StorageKey::Bets(user_addr.clone()))
    .unwrap_or(Ok(Bets { yes: 0, no: 0 }))
    .unwrap();

bets.yes += amount; // change mutable variable

env.storage().persistent().set(&StorageKey::Bets(user_addr.clone()), &bets);

Because each user has their own storage key, updating one user’s data only rewrites that entry. You don’t have to load or rewrite a giant map of all users. If you tried to store all users in one big map value, you’d have to read and write the entire map at once. This could cause potential denial of service attacks, high transaction fees and scaling issues. Using the enum key avoids all that.

In Solidity a mapping does this implicitly: bets[user] = whatever touches only that key. In Soroban, using an enum-key provides the same data flow to store the data as a persistant individual variable on-chain.

Iterating Over Mapped Entries

This is always a challenge within smart contracts due to the limitations of decentralized computing, particularly when we want to iterate over user data.

For example you might want to have a contract function which pays out every user an incentive token or something. The problem comes when we have an expanding amount of users and we start to hit transaction limits rendering that function unusable.

Soroban offers env.storage().persistent().keys(prefix, limit) to fetch keys, but you must supply a starting prefix and a maximum count. There’s no built-in “get all keys” due to resource constraints.

In practice it’s better to store a separate list of user addresses in an array or use an off-chain service to index contract data and then page through it as required by accepting prefix and limits variables into the public function.


For simple balances take a look at this example from the Stellar blog post series for EVM > Soroban which demonstrates how you can use a nested mapping for Address > U64 without a custom struct.

#![no_std]

use soroban_sdk::{contract, contractimpl, contracttype, Address, Env};

#[derive(Clone, PartialEq, Eq)]
#[contracttype]
pub enum DataKey {
    Map(Address),
    NestedMap(Address, u64),
}

#[contract]
pub struct MappingContract;

#[contractimpl]
impl MappingContract {
    pub fn get(env: &Env, addr: Address) -> u64 {
        env.storage()
            .persistent()
            .get(&DataKey::Map(addr))
            .unwrap_or(0)
    }

    pub fn set(env: &Env, addr: Address, value: u64) {
        env.storage().persistent().set(&DataKey::Map(addr), &value);
    }

    pub fn remove(env: &Env, addr: Address) {
        env.storage().persistent().remove(&DataKey::Map(addr));
    }

    pub fn get_nested_map(env: &Env, addr: Address, index: u64) -> bool {
        env.storage()
            .persistent()
            .get(&DataKey::NestedMap(addr, index))
            .unwrap_or(false)
    }

    pub fn set_nested_map(env: &Env, addr: Address, index: u64, value: bool) {
        env.storage()
            .persistent()
            .set(&DataKey::NestedMap(addr, index), &value);
    }

    pub fn remove_nested_map(env: &Env, addr: Address, index: u64) {
        env.storage()
            .persistent()
            .remove(&DataKey::NestedMap(addr, index));
    }
}

Using enums as data storage keys provides a dynamic, expandable and efficient method of storing user data within a Soroban smart contract. While we still need to be wary of iterating over that data it provides a very scalable method of storing structured data to create our decentralized applications.

Here is an example where I use this data structure to create a binary prediction market:

James On YouTube


Get The Blockchain Sector Newsletter, binge the YouTube channel and connect with me on Twitter

The Blockchain Sector newsletter goes out a few times a month when there is breaking news or interesting developments to discuss. All the content I produce is free, if you’d like to help please share this content on social media.

Thank you.

James Bachini

Disclaimer: Not a financial advisor, not financial advice. The content I create is to document my journey and for educational and entertainment purposes only. It is not under any circumstances investment advice. I am not an investment or trading professional and am learning myself while still making plenty of mistakes along the way. Any code published is experimental and not production ready to be used for financial transactions. Do your own research and do not play with funds you do not want to lose.


Posted

in

, , , ,

by