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.
Run the Example
First go through the Setup process to get your development environment configured, then clone the v22.0.1
tag of soroban-examples
repository:
git clone -b v22.0.1 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
#[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/v22.0.1/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.
#[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 stellar contract build
command.
stellar 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 stellar-cli
installed, you can invoke contract functions in the Wasm using it.
- macOS/Linux
- Windows (PowerShell)
stellar contract invoke \
--wasm target/wasm32-unknown-unknown/release/soroban_custom_types_contract.wasm \
--id 1 \
-- \
increment \
--incr 5
stellar 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 stellar-cli
to inspect what the counter is after a few runs.
stellar contract read --id 1 --key STATE
STATE,"{""count"":25,""last_incr"":15}"