Dojo 101 Tutorial
Dojo in 15 minutes or less
To start, let's create a new project to run locally on your machine.
sozo init dojo-starter
Congratulations! You now have a local Dojo project!
This command creates a dojo-starter
project in your current directory from the Dojo starter template. It's the ideal starting point for a new project and equips you with everything you need to begin hacking.
Anatomy of a Dojo Project
Inspect the contents of the dojo-starter
project, and you'll notice the following structure (excluding the non-Cairo files):
├── Scarb.toml
├── dojo_dev.toml
├── dojo_release.toml
└── src
├── lib.cairo
├── models.cairo
├── systems
│ └── actions.cairo
└── tests
└── test_world.cairo
The scarb manifest (Scarb.toml
) is a configuration file where project dependencies, metadata and other configurations are defined.
Models
Next, open the src/models.cairo
file to continue.
// ...
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Moves {
#[key]
player: ContractAddress,
remaining: u8,
...
}
// ...
Notice the #[dojo::model]
attribute.
For a model to be recognized, we must add this attribute to a Cairo struct. This tells to the Dojo compiler that this struct should be treated as a model.
Understanding the #[key]
Attribute in the Moves Model
Our Moves model includes a player
field, which is crucial for how Dojo manages and queries data. Models act like structured database entries, managing and organizing your onchain data. Like a regular ORM - but onchain!
Next, lets have a look at the src/models/position.cairo
file.
// ...
#[derive(Drop, Copy, Serde)]
#[dojo::model]
struct Position {
// Define a Key. This acts like a primary key does in a regular ORM
#[key]
player: ContractAddress,
// Define a value field. This acts like a column in a regular ORM.
vec: Vec2,
}
// define Introspect struct
#[derive(Drop, Copy, Serde, Introspect)]
struct Vec2 {
// Define a value field. This acts like a column in a regular ORM.
x: u32,
y: u32
}
// ...
The Position
model, like Moves
, is indexed by the player
field and includes a Vec2
struct for x and y coordinates. Models can contain any Cairo struct that derives the Introspect
trait, which allows the compiler to introspect the struct and generate the necessary code to interact with it.
Contract Systems
A dojo contract is just a regular Starknet contract which is defined with the #[dojo::contract]
attribute.
First, a dojo contract must define one (or more) Dojo interfaces.
#[starknet::interface]
pub trait IActions<T> {
fn spawn(ref self: T);
fn move(ref self: T, direction: Direction);
}
Now, let's examine a contract implementation of the src/systems/actions.cairo
file.
use dojo_starter::models::{Direction, Position};
// define the interface
#[starknet::interface]
trait IActions<T> {
fn spawn(ref self: T);
fn move(ref self: T, direction: Direction);
}
// dojo decorator
#[dojo::contract]
pub mod actions {
use super::{IActions, Direction, Position, next_position};
use starknet::{ContractAddress, get_caller_address};
use dojo_starter::models::{Vec2, Moves, DirectionsAvailable};
use dojo::model::{ModelStorage, ModelValueStorage};
use dojo::event::EventStorage;
#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct Moved {
#[key]
pub player: ContractAddress,
pub direction: Direction,
}
#[abi(embed_v0)]
impl ActionsImpl of IActions<ContractState> {
fn spawn(ref self: ContractState) {
// Get the default world.
let mut world = self.world_default();
// Get the address of the current caller, possibly the player's address.
let player = get_caller_address();
// Retrieve the player's current position from the world.
let position: Position = world.read_model(player);
// Update the world state with the new data.
// 1. Move the player's position 10 units in both the x and y direction.
let new_position = Position {
player, vec: Vec2 { x: position.vec.x + 10, y: position.vec.y + 10 }
};
// Write the new position to the world.
world.write_model(@new_position);
// 2. Set the player's remaining moves to 100.
let moves = Moves {
player, remaining: 100, last_direction: Direction::None(()), can_move: true
};
// Write the new moves to the world.
world.write_model(@moves);
}
// Implementation of the move function for the ContractState struct.
fn move(ref self: ContractState, direction: Direction) {
// Get the default world.
let mut world = self.world_default();
// Get the address of the current caller, possibly the player's address.
let player = get_caller_address();
// Retrieve the player's current position and moves data from the world.
let position: Position = world.read_model(player);
let mut moves: Moves = world.read_model(player);
// Deduct one from the player's remaining moves.
moves.remaining -= 1;
// Update the last direction the player moved in.
moves.last_direction = direction;
// Calculate the player's next position based on the provided direction.
let next = next_position(position, direction);
// Write the new position to the world.
world.write_model(@next);
// Write the new moves to the world.
world.write_model(@moves);
// Emit an event to the world to notify about the player's move.
world.emit_event(@Moved { player, direction });
}
}
#[generate_trait]
impl InternalImpl of InternalTrait {
// Use the default namespace "dojo_starter".
// This function is handy since the ByteArray can't be const.
fn world_default(self: @ContractState) -> dojo::world::WorldStorage {
self.world(@"dojo_starter")
}
}
}
// Define function like this:
fn next_position(mut position: Position, direction: Direction) -> Position {
match direction {
Direction::None => { return position; },
Direction::Left => { position.vec.x -= 1; },
Direction::Right => { position.vec.x += 1; },
Direction::Up => { position.vec.y -= 1; },
Direction::Down => { position.vec.y += 1; },
};
position
}
Using the world.write_model
method
Here we use the world.write_model
method to set the Moves
and Position
models for the player
entity. This method is used to update the world state with the new data.
let moves = Moves {
player, remaining: 100, last_direction: Direction::None(()), can_move: true
};
// Write the new moves to the world.
world.write_model(@moves);
Deeper into Systems
Learn more about systems and how they are used in Dojo.
Deeper into Macros
Learn more about the macros used in Dojo contracts.
We covered a lot here in a short time. Let's recap:
- Explained the anatomy of a Dojo project
- Explained the importance of the
#[dojo::model]
attribute and how models are defined - Explained how
#[dojo::contract]
are used to define systems - Explained how to inject the world into a system
- Touched on the
world.read_model
andworld.write_model
methods to interact with the world
Deploy it locally
Enough theory! Let's build the Dojo project! In your primary terminal:
sozo build
That compiled the models and systems into artifacts that can be deployed. Simple as that!
Now, let's deploy it to Katana! First, we need to get Katana running. Open a second terminal and execute:
katana --dev --dev.no-fee
Success! Katana should now be running locally on your machine. Now, let's inspect the world. In your primary terminal, execute:
sozo inspect
This command provides a preview of your World's state and contract addresses. It performs a read-only comparison between local and remote resources without sending any transactions. Now, let's deploy! In your primary terminal, execute:
sozo migrate
This command will deploy the artifact to Katana
. You should see terminal output similar to this:
profile | chain_id | rpc_url
---------+----------+------------------------
dev | KATANA | http://localhost:5050/
⛩️ Migration successful with world at address 0x06171ed98331e849d6084bf2b3e3186a7ddf35574dd68cab4691053ee8ab69d7
Your 🌎 is now deployed at 0x06171ed98331e849d6084bf2b3e3186a7ddf35574dd68cab4691053ee8ab69d7
!
This establishes the world address for your project.
Let's discuss the Scarb.toml
file in the project. This file contains environment variables that make running CLI commands in your project a breeze (read more about it here).
[scripts]
migrate = "sozo build && sozo migrate" # scarb run migrate
spawn = "sozo execute dojo_starter-actions spawn --wait" # scarb run spawn
move = "sozo execute dojo_starter-actions move -c 1 --wait" # scarb run move
At the same time, make sure your file specifies the version of Dojo you have installed! In this case version v1.0.0
.
[dependencies]
dojo = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0" }
Indexing
With your local world address established, let's delve into indexing. You can index the entire world. To accomplish this we have to copy your world address
from the output of sozo migrate
. Now Open a new terminal and input this simple command that includes your own world address:
torii --world 0xb4079627ebab1cd3cf9fd075dda1ad2454a7a448bf659591f259efa2519b18 --http.cors_origins "*"
Running the command mentioned above starts a Torii server on your local machine. This server uses SQLite as its database and is accessible at http://0.0.0.0:8080/graphql. Torii
will automatically organize your data into tables, making it easy for you to perform queries using GraphQL. When you run the command, you'll see terminal output that looks something like this:
2024-06-14T04:38:33.450886Z INFO torii::relay::server: Relay peer id. peer_id=12D3KooWNJYDBVvnrWgi6QeVaQr6TZMEgJNni51UhZVGw1is4i9P
2024-06-14T04:38:33.451533Z INFO libp2p_swarm: local_peer_id=12D3KooWNJYDBVvnrWgi6QeVaQr6TZMEgJNni51UhZVGw1is4i9P
2024-06-14T04:38:33.452284Z INFO torii::cli: Starting torii endpoint. endpoint=http://127.0.0.1:8080
2024-06-14T04:38:33.452289Z INFO torii::cli: Serving Graphql playground. endpoint=http://127.0.0.1:8080/graphql
2024-06-14T04:38:33.452292Z INFO torii::cli: Serving World Explorer. url=https://worlds.dev/torii?url=http%3A%2F%2F127.0.0.1%3A8080%2Fgraphql
2024-06-14T04:38:33.452388Z INFO torii::relay::server: New listen address. address=/ip4/127.0.0.1/tcp/9090
2024-06-14T04:38:33.452405Z INFO torii::relay::server: New listen address. address=/ip4/192.168.1.249/tcp/9090
2024-06-14T04:38:33.452417Z INFO torii::relay::server: New listen address. address=/ip4/127.0.0.1/udp/9090/quic-v1
2024-06-14T04:38:33.452422Z INFO torii::relay::server: New listen address. address=/ip4/192.168.1.249/udp/9090/quic-v1
2024-06-14T04:38:33.452441Z INFO torii::relay::server: New listen address. address=/ip4/127.0.0.1/udp/9091/webrtc-direct/certhash/uEiCGVkq8QLf7zYLTYj389DZOcEjPOlTTj0D8VdFWb8qg9w
2024-06-14T04:38:33.452451Z INFO torii::relay::server: New listen address. address=/ip4/192.168.1.249/udp/9091/webrtc-direct/certhash/uEiCGVkq8QLf7zYLTYj389DZOcEjPOlTTj0D8VdFWb8qg9w
2024-06-14T04:38:33.455849Z INFO tori_core::engine: Processed block. block_number=3
2024-06-14T04:38:33.455857Z INFO tori_core::engine: Processed block. block_number=7
2024-06-14T04:38:33.455860Z INFO tori_core::engine: Processed block. block_number=9
2024-06-14T04:38:33.455862Z INFO tori_core::engine: Processed block. block_number=10
2024-06-14T04:38:33.455864Z INFO tori_core::engine: Processed block. block_number=11
2024-06-14T04:38:33.466242Z INFO torii_core::processors::register_model: Registered model. name=DirectionsAvailable
2024-06-14T04:38:33.474421Z INFO torii_core::processors::register_model: Registered model. name=Moves
2024-06-14T04:38:33.481562Z INFO torii_core::processors::register_model: Registered model. name=Position
2024-06-14T04:38:33.483915Z INFO torii_core::processors::metadata_update: Resource metadata set. resource=0x0 uri=ipfs://QmPVJzGEcj9Buy1mxTaXf43Fsv8CAzAwLNrjAN8JbigNQE/
2024-06-14T04:38:33.483953Z INFO torii_core::processors::metadata_update: Resource metadata set. resource=0x2491b781c4270f3f07a935b043ba8e1adf26afff83a3de3c6d5b87f1d0e23a5 uri=ipfs://QmbfMea3hDYMsS4gKpHjWW1h7RTPZv86Ua9BwEsHGzoZiH/
2024-06-14T04:38:33.483989Z INFO torii_core::processors::metadata_update: Resource metadata set. resource=0x23a5929b01fe8ac7a5c4ac078445d94c81ecdc23ae2c5c8555b3a4e0280964a uri=ipfs://QmeEbEAi3yr8Rxzi5TWc3MBncj295waxgeftQwbhNzrWnz/
2024-06-14T04:38:33.484024Z INFO torii_core::processors::metadata_update: Resource metadata set. resource=0x19a4478427ad87dac878352f7b5c33354395e17e7041e759f9581174962fe72 uri=ipfs://QmV3jymZLP6HJe26m9JchTogDY6xNedmT4h88xAvPfAroT/
2024-06-14T04:38:33.484059Z INFO torii_core::processors::metadata_update: Resource metadata set. resource=0x3610b797baec740e2fa25ae90b4a57d92b04f48a1fdbae1ae203eaf9723c1a0 uri=ipfs://Qmb9xTtZ7seVpmBj3vMdv4ox9PJMypAzzXdYgm9TpXR5cX/
2024-06-14T04:38:34.061939Z INFO torii_core::processors::metadata_update: Updated resource metadata from ipfs. resource=0x19a4478427ad87dac878352f7b5c33354395e17e7041e759f9581174962fe72
2024-06-14T04:38:34.066134Z INFO torii_core::processors::metadata_update: Updated resource metadata from ipfs. resource=0x2491b781c4270f3f07a935b043ba8e1adf26afff83a3de3c6d5b87f1d0e23a5
2024-06-14T04:38:34.077085Z INFO torii_core::processors::metadata_update: Updated resource metadata from ipfs. resource=0x23a5929b01fe8ac7a5c4ac078445d94c81ecdc23ae2c5c8555b3a4e0280964a
2024-06-14T04:38:34.091113Z INFO torii_core::processors::metadata_update: Updated resource metadata from ipfs. resource=0x3610b797baec740e2fa25ae90b4a57d92b04f48a1fdbae1ae203eaf9723c1a0
2024-06-14T04:38:34.776025Z INFO torii_core::processors::metadata_update: Updated resource metadata from ipfs. resource=0x0
You can observe that our Moves
and Position
models have been successfully registered.
GraphQL Queries
Next, let's use the GraphQL IDE to retrieve data from all the models that have been registered. In your web browser, navigate to http://localhost:8080/graphql
, and enter the following query:
query {
models {
edges {
node {
id
name
classHash
contractAddress
}
}
totalCount
}
}
After you run the query, you will receive an output like this:
{
"data": {
"models": {
"edges": [
{
"node": {
"id": "0x77844f1facb51e60e546a9832d56c6bd04fa23be4fd5b57290caae5e9a3c1e4",
"name": "DirectionsAvailable",
"classHash": "0x7deb48ccf95cc441a0489cfefdae54aeb6f8ec462ba13ff25e23f080e66cc2f",
"contractAddress": "0x410c3b01e8209f1bb0c6591283efebddd5034b4baed4f0e9ea3c318d9fbae0b"
}
},
{
"node": {
"id": "0x504403e5c02b6442527721fc464d9ea5fc8f1ee600ab5ccd5c0845d36fd45f1",
"name": "Moved",
"classHash": "0x5be0a05a5df3bd3b4fc17f8b1feb395cb463ced20ea41d4fbb9b86a4d7efc66",
"contractAddress": "0x47d654cf2ea600e9689219b1b8545c742c1229ac948c87a01787462f3e93f96"
}
},
{
"node": {
"id": "0x2ac8b4c190f7031b9fc44312e6b047a1dce0b3f2957c33a935ca7846a46dd5b",
"name": "Position",
"classHash": "0x2283c68ecba5c60bbbbd3b00659808a02244468e41a1d2cdba1312d65b83594",
"contractAddress": "0x6e2770c9bbcf3f4f11529780fc04c2041f186225634541da225b93fe98b5bfd"
}
},
{
"node": {
"id": "0x2a29373f1af8348bd366a990eb3a342ef2cbe5e85160539eaca3441a673f468",
"name": "Moves",
"classHash": "0x70edf8f3be0b118e78f856f3ea9ebb652cba3684abaf7f299bfa6f93bf907c9",
"contractAddress": "0x554606894a9be2241c0dc3735c0f322c8d4816bfe292165614ca9a455b47503"
}
}
],
"totalCount": 4
}
}
}
Awesome, now let's work with subscriptions to get real-time updates. Let's clean up your workspace on the GraphQL IDE and input the following subscription:
subscription {
entityUpdated {
id
keys
eventId
createdAt
updatedAt
}
}
Once you execute the subscription, you will receive notifications whenever new entities are updated or created. For now, don't make any changes to it, and proceed to create a new entities.
To accomplish this, we have to execute spawn
function from actions
contract. In your main local terminal, run the following command:
sozo execute dojo_starter-actions spawn
By running this command, you've executed the spawn system, resulting in the creation of a new entity.
Now, go back to your GraphQL IDE, and you will notice that you have received the subscription's results, which should look something like this:
{
"data": {
"entityUpdated": {
"id": "0x43ebbfee0476dcc36cae36dfa9b47935cc20c36cb4dc7d014076e5f875cf164",
"keys": [
"0x127fd5f1fe78a71f8bcd1fec63e3fe2f0486b6ecd5c86a0466c3a21fa5cfcec"
],
"eventId": "0x0000000000000000000000000000000000000000000000000000000000000b:0x3baa02ae902388ed39ba124d1538e889d8265cfc99d461dc30781c478f4799:0x01",
"createdAt": "2024-11-12T13:23:35Z",
"updatedAt": "2024-11-12T13:23:35Z"
}
}
}
--------------------------------------------------------------------------------------------------------
{
"data": {
"entityUpdated": {
"id": "0x43ebbfee0476dcc36cae36dfa9b47935cc20c36cb4dc7d014076e5f875cf164",
"keys": [
"0x127fd5f1fe78a71f8bcd1fec63e3fe2f0486b6ecd5c86a0466c3a21fa5cfcec"
],
"eventId": "0x0000000000000000000000000000000000000000000000000000000000000b:0x3baa02ae902388ed39ba124d1538e889d8265cfc99d461dc30781c478f4799:0x00",
"createdAt": "2024-11-12T13:23:35Z",
"updatedAt": "2024-11-12T13:23:35Z"
}
}
}
In the GraphQL IDE, by clicking the DOCS
-button on the right, you can open the API documentation. This documentation is auto-generated based on our schema definition and displays all API operations and data types of our schema. In order to know more about query and subscription, you can jump to GraphQL section.
We've covered quite a bit! Here's a recap:
- Built a Dojo world
- Deployed the project to Katana
- Indexed the world with Torii
- Executed the
spawn
system locally - Interacted with GraphQL
Next Steps
This overview provides a rapid end-to-end glimpse of Dojo. However, the potential of these worlds is vast! Designed to manage hundreds of systems and models, Dojo is equipped for expansive creativity. So, what will you craft next?