Build a contract schema#

This guide will show you how to build a smart contract schema, how to export it to a file, and/or embed the schema into the smart contract module, all using cargo-concordium.

Note

Schemas are used by off-chain tools to represent byte data in a more human-readable manner.

Preparation#

First, ensure you have cargo-concordium installed and if not the guide Install tools for development will help you.

We also need the Rust source code of the smart contract you wish to build a schema for.

Setup the contract for a schema#

In order to build a contract schema, we first have to prepare our smart contract for building the schema.

You can choose which parts of the smart contract to include in the schema. For each init function, you can choose to include a schema for the parameter, the errors, and/or the events. And for each receive function, you can choose to include a schema for the parameter, the return value, and/or the errors.

Every type we want to include in the schema must implement the SchemaType trait. This is already done for all base types and some other types (see list of types implementing the SchemaType). For most other cases, it can also be derived automatically, using #[derive(SchemaType)]:

#[derive(SchemaType)]
struct SomeType {
    ...
}

Implementing the SchemaType trait manually only requires specifying one function (get_type()), which is a getter for a schema::Type, which essentially describes how this type is represented as bytes and how to represent it as JSON.

For example, the Cis2 library implements the SchemaType trait manually for a few types that can not derive their SchemaType from base types. The example below explores the manual SchemaType trait implementation of the Cis2Event<T, A>:

impl<T: IsTokenId, A: IsTokenAmount> schema::SchemaType for Cis2Event<T, A> {
   fn get_type() -> schema::Type {
      let mut event_map = BTreeMap::new();
      event_map.insert(
         TRANSFER_EVENT_TAG,
         (
            "Transfer".to_string(),
            schema::Fields::Named(vec![
               (String::from("token_id"), T::get_type()),
               (String::from("amount"), A::get_type()),
               (String::from("from"), Address::get_type()),
               (String::from("to"), Address::get_type()),
            ]),
         ),
      );

      ...

      schema::Type::TaggedEnum(event_map)
   }
}

Note

Event schemas can be represented by the TaggedEnum Type (see list of types available to implement a schema). The first byte of the event data logged includes a tag to distinguish the different events on-chain. For example, the TRANSFER_EVENT_TAG is 255 (u8::MAX) which is the key used in the above BTreeMap to store the Transfer event schema. The value of the Transfer event in the above BTreeMap is a tuple consisting of a String (named Transfer) and named Fields (token_id, amount, from, and to). Off-chain tools use this tuple to represent the Transfer event in a more human-readable manner. In detail, the String is used as the key in the JSON representation, and the Fields are used as the values in the JSON representation by off-chain tools.

Including schemas for init#

To generate and include the schema for the parameter, the errors, and/or the events for the init function, set the optional parameter, error, and event attributes for the #[init(..)]-macro:

#[derive(SchemaType)]
enum InitParameter { ... }

#[derive(Serial, Reject, SchemaType)]
enum InitError { ... }

#[derive(SchemaType)]
enum InitEvent { ... }

#[init(contract = "my_contract", parameter = "InitParameter",
error = "InitError", event = "InitEvent")]
fn contract_init<...>(...) -> <..., InitError> { ... }

Note

The event schema attached to the init function is globally available. Any event logged by init or receive functions is represented by off-chain tools using this event schema.

Including schemas for receive#

To generate and include the schema for the parameter, the return value, and/or the errors for receive functions, set the optional parameter, return_value, and error attributes for the #[receive(..)]-macro:

#[derive(SchemaType)]
enum ReceiveParameter { ... }

#[derive(SchemaType)]
enum ReceiveReturnValue { ... }

#[derive(Serial, Reject, SchemaType)]
enum ReceiveError { ... }

#[receive(contract = "my_contract", name = "just_param", parameter = "String")]
fn contract_receive_just_param<...> (...) -> ReceiveResult<String> { ... }

#[receive(contract = "my_contract", name = "just_return", return_value = "Vec<u64>")]
fn contract_receive_just_return<...> (...) -> ReceiveResult<Vec<u64>> { ... }

#[receive(contract = "my_contract", name = "just_error", error = "ReceiveError")]
fn contract_receive_just_error<...> (...) -> Result<Vec<u64>, ReceiveError> { ... }

#[receive(
    contract = "my_contract",
    name = "param_and_return_and_error",
    parameter = "ReceiveParameter",
    return_value = "ReceiveReturnValue",
    error = "ReceiveError"
)]
fn contract_receive<...> (...) -> Result<ReceiveReturnValue, ReceiveError> { ... }

Building the schema#

Now, we are ready to build the actual schema using cargo-concordium, and we have the options to embed the schema and/or write the schema to a file.

See also

For more on which to choose see here.

Embedding the schema#

In order to embed the schema into the smart contract module, we add --schema-embed to the build command

$cargo concordium build --schema-embed

If successful the output of the command will tell you the total size of the schema in bytes.

Outputting a schema file#

To output the schema into a file, we can use the --schema-out=FILE where FILE is a path of the file to create:

$cargo concordium build --schema-out="/some/path/schema.bin"