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):
-
Generate the TypeScript types for your world:
sozo build --typescript
-
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:
SubscriptionQueryType
: Used for real-time subscriptions to entity and event updates.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.
- Supports only the
-
QueryType
:-
Supports a variety of operators for more advanced filtering:
Operator Description $eq
Equal to $neq
Not equal to $gt
Greater than $gte
Greater than or equal to $lt
Less than $lte
Less 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.
- 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:
- Update entity state optimistically.
- Wait for condition (e.g., a specific state change).
- 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);
}
}
);