Component Dependencies

A component with a dependency on a trait T can be used in a contract as long as the contract implements the trait T.

We will use a new Countable component as an example:

#[starknet::interface]
pub trait ICountable<TContractState> {
    fn get(self: @TContractState) -> u32;
    fn increment(ref self: TContractState);
}

#[starknet::component]
pub mod countable_component {
    #[storage]
    struct Storage {
        countable_value: u32,
    }

    #[embeddable_as(Countable)]
    impl CountableImpl<
        TContractState, +HasComponent<TContractState>
    > of super::ICountable<ComponentState<TContractState>> {
        fn get(self: @ComponentState<TContractState>) -> u32 {
            self.countable_value.read()
        }

        fn increment(ref self: ComponentState<TContractState>) {
            self.countable_value.write(self.countable_value.read() + 1);
        }
    }
}

We want to add a way to enable or disable the counter, in a way that calling increment on a disabled counter will not increment it. But we don't want to add this switch logic to the Countable component itself. Instead, we add the trait Switchable as a dependency to the Countable component.

Implementation of the trait in the contract

First, we import the ISwitchable trait defined in chapter "Components How-To":

pub trait ISwitchable<TContractState> {
    fn is_on(self: @TContractState) -> bool;
    fn switch(ref self: TContractState);
}

Then we can modify the implementation of the Countable component to depend on the ISwitchable trait:

#[starknet::component]
pub mod countable_component {
    use components::countable::ICountable;
    use components::switchable::ISwitchable;

    #[storage]
    struct Storage {
        countable_value: u32,
    }

    #[embeddable_as(Countable)]
    impl CountableImpl<
        TContractState, +HasComponent<TContractState>, +ISwitchable<TContractState>
    > of ICountable<ComponentState<TContractState>> {
        fn get(self: @ComponentState<TContractState>) -> u32 {
            self.countable_value.read()
        }

        fn increment(ref self: ComponentState<TContractState>) {
            if (self.get_contract().is_on()) {
                self.countable_value.write(self.countable_value.read() + 1);
            }
        }
    }
}


#[starknet::contract]
mod MockContract {
    use super::countable_component;
    use components::switchable::ISwitchable;

    component!(path: countable_component, storage: counter, event: CountableEvent);

    #[storage]
    struct Storage {
        #[substorage(v0)]
        counter: countable_component::Storage,
        switch: bool,
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        CountableEvent: countable_component::Event,
    }

    #[abi(embed_v0)]
    impl CountableImpl = countable_component::Countable<ContractState>;
    #[abi(embed_v0)]
    impl Switchable of ISwitchable<ContractState> {
        fn switch(ref self: ContractState) {}

        fn is_on(self: @ContractState) -> bool {
            true
        }
    }
}


#[cfg(test)]
mod test {
    use super::MockContract;
    use components::countable::{ICountableDispatcher, ICountableDispatcherTrait};
    use starknet::syscalls::deploy_syscall;
    use starknet::SyscallResultTrait;

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

    #[test]
    fn test_get() {
        let countable = deploy_countable();
        assert_eq!(countable.get(), 0);
    }

    #[test]
    fn test_increment() {
        let countable = deploy_countable();
        countable.increment();
        assert_eq!(countable.get(), 1);
    }
}

A contract that uses the Countable component must implement the ISwitchable trait:

#[starknet::contract]
mod CountableContract {
    use components_dependencies::countable_dep_switch::countable_component;
    use components::switchable::ISwitchable;

    component!(path: countable_component, storage: counter, event: CountableEvent);

    #[abi(embed_v0)]
    impl CountableImpl = countable_component::Countable<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        counter: countable_component::Storage,
        switch: bool
    }

    // Implementation of the dependency:
    #[abi(embed_v0)]
    impl Switchable of ISwitchable<ContractState> {
        fn switch(ref self: ContractState) {
            self.switch.write(!self.switch.read());
        }

        fn is_on(self: @ContractState) -> bool {
            self.switch.read()
        }
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.switch.write(false);
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        CountableEvent: countable_component::Event,
    }
}

#[cfg(test)]
mod tests {
    use super::CountableContract;
    use components::countable::{ICountable, ICountableDispatcher, ICountableDispatcherTrait};
    use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};

    use starknet::storage::StorageMemberAccessTrait;
    use starknet::SyscallResultTrait;
    use starknet::syscalls::deploy_syscall;

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

        (ICountableDispatcher { contract_address }, ISwitchableDispatcher { contract_address },)
    }

    #[test]
    #[available_gas(2000000)]
    fn test_init() {
        let (mut counter, mut switch) = deploy();

        assert(counter.get() == 0, 'Counter != 0');
        assert(switch.is_on() == false, 'Switch != false');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_switch_off() {
        let (mut counter, mut switch) = deploy();

        counter.increment();
        assert(counter.get() == 0, 'Counter incremented');
        assert(switch.is_on() == false, 'Switch != false');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_switch_on() {
        let (mut counter, mut switch) = deploy();

        switch.switch();
        assert(switch.is_on() == true, 'Switch != true');

        counter.increment();
        assert(counter.get() == 1, 'Counter did not increment');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_multiple_switches() {
        let (mut counter, mut switch) = deploy();

        switch.switch();

        counter.increment();
        counter.increment();
        counter.increment();
        assert(counter.get() == 3, 'Counter did not increment');

        switch.switch();
        counter.increment();
        counter.increment();
        counter.increment();

        switch.switch();

        counter.increment();
        counter.increment();
        counter.increment();
        assert(counter.get() == 6, 'Counter did not increment');
    }
}

Implementation of the trait in another component

In the previous example, we implemented the ISwitchable trait in the contract.

We already implemented a Switchable component that provides an implementation of the ISwitchable trait. By using the Switchable component in a contract, we can embed the implementation of the ISwitchable trait in the contract and resolve the dependency on the ISwitchable trait.

#[starknet::contract]
mod CountableContract {
    use components_dependencies::countable_dep_switch::countable_component;
    use components::switchable::switchable_component;

    component!(path: countable_component, storage: counter, event: CountableEvent);
    component!(path: switchable_component, storage: switch, event: SwitchableEvent);

    #[abi(embed_v0)]
    impl CountableImpl = countable_component::Countable<ContractState>;
    #[abi(embed_v0)]
    impl SwitchableImpl = switchable_component::Switchable<ContractState>;
    impl SwitchableInternalImpl = switchable_component::SwitchableInternalImpl<ContractState>;

    #[storage]
    struct Storage {
        #[substorage(v0)]
        counter: countable_component::Storage,
        #[substorage(v0)]
        switch: switchable_component::Storage
    }

    #[constructor]
    fn constructor(ref self: ContractState) {
        self.switch._off();
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        CountableEvent: countable_component::Event,
        SwitchableEvent: switchable_component::Event,
    }
}

#[cfg(test)]
mod tests {
    use super::CountableContract;
    use components::countable::{ICountable, ICountableDispatcher, ICountableDispatcherTrait};
    use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};

    use starknet::storage::StorageMemberAccessTrait;
    use starknet::SyscallResultTrait;
    use starknet::syscalls::deploy_syscall;

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

        (ICountableDispatcher { contract_address }, ISwitchableDispatcher { contract_address },)
    }

    #[test]
    #[available_gas(2000000)]
    fn test_init() {
        let (mut counter, mut switch) = deploy();

        assert(counter.get() == 0, 'Counter != 0');
        assert(switch.is_on() == false, 'Switch != false');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_switch_off() {
        let (mut counter, mut switch) = deploy();

        counter.increment();
        assert(counter.get() == 0, 'Counter incremented');
        assert(switch.is_on() == false, 'Switch != false');
    }

    #[test]
    #[available_gas(2000000)]
    fn test_increment_switch_on() {
        let (mut counter, mut switch) = deploy();

        switch.switch();
        assert(switch.is_on() == true, 'Switch != true');

        counter.increment();
        assert(counter.get() == 1, 'Counter did not increment');
    }

    #[test]
    #[available_gas(3000000)]
    fn test_increment_multiple_switches() {
        let (mut counter, mut switch) = deploy();

        switch.switch();

        counter.increment();
        counter.increment();
        counter.increment();
        assert(counter.get() == 3, 'Counter did not increment');

        switch.switch();
        counter.increment();
        counter.increment();
        counter.increment();

        switch.switch();

        counter.increment();
        counter.increment();
        counter.increment();
        assert(counter.get() == 6, 'Counter did not increment');
    }
}

Dependency on other component's internal functions

The previous example shows how to use the ISwitchable trait implementation from the Switchable component inside the Countable component by embedding the implementation in the contract. However, suppose we would like to turn off the switch after each increment. There's no set function in the ISwitchable trait, so we can't do it directly.

But the Switchable component implements the internal function _off from the SwitchableInternalTrait that set the switch to false. We can't embed SwitchableInternalImpl, but we can add switchable::HasComponent<TContractState> as a dependency inside CountableImpl.

We make the Countable component depend on the Switchable component. This will allow to do switchable::ComponentState<TContractState> -> TContractState -> countable::ComponentState<TcontractState> and access the internal functions of the Switchable component inside the Countable component:

#[starknet::component]
pub mod countable_component {
    use components::countable::ICountable;
    use components::switchable::ISwitchable;

    // Explicitly depends on a component and not a trait
    use components::switchable::switchable_component;
    use switchable_component::{SwitchableInternalImpl, SwitchableInternalTrait};

    #[storage]
    struct Storage {
        countable_value: u32,
    }

    #[generate_trait]
    impl GetSwitchable<
        TContractState,
        +HasComponent<TContractState>,
        +switchable_component::HasComponent<TContractState>,
        +Drop<TContractState>
    > of GetSwitchableTrait<TContractState> {
        fn get_switchable(
            self: @ComponentState<TContractState>
        ) -> @switchable_component::ComponentState<TContractState> {
            let contract = self.get_contract();
            switchable_component::HasComponent::<TContractState>::get_component(contract)
        }

        fn get_switchable_mut(
            ref self: ComponentState<TContractState>
        ) -> switchable_component::ComponentState<TContractState> {
            let mut contract = self.get_contract_mut();
            switchable_component::HasComponent::<TContractState>::get_component_mut(ref contract)
        }
    }

    #[embeddable_as(Countable)]
    impl CountableImpl<
        TContractState,
        +HasComponent<TContractState>,
        +ISwitchable<TContractState>,
        +switchable_component::HasComponent<TContractState>,
        +Drop<TContractState>
    > of ICountable<ComponentState<TContractState>> {
        fn get(self: @ComponentState<TContractState>) -> u32 {
            self.countable_value.read()
        }

        fn increment(ref self: ComponentState<TContractState>) {
            if (self.get_contract().is_on()) {
                self.countable_value.write(self.countable_value.read() + 1);

                // use the switchable component internal function
                let mut switch = self.get_switchable_mut();
                switch._off();
            }
        }
    }
}

The CountableContract contract remains the same as in the previous example, only the implementation of the Countable component is different.

Last change: 2024-06-05, commit: c0ab62f