Skip to content

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.

Getting Started

Quickstart Wizard

The fastest way to get started is using our quickstart wizard.

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:

Create Your Project

Pick any JavaScript framework. We recommend pnpm as your package manager.

Install Dependencies

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:

import { createDojoConfig } from "@dojoengine/core";
import manifest from "../path/to/manifest_dev.json";
 
export const dojoConfig = createDojoConfig({ manifest });

Generate TypeScript Bindings

Generate contract bindings with Sozo, letting you import Dojo models into TypeScript:

DOJO_MANIFEST_PATH="../path/to/Scarb.toml" sozo build --typescript

Initialize the SDK

With your contract bindings generated, you can now link Dojo models to your UI.

// 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<SchemaType>({
        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(
        <DojoSdkProvider sdk={sdk} dojoConfig={dojoConfig} clientFn={setupWorld}>
            <App />
        </DojoSdkProvider>
    );
}
 
main();

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.

Querying Entities

Fetch entities using the ToriiQueryBuilder with various clause types:

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

Here is an example of a complex query that finds high-scoring warriors and mages in a specific area of the map:

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()
    ),
});

For large datasets, use pagination and ordering:

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:

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:

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 (
        <div>
            <h3>All Items with Durability 2:</h3>
            {Object.entries(items).map(([id, item]) => (
                <div key={id}>Item {id}: durability {item?.durability}</div>
            ))}
 
            <h3>Single Item:</h3>
            {item && <div>Item 1: durability {item.durability}</div>}
        </div>
    );
}

Option 2: Direct Zustand Store Access

For more advanced use cases, you can access the Zustand store directly:

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 (
        <div>
            <p>Total entities in store: {Object.keys(allEntities).length}</p>
            <p>Item entities: {itemEntities.length}</p>
            {itemEntities.map((entity) => {
                const item = entity.models.dojo_starter.Item;
                return (
                    <div key={entity.entityId}>
                        Entity {entity.entityId}: durability {item?.durability}
                    </div>
                );
            })}
        </div>
    );
}

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:

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

Querying Tokens

dojo.js can query token data (ERC20, ERC721, ERC1155) indexed by Torii. First, configure Torii to index your tokens:

# dojo_dev.toml
[indexing]
contracts = [
    "erc20:0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", # ETH
    "erc20:0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", # STRK
    "erc721:0x..." # Your NFT contract
]

Then query token balances in your React components:

import { useTokens } from "@dojoengine/sdk/react";
 
function TokenBalance({ address }: { address: string }) {
    const { tokens, balances, getBalance, toDecimal } = useTokens({
        accountAddresses: [address],
    });
 
    return (
        <div>
            <h3>Token Balances for {address}</h3>
            {tokens.map((token, idx) => (
                <div key={idx}>
                    {token.symbol}: {toDecimal(token, getBalance(token))}
                </div>
            ))}
        </div>
    );
}

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.

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 for a real-world implementation.