Custom Types

Storing Custom Types

In Starknet contracts, storing custom types in contract storage requires implementing the Store trait. While native types (like felt252, u128, etc.) can be stored directly, custom types need this additional step to generate the necessary implementation on how to handle their storage.

To make a custom type storable:

  1. Derive the starknet::Store trait for your struct

  2. Add any other necessary traits like Drop, Serde, and Copy

  3. Define your storage variables using the custom type

Here’s an example showing how to store a custom Person struct:

#[starknet::interface]
trait IStoringCustomType<TContractState> {
    fn set_person(ref self: TContractState, person: Person);
    fn set_name(ref self: TContractState, name: felt252);
}

// Deriving the starknet::Store trait
// allows us to store the `Person` struct in the contract's storage.
#[derive(Drop, Serde, Copy, starknet::Store)]
struct Person {
    age: u8,
    name: felt252,
}

#[starknet::contract]
mod StoringCustomType {
    use starknet::storage::StoragePointerWriteAccess;
    use super::Person;
    use super::IStoringCustomType;

    #[storage]
    struct Storage {
        person: Person,
    }

    #[abi(embed_v0)]
    impl StoringCustomType of IStoringCustomType<ContractState> {
        fn set_person(ref self: ContractState, person: Person) {
            self.person.write(person);
        }

        fn set_name(ref self: ContractState, name: felt252) {
            self.person.name.write(name);
        }
    }
}

For more complex types, you might need to implement the Store trait manually instead of deriving it.

Accessing Struct Members

When you derive the Store trait, Cairo automatically generates the necessary storage pointers for each struct member. This allows you to access and modify individual fields of your stored struct directly:

fn set_name(ref self: ContractState, name: felt252) {
    self.person.name.write(name);
}

Custom Types in Entrypoints

When using custom types in Starknet contract entrypoints, you need to handle serialization and deserialization of data. This is because:

  1. Input parameters are sent to entrypoints as arrays of felt252

  2. Return values must be converted back to arrays of felt252

  3. Custom types need to be converted between these formats automatically

Using the Serde Trait

The Serde trait provides the necessary serialization and deserialization capabilities for your custom types. For most simple types, you can derive this trait automatically:

#[starknet::interface]
trait ISerdeCustomType<TContractState> {
    fn person_input(ref self: TContractState, person: Person);
    fn person_output(self: @TContractState) -> Person;
}

// Deriving the `Serde` trait allows us to use
// the `Person` type as an entrypoint parameter and as a return value
#[derive(Drop, Serde)]
struct Person {
    age: u8,
    name: felt252,
}

#[starknet::contract]
mod SerdeCustomType {
    use super::Person;
    use super::ISerdeCustomType;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl SerdeCustomType of ISerdeCustomType<ContractState> {
        fn person_input(ref self: ContractState, person: Person) {}

        fn person_output(self: @ContractState) -> Person {
            Person { age: 10, name: 'Joe' }
        }
    }
}

For some complex types, you might need to implement the Serde trait manually. This gives you control over how your type is serialized and deserialized.

The Serde trait is distinct from the Store trait - Serde is for passing data in and out of entrypoints, while Store is for persisting data in contract storage.