Skip to content

Dojo SDK: Build onchain faster

The Dojo SDK provides a powerful, intuitive interface for interacting with onchain state. It streamlines data fetching and subscriptions, supporting both simple and complex queries.

Table of Contents

Key Features

  • Type Safety: Leverage TypeScript for robust, error-resistant code.
  • Intuitive query syntax: Write queries that feel natural, similar to popular OrMs.
  • Flexible subscriptions: Easily subscribe to specific state changes in your Dojo world.
  • Signed messages: Sign off-chain state and send to torii.
  • Automatic Zustand Support: Drop in zustand state management
  • Optimistic Client Rendering: Set state before a transaction has resolved to improve user experiences

Understand Entities and Models

  • Entities are uniquely identified by Keys defined in associated models
  • Entities can have multiple models, representing complex game states
  • When a subscription or query returns data - it returns the updated Entity and changed models.

Example: Subscribing to Specific Model States

Here's a concise example demonstrating how to subscribe to the item model in the world namespace, specifically filtering for items with a durability of 2:

const subscription = await sdk.subscribeEntityQuery(
    world: {
        item: {
            $: {
                where: { durability: { $is: 2 } },
            },
        },
    },
    (response) => {
        if (response.data) {
            // return data - pipe into any state management!
            console.log("Updated entities:", response.data);
        } else if (response.error) {
            // return error
            console.error("Subscription error:", response.error);
        }
    }
);

Usage

🚀 Installation

npm install @dojoengine/sdk

Generate TypeScript types directly from your world schema:

To take advantage of this type safety (You will need dojo installed):

  1. Generate the TypeScript types for your world:

    sozo build --typescript
  2. You will import these and pass into the sdk init function to give your app type. You can see all the options for sozo, like setting the output directory, with the following command:

    sozo build --help

This approach ensures that your code remains in sync with your Dojo world definition, catching potential issues early in the development process.

Initializing the SDK

import { init, SchemaType } from "@dojoengine/sdk";
 
enum Direction {
    None,
    Up,
    Down,
    Left,
    Right,
}
 
interface Moves {
    fieldOrder: string[];
    player: string;
    last_direction: Direction;
    can_move: boolean;
}
interface DirectionAvailable {
    fieldOrder: string[];
    player: string;
    direction: Direction[];
}
interface Pos {
    x: number;
    y: number;
}
interface Position {
    fieldOrder: string[];
    player: string;
    vec: Pos;
}
 
interface MockSchemaType {
    dojo_starter: {
        Moves: Moves;
        DirectionAvailable: DirectionAvailable;
        Position: Position;
    };
}
 
// Generate with sozo or define Schema
const schema: Schema = {
    // this should match namespace define in dojo
    dojo_starter: {
        // this has to match model names
        Moves: {
            fieldOrder: ["player", "remaining", "last_direction", "can_move"],
            player: "",
            remaining: 0,
            // properties have to match too.
            last_direction: Direction.None,
            can_move: false,
        },
        DirectionsAvailable: {
            fieldOrder: ["player", "directions"],
            player: "",
            directions: [],
        },
        Position: {
            fieldOrder: ["player", "vec"],
            player: "",
            vec: { x: 0, y: 0 },
        },
    },
};
 
// Initialize the SDK
const db = await init<MockSchemaType>(
    {
        client: {
            rpcUrl: "your-rpc-url",
            toriiUrl: "your-torii-url",
            relayUrl: "your-relay-url",
            worldAddress: "your-world-address",
        },
        domain: {
            name: "Example",
            version: "1.0",
            chainId: "your-chain-id",
            revision: "1",
        },
    },
    schema
);
 

Understanding Queries

The SDK utilizes two primary types of queries to interact with the Dojo Engine:

  1. SubscriptionQueryType: Used for real-time subscriptions to entity and event updates.
  2. QueryType: Used for fetching entities and event messages with more flexible filtering options.

Both query types enable filtering based on entityIds and specific model properties. The key difference lies in the operators supported within the where clause:

  • SubscriptionQueryType:

    • Supports only the $is operator for exact matches.
  • QueryType:

    • Supports a variety of operators for more advanced filtering:

      OperatorDescription
      $eqEqual to
      $neqNot equal to
      $gtGreater than
      $gteGreater than or equal to
      $ltLess than
      $lteLess than or equal to
    • You combine queries with 'And' with 'Or' from deep queries. See Advanced Usage.

Querying Entities

This example fetches player entities from the world namespace where id is "1" and name is "Alice", demonstrating multiple conditions in a query.

Note: $eq is for exact matching. Other operators ($gt, $lt, etc.) are available for complex queries.

const entities = await sdk.getEntities(
    {
        // this is namespace to query models in
        world: {
            // this is model name
            player: {
                $: { where: { id: { $eq: "1" }, name: { $eq: "Alice" } } },
            },
        },
    },
    (response) => {
        if (response.data) {
            console.log("Fetched entities:", response.data);
        } else if (response.error) {
            console.error("Fetch error:", response.error);
        }
    }
);

Subscribing To Entity Changes

This example subscribes to item model updates in the world namespace, filtering for swords with durability 5. The callback triggers on matching item changes.

Key points:

  • Namespace: world, Model: item
  • Conditions: type "sword", durability 5
  • Uses $is for exact matching
const subscription = await sdk.subscribeEntityQuery(
    {
        // this is namespace to query models in
        world: {
            // this is model name
            item: {
                $: {
                    where: {
                        type: { $is: "sword" },
                        durability: { $is: 5 },
                    },
                },
            },
        },
    },
    (response) => {
        if (response.data) {
            console.log("Updated entities:", response.data);
        } else if (response.error) {
            console.error("Subscription error:", response.error);
        }
    }
);

Sending Signed Messages

NOTE: If you want messages to be actually sent and broadcasted to all of your torii client instance, you'll have to properly set relayUrl in the init function. 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%2Fonchain-dash%2Ftorii%2Fwss

// onchain_dash-Message is a composition of the ${namespace}-${Model} type you want to sign.
// Here we take example of a chat because we don't want to load up those messages onchain
// But keep in mind this could be any models defined in your cairo code
const msg = sdk.generateTypedData("onchain_dash-Message", {
    identity: account?.address,
    content: toValidAscii(data.message),
    timestamp: Date.now(),
});
 
try {
    const signature = await account.signMessage(msg);
 
    try {
        await db.client.publishMessage(
            JSON.stringify(msg),
            signature as string[]
        );
        reset();
    } catch (error) {
        console.error("failed to publish message:", error);
    }
} catch (error) {
    console.error("failed to sign message:", error);
}

Using With Zustand

This module takes the Schema and outputs a typed store you can use around your app. See example here.

  1. Import the module
import { createDojoStore } from "@dojoengine/sdk";
 
// import this outside of components
export const useDojoStore = createDojoStore<Schema>();
 
...
 
// Using in your app
const state = useDojoStore((state) => state);
const entities = useDojoStore((state) => state.entities);
 
...
 
// Adding to a callback
const subscription = await sdk.subscribeEntityQuery(
    {
        world: {
            item: {
                $: {
                    where: {
                        type: { $is: "sword" },
                        durability: { $is: 5 },
                    },
                },
            },
        },
    },
    (response) => {
        if (response.error) {
            console.error("Error setting up entity sync:", response.error);
        } else if (response.data && response.data[0].entityId !== "0x0") {
            // You just need to do this!
            state.setEntities(response.data);
        }
    }
);

Optimistic Client Rendering

We use 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.

See our example project for a real-world implementation.

Note: You will need to have a subscription running in order for the update to resolve.

export const useSystemCalls = () => {
    const state = useDojoStore((state) => state);
 
    const {
        setup: { client },
        account: { account },
    } = useDojo();
 
    const generateEntityId = () => {
        return getEntityIdFromKeys([BigInt(account?.address)]);
    };
 
    const spawn = async () => {
        // Generate a unique entity ID
        const entityId = generateEntityId();
 
        // Generate a unique transaction ID
        const transactionId = uuidv4();
 
        // The value to update
        const remainingMoves = 100;
 
        // Apply an optimistic update to the state
        // this uses immer drafts to update the state
        state.applyOptimisticUpdate(
            transactionId,
            (draft) =>
                (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);
        }
    };
 
    return {
        spawn,
    };
};

Advanced Usage

Create complex 'And' with 'Or' statements to narrow in on what you want to fetch.

Complex Queries

const entities = await sdk.getEntities(
    {
        world: {
            player: {
                $: {
                    where: {
                        And: [
                            { score: { $gt: 100 } },
                            {
                                Or: [
                                    { name: { $eq: "Alice" } },
                                    { name: { $eq: "Bob" } },
                                ],
                            },
                        ],
                    },
                },
            },
            item: {
                $: {
                    where: {
                        And: [{ durability: { $lt: 50 } }],
                    },
                },
            },
        },
    },
    (response) => {
        if (response.data) {
            console.log("Fetched entities:", response.data);
        } else if (response.error) {
            console.error("Fetch error:", response.error);
        }
    }
);