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 code 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 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:

// Simple query: Find a specific player
const entities = await sdk.getEntities({
    query: new ToriiQueryBuilder()
        .withClause(MemberClause("dojo_starter-Player", "id", "Eq", 1).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:

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.