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, theDispatcher
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);
}
}