sidebar_position | title |
---|---|
3 |
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.
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
#[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
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.
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,
}
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),
}
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()
}
}
Open the custom_types/src/test.rs
file to follow along.
#[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"),
}),
);
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
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