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:
- Add
#[external(v0)]
before each method. - 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.
- Import necessary modules and traits:
use dojo::test_utils::deploy_contract;
use tsubasa::systems::{ICreateCardDispatcher, ICreateCardDispatcherTrait};
- 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