Skip to main content

3. Storing Data

Now that we've built a basic Hello World example contract, we'll write a simple contract that stores and retrieves data. This will help you see the basics of Soroban's storage system.

This is going to follow along with the increment example, which has a single function that increments an internal counter and returns the value. If you want to see a working example, try it in GitPod.

This tutorial assumes that you've already completed the previous steps in Getting Started: Setup, Hello World, and Deploy to Testnet.

Adding the increment contract

The soroban contract init command allows us to initialize a new project with any of the example contracts from the soroban-examples repo, using the --with-example (or -w) flag.

It will not overwrite existing files, so we can also use this command to add a new contract to an existing project. Run the command again with a --with-example flag to add an increment contract to our project. From inside our getting-started-tutorial directory, run:

soroban contract init ./ --with-example increment

This will create a new contracts/increment directory with the following files:

└── contracts
├── increment
├── Cargo.lock
├── Cargo.toml
└── src
├── lib.rs
└── test.rs

The following code was added to contracts/increment/src/lib.rs. We'll go over it in more detail below.

#![no_std]
use soroban_sdk::{contract, contractimpl, log, symbol_short, Env, Symbol};

const COUNTER: Symbol = symbol_short!("COUNTER");

#[contract]
pub struct IncrementorContract;

#[contractimpl]
impl IncrementorContract {
/// Increment an internal counter; return the new value.
pub fn increment(env: Env) -> u32 {
let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0);

count += 1;

log!(&env, "count: {}", count);

env.storage().instance().set(&COUNTER, &count);

env.storage().instance().extend_ttl(100, 100);

count
}
}

mod test;

Imports

This contract begins similarly to our Hello World contract, with an annotation to exclude the Rust standard library, and imports of the types and macros we need from the soroban-sdk crate.

contracts/increment/src/lib.rs
#![no_std]
use soroban_sdk::{contract, contractimpl, log, symbol_short, Env, Symbol};

Contract Data Keys

const COUNTER: Symbol = symbol_short!("COUNTER");

Contract data is associated with a key, which can be used at a later time to look up the value.

Symbol is a short (up to 32 characters long) string type with limited character space (only a-zA-z0-9_ characters are allowed). Identifiers like contract function names and contract data keys are represented by Symbols.

The symbol_short!() macro is a convenient way to pre-compute short symbols up to 9 characters in length at compile time using Symbol::short. It generates a compile-time constant that adheres to the valid character set of letters (a-zA-Z), numbers (0-9), and underscores (_). If a symbol exceeds the 9-character limit, Symbol::new should be utilized for creating symbols at runtime.

Contract Data Access

let mut count: u32 = env
.storage()
.instance()
.get(&COUNTER)
.unwrap_or(0); // If no value set, assume 0.

The Env.storage() function is used to access and update contract data. The executing contract is the only contract that can query or modify contract data that it has stored. The data stored is viewable on ledger anywhere the ledger is viewable, but contracts executing within the Soroban environment are restricted to their own data.

The get() function gets the current value associated with the counter key.

If no value is currently stored, the value given to unwrap_or(...) is returned instead.

Values stored as contract data and retrieved are transmitted from the environment and expanded into the type specified. In this case a u32. If the value can be expanded, the type returned will be a u32. Otherwise, if a developer caused it to be some other type, a panic would occur at the unwrap.

env.storage()
.instance()
.set(&COUNTER, &count);

The set() function stores the new count value against the key, replacing the existing value.

Managing Contract Data TTLs with extend_ttl()

env.storage().instance().extend_ttl(100, 100);

All contract data has a Time To Live (TTL), measured in ledgers, that must be periodically extended. If an entry's TTL is not periodically extended, the entry will eventually become "archived." You can learn more about this in the State Archival document.

For now, it's worth knowing that there are three kinds of storage: Persistent, Temporary, and Instance. This contract only uses Instance storage: env.storage().instance(). Every time the counter is incremented, this storage's TTL gets extended by 100 ledgers, or about 500 seconds.

Build the contract

From inside getting-started-tutorial, run:

soroban contract build

Check that it built:

ls target/wasm32-unknown-unknown/release/*.wasm

You should see both soroban_hello_world_contract.wasm and soroban_increment_contract.wasm.

Tests

The following test has been added to the contracts/increment/src/test.rs file.

contracts/incrementor/src/test.rs
use crate::{IncrementorContract, IncrementorContractClient};
use soroban_sdk::Env;

#[test]
fn increment() {
let env = Env::default();
let contract_id = env.register_contract(None, IncrementorContract);
let client = IncrementorContractClient::new(&env, &contract_id);

assert_eq!(client.increment(), 1);
assert_eq!(client.increment(), 2);
assert_eq!(client.increment(), 3);
}

This uses the same concepts described in the Hello World example.

Make sure it passes:

cargo test

You'll see that this runs tests for the whole workspace; both the Hello World contract and the new Increment contract.

If you want to see the output of the log! call, run the tests with --nocapture:

cargo test -- --nocapture

You should see the output:

running 1 test
count: U32(0)
count: U32(1)
count: U32(2)
test test::incrementor ... ok

Take it further

Can you figure out how to add get_current_value function to the contract? What about decrement or reset functions?

Summary

In this section, we added a new contract to this project, that made use of Soroban's storage capabilities to store and retrieve data. We also learned about the different kinds of storage and how to manage their TTLs.

Next we'll learn a bit more about deploying contracts to Soroban's Testnet network and interact with our incrementor contract using the CLI.