Component Dependencies

Working with components becomes more complex when we try to use one component inside another. As mentioned earlier, a component can only be embedded within a contract, meaning that it's not possible to embed a component within another component. However, this doesn't mean that we can't use one component inside another. In this section, we will see how to use a component as a dependency of another component.

Consider a component called OwnableCounter whose purpose is to create a counter that can only be incremented by its owner. This component can be embedded in any contract, so that any contract that uses it will have a counter that can only be incremented by its owner.

The first way to implement this is to create a single component that contains both counter and ownership features from within a single component. However, this approach is not recommended: our goal is to minimize the amount of code duplication and take advantage of component reusability. Instead, we can create a new component that depends on the Ownable component for the ownership features, and internally defines the logic for the counter.

Listing 16-1 shows the complete implementation, which we'll break down right after:

#![allow(unused)]
fn main() {
use starknet::ContractAddress;

#[starknet::interface]
trait IOwnableCounter<TContractState> {
    fn get_counter(self: @TContractState) -> u32;
    fn increment(ref self: TContractState);
    fn transfer_ownership(ref self: TContractState, new_owner: ContractAddress);
}

#[starknet::component]
mod OwnableCounterComponent {
    use listing_03_component_dep::owner::{ownable_component, ownable_component::InternalImpl};
    use starknet::ContractAddress;

    #[storage]
    struct Storage {
        value: u32
    }

    #[embeddable_as(OwnableCounterImpl)]
    impl OwnableCounter<
        TContractState,
        +HasComponent<TContractState>,
        +Drop<TContractState>,
        impl Owner: ownable_component::HasComponent<TContractState>
    > of super::IOwnableCounter<ComponentState<TContractState>> {
        fn get_counter(self: @ComponentState<TContractState>) -> u32 {
            self.value.read()
        }

        fn increment(ref self: ComponentState<TContractState>) {
            let ownable_comp = get_dep_component!(@self, Owner);
            ownable_comp.assert_only_owner();
            self.value.write(self.value.read() + 1);
        }

        fn transfer_ownership(
            ref self: ComponentState<TContractState>, new_owner: ContractAddress
        ) {
            let mut ownable_comp = get_dep_component_mut!(ref self, Owner);
            ownable_comp._transfer_ownership(new_owner);
        }
    }
}
}

Listing 16-1: An OwnableCounter Component.

Specificities

Specifying Dependencies on Another Component

#![allow(unused)]
fn main() {
    impl OwnableCounter<
        TContractState,
        +HasComponent<TContractState>,
        +Drop<TContractState>,
        impl Owner: ownable_component::HasComponent<TContractState>
    > of super::IOwnableCounter<ComponentState<TContractState>> {
}

In Chapter 8, we introduced trait bounds, which are used to specify that a generic type must implement a certain trait. In the same way, we can specify that a component depends on another component by restricting the impl block to be available only for contracts that contain the required component. In our case, this is done by adding a restriction impl Owner: ownable_component::HasComponent<TContractState>, which indicates that this impl block is only available for contracts that contain an implementation of the ownable_component::HasComponent trait. This essentially means that the `TContractState' type has access to the ownable component (See Components under the hood for more information).

Although most of the trait bounds were defined using anonymous parameters, the dependency on the Ownable component is defined using a named parameter (here, Owner). We will need to use this explicit name when accessing the Ownablecomponent within theimpl block.

While this mechanism is verbose and may not be easy to approach at first, it is a powerful leverage of the trait system in Cairo. The inner workings of this mechanism are abstracted away from the user, and all you need to know is that when you embed a component in a contract, all other components in the same contract can access it.

Using the Dependency

Now that we have made our impl depend on the Ownable component, we can access its functions, storage, and events within the implementation block. To bring the Ownable component into scope, we have two choices, depending on whether we intend to mutate the state of the Ownable component or not. If we want to access the state of the Ownable component without mutating it, we use the get_dep_component! macro. If we want to mutate the state of the Ownable component (for example, change the current owner), we use the get_dep_component_mut! macro. Both macros take two arguments: the first is self, either as a snapshot or by reference depending on mutability, representing the state of the component using the dependency, and the second is the component to access.

#![allow(unused)]
fn main() {
        fn increment(ref self: ComponentState<TContractState>) {
            let ownable_comp = get_dep_component!(@self, Owner);
            ownable_comp.assert_only_owner();
            self.value.write(self.value.read() + 1);
        }
}

In this function, we want to make sure that only the owner can call the increment function. We need to use the assert_only_owner function from the Ownable component. We'll use the get_dep_component! macro which will return a snapshot of the requested component state, and call assert_only_owner on it, as a method of that component.

For the transfer_ownership function, we want to mutate that state to change the current owner. We need to use the get_dep_component_mut! macro, which will return the requested component state as a mutable reference, and call transfer_ownership on it.

#![allow(unused)]
fn main() {
        fn transfer_ownership(
            ref self: ComponentState<TContractState>, new_owner: ContractAddress
        ) {
            let mut ownable_comp = get_dep_component_mut!(ref self, Owner);
            ownable_comp._transfer_ownership(new_owner);
        }
}

It works exactly the same as get_dep_component! except that we need to pass the state as a ref so we can mutate it to transfer the ownership.