Skip to main content

Custom Types

The custom types example demonstrates how to define your own data structures that can be stored on the ledger, or used as inputs and outputs to contract invocations. This example is an extension of the storing data example.

Open in Gitpod

Run the Example

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

git clone -b v20.0.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 custom_types directory, and use cargo test.

cd custom_types
cargo test

You should see the output:

running 1 test
test test::test ... ok

Code

custom_types/src/lib.rs
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct State {
pub count: u32,
pub last_incr: u32,
}

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

#[contract]
pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {
/// Increment increments an internal counter, and returns the value.
pub fn increment(env: Env, incr: u32) -> u32 {
// Get the current count.
let mut state = Self::get_state(env.clone());

// Increment the count.
state.count += incr;
state.last_incr = incr;

// Save the count.
env.storage().instance().set(&STATE, &state);

// Return the count to the caller.
state.count
}
/// Return the current state.
pub fn get_state(env: Env) -> State {
env.storage().instance().get(&STATE).unwrap_or(State {
count: 0,
last_incr: 0,
}) // If no value set, assume 0.
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v20.0.0/custom_types

How it Works

Custom types are defined using the #[contracttype] attribute on either a struct or an enum.

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

Custom Type: Struct

Structs are stored on ledger as a map of key-value pairs, where the key is up to a 32 character string representing the field name, and the value is the value encoded.

Field names must be no more than 32 characters.

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct State {
pub count: u32,
pub last_incr: u32,
}

Custom Type: Enum

The example does not contain enums, but enums may also be contract types.

Enums containing unit and tuple variants are stored on ledger as a two element vector, where the first element is the name of the enum variant as a string up to 32 characters in length, and the value is the value if the variant has one.

Only unit variants and single value variants, like A and B below, are supported.

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Enum {
A,
B(...),
}

Enums containing integer values are stored on ledger as the u32 value.

#[contracttype]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u32)]
pub enum Enum {
A = 1,
B = 2,
}

Using Types in Functions

Types that have been annotated with #[contracttype] can be stored as contract data and retrieved later.

Types can also be used as inputs and outputs on contract functions.

pub fn increment(env: Env, incr: u32) -> u32 {
let mut state = Self::get_state(env.clone());
state.count += incr;
state.last_incr = incr;
env.storage().instance().set(&STATE, &state);
state.count
}

pub fn get_state(env: Env) -> State {
env.storage().instance().get(&STATE).unwrap_or(State {
count: 0,
last_incr: 0,
}) // If no value set, assume 0.
}

Tests

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

custom_types/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), 1);
assert_eq!(client.increment(&10), 11);
assert_eq!(
client.get_state(),
State {
count: 11,
last_incr: 10
}
);
}

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 test invokes the increment function on the registered contract that causes the State type to be stored and updated a couple times.

assert_eq!(client.increment(&1), 1);
assert_eq!(client.increment(&10), 11);

The test then invokes the get_state function to get the State value that was stored, and can assert on its values.

assert_eq!(
client.get_state(),
State {
count: 11,
last_incr: 10
}
);

Build the Contract

To build the contract, use the soroban contract build command.

soroban contract build

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

target/wasm32-unknown-unknown/release/soroban_custom_types_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_custom_types_contract.wasm \
--id 1 \
-- \
increment \
--incr 5

The following output should occur using the code above.

5

Run it a few more times with different increment amounts to watch the count change.

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

soroban contract read --id 1 --key STATE
STATE,"{""count"":25,""last_incr"":15}"