Migration Guide to 0.3.0

0.3.0 introduced some breaking changes to Systems and Models which requires reworking of your worlds.

Components to Models

In version 0.3.0, "components" have been renamed to "models". This has been done due to Cairo introducing the concept of Components natively.

You must:

  • Replace #[component] with #[model].
  • Update #[derive(Component)] to #[derive(Model)] throughout your code.

Note: Ensure all related files and imports are updated accordingly.

Changes in Model Implementation

The trait SerdeLen is no longer implemented for models. If you relied on this previously, you should now use SchemaIntrospection.

Schema Introduction

For models containing complex types, it's crucial to implement the SchemaIntrospection trait.

Consider the model below:

struct Card {

    #[key]
    token_id: u256,
    /// The card's designated role.
    role: Roles,
}

For complex types, like Roles in the above example, you need to implement SchemaIntrospection. Here's how:

impl RolesSchemaIntrospectionImpl of SchemaIntrospection<Roles> {
    #[inline(always)]
    fn size() -> usize {
        1 // Represents the byte size of the enum.
    }

    #[inline(always)]
    fn layout(ref layout: Array<u8>) {
        layout.append(8); // Specifies the layout byte size;
    }

    #[inline(always)]
    fn ty() -> Ty {
        Ty::Enum(
            Enum {
                name: 'Roles',
                attrs: array![].span(),
                children: array![
                    ('Goalkeeper', serialize_member_type(@Ty::Tuple(array![].span()))),
                    ('Defender', serialize_member_type(@Ty::Tuple(array![].span()))),
                    ('Midfielder', serialize_member_type(@Ty::Tuple(array![].span()))),
                    ('Attacker', serialize_member_type(@Ty::Tuple(array![].span()))),
                ]
                .span()
            }
        )
    }
}

Key Takeaways from custom types:

  • size: Defines the byte size of the type.
  • layout: Outlines the byte structure/layout for the type. Validate and adjust as necessary.
  • ty: Details the specific type, attributes, and subcomponents. For enums, like Roles, you need to specify each member and its type.

Systems Update

Systems in 0.3.0 are very similar now to Cairo Contracts. You can write your systems just like regular contracts, and each dojo contract can contain mulitple systems.

Important high level changes:

  • Systems are now starknet contracts
  • Define Interfaces for each system contract
  • New optional #[dojo::contract] decorator defining systems
  • Multiple systems per dojo contract, rather than singular
  • execute is no longer required system selector name

Interface Creation

System management has been revamped. Start by defining an interface for each system, which specifies its implementation:

#[starknet::interface]
trait ICreateCard<TContractState> {
    fn create_card(
        self: @TContractState,
        world: IWorldDispatcher,
        token_id: u256,
        dribble: u8,
        defense: u8,
        cost: u8,
        role: Roles,
        is_captain: bool
    );
}

Ensure the trait is typed with TContractState.

Note: Earlier versions required functions within the system to be named execute. This is no longer the case.

Interface Implementation

To implement the interface:

  1. Add #[external(v0)] before each method.
  2. Ensure to reference the created interface in the module with use super::ICreateCard;.
#[external(v0)]
impl CreateCardImpl of ICreateCard<ContractState> {
    fn create_card(
        self: @ContractState,
        world: IWorldDispatcher,
        token_id: u256,
        dribble: u8,
        defense: u8,
        cost: u8,
        role: Roles,
        is_captain: bool
    ) {
        // your logic here
    }
}

This then allows the create_card to be called just like a regular starknet function.

#[dojo::contract] decorator

0.3.0 introduces a new optional decorator #[dojo::contract] which indicates to the compiler to inject imports and the world dispatcher. This allows for minimal boilerplate.

#[dojo::contract]
mod move {
....code TODO
}

Events

Events should now reside within the models. Here's an example of how to migrate your events:

Previous Format:

#[derive(Drop, starknet::Event, Copy)]
struct DeckCreated {
    player: ContractAddress,
    token_list: Span<u256>,
}

New Format:

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

#[derive(Drop, starknet::Event)]
struct DeckCreated {
    player: ContractAddress,
    token_list: Span<u256>,
}

Testing Changes

Setup

Testing has seen significant changes with the change to systems as Contracts. Instead of using world.execute, use the dispatcher.

  1. Import necessary modules and traits:
use dojo::test_utils::deploy_contract;
use tsubasa::systems::{ICreateCardDispatcher, ICreateCardDispatcherTrait};
  1. Deploy the contract and instantiate the dispatcher:
let contract_create_card = deploy_contract(
    create_card_system::TEST_CLASS_HASH, array![].span()
);
let create_card_system = ICreateCardDispatcher { contract_address: contract_create_card };

Function Testing

With the contract deployed and the dispatcher instantiated, proceed to test your functions:

// ... (previous setup code)

let result = create_card_system.create_card(
    // ... provide necessary parameters here
);

// Assert or validate the 'result' as per your test conditions