Contract interfaces and Traits generation

Contract interfaces define the structure and behavior of a contract, serving as the contract's public ABI. They list all the function signatures that a contract exposes. For a detailed explanation of interfaces, you can refer to the Cairo Book.

In Cairo, to specify the interface you need to define a trait annotated with #[starknet::interface] and then implement that trait in the contract.

When a function needs to access the contract state, it must have a self parameter of type ContractState. This implies that the corresponding function signature in the interface trait must also take a TContractState type as a parameter. It's important to note that every function in the contract interface must have this self parameter of type TContractState.

You can use the #[generate_trait] attribute to implicitly generate the trait for a specific implementation block. This attribute automatically generates a trait with the same functions as the ones in the implemented block, replacing the self parameter with a generic TContractState parameter. However, you will need to annotate the block with the #[abi(per_item)] attribute, and each function with the appropriate attribute depending on whether it's an external function, a constructor or an L1 handler.

In summary, there's two ways to handle interfaces:

  • Explicitly, by defining a trait annotated with #[starknet::interface]
  • Implicitly, by using #[generate_trait] combined with the #[abi(per_item)] attributes, and annotating each function inside the implementation block with the appropriate attribute.

Explicit interface

#[starknet::interface]
pub trait IExplicitInterfaceContract<TContractState> {
    fn get_value(self: @TContractState) -> u32;
    fn set_value(ref self: TContractState, value: u32);
}

#[starknet::contract]
pub mod ExplicitInterfaceContract {
    #[storage]
    struct Storage {
        value: u32
    }

    #[abi(embed_v0)]
    impl ExplicitInterfaceContract of super::IExplicitInterfaceContract<ContractState> {
        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }

        fn set_value(ref self: ContractState, value: u32) {
            self.value.write(value);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        IExplicitInterfaceContract, ExplicitInterfaceContract, IExplicitInterfaceContractDispatcher,
        IExplicitInterfaceContractDispatcherTrait
    };
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    #[test]
    fn test_interface() {
        let (contract_address, _) = deploy_syscall(
            ExplicitInterfaceContract::TEST_CLASS_HASH.try_into().unwrap(),
            0,
            array![].span(),
            false
        )
            .unwrap_syscall();
        let mut contract = IExplicitInterfaceContractDispatcher { contract_address };

        let value: u32 = 20;
        contract.set_value(value);

        let read_value = contract.get_value();

        assert_eq!(read_value, value);
    }
}

Implicit interface

#[starknet::contract]
pub mod ImplicitInterfaceContract {
    #[storage]
    struct Storage {
        value: u32
    }

    #[abi(per_item)]
    #[generate_trait]
    pub impl ImplicitInterfaceContract of IImplicitInterfaceContract {
        #[external(v0)]
        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }

        #[external(v0)]
        fn set_value(ref self: ContractState, value: u32) {
            self.value.write(value);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        ImplicitInterfaceContract, ImplicitInterfaceContract::valueContractMemberStateTrait,
        ImplicitInterfaceContract::IImplicitInterfaceContract
    };
    use starknet::{
        ContractAddress, SyscallResultTrait, syscalls::deploy_syscall, testing::set_contract_address
    };

    #[test]
    fn test_interface() {
        let (contract_address, _) = deploy_syscall(
            ImplicitInterfaceContract::TEST_CLASS_HASH.try_into().unwrap(),
            0,
            array![].span(),
            false
        )
            .unwrap_syscall();
        set_contract_address(contract_address);
        let mut state = ImplicitInterfaceContract::contract_state_for_testing();

        let value = 42;
        state.set_value(value);
        let read_value = state.get_value();

        assert_eq!(read_value, value);
    }
}

Note: You can import an implicitly generated contract interface with use contract::{GeneratedContractInterface}. However, the Dispatcher will not be generated automatically.

Internal functions

You can also use #[generate_trait] for your internal functions. Since this trait is generated in the context of the contract, you can define pure functions as well (functions without the self parameter).

#[starknet::interface]
pub trait IImplicitInternalContract<TContractState> {
    fn add(ref self: TContractState, nb: u32);
    fn get_value(self: @TContractState) -> u32;
    fn get_const(self: @TContractState) -> u32;
}

#[starknet::contract]
pub mod ImplicitInternalContract {
    #[storage]
    struct Storage {
        value: u32
    }

    #[generate_trait]
    impl InternalFunctions of InternalFunctionsTrait {
        fn set_value(ref self: ContractState, value: u32) {
            self.value.write(value);
        }

        fn get_const() -> u32 {
            42
        }
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.set_value(0);
    }

    #[abi(embed_v0)]
    impl ImplicitInternalContract of super::IImplicitInternalContract<ContractState> {
        fn add(ref self: ContractState, nb: u32) {
            self.set_value(self.value.read() + nb);
        }

        fn get_value(self: @ContractState) -> u32 {
            self.value.read()
        }

        fn get_const(self: @ContractState) -> u32 {
            self.get_const()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        IImplicitInternalContract, ImplicitInternalContract, IImplicitInternalContractDispatcher,
        IImplicitInternalContractDispatcherTrait
    };
    use starknet::{ContractAddress, SyscallResultTrait, syscalls::deploy_syscall};

    #[test]
    fn test_interface() {
        // Set up.
        let (contract_address, _) = deploy_syscall(
            ImplicitInternalContract::TEST_CLASS_HASH.try_into().unwrap(), 0, array![].span(), false
        )
            .unwrap_syscall();
        let mut contract = IImplicitInternalContractDispatcher { contract_address };

        let initial_value: u32 = 0;
        assert_eq!(contract.get_value(), initial_value);

        let add_value: u32 = 10;
        contract.add(add_value);

        assert_eq!(contract.get_value(), initial_value + add_value);
    }
}
Last change: 2024-06-09, commit: 3fbfb60