DEV Community

Cover image for Soroban Quest - Custom Types
Altuğ Bakan
Altuğ Bakan

Posted on • Updated on

Soroban Quest - Custom Types

After successfully completing the complicated Cross Contract quest on last post, we are ready for the Custom Types quest. Beware though, this quest is quite long and challenging, just what we like!

If you haven’t checked out the discussion on custom Soroban types, please take a look at the Using get to Retrieve Data section on the Auth Store tutorial.

Understanding README.md

Let's once again check out the README.md file for this quest.

README

Setting Up Our Quest Account

Use the usual commands, sq login if required, and sq play 5 to start your adventure.

Examining the Contract

Let's start with checking out the lib.rs file.

use types::*;

pub struct TypesContract;

#[contractimpl]
impl TypesContract {
    pub fn c_rect(_env: Env, _rect: Rectangle) {}
    pub fn c_animal(_env: Env, _animal: Animal) {}
    pub fn c_user(_env: Env, _user: User) {}
    pub fn c_rgb(_env: Env, _rgb: RGB) {}
    pub fn c_color(_env: Env, _val: Color) {}
    pub fn c_part(_env: Env, _participant: Participant) {}
    pub fn c_card(_env: Env, _card: RoyalCard) {}
}
Enter fullscreen mode Exit fullscreen mode

It has some empty functions! The arguments of the functions are different types, which are seemingly imported from the types.rs file, as there is the use types::*; import up top.

We won't add any functionality to this file, so let's check out the types.rs file.

Filling Out types.rs

The types.rs file already has two examples: a struct and an enum.

#[contracttype]
pub struct Cylinder {
    pub radius: u32,
    pub height: u32,
}


#[contracttype]
pub enum Car {
    Chevrolet,
    Ford,
    Mercedes,
    Porsche,
    Honda,
    Toyota
}
Enter fullscreen mode Exit fullscreen mode

Using Custom Types

The structs are used for creating objects that have multiple different values. These values can consist of basic types like u32 and Bytes, another structs, enums, arrays, and so on. The structs allow us to carry all properties of an object together on a single variable. Well, it is useful to pass a Car struct to a insert_to_garage function instead of tires, engine, color, fuel_type, license_plate, total_kilometers...

The enums are used for creating numeric types with names for a limited list. It is mostly useful for programmers, as trying to remember the ID of a student in a classroom might not be helpful, but you would know who Bob is. The compiler is not interested in those names, and will happily use their numeric values in your code.

We have a lot of empty structs and enums, waiting for us to implement them. Before starting out, please check out the Custom Types section from the Soroban Docs to learn more about the type system of Soroban.

Implementing Rectangle

Let's start with implementing the Rectangle struct.

#[contracttype]
pub struct Rectangle {
    // TODO: create your fields here for your `Rectangle` type
}
Enter fullscreen mode Exit fullscreen mode

From README.md, we can see that

The Rectangle type must be a struct, with two fields: width and height which both must be a u32 value.

So, we can implement Rectangle as follows:

pub struct Rectangle {
    pub width: u32,
    pub height: u32,
}
Enter fullscreen mode Exit fullscreen mode

Implementing Animal

Next, let's check out the Animal enum. From README.md, we can see that

The Animal type must be an enum, with at least two variations: Cat and Dog.

So, we can implement Animal as:

pub enum Animal {
    Dog,
    Cat,
    Bear,
    Wolf,
    Gorilla /* and many more */
}
Enter fullscreen mode Exit fullscreen mode

Implementing User

Let's move on to the User struct. Again, from README.md, we see that

The User type must be a struct with name, age, and pet fields, corresponding to Bytes, u32, and Animal values, respectively.

Let's implement the User type. It consists of another struct, but that's nothing hard to implement.

pub struct User {
    pub name: Bytes,
    pub age: u32,
    pub pet: Animal,
}
Enter fullscreen mode Exit fullscreen mode

Implementing RGB

We are almost halfway done! Let's now implement the RGB struct.

The RGB type must be a tuple struct type made with a tuple of 3 u32 values.

Since it should be defined using unnamed u32 types, we can define it as a tuple using the following definition.

#[contracttype]
pub struct RGB(pub u32, pub u32, pub u32);
Enter fullscreen mode Exit fullscreen mode

Implementing Color

More than halfway done! Now, we must implement the Color enum, using the following definiton.

The Color type will combine the RGB custom type nested within a tuple enum type. Construct your RGB struct type as described bove, Then, your Color enum type must be defined as a variant with a name of "RGB" and an instance of your RGB type.

An enums type definitions can be of any type, including custom structs.

pub enum Color {
    RGB(RGB)
}
Enter fullscreen mode Exit fullscreen mode

Implementing Participant

Almost done! We now need to implement the Participant enum type. The definition is

The Participant type must be an enum with single-value tuple variants as follows:

  • An "Account" variant with an AccountId type
  • A "Contract" variant with a BytesN<32> type

So, Participant should be composed from two different Soroban types: AccountId, and BytesN<32>. Since we know how to implement custom types on enums, let's go ahead and define the Participant enum.

pub enum Participant {
    Contract(BytesN<32>),
    Account(AccountId),
}
Enter fullscreen mode Exit fullscreen mode

Implementing RoyalCard

Only one left! We now just need to implement the RoyalCard enum.

The RoyalCard type must be an enum containing three u32 integer variations as follows:

  • A "Jack" variant, with a value of 11
  • A "Queen" variant, with a value of 12
  • A "King" variant, with a value of 13

It is quite simple to define an enum with given values, so we can implement it as follows.

#[repr(u32)]
pub enum RoyalCard {
    Jack = 11,
    Queen = 12,
    King = 13
}
Enter fullscreen mode Exit fullscreen mode

The #[repr(u32)] attribute tells the compiler to pack each enum element into an 32 bit memory slot, which is the size of an u32 variable.

We have accomplished quite much! But the quest isn't over, since we still have to invoke the "empty" functions successfully using soroban CLI. To learn more about how we can invoke those functions, let's check out the test.rs file.

Examining the Test

The test has only one function, test_types, which tests the types of all of the custom structs and enums we have created by constructing the corresponding type, and calling the corresponding function. Let's check if our implementations of the custom types are correct using

cargo test -p soroban-custom-types-contract
Enter fullscreen mode Exit fullscreen mode

Custom Types Test

Success!

Solving the Quest

Now, we can try to solve the quest by creating the arguments to invoke each function on the implementation of our Soroban contract.

Deploying the Contract

Before we start invoking, let's deploy our contract using

cargo build --target wasm32-unknown-unknown --release
soroban deploy --wasm target/wasm32-unknown-unknown/release/soroban_custom_types_contract.wasm
Enter fullscreen mode Exit fullscreen mode

Contract Deployment

Invoking c_rect

First, we have to call the c_rect function, which has the argument _rect of type Rectangle. If we check out the README.md file, we see that we can

Invoke the c_rect function to create a Rectangle using something like:

soroban invoke \
    --id <contract-id> \
    --fn c_rect \
    --arg '{"object":{"map":[{"key":{"symbol":"width"},"val":{"u32":<a-u32-integer>}},{"key":{"symbol":"height"},"val":{"u32":<a-u32-integer>}}]}}'

We can see that we should change the values for the sections marked with <a-u32-integer> to an actual u32 integer, and <contract-id> to our contract ID. You can choose any height or width for your rectangle. After you are happy with your choices, invoke the contract.

soroban invoke \
    --id <contract-id> \
    --fn c_rect \
    --arg '{"object":{"map":[{"key":{"symbol":"width"},"val":{"u32":<a-u32-integer>}},{"key":{"symbol":"height"},"val":{"u32":<a-u32-integer>}}]}}'
Enter fullscreen mode Exit fullscreen mode

Invoke c_rect

Invoking c_animal

After successfully invoking the c_rect function, we now will move on to the c_animal function. From README.md, we can

Invoke the c_animal function to create an Animal using something like:

soroban invoke \
    --id <contract-id> \
    --fn c_animal \
    --arg '{"object":{"vec":[{"symbol":"<a-relevant-animal-symbol>"}]}}'

Again, we should change the <contract-id> to our contract ID, and <a-relevant-animal-symbol> to an animal enum we have created. Since the Animal struct is a custom type we have created, we should create the corresponding object as a JSON.

Invoke c_animal

Invoking c_user

Now, it's c_user function's turn. Again, checking out README.md, we see that we could

Invoke the c_user function to create a User using something like:

soroban invoke \
    --id <contract-id> \
    --fn c_user \
    --arg '{"object":{"map":[{"key":{"symbol":"name"},"val":{"object":{"bytes":"<a-hex-encoded-string>"}}},{"key":{"symbol":"age"},"val":{"u32":<a-u32-integer>}},{"key":> {"symbol":"pet"},"val":<an-animal-object>}]}}'

That's quite the prompt! We should find the hexadecimal encoded string of a name, change the <a-u32-integer> to an age, and <an-animal-object> to the JSON representation of an animal object.

Remember that we could use the same Dog object that we used for the c_animal invocation, but feel free to use any animal you'd like.

Invoke c_user

Invoking c_rgb

That one was the hardest! Now, we can invoke the c_rgb function. Again, if we check out README.md, we will see that we can

Invoke the c_rgb function to create a RGB value using something like:

soroban invoke \
    --id <contract-id> \
    --fn c_rgb \
    --arg '{"object":{"vec":[{"u32":<a-u32-integer>},{"u32":<a-u32-integer>},{"u32":<a-u32-integer>}]}}'

We again have to change the <a-u32-integer> values to any integer values that we'd like.

Invoke c_rgb

Invoking c_color

Keep going! Now, we can try invoking the c_color function. The README.md file states that we should

Invoke the c_color function to create a Color using something like:

soroban invoke \
    --id <contract-id> \
    --fn c_color \
    --arg '{"object":{"vec":[{"symbol":"RGB"},<a-rgb-object>]}}'

where <a-rgb-object> should be a JSON object representation of the RGB struct. Try to construct the object without looking below!

Invoke c_color

Invoking c_part

You are close! Now it's time to call the c_part function. Let's check out the README.md file once more.

Invoke the c_part function to create an account Participant using something like:

soroban invoke \
    --id <contract-id> \
    --fn c_part \
    --arg '{"object":{"vec":[{"symbol":"Account"},{"object":{"accountId":{"publicKeyTypeEd25519":"<hex-encoded-account-id>"}}}]}}'

Also invoke the c_part function to create a contract Participant using something like:

soroban invoke \
    --id <contract-id> \
    --fn c_part \
    --arg '{"object":{"vec":[{"symbol":"Contract"},{"object":{"bytes":"<contract-id>"}}]}}'

Do you remember how to obtain your hexadecimal encoded public key? (Hint: using Python!). Try to find out what to put into <hex-encoded-account-id>. The <contract-id> parameter is pretty simple, as we have been using that one for all function invocations that we have done.

Invoke c_part

Invoking c_card

Last one! You should be pretty tired at this point, but bear with me. Luckily, the last invocation is quite simple, as we only have to

Invoke the c_card function using something like:

soroban invoke \
    --id <contract-id> \
    --fn c_foo \
    --arg '{"u32":<a-u32-integer>}'

Remember that the u32 argument should be one of 11, 12, or 13, as we only defined those values. And c_foo should be c_card, if that wasn't clear.

Invoke c_card

verifying Our Implementation

That was a lot of invocations! Finally, we must invoke the verify function of the verification contract 40d12b03a08f5dde4e0068aa752fa65eddf905e82a18f522efe350e0cd268b8a to make the verification contract verify the correctness of our deployed contract. Can you guess which argument we should supply? If you guessed the contract ID of the contract we have deployed, you are right! We can use

soroban invoke \
--id 40d12b03a08f5dde4e0068aa752fa65eddf905e82a18f522efe350e0cd268b8a \
--fn verify \
--arg [your-contract-id]
Enter fullscreen mode Exit fullscreen mode

We should get a reply indicating that all of our custom implementations have succeeded, and none of them have failed.

Invoke Verify

Nicely done! You have definitely earned your reward! Use sq check 5 to claim your reward 🎉!

Top comments (1)

Collapse
 
altug profile image
Altuğ Bakan • Edited

Hope you enjoyed this post!

If you want to support my work, you can send me some XLM at

GB2M73QXDA7NTXEAQ4T2EGBE7IYMZVUX4GZEOFQKRWJEMC2AVHRYL55V
Stellar Wallet

Thanks for all your support!