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.

Torii - a ECS query system powerhouse

Before going further be sure to read this documentation of Torii queries Torii provides a grpc server that wraps all required tools to query your entities in a performant manner.

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:

// imports 
import {
    useDojoSDK,
    useEntityId,
    useEntityQuery,
    // useModels fetches all entities : return an array
    useModels,
    // Single model retrieval from store : useful when you know the entityId. Returns a single entity
    useModel,
} from "@dojoengine/sdk/react";
import { KeysClause, ToriiQueryBuilder } from "@dojoengine/sdk";
 
 
 
// inside a react component
useEntityQuery(
    new ToriiQueryBuilder()
        .withClause(
            MemberClause("world-Item", "durability", "Eq", 2).build()
        )
        .includeHashedKeys()
);
 
// Fetch all items from the store
const items = useModels("world-Item")
 
// Fetches a single item from the store
// For instance, if I know my item has an id of 1, I can fetch it like this :
const entityId = useEntityId(1)
const item = useModel(entityId, "world-Item")

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
    # or if you want to specify the output directory
    sozo build --typescript --bindings-output ./desired-output-path
  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 } from "@dojoengine/sdk";
import {
    SchemaType,
    schema,
} from "../where-you-generated-your-models/models.gen";
import { dojoConfig } from "../your-dojo-config-path";
 
// Initialize the SDK
const db = await init<SchemaType>(
    {
        client: {
            toriiUrl: dojoConfig.toriiUrl,
            relayUrl: dojoConfig.relayUrl,
            worldAddress: dojoConfig.manifest.world.address,
        },
        domain: {
            name: "Example",
            version: "1.0",
            chainId: "your-chain-id",
            revision: "1",
        },
    },
    schema
);

Understanding Queries

Here you can find a detailed explanation over how Torii gRPC works

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:
    • 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.

// This is how a raw Torii query would look like.
{
    "Composite": {
        "operator": "And",
        "clauses": [
            {
                "Member": {
                    "model": "world-Player",
                    "member": "id",
                    "operator": "Eq",
                    "value": {
                        "Primitive": {
                            "U32": 1
                        }
                    }
                }
            },
            {
                "Member": {
                    "model": "world-Player",
                    "member": "name",
                    "operator": "Eq",
                    "value": "Alice",
                }
            },
    ]
  }
}
 
// ClauseBuilder version
const entities = await sdk.getEntities(
  new ToriiQueryBuilder().withClause(
    new ClauseBuilder().compose().and(
      new ClauseBuilder().where("world-Player", 'id', 'Eq', 1),
      new ClauseBuilder().where("world-Player", 'name', 'Eq', "Alice"),
    ).build();
  ).build()
);
// Or
const entities = await sdk.getEntities(
  new ToriiQueryBuilder().withClause(
    AndComposeClause([
      MemberClause("world-Player", 'id', 'Eq', 1),
      MemberClause("world-Player", 'name', 'Eq', "Alice"),
    ]).build();
  ).build()
);

Subscribing To Entity Changes

Subscribing to entity changes is a bit more trivial. Because Torii subscribe functions only works with KeysClause or HashedKeysClause you need first to query your model and then subscribe to those using entityIds. 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 [initialEntities, subscription] = await sdk.subscribeEntities({
    query: new ToriiQueryBuilder()
        .withClause(
            AndComposeClause([
                MemberClause(
                    "world-item",
                    "type",
                    "Eq",
                    "sword"
                ),
                MemberClause(
                    "world-item",
                    "durability",
                    "Eq",
                    5
                ),
            ])
            .build()
        )
        .includeHashedKeys(),
    callback:  ({ data, error}) => {
        if (data) {
            console.log("Updated entities:", data);
        }
        if (error) {
            console.error("Subscription error:", error);
        }
    }
});

sdk first query entities and then use entityIds for subscription so you only have to care about what you original query was.

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%2Fyour-slot-deployment-name%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 { DojoSdkProvider } from "@dojoengine/sdk/react";
import { setupWorld } from "../generated/typescript/contracts.gen";
import { dojoConfig } from "../your-dojo-config-path/dojoConfig.ts";
 
const sdk = await init<SchemaType>(
    {
        client: {
            toriiUrl: dojoConfig.toriiUrl,
            relayUrl: dojoConfig.relayUrl,
            worldAddress: dojoConfig.manifest.world.address,
        },
        domain: {
            name: "Example",
            version: "1.0",
            chainId: "your-chain-id",
            revision: "1",
        },
    },
);
<DojoSdkProvider
    sdk={sdk}
    dojoConfig={dojoConfig}
    clientFn={setupWorld}
>
    <App />
</DojoSdkProvider>
 
...
 
// Using in your app
const state = useDojoStore((state) => state);
const entities = useDojoStore((state) => state.entities);
 
...
 
// Fetching entities from torii and binding them into dojo store directly
useEntityQuery(
    new ToriiQueryBuilder()
        .withClause(
            MemberClause("world-Item", "durability", "Eq", 2).build()
        )
        .includeHashedKeys()
);
 
// Get all models from the store
const models = useModels("world-Item")
 
// Get a single model from the store
// entityId = values of your model's keys
// Here Item key = 1
const entityId = useEntityId(1);
const model = useModel(entityId, "world-Item")

Zustand vanilla

You can also use a vanilla store which can be imported :

import { createDojoStore } from "@dojoengine/sdk/state";
const store = createDojoStore<Schema>();
// Contrary to react, zustand vanilla store is not wrapped into a bound store.
// to use your store :
const state = store.getState();
const entities = state.getEntities();

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({
    query: new ToriiQueryBuilder()
        .withClause(
            new ClauseBuilder()
                .compose()
                .and([
                    new ClauseBuilder()
                        .compose()
                        .and([
                            new ClauseBuilder().where(
                                "world-player",
                                "score",
                                "Gt",
                                100
                            ),
                            new ClauseBuilder()
                                .compose()
                                .or([
                                    new ClauseBuilder().where(
                                        "world-player",
                                        "name",
                                        "Eq",
                                        "Bob"
                                    ),
                                    new ClauseBuilder().where(
                                        "world-player",
                                        "name",
                                        "Eq",
                                        "Alice"
                                    ),
                                ]),
                        ]),
                    new ClauseBuilder().where(
                        "world-item",
                        "durability",
                        "Lt",
                        50
                    ),
                ])
                .build()
        ),
});
 
// OR (but only if you're a lazy ninja)
const entities = await sdk.getEntities({
    query: new ToriiQueryBuilder()
        .withClause(
            AndComposeClause([
                AndComposeClause([
                    MemberClause("world-player", "score", "Gt", 100),
                    OrComposeClause([
                        MemberClause("world-player", "name", "Eq", "Bob"),
                        MemberClause("world-player", "name", "Eq", "Alice"),
                    ]),
                ]),
                MemberClause("world-item", "durability", "Lt", 50),
            ]).build()
        )
        .build()
});