Skip to main content

2. Storing Data

The increment example demonstrates how to write a simple contract that stores data, with a single function that increments an internal counter and returns the value.

Open in Gitpod

Run the Example

First go through the Setup process to get your development environment configured, then clone the v0.6.0 tag of soroban-examples repository:

git clone -b v0.6.0 https://github.com/stellar/soroban-examples

Or, skip the development environment setup and open this example in Gitpod.

To run the tests for the example, navigate to the increment directory, and use cargo test.

cd increment
cargo test -- --nocapture

You should see the output:

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

Code

increment/src/lib.rs
const COUNTER: Symbol = symbol!("COUNTER");

pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {
pub fn increment(env: Env) -> u32 {
let mut count: u32 = env
.storage()
.get(COUNTER)
.unwrap_or(Ok(0)) // If no value set, assume 0.
.unwrap(); // Panic if the value of COUNTER is not u32.
log!(&env, "count: {}", count);
count += 1;
env.storage().set(COUNTER, count);
count
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v0.6.0/increment

How it Works

Open the increment/src/lib.rs file to follow along.

Contract Data Keys

Contract data is associated with a key. The key is the value that can be used at a later time to lookup the value.

Symbols are a space and execution efficient value to use as static keys or names of things. They can also be used as short strings. When produced using symbol!(...) they are computed at compile time and stored in code as a 64-bit value. Their maximum character length is 10.

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

Contract Data Access

The Env data() function is used to retrieve access and update a counter. 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.

let mut count: u32 = env
.storage()
.get(COUNTER)
.unwrap_or(Ok(0)) // If no value set, assume 0.
.unwrap(); // Panic if the value of COUNTER is not u32.

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 an Ok(u32), otherwise the type returned will be an Err(_). The final unwrap() in the above code snippet is assuming the value will always be a u32. If a developer caused it to be some other type a panic would occur at the unwrap.

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

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

Tests

Open the increment/src/test.rs file to follow along.

increment/src/test.rs
#[test]
fn test() {
let env = Env::default();
let contract_id = env.register_contract(None, IncrementContract);
let client = IncrementContractClient::new(&env, &contract_id);

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

In any test the first thing that is always required is an Env, which is the Soroban environment that the contract will run in.

let env = Env::default();

The contract is registered with the environment using the contract type.

let contract_id = env.register_contract(None, IncrementContract);

All public functions within an impl block that is annotated with the #[contractimpl] attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with Client appended. For example, in our contract the contract type is IncrementContract, and the client is named IncrementContractClient.

let client = IncrementContractClient::new(&env, &contract_id);

The values returned by functions can be asserted on:

assert_eq!(client.increment(), 1);

Build the Contract

To build the contract, use the cargo build command.

cargo build --target wasm32-unknown-unknown --release

A .wasm file should be outputted in the ../target directory:

../target/wasm32-unknown-unknown/release/soroban_increment_contract.wasm

Run the Contract

If you have soroban-cli installed, you can invoke contract functions in the WASM using it.

soroban contract invoke \
--wasm ../target/wasm32-unknown-unknown/release/soroban_increment_contract.wasm \
--id 1 \
--fn increment

The following output should occur using the code above.

1

Rerun the invoke with the --footprint option to view the footprint of the invocation, which is the ledger entries that the contract will have read or written to.

soroban contract invoke \
--wasm ../target/wasm32-unknown-unknown/release/soroban_increment_contract.wasm \
--id 1 \
--fn increment \
--footprint
Footprint: {"readOnly":[{"contractData":{"contractId":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"key":{"static":"ledgerKeyContractCode"}}}],"readWrite":[{"contractData":{"contractId":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"key":{"symbol":[67,79,85,78,84,69,82]}}}]}
info

Soroban is a pre-release and at this time outputs footprints in an unstable JSON format.

Run it a few more times to watch the count change.

Use the soroban to inspect what the counter is after a few runs.

soroban contract read --id 1 --key COUNTER