Dojo Rust SDK
Dojo is built in Rust, making it seamless to integrate into your Rust projects. Simply import the required crates and you're ready to build powerful applications that interact with Dojo worlds.
Core Components
The Dojo Rust ecosystem provides several key crates for different use cases:
dojo-types
: Core types and data structures for Dojodojo-world
: World contract interaction and managementtorii-client
: Client for connecting to Torii indexertorii-grpc
: gRPC client for real-time data streamingtorii-relay
: P2P networking and relay functionalitycainome
: Contract bindings generation for Cairo contracts
Getting Started
Add these dependencies to your Cargo.toml
:
[dependencies]
# Core Dojo types and functionality
dojo-types = { git = "https://github.com/dojoengine/dojo", tag = "v1.7.0-alpha.0" }
dojo-world = { git = "https://github.com/dojoengine/dojo", tag = "v1.7.0-alpha.0" }
# Torii client for indexing and real-time data (separate repository since 1.5.0)
torii-client = { git = "https://github.com/dojoengine/torii", tag = "v1.6.1-preview.2" }
torii-grpc-client = { git = "https://github.com/dojoengine/torii", tag = "v1.6.1-preview.2" }
# Contract bindings generation
cainome = { git = "https://github.com/cartridge-gg/cainome", rev = "7d60de1", features = ["abigen-rs"] }
cainome-cairo-serde = { git = "https://github.com/cartridge-gg/cainome", rev = "7d60de1" }
# Starknet integration
starknet = "0.17.0-rc.2"
starknet-crypto = "0.7.4"
starknet-types-core = "0.1.7"
# Async runtime
tokio = { version = "1.39", features = ["full"] }
Basic Usage
Connecting to a Dojo World
// Import necessary types for connecting to Dojo
use torii_client::client::Client;
use starknet_crypto::Felt;
// The #[tokio::main] attribute makes this function run in an async runtime
// This is required because we'll be making network calls
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure connection URLs for your Dojo world
let torii_url = "http://localhost:8080".to_string(); // Torii indexer endpoint
let rpc_url = "http://localhost:5050".to_string(); // Starknet RPC endpoint (Katana)
let relay_url = "http://localhost:9090".to_string(); // P2P relay for real-time updates
// The world address is a unique identifier for your Dojo world on Starknet
let world_address = Felt::from_hex_unchecked("0x123...");
// Create a new Torii client connection
let client = Client::new(
torii_url,
rpc_url,
relay_url,
world_address,
).await?;
// At this point, you have a connected client ready to interact with your Dojo world
// You can now query models, subscribe to events, etc.
Ok(())
}
Subscribing to Events
// Import types needed for event subscriptions and stream processing
use torii_grpc_client::client::{EntityKeysClause, KeysClause, PatternMatching};
use futures::StreamExt; // Provides the .next() method on streams
// Subscribe to all entity updates in the Dojo world
// The `mut` keyword makes the subscription mutable so we can read from it
let mut subscription = client
.on_event_message_updated(
// Create a filter for which entities/models to watch
vec![EntityKeysClause::Keys(KeysClause {
keys: vec![], // Empty = watch all entities
pattern_matching: PatternMatching::VariableLen, // Allow flexible key matching
models: vec![], // Empty = watch all models
})],
true, // Include historical data (events that happened before we subscribed)
)
.await?; // Wait for the subscription to be established
// Process incoming updates in a loop
// This will run continuously, waiting for new events
while let Some(Ok((_, entity))) = subscription.next().await {
// The `Some(Ok(...))` pattern handles the Result<Option<...>> type:
// - Some(...) means we got data (not the end of stream)
// - Ok(...) means no error occurred
// - The (_, entity) destructures the tuple, ignoring the first value
println!("Entity updated: {:?}", entity);
// Here you can add your custom logic to handle the entity update
// For example: update a database, trigger game logic, send notifications, etc.
}
Discord Bot Example
This example demonstrates how to build a Discord bot that connects to a Dojo World using the Rust SDK. The bot monitors world events and posts updates to a Discord channel. This example uses the Shuttle runtime for easy deployment.
You will need a Discord bot token to use this example. You can get one by creating an application and bot in the Discord Developer Portal.
Prerequisites
- Download and install the Rust compiler from rust-lang.org
- Set up a new Rust project:
cargo new dojo-discord-bot
- Install the Shuttle CLI: Shuttle Installation Guide
- Create a Discord application and bot in the Discord Developer Portal
Setup
Create a Secrets.toml
file in the root of your project:
DISCORD_TOKEN = "your_discord_token_here"
TORII_URL = "http://localhost:8080"
NODE_URL = "http://localhost:5050"
TORII_RELAY_URL = "http://localhost:9090"
WORLD_ADDRESS = "your_world_address_here"
CHANNEL_ID = "your_discord_channel_id_here"
Add these dependencies to your Cargo.toml
:
[dependencies]
# Discord bot framework
poise = "0.6.1"
serenity = { version = "0.12.0", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] }
# Utility dependencies
toml = { version = "0.7", default-features = false, features = ["parse", "display"] }
# Shuttle deployment platform
shuttle-runtime = "0.48.0"
shuttle-serenity = "0.48.0"
shuttle-rocket = "0.48.0"
# Dojo components
dojo-types = { git = "https://github.com/dojoengine/dojo", tag = "v1.7.0-alpha.0" }
dojo-world = { git = "https://github.com/dojoengine/dojo", tag = "v1.7.0-alpha.0" }
# Torii components
torii-client = { git = "https://github.com/dojoengine/torii", tag = "v1.6.1-preview.2" }
torii-grpc-client = { git = "https://github.com/dojoengine/torii", tag = "v1.6.1-preview.2" }
torii-relay = { git = "https://github.com/dojoengine/torii", tag = "v1.6.1-preview.2" }
# Contract bindings and Cairo tooling
cainome = { git = "https://github.com/cartridge-gg/cainome", rev = "7d60de1", features = ["abigen-rs"] }
cainome-cairo-serde = { git = "https://github.com/cartridge-gg/cainome", rev = "7d60de1" }
cairo-lang-filesystem = "=2.8.4"
scarb = { git = "https://github.com/software-mansion/scarb", rev = "4fdeb7810" }
# Starknet integration
starknet = "0.17.0-rc.2"
starknet-crypto = "0.7.4"
starknet-types-core = "0.1.7"
# Async runtime
tokio = { version = "1.39", features = ["full"] }
Code
// Standard library imports
use std::{num::NonZero, sync::Arc, time::Duration};
// Discord library imports - Serenity is the main Discord API wrapper for Rust
use serenity::{
all::{ChannelId, CreateMessage, GatewayIntents, Http}, // Discord API types
futures::StreamExt, // Stream processing utilities
Client, // Discord client
};
use shuttle_runtime::SecretStore; // For accessing environment variables securely
use starknet_crypto::Felt; // Starknet field elements for addresses
use torii_grpc_client::client::{EntityKeysClause, KeysClause, PatternMatching}; // Dojo event filtering
// Type aliases to make error handling cleaner
// Box<dyn std::error::Error + Send + Sync> is Rust's way of saying "any error type"
pub type Error = Box<dyn std::error::Error + Send + Sync>;
// Context is the type passed to Discord command functions
pub type Context<'a> = poise::Context<'a, Data, Error>;
// Data structure to share state between Discord commands
// Currently empty, but you could add database connections, etc.
pub struct Data {}
// Discord slash command definition
#[poise::command(slash_command)]
pub async fn hello(ctx: Context<'_>) -> Result<(), Error> {
// ctx.say() sends a message back to Discord in response to the command
ctx.say("🤖 Hello! I'm your Dojo Discord Bot - monitoring world events and ready to help!").await?;
Ok(())
}
#[poise::command(slash_command)]
pub async fn world_status(ctx: Context<'_>) -> Result<(), Error> {
ctx.say("🌍 Connected to Dojo World! I'm watching for all the exciting things happening in your autonomous world.").await?;
Ok(())
}
// Configuration structure to hold all the connection details
// This keeps our configuration organized and type-safe
struct Config {
discord_token: String, // Discord bot authentication token
channel_id: NonZero<u64>, // Discord channel ID (NonZero ensures it's never 0)
torii_url: String, // URL to connect to Torii indexer
node_url: String, // URL to connect to Starknet node (Katana)
torii_relay_url: String, // URL for P2P relay connection
world_address: String, // Hex string of the Dojo world address
}
// Implementation block for Config - this is where we define methods on the Config struct
impl Config {
// Constructor method to create Config from Shuttle's secret store
pub fn from_secrets(secret_store: SecretStore) -> Self {
// Extract Discord bot token from environment variables
let discord_token = secret_store.get("DISCORD_TOKEN").unwrap();
// Parse channel ID from string to number
// This chain: get string -> parse to u64 -> wrap in NonZero -> unwrap all results
let channel_id = NonZero::new(
secret_store
.get("CHANNEL_ID")
.unwrap() // Get the string value
.parse::<u64>() // Convert string to 64-bit unsigned integer
.unwrap(), // Handle parsing errors by crashing
)
.unwrap();
// Extract all the Dojo connection URLs
let torii_url = secret_store.get("TORII_URL").unwrap();
let node_url = secret_store.get("NODE_URL").unwrap();
let torii_relay_url = secret_store.get("TORII_RELAY_URL").unwrap();
let world_address = secret_store.get("WORLD_ADDRESS").unwrap();
// Create and return a new Config instance
// The `fieldname,` syntax is shorthand for `fieldname: fieldname,`
Config {
discord_token,
channel_id,
torii_url,
node_url,
torii_relay_url,
world_address,
}
}
}
// This attribute tells Shuttle this is the main entry point for our application
#[shuttle_runtime::main]
async fn main(
// This parameter injection gives us access to the SecretStore containing our environment variables
#[shuttle_runtime::Secrets] secret_store: SecretStore,
) -> shuttle_serenity::ShuttleSerenity {
// Set up Discord permissions - non_privileged() gives us basic bot permissions
// You might need more permissions depending on what your bot does
let intents = GatewayIntents::non_privileged();
let config = Config::from_secrets(secret_store);
// Set up the Discord command framework (Poise)
// This handles parsing slash commands and routing them to our functions
let framework = poise::Framework::builder()
.options(poise::FrameworkOptions {
commands: vec![hello(), world_status()],
..Default::default()
})
.setup(|ctx, _ready, framework| {
// This closure runs when the bot connects to Discord
Box::pin(async move {
// Register our commands globally so they appear in all servers
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Data {})
})
})
.build();
// Create the Discord client with our bot token and the command framework
let client = Client::builder(config.discord_token.clone(), intents)
.framework(framework)
.await
.expect("Failed to build Discord client");
// Spawn a separate async task to handle Dojo world monitoring
// This runs concurrently with the Discord bot
// `move` captures the config by value so it can be used in the async block
tokio::spawn(async move {
// Create a connection to the Dojo world
let torii_client = torii_client::client::Client::new(
config.torii_url.clone(),
config.node_url.clone(),
config.torii_relay_url.clone(),
Felt::from_hex_unchecked(&config.world_address.clone()),
)
.await
.expect("Failed to create Torii client");
// Start monitoring the Dojo world and sending updates to Discord
subscribe(torii_client, config).await;
});
// Return the Discord client wrapped in Shuttle's expected type
Ok(client.into())
}
// This function handles subscribing to Dojo world events and forwarding them to Discord
// It includes retry logic to handle network issues gracefully
async fn subscribe(client: torii_client::client::Client, config: Config) {
let mut tries = 0; // Current number of failed attempts
let max_num_tries = 200; // Maximum attempts before giving up
// Exponential backoff for reconnection attempts
let mut backoff = Duration::from_secs(1);
let max_backoff = Duration::from_secs(60);
// Create a Discord HTTP client for sending messages
// Arc (Atomically Reference Counted) allows sharing this across async tasks safely
let http = Arc::new(Http::new(&config.discord_token.clone()));
// Main monitoring loop - this runs continuously
loop {
// Attempt to subscribe to world events
let rcv: Result<
torii_grpc_client::client::EntityUpdateStreaming, // Success type: stream of updates
torii_client::client::error::Error, // Error type
> = client
.on_event_message_updated(
// Set up event filtering
vec![EntityKeysClause::Keys(KeysClause {
keys: vec![], // Empty = all entities
pattern_matching: PatternMatching::VariableLen, // Flexible key matching
models: vec![], // Empty = all models
})],
true, // Include historical events (backfill)
)
.await;
// Handle the subscription result
match rcv {
// Successfully connected - process incoming events
Ok(mut rcv) => {
// Reset backoff delay since we connected successfully
backoff = Duration::from_secs(1);
// Process each event as it comes in
while let Some(Ok((_, entity))) = rcv.next().await {
// Format the entity data as a Discord message
// {:#?} creates a pretty-printed debug representation
let entity_message = format!("🎮 **Dojo World Update!**\n```\n{:#?}\n```", entity);
let content = CreateMessage::new().content(entity_message);
// Send the message to Discord
// We use `if let Err(e)` to handle errors without crashing
if let Err(e) = ChannelId::from(config.channel_id)
.send_message(http.clone(), content)
.await
{
println!("Failed to send Discord message: {}", e);
}
}
// If we get here, the stream ended (connection lost)
}
// Failed to connect - we'll retry
Err(_) => {
println!("Subscription was lost, attempting to reconnect");
tries += 1;
}
}
// Wait before trying to reconnect (exponential backoff)
tokio::time::sleep(backoff).await;
backoff = std::cmp::min(backoff * 2, max_backoff);
if tries >= max_num_tries {
println!("Max number of tries reached, exiting");
break;
}
}
println!("Torii client disconnected");
}
Deployment
Local Development
Run the bot locally for testing:
shuttle run
This will start the bot using your local Secrets.toml
file.
The bot will connect to your specified Dojo world and Discord channel.
Production Deployment
Deploy the bot to Shuttle's cloud platform:
shuttle deploy
The bot will be hosted on Shuttle's infrastructure and run continuously.
Make sure your Secrets.toml
contains production-ready values before deploying.
Next Steps
This example demonstrates the basic integration between Dojo and Discord using Rust. You can extend it by:
- Adding more Discord commands to interact with your world
- Filtering events by specific models or entities
- Formatting Discord messages with rich embeds
- Adding database persistence using Shuttle's shared database
- Implementing user authentication and authorization
- Adding custom event processing logic