Skip to content

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);

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 and world.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?