diff --git a/crates/torii/core/src/types.rs b/crates/torii/core/src/types.rs index be120e50ad..8c57e63cb6 100644 --- a/crates/torii/core/src/types.rs +++ b/crates/torii/core/src/types.rs @@ -121,6 +121,28 @@ pub struct Event { pub executed_at: DateTime, pub created_at: DateTime, } + +#[derive(FromRow, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Token { + pub id: String, + pub contract_address: String, + pub name: String, + pub symbol: String, + pub decimals: u8, + pub metadata: String, +} + +#[derive(FromRow, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TokenBalance { + pub id: String, + pub balance: String, + pub account_address: String, + pub contract_address: String, + pub token_id: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] pub struct Contract { pub address: Felt, diff --git a/crates/torii/grpc/proto/types.proto b/crates/torii/grpc/proto/types.proto index 6b2abb2ba1..d474065f63 100644 --- a/crates/torii/grpc/proto/types.proto +++ b/crates/torii/grpc/proto/types.proto @@ -149,4 +149,19 @@ enum ComparisonOperator { GTE = 3; LT = 4; LTE = 5; +} + +message Token { + string contract_address = 2; + string name = 3; + string symbol = 4; + uint32 decimals = 5; + string metadata = 6; +} + +message TokenBalance { + string balance = 1; + string account_address = 2; + string contract_address = 3; + string token_id = 4; } \ No newline at end of file diff --git a/crates/torii/grpc/proto/world.proto b/crates/torii/grpc/proto/world.proto index 4898c44b8e..2c7e7b1270 100644 --- a/crates/torii/grpc/proto/world.proto +++ b/crates/torii/grpc/proto/world.proto @@ -42,6 +42,36 @@ service World { // Subscribe to events rpc SubscribeEvents (SubscribeEventsRequest) returns (stream SubscribeEventsResponse); + + // Retrieve tokens + rpc RetrieveTokens (RetrieveTokensRequest) returns (RetrieveTokensResponse); + + // Retrieve token balances + rpc RetrieveTokenBalances (RetrieveTokenBalancesRequest) returns (RetrieveTokenBalancesResponse); +} + +// A request to retrieve tokens +message RetrieveTokensRequest { + // The list of contract addresses to retrieve tokens for + repeated bytes contract_addresses = 1; +} + +// A response containing tokens +message RetrieveTokensResponse { + repeated types.Token tokens = 1; +} + +// A request to retrieve token balances +message RetrieveTokenBalancesRequest { + // The account addresses to retrieve balances for + repeated bytes account_addresses = 1; + // The list of token contract addresses to retrieve balances for + repeated bytes contract_addresses = 2; +} + +// A response containing token balances +message RetrieveTokenBalancesResponse { + repeated types.TokenBalance balances = 1; } // A request to subscribe to indexer updates. diff --git a/crates/torii/grpc/src/server/mod.rs b/crates/torii/grpc/src/server/mod.rs index 858c4c523c..23284c16d2 100644 --- a/crates/torii/grpc/src/server/mod.rs +++ b/crates/torii/grpc/src/server/mod.rs @@ -43,6 +43,7 @@ use torii_core::error::{Error, ParseError, QueryError}; use torii_core::model::{build_sql_query, map_row_to_ty}; use torii_core::sql::cache::ModelCache; use torii_core::sql::utils::sql_string_to_felts; +use torii_core::types::{Token, TokenBalance}; use tower_http::cors::{AllowOrigin, CorsLayer}; use self::subscriptions::entity::EntityManager; @@ -53,10 +54,11 @@ use crate::proto::types::member_value::ValueType; use crate::proto::types::LogicalOperator; use crate::proto::world::world_server::WorldServer; use crate::proto::world::{ - RetrieveEntitiesStreamingResponse, RetrieveEventMessagesRequest, SubscribeEntitiesRequest, - SubscribeEntityResponse, SubscribeEventMessagesRequest, SubscribeEventsResponse, - SubscribeIndexerRequest, SubscribeIndexerResponse, UpdateEventMessagesSubscriptionRequest, - WorldMetadataRequest, WorldMetadataResponse, + RetrieveEntitiesStreamingResponse, RetrieveEventMessagesRequest, RetrieveTokenBalancesRequest, + RetrieveTokenBalancesResponse, RetrieveTokensRequest, RetrieveTokensResponse, + SubscribeEntitiesRequest, SubscribeEntityResponse, SubscribeEventMessagesRequest, + SubscribeEventsResponse, SubscribeIndexerRequest, SubscribeIndexerResponse, + UpdateEventMessagesSubscriptionRequest, WorldMetadataRequest, WorldMetadataResponse, }; use crate::proto::{self}; use crate::types::schema::SchemaError; @@ -87,6 +89,29 @@ impl From for Error { } } +impl From for proto::types::Token { + fn from(value: Token) -> Self { + Self { + contract_address: value.contract_address, + name: value.name, + symbol: value.symbol, + decimals: value.decimals as u32, + metadata: serde_json::to_string(&value.metadata).unwrap(), + } + } +} + +impl From for proto::types::TokenBalance { + fn from(value: TokenBalance) -> Self { + Self { + balance: value.balance, + account_address: value.account_address, + contract_address: value.contract_address, + token_id: value.token_id, + } + } +} + #[derive(Debug, Clone)] pub struct DojoWorld { pool: Pool, @@ -789,6 +814,74 @@ impl DojoWorld { }) } + async fn retrieve_tokens( + &self, + contract_addresses: Vec, + ) -> Result { + let query = if contract_addresses.is_empty() { + "SELECT * FROM tokens".to_string() + } else { + format!( + "SELECT * FROM tokens WHERE contract_address IN ({})", + contract_addresses + .iter() + .map(|address| format!("{:#x}", address)) + .collect::>() + .join(", ") + ) + }; + + let tokens: Vec = sqlx::query_as(&query) + .fetch_all(&self.pool) + .await + .map_err(|e| Status::internal(e.to_string()))?; + + let tokens = tokens.iter().map(|token| token.clone().into()).collect(); + Ok(RetrieveTokensResponse { tokens }) + } + + async fn retrieve_token_balances( + &self, + account_addresses: Vec, + contract_addresses: Vec, + ) -> Result { + let mut query = "SELECT * FROM token_balances".to_string(); + + let mut conditions = Vec::new(); + if !account_addresses.is_empty() { + conditions.push(format!( + "account_address IN ({})", + account_addresses + .iter() + .map(|address| format!("{:#x}", address)) + .collect::>() + .join(", ") + )); + } + if !contract_addresses.is_empty() { + conditions.push(format!( + "contract_address IN ({})", + contract_addresses + .iter() + .map(|address| format!("{:#x}", address)) + .collect::>() + .join(", ") + )); + } + + if !conditions.is_empty() { + query += &format!(" WHERE {}", conditions.join(" AND ")); + } + + let balances: Vec = sqlx::query_as(&query) + .fetch_all(&self.pool) + .await + .map_err(|e| Status::internal(e.to_string()))?; + + let balances = balances.iter().map(|balance| balance.clone().into()).collect(); + Ok(RetrieveTokenBalancesResponse { balances }) + } + async fn subscribe_indexer( &self, contract_address: Felt, @@ -1165,6 +1258,45 @@ impl proto::world::world_server::World for DojoWorld { Ok(Response::new(WorldMetadataResponse { metadata })) } + async fn retrieve_tokens( + &self, + request: Request, + ) -> Result, Status> { + let RetrieveTokensRequest { contract_addresses } = request.into_inner(); + let contract_addresses = contract_addresses + .iter() + .map(|address| Felt::from_bytes_be_slice(address)) + .collect::>(); + + let tokens = self + .retrieve_tokens(contract_addresses) + .await + .map_err(|e| Status::internal(e.to_string()))?; + Ok(Response::new(tokens)) + } + + async fn retrieve_token_balances( + &self, + request: Request, + ) -> Result, Status> { + let RetrieveTokenBalancesRequest { account_addresses, contract_addresses } = + request.into_inner(); + let account_addresses = account_addresses + .iter() + .map(|address| Felt::from_bytes_be_slice(address)) + .collect::>(); + let contract_addresses = contract_addresses + .iter() + .map(|address| Felt::from_bytes_be_slice(address)) + .collect::>(); + + let balances = self + .retrieve_token_balances(account_addresses, contract_addresses) + .await + .map_err(|e| Status::internal(e.to_string()))?; + Ok(Response::new(balances)) + } + async fn subscribe_indexer( &self, request: Request,