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.

Run the Example

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

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

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 enum Name {
None,
FirstLast(FirstLast),
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FirstLast {
pub first: Symbol,
pub last: Symbol,
}

pub struct CustomTypesContract;

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

#[contractimpl]
impl CustomTypesContract {
pub fn store(env: Env, name: Name) {
env.contract_data().set(NAME, name);
}

pub fn retrieve(env: Env) -> Name {
env.contract_data()
.get(NAME) // Get the value associated with key NAME.
.unwrap_or(Ok(Name::None)) // If no value, use None instead.
.unwrap()
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v0.0.4/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 a 10 character string representing the field name, and the value is the value encoded.

Field names must be no more than 10 characters.

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FirstLast {
pub first: Symbol,
pub last: Symbol,
}

Custom Type: Enum

Enums 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 10 characters in length, and the value is the value if the variant has one.

Only unit variants and single value variants, like None and FirstLast below, are supported.

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Name {
None,
FirstLast(FirstLast),
}

Using Structs and Enums in Functions

Types that have been annotated with #[contracttype] can be used as inputs and outputs on contract functions, and the values can be stored as contract data and retrieved later.

pub struct CustomTypesContract;

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

#[contractimpl]
impl CustomTypesContract {
pub fn store(env: Env, name: Name) {
env.contract_data().set(NAME, name);
}

pub fn retrieve(env: Env) -> Name {
env.contract_data()
.get(NAME) // Get the value associated with key NAME.
.unwrap_or(Ok(Name::None)) // If no value, use None instead.
.unwrap()
}
}

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 = BytesN::from_array(&env, &[0; 32]);
env.register_contract(&contract_id, CustomTypesContract);
let client = CustomTypesContractClient::new(&env, &contract_id);

assert_eq!(client.retrieve(), Name::None);

client.store(&Name::FirstLast(FirstLast {
first: symbol!("first"),
last: symbol!("last"),
}));

assert_eq!(
client.retrieve(),
Name::FirstLast(FirstLast {
first: symbol!("first"),
last: symbol!("last"),
}),
);
}

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();

Contracts must be registered with the environment with a contract ID, which is a 32-byte value.

let contract_id = BytesN::from_array(&env, [0; 32]);
env.register_contract(&contract_id, CustomTypesContract);

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 CustomTypesContract, and the client is named CustomTypesContractClient.

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

The test invokes the retrieve function on the registered contract, and asserts that it returns Name::None.

assert_eq!(client.retrieve(), Name::None);

The test then invokes the store function on the registered contract, to change the name that is stored.

client.store(&Name::FirstLast(FirstLast {
first: symbol!("first"),
last: symbol!("last"),
}));

The test invokes the retrieve function again, to assert that it returns the name that was previously stored.

assert_eq!(
client.retrieve(),
Name::FirstLast(FirstLast {
first: symbol!("first"),
last: symbol!("last"),
}),
);

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_custom_types_contract.wasm

Run the Contract

The soroban-cli is in early development and does not yet accept user-defined types as arguments at the command line. Follow the GitHub repository to find out about new releases:
https://github.com/stellar/soroban-cli