Skip to content

Commit

Permalink
Feat: Component storage collision example + Switchable component re…
Browse files Browse the repository at this point in the history
…factor (#153)

* feat: Components How-To with simple `Switch` Component

* chore: remove `ch0*` prefix for improved modularity

* Refactor switch component to switchable component

* feat: Component storage collisions
  • Loading branch information
julio4 authored Dec 13, 2023
1 parent ae353e6 commit d47b342
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 29 deletions.
4 changes: 4 additions & 0 deletions listings/applications/components/src/contracts.cairo
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
mod switch;
mod switch_collision;

#[cfg(test)]
mod tests;
23 changes: 11 additions & 12 deletions listings/applications/components/src/contracts/switch.cairo
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
// ANCHOR: contract
#[starknet::contract]
mod SwitchContract {
use components::switch::switch_component;
// This is needed to be able to use internal functions of the switch component.
use components::switch::switch_component::InternalSwitchImpl;
use components::switchable::switchable_component;

component!(path: switch_component, storage: switch, event: SwitchEvent);
component!(path: switchable_component, storage: switch, event: SwitchableEvent);

#[abi(embed_v0)]
impl SwitchImpl = switch_component::Switch<ContractState>;
impl SwitchInternalImpl = switch_component::InternalSwitchImpl<ContractState>;
impl SwitchableImpl = switchable_component::Switchable<ContractState>;
impl SwitchableInternalImpl = switchable_component::InternalImpl<ContractState>;

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

#[constructor]
Expand All @@ -25,15 +23,16 @@ mod SwitchContract {
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
SwitchEvent: switch_component::Event
SwitchableEvent: switchable_component::Event,
}
}
// ANCHOR_END: contract

#[cfg(test)]
mod tests {
use components::switch::switch_component::InternalSwitchTrait;
use components::switch::ISwitchComponent;
use components::switchable::switchable_component::InternalTrait;
use components::switchable::ISwitchable;

use core::starknet::storage::StorageMemberAccessTrait;
use super::SwitchContract;

Expand Down Expand Up @@ -64,10 +63,10 @@ mod tests {
#[available_gas(2000000)]
fn test_value() {
let mut state = STATE();
assert(state.value() == state.switch.value.read(), 'Wrong value');
assert(state.value() == state.switch.switchable_value.read(), 'Wrong value');

state.switch.switch();
assert(state.value() == state.switch.value.read(), 'Wrong value');
assert(state.value() == state.switch.switchable_value.read(), 'Wrong value');
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// ANCHOR: interface
#[starknet::interface]
trait ISwitchCollision<TContractState> {
fn set(ref self: TContractState, value: bool);
fn get(ref self: TContractState) -> bool;
}
// ANCHOR_END: interface

#[starknet::contract]
mod SwitchCollisionContract {
use components::switchable::switchable_component;

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

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

// ANCHOR: storage
#[storage]
struct Storage {
switchable_value: bool,
#[substorage(v0)]
switch: switchable_component::Storage,
}
// ANCHOR_END: storage

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

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

#[external(v0)]
impl SwitchCollisionContract of super::ISwitchCollision<ContractState> {
fn set(ref self: ContractState, value: bool) {
self.switchable_value.write(value);
}

fn get(ref self: ContractState) -> bool {
self.switchable_value.read()
}
}
}
1 change: 1 addition & 0 deletions listings/applications/components/src/contracts/tests.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod switch_collision_tests;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
mod switch_collision_tests {
use components::switchable::switchable_component::InternalTrait;
use components::switchable::{ISwitchable, ISwitchableDispatcher, ISwitchableDispatcherTrait};

use components::contracts::switch_collision::{
SwitchCollisionContract, ISwitchCollisionDispatcher, ISwitchCollisionDispatcherTrait
};

use core::starknet::storage::StorageMemberAccessTrait;
use starknet::deploy_syscall;

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

(
ISwitchCollisionDispatcher { contract_address },
ISwitchableDispatcher { contract_address },
)
}

#[test]
#[available_gas(2000000)]
// ANCHOR: collision
fn test_collision() {
let (mut contract, mut contract_iswitch) = deploy();

assert(contract.get() == false, 'value !off');
assert(contract_iswitch.value() == false, 'switch !off');

contract_iswitch.switch();
assert(contract_iswitch.value() == true, 'switch !on');
assert(contract.get() == true, 'value !on');

// `collision` between component storage 'value' and contract storage 'value'
assert(contract.get() == contract_iswitch.value(), 'value != switch');

contract.set(false);
assert(contract.get() == contract_iswitch.value(), 'value != switch');
}
// ANCHOR_END: collision
}
4 changes: 3 additions & 1 deletion listings/applications/components/src/lib.cairo
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
// Components
mod switchable;

mod contracts;
mod switch;
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#[starknet::interface]
trait ISwitchComponent<TContractState> {
trait ISwitchable<TContractState> {
fn value(self: @TContractState) -> bool;
fn switch(ref self: TContractState);
}

#[starknet::component]
mod switch_component {
mod switchable_component {
#[storage]
struct Storage {
value: bool,
switchable_value: bool,
}

#[derive(Drop, starknet::Event)]
Expand All @@ -20,26 +20,26 @@ mod switch_component {
SwitchEvent: SwitchEvent,
}

#[embeddable_as(Switch)]
impl SwitchImpl<
#[embeddable_as(Switchable)]
impl SwitchableImpl<
TContractState, +HasComponent<TContractState>
> of super::ISwitchComponent<ComponentState<TContractState>> {
> of super::ISwitchable<ComponentState<TContractState>> {
fn value(self: @ComponentState<TContractState>) -> bool {
self.value.read()
self.switchable_value.read()
}

fn switch(ref self: ComponentState<TContractState>) {
self.value.write(!self.value.read());
self.switchable_value.write(!self.switchable_value.read());
self.emit(Event::SwitchEvent(SwitchEvent {}));
}
}

#[generate_trait]
impl InternalSwitchImpl<
impl InternalImpl<
TContractState, +HasComponent<TContractState>
> of InternalSwitchTrait<TContractState> {
> of InternalTrait<TContractState> {
fn _off(ref self: ComponentState<TContractState>) {
self.value.write(false);
self.switchable_value.write(false);
}
}
}
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Summary

# Components
- [Components How-To](./components/how_to.md)
- [Storage Collisions](./components/collisions.md)

<!-- ch01 -->
# Applications
Expand Down
30 changes: 30 additions & 0 deletions src/components/collisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Component-Contract Storage Collision

Components can declare their own storage variables.

When a contract use a component, the component storage is merged with the contract storage.
The storage layout is only determined by the variables names, so variables with the same name will collide.

> In a future release, the `#[substorage(v1)]` will determine the storage layout based on the component as well, so collisions will be avoided.
A good practice is to prefix the component storage variables with the component name, as shown in the [Switchable component example](./how_to.md).

#### Example

Here's an example of a collision on the `switchable_value` storage variable of the `Switchable` component.

Interface:
```rust
{{#include ../../listings/applications/components/src/contracts/switch_collision.cairo:interface}}
```

Here's the storage of the contract (you can expand the code snippet to see the full contract):
```rust
{{#rustdoc_include ../../listings/applications/components/src/contracts/switch_collision.cairo:storage}}
```

Both the contract and the component have a `switchable_value` storage variable, so they collide:

```rust
{{#rustdoc_include ../../listings/applications/components/src/contracts/tests/switch_collision_tests.cairo:collision}}
```
18 changes: 13 additions & 5 deletions src/components/how_to.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,33 @@ Key characteristics:

## How to create a component

The following example shows a simple `Switch` component that can be used to turn a boolean on or off.
It contains a storage variable `value`, a function `switch` and an event `Switch`.
The following example shows a simple `Switchable` component that can be used to add a switch that can be either on or off.
It contains a storage variable `switchable_value`, a function `switch` and an event `Switch`.

> It is a good practice to prefix the component storage variables with the component name to [avoid collisions](./collisions.md).
```rust
{{#include ../../listings/applications/components/src/switch.cairo}}
{{#include ../../listings/applications/components/src/switchable.cairo}}
```

A component in itself is really similar to a contract, it *can* also have:
- An interface defining entrypoints (`ISwitchComponent<TContractState>`)
- An interface defining entrypoints (`ISwitchableComponent<TContractState>`)
- A Storage struct
- Events
- Internal functions

It don't have a constructor, but you can create a `_init` internal function and call it from the contract's constructor. In the previous example, the `_off` function is used this way.

> It's currently not possible to use the same component multiple times in the same contract.
> This is a known limitation that may be lifted in the future.
>
> For now, you can view components as an implementation of a specific interface/feature (`Ownable`, `Upgradeable`, ... `~able`).
> This is why we called it `Switchable` and not `Switch`; The contract *is switchable*, not *has a switch*.
## How to use a component

Now that we have a component, we can use it in a contract.
The following contract incorporates the `Switch` component:
The following contract incorporates the `Switchable` component:

```rust
{{#include ../../listings/applications/components/src/contracts/switch.cairo:contract}}
Expand Down

0 comments on commit d47b342

Please sign in to comment.