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 stellar 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 soroban-hello-world
directory, run:
stellar 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.
#![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 Symbol
s.
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 soroban-hello-world
, run:
stellar contract build
Check that it built:
ls target/wasm32-unknown-unknown/release/*.wasm
You should see both hello_world.wasm
and soroban_increment_contract.wasm
.
Tests
The following test has been added to the contracts/increment/src/test.rs
file.
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.