Custom types in entrypoints

Using custom types in entrypoints requires our type to implement the Serde trait. This is because when calling an entrypoint, the input is sent as an array of felt252 to the entrypoint, and we need to be able to deserialize it into our custom type. Similarly, when returning a custom type from an entrypoint, we need to be able to serialize it into an array of felt252. Thankfully, we can just derive the Serde trait for our custom type.

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

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

    #[storage]
    struct Storage {}

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

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

#[cfg(test)]
mod tests {
    use super::{
        SerdeCustomType, Person, ISerdeCustomTypeDispatcher, ISerdeCustomTypeDispatcherTrait
    };
    use starknet::{ContractAddress, syscalls::deploy_syscall, SyscallResultTrait};

    fn deploy() -> ISerdeCustomTypeDispatcher {
        let (contract_address, _) = deploy_syscall(
            SerdeCustomType::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        ISerdeCustomTypeDispatcher { contract_address }
    }

    #[test]
    fn should_deploy() {
        deploy();
    }

    #[test]
    fn should_get_person_output() {
        let contract = deploy();
        let expected_person = Person { age: 10, name: 'Joe' };
        let received_person = contract.person_output();
        let age_received = received_person.age;
        let name_received = received_person.name;

        assert(age_received == expected_person.age, 'Wrong age value');
        assert(name_received == expected_person.name, 'Wrong name value');
    }

    #[test]
    #[available_gas(2000000000)]
    fn should_call_person_input() {
        let contract = deploy();
        let expected_person = Person { age: 10, name: 'Joe' };
        contract.person_input(expected_person);
    }
}

Note: The purpose of this example is to demonstrate the ability to use custom types as inputs and outputs in contract calls. For simplicity, we are not using getters and setters to manage the contract's state.

Last change: 2024-06-09, commit: 3fbfb60