Storing Custom Types

While native types can be stored in a contract's storage without any additional work, custom types require a bit more work. This is because at compile time, the compiler does not know how to store custom types in storage. To solve this, we need to implement the Store trait for our custom type. It is enough to just derive this trait, unless our custom type contains arrays or dictionaries.

#[starknet::interface]
pub 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)]
pub struct Person {
    pub age: u8,
    pub name: felt252
}

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

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

    #[abi(embed_v0)]
    impl StoringCustomType of super::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);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{IStoringCustomType, StoringCustomType, Person,};
    use starknet::storage::StoragePointerReadAccess;

    #[test]
    fn can_call_set_person() {
        let mut state = StoringCustomType::contract_state_for_testing();

        let person = Person { age: 10, name: 'Joe' };

        state.set_person(person);

        let read_person = state.person.read();

        assert_eq!(person.age, read_person.age);
        assert_eq!(person.name, read_person.name);
    }

    #[test]
    fn can_call_set_name() {
        let mut state = StoringCustomType::contract_state_for_testing();

        state.set_name('John');

        let read_person = state.person.read();
        assert_eq!(read_person.name, 'John');
    }
}

Note that it is also possible to individually access the members of the stored struct. This is possible because deriving the Store trait also generates the corresponding StoragePointer for each member.

#[starknet::interface]
pub 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)]
pub struct Person {
    pub age: u8,
    pub name: felt252
}

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

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

    #[abi(embed_v0)]
    impl StoringCustomType of super::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);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{IStoringCustomType, StoringCustomType, Person,};
    use starknet::storage::StoragePointerReadAccess;

    #[test]
    fn can_call_set_person() {
        let mut state = StoringCustomType::contract_state_for_testing();

        let person = Person { age: 10, name: 'Joe' };

        state.set_person(person);

        let read_person = state.person.read();

        assert_eq!(person.age, read_person.age);
        assert_eq!(person.name, read_person.name);
    }

    #[test]
    fn can_call_set_name() {
        let mut state = StoringCustomType::contract_state_for_testing();

        state.set_name('John');

        let read_person = state.person.read();
        assert_eq!(read_person.name, 'John');
    }
}
Last change: 2024-09-19, commit: 3b416c6