Working with Soroban SDK Types

Learn about how you can use the very Soroban SDK inside Zephyr

This section is dedicated to showcasing the amazing features in Zephyr that are enabled by having hardwired Soroban within the Zephyr Virtual Machine.

As a matter of fact, you can use the same Soroban SDK you use to write your smart contracts to write Zephyr indexing programs!

This not only brings innovation, powerful helpers, and potential use cases to the world of indexing but also makes the experience of working with the chain's contract data types in a way no other chain has seen before!

This part of the documentation is just getting started, more features and explainers coming soon!

Working with ScVals

One of the most painful things about working with on-chain smart contract data in general is their complexity. When we deal with this data in the contract's environment everything is neatly defined according to the language's structures. However, once this data is translated into the chain's data format for consensus these become complex nested objects which can be difficult and time-consuming to navigate, making maintainability even more difficult.

In Zephyr, this is not a concern, because you're "kind of" running on a contract environment! Your programs are still executed by the ZephyrVM, tailored for working with Mercury, not by the Soroban VM. However, we have manipulated the Soroban host environment to link Soroban's host functions into Zephyr, and abstract the WASMI Soroban host implementation to enable Soroban to actually execute within Zephyr.

As a result, something that should look like this:

#[no_mangle]
pub extern "C" fn on_close() {
    let env = EnvClient::new();

    let events = env.reader().soroban_events();
    for event in events {
        let ContractEventBody::V0(event) = event.body;

        let action = match &event.topics[0] {
            ScVal::Symbol(symbol) => {
                symbol.0.to_string()
            },
            _ => return
        };

        if &action == "deposit" {
            // ...
        }
    }
}

Can be replaced with:

#[no_mangle]
pub extern "C" fn on_close() {
    let env = EnvClient::new();

    let events = env.reader().soroban_events();
    for event in events {
        let ContractEventBody::V0(event) = event.body;

        if env.from_scval::<Symbol>(&event.topics[0]) == Symbol::new(env.soroban(), "deposit") {
            // ...
        }
    }
}

What's happening here is that we've been able to reduce the work of comparing the first topic of the event with a specific string deposit using Soroban SDK's functions (env.from_scval() and Symbol::new()). The topic and the string are both converted to Symbol and therefore compared.

This is already great when working with very simple types such as symbols, but wait until you see what you can do with more complex data types.

Shared Contract <> Indexer contracttypes?

Let's assume that in your contract, you emit an event with the following structure in its data:

#[contracttype]
pub enum SomeAction {
    Perform(Symbol),
    NotPerform,
    PartiallyPerform
}

#[contracttype]
pub struct MyCustomData {
    action: SomeAction,
    data: i128
}

This is more complex in structure compared to a Symbol ScVal.

Generally, what you should do if you want to for example aggregate the data integer depending on the action would be to parse the ScVal object tree you retrieve from the event. This means that you'd have to parse something that looks like this:

{
  "map": [
    { "key": { "symbol": "action" }, "val": { "vec": [{ "vec": [{ "symbol": "perform" }, { "symbol": "somesym" }] }] } },
    { "key": { "symbol": "data" }, "val": { "i128": { "i128parts": {hi, lo} } } }
  ]
}

This is not nice, and it's why when writing Soroban contracts you don't have to parse this.

Guess what, you don't need to do so even in Zephyr!

You could either import the types from your contract crate in the indexer or define them in the indexer the same way you'd do it on your contract and then simply:

let constructed: MyCustomData = env.from_scval(&event.data);

match constructed.action {
    SomeAction::Perform(_) => (), // do something
    _ => () // do something else
}

As we have seen before, defining MyCustomData in the indexer allows for accessing directly the struct in the program: here we are retrieving its action field for example. A combination of Soroban SDK functions and using contract custom types makes the magic, allowing you to write your indexer as you write smart contracts.

Using SDK helpers

In Zephyr, you can also use SDK functions like address calculation with the deployer and crypto operations. Not as exciting as working with ScVals or invoking contracts, but can still come in handy.

Invoking contracts

You can now invoke a contract directly from within a Zephyr program! This is the final milestone for the Soroban integration in Zephyr. Invoking contracts within Zephyr allows for endless innovation in how we conceive off-chain blockchain data services.

You could be running contract functions for each user for every checkpoint to monitor that everything is running as expected (for example, make sure that all users can withdraw all their funds to detect rounding errors), make sure that cargo tests are executing correctly with on-chain data and be alerted if otherwise, or your bot could simulate or "fuzz" the outcomes of a transaction.

Last updated