# Dojo Documentation > Dojo | A Toolchain for Building Provable Games and Applications ## Brand Assets Download official logos and assets for Dojo and its ecosystem. Right-click any image and select "Save Image As" to download. ### Core Dojo Brand The Dojo logo represents the complete framework and toolchain for building provable games and applications. For an overview of how the tools work together, see the [Dojo Overview](/overview).
Dojo Full Logo

Full Logo

Icon + Wordmark

{" "}
Dojo Icon

Icon Only

Square format

Dojo Wordmark

Wordmark

Text only

### Toolchain The Dojo toolchain consists of specialized tools for building, testing, and deploying provable applications. #### Katana Katana is a blazing-fast sequencer for local development and testing.
Katana Logo
[Read more about Katana](/toolchain/katana) #### Torii Torii automatically generates GraphQL and gRPC APIs for real-time state updates.
Torii Logo
[Read more about Torii](/toolchain/torii) #### Sozo Sozo is the command-line interface for managing Dojo projects and deployments.
Sozo Logo
[Read more about Sozo](/toolchain/sozo) #### Saya Saya provides proving infrastructure for Dojo applications.
Saya Logo
[Read more about Saya](/toolchain/saya) #### Slot Slot is a managed-service execution layer for deploying and scaling provable applications.
Slot Logo
[Read more about Slot](https://github.com/cartridge-gg/slot) ### Ecosystem Libraries #### Origami Origami provides a collection of Cairo utilities and primitives for building Dojo applications.
Origami Logo
[Read more about Origami](/libraries/origami) #### Alexandria Alexandria is a comprehensive library collection for Cairo development.
Alexandria Logo
[Read more about Alexandria](/libraries/alexandria) ### SDK Platform Icons Dojo provides client SDKs for multiple platforms and game engines.
JavaScript

JavaScript

{" "}
Rust

Rust

{" "}
C/C++

C/C++

{" "}
Unity

Unity

{" "}
Godot

Godot

{" "}
Bevy

Bevy

{" "}
Telegram

Telegram

Discord

Discord

[Explore all SDKs](/client/sdk) ### Brand Guidelines #### Colors The Dojo brand uses a distinctive color palette: * **Primary Accent**: `#ee2d3f` (Red) * **Background**: `#0c0c0c` (Black) * **Dark Background**: `#121212` (Dark Gray) #### Usage When using Dojo brand assets: * **Do** maintain clear space around logos equal to the height of the icon * **Do** use appropriate contrast for backgrounds * **Do** maintain original aspect ratios * **Don't** modify colors or proportions * **Don't** add effects, shadows, or distortions * **Don't** combine with other brand elements #### Typography The Dojo documentation uses **Open Sans** as the primary font family. ### Partner Logos
Starknet
{" "}
StarkWare
{" "}
Cartridge
Celestia
### Contact For brand partnership inquiries or additional asset requests, please reach out through our [Discord community](https://discord.gg/dojoengine). ## FAQ ### Provable Games #### What is an onchain game? Onchain games are games that exist entirely on a public blockchain network; all states and logic are onchain. Clients (like web browsers) do not exist on the chain but exist purely to interact with and interpret the onchain state. #### What is a provable game? Thanks to the magic of zero-knowledge proofs, we can ensure a game is fair by verifying a zk proof created off-chain. But what does that entail? Consider a game of chess. We aim for an experience where players trust each other's moves. In a straightforward approach — and given the simple rules of chess — if this were in a blockchain environment, every move would be a transaction on the blockchain. This is costly. We just want to know the winner, not every move. With zk proofs and client communications, players can establish a state channel, sharing moves off-chain and ensuring their validity. At the end, a zk proof can be submitted to the blockchain to confirm the game's fairness. This constitutes a provable game. #### What is an autonomous world? An autonomous world is one that exists entirely onchain. It's not controlled by any single entity but is instead governed by the rules set within that world. [Dive deeper into the topic here](/theory/autonomous-worlds). ### Dojo Development #### Why Dojo? Dojo was created to solve problems the founders faced when building onchain games in Cairo. It standardizes the process of building such games and provides a suite of tools to make it easier. #### What is Cairo? Cairo is an open-source programming language invented by StarkWare. It's a Turing-complete low-level language designed to compile to the Cairo Virtual Machine. Learn more about it here: [Cairo](https://www.cairo-lang.org/). #### Can I deploy Dojo to Starknet? Yes! Dojo can run on any StarknetVM including the public blockchains. Within the Dojo toolchain exists [Katana](/toolchain/katana) which is a gaming specific sequencer, which is perfectly suited to Dojo games. #### Can Dojo do client side proofs? The ability to execute Dojo programs in the browser is entirely plausible and is on our roadmap. If you are a specalist in this, jump into the Github and help out! #### Who maintains Dojo? Dojo is an open-source initiative, licensed under Apache 2.0, dedicated to promoting and advancing the concept of Autonomous Worlds (AWs). It is developement is led by [Cartridge](https://cartridge.gg/), with significant contributions from [Realms](https://realms.world/), [BibliothecaDAO](https://bibliothecadao.xyz/) and [many more](https://github.com/orgs/dojoengine/people). #### Where is the Dojo roadmap? Dojo is rapidly evolving. You can find open issues on the [Dojo Github](https://github.com/dojoengine/dojo/issues) and join the [Discord](https://discord.gg/invite/dojoengine) to get involved. If you have ideas for the project, please open an issue. #### How do I get involved? Check out our [Github](https://github.com/dojoengine/dojo/blob/main/CONTRIBUTING.md), [Twitter](https://x.com/ohayo_dojo), or [Discord](https://discord.gg/invite/dojoengine). ### Known Limitations #### Starknet contract size limit Starknet enforces a maximum size for contract classes. If your Dojo contract grows too large (many systems or complex logic in a single contract), deployment will fail with a size limit error. The mitigation is to split logic across multiple contracts or use [Dojo libraries](/libraries/origami) to separate reusable logic from your contracts via library calls. #### Models inside models A `#[dojo::model]` struct cannot be used as a field inside another model. Use a plain struct deriving `Introspect` instead. See [Model Composition](/framework/models#model-composition) for details. import { LinkCard } from "../components/LinkCard"; ## Installing Dojo Dojo's installation process was designed to be idiomatically consistent with its dependencies, Rust and Cairo. You can get up-and-running in just a few steps, and be building provable games in no time. :::warning Windows is not natively supported. We recommend [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) for Windows development. ::: ### Quick Start For existing Cairo developers, the fastest way to get started is with our `asdf` installer. Simply run the following command in your terminal to install the Dojo toolchain. ```bash curl -L https://install.dojoengine.org | bash ``` This script will install the `asdf` plugins for Sozo, Katana, and Torii, install their latest versions, and set them for global use. Skip to [Verification](#verification) to test your installation. :::note This script requires `asdf` to be installed. See [here](https://asdf-vm.com/guide/getting-started.html) for installation instructions. ::: ### Detailed Installation #### System Requirements Before installing Dojo, ensure your system meets these requirements: * **Operating System**: macOS, Linux, or Windows with WSL2 * **Memory**: At least 4GB RAM (8GB recommended) * **Storage**: 2GB free disk space for toolchains and dependencies * **Network**: Stable internet connection for downloading binaries * **Dependencies**: `curl`, `jq`, and `git` (usually pre-installed on most systems) ### Prerequisites Before installing Dojo, you'll need these foundational tools: ##### 1. Install Rust with `rustup` Rust is required for Dojo's infrastructure tools (Katana and Torii). **Minimum version**: 1.85.0 ```bash curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh ``` This installs Rust's executables, including the [`cargo`](https://doc.rust-lang.org/cargo/) package manager, in the `~/.cargo` directory. **Verify installation:** ```bash cargo --version # Should output: cargo 1.85.0 (or later) ``` For more information, see the [Rust installation guide](https://doc.rust-lang.org/book/ch01-01-installation.html). ##### 2. Install Cairo with `starkup` Dojo is written in [Cairo](https://www.cairo-lang.org/), StarkWare's language for creating provable programs. **Minimum version**: 2.10.1 ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.starkup.sh | sh ``` This installs Cairo's toolchain, including the [`scarb`](https://docs.swmansion.com/scarb/) package manager and build tool, in the `~/.asdf` directory. :::note `asdf` does not automatically add itself to your PATH and must be configured manually. See more information about configuring `asdf` [here](https://asdf-vm.com/guide/getting-started.html#_2-configure-asdf). ::: **Verify installation:** ```bash scarb --version # Should output: scarb 2.10.1 (or later) ``` For more information, see the [starkup documentation](https://github.com/software-mansion/starkup). ### Installing Dojo The Dojo framework consists of a Cairo framework and three Rust-based tools: `katana`, `torii`, and `sozo`. :::note The Cairo framework is managed by Scarb as a compile-time dependency. See the [Dojo configuration](/framework/configuration) documentation for more info. ::: #### Installing with `asdf` [`asdf`](https://asdf-vm.com/) is a popular version manager, supporting programmatic management of dependencies. `asdf` is the preferred way to install Dojo. :::tip You can use our `asdf` installer to install the toolchain in a single command: ```bash curl -L https://install.dojoengine.org | bash ``` ::: :::steps ##### Install asdf Follow the [asdf installation instructions](https://asdf-vm.com/guide/getting-started.html) for your platform. ##### Add the asdf plugins ```bash asdf plugin add katana https://github.com/dojoengine/asdf-katana.git asdf plugin add torii https://github.com/dojoengine/asdf-torii.git asdf plugin add sozo https://github.com/dojoengine/asdf-sozo.git ``` ##### Install the Dojo toolchain ```bash # Install the latest version asdf install {sozo,katana,torii} latest # Install specific versions asdf install {sozo,katana,torii} 1.6.0 # List available versions asdf list all {sozo,katana,torii} ``` ##### Set the version ```bash # Set version locally (in project .tool-versions) asdf set {sozo,katana,torii} latest # Set version globally (in user home .tool-versions) asdf set --home {sozo,katana,torii} latest ``` ##### Using `.tool-versions` `asdf` supports programmatic package management through the `.tool-versions` file. ```toml # path/to/project/.tool-versions scarb 2.12.2 sozo 1.7.1 katana 1.7.0 torii 1.8.3 starknet-foundry 0.43.1 ``` Simply run `asdf install` in the project root to install all dependencies. If needed, `asdf` will download the correct version of all dependencies. ::: #### Installing with `dojoup` [`dojoup`](https://install.dojoengine.org) is an alternative way to install and manage Dojo installations. Dojoup is version-aware and will automatically install compatible versions of `katana`, `torii`, and `sozo`. :::warning Dojoup is deprecated and will be removed in the future. Use the `asdf` installer instead. ::: ::::steps ##### Install `dojoup` ```bash curl -L https://raw.githubusercontent.com/dojoengine/dojo/main/dojoup/dojoup-install | bash ``` This command installs `dojoup` in the `~/.dojo` directory and adds it to your shell profile. :::info You need to restart your terminal for the `dojoup` command to be available. ::: ##### Install the Dojo toolchain In a new terminal window, run: ```bash dojoup install ``` This installs `katana`, `torii`, and `sozo` in the `~/.dojo` directory. ##### Advanced Usage ```bash # Install a specific version dojoup install 1.6.0 # Get help dojoup --help ``` :::: ### Verification After installation, verify that all tools are working correctly: #### Check Dojo Tools ```bash # Check Sozo (CLI tool) sozo --version # Should output: sozo 1.6.0 (or later) # Check Katana (sequencer) katana --version # Should output: katana 1.6.0 (or later) # Check Torii (indexer) torii --version # Should output: torii 1.6.0 (or later) ``` #### Test Your Installation Create a simple test project to verify everything works: ```bash # Clone the dojo-starter repository git clone https://github.com/dojoengine/dojo-starter # Navigate to the project cd dojo-starter # Build the project sozo build # Test successful if no errors appear ``` ### Claude Code Skills Dojo provides official [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) skills that give Claude relevant context for working with models, systems, testing, deployment, and more. For an overview of available skills, see the [Dojo Overview](/overview#ai-assisted-development). To install the skills, run the following in your terminal: ```bash npx skills add dojoengine/book ``` ### Troubleshooting #### Common Installation Issues ##### `dojoup: command not found` **Problem**: After installing dojoup, the command is not found. **Solution**: 1. Restart your terminal completely 2. If still not working, manually source the environment: `source ~/.dojo/env` 3. Check that `~/.dojo/bin` is in your PATH: `echo $PATH | grep -o ~/.dojo` ##### `starkup` installation fails **Problem**: `starkup` fails to install or configure properly. **Solution**: 1. Ensure `asdf` is properly installed 2. Add `~/.asdf/shims` to your PATH: `export PATH="$HOME/.asdf/shims:$PATH"` 3. Or, add it via your shell profile (`.bashrc`, `.zshrc`, etc.) ##### Version compatibility issues **Problem**: Incompatible versions between Rust, Cairo, or Dojo components. **Solution**: 1. Check current versions: ```bash cargo --version # Should be 1.85.0+ scarb --version # Should be 2.10.1+ sozo --version # Should be 1.6.0+ ``` 2. Update if needed: ```bash rustup update # Update Rust asdf install scarb latest # Update Cairo/Scarb dojoup install # Update Dojo ``` ##### Build failures in test project **Problem**: `sozo build` fails in the test project. **Solution**: 1. Ensure all dependencies are installed 2. Try cleaning and rebuilding: `sozo clean && sozo build` 3. Check for [version mismatches](https://github.com/dojoengine/dojo/blob/main/versions.json) between your installed tools #### Getting Additional Help If you continue to experience issues: 1. Check the [Dojo GitHub Issues](https://github.com/dojoengine/dojo/issues) for known problems 2. Join our [Discord](https://discord.gg/invite/dojoengine) for community support 3. Review the [Dojo Book](https://book.dojoengine.org/) for comprehensive documentation > **Note**: Version numbers in this guide are current as of the latest stable release. > For the most current version numbers, check the [Dojo releases page](https://github.com/dojoengine/dojo/releases). ### Code Editor Support #### Visual Studio Code / Cursor Install the [Cairo 1.0](https://marketplace.visualstudio.com/items?itemName=starkware.cairo1) extension for: * Syntax highlighting * Error detection * Auto-completion * Code formatting ### Getting Help If you run into problems getting started with Dojo, join our [Discord](https://discord.gg/invite/dojoengine) and ask for help. ### Next Steps With Dojo installed, you're ready to start building:
import { LinkCard } from "../components/LinkCard"; import Dojo from "../public/dojo-logo.svg?react";

Dojo is a cutting-edge Entity-Component-System (ECS) framework and toolchain for building provable applications in Cairo, versatile enough to support everything from simple NFT services to the most complex onchain games.
Dojo lets you skip the hassle of managing intricate contract patterns, writing complex indexers, or setting up detailed query systems — letting you focus on bringing your ideas to life quickly and efficiently.

![Dojo Feature Matrix](/feature-matrix.png) ### Dojo ECS Framework Instead of writing raw contracts, Dojo enables a modern [Entity-Component-System](https://github.com/SanderMertens/ecs-faq) approach to building interactive applications. Write like you would a database schema and have it reflected onchain and synced with your client. [Read more about the framework](/framework) ### Katana High-Performance Sequencer Katana is a high-performance sequencer you can spin up with a single command. Blazing fast and easy to use, Katana gets you up and running quickly. [Read more about Katana](/toolchain/katana) ### Torii Indexer for Real-Time State Updates Torii is an indexer that automatically generates GraphQL and gRPC APIs for real-time state updates. It is designed to make it easy to query and subscribe to onchain state changes. [Read more about Torii](/toolchain/torii) ### Sozo CLI for Smart Contract Management Sozo is a CLI tool for planning and managing smart contract deployments, upgrades, and maintenance. It is designed to make it easy to manage your onchain application's lifecycle. [Read more about Sozo](/toolchain/sozo) ### Multiplatform Client SDKs Dojo provides client SDKs for interacting with your onchain state from platforms like Telegram and Discord, as well as game engines like Godot, Unity, and Unreal. These libraries make it easy to connect your onchain state to your favorite frontend. [Read more about the SDKs](/client/sdk) ### Slot Deployment Infrastructure Cartridge offers Slot, a managed-service execution layer that lets you easily deploy and scale your provable applications. [Read more about Slot](https://github.com/cartridge-gg/slot) ### Autonomous Worlds Dojo is purpose-built for Autonomous Worlds — persistent, permissionless onchain environments governed entirely by their own rules. Every model, system, and event lives on the blockchain, enabling players and builders to interact with a world that no single party controls. [Learn more about Autonomous Worlds](/theory/autonomous-worlds) ### AI-Assisted Development Dojo provides AI skills for use with AI coding assistants. Install all Dojo skills: ```bash npx skills add dojoengine/book ``` Available skills: `dojo-model`, `dojo-system`, `dojo-test`, `dojo-client`, `dojo-config`, `dojo-deploy`, `dojo-init`, `dojo-migrate`, `dojo-token`, `dojo-world`, `dojo-indexer`, `dojo-review` [Browse all skills](https://github.com/dojoengine/book/tree/main/skills) For detailed installation instructions, see the [Installation Guide](/installation#claude-code-skills).
![Cover Image](/blog/building-games-starknet/cover.png) ## Building Games on Starknet: The Complete Stack ::authors Building a game on Starknet isn't just about writing Cairo. You need a full stack: an execution environment, state management, client synchronization, and player onboarding. Here's how the pieces fit together. ### The Challenge Traditional game development has decades of tooling. Engines like Unity and Unreal handle rendering, physics, networking, and asset management out of the box. Onchain games are different. The blockchain is your backend, but you still need: * A way to model game state * Fast, cheap execution for player actions * Clients that stay in sync with onchain state * Wallets that don't interrupt gameplay Dojo solves this by providing a complete framework purpose-built for onchain games. ### Layer 1: The Framework Dojo brings familiar game development patterns to Cairo: **Entity Component System (ECS)** Games think in entities (players, items, tiles) and components (position, health, inventory). Dojo's ECS maps directly to onchain storage while generating typed clients automatically. ```cairo #[derive(Component)] struct Position { x: u32, y: u32, } #[derive(Component)] struct Health { current: u32, max: u32, } ``` **World Contract** Every Dojo game deploys a World contract that manages all state. Systems (your game logic) read and write through the World, keeping everything consistent. **Typed Clients** Dojo generates TypeScript and Unity clients from your contracts. No manual ABI parsing. State updates flow automatically from chain to client. ### Layer 2: Execution Games can't run on Starknet mainnet directly — transaction costs and latency would kill the experience. Instead, games run on dedicated execution layers. [Cartridge Slot](https://cartridge.gg) provides application-specific rollups for Dojo games: * Sub-second transaction finality * Negligible costs per action * Full Starknet compatibility * Proofs that settle back to L1 This means your game logic executes fast and cheap while inheriting Starknet's security guarantees. ### Layer 3: Indexing Onchain state is authoritative but slow to query. Torii, Dojo's indexer, maintains a synchronized database of your World state. Your client queries Torii for fast reads while writes go directly to the chain. When state changes onchain, Torii updates and pushes to connected clients via GraphQL subscriptions. ```typescript // Subscribe to position changes const positions = await client.getEntities({ model: "Position", where: { player_id: playerId } }); ``` ### Layer 4: Player Identity The final piece is onboarding. Players shouldn't need to understand gas, transactions, or seed phrases. [Cartridge Controller](https://cartridge.gg) handles this: * Email/social login that creates a Starknet account * Session keys for seamless gameplay (no popups per action) * Account abstraction for gas sponsorship * Cross-game identity and inventory From the player's perspective, they're just logging into a game. The blockchain complexity is invisible. ### Putting It Together Here's the full stack for a Dojo game: ``` ┌─────────────────────────────────────────┐ │ Game Client (React/Unity) │ ├─────────────────────────────────────────┤ │ Cartridge Controller (Identity + Wallet)│ ├─────────────────────────────────────────┤ │ Torii (Indexer + Subscriptions) │ ├─────────────────────────────────────────┤ │ Cartridge Slot (Execution Layer) │ ├─────────────────────────────────────────┤ │ Dojo World Contract (Game Logic) │ ├─────────────────────────────────────────┤ │ Starknet (Settlement) │ └─────────────────────────────────────────┘ ``` Each layer handles a specific concern. Developers focus on game design while the infrastructure handles the hard parts. ### Getting Started The fastest path to a running game: 1. **Install Dojo**: `curl -L https://install.dojoengine.org | bash` 2. **Scaffold a project**: `sozo init my-game` 3. **Deploy locally**: `katana` for a local devnet, then `sozo build && sozo migrate` 4. **Generate client**: Dojo outputs TypeScript bindings automatically From zero to playable prototype in an afternoon. ### What's Next The stack keeps improving. Faster provers mean quicker settlement. Better indexers mean richer queries. More client SDKs mean more platforms. Games shipping today on Dojo — like [Eternum](https://realms.world), [Loot Survivor](https://lootsurvivor.io), and others in the [Cartridge ecosystem](https://cartridge.gg) — are stress-testing this infrastructure and pushing it forward. The goal: make building onchain games as straightforward as building traditional ones, while preserving everything that makes them special. *** *Ready to build? Check out the [Dojo documentation](https://book.dojoengine.org), join the [Discord](https://discord.gg/dojoengine), or explore games on [Cartridge](https://cartridge.gg).* ## Building World Model Agents for Onchain Games Most game agents react. They see a state, they pick an action. It works—until it doesn't. When the game gets complex, when opponents adapt, when you need to think three moves ahead, reactive agents hit a wall. World model agents think differently. They build an internal model of how the game works, then *imagine* what happens if they take different actions. They simulate futures before committing to one. This is closer to how humans actually play games—we don't just react to the board, we picture what happens next. And here's the twist: onchain games are uniquely suited for this approach. The state is fully observable, transitions are deterministic, and historical data is permanently indexed. You don't have to guess how the game engine works—you can learn it from data that can't lie. This article is technical. We'll cover the theory, the architecture, and actual implementation patterns. By the end, you'll understand how to build agents that don't just play games—they understand them. ### What World Models Actually Are Let's be precise about terminology. A world model is a learned function that predicts future states given current state and actions. More formally: ``` s_{t+1} = f(s_t, a_t) ``` Where `s_t` is the state at time t, `a_t` is the action taken, and `f` is your world model. Simple in principle, complicated in practice. #### The Classic Frame: Ha & Schmidhuber's Architecture The 2018 "World Models" paper by Ha and Schmidhuber laid out an architecture that's become the standard reference point. It has three components: 1. **Vision Model (V)**: Compresses raw observations into a compact latent representation 2. **Memory Model (M)**: An RNN (they used MDN-RNN) that predicts future latent states 3. **Controller (C)**: A small neural network that selects actions given the latent state The key insight was training inside the "dream"—the controller can be trained entirely within the world model's imagination, without touching the real environment. This is massively more sample-efficient than reinforcement learning directly on the environment. #### Why Latent Spaces Matter You might wonder: why not just predict raw states directly? For visual games, that means predicting pixels. This works but it's wasteful. Most pixels in a game frame are irrelevant noise—background textures, UI elements, decorative particles. A latent space compresses the observation to what matters strategically. In the learned representation, a "dangerous enemy nearby" and "safe path ahead" might be just a few dimensions, not thousands of pixels. For onchain games, raw observations aren't pixels—they're structured data from Torii. But the principle still applies. A game might have hundreds of entity properties, but the strategically relevant state might be much smaller. Your world model learns that compression automatically. ```python # Raw game state from Torii: hundreds of fields raw_state = { "player": {"x": 45, "y": 120, "health": 80, "mana": 35, ...}, "enemies": [...], # Could be dozens "items": [...], "terrain": [...], "events": [...], # ... many more } # Latent representation: 64-dimensional vector # Captures: "medium health, near enemy cluster, low resources" latent = encoder(raw_state) # shape: (64,) ``` The encoder learns which aspects of `raw_state` actually affect what happens next. Everything else gets discarded. #### Determinism Changes Everything Here's where onchain games diverge from the scenarios most world model research tackles. Traditional game AI deals with: * Partially observable states (fog of war, hidden information) * Stochastic transitions (random enemy spawns, dice rolls) * Complex physics that are hard to model exactly Onchain games, by design, have: * **Fully observable state**: Everything is on-chain. You can query every player's position, every item's location, every variable in every contract. * **Deterministic transitions**: Given the same state and action, you get the same next state. Always. (Even "random" events use verifiable randomness with known seeds.) * **Perfect historical record**: Torii indexes every state change. You have complete ground truth for training. This means your world model can be *exact* for the game mechanics. Not a statistical approximation—an exact function. The only uncertainty is what other players will do. ```python # In a stochastic environment (traditional games): # Model must learn P(s_{t+1} | s_t, a_t) - a probability distribution # In a deterministic onchain game: # Model learns s_{t+1} = f(s_t, a_t) - a direct mapping # Uncertainty only in opponent actions ``` This changes the architecture. You don't need probabilistic world models with variance estimation. You need accurate state prediction, plus opponent modeling as a separate concern. ### The Architecture: Perception → World Model → Planning → Action Let's break down the complete pipeline for a world model agent on Dojo. #### 1. Perception: Torii → Structured State Your agent starts by querying Torii. Here's a practical Python setup: ```python import httpx from dataclasses import dataclass from typing import List, Optional import json TORII_URL = "https://api.cartridge.gg/x/your-game/torii/graphql" @dataclass class PlayerState: address: str position_x: int position_y: int health: int resources: dict inventory: List[str] @dataclass class GameState: tick: int players: List[PlayerState] world_entities: List[dict] async def fetch_game_state(client: httpx.AsyncClient) -> GameState: """Fetch complete game state from Torii.""" query = """ query GetWorld { playerModels(first: 100) { edges { node { address position_x position_y health resources inventory } } } worldModels(first: 1) { edges { node { current_tick } } } entityModels(first: 500) { edges { node { entity_id entity_type position_x position_y properties } } } } """ response = await client.post( TORII_URL, json={"query": query} ) data = response.json()["data"] players = [ PlayerState( address=edge["node"]["address"], position_x=edge["node"]["position_x"], position_y=edge["node"]["position_y"], health=edge["node"]["health"], resources=json.loads(edge["node"]["resources"]), inventory=json.loads(edge["node"]["inventory"]) ) for edge in data["playerModels"]["edges"] ] tick = data["worldModels"]["edges"][0]["node"]["current_tick"] entities = [edge["node"] for edge in data["entityModels"]["edges"]] return GameState(tick=tick, players=players, world_entities=entities) ``` Notice we're pulling *everything*—not just our own player. World models need to understand the complete game state to predict what happens when we act. #### 2. State Encoding: Structured Data → Latent Vector The world model operates on fixed-size vectors, not variable-length GraphQL responses. We need an encoder: ```python import torch import torch.nn as nn from typing import Tuple class GameStateEncoder(nn.Module): """Encode variable-size game state into fixed latent vector.""" def __init__( self, player_dim: int = 32, entity_dim: int = 16, latent_dim: int = 128, max_players: int = 50, max_entities: int = 200 ): super().__init__() self.max_players = max_players self.max_entities = max_entities # Player encoder self.player_encoder = nn.Sequential( nn.Linear(10, 64), # position, health, resources nn.ReLU(), nn.Linear(64, player_dim) ) # Entity encoder self.entity_encoder = nn.Sequential( nn.Linear(8, 32), nn.ReLU(), nn.Linear(32, entity_dim) ) # Attention pooling for variable-length sequences self.player_attention = nn.MultiheadAttention( embed_dim=player_dim, num_heads=4, batch_first=True ) self.entity_attention = nn.MultiheadAttention( embed_dim=entity_dim, num_heads=4, batch_first=True ) # Final projection self.project = nn.Sequential( nn.Linear(player_dim + entity_dim, 256), nn.ReLU(), nn.Linear(256, latent_dim) ) def forward( self, player_features: torch.Tensor, # (batch, n_players, 10) entity_features: torch.Tensor, # (batch, n_entities, 8) player_mask: torch.Tensor, entity_mask: torch.Tensor ) -> torch.Tensor: # Encode each player/entity players_encoded = self.player_encoder(player_features) entities_encoded = self.entity_encoder(entity_features) # Attention pooling with masks players_pooled, _ = self.player_attention( players_encoded, players_encoded, players_encoded, key_padding_mask=~player_mask ) entities_pooled, _ = self.entity_attention( entities_encoded, entities_encoded, entities_encoded, key_padding_mask=~entity_mask ) # Mean pool over sequence player_repr = (players_pooled * player_mask.unsqueeze(-1)).sum(1) player_repr = player_repr / player_mask.sum(1, keepdim=True).clamp(min=1) entity_repr = (entities_pooled * entity_mask.unsqueeze(-1)).sum(1) entity_repr = entity_repr / entity_mask.sum(1, keepdim=True).clamp(min=1) # Combine and project combined = torch.cat([player_repr, entity_repr], dim=-1) return self.project(combined) def preprocess_game_state(state: GameState, my_address: str) -> Tuple[torch.Tensor, torch.Tensor]: """Convert GameState to tensors for the encoder.""" # Player features: relative position to me, health, resources my_player = next(p for p in state.players if p.address == my_address) player_features = [] for p in state.players: features = [ p.position_x - my_player.position_x, # Relative x p.position_y - my_player.position_y, # Relative y p.health / 100.0, # Normalized health p.resources.get("wood", 0) / 1000.0, p.resources.get("stone", 0) / 1000.0, p.resources.get("gold", 0) / 1000.0, 1.0 if p.address == my_address else 0.0, # Is self len(p.inventory) / 10.0, # Inventory fullness # ... more features ] player_features.append(features) # Pad to fixed size while len(player_features) < 50: player_features.append([0.0] * 10) # Entity features: type, position, properties entity_features = [] for e in state.world_entities: features = [ e["position_x"] - my_player.position_x, e["position_y"] - my_player.position_y, hash(e["entity_type"]) % 100 / 100.0, # Type embedding # ... extract relevant properties ] entity_features.append(features) while len(entity_features) < 200: entity_features.append([0.0] * 8) return ( torch.tensor(player_features[:50], dtype=torch.float32), torch.tensor(entity_features[:200], dtype=torch.float32) ) ``` This encoder handles variable numbers of players and entities through attention pooling—a technique borrowed from transformer architectures. It's more sophisticated than simple concatenation but handles the variability gracefully. #### 3. The World Model: Predicting Future States Now the core: a model that predicts what happens when actions are taken. ```python class WorldModel(nn.Module): """ Deterministic world model for onchain games. Predicts next latent state given current state and action. """ def __init__( self, latent_dim: int = 128, action_dim: int = 16, # One-hot or embedded action hidden_dim: int = 256 ): super().__init__() # Action embedding self.action_embed = nn.Embedding(20, action_dim) # 20 possible actions # Transition model self.transition = nn.Sequential( nn.Linear(latent_dim + action_dim, hidden_dim), nn.LayerNorm(hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.LayerNorm(hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, latent_dim) ) # Reward predictor (auxiliary task) self.reward_head = nn.Sequential( nn.Linear(latent_dim, 64), nn.ReLU(), nn.Linear(64, 1) ) # Done predictor (game over / death) self.done_head = nn.Sequential( nn.Linear(latent_dim, 64), nn.ReLU(), nn.Linear(64, 1), nn.Sigmoid() ) def forward( self, state: torch.Tensor, # (batch, latent_dim) action: torch.Tensor # (batch,) action indices ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """Predict next state, reward, and done probability.""" action_emb = self.action_embed(action) combined = torch.cat([state, action_emb], dim=-1) next_state = self.transition(combined) reward = self.reward_head(next_state) done = self.done_head(next_state) return next_state, reward.squeeze(-1), done.squeeze(-1) def rollout( self, initial_state: torch.Tensor, action_sequence: torch.Tensor, # (batch, horizon) ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """ Simulate a sequence of actions. Returns trajectory of states, rewards, and done flags. """ batch_size, horizon = action_sequence.shape states = [initial_state] rewards = [] dones = [] current_state = initial_state for t in range(horizon): next_state, reward, done = self(current_state, action_sequence[:, t]) states.append(next_state) rewards.append(reward) dones.append(done) current_state = next_state return ( torch.stack(states, dim=1), # (batch, horizon+1, latent_dim) torch.stack(rewards, dim=1), # (batch, horizon) torch.stack(dones, dim=1) # (batch, horizon) ) ``` The `rollout` method is where the magic happens. Given an initial state and a sequence of planned actions, the model imagines the entire trajectory. This lets us evaluate plans before executing them. #### 4. Training: Learning from Torii History Here's the beautiful part of onchain games: you have perfect training data. Torii stores every state transition that ever happened. ```python async def fetch_historical_transitions( client: httpx.AsyncClient, limit: int = 10000 ) -> List[dict]: """ Fetch historical state transitions from Torii. Each transition is a (state, action, next_state, reward) tuple. """ query = """ query GetHistory($limit: Int!) { eventMessagesHistorical( first: $limit, orderBy: { field: TIMESTAMP, direction: DESC } ) { edges { node { keys data transactionHash createdAt } } } } """ response = await client.post( TORII_URL, json={"query": query, "variables": {"limit": limit}} ) events = response.json()["data"]["eventMessagesHistorical"]["edges"] # Parse events into transitions transitions = [] for event in events: parsed = parse_game_event(event["node"]) if parsed: transitions.append(parsed) return transitions def parse_game_event(event: dict) -> Optional[dict]: """ Parse raw event into a structured transition. This is game-specific—adapt to your game's events. """ keys = event["keys"] data = event["data"] # Example: "PlayerMoved" event if "PlayerMoved" in keys[0]: return { "event_type": "move", "player": keys[1], "from_x": int(data[0], 16), "from_y": int(data[1], 16), "to_x": int(data[2], 16), "to_y": int(data[3], 16), "timestamp": event["createdAt"] } # Example: "ResourceHarvested" event if "ResourceHarvested" in keys[0]: return { "event_type": "harvest", "player": keys[1], "resource": data[0], "amount": int(data[1], 16), "timestamp": event["createdAt"] } return None class TransitionDataset(torch.utils.data.Dataset): """Dataset of (state, action, next_state, reward) tuples.""" def __init__(self, transitions: List[dict], encoder: GameStateEncoder): self.transitions = transitions self.encoder = encoder self.data = self._process_transitions() def _process_transitions(self): # Group events by timestamp to reconstruct state snapshots # This is the tricky part—you need to reconstruct # what the full game state was at each transition processed = [] for i, transition in enumerate(self.transitions[:-1]): # Get state before and after state_before = self._reconstruct_state(i) state_after = self._reconstruct_state(i + 1) action = self._extract_action(transition) reward = self._compute_reward(transition) processed.append({ "state": state_before, "action": action, "next_state": state_after, "reward": reward }) return processed def __len__(self): return len(self.data) def __getitem__(self, idx): item = self.data[idx] return { "state": torch.tensor(item["state"], dtype=torch.float32), "action": torch.tensor(item["action"], dtype=torch.long), "next_state": torch.tensor(item["next_state"], dtype=torch.float32), "reward": torch.tensor(item["reward"], dtype=torch.float32) } def train_world_model( model: WorldModel, dataset: TransitionDataset, epochs: int = 100, batch_size: int = 64, lr: float = 1e-4 ): """Train the world model on historical transitions.""" dataloader = torch.utils.data.DataLoader( dataset, batch_size=batch_size, shuffle=True ) optimizer = torch.optim.AdamW(model.parameters(), lr=lr) state_loss_fn = nn.MSELoss() reward_loss_fn = nn.MSELoss() for epoch in range(epochs): total_loss = 0.0 for batch in dataloader: optimizer.zero_grad() pred_next, pred_reward, pred_done = model( batch["state"], batch["action"] ) # State prediction loss state_loss = state_loss_fn(pred_next, batch["next_state"]) # Reward prediction loss reward_loss = reward_loss_fn(pred_reward, batch["reward"]) # Combined loss loss = state_loss + 0.1 * reward_loss loss.backward() optimizer.step() total_loss += loss.item() if epoch % 10 == 0: avg_loss = total_loss / len(dataloader) print(f"Epoch {epoch}: Loss = {avg_loss:.4f}") ``` The training loop is standard supervised learning. The key insight is that *Torii gives you perfect supervision*. Every state transition that occurred in the game's history is recorded and queryable. You're not estimating—you're learning from ground truth. #### 5. Planning: Imagining Futures With a trained world model, your agent can simulate futures before acting. The simplest approach is random shooting: ```python class PlanningAgent: """ Agent that uses world model for planning. Evaluates many action sequences and picks the best. """ def __init__( self, encoder: GameStateEncoder, world_model: WorldModel, n_candidates: int = 256, horizon: int = 10 ): self.encoder = encoder self.world_model = world_model self.n_candidates = n_candidates self.horizon = horizon self.n_actions = 20 # Number of possible actions def plan(self, current_state: torch.Tensor) -> int: """ Find the best action by simulating many possible futures. Returns the first action of the best trajectory. """ # Generate random action sequences action_sequences = torch.randint( 0, self.n_actions, (self.n_candidates, self.horizon) ) # Expand state for all candidates states = current_state.unsqueeze(0).expand(self.n_candidates, -1) # Simulate all candidates with torch.no_grad(): _, rewards, dones = self.world_model.rollout( states, action_sequences ) # Compute trajectory values (sum of discounted rewards) discount = 0.99 discounts = discount ** torch.arange(self.horizon) # Mask rewards after done done_mask = torch.cumprod(1 - dones, dim=1) masked_rewards = rewards * done_mask trajectory_values = (masked_rewards * discounts).sum(dim=1) # Pick best trajectory best_idx = trajectory_values.argmax() best_action = action_sequences[best_idx, 0].item() return best_action async def act(self, game_state: GameState, my_address: str) -> int: """Full pipeline: observe → encode → plan → return action.""" # Encode current state player_features, entity_features = preprocess_game_state( game_state, my_address ) latent = self.encoder( player_features.unsqueeze(0), entity_features.unsqueeze(0), torch.ones(1, 50, dtype=torch.bool), torch.ones(1, 200, dtype=torch.bool) ) # Plan best action action = self.plan(latent.squeeze(0)) return action ``` Random shooting works surprisingly well for short horizons. For longer planning, you'd want something smarter: * **Cross-Entropy Method (CEM)**: Iteratively refine the action distribution toward higher rewards * **Model Predictive Control (MPC)**: Re-plan every step as new observations arrive * **Monte Carlo Tree Search (MCTS)**: Build a search tree, especially powerful for discrete action games ```python def cem_planning( world_model: WorldModel, initial_state: torch.Tensor, n_iterations: int = 5, n_candidates: int = 256, n_elite: int = 32, horizon: int = 10, n_actions: int = 20 ) -> int: """ Cross-Entropy Method planning. Iteratively improves action distribution toward high-reward sequences. """ # Initialize uniform action distribution action_means = torch.ones(horizon, n_actions) / n_actions for iteration in range(n_iterations): # Sample action sequences from current distribution action_probs = action_means.unsqueeze(0).expand(n_candidates, -1, -1) action_sequences = torch.multinomial( action_probs.reshape(-1, n_actions), num_samples=1 ).reshape(n_candidates, horizon) # Evaluate all sequences states = initial_state.unsqueeze(0).expand(n_candidates, -1) with torch.no_grad(): _, rewards, dones = world_model.rollout(states, action_sequences) # Compute trajectory values discount = 0.99 discounts = discount ** torch.arange(horizon) done_mask = torch.cumprod(1 - dones, dim=1) values = (rewards * done_mask * discounts).sum(dim=1) # Select elite trajectories elite_indices = values.topk(n_elite).indices elite_sequences = action_sequences[elite_indices] # Update action distribution toward elite actions for t in range(horizon): counts = torch.zeros(n_actions) for seq in elite_sequences: counts[seq[t]] += 1 action_means[t] = counts / n_elite # Add small uniform noise to prevent collapse action_means = 0.9 * action_means + 0.1 / n_actions # Return most likely first action return action_means[0].argmax().item() ``` CEM typically finds better plans than random shooting because it focuses sampling on promising regions of action space. #### 6. Action Execution: Controller CLI Once planning selects an action, execute it via Controller: ```python import subprocess import json ACTION_MAPPING = { 0: {"entrypoint": "move", "calldata_fn": lambda: ["1", "0"]}, # Move right 1: {"entrypoint": "move", "calldata_fn": lambda: ["-1", "0"]}, # Move left 2: {"entrypoint": "move", "calldata_fn": lambda: ["0", "1"]}, # Move up 3: {"entrypoint": "move", "calldata_fn": lambda: ["0", "-1"]}, # Move down 4: {"entrypoint": "harvest", "calldata_fn": lambda: []}, 5: {"entrypoint": "attack", "calldata_fn": lambda: []}, # ... more actions } GAME_CONTRACT = "0x..." # Your game's contract address RPC_URL = "https://api.cartridge.gg/x/starknet/sepolia" def execute_action(action_id: int) -> dict: """Execute a planned action via Controller CLI.""" if action_id not in ACTION_MAPPING: raise ValueError(f"Unknown action: {action_id}") action = ACTION_MAPPING[action_id] calldata = action["calldata_fn"]() cmd = [ "controller", "execute", "--contract", GAME_CONTRACT, "--entrypoint", action["entrypoint"], "--calldata", ",".join(calldata) if calldata else "", "--rpc-url", RPC_URL, "--json" ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"Execution failed: {result.stderr}") return json.loads(result.stdout) ``` ### Handling Opponents: The Multi-Agent Problem So far we've modeled the world as if our agent acts alone. Real games have opponents. Their actions affect our state too. There are several approaches: #### 1. Opponent Modeling as Distribution Treat opponent actions as a learned distribution: ```python class OpponentModel(nn.Module): """Predict probability distribution over opponent's next action.""" def __init__(self, latent_dim: int, n_actions: int): super().__init__() self.net = nn.Sequential( nn.Linear(latent_dim, 128), nn.ReLU(), nn.Linear(128, n_actions), nn.Softmax(dim=-1) ) def forward(self, state: torch.Tensor) -> torch.Tensor: return self.net(state) ``` Train this on historical opponent behavior from Torii. Then sample from it during planning: ```python def plan_with_opponents( world_model: WorldModel, opponent_model: OpponentModel, initial_state: torch.Tensor, n_candidates: int = 256, n_opponent_samples: int = 8 ) -> int: """Plan considering likely opponent responses.""" # For each candidate action sequence, simulate multiple # possible opponent behaviors my_actions = torch.randint(0, 20, (n_candidates, 10)) total_values = torch.zeros(n_candidates) for _ in range(n_opponent_samples): states = initial_state.unsqueeze(0).expand(n_candidates, -1) trajectory_reward = torch.zeros(n_candidates) for t in range(10): # Predict opponent action distribution opp_probs = opponent_model(states) opp_actions = torch.multinomial(opp_probs, 1).squeeze() # Combined state transition (my action + opponent action) # This requires a world model that handles multi-agent transitions next_states, rewards, _ = world_model.step_multi_agent( states, my_actions[:, t], opp_actions ) trajectory_reward += rewards * (0.99 ** t) states = next_states total_values += trajectory_reward # Average across opponent samples total_values /= n_opponent_samples return my_actions[total_values.argmax(), 0].item() ``` #### 2. Best-Response Planning Assume opponents play optimally against you. This is conservative—it prevents strategies that only work against weak opponents: ```python def minimax_planning(world_model, state, depth=3): """ Minimax search: assume opponent plays optimally. Good for zero-sum competitive games. """ def evaluate(state, is_my_turn, d): if d == 0: return reward_head(state).item(), None if is_my_turn: best_value = -float('inf') best_action = None for action in range(n_actions): next_state = world_model(state, action) value, _ = evaluate(next_state, False, d-1) if value > best_value: best_value = value best_action = action return best_value, best_action else: # Opponent's turn: assume they minimize our value worst_value = float('inf') for action in range(n_actions): next_state = world_model(state, action) value, _ = evaluate(next_state, True, d-1) worst_value = min(worst_value, value) return worst_value, None _, best_action = evaluate(state, True, depth) return best_action ``` #### 3. Population-Based Opponent Modeling Train your world model against a diverse population of agent policies. This creates robust behavior: ```python # Train world model that's seen many opponent types opponent_policies = load_diverse_policies() # Historical winners, rule-based, etc. for epoch in range(epochs): # Sample opponent from population opponent = random.choice(opponent_policies) # Generate rollouts against this opponent transitions = generate_rollouts(world_model, opponent) # Train world model on these transitions train_step(world_model, transitions) ``` ### Challenges and Solutions #### Compute Constraints World models need GPU compute for training and inference. For onchain games, you're running this off-chain—the model lives on your server, not on Starknet. **Solutions:** * Train offline on historical data (cheap, one-time cost) * Use small models (64-128 dim latent spaces work fine for most games) * Cache planning results for similar states * Run inference on CPU for simple games (it's fast enough) #### Partial Observability Some onchain games have fog of war or hidden information (e.g., opponent's hand in a card game). **Solutions:** * Learn a belief state that represents uncertainty * Use recurrent models that accumulate information over time * Monte Carlo rollouts over possible hidden states ```python class BeliefWorldModel(nn.Module): """World model that maintains uncertainty over hidden state.""" def __init__(self, obs_dim, hidden_dim, latent_dim): super().__init__() # GRU to accumulate observations into belief self.belief_rnn = nn.GRU(obs_dim, hidden_dim, batch_first=True) # Standard world model on belief state self.transition = WorldModel(hidden_dim, latent_dim) def forward(self, observation_sequence, actions): # Build belief from observation history belief, _ = self.belief_rnn(observation_sequence) belief_current = belief[:, -1] # Final belief state # Predict from belief return self.transition(belief_current, actions) ``` #### Distribution Shift Your world model was trained on historical data. But as you deploy better agents, the game dynamics change—other players adapt, meta shifts. Your model becomes stale. **Solutions:** * Continual learning: fine-tune on recent data periodically * Ensemble models: train multiple world models, use disagreement as uncertainty * Online learning: update the model after every episode ```python def update_world_model_online(model, buffer, batch_size=32): """Lightweight online update from recent experience.""" if len(buffer) < batch_size: return batch = random.sample(buffer, batch_size) # Quick gradient update optimizer.zero_grad() loss = compute_loss(model, batch) loss.backward() optimizer.step() # Keep buffer bounded while len(buffer) > 10000: buffer.pop(0) ``` ### The Full Pipeline Let's put it all together into a complete agent: ```python import asyncio import httpx async def run_world_model_agent( my_address: str, game_contract: str, torii_url: str, model_path: str ): """Complete world model agent loop.""" # Load trained models encoder = GameStateEncoder() world_model = WorldModel() encoder.load_state_dict(torch.load(f"{model_path}/encoder.pt")) world_model.load_state_dict(torch.load(f"{model_path}/world_model.pt")) encoder.eval() world_model.eval() # Initialize planner agent = PlanningAgent(encoder, world_model) # Experience buffer for online learning experience_buffer = [] async with httpx.AsyncClient() as client: while True: # 1. Observe game_state = await fetch_game_state(client) # 2. Encode & Plan action = await agent.act(game_state, my_address) # 3. Execute print(f"Executing action {action}") result = execute_action(action) print(f"TX: {result.get('transaction_hash')}") # 4. Wait for state update await asyncio.sleep(5) # 5. Observe new state new_state = await fetch_game_state(client) # 6. Store experience for online learning experience_buffer.append({ "state": game_state, "action": action, "next_state": new_state, "reward": compute_reward(game_state, new_state) }) # 7. Periodic online update if len(experience_buffer) % 100 == 0: update_world_model_online(world_model, experience_buffer) # Loop delay await asyncio.sleep(30) if __name__ == "__main__": asyncio.run(run_world_model_agent( my_address="0x...", game_contract="0x...", torii_url="https://api.cartridge.gg/x/your-game/torii/graphql", model_path="./trained_models" )) ``` ### Looking Forward: Shared Infrastructure Today, every developer trains their own world model for their own game. But there's an interesting future: shared world models. Imagine a foundation model trained on every Dojo game's transition data. It learns the grammar of onchain games—how entities move, how resources flow, how combat resolves. Fine-tuning this foundation for a specific game would be much cheaper than training from scratch. The data is already there. Torii indexes it all. The model architectures exist. Someone just needs to build it. Even more speculative: decentralized world model training. Multiple agents contribute training compute and share the resulting model. The model itself could live on-chain (well, the weights could be committed to IPFS with hashes on-chain). True AI infrastructure for autonomous worlds. We're not there yet. But the pieces are falling into place. ### Conclusion World model agents represent a shift from reactive to predictive game AI. Instead of learning a policy directly (state → action), they learn a simulator (state + action → next state) and plan within that simulation. For onchain games built on Dojo: 1. **Torii gives you perfect training data**—every transition ever, with ground truth labels 2. **Deterministic game logic** means your world model can be exact, not probabilistic 3. **Controller CLI** provides secure, scoped execution for agent actions 4. **The full pipeline**—observe, encode, simulate, plan, act—runs off-chain while interacting with on-chain state Start with the basics: a simple encoder, a feedforward world model, random shooting for planning. Get that working. Then add complexity—better architectures, smarter planning, opponent modeling. The code in this article is a foundation, not a finished product. The architecture scales from simple experiments to sophisticated autonomous systems. Where you take it depends on how deeply you want to understand the game you're playing. *** *This article is part of the Agent-Native Games series. Previous: [Building Your First Dojo Game Agent](#TODO)* **Resources:** * [World Models paper (Ha & Schmidhuber, 2018)](https://worldmodels.github.io/) * [Dreamer (Hafner et al.)](https://danijar.com/project/dreamer/) * [Torii Documentation](https://book.dojoengine.org/toolchain/torii) * [Controller CLI](https://github.com/cartridge-gg/controller-cli) * [Dojo Book](https://book.dojoengine.org/) ## Building Your First Dojo Game Agent You've read about why onchain games are perfect for AI agents. The theory makes sense—verifiable state, permissionless execution, real stakes. But how do you actually *build* one? That's what we're doing today. No hand-waving, no "exercise left to the reader" cop-outs. By the end of this tutorial, you'll have a working game agent that can observe game state, make decisions, and execute actions on a Dojo-powered game. The whole loop. We'll build something simple: an agent for a hypothetical resource management game. Think Settlers of Catan meets automation. Your agent will monitor resources, spot opportunities, and act on them—all without you clicking a single button. Let's get into it. ### What You'll Need Before we start coding, make sure you've got: * **Node.js 18+** — We're writing the agent in JavaScript/TypeScript. You could use Python or Rust, but JS has the lowest friction for a tutorial. * **Controller CLI** — Cartridge's tool for programmatic Starknet transactions. This is how your agent gets hands. * **Basic Starknet knowledge** — You should know what a transaction is, what felt252 means, and roughly how smart contracts work. If terms like "calldata" make you nervous, spend 30 minutes with the Starknet docs first. * **A Dojo game to target** — We'll use examples from a generic game, but you'll want to adapt this to a real deployed game. Eternum is a great candidate once you're comfortable. Got all that? Good. Let's build. ### Step 1: Understanding the Game World Contract Before your agent can play a game, it needs to understand the game. This isn't like screen-scraping a traditional game where you're guessing at internal state from pixels. Onchain games give you *everything*. #### The World Contract Every Dojo game has a World contract—the single source of truth for all game state. Models (think of them as tables) store entities and their components. Events emit whenever something interesting happens. For our example game, imagine these models exist: ```cairo #[derive(Model, Copy, Drop, Serde)] struct Player { #[key] player_id: ContractAddress, wood: u32, stone: u32, gold: u32, last_harvest: u64, } #[derive(Model, Copy, Drop, Serde)] struct ResourceNode { #[key] node_id: u32, resource_type: ResourceType, amount: u32, position_x: u32, position_y: u32, } ``` Your agent needs to read this data. How? Not by calling the contract directly (that's slow and limited). Instead, we use Torii. #### Torii: Your Window into the Game Torii is Dojo's indexer. It watches the blockchain, processes events, and serves game state through a GraphQL API. Think of it as a read-optimized view of the World contract. Every Dojo game deployment includes a Torii instance. You'll typically find it at a URL like: ``` https://api.cartridge.gg/x/your-game/torii ``` Let's explore what's available. Open GraphQL Playground (usually at `/graphql` on the Torii URL) and run: ```graphql { __schema { types { name } } } ``` This shows you all the queryable types—essentially, all the models your target game has defined. For our resource game, we'd see `Player`, `ResourceNode`, and whatever else the devs created. #### Reading Your Player State Here's a practical query to get a player's resources: ```graphql query GetPlayer($playerId: String!) { playerModels(where: { player_id: $playerId }) { edges { node { player_id wood stone gold last_harvest } } } } ``` Run this with your wallet address as `playerId` and you'll see exactly what the blockchain knows about your position in the game. No API keys. No rate limits (usually). Just direct access to truth. This is the first superpower of onchain game agents: perfect information. Your agent sees the same state the game sees, because they're reading from the same source. ### Step 2: Setting Up Controller Reading state is only half the equation. Your agent also needs to *act*—submit transactions that change game state. That's where Controller CLI comes in. #### Why Controller? Normally, Starknet transactions require a wallet signature. Every. Single. Time. That's fine for humans clicking buttons, but disastrous for an agent that might execute hundreds of actions per hour. Controller solves this with session keys. You authorize a temporary keypair to sign transactions on your behalf, scoped to specific contracts and methods. Even if someone steals the session key, the damage is limited—they can only call what you authorized, and the session expires. #### Installing Controller CLI ```bash curl -fsSL https://raw.githubusercontent.com/cartridge-gg/controller-cli/main/install.sh | bash ``` Add it to your PATH if needed: ```bash export PATH="$PATH:$HOME/.local/bin" ``` Verify it works: ```bash controller --version ``` #### Generating Your Keypair First, create the session keypair that'll live on your agent's machine: ```bash controller generate --json ``` You'll get something like: ```json { "public_key": "0x1234567890abcdef...", "stored_at": "~/.config/controller-cli", "message": "Keypair generated successfully." } ``` Keep that public key handy—you'll need it for registration. The private key is stored locally. Even if someone got it, they couldn't do much without an authorized session. But still, treat it like a credential. #### Creating Your Session Policy The policy defines what your agent can do. This is the most important security boundary. Be specific. Create `policy.json`: ```json { "contracts": { "0x": { "name": "ResourceGame", "methods": [ { "name": "harvest", "entrypoint": "harvest", "description": "Harvest resources from a node" }, { "name": "build", "entrypoint": "build", "description": "Build structures" }, { "name": "trade", "entrypoint": "trade", "description": "Trade resources with other players" } ] } } } ``` Notice what's *not* here: `transfer`, `withdraw`, or anything that moves tokens out of the game. Your agent can play the game but can't drain your wallet. That's intentional. #### Registering the Session This is the one step that requires human involvement. By design. ```bash controller register \ --file policy.json \ --rpc-url https://api.cartridge.gg/x/starknet/sepolia \ --json ``` You'll get a URL: ```json { "authorization_url": "https://x.cartridge.gg/session?public_key=0x...", "short_url": "https://api.cartridge.gg/s/abc123", "message": "Open this URL in your browser to authorize..." } ``` Open that URL in your browser. Review the permissions (they should match your policy). Approve with your wallet. The CLI waits for confirmation, then stores the active session. Run `controller status --json` to verify: ```json { "status": "active", "session": { "address": "0x", "chain_id": "SN_SEPOLIA", "expires_at": 1735689600, "expires_in_seconds": 604800 } } ``` You're authorized. Your agent now has hands. ### Step 3: Querying State via Torii Let's write real code. We'll create a function that fetches game state from Torii. ```javascript // agent.js const TORII_URL = 'https://api.cartridge.gg/x/resource-game/torii/graphql'; async function queryTorii(query, variables = {}) { const response = await fetch(TORII_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }), }); const result = await response.json(); if (result.errors) { throw new Error(`Torii query failed: ${JSON.stringify(result.errors)}`); } return result.data; } async function getPlayerState(playerId) { const query = ` query GetPlayer($playerId: String!) { playerModels(where: { player_id: $playerId }) { edges { node { player_id wood stone gold last_harvest } } } } `; const data = await queryTorii(query, { playerId }); const edges = data.playerModels?.edges || []; return edges[0]?.node || null; } async function getResourceNodes() { const query = ` query GetNodes { resourceNodeModels(first: 100) { edges { node { node_id resource_type amount position_x position_y } } } } `; const data = await queryTorii(query); return data.resourceNodeModels?.edges.map(e => e.node) || []; } ``` Simple, right? GraphQL makes this feel almost like querying a regular database. But remember—this "database" is cryptographically verifiable and can't lie to you. #### Subscribing to Events For more sophisticated agents, you might want real-time updates instead of polling. Torii supports GraphQL subscriptions: ```javascript // Using graphql-ws (simplified example) import { createClient } from 'graphql-ws'; const client = createClient({ url: 'wss://api.cartridge.gg/x/resource-game/torii/graphql', }); function subscribeToEvents(callback) { return client.subscribe( { query: ` subscription { eventEmitted { keys data transactionHash } } `, }, { next: (data) => callback(data.data.eventEmitted), error: console.error, complete: () => console.log('Subscription closed'), } ); } ``` For this tutorial, we'll stick with polling. It's simpler and good enough for most use cases. ### Step 4: Making Decisions Here's where your agent gets a brain. Given the game state, what should it do? #### Rule-Based Logic (Start Here) Don't overcomplicate it. Start with simple rules: ```javascript function decideAction(playerState, resourceNodes) { // Rule 1: If we have few resources, harvest if (playerState.wood < 50 || playerState.stone < 50) { const bestNode = findBestHarvestNode(playerState, resourceNodes); if (bestNode) { return { type: 'harvest', nodeId: bestNode.node_id, }; } } // Rule 2: If we have enough resources, build if (playerState.wood >= 100 && playerState.stone >= 100) { return { type: 'build', structure: 'workshop', }; } // Rule 3: If we have excess gold, trade for what we need if (playerState.gold > 500 && playerState.wood < 50) { return { type: 'trade', give: { gold: 100 }, receive: { wood: 50 }, }; } // No action needed return null; } function findBestHarvestNode(playerState, nodes) { // Find the node with the most resources // In a real game, you'd factor in distance, competition, etc. return nodes .filter(n => n.amount > 0) .sort((a, b) => b.amount - a.amount)[0]; } ``` These rules are dumb. They don't consider timing, other players, or strategy. But they *work*. You can watch your agent play and improve the rules based on what you observe. #### LLM-Powered Decisions (Advanced) Want your agent to think more like a human? You can use an LLM: ```javascript import Anthropic from '@anthropic-ai/sdk'; const anthropic = new Anthropic(); async function decideLLM(playerState, resourceNodes, recentEvents) { const gameContext = ` You're playing a resource management game. Your current state: - Wood: ${playerState.wood} - Stone: ${playerState.stone} - Gold: ${playerState.gold} - Last harvest: ${playerState.last_harvest} Available resource nodes: ${JSON.stringify(resourceNodes, null, 2)} Recent game events: ${JSON.stringify(recentEvents, null, 2)} What action should you take? Respond with JSON: { "action": "harvest" | "build" | "trade" | "wait", "params": { ... }, "reasoning": "brief explanation" } `; const response = await anthropic.messages.create({ model: 'claude-sonnet-4-20250514', max_tokens: 500, messages: [{ role: 'user', content: gameContext }], }); return JSON.parse(response.content[0].text); } ``` LLMs shine when the optimal action depends on context that's hard to encode in rules. But they're slower, cost money per request, and can make bizarre mistakes. Start with rules, add LLM reasoning when you hit limits. A hybrid approach often works best: ```javascript async function decideHybrid(playerState, resourceNodes, recentEvents) { // Try rule-based first (fast, free, predictable) const rulesAction = decideAction(playerState, resourceNodes); // If rules have a clear answer, use it if (rulesAction && rulesAction.confidence > 0.8) { return rulesAction; } // For ambiguous situations, ask the LLM if (shouldConsultLLM(playerState, recentEvents)) { return await decideLLM(playerState, resourceNodes, recentEvents); } return rulesAction; } function shouldConsultLLM(playerState, recentEvents) { // Consult LLM for complex scenarios: // - Multiple viable options // - Recent threatening player activity // - Resource levels are moderate (not obviously low or high) const hasThreats = recentEvents.some(e => e.type === 'attack_nearby'); const resourcesAmbiguous = playerState.wood > 30 && playerState.wood < 150; return hasThreats || resourcesAmbiguous; } ``` This gives you the best of both worlds: fast execution for obvious decisions, nuanced reasoning when it matters. ### Step 5: Executing Actions Decision made. Now your agent needs to do something about it. #### Building Calldata Each action becomes a transaction. The game's contract defines what calldata it expects: ```javascript function buildCalldata(action) { switch (action.type) { case 'harvest': // harvest(node_id: u32) return { contract: GAME_CONTRACT, entrypoint: 'harvest', calldata: [action.nodeId.toString()], }; case 'build': // build(structure_type: felt252) const structureTypes = { workshop: '0x1', warehouse: '0x2', barracks: '0x3', }; return { contract: GAME_CONTRACT, entrypoint: 'build', calldata: [structureTypes[action.structure]], }; case 'trade': // trade(give_type: felt, give_amount: u32, receive_type: felt, receive_amount: u32) return { contract: GAME_CONTRACT, entrypoint: 'trade', calldata: [ resourceToFelt(Object.keys(action.give)[0]), Object.values(action.give)[0].toString(), resourceToFelt(Object.keys(action.receive)[0]), Object.values(action.receive)[0].toString(), ], }; default: return null; } } function resourceToFelt(resource) { const mapping = { wood: '0x1', stone: '0x2', gold: '0x3' }; return mapping[resource]; } ``` The exact format depends on the game's contract. Read the source (it's public!) or check their docs. #### Executing via Controller Now we call Controller CLI to submit the transaction: ```javascript import { execSync } from 'child_process'; function executeAction(calldata) { if (!calldata) return null; const { contract, entrypoint, calldata: args } = calldata; const calldataStr = args.join(','); try { const result = execSync( `controller execute ${contract} ${entrypoint} ${calldataStr} \ --rpc-url https://api.cartridge.gg/x/starknet/sepolia \ --json`, { encoding: 'utf-8' } ); const parsed = JSON.parse(result); console.log(`Transaction submitted: ${parsed.transaction_hash}`); console.log(`View on Voyager: https://sepolia.voyager.online/tx/${parsed.transaction_hash}`); return parsed; } catch (error) { console.error('Execution failed:', error.message); throw error; } } ``` #### Handling Failures Transactions can fail for many reasons. Handle them gracefully: ```javascript async function executeWithRetry(calldata, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const result = executeAction(calldata); // Check for known error codes if (result.status === 'error') { const { error_code, recovery_hint } = result; if (error_code === 'SessionExpired') { console.log('Session expired. Please re-register.'); // In practice, you might notify yourself to re-auth process.exit(1); } if (error_code === 'ManualExecutionRequired') { console.log('Action not authorized in session policy'); return null; } console.log(`Error: ${error_code}. Hint: ${recovery_hint}`); } return result; } catch (error) { console.log(`Attempt ${attempt} failed: ${error.message}`); if (attempt < maxRetries) { await sleep(1000 * attempt); // Exponential backoff } } } console.error('All retries exhausted'); return null; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } ``` The recovery hints from Controller are genuinely useful. Don't ignore them. ### Step 6: The Game Loop Time to put it all together. The classic agent loop: observe, decide, act, repeat. ```javascript const PLAYER_ADDRESS = '0x'; const POLL_INTERVAL = 30000; // 30 seconds async function runAgent() { console.log('Starting game agent...'); console.log(`Player: ${PLAYER_ADDRESS}`); console.log(`Polling every ${POLL_INTERVAL / 1000}s`); // Check session is active const status = JSON.parse( execSync('controller status --json', { encoding: 'utf-8' }) ); if (status.status !== 'active') { console.error('No active session. Run: controller register --file policy.json ...'); process.exit(1); } console.log(`Session expires: ${status.session.expires_at_formatted}`); console.log('---'); while (true) { try { // 1. OBSERVE: Get current game state const playerState = await getPlayerState(PLAYER_ADDRESS); const resourceNodes = await getResourceNodes(); if (!playerState) { console.log('Player not found in game. Have you joined?'); await sleep(POLL_INTERVAL); continue; } console.log(`[${new Date().toISOString()}] State:`, { wood: playerState.wood, stone: playerState.stone, gold: playerState.gold, }); // 2. DECIDE: What should we do? const action = decideAction(playerState, resourceNodes); if (!action) { console.log('No action needed.'); await sleep(POLL_INTERVAL); continue; } console.log('Decided action:', action); // 3. ACT: Execute the decision const calldata = buildCalldata(action); const result = await executeWithRetry(calldata); if (result?.transaction_hash) { console.log('Action executed successfully!'); // Wait a bit longer after acting to let state update await sleep(5000); } } catch (error) { console.error('Loop error:', error.message); } await sleep(POLL_INTERVAL); } } // Entry point runAgent().catch(console.error); ``` Run it: ```bash node agent.js ``` And watch your agent play. It'll log what it sees, what it decides, and what it does. When something looks wrong, stop it, tweak the rules, and restart. #### Making It Robust The loop above works, but it's fragile. Here's how to make it production-ready: **Graceful shutdown**: Handle signals properly so you don't corrupt state mid-action. ```javascript let running = true; process.on('SIGINT', () => { console.log('\nShutting down gracefully...'); running = false; }); // In your loop: while (running) { // ... your logic } ``` **Session monitoring**: Don't wait for your session to expire mid-execution. Check proactively: ```javascript function checkSessionHealth() { const status = JSON.parse( execSync('controller status --json', { encoding: 'utf-8' }) ); // Warn if less than 24 hours remaining if (status.session?.expires_in_seconds < 86400) { console.warn('⚠️ Session expires in <24h. Consider re-registering.'); } // Stop if less than 1 hour remaining if (status.session?.expires_in_seconds < 3600) { console.error('Session nearly expired. Stopping agent.'); return false; } return status.status === 'active'; } ``` **Rate limiting**: Blockchains have throughput limits, and games often have cooldowns. Respect both: ```javascript const actionCooldowns = new Map(); function canExecute(actionType) { const lastExecution = actionCooldowns.get(actionType) || 0; const cooldownMs = 60000; // 1 minute between same actions if (Date.now() - lastExecution < cooldownMs) { return false; } actionCooldowns.set(actionType, Date.now()); return true; } ``` **Logging and metrics**: You'll want to know what your agent did while you were asleep: ```javascript const fs = require('fs'); function logAction(action, result) { const entry = { timestamp: new Date().toISOString(), action, success: !!result?.transaction_hash, txHash: result?.transaction_hash, }; fs.appendFileSync( 'agent-log.jsonl', JSON.stringify(entry) + '\n' ); } ``` **Multiple agents**: Some strategies work better with coordination. But be careful—this can get complex fast. Start with one agent that works reliably before scaling up. ### Debugging Your Agent Things will go wrong. Here's how to figure out what: **State mismatch**: Your agent thinks it should harvest, but the transaction fails. Check if someone else harvested the node first. Onchain games are competitive—state changes between your read and your write. ```javascript // Before executing, verify the opportunity still exists async function verifyOpportunity(action, currentNodes) { if (action.type === 'harvest') { const node = currentNodes.find(n => n.node_id === action.nodeId); if (!node || node.amount === 0) { console.log('Node depleted since we last checked. Skipping.'); return false; } } return true; } ``` **Session issues**: If you see `ManualExecutionRequired`, your session doesn't authorize the action you're trying to take. Either update your policy or your decision logic is generating invalid actions. **Transaction debugging**: Use Voyager to inspect failed transactions. The error message is usually in the execution trace. Common issues: * Wrong calldata format (check argument types) * Insufficient resources (your agent tried to spend what it doesn't have) * Game-specific validation (cooldowns, turn requirements, etc.) **Torii lag**: The indexer isn't instant. If you act immediately after state changes, you might read stale data. Add a small delay after your own transactions: ```javascript if (result?.transaction_hash) { console.log('Action executed. Waiting for indexer...'); await sleep(10000); // Give Torii time to process } ``` Most bugs fall into these categories. When something new happens, add it to your logging and you'll spot patterns. ### What You've Built Let's recap. You now have: 1. **State observation** via Torii GraphQL—perfect information about the game 2. **Autonomous signing** via Controller sessions—scoped, time-limited, secure 3. **Decision logic** that can be as simple or sophisticated as you want 4. **Transaction execution** with proper error handling 5. **A running loop** that ties it all together This isn't a toy. Agents built on this pattern are already playing real games with real tokens at stake. The architecture scales from hobby experiments to serious autonomous systems. ### Next Steps You've got the foundation. Where you take it depends on your interests: **Better strategies**: The decision function is where competitive advantage lives. Can you model other players? Predict market movements? Find arbitrage opportunities? **Multi-agent coordination**: What if you had 10 agents working together? Or competing against each other to test strategies? **LLM integration**: We showed a basic example. But you could give the LLM memory, tools for analysis, or let it learn from outcomes. **Real deployment**: Run this on a server 24/7. Add monitoring. Maybe even let it manage meaningful amounts of value. The code from this tutorial is a starting point, not a ceiling. Fork it, break it, make it your own. *** *This article is part of the Agent-Native Games series. Read the previous article: [Why Onchain Games Are Perfect for AI Agents](/blog/why-onchain-games-perfect-for-ai-agents)* **Resources:** * [Controller CLI Documentation](https://github.com/cartridge-gg/controller-cli/blob/main/LLM_USAGE.md) * [Torii Documentation](https://book.dojoengine.org/toolchain/torii) * [Dojo Book](https://book.dojoengine.org/) import { BlogBanner } from "../../components/BlogBanner"; import { BlogPosts } from "../../components/BlogPosts";
![Cover Image](/blog/provable-games/cover.png) ## Provable Games ::authors [Autonomous Worlds (AWs)](https://aw.network/posts/the-case-for-autonomous-worlds) are digitally native worlds, defined and governed by their smart contracts, existing entirely onchain and inheriting the unique affordances of the blockchain: composable, extensible, permissionless and persistent. Unencumbered by platform risks, AWs promise a new medium for our collective expression, a natural extension of our digital culture, with a credible commitment to neutrality. They represent [a new paradigm in game design](https://aw.network/posts/composable-engineering): where core mechanics can be openly defined, where primitives can be permissionlessly extended, and where a world's substrate can be continually reinterpreted into new experiences. An AW's super power is its [hardness](https://stark.mirror.xyz/n2UpRqwdf7yjuiPKVICPpGoUNeDhlWxGqjulrlpyYi0). Its ability to credibly commit to future states: that it will exist according to its rules and that it will exist as long as it is desired. Blockchains accomplish this trivially, providing a credibly neutral, persistent shared computing environment for the AWs smart contracts; however, achieving these properties at the scale necessary for complex, interactive onchain worlds, without making significant tradeoffs, is a fundamental challenge. #### Introducing Dojo: the provable game engine ![Illustration depicting the Dojo stack](/blog/provable-games/architecture.png) Our solution is Dojo, a provable game engine, enabling games execution to be proved and attested to by a validity proof, a capability stemming from its [implementation in Cairo](https://github.com/starkware-libs/cairo). Dojo serves as the world's engine, empowering developers to concentrate on creating games without the burden of developing all of the auxiliary infrastructure required to realize them. It presents a solution to the challenge of fully deploying complex, immersive experiences, such as MMORPGs, on the blockchain. We achieve this by moving computation out of a blockchain's shared execution context to specialized "micro-rollups", complemented by [fractal scaling](https://medium.com/starkware/fractal-scaling-from-l2-to-l3-7fe238ecfb4f) techniques, to provide cheap and abundant computation. This is enabled through zero-knowledge proofs, which are ideally suited for balkanized execution and long tail computation typical of games, allowing the use of powerful centralized sequencers to achieve the performance necessary for immersive real-time games. During the execution of a Dojo game, execution and sequencing can be done [natively](https://github.com/lambdaclass/cairo_native), [significantly out performing vm based execution](https://twitter.com/fede_intern/status/1729165583596597647), and then asynchronously proved to generate and settle a zero-knowledge proof that attests to the validity of the execution onchain, inheriting the full security of a base layer, like Ethereum. #### Execution sharding with proofs ![Illustration depicting a fractal scaling heirarchy](/blog/provable-games/fractal.png) A core insight is that gaming experiences, outside of their shared economy provided by a decentralized blockchain, can be naturally sharded into more narrowly scoped execution contexts, removing the necessity to compete for computational resources in a zero-sum execution context. Consider this World of Warcraft example, where there exists several layers: * **Cities** (Orgrimmar, Stormwind) with their global execution context (auction house, messaging, trading) for thousands of players with low frequency actions (no combat, etc). * **Regions** (Barrens, Strangle Thorn Vale) with their own regional execution shard for hundreds of players with sparse, average frequency actions (fighting mobs, turning in quests). * **Instances** (Scarlet Monastery, Molten Core) with their own instance specific execution shard for up to tens of players, with high frequency actions (fighting bosses). This is something uniquely enabled by zero-knowledge technology, since the security assumptions of optimistic approaches (that some honest party will validate the chain through re-execution) breaks down as the number of participants reduces and economic incentives scatter. In particular, it is not possible to shard to the instance level with an optimistic rollup and maintain the security guarantees from the base layer, where there is no incentive for a third party to re-execute. Approximately, this tradeoff looks something like this: ![Chart depicting the security tradeoff of optimistic vs zk rollups vs number of participants](/blog/provable-games/security.png) #### Towards cheap, abundant compute In addition, we get several other nice properties: * **Proof recursion** enables temporal compression of an entire sequence of actions from an instance run into a single attestation, massively reducing the data availability cost and enabling us to cheaply settle on robust data availability layers like Ethereum. In contrast to optimistic approaches, where the input calldata for every action inside a game needs to be provided to the data availability layer to enable re-execution, forcing these approaches to make security tradeoffs in order to scale data availability bandwidth. Another cool side effect is it allows players to keep their strategies secret, by not revealing the intermediate steps in a sequence that led to the defeat of a final boss, while still credibly convincing everyone that they accomplished the task and allowing them to unlock the reward. * **Proof aggregation** enables proofs from every execution shard to be combined and the settlement cost amortized across those players, reducing transaction costs as the number of players increases (!). This is possible not only across a particular game, but globally, across all Cairo based proving systems (starknets, katanas, coprocessors, etc), by leveraging shared proving infrastructure provided by SHARP. ![Illustration depicting proof aggregation](/blog/provable-games/aggregation.png) #### What are the tradeoffs vs Optimistic approaches? While this approach is incredibly well suited for the type of workload we are targeting, it does introduce some limitations compared to optimistic approaches: * Maintaining provability gives us less flexibility to use tools such as precompiles and in-node modifications to enshrine game logic into the sequencer itself. While we are able to introduce our own syscalls (similar to precompiles), their outputs are untrusted and need to be verified in the provable program. However, our opinion is that in-node modifications should be limited to the extent possible anyways, since in-node changes add significant distribution overhead to sequencer binaries in order to maintain the optimistic security model and introduce a two-tiered development model, where node contributors are significantly more influential than smart contract developers, compromising on platform neutrality. * Optimistic rollups with large execution contexts and many shared stakeholders, where the honest actor assumption is compelling, can maintain a strong security model and in some cases, can provide cheaper execution costs than a provable network where every execution needs to be proved. In this regard, I believe there is an opportunity for engines like MUD and Dojo to be complementary (more on that in the future). #### Conclusion Dojo provides a developer friendly framework for developing and scaling onchain games and autonomous worlds that are composable, extensible, permissionless and persistent. We do so by providing a \~zero-cost abstraction for developers to succinctly define their onchain world using Cairo and a robust toolchain for building, migrating, deploying, proving and settling these worlds in production. Leveraging execution sharding and fractal scaling, we're able to scale computation to meet the needs of complex, interactive experiences, while maintaining the security properties of Ethereum. Stay tuned as we delve into the depths of the Dojo stack, sharing insights on what we've built and what we plan to build, including exciting developments in client side proving, hidden information, fractal scaling, randomness, cross-chain messaging, and real-time communication. In the meantime, checkout our website, read our book, and dig into the code for more info! *Special thanks to Loaf and Gabe for feedback and review* ## Why Onchain Games Are Perfect for AI Agents AI agents are getting good at games. Really good. But here's the thing most people miss: the limiting factor isn't intelligence anymore. It's access. An agent can reason about complex strategy, optimize resource allocation, and learn from thousands of simulated games. What it can't do? Reliably interact with most games. Traditional games weren't built for programmatic players — they were built to keep them out. Onchain games flip this entirely. They're permissionless by default, transparent by design, and cryptographically verifiable. For developers building AI game agents, this isn't just convenient. It's transformative. Let's break down exactly why onchain architecture gives agents what they need, and how you can use the Dojo stack to build agents that actually work. !\[Diagram showing agent architecture connecting to onchain game state]\(placeholder: technical diagram showing AI agent → Torii → World Contract → execution flow, clean line art style) *The agent stack: query state via Torii, reason locally, execute via Controller* ### What Agents Actually Need Before we talk about solutions, let's be precise about the problem. An AI agent trying to play a game needs four things: **Observable state.** The agent needs to see what's happening. Not through pixel parsing or screen scraping — through structured data it can reason about. Give it a JSON object with game state, not a 1920x1080 screenshot. **Reliable execution.** When the agent decides to make a move, that move needs to happen. Consistently. No random failures, no "please complete this CAPTCHA," no silent drops. **Verifiable outcomes.** The agent needs to know its action worked. Not "the server said it worked" — cryptographic proof that the state changed as expected. This matters when real value is on the line. **Economic alignment.** Agents need skin in the game. If playing well leads to real rewards (and playing poorly to real losses), you get agents that optimize for actually winning, not just exploring the action space. Traditional games give you maybe one of these. Onchain games give you all four. ### Why Traditional Games Fail Agents I've built game bots before. You probably have too. And you know the pain. #### Screen Scraping Is Brittle The classic approach: capture frames, run computer vision, extract state. It works... until it doesn't. ```python # The traditional approach screenshot = capture_screen() health_bar = detect_region(screenshot, HEALTH_BAR_COORDS) current_health = ocr_number(health_bar) # Good luck with this ``` Font changes break your OCR. UI updates shift your detection regions. Anti-aliasing makes edge detection unreliable. You spend more time maintaining the scraper than building the actual agent logic. And that's assuming you can capture the screen at all. Many games detect and block screen capture software. #### Private Servers Mean Trust Issues Some games offer APIs. Great! Except you're trusting that: * The server isn't lying about game state * Other players aren't getting different information * The server won't change behavior when it detects bot-like patterns * Your API access won't get revoked With private servers, you're playing in someone else's sandbox. They can change the rules whenever they want. #### Permission Gates Kill Innovation Want to build an agent for a popular game? Here's your path: 1. Apply for API access 2. Wait weeks/months for approval 3. Get rejected because "bots hurt player experience" 4. Try to reverse-engineer the protocol 5. Get banned The games that do allow programmatic access typically lock it behind expensive licensing deals or restrict it to "approved partners." Innovation happens at the edges, and these gates keep independent developers out. #### Anti-Bot Measures Are Adversarial Game companies spend millions fighting bots. CAPTCHAs, behavioral analysis, device fingerprinting, rate limiting, shadow bans. It's an arms race, and as an agent developer, you're the enemy. Even if your agent provides value — even if it makes the game more interesting — you're fighting the infrastructure instead of building cool stuff. ### How Onchain Games Deliver Now let's flip the script. Here's what building agents looks like with onchain games. #### Verifiable State via Torii Every Dojo game exposes its state through Torii, an indexer that makes all game data queryable via GraphQL or gRPC. No screen scraping. No reverse engineering. Just query the data you need. ```graphql # Query all realms with their resources and positions query GetGameState { entities(keys: ["*"]) { edges { node { keys models { ... on Position { x y } ... on Resources { wood stone food } ... on Army { units strength } } } } } } ``` This isn't cached data from a friendly API. It's indexed directly from the blockchain. The state you're querying is the same state everyone else sees — cryptographically guaranteed. Want to know if an opponent is massing troops near your border? Query it. Want historical data about price movements in the in-game market? It's all there, indexed and searchable. ```javascript // Real-time subscription to game events const subscription = gql` subscription OnGameEvents { events(keys: ["game_id"]) { data keys transactionHash } } `; client.subscribe({ query: subscription }).subscribe({ next(event) { agent.processEvent(event.data); } }); ``` Your agent can subscribe to events and react in real-time. When an opponent moves, you know immediately — not when the game client decides to render it. #### Permissionless Execution Here's the part that changes everything: you don't need permission to play. Onchain games are smart contracts. Anyone can call them. No API keys, no approval process, no Terms of Service that ban "automated play." If you can construct a valid transaction, you can execute it. ```bash # Execute a game action via Controller CLI controller execute \ --contract 0x123...abc \ --entrypoint move_army \ --calldata 0x5,0x10,0x3 \ --rpc-url https://api.cartridge.gg/x/starknet/mainnet \ --json ``` Your agent has the exact same interface as a human player. Better, actually — humans use clunky GUIs while your agent talks directly to the contract. This isn't just convenient. It's a different relationship with the game. You're not a guest hoping the game company lets you play. You're a participant in a permissionless protocol. #### Provable Logic with Cairo Dojo games are written in Cairo, a provably-correct smart contract language. This means: **Deterministic outcomes.** Given the same inputs, you always get the same outputs. Your agent can simulate actions locally before committing them onchain. **Auditable rules.** The game logic is public. You can read the contracts, understand exactly how combat resolves, how resources accumulate, how edge cases are handled. No hidden mechanics. **Guaranteed execution.** If your transaction is valid and included in a block, it will execute exactly as the contract specifies. No server-side fudging. ```rust // Cairo game logic - completely transparent fn attack( ref self: ContractState, attacker_id: u128, defender_id: u128 ) { let attacker = get!(self.world, attacker_id, (Army)); let defender = get!(self.world, defender_id, (Army)); // Combat resolution is deterministic and auditable let damage = calculate_damage(attacker.strength, defender.defense); // State changes are atomic - all or nothing set!(self.world, ( Army { id: defender_id, health: defender.health - damage } )); emit!(self.world, AttackEvent { attacker_id, defender_id, damage }); } ``` Your agent can reason about this logic directly. No guessing how the black box works. #### Real Economic Stakes Onchain games use real tokens. Not points. Not "premium currency." Actual transferable assets with market value. This creates genuine incentives. An agent that plays well accumulates valuable resources. One that plays poorly loses them. The feedback loop is real, immediate, and denominated in something that matters. For agent developers, this is huge. You can build agents that earn their own operating costs. You can create agents that compete for profit. The economics align with performance. Consider the training implications. With traditional games, you might run millions of simulated games to train an agent, but deployment is disconnected from training. The simulation doesn't match reality perfectly, and there's no economic feedback. With onchain games, your agent learns from real stakes from day one. Bad decisions cost real resources. Good decisions accumulate real value. The agent isn't optimizing for a proxy metric — it's optimizing for the actual thing that matters. This also enables new business models. Imagine an agent-as-a-service that plays on behalf of users, taking a cut of winnings. Or a DAO-owned agent that distributes profits to token holders. The permissionless nature of onchain games makes these arrangements enforceable and trustless. ### The Dojo Stack for Agents Let's get concrete. Here's how the pieces fit together. #### World Contract Every Dojo game has a World contract — a single entry point that manages all game state. Think of it as the game's database and execution engine rolled into one. ``` World Contract (0x123...abc) ├── Models (data schemas) │ ├── Position { entity_id, x, y } │ ├── Resources { entity_id, wood, stone, food } │ └── Army { entity_id, units, strength } ├── Systems (game logic) │ ├── move_army(army_id, target_x, target_y) │ ├── gather_resources(worker_id) │ └── attack(attacker_id, defender_id) └── Events (state changes) ├── ArmyMoved ├── ResourcesGathered └── CombatResolved ``` For agents, this is the API. Query models for state, call systems to take actions, subscribe to events for updates. #### Torii Indexer Torii watches the blockchain and indexes everything that happens in the game. It provides: * **GraphQL API** for complex queries * **gRPC** for high-performance streaming * **WebSocket subscriptions** for real-time updates ```javascript // Initialize Torii client import { ToriiClient } from "@dojoengine/torii-client"; const client = await ToriiClient.create({ toriiUrl: "https://api.cartridge.gg/x/eternum/torii", worldAddress: "0x123...abc", }); // Fetch all entities matching a query const entities = await client.getEntities({ model: "Position", query: { x: { $gte: 0, $lte: 100 } } }); // Subscribe to model updates client.onEntityUpdated((entity) => { console.log("Entity changed:", entity); agent.onStateChange(entity); }); ``` The indexer is your agent's eyes. Query frequently, subscribe to what matters, and keep your local state synchronized. #### Controller CLI Here's where agent execution gets practical. The Controller CLI lets you sign and submit transactions programmatically — no browser wallet, no popup confirmations. **Generate a keypair:** ```bash controller generate-keypair --json # Output: { "publicKey": "0x...", "privateKey": "0x..." } ``` **Check session status:** ```bash controller status --json # Output: { "status": "no_session" | "keypair_only" | "active" } ``` **Register a session with policies:** ```json // policy.json - defines what actions the session can take { "contracts": { "0x123...abc": { "methods": ["move_army", "gather_resources", "attack"] } }, "expires": "2026-03-12T00:00:00Z" } ``` ```bash controller register-session policy.json \ --rpc-url https://api.cartridge.gg/x/starknet/mainnet \ --json # Opens browser for user approval, then session is active ``` **Execute transactions:** ```bash controller execute \ --contract 0x123...abc \ --entrypoint move_army \ --calldata 0x42,0x10,0x15 \ --rpc-url https://api.cartridge.gg/x/starknet/mainnet \ --json ``` The key insight: sessions are pre-authorized. Once a user approves a session policy, the agent can execute any action within that policy without further human intervention. Your agent can run autonomously for days or weeks. #### Session Key Security "Wait, doesn't that mean if someone gets my session key, they can drain my account?" Not quite. Session keys are scoped: * **Contract whitelist** — only call specific game contracts * **Method whitelist** — only call specific functions * **Expiration** — sessions automatically expire * **No token transfers** — session can't move assets out of the game Even in a worst case leak, the damage is limited to in-game actions the session was authorized for. An attacker could make your agent play badly, but they can't steal your tokens. ### Design Patterns for Agent-Friendly Games If you're building a game (not just an agent), here's how to make it agent-friendly. #### Atomic Actions Each transaction should represent one complete decision. Don't make agents submit multi-step flows. ```rust // Good: single atomic action fn reinforce_and_attack( ref self: ContractState, reinforcement_count: u32, target_id: u128 ) { // Reinforcement and attack in one tx } // Bad: requires two transactions with state between them fn prepare_attack(...) { } // First tx fn execute_attack(...) { } // Second tx, might fail if state changed ``` Atomic actions let agents reason about outcomes confidently. Multi-step flows create race conditions. #### Rich Event Emission Emit events for everything. Agents need to know what happened and why. ```rust // Emit detailed events emit!(self.world, CombatResolved { attacker_id, defender_id, damage_dealt, defender_destroyed: defender.health <= 0, attacker_remaining_strength, block_timestamp: starknet::get_block_timestamp() }); ``` Events are cheap. Debugging agents without them is expensive. #### Composable Game Logic Build systems that can be combined. Agents excel at finding optimal combinations humans wouldn't try. ```rust // Separate systems that compose fn move_army(...) { } fn set_army_stance(...) { } fn declare_alliance(...) { } fn trade_resources(...) { } // Agents can orchestrate these in creative ways // Human: move, then attack // Agent: move, set defensive stance, trade resources to ally, // have ally attack while you defend ``` The more composable your systems, the more interesting the agent strategies. #### Query-Friendly State Structure your models for efficient querying. Agents will hammer your indexer. ```rust // Include commonly-queried fields directly struct Army { id: u128, owner: ContractAddress, position_x: u32, // Denormalized for query efficiency position_y: u32, strength: u32, last_action_time: u64, // Useful for cooldown calculations } ``` Think about what queries agents will run and optimize for them. #### Information Visibility Be intentional about what's public and what's hidden. Onchain state is public by default, but you can design around this. Some information should be visible — it creates strategic depth when agents can observe and react to each other. Army positions, resource levels, territorial control. Other information might benefit from fog of war. You can implement commitment schemes where agents commit to actions in one transaction and reveal them later. This prevents reactive counter-play and rewards genuine prediction. ```rust // Commitment scheme for hidden actions fn commit_action(ref self: ContractState, commitment: felt252) { // Store hash of (action, salt) set!(self.world, (Commitment { player: get_caller_address(), hash: commitment })); } fn reveal_action(ref self: ContractState, action: felt252, salt: felt252) { let commitment = get!(self.world, get_caller_address(), (Commitment)); assert(poseidon_hash(action, salt) == commitment.hash, 'invalid reveal'); // Execute the revealed action } ``` The key is making information visibility a deliberate design choice, not an accident of architecture. ### The Agent Development Loop Here's the actual workflow for building a game agent: 1. **Explore state** — Query Torii, understand what data is available 2. **Understand actions** — Read the system contracts, know what you can do 3. **Build perception** — Write code to fetch and parse game state into useful representations 4. **Implement strategy** — Rule-based, ML-powered, or LLM-driven decision making 5. **Execute actions** — Submit transactions via Controller 6. **Monitor and iterate** — Watch performance, refine strategy ```python # The core agent loop async def agent_loop(): while True: # Perceive state = await torii.query_game_state() # Decide action = strategy.decide(state) # Act if action: tx = await controller.execute(action) await wait_for_confirmation(tx) # Rate limit appropriately await asyncio.sleep(POLL_INTERVAL) ``` It's simple in structure, complex in the details. But the infrastructure handles the hard parts — you focus on strategy. ### What's Next We're at the early days of agent-native gaming. The tools exist. The games are launching. What's missing is developers building agents. If you want to go deeper: * **Build something**: Check out [Building Your First Dojo Game Agent](/blog/building-your-first-dojo-agent) for a hands-on walkthrough * **Understand the context**: Read [The Rise of Agent-Native Gaming](https://cartridge.gg/blog/agent-native-gaming) for the bigger picture * **Get the tools**: [Controller CLI documentation](https://github.com/cartridge-gg/controller-cli) has everything you need to start executing transactions Onchain games aren't just compatible with AI agents. They're waiting for them. The question is: what will you build? *** *Building an agent for a Dojo game? Share what you're working on — we'd love to see it.* import { LinkCard } from "../../components/LinkCard"; ## Dojo Framework Overview :::note Dojo is built on top of Cairo. We suggest [familiarizing yourself with Cairo](https://book.cairo-lang.org/) before using Dojo. ::: ### What is Dojo? Dojo is a comprehensive framework for building provable games and autonomous worlds on Starknet. It combines the power of Cairo smart contracts with Entity-Component-System (ECS) architecture to create scalable, composable onchain applications. **Key Benefits:** * **Provable**: All game logic and state changes are verifiable onchain * **Composable**: Modular design allows for easy extension and integration * **Scalable**: Optimized for high-performance onchain applications * **Developer-friendly**: Rich tooling and familiar development patterns ![Dojo Overview](/framework/dojo-overview.png) ### Understanding ECS Architecture [Entity-Component-System](https://en.wikipedia.org/wiki/Entity_component_system) (ECS) is a design pattern that separates data from logic, enabling highly modular and scalable applications. **The Problem ECS Solves:** Traditional object-oriented programming can lead to complex inheritance hierarchies and tight coupling between data and behavior. ECS addresses these issues by decomposing your application into three distinct parts: #### The ECS Trinity ![ECS Pattern](/framework/ECS.png) * **Entities**: The *objects* in your game — characters, items, etc. * **Components**: The *properties* of your entities — position, durability, etc. * **Systems**: The *rules* that govern your game — movement, combat, etc. With ECS, you assign **components** to **entities**, and operate on them using **systems**. This approach allows for a more modular and scalable design, as well as better performance and memory usage. Entities are typically represented as a unique identifier, to which components are assigned. Systems can then operate over large numbers of entities at once, depending on the components they have. As an example, a combat system might reduce the durability of all weapons used in a battle, while a rest system might increase the health of all members of a party. **ECS Benefits:** * **Modularity**: Components can be mixed and matched across entity types * **Performance**: Systems can efficiently process large numbers of entities * **Flexibility**: Easy to add new components or systems without affecting existing code #### Real-World Example Consider a simple RPG where both players and monsters can move and fight: ```rust // Components (Data) #[derive(Copy, Drop, Serde)] #[dojo::model] struct Position { #[key] entity_id: u32, x: u32, y: u32, } #[derive(Copy, Drop, Serde)] #[dojo::model] struct Health { #[key] entity_id: u32, hp: u32, max_hp: u32, } // Systems (Logic) #[starknet::interface] trait IGameSystem { fn move(ref self: T, entity_id: u32, direction: Direction); fn fight(ref self: T, attacker_id: u32, target_id: u32); } #[dojo::contract] mod game_system { use super::{Position, Health, Direction, IGameSystem}; use dojo::model::{ModelStorage}; use dojo::world::{WorldStorage, WorldStorageTrait}; #[abi(embed_v0)] impl GameSystemImpl of IGameSystem { fn move(ref self: ContractState, entity_id: u32, direction: Direction) { let mut world = self.world(@"my_game"); let mut position: Position = world.read_model(entity_id); // Update position based on direction world.write_model(@position); } fn fight(ref self: ContractState, attacker_id: u32, target_id: u32) { let mut world = self.world(@"my_game"); let mut attacker_health: Health = world.read_model(attacker_id); let mut target_health: Health = world.read_model(target_id); // Handle combat logic world.write_model(@target_health); } } } ``` Both player and monsters can have Position and Health components, and the same systems work for both. ### Dojo's ECS Implementation Dojo adapts ECS for the onchain environment with three core concepts: #### 1. Models (Components) Models are Cairo structs that define your application's data structures. They act as *components* in the ECS pattern. ```rust #[derive(Copy, Drop, Serde)] #[dojo::model] // Defines a model, used to generate Torii events struct Position { #[key] // Defines a model's key, like an ORM primary key pub player: ContractAddress, // The rest of the model's attributes pub x: u32, pub y: u32, } ``` :::note `#[dojo::model]` and `#[key]` are examples of [procedural macros](https://book.cairo-lang.org/ch12-10-procedural-macros.html), which tell the Cairo compiler where to add Dojo-specific code. ::: **Key Features:** * **Automatic indexing**: All model changes are automatically indexed by Torii * **Type safety**: Full Cairo type system support * **Composability**: Models can be reused across different entity types #### 2. Systems (Smart Contracts) Systems implement your application's business logic as Cairo contract functions. ```rust // First define the interface #[starknet::interface] trait IActions { fn move_player(ref self: T, direction: Direction); } #[dojo::contract] // Defines a Dojo contract mod actions { use super::IActions; use starknet::{ContractAddress, get_caller_address}; use dojo::model::{ModelStorage}; use dojo::world::{WorldStorage, WorldStorageTrait}; #[abi(embed_v0)] impl ActionsImpl of IActions { fn move_player(ref self: ContractState, direction: Direction) { // Get the world and player entity key let mut world = self.world(@"my_game"); let player = get_caller_address(); // Read current position let mut position: Position = world.read_model(player); // Update position based on direction match direction { Direction::Up => position.y += 1, Direction::Down => position.y -= 1, Direction::Left => position.x -= 1, Direction::Right => position.x += 1, } // Write updated position world.write_model(@position); } } } ``` **Key Features:** * **Permissioned access**: Fine-grained control over who can modify what * **Atomic operations**: All state changes happen in a single transaction * **Event emission**: Automatic event generation for state changes #### 3. World (Resource Registry) The World contract serves as the central coordinator, managing all models and systems. ```rust // The World provides a unified interface for all operations let mut world = self.world(@"my_game"); // Cairo's typing tells us we are querying the player's position let position: Position = world.read_model(player_id); // Update game state world.write_model(@new_position); // Emit custom events if needed world.emit_event(@PlayerMoved { player, direction }); ``` **Key Features:** * **Centralized state**: Single source of truth for all application data * **Permission management**: Hierarchical authorization system * **Upgradeability**: Safe model and system upgrades * **Introspection**: Rich metadata and schema information ### Architecture Overview ![world-map](/shared/world-map.png) ### Development Workflow 1. **Design Models**: Define your data structures as Dojo models 2. **Implement Systems**: Create contract functions that operate on models 3. **Configure Permissions**: Set up authorization for your systems 4. **Deploy to World**: Register your models and systems 5. **Build Client**: Use indexed data to create rich user experiences ### Best Practices #### Model Design * **Keep models small and focused** - Each model should represent a single concept * **Design for reusability** - Models should be composable across different entity types * **Plan for upgrades** - Consider future schema changes when designing models * **Always derive required traits** - All models must derive `Drop` and `Serde` * **Use key fields correctly** - All `#[key]` fields must come before non-key fields #### System Design * **Minimize gas costs** - Batch operations and optimize storage access * **Handle errors gracefully** - Use assertions and proper error handling * **Design for permissions** - Group related systems by permission requirements * **Define interfaces first** - Always create a `#[starknet::interface]` before implementing systems * **Use proper mutability** - Make world reference mutable when writing models #### Architecture * **Separate concerns** - Keep data (models) and logic (systems) distinct * **Use events effectively** - Emit events for important state changes * **Plan your namespaces** - Organize resources logically and use consistent namespace strings * **Follow ECS principles** - Compose entities using multiple small, focused models ### Next Steps Ready to start building with Dojo? Here's your learning path:
{" "} {" "}
## Development Workflow Now that you understand Dojo's toolchain, let's explore the typical development workflow. You'll learn how to iterate efficiently, test your code, and debug issues as you build your Dojo applications. ### The Development Cycle A typical Dojo development session follows this pattern: ``` 1. Plan & Design ↓ 2. Write Cairo Code ↓ 3. Test Locally ↓ 4. Debug & Iterate ↓ 5. Deploy & Verify ↓ 6. Repeat ``` Let's walk through each step with practical examples. ### Setting Up Your Development Environment Start each development session by launching your tools: ```bash # Terminal 1: Start Katana katana --dev # Terminal 2: Deploy and start Torii sozo migrate --dev torii --dev # Terminal 3: Your workspace for commands # (keep this free for running sozo commands) ``` > **Tip**: Create a simple script to start all tools at once. > Save this as `start-dev.sh`: ```bash #!/bin/bash # Start development environment katana --dev & sleep 2 sozo migrate --dev torii --dev & echo "🚀 Development environment ready!" ``` ### Planning and Design Before writing code, think about your game or application structure: #### Define Your Models (Components) What data does your application need to track? ```cairo // Example: A simple trading card game #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Card { #[key] pub id: u32, pub owner: ContractAddress, pub attack: u8, pub defense: u8, pub cost: u8, } #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Game { #[key] pub id: u32, pub player1: ContractAddress, pub player2: ContractAddress, pub current_turn: ContractAddress, pub status: GameStatus, } ``` #### Plan Your Systems (Actions) What actions can players take? ```cairo #[dojo::interface] trait IGameActions { fn create_game(ref world: IWorldDispatcher, opponent: ContractAddress); fn play_card(ref world: IWorldDispatcher, game_id: u32, card_id: u32); fn end_turn(ref world: IWorldDispatcher, game_id: u32); } ``` ### Writing Cairo Code #### Start with Models Begin by implementing your data models in `src/models.cairo`: ```cairo #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Player { #[key] pub address: ContractAddress, pub name: ByteArray, pub wins: u32, pub losses: u32, } #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Inventory { #[key] pub player: ContractAddress, pub cards: Array, } ``` #### Implement Systems Incrementally Add one system at a time in `src/actions.cairo`: ```cairo #[dojo::contract] mod actions { use super::{Player, Inventory}; #[abi(embed_v0)] impl ActionsImpl of super::IGameActions { fn create_player(ref world: IWorldDispatcher, name: ByteArray) { let caller = get_caller_address(); // Check if player already exists let existing_player = get!(world, caller, Player); assert(existing_player.address.is_zero(), 'Player already exists'); // Create new player set!(world, Player { address: caller, name: name, wins: 0, losses: 0, }); // Initialize empty inventory set!(world, Inventory { player: caller, cards: array![], }); } } } ``` #### Compile Early and Often After adding each piece, compile to catch errors: ```bash sozo build ``` Fix any compilation errors before moving on to the next feature. ### Testing Your Code #### Local Testing Workflow 1. **Deploy your changes**: ```bash sozo migrate --dev ``` 2. **Test basic functionality**: ```bash # Create a player sozo execute create_player --calldata "Alice" --dev # Verify it worked sozo model get Player $ACCOUNT_ADDRESS --dev ``` 3. **Test edge cases**: ```bash # Try to create the same player again (should fail) sozo execute create_player --calldata "Alice" --dev ``` #### Using GraphQL for Complex Queries Test your data relationships using Torii's GraphQL interface at `http://localhost:8080/graphql`: ```graphql query GetPlayersWithInventories { playerModels { edges { node { address name wins losses } } } inventoryModels { edges { node { player cards } } } } ``` #### Automated Testing For more comprehensive testing, use Sozo's built-in test framework: ```bash # Run all tests sozo test # Run tests with output sozo test -v # Run specific test sozo test test_create_player ``` Create tests in your `src/` directory: ```cairo #[cfg(test)] mod tests { use super::{Player, actions}; use dojo::test_utils::{spawn_test_world, deploy_contract}; #[test] fn test_create_player() { let world = spawn_test_world!(); let contract_address = deploy_contract!(world, actions); let actions_dispatcher = IGameActionsDispatcher { contract_address }; // Test player creation actions_dispatcher.create_player("Alice"); let player = get!(world, get_caller_address(), Player); assert(player.name == "Alice", 'Wrong name'); assert(player.wins == 0, 'Wrong wins'); } } ``` ### Debugging and Troubleshooting #### Common Development Issues ##### Compilation Errors ```bash # Error: trait has no implementation # Solution: Make sure all traits are properly implemented sozo build 2>&1 | grep -A 5 "error" ``` ##### Transaction Failures ```bash # Check recent transactions sozo events --dev # Get detailed transaction info sozo call get_transaction --calldata $TX_HASH --dev ``` ##### Data Not Updating ```bash # Restart Torii to refresh indexing pkill torii torii --dev ``` #### Debugging Techniques ##### 1. Use Events for Logging Add events to your systems for debugging: ```cairo #[derive(Copy, Drop, Serde)] #[dojo::event] pub struct PlayerCreated { #[key] pub player: ContractAddress, pub name: ByteArray, } // In your system: emit!(world, PlayerCreated { player: caller, name: name }); ``` ##### 2. Check State Step by Step After each operation, verify the state: ```bash # Execute action sozo execute create_player --calldata "Bob" --dev # Check immediate result sozo model get Player $ACCOUNT_ADDRESS --dev # Check related data sozo model get Inventory $ACCOUNT_ADDRESS --dev ``` ##### 3. Use Verbose Logging Enable detailed logging on your tools: ```bash # Katana with debug info katana --dev -vvv # Torii with debug logging torii --dev --log-level debug ``` ### Iterating on Features #### Feature Development Pattern 1. **Implement minimal version**: ```cairo fn attack(ref world: IWorldDispatcher, target: ContractAddress) { // Basic implementation let attacker = get_caller_address(); // TODO: Add damage calculation // TODO: Add health reduction // TODO: Add death handling } ``` 2. **Test basic functionality**: ```bash sozo execute attack --calldata $TARGET_ADDRESS --dev ``` 3. **Add complexity incrementally**: ```cairo fn attack(ref world: IWorldDispatcher, target: ContractAddress) { let attacker = get_caller_address(); let attacker_stats = get!(world, attacker, CombatStats); let mut target_stats = get!(world, target, CombatStats); // Calculate damage let damage = attacker_stats.attack - target_stats.defense; if damage > 0 { target_stats.health -= damage; set!(world, target_stats); emit!(world, AttackExecuted { attacker, target, damage }); } } ``` 4. **Test edge cases**: ```bash # Test with different scenarios sozo execute attack --calldata $WEAK_TARGET --dev sozo execute attack --calldata $STRONG_TARGET --dev sozo execute attack --calldata $DEAD_TARGET --dev ``` #### Managing Code Changes When modifying existing models, you might need to reset your local state: ```bash # Clean rebuild sozo clean sozo build sozo migrate --dev # If you have persistent data you want to keep: # Back up important state first sozo model get Player $ACCOUNT_ADDRESS --dev > player_backup.json ``` ### Performance Optimization #### Efficient Data Queries Structure your GraphQL queries to minimize data transfer: ```graphql # Good: Specific fields only query GetActiveGames { gameModels(where: {status: {eq: "ACTIVE"}}) { edges { node { id player1 player2 current_turn } } } } # Avoid: Fetching all data query GetAllGames { gameModels { edges { node { # all fields... } } } } ``` #### Gas Optimization Keep transactions lightweight: ```cairo // Good: Single batch operation fn play_multiple_cards(ref world: IWorldDispatcher, cards: Array) { let mut i = 0; loop { if i >= cards.len() { break; } // Process card i += 1; }; } // Less efficient: Multiple separate transactions // (would require multiple sozo execute calls) ``` ### Deployment Strategy #### Local to Testnet Progression 1. **Perfect locally**: ```bash # Ensure everything works sozo migrate --dev # Test all features thoroughly ``` 2. **Deploy to Sepolia**: ```bash # Configure Sepolia profile in dojo_dev.toml sozo migrate --profile sepolia ``` 3. **Test on testnet**: ```bash # Use real testnet for final validation sozo execute create_player --calldata "TestUser" --profile sepolia ``` 4. **Deploy to mainnet** (when ready): ```bash sozo migrate --profile mainnet ``` ### Development Best Practices #### Keep Your Workspace Organized ``` my-game/ ├── src/ │ ├── models/ # Separate model files │ │ ├── player.cairo │ │ ├── game.cairo │ │ └── card.cairo │ ├── systems/ # Separate system files │ │ ├── player_actions.cairo │ │ ├── game_actions.cairo │ │ └── card_actions.cairo │ └── lib.cairo # Main exports ├── tests/ # Dedicated test directory ├── scripts/ # Utility scripts └── docs/ # Game design documents ``` #### Version Control Best Practices Commit frequently with meaningful messages: ```bash git add -A git commit -m "Add player creation system with basic validation" # Before major changes git branch feature/card-trading git checkout feature/card-trading ``` #### Documentation Keep a development log of major decisions: ```markdown # Development Log ## 2024-01-15: Player System - Added basic player creation - Implemented inventory management - Next: Add card trading mechanics ## 2024-01-16: Combat System - Added basic attack/defense - Issue: Need to handle death states - TODO: Add resurrection mechanics ``` ### Quick Reference: Development Commands #### Starting Fresh ```bash # Terminal 1: Start blockchain katana --dev # Terminal 2: Build and deploy sozo build && sozo migrate # Terminal 3: Start indexer torii --dev ``` #### Making Changes ```bash # After editing contracts sozo build && sozo migrate # After changing configuration # Restart the affected service ``` #### Testing Your Changes ```bash # Execute a system to test sozo execute di-actions spawn # Query model data sozo model get Position # Check events sozo events ``` #### Common Issues ```bash # Clean rebuild sozo clean && sozo build && sozo migrate # Check service status curl http://localhost:5050 # Katana curl http://localhost:8080 # Torii # Restart development environment pkill katana && pkill torii # Then restart services ``` ### Next Steps You now have a solid understanding of the Dojo development workflow! For detailed information about each tool and its configuration options, see [Understanding the Toolchain](./understanding-the-toolchain). Ready to explore what comes next in your journey? Continue to [Next Steps](./next-steps) to discover advanced topics, deployment strategies, and community resources. ## Getting Started with Dojo Welcome to Dojo! This learning path will guide you from your first "Hello World" to building and deploying your own provable games and autonomous worlds on Starknet. Dojo is a comprehensive toolchain for building fully onchain applications using the Entity Component System (ECS) architecture. Whether you're new to blockchain development or an experienced smart contract developer, these tutorials will help you understand Dojo's unique approach to onchain applications. ### Learning Path Overview Follow these tutorials in order to build your understanding progressively: #### 1. [Your First Dojo App](/getting-started/your-first-dojo-app) **Time: 20-30 minutes** | **Prerequisites: None** Create and deploy your first Dojo project using the spawn-and-move example. You'll learn: * How to set up a new Dojo project * Basic ECS concepts (Entities, Components, Systems) * Running local development tools (Katana, Torii) * Deploying your first World #### 2. [Understanding the Toolchain](/getting-started/understanding-the-toolchain) **Time: 15-20 minutes** | **Prerequisites: First Dojo App** Explore the tools that make Dojo development possible. You'll understand: * **Katana**: Local Starknet sequencer for development * **Torii**: Indexing engine for efficient data queries * **Sozo**: CLI tool for project management and deployment * How these tools work together in your development workflow #### 3. [Development Workflow](/getting-started/development-workflow) Coming soon! **Time: 25-35 minutes** | **Prerequisites: Understanding the Toolchain** Learn the typical development process for Dojo projects. You'll practice: * Writing and testing new Systems * Adding and modifying Components * Using Sozo for common development tasks * Debugging and iterating on your application #### 4. [Next Steps](/getting-started/next-steps) Coming soon! **Time: 10 minutes** | **Prerequisites: Development Workflow** Discover where to go next in your Dojo journey. You'll explore: * Advanced tutorials and projects * Client SDK integration options * Community resources and support * Deployment to testnet and mainnet ### What You'll Build Throughout this learning path, you'll work with a simple but complete game where a player can: * Spawn characters in a virtual world * Move characters around using directional commands * Subscribe to UI updates through GraphQL This foundation demonstrates all core Dojo concepts while remaining simple enough to understand quickly. ### Prerequisites Before starting, ensure you have: * **Dojo installed**: Follow the [installation guide](/installation) if you haven't already * **Basic command line knowledge**: You'll be running terminal commands * **Text editor**: Any editor that supports Cairo syntax highlighting (VS Code recommended) > **Note**: No prior blockchain or Cairo experience is required. > We'll explain concepts as we go. ### Getting Help If you get stuck during these tutorials: * Check the [FAQ](/faq) for common issues * Join our [Discord community](https://discord.gg/dojoengine) for real-time help * Browse the [reference documentation](/framework) for detailed explanations ### Ready to Start? Let's begin with [your first Dojo app](/getting-started/your-first-dojo-app) and get your development environment running! ## Next Steps Congratulations! You've completed the getting started path and now have a solid foundation in Dojo development. Here's your roadmap to becoming a proficient Dojo developer and building production-ready applications. ### What You've Accomplished Through this learning path, you've: * ✅ Built and deployed your first Dojo application * ✅ Understood the ECS architecture and Dojo's toolchain * ✅ Learned the development workflow and best practices * ✅ Experienced the complete cycle from code to deployment You're now ready to tackle more advanced topics and build more complex applications. ### Choose Your Path Forward Depending on your goals and experience level, here are the recommended next steps: #### 🎮 For Game Developers ##### Intermediate Game Development Start with these tutorials to build more complex games: 1. **Multiplayer Game Tutorial** (Coming Soon) * Real-time player interactions * State synchronization patterns * Handling concurrent actions 2. **Complex Systems Tutorial** (Coming Soon) * Advanced ECS patterns * System interdependencies * Performance optimization 3. **NFT Integration** (Coming Soon) * Token-gated gameplay * Asset ownership mechanics * Cross-game interoperability #### 🛠️ For Application Developers ##### Client Integration Connect your Dojo world to user interfaces: 1. **[JavaScript SDK](/client/sdk/javascript/)** * React applications with real-time updates * Web-based game clients * Mobile web applications 2. **[Unity Integration](/client/sdk/unity)** * Desktop and mobile game clients * Cross-platform development * Native game engine integration 3. **[Other Platforms](/client/sdk/)** * Unreal Engine, Godot, Bevy * Custom C/C++ applications * Telegram and Discord bots #### 🚀 For Production Deployment ##### Deployment and Infrastructure Take your applications live: 1. **[Deploy to Mainnet](/tutorials/deploy-to-mainnet/main)** * Production deployment checklist * Security considerations * Cost optimization 2. **[Deploy using Slot](/tutorials/deploy-using-slot/main)** * Managed deployment platform * Simplified infrastructure * Monitoring and analytics 3. **[Custom Infrastructure](/toolchain/katana/)** * Self-hosted deployment * Custom Katana configurations * Advanced Torii setups ##### Scaling and Performance Handle production workloads: * **[Execution Sharding](/scaling/execution-sharding)**: Scale beyond single chain limits * **[Sovereign Rollups](/scaling/sovereign-rollups)**: Custom execution environments * **[Indexer Optimization](/toolchain/torii/)**: Efficient data access patterns #### 📚 For Framework Contributors ##### Contributing to Dojo Help improve the ecosystem: 1. **Code Contributions** (Coming Soon) * Core framework development * Tool improvements * Bug fixes and optimizations 2. **Documentation** (Coming Soon) * Tutorial writing * API documentation * Example applications 3. **Community Support** * Discord participation * Forum discussions * Mentoring newcomers ### Recommended Learning Projects #### Beginner Projects (1-2 weeks each) 1. **Turn-Based Strategy Game** * Grid-based movement * Resource management * AI opponents 2. **Trading Card Game** * Deck building mechanics * Card effects and abilities * Tournament system 3. **Virtual Pet Game** * Pet care mechanics * Evolution systems * Social features #### Intermediate Projects (2-4 weeks each) 1. **Decentralized Exchange** * Order book mechanics * Automated market making * Liquidity provision 2. **Battle Royale Game** * Large-scale multiplayer * Shrinking play area * Real-time combat 3. **City Builder** * Complex resource chains * Player interactions * Economic simulation #### Advanced Projects (1-3 months each) 1. **MMORPG Foundation** * Persistent world state * Character progression * Guild systems 2. **Cross-Chain Protocol** * Multi-world interactions * Asset bridging * Unified identity 3. **Autonomous World Platform** * User-generated worlds * Modding systems * Governance mechanisms ### Essential Resources #### Community and Support Stay connected and get help: * **[Discord Community](https://discord.gg/dojoengine)**: Real-time discussions and support * **[GitHub Repository](https://github.com/dojoengine/dojo)**: Latest code and issues * **Community Showcase**: Learn from other projects (Coming Soon) #### Tools and Libraries Extend your development capabilities: * **[Origami Library](/libraries/origami/)**: Common game mechanics * **[Alexandria Library](/libraries/alexandria/)**: Data structures and algorithms * **Community Libraries**: Third-party extensions (Coming Soon) ### Setting Up for Advanced Development #### Enhanced Development Environment Create a more sophisticated setup: ```bash # Project structure for larger applications mkdir my-advanced-game && cd my-advanced-game # Initialize with custom structure sozo init . --template minimal mkdir -p src/{models,systems,tests,utils} mkdir -p client/{web,mobile} mkdir -p docs/{design,api} ``` #### Development Scripts Create helper scripts for common workflows: ```bash # scripts/dev-setup.sh #!/bin/bash echo "🚀 Starting development environment..." katana --dev & sleep 3 sozo migrate --dev torii --dev & echo "✅ Ready for development!" # scripts/test-all.sh #!/bin/bash echo "🧪 Running comprehensive tests..." sozo test # Add integration tests # Add performance benchmarks ``` #### Advanced Tooling Set up additional development tools: * **Code formatters**: Scarb + Cairo formatter * **VS Code extensions**: Cairo language support * **Git hooks**: Pre-commit testing and formatting * **CI/CD pipelines**: Automated testing and deployment ### Staying Current The Dojo ecosystem evolves rapidly. Stay updated: #### Regular Updates * **Monthly**: Check for new Dojo releases * **Weekly**: Read community updates and discussions * **Daily**: Monitor Discord for breaking changes during development #### Best Practices * **Version pinning**: Lock Dojo versions in production * **Migration guides**: Follow upgrade paths carefully * **Breaking changes**: Test thoroughly before updating #### Community Engagement * **Share your projects**: Show off what you're building * **Ask questions**: The community is helpful and welcoming * **Contribute back**: Help others learn and improve the ecosystem ### Getting Unstuck When you encounter challenges: #### Technical Issues 1. **Check the [FAQ](/faq)** for common problems 2. **Search [GitHub issues](https://github.com/dojoengine/dojo/issues)** for similar problems 3. **Ask in [Discord](https://discord.gg/dojoengine)** with specific error messages 4. **Review troubleshooting guides** for systematic debugging (Coming Soon) #### Design Decisions 1. **Study existing games** in the showcase (Coming Soon) 2. **Read best practices guides** (Coming Soon) 3. **Discuss architecture** with the community 4. **Prototype quickly** to test ideas #### Learning Gaps 1. **Practice with [tutorials](/tutorials)** at your level 2. **Study reference documentation** for detailed APIs (Coming Soon) 3. **Build small projects** to reinforce learning ### Your Journey Continues Building with Dojo is a journey, not a destination. The ecosystem is rapidly evolving, with new patterns, tools, and possibilities emerging regularly. Whether you're building the next viral onchain game, creating innovative DeFi mechanisms, or pushing the boundaries of autonomous worlds, you now have the foundation to succeed. **Ready to build something amazing?** * Join our [Discord community](https://discord.gg/dojoengine) to connect with other builders * Explore the [tutorials section](/tutorials) for your next learning challenge * Check out the ecosystem showcase for inspiration (Coming Soon) Welcome to the future of onchain applications. Let's build something incredible together! 🚀 ## Understanding the Toolchain Now that you've built your first Dojo app, let's explore the tools that made it possible. Understanding each tool's role will help you develop more effectively and debug issues when they arise. :::note The code examples are run in the context of the `dojo-intro` repo. ::: ### Katana: Your Local Starknet **What it is**: Katana is a high-performance Starknet sequencer designed for development. **What it does**: * Provides a local sequencer environment * Mines blocks instantly for fast development * Comes with pre-funded accounts for testing * Supports gRPC for faster network communication #### Key Features When you run `katana --dev`, you get: ``` ██╗ ██╗ █████╗ ████████╗ █████╗ ███╗ ██╗ █████╗ ██║ ██╔╝██╔══██╗╚══██╔══╝██╔══██╗████╗ ██║██╔══██╗ █████╔╝ ███████║ ██║ ███████║██╔██╗ ██║███████║ ██╔═██╗ ██╔══██║ ██║ ██╔══██║██║╚██╗██║██╔══██║ ██║ ██╗██║ ██║ ██║ ██║ ██║██║ ╚████║██║ ██║ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ PREDEPLOYED CONTRACTS ================== | Contract | ETH Fee Token | Address | 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 | Class Hash | 0x00a2475bc66197c751d854ea8c39c6ad9781eb284103bcd856b58e6b500078ac ... PREFUNDED ACCOUNTS ================== | Account address | 0x127fd5f1fe78a71f8bcd1fec63e3fe2f0486b6ecd5c86a0466c3a21fa5cfcec | Private key | 0xc5b2fcab997346f3ea1c00b002ecf6f382c5f9c9659a3894eb783c5320f912 | Public key | 0x33246ce85ebdc292e6a5c5b4dd51fab2757be34b8ffda847ca6925edf31cb67 ... ACCOUNTS SEED ============= 0 2025-07-10T19:40:46.325567Z INFO katana_node: Starting node. chain=0x4b4154414e41 2025-07-10T19:40:46.325662Z INFO rpc: RPC server started. addr=127.0.0.1:5050 ``` :::note Katana's chain id, `0x4b4154414e41`, is just "KATANA" in ASCII! ::: #### Development vs Production **Development mode** (`--dev`): * Instant block mining * Pre-funded accounts * No fees or account validation **Production mode**: * Configurable mining intervals * Can fork existing chains * Can persist state to a database #### Katana Configuration Customize Katana's behavior: ```bash # Start in development mode without fees katana --dev --dev.no-fee # Automatically deploy the Controller contract katana --cartridge.controllers # Fork off of a specific chain & block katana --rpc-url --fork-block-number ``` ### Torii: Your Data Layer **What it is**: Torii is an automatic indexing engine that makes your World's data easily queryable. **What it does**: * Introspects and monitors all events from your World * Builds a searchable database of application state * Provides flexible GraphQL and gRPC APIs * Enables real-time client subscriptions #### How Torii Works 1. **Event Listening**: Monitors your World for state changes 2. **Data Processing**: Converts Cairo events into structured data 3. **Database Storage**: Maintains a SQL database of current state 4. **API Generation**: Automatically creates GraphQL schemas 5. **Real-time Updates**: Pushes changes to connected clients #### Using Torii's APIs After starting `torii --world `, you can access data through multiple interfaces. Let's make a query to retrieve all the instances of the `Position` model. ##### GraphiQL (Browser) Visit `http://localhost:8080/graphql` for an interactive query interface: ```graphql query { diPositionModels { edges { node { player x y } } } } ``` ##### GraphQL (Programmatic) ```bash curl -X POST http://localhost:8080/graphql \ -H "Content-Type: application/json" \ -d '{"query": "{ diPositionModels { edges { node { player x y } } } }"}' ``` :::tip You can learn more about GraphQL [here](https://graphql.org/learn). ::: ##### gRPC For high-performance applications, use the gRPC endpoint at `localhost:8080`. #### Torii Configuration Customize Torii's behavior: ```bash # Set the RPC endpoint to index (default localhost:5050) torii --rpc # Path to persistent storage (recommended for production) torii --db-dir torii.db # Enable CORS for local development torii --http.cors_origins "*" ``` ### Sozo: Your Command Center **What it is**: Sozo is the command-line interface for managing Dojo projects. **What it does**: * Initializes new projects * Compiles Cairo code * Deploys contracts * Manages World interactions * Handles authorization > **New to Sozo?** See our [complete Sozo reference](/toolchain/sozo/) for all available commands and options. #### Project Lifecycle with Sozo ##### 1: Project Creation ```bash # Clone the template into the my-game directory sozo init my-game --template dojoengine/dojo-starter (default) ``` ##### 2. Development ```bash # Compile your code (defaults to dev profile) sozo build # Compile your code with a different profile sozo build --profile staging # Run tests sozo test # Clean build artifacts sozo clean ``` :::info To build new profiles, add `[profile.]` to your project's `Scarb.toml`. ::: ##### 3. Deployment ```bash # Deploy to local Katana (defaults to dev profile) sozo migrate # Deploy to a staging environment sozo migrate --profile staging # Deploy to a production environment sozo migrate --profile mainnet ``` **Expected output for local build and deployment:** ``` # sozo build Compiling dojo_intro v1.5.0 Finished `dev` profile target(s) in 8 seconds # sozo migrate 🌍 World deployed at block 2 with txn hash: 0x06a03... ⛩️ Migration successful with world at address 0x04d97... ``` :::info To define new profiles, create a `dojo_.toml` file in your contract directory. ::: :::warning You **must** have a `dojo_dev.toml` file in your contract directory, or Sozo will complain. ::: After migrating, Sozo will generate a `manifest_.json` file, which contains information about the deployed contracts and interfaces. This file is meant to be consumed by clients to help them interact with the world. ##### 4. Interaction ```bash # Execute a system (note the namespacing) sozo execute di-actions spawn # Query historical events sozo events # Query model data (get keys from events) sozo model get Position ``` #### Sozo Profiles Manage different deployment environments with profiles: ``` contracts/ ├── dojo_dev.toml ├── dojo_staging.toml ├── Scarb.toml └── ... ``` Each profile defines basic data for a deployment: ```toml [world] name = "Dojo intro" description = "A simple intro to Dojo." seed = "123" # Changes the World address [namespace] default = "di" # Default namespace for the world [env] rpc_url = "http://localhost:5050/" # Katana instance account_address = "0x127f..." # Deployer address private_key = "0xc5b2..." # Deployer private key world_address = "0x04d9..." # For previously deployed worlds [writers] "di" = ["di-actions"] # target = grantee ``` See [this page](/framework/configuration) for more information about Sozo profiles. ### Saya: Your Proof Generator **What it is**: Saya generates cryptographic proofs of your World's execution. **What it does**: * Creates STARK proofs of game state transitions * Enables provable gaming and autonomous worlds * Supports different proving backends * Handles proof verification and settlement :::tip You **do not** need Saya for basic Dojo development. ::: #### When You Need Saya Saya becomes important when you want: * **Provable fairness**: Public guarantees of correct execution * **L1 settlement**: Settle state changes on Ethereum or other chains * **Competition integrity**: Tamper-proof tournaments and rankings For more information about Saya, [see the docs](/toolchain/saya). ### How The Toolchain Works Together Here's the typical development flow: 1. **Sozo** compiles your Cairo code into deployable contracts 2. **Katana** provides a local sequencer to deploy and test on 3. **Sozo** deploys your World to Katana 4. **Torii** automatically indexes your World's data 5. **Saya** (optionally) generates proofs for settlement ### Tool Configuration Each tool can be configured through various methods: #### Configuration Priority (highest to lowest) 1. Command-line flags 2. Environment variables 3. Configuration files 4. Default values #### Common Configuration Files * `dojo_dev.toml`: Sozo project and World settings * `torii_dev.toml`: Torii indexing and World settings * `katana.toml`: Katana network configuration settings ### Development Best Practices #### Keep Tools Running During development, keep all tools running in separate terminals: ```bash # Terminal 1 katana --config katana.toml # Terminal 2 torii --config torii_dev.toml # Terminal 3 # Your sozo commands ``` #### Use Profiles Set up different profiles for different environments: ```bash # Development sozo execute di-actions spawn # Testing on staging sozo execute di-actions spawn --profile staging ``` ### Troubleshooting Common Issues #### Katana Won't Start * Verify your `katana.toml` configuration is correct * Check if the port is in use (`lsof -i :5050`) #### Torii Can't Connect * Verify the World address in `torii_dev.toml` matches your migration output * Check that the World was successfully deployed (`sozo inspect`) #### Sozo Commands Fail * Verify you're in a Dojo project directory (look for `Scarb.toml`) * Check that you're using the correct account address and private key ### Next Steps Now that you understand the toolchain, you're ready to learn about the [development workflow](/getting-started/development-workflow) and start building more complex applications. Each tool has extensive documentation in the toolchain section for when you need to dive deeper into specific features and configurations. ## Your First Dojo App In this tutorial, you'll create your first Dojo project from scratch and deploy it locally. By the end, you'll have a working game where you can spawn and move a character using a browser-based client. ### What You'll Learn * The structure of a Dojo project * Basic Entity Component System (ECS) concepts * How to start the development environment * Deploying your first World * Interacting with your deployed game ### Step 1: Create Your Project First, clone the [Dojo Intro](https://github.com/dojoengine/dojo-intro) project, which implements a simple spawn-and-move game: ```bash git clone https://github.com/dojoengine/dojo-intro && cd dojo-intro ``` Now open up the repo in your code editor and explore the project: ``` dojo-intro/ ├── client/ ├── contracts/ │ ├── src/ │ │ ├── systems/ # Game logic (Systems) │ │ │ └── actions.cairo │ │ ├── lib.cairo # Main Cairo entry point │ │ └── models.cairo # Data models (Components) │ ├── dojo_dev.toml # Dojo configuration │ ├── katana.toml # Katana configuration │ ├── Scarb.toml # Cairo configuration │ └── torii_dev.toml # Torii configuration └── README.md # Project readme ``` > Note: Some files and directories are omitted for simplicity ### Step 2: Understand the Contract Structure Let's examine the key `contracts/` files to understand what we're building: #### Models (Entities & Components) Look at `contracts/src/models.cairo` to see the data structures: ```cairo #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Position { #[key] pub player: ContractAddress, pub x: u32, pub y: u32, } #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Moves { #[key] pub player: ContractAddress, pub remaining: u8, } ``` These models define: * **Position**: Where each player is located (x, y coordinates) * **Moves**: How many moves each player has remaining The `#[dojo::model]` attribute tells Cairo that the struct is a Dojo model. The `#[key]` attribute tells Cairo that `player` is the Dojo entity identifier for these components. :::info These attributes are examples of "procedural macros," a [Cairo language feature](https://www.starknet.io/cairo-book/ch12-10-procedural-macros.html) which tells the compiler to generate additional code at compile-time. This additional code is what enables Dojo functionality, such as specialized storage formats or events for the Torii indexer. ::: #### Contracts (Systems) Open `contracts/src/systems/actions.cairo` to see the game logic: ```cairo #[starknet::interface] pub trait IActions { fn spawn(ref self: T); fn move(ref self: T, direction: Direction); fn move_random(ref self: T); } #[dojo::contract] pub mod actions { // Implementation details... fn spawn(ref self: ContractState) { // Create a new player with starting position and moves } fn move(ref self: ContractState, direction: Direction) { // Move the player and decrease remaining moves } fn move_random(ref self: ContractState) { // Move the player randomly and decrease remaining moves } } ``` These functions define the game's core actions: * **spawn**: Create a new player at the starting position * **move**: Move a player in a direction (up, down, left, right) * **move\_random**: Use the [Cartridge vRNG](https://docs.cartridge.gg/slot/vrng) to move in a random direction #### Project Configuration Open `contracts/dojo_dev.toml` to see the project's Dojo configuration: ```toml [world] name = "Dojo intro" ... additional project details [namespace] default = "di" [env] rpc_url = "http://localhost:5050/" ... additional environment details [writers] "di" = ["di-actions"] ``` This configuration tells Dojo how to organize permissions when deploying the world. Specifically, it defines `di` as the default namespace and permits the systems defined in `actions.cairo` to write game state. For more details about configuration options, see the [configuration guide](/framework/configuration). ### Step 3: Start the Development Environment Now let's get the development tools running. You'll run three tools simultaneously, each in its own terminal: | Tool | Purpose | Terminal | | ---------- | ------------------------------- | ---------- | | **Katana** | Local blockchain sequencer | Terminal 1 | | **Sozo** | Build and deploy your contracts | Terminal 2 | | **Torii** | Index and serve game data | Terminal 3 | #### Terminal 1: Start Katana (Local Blockchain) First, start [Katana](/toolchain/katana), Dojo's high-performance sequencer: ```bash cd contracts && katana --config katana.toml ``` *This creates a fast, local Starknet environment for development* You'll see output like this: ``` 2025-07-10T15:46:43.495560Z INFO katana_core::backend: Genesis initialized ██╗ ██╗ █████╗ ████████╗ █████╗ ███╗ ██╗ █████╗ ██║ ██╔╝██╔══██╗╚══██╔══╝██╔══██╗████╗ ██║██╔══██╗ █████╔╝ ███████║ ██║ ███████║██╔██╗ ██║███████║ ██╔═██╗ ██╔══██║ ██║ ██╔══██║██║╚██╗██║██╔══██║ ██║ ██╗██║ ██║ ██║ ██║ ██║██║ ╚████║██║ ██║ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ PREDEPLOYED CONTRACTS ================== | Contract | ETH Fee Token | Address | 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 | Class Hash | 0x00a2475bc66197c751d854ea8c39c6ad9781eb284103bcd856b58e6b500078ac ... PREFUNDED ACCOUNTS ================== | Account address | 0x127fd5f1fe78a71f8bcd1fec63e3fe2f0486b6ecd5c86a0466c3a21fa5cfcec | Private key | 0xc5b2fcab997346f3ea1c00b002ecf6f382c5f9c9659a3894eb783c5320f912 | Public key | 0x33246ce85ebdc292e6a5c5b4dd51fab2757be34b8ffda847ca6925edf31cb67 ... ACCOUNTS SEED ============= 0 2025-07-10T15:46:43.496299Z INFO katana_node: Starting node. chain=0x4b4154414e41 2025-07-10T15:46:43.496406Z INFO rpc: RPC server started. addr=127.0.0.1:5050 ``` Keep this running - it's your local sequencer environment. :::tip By default, Katana runs on port 5050 ::: #### Terminal 2: Deploy Your World In a new terminal, run the following command using [Sozo](/toolchain/sozo), Dojo's build tool. ```bash cd contracts && sozo build && sozo migrate ``` *This compiles your Cairo contracts and deploys them to the local sequencer* **Expected output:** ``` # sozo build Compiling dojo_intro v1.5.0 (~/dojo-intro/contracts/Scarb.toml) Finished `dev` profile target(s) in 8 seconds # sozo migrate profile | chain_id | rpc_url ---------+----------+------------------------ dev | KATANA | http://localhost:5050/ 🌍 World deployed at block 2 with txn hash: 0x06a03fcc52739807ff4e7cd96fceb40b8fc194f3d7b370c1c8388eb380a4a069 ⛩️ Deploying 0 external contracts... ⛩️ Migration successful with world at address 0x04d9778a74d2c9e6e7e4a24cbe913998a80de217c66ee173a604d06dea5469c3 ``` #### Terminal 3: Start Torii (Indexer) In a third terminal you'll start Torii, Dojo's indexer: ```bash cd contracts && torii --config torii_dev.toml ``` *This watches your blockchain and creates queryable APIs for your game data* :::note If you look at `torii_dev.toml`, you'll see the same world address as that generated by `sozo migrate`. Sozo migrations are deterministic based on the seed defined in `dojo_dev.toml`. If you change the seed, the world address will change. ::: Torii will start indexing your world's data. You'll see: ``` 2025-07-10T15:59:27.961623Z INFO torii::relay::server: Relay peer id. peer_id=12D3KooWLaZ5bummiPSM2HHtRahELYUFWQA12NWNeMQ4aYpVhk9h 2025-07-10T15:59:27.966166Z INFO torii:runner: Starting torii endpoint. endpoint=127.0.0.1:8080 2025-07-10T15:59:27.966177Z INFO torii:runner: Serving Graphql playground. endpoint=127.0.0.1:8080/graphql 2025-07-10T15:59:27.966202Z INFO torii:runner: Serving SQL playground. endpoint=127.0.0.1:8080/sql 2025-07-10T15:59:27.966207Z INFO torii:runner: Serving MCP endpoint. endpoint=127.0.0.1:8080/mcp 2025-07-10T15:59:27.966210Z INFO torii:runner: Serving World Explorer. url=https://worlds.dev/torii?url=127.0.0.1%3A8080%2Fgraphql 2025-07-10T15:59:27.966212Z INFO torii:runner: Serving ERC artifacts at path path=/var/folders/dh/khjwlwbd6tx9p785khl57xjr0000gn/T/.tmpqeRBF0 2025-07-10T15:59:27.967054Z INFO torii::relay::server: Serving libp2p Relay. address=/ip4/127.0.0.1/tcp/9090 2025-07-10T15:59:27.967228Z INFO torii::relay::server: Serving libp2p Relay. address=/ip4/127.0.0.1/udp/9090/quic-v1 2025-07-10T15:59:27.967369Z INFO torii::relay::server: Serving libp2p Relay. address=/ip4/127.0.0.1/udp/9091/webrtc-direct/certhash/uEiB-v7fMcMjwd5WT_kgzPYOETr_IcyJ91zKbpG3AilHV_w 2025-07-10T15:59:27.967637Z INFO torii::relay::server: Serving libp2p Relay. address=/ip4/127.0.0.1/tcp/9092/ws 2025-07-10T15:59:27.974035Z INFO torii::indexer::processors::register_model: Registered model. namespace=di name=Moves 2025-07-10T15:59:27.974166Z INFO torii::indexer::processors::register_model: Registered model. namespace=di name=Position ``` Reading this output, you'll see that Torii has set up some local endpoints, which the client will use to fetch game state. You'll also see that Torii has registered the game models `Moves` and `Position` we saw earlier, within the `di` namespace. :::tip By default, Torii runs on port 8080 ::: Congratulations! Your Dojo development environment is up-and-running. ### Step 4: Launch Your Game Client Now let's test your game! First, let's launch your game client: ```bash cd client && pnpm install && pnpm run dev ``` You should see: ``` # pnpm install # Installation details ... Done in 409ms using pnpm v10.6.4 # pnpm run dev > client@1.0.0 dev /Users/kronosapiens/code/cartridge/dojo-intro/client > vite dev VITE v6.3.4 ready in 152 ms ➜ Local: https://localhost:5173/ ➜ Network: use --host to expose ➜ press h + enter to show help ``` Now navigate to [https://localhost:5173/](https://localhost:5173/) to reach the running client. :::note The Cartridge Controller *requires* https to connect. ::: You should see a page which looks like this: ![client-1](/getting-started/client-1.png) Click `Connect` to launch the Cartridge Controller log-in flow. Once you have logged in with Controller, you should be able to spawn a character and move them around. :::info Take a moment to appreciate what's happening under the hood. Every system execution is being sent by Controller as a transaction to Katana, which emits an event that is picked up by Torii, which is sent back to the client and rendered for you! ::: After a while, you should have something which looks like this: ![client-2](/getting-started/client-2.png) **Congratulations, you've just created your first provable game with Dojo!** ### Understanding What Happened Let's break down what you just accomplished: 1. **Defined Components**: `Position` and `Moves` store data about each player 2. **Implemented Systems**: The `spawn` and `move` functions modify component data 3. **Created a World**: Your World acts as the central registry for all game data, logic, and access control 4. **Used the Toolchain**: * Katana provided a local sequencer * Sozo managed compilation and deployment * Torii indexed data and sent it to the client ### Key Concepts #### Entity Component System (ECS) * **Entities**: Players in your game (identified by their address) * **Components**: Data attached to entities (`Position`, `Moves`) * **Systems**: Functions that operate on components (`spawn`, `move`) #### Dojo World Your World coordinates everything: * Registers all models and systems * Handles permissions and authorization * Provides a consistent interface for data access #### Event-Driven Updates When you execute a system, it emits events that Torii captures and indexes, which clients can subscribe to. ### Next Steps Congratulations! You've successfully: * ✅ Understood basic ECS concepts * ✅ Created and deployed your first Dojo project * ✅ Used the core development tools * ✅ Interacted with your deployed world Ready to dive deeper? Continue to [Understanding the Toolchain](./understanding-the-toolchain) to learn more about the tools that make Dojo development possible. When you're ready to explore the broader Dojo ecosystem, see [Next Steps](./next-steps) for advanced topics and deployment strategies. ## Execution Sharding on Starknet :::note The execution sharding architecture described in this document represents **planned functionality** currently under development. While the design is established, the implementation is not yet available in production. See the [Saya docs](/toolchain/saya/persistent) for more information. ::: ### Overview Execution sharding enables isolated transaction execution within independent shard environments ("Layer 3"s). Shards are independent sequencers that branch from Starknet mainnet, process transactions, then merge final state back to mainnet. This enables parallel processing across multiple shards while maintaining unified state anchored to Starknet mainnet. :::info Execution sharding differs from [sovereign rollups](/scaling/sovereign-rollups), in that sovereign chains are long-lived and **do not** commit state back to Starknet. ::: #### Key Benefits * **Parallel Execution**: Multiple shards process transactions simultaneously * **Cost Efficiency**: Only final state changes settle on mainnet * **Low Latency**: Gasless, high-throughput execution environments * **Unified State**: All shards merge back to single Starknet state ### Planned Architecture The proposed execution sharding system involves five key components: 1. **Starknet Mainnet**: The settlement layer where Dojo worlds are deployed 2. **World Contract**: Receives and incorporates state changes from completed shards 3. **Katana**: Acts as the sequencer running individual shards 4. **Saya**: Generates proofs of shard execution and submits them to the World Contract 5. **DojoOS**: A specialized Cairo program generating execution traces for validation ![Sharding execution overview](https://hackmd.io/_uploads/HyF9QoVX1g.png) ### Proposed Shard Initialization A shard begins when an initialization transaction is submitted to the world contract, specifying: * Unique shard identifier * Designated operator address * Initial state conditions * Settlement configuration for mainnet data merging The operator then spins up a Katana instance, branching from Starknet mainnet to create the shard environment. #### Planned Data Structure The shard initialization would use a data structure similar to: ```rust struct ShardStartData { // Unique identifier for the shard shard_id: felt252, // Address of the shard operator running the sequencer operator: felt252, // Initial state configuration initial_state: ShardInitialState { // Contracts authorized for execution within the shard contracts: Span, // Event indicating shard completion end_event: EventSelector { // Contract address that will emit the completion event from: felt252, // Event selector to monitor selector: felt252, }, }, // Settlement configuration settlement_config: SettlementConfig { // Storage addresses to update on the World Contract storage_addresses: Span, }, } ``` #### Execution Environment The shard branches from mainnet at the initialization block, then operates independently as a gasless environment. Since contract logic is already defined on mainnet, shards execute without additional contract declarations while maintaining mainnet compatibility. ### Planned Execution Flow Once initialized, the shard sequencer processes transactions within the isolated environment. #### Access Control Options 1. **Contract Whitelisting**: Restrict execution to predefined contracts 2. **Account Whitelisting**: Allow only specific accounts to submit transactions 3. **Declaration Restrictions**: Prevent new contract declarations 4. **Permissionless Execution**: Rely on existing mainnet permissions Access restrictions may be necessary to prevent DoS attacks, depending on use case and security requirements. ![Shard execution flow](https://hackmd.io/_uploads/Sk6WByV7yg.png) ### Planned Completion Detection and Settlement #### Completion Detection A specific event emitted by a designated Cairo contract signals shard completion, indicating all intended transactions are processed and the shard is ready for settlement. #### Proposed Settlement Pipeline Once the completion event is detected, [Saya](/toolchain/saya) would initiate the settlement process: 1. **Execution Trace Generation**: DojoOS generates an execution trace similar to SNOS but optimized for shard validation: * Validates transaction sequences rather than blocks * Outputs only modified storage addresses per settlement config * Supports shard-specific validation rules 2. **Proof Generation**: Saya submits the trace to a proving service (e.g., Atlantic) for cryptographic proof generation 3. **Layout Bridge**: Proof conversion to `recursive_with_poseidon` layout for Starknet verification 4. **Settlement**: Final proof and storage updates submitted to the world contract :::note The DojoOS component and shard settlement pipeline are currently in development. The existing Saya proving service supports SNOS-based proving for standard Starknet blocks. ::: ### Proposed Concurrency Management with CRDTs Concurrent shard execution requires managing consistent state across Starknet to prevent inconsistencies during settlement. #### CRDT Integration The Dojo team is exploring Conflict-Free Replicated Data Types (CRDTs) for concurrency management. CRDTs enable concurrent updates across distributed systems without complex locking, with mathematical guarantees that changes converge to a valid global state during settlement. #### Game Example: Dungeon Adventures Consider a dungeon game where multiple players participate across different shards: ##### Experience Points (Grow-Only Counter) XP can only increase, ideal for grow-only counter CRDTs. Each shard independently awards XP; final XP is the sum across all shards. ##### Gold (Escrow-based System) Gold can increase/decrease, requiring careful handling. Player gold is "escrowed" (locked on mainnet) when entering a shard, preventing double-spending. ##### Unique Items (Lock-based Control) Legendary items require exclusive access. Items lock on mainnet when used in a shard, releasing when execution completes. #### Planned CRDT Types The research focuses on three CRDT patterns for initial implementation: 1. **Grow-Only Counters**: For monotonically increasing values 2. **Escrow Mechanisms**: For controlled resource spending 3. **Lock-based Controls**: For unique assets requiring exclusive access ![CRDT execution model](https://hackmd.io/_uploads/r1_3r1E7Jg.png) ### Current Limitations and Challenges While execution sharding offers significant potential for scaling and parallel execution, several technical challenges must be addressed: #### 1. Proving Time Bottleneck Proof generation and verification is computationally intensive, potentially creating settlement delays and state update backlogs. #### 2. All-or-Nothing Proof Validation Invalid proofs require discarding entire shard execution, even if only a small portion of transactions were problematic. #### 3. Limited Cross-Shard Communication No direct communication between active shards restricts complex interactions requiring immediate state awareness across parallel executions. #### 4. Implementation Status The complete system remains in development: * DojoOS Cairo program for shard validation * CRDT integration for concurrency management * Shard initialization and completion mechanisms * Integration testing across all components :::note Current development focuses on the core proving infrastructure through Saya and SNOS integration. The specialized DojoOS and shard management components are planned for future releases. ::: ## Sovereign Rollup Using Celestia and Dojo ![Sovereign Worlds](/scaling/sw.png) ### Introduction This documentation outlines the design and implementation of sovereign rollups built with Celestia and Dojo's infrastructure stack. Sovereign rollups provide scalable and decentralized execution by leveraging Celestia's modular data availability layer combined with Starknet's cryptographic proofs. For alternative scaling approaches that maintain stronger connection to mainnet, see [execution sharding](/scaling/execution-sharding). :::tip See [Mage Duel](https://mageduel.evolute.network/) for an example of a game built using this architecture. ::: #### Architecture Overview Three components power the sovereign rollup: * [**Katana**](/toolchain/katana): Sequences transactions into blocks * [**Saya**](/toolchain/saya): Generates proofs from Katana blocks and posts to Celestia * [**Celestia**](https://celestia.org/): Stores proofs as blobs with retrieval metadata #### Key Features * **State Reconstruction**: Full state rebuilding from latest proof * **Proof Chaining**: Headers enable retrieval of previous proofs via commitment/height * **Batch Aggregation**: Multiple blocks can be proven together for efficiency :::note This implementation shares conceptual similarities with **Rollkit**, another framework for building sovereign rollups using modular blockchain architecture. The difference lies in the underlying infrastructure: this solution is built specifically around Dojo's proven stack (Katana and Saya), while Rollkit uses different technical components. Both approaches enable developers to deploy customizable sovereign rollups with similar capabilities. ::: ### Technical Architecture ![Sovereign Rollup Architecture](/scaling/celestia-sw-diagram.png) #### 1. Katana - Block Producer Katana sequences transactions into blocks within the sovereign rollup. ##### Sync Mode Katana reconstructs complete state from Celestia proofs: * **State Recovery**: Rebuilds rollup state from latest Celestia proof * **Requires**: Celestia height and commitment of latest proof * **Process**: Retrieves prior proofs backward until full state reconstruction * **Independence**: Nodes start/resume using only Celestia data #### 2. Saya - Prover and Proof Poster Saya generates proofs from Katana blocks and posts them to Celestia. ##### Workflow * **Polling**: Actively retrieves new blocks from Katana * **STARK Proving**: Generates cryptographic proofs of block validity and state updates * **Posting**: Submits proofs to Celestia for storage ##### Proof Aggregation * **Batching**: Combines multiple blocks into single proofs * **Efficiency**: Reduces proving and storage costs * **Scalability**: Handles higher transaction volumes * **Completeness**: Maintains all state transition data #### 3. Celestia - Data Availability Celestia provides decentralized storage for rollup proofs. ##### Blob Storage * **Proof Blobs**: Stores STARK proofs with retrieval metadata * **Navigation**: Headers enable proof retrieval via commitment/height * **Reconstruction**: State rebuilding by following proof chains * **Decentralization**: No centralized coordinator required ##### Header Format Each blob contains metadata for rollup continuity: **`prev_state_root`**: Previous rollup state root, ensuring transition continuity **`state_root`**: Current rollup state root after STARK-verified transitions **`prev_height`**: Celestia height of previous proof blob for efficient retrieval **`prev_commitment`**: Previous blob's commitment hash, required for Celestia retrieval **`proof`**: STARK proof with state updates enabling integrity verification ### Implementation Notes Current optimizations under consideration: #### Network Architecture * **Progress Tracking**: Nodes use latest Celestia height/commitment only * **Namespace**: Predetermined and shared among participants * **Future**: P2P network for propagating chain heads and updates #### Proof Format * **Current**: String format converted to bytes for Celestia * **Future**: Compression using Starknet felts for improved efficiency ### Onchain games to provable games Onchain games promise us freedom of expression and sovereignty over our information. They possess these properties because they run on a blockchain verified by consensus. Provable games, using zk proofs, allow for the verification of game state and computations without large consensus schemes. Written in languages like Cairo, Noir or running RISC-Zero, these games can operate independently on an isolated zkVM like a browser, with verifiable outputs ensuring truthful execution. This broadens our possibilities in the onchain gaming industry. An illustrative example is a game like Donkey Kong. Currently, to have your high score recognized on the leaderboard, you must play on a certified machine to prevent cheating, while recording your gameplay. However, if Donkey Kong were a provable game, players could compete in isolation. Achieving a high score would simply require submitting a proof to the Donkey Kong organization for verification. This method allows players to establish themselves as the King of Kong from the comfort of their home, without the need to record their gameplay! It's important to note that the approach isn't binary. For instance, you could operate an onchain game on an EVM and then layer a Cairo-based game on top, enhancing the core game while broadening its capabilities. For more information about Starknet, Cairo and its tech stack, check out the [Starknet & Cairo book](https://book.starknet.io/). ### Autonomous Worlds > "Autonomous worlds represent persistent, permissionless, and decentralized open environments that users can freely interact with and contribute to." The precise definition of Autonomous Worlds (AWs) remains somewhat elusive, as it is more of an abstract concept that has yet to be fully crystallized. Lattice first [introduced](https://0xparc.org/blog/autonomous-worlds) the terminology in 2022, but the notion of open worlds operating on the blockchain has been around for a while. The abstraction introduced by MUD served as a catalyst for the market to recognize the potential of these worlds. Autonomous Worlds share notable similarities with blockchains in their fundamental nature. Once established, they persist, maintaining their state throughout the lifespan of the chain. Players can join or leave, and developers can expand these worlds by deploying features in a permissionless manner, much like how contracts are added to a chain. While there is no universally accepted definition for an Autonomous World, we believe that a game must possess at least the following two essential features to be considered as such: 1. Decentralized data availability layer: While the state execution may reside on a centralized layer, it is crucial that the state can be reconstructed if the execution layer ceases to exist. Rollups offer a solution, providing increased capacity execution layers while ensuring data is permanently settled on Ethereum. This guarantees the world's perpetual persistence. 2. Permissionless entry point for expanding the world: The World contract must be capable of accepting new systems and components without requiring permission. While this doesn't imply that every component and system will be utilized, they must adhere to this pattern, ensuring open and unrestricted access for potential enhancements. We're firm believers in the potential for Autonomous Worlds to catalyze the exploration of novel forms in the medium provided by zk proofs and blockchain technology. This is not only about games, but also about new forms of artwork, coordination, fun, emerging from tinkering and radical innovation, eventually questioning the very notion of "play" in this brave new decentralized and trustless world. #### Homework * [Wired - Autonomous Worlds Primer](https://www.wired.com/story/autonomous-worlds-aim-to-free-online-games-from-corporate-control/) * [0xParc - Autonomous Worlds (Part 1)](https://0xparc.org/blog/autonomous-worlds) * [Gubsheep - The Strongest Crypto Gaming Thesis](https://gubsheep.substack.com/p/the-strongest-crypto-gaming-thesis) * [Lattice - MUD: An engine for Autonomous Worlds](https://lattice.xyz/blog/mud-an-engine-for-autonomous-worlds) * [Guiltygyoza - Game 2.0](https://www.guiltygyoza.xyz/2022/07/game2) * [Guiltygyoza - Composable Engineering](https://www.guiltygyoza.xyz/2023/05/composable-engineering) * [Jay Springett - Wind-up Worlds](https://www.thejaymo.net/2022/05/06/wind-up-worlds/) * [Are.na collection on Autonomous Worlds](https://www.are.na/sylve-chevet/on-chain-realities-and-autonomous-worlds) * [Tarrence - Provable Games](https://www.dojoengine.org/en/articles/provable-games/) * [Loaf - Provable Goblins](https://loaf.coffee/posts/provable-goblins) ## Cainome [**Cainome**](https://github.com/cartridge-gg/cainome) is the foundational binding generator for Cairo smart contracts, providing the parsing infrastructure and plugin architecture that powers client binding generation -- enabling type-safe interaction with Cairo contracts. The name combines "Cairo" and "Genome" - representing the DNA of the Cairo ecosystem that expresses into different language bindings. ##### Core Value Proposition * **Type Safety**: Compile-time validation prevents runtime errors * **Code Generation**: Eliminates manual binding creation and maintenance * **Multi-Language Foundation**: Single parser supports multiple target languages * **Cairo Expertise**: Deep understanding of Cairo's unique type system and serialization #### What are Bindings? **Language bindings** are code libraries that allow programs written in one language to interact with systems written in another language. In blockchain development, smart contracts are often written in specialized languages (like Cairo or Solidity), while client applications use general-purpose languages (like Rust or TypeScript). Bindings solve this integration challenge by: * **Translating function calls** from the client language to the contract's native format * **Converting data types** between different language type systems * **Handling serialization** to transform data for network transmission * **Providing type safety** to catch errors at compile-time rather than runtime For example, without bindings, calling a Cairo contract from Rust would require manually: ```rust // Manual contract interaction (error-prone) let calldata = vec![ Felt::from_hex("0x123...")?, // What does this represent? Felt::from(42_u32), // Raw numeric conversion // ... more manual serialization ]; let result = provider.call(contract_address, "mystery_function", calldata).await?; // Result is raw Felt values - what do they mean? ``` With generated bindings, the same interaction becomes: ```rust // Type-safe binding (clear and safe) let result = contract.transfer_tokens(recipient_address, amount).call().await?; // Result is a strongly-typed struct with meaningful fields ``` ### Architecture Cainome uses a plugin-based architecture with a shared parsing core: ``` Cairo Contract ABI ↓ Cainome Parser (Rust) ↓ Token Representation ↓ Language Plugins ↓ ↓ ↓ Rust Go TypeScript ``` #### Core Components * **Parser Core**: Rust ABI parser handles both current and legacy Cairo formats * **Plugin System**: Extensible architecture supports built-in and external plugins * **Type Mapping**: Sophisticated mapping between Cairo types and language types * **Serialization**: Automatic handling of Cairo's unique serialization Cainome provides the foundation for multiple language bindings through its plugin system. Several targets (Rust, Go) are supported by Cainome's CLI directly, while others (TypeScript, Unity) are available through ecosystem plugins. ### Usage Cainome provides both compile-time and CLI approaches for binding generation: #### Rust Integration **Compile-time macro (automatic):** ```rust use cainome::rs::abigen; // Bindings generated automatically during compilation abigen!(MyContract, "./target/dev/contract.contract_class.json"); #[tokio::main] async fn main() { // Contract struct immediately available let contract = MyContract::new(contract_address, account); // Call view functions let balance = contract.get_balance(player_address).call().await.unwrap(); println!("Player balance: {}", balance); } ``` **CLI generation (manual):** ```bash cainome --rust --output-dir ./bindings --execution-version v3 contract.json ``` #### Go Integration **Inline directive (automatic):** ```go //go:generate cainome --golang --golang-package mycontract --output-dir ./bindings ./contract.json package main import ( "context" "fmt" "mycontract" // Generated bindings ) func main() { // Run: go generate ./... // Create contract reader for view functions reader := mycontract.NewReader(contractAddress, provider) // Call view functions with type safety balance, err := reader.GetBalance(context.Background(), playerAddress) if err != nil { panic(err) } fmt.Printf("Player balance: %s\n", balance) } ``` **CLI generation (manual):** ```bash cainome --golang --golang-package mycontract --output-dir ./bindings contract.json ``` #### Other Targets **TypeScript/Unity (via Sozo):** ```bash sozo bindgen --typescript sozo bindgen --unity ``` :::tip For more binding generation workflows, see the [Sozo bindgen documentation](/toolchain/sozo/binding-generation). ::: ## Advanced Tutorials This section covers more complex topics and advanced use cases. Each tutorial builds upon the foundational concepts. ### Prerequisites Before starting these tutorials, you should have completed the [Dojo Starter tutorial](/tutorials/dojo-starter), which covers the foundational concepts. You'll also need a good understanding of the core concepts. ### Tutorial List 1. **Advanced Configuration** - Learn how to customize advanced settings. This includes performance tuning and optimization techniques. 2. **Integration Patterns** - Explore different ways to integrate with external systems. We'll cover REST APIs, webhooks, and message queues. 3. **Scalability Considerations** - Understand how to scale your application. This tutorial covers load balancing and distributed architectures. ### Getting Help If you run into issues, check the troubleshooting guide first. You can also reach out on our community forum for assistance. ## Dojo React import { LinkCard } from "../../components/LinkCard"; ## Dojo 101 Tutorial :::note This section assumes that you have already installed the Dojo toolchain and are familiar with Cairo. If not, please refer to the [installation](/installation) section and the [Cairo book](https://book.cairo-lang.org). ::: ### Dojo in 15 minutes or less To start, let's create a new project to run locally on your machine. ```bash 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](https://github.com/dojoengine/dojo-starter). 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): ```bash ├── 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. ```rust // ... #[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. ```rust // ... #[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`](/framework/models/introspection) trait, which allows the compiler to introspect the struct and generate the necessary code to interact with it.
### Contract Systems A dojo contract is a Starknet contract annotated with the `#[dojo::contract]` attribute. Like any Starknet contract, it defines an interface and an implementation. Let's examine the `src/systems/actions.cairo` file. ```rust use dojo_starter::models::{Direction, Position}; // define the interface #[starknet::interface] trait IActions { 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 { 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. ```rust 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: ```bash sozo build ``` That compiled the models and systems into artifacts that can be deployed. Simple as that! Now, let's deploy it to [Katana](/toolchain/katana)! First, we need to get Katana running. Open a second terminal and execute: ```bash katana --dev --dev.no-fee ``` Success! [Katana](/toolchain/katana) should now be running locally on your machine. Now, let's inspect the world. In your primary terminal, execute: ```bash 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: ```bash sozo migrate ``` This command will deploy the artifact to `Katana`. You should see terminal output similar to this: ```console 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](/framework/configuration)). ```toml [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`. ```toml [dependencies] dojo = "1.8.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: ```bash torii --world 0xb4079627ebab1cd3cf9fd075dda1ad2454a7a448bf659591f259efa2519b18 --http.cors_origins "*" ``` Running the command mentioned above starts a [Torii](/toolchain/torii) server on your local machine. This server uses SQLite as its database and is accessible at [http://0.0.0.0:8080/graphql](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: ```console 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 // [!code focus] 2024-06-14T04:38:33.481562Z INFO torii_core::processors::register_model: Registered model. name=Position // [!code focus] 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: ```graphql query { models { edges { node { id name classHash contractAddress } } totalCount } } ``` After you run the query, you will receive an output like this: ```json { "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: ```graphql 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: ```bash 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: ```json { "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](/toolchain/torii/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?
Coming soon! import { LinkCard } from "../../components/LinkCard"; #### Quickstart with React Concise. Get started with Dojo in minutes. This assumes knowledge of React and the Dojo CLI. Prerequisites: * [Dojo CLI installed](/installation) 1. Create project ```bash npx @dojoengine/create-dojo start -t example-vite-react-sdk ``` 2. Run development network with Katana ```bash # In new terminal katana --dev --dev.no-fee --http.cors_origins "*" ``` 3. Build and deploy contracts ```bash cd dojo-starter sozo build sozo migrate ``` From the `sozo migrate` command output, find the deployed world address. For example: ```log profile | chain_id | rpc_url ---------+----------+------------------------ dev | KATANA | http://localhost:5050/ 🌍 World deployed at block 2 with txn hash: 0x038e984efa3e91e045b33d14e63c5e9f765e5a8fe2b3546fc3ab872f608e37a2 ⛩️ Migration successful with world at address 0x00e2ea9b5dd9804d13903edf712998943b7d5d606c139dd0f13eeb8f5b84da8d ``` The world address in this case would be `0x00e2ea9b5dd9804d13903edf712998943b7d5d606c139dd0f13eeb8f5b84da8d`. 4. Run indexer with `torii` Start the indexer `torii` with the world address from the last step: ```bash # In new terminal torii --http.cors_origins "*" --world YOUR_WORLD_ADDRESS ``` The command for the example world address above would be: ```bash # Example command only. Use your own world address. torii --http.cors_origins "*" --world 0x00e2ea9b5dd9804d13903edf712998943b7d5d606c139dd0f13eeb8f5b84da8d ``` 5. Setup client ```bash cd client pnpm i pnpm dev ``` Visit `http://localhost:5173` to view the project. You should now see the project connected to the network and indexed! #### Next Steps This should have given you a good starting point to build your first application with Dojo. Here are some next steps to take:
## dojo.bevy Bevy is a modern, data-driven game engine built in Rust that leverages the Entity Component System (ECS) architecture. Known for its performance, modularity, and ergonomic API, Bevy enables developers to create everything from simple 2D games to complex 3D experiences with compile-time safety and zero-cost abstractions. dojo.bevy is the official Bevy engine SDK for interacting with Dojo worlds, providing native Rust integration for building high-performance onchain games. Built specifically for Bevy's ECS architecture, it seamlessly integrates with Bevy's component system while maintaining the performance and safety guarantees that Rust developers expect. ### Core Concepts Before diving into building onchain games with Bevy, let's explore the essential components of the dojo.bevy architecture: #### `DojoPlugin` The **`DojoPlugin`** is the central Bevy plugin that manages all connections to Torii and Starknet. It handles async task coordination and event emission within Bevy's single-threaded execution model. ```rust use bevy::prelude::*; use dojo_bevy_plugin::DojoPlugin; fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugins(DojoPlugin) // Add the Dojo plugin .run(); } ``` #### `DojoResource` The **`DojoResource`** serves as the main interface for all Dojo operations. It manages connections to both Torii (for querying entities) and Starknet (for executing transactions). ```rust use dojo_bevy_plugin::{DojoResource, TokioRuntime}; fn connect_to_dojo( mut dojo: ResMut, tokio: Res ) { // Connect to Torii indexer dojo.connect_torii(&tokio, "http://localhost:8080".to_string(), world_address); // Connect to Starknet using predeployed account dojo.connect_predeployed_account(&tokio, "http://localhost:5050".to_string(), 0); } ``` #### Event System dojo.bevy leverages Bevy's event system for reactive blockchain interactions: * **`DojoInitializedEvent`**: Emitted when connections to Torii and Starknet are established * **`DojoEntityUpdated`**: Emitted when entity state changes are received from Torii ```rust use dojo_bevy_plugin::{DojoInitializedEvent, DojoEntityUpdated}; fn handle_dojo_events( mut ev_initialized: EventReader, mut ev_entity_updated: EventReader, ) { for _ in ev_initialized.read() { info!("Dojo connections established!"); } for ev in ev_entity_updated.read() { info!("Entity {} updated with {} models", ev.entity_id, ev.models.len()); } } ``` #### `TokioRuntime` The **`TokioRuntime`** resource provides async execution capabilities within Bevy's single-threaded environment. This is essential for blockchain operations that require asynchronous processing. ### Getting Started #### Prerequisites Before getting started, ensure you have: * [Rust](https://rustup.rs/) `>= 1.70.0` installed * [Bevy](https://bevyengine.org/) `>= 0.16.0` in your project #### Installation Add dojo.bevy to your `Cargo.toml`: ```toml [dependencies] starknet = "0.16" bevy = "0.16.0" dojo_bevy_plugin = { git = "https://github.com/dojoengine/dojo.bevy" } tokio = { version = "1.0", features = ["full"] } ``` #### Basic Setup Create a basic Bevy application with Dojo integration: ```rust use bevy::prelude::*; use dojo_bevy_plugin::{ DojoPlugin, DojoResource, TokioRuntime, DojoInitializedEvent, DojoEntityUpdated }; use starknet::core::types::Felt; // Your world and contract addresses (from sozo migrate output) const WORLD_ADDRESS: Felt = Felt::from_hex_unchecked("0x07cb61df..."); const TORII_URL: &str = "http://localhost:8080"; const KATANA_URL: &str = "http://localhost:5050"; fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugins(DojoPlugin) .init_resource::() .init_resource::() .add_systems(Startup, setup_dojo) .add_systems(Update, handle_dojo_events) .run(); } fn setup_dojo( mut dojo: ResMut, tokio: Res ) { // Initialize connections dojo.connect_torii(&tokio, TORII_URL.to_string(), WORLD_ADDRESS); dojo.connect_predeployed_account(&tokio, KATANA_URL.to_string(), 0); } fn handle_dojo_events( mut ev_initialized: EventReader, mut ev_entity_updated: EventReader, ) { for _ in ev_initialized.read() { info!("Dojo initialized successfully!"); } for ev in ev_entity_updated.read() { info!("Entity {} updated", ev.entity_id); } } ``` ### Usage Patterns #### Querying Entities Fetch entities from your Dojo world using Torii queries: ```rust use torii_grpc_client::types::{Query as ToriiQuery, Pagination, PaginationDirection}; fn fetch_entities( mut dojo: ResMut, tokio: Res, keyboard: Res>, ) { if keyboard.just_pressed(KeyCode::KeyF) { let query = ToriiQuery { clause: None, pagination: Pagination { limit: 100, cursor: None, direction: PaginationDirection::Forward, order_by: vec![], }, no_hashed_keys: false, models: vec![], historical: false, }; dojo.queue_retrieve_entities(&tokio, query); } } ``` #### Subscribing to Updates Set up real-time subscriptions to track entity changes: ```rust fn setup_subscriptions( mut dojo: ResMut, tokio: Res, keyboard: Res>, ) { if keyboard.just_pressed(KeyCode::KeyS) { // Subscribe to all position updates dojo.subscribe_entities(&tokio, "position_updates".to_string(), None); } } ``` #### Executing Transactions Send transactions to your Dojo systems: ```rust use starknet::core::types::Call; use starknet::macros::selector; // Contract addresses from your deployment const ACTION_ADDRESS: Felt = Felt::from_hex_unchecked("0x0693bc04..."); fn handle_player_actions( mut dojo: ResMut, tokio: Res, keyboard: Res>, ) { if keyboard.just_pressed(KeyCode::Space) { // Spawn a new entity let spawn_call = Call { to: ACTION_ADDRESS, selector: selector!("spawn"), calldata: vec![], }; dojo.queue_tx(&tokio, vec![spawn_call]); } if keyboard.just_pressed(KeyCode::ArrowLeft) { // Move entity left let move_call = Call { to: ACTION_ADDRESS, selector: selector!("move"), calldata: vec![Felt::from(0u32)], // Direction: Left }; dojo.queue_tx(&tokio, vec![move_call]); } } ``` ### Example Project Here's a pedagogical example showing the key concepts for a 3D game where players can spawn and move cubes: ```rust use bevy::prelude::*; use dojo_bevy_plugin::{ DojoPlugin, DojoResource, TokioRuntime, DojoInitializedEvent, DojoEntityUpdated }; use starknet::core::types::{Call, Felt}; use starknet::macros::selector; // Configuration constants const WORLD_ADDRESS: Felt = Felt::from_hex_unchecked("0x07cb61df..."); const ACTION_ADDRESS: Felt = Felt::from_hex_unchecked("0x0693bc04..."); fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugins(DojoPlugin) // 1. Add the Dojo plugin .init_resource::() .init_resource::() .add_systems(Startup, setup_dojo) .add_systems(Update, (handle_keyboard_input, handle_dojo_events)) .run(); } fn setup_dojo( mut dojo: ResMut, tokio: Res ) { // 2. Connect to Torii and Starknet dojo.connect_torii(&tokio, "http://localhost:8080".to_string(), WORLD_ADDRESS); dojo.connect_predeployed_account(&tokio, "http://localhost:5050".to_string(), 0); } fn handle_keyboard_input( mut dojo: ResMut, tokio: Res, keyboard: Res>, ) { if keyboard.just_pressed(KeyCode::Space) { // 3. Execute transactions let spawn_call = Call { to: ACTION_ADDRESS, selector: selector!("spawn"), calldata: vec![], }; dojo.queue_tx(&tokio, vec![spawn_call]); } } fn handle_dojo_events( mut ev_initialized: EventReader, mut ev_entity_updated: EventReader, ) { // 4. React to blockchain events for _ in ev_initialized.read() { info!("Connected to Dojo world!"); } for ev in ev_entity_updated.read() { info!("Entity {} updated with {} models", ev.entity_id, ev.models.len()); // Process entity updates and sync with Bevy components } } ``` #### Key Learning Points 1. **Plugin Integration**: Add `DojoPlugin` to enable blockchain connectivity 2. **Resource Management**: Use `DojoResource` and `TokioRuntime` for async operations 3. **Transaction Execution**: Queue transactions with `dojo.queue_tx()` 4. **Event Handling**: React to `DojoInitializedEvent` and `DojoEntityUpdated` events For a complete implementation including 3D rendering, entity tracking, and full game logic, see the [full example](https://github.com/dojoengine/dojo.bevy/blob/main/examples/intro.rs). ### Advanced Features #### Account Management For production applications, you'll want to use custom accounts instead of predeployed ones: ```rust fn setup_custom_account( mut dojo: ResMut, tokio: Res ) { let account_address = Felt::from_hex("0x1234...").unwrap(); let private_key = Felt::from_hex("0x5678...").unwrap(); dojo.connect_account( &tokio, "https://api.cartridge.gg/x/your-game/katana".to_string(), account_address, private_key ); } ``` #### Batch Transactions Execute multiple system calls in a single transaction: ```rust fn batch_operations( mut dojo: ResMut, tokio: Res ) { let calls = vec![ Call { to: ACTION_ADDRESS, selector: selector!("spawn"), calldata: vec![], }, Call { to: ACTION_ADDRESS, selector: selector!("move"), calldata: vec![Felt::from(1u32)], }, ]; dojo.queue_tx(&tokio, calls); } ``` #### Entity Management * Use Bevy's change detection to minimize unnecessary updates * Batch entity operations when possible * Consider using Bevy's sparse sets for entities with optional components ```rust fn optimized_position_updates( mut ev_position_updated: EventReader, mut query: Query<&mut Transform, (With, Changed)>, ) { // Only process entities that have actually changed for ev in ev_position_updated.read() { // Update logic here } } ``` ### Troubleshooting Enable debug logging to troubleshoot issues: ```rust use bevy::log::LogPlugin; fn main() { App::new() .add_plugins(DefaultPlugins.set(LogPlugin { level: bevy::log::Level::DEBUG, ..default() })) .add_plugins(DojoPlugin) .run(); } ``` ## dojo.godot Godot Engine is a free, open-source cross-platform game engine renowned for its flexibility, ease of use, and powerful scene system. With its intuitive node-based architecture, GDScript scripting language, and robust 2D and 3D capabilities, Godot empowers developers to create everything from indie platformers to complex multiplayer experiences. dojo.godot is the official Godot Engine SDK for building onchain games powered by Dojo. This GDExtension seamlessly integrates blockchain functionality into your Godot projects, built on the [dojo.c](/client/sdk/c) foundation. It enables you to create fully decentralized games without compromising on performance or developer experience. :::tip Always [Download](https://github.com/lonewolftechnology/godot-dojo/releases) the latest version. ::: :::info If there is something that is not covered here, please refer to the in-editor documentation. You can access it by pressing Ctrl + Left Click on a class name, or by pressing F1 and using the search bar. ::: ### Core Concepts #### Torii Client The `ToriiClient` is your gateway to the Dojo world, managing all communication with the blockchain indexer. Key responsibilities: * **Connection Management**: Establish and maintain connections to Torii servers. * **Entity Queries**: Fetch game entities and their associated models from the blockchain. * **Event Subscriptions**: Subscribe to real-time blockchain events and entity updates. #### DojoSessionAccount The `DojoSessionAccount` handles all transaction-related operations and wallet management. Core features: * **Wallet Authentication**: Secure connection to Cartridge Controller accounts. * **Transaction Execution**: Execute smart contract calls with proper signing. * **Session Management**: Maintain authenticated sessions with configurable policies. #### Dojo Calls Used for smart contract function calls. Structure: * **Contract Address**: The address of the target smart contract. * **Function Name (`entrypoint/method`)**: The name of the function to call. * **Call Data**: An array of parameters to pass to the function. #### Helper Classes Two helper classes were added to simplify common tasks: * `ControllerHelper`: Provides utility functions for session management, key generation, and calldata handling. * `GodotDojoHelper`: Offers utility for float-point conversion and extension configuration. ### Editor Utilities Some in-editor tools were added under `Project -> Tools -> Godot Dojo Tools` #### Cairo Type System dojo.godot automatically handles conversions between Cairo types and Godot equivalents: * **Primitives**: `u8`, `u16`, `u32`, `u64`, `u128`, `u256`, `felt252` map to Godot integers and strings. * **Structures**: Cairo structs convert to Godot Dictionaries with proper field mapping. * **Arrays**: Cairo arrays become Godot Arrays with automatic element conversion. * **Enums**: Cairo enums map to Godot integers with enumeration support. :::info Godot does not natively support big integers like `i128`, `u128`, and `u256`. However, the extension supports them using wrapper classes to store and display the data. `I128`, `U128`, and `U256` wrappers can be used directly inside the `calldata` array. ::: ### Getting Started We will be using `dojo-starter` contract for this example. ::::steps ##### Download the latest version Visit the [release page](https://github.com/lonewolftechnology/godot-dojo/releases) and download the latest version for your platform. ##### Create Your Godot Project Create a new Godot project or open an existing one. ##### Install the GDExtension 1. Create an `addons` folder in the root of your project if it does not exist. 2. Copy the downloaded files into the `addons` folder. 3. Restart Godot to ensure the extension is correctly loaded. ##### Setup `ToriiClient` * Add a `ToriiClient` node to your scene tree. * Connect the client: ```gdscript var _success = torii_client.connect("http://localhost:8080") ``` * Register a subscription: :::tip If `ToriiClient.subscribe_entity_updates` is used with an empty `DojoClause`, it retrieves ALL entity updates. ::: ```gdscript func _entity_callback(entity: Dictionary): printt("Entity Models", entity["models"]) func _register_sub(): var _dojo_callback = DojoCallback.new() _dojo_callback.on_update = _entity_callback _dojo_callback.on_error = func(_err): printt("DojoError", _err) # Can also be a lambda var dojo_worlds: Array = ["0x..."] if torii_client.is_valid(): var entity_sub: int = torii_client.subscribe_entity_updates(DojoClause.new(), dojo_worlds, _dojo_callback) ``` :::note If `torii_client.connect` is used in the same method, you can use its return value instead of checking `is_valid()`. ::: ##### Connect `DojoSessionAccount` Add a `DojoSessionAccount` node to your scene tree. Generate the URL to start the login process and open it in the browser: ```gdscript func trigger_login(): # Create policies var policies = { "policies": [ { "target": "0x...", "method": "move" }, { "target": "0x...", "method": "spawn" } ]} # Generate private key. # If a previously generated key is used, you can create a session without logging in again. _priv_key = ControllerHelper.generate_private_key() # Save this key for the next step # Create session registration URL var session_url = ControllerHelper.create_session_registration_url(_priv_key, policies, "http://localhost:5050") # Open the default web browser OS.shell_open(session_url) ``` Once the session is successfully registered, fetch the session: ```gdscript dojo_session_account.max_fee = "0x100000" dojo_session_account.full_policies = policies dojo_session_account.create_from_subscribe( _priv_key, "http://localhost:5050", # Katana URL policies, # Optional if `full_policies` is set "https://api.cartridge.gg" # Controller URL (defaults to this value) ) ``` :::: #### Creating Contract Calls When creating calls, you need the contract address, the selector/function name, and call arguments. :::warning The `calldata` is an array and an optional parameter. If the function in your contract does not take arguments, you can pass an empty array. Otherwise, arguments must be inside an array. Calldata is always flattened. This means that if it has other arrays inside, they will be merged into a single array. ::: :::info If your function uses a custom type defined on your contract, you need to send all members in order inside an array or directly on the calldata array. In the example a Vector3 is used. But it works with any type defined on your contract. ::: :::tip `execute` and `execute_from_outside` can make calls in bulk by using an array. `execute_single` and `execute_from_outside_single` were added as a utility for a simple unique transaction. ::: ```gdscript var new_pos:Vector3 = Vector3(6, 2.5, -6) var position:Array = [new_pos.x, new_pos.y, new_pos.z] var direction:int = Directions.LEFT func move_to(_position:Array, _direction:int) -> void: var move_call:Dictionary = { "contract_address": "0x...", "entrypoint": "move", "calldata": [_position, _direction] } dojo_session_account.execute( [move_call] ) # or dojo_session_account.execute_single( "0x...", "move", [_position, _direction] ) ``` #### Subscriptions Subscriptions can be created through `ToriiClient`. When a subscription is successfully created, it returns a `sub_id`. This `sub_id` is required to update or cancel the subscription. Subscriptions are automatically cancelled when closing the game. :::tip Refer to the in-editor documentation to see other available subscriptions. ::: ##### Subscribing to Events Listen for blockchain events in real-time. The first parameter is always a `Callable`: This can be a lambda function, a `Callable` constructed from another object's function, or any function in the current script. ```gdscript # Configured in inspector @export var entity_sub: int @export var event_sub: int func setup_subscriptions() -> void: # Subscribe to all events var _dojo_callback_message: DojoCallback = DojoCallback.new() _dojo_callback_message.on_update = callback.bind("Message") # We add an extra parameter to the callback _dojo_callback_message.on_error = error_callback.bind("Message") message_sub = torii_client.subscribe_event_updates([], _dojo_callback_message) # Subscribe to entity updates var _dojo_callback: DojoCallback = DojoCallback.new() _dojo_callback.on_update = callback.bind("Entity") _dojo_callback.on_error = error_callback.bind("Entity") entity_sub = torii_client.subscribe_entity_updates(DojoClause.new(), ["0x..."], _dojo_callback) func callback(data: Dictionary, type: String): if type == "Entity": var result = data["models"] if type == "Message": var _data = data["data"] for _key in _data: printt("**", _key) func error_callback(err, type: String): push_error("Error on %s subscription: %s" % [type, err]) ``` #### Querying Entities Fetch the current blockchain state using queries: :::tip An empty query retrieves **ALL** entities across **ALL** worlds indexed by Torii. ::: :::note `DojoQuery` is used for retrieving entities. For other query types, refer to the `ToriiClient` in-editor documentation. ::: ```gdscript func query_players() -> void: var query: DojoQuery = DojoQuery.new() var entities = torii_client.entities(query) for entity in entities: process_entity(entity) func process_entity(entity: Dictionary) -> void: for model in entity["items"]: for key in model: var data = model[key] # Process based on your model structure if data.has("dojo_starter-Position"): var position_model = data["dojo_starter-Position"] # Structure depends on your contract models/ABI var position = Vector2(position_model['vec']['x'], position_model['vec']['y']) var id: String = position_model['player'] printt(id, position) ``` ##### Advanced Queries (Builder Pattern) All queries follow the **Builder Pattern**, allowing for flexible and readable construction of complex filters: ```gdscript var query: DojoQuery = DojoQuery.new() var clause = MemberClause.new() clause.member("id") clause.op(MemberClause.ComparisonOperator.Eq) var _addr = "0x..." # Player address clause.hex(_addr, MemberClause.PrimitiveTag.ContractAddress) clause.model("dojo_starter-Player") query.with_clause(clause) query.models(["dojo_starter-Moves", "dojo_starter-Position"]) var data: Dictionary = torii_client.entities(query) var _player_items: Array = data['items'] printt("ITEMS", _player_items) ``` You can also create the query in a single line: ```gdscript var query: DojoQuery = DojoQuery.new() \ .with_clause(MemberClause.new() \ .member("id") \ .op(MemberClause.ComparisonOperator.Eq) \ .hex("0x...", MemberClause.PrimitiveTag.ContractAddress) \ .model("dojo_starter-Player")) \ .models(["dojo_starter-Moves", "dojo_starter-Position"]) var data: Dictionary = torii_client.entities(query) var _player_items: Array = data['items'] printt("ITEMS", _player_items) ``` #### Managing Policies The recommended structure for policies for this extension is the following: ```gdscript # Create policy var full_policies: Dictionary = { "max_fee": "0x100000", "policies": [{ "contract_address": "0x...", "methods": [ "spawn", "move" ] }] } # Use with session account dojo_session_account.max_fee = full_policies["max_fee"] # Can be omitted if present on full_policies dictionary dojo_session_account.full_policies = full_policies dojo_session_account.create_from_subscribe( _priv_key, "http://localhost:5050", # Katana full_policies, # Optional if `full_policies` is set "https://api.cartridge.gg" # Optional, defaults to `https://api.cartridge.gg` ) ``` #### Type Conversion dojo.godot handles type conversion automatically: Every struct/type in your contract will return inside a Dictionary with its name and parameters. In the Starter Project there is a custom struct `Vec2`, the following snippet is an example. :::info Cairo native types are converted to Godot's types automatically. ::: ```gdscript # Cairo Vec2 becomes Godot Vector2 func handle_position_update(data: Dictionary) -> void: if data.has("Vec2"): var position = Vector2(data["Vec2"]["x"], data["Vec2"]["y"]) player.position = position # Enums convert to integers enum Direction { LEFT, RIGHT, UP, DOWN } func move_player(direction: Direction) -> void: move_call.calldata = [direction] # Automatically converts to felt252 controller_account.execute_from_outside([move_call]) ``` ### Building and Deployment We use scons build system as it is what Godot uses. However, a Cmake equivalent is placed so the extension can be developed using any modern IDE. The extension compiles and places itself under *demo/addons/godot-dojo*. As of `godot-dojo` [v0.7.2](https://github.com/lonewolftechnology/godot-dojo/releases/tag/v0.7.2), `Linux`, `Windows`, `MacOS` , `Android` and `iOS` are supported. Make sure to check new versions under the [releases tab](https://github.com/lonewolftechnology/godot-dojo/releases) :::note `Linux` and `Windows` builds are x86\_64 only. You can build arm64 builds. Let us know if it works by opening an issue or by leaving a message on `Dojo's Discord` under the `Godot section`. `MacOS` builds are not universal, so while they work on both Intel and Apple Silicon, they require separate binaries. A universal build could be created, but we have not been able to properly test it yet. ::: #### Editor builds The editor builds contain code that only runs in-editor and is only found in-editor. This is the only required build to run the extension; for exporting the templates are required. ```bash scons platform=linux target=editor ``` #### Development Builds For development, use debug builds to enable logging: ```bash scons platform=linux target=template_debug scons platform=windows target=template_debug scons platform=macos target=template_debug ``` You can set environment variables in your game for detailed logging. This is useful when developing the extension or reporting bugs: ```gdscript func _ready() -> void: OS.set_environment("RUST_BACKTRACE", "full") OS.set_environment("RUST_LOG", "debug") ``` #### Production Builds Create optimized builds for distribution: ```bash scons platform=linux target=template_release scons platform=windows target=template_release scons platform=macos target=template_release ``` ### Example Project The dojo.godot repository includes a complete demo project showcasing: * **Player Movement**: Onchain player spawning and movement using arrow keys. * **Real-time Updates**: Live synchronization between blockchain state and game visuals. * **Controller Integration**: Seamless wallet authentication and transaction signing. * **Event Handling**: Processing both events and entity updates from subscriptions. To run the demo: :::note Follow the [dojo-starter](/tutorials/dojo-starter) or [dojo-intro](/getting-started/your-first-dojo-app) guide. ::: 1. Set up a local Dojo Starter environment. 2. Build/download the dojo.godot extension following the instructions above. 3. Open the `demo` folder in Godot and run the project. The demo connects to a live testnet deployment, demonstrating real blockchain integration in a simple 2D movement game. ### Troubleshooting Enable detailed logging for troubleshooting: ```gdscript func _ready() -> void: OS.set_environment("RUST_LOG", "debug,tokio=info,hyper=info") OS.set_environment("RUST_BACKTRACE", "full") ``` Check the Godot console for detailed error messages and stack traces. ## Dojo SDK Architecture Dojo provides a comprehensive suite of SDKs for building onchain games and applications across multiple platforms. All SDKs share a unified foundation built on **dojo.c**, ensuring consistent functionality with platform-specific optimizations. ### Foundation-First Architecture The Dojo SDK ecosystem is built on a two-layer foundation that ensures both consistency and type safety across all platforms. #### Layer 1: Cainome - Type-Safe Binding Generation **Cainome** generates type-safe bindings from Cairo contract ABIs, providing the compile-time foundation for all SDK interactions. **Key capabilities:** * **ABI Parsing**: Converts Cairo contract ABIs into platform-specific type definitions * **Type Safety**: Ensures compile-time guarantees for contract interactions * **Multi-Language Support**: Generates bindings for Rust, TypeScript, C#, and other languages * **Serialization Handling**: Automatically handles Cairo ↔ native type conversions #### Layer 2: dojo.c - Runtime Blockchain Integration **dojo.c** provides the runtime foundation that handles all blockchain interactions across platforms. **Key capabilities:** * **Account Management**: Support for controller accounts, session accounts, and burner wallets * **Transaction Handling**: Sign and execute transactions with proper gas estimation * **Torii Client Integration**: Query entities, subscribe to real-time updates, and sync world state * **Cross-Platform Compatibility**: Compiles to both native binaries (via C bindings) and WebAssembly **Dual Compilation Strategy:** * **Native Platforms**: Uses `cbindgen` to generate C headers for Unity, Unreal, and other native integrations * **Web Platforms**: Uses `wasm-bindgen` to generate WebAssembly modules for JavaScript/TypeScript applications #### The Complete Foundation **Cainome** (compile-time) + **dojo.c** (runtime) = **Complete SDK Foundation** This two-layer architecture ensures that: * Contract interactions are type-safe and validated at compile time (Cainome) * Blockchain operations are consistent and optimized at runtime (dojo.c) * Platform SDKs can focus on providing idiomatic APIs for their ecosystems ### Common Patterns Regardless of which SDK you choose, all Dojo applications follow similar patterns: #### Core Game Loop ``` Client → SDK → Katana (sequencer) → Torii (indexer) → SDK → Client ``` All SDKs provide mechanisms to send transactions and query entities from your world. Player actions in the client are translated by the SDK into transactions that are sent to the sequencer. The sequencer executes the transactions and the indexer updates the world state. The client can then query the world state to get the latest state, which is then rendered to the user. #### Account Management * **Session Accounts**: Temporary accounts for seamless gameplay * **Controller Accounts**: Delegate specific permissions to game contracts * **Burner Accounts**: Disposable accounts funded by a master account #### Transaction Flow 1. **Prepare**: Build transaction calls using contract bindings 2. **Sign**: Use account credentials to sign transaction 3. **Execute**: Submit transaction to Katana sequencer 4. **Wait**: Monitor transaction status and confirmation 5. **Sync**: Update local state with new world state ### Platform-Specific SDKs #### Production Ready ##### JavaScript/TypeScript SDK **Best for:** Web applications, React/Vue/Svelte apps, Node.js backends * Full-featured SDK with React hooks and state management * Multiple examples: Vanilla JS, React, Vue, Svelte, Phaser integration * Real-time entity synchronization with RECS (Reactive Entity Component System) * Built-in support for wallet connections and burner accounts ##### Unity SDK **Best for:** 2D and 3D games, cross-platform game development * Native C# bindings built on dojo.c foundation * Unity-specific components and prefabs for common patterns * Support for WebGL, desktop, and mobile platforms * Integrated world state synchronization with Unity's component system #### Active Development ##### Bevy SDK **Best for:** Rust-based game development * ECS-native integration with Bevy's component system * Rust-first development experience with compile-time safety * Direct access to dojo.c functionality without FFI overhead ##### Unreal Engine SDK **Best for:** High-fidelity 3D games and applications * C++ integration with Unreal's Blueprint system * Support for complex game mechanics and AAA-quality experiences * Native performance with dojo.c foundation #### Experimental ##### C/C++ Bindings **Best for:** Custom integrations, maximum performance requirements * Direct access to dojo.c API without additional abstraction layers * Full control over memory management and optimization * Foundation for building custom SDK wrappers ##### Native Rust Integration **Best for:** Rust applications requiring direct Dojo integration * Import Dojo as a native Rust crate * Zero-cost abstractions with compile-time optimization * Suitable for high-performance backends and custom tooling ### Choosing the Right SDK | Platform/Framework | Recommended SDK | Maturity Level | | ------------------------ | --------------------- | --------------------- | | Web (React, Vue, Svelte) | JavaScript/TypeScript | ✅ Production | | Unity Games | Unity SDK | ✅ Production | | Bevy Games | Bevy SDK | 🔄 Active Development | | Unreal Engine | Unreal SDK | 🔄 Active Development | | Custom C/C++ | C Bindings | ⚗️ Experimental | | Rust Applications | Native Rust | ⚗️ Experimental | ### Getting Started To begin development with any Dojo SDK: 1. **Set up your development environment** with Katana and Torii 2. **Choose your SDK** based on your platform and requirements 3. **Follow the platform-specific setup guide** linked below 4. **Explore examples** to understand integration patterns #### Quick Links * [JavaScript/TypeScript SDK →](./javascript) * [Unity SDK →](./unity) * [Bevy SDK →](./bevy) * [Unreal Engine SDK →](./unrealengine) * [C Bindings →](./c) * [Rust Integration →](./rust) ### Architecture Benefits This foundation-first approach provides several key advantages: * **Consistency**: Core blockchain logic is identical across all platforms * **Maintainability**: Bug fixes and features in dojo.c benefit all SDKs * **Performance**: Native compilation ensures optimal performance on each platform * **Interoperability**: Applications built with different SDKs can interact seamlessly * **Rapid Development**: New platform support can be added by wrapping dojo.c ## dojo.js The dojo.js SDK provides a powerful, intuitive interface for interacting with onchain state in JavaScript. It streamlines data fetching and subscriptions, supporting both simple and complex queries. ### Key Features * **Type Safety**: Leverage TypeScript for robust, error-resistant code. * **Intuitive query syntax**: Write ORM-like queries that feel natural. * **Flexible subscriptions**: Subscribe to granular state changes in your world. * **Built-in Zustand support**: Reactive state management. * **Signed messages**: Sign off-chain state and send to Torii. * **Optimistic Client Rendering**: Update state before a transaction has finalized. :::note dojo.js is a wrapper around [dojo.c](/client/sdk/c/wasm-bindings) that exposes Torii client features via WASM. For more information about the Torii gRPC client, check out [this documentation](/toolchain/torii/grpc). ::: ### Getting Started #### Quickstart Wizard The fastest way to get started is using our quickstart wizard. ```bash pnpx @dojoengine/create-dojo start ``` This will guide you through a series of prompts to configure your project. #### Manual Setup For full control over your project structure, follow these steps: ::::steps ##### Create Your Project Pick any JavaScript framework. We recommend [pnpm](https://pnpm.io/) as your package manager. ##### Install Dependencies ```bash mkdir {project_name} && cd {project_name} && pnpm init # Essential packages pnpm add @dojoengine/core @dojoengine/sdk @dojoengine/torii-client # For React integration pnpm add @dojoengine/create-burner @dojoengine/utils # For state management (v1.6+) pnpm add @dojoengine/state # Build tools for WASM support pnpm add -D vite-plugin-wasm vite-plugin-top-level-await # Additional dependencies (if using React) pnpm add zustand immer ``` ##### Create `dojoConfig.ts` Create a `dojoConfig.ts` file and pass in your project's manifest: ```typescript import { createDojoConfig } from "@dojoengine/core"; import manifest from "../path/to/manifest_dev.json"; export const dojoConfig = createDojoConfig({ manifest }); ``` ##### Generate TypeScript Bindings [Generate contract bindings](/toolchain/sozo/binding-generation) with Sozo, letting you import Dojo models into TypeScript: ```bash DOJO_MANIFEST_PATH="../path/to/Scarb.toml" sozo build --typescript ``` :::note These instructions assume you have Dojo contracts relative to your client root. ::: ##### Initialize the SDK With your contract bindings generated, you can now link Dojo models to your UI. ```typescript // main.tsx // React imports import { createRoot } from "react-dom/client"; import App from "./App.tsx"; // Dojo imports import { init } from "@dojoengine/sdk"; import { DojoSdkProvider } from "@dojoengine/sdk/react"; // Local imports import { dojoConfig } from "./dojoConfig.ts"; import { setupWorld } from "./bindings/typescript/contracts.gen.ts"; import type { SchemaType } from "./bindings/typescript/models.gen.ts"; async function main() { // Initialize the SDK with configuration options const sdk = await init({ client: { // Required: Address of the deployed World contract worldAddress: dojoConfig.manifest.world.address, // Optional: Torii indexer URL (defaults to http://localhost:8080) toriiUrl: dojoConfig.toriiUrl || "http://localhost:8080", // Optional: Relay URL for real-time messaging relayUrl: dojoConfig.relayUrl || "/ip4/127.0.0.1/tcp/9090", }, // Domain configuration for typed message signing (SNIP-12) domain: { name: "MyDojoProject", version: "1.0", chainId: "KATANA", // or "SN_MAIN", "SN_SEPOLIA" revision: "1", }, }); createRoot(document.getElementById("root")!).render( ); } main(); ``` :::note If using `starknet-react`, wrap `DojoSdkProvider` *around* `StarknetProvider`: ```typescript ``` ::: :::warning Call `init` only once to avoid creating multiple Torii clients. ::: :::: ### Usage Overview #### Core SDK Methods The SDK provides several key methods for interacting with your Dojo world: * **`getEntities()`** - Fetch entities with flexible filtering * **`subscribeEntityQuery()`** - Subscribe to real-time entity updates * **`getEventMessages()`** - Fetch historical events * **`subscribeEventQuery()`** - Subscribe to real-time event updates * **`sendMessage()`** - Send signed off-chain messages All queries use the same `ToriiQueryBuilder` and support the same filtering operators: `Eq`, `Neq`, `Gt`, `Gte`, `Lt`, `Lte`, `In`, `NotIn`. :::note For more information about Torii queries, consult the [Torii gRPC API reference](/toolchain/torii/grpc). ::: #### Querying Entities Fetch entities using the `ToriiQueryBuilder` with various clause types: ```typescript import { ToriiQueryBuilder, KeysClause } from "@dojoengine/sdk"; // Simple query: Find a specific player with KeysClause const entities = await sdk.getEntities({ query: new ToriiQueryBuilder().withClause( KeysClause(["dojo_starter-Player"], ["0xabcde..."], "FixedLen").build() ), }); // Access the results entities.items.forEach((entity) => { const player = entity.models.dojo_starter.Player; console.log(`Player: ${player?.name}, Score: ${player?.score}`); }); ``` :::note Models are accessed using the pattern `entity.models.{namespace}.{ModelName}` where: * `{namespace}` is your project's namespace (e.g., `dojo_starter`, `world`, `game`) * `{ModelName}` is the exact model name as defined in your Cairo code ::: :::tip You do not need to `build()` your `query`; the sdk will build it automatically. ::: Here is an example of a complex query that finds high-scoring warriors and mages in a specific area of the map: ```typescript import { ToriiQueryBuilder, MemberClause } from "@dojoengine/sdk"; // Complex query: find matching entities with MemberClause const entities = await sdk.getEntities({ query: new ToriiQueryBuilder().withClause( AndComposeClause([ // Player conditions MemberClause("world-Player", "score", "Gt", 100), OrComposeClause([ MemberClause("world-Player", "class", "Eq", "warrior"), MemberClause("world-Player", "class", "Eq", "mage"), ]), // Position conditions (same entities) MemberClause("world-Position", "x", "Lt", 50), MemberClause("world-Position", "y", "Gt", 20), ]).build() ), }); ``` :::note When you use AND with different model types, you're looking for **entities that have both components**. ::: For large datasets, use pagination and ordering: ```typescript const entities = await sdk.getEntities({ query: new ToriiQueryBuilder() .withClause( MemberClause("dojo_starter-Player", "score", "Gt", 0).build() ) .withLimit(10) // Limit to 10 results .withOffset(0) // Start from beginning .withOrderBy([ { // Order by score descending field: "score", direction: "Desc", }, ]), }); ``` #### Subscribing To Entity Changes Subscribe to real-time updates for entities matching your query criteria: ```typescript const [initialEntities, subscription] = await sdk.subscribeEntityQuery({ query: new ToriiQueryBuilder() .withClause( MemberClause("dojo_starter-Player", "score", "Gt", 100).build() ) .includeHashedKeys(), callback: ({ data, error }) => { if (data) { console.log("Player updated:", data); data.forEach((entity) => { const player = entity.models.dojo_starter.Player; console.log(`Player ${player?.id}: ${player?.score} points`); }); } if (error) { console.error("Subscription error:", error); } }, }); // Cancel the subscription when no longer needed // subscription.cancel(); ``` #### Saving State With Zustand The SDK integrates with Zustand for reactive state management, updating your components automatically when blockchain data changes: ``` 1. useEntityQuery() subscribes to Torii for real-time updates ↓ 2. Blockchain state changes (transaction, new entity, etc.) ↓ 3. Torii detects the change and pushes update to your client ↓ 4. SDK automatically updates the Zustand store ↓ 5. React components using useModels/useModel re-render automatically ↓ 6. UI shows the latest data ``` ##### Option 1: Using Convenience Hooks (Recommended) The easiest way is to use the provided React hooks, which abstract away the store: ```typescript import { useEntityQuery, useModels, useModel, useEntityId } from "@dojoengine/sdk/react"; function MyComponent() { // Subscribe to entity changes - data automatically goes into the store useEntityQuery( new ToriiQueryBuilder() .withClause(MemberClause("dojo_starter-Item", "durability", "Eq", 2).build()) .includeHashedKeys() ); // Get all items from the store using convenience hooks const items = useModels("dojo_starter-Item"); // Get a single item by entity ID const entityId = useEntityId(1); const item = useModel(entityId, "dojo_starter-Item"); return (

All Items with Durability 2:

{Object.entries(items).map(([id, item]) => (
Item {id}: durability {item?.durability}
))}

Single Item:

{item &&
Item 1: durability {item.durability}
}
); } ``` ##### Option 2: Direct Zustand Store Access For more advanced use cases, you can access the Zustand store directly: ```typescript import { useDojoSDK, useEntityQuery } from "@dojoengine/sdk/react"; function MyComponent() { const { useDojoStore } = useDojoSDK(); // The Zustand store // Subscribe to entity changes useEntityQuery( new ToriiQueryBuilder() .withClause(MemberClause("dojo_starter-Item", "durability", "Eq", 2).build()) .includeHashedKeys() ); // Access the raw store const allEntities = useDojoStore((state) => state.entities ); const itemEntities = useDojoStore((state) => state.getEntitiesByModel("dojo_starter", "Item") ); return (

Total entities in store: {Object.keys(allEntities).length}

Item entities: {itemEntities.length}

{itemEntities.map((entity) => { const item = entity.models.dojo_starter.Item; return (
Entity {entity.entityId}: durability {item?.durability}
); })}
); } ``` #### Sending Signed Messages Signed messages allow you to send **authenticated off-chain data** to Torii without expensive blockchain transactions. This can be used to implement things like **chat systems, leaderboards, social features, game coordination, and real-time events.** The key benefit: **Players authenticate the data** (proving it came from them) **without gas fees**, while Torii broadcasts it to all connected clients in real-time. Here's an example of how to send a signed message: ```typescript // Generate typed data for a chat message model const typedData = sdk.generateTypedData("world-Message", { identity: account?.address, content: messageContent, timestamp: Date.now(), }); try { // Sign and send the message using the SDK const result = await sdk.sendMessage(typedData, account); if (result.isOk()) { console.log("Message sent successfully:", result.value); } else { console.error("Failed to send message:", result.error); } } catch (error) { console.error("Error sending message:", error); } ``` :::note If you want messages to be broadcast to all of your Torii client instances, you'll have to pass a `relayUrl` to `init`. `relayUrl` is a *multiaddr* format which looks like something like this when deployed on slot: `/dns4/api.cartridge.gg/tcp/443/x-parity-wss/%2Fx%2Fyour-slot-deployment-name%2Ftorii%2Fwss` ::: #### Querying Tokens dojo.js can query token data (ERC20, ERC721, ERC1155) indexed by Torii. First, configure Torii to index your tokens: ```toml # dojo_dev.toml [indexing] contracts = [ "erc20:0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", # ETH "erc20:0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", # STRK "erc721:0x..." # Your NFT contract ] ``` Then query token balances in your React components: ```typescript import { useTokens } from "@dojoengine/sdk/react"; function TokenBalance({ address }: { address: string }) { const { tokens, balances, getBalance, toDecimal } = useTokens({ accountAddresses: [address], }); return (

Token Balances for {address}

{tokens.map((token, idx) => (
{token.symbol}: {toDecimal(token, getBalance(token))}
))}
); } ``` #### Optimistic Client Rendering We use [immer](https://immerjs.github.io/immer/) for efficient optimistic rendering. This allows instant client-side entity state updates while awaiting blockchain confirmation. **The process:** 1. Update entity state optimistically. 2. Wait for condition (e.g., a specific state change). 3. Resolve update, providing immediate user feedback. This ensures a responsive user experience while maintaining blockchain data integrity. :::note You will need to have a subscription running in order for the update to resolve. ::: ```typescript import { useCallback } from "react"; import { v4 as uuidv4 } from "uuid"; import { useDojoSDK } from "@dojoengine/sdk/react"; import { useAccount } from "@starknet-react/core"; import { getEntityIdFromKeys } from "@dojoengine/utils"; export function useSystemCalls(entityId: string) { const { account } = useAccount(); const { useDojoStore, client } = useDojoSDK(); const state = useDojoStore((s) => s); const spawn = useCallback(async () => { if (!account) return; // Generate a unique transaction ID const transactionId = uuidv4(); // The value to update the Moves model with const remainingMoves = 100; // Apply an optimistic update to the state // this uses immer drafts to update the state state.applyOptimisticUpdate(transactionId, (draft) => { if (draft.entities[entityId]?.models?.dojo_starter?.Moves) { draft.entities[entityId].models.dojo_starter.Moves!.remaining = remainingMoves; } }); try { // Execute the spawn action await client.actions.spawn({ account }); // Wait for the entity to be updated with the new state await state.waitForEntityChange(entityId, (entity) => { return ( entity?.models?.dojo_starter?.Moves?.remaining === remainingMoves ); }); } catch (error) { // Revert the optimistic update if an error occurs state.revertOptimisticUpdate(transactionId); console.error("Error executing spawn:", error); throw error; } finally { // Confirm the transaction if successful state.confirmTransaction(transactionId); } }, [account, client, entityId, state]); return { spawn }; } ``` ### Additional Examples See this [example project](https://github.com/dojoengine/dojo.js/tree/main/examples/example-vite-react-sdk) for a real-world implementation. ## Dojo Rust SDK Dojo is built in Rust, making it seamless to integrate into your Rust projects. Simply import the required crates and you're ready to build powerful applications that interact with Dojo worlds. The Dojo Rust SDK provides access to the core framework functionality, built on the same [dojo.c foundation](/client/sdk/c) that powers other language bindings. ### Core Components The Dojo Rust ecosystem provides several key crates for different use cases: * **`dojo-types`**: Core types and data structures for Dojo * **`dojo-world`**: World contract interaction and management * **`torii-client`**: Client for connecting to Torii indexer * **`torii-grpc`**: gRPC client for real-time data streaming * **`torii-relay`**: P2P networking and relay functionality * **`cainome`**: Contract bindings generation for Cairo contracts ### Getting Started Add these dependencies to your `Cargo.toml`: ```toml [dependencies] # Core Dojo types and functionality dojo-types = { git = "https://github.com/dojoengine/dojo", tag = "v1.7.0-alpha.0" } dojo-world = { git = "https://github.com/dojoengine/dojo", tag = "v1.7.0-alpha.0" } # Torii client for indexing and real-time data (separate repository since 1.5.0) torii-client = { git = "https://github.com/dojoengine/torii", tag = "v1.6.1-preview.2" } torii-grpc-client = { git = "https://github.com/dojoengine/torii", tag = "v1.6.1-preview.2" } # Contract bindings generation cainome = { git = "https://github.com/cartridge-gg/cainome", rev = "7d60de1", features = ["abigen-rs"] } cainome-cairo-serde = { git = "https://github.com/cartridge-gg/cainome", rev = "7d60de1" } # Starknet integration starknet = "0.17.0-rc.2" starknet-crypto = "0.7.4" starknet-types-core = "0.1.7" # Async runtime tokio = { version = "1.39", features = ["full"] } ``` ### Basic Usage #### Connecting to a Dojo World ```rust // Import necessary types for connecting to Dojo use torii_client::client::Client; use starknet_crypto::Felt; // The #[tokio::main] attribute makes this function run in an async runtime // This is required because we'll be making network calls #[tokio::main] async fn main() -> Result<(), Box> { // Configure connection URLs for your Dojo world let torii_url = "http://localhost:8080".to_string(); // Torii indexer endpoint let rpc_url = "http://localhost:5050".to_string(); // Starknet RPC endpoint (Katana) let relay_url = "http://localhost:9090".to_string(); // P2P relay for real-time updates // The world address is a unique identifier for your Dojo world on Starknet let world_address = Felt::from_hex_unchecked("0x123..."); // Create a new Torii client connection let client = Client::new( torii_url, rpc_url, relay_url, world_address, ).await?; // At this point, you have a connected client ready to interact with your Dojo world // You can now query models, subscribe to events, etc. Ok(()) } ``` #### Subscribing to Events ```rust // Import types needed for event subscriptions and stream processing use torii_grpc_client::client::{EntityKeysClause, KeysClause, PatternMatching}; use futures::StreamExt; // Provides the .next() method on streams // Subscribe to all entity updates in the Dojo world // The `mut` keyword makes the subscription mutable so we can read from it let mut subscription = client .on_event_message_updated( // Create a filter for which entities/models to watch vec![EntityKeysClause::Keys(KeysClause { keys: vec![], // Empty = watch all entities pattern_matching: PatternMatching::VariableLen, // Allow flexible key matching models: vec![], // Empty = watch all models })], true, // Include historical data (events that happened before we subscribed) ) .await?; // Wait for the subscription to be established // Process incoming updates in a loop // This will run continuously, waiting for new events while let Some(Ok((_, entity))) = subscription.next().await { // The `Some(Ok(...))` pattern handles the Result> type: // - Some(...) means we got data (not the end of stream) // - Ok(...) means no error occurred // - The (_, entity) destructures the tuple, ignoring the first value println!("Entity updated: {:?}", entity); // Here you can add your custom logic to handle the entity update // For example: update a database, trigger game logic, send notifications, etc. } ``` ### Discord Bot Example This example demonstrates how to build a Discord bot that connects to a Dojo world using the Rust SDK. The bot monitors world events and posts updates to a Discord channel. This example uses the Shuttle runtime for easy deployment. You will need a Discord bot token to use this example. You can get one by creating an application and bot in the [Discord Developer Portal](https://discord.com/developers/applications). #### Prerequisites * Download and install the Rust compiler from [rust-lang.org](https://rustup.rs/) * Set up a new Rust project: `cargo new dojo-discord-bot` * Install the Shuttle CLI: [Shuttle Installation Guide](https://docs.shuttle.rs/getting-started/installation) * Create a Discord application and bot in the Discord Developer Portal #### Setup Create a `Secrets.toml` file in the root of your project: ```toml DISCORD_TOKEN = "your_discord_token_here" TORII_URL = "http://localhost:8080" NODE_URL = "http://localhost:5050" TORII_RELAY_URL = "http://localhost:9090" WORLD_ADDRESS = "your_world_address_here" CHANNEL_ID = "your_discord_channel_id_here" ``` Add these dependencies to your `Cargo.toml`: ```toml [dependencies] # Discord bot framework poise = "0.6.1" serenity = { version = "0.12.0", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] } # Utility dependencies toml = { version = "0.7", default-features = false, features = ["parse", "display"] } # Shuttle deployment platform shuttle-runtime = "0.48.0" shuttle-serenity = "0.48.0" shuttle-rocket = "0.48.0" # Dojo components dojo-types = { git = "https://github.com/dojoengine/dojo", tag = "v1.7.0-alpha.0" } dojo-world = { git = "https://github.com/dojoengine/dojo", tag = "v1.7.0-alpha.0" } # Torii components torii-client = { git = "https://github.com/dojoengine/torii", tag = "v1.6.1-preview.2" } torii-grpc-client = { git = "https://github.com/dojoengine/torii", tag = "v1.6.1-preview.2" } torii-relay = { git = "https://github.com/dojoengine/torii", tag = "v1.6.1-preview.2" } # Contract bindings and Cairo tooling cainome = { git = "https://github.com/cartridge-gg/cainome", rev = "7d60de1", features = ["abigen-rs"] } cainome-cairo-serde = { git = "https://github.com/cartridge-gg/cainome", rev = "7d60de1" } cairo-lang-filesystem = "=2.8.4" scarb = { git = "https://github.com/software-mansion/scarb", rev = "4fdeb7810" } # Starknet integration starknet = "0.17.0-rc.2" starknet-crypto = "0.7.4" starknet-types-core = "0.1.7" # Async runtime tokio = { version = "1.39", features = ["full"] } ``` #### Code ````rust // Standard library imports use std::{num::NonZero, sync::Arc, time::Duration}; // Discord library imports - Serenity is the main Discord API wrapper for Rust use serenity::{ all::{ChannelId, CreateMessage, GatewayIntents, Http}, // Discord API types futures::StreamExt, // Stream processing utilities Client, // Discord client }; use shuttle_runtime::SecretStore; // For accessing environment variables securely use starknet_crypto::Felt; // Starknet field elements for addresses use torii_grpc_client::client::{EntityKeysClause, KeysClause, PatternMatching}; // Dojo event filtering // Type aliases to make error handling cleaner // Box is Rust's way of saying "any error type" pub type Error = Box; // Context is the type passed to Discord command functions pub type Context<'a> = poise::Context<'a, Data, Error>; // Data structure to share state between Discord commands // Currently empty, but you could add database connections, etc. pub struct Data {} // Discord slash command definition #[poise::command(slash_command)] pub async fn hello(ctx: Context<'_>) -> Result<(), Error> { // ctx.say() sends a message back to Discord in response to the command ctx.say("🤖 Hello! I'm your Dojo Discord Bot - monitoring world events and ready to help!").await?; Ok(()) } #[poise::command(slash_command)] pub async fn world_status(ctx: Context<'_>) -> Result<(), Error> { ctx.say("🌍 Connected to Dojo World! I'm watching for all the exciting things happening in your autonomous world.").await?; Ok(()) } // Configuration structure to hold all the connection details // This keeps our configuration organized and type-safe struct Config { discord_token: String, // Discord bot authentication token channel_id: NonZero, // Discord channel ID (NonZero ensures it's never 0) torii_url: String, // URL to connect to Torii indexer node_url: String, // URL to connect to Starknet node (Katana) torii_relay_url: String, // URL for P2P relay connection world_address: String, // Hex string of the Dojo world address } // Implementation block for Config - this is where we define methods on the Config struct impl Config { // Constructor method to create Config from Shuttle's secret store pub fn from_secrets(secret_store: SecretStore) -> Self { // Extract Discord bot token from environment variables let discord_token = secret_store.get("DISCORD_TOKEN").unwrap(); // Parse channel ID from string to number // This chain: get string -> parse to u64 -> wrap in NonZero -> unwrap all results let channel_id = NonZero::new( secret_store .get("CHANNEL_ID") .unwrap() // Get the string value .parse::() // Convert string to 64-bit unsigned integer .unwrap(), // Handle parsing errors by crashing ) .unwrap(); // Extract all the Dojo connection URLs let torii_url = secret_store.get("TORII_URL").unwrap(); let node_url = secret_store.get("NODE_URL").unwrap(); let torii_relay_url = secret_store.get("TORII_RELAY_URL").unwrap(); let world_address = secret_store.get("WORLD_ADDRESS").unwrap(); // Create and return a new Config instance // The `fieldname,` syntax is shorthand for `fieldname: fieldname,` Config { discord_token, channel_id, torii_url, node_url, torii_relay_url, world_address, } } } // This attribute tells Shuttle this is the main entry point for our application #[shuttle_runtime::main] async fn main( // This parameter injection gives us access to the SecretStore containing our environment variables #[shuttle_runtime::Secrets] secret_store: SecretStore, ) -> shuttle_serenity::ShuttleSerenity { // Set up Discord permissions - non_privileged() gives us basic bot permissions // You might need more permissions depending on what your bot does let intents = GatewayIntents::non_privileged(); let config = Config::from_secrets(secret_store); // Set up the Discord command framework (Poise) // This handles parsing slash commands and routing them to our functions let framework = poise::Framework::builder() .options(poise::FrameworkOptions { commands: vec![hello(), world_status()], ..Default::default() }) .setup(|ctx, _ready, framework| { // This closure runs when the bot connects to Discord Box::pin(async move { // Register our commands globally so they appear in all servers poise::builtins::register_globally(ctx, &framework.options().commands).await?; Ok(Data {}) }) }) .build(); // Create the Discord client with our bot token and the command framework let client = Client::builder(config.discord_token.clone(), intents) .framework(framework) .await .expect("Failed to build Discord client"); // Spawn a separate async task to handle Dojo world monitoring // This runs concurrently with the Discord bot // `move` captures the config by value so it can be used in the async block tokio::spawn(async move { // Create a connection to the Dojo world let torii_client = torii_client::client::Client::new( config.torii_url.clone(), config.node_url.clone(), config.torii_relay_url.clone(), Felt::from_hex_unchecked(&config.world_address.clone()), ) .await .expect("Failed to create Torii client"); // Start monitoring the Dojo world and sending updates to Discord subscribe(torii_client, config).await; }); // Return the Discord client wrapped in Shuttle's expected type Ok(client.into()) } // This function handles subscribing to Dojo world events and forwarding them to Discord // It includes retry logic to handle network issues gracefully async fn subscribe(client: torii_client::client::Client, config: Config) { let mut tries = 0; // Current number of failed attempts let max_num_tries = 200; // Maximum attempts before giving up // Exponential backoff for reconnection attempts let mut backoff = Duration::from_secs(1); let max_backoff = Duration::from_secs(60); // Create a Discord HTTP client for sending messages // Arc (Atomically Reference Counted) allows sharing this across async tasks safely let http = Arc::new(Http::new(&config.discord_token.clone())); // Main monitoring loop - this runs continuously loop { // Attempt to subscribe to world events let rcv: Result< torii_grpc_client::client::EntityUpdateStreaming, // Success type: stream of updates torii_client::client::error::Error, // Error type > = client .on_event_message_updated( // Set up event filtering vec![EntityKeysClause::Keys(KeysClause { keys: vec![], // Empty = all entities pattern_matching: PatternMatching::VariableLen, // Flexible key matching models: vec![], // Empty = all models })], true, // Include historical events (backfill) ) .await; // Handle the subscription result match rcv { // Successfully connected - process incoming events Ok(mut rcv) => { // Reset backoff delay since we connected successfully backoff = Duration::from_secs(1); // Process each event as it comes in while let Some(Ok((_, entity))) = rcv.next().await { // Format the entity data as a Discord message // {:#?} creates a pretty-printed debug representation let entity_message = format!("🎮 **Dojo World Update!**\n```\n{:#?}\n```", entity); let content = CreateMessage::new().content(entity_message); // Send the message to Discord // We use `if let Err(e)` to handle errors without crashing if let Err(e) = ChannelId::from(config.channel_id) .send_message(http.clone(), content) .await { println!("Failed to send Discord message: {}", e); } } // If we get here, the stream ended (connection lost) } // Failed to connect - we'll retry Err(_) => { println!("Subscription was lost, attempting to reconnect"); tries += 1; } } // Wait before trying to reconnect (exponential backoff) tokio::time::sleep(backoff).await; backoff = std::cmp::min(backoff * 2, max_backoff); if tries >= max_num_tries { println!("Max number of tries reached, exiting"); break; } } println!("Torii client disconnected"); } ```` #### Deployment ##### Local Development Run the bot locally for testing: ```bash shuttle run ``` This will start the bot using your local `Secrets.toml` file. The bot will connect to your specified Dojo world and Discord channel. ##### Production Deployment Deploy the bot to Shuttle's cloud platform: ```bash shuttle deploy ``` The bot will be hosted on Shuttle's infrastructure and run continuously. Make sure your `Secrets.toml` contains production-ready values before deploying. #### Next Steps This example demonstrates the basic integration between Dojo and Discord using Rust. You can extend it by: * Adding more Discord commands to interact with your world * Filtering events by specific models or entities * Formatting Discord messages with rich embeds * Adding database persistence using Shuttle's shared database * Implementing user authentication and authorization * Adding custom event processing logic ## Dojo Telegram Integration Build fully onchain games and applications that run seamlessly within Telegram using Dojo and the [Cartridge Controller](https://docs.cartridge.gg/controller/overview). Telegram [Mini Apps](https://core.telegram.org/bots/webapps) are web applications that run directly inside Telegram, providing users with rich, interactive experiences directly inside the messaging platform. ### Architecture Overview A typical Dojo Telegram app consists of: 1. **Frontend**: React-based web app using the Telegram SDK 2. **Wallet Integration**: Cartridge Controller for account management 3. **Blockchain Layer**: Dojo smart contracts on Starknet 4. **State Management**: Torii client for real-time entity subscriptions ### Getting Started #### Prerequisites Before building your Telegram Mini App, ensure you have a Bot Token from [@BotFather](https://t.me/botfather). :::tip See the [JavaScript SDK docs](/client/sdk/javascript) for more information about using Dojo with React. ::: #### Quick Start :::steps ##### Create Your Project Set up a new React project with Vite: ```bash pnpm create vite my-telegram-dojo-app --template react-ts cd my-telegram-dojo-app && pnpm install ``` ##### Install Dependencies Add the essential packages for Telegram and Dojo integration: ```bash # Dojo SDK packages pnpm add @dojoengine/core @dojoengine/sdk @dojoengine/torii-client @dojoengine/torii-wasm # Telegram integration pnpm add @telegram-apps/sdk-react # Cartridge Controller for wallet management pnpm add @cartridge/connector @cartridge/controller @cartridge/account-wasm # Additional utilities pnpm add @dojoengine/utils zustand immer # Development tools pnpm add -D vite-plugin-wasm vite-plugin-top-level-await ``` ##### Configure Vite Update your `vite.config.ts` to support WASM modules: ```typescript import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import wasm from "vite-plugin-wasm"; import topLevelAwait from "vite-plugin-top-level-await"; export default defineConfig({ plugins: [react(), wasm(), topLevelAwait()], server: { fs: { allow: [".."], }, }, }); ``` ##### Initialize the Application Set up your main application entry point: ```typescript // src/main.tsx import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import { Route, BrowserRouter as Router, Routes } from "react-router-dom"; import { SDKProvider } from "@telegram-apps/sdk-react"; import { AccountProvider } from "./hooks/useAccount"; ReactDOM.createRoot(document.getElementById("root")!).render( } /> ); ``` ::: ### Account Management with Controller #### Session Key Management Beast Slayers demonstrates a robust account management pattern using Telegram's cloud storage for session persistence. **Key concepts:** ```typescript import { useUtils } from "@telegram-apps/sdk-react"; import { CartridgeSessionAccount } from "@cartridge/account-wasm/session"; import * as Dojo from "@dojoengine/torii-wasm"; // Generate session keys with Dojo utilities const privateKey = Dojo.signingKeyNew(); const publicKey = Dojo.verifyingKeyNew(privateKey); storage.set("sessionSigner", JSON.stringify({ privateKey, publicKey })); // Create Cartridge session account const account = CartridgeSessionAccount.new_as_registered( rpcUrl, privateKey, address, ownerGuid, chainId, { expiresAt, policies } ); // Open Cartridge Controller for session registration const utils = useUtils(); utils.openLink(keychainUrl + sessionParams); ``` **Complete implementation**: See [`src/hooks/useAccount.tsx`](https://github.com/cartridge-gg/beast-slayers/blob/main/src/hooks/useAccount.tsx) **Key integration points:** * **Telegram Cloud Storage**: Persists session keys across app sessions * **Launch Parameters**: Receives account data via `initData.startParam` * **Session Policies**: Define which contract methods the session can execute * **Dojo Integration**: Uses Dojo's cryptographic utilities for key generation ### Dojo Integration #### Game Actions Execute game actions using the session account: ```typescript const handleAttack = async () => { await account.execute([ { contractAddress: ACTIONS_ADDRESS, entrypoint: "attack", calldata: [], }, ]); }; ``` **Complete Implementation**: [`src/App.tsx`](https://github.com/cartridge-gg/beast-slayers/blob/main/src/App.tsx) #### Torii Client Setup Initialize the Torii client for real-time blockchain data synchronization: ```typescript import { useState, useEffect } from "react"; import { createClient, ToriiClient } from "@dojoengine/torii-wasm"; const [client, setClient] = useState(); useEffect(() => { createClient({ toriiUrl: "https://api.cartridge.gg/x/yourapp/torii", rpcUrl: "https://api.cartridge.gg/x/starknet/mainnet", worldAddress: "0x...", }).then(setClient); }, []); ``` #### Entity Queries and Subscriptions Use the Torii client to query and subscribe to entities: ```typescript // Entity queries const entities = await client.getEntities({ clause: { Keys: { keys: ["0xfea4"], // Entity key models: ["beastslayers-Game"], }, }, }); // Real-time subscriptions const subscription = await client.onEntityUpdated( [{ HashedKeys: Object.keys(entities) }], (hashedKeys, models) => { // Handle state updates updateGameState(models["beastslayers-Game"]); } ); ``` **Complete implementations:** * **Entity Subscriptions**: [`src/hooks/useBeast.tsx`](https://github.com/cartridge-gg/beast-slayers/blob/main/src/hooks/useBeast.tsx) * **Player State**: [`src/hooks/useWarrior.tsx`](https://github.com/cartridge-gg/beast-slayers/blob/main/src/hooks/useWarrior.tsx) * **Token Balances**: [`src/hooks/useThingBalances.tsx`](https://github.com/cartridge-gg/beast-slayers/blob/main/src/hooks/useThingBalances.tsx) #### Telegram-Specific Features ```typescript const viewport = useViewport(); useEffect(() => { viewport?.expand(); // Expand Mini App viewport }, [viewport]); ``` **Complete Implementation**: [`src/App.tsx`](https://github.com/cartridge-gg/beast-slayers/blob/main/src/App.tsx) ### Deployment and Testing #### Telegram Bot Setup 1. Message [@BotFather](https://t.me/botfather) to create a new bot 2. Use `/newapp` command to create a Mini App 3. Set the Web App URL to your development or production URL 4. Configure the bot description and about text #### Local Development ```bash pnpm run dev # Start development server npx ngrok http 5173 # Create HTTPS tunnel for Telegram ``` #### Production Deployment ```bash pnpm run build && npx vercel ``` ### Complete Example **[Beast Slayers](https://github.com/cartridge-gg/beast-slayers/blob/main)** is a fully functional Telegram Mini App that demonstrates: * **Account Management**: [`src/hooks/useAccount.tsx`](https://github.com/cartridge-gg/beast-slayers/blob/main/src/hooks/useAccount.tsx) - Session keys & cloud storage * **Real-time State**: [`src/hooks/useBeast.tsx`](https://github.com/cartridge-gg/beast-slayers/blob/main/src/hooks/useBeast.tsx) - Entity subscriptions and live updates * **Game Actions**: [`src/App.tsx`](https://github.com/cartridge-gg/beast-slayers/blob/main/src/App.tsx) - Session-based transaction execution * **Telegram Integration**: [`src/main.tsx`](https://github.com/cartridge-gg/beast-slayers/blob/main/src/main.tsx) - SDKProvider and routing setup * **Dependencies**: [`package.json`](https://github.com/cartridge-gg/beast-slayers/blob/main/package.json) - Complete package configuration The game showcases how to build engaging blockchain-based experiences that feel native to the Telegram platform while leveraging Dojo's ECS architecture. ## dojo.unity Unity is one of the world's most popular cross-platform game engines, powering millions of games across mobile, desktop, console, and web platforms. With its intuitive visual editor, robust scripting capabilities in C#, and extensive asset ecosystem, Unity enables developers to create everything from simple 2D indies to complex 3D AAA titles. dojo.unity is the official Unity Engine SDK for interacting with Dojo worlds to develop web and desktop 2D and 3D games. Whether you're creating a tactical RPG, a real-time strategy game, or an immersive 3D world, dojo.unity provides the tools you need to bring your onchain game vision to life. ### Core Concepts Before diving into the exciting world of onchain games and worlds with Unity, let's explore some essential concepts: #### WorldManager The **WorldManager** is the central hub for organizing and controlling entities within your Dojo world in Unity. ![world-manager](/client/unity/world-manager.webp) During initialization, the WorldManager receives `WorldManagerData`, which defines essential settings like your Torii URL, RPC URL, and world address. While these settings are initially provided, you have the flexibility to adjust them by creating different scriptable objects. ![world-manager-data](/client/unity/world-manager-data.webp) In Unity, entities are represented by `GameObject` instances. The WorldManager simplifies their management by offering methods to both add/remove entitites and access them collectively or by individual identifiers. #### Synchronization Master The Synchronization Master acts as the bridge between Unity and your Dojo world, seamlessly synchronizing and managing entities. ![sync-master](/client/unity/sync-master.webp) Key Features: * Control synchronization: Set the maximum number of entities to synchronize. * Event-driven communication: * `OnSynchronized`: Notifies you when entities were successfully synchronized from Dojo world to Unity. * `OnEntitySpawned`: Triggered whenever a new entity is spawned in the Unity environment. * Dynamic entity management: * `SynchronizeEntities`: Asynchronously retrieves and spawns entities from the Dojo world in the Unity environment. * `HandleEntityUpdate`: Dynamically updates existing entities or spawns new ones based on changes received from the Dojo world, ensuring seamless synchronization. #### Contract Bindings In order to link your Dojo code, written in Cairo, with your Unity code, written in C#, we rely on something known as a "contract binding". A contract binding is an automatically-generated "stub" allowing code in one language to call functions implemented in another language. Dojo's Sozo CLI provides built-in support for contract bindings, through [Cainome](/toolchain/cainome). You can learn more about Sozo's binding generation features [here](/toolchain/sozo/binding-generation). ### Getting Started To get started with the dojo.unity SDK, follow these steps: ::::steps ##### Prerequisites Before getting started, ensure you have [Unity](https://unity.com/download) `>= 2022.3.15f1` installed. ##### Download dojo.unity Visit the [dojo.unity release page](https://github.com/dojoengine/dojo.unity/releases) and download the latest version of `dojo.unitypackage`. ##### Open or create a Unity project Launch Unity and either create a new project or open an existing one where you intend to integrate dojo.unity ##### Import `dojo.unitypackage` Navigate to `Assets/Import Package/Custom Package` within your Unity project. Choose the downloaded `dojo.unitypackage` file. ![unitypackage01](/client/unity/import-unitypackage-01.webp) Finally, ensure to check only the intended platforms for your project. ![unitypackage02](/client/unity/import-unitypackage-02.webp) :::warning If your project includes the `Plugins/iOS` directory, note that it requires **Git Large File Storage (LFS)** to be uploaded. Refer to [GitHub's documentation](https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-git-large-file-storage) for more information. ::: ##### Import Newtonsoft's [Json.NET](https://www.newtonsoft.com/json) dependency In Unity, navigate to `Window/Package Manager`. ![unitypackage01](/client/unity/unitypackage-dependencies-01.webp) Once the `Package Manager` window opens, select `Add package from git URL` ![unitypackage02](/client/unity/unitypackage-dependencies-02.webp) Enter `com.unity.nuget.newtonsoft-json` as the package URL, click `Add` and then `Done` to import the dependency. :::: ### Setting up a Unity scene :::steps ##### Add Essential Prefabs In your Unity project, navigate to the scene where you want to integrate the SDK. 1. From the `Project` window, locate the `Assets/Dojo/Prefabs` folder. 2. Drag the `WorldManager` prefab into your scene. This prefab acts as the central hub for managing entities in your Dojo world. 3. Additionally, navigate to the `Assets/Dojo/Runtime` folder and drag the `UnityMainThreadDispatcher` prefab into your scene. ##### Configuring the `WorldManager` **Default Configuration** The WorldManager operates with a default configuration called `WorldManagerDataLocalConfig`, residing in `Dojo/Runtime/Config`. ![world-manager-data](/client/unity/world-manager-data.webp) Feel free to modify this configuration directly if it suits your project's requirements. **Creating Custom Configurations** To create separate configuration files for different environments (like Slot), first navigate to the `Project` window, then right-click and choose `Create > ScriptableObjects > WorldManagerData`. Customize the configuration values within this new `ScriptableObject` instance. **Applying a Configuration** To use a specific configuration, locate the `WorldManager` game object in your scene. Drag the desired `ScriptableObject` (either the default one or your custom configuration) onto the `DojoConfig` field within the `WorldManager` component. ##### Adding model bindings 1. Generate model bindings: If you haven't already created your model bindings, please refer to the [bindgen section](/toolchain/sozo/binding-generation#unity) for instructions. 2. Import model bindings: Locate the `bindings/client/unity/Models` folder within your Dojo project, and drag the desired `model` files from this folder into your Unity project. The [Synchronization Master](#synchronization-master) will automatically detect and load these models for seamless data exchange. ![bindings-example](/client/unity/bindings-example.webp) ::: ### Calling Dojo Systems This section explores the process of interacting with Dojo systems from Unity. #### Account Creation Every transaction to a Dojo system must come from an **account**. Accounts are required to sign and execute transactions that modify your game's onchain state. We have two options for creating an account: a simple account, or a burner account. To create a **simple account**, use this code: ```cs using Dojo; using Dojo.Starknet; using UnityEngine; void Start() { var provider = new JsonRpcClient(dojoConfig.rpcUrl); var signer = new SigningKey(masterPrivateKey); var account = new Account(provider, signer, new FieldElement(masterAddress)); } ``` For a **burner account**, use this code: ```cs using Dojo; using Dojo.Starknet; using UnityEngine; async void Start() { Account burnerAccount = await CreateBurnerAccount(dojoConfig.rpcUrl, masterAddress, masterPrivateKey); } private async Task CreateBurnerAccount(string rpcUrl, string masterAddress, string masterPrivateKey ) { var provider = new JsonRpcClient(rpcUrl); var signer = new SigningKey(masterPrivateKey); var account = new Account(provider, signer, new FieldElement(masterAddress)); BurnerManager burnerManager = new BurnerManager(provider, account); return await burnerManager.DeployBurner(); } ``` :::tip Replace `masterAddress` and `masterPrivateKey` with the **account Address** and **private key** of the prefunded Katana account. ::: #### System Execution Once we have an [account](#account-creation), we must execute a call to a Dojo system. To do this, we must first teach our Unity project about our Dojo contracts using [contract bindings](#contract-bindings). Sozo's [bindgen](/toolchain/sozo/binding-generation#unity) generates bindings for contracts, which must be transferred into your Unity project. Let's consider a practical example: a `PlayerActions` contract that handles player creation in an RPG game. This system allows players to create their character by choosing a name and selecting their gender, then stores this information onchain as part of the game state. ```rust #[starknet::interface] pub trait IPlayerActions { fn create( ref self: T, player_name: ByteArray, gender_id: u32 ); } #[dojo::contract] pub mod player_actions { use super::IPlayerActions; use starknet::{ContractAddress, get_caller_address}; use dojo::model::ModelStorage; use dojo::world::{WorldStorage, WorldStorageTrait}; #[abi(embed_v0)] impl PlayerActionsImpl of IPlayerActions { fn create(ref self: ContractState, player_name: ByteArray, gender_id: u32) { let mut world = self.world(@"namespace"); let player = get_caller_address(); // Create a new player model with the provided data let player_data = Player { player, name: player_name, gender: gender_id, level: 1, experience: 0 }; // Write the player data to the world state world.write_model(@player_data); } } } ``` The generated bindings would be as follows: ```cs using System; using System.Threading.Tasks; using Dojo; using Dojo.Starknet; using UnityEngine; using dojo_bindings; public class PlayerActions : MonoBehaviour { // The address of this contract public string contractAddress; // Call the `create` system with the specified Account and calldata // Returns the transaction hash. Use `WaitForTransaction` to wait for the transaction to be confirmed. public async Task Create(Account account, string player_name, uint gender_id) { return await account.ExecuteRaw(new dojo.Call[] { new dojo.Call{ to = contractAddress, selector = "create", calldata = new dojo.FieldElement[] { new FieldElement(player_name).Inner(), new FieldElement(gender_id.ToString()).Inner() } } }); } } ``` Let's break down the concepts: * `public string contractAddress;`: The contract address of the `PlayerActions` system, obtained as output from `sozo migrate`. * `new dojo.Call{ ... }`: Creates a new call, where the `selector` is the name of the system function ("create"), and `calldata` contains the serialized parameters (player name and gender ID). * `account.ExecuteRaw(new dojo.Call[] { ... })`: Executes the transaction onchain, creating the player character with the specified attributes. The `account` can be either a simple account or a burner account. When this function is called from Unity (e.g., when a player fills out a character creation form), it will create a new onchain player entity that persists in your Dojo world and can be queried by other systems or clients. :::tip It is possible to execute an array of calls simultaneously, by passing multiple `dojo.Call` instances to `ExecuteRaw`. ::: ### Building your Dojo Game The final stage is building your onchain game for interaction and deployment. Dojo currently supports building for both **desktop** and **WebGL** platforms. #### Building for Desktop Follow these instructions to build your game for Windows, Mac, or Linux. 1. Navigate to `File/Build Settings`. 2. From the right menu choose the `Windows, Mac, Linux` option. 3. From the Platform dropdown, select the target desktop platform. 4. Click the Build button to initiate the build process. ![build-desktop](/client/unity/build-desktop.webp) #### Building for WebGL :::steps ##### Ensure the WebGL module is installed 1. Open the `Unity Hub`. 2. Go to `Installs`. 3. Select the Unity version matching your project. 4. Click `Add Modules`. 5. Under the Modules tab, locate and install the `WebGL` module. ![unityhub-add-module](/client/unity/webgl-module.webp) ##### Configure the WebGL player settings 1. Go to `Edit/Project Settings/Player` (or navigate directly using the Project Settings window). 2. Select the `WebGL` tab. 3. Under `Resolution and Presentation`, ensure the `Dojo Template` is selected. ![unityhub-add-module](/client/unity/webgl-player-settings.webp) ##### Build your project 1. Navigate to `File/Build Settings`. 2. From the right menu choose `WebGL` option. 3. Click the Build button to build your game for WebGL. ![build-webgl](/client/unity/build-webgl.webp) ::: ### Troubleshooting #### Build Issues ##### Model Binding Errors When modifying the bindings generated during [bindgen](/toolchain/sozo/binding-generation#unity), ensure that all fields in the model bindings are declared as public. :::warning Failing to do so can result in the values of the fields not being loaded properly. ::: ##### WebGL Build Errors You may encounter the following error while building for WebGL: ![webgl-error](/client/unity/webgl-error.webp) Here are the steps to address it: 1. **Verify Dojo Template Selection**: * Navigate to `Edit > Project Settings > Player` (or directly through the Project Settings window). ![webgl-error](/client/unity/webgl-build-fail.webp) > Example without `Dojo` template selected * Select the WebGL tab. * Under `Resolution and Presentation`, ensure the `Dojo` Template is selected. * If the Dojo template is missing, proceed to `step 2`. 2. **Download WebGL Templates Folder**: If the Dojo template is unavailable in Player Settings, it's likely missing from your project. * Navigate to the [Dojo Unity repository](https://github.com/dojoengine/dojo.unity) * Download the `WebGL templates` folder. * Add this folder to your project's Assets directory. 3. **Rebuild Your Project**: After ensuring the Dojo template is selected or added, try rebuilding your project for WebGL. ##### Slot on Desktop Currently, Slot functionality is not available on desktop platforms due to a server error preventing the initialization of the ToriiClient within the WorldManager component. Attempting to use Slot with a desktop build will result in the following exception: ```cs Exception: status: Unknown, message: "h2 protocol error: http2 error: connection error detected: frame with invalid size", details: [], metadata: MetadataMap { headers: {} } Dojo.Torii.ToriiClient..ctor (System.String toriiUrl, System.String rpcUrl, System.String world, dojo_bindings.dojo+KeysClause[] entities) (at Assets/Dojo/Runtime/Torii/ToriiClient.cs:40) Dojo.WorldManager.Start () (at Assets/Dojo/Runtime/WorldManager.cs:28) ``` :::note If you intend to deploy with Slot, build the game for WebGL instead. ::: #### Runtime Issues ##### Async Calls Failing If asynchronous (`await`) calls to your Dojo systems are not working, ensure that the `UnityMainThreadDispatcher` is present in your scene. This prefab should have been added during the [scene setup](#add-essential-prefabs) process. :::info The `UnityMainThreadDispatcher` can be found in `Assets/Dojo/Runtime directory`. ::: ##### Authentication Errors While executing a transaction from your Unity project, you may encounter the following error in the Katana logs: ```bash 2024-03-19T18:05:46.841197Z WARN executor: Transaction execution error: "Error in the called contract (0x00280a3deba2004bbbdb3d60a619f3059305f2399ab1e1cd630ec20249abe5fa): Error at pc=0:4573: Got an exception while executing a hint. Cairo traceback (most recent call last): Unknown location (pc=0:67) Unknown location (pc=0:1835) Unknown location (pc=0:2478) Unknown location (pc=0:3255) Unknown location (pc=0:3795) Error in the called contract (0x05024efa0bbd4ec33a2f56251a5d67d8ed2b1e88cbdba566cbce6d3d757db21f): Error at pc=0:1867: Got an exception while executing a hint: Hint Error: Execution failed. Failure reason: 0x6e6f7420777269746572 ('not writer'). Cairo traceback (most recent call last): Unknown location (pc=0:256) Unknown location (pc=0:634) Error in the called contract (0x01bf3dfc0c2b66b3d4abb47e9c8e4c5552992dbc70bb2566b9f6d6ee9b707317): Execution failed. Failure reason: 0x6e6f7420777269746572 ('not writer'). ``` The solution is to navigate to the `src` directory within your Dojo project and run the `default_auth.sh` script in your shell. ![auth](/client/unity/auth.webp) ### Example Project This section provides a walkthrough for running the example from the dojo.unity repository using the `Dojo Starter` repository. [![dojo.unity demo](https://markdown-videos-api.jorgenkh.no/url?url=https%3A%2F%2Fyoutu.be%2F25ocgPsHs4w)](https://youtu.be/25ocgPsHs4w) ::::steps ##### Prerequisites Clone the [dojo.unity](https://github.com/dojoengine/dojo.unity) and [Dojo Starter](https://github.com/dojoengine/dojo-starter) repositories. ##### Setting up Dojo Starter Follow the steps outlined in the [Dojo Starter setup guide](/tutorials/dojo-starter) to deploy your Dojo project locally: 1) launch Katana, 2) build with Sozo, and 3) launch Torii. ##### Setting up dojo.unity 1. Open the scene: In the `Project tab`, navigate to `Assets/Spawn And Move/Scenes/Sample scene` 2. Adjusting Scriptable Objects: * Verify that the `player address` and `player private` key in `Assets/Spawn And Move/Dojo5.0Data` match the output of the Katana terminal. * Verify that the `world address` in the Scriptable Object located at `Assets/Dojo/Runtime/Config/WorldManagerLocalConfig` matches the output of the Sozo migrate command. ##### Running the Example 1. Play the opened scene. 2. To spawn an entity, press the `space key`. 3. To move the entity, use the keys: `a (left)`, `w (forward)`, `s (backward)`, `d (right)`. :::tip You can create multiple entities by pressing the `space` key. To select an entity for movement, simply `right-click` on it. ::: :::: \:::: ## dojo.unreal [dojo.unreal](https://github.com/dojoengine/dojo.unreal) is the official Unreal Engine 5 SDK for Dojo. With this SDK, you can combine the power of Dojo and Unreal to develop mobile and desktop 2D and 3D games. ### Prerequisites Before getting started, ensure you have the Unreal Engine installed. To install the Unreal Engine, [follow these instructions](https://www.unrealengine.com/en-US/download). ### Getting Started To get started with the dojo.unreal SDK, follow these steps: :::steps ##### Download dojo.unreal Obtain the Dojo plugin by visiting [dojo.unreal](https://github.com/dojoengine/dojo.unreal). Either clone the repository or download it as a ZIP file to access the plugin. ![unrealdl](/client/unreal/downloadzip.webp) ##### Set Up Your Unreal Project Launch Unreal Engine 5 and create a new project or open an existing one where you will implement dojo.unreal. ![unrealcreate](/client/unreal/create_new_cpp_project.webp) ##### Import the Plugin 1. Navigate to your project directory by right-clicking the project in Epic Games Launcher (Unreal Engine > Library > My Projects) ![unrealdir](/client/unreal/open_project_directory.webp) 2. Create a Plugins directory if one does not exist 3. Copy the Plugins/Dojo directory from dojo.unreal into your project's Plugins folder 4. Verify the plugin version in `Plugins/Dojo/Source/Dojo/Dojo.Build.cs`. For version updates or platform-specific deployments, refer to [Update the plugin](#update-the-plugin) or [Add a new platform](#add-a-new-platform) respectively. 5. Enable the Dojo plugin by adding `"Dojo"` to the `PublicDependencyModuleNames.AddRange` list in `Source/DojoBookTest/PROJECTNAME.Build.cs` 6. Generate bindings using `sozo build --unrealengine` and add the resulting `DojoHelpers.cpp` and `DojoHelpers.h` to `Source/PROJECTNAME` 7. Regenerate project files (see [Regenerate project files](#regenerate-project-files) for detailed instructions) ##### Configure the Project 1. Open your project in Unreal Engine 5 to initiate the rebuild ![ue5rebuild](/client/unreal/rebuild.webp) 2. Create a new blueprint ![unrealbp](/client/unreal/create_new_blueprint.webp) 3. Initialize the DojoHelpers actor and store it as a variable ![unrealspawnactor](/client/unreal/create_dojo_helpers_actor.webp) 4. Implement Torii connections and other desired functionality ![unrealusehelpers](/client/unreal/use_dojo_helpers.webp) 5. Launch the game and verify the connection in the Output Log ![unrealtoriiconnected](/client/unreal/torii_connected.webp) ::: ### Regenerating Files Follow these instructions to regenerate your project files: :::steps ##### Delete the following directories if they exist: * Binaries/ * Saved/ * Intermediate/ * DerivedDataCache/ ##### Generate fresh project files Follow the instructions for your platform: **Mac** Open Terminal in your project directory and run: ```bash # For UE 5.5 /Users/Shared/Epic\ Games/UE_5.5/Engine/Build/BatchFiles/Mac/GenerateProjectFiles.sh -project="$PWD/ProjectName.uproject" -game # For other UE versions, adjust the path accordingly ``` **Linux** Run from project directory: ```bash ~/UnrealEngine/Engine/Build/BatchFiles/Linux/GenerateProjectFiles.sh -project="$PWD/ProjectName.uproject" ``` **Windows** * Right click on your project file `ProjectName.uproject` * Select "Generate Visual Studio Project Files" * Or run this from PowerShell: ``` & 'C:\Program Files\Epic Games\UE_5.5\Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.exe' -projectfiles -project="$PWD\ProjectName.uproject" -game -engine ``` ::: ### Integrating Bindings Using the command `sozo build --unrealengine`, you can add the `DojoHelpers.h` and `DojoHelpers.cpp` files to your project. These files use the Dojo plugin and expose simple functions that you can use from another `cpp` file or directly from a Blueprint. If you have set up everything correctly, you will have a connected Torii client in a Blueprint. :::info You can learn more about Unreal Engine Blueprints [here](https://www.unrealengine.com/en-US/blog/introduction-to-blueprints). ::: Here are all the things you need to set up: ::::steps ##### Create public variables in your `DojoGameLogic` Blueprint ![unrealinspector](/client/unreal/configinspector2.webp) :::tip Click on the closed eye to be able to change it in the Inspector. ::: ![unrealinspector2](/client/unreal/configinspector.webp) ##### Create your `DojoHelpers` actor ![unrealhelpers](/client/unreal/spawnhelpers.webp) ##### Connect to Torii and set contract addresses ![unrealtoriicontracts](/client/unreal/connect_setup.webp) ##### Subscribe to and fetch existing models This is where you will retrieve all the models that are stored onchain. Both subscription and fetch existing models will trigger a custom event called `OnDojoModelUpdate`. Always bind this custom event before calling any of these functions. ![unrealmodels0](/client/unreal/getmodelupdate.webp) You can then create functions for each model to retrieve the values and update your game. ![unrealmodels3](/client/unreal/accessmodel2.webp) Generated models follow the Unreal Engine 5 convention in `cpp`. For example, if you have a `Vec2` model, it will be called `FVec2`. For enums, to avoid conflicts, they are prefixed with "ED" (E for Enums, D for Dojo). For example, `Direction` enum becomes `EDDirection`. When used in a Blueprint, the prefix is removed. ##### Controller To send transactions, you need to use the [Cartridge Controller](https://docs.cartridge.gg/controller/overview). When a player connects, a new browser tab opens to handle authentication. The connection result is returned through a custom event after the player successfully connects. :::tip Always bind this Custom Event before calling either `ControllerConnect` or `ControllerGetAccountOrConnect`. ::: The `ControllerAccount` method always opens a new browser tab for authentication. `GetAccountOrConnect` first attempts to retrieve a previously stored account, and only opens a new browser tab if no existing account is found. :::tip If the account address is `0x0`, it means the player did not connect to the Controller. ::: We also call the `CallControllerDojoStarterActionsSpawn` function. ![unrealcontroller](/client/unreal/controllerconnectandcall.webp) More information about calls is provided below. ##### Calls There are two functions available for each selector of your contracts. If you used `CreateBurnerDeprecated`, use functions prefixed with `Call`. If you used the Controller Connect, use functions prefixed with `CallController`. The format for each function is `` with all the required parameters ![unrealcall](/client/unreal/callmethod.webp) :::: ### Updating the Plugin The dojo.unreal plugin is built on [dojo.c](/client/sdk/c), the foundational C library that powers all Dojo SDKs. To update the plugin to a new version: 1. Build the `dojo.c` library for your platform 2. In your UE5 project directory, create a new directory next to `Dojo.Build.cs` named after your version number (e.g. "1.7.0") 3. Copy the `dojo.h` header file into this new version directory 4. Create a `lib/` subdirectory and copy the `libdojo_c` library file into it 5. Update the `Dojo.Build.cs` file to reference your new version number 6. Regenerate the project files following the instructions above ### Adding a New Platform To add support for a new platform: :::steps ##### Build the `dojo.c` library for your target platform * Use `cargo build --release --target ` * Common targets include: * iOS: `aarch64-apple-ios` * Android: `aarch64-linux-android` * Windows: `x86_64-pc-windows-msvc` * Mac: `aarch64-apple-darwin` ##### Create the platform-specific directory structure * Navigate to your version folder (e.g. `1.7.0/`) * Create `lib/` directory (e.g. `lib/iOS`, `lib/Android`) * Platform names should match Unreal's naming conventions ##### Copy the built library files * Place the compiled library in the platform directory ##### Update `Dojo.Build.cs` * Add platform-specific conditional logic if needed * Add library path definitions for your platform * Test compilation for the new platform ::: ### Sample Project The dojo.unreal repository contains `ue5dojostarter`, a complete sample project that demonstrates how Dojo can be integrated with Unreal Engine. This sample is built on top of the Unreal Engine 5 First Person template and showcases: * **Complete Dojo Integration**: Working examples of connecting to Torii, subscribing to model updates, and calling system functions * **Blueprint Implementation**: All Dojo functionality implemented through Blueprint nodes for easy understanding * **Game Mechanics**: A simple boat movement game where players can spawn and move around a world * **Controller Authentication**: Integration with Cartridge Controller for wallet authentication * **Model Synchronization**: Real-time updates between onchain state and game visuals The sample project works with the [Dojo Starter](https://github.com/dojoengine/dojo-starter) contracts, providing a complete end-to-end example of an onchain game. To run the sample project locally: 1. Set up and deploy the Dojo Starter contracts following the [getting started guide](/getting-started) 2. Clone the dojo.unreal repository 3. Follow the detailed setup instructions in the [repository README](https://github.com/dojoengine/dojo.unreal) 4. Configure the game with your deployed contract addresses and RPC endpoints The sample project serves as both a learning resource and a starting point for your own Dojo-powered Unreal Engine games. ## Configuration Dojo uses a **profile-based configuration system** that allows you to manage different settings for development, testing, and production environments. Every Dojo command operates within a specific profile context, giving you fine-grained control over how your project builds, deploys, and runs. ### Understanding Profiles Profiles separate your project configuration into distinct environments. This allows you to use different settings for local development versus production deployment without manually changing configuration files. ```bash # Use the default 'dev' profile sozo build # Use a specific profile sozo build --profile mainnet sozo migrate --profile staging ``` **Profile Resolution:** 1. **Specified profile**: If you provide `--profile `, Dojo looks for `dojo_.toml` 2. **Default fallback**: If no profile is specified, Dojo uses the `dev` profile (`dojo_dev.toml`) 3. **Configuration loading**: Each profile file contains deployment and environment settings for that specific context :::warning If a `dojo_dev.toml` does not exist in the contract directory, Dojo will return an error. ::: ### Configuration Files Dojo projects use two main configuration files: #### `Scarb.toml` - Cairo Project Manifest The Scarb manifest defines your project's dependencies, build settings, and Cairo compilation options. This file is required for all Cairo projects and controls how your code compiles. #### `dojo_.toml` - Dojo Profile Configuration Profile-specific files contain deployment settings, world metadata, and environment variables. These files control how your world deploys and behaves in different environments. ```text my-dojo-project/ ├── Scarb.toml # Project manifest & dependencies ├── dojo_dev.toml # Development profile ├── dojo_staging.toml # Staging profile └── dojo_mainnet.toml # Production profile ``` ### Project Manifest (`Scarb.toml`) Your `Scarb.toml` file defines the core project structure and dependencies. Here is the minimum configuration needed for a Dojo project: ```toml [package] cairo-version = "2.12.2" name = "my-dojo-game" version = "1.0.0" edition = "2024_07" [[target.starknet-contract]] sierra = true build-external-contracts = ["dojo::world::world_contract::world"] [dependencies] starknet = "2.12.2" dojo = "1.7.1" [dev-dependencies] cairo_test = "2.12.2" dojo_cairo_test = "1.7.1" [tool.scarb] allow-prebuilt-plugins = ["dojo_cairo_macros"] [features] default = [] ``` You can refer to the [Scarb documentation](https://docs.swmansion.com/scarb/docs/guides/defining-custom-profiles.html) for more information. #### Custom Profiles in Scarb Add custom profiles to control build settings for different environments: ```toml # Development profile (default) [profile.dev] # Fast builds, debug symbols enabled [profile.staging] # Settings specific to staging [profile.mainnet] # Optimized builds for production ``` :::note Every `dojo_.toml` must have a matching section header in `Scarb.toml`. ::: #### External Contract Dependencies When using external libraries with models, you must explicitly build their contracts: ```toml [[target.starknet-contract]] build-external-contracts = [ "dojo::world::world_contract::world", # Used by Sozo to check the world version "armory::models::m_Flatbow", # External model contract "tokens::models::m_ERC20Token" # Another external model ] ``` **Pattern for External Models:** * Format: `::::m_` * Example: If `armory` crate has `models::Flatbow` model, include `"armory::models::m_Flatbow"` :::warning Missing external contracts will not cause compilation errors but will cause runtime failures when the world tries to interact with missing model contracts, or when Torii cannot find model definitions to match blockchain event data. ::: ### Profile Configuration (`dojo_.toml`) Profile configuration files contain all deployment and runtime settings for specific environments. Each section controls a different aspect of your Dojo world. #### Basic World Configuration ```toml [world] name = "My Dojo Game" description = "An awesome on-chain game built with Dojo" seed = "my-unique-seed" cover_uri = "file://assets/cover.png" icon_uri = "file://assets/icon.png" website = "https://mydojogame.com" [world.socials] x = "https://x.com/mydojogame" discord = "https://discord.gg/mydojogame" github = "https://github.com/mydojogame/contracts" ``` **Required Fields:** * `name` - Human-readable world name * `seed` - Unique identifier for deterministic world address generation **Optional Fields:** * `description` - World description for metadata * `cover_uri` / `icon_uri` - Visual assets (supports `file://` and `ipfs://`) * `website` - Project website URL * `socials` - Social media links See more information about world metadata [here](/framework/world/metadata). #### Environment Settings ```toml [env] rpc_url = "http://localhost:5050/" account_address = "0x127fd..." private_key = "0xc5b2f..." keystore_path = "/path/to/keystore" world_address = "0x077c0..." ``` **Environment Variables:** * `rpc_url` - Starknet RPC endpoint * `account_address` - Deployer account address * `private_key` - Deployer private key (use `keystore_path` in production) * `keystore_path` - The path to the file containing the deployer's private key. * `world_address` - Deployed world address (set after first deployment) #### Namespace Management ```toml [namespace] default = "game" mappings = { "weapons" = ["Sword", "Bow", "Shield"], "characters" = ["Player", "Enemy", "NPC"] } ``` **How Namespace Mapping Works:** 1. **Default namespace**: All resources not explicitly mapped use this namespace 2. **Explicit mappings**: Map specific models/contracts to custom namespaces 3. **Resource tags**: Final resource tags become `-` **Example:** * `Player` model → `characters-Player` tag * `GameState` model → `game-GameState` tag (uses default namespace) #### Permission Configuration ```toml # Format: "" = [""] [writers] # Namespace-level permissions "game" = ["game-actions", "game-admin"] # Resource-specific permissions "game-Position" = ["game-movement"] "weapons-Sword" = ["weapons-combat", "game-actions"] [owners] # Namespace ownership "game" = ["game-admin"] # Resource ownership "weapons" = ["weapons-manager"] ``` **Permission Hierarchy:** * **Namespace permissions** - Control access to all resources in a namespace * **Resource permissions** - Control access to specific models/contracts * **Writers** - Can modify data in models * **Owners** - Can modify data AND manage permissions For more details on managing permissions at runtime, see the [World Permissions](/framework/world/permissions) guide. #### Contract Initialization By default, Dojo contracts do not have initialization arguments. However, you can pass init arguments to Dojo contracts using a `dojo_init` function: ```cairo #[dojo::contract] mod my_system { fn dojo_init(ref self: ContractState, arg1: felt252, arg2: u256) { // ... } } ``` ```toml [init_call_args] "game-my_system" = ["0x123", "u256:0x456"] ``` :::tip See the [Sozo calldata format](/toolchain/sozo#data-format-reference) for initialization argument formatting. ::: #### External Contract Deployment Deploy and manage external (non-Dojo) contracts alongside your world: ```toml [[external_contracts]] contract_name = "ERC20Token" instance_name = "GoldToken" # If deploying multiple instances salt = "1" # Hashed with contract_name/instance_name to generate contract address constructor_data = [ "str:Gold Coin", # Token name "sstr:GOLD", # Symbol "u256:1000000000000000000", # Total supply "0x1234567890abcdef..." # Owner address ] ``` **External Contract Fields:** * `contract_name` - Cairo contract name to deploy * `instance_name` - Unique instance identifier (for multiple deployments) * `salt` - Deterministic address generation salt * `constructor_data` - Arguments for contract constructor :::tip See the [Sozo calldata format](/toolchain/sozo#data-format-reference) for initialization argument formatting. ::: #### Migration Control ```toml [migration] order_inits = ["game-registry", "game-actions", "token-manager"] skip_contracts = ["game-debug_tools", "test-helpers"] disable_multicall = false ``` **Migration Options:** * `order_inits` - Specific order for contract initialization calls * `skip_contracts` - Do not deploy these contracts (but still build them) * `disable_multicall` - Force individual transactions instead of batching #### Resource Metadata Add descriptive metadata for your models, contracts, and events: ```toml [[models]] tag = "game-Position" description = "Player position in the game world" icon_uri = "file://icons/position.png" [[contracts]] tag = "game-actions" description = "Core gameplay actions and mechanics" icon_uri = "file://icons/actions.png" [[events]] tag = "game-PlayerMoved" description = "Emitted when a player changes position" ``` See more information about resource metadata [here](/framework/world/metadata). ### Configuration Examples #### Development Profile (`dojo_dev.toml`) ```toml [world] name = "My Game (Development)" seed = "dev-my-game" [env] rpc_url = "http://localhost:5050/" account_address = "0x127fd..." private_key = "0xc5b2f..." [namespace] default = "dev" [writers] "dev" = ["dev-actions"] [migration] disable_multicall = true # Easier debugging ``` #### Production Profile (`dojo_mainnet.toml`) ```toml [world] name = "My Game" seed = "prod-my-game" description = "Production deployment of My Game" cover_uri = "ipfs://YourCoverHash" icon_uri = "ipfs://YourIconHash" website = "https://mygame.com" [env] rpc_url = "https://api.cartridge.gg/x/prod-my-game/katana" account_address = "0x127fd..." keystore_path = "~/.starknet_accounts/mainnet.json" [env.ipfs_config] url = "https://ipfs.infura.io:5001" username = "prod_username" password = "prod_password" [namespace] default = "game" mappings = { "items" = ["Sword", "Shield", "Potion"] } [writers] "game" = ["game-actions"] "items" = ["game-actions", "item-manager"] [owners] "game" = ["game-admin"] ``` ### See Also * **[World Permissions](/framework/world/permissions)** - Runtime permission management * **[Sozo Reference](/toolchain/sozo)** - Command-line tool documentation * **[Calldata Format](/toolchain/sozo#data-format-reference)** - Constructor argument formatting * **[World Metadata](/framework/world/metadata)** - World and resource metadata ## Model API Reference This reference covers the complete Model API in Dojo, including storage operations, field access patterns, and performance optimizations. ### Core Model Trait Every model in Dojo implements the [`Model` trait](https://github.com/dojoengine/dojo/blob/main/crates/dojo/core/src/model/model.cairo), which provides comprehensive functionality for model operations. The following examples are based on the following simple `Position` model: ```cairo #[derive(Drop, Serde)] #[dojo::model] struct Position { #[key] id: u32, x: u32, y: u32 } ``` ##### Model Identity ```cairo use dojo::model::Model; // Get the entity ID for this model instance let entity_id: felt252 = model.entity_id(); // Get model name let name: ByteArray = Model::::name(); // Get model selector for a namespace let selector: felt252 = Model::::selector(namespace_hash); // Get model definition let definition: ModelDef = Model::::definition(); // Get model schema information let schema: Struct = Model::::schema(); // Get model layout information let layout: Layout = Model::::layout(); // Get specific field layout let field_layout: Option = Model::::field_layout(selector!("x")); // Get model size information let unpacked_size: Option = Model::::unpacked_size(); let packed_size: Option = Model::::packed_size(); // Get instance layout from model let instance_layout: Layout = model.instance_layout(); ``` ##### Model Serialization ```cairo use dojo::model::Model; // Serialize keys and values let keys: Span = model.serialized_keys(); let values: Span = model.serialized_values(); // Reconstruct model from serialized data let model: Option = Model::::from_serialized(keys, values); ``` ##### Model Pointers ```cairo use dojo::model::{Model, ModelPtr}; // Create pointer from keys (advanced usage) let ptr: ModelPtr = Model::::ptr_from_keys(player_address); // Create pointers from multiple keys (advanced usage) let ptrs: Span> = Model::::ptrs_from_keys( array![player1, player2, player3].span() ); // Create pointer from entity ID (advanced usage) let ptr: ModelPtr = Model::::ptr_from_id(entity_id); // Get pointer from model instance (advanced usage) let ptr: ModelPtr = model.ptr(); ``` :::note Model pointers are primarily used internally for advanced operations like field-level access. For most use cases, prefer the simpler `world.read_model()` and `world.write_model()` methods. ::: ### Model Storage Operations The [`ModelStorage` trait](https://github.com/dojoengine/dojo/blob/main/crates/dojo/core/src/model/storage.cairo) provides all storage operations for models: ```cairo use dojo::model::{ModelStorage}; use dojo::world::{WorldStorage, WorldStorageTrait}; use starknet::get_caller_address; let mut world: WorldStorage = self.world(@"my_game"); ``` #### Basic Storage Operations ##### Writing Models ```cairo // Write single model let player = get_caller_address(); let position = Position { player, x: 10, y: 20 }; world.write_model(@position); // Write multiple models let positions = array![ @Position { player: player1, x: 10, y: 20 }, @Position { player: player2, x: 30, y: 40 } ]; world.write_models(positions.span()); ``` ##### Reading Models ```cairo // Read single model let player = get_caller_address(); let position: Position = world.read_model(player); // Read multiple models let players = array![player1, player2, player3]; let positions: Array = world.read_models(players.span()); ``` ##### Erasing Models ```cairo // Erase single model world.erase_model(@position); // Erase multiple models world.erase_models(positions.span()); // Erase by model pointer let ptr = Model::::ptr_from_keys(player_address); world.erase_model_ptr(ptr); // Erase multiple models by pointers let ptrs = Model::::ptrs_from_keys(players.span()); world.erase_models_ptrs(ptrs); ``` #### Field-Level Operations Field operations are more efficient when you only need to update specific fields: :::warning Field-level operations are advanced features that require careful use. For most applications, use the simpler `world.read_model()` and `world.write_model()` methods. ::: ##### Reading Fields ```cairo // Read specific field (advanced usage) let player = get_caller_address(); let x_coord: u32 = world.read_member( Model::::ptr_from_keys(player), selector!("x") ); // Read same field from multiple models (advanced usage) let ptrs = Model::::ptrs_from_keys(players.span()); let x_coords: Array = world.read_member_of_models(ptrs, selector!("x")); ``` ##### Writing Fields ```cairo // Write specific field (advanced usage) let player = get_caller_address(); world.write_member( Model::::ptr_from_keys(player), selector!("x"), 42_u32 ); // Write same field for multiple models (advanced usage) let ptrs = Model::::ptrs_from_keys(players.span()); let new_x_values = array![10, 20, 30]; world.write_member_of_models(ptrs, selector!("x"), new_x_values.span()); ``` #### Schema Reading You can read models using custom schemas that implement the `Introspect` trait: ```cairo #[derive(Drop, Serde, Introspect)] struct PositionSummary { x: u32, y: u32, } // Read using custom schema let player = get_caller_address(); let ptr = Model::::ptr_from_keys(player); let summary: PositionSummary = world.read_schema(ptr); // Read multiple with custom schema let ptrs = Model::::ptrs_from_keys(players.span()); let summaries: Array = world.read_schemas(ptrs); ``` ### Model Values Model values contain only the non-key fields of a model: ```cairo #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] player_id: ContractAddress, name: ByteArray, score: u32, level: u8, } // Dojo automatically generates PlayerValue: // struct PlayerValue { // name: ByteArray, // score: u32, // level: u8, // } ``` :::info Under-the-hood, the `*Value` structs are what actually get stored onchain. The `key` fields are stripped out, and are used only for storage addressing. ::: #### Model Value Operations The `ModelValue` trait provides operations for value-only access: ```cairo use dojo::model::{ModelValue}; // Get value serialization let values: Span = player_value.serialized_values(); // Reconstruct from serialized values let player_value: Option = ModelValue::::from_serialized(values); // Get value name and layout let name: ByteArray = ModelValue::::name(); let layout: Layout = ModelValue::::layout(); // Get value selector, i.e. poseidon(namespace_hash, model_name) let selector: felt252 = ModelValue::::selector(namespace_hash); // Get instance layout from value let instance_layout: Layout = player_value.instance_layout(); ``` :::info Under the hood, Dojo addresses models as `poseidon(selector, entity_id)` ::: #### Model Value Storage Operations The `ModelValueStorage` trait provides value-only storage operations that work with just the non-key fields: ```cairo use dojo::model::{ModelValueStorage}; // Read value from entity ID let player = get_caller_address(); let player_felt: felt252 = player.into(); let entity_id = dojo::utils::entity_id_from_serialized_keys([player_felt].span()); let player_value: PlayerValue = world.read_value_from_id(entity_id); // Write value to entity ID let new_value = PlayerValue { name: "Alice", score: 100, level: 5 }; world.write_value_from_id(entity_id, @new_value); // Read multiple values from entity IDs let entity_ids = array![id1, id2, id3]; let values: Array = world.read_values_from_ids(entity_ids.span()); // Write multiple values to entity IDs let values = array![@value1, @value2, @value3]; world.write_values_from_ids(entity_ids.span(), values.span()); // Erase value from entity ID world.erase_value_from_id(entity_id); // Erase multiple values from entity IDs world.erase_values_from_ids(entity_ids.span()); ``` ### Performance Optimizations #### Batch Operations Use batch operations when working with multiple models: ```cairo // Efficient: batch read let players = array![player1, player2, player3]; let positions: Array = world.read_models(players.span()); // Inefficient: individual reads let pos1: Position = world.read_model(player1); let pos2: Position = world.read_model(player2); let pos3: Position = world.read_model(player3); ``` #### Field Access Patterns Use field operations for partial updates: ```cairo // Efficient: update only needed field (advanced usage) let player = get_caller_address(); world.write_member( Model::::ptr_from_keys(player), selector!("x"), new_x_value ); // Ergonomic: read full model, update, write back let mut position: Position = world.read_model(player); position.x = new_x_value; world.write_model(@position); ``` #### Storage Layout Considerations Choose the right introspection trait based on your needs: ```cairo // For upgradeable models (flexible but less efficient) #[derive(Drop, Serde, Introspect)] #[dojo::model] struct FlexibleModel { #[key] id: u32, data: ByteArray, } // For stable models (efficient but not upgradeable) #[derive(Drop, Serde, IntrospectPacked)] #[dojo::model] struct StableModel { #[key] id: u32, x: u32, y: u32, } ``` ### Common Patterns #### Singleton Models For global settings or counters: ```cairo const GAME_CONFIG_ID: u32 = 999999; #[derive(Copy, Drop, Serde)] #[dojo::model] struct GameConfig { #[key] id: u32, max_players: u32, game_duration: u64, } // Usage let config: GameConfig = world.read_model(GAME_CONFIG_ID); ``` #### Composite Key Models For relationships between entities: ```cairo #[derive(Copy, Drop, Serde)] #[dojo::model] struct Ownership { #[key] owner: ContractAddress, #[key] item_id: u32, quantity: u32, } // Usage let ownership: Ownership = world.read_model((owner_address, item_id)); ``` #### Entity Component Pattern Following ECS principles: ```cairo // Components #[derive(Copy, Drop, Serde)] #[dojo::model] struct Position { #[key] entity_id: u32, x: u32, y: u32, } #[derive(Copy, Drop, Serde)] #[dojo::model] struct Health { #[key] entity_id: u32, current: u32, max: u32, } // Systems operate on entities with specific components fn heal_entity(ref world: WorldStorage, entity_id: u32, amount: u32) { let mut health: Health = world.read_model(entity_id); health.current = core::cmp::min(health.current + amount, health.max); world.write_model(@health); } ``` ### Nonexistent Models When reading non-existent models, the API returns default values (all fields set to 0) rather than failing: ```cairo // Reading a non-existent model returns default values let player: ContractAddress = 0.try_into().unwrap(); let position: Position = world.read_model(player); // If model doesn't exist: position.vec.x = 0, position.vec.y = 0 // For optional model reading, check if all fields are default let position: Position = world.read_model(player); let is_default = position.vec.x == 0 && position.vec.y == 0; if is_default { // Handle case where model might not exist } ``` ### Best Practices 1. **Use appropriate traits**: Choose between `Introspect` and `IntrospectPacked` based on your needs 2. **Batch operations**: Use bulk read/write methods when working with multiple models 3. **Field access**: Use field operations for partial updates 4. **Model design**: Keep models small and focused (ECS principle) 5. **Key design**: Use meaningful, collision-resistant keys 6. **Performance**: Consider storage layout and access patterns For more advanced usage and examples, see the [Model Examples](https://github.com/dojoengine/dojo/tree/main/examples) in the Dojo repository. ### Complete Real-World Example Here's a complete example from a typical Dojo game showing common patterns: ```cairo use dojo::model::{ModelStorage}; use dojo::world::{WorldStorage, WorldStorageTrait}; use starknet::{ContractAddress, get_caller_address}; #[derive(Copy, Drop, Serde, Debug)] #[dojo::model] struct Player { #[key] pub owner: ContractAddress, pub experience: u32, pub health: u32, pub coins: u32, } #[derive(Copy, Drop, Serde, Debug)] #[dojo::model] struct Position { #[key] pub player: ContractAddress, pub x: u32, pub y: u32, } #[dojo::contract] mod game_actions { use super::{Player, Position}; use dojo::model::{ModelStorage}; use dojo::world::{WorldStorage}; use starknet::get_caller_address; #[abi(embed_v0)] impl ActionsImpl of IActions { fn spawn_player(ref self: ContractState) { let mut world = self.world(@"my_game"); let playerAddress = get_caller_address(); // Create new player let player = Player { owner: player, experience: 0, health: 100, coins: 0 }; // Set starting position let position = Position { player, x: 0, y: 0 }; // Write both models world.write_model(@player); world.write_model(@position); } fn move_player(ref self: ContractState, x: u32, y: u32) { let mut world = self.world(@"my_game"); let playerAddress = get_caller_address(); // Read current position let mut position: Position = world.read_model(playerAddress); // Update position position.x = x; position.y = y; // Write updated position world.write_model(@position); } fn add_experience(ref self: ContractState, amount: u32) { let mut world = self.world(@"my_game"); let playerAddress = get_caller_address(); // Read current player state let mut player: Player = world.read_model(playerAddress); // Update experience player.experience += amount; // Write updated player world.write_model(@player); } } } ``` ## Entities **Entities are the primary keys to which models are attached.** An entity is described by a `felt252` identifier that serves as a common key across models, allowing you to group related data together. ### ECS Theory Entities in Dojo follow the Entity-Component-System (ECS) architectural pattern: * **Entities**: Unique identifiers that group related components * **Components**: Data containers (your Dojo models) that store entity state * **Systems**: Functions that operate on entities with specific component combinations This separation allows for: * **Composition over inheritance**: Build complex entities from simple components * **Performance**: Efficient data access and cache-friendly operations * **Flexibility**: Easy to add, remove, or modify entity behaviors > For deeper understanding of ECS concepts, read the [ECS-FAQ](https://github.com/SanderMertens/ecs-faq) ### Entity Concepts #### Entity as Primary Key Entities in Dojo are not objects themselves (they have no state), but rather unique identifiers that models use as storage keys. Multiple models can share the same entity ID, effectively creating a collection of components for that entity. ```cairo #[derive(Drop, Serde)] #[dojo::model] struct Position { #[key] entity_id: u32, x: u32, y: u32, } #[derive(Drop, Serde)] #[dojo::model] struct Health { #[key] entity_id: u32, current: u32, max: u32, } ``` #### Entity ID Generation Entity IDs in Dojo are automatically generated from model keys through a deterministic process: **How Keys Become Entity IDs:** 1. **Key Serialization**: All `#[key]` fields are serialized using Cairo's `Serde` trait 2. **Poseidon Hashing**: The serialized keys are hashed using `poseidon_hash_span()` 3. **Entity ID Result**: The hash becomes the unique `felt252` entity ID ```cairo // For a single key model: #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] pub address: ContractAddress, pub name: ByteArray, } // Entity ID is computed as: // poseidon_hash_span([address.into()].span()) ``` ```cairo // For multiple key models: #[derive(Drop, Serde)] #[dojo::model] struct ServerProfile { #[key] pub player: ContractAddress, #[key] pub server_id: u32, pub name: ByteArray, } // Entity ID is computed as: // poseidon_hash_span([player.into(), server_id.into()].span()) ``` :::info You can learn more about Cairo's Poseidon hash function [here](https://www.starknet.io/cairo-book/ch204-02-07-poseidon.html). ::: **Manual Entity ID Generation:** For cases where you need to generate entity IDs manually: ```cairo use dojo::utils::entity_id_from_serialized_keys; // Direct computation from serialized keys let player_felt: felt252 = player_address.into(); let entity_id = entity_id_from_serialized_keys([player_felt].span()); // For composite keys let keys = [player_address.into(), server_id.into()]; let entity_id = entity_id_from_serialized_keys(keys.span()); // Use the World's sequential unique ID generator let unique_id: u32 = world.uuid(); // Use predictable IDs for specific entities const GAME_CONFIG_ENTITY: u32 = 999999; ``` **Key Properties:** * **Deterministic**: Same keys always produce the same entity ID * **Collision Resistant**: Poseidon hash ensures unique IDs for different key combinations * **Order Sensitive**: Key order in the struct determines the serialization order #### Player-Based Entities A common pattern is using player addresses as entity identifiers: ```cairo #[derive(Drop, Serde)] #[dojo::model] struct PlayerStats { #[key] player: ContractAddress, level: u32, experience: u64, } // Use the caller address as model key let player = get_caller_address(); let stats: PlayerStats = world.read_model(player); ``` ### Entity Relationships #### One-to-One Relationships Each entity has exactly one instance of a model: ```cairo // Each player maps to exactly one Character #[derive(Drop, Serde)] #[dojo::model] struct Character { #[key] player: u32, name: ByteArray, class: u8, } ``` #### One-to-Many Relationships Use composite keys for one-to-many relationships: ```cairo // One player can have many inventory slots #[derive(Drop, Serde)] #[dojo::model] struct Inventory { #[key] player: u32, #[key] slot: u32, item_id: u32, quantity: u32, } ``` #### Many-to-Many Relationships Use junction models for many-to-many relationships: ```cairo #[derive(Drop, Serde)] #[dojo::model] struct Guild { #[key] guild_id: u32, name: ByteArray, leader: ContractAddress, } #[derive(Drop, Serde)] #[dojo::model] struct GuildMembership { #[key] guild_id: u32, #[key] player: ContractAddress, role: u8, joined_at: u64, } ``` ### Entity Lifecycle Management #### Creating Entities ```cairo fn spawn(ref world: WorldStorage, name: ByteArray) { let player = get_caller_address(); world.write_model(@Character { player, name, class: 1 }); // Warrior world.write_model(@Position { player, x: 0, y: 0 }); world.write_model(@Health { player, current: 100, max: 100 }); } ``` #### Updating Entities ```cairo fn move_character(ref world: WorldStorage, entity_id: u32, new_x: u32, new_y: u32) { let mut position: Position = world.read_model(entity_id); position.x = new_x; position.y = new_y; world.write_model(@position); } ``` #### Deleting Entities ```cairo fn destroy_character(ref world: WorldStorage, entity_id: u32) { // Remove all components using the key world.erase_model_ptr(Model::::ptr_from_keys(entity_id)); world.erase_model_ptr(Model::::ptr_from_keys(entity_id)); world.erase_model_ptr(Model::::ptr_from_keys(entity_id)); } // You can alternatively pass in a model instance to `erase_model` let character: Character = world.read_model(entity_id); world.erase_model(@character); ``` ### Entity Queries and Operations #### Bulk Entity Operations ```cairo fn heal_multiple_entities(ref world: WorldStorage, entity_ids: Span, amount: u32) { let healths: Array = world.read_models(entity_ids); let mut healed_entities = array![]; let mut i = 0; while i < healths.len() { let mut health = *healths.at(i); health.current = core::cmp::min(health.current + amount, health.max); healed_entities.append(@health); i += 1; }; world.write_models(healed_entities.span()); } ``` ### Entity Patterns #### Archetype Pattern Group entities by their component combinations: ```cairo // Player archetype: has Position, Health, Inventory fn create_player(ref world: WorldStorage, player: ContractAddress) { world.write_model(@Position { entity_id: player.into(), x: 0, y: 0 }); world.write_model(@Health { entity_id: player.into(), current: 100, max: 100 }); world.write_model(@Inventory { entity_id: player.into(), slots: 20 }); } // Enemy archetype: has Position, Health, AI fn create_enemy(ref world: WorldStorage, entity_id: u32) { world.write_model(@Position { entity_id, x: 10, y: 10 }); world.write_model(@Health { entity_id, current: 50, max: 50 }); world.write_model(@AI { entity_id, behavior: 1 }); } ``` #### Prefab Pattern Create reusable entity templates: ```cairo #[generate_trait] impl EntityFactory of EntityFactoryTrait { fn create_warrior(ref world: WorldStorage, entity_id: u32) -> u32 { world.write_model(@Character { entity_id, name: "Warrior", class: 1 }); world.write_model(@Position { entity_id, x: 0, y: 0 }); world.write_model(@Health { entity_id, current: 150, max: 150 }); world.write_model(@Combat { entity_id, attack: 20, defense: 15 }); entity_id } fn create_mage(ref world: WorldStorage, entity_id: u32) -> u32 { world.write_model(@Character { entity_id, name: "Mage", class: 2 }); world.write_model(@Position { entity_id, x: 0, y: 0 }); world.write_model(@Health { entity_id, current: 80, max: 80 }); world.write_model(@Combat { entity_id, attack: 30, defense: 8 }); world.write_model(@Mana { entity_id, current: 100, max: 100 }); entity_id } } ``` ### Best Practices #### Entity ID Management 1. **Use consistent types**: Stick to `u32` or `felt252` for entity IDs 2. **Avoid ID collisions**: Use UUID generation or proper namespacing 3. **Document ID schemes**: Make it clear how IDs are generated and used #### Component Design 1. **Single responsibility**: Each model should represent one aspect of an entity 2. **Avoid deep nesting**: Keep models flat for better performance 3. **Use appropriate keys**: Choose between entity-based and composite keys wisely #### Performance Considerations 1. **Batch operations**: Use bulk read/write for multiple entities 2. **Avoid unnecessary queries**: Check component existence before operations 3. **Use field operations**: Update specific fields rather than entire models #### Code Organization 1. **Group related models**: Keep entity-related models together 2. **Use traits for common operations**: Create traits for entity lifecycle management 3. **Document relationships**: Make entity relationships clear in code comments ## Enums ### What are Enums Enums, or **enumerations**, are a way to define a custom data type that consists of a fixed set of named values, or **variants**. Enums are useful for representing a collection of related values where each value is distinct and has a specific meaning. Enums can be used in game development to represent game states, player actions, or any other set of related constants that a game needs to track. In this example, we've defined an enum called `PlayerCharacter` with four variants: `Godzilla`, `Dragon`, `Fox`, and `Rhyno`. Each variant represents a distinct value of the `PlayerCharacter` type. ```rust #[derive(Serde, Drop, Introspect)] enum PlayerCharacter { Godzilla, Dragon, Fox, Rhyno } ``` :::tip The naming convention is to use `PascalCase` for enum variants. ::: We can also define enums to contain additional values. Here is an alternative definition for `PlayerCharacter`: ```rust #[derive(Serde, Drop, Introspect)] enum PlayerCharacter { Godzilla: u128, Dragon: u32, Fox: (u8, u8), Rhyno: ByteArray } ``` In Cairo, enum variants can contain different data types. :::warning Enums with variant data of different sizes cannot derive `IntrospectPacked` but must derive `Introspect`. If all variant data share the same type, the enum can be packed using the `IntrospectPacked` derive attribute. ::: ### Traits and Enums Traits are a way to define shared behavior across types. By defining traits and implementing them for your enums, you can encapsulate common behaviors. This approach enhances code reusability and maintainability, critical when developing complex systems like games. Consider the `GameStatus` enum, which represents the various game states. ```cairo #[derive(Serde, Copy, Drop, Introspect, PartialEq, Debug)] enum GameStatus { NotStarted, Lobby, InProgress, Finished } ``` Now, we implement the `Into` trait, enabling type coercion for `GameStatus`: ```cairo impl GameStatusFelt252 of Into { fn into(self: GameStatus) -> felt252 { match self { GameStatus::NotStarted => 0, GameStatus::Lobby => 1, GameStatus::InProgress => 2, GameStatus::Finished => 3, } } } ``` ### Enums in Practice Building upon the `GameStatus` enum, we can define a `Game` struct that includes a `GameStatus` field. By implementing a custom trait for the `Game` struct, we can encapsulate game-specific logic and assertions. ```cairo #[derive(Copy, Drop, Serde)] #[dojo::model] struct Game { #[key] id: u64, status: GameStatus, } #[generate_trait] impl GameImpl of GameTrait { // Asserts that the game is in progress fn assert_in_progress(self: Game) { assert(self.status == GameStatus::InProgress, "Game not started"); } // Asserts that the game is in the lobby fn assert_lobby(self: Game) { assert(self.status == GameStatus::Lobby, "Game not in lobby"); } } ``` It is typically best practice to define enums in the same file or module where they are used, especially if they are closely tied to the functionality of that system. This makes the code easier to understand and maintain, as the enum is defined in context. However, if the enum is used across multiple systems or components, it might make sense to define it in a common module that can be imported wherever needed. This approach helps to avoid duplication and keeps the codebase organized. ### Benefits Of Enums 1. Semantic Clarity: Enums provide semantic clarity by giving meaningful names to specific values. Instead of using arbitrary integers or strings, you can use descriptive identifiers. For example, consider an enum representing different player states: `Idle`, `Running`, `Jumping`, and `Attacking`. These names convey the purpose of each state more effectively than raw numeric values. 2. Avoiding Magic Numbers: Magic numbers (hard-coded numeric values) in your code can be confusing and error-prone. Enums help you avoid this pitfall. Suppose you have an event system where different events trigger specific actions. Instead of using 0, 1, 2, etc., you can define an enum like this: ```cairo enum Event { PlayerSpawned, EnemyDefeated, ItemCollected, } ``` Now, when handling events, you can use `Event::PlayerSpawned` instead of an arbitrary number. 3. Type Safety: Enums provide type safety. Each enum variant has a type, preventing accidental mixing of incompatible values. For instance, if you have an enum representing different power-ups, you cannot mistakenly assign a PowerUp value to a variable expecting a different type. ```cairo enum PowerUp { Health, SpeedBoost, Invincibility, } ``` 4. Pattern Matching: Enums shine when used in pattern matching (also known as switch/case statements). You can handle different cases based on the enum variant, making your code more expressive and concise. Example: ```cairo fn handle_power_up(power_up: PowerUp) { match power_up { PowerUp::Health => println!("Restored health!"), PowerUp::SpeedBoost => println!("Zooming ahead!"), PowerUp::Invincibility => println!("Invincible!"), } } ``` 5. Extensibility: Enums allow you to add new variants without breaking existing code. Suppose you later introduce a DoubleDamage power-up. You can simply extend the PowerUp enum: ```cairo enum PowerUp { Health, SpeedBoost, Invincibility, DoubleDamage, } ``` Enums serve as powerful tools for creating expressive, self-documenting code. They enhance readability, prevent errors, and facilitate better software design. Read more about Cairo enums [here](https://book.cairo-lang.org/ch06-00-enums-and-pattern-matching.html) ## Models > Models = Data ***TL;DR*** * Models store structured data in your world. * Use the `#[dojo::model]` attribute to define them. * Models must have at least one key. * Define the key(s) using the `#[key]` attribute. * Models are Cairo structs with automatic onchain introspection. * Custom enums and types are supported if they implement [`Introspect`](./introspection). ### What are models? Models are Cairo structs annotated with the `#[dojo::model]` attribute. Consider these models as a key-value store, where the `#[key]` attribute defines the key. While models can contain any number of fields, adhering to best practices in Entity-Component-System (ECS) design involves maintaining small, isolated models. This approach fosters modularity and composability, enabling you to reuse models across various entity types. ```cairo #[derive(Drop, Serde)] #[dojo::model] struct Moves { #[key] player: ContractAddress, remaining: u8, } ``` :::tip The `#[derive(Drop, Serde)]` traits are required, respectively used by Cairo's [ownership](https://book.cairo-lang.org/ch04-01-what-is-ownership.html) system and [serializing](https://book.cairo-lang.org/appendix-03-derivable-traits.html?#serializing-with-serde) model. Omitting them will lead to a compilation error. You can add additional traits, such as `Copy`, as needed. ::: ### The `#[key]` attribute The `#[key]` attribute indicates to Dojo which fields will be used to index the model. In the previous example, the model is indexed by the `player` field. A field that is identified as a `#[key]` is not itself stored. Rather, it is used by the Dojo database system to uniquely identify the storage location of the model. **You need to define at least one key for each model**. However, you can create composite keys by defining multiple fields as keys. :::info All keys must come before any non-key members in the struct. ::: ```rust #[derive(Copy, Drop, Serde)] #[dojo::model] struct GameResource { #[key] player: ContractAddress, #[key] location: ContractAddress, balance: u8, } ``` In this case you would then use [`read_model`](/framework/world/api) with both the player and location fields: ```cairo let player = get_caller_address(); let location = 0x1234; world.read_model((player, location)); ``` :::warning If you define multiple keys, they must **all** be provided to query the model. ::: ### Models in Practice #### Model Composition Let's explore ECS composition through a concrete gaming analogy: Orcs and Humans. While they possess intrinsic differences, they share common traits, such as having a position and health. Humans, however, possess an additional model - potions. ```cairo #[derive(Copy, Drop, Serde)] #[dojo::model] struct Position { #[key] id: u32, x: u32, y: u32 } #[derive(Copy, Drop, Serde)] #[dojo::model] struct Health { #[key] id: u32, health: u8, } #[derive(Copy, Drop, Serde)] #[dojo::model] struct Potions { #[key] id: u32, quantity: u8, } ``` Human entities will have `Health`, `Position`, and `Potions` models, while Orcs will have only `Health` and `Position`. This lets us re-use models to create a variety of different entities. For more details on entity composition patterns, see [entities](./entities). :::warning A `#[dojo::model]` struct cannot be used as a field inside another model. This is because the key retrieval system requires fields to be serializable in a way that nested models do not support. If you need a composite data type as a model field, define a plain struct and derive [`Introspect`](./introspection) instead: ```cairo #[derive(Drop, Serde, Introspect)] struct Stats { atk: u8, def: u8, } #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] id: u32, stats: Stats, // Plain struct with Introspect, not a model } ``` ::: The game contract would look like this: ```cairo #[dojo::contract] mod spawnHuman { use dojo::model::{ModelStorage}; use dojo::world::{WorldStorage, WorldStorageTrait}; use dojo_examples::models::{Position, Health, Potions}; #[abi(embed_v0)] impl ActionsImpl of IActions { fn spawn_entities(ref self: ContractState) { let mut world = self.world(@"dojo_starter"); // spawn a human let human_id = world.uuid() world.write_model(@Health { id: human_id, health: 100 }); world.write_model(@Position { id: human_id, x: 0, y: 0 }); world.write_model(@Potions { id: human_id, quantity: 10 }); // spawn an orc let orc_id = world.uuid() world.write_model(@Health { id: orc_id, health: 100 }); world.write_model(@Position { id: orc_id, x: 10, y: 10 }); } } } ``` #### Global Settings Suppose we want to store a global game value, which we may want to modify in the future. To achieve this, we can create a model to store this value, while also allowing for its future modification. The key difference is that, instead of a variable key, we would use a **constant identifier**. ```cairo const RESPAWN_DELAY: u128 = 9999999999999; #[derive(Copy, Drop, Serde)] #[dojo::model] struct GameSetting { #[key] setting_id: u128, setting_value: felt252, } // Set respawn delay to 10 minutes world.write_model(@GameSetting { setting_id: RESPAWN_DELAY, setting_value: (10 * 60).into() }); ``` ## Introspection :::note Introspection is an advanced Dojo topic. Beginning Dojo developers can skip this section. ::: In Dojo, every model automatically implements the [`Introspect` trait](https://github.com/dojoengine/dojo/blob/main/crates/dojo/core/src/meta/introspect.cairo). This trait outlines the data structure of the model, which is utilized by both the world database engine and [Torii](/toolchain/torii) for automatic data indexing. The `dojo/core` library already implements the `Introspect` trait for Cairo built-in types including: * All primitive types (`u8`, `u16`, `u32`, `u64`, `u128`, `u256`, `felt252`, `bool`) * Starknet types (`ContractAddress`, `ClassHash`, `EthAddress`) * Container types (`Array`, `Option`, `ByteArray`) * Tuple types up to reasonable complexity :::info `Introspect` is *not* a Cairo language feature, but a special feature of the Dojo framework. ::: ### Custom Types User-defined types must implement the `Introspect` trait to be used inside of a model. Note that a `#[dojo::model]` struct cannot be embedded as a field in another model — use a plain struct with `Introspect` instead. See [Model Composition](/framework/models#model-composition) for details. #### Automatic Implementation If the user-defined type **contains only Cairo built-in types** and is defined within your project, simply derive `Introspect` and the implementation for the type will be handled automatically by Cairo. ```cairo #[derive(Drop, Serde, Introspect)] struct Stats { atk: u8, // Cairo primitive type def: u8, } ``` #### Manual Implementation If the user-defined type **includes a type that is either defined outside of your project or is an unsupported type**, you will need to manually implement `Introspect`: ```cairo trait Introspect { fn size() -> Option; fn layout() -> Layout; fn ty() -> Ty; } ``` :::info The `size` function should return `None` if your model, or any type within it, includes at least one dynamic type such as `ByteArray` or `Array`. ::: The [`Layout`](https://github.com/dojoengine/dojo/blob/main/crates/dojo/core/src/meta/layout.cairo#L10) enum describes **how** data is stored in the world database: ```cairo enum Layout { Fixed: Span, // Fixed-size layout (packed) - all data in single storage slot Struct: Span, // Struct with field layouts - each field in separate storage slot Tuple: Span, // Tuple of layouts - ordered collection of different types Array: Span, // Array of elements - dynamic collection of same type FixedArray: Span<(Layout, u32)>, // Fixed-size array with element layout and size ByteArray, // Dynamic byte array - variable-length string data Enum: Span, // Enum variants - discriminated union with variant layouts } ``` * **Fixed**: All fields packed together in a single storage slot (most gas-efficient) * **Struct**: Each field stored in its own storage slot (more flexible, allows upgrades) * **Tuple**: Describes the layout of contained elements in order * **Array**: Dynamic collection - describes the layout of contained elements * **FixedArray**: Fixed-size collection - describes element layout and array size * **ByteArray**: Special handling for dynamic strings * **Enum**: Stores discriminant plus the layout of the active variant The [`Ty`](https://github.com/dojoengine/dojo/blob/main/crates/dojo/core/src/meta/introspect.cairo#L176) (`type`) enum tells Dojo's indexer (Torii) **what** your data represents: ```cairo enum Ty { Primitive: felt252, // Primitive type name (like 'u32', 'felt252') Struct: Struct, // Struct definition with field names and types Enum: Enum, // Enum definition with variant names and types Tuple: Span, // Tuple element types in order Array: Span, // Array element type(s) - usually single element FixedArray: Span<(Ty, u32)>, // Fixed-size array with element type and size ByteArray, // Dynamic byte array type marker } ``` * **Primitive**: Basic types like `u32`, `felt252`, `bool` - identified by name * **Struct**: Complex types with named fields - includes field names and their types * **Enum**: Discriminated unions - includes variant names and their associated data types * **Tuple**: Ordered collections of different types - useful for composite keys * **Array**: Dynamic collections of the same type - element type is specified * **FixedArray**: Fixed-size collections - element type and size are specified * **ByteArray**: Variable-length strings - special handling for text data #### Understanding the Introspect Trait Each type implements `Introspect` and returns exactly **one** `Layout` variant and **one** `Ty` variant that describes itself: ```cairo // u32 returns: Layout::Fixed(...) // Simple fixed-size storage Ty::Primitive('u32') // Primitive type identifier // Array returns: Layout::Array(...) // Dynamic array storage Ty::Array(...) // Array type with element info // Position struct returns: Layout::Struct(...) // Multi-field storage layout Ty::Struct(...) // Struct type with field info ``` The enum variants provide flexibility to describe **any** type while maintaining type safety. Think of it as a "universal descriptor" - each type knows its own storage strategy and provides that information through the appropriate enum variant. As an example of implementation, consider the following case of an externally-imported struct being used as part of your game logic: ```cairo // Your project #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] token_id: u256, stats: Stats, } // Imported from another project struct Stats { atk: u8, def: u8, } ``` You would need to implement `Introspect` for the struct, as follows: ```cairo impl StatsIntrospect of dojo::meta::introspect::Introspect { #[inline(always)] fn size() -> Option { // An Option representing the two struct fields Option::Some(2) } fn layout() -> dojo::meta::introspect::Layout { // A Layout value representing a fixed-size struct dojo::meta::introspect::Layout::Struct( array![ // A FieldLayout struct representing a single struct field dojo::meta::introspect::FieldLayout { selector: selector!("atk"), layout: dojo::meta::introspect::Introspect::::layout() }, dojo::meta::introspect::FieldLayout { selector: selector!("def"), layout: dojo::meta::introspect::Introspect::::layout() }, ] .span() ) } #[inline(always)] fn ty() -> dojo::meta::introspect::Ty { // A Ty value representing a fixed-size struct dojo::meta::introspect::Ty::Struct( // A Struct struct representing the entire struct dojo::meta::introspect::Struct { name: 'Stats', attrs: array![].span(), children: array![ // A Struct struct representing a single struct field dojo::meta::introspect::Member { name: 'atk', attrs: array![].span(), ty: dojo::meta::introspect::Introspect::::ty() }, // How many times can we say struct in one snippet? dojo::meta::introspect::Member { name: 'def', attrs: array![].span(), ty: dojo::meta::introspect::Introspect::::ty() }, ] .span() } ) } } ``` :::warning Use `#[inline(always)]` wisely to avoid hidden bugs during the Cairo to sierra compilation. Usually it's fine to use it with Dojo utils functions. In case you're using a function you do not know the complexity of, you should avoid using it. ::: ### IntrospectPacked trait In some situations, you might want to store a model in a packed way -- useful when you know the size of the model and want to save some storage space. For this, you can derive the `IntrospectPacked` trait, which will force the use of the [`Fixed` layout](https://github.com/dojoengine/dojo/blob/main/crates/dojo/core/src/meta/introspect.cairo). ```cairo #[derive(Drop, Serde, IntrospectPacked)] struct Stats { atk: u8, def: u8, } ``` :::warning Dynamic types such as `ByteArray` and `Array` are prohibited in a packed model. ::: #### IntrospectPacked vs Introspect | Feature | IntrospectPacked | Introspect | | ------------------ | -------------------------- | -------------------------- | | **Storage** | Fewer storage slots | More storage slots | | **Gas cost** | Lower (fewer reads/writes) | Higher (more reads/writes) | | **Upgrade safety** | Not upgradeable | Upgradeable | | **Dynamic types** | Not supported | Supported | | **Field access** | Must read entire model | Can read individual fields | See [Model Upgrades](/framework/models/upgrades) for detailed information about upgrade safety trade-offs. #### When to Use IntrospectPacked Use `IntrospectPacked` when: * Model has a fixed, known size * Model structure is stable (will not change) * Performance is critical * Model is read/written frequently as a whole ```cairo // Good for packed: stable, small, fixed-size #[derive(Copy, Drop, Serde, IntrospectPacked)] struct Position { x: u32, y: u32, } // Bad for packed: dynamic size, may need upgrades #[derive(Drop, Serde, Introspect)] struct PlayerData { name: ByteArray, inventory: Array, } ``` #### Nested Packed Types You can nest structs within a packed model, provided they also implement `IntrospectPacked`. ```cairo #[derive(Copy, Drop, Serde, IntrospectPacked)] struct Vec2 { x: u32, y: u32, } #[derive(Copy, Drop, Serde, IntrospectPacked)] struct Transform { position: Vec2, rotation: u16, scale: Vec2, } ``` :::tip Old Dojo versions (before `0.7.0`) used to implement only the `IntrospectPacked` trait. Hence, you should use this trait if you are upgrading from an old version of Dojo. ::: #### Storage Optimization Tips 1. **Use packed layouts** for stable, frequently-accessed models 2. **Group related fields** to minimize storage slots 3. **Consider field ordering** - place smaller fields together 4. **Use appropriate types** - do not use `u256` when `u32` suffices ### Best Practices #### Trait Selection 1. **Use `Introspect` by default** - provides flexibility and upgrade safety 2. **Use `IntrospectPacked` for performance** - when model is stable and frequently accessed 3. **Mix approaches** - use packed for stable core models, unpacked for extensible ones #### Implementation Guidelines 1. **Auto-derive when possible** - manual implementation is error-prone 2. **Test thoroughly** - validate serialization/deserialization 3. **Document implementations** - explain why custom implementation was needed 4. **Use consistent naming** - follow naming conventions for fields and types #### Future-Proofing 1. **Plan for upgrades** - use unpacked layouts for evolving models 2. **Version your data** - include version fields for complex models 3. **Document constraints** - clearly document why certain layouts were chosen 4. **Test upgrade paths** - validate that model changes work as expected By following these guidelines, you can create efficient, maintainable models that work well with Dojo's introspection system. ## Model Upgrades In Dojo, all models are upgradeable. When their code changes, the contracts are redeployed to the same address -- preserving the existing model storage and data. Upgrading is safe as long as the changes do not affect the existing data layout and schema. If the layout or the schema has changed, however, the upgrade will fail. Suppose we have a model called `Player` that represents player information: ```cairo #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] player_id: u64, username: ByteArray, score: u32, } ``` Now, let's say we want to enhance our system by adding a new field called `level`. ```cairo #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] player_id: u64, username: ByteArray, score: u32, level: u8, // New field } ``` When we re-deploy the contract using Sozo, our data remains intact. Retrieving player information using the original fields works seamlessly. For example, querying `player_id = 123` with `username = "Alice"` and `score = 100` still provides accurate results. In addition, post-upgrade, we can now store the `level` value for the `Player`. :::info When you upgrade a model, Dojo checks that the new model definition is compatible with the existing data layout. If the upgrade would break existing data, the upgrade will fail. ::: ### General rules * To be upgradeable, the layout of a model must not be packed (using `IntrospectPacked`). * For composite data structures like `struct`, `enum`, `tuple` and `array`: * they are upgreadable as long as all their elements are upgreadable * existing elements cannot be removed, only modified * new elements can be freely added. * Each element of a data structure must keep the same type (i.e a `tuple` must remain a `tuple`), the same name and the same attributes if any (such as `#[key]` for model members). * A primitive type can be upgraded to a larger primitive type as long as its `felt252` representation does not change (`u8` to `u128`, but not `u128` to `u256`). * A key model member is upgradeable only if its type is an upgreadable primitive or an enum with new variants only (existing variants cannot be modified for a key member). * The new fields must be added at the end of the model to ease the upgrade checks in Cairo. ### Primitive upgrades This table lists the allowed upgrades for every primitive type. :::note The type `usize` is not supported since it is a architecture-dependent type. ::: | Current | Allowed upgrades | | --------------- | ----------------------------------------------- | | bool | bool, felt252 | | u8 | u8 to u128, felt252 | | u16 | u16 to u128, felt252 | | u32 | u32 to u128, felt252 | | u64 | u64 and u128, felt252 | | u128 | u128, felt252 | | u256 | u256 | | i8 | i8 to i128, felt252 | | i16 | i16 to i128, felt252 | | i32 | i32 to i128, felt252 | | i64 | i64 and i128, felt252 | | i128 | i128, felt252 | | felt252 | felt252, ClassHash, ContractAddress | | ClassHash | felt252, ClassHash, ContractAddress | | ContractAddress | felt252, ClassHash, ContractAddress | | EthAddress | felt252, ClassHash, ContractAddress, EthAddress | ### Upgrade Examples #### Safe Upgrades These upgrades preserve existing data and are allowed: ```cairo // Version 1 #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] id: u32, name: ByteArray, score: u32, } // Version 2 - Adding new fields (safe) #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] id: u32, name: ByteArray, score: u32, level: u8, // New field experience: u64, // New field } // Version 3 - Expanding primitive types (safe) #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] id: u32, name: ByteArray, score: u64, // Expanded from u32 to u64 level: u8, experience: u64, } ``` #### Unsafe Upgrades These upgrades would break existing data and are not allowed: ```cairo // Version 1 #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] id: u32, name: ByteArray, score: u32, } // UNSAFE: Removing fields #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] id: u32, // name: ByteArray, // Removed - would break upgrade score: u32, } // UNSAFE: Changing field types incompatibly #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] id: u32, name: ByteArray, score: u256, // Changed from u32 to u256 - incompatible } // UNSAFE: Reordering fields #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] id: u32, score: u32, // Moved before name name: ByteArray, // Moved after score } ``` #### Enum Upgrades Enums can be upgraded by adding new variants: ```cairo // Version 1 #[derive(Drop, Serde, Introspect)] enum PlayerClass { Warrior, Mage, } // Version 2 - Adding new variants (safe) #[derive(Drop, Serde, Introspect)] enum PlayerClass { Warrior, Mage, Archer, // New variant Rogue, // New variant } // UNSAFE: Removing or reordering variants #[derive(Drop, Serde, Introspect)] enum PlayerClass { Mage, // Reordered - would break upgrade Warrior, // Reordered - would break upgrade } ``` ### Migration Strategies #### Planning for Upgrades 1. **Design for evolution**: Plan your model structure to accommodate future changes 2. **Use versioning**: Include version fields in models that may need complex upgrades 3. **Separate concerns**: Keep stable data separate from frequently changing data ```cairo // Good: Separate stable and changeable data #[derive(Drop, Serde)] #[dojo::model] struct PlayerCore { #[key] id: u32, name: ByteArray, created_at: u64, } #[derive(Drop, Serde)] #[dojo::model] struct PlayerStats { #[key] player_id: u32, version: u8, // For future migrations level: u32, experience: u64, health: u32, } ``` #### Handling Breaking Changes When you need to make breaking changes, consider these strategies: ##### 1. Create New Models ```cairo // Create new v2 model going forward #[derive(Drop, Serde)] #[dojo::model] struct PlayerV2 { #[key] id: u32, // New structure profile: PlayerProfile, stats: PlayerStats, } // Keep old model for backward compatibility #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] id: u32, name: ByteArray, score: u32, } ``` ##### 2. Use Migration Systems Create systems to migrate data from old to new models: ```cairo #[dojo::contract] mod migration { use super::{Player, PlayerV2}; #[abi(embed_v0)] impl MigrationImpl of IMigration { fn migrate_player(ref self: ContractState, player_id: u32) { let mut world = self.world(@"my_game"); // Read old model let old_player: Player = world.read_model(player_id); // Create new model let new_player = PlayerV2 { id: old_player.id, profile: PlayerProfile { name: old_player.name, // ... other fields }, stats: PlayerStats { score: old_player.score, // ... other fields }, }; // Write new model world.write_model(@new_player); // Optionally remove old model world.erase_model(@old_player); } } } ``` ### Best Practices #### Model Design 1. **Start with unpacked models**: Use `Introspect` for new models to maintain flexibility 2. **Group stable fields**: Keep frequently changing fields separate from stable ones 3. **Use appropriate types**: Don't over-size fields, but leave room for growth 4. **Document constraints**: Clearly document which fields can be changed #### Upgrade Process 1. **Test upgrades locally**: Always test model upgrades in a development environment 2. **Gradual rollout**: Consider phased upgrades for critical systems 3. **Monitor compatibility**: Use tools to validate upgrade compatibility 4. **Backup data**: Ensure you can recover if an upgrade fails #### Common Upgrade Failures 1. **Layout conflicts**: Changing from packed to unpacked layout 2. **Type incompatibility**: Upgrading to incompatible types 3. **Field removal**: Trying to remove existing fields 4. **Key changes**: Modifying key field types or order By following these guidelines and understanding the upgrade constraints, you can design models that evolve safely with your application while preserving existing data. ## System Architecture System architecture defines how you structure and organize your systems to create maintainable, scalable, and coherent applications. Good architecture makes your codebase easier to understand, modify, and extend as your application grows in complexity. ### Architectural Principles #### Separation of Concerns Each system should have a clear, distinct responsibility that doesn't overlap with other systems. This makes your codebase more modular and easier to maintain. ```cairo // Good: Separated concerns #[dojo::contract] mod movement_system { use starknet::{ContractAddress, get_caller_address}; use dojo::model::{ModelStorage}; use dojo::world::{WorldStorage, WorldStorageTrait}; fn move(ref self: ContractState, direction: Direction) { /* ... */ } fn teleport(ref self: ContractState, target: Vec2) { /* ... */ } } #[dojo::contract] mod combat_system { use starknet::{ContractAddress, get_caller_address}; use dojo::model::{ModelStorage}; use dojo::world::{WorldStorage, WorldStorageTrait}; fn attack(ref self: ContractState, target: ContractAddress) { /* ... */ } fn defend(ref self: ContractState) { /* ... */ } } #[dojo::contract] mod inventory_system { use starknet::{ContractAddress, get_caller_address}; use dojo::model::{ModelStorage}; use dojo::world::{WorldStorage, WorldStorageTrait}; fn pickup_item(ref self: ContractState, item_id: u32) { /* ... */ } fn drop_item(ref self: ContractState, item_id: u32) { /* ... */ } } ``` #### Dependency Direction Systems should depend on abstractions (models and world interface) rather than concrete implementations. This creates a clean dependency flow and makes testing easier. ``` ┌─────────────────────────────────────────────────┐ │ Dependency Flow │ ├─────────────────────────────────────────────────┤ │ Systems → World Contract → Models/Events │ │ (Logic) (Interface) (Data) │ └─────────────────────────────────────────────────┘ ``` #### Interface Segregation Design focused interfaces that expose only what's necessary. Large, monolithic interfaces become difficult to implement and maintain. ```cairo // Good: Focused interfaces #[starknet::interface] trait IMovement { fn move(ref self: T, direction: Direction); fn get_position(self: @T) -> Vec2; } #[starknet::interface] trait ICombat { fn attack(ref self: T, target: ContractAddress); fn defend(ref self: T); } // Bad: Monolithic interface #[starknet::interface] trait IGameActions { fn move(ref self: T, direction: Direction); fn attack(ref self: T, target: ContractAddress); fn craft_item(ref self: T, recipe: Recipe); fn trade(ref self: T, other: ContractAddress); // ... 20+ functions } ``` ### Structural Patterns #### Single System Per Contract The simplest pattern: one system per contract. This provides maximum isolation and permission granularity. ```cairo // movement.cairo #[dojo::contract] mod movement { use super::IMovement; #[abi(embed_v0)] impl MovementImpl of IMovement { fn move(ref self: ContractState, direction: Direction) { let mut world = self.world(@"namespace"); // Movement logic } } } // combat.cairo #[dojo::contract] mod combat { use super::ICombat; #[abi(embed_v0)] impl CombatImpl of ICombat { fn attack(ref self: ContractState, target: ContractAddress) { let mut world = self.world(@"namespace"); // Combat logic } } } ``` **Benefits:** * Clear permission boundaries * Easy to test and deploy independently * Minimal complexity **Drawbacks:** * More contracts to manage * Potential gas overhead for cross-system operations #### Grouped Systems Related systems grouped within a single contract. This pattern balances modularity with operational efficiency. ```cairo // player_actions.cairo #[dojo::contract] mod player_actions { use super::{IMovement, ICombat, IInventory}; #[abi(embed_v0)] impl MovementImpl of IMovement { fn move(ref self: ContractState, direction: Direction) { /* ... */ } } #[abi(embed_v0)] impl CombatImpl of ICombat { fn attack(ref self: ContractState, target: ContractAddress) { /* ... */ } } #[abi(embed_v0)] impl InventoryImpl of IInventory { fn pickup_item(ref self: ContractState, item_id: u32) { /* ... */ } } } ``` **Benefits:** * Shared permissions and internal helpers * Fewer contracts to manage * Efficient cross-system operations **Drawbacks:** * Larger contract size * More complex testing * Potential for tight coupling #### Hierarchical Systems Systems organized in a hierarchy, where higher-level systems coordinate lower-level ones. ```cairo // game_coordinator.cairo #[dojo::contract] mod game_coordinator { use super::IGameCoordinator; #[abi(embed_v0)] impl CoordinatorImpl of IGameCoordinator { fn process_turn(ref self: ContractState, player: ContractAddress) { let mut world = self.world(@"namespace"); // Coordinate multiple subsystems self.handle_movement(player); self.handle_combat(player); self.handle_resource_generation(player); } } #[generate_trait] impl InternalImpl of InternalTrait { fn handle_movement(self: @ContractState, player: ContractAddress) { /* ... */ } fn handle_combat(self: @ContractState, player: ContractAddress) { /* ... */ } fn handle_resource_generation(self: @ContractState, player: ContractAddress) { /* ... */ } } } ``` **Benefits:** * Clear system orchestration * Simplified external interface * Coordinated operations **Drawbacks:** * Potential single point of failure * Complex internal logic * Harder to extend independently ### Permission Architecture For comprehensive information about managing permissions, see the [Permissions](/framework/world/permissions) guide. #### Granular Permissions Assign permissions at the finest grain possible while maintaining operational efficiency. ```cairo // Each system gets specific permissions [writers] "namespace-Position" = ["namespace-movement"] "namespace-Health" = ["namespace-combat"] "namespace-Inventory" = ["namespace-inventory"] // Avoid overly broad permissions # Bad: Too broad # "namespace" = ["namespace-player_actions"] ``` #### Permission Inheritance Use namespace-level permissions for systems that need broad access. ```cairo // For systems that coordinate multiple models [writers] "namespace" = ["namespace-coordinator"] // Can write to all models in namespace // For systems with specific access "namespace-PlayerStats" = ["namespace-character_system"] "namespace-MarketData" = ["namespace-trading_system"] ``` #### Permission Hierarchies Design permission hierarchies that reflect your system architecture. ``` World Owner (Admin) ├── Namespace Owner (Game Admin) │ ├── Coordinator System (Full namespace access) │ ├── Movement System (Position models) │ ├── Combat System (Health, Combat models) │ └── Trading System (Inventory, Market models) ``` ### Modular Design #### Core Systems Identify and implement core systems that provide fundamental functionality. ```cairo // Core systems that most games need mod core_systems { pub mod identity; // Player registration and identity pub mod movement; // Position and movement mechanics pub mod ownership; // Asset and item ownership pub mod permissions; // Access control and authorization } ``` #### Domain Systems Implement domain-specific systems that provide specialized functionality. ```cairo // Domain-specific systems mod game_systems { pub mod combat; // Fighting mechanics pub mod crafting; // Item creation pub mod trading; // Marketplace functionality pub mod guilds; // Social organization } ``` #### Extension Systems Design extension points for adding new functionality without modifying core systems. ```cairo // Extension system interface #[starknet::interface] trait IExtension { fn process_event(ref self: T, event: GameEvent); fn get_name(self: @T) -> ByteArray; } // Extensions can be added without modifying core systems #[dojo::contract] mod achievement_extension { impl AchievementExtension of IExtension { fn process_event(ref self: ContractState, event: GameEvent) { // Handle achievement triggers } fn get_name(self: @ContractState) -> ByteArray { "achievements" } } } ``` ### Composability Patterns #### Mixin Pattern Use internal traits to compose functionality across systems. ```cairo #[generate_trait] impl ValidationMixin of ValidationTrait { fn validate_player_exists(self: @ContractState, player: ContractAddress) -> bool { let mut world = self.world(@"namespace"); let player_data: Player = world.read_model(player); player_data.exists } fn validate_position_in_bounds(self: @ContractState, pos: Vec2) -> bool { pos.x < MAX_X && pos.y < MAX_Y } } // Use in multiple systems #[dojo::contract] mod movement { impl MovementImpl of IMovement { fn move(ref self: ContractState, direction: Direction) { let player = get_caller_address(); assert(self.validate_player_exists(player), 'Player not found'); // Movement logic } } } ``` #### Trait Composition Compose system behavior through trait implementations. Traits allow you to share common functionality across multiple systems. ```cairo // Define reusable utility traits #[generate_trait] impl ValidationMixin of ValidationTrait { fn validate_player_exists(self: @ContractState, player: ContractAddress) -> bool { let mut world = self.world(@"namespace"); let player_data: Player = world.read_model(player); player_data.exists } fn validate_cooldown(self: @ContractState, last_action: u64, cooldown: u64) -> bool { get_block_timestamp() >= last_action + cooldown } } // Systems compose traits for shared functionality #[dojo::contract] mod movement { impl MovementImpl of IMovement { fn move(ref self: ContractState, direction: Direction) { let player = get_caller_address(); // Use trait methods directly assert(self.validate_player_exists(player), 'Player not found'); let mut player_data: Player = world.read_model(player); assert(self.validate_cooldown(player_data.last_move, COOLDOWN), 'Move on cooldown'); // Movement logic... } } } #[dojo::contract] mod combat { impl CombatImpl of ICombat { fn attack(ref self: ContractState, target: ContractAddress) { let attacker = get_caller_address(); // Reuse the same validation logic assert(self.validate_player_exists(attacker), 'Invalid attacker'); assert(self.validate_player_exists(target), 'Invalid target'); // Combat logic... } } } ``` ### Evolution Strategies #### Migration Patterns Plan for system upgrades and data migration. ```cairo #[dojo::contract] mod migration_system { fn migrate_player_data(ref self: ContractState, player: ContractAddress) { let mut world = self.world(@"namespace"); // Read old format let old_data: PlayerV1 = world.read_model(player); // Convert to new format let new_data = PlayerV2 { player: old_data.player, level: old_data.level, experience: old_data.experience, // New fields with defaults class: Class::Warrior, specialization: Specialization::None, }; // Save new state world.erase_model(@old_data); world.write_model(@new_data); } } ``` #### Backward Compatibility Maintain compatibility with existing clients during upgrades. ```cairo #[starknet::interface] trait IMovementV1 { fn move(ref self: T, direction: Direction); } #[starknet::interface] trait IMovementV2 { fn move(ref self: T, direction: Direction); fn move_with_modifiers(ref self: T, direction: Direction, modifiers: Span); } // Implement both interfaces for backward compatibility #[dojo::contract] mod movement_v2 { #[abi(embed_v0)] impl MovementV1 of IMovementV1 { fn move(ref self: ContractState, direction: Direction) { // Call v2 implementation with empty modifiers self.move_with_modifiers(direction, [].span()); } } #[abi(embed_v0)] impl MovementV2 of IMovementV2 { fn move(ref self: ContractState, direction: Direction) { self.move_with_modifiers(direction, [].span()); } fn move_with_modifiers(ref self: ContractState, direction: Direction, modifiers: Span) { // Full v2 implementation } } } ``` ### Anti-Patterns #### God Systems Avoid systems that handle too many responsibilities. ```cairo // Bad: God system #[dojo::contract] mod game_system { fn handle_everything(ref self: ContractState, action: GameAction) { match action { GameAction::Move(_) => { /* movement logic */ }, GameAction::Attack(_) => { /* combat logic */ }, GameAction::Craft(_) => { /* crafting logic */ }, GameAction::Trade(_) => { /* trading logic */ }, // ... 50+ different actions } } } ``` #### Tight Coupling Avoid systems that directly depend on each other's implementation details. ```cairo // Bad: Tight coupling #[dojo::contract] mod movement_system { fn move(ref self: ContractState, direction: Direction) { // Direct dependency on combat system internals let combat_system = ICombatDispatcher { contract_address: COMBAT_ADDRESS }; combat_system.internal_update_position(new_position); } } ``` #### Shared Mutable State Avoid systems that share mutable state outside of the world contract. ```cairo // Bad: Shared mutable state static mut GAME_STATE: GameState = GameState::new(); #[dojo::contract] mod system_a { fn action_a(ref self: ContractState) { unsafe { GAME_STATE.modify(); // Dangerous shared state } } } ``` ### System Discovery Systems can be discovered through the world's DNS (Dojo Name System). ```cairo // Register a system with the world fn register_system(ref self: ContractState, system_name: ByteArray, class_hash: ClassHash) { let mut world = self.world(@"namespace"); // Register the system contract world.register_contract(0, system_name, class_hash); } // Discover systems through DNS fn find_system(self: @ContractState, system_name: ByteArray) -> Option { let mut world = self.world(@"namespace"); if let Some((address, _)) = world.dns(@system_name) { Option::Some(address) } else { Option::None } } ``` ### Best Practices 1. **Start Simple**: Begin with single-system contracts and evolve as needed 2. **Plan Permissions**: Design your permission model before implementing systems 3. **Test Boundaries**: Ensure each system can be tested in isolation 4. **Document Interfaces**: Clearly document what each system does and expects 5. **Version Thoughtfully**: Plan for system evolution from the beginning 6. **Measure Performance**: Monitor gas usage and optimize based on real data Good system architecture is the foundation of maintainable Dojo applications. Take time to design your architecture thoughtfully - it will pay dividends as your world grows in complexity. ### Next Steps * **[System Coordination](./coordination)** - Learn how systems interact and coordinate ## System Coordination System coordination is the art of making multiple systems work together harmoniously to create complex, emergent behaviors in your application. While individual systems handle specific responsibilities, coordination ensures they collaborate effectively to deliver seamless user experiences. ### Coordination Fundamentals All systems coordinate through the shared world state. This creates a natural mechanism where systems communicate through data rather than direct calls. ```cairo use starknet::{ContractAddress, get_caller_address}; use dojo::model::{ModelStorage}; use dojo::world::{WorldStorage, WorldStorageTrait}; // Systems coordinate through shared models // Movement system updates position fn move(ref self: ContractState, direction: Direction) { let mut world = self.world(@"game"); let player = get_caller_address(); let mut position: Position = world.read_model(player); // Update position based on direction enum match direction { Direction::Up => position.vec.y -= 1, Direction::Down => position.vec.y += 1, Direction::Left => position.vec.x -= 1, Direction::Right => position.vec.x += 1, } world.write_model(@position); // Other systems can read this } // Combat system reads position for range calculations fn attack(ref self: ContractState, target: ContractAddress) { let mut world = self.world(@"game"); let attacker = get_caller_address(); let attacker_pos: Position = world.read_model(attacker); let target_pos: Position = world.read_model(target); let distance = calculate_distance(attacker_pos.vec, target_pos.vec); assert(distance <= ATTACK_RANGE, 'Target out of range'); // Combat logic } ``` ### Coordination Patterns #### Temporal Coordination Systems coordinate across time through timestamps and sequence numbers. ```cairo #[derive(Copy, Drop, Serde)] #[dojo::model] struct ActionHistory { #[key] player: ContractAddress, last_action: u64, action_count: u32, } fn coordinated_action(ref self: ContractState) { let mut world = self.world(@"game"); let player = get_caller_address(); let mut history: ActionHistory = world.read_model(player); let current_time = get_block_timestamp(); // Enforce cooldown coordination assert(current_time >= history.last_action + COOLDOWN_DURATION, 'Action on cooldown'); history.last_action = current_time; history.action_count += 1; world.write_model(@history); } ``` #### Producer-Consumer Pattern One system produces data that other systems consume. ```cairo // Resource system produces resources #[dojo::contract] mod resource_system { fn generate_resources(ref self: ContractState) { let mut world = self.world(@"game"); // Produce resources at regular intervals let mut resource_node: ResourceNode = world.read_model(NODE_ID); resource_node.available_resources += GENERATION_RATE; world.write_model(@resource_node); } } // Crafting system consumes resources #[dojo::contract] mod crafting_system { fn craft_item(ref self: ContractState, recipe: Recipe) { let mut world = self.world(@"game"); // Consume resources for crafting let mut resource_node: ResourceNode = world.read_model(NODE_ID); assert(resource_node.available_resources >= recipe.required_resources, 'Insufficient resources'); resource_node.available_resources -= recipe.required_resources; world.write_model(@resource_node); // Create item let item = Item { id: world.uuid(), recipe_id: recipe.id }; world.write_model(@item); } } ``` #### Observer Pattern Systems observe and react to changes made by other systems. ```cairo // Health system manages health changes #[dojo::contract] mod health_system { fn take_damage(ref self: ContractState, amount: u32) { let mut world = self.world(@"game"); let player = get_caller_address(); let mut health: Health = world.read_model(player); health.current = health.current.saturating_sub(amount); world.write_model(@health); // Emit event for observers world.emit_event(@HealthChanged { player, new_health: health.current }); } } // Achievement system observes health changes #[dojo::contract] mod achievement_system { fn check_survival_achievements(ref self: ContractState, player: ContractAddress) { let mut world = self.world(@"game"); let health: Health = world.read_model(player); let mut achievements: PlayerAchievements = world.read_model(player); // React to low health if health.current <= 1 && !achievements.near_death_unlocked { achievements.near_death_unlocked = true; world.write_model(@achievements); } } } ``` #### Coordinator Pattern A central system coordinates multiple subsystems. ```cairo #[dojo::contract] mod turn_coordinator { fn process_turn(ref self: ContractState, player: ContractAddress) { let mut world = self.world(@"game"); // Coordinate multiple subsystems in order self.process_movement(player); self.process_combat(player); self.process_resource_generation(player); self.process_status_effects(player); // Update turn counter let mut game_state: GameState = world.read_model(0); game_state.current_turn += 1; world.write_model(@game_state); } #[generate_trait] impl InternalImpl of InternalTrait { fn process_movement(self: @ContractState, player: ContractAddress) { // Movement coordination logic } fn process_combat(self: @ContractState, player: ContractAddress) { // Combat coordination logic } fn process_resource_generation(self: @ContractState, player: ContractAddress) { // Resource coordination logic } fn process_status_effects(self: @ContractState, player: ContractAddress) { // Status effect coordination logic } } } ``` #### State Machine Pattern Systems coordinate through well-defined state transitions. ```cairo #[derive(Copy, Drop, Serde, PartialEq)] enum GamePhase { Setup, Playing, Paused, Ended, } #[derive(Copy, Drop, Serde)] #[dojo::model] struct GameState { #[key] game_id: u32, phase: GamePhase, turn_count: u32, } // Systems coordinate through game phase fn player_action(ref self: ContractState, action: PlayerAction) { let mut world = self.world(@"game"); let game_state: GameState = world.read_model(GAME_ID); match game_state.phase { GamePhase::Setup => { assert(matches!(action, PlayerAction::Join(_)), 'Only join allowed in setup'); self.handle_join(action); }, GamePhase::Playing => { self.handle_gameplay_action(action); }, GamePhase::Paused => { assert(matches!(action, PlayerAction::Resume), 'Only resume allowed when paused'); self.handle_resume(); }, GamePhase::Ended => { panic!('Game has ended'); }, } } ``` ### Best Practices 1. **Design for Coordination**: Consider how systems will interact from the beginning 2. **Use Shared Models**: Design models that multiple systems can use for coordination 3. **Test Coordination Scenarios**: Write tests that verify systems work together correctly Effective system coordination is essential for creating complex, engaging applications. By understanding these patterns, you can build systems that work together seamlessly to create emergent behaviors and rich user experiences. ## Systems > Systems = Business Logic in ECS Systems are the behavioral layer of Dojo's Entity Component System (ECS) architecture. They encapsulate business logic, orchestrate state changes, and define how your application evolves over time. ### What are Systems? In Dojo's ECS paradigm, systems represent the **logic** that operates on data stored in models. While models define **what** your world contains, systems define **how** it behaves. ``` ┌─────────────────────────────────────────────────┐ │ ECS Trinity │ ├─────────────────────────────────────────────────┤ │ Entities │ Components │ Systems │ │ (Who) │ (What) │ (How) │ ├─────────────────────────────────────────────────┤ │ Players │ Position │ Movement Logic │ │ Monsters │ Health │ Combat Logic │ │ Items │ Inventory │ Trading Logic │ └─────────────────────────────────────────────────┘ ``` Systems are **stateless functions** that: * Read current world state from models * Apply business logic and rules * Write updated state back to models * Emit events for external observation :::warning In order to write data to the world, a system needs explicit permission from the model owner. Permissions are defined at the contract (address) level, which means that all the systems inside the same contract will inherit the same permissions. ::: ### System Design Philosophy #### Single Responsibility Principle Each system should have one clear, focused responsibility. This promotes modularity, testability, and maintainability. **Good Examples:** * `MovementSystem`: Handles player/entity movement * `CombatSystem`: Manages battles and damage * `InventorySystem`: Manages item collection and usage * `TradingSystem`: Handles marketplace transactions **Poor Examples:** * `GameSystem`: Handles everything (too broad) * `PlayerSystem`: Manages movement, combat, and inventory (mixed concerns) #### Stateless Design Systems should be stateless, deriving all necessary information from the world state. This ensures predictable behavior and easier testing. ```cairo // Good: Stateless system fn attack(ref self: ContractState, target: ContractAddress) { let mut world = self.world(@"game"); let attacker = get_caller_address(); // Read current state let attacker_stats: Combat = world.read_model(attacker); let mut target_stats: Combat = world.read_model(target); // Apply business logic target_stats.health -= attacker_stats.damage; // Write updated state world.write_model(@target_stats); } ``` #### Minimal Surface Area Systems should expose only the necessary functions to external callers. Internal helper functions should be private and focused. ```cairo #[starknet::interface] trait IActions { // Public interface - minimal and focused fn spawn(ref self: T); fn move(ref self: T, direction: Direction); fn attack(ref self: T, target: ContractAddress); } #[generate_trait] impl InternalImpl of InternalTrait { // Private helpers - implementation details fn validate_move(self: @ContractState, from: Vec2, to: Vec2) -> bool; fn calculate_damage(self: @ContractState, attacker: ContractAddress) -> u32; } ``` ### System Boundaries #### What Systems Should Do 1. **Business Logic**: Implement game rules and mechanics 2. **State Transitions**: Orchestrate changes between valid states 3. **Validation**: Ensure actions comply with game rules 4. **Coordination**: Manage interactions between different models 5. **Event Emission**: Signal important state changes #### What Systems Should Not Do 1. **Data Storage**: Systems don't store persistent state 2. **UI Logic**: Keep presentation concerns separate 3. **External Integration**: Avoid direct external service calls 4. **Complex Calculations**: Delegate to specialized libraries when possible ### System Interaction Models #### Direct Model Access Systems directly read and write models through the world contract. This is the most common and efficient pattern. ```cairo fn spawn(ref self: ContractState) { let mut world = self.world(@"game"); let player = get_caller_address(); // Direct model access let position = Position { player, vec: Vec2 { x: 0, y: 0 } }; let health = Health { player, value: 100 }; world.write_model(@position); world.write_model(@health); } ``` #### System Composition Systems can be composed within contracts to create logical groupings. This allows for shared permissions and coordinated operations. ```cairo #[dojo::contract] mod game_actions { // Multiple related systems in one contract impl PlayerActionsImpl of IPlayerActions { fn move(ref self: ContractState, direction: Direction) { /* ... */ } fn rest(ref self: ContractState) { /* ... */ } } impl CombatActionsImpl of ICombatActions { fn attack(ref self: ContractState, target: ContractAddress) { /* ... */ } fn defend(ref self: ContractState) { /* ... */ } } } ``` ### System Lifecycle #### Initialization Systems are stateless functions and don't have constructors. However, Dojo contracts support a `dojo_init` function that acts as a constructor-equivalent. The World calls `dojo_init` on each contract during `sozo migrate`, after the contract is registered. ```cairo #[dojo::contract] mod my_system { fn dojo_init(ref self: ContractState, arg1: felt252, arg2: u256) { // Called once during migration - use for one-time setup } } ``` Initialization arguments are configured in your profile's `[init_call_args]` section. See [Contract Initialization](/framework/configuration#contract-initialization) for details. #### Execution Systems execute in response to external calls or internal triggers. Each execution should be atomic and leave the world in a valid state. #### Validation Systems should validate inputs and world state before making changes. Use Cairo's `assert` mechanism for clear error reporting. ```cairo fn move(ref self: ContractState, direction: Direction) { let mut world = self.world(@"game"); let player = get_caller_address(); let moves: Moves = world.read_model(player); assert(moves.remaining > 0, 'No moves remaining'); assert(moves.can_move, 'Movement disabled'); // Proceed with movement logic } ``` ### Design Patterns #### Command Pattern Systems often implement the command pattern, where each public function represents a discrete action. ```cairo // Each function is a command fn spawn(ref self: ContractState) { /* ... */ } fn move(ref self: ContractState, direction: Direction) { /* ... */ } fn attack(ref self: ContractState, target: ContractAddress) { /* ... */ } ``` #### State Machine Pattern Systems can implement state machines for complex entity behaviors. ```cairo fn process_turn(ref self: ContractState, player: ContractAddress) { let mut world = self.world(@"game"); let mut game_state: GameState = world.read_model(player); match game_state.phase { GamePhase::Setup => self.handle_setup(player), GamePhase::Playing => self.handle_playing(player), GamePhase::Ended => self.handle_ended(player), } } ``` ### System Testing Philosophy Systems should be designed for testability: 1. **Pure Functions**: Business logic should be extractable as pure functions 2. **Dependency Injection**: Use world storage abstraction for mocking 3. **Isolated Testing**: Each system should be testable in isolation 4. **Integration Testing**: Test system interactions through the world contract ### Best Practices 1. **Keep Systems Small**: A system should fit in your head 2. **Use Descriptive Names**: Function names should clearly indicate their purpose 3. **Validate Early**: Check preconditions before making changes 4. **Handle Errors Gracefully**: Use meaningful error messages 5. **Document Assumptions**: Make implicit requirements explicit 6. **Test Thoroughly**: Systems are critical paths in your application ### Next Steps Understanding system design philosophy is crucial for building robust Dojo applications. Explore the deeper aspects of system implementation: * **[Entities](/framework/models/entities)** - How entities work in Dojo's ECS model * **[System Architecture](/framework/systems/architecture)** - Structural patterns and organization * **[System Coordination](/framework/systems/coordination)** - How systems interact and coordinate Systems are the heart of your application - design them thoughtfully and they'll serve you well. ## Libraries in Starknet In Starknet, a contract can call another contract in two ways: 1. [Contract call](https://www.starknet.io/cairo-book/ch102-02-interacting-with-another-contract.html?highlight=contract%20call#interacting-with-another-contract), where the caller and called contract have their own respective storage. 2. [Library call](https://www.starknet.io/cairo-book/ch102-03-executing-code-from-another-class.html?highlight=library%20call#library-calls), where the called contract is only used as an execution library, but the storage is still the same as the caller. Doing a library call only requires the contract to be declared (no need to have it deployed). :::warning Calling another contract with a library call can be very unsafe. Ensure you only call libraries you trust. An untrusted library may upgrade your own contract and take control over it. ::: ## Libraries in Dojo In Dojo, this concept is abstracted by the `#[dojo::library]` attribute, which enables this concept of libraries to embrace the full power of Dojo's ECS architecture. In comparison to contracts which are deployed (hence have an address), libraries are not deployed, but rather only declared. As you may expect already, this means that libraries **are not upgradeable**. Changing the logic of a library requires to re-declare a new class. Dojo has a special way to treat libraries upgrades to mirror how package managers would do it, using versions. ### Define a library To define a library in Dojo, is very similar to defining a contract with systems: ```rust #[starknet::interface] pub trait SimpleMath { fn decrement_saturating(self: @T, value: u8) -> u8; } /// Note here the `dojo::library` attribute, which is used to define the library. #[dojo::library] pub mod simple_math { use core::num::traits::SaturatingSub; use super::SimpleMath; #[abi(embed_v0)] impl SimpleMathImpl of SimpleMath { fn decrement_saturating(self: @ContractState, value: u8) -> u8 { value.saturating_sub(1) } } } ``` As you can see, as contracts we can have a trait defining a `#[starknet::interface]` which will be used to define the library. ### Configure a library In order to instruct the Dojo toolchain that you have a library to use, you need to configure it in your [Dojo configuration file](/framework/configuration). If we take the example of the previous library, we would need to configure it like this: ```toml [lib_versions] "-simple_math" = "0_1_0" ``` Doing so, we instruct Sozo to register the library in the world with the version `0_1_0`. Sozo will use the code you have locally defined in your project for this library. If you change the library, rebuild the code with `sozo build` and then change the version in the configuration file. Then, run `sozo migrate` to declare the new library class and register it in the world. As you have already seen, the Dojo resources are identified by a selector, which is for contracts/models/events the poseidon hash of the namespace and the resource name. For libraries, it is similar but the version has to be added in order to avoid collisions: ``` poseidon_hash("", "_v") ``` This concatenation is done automatically by the world while registering the library, since the library name and version are passed as separate arguments. ### Use a library Once the library is registered in the world, you can leverage the Dojo's DNS in order to get the library's class hash without having to hardcode it. ```rust use path::to::libary::{SimpleMathLibraryDispatcher, SimpleMathLibraryDispatcherTrait}; let (_, class_hash) = world.dns(@"simple_math_v0_1_0").unwrap(); // or let class_hash = world.dns_class_hash(@"simple_math_v0_1_0").unwrap(); let simple_math_library = SimpleMathLibraryDispatcher { class_hash }; let r = simple_math_library.decrement_saturating(123_u8); ``` As you can note here, the DNS is expecting the library name and version as a single argument, separated by an underscore. Instead of `unwrap`, in production code you can use `.expect` in order to have the revert message identified more easily. ### Why using libraries? The major benefits of using libraries are to separate the logic from the contract and therefore reducing the code size of contracts. Either related to storage or not, libraries are a way to share logic between contracts. When you face yourself hitting the limit of the contract size of the network, libraries are a way to split your code into smaller pieces. ## Dojo 1.0 Overview :::note This guide focuses on the major changes from Dojo 0.x to Dojo 1.0, [released Nov 2024](https://github.com/dojoengine/dojo/releases/tag/v1.0.0). If you are new to Dojo, you can skip this section. ::: Dojo is composed of 5 basic resources: * `world`: The smart contract that manages the state of your game, everything is stored here. * `namespace`: Every resource that is not world or namespace must be namespaced when registered into the world. Namespaces are logical groups of resources, and allow you to organize your resources and permissions. * `model` (namespaced): A model defines data that can be stored in the world. * `event` (namespaced): An event also defines data, but meant to be stored offchain. * `contract` (namespaced): Where you define your business logic, and interact with the world to write/read models and emit events. A function into a contract is called a `System`, which is an entrypoint for users to interact with the world. Every resource in the world is identified by a dojo selector, a single felt identifier obtained by hashing. For human readability, namespaced resources can also be identified by what's called a `Tag`, which is a combination of the namespace and the resource name: `namespace-resource_name`. The tag can be used to obtain the dojo selector of the resource. A single resource can be registered multiple times into the world using different namespaces. All the resources without exception in the world are permissioned. This means that only the specified addresses can write/own resources. There's only two permissions in Dojo: * `writer`: Can write to the resource. * `owner`: Can grant/revoke writer permissions, can register/upgrade resources. ### Interacting with the world and its data First, when you are inside a dojo contract (define with `#[dojo::contract]`), you have to retrieve the world's instance. As mentioned previously, all the resources are namespaced, so you have to specify the default namespace to use: ```rust // Get the world instance, using the namespace "ns": let world = self.world(@"ns"); ``` This `world` instance provides you some functionalities to interact with the world and its data. ```rust // Using the DNS to get a contract address and class hash from its tag. // Since the default namespace has been set to "ns", the tag identifying // the resource will be "ns-my_contract". if let Some((contract_address, class_hash)) = world.dns("my_contract") { // Do something with the contract address and class hash. } ``` To change the namespace you want to write/read from, you can use the `set_namespace` method: ```rust world.set_namespace(@"ns2"); // At this point, using the DNS will return the resource from the "ns2" // namespace, if it exists. if let Some((contract_address, class_hash)) = world.dns("my_contract") { // Do something with the contract address and class hash. } ``` To read/write data to models, you have to import the `ModelStorage` trait: ```rust use dojo::model::ModelStorage; #[dojo::model] struct MyModel { #[key] id: u32, value: u32, } let mut world = self.world(@"ns"); // Note here the type specified for the compiler, this ensures the // compiler can infer which data to retrieve: let id = 1; let mut model: MyModel = world.read_model(id); model.value = 123; world.write_model(@model); world.erase_model(@model); ``` The full API of the `ModelStorage` can be found [here](https://github.com/dojoengine/dojo/blob/ab081b9fb8444d84aecaba848126f8c64db45eb8/crates/dojo/core/src/model/storage.cairo#L9) before more documentation is written. The important concept to keep in mind is that the data stored in the world are identified by the `keys` you are adding using `#[key]` in models and events. A model/event can have one or multiple keys. When those keys are hashed, it's called the `entity_id`. Events are only emitted by the world, and never stored onchain. Instead, Torii will index them and store them in the SQL database. However, they are subjected to the same namespace rules as models. To emit an event, you have to import the `EventStorage` trait: ```rust use dojo::event::EventStorage; #[dojo::event] struct MyEvent { #[key] id: u32, value: u32, } let mut world = self.world(@"ns"); let e = MyEvent { id: 1, value: 123 }; world.emit_event(@e); ``` ### Permissions As mentioned previously, all the resources are permissioned. Some examples of the permission API: ```rust use dojo::world::IWorldDispatcherTrait; // How to check that the caller is a owner of the current contract // executing code: fn system_1(ref self: ContractState) { let mut world = self.world(@"ns"); // A dojo selector is computed from namespace and name. // The namespace is already set by the `world` instance, // so we just have to use the dojo name of the contract. // Every contract has a `dojo_name` function available. let current_contract_selector = world.contract_selector( @self.dojo_name() ); // Using the world dispatcher to call the world contract // and verify that the resource (the current contract) // is owned by the caller. world.dispatcher.is_owner( current_contract_selector, starknet::get_caller_address() ); } ``` ### Events and Torii Events are not stored onchain, they are indexed by Torii. And by default, events behave like models, which means only the latest state is kept. However, you may want sometimes to keep events historical as it's regurlarly for blockchain events. To do so, nothing to change onchain, only one way to define events: ```rust #[dojo::event] struct MyEvent { #[key] id: u32, data: felt252, } ``` On the torii side, you can start it from the CLI or using a configuration file with the `historical_events` options, by providing tags of events you want to keep historical. ```bash torii start --events.historical ns-MyEvent,ns-MyOtherEvent --world 0x00e2ea9b5dd9804d13903edf712998943b7d5d606c139dd0f13eeb8f5b84da8d ``` Or using a configuration file in `toml` format: ```toml world_address = "0x00e2ea9b5dd9804d13903edf712998943b7d5d606c139dd0f13eeb8f5b84da8d" [events] historical = ["ns-MyEvent", "ns-MyOtherEvent"] ``` ### Testing Currently, Dojo is still only supporting the `cairo-test` test runner. Soon `starknet-foundry` will be unlocked once `scarb` and `cairo-lang` merge some missing features. In the meantime, here's how you can test your contracts. As we've seen, resources like contracts, models and events are namespaced, so you have to specify the namespace you want to use when testing. Before starting to test, here's the flow that `Sozo` follows to migrate a world: 1. First of all, `Sozo` will migrate the world itself. 2. Then, `Sozo` will register all the resources. Registering the resources means that all models/events/contracts will be declared and deployed onchain. None of those contracts are using constructor calldata, hence `Sozo` can deploy them without prior inputs. All resources are registered to the world and deployed through the world contract. 3. Once all the resources are registered, `Sozo` will synchronize the permissions that are given in the `dojo_.json` file. 4. Finally, `Sozo` will initialize all the contracts. Since the contracts initialization function is very likely to interact with models, at this point all permissions are synchronized and the world is ready to use. This is important to keep this in mind, since the testing flow must be similar to the migration flow. Now, let's move on to testing. First, you have to use the `dojo_cairo_test` crate to use dojo utilities in your tests. ```toml # Scarb.toml [dev-dependencies] dojo_cairo_test = { git = "https://github.com/dojoengine/dojo.git", tag = "v1.0.0" } ``` To define some namespace configurations you will use the [NamespaceDef](https://github.com/dojoengine/dojo/blob/ab081b9fb8444d84aecaba848126f8c64db45eb8/crates/dojo/core-cairo-test/src/world.cairo#L51) and associated definitions: ```rust use dojo::model::{ModelStorage, ModelValueStorage, ModelStorageTest}; use dojo::world::WorldStorageTrait; use dojo_cairo_test::{ spawn_test_world, NamespaceDef, TestResource, ContractDefTrait, ContractDef, WorldStorageTestTrait }; // First to note here, Dojo is generating contracts for each model // and event. // The name of this generated contract is always the resource name, // prefixed by "m_" or "e_" respectively. use dojo_starter::models::{ Position, m_Position, Moves, m_Moves, Direction }; ``` Then, for each resource, you can add them to a specific namespace. Once again, the same model or event type can be registered multiple times into different namespaces, which will yield different resources. ```rust // Here we map the resource to the namespace "ns". // They will be used to register the resources to the world. fn namespace_def() -> NamespaceDef { let ndef = NamespaceDef { namespace: "ns", resources: [ TestResource::Model(m_Position::TEST_CLASS_HASH), TestResource::Model(m_Moves::TEST_CLASS_HASH), TestResource::Event(actions::e_Moved::TEST_CLASS_HASH), TestResource::Contract(actions::TEST_CLASS_HASH), ].span() }; ndef } ``` Let's then prepare some contracts definitions [defined here](https://github.com/dojoengine/dojo/blob/ab081b9fb8444d84aecaba848126f8c64db45eb8/crates/dojo/core-cairo-test/src/world.cairo#L57): ```rust // Here, we have one contract, and we define at this step // the permission of the contract and initialization data (if any). fn contract_defs() -> Span { [ ContractDefTrait::new(@"ns", @"actions") .with_writer_of([dojo::utils::bytearray_hash(@"ns")].span()) // .with_init_calldata // .with_owner_of ].span() } ``` Once you have a namespace definition and contracts definitions, you can spawn a test world with it. The function `spawn_test_world` will register all the resources and return a world instance we've seen previously. ```rust #[test] fn test_world_test_set() { let ndef = namespace_def(); let mut world = spawn_test_world([ndef].span()); // At this point, the resources are registered, but permissions // are not set and contracts are not initialized // (dojo_init has not be called). world.sync_perms_and_inits(contract_defs()); // At this point, permissions are synchronized and // contracts are initialized. } ``` By having the registration of the resources and the synchronization of the permissions/init separated, you can easily separate tests functions to setup the world at your will to test different scenarios. As you remember, the resources are also permissioned. In some occasions, you may want to interact with the world bypassing the permission check. For this, you can use the `test_only` world: ```rust let m = MyModel { id: 1, value: 123 }; // Bypass any permission check, and will write into the world's storage. world.write_model_test(@m); ``` ### Configuration The configuration of your dojo project is now fully managed by a dojo configuration file alongside the `Scarb.toml` manifest file. This ease the profile management and regroup all the functionalities at the same place. You can find detailed information about the configuration file [here](/framework/configuration). ### Sozo Sozo has changed to be more robust and 100% stateless for the migration. All the data required to compute diffs and migration strategy are locally built or onchain. As you remember, Dojo is profile based. Hence, the build and migration must respond to the profile. To specify a profile, use `-P` or `--profile` argument. Basic commands: ```bash # Builds a project. sozo build # Inspects the current state of the project by comparing local # and remote resources. # Inspect the full world. sozo inspect # Inspect a specific resource. sozo inspect # Migrate all the resources to the remote state. # Generates a `manifest_.json` file. sozo migrate ``` If you change a permission, you just have to run `sozo migrate` again and the permissions will be updated. We recommend using `sozo inspect` instead of reading output of migration or the build. The `inspect` commands gives you summary of the world or specific resource. Use it at your advantage. ### Sozo useful commands Sozo provides different commands to help you manage your world. ```bash # Inspect the current state of the project by comparing local # and remote resources. sozo inspect ``` ```bash # To enable the debugging experience on Walnut and verify your project, run: sozo walnut verify ``` ```bash # Computes selectors on the fly and different hashes results, # useful for entity_id computation too. sozo hash ns-actions sozo hash 1,2,3,4 sozo hash hello ``` ```bash # Gathers all the events of the world and output the changes in # model storage in the terminal + transaction hash and block number. sozo events ``` ```bash # Sometimes, you may want to introspect a model from the chain without # using Torii indexed data. # You can inspect the schema of a model from the chain directly by using # the following command: sozo model schema dojo_starter-Position struct Position { #[key] player: ContractAddress, vec: Vec2, } struct Vec2 { x: u32, y: u32, } ``` ```bash # You can also inspect the storage state of a model providing the keys, # as you remember the keys are used to identify the entity_id # which defined the storage slot of the model. sozo model get dojo_starter-Position 0x123 { player : 0x0000000000000000000000000000000000000000000000000000000000000123, vec : { x : 0, y : 0 } } # As a reminder, keys are never stored! For this reason, # the value of the key will ALWAYS be the same as the one provided. ``` ```bash # Manage the permissions of the world from the command line, # start by listing them: sozo auth list # Grant/revoke a owner/writer to any resource: sozo auth grant owner ns,0x1234 ns-Position,ns-c1 # Clone all the resources `0xa` has to `0xb`: sozo auth clone --from 0xa --to 0xb # You can optionally revoke all the resource of `from` # while doing the clone: sozo auth clone --from 0xa --to 0xb --revoke-from ``` ### Road to mainnet Mainnet is a network with a huge history and thousands of blocks. Currently, some nodes are not supported syncing the events providing block ranges that are too wide. For this reason, when you target mainnet, you should do the following: ```bash # Build the project. sozo build --profile mainnet # Migrate the project. sozo migrate --profile mainnet ``` During the migration, sozo will output the block at which the world has been migrated and the address of the world at the end of the migration: ```bash 🌍 World deployed at block 821000 with txn hash: 0x038e984efa3e91e045b33d14e63c5e9f765e5a8fe2b3546fc3ab872f608e37a2 ⛩️ Migration successful with world at address 0x00e2ea9b5dd9804d13903edf712998943b7d5d606c139dd0f13eeb8f5b84da8d ``` To ensure the nodes serving mainnet data are accepting `Sozo` requests, you must set the `world_block` key in the `dojo_.json` file. Also, once the first migration of your world is done and you have a world address, you must set the `world_address` to ensure `Sozo` can easily detect upgrade of Dojo in the future. ```toml [env] # .. other configs world_block = 821000 world_address = 0x00e2ea9b5dd9804d13903edf712998943b7d5d606c139dd0f13eeb8f5b84da8d ``` ### Dojo 1.0.0 Breaking Changes [Dojo 1.0.0](https://github.com/dojoengine/dojo/releases/tag/v1.0.0) introduces a number of breaking changes. * Macros `set/get/delete` are currently not supported. They may be added again in the future. * When working with contracts, the `world` is no longer automatically injected. You must use regular starknet interfaces and `self` with `ContractState`. * World's metadata are not uploaded yet, this should be added back soon. * You must remove `[[target.dojo]]` and use `[[target.starknet-contract]]` instead, not forgetting to add the dojo world in the `build-external-contracts`. * Overlays files and intermediate manifests that were before produced can be deleted, they are no longer used. * The `Model` API has changed, please refer to the new one [here](https://github.com/dojoengine/dojo/blob/ab081b9fb8444d84aecaba848126f8c64db45eb8/crates/dojo/core/src/model/model.cairo#L31). * Katana has a different CLI arguments, please refer to `katana --help` for more details at the moment. * Use transaction v3 by default paying in STRK, you must specify `--fee eth` if you want to use transaction v1 paying in ETH. * Event messages are toggles historical from Torii, no longer from the chain using `#[dojo::event(historical = true)]`. ## Dojo 1.7 Overview Dojo 1.7 is a minor release of the Dojo stack, bringing Sozo updates, RPC 0.9 support, and a new `DojoStore` trait for enhanced model serialization. ### Sozo Updates Dojo 1.7 brings major changes to Sozo, Dojo's build tool. Prior to 1.7, Sozo implemented Dojo's specialized functionality (like `#[dojo::model]`) through Cairo compiler plugins. This was an advanced feature that few other teams used, meaning that Dojo had to essentially maintain a separate fork of Scarb, Cairo's build tool. This extra complexity made Dojo development slower and more difficult than it otherwise would have been. With Dojo 1.7, Sozo will instead began relying on "proc macros" (procedural macros) to implement specialized Dojo functionality. With proc macros, Dojo functionality can be accessed by Scarb at compile-time, rather than requiring separate pre-compilation. This means that Sozo can leverage the mainstream Scarb directly; going forward, calls to `sozo build` will be thin wrappers around underlying Scarb functionality. The move to mainstream Scarb has reduced typical compile-times by about 3x, as well as unblocked quality-of-life improvements like in-editor syntax highligting and terminal text coloring. Most importantly, this change will make it easier to maintain and improve Dojo and Sozo going forward. The first action you need to take is to update your `Scarb.toml` file to add the `dojo_macros` dependency. ```toml [dependencies] dojo = "=1.7.2" [dev-dependencies] cairo_test = "2.12.2" dojo_cairo_test = "=1.7.2" [tool.scarb] allow-prebuilt-plugins = ["dojo_cairo_macros"] ``` :::note Since `1.8.0` contains a very small but breaking change, you must use the `=1.7.2` to ensure that Scarb is not fetching `1.8.0` or greater instead. ::: The `allow-prebuilt-plugins` attribute is not available if you are using `1.7.0` or earlier. You need to add the `dojo_cairo_macros` dependency instead. See the note below for more details. :::note If you are using `1.7.0` or earlier, you need to add the `dojo_cairo_macros` dependency. ```toml [dependencies] dojo = "=1.7.0" dojo_cairo_macros = "=1.7.0" [dev-dependencies] cairo_test = "2.12.2" dojo_cairo_test = "=1.7.0" ``` Also, precompiled proc macros are only available if you are using `1.7.1` or later. Therefore, if you have an issue while compiling the project, ensure that you have rust `1.90` correctly installed locally. Starting from `1.7.1`, the Dojo proc macros are pre-compiled which removes the need of having Cargo installed locally. ::: ### Starknet 0.14.0 :::info Starknet's 0.14.0 upgrade went live on mainnet September 1, 2025. ::: Dojo 1.7 was timed to coincide with [Starknet's 0.14.0 upgrade](https://governance.starknet.io/voting-proposals/9), which brought several major changes to the network: * The introduction of RPC 0.9 and the introduction of the `PRE_CONFIRMED` transaction status. * The migration to a multi-sequencer architecture * The introduction of an EIP-1559-style fee market for transaction prices Dojo 1.7 brings support for RPC 0.9 to the entire stack, including Torii, Katana, and the client SDKs. See [this guide](https://hackmd.io/8ILy9nLgTmaEJ98mrtPP3A) for more context and a migration guide for RPC 0.9. ### The `DojoStore` trait :::warning This is a **breaking change**; while migration is straightforward, existing projects which do not migrate are at risk of data loss. ::: **TL;DR: all Enums which are stored inside of Dojo models must derive the `Default` trait and set a `#[default]` value.** In response to a potential vulnerability identified with the existing implementation of Dojo storage and uninitialized storage, a new `DojoStore` trait was introduced to give developers more fine-grained control of model storage. This trait will affect data serialization and requires some code updates to handle correctly if you have an existing project. #### Dojo Storage Overview Before describing the issue, here's a brief summary of how Dojo storage works: 1. A model is defined as a Cairo struct. 2. This model is serialized using the `Serde` trait and written to world storage via `world.model_write(@m)`. 3. The world contract's storage acts as a database, where serialized data is written through syscalls to specific storage locations. Since serialization is handled by the `Serde` trait, enums are serialized as follows: 1. The variant index is stored as the first `felt`. 2. If the variant contains a value (i.e., is not the unit type `()`), the serialized value occupies the remaining `felt`s. For example, the `Option` enum is serialized as: ```rust enum Option { Some: T, None, } let a = Option::None; // Serialized = [0x1] let b = Option::Some(2); // Serialized = [0x0, 0x2] ``` In Starknet contracts, the Cairo compiler increments variant indices by `1`, ensuring that uninitialized storage defaults to a predictable variant. Since Dojo uses `Serde`, this increment is not happening. #### Security Considerations Given this behavior, consider the following model: ```rust struct MyModel { #[key] id: u32, score: Option, } ``` If this model is read from storage before being explicitly written, the world's storage remains uninitialized (filled with `0x0`s). This results in: ```rust let my_key: u32 = 0x1234; // Reading an uninitialized model from storage. let m = world.read_model(my_key); // This assertion will revert Cairo execution. // assert(m == None) if m.score.is_some() { // Unexpected execution: `Some(0)` is returned instead of `None`. } else { // Expected behavior, but will not occur in this case. } ``` Here, `Some(0)` is returned instead of `None` because `Some` is the first variant of `Option`, leading to unintended behavior when relying on `score` for logic for uninitialized models. For custom enums, consider: ```rust enum HeroState { Alive: u32, Injured: u32, Dead, } ``` Reading uninitialized storage will return `HeroState::Alive(0)`, as it is the first variant but we might expect another default value associated to `HeroState::Alive`. #### Introduction of a new `DojoStore` trait From Dojo 1.7.0, models are serialized using a new `DojoStore` trait, which basically does the same thing than `Serde` except for enums. When reading an uninitialized model containing an enum, `DojoStore` will automatically use the default variant configured at enum level for deserialization. Let's see an example: ```rust #[derive(Drop)] enum HeroState { Alive: u32, Injured: u32, Dead, } impl HeroStateDefault of Default { fn default() -> HeroState { HeroState::Alive(200) } } #[derive(Drop, Default)] struct MyModel { state: HeroState, } ``` Here, when `DojoStore` deserializes an uninitialized model, it uses the default value of `HeroState` which is `HeroState::Alive(200)`. Of course, you can also use the `Default` derive attribute and tag the default variant when there is no need to configure a specific variant data: ```rust #[derive(Drop, Default)] enum MyEnum { Variant1, #[default] Variant2, Variant3 } ``` In this case, `Variant2` is used for deserializing an uninitialized model containing a `MyEnum` field. For `Option`, the default value is already configured as `None`. #### What to do for a new Dojo project ? For a new Dojo project, just add the `DojoStore` derive attribute to all the data structures used in models (basically all the data structures aimed to be stored). For stored enums, you must also add the `Default` derive attribute and configure a default variant (or implement the `Default` trait like in the previous example). You can omit the `DojoStore` attribute on the model `struct` itself because it will be automatically added when a `struct` is tagged with `dojo::model`. Same for `Introspect`, `Drop` and `Serde`. Note that Dojo events and all the data structures used in events are not stored and so, do not need the `DojoStore` attribute. Of course, if a data structure is used in both Dojo models and events, you have to add the `DojoStore` attribute. Some examples: ```rust #[derive(Drop, Serde, DojoStore, Default)] enum MyEnum { Variant1, #[default] Variant2, Variant3 } #[dojo::model] struct M1 { #[key] k: u32, v1: MyEnum } #[dojo::event] struct E1 { v1: MyEnum } enum AnotherEnum { Variant1, Variant2 } #[dojo::event] struct E2 { v1: AnotherEnum } ``` #### How to migrate an existing Dojo project ? If your project is already deployed on mainnet, there are two cases for each of your models. 1. The model does not contain any enum/option, directly in the model `struct` or in any nested data structures. In this case, just use the `DojoStore` trait as explained in the previous chapter about new projects. Already stored data will be preserved as `DojoStore` do the same thing than `Serde` for all data types other than enums. 2. The model contains at least an enum/option (directly in the model `struct` or in nested data structures). In this case, you must keep the old Dojo storage behaviour to preserve already stored data. To do that, you must add the `DojoLegacyStore` derive attribute to your model `struct` only. ```rust // The enum only need to derive Serde, which should already be the case // since a model requires all its fields to derive Serde. #[derive(Serde)] enum MyEnum { Variant1, Variant2, Variant3 } #[derive(DojoLegacyStore)] #[dojo::model] struct MyModel { #[key] k: u32, v: MyEnum } ``` That means, you still have the potential issue described earlier with uninitialized storage and enums, but there are some solutions to mitigate the risks: * Ensure models are explicitly initialized before being used. * Avoid relying on `Option` for initialization checks. Instead, use a separate `bool` or `integer` field, as these default to `0x0`. * Define the default variant as the first variant to ensure correct behavior when reading uninitialized storage, and if you define an associated variant data, keep in mind that it will be set to 0 by default. :::warning Due to how `DojoStore` is implemented, you may have to rename few methods to interact with models. All the following methods now have an additional `_legacy` version that must be used for the models using `DojoLegacyStore`. ``` read_member_legacy read_member_of_models_legacy write_member_legacy write_member_of_models_legacy read_schema_legacy read_schemas_legacy ``` ::: #### Conclusion to avoid an issue with uninitialized storage and enums If your project relies on `Option` or custom enums, this issue may be critical. We recommend reviewing your usage and considering explicit initialization strategies when applicable. For projects already on `mainnet`, upgrading the contract to modify logic or adding a dedicated initialization field can mitigate potential security risks. This issue affects all versions since Dojo `1.0.0`. From Dojo `1.7.0`, the `DojoStore` trait ensures that uninitialized storage is handled correctly for enums and `Option` and custom enums with a default variant. #### Testing with `dojo-cairo-test` Since `1.7.0`, the `TEST_CLASS_HASH` is now an actual `ClassHash`. The API of `spawn_test_world` has also been updated to ensure we can publish the package on `scarb.xyz`. You now have to import the `world` and pass its class hash to the `spawn_test_world` function. There is no more need of casting the `TEST_CLASS_HASH` to a `ClassHash`. ```rust use dojo::world::{WorldStorageTrait, world}; use dojo_cairo_test::{ NamespaceDef, TestResource, spawn_test_world, }; fn namespace_def() -> NamespaceDef { let ndef = NamespaceDef { namespace: "dojo_starter", resources: [ TestResource::Model(m_Position::TEST_CLASS_HASH), TestResource::Model(m_Moves::TEST_CLASS_HASH), TestResource::Event(actions::e_Moved::TEST_CLASS_HASH), TestResource::Contract(actions::TEST_CLASS_HASH), ] .span(), }; ndef } #[test] fn test_world_test_set() { let ndef = namespace_def(); let mut world = spawn_test_world(world::TEST_CLASS_HASH, [ndef].span()); } ``` #### Using Starknet Foundry Now that Starknet Foundry is supported for Dojo contracts, you can opt to use it instead of `dojo-cairo-test` for testing. YOu can use the whole Starknet Foundry test suite and cheatcodes. Update your `Scarb.toml` to add the `dojo_snf_test` dependency: ```toml [dev-dependencies] dojo_snf_test = "1.7.0" ``` The API is very similar to `dojo-cairo-test` to setup your tests: ```rust use dojo::model::{ModelStorage, ModelStorageTest, ModelValueStorage}; use dojo::world::WorldStorageTrait; use dojo_examples::models::{Direction, Moves, Position, PositionValue}; use dojo_snf_test::{ ContractDef, ContractDefTrait, NamespaceDef, TestResource, WorldStorageTestTrait, set_caller_address, spawn_test_world, }; use starknet::ContractAddress; fn namespace_def() -> NamespaceDef { let ndef = NamespaceDef { namespace: "ns", resources: [ TestResource::Model("Position"), TestResource::Model("Moves"), TestResource::Event("Moved"), TestResource::Contract("actions"), TestResource::Library(("simple_math", "0_1_0")), ] .span(), }; ndef } #[test] fn test_world_test_set() { let caller = NULL_ADDRESS; let ndef = namespace_def(); let mut world = spawn_test_world([ndef].span()); world.sync_perms_and_inits(contract_defs()); } ``` ### Troubleshooting As with any major upgrade, there are always "gotchas" to be aware of. This section will help you address some common issues. #### Toolchain compatibility guide :::warning This compatibility guide is rapidly changing and may be slightly out of date. For the most up-to-date information, [visit our Discord](https://discord.gg/dojoengine). ::: The following is the **latest** compatibility guide for Dojo 1.7. Add these to your `.tool-versions` for best results: ```txt scarb 2.12.2 sozo 1.7.0 katana 1.7.0 torii 1.7.0 ``` #### Sozo build errors If you're having trouble compiling your contracts with Sozo, try adding `dojo_macros` to your `Scarb.toml`: ```toml [dependencies] starknet = ">=2.12.2" dojo = { git = "https://github.com/dojoengine/dojo", tag = "v1.7.0" } dojo_macros = { git = "https://github.com/dojoengine/dojo", tag = "v1.7.0" } # Add this ``` ## Upgrading Overview ### Major Releases #### [Dojo 1.x](/framework/upgrading/dojo-1-0) Dojo's first **major release** in November 2024, which stabilized the Dojo API. [See the Dojo 1.0.0 release notes.](https://github.com/dojoengine/dojo/releases/tag/v1.0.0) :::note It is unlikely that a new Dojo developer will need to upgrade from Dojo 0.x ::: ### Minor Releases #### [Dojo 1.7.x](/framework/upgrading/dojo-1-7) **Dojo 1.7.0 key changes:** * Stabilizes the use of RPC 0.9 of Starknet. * Introduces a new `DojoStore` trait for model serialization. [See the Dojo 1.7.0 release notes.](https://github.com/dojoengine/dojo/releases/tag/v1.7.0) #### Dojo 1.6.x **Dojo 1.6.0 key changes:** * Stabilizes the use of RPC 0.8 of Starknet. [See the Dojo 1.6.0 release notes.](https://github.com/dojoengine/dojo/releases/tag/v1.6.0) #### Dojo 1.5.x This release marked the shift from the "monorepo" approach in which Katana and Torii were kept in the Dojo repo, to a multi-repository approach. As such, this is the first release in which version compatibility became an issue. **Dojo 1.5.1 key changes:** * Dojo lang: introspection is now correctly handling the unity type when explicitly used in enums variant (()). * World: now that the syscall to get the class hash is supported by the network, using the dns correctly returns the class hash relying on the get\_class\_hash\_at syscall. [See the Dojo 1.5.1 release notes.](https://github.com/dojoengine/dojo/releases/tag/v1.5.1) **Dojo 1.5.0 key changes:** * Support for Cairo 2.10 (Dojo lang is still a built-in compiler plugin, no scarbs.xyz at the moment). * The world now keeps track of the ownership counter on resources. It has a new API to verify the ownership of a resource owners\_count. * Signed integers are now fully supported by the introspection. [See the Dojo 1.5.0 release notes.](https://github.com/dojoengine/dojo/releases/tag/v1.5.0) ### Version Compatibility Guide Dojo framework versions are tightly coupled with the rest of the toolchain. Please consult this guide for release and compatibility information. | Dojo Version | 1.5.0 | 1.5.1 | 1.6.0 | | ------------ | ----- | ----- | ----- | | Katana | | | | | 1.6.3 | ❌ | ❌ | ✅ | | 1.6.2 | ❌ | ❌ | ✅ | | 1.6.1 | ❌ | ❌ | ✅ | | 1.6.0 | ❌ | ❌ | ✅ | | 1.5.4 | ✅ | ✅ | ❌ | | 1.5.3 | ✅ | ✅ | ❌ | | 1.5.2 | ✅ | ✅ | ❌ | | 1.5.1 | ✅ | ✅ | ❌ | | 1.5.0 | ✅ | ✅ | ❌ | | Torii | | | | | 1.5.9 | ❌ | ❌ | ✅ | | 1.5.8 | ❌ | ✅ | ✅ | | 1.5.7 | ❌ | ✅ | ✅ | | 1.5.6 | ❌ | ✅ | ✅ | | 1.5.5 | ✅ | ✅ | ✅ | | 1.5.4 | ✅ | ✅ | ✅ | | 1.5.3 | ✅ | ✅ | ✅ | | 1.5.2 | ✅ | ✅ | ✅ | | 1.5.1 | ✅ | ✅ | ✅ | | 1.5.0 | ✅ | ✅ | ✅ | :::info Compatibility data drawn from [this file](https://github.com/dojoengine/dojo/blob/main/versions.json). ::: > For Starknet-wide compatibility information, see the Starknet [version notes](https://docs.starknet.io/learn/cheatsheets/version-notes) and[ compatibility guide](https://docs.starknet.io/learn/cheatsheets/compatibility). ## Cairo Testing Cheat Codes: A Comprehensive Guide The Cairo Testing Cheat Codes allow you to set and manipulate various execution context variables, such as block number, caller address, contract address, to test your contracts in different scenarios. In this guide, we will explore each cheat code in detail, providing explanations and examples to help you understand how to use them effectively in your tests. ### `set_block_number` This cheat code helps you set the current block number to the specified value, allowing you to simulate different block heights for testing purposes. This one is helpful to test a contract's behavior at a specific block height, such as checking if a certain function is only callable after a certain block number. ```rust use starknet::{testing, get_block_number}; #[test] fn f1() { testing::set_block_number(1234567); assert!(get_block_number() == 1234567, 'bad block number'); } ``` ### `set_caller_address` This cheat code helps you set the caller address to the provided contract address, enabling you to test contract interactions with different callers. This cheat code can be applied when: * Testing a contract's access control mechanisms, such as only allowing certain addresses to call specific functions. * Simulating a scenario where a contract is called by a different address. ```rust use starknet::{testing, get_caller_address, contract_address_const}; #[test] fn f2() { let user_one = contract_address_const::<'user1'>(); testing::set_caller_address(user_one); assert(get_caller_address() == user_one, 'bad caller'); } ``` ### `set_contract_address` This cheat code helps you set the contract address to the provided value, allowing you to test contract deployment and interactions. It is important to note that any test function is considered a contract, which by default uses the `0` address. Using `set_contract_address` allows you to mock the current address of the testing function, making it useful to call other contract that may use `get_caller_address`. ```rust use starknet::{testing, get_contract_address, contract_address_const}; #[test] fn f3() { const HUB_ADDRESS: ContractAddress = contract_address_const::<'hub'>() testing::set_contract_address(HUB_ADDRESS); assert(get_contract_address() == HUB_ADDRESS, 'BAD ADDRESS'); } ``` ### `set_block_timestamp` This cheat code helps you set the block timestamp to the specified value, allowing you to test contract behavior at different points in time. You can apply this code when: * Testing a contract's behavior at a specific point in time, such as checking if a certain function is only callable during a certain time period. * Simulating a scenario where a contract is deployed at a different point in time. ```rust use starknet::{testing, get_block_timestamp}; #[test] fn f4() { testing::set_block_timestamp(123456); assert(get_block_timestamp == 123456, 'bad timestamp'); } ``` ### `set_version` This cheat code helps one set the transcation version to the provided value. ```rust use starknet::{testing, get_tx_info}; #[test] fn f5() { testing::set_version('0.1.0'); assert_eq!(get_tx_info().unbox().version, 1_felt252); } ``` ### `set_account_contract_address` This cheat code helps you set the account contract address to the provided value, allowing you to test contract interactions with different account contracts. You can apply when simulating a scenario where a contract is called by a different account contract. ```rust use starknet::{testing, get_tx_info, contract_address_const}; #[test] fn f6() { const contract = contract_address_const::<'contract'>(); testing::set_account_contract_address(contract); assert_eq!(get_tx_info().unbox().account_contract_address.into(), contract); } ``` ### `set_max_fee` This cheat code helps you set the maximum fee to the provided value, enabling you to test contract behavior with different fee structures. You can apply this when: * Testing a contract's behavior with different fee structures, such as checking if a certain function is only callable with a specific fee. * Simulating a scenario where a contract is deployed with a different fee structure. ```rust use starknet::{testing, get_tx_info}; #[test] fn f7() { testing::set_max_fee(123456); assert_eq!(get_tx_info().unbox().max_fee.into(), 123456); } ``` ### `set_transaction_hash` This cheat code helps one set the transaction hash to the provided value, allowing you to test contract behavior with different transaction hashes. You can apply this when: * Testing a contract's behavior with different transaction hashes, such as checking if a certain function is only callable with a specific transaction hash. * Simulating a scenario where a contract is called with a different transaction hash. ```rust use starknet::{testing, get_tx_info}; #[test] fn f8() { testing::set_transaction_hash('12345678'); assert_eq!(get_tx_info().unbox().transcation_hash.into(), '12345678'); } ``` ### `set_chain_id` This cheat code helps one set the chain ID to the provided value, enabling you to test contract behavior on different chains. You can apply this when: * Testing a contract's behavior on different chains, such as checking if a certain function is only callable on a specific chain. * Simulating a scenario where a contract is deployed on a different chain. ```rust use starknet::{testing, get_tx_info}; #[test] fn f9() { testing::set_chain_id('test_chain_id'); assert_eq!(get_tx_info().unbox().chain_id.into(), 'test_chain_id'); } ``` ### `set_nonce` This cheat code helps one set the nonce to the provided value, allowing you to test contract behavior with different nonces. You can apply this when: * Testing a contract's behavior with different nonces, such as checking if a certain function is only callable with a specific nonce. * Simulating a scenario where a contract is called with a different nonce. ```rust use starknet::{testing, get_tx_info}; #[test] fn f10() { testing::set_nonce('test_nonce'); assert_eq!(get_tx_info().unbox().nonce(), 'test_nonce'); } ``` ### `set_signature` This cheat code helps one set the signature to the provided value, enabling you to test contract behavior with different signatures. ```rust use starknet::{testing, get_tx_info}; #[test] fn f11() { testing::set_signature(array!['r', 's'].span()); let s = get_tx_info().unbox().signature; assert_eq!(*s[0], 'r'); assert_eq!(*s[1], 's'); } ``` ### `set_block_number` This cheat code helps one set a specific block number, allowing you to test contract behavior with different block numbers. ```rust use starknet::{testing, get_block_info}; #[test] fn f12() { testing::set_block_number(12345678); assert_eq!(get_block_info().unbox().block_number, 12345678); } ``` ### `pop_log_raw` This cheat code helps one pop the earliest unpopped logged event for the contract, returning the event data and keys. ```rust use starknet::{testing}; #[test] fn f13() { let contract_address = starknet::contract_address_const::<0x42>(); let _log = testing::pop_log_raw(contract_address); } ``` ### `pop_log` This cheat code helps one pop the earliest unpopped logged event for the contract as the requested type, deserializing the event data into the specified type. You can apply this when: * Testing a contract's event handling mechanism, such as checking if a certain event is handled correctly. * Debugging a contract's behavior by inspecting the handled events. ```rust use starknet::{testing, get_caller_address}; use core::starknet::SyscallResultTrait; #[starknet::interface] pub trait IContract { fn post(ref self: T); } #[event] #[derive(Drop, starknet::Event)] pub enum Event { Post: Post, } #[derive(Drop, starknet::Event)] pub struct Post { post_id: u256, transaction_executor: ContractAddress, block_timestamp: u64, } #[starknet::contract] pub mod my_contract { #[abi(embed_v0)] impl IContractImpl of IContract { fn post(ref self: ContractState) { starknet::emit( Post { post_id: pub_id_assigned, transaction_executor: get_caller_address(), block_timestamp: get_block_timestamp(), } ); } } } #[test] fn f14() { let (contract_address, _) = starknet::syscalls::deploy_syscall( my_contract::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata, false ) .unwrap(); let dispatcher = IContractDispatcher { contract_address }; dispatcher.post(); let _log = testing::pop_log::(contract_address); } ``` ### `pop_l2_to_l1_message` This cheat code helps one pop the earliest unpopped L2 to L1 message for the contract, returning the message data and keys. You can apply this when: * Testing a contract's L2 to L1 messaging mechanism, such as checking if a certain message is sent correctly. * Debugging a contract's behavior by inspecting the sent messages. ```rust use starknet::{testing}; #[test] fn f15() { let contract_address = starknet::contract_address_const::<0x42>(); let _msg = testing::pop_l2_to_l1_message(contract_address); } ``` These cheat codes provide a powerful toolset for testing and debugging Starknet contracts. By mastering these cheat codes, you can simulate various scenarios, test edge cases, and ensure the correctness of your contracts. Remember to use them wisely and in conjunction with other testing techniques to achieve comprehensive coverage. With this guide, you are now well-equipped to tackle complex testing challenges and build robust contracts on the Starknet network. Happy testing! ## Testing Testing is a crucial part of any software development process. Dojo provides a testing framework that allows you to write tests for your smart contracts. Since Dojo uses a custom compiler, you need to use [Sozo](/toolchain/sozo/) to test your contracts. From your project directory, run: ```bash sozo test ``` This will search for all tests within your project and run them. ### Writing Unit Tests It is best practise to include unit tests in the same file as the [model](/framework/models/) / [system](/framework/systems/) you are writing. Lets show a `model` test example from the [Dojo-starter](https://github.com/dojoengine/dojo-starter): ```cairo // models.cairo // ... #[cfg(test)] mod tests { use super::{Position, Vec2, Vec2Trait}; #[test] #[available_gas(100000)] fn test_vec_is_zero() { assert!(Vec2Trait::is_zero(Vec2 { x: 0, y: 0 }), "not zero"); } #[test] #[available_gas(100000)] fn test_vec_is_equal() { let position = Vec2 { x: 420, y: 0 }; assert!(position.is_equal(Vec2 { x: 420, y: 0 }), "not equal"); } } ``` In this test we are testing the `is_zero` and `is_equal` functions of the `Position` model. :::tip It is good practise to test all functions of your models. ::: ### Writing Integration Tests Integration tests are e2e tests that test the entire [system](/framework/systems/). You can write integration tests for your world by creating a `tests` directory in your project root. Then create a file for each integration test you want to write. This is the example from the [Dojo-starter](https://github.com/dojoengine/dojo-starter): ```rust // move.cairo #[cfg(test)] mod tests { use dojo::model::{ModelStorage, ModelValueStorage, ModelStorageTest}; use dojo::world::WorldStorageTrait; use dojo_cairo_test::{spawn_test_world, NamespaceDef, TestResource, ContractDefTrait}; use dojo_starter::systems::actions::{actions, IActionsDispatcher, IActionsDispatcherTrait}; use dojo_starter::models::{Position, m_Position, Moves, m_Moves, Direction}; fn namespace_def() -> NamespaceDef { NamespaceDef { namespace: "dojo_starter", resources: [ TestResource::Model(m_Position::TEST_CLASS_HASH), TestResource::Model(m_Moves::TEST_CLASS_HASH), TestResource::Event(actions::e_Moved::TEST_CLASS_HASH), TestResource::Contract(actions::TEST_CLASS_HASH) ].span() } } fn contract_defs() -> Span { [ ContractDefTrait::new(@"dojo_starter", @"actions") .with_writer_of([dojo::utils::bytearray_hash(@"dojo_starter")].span()) ].span() } #[test] fn test_world_test_set() { // Initialize test environment let caller = starknet::contract_address_const::<0x0>(); let ndef = namespace_def(); let mut world = spawn_test_world([ndef].span()); // Test initial position let mut position: Position = world.read_model(caller); assert(position.vec.x == 0 && position.vec.y == 0, "initial position wrong"); // Test write_model_test position.vec.x = 122; position.vec.y = 88; world.write_model_test(@position); let mut position: Position = world.read_model(caller); assert(position.vec.y == 88, "write_value_from_id failed"); // Test model deletion world.erase_model(@position); let position: Position = world.read_model(caller); assert(position.vec.x == 0 && position.vec.y == 0, "erase_model failed"); } #[test] #[available_gas(30000000)] fn test_move() { let caller = starknet::contract_address_const::<0x0>(); let ndef = namespace_def(); let mut world = spawn_test_world([ndef].span()); // Ensures permissions and initializations are synced. world.sync_perms_and_inits(contract_defs()); let (contract_address, _) = world.dns(@"actions").unwrap(); let actions_system = IActionsDispatcher { contract_address }; actions_system.spawn(); let initial_moves: Moves = world.read_model(caller); let initial_position: Position = world.read_model(caller); assert( initial_position.vec.x == 10 && initial_position.vec.y == 10, "wrong initial position" ); actions_system.move(Direction::Right(())); let moves: Moves = world.read_model(caller); let right_dir_felt: felt252 = Direction::Right(()).into(); assert(moves.remaining == initial_moves.remaining - 1, "moves is wrong"); assert(moves.last_direction.into() == right_dir_felt, "last direction is wrong"); let new_position: Position = world.read_model(caller); assert(new_position.vec.x == initial_position.vec.x + 1, "position x is wrong"); assert(new_position.vec.y == initial_position.vec.y, "position y is wrong"); } } ``` ### Dojo Test Utilities Dojo includes some helpful utilities to make testing easier: * [`spawn_test_world`](https://github.com/dojoengine/dojo/blob/main/crates/dojo/dojo-snf-test/src/world.cairo#L140) - Deploy a new world and register the models passed in. * [`deploy_contract`](https://github.com/dojoengine/dojo/blob/main/crates/dojo/dojo-snf-test/src/world.cairo#L106) - Deploy a new contract and return the contract address. For advanced testing capabilities and additional utilities, see the [cheat codes reference](./cheat-codes). ## World API Reference The World API provides a comprehensive interface for interacting with your Dojo world. This guide covers the most commonly used developer functions with practical examples and best practices. ### Quick Reference The World API is organized into these main categories: * **[Model Operations](#model-operations)** - Read, write, and manage model data * **[Event System](#event-system)** - Emit and handle events * **[Permission Management](#permission-management)** - Control access to resources * **[Resource Management](#resource-management)** - Manage models, systems, and contracts * **[Utility Functions](#utility-functions)** - Helper functions and tools * **[Advanced Functions](#advanced-functions)** - System-level operations for framework developers ### Model Operations #### Reading Models ##### `read_model` Reads a model from the world state. ```cairo // Read model with single key let player = get_caller_address(); let position: Position = world.read_model(player); // Read model with multiple keys let resource: GameResource = world.read_model((player, location)); ``` :::note Cairo's strong typing allows it to infer that the `Position` model is being read. ::: **Key Points:** * Model must have at least one `#[key]` field * Returns default values (0, false, etc.) for unset fields ##### `read_member` Reads a specific member from a model without loading the entire model. ```cairo // Read a specific member let vec: Vec2 = world.read_member( Model::::ptr_from_keys(player), selector!("vec") ); ``` :::note The `ptr_from_keys` function is used to get the pointer to the model in storage, as type inference is not possible. The `selector!` macro is used to get the member to target. ::: **When to Use:** * When you only need one field from a large model * For gas optimization in read-heavy operations * When working with frequently accessed fields ##### `read_member_of_models` Reads a specific member from multiple models in a single call. ```cairo // Read the same member from multiple entities let players = [player1, player2, player3]; let positions: Array = world.read_member_of_models( Model::::ptr_from_keys(players.span()), selector!("vec") ); ``` **Use Cases:** * Batch reading for dashboards * Leaderboard calculations * Mass updates based on conditions ##### `read_schema` Efficiently reads a subset of a model using a custom schema. ```cairo // Original model #[derive(Drop, Serde)] #[dojo::model] struct Player { #[key] player: ContractAddress, strength: u8, dexterity: u8, charisma: u8, wisdom: u8, } // Query schema #[derive(Serde, Introspect)] struct PlayerSchema { strength: u8, dexterity: u8, } // Will return only the strength and dexterity members let schema: PlayerSchema = world.read_schema( Model::::ptr_from_keys(player) ); ``` **Requirements:** * Schema fields must match model fields exactly (name and type) * Schema must derive `Serde` and `Introspect` * More efficient than `read_member` for 2+ fields #### Writing Models ##### `write_model` Writes a complete model to the world state. ```cairo let mut position: Position = world.read_model(player); position.vec.x += 1; world.write_model(@position); ``` **Best Practices:** * Use `@` (snapshot) when passing to `write_model` * Consider using `write_member` for single field updates ##### `write_member` Updates a specific field in a model without loading the entire model. ```cairo let position = Vec2 { x: 10, y: 20 }; world.write_member( Model::::ptr_from_keys(player), selector!("vec"), position ); ``` **Advantages:** * More gas efficient for single field updates * Reduces transaction size * Prevents race conditions on concurrent updates ##### `write_member_of_models` Updates the same field across multiple models. ```cairo let players = [player1, player2, player3]; let new_scores = [100, 200, 300]; world.write_member_of_models( Model::::ptr_from_keys(players.span()), selector!("score"), new_scores.span() ); ``` ##### `erase_model` Resets a model to its default state (all non-key fields become 0/false/empty). ```cairo let moves: Moves = world.read_model(player); world.erase_model(@moves); ``` #### Batch Operations For better gas efficiency, use batch operations when working with multiple models of the same type: ```cairo let position1 = Position { player1, vec: Vec2 { x: 10, y: 10 } }; let position2 = Position { player2, vec: Vec2 { x: 10, y: 10 } }; world.write_models([@position1, @position2].span()); ``` ### Event System ##### `emit_event` Emits a custom event that gets indexed by [Torii](/toolchain/torii). ```cairo #[derive(Copy, Drop, Serde)] #[dojo::event] pub struct PlayerMoved { #[key] pub player: ContractAddress, pub from: Vec2, pub to: Vec2, } // Emit the event world.emit_event(@PlayerMoved { player, from: old_position.vec, to: new_position.vec, }); ``` **Event Requirements:** * Must be annotated with `#[dojo::event]` * Must have at least one `#[key]` field * All types must derive `Introspect` ### Permission Management #### Checking Permissions ##### `is_owner` Checks if an address has owner permission for a resource. ```cairo let resource_selector = selector_from_tag!("my_game-Position"); let is_owner = world.is_owner(resource_selector, user_address); ``` ##### `is_writer` Checks if a contract has writer permission for a resource. ```cairo let can_write = world.is_writer(resource_selector, contract_address); ``` #### Granting Permissions ##### `grant_owner` Grants owner permission to an address. ```cairo // Only existing owners or world admin can grant ownership world.grant_owner(resource_selector, new_owner_address); ``` ##### `grant_writer` Grants writer permission to a contract. ```cairo // Only resource owners can grant writer permission world.grant_writer(resource_selector, contract_address); ``` #### Revoking Permissions ##### `revoke_owner` Revokes owner permission from an address. ```cairo world.revoke_owner(resource_selector, owner_address); ``` ##### `revoke_writer` Revokes writer permission from a contract. ```cairo world.revoke_writer(resource_selector, contract_address); ``` ### Resource Management #### Resource Information ##### `resource` Gets information about a resource. ```cairo let resource_info = world.resource(selector_from_tag!("my_game-Position")); ``` ##### `metadata` Gets metadata for a resource. ```cairo let metadata = world.metadata(selector_from_tag!("my_game-Position")); ``` ##### `set_metadata` Sets metadata for a resource. ```cairo let metadata = ResourceMetadata { resource_id: selector_from_tag!("my_game-Position"), metadata_uri: "ipfs://...", metadata_hash: 0x..., }; world.set_metadata(metadata); ``` ### Utility Functions #### `uuid` Generates a unique, sequential identifier. ```cairo let game_id = world.uuid(); let match_id = world.uuid(); ``` :::warning This impacts transaction parallelization since it writes to the same storage slot. Use sparingly in high-throughput scenarios. ::: #### DNS Functions The DNS (Dojo Name System) functions allow you to resolve contract names to their addresses and class hashes. ##### `dns` Resolves a contract name to its address and class hash. ```cairo // Get both address and class hash if let Option::Some((contract_address, class_hash)) = world.dns(@"my_contract") { // Use the contract address and class hash let my_contract = IMyContractDispatcher { contract_address }; } ``` ##### `dns_address` Gets just the contract address from a contract name. ```cairo // Get only the address if let Option::Some(address) = world.dns_address(@"my_contract") { let my_contract = IMyContractDispatcher { contract_address: address }; } ``` ##### `dns_class_hash` Gets just the class hash from a contract name. ```cairo // Get only the class hash if let Option::Some(class_hash) = world.dns_class_hash(@"my_contract") { // Use class hash for library calls or upgrades } ``` **Key Points:** * DNS lookups resolve contract names to deployed contracts and libraries * Returns `Option::None` if the contract name is not found * Works with both deployed contracts and libraries * DNS lookups use the contract name without the namespace prefix ### Advanced Functions The following functions are primarily used by framework developers, tooling, and migration scripts. Most application developers won't touch these directly: #### Entity Operations Low-level functions for direct entity manipulation: * **`entity`** - Gets values of a model entity/member * **`entities`** - Gets model values for multiple entities * **`set_entity`** - Sets model value for an entity/member * **`set_entities`** - Sets model values for multiple entities * **`delete_entity`** - Deletes a model value for an entity/member * **`delete_entities`** - Deletes model values for multiple entities #### System Management Functions for registering and upgrading system resources: * **`register_namespace`** - Registers a namespace in the world * **`register_event`** - Registers an event in the world * **`register_model`** - Registers a new model in the world * **`register_contract`** - Registers and deploys a new contract * **`register_library`** - Registers and declares a library * **`init_contract`** - Initializes a registered contract * **`upgrade_event`** - Upgrades an event in the world * **`upgrade_model`** - Upgrades a model in the world * **`upgrade_contract`** - Upgrades a deployed contract * **`upgrade`** - Upgrades the world with new class hash #### Permission Utilities Additional permission management functions: * **`owners_count`** - Gets the number of owners for a resource ### Performance Tips 1. **Use batch operations** for multiple model updates 2. **Use `read_member`** for single field access 3. **Use `write_member`** for single field updates 4. **Cache frequently accessed data** in local variables 5. **Use `read_schema`** for partial model reads (2+ fields) 6. **Minimize `uuid()` calls** to maintain parallelization ### Common Pitfalls 1. **Forgetting to make world mutable** when writing 2. **Not using `@` when passing models to write functions** 3. **Reading entire models when only one field is needed** 4. **Not checking permissions before operations** 5. **Emitting events without proper key fields** ## World Events Events are the backbone of real-time updates and indexing in Dojo worlds. The World contract automatically emits events for all state changes, and you can create custom events for your specific use cases. ### Overview Dojo uses a two-tier event system: 1. **Built-in World Events**: Automatically emitted by the World contract for all operations 2. **Custom Events**: Developer-defined events for specific application needs All events are automatically indexed by [Torii](/toolchain/torii), making them queryable from your frontend applications. ### Built-in World Events The World contract emits comprehensive events for all operations, providing a complete audit trail of your world's state changes. :::info For the definitive list of world events and their signatures, see the [world contract source code](https://github.com/dojoengine/dojo/blob/main/crates/dojo/core/src/world/world_contract.cairo). ::: #### Model Events ##### `StoreSetRecord` Emitted when a model is written to the world. ```cairo #[derive(Drop, starknet::Event)] pub struct StoreSetRecord { #[key] pub selector: felt252, // Model selector #[key] pub entity_id: felt252, // Entity identifier pub keys: Span, // Entity keys pub values: Span, // Model data } ``` **When emitted**: Every time `world.write_model()` is called. ##### `StoreUpdateRecord` Emitted when a model is updated. ```cairo #[derive(Drop, starknet::Event)] pub struct StoreUpdateRecord { #[key] pub selector: felt252, // Model selector #[key] pub entity_id: felt252, // Entity identifier pub values: Span, // Updated values } ``` **When emitted**: When existing model data is modified. ##### `StoreUpdateMember` Emitted when a specific model member is updated. ```cairo #[derive(Drop, starknet::Event)] pub struct StoreUpdateMember { #[key] pub selector: felt252, // Model selector #[key] pub entity_id: felt252, // Entity identifier #[key] pub member_selector: felt252, // Member being updated pub values: Span, // New values } ``` **When emitted**: When using `world.write_member()`. ##### `StoreDelRecord` Emitted when a model is deleted from the world. ```cairo #[derive(Drop, starknet::Event)] pub struct StoreDelRecord { #[key] pub selector: felt252, // Model selector #[key] pub entity_id: felt252, // Entity identifier } ``` **When emitted**: When `world.erase_model()` is called. #### Resource Management Events ##### `ModelRegistered` **When emitted**: When a new model is registered with the world **Key fields**: `name`, `namespace`, `class_hash`, `address` ##### `EventRegistered` **When emitted**: When a new event is registered with the world **Key fields**: `name`, `namespace`, `class_hash`, `address` ##### `ContractRegistered` **When emitted**: When a new contract is registered with the world **Key fields**: `name`, `namespace`, `address`, `class_hash`, `salt` ##### `NamespaceRegistered` **When emitted**: When a new namespace is registered **Key fields**: `namespace`, `hash` ##### `LibraryRegistered` **When emitted**: When a new library is registered with the world **Key fields**: `name`, `namespace`, `class_hash` #### Upgrade Events ##### `ModelUpgraded` **When emitted**: When a model is upgraded to a new class hash **Key fields**: `selector`, `class_hash`, `address`, `prev_address` ##### `EventUpgraded` **When emitted**: When an event is upgraded to a new class hash **Key fields**: `selector`, `class_hash`, `address`, `prev_address` ##### `ContractUpgraded` **When emitted**: When a contract is upgraded to a new class hash **Key fields**: `selector`, `class_hash` ##### `WorldUpgraded` **When emitted**: When the world contract itself is upgraded **Key fields**: `class_hash` #### Permission Events ##### `OwnerUpdated` Emitted when owner permissions change. **When emitted**: When owner permissions change. **Key fields**: `resource`, `contract`, `value` ##### `WriterUpdated` **When emitted**: When writer permissions change. **Key fields**: `resource`, `contract`, `value` #### System Events ##### `WorldSpawned` **When emitted**: When the world contract is deployed **Key fields**: `creator`, `class_hash` ##### `EventEmitted` **When emitted**: When calling the `emit_event()` function. **Key fields**: `selector`, `system_address`, `keys`, `values` **Full signature** (this is the most commonly used system event): ```cairo #[derive(Drop, starknet::Event)] pub struct EventEmitted { #[key] pub selector: felt252, // Event selector #[key] pub system_address: ContractAddress, // Emitting system pub keys: Span, // Event keys pub values: Span, // Event values } ``` ##### `ContractInitialized` **When emitted**: When a contract's `dojo_init` function is called **Key fields**: `selector`, `init_calldata` ##### `MetadataUpdate` **When emitted**: When resource metadata is updated **Key fields**: `resource`, `uri` ### Custom Events Custom events allow you to emit domain-specific events for your application. They are particularly useful for: * Game-specific UI updates * Non-historical data * Off-chain analytics Custom events are defined similarly to models, but with the `#[dojo::event]` attribute: ```cairo #[derive(Copy, Drop, Introspect)] struct Emote { Smile, Smirk, Frown, } #[derive(Copy, Drop, Serde)] #[dojo::event] pub struct PlayerEmote { #[key] player: ContractAddress, emote: Emote, } ``` The event can then be emitted using the [world API](/framework/world/api): ```cairo world.emit_event(@PlayerEmote { player, mood: Mood::Smile }); ``` **Event Requirements**: * Must be annotated with `#[dojo::event]` * Must have at least one `#[key]` field * All interior types must derive `Introspect` * Must derive `Copy`, `Drop`, and `Serde` #### Examples ```cairo #[derive(Copy, Drop, Serde)] #[dojo::event] pub struct BattleStarted { #[key] pub battle_id: u64, pub attacker: ContractAddress, pub defender: ContractAddress, pub location: Vec2, pub timestamp: u64, } #[derive(Copy, Drop, Serde)] #[dojo::event] pub struct ItemCrafted { #[key] pub player: ContractAddress, #[key] pub item_id: u64, pub item_type: ItemType, pub rarity: Rarity, pub materials_used: Span, } #[derive(Copy, Drop, Serde)] #[dojo::event] pub struct TradeExecuted { #[key] pub trade_id: u64, pub seller: ContractAddress, pub buyer: ContractAddress, pub item_id: u64, pub price: u256, pub currency: ContractAddress, } #[derive(Copy, Drop, Serde)] #[dojo::event] pub struct MarketListingCreated { #[key] pub listing_id: u64, pub seller: ContractAddress, pub item_id: u64, pub price: u256, pub expiration: u64, } ``` Custom events are a powerful tool for building responsive, real-time applications on top of Dojo. Use them thoughtfully to create engaging user experiences while maintaining performance and efficiency. ## World Contract The [World contract](https://github.com/dojoengine/dojo/tree/main/crates/dojo/core/src/world) is the beating heart of every Dojo application. Think of it as a sophisticated database and orchestrator that manages all your models, systems, and permissions while providing a unified interface for your autonomous world. ![World Contract Overview](/framework/world-map.png) ### What is the World Contract? The World contract serves as: * **Central Database**: Stores all your application's models and their data * **Permission Manager**: Controls who can write to your models * **Event Hub**: Emits events for state changes and custom events * **Resource Registry**: Manages models, systems, and contracts within namespaces * **Upgrade Coordinator**: Handles safe upgrades of your application components ```cairo // Every Dojo system gets access to the world let mut world = self.world(@"my_namespace"); // Read a model let position: Position = world.read_model(player); // Write a model world.write_model(@position); // Emit an event world.emit_event(@Moved { player, direction }); ``` ### Core Concepts #### Resources and Namespaces In Dojo, everything is a **resource** - models, systems, events, and even the world itself. Resources are organized within **namespaces** to prevent conflicts and enable modular development. ```cairo // Resources are identified by their namespace and name let world = self.world(@"my_namespace"); // Cairo's type system infers we want the Position model let position: Position = world.read_model(player); ``` ##### Resource Tags and Selectors Resources in Dojo are identified by **tags**, which follow the `namespace-resource` format. Tags provide a human-readable way to reference resources in code. ```cairo // Resource tags follow the "namespace-resource" format // Examples: // - "dojo_starter-Position" (Position model in dojo_starter namespace) // - "my_game-PlayerStats" (PlayerStats model in my_game namespace) // - "dojo_starter-actions" (actions contract in dojo_starter namespace) // Use tags with selector_from_tag! for permissions world.grant_writer(selector_from_tag!("my_game-Position"), address); world.grant_owner(selector_from_tag!("my_game-PlayerStats"), address); ``` **Tag Structure:** * **Namespace**: The logical grouping (e.g., `"my_game"`, `"dojo_starter"`) * **Separator**: Always a hyphen (`-`) * **Resource Name**: The specific resource (e.g., `"Position"`, `"PlayerStats"`) **Common Tag Patterns:** ```cairo // Namespace tag (for namespace-level permissions) "namespace" // e.g., "my_game" // Model tags "namespace-ModelName" // e.g., "my_game-Position" // System tags (contract names) "namespace-ContractName" // e.g., "my_game-actions" // Event tags "namespace-EventName" // e.g., "my_game-PlayerMoved" ``` **Selectors:** Tags are converted to `felt252` **selectors** using `selector_from_tag!`, which computes: ``` resource_selector = poseidon_hash( poseidon_string_hash(resource_namespace), poseidon_string_hash(resource_name) ) ``` :::tip The world itself is a special resource, with the resource selector `0`. ::: #### Entity-Component-System (ECS) Architecture The World contract implements the ECS pattern: * **Entities**: Unique identifiers (often player addresses or generated IDs) * **Components**: Your models (Position, Health, Inventory, etc.) * **Systems**: Functions that operate on components ```cairo // Entity: player address let player = get_caller_address(); // Components: models attached to the entity let position: Position = world.read_model(player); let health: Health = world.read_model(player); // Systems: functions that modify components world.write_model(@updated_position); ``` #### Permissions and Security The World contract implements a resource-based permission system with two permission types. For detailed information about permission management and configuration, see the [Permissions](/framework/world/permissions) guide. * **Owner**: Can manage resources, grant permissions, and upgrade resources * **Writer**: Can write data into resource storage **Resource Hierarchy** (order of precedence): 1. **World** → Can access all resources 2. **Namespace** → Can access all resources in that namespace 3. **Model/Contract/Event** → Can access the specific resource **Key Points**: * Reading is always permissionless * Writing requires Writer permission on the resource or its namespace * When you deploy to a world, you automatically become owner of your namespace ### Getting Started #### Basic World Access Every Dojo system gets access to the world through the namespace-specific `world()` function: ```cairo #[dojo::contract] mod actions { use super::IActions; use dojo::model::ModelStorage; use dojo::world::WorldStorage; #[abi(embed_v0)] impl ActionsImpl of IActions { fn spawn(ref self: ContractState) { // Access world with your namespace let mut world = self.world(@"my_game"); // Now you can read models (always allowed) and write models (requires permission) let player = get_caller_address(); let position = Position { player, vec: Vec2 { x: 0, y: 0 } }; world.write_model(@position); } } } ``` #### Common Usage Patterns **Reading Models**: ```cairo // Single key let position: Position = world.read_model(player); // Multiple keys let resource: GameResource = world.read_model((player, location)); ``` **Writing Models**: ```cairo let mut position: Position = world.read_model(player); position.vec.x += 1; world.write_model(@position); ``` **Emitting Events**: ```cairo #[derive(Copy, Drop, Serde)] #[dojo::event] pub struct Moved { #[key] pub player: ContractAddress, pub direction: Direction, } world.emit_event(@Moved { player, direction }); ``` ### Key Benefits #### Unified Interface Instead of managing multiple contracts and their interactions, you have one consistent interface: ```cairo // All operations go through the world world.read_model(keys); world.write_model(@model); world.emit_event(@event); ``` #### Automatic Indexing The World contract automatically emits events for all state changes, enabling automatic indexing by [Torii](/toolchain/torii) for your frontend applications. #### Upgradeable Architecture Components can be upgraded safely without breaking existing functionality or losing data: ```bash # Make changes to your models or systems, then build sozo build # Run migrate - automatically detects and upgrades changed resources sozo migrate ``` The `sozo migrate` command automatically detects which resources have changed and calls the appropriate upgrade functions on the World contract. #### Gas Optimization The World contract includes several optimizations: * **Batch Operations**: Write multiple models in one transaction * **Efficient Storage**: Optimized storage layouts for different data types * **Permission Caching**: Hierarchical permission checks reduce gas costs ### The World Interface The World contract exposes a complete interface for external interactions. While you typically use the high-level API in your systems, understanding the full interface helps with advanced use cases: ```cairo // Generate unique IDs let new_id = world.uuid(); // Check permissions let can_write = world.is_writer(resource_selector, address); // Manage permissions world.grant_writer(resource_selector, address); ``` ### Next Steps Now that you understand the World contract's role, dive deeper into specific areas: * **[API Reference](/framework/world/api)** - Complete API documentation with examples * **[Permissions](/framework/world/permissions)** - Understanding and managing permissions * **[Events](/framework/world/events)** - Working with the event system * **[Metadata](/framework/world/metadata)** - Configuring world and resource metadata ### Integration with Other Components The World contract integrates seamlessly with other Dojo components: * **[Models](/framework/models)** - Define your data structures * **[Systems](/framework/systems)** - Implement your game logic * **[Sozo](/toolchain/sozo)** - Deploy and manage your world * **[Torii](/toolchain/torii)** - Index and query your world's data The World contract is your application's foundation - everything else builds on top of it. ## Metadata Dojo supports associating offchain metadata with the **world** and its **resources**. This provides additional context about the world and its resources, such as their name, description, icon URI, and more. This enables external services to easily index and distribute worlds and experiences built on them. During migration, `sozo` automatically manages metadata for you, uploading it to IPFS in `JSON` format and registering it in the world contract through the `ResourceMetadata` Dojo model. `sozo` does this by parsing the metadata defined in the Dojo profile configuration `dojo_.toml`. ### World Metadata To set world metadata, create the following section in your `dojo_.toml`: ```toml [world] name = "example" seed = "dojo_examples" description = "example world" icon_uri = "file://assets/icon.png" cover_uri = "file://assets/cover.png" website = "https://dojoengine.org" [world.socials] x = "https://twitter.com/dojostarknet" github = "https://github.com/dojoengine/dojo" discord = "https://discord.gg/FB2wR6uF" ``` The toolchain supports the `name`, `description`, `icon_uri`, `cover_uri`, `website` and `socials` attributes by default. `*_uri` attributes can point to an asset in the repo using the `file://` schema or to remote resources using either `ipfs://` or `https://`. For local assets, `sozo` will upload them to IPFS and replace the corresponding URIs with an IPFS URI. Arbitrary social links can be set by adding key-value pairs under the `socials` section. For example, you could add `telegram = "https://t.me/dojoengine"`. ### Resource Metadata To set resource metadata, create the following sections in your `dojo_.toml`, using `[[models]]`, `[[contracts]]` or `[[events]]` depending on the type of the resource: ```toml [[models]] tag = "ns-Position" description = "Position of the player in the world" icon_uri = "file://assets/position_icon.png" [[events]] tag = "ns-Moved" description = "Event emitted when a player has moved" icon_uri = "file://assets/moved_icon.png" [[contracts]] tag = "ns-actions" description = "Available actions for a player in the world" icon_uri = "file://assets/actions_icon.png" ``` For each type of resource, the toolchain supports the `description` and `icon_uri` attributes by default. The `name` of the resource, retrieved from its `tag`, is also stored in the resource metadata. `*_uri` attributes can point to an asset in the repo using the `file://` schema or to remote resources using either `ipfs://` or `https://`. For local assets, `sozo` will upload them to IPFS and replace the corresponding URIs with an IPFS URI. ### IPFS Configuration The toolchain supports IPFS configuration options in the profile configuration file `dojo_.toml`. This configuration is required for uploading local assets and metadata to IPFS. ```toml [env] ipfs_config.url = "https://ipfs.infura.io:5001" ipfs_config.username = "2EBrzr7ZASQZKH32sl2xWauXPSA" ipfs_config.password = "12290b883db9138a8ae3363b6739d220" ``` :::warning If IPFS configuration is not provided, metadata will not be uploaded to IPFS and a warning will be displayed during migration. Local assets using `file://` URIs will not be processed. ::: ### Technical Implementation Dojo uses the `ResourceMetadata` model to store metadata on-chain. This model is [defined in Cairo](https://github.com/dojoengine/dojo/blob/main/crates/dojo/core/src/model/metadata.cairo) as: ```cairo #[derive(Introspect, Drop, Serde, PartialEq, Clone, Debug)] #[dojo::model] pub struct ResourceMetadata { #[key] pub resource_id: felt252, pub metadata_uri: ByteArray, pub metadata_hash: felt252, } ``` ##### Fields * `resource_id`: A unique identifier for the resource (world, model, contract, or event) * `metadata_uri`: The URI pointing to the metadata JSON file (typically an IPFS URI) * `metadata_hash`: A hash of the metadata content for integrity verification ## World Permissions The Dojo World contract implements a sophisticated hierarchical permission system that controls access to resources. This system works at both configuration time (via `dojo_.toml`) and runtime (via World contract API). ### Core Permissions Concepts #### Permission For Who? Permissions are assigned to **addresses**. These can be **externally-owned addresses**, like that of a user deploying or upgrading a World. They can also be **the addresses of game systems**, typical for regular application use. #### Permission for What? Permissions are given for **resources**. A **resource** can be a world, namespace, system, or model. Resources are indicated by **tags** -- written as `"namespace-resource"`. ![Dojo Permission System](/framework/dojo-auth.png) ### Configuration vs. Runtime Permissions Dojo provides two complementary approaches to permission management: #### Configuration-Time Permissions Set up initial permissions during deployment via your `dojo_.toml` file: ```toml [writers] # Any model in the `dojo_starter` namespace can be written by the `dojo_starter-actions` contract "dojo_starter" = ["dojo_starter-actions"] # Only the `dojo_starter-DirectionsAvailable` model can be written by the `dojo_starter-actions` contract "dojo_starter-DirectionsAvailable" = ["dojo_starter-actions"] [owners] "dojo_starter" = ["dojo_starter-admin"] ``` :::note In this example, permissions are given to **systems**, indicated by tags. ::: #### Runtime Permissions Manage permissions dynamically after deployment using the World contract API: ```cairo // Grant runtime permissions world.grant_owner(selector_from_tag!("dojo_starter-GameState"), new_owner_address); world.grant_writer(selector_from_tag!("dojo_starter-Position"), new_writer_address); ``` :::note In this example, permissions are given to **addresses**, which can be systems or accounts. ::: **When to Use Each:** * **Configuration**: Initial setup, predictable permissions, deployment automation * **Runtime**: Dynamic permission changes, game progression, admin functions ### Permission Types Dojo has two permission types: #### Owner Permission **Owners** have full control over a resource and can: * Write data into the storage of the resource * Grant and revoke permissions to other addresses * Upgrade the resource * Set metadata for the resource #### Writer Permission **Writers** can: * Write data into the storage of the resource * Cannot grant permissions to others * Cannot upgrade the resource ```cairo // Check if an address is an owner let is_owner = world.is_owner(resource_selector, address); // Check if a contract has writer permission let can_write = world.is_writer(resource_selector, address); // Reading is always permissionless let position: Position = world.read_model(player); ``` ### Permission Hierarchy The permission system is resource-based with the following hierarchy: #### 1. World Owner (Highest Level) The **World Owner** can perform any operation on any resource in the world. ```cairo // World owner can access any resource world.grant_writer(any_resource_selector, any_address); world.upgrade_model(any_namespace, any_class_hash); ``` :::note Currently, the **address that deployed the world contract** is the default world owner. In the future, Dojo will add support for a governance system where a contract manages the world, rather than a single address. ::: #### 2. Namespace Owner A **Namespace Owner** can manage all resources within their specific namespace. ```cairo // Namespace owner can manage all resources in "my_game" world.grant_writer(selector_from_tag!("my_game"), new_writer_address); world.grant_owner(selector_from_tag!("my_game"), new_owner_ddress); ``` :::note When you deploy to a world, you automatically become the owner of that namespace, if it's not already registered. ::: **Namespace Owner Rights**: * Can manage the namespace and write to it * Can add more people to the namespace * Can grant writer permissions to resources in the namespace * Can register new models/contracts/events in the namespace #### 3. Resource Owner A **Resource Owner** can manage a specific resource (model, event, or contract). ```cairo // Grant ownership of a specific model world.grant_owner(selector_from_tag!("my_game-Position"), new_owner_address); // Resource owner can manage this specific resource world.grant_writer(selector_from_tag!("my_game-Position"), new_writer_address); ``` #### 4. Writer A **Writer** can write data into the storage of a specific resource but cannot manage permissions. ```cairo // Grant write permission to a system contract world.grant_writer(selector_from_tag!("my_game-Position"), movement_system); // Writer can write to the model let position: Position = world.read_model(player); world.write_model(@updated_position); // Grant write access to entire namespace world.grant_writer(selector_from_tag!("my_game"), system_contract); // This system can now write to ANY resource in the "my_game" namespace // - my_game-Position, my_game-Health, my_game-Inventory, etc. ``` ### Resource-Based Permissions All permissions in Dojo are resource-based. Every component is a resource: * **World** → A resource (selector `0`) * **Namespace** → A resource (e.g., `"my_game"`) * **Model** → A resource (e.g., `"my_game-Position"`) * **Contract** → A resource (e.g., `"my_game-actions"`) * **Event** → A resource (e.g., `"my_game-PlayerMoved"`) **Permission Hierarchy** (order of precedence): 1. **World** (highest) 2. **Namespace** 3. **Model/Contract/Event** (lowest) **Key Points**: * Managing permissions requires **Owner** permission on the resource or its namespace * Writing requires **Writer** permission on the resource or its namespace * Reading is **always permissionless** ```cairo // Example hierarchy for a game world.grant_owner(selector_from_tag!("my_game"), game_admin); // Namespace owner world.grant_owner(selector_from_tag!("my_game-PlayerStats"), stats_manager); // Resource owner world.grant_writer(selector_from_tag!("my_game-PlayerPosition"), movement_system); // Resource writer world.grant_writer(selector_from_tag!("my_game"), system_contract); // Namespace writer (can write to ALL resources in namespace) ``` Here is a simple way to think about organizing permissions in your Dojo application: ![System Permissions](/framework/world/permissions.png) ### Managing Permissions #### Granting Permissions ##### `grant_owner` Only existing owners or higher-level permissions can grant ownership. ```cairo // World owner can grant namespace ownership world.grant_owner(selector_from_tag!("my_game"), game_admin); // Namespace owner can grant resource ownership within their namespace world.grant_owner(selector_from_tag!("my_game-PlayerStats"), stats_manager); // Resource owner can grant ownership of their resource world.grant_owner(selector_from_tag!("my_game-PlayerPosition"), position_manager); ``` ##### `grant_writer` Resource owners can grant writer permissions to contracts. ```cairo // Grant writer permission to a contract world.grant_writer(selector_from_tag!("my_game-PlayerPosition"), movement_contract); // Grant writer permission to multiple contracts world.grant_writer(selector_from_tag!("my_game-PlayerHealth"), combat_contract); world.grant_writer(selector_from_tag!("my_game-PlayerHealth"), healing_contract); ``` #### Revoking Permissions ##### `revoke_owner` Remove ownership from an address. ```cairo // Revoke ownership (only higher-level owners can do this) world.revoke_owner(selector_from_tag!("my_game-PlayerStats"), old_stats_manager); ``` ##### `revoke_writer` Remove writer permission from a contract. ```cairo // Revoke writer permission world.revoke_writer(selector_from_tag!("my_game-PlayerPosition"), old_movement_contract); ``` ### Common Permission Patterns #### Principle of Least Privilege ```cairo // Good: Specific permissions for specific functions world.grant_writer(selector_from_tag!("my_game-PlayerPosition"), movement_contract); world.grant_writer(selector_from_tag!("my_game-PlayerHealth"), combat_contract); // Bad: Overly broad permissions world.grant_owner(selector_from_tag!("my_game"), movement_contract); ``` #### Game Administration ```cairo // Set up admin permissions for a game let game_admin = get_caller_address(); let player_contract = get_contract_address(); // Admin owns the game namespace world.grant_owner(selector_from_tag!("my_game"), game_admin); // Admin owns critical game state world.grant_owner(selector_from_tag!("my_game-GameConfig"), game_admin); world.grant_owner(selector_from_tag!("my_game-GlobalSettings"), game_admin); // Player contract can write to player-specific models world.grant_writer(selector_from_tag!("my_game-PlayerPosition"), player_contract); world.grant_writer(selector_from_tag!("my_game-PlayerInventory"), player_contract); world.grant_writer(selector_from_tag!("my_game-PlayerStats"), player_contract); ``` #### Multi-System Architecture ```cairo // Different systems handle different aspects world.grant_writer(selector_from_tag!("my_game-PlayerPosition"), movement_system); world.grant_writer(selector_from_tag!("my_game-PlayerHealth"), combat_system); world.grant_writer(selector_from_tag!("my_game-PlayerInventory"), inventory_system); world.grant_writer(selector_from_tag!("my_game-PlayerInventory"), trading_system); // Admin system can manage all player data world.grant_owner(selector_from_tag!("my_game-PlayerPosition"), admin_system); world.grant_owner(selector_from_tag!("my_game-PlayerHealth"), admin_system); world.grant_owner(selector_from_tag!("my_game-PlayerInventory"), admin_system); ``` #### Modular Permissions ```cairo // Base game permissions world.grant_writer(selector_from_tag!("my_game-Position"), base_game_contract); world.grant_writer(selector_from_tag!("my_game-Health"), base_game_contract); // Expansion permissions (new contracts can access base models) world.grant_writer(selector_from_tag!("my_game-Position"), expansion_contract); world.grant_writer(selector_from_tag!("my_game-MagicSpells"), expansion_contract); ``` ### Permission Events The World contract emits events when permissions change: ```cairo // Emitted when owner permission is granted/revoked #[derive(Drop, starknet::Event)] pub struct OwnerUpdated { #[key] pub resource: felt252, #[key] pub contract: ContractAddress, pub value: bool, } // Emitted when writer permission is granted/revoked #[derive(Drop, starknet::Event)] pub struct WriterUpdated { #[key] pub resource: felt252, #[key] pub contract: ContractAddress, pub value: bool, } ``` Use these events to track permission changes: ```cairo // Listen for permission changes in your indexer match event { Event::OwnerUpdated(owner_event) => { // Handle ownership change update_owner_permissions(owner_event.resource, owner_event.contract); }, Event::WriterUpdated(writer_event) => { // Handle writer permission change update_writer_permissions(writer_event.resource, writer_event.contract); }, } ``` ### Migration and Permissions During migration, permissions are automatically set up from your configuration: ```toml # dojo_dev.toml [world] name = "my_game" description = "My awesome game" # Configuration-time permissions [writers] "my_game" = ["my_game-actions"] "my_game-Position" = ["my_game-actions"] [owners] "my_game" = ["my_game-admin"] [[contracts]] tag = "my_game-actions" description = "Game actions contract" [[models]] tag = "my_game-Position" description = "Player position model" ``` After migration, you can adjust permissions using the World contract API: ```bash # Grant writer permission using sozo auth sozo auth grant writer my_game-Position,my_game-actions # Grant owner permission using sozo auth sozo auth grant owner my_game,my_game-admin # Check writer permissions sozo call world is_writer 0x123 0x456 # resource_selector contract_address # Check owner permissions sozo call world is_owner 0x123 0x456 # resource_selector contract_address # List all permissions sozo auth list ``` **Configuration Reference:** For detailed configuration options, see [Configuration](/framework/configuration). ### Debugging Permission Issues 1. **Check Resource Selector**: Ensure you're using the correct resource selector 2. **Verify Caller**: Confirm the caller address is what you expect 3. **Check Hierarchy**: Verify the permission hierarchy is set up correctly 4. **Use Events**: Monitor permission events to track changes ```cairo // Debug helper function fn debug_permissions(world: @WorldStorage, resource: felt252, address: ContractAddress) { let is_owner = world.is_owner(resource, address); let is_writer = world.is_writer(resource, address); println!("Resource: {}, Address: {}", resource, address); println!("Is Owner: {}, Is Writer: {}", is_owner, is_writer); } ``` The permission system in Dojo is powerful and flexible, enabling you to build secure, scalable applications. Use it thoughtfully to protect your resources while enabling the functionality your users need. ![alexandria](/libraries/alexandria.png) :::note *I can think of no other edifice constructed by man as altruistic as a lighthouse. They were built only to serve.* *- George Bernard Shaw* ::: ### Alexandria [Alexandria](https://github.com/keep-starknet-strange/alexandria) is a community maintained standard library for Cairo 1.0. It is a collection of useful algorithms and data structures implemented in Cairo. Alexandria provides essential building blocks for Cairo and Starknet development, covering mathematics, data structures, algorithms, and utilities that complement the Dojo framework. :::warning This library is built outside of the Dojo stack development. There is no online documentation yet but most of the implemented features are referenced in the readme files of the crates. ::: ### Installation Add the crates you need to your `Scarb.toml`: ```toml [dependencies] alexandria_math = { git = "https://github.com/keep-starknet-strange/alexandria.git" } alexandria_data_structures = { git = "https://github.com/keep-starknet-strange/alexandria.git" } alexandria_encoding = { git = "https://github.com/keep-starknet-strange/alexandria.git" } alexandria_sorting = { git = "https://github.com/keep-starknet-strange/alexandria.git" } # Add others as needed... ``` ### Key Crates #### [Math](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/math) Mathematical operations and utilities for numerical computations, cryptography, and algorithm implementation. **Key Features**: Prime number testing, fast power/root calculations, bitwise operations, cryptographic hashes ```cairo // Example: validating a prime-based game mechanic use alexandria_math::is_prime::is_prime; use alexandria_math::fast_power::fast_power; use alexandria_math::{BitShift, U32BitShift}; #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Player { #[key] pub address: ContractAddress, pub score: u128, pub level: u32, } // Verify if player's score qualifies for prime number bonus fn check_prime_bonus(world: WorldStorage, player: ContractAddress) -> bool { let player_data: Player = world.read_model(player); // Check if score is prime is_prime(player_data.score, 10) } // Calculate exponential level progression fn calculate_experience_needed(level: u32) -> u128 { // Experience needed = 1000 * 2^level 1000 * fast_power(2_u128, level.into()) } // Use bitwise operations for efficient flag checking fn check_player_permissions(permissions: u32, action: u32) -> bool { // Use bit shift to check if specific permission bit is set (permissions & U32BitShift::shl(1, action)) != 0 } ``` #### [Data Structures](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/data_structures) Efficient collections and data manipulation utilities including stacks, queues, and array extensions. **Key Features**: LIFO stacks, FIFO queues, array/span utilities, bit manipulation ```cairo // Example: managing a turn-based game queue use alexandria_data_structures::stack::StackTrait; use alexandria_data_structures::queue::QueueTrait; #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct GameSession { #[key] pub game_id: u32, pub current_turn: u32, pub players_count: u8, } // Track undo/redo actions with stack fn manage_game_history(ref game_stack: Felt252Stack, action: u32) { game_stack.push(action); // Limit history to last 10 actions while game_stack.len() > 10 { game_stack.pop(); } } // Process player turns in order with queue fn process_turn_queue(ref turn_queue: Queue) -> ContractAddress { match turn_queue.dequeue() { Option::Some(player) => { // Re-add player to end of queue for next round turn_queue.enqueue(player); player }, Option::None => panic!("No players in queue") } } ``` #### [Sorting & Searching](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/sorting) Algorithms for data organization and retrieval including various sorting methods and search algorithms. **Key Features**: Quick sort, merge sort, bubble sort, binary search ```cairo // Example: maintaining leaderboards and finding player rankings use alexandria_sorting::merge_sort::MergeSort; use alexandria_searching::binary_search::binary_search; #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Leaderboard { #[key] pub season: u32, pub scores: Array, pub players: Array, } // Sort player scores for leaderboard display fn update_leaderboard(ref world: WorldStorage, season: u32, new_scores: Array) { let sorted_scores = MergeSort::sort(new_scores.span()); world.write_model(@Leaderboard { season, scores: sorted_scores, players: array![], // Would need corresponding player array }); } // Find player's ranking efficiently fn find_player_rank(scores: Span, player_score: u128) -> Option { binary_search(scores, player_score) } ``` #### [Encoding](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/encoding) Data encoding and decoding utilities including Base64, RLP, and Solidity ABI support. **Key Features**: Base64 encoding/decoding, RLP serialization, Solidity ABI compatibility ```cairo // Example: encoding game data for off-chain storage or cross-chain communication use alexandria_encoding::base64::{Base64ByteArrayEncoder, Base64ByteArrayDecoder}; use alexandria_encoding::rlp::{RLPTrait}; #[derive(Drop, Serde)] pub struct GameState { pub round: u32, pub players: Array, pub moves: Array, } // Encode game state for external storage fn encode_game_state(game_state: GameState) -> ByteArray { let mut encoded_data = ArrayTrait::new(); game_state.serialize(ref encoded_data); // Convert to ByteArray then Base64 encode let byte_data = ByteArrayTrait::from_span(encoded_data.span()); Base64ByteArrayEncoder::encode(byte_data) } // Decode incoming game data fn decode_game_state(encoded_data: ByteArray) -> GameState { let decoded = Base64ByteArrayDecoder::decode(encoded_data); // Convert decoded bytes to span and deserialize let mut decoded_span = decoded.into_span(); Serde::::deserialize(ref decoded_span).unwrap() } ``` ### Additional Crates #### [ASCII](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/ascii) Text processing and character manipulation utilities * Convert integers to ASCII representations * Character encoding and string manipulation functions #### [Linalg](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/linalg) Linear algebra operations for mathematical computations * Dot product calculations for vectors * Kronecker products and matrix operations * Vector norm calculations (L1, L2, infinity norms) #### [Numeric](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/numeric) Advanced numerical operations for scientific computing * Cumulative operations (cumsum, cumprod) for statistical analysis * Numerical differentiation for calculating derivatives * Interpolation functions for data smoothing and prediction #### [Merkle Tree](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/merkle_tree) Cryptographic tree structures for efficient data verification * Binary merkle trees with Pedersen/Poseidon hashing support * Storage proofs for Starknet state verification * Efficient membership and inclusion proofs #### [Storage](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/storage) Onchain data structures optimized for Starknet storage patterns * Persistent lists with efficient append/prepend operations * Storage-optimized key-value structures for large datasets #### [Bytes](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/bytes) Low-level byte manipulation and bit operations * Bit arrays with efficient set/get operations * Byte stream readers and writers for binary data processing * Endianness conversion and byte order utilities * Reversible byte operations for data transformation #### [JSON](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/json) JSON parsing and serialization for data interchange * Deserialize JSON strings into Cairo data structures * Serialize Cairo structs to JSON format * Support for nested objects and arrays #### [Utils](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/utils) General utility functions and formatting helpers * String formatting and display utilities * Common helper functions for development #### [EVM](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/evm) Ethereum Virtual Machine compatibility utilities * EVM bytecode decoding and analysis * Ethereum-style signature verification * Cross-chain compatibility functions #### [BTC](https://github.com/keep-starknet-strange/alexandria/tree/main/packages/btc) Bitcoin protocol implementations and utilities * Bitcoin address generation and validation * BIP340 Schnorr signature support * Bitcoin script and transaction utilities * Taproot and legacy signature schemes ### Integration with Dojo Alexandria complements the Dojo framework by providing: 1. **Mathematical Foundations**: Use `alexandria_math` for game mechanics requiring number theory, cryptography, or advanced calculations 2. **Data Management**: Leverage `alexandria_data_structures` for efficient in-memory data handling in systems 3. **Cross-Chain Interoperability**: Use `alexandria_encoding` for communication with other blockchains or off-chain services 4. **Performance Optimization**: Apply `alexandria_sorting` and `alexandria_searching` for efficient data organization While Alexandria provides general utilities, developers should also consider [Origami](/libraries/origami) for game-specific primitives and algorithms tailored to onchain game development. ![origami](/libraries/origami-icon-word.png) :::note *The magic of origami is in seeing a single piece of Cairo evolve into a masterpiece through careful folds.* ::: ### Origami [Origami](https://github.com/dojoengine/origami) is a collection of essential primitives designed to facilitate the development of onchain games using the Dojo framework. It provides a set of powerful tools and libraries that enable game developers to create complex, engaging, and secure blockchain-based games. Each crate focuses on a specific aspect of game development, from mathematical operations and procedural generation to economic mechanisms and security primitives. While Origami focuses on game-specific primitives, developers should also consider [Alexandria](/libraries/alexandria) for general Cairo utilities that complement Origami's game-focused functionality. ### Installation Add the crates you need to your `Scarb.toml`: ```toml [dependencies] origami_algebra = { git = "https://github.com/dojoengine/origami" } origami_map = { git = "https://github.com/dojoengine/origami" } origami_random = { git = "https://github.com/dojoengine/origami" } # Add others as needed... ``` ### Algebra Mathematical structures and operations for 2D game development including vector operations and algebraic computations. **Key Features**: Vec2 operations (dot product, swizzling), vector utilities, matrix operations ```cairo // Example: checking if an enemy is within a player's field of view use origami_algebra::vec2::{Vec2, Vec2Trait}; #[derive(Copy, Drop, Serde, IntrospectPacked)] pub struct GameVec2 { pub x: u32, pub y: u32 } #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Position { #[key] pub entity: ContractAddress, pub vec: GameVec2, // Where the entity is } #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Facing { #[key] pub entity: ContractAddress, pub direction: GameVec2, // Where the entity is facing } fn can_see_enemy(world: WorldStorage, player: ContractAddress, enemy: ContractAddress) -> bool { let player_pos: Position = world.read_model(player); let enemy_pos: Position = world.read_model(enemy); let player_facing: Facing = world.read_model(player); // Calculate direction from player to enemy let to_enemy = Vec2Trait::new( enemy_pos.vec.x - player_pos.vec.x, enemy_pos.vec.y - player_pos.vec.y ); let facing_vec = Vec2Trait::new( player_facing.direction.x, player_facing.direction.y ); // If dot > 0, enemy is in front of player (within 180° field of view) facing_vec.dot(to_enemy) > 0 } ``` ### Map Tools for generating and manipulating 2D grid-based worlds with procedural generation algorithms and pathfinding. **Key Features**: Maze/cave/random walk generation, A\* pathfinding, object distribution, hex grids ```cairo // Example: creating and exploring a dungeon with hidden treasures use origami_map::map::{Map, MapTrait}; use origami_map::helpers::bitmap::{Bitmap, BitmapTrait}; #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct GameWorld { #[key] pub game_id: u32, pub map: Map, } #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct Treasure { #[key] pub game_id: u32, #[key] pub position: u8, pub discovered: bool, } // Create dungeon with entrance and hidden treasures fn generate_dungeon(ref world: WorldStorage, game_id: u32) { let seed = get_block_timestamp().into() + game_id.into(); // Generate closed maze with few branching paths let mut map = MapTrait::new_maze(18, 14, 0, seed); // Add entrance corridor from edge position 63 map.open_with_corridor(63, 0); // Distribute 5 treasures randomly across walkable tiles let treasure_positions = map.compute_distribution(5, seed); // Store treasures at computed positions let mut pos: u8 = 1; while pos < 252 { // 252 = 18 * 14 if Bitmap::get(treasure_positions, pos) == 1 { world.write_model(@Treasure { game_id, position: pos, discovered: false }); } pos += 1; }; world.write_model(@GameWorld { game_id, map }); } // Find path between two points fn find_path(world: WorldStorage, game_id: u32, start: u8, end: u8) -> Span { let game_world: GameWorld = world.read_model(game_id); game_world.map.search_path(start, end) } ``` ### Random Pseudo-random generation for unpredictable game mechanics including dice rolling and deck management. **Key Features**: Customizable dice, card deck shuffling/drawing, seeded generation ```cairo // Example: opening loot boxes with random rarity and item selection use origami_random::dice::{Dice, DiceTrait}; use origami_random::deck::{Deck, DeckTrait}; #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct LootBox { #[key] pub player: ContractAddress, #[key] pub box_id: u32, pub rarity: u8, pub item_id: u8, } fn open_loot_box(ref world: WorldStorage, player: ContractAddress, box_id: u32) { let seed = get_block_timestamp().into() + box_id.into(); // Roll for rarity and determine loot quality let mut rarity_dice = DiceTrait::new(6, seed); let rarity = rarity_dice.roll(); let deck_size = match rarity { 6 => 10, 5 => 20, 4 => 40, _ => 100 }; // Draw items from appropriate deck let mut item_deck = DeckTrait::new(seed, deck_size); let item_count = if rarity >= 5 { 3 } else if rarity >= 3 { 2 } else { 1 }; let mut i = 0; while i < item_count { let loot = LootBox { player, box_id: box_id + i.into(), rarity: rarity.try_into().unwrap(), item_id: item_deck.draw().try_into().unwrap(), }; world.write_model(@loot); i += 1; } } ``` ### DeFi Sophisticated auction mechanisms for in-game economies using Gradual Dutch Auctions (GDA) and Variable Rate GDAs (VRGDA). **Key Features**: Discrete/continuous GDA, linear/logistic VRGDA, dynamic pricing ```cairo // Example: setting up and querying a GDA auction use origami_defi::auction::gda::{DiscreteGDA, DiscreteGDATrait}; use cubit::f128::types::fixed::{Fixed, FixedTrait}; #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct ItemAuction { #[key] pub item_id: u32, pub gda: DiscreteGDA, pub start_time: u64, } fn setup_auction(ref world: WorldStorage, item_id: u32) { let gda = DiscreteGDA { sold: FixedTrait::new_unscaled(0, false), // 0 items sold initial_price: FixedTrait::new_unscaled(1000, false), // 1000 units available scale_factor: FixedTrait::new_unscaled(11, false) / FixedTrait::new_unscaled(10, false), // 1.1 - each item costs more decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(100, false), // 0.01 - price decays over time }; world.write_model(@ItemAuction { item_id, gda, start_time: get_block_timestamp() }); } fn get_current_price(world: WorldStorage, item_id: u32, quantity: u32) -> u128 { let auction: ItemAuction = world.read_model(item_id); let time_elapsed = get_block_timestamp() - auction.start_time; // Calculate current GDA price for the quantity let price = auction.gda.purchase_price( FixedTrait::new(time_elapsed.into(), false), FixedTrait::new_unscaled(quantity.into(), false) ); price.mag // Convert to u128 } ``` ### Rating Competitive ranking systems for multiplayer games using the industry-standard Elo rating system. **Key Features**: Elo rating calculation, configurable K-factors, win/loss/draw support ```cairo // Example: resolving a match and updating player ratings use origami_rating::elo::EloTrait; #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct PlayerRating { #[key] pub player: ContractAddress, pub rating: u64, pub games_played: u32, } fn resolve_match( ref world: WorldStorage, player1: ContractAddress, player2: ContractAddress, outcome: u8 // 0 = p2 wins, 50 = draw, 100 = p1 wins ) { let mut rating1: PlayerRating = world.read_model(player1); let mut rating2: PlayerRating = world.read_model(player2); // Calculate Elo changes let k_factor = if rating1.games_played < 30 { 40 } else { 20 }; let (change1, is_negative1) = EloTrait::rating_change( rating1.rating, rating2.rating, outcome.into(), k_factor ); // Apply rating changes rating1.rating = if is_negative1 { rating1.rating - change1 } else { rating1.rating + change1 }; rating2.rating = if is_negative1 { rating2.rating + change1 } else { rating2.rating - change1 }; rating1.games_played += 1; rating2.games_played += 1; world.write_model(@rating1); world.write_model(@rating2); } ``` ### Security Cryptographic primitives for secure game mechanics including commitment schemes for trustless gameplay. **Key Features**: Commit-reveal schemes, cryptographic verification, front-running prevention ```cairo // Example: committing and revealing an action with a salt use origami_security::commitment::{Commitment, CommitmentTrait}; use core::poseidon::poseidon_hash_span; #[derive(Copy, Drop, Serde)] #[dojo::model] pub struct PlayerCommitment { #[key] pub game_id: u32, #[key] pub player: ContractAddress, pub commitment_hash: felt252, pub revealed: bool, pub action: u8, } fn commit_action(ref world: WorldStorage, game_id: u32, player: ContractAddress, commitment_hash: felt252) { // Player calculates hash(action + salt) off-chain and submits only the hash world.write_model(@PlayerCommitment { game_id, player, commitment_hash, revealed: false, action: 0 }); } fn reveal_action(ref world: WorldStorage, game_id: u32, player: ContractAddress, action: u8, salt: felt252) -> bool { let mut commitment: PlayerCommitment = world.read_model((game_id, player)); assert(!commitment.revealed, 'Already revealed'); // Verify commitment let mut reveal_data = array![]; action.serialize(ref reveal_data); salt.serialize(ref reveal_data); let is_valid = poseidon_hash_span(reveal_data.span()) == commitment.commitment_hash; if is_valid { commitment.revealed = true; commitment.action = action; world.write_model(@commitment); } is_valid } ``` ## Advanced Features This page explores advanced Katana capabilities for production deployments, complex integration scenarios, and sophisticated development workflows. These features enable enterprise-grade blockchain infrastructure and cross-chain interoperability. ### Execution Architecture Katana's execution engine handles transaction processing and state management through a modular architecture built on the Blockifier library. This system provides high-performance Cairo contract execution with configurable validation and invocation limits. #### Blockifier Integration Katana uses Starknet's **Blockifier** library as its core execution engine. The Blockifier provides: * **Cairo VM execution** for contract logic processing * **State transition management** with Merkle tree updates * **Fee calculation and validation** for transaction costs * **Resource tracking** for gas usage and execution steps #### Execution Configuration Configure execution parameters for different environments: ```bash # Production limits katana --validate-max-steps 1000000 --invoke-max-steps 10000000 # Development with relaxed limits katana --dev --validate-max-steps 5000000 --invoke-max-steps 50000000 ``` #### Performance Considerations **Validation Steps**: Control computational limits for account validation logic. Higher limits allow more complex validation but increase resource usage. **Invocation Steps**: Set maximum Cairo steps for contract function execution. Production environments should use conservative limits to prevent DoS attacks. **Memory Management**: The execution engine uses LRU caching for compiled contract classes. Native compilation can significantly improve execution performance for compute-intensive contracts. :::tip Monitor execution metrics to optimize step limits for your specific workload. Use `--metrics` to track Cairo steps processed and execution times. ::: ### Optimistic Katana Optimistic Katana provides a pre-confirmed execution layer on top of Starknet by combining Katana's forking feature with transaction forwarding and strict whitelisting. It allows us to: * Execute and serve transactions locally, almost instantly * Forward those transactions to Starknet for canonical inclusion * Ensure state consistency through infrastructure-level control, not by reconciliation (shortcut that typically Sharding execution will solve) Optimistic Katana does not need to roll back any state. Thanks to the operator whitelisting strategy, only authorized executors can modify the Starknet state of a specified world, ensuring that no conflicts arise between the local optimistic execution and the canonical onchain result. This approach delivers near-instant feedback for users while maintaining trust and state consistency across the network. #### Architecture ##### Fork-Based Execution Optimistic Katana builds upon Katana's fork mode. In this setup: * Katana forks a live Starknet network * All user transactions are sent to the forked Katana instance * Katana executes these transactions locally, producing immediate state updates and events * In parallel, Katana forwards the same transactions to a real Starknet node for canonical inclusion Since Katana executes transactions faster than the actual network, it can serve "pre-confirmed" results almost instantly — enabling frontends and clients to interact with what feels like a live, responsive chain. Importantly, Optimistic Katana does not produce blocks itself. Instead, it maintains a local view of pre-confirmed transactions (executed locally) and exposes them as part of its state until the corresponding Starknet confirmations arrive. Every block mined on Starknet is still reflected in Optimistic Katana (only block number, the state is fetched lazily). ##### State Integrity and Whitelisting To prevent state contention and ensure consistency with the canonical Starknet state, Optimistic Katana is combined with strict operator whitelisting. Only designated operator accounts (typically Cartridge's paymaster executors) are permitted to modify the onchain state of the world. This guarantees that: * Only transactions forwarded from Katana are authorized by infrastructure to land on Starknet for this world * No external actor can submit conflicting transactions to the same contract state on Starknet * The optimistic state served by Katana will always converge with the canonical one without rollback or reconciliation logic At the infrastructure level, it ensures that only transactions originating from the authorized Optimistic Katana instance are propagated downstream to Starknet nodes. This prevents race conditions and ensures deterministic state updates across both layers. ##### World Layer: Operator Component At the smart contract level, the world contract implements the onchain enforcement of the operator whitelist through the Operator component. This component defines a simple but flexible interface for managing authorized executors, controlling who can mutate world entities onchain. ```cairo #[derive(Default, Serde, Drop, starknet::Store)] pub enum OperatorMode { #[default] Disabled, NeverExpire, ExpireAt: u64, } #[starknet::interface] pub trait IOperator { /// Changes the mode of the operator component. fn change_mode(ref self: T, mode: OperatorMode); /// Grants an operator to the contract. fn grant_operator(ref self: T, operator: ContractAddress); /// Revokes an operator from the contract. fn revoke_operator(ref self: T, operator: ContractAddress); } ``` The `OperatorMode` allows dynamic control over when and how operators can act (e.g., permanent or time-limited authorization). Only the creator of the world can change the mode. More importantly, the `set_entity` function within the world contract is gated by this operator check. This means that no contract state or world entity can be mutated unless the sender is a whitelisted operator. In practice: * Katana instances executing optimistically are assigned authorized operator addresses * The infrastructure ensures that only these operators can push mutations to Starknet * Any unauthorized attempt to modify entities is rejected at the contract level This mechanism establishes a trust boundary: Katana can optimistically execute and stage updates, but only the approved executors have authority to finalize them onchain. It is the contract-level foundation that enables Optimistic Katana to function safely without reconciliation or rollback logic. ##### Torii Integration (Indexer) On the [Torii](/toolchain/torii) side, a caching layer has been added to handle optimistic execution correctly. Specifically: * Torii now maintains a cache of processed transactions (instead of only a cursor to latest processed transaction), ensuring that pre-confirmed events are not re-processed multiple times * It is resistant to missed transactions — cases where a transaction was forwarded to Starknet before Torii fetched the corresponding pre-confirmed state (for example, if a block is very long to process) * When such cases occur, Torii will backfill the missing events once the canonical Starknet state includes them, ensuring complete and consistent indexing This design ensures that Torii's view of the world remains consistent across both the optimistic and canonical layers without duplication or event loss. :::note A possible latency can be observed if blocks are too long for Torii to process, which will cause Torii to fallback in range mode. Since Katana is in forking mode, syncing historical events may take additional time since they are lazily fetched from the Starknet network. ::: ### Cross-Layer Messaging Cross-layer messaging enables communication between Katana and external blockchain networks, supporting L1 ↔ L2 message passing for complex multi-chain applications. This system allows contracts on Katana to interact with Ethereum mainnet or other Starknet networks. #### Supported Settlement Chains **Ethereum Integration**: Production-ready messaging with Ethereum mainnet or testnets. Messages are sent directly to L1 contracts without requiring block proof verification, enabling faster development cycles. **Starknet Integration**: Experimental feature for Starknet-to-Starknet messaging. This creates a hierarchical Starknet architecture where Katana acts as an L3 settling to another Starknet L2. :::warning Starknet messaging is experimental and not recommended for production use. Use Ethereum messaging for production deployments. ::: #### Configuration Setup Enable messaging by providing a configuration file: ```bash katana --messaging messaging_config.json ``` **Configuration Structure**: ```json { "chain": "ethereum", "rpc_url": "https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY", "contract_address": "0x5FbDB...", "sender_address": "0xf39Fd...", "private_key": "0xac097...", "interval": 5, "from_block": 18500000 } ``` **Configuration Parameters**: * **`chain`**: Settlement chain type (`"ethereum"` or `"starknet"`) * **`rpc_url`**: RPC endpoint for the settlement chain * **`contract_address`**: Messaging contract address on settlement chain * **`sender_address`**: Account address for sending settlement transactions * **`private_key`**: Private key for the sender account (keep secure!) * **`interval`**: Message polling interval in seconds * **`from_block`**: Starting block for message synchronization #### Message Flow Architecture **L2 → L1 Messages**: 1. Contract calls `send_message_to_l1()` on Katana 2. Message is queued in Katana's message pool 3. Messaging service polls for new messages 4. Messages are relayed to L1 messaging contract 5. L1 contract can consume messages immediately **L1 → L2 Messages**: 1. L1 contract calls messaging bridge 2. Katana polls L1 for new messages 3. Messages are delivered to target L2 contracts 4. L2 contracts process messages via `l1_handler` functions #### Example Implementation See the [starknet-messaging-dev](https://github.com/glihm/starknet-messaging-dev) repository for a complete example demonstrating: * L1 ↔ L2 message passing implementation * Local development setup with Anvil and Katana * Contract examples for both layers * Testing workflows for cross-chain interactions ### Chain Initialization and Settlement Katana provides `katana init` for initializing new blockchain networks with configurable settlement layers. This enables deployment of rollup chains that settle to Starknet networks or sovereign chains with data availability layers. #### Settlement Models **Rollup Settlement**: Deploy a rollup chain that settles to Starknet mainnet or testnet. State commitments and proofs are verified on the settlement layer using fact registry contracts. **Sovereign Chain**: Initialize an independent blockchain that publishes state updates to a data availability layer without settlement verification. #### Chain Initialization Initialize a new rollup settling to Starknet mainnet: ```bash katana init \ --id "my_rollup" \ --settlement-chain mainnet \ --settlement-account-address 0x123... \ --settlement-account-private-key 0xabc... \ --output-path ./chain-config ``` Initialize a sovereign chain: ```bash katana init \ --sovereign \ --id "my_sovereign_chain" \ --output-path ./chain-config ``` #### Configuration Parameters **Chain Identity**: * `--id`: Unique chain identifier (required, must be valid ASCII) * `--output-path`: Directory for generated configuration files **Settlement Configuration**: * `--settlement-chain`: Target settlement network (`mainnet`, `sepolia`, or custom) * `--settlement-account-address`: Account address for settlement operations * `--settlement-account-private-key`: Private key for settlement account * `--settlement-facts-registry`: Custom fact registry contract address **Contract Deployment**: * `--settlement-contract`: Pre-deployed settlement contract address (optional) * `--settlement-contract-deployed-block`: Block number of contract deployment * If no contract address provided, init will deploy a new settlement contract #### Settlement Chain Options **Starknet Mainnet**: Production settlement with maximum security. Uses Herodotus Atlantic Fact Registry for proof verification. **Starknet Sepolia**: Testnet settlement for development and testing. Lower cost alternative with same security model. **Custom Settlement**: Specify custom RPC endpoint and fact registry. Requires `--settlement-facts-registry` parameter. #### Generated Configuration The init process creates configuration files in the specified output directory: ``` chain-config/ ├── genesis.json # Genesis state and allocations ├── config.toml # Chain configuration parameters └── messaging.json # Cross-layer messaging setup ``` #### Example Workflow 1. **Prepare Settlement Account**: Create and fund an account on the settlement chain 2. **Initialize Chain**: Run `katana init` with appropriate parameters 3. **Review Configuration**: Examine generated config files and adjust as needed 4. **Launch Network**: Start Katana using the generated configuration 5. **Monitor Settlement**: Track settlement transactions and proof submissions :::tip Use Sepolia testnet for initial development and testing before moving to mainnet settlement. This allows you to validate your rollup configuration without mainnet costs. ::: ### Starknet Toolchain Integration Katana provides full compatibility with the standard Starknet development ecosystem, enabling seamless integration with existing workflows and tooling. This allows developers to use familiar tools like Starkli for contract deployment and interaction on local Katana networks. #### Starkli Integration Starkli includes built-in support for Katana networks with pre-configured account integration. **Installing Starkli**: ```bash curl https://get.starkli.sh | sh && starkliup ``` **Environment Setup**: ```bash # Start Katana with development accounts katana --dev --dev.no-fee # Configure Starkli environment export STARKNET_ACCOUNT=katana-0 # Pre-funded account export STARKNET_RPC=http://127.0.0.1:5050 ``` **Contract Interaction**: ```bash # Deploy a Scarb build artifact starkli declare contract.sierra.json # Deploy contract instance starkli deploy # Call contract functions starkli call starkli invoke ``` :::note [Sozo](/toolchain/sozo) is the preferred build and deployment for Dojo development. Starkli integration is useful for standard Cairo contracts and production validation workflows. ::: ## Configuration Guide Katana supports flexible configuration through TOML files and command-line options. This enables you to customize everything from network settings to gas prices for both development and production deployments. #### Configuration Priority 1. **Command-line arguments** (highest) 2. **Configuration file** (via `--config`) 3. **Environment variables** 4. **Default values** (lowest) ### TOML Configuration Use a TOML configuration file to define persistent settings for your Katana instance. Pass the configuration file using the `--config` flag: ```bash katana --config katana_prod.toml ``` #### Key Configuration Sections * **Core**: Node behavior (`silent`, `no_mining`, `block_time`, `db_dir`) * **Server**: HTTP server settings (`http_addr`, `http_port`, CORS) * **Starknet**: Chain configuration (`chain_id`, execution limits) * **Gas Oracle**: Gas price settings for L1/L2 tokens * **Forking**: Network forking configuration * **Development**: Developer-friendly features * **Metrics**: Monitoring and observability #### Complete Configuration Reference ```toml # Core node settings silent = false # Don't print anything on startup (default: false) no_mining = false # Disable auto/interval mining (default: false) block_time = 6000 # Block time in milliseconds (default: none) db_dir = "./katana-db" # Database dir for non-Slot deployments (default: none) [logging] log_format = "full" # Log format: "full" or "json" (default: full) [server] http_addr = "127.0.0.1" # HTTP-RPC server interface (default: 127.0.0.1) http_port = 5050 # HTTP-RPC server port (default: 5050) http_cors_origins = ["*"] # CORS allowed origins (default: none) max_connections = 100 # Max concurrent connections (default: none) max_request_body_size = 33554432 # Max request body size in bytes (default: none) max_response_body_size = 33554432 # Max response body size in bytes (default: none) timeout = 60 # RPC server request timeout in seconds (default: none) max_event_page_size = 1024 # Max page size for event queries (default: 1024) max_proof_keys = 100 # Max keys for storage proofs (default: 100) max_call_gas = 1000000000 # Max gas for starknet_call RPC (default: 1000000000) [dev] dev = true # Enable development mode (default: true) seed = "0" # Seed for account generation (default: 0) total_accounts = 10 # Number of pre-funded accounts (default: 10) no_fee = false # Disable transaction fees (default: false) no_account_validation = false # Skip account validation (default: false) [starknet.env] chain_id = "KATANA" # Chain ID (default: KATANA) validate_max_steps = 1000000 # Max steps for validation (default: 1000000) invoke_max_steps = 10000000 # Max steps for execution (default: 10000000) [forking] fork_provider = "https://api.cartridge.gg/x/starknet/mainnet" # Fork provider URL fork_block = 1000000 # Fork at specific block number/hash [metrics] metrics = false # Enable metrics server (default: false) metrics_addr = "127.0.0.1" # Metrics server address (default: 127.0.0.1) metrics_port = 9100 # Metrics server port (default: 9100) [gpo] # Gas Price Oracle - all values are u128 strings (hex/decimal) l2_eth_gas_price = "100000000000" # L2 ETH gas price in wei l2_strk_gas_price = "1000000000000000" # L2 STRK gas price in fri l1_eth_gas_price = "100000000000" # L1 ETH gas price in wei l1_strk_gas_price = "1000000000000000" # L1 STRK gas price in fri l1_eth_data_gas_price = "100000000" # L1 ETH data gas price in wei l1_strk_data_gas_price = "10000000000" # L1 STRK data gas price in fri [cartridge] controllers = false # Declare Controller classes at genesis (default: false) paymaster = false # Use Cartridge paymaster (default: false) api = "https://api.cartridge.gg" # Cartridge API URL (default: https://api.cartridge.gg) [explorer] explorer = false # Enable explorer frontend (default: false) ``` :::tip Setting `explorer = true` will enable a browser-based block explorer at `/explorer`. ::: ### Command-Line Options All TOML configuration options can be overridden using command-line flags. Command-line arguments take precedence over configuration file settings. #### CLI Usage Most configuration options can be passed as command-line arguments: ```bash # Basic usage katana [OPTIONS] [COMMAND] # Development with custom settings katana --dev --http.port 3000 --dev.no-fee # Production with configuration file katana --config prod.toml --db-dir ./katana-db # Fork from mainnet at specific block katana --fork.provider https://api.cartridge.gg/x/starknet/mainnet --fork.block 1000000 # Advanced execution limits katana --validate-max-steps 2000000 --invoke-max-steps 20000000 ``` :::tip Use `katana --help` for a full command reference. ::: #### Subcommands Katana provides several subcommands for specialized operations beyond running the sequencer. ##### `katana completions ` Generate shell completion scripts for improved command-line experience with tab completion and argument suggestions. **Supported shells:** `bash`, `elvish`, `fish`, `powershell`, `zsh` **Usage:** ```bash katana completions ``` **Setup Examples:** ```bash # Bash - add to ~/.bashrc or ~/.bash_profile katana completions bash >> ~/.bashrc # Zsh - add to ~/.zshrc katana completions zsh >> ~/.zshrc # Fish - install to completions directory katana completions fish > ~/.config/fish/completions/katana.fish ``` ##### `katana db` Database utilities for managing persistent Katana storage and diagnostics. Display database storage information including table sizes, row counts, and storage usage. Useful for monitoring database growth and identifying performance bottlenecks. **Usage:** ```bash katana db stats [--path ] ``` **Examples:** ```bash # Check default database location katana db stats # Check specific database path katana db stats --path ./my-katana-db # Monitor production database katana db stats --path /var/lib/katana/production ``` ##### `katana init` Initialize chain configuration for rollup or sovereign blockchain deployment. Creates genesis files, chain specifications, and settlement configurations. **Usage:** ```bash katana init [OPTIONS] ``` **Common Use Cases:** * **Rollup Setup**: Initialize a new rollup that settles to Starknet * **Sovereign Chain**: Create an independent blockchain with DA layer * **Testing Networks**: Set up reproducible test environments **Quick Examples:** ```bash # Initialize a rollup settling to Starknet Sepolia katana init --id my-rollup --settlement-chain sepolia # Create a sovereign chain katana init --sovereign --id my-chain --output-path ./chain-config ``` For detailed chain initialization examples and configuration options, see the [Chain Initialization](/toolchain/katana/advanced#chain-initialization-and-settlement) section in the Advanced Features guide. ### Genesis Configuration Genesis configuration allows you to define the initial state of your blockchain network, including: * **Fee token configuration** and initial balances * **Pre-funded accounts** with custom settings * **Pre-declared classes** for immediate use * **Pre-deployed contracts** with initial storage Use genesis configuration to create consistent, repeatable network states for testing and development: ```bash katana --genesis genesis.json ``` :::tip Most users will not need to define a custom genesis configuration. ::: #### Genesis JSON Structure Genesis configuration is defined in a JSON file with the following structure: **`number`** - Genesis block number **`parentHash`** - Parent block hash **`timestamp`** - Block timestamp **`stateRoot`** - Initial state root **`sequencerAddress`** - Sequencer address **`gasPrices`** - Initial gas prices (`ETH`, `STRK`) **`feeToken`** - Network fee tokens (optional) * `name`, `symbol`, `decimals` - Token metadata * `address` - Token contract address (optional) * `class` - Token class reference (`classHash` or `name`, optional) * `classHash` - The hash of the fee token class * `name` - The name of the fee token class (defined in `classes`) * `storage` - Key-value pairs for the fee token's storage **`universalDeployer`** - The universal deployer configuration (optional) * `address` - The universal deployer contract address (optional) * `storage` - Key-value pairs for the deployer's storage (optional) **`accounts`** - Pre-funded accounts For every ``, provide: * `publicKey`, `privateKey` - Account key pair (private key optional) * `balance` - Initial token balance (optional) * `nonce` - Account nonce (optional) * `class` - Account contract class (`classHash` or `name`, optional) * `classHash` - The hash of the contract class * `name` - The name of the contract class (defined in `classes`) * `storage` - Key-value pairs for the account's storage (optional) :::note Katana in `--dev` mode comes with 10 pre-funded accounts. ::: **`contracts`** - Pre-deployed contracts For every ``, provide: * `class` - Contract class reference (`classHash` or `name`) * `classHash` - The hash of the contract class * `name` - The name of the contract class (defined in `classes`) * `balance` - Contract balance (optional) * `storage` - Key-value pairs for the contract's storage (optional) **`classes`** - Class declarations * `class` - Path to class artifact file or full class object * `classHash` - Overrides computed class hash (optional) * `name` - Reference name for `classHash` (optional) #### Example Genesis Configuration ```jsonc { "number": 0, "parentHash": "0x07b4a...", "timestamp": 1703875200, "stateRoot": "0x02a5c...", "sequencerAddress": "0x04f8b...", "gasPrices": { "ETH": 100000000000, "STRK": 1000000000000000, }, "feeToken": { "name": "ETHER", "symbol": "ETH", "decimals": 18, "class": "0x02d56...", "storage": { "0x11": "0x111", }, }, "universalDeployer": { "address": "0x041a7...", "storage": { "0x10": "0x100", }, }, "accounts": { "0x66efb...": { "publicKey": "0x07d82...", "balance": "0xD3C21BCECCEDA1000000", "class": "0x029927...", }, "0x6b86e...": { "publicKey": "0x04c12...", "balance": "0xD3C21BCECCEDA1000000", }, }, "contracts": { "0x29873...": { "class": "MyERC20", "balance": "0xD3C21BCECCEDA1000000", "storage": { "0x1": "0x1", "0x2": "0x2", }, }, }, "classes": [ { "class": "path/to/file/erc20.json", "name": "MyERC20", }, { "class": { "abi": [ { "name": "AccountCallArray", "type": "struct", "size": 4, "members": [ { "name": "to", "offset": 0, "type": "felt" }, { "name": "selector", "offset": 1, "type": "felt" }, { "name": "data_offset", "offset": 2, "type": "felt", }, { "name": "data_len", "offset": 3, "type": "felt" }, ], }, ], }, }, ], } ``` ## Development Features Katana provides essential development features designed to streamline local development and testing workflows. ### Block Production Modes Katana offers flexible block production through different block production modes. :::info By default, blocks are mined instantly when transactions are received. ::: #### Interval Mining Interval mining creates blocks at regular time intervals rather than on each transaction. Enable this mode with the `--block-time ` flag: ```bash # Produces a new block every 10 seconds katana --block-time 10000 ``` #### On-demand Mining On-demand mining gives you complete control over when blocks are created. This mode is ideal for testing scenarios where you need precise timing control. Transactions are processed immediately but remain pending until you manually trigger block creation using the [`generateBlock`](/toolchain/katana/reference#dev-namespace) RPC method. When called, all pending transactions are included in the new block. To enable on-demand mining, use the `--no-mining` flag. ```bash katana --no-mining ``` ### Storage Modes Katana supports two storage modes to match different development needs: **In-Memory (Default)**: All data is stored in RAM and cleared when Katana stops. This provides best performance by avoiding disk operations, making it perfect for rapid testing and experimentation. **Persistent Storage**: Chain state is saved to disk and restored on restart. This is useful for production deployments, extended testing sessions, or when you need to preserve complex game states. #### Persistent Storage Enable persistent storage with the `--db-dir ` flag. Katana will create a new database file or load an existing database at the specified location. :::note Forked mode only supports in-memory storage. Persistent storage for forked networks is not yet available. ::: ##### Usage Examples ```bash # Initialize a new database in the specified directory katana --db-dir ./katana-db ``` ```bash # Resume from a previously saved state katana --db-dir ./existing-katana-db ``` ### State Forking State forking lets you create a local copy of any Starknet network, allowing you to test against real deployed contracts without having to manually reconstruct the chain state. Configure forking with the `--fork.provider ` flag. Katana forks from the latest block by default, or specify a particular block with `--fork.block `. Once forked, you can: * Deploy new contracts that interact with existing live contracts * Use pre-funded Katana accounts to simulate user interactions * Test complex scenarios without mainnet costs or setup overhead This is especially valuable for testing integrations with established protocols or validating contract behavior against real network conditions before mainnet deployment. :::note Forking currently supports blockchain state (storage, classes, nonces) but not historical data (blocks, transactions, receipts, events). Full historical data support is planned. ::: ##### Usage Example Fork Starknet mainnet at a specific block. Your local node will have all mainnet state up to that block and continue with new local blocks: ```bash # Forks mainnet at block 1200 katana --fork.block 1200 \ --fork.provider "https://api.cartridge.gg/x/starknet/mainnet" ``` ### Cairo Native Compilation Cairo Native provides significant performance improvements by compiling Cairo programs to native machine code instead of using VM interpretation. This optional feature uses MLIR and LLVM for ahead-of-time compilation, offering substantial execution speed gains for compute-intensive contracts. Native compilation can provide: * **Faster contract execution** through optimized machine code * **Reduced CPU overhead** compared to VM interpretation * **Better performance scaling** for complex Cairo programs #### Enabling Native Compilation Cairo Native must be enabled at compile time and runtime: **Compile-time**: Build Katana with the `native` feature flag: ```bash cargo build --release --features native ``` :::note If installing Katana with `asdf`, pass `ASDF_NATIVE_BUILD=true` for the native build. ::: **Runtime**: Enable native compilation when starting Katana: ```bash katana --enable-native-compilation ``` #### Development Considerations **When to Use**: * Performance testing and benchmarking * Compute-heavy contract development * Production environments requiring maximum throughput **Trade-offs**: * Increased compilation time during contract loading * Additional system dependencies (LLVM 19) * Larger binary size and memory usage :::note Native compilation requires LLVM 19 dependencies. Install with `make native-deps-macos` (macOS) or `make native-deps-linux` (Linux). ::: ##### Usage Example Start Katana with native compilation for performance testing: ```bash katana --dev --enable-native-compilation --block-time 1000 ``` ### Metrics and Monitoring Katana includes a built-in metrics system that exposes performance data in [Prometheus](https://prometheus.io/) format. This enables monitoring of blockchain performance, resource usage, and transaction processing statistics. Katana also includes a browser-based block explorer for easy visualization of sequencer activity. #### Available Metrics Katana collects metrics across multiple components: * **Block production**: Gas processed, Cairo steps, block timing * **System resources**: Memory usage, CPU utilization, disk I/O * **RPC performance**: Request latency, error rates, throughput * **Transaction pool**: Pending transactions, validation times #### Enabling Metrics Start the metrics server on port 9100: ```bash katana --metrics ``` Customize the metrics server address and port: ```bash katana --metrics --metrics.addr 0.0.0.0 --metrics.port 8080 ``` Query metrics directly via HTTP: ```bash curl http://127.0.0.1:9100/metrics ``` Sample metrics output: ``` # HELP block_producer_l1_gas_processed_total The amount of L1 gas processed in a block # TYPE block_producer_l1_gas_processed_total counter block_producer_l1_gas_processed_total 2500000 # HELP process_resident_memory_bytes Resident memory size in bytes # TYPE process_resident_memory_bytes gauge process_resident_memory_bytes 45670400 ``` ##### Usage Example Enable metrics during testing to monitor performance: ```bash # Start Katana with metrics katana --dev --metrics --dev.no-fee # Monitor block production in another terminal watch -n 1 'curl -s http://127.0.0.1:9100/metrics | grep block_producer' ``` This provides real-time visibility into your local blockchain's performance characteristics during development and testing. #### Enabling explorer Enable the browser-based explorer: ```bash katana --explorer ``` This will enable the `/explorer` endpoint, which you can access from the browser to see transaction data and look up information about accounts and deployed contracts. ### Extended JSON-RPC Interface Katana provides a comprehensive RPC interface for development and interaction. RPC commands are organized across multiple namespaces: * **`starknet`**: Standard Starknet RPC methods for contract calls and queries * **`dev`**: Development utilities like manual block mining and time control * **`katana`**: Node-specific endpoints for configuration and account info * **`torii`**: ECS entity/component queries for Dojo integration ##### Usage Example Generate blocks on-demand when using `--no-mining` mode: ```bash curl -X POST http://127.0.0.1:5050 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"dev_generateBlock","params":[],"id":1}' ``` :::tip See the complete [RPC Reference](/toolchain/katana/reference#rpc-method-namespaces) for detailed method documentation and examples. ::: ![katana](/toolchain/katana-icon-word.png) ## Katana Katana is a **blazingly fast Starknet sequencer** built for onchain game developers and appchain builders, focusing on **rapid iteration** and **flexible deployment models**. :::info Unlike **miners** (who compete to solve puzzles) or **validators** (who verify blocks in consensus), a **sequencer** has singular authority over transaction ordering and block production. This enables fast finality and predictable performance, making it ideal for appchains and dev environments. ::: ### Architecture Overview Katana follows a **modular, layered architecture** with key components: **Backend**: Manages block processing and sequencer state coordination. **Block Producer**: Handles block creation with configurable mining strategies. **Executor Factory**: Creates Cairo executors using Starknet's **Blockifier**. **Storage Provider**: Abstracts database access using **Merkle Patricia Tries** via [Bonsai](https://github.com/dojoengine/bonsai-trie). **Transaction Pool**: Multi-stage validation pipeline from submission to block inclusion. **RPC Server**: Provides standard Starknet APIs plus dev-specific endpoints. **Block Explorer**: Browser-based block explorer for easily visualizing sequencer activity #### Why Katana? **Developer-First**: Tooling, flexible configuration, extensive APIs, and hot reloading. **State Forking**: Fork existing networks at any block for testing against live contracts. **Multi-Settlement**: Designed for Starknet settlement (L3 model) or sovereign operation. **Cairo Native**: Optional ahead-of-time compilation for significant performance gains. :::info Cairo Native compiles Cairo programs to native machine code via MLIR and LLVM, offering large performance improvements over vanilla VM interpretation. This feature must be enabled at compile time with the `native` feature flag and creates additional dependencies. ::: ### Getting Started :::note Katana requires glibc version 2.33 or higher. It is not available on Ubuntu 20.04 LTS, Debian 10 Buster, CentOS 7, or below. ::: #### Quick Start Start a local development sequencer with pre-funded accounts and instant mining: ```bash katana --dev --dev.no-fee ``` This launches Katana in development mode with: * An RPC server at `http://localhost:5050` * 10 pre-funded accounts * Instant block mining * Gas fees disabled #### Production Configuration For production appchain deployments, use persistent storage and custom configuration: ```bash katana --db-dir ./katana-db --config katana_prod.toml ``` Common production scenarios: ```bash # Fork from Starknet mainnet at block 1000000 katana --fork.provider https://api.cartridge.gg/x/starknet/mainnet --fork.block 1000000 # Custom mining interval (every 10 seconds) katana --block-time 10000 --db-dir ./katana-db ``` ### Installation Katana can be installed via [`dojoup`](/installation), our dedicated package manager: ```bash curl -L https://install.dojoengine.org | bash # Restart your terminal dojoup install ``` :::note This will install the `katana` binary at `~/.dojo/bin` ::: :::tip Dojoup automatically synchronizes compatible versions of Dojo, Katana, and Torii ::: #### Installing with `asdf` If you prefer to install with the `asdf` version manager: ```bash asdf plugin add katana https://github.com/dojoengine/asdf-katana.git asdf install katana latest ``` :::note This will install the `katana` binary at `~/.asdf/shims` ::: #### Building from Source If you prefer to build from source: ```bash git clone https://github.com/dojoengine/katana cargo install --path ./katana/bin/katana --locked --force ``` :::note This will install the `katana` binary at `~/.cargo/bin` ::: ### Next Steps * **[Development Features](/toolchain/katana/development)**: Explore mining, storage, forking, and contract deployment * **[Configuration Guide](/toolchain/katana/configuration)**: Learn about TOML configuration files and advanced options * **[CLI and RPC Reference](/toolchain/katana/reference)**: Complete reference for commands and API endpoints * **[Advanced Features](/toolchain/katana/advanced)**: Understand execution, messaging, and settlement ## JSON-RPC API Reference This page provides complete reference documentation for Katana's JSON-RPC API and supported transaction types. Katana provides a comprehensive RPC interface for interacting with your local blockchain. The RPC server runs on `http://127.0.0.1:5050` by default and supports both HTTP and WebSocket transports by default. ### RPC Method Namespaces The RPC methods are categorized into the following namespaces: | Namespace | Description | Use Case | | ----------------------------------- | ----------------------------- | ------------------------------------- | | [`starknet`](#starknet-namespace) | Standard Starknet RPC methods | Contract calls, transaction queries | | [`dev`](#dev-namespace) | Development utilities | Block mining, time control, debugging | | [`cartridge`](#cartridge-namespace) | Cartridge-specific methods | Paymaster and external execution | Each RPC method can be invoked by prefixing the method name with the namespace name and an underscore. For example, the `generateBlock` method in the `dev` namespace can be invoked as `dev_generateBlock`. :::note Torii provides its own separate gRPC server queries and does not integrate with Katana's JSON-RPC interface. See the [Torii documentation](/toolchain/torii) for Torii's API reference. ::: #### Common Development Workflow Here's a typical workflow using the dev namespace for testing: ```bash # 1. Get predeployed accounts curl -X POST http://127.0.0.1:5050 -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"dev_predeployedAccounts","params":[],"id":1}' # 2. Set specific timestamp for time-dependent testing curl -X POST http://127.0.0.1:5050 -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"dev_setNextBlockTimestamp","params":[1704067200],"id":2}' # 3. Generate block with transactions curl -X POST http://127.0.0.1:5050 -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"dev_generateBlock","params":[],"id":3}' ``` #### `starknet` Namespace Katana supports version **0.8.1** of the Starknet JSON-RPC specification. The full documentation for the RPC methods can be found [here](https://github.com/starkware-libs/starknet-specs). ##### Read API **Block and State Queries:** * `starknet_blockNumber` * `starknet_blockHashAndNumber` * `starknet_getBlockWithTxs` * `starknet_getBlockWithTxHashes` * `starknet_getBlockTransactionCount` * `starknet_getBlockWithReceipts` * `starknet_getStateUpdate` **Transaction Queries:** * `starknet_getTransactionByHash` * `starknet_getTransactionStatus` * `starknet_getTransactionReceipt` * `starknet_getTransactionByBlockIdAndIndex` **Contract and Storage:** * `starknet_call` * `starknet_getNonce` * `starknet_getStorageAt` * `starknet_getStorageProof` * `starknet_getClassHashAt` * `starknet_getClass` * `starknet_getClassAt` **Events and Messages:** * `starknet_getEvents` * `starknet_getMessagesStatus` **Network Status:** * `starknet_specVersion` * `starknet_chainId` * `starknet_syncing` **Fee Estimation:** * `starknet_estimateFee` * `starknet_estimateMessageFee` ##### Write API * `starknet_addInvokeTransaction` * `starknet_addDeclareTransaction` * `starknet_addDeployAccountTransaction` ##### Trace API * `starknet_traceTransaction` * `starknet_simulateTransactions` * `starknet_traceBlockTransactions` #### `dev` Namespace The `dev` API provides a way to manipulate the blockchain state at runtime. This namespace is only accessible when the `--dev` flag is enabled. ##### `dev_generateBlock` Mines a new block which includes all currently pending transactions. **Method invocation:** ```json { "jsonrpc": "2.0", "method": "dev_generateBlock", "params": [], "id": 1 } ``` ##### `dev_nextBlockTimestamp` Get the timestamp for the next block. **Method invocation:** ```json { "jsonrpc": "2.0", "method": "dev_nextBlockTimestamp", "params": [], "id": 1 } ``` ##### `dev_increaseNextBlockTimestamp` Increase the time for the block by a given amount of time, in seconds. **Method invocation:** ```json { "jsonrpc": "2.0", "method": "dev_increaseNextBlockTimestamp", "params": [300], "id": 1 } ``` ##### `dev_setNextBlockTimestamp` Similar to `dev_increaseNextBlockTimestamp` but takes the exact timestamp that you want in the next block. **Method invocation:** ```json { "jsonrpc": "2.0", "method": "dev_setNextBlockTimestamp", "params": [1703875200], "id": 1 } ``` ##### `dev_predeployedAccounts` Get information for all predeployed accounts. **Method invocation:** ```json { "jsonrpc": "2.0", "method": "dev_predeployedAccounts", "params": [], "id": 1 } ``` **Response:** ```json { "jsonrpc": "2.0", "result": [ { "address": "0x517eb...", "public_key": "0x1ef15...", "private_key": "0x1800000000300000180000000000030000000000003006001800006600" } ], "id": 1 } ``` ##### `dev_setStorageAt` Set storage value at a specific key for a contract address. **Method invocation:** ```json { "jsonrpc": "2.0", "method": "dev_setStorageAt", "params": ["0x1234...", "0x5678...", "0x9abc..."], "id": 1 } ``` **Parameter descriptions:** * `contract_address`: The contract address to modify * `key`: The storage key * `value`: The value to set #### `cartridge` Namespace Cartridge-specific methods for paymaster support and external execution in local development. :::info The `cartridge` namespace is only available when Katana is built with the `cartridge` feature enabled. ::: ##### `cartridge_addExecuteOutsideTransaction` Execute an outside transaction with paymaster support. **Method invocation:** ```json { "jsonrpc": "2.0", "method": "cartridge_addExecuteOutsideTransaction", "params": [ "0x1234...", { "caller": "0x5678...", "nonce": "0x1", "execute_after": 1234567890, "execute_before": 1234567900, "calls": [] }, ["0x9abc...", "0xdef0..."] ], "id": 1 } ``` **Parameter descriptions:** * `address`: Contract address to execute on * `outside_execution`: Outside execution object with caller, nonce, timing, and calls * `signature`: Array of signature components :::note This API is designed for local development with Cartridge controllers and is not intended for production use. ::: ### Supported Transaction Types Katana aims to follow the Starknet specifications as closely as possible, mimicking the features currently supported on mainnet. Katana currently supports the following Starknet transaction types: | Type | Version | Description | | ------------------- | ------- | --------------------------------------- | | **INVOKE** | 1, 3 | Execute functions on deployed contracts | | **DECLARE** | 1, 2, 3 | Declare contract classes | | **DEPLOY\_ACCOUNT** | 1, 3 | Deploy new account contracts | #### Transaction Versions **Version 1**: Legacy transaction format with lower gas efficiency. **Version 3**: Current transaction format with improved gas efficiency and fee estimation. Recommended for new development. **DECLARE Version 2**: Introduces Sierra compilation for improved contract verification. :::tip Use transaction version 3 for new development to benefit from improved gas efficiency and better fee estimation. ::: To learn more about the different transaction types, refer to the [Starknet documentation](https://docs.starknet.io/architecture/transactions/). ![saya](/toolchain/saya-icon-word.png) Saya is the settlement orchestrator for Katana, it is responsible for fetching the blocks from Katana and prove them. Once the block execution is proven, Saya can settle the block either by verifying it on a settlement layer and update the state of the core contract, or by posting the data to a data availability layer. ### Architecture ![saya](/toolchain/saya-overview.png) The core steps of Saya are: 1. Fetch the blocks from Katana 2. For each block, `StarknetOS` trace is generated. 3. `StarknetOS` trace is sent to [Herodotus proving service](https://herodotus.cloud) to get the proof. 4. The proof is then sent to the settlement layer for verification and the state of the core contract is updated and/or the compressed state diff is sent to the data availability layer. :::note Currently, Saya is not generalized, which means that each running Katana node needs to have its own Saya instance. This will change in the future, and Saya will be able to aggregate multiple Katana nodes blocks into a single instance. ::: ### Operating Modes Currently, Saya can operate in two modes: 1. [**Persistent mode**](/toolchain/saya/persistent): Saya will fetch the blocks from Katana and will verify the proof and update the state of the core contract on the settlement layer. :::info At the moment only [Starknet](https://starknet.io/) is supported as a settlement layer. ::: 2. [**Sovereign mode**](/toolchain/saya/sovereign): Saya will fetch the blocks from Katana and will post the proof and associated compressed state diff to a data availability layer. :::info At the moment only [Celestia](https://celestia.org/) is supported as a data availability layer. ::: ### Herodotus Saya relies on Herodotus to prove blocks. This service can handle trace generation and proof generation, by proxying the proof generations to [SHARP](https://docs.starknet.io/architecture-and-concepts/provers-overview/). You will need to create an account on the [Herodotus portal](https://herodotus.cloud) and provide the account API key in the `ATLANTIC_KEY` environment variable. :::tip If you are testing Saya for a Dojo project, contact us in the [Dojo Discord](https://discord.gg/dojoengine) for some credits. ::: ### Installation The source code of Saya is available on GitHub at [dojoengine/saya](https://github.com/dojoengine/saya). Saya is available as a binary for `linux` and `macOS` (both `amd64` and `arm64`). You can also run Saya from the [docker image](https://github.com/dojoengine/saya/pkgs/container/saya). ## Persistent mode In persistent mode, Saya fetches blocks from Katana, verifies proofs, and updates the core contract state on the settlement layer. ![saya](/toolchain/saya-persistent.png) :::tip For data availability without settlement layer integration, see [Sovereign mode](./sovereign). ::: :::info The core contract currently used on Starknet is [Piltover](https://github.com/keep-starknet-strange/piltover). Piltover acts as the settlement layer contract that receives and verifies state updates from your rollup, ensuring the integrity of state transitions on the base layer. ::: ### Setup Katana Katana must be configured in provable mode to work with Saya. First, initialize a new chain spec: ```bash katana init --id per1 \ --settlement-chain \ --settlement-account-address
\ --settlement-account-private-key ``` This automatically deploys a fresh core contract on the settlement layer. If you want to use a specific facts registry contract, set it via the `settlement-facts-registry` argument: ```bash katana init --id per1 \ --settlement-chain \ --settlement-account-address
\ --settlement-account-private-key --settlement-facts-registry
``` :::tip You can inspect the chain by running `katana config per1` ::: :::note The settlement core contract must receive configuration parameters on deployment. It's recommended to let Katana handle this. If the core contract is already deployed, you should provide it so Katana can verify the configuration parameters. ::: When working with Katana in provable mode, two additional parameters are required: 1. `block-time`: Since every block is proven, it is recommended to use a block time instead of the default mode where a block is mined for each transaction. This prevents overwhelming the prover with too many blocks and ensures consistent proving performance. 2. `block-max-cairo-steps`: In the current implementation of Katana, the default Cairo steps limit in a block is `50` million. For provable mode with Saya, it is recommended to use `16` million to ensure the proving step succeeds reliably. This limit exists due to Cairo VM constraints and proving complexity - larger blocks may fail to prove or timeout. ```bash katana --chain per1 \ --block-time 30000 \ --sequencing.block-max-cairo-steps 16000000 ``` :::note You can define an `--output-path` when working with Katana init to output the configuration files in the given directory. You will then want to start Katana with the `--chain /path` instead of `--chain `. ::: ### Run Saya If you haven't already, consult the [Herodotus guide](/toolchain/saya) to get an account and an API key. If you are not running Saya in [docker](https://github.com/dojoengine/saya/pkgs/container/saya), you can download the SNOS program and the Layout Bridge program from the [Saya releases](https://github.com/dojoengine/saya/releases). If you are running Saya in [docker](https://github.com/dojoengine/saya/pkgs/container/saya), the programs are already present in the `/programs` directory. :::tip To ease configuration, Saya can be run with environment variables (which can be overridden by command line arguments). ::: ```bash # .env.persistent # The number of blocks to process in parallel in Saya. BLOCKS_PROCESSED_IN_PARALLEL=60 # The database directory, to ensure long running queries are tracked # and not re-run if Saya is restarted. DB_DIR=/tmp/saya_persistent # The Atlantic key, obtained from https://herodotus.cloud. ATLANTIC_KEY= # The path to the compiled SNOS program to be run against each block. SNOS_PROGRAM=./programs/snos.json # The path to the compiled layout bridge program to be run against each block. LAYOUT_BRIDGE_PROGRAM=./programs/layout_bridge.json # In persistent mode, the rollup RPC to pull the blocks from. ROLLUP_RPC=http://0.0.0.0:5050 # Integrity verifier contract address. # https://github.com/HerodotusDev/integrity/blob/main/deployed_contracts.md SETTLEMENT_INTEGRITY_ADDRESS=0x04ce7851f00b6c3289674841fd7a1b96b6fd41ed1edc248faccd672c26371b8c # Settlement chain. SETTLEMENT_RPC=https://api.cartridge.gg/x/starknet/sepolia SETTLEMENT_PILTOVER_ADDRESS= SETTLEMENT_ACCOUNT_ADDRESS= SETTLEMENT_ACCOUNT_PRIVATE_KEY= ``` Export those variables in your shell by sourcing the file or running: ```bash export $(grep -v '^#' .env.persistent | xargs) ``` Then, you can start Saya with: ```bash saya persistent start ``` :::info To avoid double spending of Herodotus credits, Saya has an internal database to track the blocks that have been proven. The `DB_DIR` is important to ensure that the database is not lost when Saya is restarted if you have a long running Saya instance. ::: ## Sovereign mode In sovereign mode, Saya fetches blocks from Katana and posts the proof and associated compressed state diff to a data availability layer. There is no core contract on a settlement layer. ![saya](/toolchain/saya-celestia.png) :::tip For settlement layer integration with state updates, see [Persistent mode](/toolchain/saya/persistent). ::: Once available, this allows any Katana to sync from the data availability layer information given the `commitment` and the `block height` of the latest Katana block that has been posted to the data availability layer. Katana will then sync backwards to the genesis block. :::warning Currently, it is not possible to sync a Katana instance from a data availability layer. This functionality is planned and will be available soon. ::: ### Setup Celestia A Celestia node is required to post the blobs to the data availability layer, and the node must be running with a funded account. You can refer to the [official documentation for the instructions](https://docs.celestia.org/how-to-guides/light-node) to run a Celestia light node. In the Saya repository, there is also a `celestia.sh` script to help you set up a Celestia node using docker: #### 1. Initialize the node ```bash ./celestia.sh init ``` Take note of the Celestia account created and its address and also the auth token used to send requests to the node. A new docker volume named `celestia-light-mocha` will be created, and the key information will be stored there and mounted to the container when running the node. #### 2. Fund the account The account can be funded by sending a message to the `#mocha-faucet` channel on the [Celestia discord](https://discord.com/invite/YsnTPcSfWQ): ```bash # Send this message in the #mocha-faucet channel: $request ``` #### 3. Start the node for syncing ```bash ./celestia.sh ``` ### Setup Katana Katana must be configured in provable mode to work with Saya. You can choose to just enter `katana init` and follow the instructions to set up a new instance. Or you can go quicker by using the following arguments: ```bash katana init --id sov1 --sovereign ``` :::note You can inspect the chain spec by running `katana config sov1`. ::: When working with Katana in provable mode, two additional parameters are required: 1. `block-time`: Since every block is proven, it is recommended to use a block time instead of the default mode where a block is mined for each transaction. 2. `block-max-cairo-steps`: In the current implementation of Katana, the default Cairo steps limit in a block is `50` million. For provable mode with Saya, it is recommended to use `16` million to ensure the proving step succeeds reliably. ```bash katana --chain sov1 \ --block-time 30000 \ --sequencing.block-max-cairo-steps 16000000 ``` :::note You can define an `--output-path` when working with katana init to output the configuration files in the given directory. You will then want to start katana with the `--chain /path` instead of `--chain `. ::: ### Run Saya If you haven't already, consult the [Herodotus guide](/toolchain/saya) to get an account and an API key. If you are not running Saya in [docker](https://github.com/dojoengine/saya/pkgs/container/saya), you can download the SNOS program from the [Saya releases](https://github.com/dojoengine/saya/releases). If you are running Saya in [docker](https://github.com/dojoengine/saya/pkgs/container/saya), the programs are already present in the `/programs` directory. :::tip To ease configuration, Saya can be run with environment variables (which can be overridden by command line arguments). ::: ```bash # .env.sovereign # The number of blocks to process in parallel in Saya (required). BLOCKS_PROCESSED_IN_PARALLEL=60 # The database directory, to ensure long running queries are tracked # and not re-run if Saya is restarted. DB_DIR=/tmp/saya_sovereign # The Atlantic key, obtained from https://herodotus.cloud. ATLANTIC_KEY= # The path to the compiled SNOS program to be run against each block. SNOS_PROGRAM=./programs/snos.json # The Starknet RPC URL to fetch the blocks from. STARKNET_RPC=http://localhost:5050 # The first block to process. GENESIS_FIRST_BLOCK_NUMBER=0 # The Celestia RPC URL to fetch the blocks from. CELESTIA_RPC=http://localhost:26658 # Celestia defaults the key name to `my_celes_key` if not specified. # CELESTIA_KEY_NAME=my_celes_key # Default namespace will be sayaproofs, but can be overriden. # CELESTIA_NAMESPACE=sayaproofs # The Celestia token to post the proof. # When running a node with `scripts/celestia.sh`, you can find the token in the logs before the node starts. CELESTIA_TOKEN= ``` :::note `BLOCKS_PROCESSED_IN_PARALLEL` is **required** when configuring Saya in sovereign mode ::: Export those variables in your shell by sourcing the file or running: ```bash export $(grep -v '^#' .env.sovereign | xargs) ``` Then, you can start Saya with: ```bash saya sovereign start ``` :::info To avoid double spending of Herodotus credits, Saya has an internal database to track the blocks that have been proven. The `DB_DIR` is important to ensure that the database is not lost when Saya is restarted if you have a long running Saya instance. ::: ## Binding Generation Client bindings bridge the gap between your Cairo smart contracts and client applications. Sozo's `bindgen` generates type-safe, platform-specific code that enables native interaction with your Dojo world. ### Basic Workflow ```bash sozo build # Compile contracts sozo bindgen --binding-target typescript # Generate TypeScript bindings # Use generated bindings in your client app ``` Sozo provides a shorthand for compiling contracts and generating bindings in one command: ```bash sozo build --typescript # Compile contracts + generate bindings ``` Bindings are saved to the `bindings/` directory by default. **What gets generated:** * **Type-safe interfaces** for all your models and systems * **Serialization/deserialization** handling * **Contract interaction methods** with proper typing * **Integration helpers** for each platform :::tip For the architectural details of binding generation, see the [Cainome documentation](/toolchain/cainome). ::: ### Supported Platforms #### TypeScript Starting with your Cairo contracts, generate and use type-safe TypeScript definitions: **1. Your Cairo models:** ```cairo use starknet::ContractAddress; #[derive(Copy, Drop, Serde)] #[dojo::model] struct Position { #[key] player: ContractAddress, vec: Vec2, } #[derive(Copy, Drop, Serde)] #[dojo::model] struct Moves { #[key] player: ContractAddress, remaining: u8, last_direction: Direction, } #[derive(Copy, Drop, Serde)] enum Direction { None, Left, Right, Up, Down, } ``` **2. Generate TypeScript bindings:** ```bash sozo build --typescript ``` Bindings will be saved as `bindings/typescript/{contracts, models}.gen.ts`. **3. Use in your TypeScript application:** ```typescript // Import your Cairo types - now available as TypeScript interfaces import { Position, Moves, Direction } from "./bindings/typescript/models.ts"; import { Client, createClient } from "@dojoengine/torii-client"; const client = await createClient({ toriiUrl: "http://localhost:8080", worldAddress: worldAddress, }); // Query entities with full type safety const entities = await client.getEntities({ clause: { Keys: { keys: [playerId], pattern_matching: "FixedLen", models: ["Position", "Moves"], }, }, }); // Your Cairo models are now typed TypeScript objects const position = entities[0].models.find( (m) => m.name === "Position" ) as Position; const moves = entities[0].models.find((m) => m.name === "Moves") as Moves; // TypeScript knows about your Cairo enum values if (moves.last_direction.type === "Left") { console.log("Player moved left"); } ``` #### Unity Starting with the same Cairo contracts, generate C# classes for Unity: **1. Your Cairo models** (same as above) **2. Generate C# bindings:** ```bash sozo build --unity ``` **3. Use in Unity:** ```csharp using Dojo.Starknet; using UnityEngine; public class GameManager : MonoBehaviour { void Start() { // Your Cairo models are now available as C# classes var playerPosition = new Position { player = new FieldElement("0x123..."), x = 10, y = 5 }; // Use your Cairo types in Unity UpdatePlayerTransform(playerPosition); } void UpdatePlayerTransform(Position position) { // Type-safe usage of your Cairo structs transform.position = new Vector3( (float)position.x, // Cairo u32 -> C# uint -> float 0f, (float)position.y ); Debug.Log($"Player {position.player} moved to ({position.x}, {position.y})"); } } ``` :::note Ensure your generated bindings are in the `Assets/` folder of your Unity project. ::: #### Additional Targets * `typescript-v2` - Alternative TypeScript format * `unrealengine` - C++ bindings for Unreal Engine * `recs` - RECS framework integration ![sozo](/toolchain/sozo-icon-word.png) ## Sozo Sozo is the **comprehensive command-line interface** for Dojo world development, covering the entire lifecycle from project development to production deployment. It provides a unified interface that abstracts blockchain complexity while maintaining full control over your Dojo worlds. Sozo's key capabilities include **automatic world introspection** (discovering and working with any deployed world), **intelligent migration management** (reconciling local changes with deployed state), and **tag-based contract resolution** (using human-readable names instead of addresses). ### Core Functionality Sozo's functionality spans two primary domains: **Project Management**: Development lifecycle tools including project scaffolding, contract compilation, testing, and deployment to any Starknet-compatible network. **World Interaction**: Runtime operations for deployed worlds including system execution, data querying, permission management, and world state inspection. #### Resource Types Sozo manages five core resource types that form the building blocks of Dojo worlds: **Models**: Data structures that define the ECS components of your world state (e.g., `Position`, `Health`, `Inventory`). **Systems**: Smart contracts containing the game logic that operates on models (e.g., `MovementSystem`, `CombatSystem`). **Events**: Structured notifications emitted by systems to communicate state changes and enable efficient indexing. **Libraries**: Reusable Cairo code that can be shared across multiple systems within the world. **External Contracts**: Non-Dojo contracts that integrate with your world but exist outside the ECS framework. For more information about working with these resources, see the [Framework documentation](/framework). #### World Introspection Sozo can automatically discover and work with any deployed Dojo world through blockchain introspection, without requiring local build artifacts. **Automatic Discovery**: Given a world address, Sozo queries the World contract to discover all registered resources (models, systems, events, libraries) along with their metadata, ABIs, and permissions. **Universal Compatibility**: You can use Sozo to interact with worlds deployed by others, inspect unfamiliar world state, or recover from lost local artifacts by rebuilding complete world understanding from chain state. **Dynamic Schema Detection**: Sozo reconstructs model schemas, system interfaces, and event definitions from onchain registrations, enabling type-aware interactions with any world. #### Migration System Sozo's migration system automatically reconciles local world state with deployed blockchain state through precise diff analysis and ordered execution. **State Diffing**: `sozo migrate` compares local world configuration against deployed state by examining class hashes, resource registrations, and permissions to identify changes. **Ordered Execution**: Migration follows a strict sequence: 1. Sync namespaces (all resources are namespaced) 2. Declare new classes and register/upgrade resources (models, systems, events, libraries) 3. Apply permission changes (writers and owners) 4. Initialize newly deployed contracts **Transaction Optimization**: Operations are batched into multicall transactions to reduce gas costs and improve atomicity, with fallback to sequential execution. **Multi-Account Parallelization**: Class declarations can be parallelized across multiple accounts to speed up large world deployments. #### Manifest Integration Sozo automatically generates and maintains deployment manifests that eliminate manual address management. **Generated Manifests**: After each `sozo migrate`, Sozo writes a `manifest_{profile}.json` file containing complete deployment state: contract addresses, class hashes, ABIs, and metadata for all resources. **Tag-Based Contract Resolution**: Commands like `sozo execute` and `sozo call` accept human-readable contract tags (e.g., `Actions`, `dojo_examples-actions`) instead of raw addresses. Sozo resolves tags by consulting the local manifest first, then falling back to live chain introspection. **Fallback to Chain State**: When manifests are missing or `--diff` is used, Sozo rebuilds contract mappings by querying deployed world state directly. **Cross-Environment Consistency**: Each profile maintains its own manifest file, enabling seamless switching between local development, testnet, and mainnet deployments. ### Getting Started #### Quick Start Initialize a new Dojo project and deploy to local Katana: ```bash # Start `katana --dev --dev.no-fee` in a separate terminal sozo build && sozo migrate ``` ### Installation Sozo can be installed via [`dojoup`](/installation), our dedicated package manager: ```bash curl -L https://install.dojoengine.org | bash # Restart your terminal dojoup install ``` :::note This will install the `sozo` binary at `~/.dojo/bin` ::: :::tip Dojoup automatically synchronizes compatible versions of Dojo, Katana, and Torii ::: #### Installing with `asdf` If you prefer to install with the `asdf` version manager: ```bash asdf plugin add sozo https://github.com/dojoengine/asdf-sozo.git asdf install sozo latest ``` :::note This will install the `sozo` binary at `~/.asdf/shims` ::: #### Building from Source If you prefer to build from source: ```bash git clone https://github.com/dojoengine/dojo cargo install --path .dojo/bin/sozo --locked --force ``` :::note This will install the `sozo` binary at `~/.cargo/bin` ::: ### Data Format Reference When interacting with Dojo systems through Sozo, you'll need to provide calldata in the proper format. Sozo uses a prefixed format that allows explicit type specification for Cairo values. By default, calldata values are treated as a `felt252` or any type that fits into one felt: ```bash sozo execute Actions move 10 20 # Two felt252 values ``` For complex Cairo types, use these prefixes to ensure proper encoding: | Prefix | Description | Example | | ---------- | ---------------------------------------------------- | ------------------------------------ | | `u256` | a 256-bit unsigned integer | `u256:0x1234` | | `str` | a Cairo string (ByteArray) | `str:hello` or `str:'hello world'` | | `sstr` | a Cairo short string | `sstr:hello` or `sstr:'hello world'` | | `int` | a signed integer that fits into an `i128` | `int:-1234` | | `arr` | a dynamic array of values that fits into one felt | `arr:0x01,0x02,0x03` | | `u256arr` | a dynamic array of 256-bit unsigned integers | `u256arr:0x01,0x02,0x03` | | `farr` | a fixed-size array of values that fits into one felt | `farr:0x01,0x02,0x03` | | `u256farr` | a fixed-size array of 256-bit unsigned integers | `u256farr:0x01,0x02,0x03` | #### Usage Examples ```bash # Execute a system with mixed types sozo execute Actions create_player str:Alice u256:1000 arr:1,2,3 # Call a view function with complex parameters sozo call GameSystem get_player_stats str:Alice int:-50 ``` ### Next Steps * **[Project Management](/toolchain/sozo/project-management)**: Learn the development workflow from init to deploy * **[World Interaction](/toolchain/sozo/world-interaction)**: Master runtime operations for deployed worlds ## Project Management Sozo provides a complete development lifecycle for Dojo projects, from initial scaffolding to production deployment. This page covers the essential workflows and commands that take you from idea to deployed world. :::tip For detailed command options, use the built-in `sozo --help` and `sozo --help` ::: ### Development Workflow Building a Dojo project follows a predictable cycle that Sozo streamlines with intelligent automation: 1. **Project Setup**: Initialize structure and configure accounts 2. **Development Cycle**: Write, build, and test your contracts iteratively 3. **Deployment**: Deploy to local/remote networks with automatic state management 4. **Updates**: Iterate with automatic diff detection and minimal migrations ##### From Zero to Deployed ```bash # 1. Create and set up new project sozo init my-game cd my-game # 2. Set up development account (using starkli or other account tool) # Note: Account management is handled separately from Sozo # 3. Development cycle sozo build # Compile contracts sozo test # Run tests sozo clean # Clean artifacts if needed # 4. Deploy to local Katana katana --dev --dev.no-fee & # Start local sequencer sozo migrate # Deploy world and all resources # 5. Make changes and update # Edit your contracts... sozo build && sozo test # Rebuild and test sozo migrate # Automatic diff and deploy only changes # 6. Deploy to other environments sozo migrate --profile staging # Deploy to testnet sozo migrate --profile prod # Deploy to mainnet ``` ##### Key Concepts **Automatic State Management**: Sozo tracks your world's state across deployments, automatically detecting changes and generating minimal migration transactions. **Profile-Based Configuration**: Manage multiple environments (dev, staging, prod) with separate configuration files, allowing seamless deployment across networks. **Incremental Development**: The build-test-migrate cycle is optimized for rapid iteration, with Sozo handling the complexity of blockchain state management. ### Project Setup #### `sozo init` Create a new Dojo project with the standard structure and example code: ```bash sozo init my-game # Create project sozo init my-game --git # Create project + initialize git repo # Use a custom template sozo init my-game --template https://github.com/myorg/custom-dojo-template sozo init my-game --template myorg/custom-dojo-template # Short form ``` :::note By default, this clones the [dojo-starter](https://github.com/dojoengine/dojo-starter) template, giving you a working foundation with: * Example models (Position, Moves) and systems (Actions) * Pre-configured Scarb.toml with Dojo dependencies * Basic tests and deployment configuration ::: **Account Management**: Sozo uses accounts defined in your `dojo_.toml` files. See the [configuration guide](/framework/configuration) for more information. :::tip Use a tool like [starkli](https://book.starkli.rs/accounts) to create and manage accounts and keystores. ::: ### Development Cycle The core development loop involves building, testing, and iterating on your contracts: #### `sozo build` Compile your Cairo contracts and generate deployment artifacts: ```bash sozo build # Compile all contracts sozo build --unity # Compile + generate Unity bindings sozo build --typescript # Compile + generate TypeScript bindings ``` **What it does:** * Compiles Cairo contracts using Scarb * Validates contract syntax and imports * Generates Dojo manifests for deployment * (Optional) Creates language bindings for client integration #### `sozo test` Run your Cairo tests to verify contract behavior: ```bash sozo test # Run all tests ``` Tests are Cairo functions marked with `#[test]`. Sozo runs them using the Cairo test runner, giving you confidence in your logic before deployment. See the [testing guide](/framework/testing) for more information about testing your Dojo contracts. #### `sozo clean` Remove stale build files when needed: ```bash sozo clean # Clean current profile manifest files sozo clean --all-profiles # Clean all profile manifest files ``` **When to clean:** * After major contract restructuring * When build artifacts seem corrupted * Before important deployments to ensure fresh state **What gets cleaned:** * Manifest files for the current profile (or all profiles with `--all-profiles`) * Generated metadata and artifacts * Does NOT clean Scarb's `target/` directory (use `scarb clean` for that) #### `sozo bindgen` Create client bindings for various platforms after building: ```bash sozo bindgen # Generate all configured bindings sozo bindgen --typescript # Generate TypeScript bindings only sozo bindgen --unity # Generate Unity bindings only ``` Bindgen reads from your build artifacts to create platform-specific client code for interacting with your world. #### `sozo dev` Automatically rebuild and migrate your world when files change during development: ```bash sozo dev # Watch files and auto-rebuild sozo dev --unity # Watch + generate Unity bindings sozo dev --typescript # Watch + generate TypeScript bindings # With contract verification sozo dev --verify voyager # Auto-verify on Voyager after each rebuild ``` This starts a file watcher that automatically runs `sozo build` and `sozo migrate` whenever you modify Cairo contracts. Perfect for rapid development iteration without manual rebuilds. **Key Features:** * **File watching**: Monitors Cairo source files for changes * **Automatic rebuild**: Runs build + migrate when files change * **Binding generation**: Can generate client bindings on each rebuild * **Hot reload**: Instant feedback loop for development **Common Use Cases:** * Development with live-reloading game clients * Rapid contract iteration and testing * Continuous integration during development * Client binding updates alongside contract changes ### Deployment and Updates Sozo's migration system is the heart of Dojo deployment, automatically handling the complex process of deploying and updating worlds. ![Dojo Sozo Workflow](/toolchain/dojo-sozo-workflow.jpg) #### `sozo migrate` Deploy your world for the first time or update existing deployments: ```bash # First deployment to local Katana katana --dev --dev.no-fee & # Start local sequencer sozo migrate # Deploy world and all resources # Deploy to testnet (requires profile configuration) sozo migrate --profile staging # Deploy to mainnet sozo migrate --profile prod # If you need more information about the migration, you have 3 levels of verbosity: # Verbose mode, with additional info. sozo migrate -v # Debug mode, with even more info. sozo migrate -vv # Trace mode, with the most info + technical details. sozo migrate -vvv ``` :::tip You can use `-P` instead of `--profile` for simplicity. ::: The migration outputs a manifest file, which is a JSON file that contains information about deployed contracts and resources. By default, the manifest will contains all the ABIs merged in only one entry "abis", where Sozo deduplicates the ABIs and makes the manifest easier to read/track diffs (this is called the `all_in_one` format). There is a second per contract (`per_contract` format), where the ABIs are separated by contract. :::warning The `per_contract` format is currently required in one specific case: if you are using `dojo.js` and if you have the same function name in multiple contracts. You can toggle the abi format mode by using `--manifest-abi-format all_in_one/per_contract` directly from the command line when running the `sozo migrate` command. ```bash sozo migrate --manifest-abi-format per_contract ``` You can also set the default abi format mode in your `dojo_.toml` file by setting the `manifest_abi_format` key to `all_in_one` or `per_contract`. ```toml [migration] manifest_abi_format = "per_contract" ``` ::: :::info Since the 0.14 network upgrade, the PRE-CONFIRMED state may be lagging on nodes with load-balanced infrastructure. By default, Sozo is now using `ACCEPTED_ON_L2` as the default transaction finality status. You may want to speed up the migration process by using `PRE_CONFIRMED` instead, but this may come to the of the migration failing and having to be restarted. ::: ##### How Migration Works **Automatic Diff Analysis**: Sozo compares your local world state against the deployed state, identifying exactly what has changed: * New or modified models, systems, events, libraries * Updated class hashes and resource registrations * Permission changes (writers and owners) **Intelligent Updates**: Instead of redeploying everything, Sozo generates minimal migration transactions: * Declares only new or changed classes * Updates only modified resources * Applies permission changes incrementally * Initializes new contracts as needed **Multi-Environment Management**: Each profile tracks its own deployment state: ```bash sozo migrate # Uses 'dev' profile (local Katana) sozo migrate --profile staging # Uses 'staging' profile (testnet) sozo migrate --profile prod # Uses 'prod' profile (mainnet) ``` For subsequent deployments, record the world address in your profile: ```toml [env] world_address = "0x06171ed98331e849d6084bf2b3e3186a7ddf35574dd68cab4691053ee8ab69d7" rpc_url = "https://starknet-sepolia.public.blastapi.io/rpc/v0_7" account_address = "0x..." private_key = "0x..." ``` This tells Sozo to update the existing world rather than deploy a new one. :::tip Make sure to set the `world_address` value after your initial deployment. This will ensure that future migrations are made to your existing world. ::: ##### Local Development ```bash # Standard local development flow katana --dev --dev.no-fee & # Start local sequencer sozo build && sozo test # Build and test sozo migrate # Deploy to local Katana # Test your world... # Update your contracts... sozo migrate # Deploy any changes (automatic diff) ``` ##### Remote Deployment ```bash # Final validation before production sozo build && sozo test sozo clean && sozo build # Clean build for production # Deploy to mainnet sozo migrate --profile prod ``` ### Development Utilities #### `sozo hash` Debug and verify resource deployments: ```bash # Compute hash for a resource sozo hash compute Position # Get hash for Position model sozo hash compute Actions # Get hash for Actions system # Find resource by hash sozo hash find 0x1234... --namespaces ns --resources Position,Actions ``` #### `sozo walnut` Verify your contracts on Walnut for debugging: ```bash sozo walnut verify # Verify all contracts on Walnut ``` This uploads your contract source and ABIs to [Walnut](https://walnut.dev) for debugging and inspection. For more details, refer to [Walnut documentation](https://docs.walnut.dev/overview/debug-dojo-with-walnut). :::note The `walnut` command requires Sozo to be built with the feature enabled. If you see "unrecognized subcommand," rebuild from source using `cargo build --features walnut`. ::: ### Project Structure Scarb, which is used to build and test your project under the hood, supports [workspaces](https://docs.swmansion.com/scarb/docs/reference/workspaces.html#workspaces). Sozo also supports workspaces. However, Sozo requires two additional things: 1. A main package from which Sozo will extract the package's name (for binding generation and migration) 2. [Dojo configuration files](/framework/configuration/#configuration-files) to inject deployment settings during migration From here, you have several options for laying out your project: #### Single package The simplest way to lay out your project is to have a single package where all the contracts and libraries are placed. ``` ├── my-project/ │ ├── dojo_dev.toml │ ├── dojo_sepolia.toml │ ├── Scarb.toml │ └── src │ ├── lib.cairo │ ├── models.cairo │ ├── systems.cairo ``` With this setup, running `sozo build`, `sozo test`, and `sozo migrate` will work as expected at the root of the project. :::note If in your project you have other folders (not related to Cairo), opening the project at the root is currently not supported by the Cairo language server. You must have a root `Scarb.toml` file. This issue is being worked on by the Scarb team. In the meantime, you can open the project in your contracts directory, or you can add a virtual workspace to your project as shown in the next section. ::: #### Multi-package If you want to split your project into multiple packages, you can do so by creating a `packages` directory and placing your packages inside it. In the example below, only the `my-world` package is deployed onchain with a Dojo world. The configuration files are placed inside the `my-world` package. ``` ├── my-project/ │ ├── Scarb.toml │ └── packages │ ├── my-world/ │ │ ├── dojo_dev.toml │ │ ├── dojo_sepolia.toml │ │ ├── Scarb.toml │ │ ├── src │ │ │ ├── lib.cairo │ │ │ ├── models.cairo │ │ │ ├── systems.cairo │ │ │ └── tests.cairo │ ├── package2/ │ │ ├── Scarb.toml │ │ ├── lib.cairo │ └── package3/ │ ├── Scarb.toml │ ├── lib.cairo ``` To make this layout work, you will need the root `Scarb.toml` to be a [virtual workspace](https://docs.swmansion.com/scarb/docs/reference/workspaces.html#virtual-workspace) to ease dependency management. You will be able to run `sozo test` at the workspace level. However, since the Dojo configuration files are placed inside the `my-world` package, you will need to run `sozo build` and `sozo migrate` at the package level. This will generate the `target` directory and the `manifest_.json` file **at the package level**. If you prefer managing everything from the workspace level, you can simply move the Dojo configuration files to the workspace level. ``` ├── my-project/ │ ├── dojo_dev.toml │ ├── dojo_sepolia.toml │ ├── Scarb.toml │ └── packages │ ├── my-world/ │ │ ├── Scarb.toml │ │ ├── src │ │ │ ├── lib.cairo │ │ │ ├── models.cairo │ │ │ ├── systems.cairo │ │ │ └── tests.cairo │ ├── package2/ │ │ ├── Scarb.toml │ │ ├── lib.cairo │ └── package3/ │ ├── Scarb.toml │ ├── lib.cairo ``` With this setup, the `target` directory and the `manifest_.json` file will be generated at the workspace level. Therefore, you will need to run `sozo build`, `sozo test`, and `sozo migrate` at the workspace level. ##### Example layout When you want to ship both Cairo libraries and a Dojo world to be deployed onchain, one way to lay out the project is by creating a `contracts` or `world` package with the name of your project as the package name in its `Scarb.toml` and library packages. :::note This layout is not mandatory—it is an example of how to lay out your project. Using a virtual workspace helps manage dependencies between packages. ::: ``` ├── my-project/ │ ├── dojo_dev.toml │ ├── dojo_sepolia.toml │ ├── Scarb.toml │ ├── contracts │ │ ├── Scarb.toml │ │ ├── src │ │ │ ├── lib.cairo │ │ │ ├── models.cairo │ │ │ ├── systems.cairo │ │ │ └── tests.cairo │ └── packages │ ├── package1/ │ │ ├── Scarb.toml │ │ ├── src │ │ │ ├── lib.cairo │ └── package2/ │ ├── Scarb.toml │ ├── src │ │ ├── lib.cairo │ │ ├── models.cairo ``` :::warning If you want to manage a single repository with multiple worlds to be deployed, you will not be able to have all of them in the same workspace. For such a setup, it is recommended to keep them as independent directories and manage them as separate packages by running `sozo build` and `sozo migrate` at the package level. ``` ├── my-project/ │ ├── world1/ │ │ ├── Scarb.toml │ │ ├── dojo_dev.toml │ │ ├── dojo_sepolia.toml │ │ ├── src │ │ │ ├── lib.cairo │ │ │ ├── models.cairo │ │ │ ├── systems.cairo │ │ │ └── tests.cairo │ └── world2/ │ ├── Scarb.toml │ ├── dojo_dev.toml │ ├── dojo_sepolia.toml │ ├── src │ │ ├── lib.cairo │ │ ├── models.cairo │ │ ├── systems.cairo │ │ └── tests.cairo ``` Sozo will output an error if you have multiple worlds in the same workspace. ::: ## World Interaction Once your world is deployed, these commands let you interact with it during development, testing, and runtime operations. This page covers executing game logic, querying world state, managing permissions, and debugging your deployed worlds. :::tip For detailed command options, use the built-in `sozo --help` and `sozo --help` ::: ### Interacting with a World ```bash # Execute game actions (sends transactions) sozo execute ns-Actions spawn # Create a new player sozo execute ns-Actions attack str:goblin # Attack a goblin # Query game state (read-only) sozo model get ns-Health 0x123... # Check player health sozo call ns-GameView get_leaderboard # Get top players ``` ### System Execution #### `sozo execute` Execute system functions that modify world state. These operations send transactions and cost gas. ```bash # Separate system calls sozo execute ns-Actions spawn # Call spawn() on Actions system sozo execute ns-Actions move 10 5 # Call move(10, 5) sozo execute ns-Shop buy_item str:sword u256:100 # Buy sword for 100 gold # Batch multiple calls (separated by /) sozo execute ns-Actions spawn / ns-Actions move 5 3 / ns-Actions open_chest ``` **Key Features:** * **Transaction-based**: Requires account/signer configuration * **Tag resolution**: Use contract tags (e.g., `Actions`) or addresses * **Multicall support**: Batch multiple system calls efficiently * **Type-aware calldata**: Supports Dojo's calldata format **Common Use Cases:** * Player actions (move, attack, trade) * Game state changes (spawn entities, update scores) * Administrative actions (set permissions, update config) ### Data Querying #### `sozo call ` Call view functions and read-only system methods. These operations do not send transactions or require gas. ```bash # Query game state sozo call ns-GameView get_player_stats 0x123... # Get player stats sozo call ns-MapSystem get_tile_info 5 10 # Get tile at (5,10) sozo call ns-Leaderboard get_top_players # Get leaderboard # Historical queries (specify block) sozo call ns-GameView get_player_stats 0x123... --block-id 12345 ``` **Key Features:** * **Read-only**: No transactions, no gas costs, no account required * **Return values**: Get data back from function calls * **Historical queries**: Query state at specific blocks * **Instant results**: No waiting for transaction confirmation **Common Use Cases:** * Query player stats, positions, inventory * Check game state before making moves * Validate game logic during development * Build dashboards and analytics #### `sozo model` Query model data and inspect model schemas. Models store your game's ECS component data. ```bash # Query entity data sozo model get ns-Position 0x123... # Get Position for entity 0x123... sozo model get ns-Health 0x123... 0x456... # Get Health for entity with composite key sozo model get ns-Inventory 0x123... # Get player's inventory # Inspect model structure sozo model schema ns-Position # See Position model fields sozo model schema ns-Health # See Health model structure sozo model class-hash ns-Position # Get Position class hash sozo model contract-address ns-Position # Get Position contract address ``` **Key Features:** * **Entity queries**: Get model data for specific entities using their keys * **Schema inspection**: Understand model structure and field types * **Composite keys**: Support for models with multiple key fields * **Contract metadata**: Access class hashes and addresses **Common Use Cases:** * Debug game state during development * Query player data (position, health, inventory) * Inspect model schemas when integrating clients * Verify entity state after system calls #### `sozo events ` Query world events to track game activity and debug system behavior. ```bash # Get recent events sozo events --chunk-size 10 # Get latest 10 events sozo events ns-PlayerMoved --chunk-size 5 # Get latest 5 PlayerMoved events sozo events ns-PlayerMoved ns-GameEnd --chunk-size 20 # Get PlayerMoved and GameEnd events # Filter by block range sozo events --from-block 1000 --to-block 2000 --chunk-size 50 sozo events ns-PlayerAttack --from-block 1500 --chunk-size 10 # Export to JSON for analysis sozo events --chunk-size 100 --json > game_events.json ``` **Key Features:** * **Event filtering**: Query specific event types or all events * **Block range queries**: Get events from specific time periods * **Pagination**: Handle large event sets with chunk-size * **JSON export**: Export events for analysis or integration **Common Use Cases:** * Debug system execution and event emission * Track player activity and game metrics * Build event-driven analytics * Monitor world activity during development ### Permissions Management #### `sozo auth ` Manage access control for your world resources. Control who can modify models and own contracts. ```bash # Grant system write access to models sozo auth grant writer ns-Position,0x123... # Let contract 0x123... write to Position sozo auth grant writer ns-Health,0x456... # Let contract 0x456... write to Health sozo auth grant writer ns-Inventory,0x789... # Let contract 0x789... write to Inventory # Grant ownership of resources sozo auth grant owner ns-Position,0xabc... # Give ownership of Position model sozo auth grant owner ns-Actions,0xdef... # Give ownership of Actions contract # Revoke permissions sozo auth revoke writer ns-Position,0x123... # Remove write access sozo auth revoke owner ns-Position,0xabc... # Remove ownership # Batch permission changes (more efficient) sozo auth grant writer ns-Position,0x123... ns-Health,0x123... ns-Inventory,0x123... ``` **Permission Types:** * **Writer**: Can modify model data (required for systems that update game state) * **Owner**: Can modify resource permissions and upgrade contracts **Key Features:** * **Multicall batching**: Multiple permission changes in one transaction * **Resource targeting**: Models, contracts, and namespaces * **Flexible syntax**: Use contract tags or addresses **Common Use Cases:** * Grant systems write access to models they need to update * Transfer ownership for contract upgrades * Set up multi-system architectures with proper permissions * Delegate administrative control to other accounts #### `sozo register ` Register new models to your world after they have been declared but not yet registered. ```bash # Register a model by class hash sozo register model 0x76490... # Register multiple models sozo register model 0x123... 0x456... 0x789... ``` **When to Use:** * After declaring a new model class that was not included in the original world deployment * When adding models from external libraries to your world * During development when iterating on model schemas :::note Most model registration happens automatically during `sozo migrate`. Manual registration is typically only needed for external models or advanced deployment scenarios. ::: ### Development & Debugging #### `sozo inspect` Inspect deployed world configuration, resources, and permissions. ```bash sozo inspect # Inspect current world sozo inspect --world 0x123... # Inspect specific world address ``` **What it shows:** * Registered models, systems, events, and libraries * Resource permissions and ownership * World metadata and configuration * Class hashes and contract addresses **Common Use Cases:** * Debug permission issues * Understand deployed world structure * Verify resource registration * Audit world state and permissions #### `sozo version` Display Sozo version information: ```bash sozo version # Show version details ``` #### `sozo mcp` Start an MCP (Model Context Protocol) server for development tooling: ```bash sozo mcp # Start MCP server ``` This enables enhanced IDE integration and development tool support for Dojo projects. ## Configuration Guide Torii supports TOML configuration files for complex deployments. Configuration provides structured control over indexing, performance, security, and monitoring settings. ### Configuration Priority Configuration priority follows a standard hierarchy: 1. **Command-line arguments** (highest) 2. **Configuration file** (via `--config`) 3. **Environment variables** 4. **Default values** (lowest) ### CLI Usage Most configuration options can be passed as command-line arguments: ```bash # Basic usage torii --world 0x1234... # Configuration options via CLI torii --world 0x1234... \ --http.cors_origins "*" \ --indexing.controllers \ --indexing.polling_interval 1000 # Using configuration file (recommended for complex setups) torii --config torii_prod.toml # See command-line options torii --help ``` :::info Use `torii --help` for a full command reference. ::: For complex deployments, using a configuration file is recommended over lengthy command lines. ### TOML Configuration #### Basic Configuration Basic settings for getting Torii running: ```toml world_address = "0x01b2e..." # World contract to index (optional as of Torii 1.6.1). Can also be passed in the list of contracts. rpc = "http://0.0.0.0:5050" # Sequencer RPC endpoint (default: http://0.0.0.0:5050) db_dir = "torii.db" # Persistent database (omit for in-memory) ``` :::info AS of Torii 1.6.0, the World address is no longer required, and Torii will sync events from all contracts passed to `--indexing.contracts`. Under the hood, a value passed to `world` is simply appended to the `contracts` array. ::: #### Runner Configuration Development and runtime options: ```toml [runner] explorer = false # Open World Explorer in browser (default: false) check_contracts = false # Verify contracts before starting (default: false) ``` #### Indexing Configuration Control what data to index and how: ```toml [indexing] # Contracts to index contracts = [ "WORLD:0xab931...", "WORLD:0xc34da...", "ERC20:0x023f8e...", "ERC721:0xfe8a1...", ] # Content selection pending = false # Include pending transactions transactions = true # Store transaction data namespaces = ["game", "market"] # Specific namespaces only (empty = all) models = ["Position", "Move"] # Specific models only (empty = all) world_block = 0 # Starting block number (default: 0) # Performance tuning events_chunk_size = 1024 # Events per RPC request (default: 1024) blocks_chunk_size = 10240 # Blocks per DB commit (default: 10240) polling_interval = 500 # Check interval in ms (default: 500) max_concurrent_tasks = 100 # Parallel processing (default: 100) # Advanced options controllers = true # Index Cartridge controllers (default: false) strict_model_reader = false # Read models from registration block (default: false) batch_chunk_size = 1024 # Batch request chunk size (default: 1024) [events] raw = false # Store raw blockchain events (dev only) ``` :::tip Most Dojo development will set `controllers = true` ::: #### ERC Configuration Token indexing settings: ```toml [erc] max_metadata_tasks = 100 # Concurrent metadata tasks (default: 100) # Optional: ERC artifacts storage artifacts_path = "/path/to/artifacts" ``` #### SQL Configuration SQLite performance and indexing settings: ```toml [sql] page_size = 32768 # Page size in bytes (default: 32768, range: 512-65536) cache_size = -500000 # Cache size: negative = KiB, positive = pages (default: -500MB) all_model_indices = false # Auto-create all indices (resource intensive) # Database performance tuning wal_autocheckpoint = 10000 # Pages interval for autocheckpoint (default: 10000) busy_timeout = 60000 # Database busy timeout in ms (default: 60000) # Historical data retention historical = ["game-Battle", "market-Trade"] # SQL hooks for events (format: "event:data:statement") hooks = [] # Optional: Custom migrations directory migrations = "/path/to/migrations" # Custom indices for specific model queries # Spatial queries [[sql.model_indices]] model_tag = "game-Position" fields = ["external_x", "external_y"] # Leaderboards [[sql.model_indices]] model_tag = "game-Player" fields = ["external_score", "external_level"] ``` #### Snapshot Configuration Snapshot loading options: ```toml [snapshot] # Optional: Snapshot URL and version url = "https://example.com/snapshot.tar.gz" version = "1.0.0" ``` #### Metrics Configuration Prometheus metrics and observability. If enabled, metrics will be served at the `/metrics` endpoint. ```toml [metrics] metrics = true # Enable /metrics endpoint metrics_addr = "127.0.0.1" # Metrics server address metrics_port = 9200 # Metrics port ``` :::info [Prometheus](https://prometheus.io/) is an open-source monitoring and alerting system that collects metrics from applications and stores them in a time-series database. When enabled, Torii exposes metrics like indexing performance, database query times, and system resource usage at the `/metrics` endpoint. ::: :::tip When deploying Torii with [Slot](https://docs.cartridge.gg/slot), monitoring is enabled by default. ::: #### Server Configuration HTTP API and network settings: ```toml [server] http_addr = "127.0.0.1" # Listen address (default: 127.0.0.1) http_port = 8080 # API port http_cors_origins = [ "*" ] # CORS allowed origins # Optional: HTTPS with TLS certificates tls_cert_path = "/etc/ssl/torii.crt" tls_key_path = "/etc/ssl/torii.key" ``` #### P2P Relay Configuration Multi-region synchronization and relay: ```toml [relay] port = 9090 # TCP/UDP QUIC port webrtc_port = 9091 # WebRTC port websocket_port = 9092 # WebSocket port peers = [] # List of peer relay addresses # Identity and certificates local_key_path = "/etc/torii/identity.key" cert_path = "/etc/torii/webrtc.cert" ``` :::info P2P relay enables Torii instances to communicate directly with each other for data synchronization across regions or networks. This is useful for distributed deployments where multiple Torii instances need to share indexed data or coordinate processing without going through centralized infrastructure. ::: Timestamp validation for P2P messaging and cross-chain communication: ```toml [messaging] max_age = 300 # Maximum age in seconds for valid timestamps (default: 300) future_tolerance = 60 # Maximum seconds in future for timestamps (default: 60) require_timestamp = false # Whether timestamps required in all messages (default: false) ``` #### gRPC Configuration Settings for gRPC API and subscriptions: ```toml [grpc] subscription_buffer_size = 256 # Subscription channel buffer size (default: 256) optimistic = false # Broadcast optimistic updates (default: false) tcp_keepalive_interval = 60 # TCP keepalive interval in seconds (default: 60) http2_keepalive_interval = 30 # HTTP/2 keepalive interval in seconds (default: 30) http2_keepalive_timeout = 10 # HTTP/2 keepalive timeout in seconds (default: 10) ``` ### Configuration Examples #### Development Fast iteration with local Katana: ```toml world_address = "0x1234..." rpc = "http://0.0.0.0:5050" # No db_dir = in-memory database [indexing] polling_interval = 100 # Fast updates (vs default 500ms) pending = true # Include pending txs [server] http_cors_origins = ["*"] # Allow all origins ``` #### Production Optimized for stability and performance: ```toml world_address = "0x9abc..." rpc = "https://api.cartridge.gg/x/starknet/mainnet" db_dir = "/var/lib/torii/production" [indexing] polling_interval = 1000 # Conservative polling (vs default 500ms) max_concurrent_tasks = 200 # Scale for load (vs default 100) pending = false # Stability over speed [sql] cache_size = -2000000 # 2GB cache in KiB (vs default -500MB) page_size = 65536 # Large pages (vs default 32768) [metrics] metrics = true # Essential monitoring ``` ### Best Practices **Performance**: * Use persistent storage (`db_dir`) in production * Tune chunk sizes based on RPC performance * Enable indices only for frequently queried fields * Monitor memory usage with high concurrency **Security**: * Bind to `127.0.0.1` for local-only access * Use specific CORS origins in production * Enable TLS for external-facing deployments * Secure P2P relay certificates **Monitoring**: * Always enable metrics in production * Set up Prometheus scraping and alerting * Monitor database growth and query performance * Track indexing lag and error rates ## GraphQL API Torii's GraphQL API provides type-safe access to your indexed Dojo world data. The schema is dynamically generated from your world's models, offering both flexible queries and real-time subscriptions. ### Quick Start Start Torii and access the GraphQL endpoint: ```bash torii --world ``` **Endpoints:** * GraphQL API: `http://localhost:8080/graphql` * GraphiQL IDE: `http://localhost:8080/graphql` (in browser) ### Core Features * **Dynamic Schema**: Auto-generated from your world's model definitions * **Real-time Subscriptions**: WebSocket-based live updates * **Flexible Pagination**: Cursor-based and offset/limit support * **Type Safety**: Fully typed with introspection support ### Schema Structure Torii generates two types of queries: **Generic Queries:** * `entities` - Access all entities with flexible filtering * `models` - Retrieve model definitions and metadata * `transactions` - Query indexed transaction data **Model-Specific Queries:** * `{modelName}Models` - Custom queries for each registered model * Example: `positionModels`, `movesModels`, `playerModels` Model-specific queries provide optimized filtering, sorting, and type-safe field access. ### Basic Queries #### Model Metadata Query model information using the model selector: ```graphql query { model(id: "0x28b9a...") { id name classHash } } ``` This query will return an output like this: ```json { "data": { "model": { "id": "0x28b9a...", "name": "Position", "classHash": "0x2e9c4..." } } } ``` :::tip Use [stark-utils](https://www.stark-utils.xyz/) or `starkli` to compute model selectors. ::: :::info You can find information about your schema definitions in the **Documentation Explorer** section of the GraphQL IDE. ::: #### Model Data Query specific model data with automatic pagination: ```graphql query { movesModels { edges { node { player remaining last_direction } } } } ``` This query will return an output like this: ```json { "data": { "movesModels": { "edges": [ { "node": { "player": "0xb3ff4...", "remaining": 100, "last_direction": "None" } } ] } } } ``` #### Transactions Query world transactions with pagination: ```graphql query { transactions { edges { node { id transactionHash senderAddress } } totalCount } } ``` This query will return an output like this: ```json { "data": { "transactions": { "edges": [ { "node": { "id": "0x00000...:0x4b264...", "transactionHash": "0x4b264...", "senderAddress": "0xb3ff4...", "calldata": [ "0x1", "0x7ec42...", "0x217c7...", "0x0" ] } } ], "totalCount": 5 } } } ``` ### Pagination Torii supports both cursor-based and offset/limit pagination using GraphQL [Connection](https://relay.dev/graphql/connections.htm#sec-Connection-Types) types. #### Cursor-Based Pagination Recommended for performance. Use `first`/`after` for forward pagination, `last`/`before` for backward: ```graphql query { entities(first: 10) { edges { cursor node { id } } pageInfo { hasNextPage endCursor } } } ``` This query will return an output like this, with two results out of five: ```json { "entities": { "totalCount": 5, "edges": [ { "cursor": "Y3Vyc29yX29uZQ==", "node": { "id": "0x54f58..." } }, { "cursor": "Y3Vyc29yX3R3bw==", "node": { "id": "0x2c2ed..." } } ] } } ``` Next page, using the cursor from the previous query: ```graphql query { entities(first: 3, after: "Y3Vyc29yX3R3bw==") { # ... same fields } } ``` #### Offset/Limit Pagination Simpler but less efficient for large datasets: ```graphql query { entities(offset: 20, limit: 10) { edges { node { id } } totalCount } } ``` ### Real-time Subscriptions Subscriptions provide WebSocket-based real-time updates when world state changes. #### Model Registration Listen for new model registrations: ```graphql subscription { modelRegistered { id name namespace } } ``` #### Entity Updates Subscribe to specific entity changes: ```graphql subscription { entityUpdated(id: "0x54f58...") { id updatedAt models { __typename ... on Position { vec { x y } } ... on Moves { remaining } } } } ``` #### Event Stream Monitor all world events with filtering support: ```graphql subscription { eventEmitted { id keys data transactionHash } } ``` ### GraphiQL IDE Use the built-in GraphiQL IDE at `http://localhost:8080/graphql` to: * **Explore Schema**: Browse all available queries, mutations, and subscriptions * **Auto-completion**: IntelliSense support for writing queries * **Real-time Testing**: Test subscriptions with live data * **Documentation**: Built-in schema documentation ### Best Practices **Query Optimization:** * Use model-specific queries instead of generic `entities` for better performance * Request only the fields you need * Use cursor-based pagination for large datasets **Real-time Updates:** * Subscribe to specific entities rather than all events when possible * Handle connection drops and reconnection in production * Use subscription filters to reduce bandwidth ## gRPC API Torii's gRPC API provides high-performance access to indexed world data through binary serialization and streaming capabilities. It is designed for applications requiring low latency and efficient data fetching. ### Quick Start **Endpoint**: `http://localhost:8080` (gRPC protocol) **Protocol Type Definitions**: [torii/proto/types](https://github.com/dojoengine/torii/blob/main/crates/proto/proto/types.proto) **Client Libraries**: * [dojo.js](/client/sdk/javascript) - JavaScript/TypeScript * [dojo.c playground](https://github.com/dojoengine/dojo.c/tree/main/playground) - C/C++ ### Core Concepts #### Query Structure Both `Retrieve` and `Subscribe` queries use the `Query` message with these fields: ```cairo struct Query { clause: Clause; // What to query limit: uint32; // Max results (pagination) offset: uint32; // Skip results (pagination) dont_include_hashed_keys: bool; // Performance optimization order_by: Vec; // Sort order entity_models: Vec; // Filter models entity_updated_after: uint64; // Incremental updates } ``` #### `Clause` Types **Keys Clause** - Query by entity keys: ```json { "Keys": { "keys": ["0x127fd..."], "pattern_matching": "FixedLen", "models": ["dojo_starter-Position"] } } ``` :::info The `HashedKeys` clause can be used with the model's underlying [hashed composite key](https://dojoengine.org/framework/models/entities) ::: **Member Clause** - Query by field values: ```json { "Member": { "model": "dojo_starter-Moves", "member": "remaining", "operator": "Gt", "value": { "Primitive": { "U32": 10 } } } } ``` **Composite Clause** - Combine multiple queries: ```json { "Composite": { "operator": "And", "clauses": [ /* multiple clauses */ ] } } ``` ### Query Examples #### Basic Entity Query Get all entities with any key: ```json { "clause": { "Keys": { "keys": [""], "pattern_matching": "VariableLen", "models": [] } }, "limit": 50 } ``` #### Field-Based Query Find players with moves remaining: ```json { "clause": { "Member": { "model": "dojo_starter-Moves", "member": "remaining", "operator": "Gt", "value": { "Primitive": { "U32": 0 } } } } } ``` #### Position Range Query Find entities in coordinate range: ```json { "clause": { "Composite": { "operator": "And", "clauses": [ { "Member": { "model": "dojo_starter-Position", "member": "x", "operator": "Gte", "value": { "Primitive": { "U32": 0 } } } }, { "Member": { "model": "dojo_starter-Position", "member": "x", "operator": "Lt", "value": { "Primitive": { "U32": 100 } } } } ] } } } ``` ### Pagination & Sorting #### Basic Pagination with `limit` and `offset` ```json { "limit": 25, "offset": 50, "clause": null } ``` #### Ordered Results with `order_by` ```json { "order_by": [ { "model": "dojo_starter-Moves", "member": "remaining", "direction": "Desc" } ] } ``` #### Model Filtering with `entity_models` ```json { "entity_models": ["dojo_starter-Position", "dojo_starter-Moves"] } ``` ### Operators & Values #### Comparison Operators * `Eq`, `Neq` - Equal, not equal * `Gt`, `Gte` - Greater than, greater than or equal * `Lt`, `Lte` - Less than, less than or equal * `In`, `NotIn` - In list, not in list #### Value Types ```json { "Primitive": { "U32": 42, // Numbers "Bool": true, // Booleans "felt252": [...] // Byte arrays for felt252 } } ``` #### Pattern Matching * `FixedLen` - Exact key match * `VariableLen` - Prefix match (at least these keys) ### Performance Tips **Query Optimization**: * Set `dont_include_hashed_keys: true` for better performance if you do not need entity IDs * Use specific models in `entity_models` to reduce data transfer * Prefer `VariableLen` pattern matching for flexible key queries **Incremental Updates**: * Use `entity_updated_after` with timestamps for efficient polling * Combine with subscriptions for real-time updates **Pagination**: * Use reasonable `limit` values (25-100 entities) * Implement pagination for large datasets ### Subscriptions gRPC supports streaming subscriptions for real-time updates: * **Entity Updates**: Subscribe to changes in specific entities * **Event Streams**: Monitor world events as they occur * **Model Changes**: Track updates to specific model types :::note Subscription examples require protocol buffer definitions. See the [protocol files](https://github.com/dojoengine/torii/blob/main/crates/proto/proto/types.proto) for complete streaming API documentation. ::: ### Best Practices **Connection Management**: * Use connection pooling for high-throughput applications * Handle reconnection logic for streaming subscriptions * Set appropriate timeouts for long-running queries **Query Design**: * Start with simple queries and add complexity as needed * Use composite queries sparingly - prefer multiple simple queries * Test query performance with realistic data volumes **Data Handling**: * Process results incrementally for large datasets * Cache frequently accessed entities locally * Use model filtering to minimize bandwidth ![torii](/toolchain/torii-icon-word.png) ## Torii Torii is the official indexing engine for Dojo worlds, designed to provide real-time synchronization between onchain game state and client applications. Built in Rust for performance and reliability, Torii automatically tracks all changes to your game's Entity Component System (ECS) data and makes it accessible through multiple API interfaces. ### Architecture Overview #### Core Components Torii operates as a multi-layered system that bridges the gap between Starknet blockchain data and client applications: **Indexing Engine**: The core component that continuously monitors the blockchain for events related to your Dojo world. It processes transactions, extracts ECS state changes, and maintains a synchronized local database. **Storage Layer**: A high-performance SQLite database that stores indexed world state, including entities, models, events, and metadata. The database schema is dynamically generated based on your world's model definitions. **API Layer**: Multiple interfaces for accessing indexed data: * **[GraphQL API](/toolchain/torii/graphql)**: Provides a flexible, typed interface with real-time subscriptions * **[gRPC API](/toolchain/torii/grpc)**: High-performance binary protocol for efficient data fetching * **[SQL Endpoint](/toolchain/torii/sql)**: Direct database access for custom queries #### Data Flow 1. **World Introspection**: Torii automatically discovers your world's structure by reading model and system registrations from the world contract 2. **Event Processing**: Monitors for Dojo-specific events (`StoreSetRecord`, `StoreUpdateRecord`, `StoreDelRecord`, etc.) and ERC token transfers 3. **ECS Synchronization**: Translates blockchain events into ECS entity and component updates 4. **Real-time Broadcasting**: Propagates changes to connected clients via GraphQL subscriptions #### Scaling and Performance Torii is designed for production deployment with several performance optimizations: * **Parallel Processing**: Events are processed concurrently using a task manager system * **Efficient Batching**: Blockchain data is fetched in configurable chunks to optimize RPC usage * **Caching Layer**: In-memory caches reduce database load and improve query performance * **Database Optimization**: Configurable indices and query optimization for large datasets ### Getting Started #### Quick Start Torii leverages world introspection to bootstrap directly from an onchain deployment. For local development with [Katana](/toolchain/katana) sequencer: ```bash torii --world ``` This starts Torii with default settings: * GraphQL API at `http://localhost:8080/graphql` * gRPC API at `http://localhost:8080` * In-memory database (for development) #### Production Configuration For production deployments, use persistent storage and custom configuration: ```bash torii --world --db-dir ./torii-db --config torii_prod.toml ``` ### Installation Torii can be installed via [`dojoup`](/installation), our dedicated package manager: ```bash curl -L https://install.dojoengine.org | bash # Restart your terminal dojoup install ``` :::note This will install the `torii` binary at `~/.dojo/bin` ::: :::tip Dojoup automatically synchronizes compatible versions of Dojo, Katana, and Torii ::: #### Installing with `asdf` If you prefer to install with the `asdf` version manager: ```bash asdf plugin add torii https://github.com/dojoengine/asdf-torii.git asdf install torii latest ``` :::note This will install the `torii` binary at `~/.asdf/shims` ::: #### Building from Source If you prefer to build from the source code: ```bash git clone https://github.com/dojoengine/torii.git cargo install --path ./torii/bin/torii --profile local --force ``` :::note This will install the `torii` binary at `~/.cargo/bin` ::: ### Next Steps * **[Configuration Guide](/toolchain/torii/configuration)**: Learn how to configure Torii with TOML files and CLI arguments * **[GraphQL API](/toolchain/torii/graphql)**: Explore the GraphQL interface for flexible data queries * **[gRPC API](/toolchain/torii/grpc)**: Use the high-performance gRPC interface ## SQL Endpoint Torii exposes a SQL endpoint for direct database access, enabling custom queries, analytics, and reporting beyond what GraphQL and gRPC provide. :::warning The SQL endpoint is under active development. Database schema and query behavior may change between versions. ::: ### Overview **Key Features**: * **Direct Access**: Query the underlying SQLite database directly * **Custom Analytics**: Build complex aggregations and reports * **Schema Introspection**: Explore database structure and relationships * **Read-Only**: Database cannot be modified through this interface **Use Cases**: * Custom dashboards and analytics * Data export and migration * Advanced filtering not available in GraphQL * Database schema exploration * Performance debugging and optimization ### Endpoint Access The SQL endpoint is available at `/sql` on your Torii server: * **Base URL**: `http://localhost:8080/sql` * **Methods**: GET (query parameter) or POST (request body) * **Format**: Raw SQL queries * **Response**: JSON format with query results #### Interactive SQL Playground Visiting `http://localhost:8080/sql` in your browser opens an interactive SQL playground featuring: * **Monaco Editor**: Full-featured SQL editor with syntax highlighting and auto-completion * **Schema Explorer**: Browse database tables and columns with expandable tree view * **Query History**: Automatic query history with favorites and timestamps * **Real-time Results**: Execute queries and view results in formatted tables * **Export Options**: Download results as JSON files * **Performance Metrics**: Query execution time and row count display :::note The SQL playground is marked as BETA and actively under development. ::: ### Database Schema Understanding Torii's database structure is essential for effective querying: #### Core System Tables ##### `entities` All entities with at least one component (ID is Poseidon hash of keys) ##### `models` Registry of all models and events with metadata ##### `entity_model` Junction table mapping entities to their models ##### `model_members` Schema definition for model fields and types #### Optional Data Tables ##### `events` Raw blockchain events from world contract :::info Requires `--events.raw` ::: ##### `transactions` World-related transactions with calldata :::info Requires `--indexing.transactions` ::: ##### `event_messages` Custom events emitted via `world.emit_event` API ##### `event_messages_historical` Preserved historical event messages :::info Requires `--events.historical` ::: #### ERC Token Tables ##### `balances` ERC20/ERC721/ERC1155 token balances by account ##### `tokens` Token metadata (name, symbol, decimals) ##### `erc_transfers` Token transfer events with amounts and parties :::info These tables require ERC contract configuration ::: #### Additional Tables ##### `controllers` Cartridge controller integration :::info Requires `--indexing.controllers` ::: ##### `transaction_calls` Detailed transaction call information with entrypoints ##### `entities_historical` Entity state snapshots over time :::info Requires `--historical` ::: #### Dynamic Model Tables Torii automatically creates tables for each registered model: **Table Naming Convention**: * Format: `-` * Example: `game-Position`, `combat-Health` :::info Model table names contain hyphens and must be escaped with square brackets `[table-name]` or double quotes `"table-name"` in SQL queries ::: **Field Mapping**: * Model fields are prefixed with `external_` in the database * Primitive types (felt252, u32, bool, ByteArray) are stored directly * Complex types (arrays, enums, structs) create separate tables: * Format: `-$` * Example: `game-Inventory$items` **Key Fields**: * Fields marked with `#[key]` in your model are used for entity identification * Composite keys are supported for multi-key entities * Key fields are automatically indexed for query performance ### Endpoint queries To submit a query to the SQL endpoint, append `/sql` to the Torii URL. You can submit the query using a `GET` or `POST` request. #### Using GET The query is sent as a URL parameter. Both `q` and `query` parameters are supported: ```bash query=$(printf '%s' "SELECT * FROM [ns-Position];" | jq -s -R -r @uri) curl "0.0.0.0:8080/sql?query=${query}" | jq ``` ```bash curl "0.0.0.0:8080/sql?query=SELECT%20*%20FROM%20models;" | jq ``` :::tip The `jq -s -R -r @uri` command URL-encodes the SQL query to handle special characters like spaces, brackets, and semicolons in HTTP URLs. ::: #### Using POST The query is sent as the body of the request. ```bash curl -X POST "0.0.0.0:8080/sql" -d "SELECT * FROM [ns-Position];" | jq ``` ### Common Query Examples #### Schema Exploration List all tables in the database: ```sql SELECT name FROM sqlite_master WHERE type='table' ORDER BY name; ``` Get table schema information: ```sql SELECT m.name as table_name, p.name as column_name, p.type as data_type, p.pk as is_primary_key, p."notnull" as not_null FROM sqlite_master m JOIN pragma_table_info(m.name) p WHERE m.type = 'table' AND m.name NOT LIKE 'sqlite_%' ORDER BY m.name, p.cid; ``` #### Entity Queries Find entities with specific models: ```sql SELECT e.id, e.keys, e.updated_at FROM entities e JOIN entity_model em ON e.id = em.entity_id JOIN models m ON em.model_id = m.id WHERE m.name = 'Position' LIMIT 100; ``` Query model-specific data (remember to quote table names): ```sql SELECT external_player, external_x, external_y FROM "dojo_starter-Position" WHERE external_x > 0 AND external_y > 0; ``` #### Performance Analytics Entity count by model: ```sql SELECT m.name, COUNT(*) as entity_count FROM models m JOIN entity_model em ON m.id = em.model_id GROUP BY m.id, m.name ORDER BY entity_count DESC; ``` Recent activity: ```sql SELECT COUNT(*) as recent_entities FROM entities WHERE updated_at > datetime('now', '-1 hour'); ``` ### Sepolia / Mainnet Deployment This is a guide to deploy a Dojo world to Sepolia. The steps for Mainnet are exactly the same, just replace the chain name and ID when necessary. #### Setup * You need a [Starknet RPC Provider](https://www.starknet.io/fullnodes-rpc-services/) to deploy contracts onchain. You can use the *Cartridge RPC provider* for this. ```bash https://api.cartridge.gg/x/starknet/mainnet # mainnet https://api.cartridge.gg/x/starknet/sepolia # sepolia ``` After you get yours, you can check if it works and is on the correct chain (`SN_SEPOLIA` or `SN_MAIN`) ```bash # run this... curl --location '' \ --header 'Content-Type: application/json' \ --data '{"id": 0,"jsonrpc": "2.0","method": "starknet_chainId","params": {}}' # you should get an output like this... {"jsonrpc":"2.0","result":"0x534e5f5345504f4c4941","id":0} # now paste the hex result part on this command... echo 0x534e5f5345504f4c4941 | xxd -r -p # which !must! output SN_SEPOLIA or SN_MAIN SN_SEPOLIA ``` * Declare the `sepolia` profile in [`Scarb.toml`](https://github.com/rsodre/512karat/blob/main/dojo/Scarb.toml) ```toml [profile.sepolia] ``` * Create the [`dojo_sepolia.toml`](https://github.com/rsodre/512karat/blob/main/dojo/dojo_sepolia.toml) Dojo config file, with the same contents of [`dojo_dev.toml`](https://github.com/rsodre/512karat/blob/main/dojo/dojo_dev.toml), except for `[env]`, in which we're going to expose the `world_address` only: ```toml [env] # rpc_url = "" # env: STARKNET_RPC_URL # account_address = "" # env: DOJO_ACCOUNT_ADDRESS # private_key = "" # env: DOJO_PRIVATE_KEY # world_address = "" ``` * It's recommended to keep the `world_address` empty, on the first deployment it will be outputed by the deployment script. Then you should expose it. * Clone the [`dev`](https://github.com/rsodre/512karat/blob/main/dojo/overlays/dev/) overlays to [`sepolia`](https://github.com/rsodre/512karat/blob/main/dojo/overlays/sepolia/) * Create `.env.sepolia` containing your RPC provider, account and private key. Make sure that account is deployed and has some [ETH](https://starknet-faucet.vercel.app) in it (0.001 is more than enough). ```bash # usage: source .env.sepolia export STARKNET_RPC_URL= export DOJO_ACCOUNT_ADDRESS= export DOJO_PRIVATE_KEY= ``` #### Deployment * To load the env variables and deploy to the chain you can use this script (for mainnet, just replace `sepolia` with `mainnet`): ```bash # Stop script on error set -e # Load environment variables from the appropriate file ENV_FILE=".env.sepolia" if [ -f "$ENV_FILE" ]; then echo "Loading environment variables from $ENV_FILE..." export $(grep -v '^#' "$ENV_FILE" | xargs) else echo "Environment file $ENV_FILE not found!" exit 1 fi # Define a cleanup function to clear environment variables cleanup_env() { echo "Cleaning up environment variables..." unset STARKNET_RPC_URL unset DOJO_ACCOUNT_ADDRESS unset DOJO_PRIVATE_KEY echo "Environment variables cleared." } # Set the trap to execute cleanup on script exit or error trap cleanup_env EXIT # Build the project echo "Building the project..." sozo -P sepolia build # Deploy the project echo "Deploying to Sepolia..." sozo -P sepolia migrate # Deployment succeeded message echo "Deployment completed successfully." ``` * For this script to work don't forget to give it execution permissions: `chmod +x .sh` * This localises the env variables to the deployment script, so if for any reason the deployment is aborted, it cleans up the env variables. * Sozo will output the rpc url, account address and deployed block. ```bash profile | chain_id | rpc_url ---------+----------+------------------------ sepolia | SN_SEPOLIA | 🌍 World deployed at block with txn hash: ⛩️ Migration successful with world at address ``` :::note If the world is already deployed by other user, you must change your `seed` in `dojo_.toml` config file. This will yield a different world address. ::: Your world is deployed! * Once the world is deployed, you need to add the world\*block in the `dojo*.toml` file. ```toml rpc_url = "https://api.cartridge.gg/x/starknet/mainnet" # account_address = "" # env: DOJO_ACCOUNT_ADDRESS # private_key = "" # env: DOJO_PRIVATE_KEY world_address = world_block = 42069 # Here you add the block number where the world was deployed ``` #### Torii Indexer Now, if you're building a Dojo client, you will need a Torii service to index our world... * Install [slot](https://github.com/cartridge-gg/slot) or update it. You can find the docs [here](https://docs.cartridge.gg/slot/getting-started). ```bash slotup ``` * Authorize ```bash slot auth login ``` * Create a [Torii configuration file](/toolchain/torii/configuration) with your world address and RPC endpoint: * `WORLD_ADDRESS`: from your Dojo config file `dojo_sepolia.toml` or from the deployment output * `RPC_URL`: your RPC provider url ```toml # torii.toml world_address = "" rpc = "" ``` * Create Torii service with this command, replacing... * `SERVICE_NAME` can be the name of the game/dapp. Once you create it, you own that name. * `DOJO_VERSION`: your Dojo version (ex: `v1.0.1`) ```bash slot deployments create torii --config torii.toml --version ``` * slot will output something like this. Save it for later, you will need the endpoints on your client. ``` Deployment success 🚀 Stream logs with `slot deployments logs torii -f` ``` * If for any reasons we need to recreate Torii, we can just delete it and run the create command again. This is safe, all your data is onchain. ```bash slot deployments delete torii ``` #### Some notes on the client side * The `migrate` script is copying manifests to `/client/src/dojo/generated/`, each chain needs to use their own manifest! * The client needs the env variable `VITE_PUBLIC_CHAIN_ID` to be set to your chain id. Configure on your sever and add it to your `.env` to access your deployment localy: ``` VITE_PUBLIC_CHAIN_ID=SN_SEPOLIA ``` or... ``` VITE_PUBLIC_CHAIN_ID=SN_MAIN ``` #### Debug with Walnut Use [Walnut](https://walnut.dev) to debug your onchain transactions on Mainnet, Sepolia, or Slot deployments. Walnut helps you inspect transaction details, understand execution flow, and troubleshoot issues. ##### Step 1: Verify your Contracts To use full debugging capabilities, first verify your contracts using Walnut: ```bash sozo walnut verify ``` You'll see output similar to: ```console 🌰 Verifying classes with Walnut... > "Contract verification has started. You can check the verification status at the following link: https://app.walnut.dev/verification/status/c9f415ec-1257-4f34-959c-5ccf51662e53" ``` ##### Step 2: Debug Your Transaction After successful verification, follow these steps to debug your transaction on Walnut: **For Starknet Mainnet or Testnet(Sepolia):** 1. Open the [Walnut app](https://app.walnut.dev). 2. Enter your transaction hash in the search bar. 3. Click on your transaction to view debug information. > Example: To see Walnut Debugger in action, open this [example Dojo transaction](https://app.walnut.dev/transactions?chainId=SN_SEPOLIA\&txHash=0x06553f6543e0afbfcfa3ba22223a50cd36db75c7be7e53fba38786908a550c9b). **For Slot Deployments:** You need to set up your Slot RPC URL in Walnut before debugging: 1. Follow this [custom networks setup guide](https://docs.walnut.dev/custom-networks). 2. Once set up, use the Walnut app as described above. For detailed instructions and advanced use cases, visit the official [Walnut Documentation](https://docs.walnut.dev). ![slot](/tutorials/slot-icon-word.png) ## Deploy your game using Slot Welcome to this tutorial where we'll guide you through deploying a project using the Slot. *** Before we start, make sure you are using the latest Dojo version. Run `dojoup` to have the latest version installed. Now, let's create a new project and initialize it with Sozo. ```bash sozo init dojo-starter && cd dojo-starter ``` First, we need to set up our configuration, starting by authenticating with Cartridge. To do this, run the following command, which will then prompt a new screen where you will need to go through the authentication process. ```bash slot auth login # Slot Auth debug (if old auth credentials): rm ~/Library/Application\ Support/slot/credentials.json ``` Once successful, you can create a new deployment with a unique `DEPLOYMENT_NAME`. The simplest way to get started is using `--optimistic` mode: ```bash slot deployments create DEPLOYMENT_NAME katana --optimistic ``` Alternatively, you can provide a [Katana configuration file](/toolchain/katana/configuration) via `--config`: ```bash slot deployments create DEPLOYMENT_NAME katana --config katana.toml ``` After that, you should receive the RPC endpoint for the development network. Now, you can use that and update your `Scarb.toml` file with the new RPC endpoint as follows: ```toml [tool.dojo.env] rpc_url = "YOUR_NEW_RPC_URL" ``` Now, you can stream Katana in a new terminal. Open a new terminal and run the following command: ```bash slot deployments logs DEPLOYMENT_NAME katana -f ``` Then, copy the account address and the private key from the first account into the `Scarb.toml` file and replace the existing ones as follows: ```toml account_address = "YOUR_NEW_ACCOUNT_ADDRESS" private_key = "YOUR_NEW_PRIVATE_KEY" ``` Note: For each new Katana slot, a different account seed is used, making all the accounts unique! *** Once finished with the new configurations, we are ready to build and migrate the project. To build the project, run the following command: ```bash sozo build ``` Now, let's migrate the project to our new development network: ```bash sozo migrate ``` If the migrations have been successful, you will receive the world address, which then you can use to interact with your world. ```bash 🎉 Successfully migrated World at address WORLD_ADDRESS ✨ Updating manifest.json... ✨ Done. ``` Congratulations! You have successfully deployed your project with a Katana slot. ### Torii For comprehensive Torii indexer deployment including advanced configuration options, service management, and production considerations, see [Deploy to Mainnet](/tutorials/deploy-to-mainnet/main). To deploy a basic Torii indexer slot, first create a [Torii configuration file](/toolchain/torii/configuration) with your world address and RPC endpoint: ```toml # torii.toml world_address = "YOUR_WORLD_ADDRESS" rpc = "YOUR_NEW_RPC_URL" ``` Then create the deployment: ```bash slot deployments create DEPLOYMENT_NAME torii --config torii.toml ``` You can also specify a Dojo version with `--version`: ```bash slot deployments create DEPLOYMENT_NAME torii --config torii.toml --version v1.8.0 ``` Once deployment is successful, you should receive the endpoints for GraphQL and gRPC. If you wish to stream the logs, you can run the following command in a new terminal: ```bash slot deployments logs DEPLOYMENT_NAME torii -f ``` ## Deploy your Cairo smart contract using Katana. Advanced Tutorial Before starting, we recommend following the [Katana Getting Started guide](/toolchain/katana#getting-started) to gain a basic understanding of Katana, Starkli and Scarb. For users wanting to work with the complete Dojo framework specifically, see the [Dojo starter tutorial](/tutorials/dojo-starter). ### Contract Deployment and Interaction #### Create a Vote project ```bash scarb new vote ``` Add contract dependencies to scarb.toml ```toml [dependencies] starknet = "2.5.4" [[target.starknet-contract]] ``` Copy the vote contract to lib.cairo ```rust /// @dev Core Library Imports for the Traits outside the Starknet Contract use starknet::ContractAddress; /// @dev Trait defining the functions that can be implemented or called by the Starknet Contract #[starknet::interface] trait VoteTrait { /// @dev Function that returns the current vote status fn get_vote_status(self: @T) -> (u8, u8, u8, u8); /// @dev Function that checks if the user at the specified address is allowed to vote fn voter_can_vote(self: @T, user_address: ContractAddress) -> bool; /// @dev Function that checks if the specified address is registered as a voter fn is_voter_registered(self: @T, address: ContractAddress) -> bool; /// @dev Function that allows a user to vote fn vote(ref self: T, vote: u8); } /// @dev Starknet Contract allowing three registered voters to vote on a proposal #[starknet::contract] mod Vote { use starknet::ContractAddress; use starknet::get_caller_address; const YES: u8 = 1_u8; const NO: u8 = 0_u8; /// @dev Structure that stores vote counts and voter states #[storage] struct Storage { yes_votes: u8, no_votes: u8, can_vote: LegacyMap::, registered_voter: LegacyMap::, } /// @dev Contract constructor initializing the contract with a list of registered voters and 0 vote count #[constructor] fn constructor( ref self: ContractState, voter_1: ContractAddress, voter_2: ContractAddress, voter_3: ContractAddress ) { // Register all voters by calling the _register_voters function self._register_voters(voter_1, voter_2, voter_3); // Initialize the vote count to 0 self.yes_votes.write(0_u8); self.no_votes.write(0_u8); } /// @dev Event that gets emitted when a vote is cast #[event] #[derive(Drop, starknet::Event)] enum Event { VoteCast: VoteCast, UnauthorizedAttempt: UnauthorizedAttempt, } /// @dev Represents a vote that was cast #[derive(Drop, starknet::Event)] struct VoteCast { voter: ContractAddress, vote: u8, } /// @dev Represents an unauthorized attempt to vote #[derive(Drop, starknet::Event)] struct UnauthorizedAttempt { unauthorized_address: ContractAddress, } /// @dev Implementation of VoteTrait for ContractState #[abi(embed_v0)] impl VoteImpl of super::VoteTrait { /// @dev Returns the voting results fn get_vote_status(self: @ContractState) -> (u8, u8, u8, u8) { let (n_yes, n_no) = self._get_voting_result(); let (yes_percentage, no_percentage) = self._get_voting_result_in_percentage(); (n_yes, n_no, yes_percentage, no_percentage) } /// @dev Check whether a voter is allowed to vote fn voter_can_vote(self: @ContractState, user_address: ContractAddress) -> bool { self.can_vote.read(user_address) } /// @dev Check whether an address is registered as a voter fn is_voter_registered(self: @ContractState, address: ContractAddress) -> bool { self.registered_voter.read(address) } /// @dev Submit a vote fn vote(ref self: ContractState, vote: u8) { assert!(vote == NO || vote == YES, "VOTE_0_OR_1"); let caller: ContractAddress = get_caller_address(); self._assert_allowed(caller); self.can_vote.write(caller, false); if (vote == NO) { self.no_votes.write(self.no_votes.read() + 1_u8); } if (vote == YES) { self.yes_votes.write(self.yes_votes.read() + 1_u8); } self.emit(VoteCast { voter: caller, vote: vote, }); } } /// @dev Internal Functions implementation for the Vote contract #[generate_trait] impl InternalFunctions of InternalFunctionsTrait { /// @dev Registers the voters and initializes their voting status to true (can vote) fn _register_voters( ref self: ContractState, voter_1: ContractAddress, voter_2: ContractAddress, voter_3: ContractAddress ) { self.registered_voter.write(voter_1, true); self.can_vote.write(voter_1, true); self.registered_voter.write(voter_2, true); self.can_vote.write(voter_2, true); self.registered_voter.write(voter_3, true); self.can_vote.write(voter_3, true); } } /// @dev Asserts implementation for the Vote contract #[generate_trait] impl AssertsImpl of AssertsTrait { // @dev Internal function that checks if an address is allowed to vote fn _assert_allowed(ref self: ContractState, address: ContractAddress) { let is_voter: bool = self.registered_voter.read((address)); let can_vote: bool = self.can_vote.read((address)); if (can_vote == false) { self.emit(UnauthorizedAttempt { unauthorized_address: address, }); } assert!(is_voter == true, "USER_NOT_REGISTERED"); assert!(can_vote == true, "USER_ALREADY_VOTED"); } } /// @dev Implement the VotingResultTrait for the Vote contract #[generate_trait] impl VoteResultFunctionsImpl of VoteResultFunctionsTrait { // @dev Internal function to get the voting results (yes and no vote counts) fn _get_voting_result(self: @ContractState) -> (u8, u8) { let n_yes: u8 = self.yes_votes.read(); let n_no: u8 = self.no_votes.read(); (n_yes, n_no) } // @dev Internal function to calculate the voting results in percentage fn _get_voting_result_in_percentage(self: @ContractState) -> (u8, u8) { let n_yes: u8 = self.yes_votes.read(); let n_no: u8 = self.no_votes.read(); let total_votes: u8 = n_yes + n_no; if (total_votes == 0_u8) { return (0, 0); } let yes_percentage: u8 = (n_yes * 100_u8) / (total_votes); let no_percentage: u8 = (n_no * 100_u8) / (total_votes); (yes_percentage, no_percentage) } } } ``` #### Compile contract and Environment variables setup Compile your contract using Scarb ```bash scarb build ``` Place the following environment variables in a .env file within the `src/` directory. ```bash export STARKNET_ACCOUNT=katana-0 #A pre-funded account on the local development network. export STARKNET_RPC=http://0.0.0.0:5050 #To specify the network, targeting the local katana devnet. ``` Then, ensure your project acknowledges the environment variables: ```bash source .env ``` #### Declare contract Make sure Katana is already running in separate terminal. Otherwise launch Katana ```bash katana --disable-fee ``` To declare your contract, execute: ```bash starkli declare target/dev/vote_Vote.contract_class.json ``` Upon successful command execution, you will obtain a contract class hash. This unique hash serves as the identifier for your contract class within Starknet. For example: ```console Class hash declared: 0x071092406ababbba5573bbff0074b068aaeb48c9a67ec66abe982ab19bc6997b ``` #### Deploy contract ```bash starkli deploy ``` The first hexadecimal input is the class hash of the contract, and the next three are the vote account addresses. We can define that the first vote account address corresponds to the address of `katana-0`, while the second and third vote account addresses are associated with `katana-1` and `katana-2`, respectively. Check the list of built-in accounts [here](https://github.com/xJonathanLEI/starkli/blob/e9a28f1b6e37bcc9fc53b7b7130e935894856739/src/account.rs#L76). ```bash starkli deploy 0x071092406ababbba5573bbff0074b068aaeb48c9a67ec66abe982ab19bc6997b 0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03 0x2d71e9c974539bb3ffb4b115e66a23d0f62a641ea66c4016e903454c8753bbc 0x6b86e40118f29ebe393a75469b4d926c7a44c2e2681b6d319520b7c1156d114 ``` After running, expect an output similar to: ```console Deploying class 0x071092406ababbba5573bbff0074b068aaeb48c9a67ec66abe982ab19bc6997b with salt 0x04baae9a396c3ce27a45b201528ec13b366c25960d640a9a32a8736814d9d8c2... The contract will be deployed at address 0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15 Contract deployment transaction: 0x071d6b51e52febfaf2e3ed7dbbf1416190f019d80c73b1bf707d375374ab7cc5 Contract deployed: 0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15 ``` #### Call contract (only read state) The first parameter is the contract address, the second parameter is the function to be called, and the third parameter is the function parameter. Let's pass the address of `Katana-0` account ```bash starkli call 0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15 voter_can_vote 0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03 ``` After running, expect an output similar to: ```console [ "0x0000000000000000000000000000000000000000000000000000000000000001" ] ``` `1` means this user address can vote. #### Invoke contract (can write state) The first parameter is the contract address, the second parameter is the function to be invoked, and the third parameter is the function parameter. Let's vote `Yes` with `katana-0` user ```bash starkli invoke 0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15 vote 1 ``` Now let's vote `No` with `katana-1` user ```bash starkli invoke 0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15 vote 0 --account katana-1 ``` Let's try to vote again with `katana-0` user ```bash starkli invoke 0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15 vote 0 ``` Since the same user/signer cannot vote repeatedly, Katana will report an error. ```console Transaction execution error: "Error in the called contract (0x06162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03): Error at pc=0:4573: Got an exception while executing a hint: Hint Error: Execution failed. Failure reason: 0x555345525f414c52454144595f564f544544 ('USER_ALREADY_VOTED'). Cairo traceback (most recent call last): Unknown location (pc=0:67) Unknown location (pc=0:1835) Unknown location (pc=0:2478) Unknown location (pc=0:3255) Unknown location (pc=0:3795) Error in the called contract (0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15): Execution failed. Failure reason: 0x555345525f414c52454144595f564f544544 ('USER_ALREADY_VOTED'). ``` #### Query transaction ```bash starkli transaction starkli transaction 0x071d6b51e52febfaf2e3ed7dbbf1416190f019d80c73b1bf707d375374ab7cc5 ``` All the above interaction processes can be seen on the Katana client. Pay attention to the status changes of Katana at each step. ## 0. Setup *Before starting recommend following the [`hello-dojo`](/tutorials/dojo-starter) chapter to gain a basic understanding of the Dojo game.* ### Initializing the Project Create and initialize a new Dojo project. You can name your project what you want. ```bash sozo init chess ``` ### Cleaning Up the Boilerplate The project comes with a lot of boilerplate codes. Clear it all. Make sure your directory looks like this ```bash ├── README.md ├── Scarb.toml └── src ├── actions.cairo ├── lib.cairo ├── models │ ├── game.cairo │ ├── piece.cairo │ └── player.cairo ├── models.cairo ├── tests │ ├── integration.cairo │ └── units.cairo └── tests.cairo ``` Remodel your `lib.cairo`, to look like this : ```rust mod actions; mod models; mod tests; ``` Remodel your `models.cairo`, to look like this : ```rust mod game; mod piece; mod player; ``` Remodel your `tests.cairo`, to look like this : ```rust mod integration; mod units; ``` Make sure your `Scarb.toml` looks like this: ```toml [package] cairo-version = "2.5.4" name = "chess" version = "0.6.0" [cairo] sierra-replace-ids = true [dependencies] dojo = { git = "https://github.com/dojoengine/dojo", tag = "v0.6.0" } [[target.dojo]] [tool.dojo] initializer_class_hash = "0xbeef" [tool.dojo.env] rpc_url = "http://localhost:5050/" # Default account for katana with seed = 0 account_address = "0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03" private_key = "0x1800000000300000180000000000030000000000003006001800006600" world_address = "0x446f1f19ba951b59935df72974f8ba6060e5fbb411ca21d3e3e3812e3eb8df8" ``` Compile your project with: ```bash sozo build ``` ### Basic Models While there are many ways to design a chess game using the ECS model, we'll follow this approach: > Every square of the chess board (e.g., A1) will be treated as an entity. > If a piece exists on a square position, that position will hold that piece. First, add this basic `player` model to `models/player.cairo` file. If you are not familar with model syntax in Dojo engine, go back to this [chapter](/framework/models). ```rust use starknet::ContractAddress; #[derive(Model, Drop, Serde)] struct Player { #[key] game_id: u32, #[key] address: ContractAddress, color: Color } #[derive(Serde, Drop, Copy, PartialEq, Introspect)] enum Color { White, Black, None, } ``` Second, we do the same for `game` model. Edit your `models/game.cairo` file and add this content. ```rust use chess::models::player::Color; use starknet::ContractAddress; #[derive(Model, Drop, Serde)] struct Game { #[key] game_id: u32, winner: Color, white: ContractAddress, black: ContractAddress } #[derive(Model, Drop, Serde)] struct GameTurn { #[key] game_id: u32, player_color: Color } ``` Lastly we create `piece` model in our `models/piece.cairo` file. ```rust use chess::models::player::Color; use starknet::ContractAddress; #[derive(Model, Drop, Serde)] struct Piece { #[key] game_id: u32, #[key] position: Vec2, color: Color, piece_type: PieceType, } #[derive(Copy, Drop, Serde, Introspect)] struct Vec2 { x: u32, y: u32 } #[derive(Serde, Drop, Copy, PartialEq, Introspect)] enum PieceType { Pawn, Knight, Bishop, Rook, Queen, King, None, } ``` ### Basic systems Starting from the next chapter, you will implement the `actions.cairo` file. This is where our game logic/contract will reside. For now, `actions.cairo` should look like this: ```rust #[dojo::contract] mod actions { } ``` It should be noted that systems function are contract methods, by implication, rather than implementing the game logic in systems, we are implementing it in a contract. ### Compile your project Now try `sozo build` to build. Complied? Great! then let's move on. If not fix the issues, so that you can run the `sozo build` command successfully. ### Implement Traits for models Before you move on, implement traits for models so we can use them in the next chapter when creating the action contract. #### Requirements First, we have to define the following traits for `Game`, `Player`, `Piece` models respectively. ```rust trait GameTurnTrait { fn next_turn(self: @GameTurn) -> Color; } trait PlayerTrait { fn is_not_my_piece(self: @Player, piece_color: Color) -> bool; } trait PieceTrait { fn is_out_of_board(next_position: Vec2) -> bool; fn is_right_piece_move(self: @Piece, next_position: Vec2) -> bool; } ``` Try to implement this code by yourself. Otherwise
Click to see full `models.cairo` code ```c // code for player.cairo file, paste it below the code you already have trait PlayerTrait { fn is_not_my_piece(self: @Player, piece_color: Color) -> bool; } impl PalyerImpl of PlayerTrait { fn is_not_my_piece(self: @Player, piece_color: Color) -> bool { *self.color != piece_color } } // code for game.cairo file, paste it below the code you already have trait GameTurnTrait { fn next_turn(self: @GameTurn) -> Color; } impl GameTurnImpl of GameTurnTrait { fn next_turn(self: @GameTurn) -> Color { match self.player_color { Color::White => Color::Black, Color::Black => Color::White, Color::None => panic(array!['Illegal turn']) } } } // code for piece.cairo file, paste it below the code you already have trait PieceTrait { fn is_out_of_board(next_position: Vec2) -> bool; fn is_right_piece_move(self: @Piece, next_position: Vec2) -> bool; } impl PieceImpl of PieceTrait { fn is_out_of_board(next_position: Vec2) -> bool { next_position.x > 7 || next_position.y > 7 } fn is_right_piece_move(self: @Piece, next_position: Vec2) -> bool { let n_x = next_position.x; let n_y = next_position.y; assert!( !(n_x == *self.position.x && n_y == *self.position.y), "Cannot move same position " ); match self.piece_type { PieceType::Pawn => { match self.color { Color::White => { (n_x == *self.position.x && n_y == *self.position.y + 1) || (n_x == *self.position.x && n_y == *self.position.y + 2) || (n_x == *self.position.x + 1 && n_y == *self.position.y + 1) || (n_x == *self.position.x - 1 && n_y == *self.position.y + 1) }, Color::Black => { (n_x == *self.position.x && n_y == *self.position.y - 1) || (n_x == *self.position.x && n_y == *self.position.y - 2) || (n_x == *self.position.x + 1 && n_y == *self.position.y - 1) || (n_x == *self.position.x - 1 && n_y == *self.position.y - 1) }, Color::None => panic(array!['Should not move empty piece']), } }, PieceType::Knight => { n_x == *self.position.x + 2 && n_y == *self.position.y + 1 }, PieceType::Bishop => { (n_x <= *self.position.x && n_y <= *self.position.y && *self.position.y - n_y == *self.position.x - n_x) || (n_x <= *self.position.x && n_y >= *self.position.y && *self.position.y - n_y == *self.position.x - n_x) || (n_x >= *self.position.x && n_y <= *self.position.y && *self.position.y - n_y == *self.position.x - n_x) || (n_x >= *self.position.x && n_y >= *self.position.y && *self.position.y - n_y == *self.position.x - n_x) }, PieceType::Rook => { (n_x == *self.position.x || n_y != *self.position.y) || (n_x != *self.position.x || n_y == *self.position.y) }, PieceType::Queen => { (n_x == *self.position.x || n_y != *self.position.y) || (n_x != *self.position.x || n_y == *self.position.y) || (n_x != *self.position.x || n_y != *self.position.y) }, PieceType::King => { (n_x <= *self.position.x + 1 && n_y <= *self.position.y + 1) || (n_x <= *self.position.x + 1 && n_y <= *self.position.y - 1) || (n_x <= *self.position.x - 1 && n_y <= *self.position.y + 1) || (n_x <= *self.position.x - 1 && n_y <= *self.position.y - 1) }, PieceType::None => panic(array!['Should not move empty piece']), } } } ```
This tutorial is extracted from [here](https://github.com/dojoengine/origami/tree/main/examples/chess) Congratulations! You've completed the basic setup for building an onchain chess game 🎉 ## 1. Actions This chapter will address implementing `actions.cairo`, which spawns the game and squares containing pieces and also allow players to move pieces. ### What is `actions` contract? To play chess, you need to start game, spawn the pieces, and move around the board. The `actions` contract has two dominant functions `spawn` function which spawns the game entity, places each piece in its proper position on the board and returns the game\_id, and the `move` funtion which allows pieces to be moved around the board.

image

### Requirements 1. Write an interface for the `actions` contract on top of the code you already have. In this case, `move` and `spawn` ```rust use starknet::ContractAddress; use chess::models::piece::Vec2; #[dojo::interface] trait IActions { fn move(curr_position: Vec2, next_position: Vec2, caller: ContractAddress, game_id: u32); fn spawn(white_address: ContractAddress, black_address: ContractAddress) -> u32; } ``` 2. Bring in required imports into the contract like this: ```rust #[dojo::contract] mod actions { use chess::models::player::{Player, Color, PlayerTrait}; use chess::models::piece::{Piece, PieceType, PieceTrait}; use chess::models::game::{Game, GameTurn, GameTurnTrait}; use super::{ContractAddress, IActions, Vec2}; } ``` Should be noted that `actions` is the contract name. 3. Write a `spawn` function that accepts the `white address`, and `black address` as input and set necessary states using `set!(...)`. Define the `player` entity from player model. Define the game entity, consisting of the `Game` model and `GameTurn` model we created in the `game.cairo`, and define the piece entities from a1 to h8 containing the correct `PieceType` in the `spawn` fn. Paste the following code inside `mod actions`. ```rust #[abi(embed_v0)] impl IActionsImpl of IActions { fn spawn( world: IWorldDispatcher, white_address: ContractAddress, black_address: ContractAddress ) -> u32 { let game_id = world.uuid(); // set Players set!( world, ( Player { game_id, address: black_address, color: Color::Black }, Player { game_id, address: white_address, color: Color::White }, ) ); // set Game and GameTurn set!( world, ( Game { game_id, winner: Color::None, white: white_address, black: black_address }, GameTurn { game_id, player_color: Color::White }, ) ); // set Pieces set!( world, (Piece { game_id, color: Color::White, position: Vec2 { x: 0, y: 0 }, piece_type: PieceType::Rook }) ); set!( world, (Piece { game_id, color: Color::White, position: Vec2 { x: 0, y: 1 }, piece_type: PieceType::Pawn }) ); set!( world, (Piece { game_id, color: Color::Black, position: Vec2 { x: 1, y: 6 }, piece_type: PieceType::Pawn }) ); set!( world, (Piece { game_id, color: Color::White, position: Vec2 { x: 1, y: 0 }, piece_type: PieceType::Knight }) ); set!( world, (Piece { game_id, color: Color::None, position: Vec2 { x: 0, y: 2 }, piece_type: PieceType::None }) ); set!( world, (Piece { game_id, color: Color::None, position: Vec2 { x: 0, y: 3 }, piece_type: PieceType::None }) ); set!( world, (Piece { game_id, color: Color::None, position: Vec2 { x: 1, y: 4 }, piece_type: PieceType::None }) ); //the rest of the positions on the board goes here.... game_id } fn move( world: IWorldDispatcher, curr_position: Vec2, next_position: Vec2, caller: ContractAddress, //player game_id: u32 ) { // Upcoming code } } ``` ## 2 Move function 1. Write a `move` function that accepts the `current position`, `next position`, `caller address`, and `game_id`. The `move` function should look like this: ```rust #[abi(embed_v0)] impl IActionsImpl of IActions { fn spawn( world: IWorldDispatcher, white_address: ContractAddress, black_address: ContractAddress ) -> u32 { // Rest of code } fn move( world: IWorldDispatcher, curr_position: Vec2, next_position: Vec2, caller: ContractAddress, //player game_id: u32 ) { let mut current_piece = get!(world, (game_id, curr_position), (Piece)); // check if next_position is out of board or not assert!(!PieceTrait::is_out_of_board(next_position), "Should be inside board"); // check if this is the right move for this piece type assert!( current_piece.is_right_piece_move(next_position), "Illegal move for type of piece" ); // Get piece data from to next_position in the board let mut next_position_piece = get!(world, (game_id, next_position), (Piece)); let player = get!(world, (game_id, caller), (Player)); // check if there is already a piece in next_position assert!( next_position_piece.piece_type == PieceType::None || player.is_not_my_piece(next_position_piece.color), "Already same color piece exist" ); next_position_piece.piece_type = current_piece.piece_type; next_position_piece.color = player.color; // make current_piece piece none current_piece.piece_type = PieceType::None; current_piece.color = Color::None; set!(world, (next_position_piece)); set!(world, (current_piece)); // change turn let mut game_turn = get!(world, game_id, (GameTurn)); game_turn.player_color = game_turn.next_turn(); set!(world, (game_turn)); } } ``` 2. Run `sozo build` to compile the code. Great, Now we can start testing our functions ### Test Flow * Spawn the test world (`spawn_test_world`) that imports the models in testing. * Deploy actions contract * Interact with `spawn` function in the `actions` contract by providing white and black player's wallet addresses as inputs. * Retrieve the game entity and piece entity created in `actions` contract. * Ensure the game has been correctly created. * Verify that each `Piece` is located in the correct position. ### Unit Tests * Copy the test below and add it to your `tests/units.cairo` file. ```rust #[cfg(test)] mod tests { use starknet::ContractAddress; use dojo::test_utils::{spawn_test_world, deploy_contract}; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; use chess::models::player::{Player, Color, player}; use chess::models::piece::{Piece, PieceType, Vec2, piece}; use chess::models::game::{Game, GameTurn, game, game_turn}; use chess::actions::{actions, IActionsDispatcher, IActionsDispatcherTrait}; // helper setup function fn setup_world() -> (IWorldDispatcher, IActionsDispatcher) { // models let mut models = array![ game::TEST_CLASS_HASH, player::TEST_CLASS_HASH, game_turn::TEST_CLASS_HASH, piece::TEST_CLASS_HASH ]; // deploy world with models let world = spawn_test_world(models); // deploy systems contract let contract_address = world .deploy_contract('salt', actions::TEST_CLASS_HASH.try_into().unwrap()); let actions_system = IActionsDispatcher { contract_address }; (world, actions_system) } #[test] #[available_gas(3000000000000000)] fn test_spawn() { let white = starknet::contract_address_const::<0x01>(); let black = starknet::contract_address_const::<0x02>(); let (world, actions_system) = setup_world(); //system calls let game_id = actions_system.spawn(white, black); //get game let game = get!(world, game_id, (Game)); let game_turn = get!(world, game_id, (GameTurn)); assert!(game_turn.player_color == Color::White, "should be white turn"); assert!(game.white == white, "white address is incorrect"); assert!(game.black == black, "black address is incorrect"); //get a1 piece let curr_pos = Vec2 { x: 0, y: 0 }; let a1 = get!(world, (game_id, curr_pos), (Piece)); assert!(a1.piece_type == PieceType::Rook, "should be Rook"); assert!(a1.color == Color::White, "should be white color"); assert!(a1.piece_type != PieceType::None, "should have piece"); } #[test] #[available_gas(3000000000000000)] fn test_move() { let white = starknet::contract_address_const::<0x01>(); let black = starknet::contract_address_const::<0x02>(); let (world, actions_system) = setup_world(); let game_id = actions_system.spawn(white, black); let curr_pos = Vec2 { x: 0, y: 1 }; let a2 = get!(world, (game_id, curr_pos), (Piece)); assert!(a2.piece_type == PieceType::Pawn, "should be Pawn"); assert!(a2.color == Color::White, "should be white color piece 1"); assert!(a2.piece_type != PieceType::None, "should have piece"); let next_pos = Vec2 { x: 0, y: 2 }; let game_turn = get!(world, game_id, (GameTurn)); assert!(game_turn.player_color == Color::White, "should be white player turn"); actions_system.move(curr_pos, next_pos, white.into(), game_id); let curr_pos = next_pos; let c3 = get!(world, (game_id, curr_pos), (Piece)); assert!(c3.piece_type == PieceType::Pawn, "should be Pawn"); assert!(c3.color == Color::White, "should be white color piece 2"); assert!(c3.piece_type != PieceType::None, "should have piece"); let game_turn = get!(world, game_id, (GameTurn)); assert!(game_turn.player_color == Color::Black, "should be black player turn"); } } ``` ### Diving into the Code #### setup\_world We should list all models with each having CLASS\_HASH as elements and then we deploy world to models with `spawn_test_world` ```rust //models let mut models = array![ game::TEST_CLASS_HASH, player::TEST_CLASS_HASH, game_turn::TEST_CLASS_HASH, piece::TEST_CLASS_HASH ]; // deploy world with models let world = spawn_test_world(models); ``` After that, we deploy our system contracts, then we return our `world` and `actions_systems` dispatchers. ```rust let contract_address = world .deploy_contract('salt', actions::TEST_CLASS_HASH.try_into().unwrap()); let actions_system = IActionsDispatcher { contract_address }; (world, actions_system) ``` #### test\_spawn First, we set up the players address and their colors. ```rust let white = starknet::contract_address_const::<0x01>(); let black = starknet::contract_address_const::<0x02>(); ``` We use `spawn` function in `actions.cairo` to put our pieces on the board. Each square position holds a piece. The system's `spawn` function needs some input i.e the addresses of the players. ```rust // spawn let game_id = actions_system.spawn(white, black); ``` Then we check if the players got their setup address. After that we check if a White rook is at (0,0). Remember, to get a piece that exists on the position, you need to use the keys of the `Piece` model, which are `game_id`, and `curr_pos`. ```rust //get a1 square let curr_pos = Vec2 { x: 0, y: 0 }; let a1 = get!(world, (game_id, curr_pos), (Piece)); assert!(a1.piece_type == PieceType::Rook, "should be Rook"); assert!(a1.color == Color::White, "should be white color"); assert!(a1.piece_type != PieceType::None, "should have piece"); ``` #### test\_move Here, after setting up the board, we use `move` function in the contract to make moves. Provide the current position, the next position, the player's address, and the game id. ```rust //Move White Pawn to (0,2) actions_system.move(curr_pos, next_pos, white.into(), game_id); ``` Then we check if a White Pawn is at the new position. ```rust let curr_pos = next_pos; let c3 = get!(world, (game_id, curr_pos), (Piece)); assert!(c3.piece_type == PieceType::Pawn, "should be Pawn"); assert!(c3.color == Color::White, "should be white color piece 2"); assert!(c3.piece_type != PieceType::None, "should have piece"); ``` ### Need help? If you are stuck, do not hesitate to ask questions at the [Dojo community](https://discord.gg/akd2yfuRS3)! ## 3 Test Contract In this chapter, we'll use everything we've learned to run a full chess game scenario. Here's what we'll do in our test: 1. Call spawn to setup `white_pawn` to (0,1) and `black_pawn` to (1,6) 2. Move `white_pawn` to (0,3) 3. Move `black_pawn` to (1,4) 4. Move `white_pawn` to (1,4) 5. Capture `black_pawn` To place the pieces, use our `spawn` function in our `actions` contract. For moving them, use the `move` contract. Remember to check if a piece can be captured when using `move`. Before we get to the code, set up your integration test like this: * Copy the test below and add it to your `tests/integration.cairo` file. ### Full Code ```rust mod tests { use chess::models::piece::{Piece, PieceType, Vec2}; use dojo::world::IWorldDispatcherTrait; use chess::tests::units::tests::setup_world; use chess::actions::{IActionsDispatcher, IActionsDispatcherTrait}; use chess::models::player::{Color}; #[test] #[available_gas(3000000000000000)] fn integration() { let white = starknet::contract_address_const::<0x01>(); let black = starknet::contract_address_const::<0x02>(); let (world, actions_system) = setup_world(); //system calls let game_id = actions_system.spawn(white, black); //White pawn is setup in (0,1) let wp_curr_pos = Vec2 { x: 0, y: 1 }; let a2 = get!(world, (game_id, wp_curr_pos), (Piece)); assert!(a2.piece_type == PieceType::Pawn, "should be Pawn in (0,1)"); assert!(a2.color == Color::White, "should be white color"); assert!(a2.piece_type != PieceType::None, "should have piece in (0,1)"); //Black pawn is setup in (1,6) let bp_curr_pos = Vec2 { x: 1, y: 6 }; let b7 = get!(world, (game_id, bp_curr_pos), (Piece)); assert!(b7.piece_type == PieceType::Pawn, "should be Pawn in (1,6)"); assert!(b7.color == Color::Black, "should be black color"); assert!(b7.piece_type != PieceType::None, "should have piece in (1,6)"); //Move White Pawn to (0,3) let wp_next_pos = Vec2 { x: 0, y: 3 }; actions_system.move(wp_curr_pos, wp_next_pos, white.into(), game_id); //White pawn is now in (0,3) let wp_curr_pos = wp_next_pos; let a4 = get!(world, (game_id, wp_curr_pos), (Piece)); assert!(a4.piece_type == PieceType::Pawn, "should be Pawn in (0,3)"); assert!(a4.color == Color::White, "should be white color"); assert!(a4.piece_type != PieceType::None, "should have piece in (0,3)"); //Move black Pawn to (1,4) let bp_next_pos = Vec2 { x: 1, y: 4 }; actions_system.move(bp_curr_pos, bp_next_pos, black.into(), game_id); //Black pawn is now in (1,4) let bp_curr_pos = bp_next_pos; let b5 = get!(world, (game_id, bp_curr_pos), (Piece)); assert!(b5.piece_type == PieceType::Pawn, "should be Pawn in (1,4)"); assert!(b5.color == Color::Black, "should be black color"); assert!(b5.piece_type != PieceType::None, "should have piece in (1,4)"); // Move White Pawn to (1,4) and capture black pawn actions_system.move(wp_curr_pos, bp_curr_pos, white.into(), game_id); let wp_curr_pos = bp_curr_pos; let b5 = get!(world, (game_id, wp_curr_pos), (Piece)); assert!(b5.piece_type == PieceType::Pawn, "should be Pawn in (1,4)"); assert!(b5.color == Color::White, "should be white color"); assert!(b5.piece_type != PieceType::None, "should have piece in (1,4)"); } } ``` Keep moving pieces and checking if they're in the right places. ### Congratulations! You've made the basic contracts for a chess game using the Dojo engine! This tutorial was just the beginning. There are many ways to make the game better, like optimizing parts, adding checks, or considering special cases. If you want to do more with this chess game, try these challenges: * Add a checkmate feature. Our game doesn't end now, so decide when it should! * Include special moves like castling, En Passant Capture, or Pawn Promotion. * Make your own chess rules! You could even create your own version of the [immortal game](https://immortal.game/) Lastly, share your project with others in the [Dojo community](https://discord.gg/akd2yfuRS3)! ## Building a Chess Game *"I just finished reading The Dojo Book. What should I do next?"* The answers to this question are always "Make something!", sometimes followed by a list of cool projects. This is a great answer for some people, but others might be looking for a little more direction. This guide is intended to fill the gap between heavily directed beginner tutorials and working on your projects. The primary goal here is to get you to write code. The secondary goal is to get you reading documentation. If you haven't read the Dojo Book yet, it is highly encouraged for you to do so before starting this project. ### What are we building? We're building an onchain chess game contract that lets you start a new game and play chess. This guide does not cover every rules of the chess game. You will build step by step as follows: 1. A system contract to spawn all the chess pieces 2. A system contract to make pieces move 3. Add some functions to check a legal move 4. Play chess ♟♙ - integration test! The full code of tutorial is based on [this repo](https://github.com/dojoengine/origami/tree/main/examples/chess). If this seems too hard, don't worry! This guide is for beginners. If you know some basics about Cairo and Dojo, you're good. We won't make a full chess game with all the rules. We're keeping it simple. ### What after this guide? We're making another guide to help design the frontend. This will make our chess game complete. After you finish all the four chapters, we can move on to the frontend guide. ## C Bindings API The **C Bindings API** provides direct access to dojo.c functionality through procedural C functions. This API is designed for building platform SDKs, native integrations, and applications requiring maximum performance or precise memory control. ### Getting Started **Prerequisites:** * Rust toolchain (for building from source) * C compiler (gcc/clang) for linking * CMake (optional, for integration) #### Installation ```bash # Clone & enter the repository git clone https://github.com/dojoengine/dojo.c && cd dojo.c # Build native library (may take 5-10 minutes on first build) cargo build --release # The library will be at: target/release/libdojo_c.{so,dylib,dll} ``` #### Basic Integration **1. Include the header:** ```c #include "dojo.h" ``` **2. Connect to a Dojo world:** ```c // world address FieldElement world; hex_to_bytes("0x01385f25d20a724edc9c7b3bd9636c59af64cbaf9fcd12f33b3af96b2452f295", &world); // Connect to Torii indexer for the specified world ResultToriiClient res = client_new("http://localhost:8080", world); if (res.tag == ErrToriiClient) { printf("Failed to create client: %s\n", res.err.message); return 1; } ToriiClient *client = res.ok; ``` **3. Set up controller account:** ```c // Define session permissions Policy policies[] = { {contract_address, "spawn", "Allow spawning players"}, {contract_address, "move", "Allow player movement"}, }; // Chain ID for the network FieldElement chain_id; hex_to_bytes("0x4b4154414e41", &chain_id); // "KATANA" in hex // Check for existing session account first ResultControllerAccount res = controller_account(policies, 2, chain_id); if (res.tag == OkControllerAccount) { // Session account already exists printf("Using existing session account\n"); ControllerAccount *account = res.ok; } else { // Need to connect new session account void on_account_connected(ControllerAccount *account) { printf("Connected: %s\n", controller_username(account)); } controller_connect( "https://api.cartridge.gg/x/controller/katana", policies, 2, on_account_connected ); } ``` ### Core API The essential functions most developers use: #### Client Management Client functions connect your application to the Dojo world state via Torii (the indexer). Use these to fetch current game state, query historical data, and receive real-time updates as the world changes. This is your **read** interface to the blockchain. | Function | Returns | Description | | --------------------------------------------------------- | -------------------- | ---------------------------------------------- | | `client_new(torii_url, world)` | `ResultToriiClient` | Creates connection to Torii indexer | | `client_entities(client, query)` | `ResultPageEntity` | Queries entities with filtering and pagination | | `client_on_entity_state_update(client, clause, callback)` | `ResultSubscription` | Subscribes to real-time entity changes | #### Account Operations Account functions handle the **write** side of blockchain interaction. Controller accounts provide session-based gameplay (users approve once, then play freely within policy limits), while direct accounts give full control for advanced use cases. Use these to execute game actions and manage player permissions. | Function | Returns | Description | | --------------------------------------------------------------- | ------------------------- | --------------------------------------- | | `controller_connect(rpc_url, policies, policies_len, callback)` | `void` | Initiates session account connection | | `controller_account(policies, policies_len, chain_id)` | `ResultControllerAccount` | Retrieves existing session account | | `controller_execute_raw(account, calls, calls_len)` | `ResultFieldElement` | Executes transactions via controller | | `account_new(provider, private_key, address)` | `ResultAccount` | Creates direct account for advanced use | #### Essential Data Types | Type | Description | | ------------------------- | ------------------------------------------------------------- | | **Input Types** | | | `FieldElement` | 32-byte array for addresses, hashes, and Cairo felt252 values | | `Call` | Transaction call with target contract, selector, and calldata | | `Query` | Entity query with filtering, pagination, and ordering | | **Return Types** | | | `ResultToriiClient` | Result wrapper containing ToriiClient pointer or error | | `ResultPageEntity` | Result wrapper containing paginated entities or error | | `ResultSubscription` | Result wrapper containing Subscription pointer or error | | `ResultControllerAccount` | Result wrapper containing ControllerAccount pointer or error | | `ResultFieldElement` | Result wrapper containing FieldElement or error | | `ResultAccount` | Result wrapper containing Account pointer or error | | `Error` | Error information with message string | | `CArrayStruct` | C array of Struct (model data) | ### Advanced API #### Token Operations | Function | Description | | ----------------------------------------------------------------------- | -------------------------------------- | | `client_tokens(client, query)` | Query ERC20/721/1155 token information | | `client_token_balances(client, query)` | Get token balances for accounts | | `client_token_collections(client, query)` | Query token collection metadata | | `client_on_token_update(client, addresses, token_ids, callback)` | Subscribe to token changes | | `client_on_token_balance_update(client, contracts, accounts, callback)` | Subscribe to balance changes | #### Event & Message System | Function | Description | | ---------------------------------------------------------- | ------------------------------------- | | `client_event_messages(client, query)` | Query event messages with filtering | | `client_publish_message(client, message)` | Publish typed data message to network | | `client_publish_message_batch(client, messages, count)` | Publish multiple messages | | `client_on_event_message_update(client, clause, callback)` | Subscribe to event messages | | `client_on_starknet_event(client, clauses, callback)` | Subscribe to Starknet events | #### Advanced Queries | Function | Description | | ------------------------------------------------------- | ---------------------------------------- | | `client_controllers(client, query)` | Query controller accounts | | `client_transactions(client, query)` | Query transaction history with filtering | | `client_on_transaction(client, filter, callback)` | Subscribe to transaction updates | | `client_metadata(client)` | Get world schema and metadata | | `on_indexer_update(client, contract_address, callback)` | Subscribe to indexer status | #### Cryptography & Utilities | Function | Description | | --------------------------------------------------- | --------------------------------------- | | `signing_key_new()` | Generate new private key | | `signing_key_sign(private_key, hash)` | Sign hash with private key | | `verifying_key_new(signing_key)` | Derive public key from private key | | `verifying_key_verify(public_key, hash, signature)` | Verify signature | | `poseidon_hash(felts, count)` | Compute Poseidon hash | | `starknet_keccak(bytes, length)` | Compute Starknet keccak hash | | `get_selector_from_name(name)` | Generate function selector from name | | `get_selector_from_tag(tag)` | Generate selector from tag | | `typed_data_encode(typed_data, address)` | Encode typed data for signing | | `cairo_short_string_to_felt(str)` | Convert ASCII string to field element | | `parse_cairo_short_string(felt)` | Convert field element to ASCII string | | `bytearray_serialize(str)` | Serialize string to field element array | | `bytearray_deserialize(felts, count)` | Deserialize field elements to string | #### Account Management | Function | Description | | -------------------------------------------------------------- | ----------------------- | | `account_deploy_burner(provider, master_account, signing_key)` | Deploy burner account | | `account_address(account)` | Get account address | | `account_chain_id(account)` | Get account chain ID | | `account_nonce(account)` | Get account nonce | | `account_execute_raw(account, calls, count)` | Execute transaction | | `account_set_block_id(account, block_id)` | Set block ID for calls | | `controller_address(controller)` | Get controller address | | `controller_username(controller)` | Get controller username | | `controller_chain_id(controller)` | Get controller chain ID | | `controller_nonce(controller)` | Get controller nonce | | `controller_execute_from_outside(controller, calls, count)` | Execute via paymaster | | `controller_clear(policies, count, chain_id)` | Clear stored sessions | #### Provider & Network | Function | Description | | ----------------------------------------------------------------- | --------------------------------- | | `provider_new(rpc_url)` | Create RPC provider | | `starknet_call(provider, call, block_id)` | Make Starknet call | | `wait_for_transaction(provider, txn_hash)` | Wait for transaction confirmation | | `hash_get_contract_address(class_hash, salt, calldata, deployer)` | Compute contract address | #### Memory Management | Function | Description | | ----------------------------------- | ------------------- | | `client_free(client)` | Free ToriiClient | | `account_free(account)` | Free Account | | `provider_free(provider)` | Free Provider | | `subscription_cancel(subscription)` | Cancel subscription | | `model_free(model)` | Free Model | | `entity_free(entity)` | Free Entity | | `error_free(error)` | Free Error | | `world_metadata_free(metadata)` | Free world metadata | | `carray_free(data, length)` | Free C array | | `string_free(string)` | Free string | ### Building Platform SDKs dojo.c is designed to be the foundation for platform-specific SDKs: **Unity Integration:** ```csharp [DllImport("libdojo_c", CallingConvention = CallingConvention.Cdecl)] private static extern ResultToriiClient client_new(CString torii_url, FieldElement world); ``` **Node.js Integration:** ```javascript const ffi = require("ffi-napi"); const dojo = ffi.Library("./libdojo_c", { client_new: ["pointer", ["string", "pointer"]], }); ``` **Python Integration:** ```python from ctypes import CDLL, c_char_p dojo = CDLL('./libdojo_c.so') dojo.client_new.argtypes = [c_char_p, c_void_p] ``` ## dojo.c: Complete Dojo Client SDK [**dojo.c**](https://github.com/dojoengine/dojo.c) is the comprehensive client library that powers all Dojo SDKs. Written in Rust, it provides everything you need for Dojo game development: world state queries via Torii, account management, transaction execution, and real-time subscriptions that work consistently across all platforms. ### Implementation Architecture Rather than implementing blockchain logic separately in each SDK, all Dojo integrations build on this single foundation: * **Unity SDK** uses dojo.c via C# P/Invoke * **JavaScript SDK** uses dojo.c compiled to WebAssembly * **Unreal Engine** integrates dojo.c through C++ bindings * **Custom integrations** can use dojo.c directly This ensures consistent behavior, shared bug fixes, and optimal performance across all platforms. Implementing in Rust provides the following advantages: * **Memory Safety**: Eliminates classes of bugs (buffer overflows, use-after-free, etc.) * **Performance**: Zero-cost abstractions compile to optimal machine code * **Concurrency**: Safe async/await and threading without data races * **Ecosystem**: Built on battle-tested Rust libraries (tokio, serde, starknet-rs) * **Cross-Platform**: Single codebase compiles to all target platforms ``` dojo.c Repository (Rust Implementation) ├── Core Rust Logic (types.rs, utils.rs, constants.rs) ├── Conditional Compilation: │ ├── Native Target → C Bindings (cbindgen) │ └── WASM Target → JavaScript Bindings (wasm-bindgen) └── Generated Outputs: ├── libdojo_c.{so,dylib,dll} + dojo.h └── dojo_c.js + dojo_c.wasm ``` The key insight: **two interfaces, one implementation**. Both C and WASM APIs share identical Rust core logic, ensuring consistency. The two interfaces are designed for different use cases: #### C Bindings API **Target**: Native platform integrations, custom SDKs, maximum performance * **Style**: Procedural C functions with callback-based async operations * **Use Cases**: Building Unity/Unreal plugins, system-level integrations * **Memory Management**: Manual with explicit cleanup functions * **Threading**: Callback-driven with internal runtime management #### WASM JavaScript API **Target**: Web applications, Node.js, rapid prototyping * **Style**: Modern async/await with object-oriented design * **Use Cases**: Browser games, web apps, Node.js backends, testing * **Memory Management**: Automatic garbage collection * **Threading**: Promise-based with native async support ### What You Get dojo.c is a "batteries included" SDK that bundles everything needed for Dojo game development: #### Torii Client (Primary Focus) * **Entity Queries**: Fetch world state with powerful filtering and pagination * **Real-time Subscriptions**: Subscribe to entity changes and events * **Transaction Monitoring**: Track transaction status and confirmations #### Integrated Account Management * **Controller Accounts**: Session-based accounts with policy-restricted permissions * **Burner Accounts**: Disposable accounts funded by a master account * **Direct Accounts**: Full control accounts for advanced use cases #### Built-in Starknet Provider * **Transaction Execution**: Submit transactions directly without separate RPC setup * **Contract Calls**: Query contract view functions * **Chain Operations**: Handle nonces, block IDs, and transaction waiting #### Asset & Token Operations * **Token Queries**: Fetch ERC20/721/1155 token information * **Balance Tracking**: Monitor token balances across accounts * **Real-time Updates**: Subscribe to token and balance changes #### Event & Message System * **Event Subscriptions**: Listen to Starknet events and Dojo model changes * **Message Publishing**: Send messages through the Dojo messaging system * **Transaction History**: Query historical transactions and events ### Choosing the Right API | Factor | C Bindings | WASM JavaScript | | ---------------------- | ------------ | ---------------- | | **Performance** | Maximum | Very Good | | **Integration Effort** | Higher | Lower | | **Platform Support** | Native Only | Web + Node.js | | **Development Speed** | Slower | Faster | | **Memory Control** | Full | Automatic | | **Debugging** | Native Tools | Browser DevTools | #### Use Case Decision Guide **🎯 Choose C Bindings when you need:** * **Native Integration**: Unity plugins, Unreal Engine modules, or desktop applications * **Maximum Performance**: Every microsecond matters for your game * **Platform SDK Development**: Creating bindings for new languages/platforms * **System-Level Control**: Direct memory management, custom threading models **🚀 Choose WASM JavaScript when you need:** * **Web Games**: Browser-based Dojo games with real-time world state * **Rapid Prototyping**: Quick development with familiar JavaScript patterns * **Node.js Backends**: Server-side game logic, bots, APIs * **Rich Debugging**: Browser DevTools, source maps, hot reload #### Integration Examples **Unity Integration Example:** ```csharp [DllImport("libdojo_c", CallingConvention = CallingConvention.Cdecl)] public static extern ResultToriiClient client_new(CString torii_url, FieldElement world); ``` **Node.js Integration Example:** ```javascript const { ToriiClient } = require("./pkg/dojo_c"); const client = await new ToriiClient(config); ``` dojo.c provides everything you need for Dojo game development in one comprehensive package. Whether you're building Unity games, web applications, or custom integrations, it delivers consistent, reliable blockchain interaction with the convenience of an integrated Torii client, account management, and Starknet provider. ### Next Steps * [Complete C API Reference →](./c-bindings) * [Complete JavaScript API Reference →](./wasm-bindings) ## WASM JavaScript API The **WASM JavaScript API** provides a modern, async/await interface to dojo.c functionality compiled to WebAssembly. This API is designed for web applications, Node.js backends, rapid prototyping, and developers who prefer JavaScript/TypeScript. :::info [WebAssembly](https://webassembly.org/) (WASM) is a binary format that allows high-performance code (like Rust) to run in web browsers and Node.js at near-native speeds. Released in 2017, it has become a core technology for modern web development. ::: ### Getting Started #### Installation ```bash # Clone & enter the repository git clone https://github.com/dojoengine/dojo.c && cd dojo.c # Build WASM module for web clients wasm-pack build --release --target web # Build for Node.js backends wasm-pack build --release --target nodejs ``` #### Basic Integration **Browser Integration:** ```html ``` **Node.js Integration:** ```javascript const { ToriiClient } = require("./pkg/dojo_c"); // Node.js polyfills global.WebSocket = require("ws"); global.WorkerGlobalScope = global; async function main() { const client = await new ToriiClient({ toriiUrl: "http://127.0.0.1:8080", worldAddress: "0x064613f376f05242dfcc9fe360fa2ce1fdd6b00b1ce73dae2ea649ea118fd9be", }); const entities = await client.getAllEntities(10); console.log(entities); } ``` ### Client Management #### Client Configuration Client functions connect your application to the Dojo world state via Torii (the indexer). Use these to fetch current game state, query historical data, and receive real-time updates as the world changes. This is your **read** interface to the blockchain. **`new ToriiClient(config)`** Creates a new client instance. ```javascript const client = await new ToriiClient({ toriiUrl: "http://localhost:8080", worldAddress: "0x...", }); ``` #### Entity Operations **`getEntities(query)`** Queries entities with advanced filtering and pagination. ```javascript const query = { pagination: { limit: 100, cursor: undefined, direction: "Forward", }, clause: { Keys: { keys: [playerAddress], pattern_matching: "FixedLen", models: ["Position", "Health"], }, }, no_hashed_keys: false, historical: false, }; const entities = await client.getEntities(query); ``` **`getAllEntities(limit?)`** Fetches all entities with optional limit. ```javascript const allEntities = await client.getAllEntities(1000); ``` **`onEntityUpdated(clause, callback)`** Subscribes to real-time entity updates. ```javascript const subscription = await client.onEntityUpdated( { Keys: { keys: [undefined], // All entities pattern_matching: "VariableLen", models: [], }, }, (entityId, models) => { console.log("Entity updated:", entityId, models); } ); // Cancel subscription when done subscription.cancel(); ``` **`updateEntitySubscription(subscription, clauses)`** Updates an existing entity subscription with new filters. #### Event Operations **`getEventMessages(query)`** Queries historical event messages. ```javascript const events = await client.getEventMessages({ pagination: { limit: 50 }, clause: { Keys: { keys: [eventSelector], pattern_matching: "FixedLen", models: [], }, }, historical: true, }); ``` **`onEventMessageUpdated(clause, callback)`** Subscribes to real-time event updates. ```javascript const eventSub = await client.onEventMessageUpdated( { Keys: { keys: [undefined], pattern_matching: "VariableLen", models: [], }, }, (entityId, models) => { console.log("New event:", entityId, models); } ); ``` **`onStarknetEvent(clauses, callback)`** Subscribes to raw Starknet events. ```javascript const starknetSub = await client.onStarknetEvent( [ { keys: [eventSelector], pattern_matching: "FixedLen", models: [], }, ], (event) => { console.log("Starknet event:", event); } ); ``` #### Token & Asset Operations **`getTokens(query)`** Queries token information. ```javascript const tokens = await client.getTokens({ contract_addresses: ["0x..."], // Optional filter token_ids: [], // Optional filter pagination: { limit: 100 }, }); ``` **`getTokenBalances(query)`** Queries token balances. ```javascript const balances = await client.getTokenBalances({ contract_addresses: ["0x..."], account_addresses: [playerAddress], token_ids: [], pagination: { limit: 100 }, }); ``` **`getTokenCollections(query)`** Queries token collections (NFT collections). ```javascript const collections = await client.getTokenCollections({ contract_addresses: [], pagination: { limit: 50 }, }); ``` **`onTokenUpdated(contract_addresses, token_ids, callback)`** Subscribes to token metadata updates. ```javascript const tokenSub = await client.onTokenUpdated( ["0x..."], // Contract addresses [], // Token IDs (empty for all) (token) => { console.log("Token updated:", token); } ); ``` **`onTokenBalanceUpdated(contract_addresses, account_addresses, token_ids, callback)`** Subscribes to token balance changes. ```javascript const balanceSub = await client.onTokenBalanceUpdated( ["0x..."], // Contract addresses [playerAddress], // Account addresses [], // Token IDs (balance) => { console.log("Balance changed:", balance); } ); ``` #### Transaction Operations **`getTransactions(query)`** Queries transaction history. ```javascript const transactions = await client.getTransactions({ filter: { transaction_hashes: [], caller_addresses: [playerAddress], contract_addresses: [], entrypoints: ["move", "attack"], from_block: undefined, to_block: undefined, }, pagination: { limit: 100 }, }); ``` **`onTransaction(filter, callback)`** Subscribes to new transactions. ```javascript const txSub = await client.onTransaction( { caller_addresses: [playerAddress], entrypoints: ["move"], }, (transaction) => { console.log("New transaction:", transaction); } ); ``` #### Controller Operations **`getControllers(query)`** Queries controller information. ```javascript const controllers = await client.getControllers({ contract_addresses: [], pagination: { limit: 100 }, }); ``` #### Messaging Operations **`publishMessage(message, signature)`** Publishes a single message. ```javascript await client.publishMessage({ message: "Hello, Dojo!", signature: await signMessage(message), }); ``` **`publishMessageBatch(messages)`** Publishes multiple messages in a batch. ```javascript await client.publishMessageBatch([ { message: "Message 1", signature: sig1 }, { message: "Message 2", signature: sig2 }, ]); ``` #### Indexer Updates **`onIndexerUpdated(callback)`** Subscribes to indexer synchronization updates. ```javascript const indexerSub = await client.onIndexerUpdated((update) => { console.log("Indexer update:", update); }); ``` #### Subscription Management **`updateEntitySubscription(subscription, clause)`** Updates an existing entity subscription with new filtering criteria. **`updateEventMessageSubscription(subscription, clause)`** Updates an existing event message subscription with new filtering criteria. **`updateTokenBalanceSubscription(subscription, contract_addresses, account_addresses, token_ids)`** Updates an existing token balance subscription with new addresses and token IDs. ### Account Operations Account functions handle the **write** side of blockchain interaction. Use these to execute game transactions, manage player accounts, and handle cryptographic operations like signing and verification. #### Signing Key Operations **`SigningKey.fromRandom()`** Generates a new random signing key. ```javascript import { SigningKey } from "./pkg/dojo_c.js"; const signingKey = SigningKey.fromRandom(); const secretScalar = signingKey.secretScalar(); const verifyingKey = signingKey.verifyingKey(); ``` #### Provider & Account Operations **`new Provider(rpc_url)`** Creates a new JSON-RPC provider. ```javascript import { Provider } from "./pkg/dojo_c.js"; const provider = await new Provider("http://localhost:5050"); const chainId = await provider.chainId(); ``` **Account Operations:** ```javascript import { Account, SigningKey } from "./pkg/dojo_c.js"; // Create account with private key string const signingKey = SigningKey.fromRandom(); const privateKeyHex = signingKey.secretScalar(); const account = await new Account(provider, privateKeyHex, address); // Execute transactions const txHash = await account.executeRaw([ { to: "0x...", selector: "move", calldata: ["0x1", "0x2"], }, ]); // Deploy burner account with private key string const burnerKey = SigningKey.fromRandom(); const burnerPrivateKey = burnerKey.secretScalar(); const burner = await account.deployBurner(burnerPrivateKey); ``` #### Data Utilities **`ByteArray` class** - For handling Cairo byte arrays: ```javascript import { ByteArray } from "./pkg/dojo_c.js"; const byteArray = new ByteArray("Hello, Dojo!"); const raw = byteArray.toRaw(); // Convert to field elements const restored = ByteArray.fromRaw(raw); // Restore from field elements ``` **`TypedData` class** - For encoding typed data: ```javascript import { TypedData } from "./pkg/dojo_c.js"; const typedData = new TypedData(JSON.stringify(typedDataObject)); const encoded = typedData.encode(accountAddress); ``` **Provider utilities:** ```javascript const result = await provider.call(call, blockId); const chainId = await provider.chainId(); ``` ### Utility Functions #### Cryptographic Functions **`poseidonHash(inputs)`** Computes Poseidon hash of field elements. ```javascript import { poseidonHash } from "./pkg/dojo_c.js"; const hash = poseidonHash(["0x123...", "0x456..."]); ``` **`starknetKeccak(data)`** Computes Starknet-compatible Keccak hash. ```javascript import { starknetKeccak } from "./pkg/dojo_c.js"; const data = new Uint8Array(Buffer.from("move", "utf8")); const hash = starknetKeccak(data); ``` **`getSelectorFromName(name)`** Gets contract function selector from name. ```javascript import { getSelectorFromName } from "./pkg/dojo_c.js"; const selector = getSelectorFromName("move"); ``` **`getSelectorFromTag(tag)`** Gets selector from Dojo tag. ```javascript import { getSelectorFromTag } from "./pkg/dojo_c.js"; const selector = getSelectorFromTag("dojo_starter-Position"); ``` #### Address & Encoding Functions **`getContractAddress(class_hash, salt, constructor_calldata, deployer_address)`** Computes contract address for deployment. ```javascript import { getContractAddress } from "./pkg/dojo_c.js"; const address = getContractAddress( classHash, salt, constructorCalldata, deployerAddress ); ``` **`cairoShortStringToFelt(str)`** Converts Cairo short string to felt. **`parseCairoShortString(felt)`** Parses felt as Cairo short string. ### Common Issues **WASM Initialization:** ```javascript // Always await init() before using any functions await init(); ``` **Node.js Compatibility:** ```javascript // Required polyfills for Node.js global.WebSocket = require("ws"); global.WorkerGlobalScope = global; ``` **Subscription Management:** ```javascript // Cancel subscriptions to prevent memory leaks subscription.cancel(); ``` **Type Safety:** Consider using TypeScript for better development experience: ```typescript import { ToriiClient, ClientConfig } from "./pkg/dojo_c"; const config: ClientConfig = { toriiUrl: "http://localhost:8080", worldAddress: "0x...", }; const client = await new ToriiClient(config); ```