Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(torii-grpc): erc tokens and balances #2698

Merged
merged 11 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions crates/torii/client/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
use starknet::providers::JsonRpcClient;
use tokio::sync::RwLock as AsyncRwLock;
use torii_grpc::client::{EntityUpdateStreaming, EventUpdateStreaming, IndexerUpdateStreaming};
use torii_grpc::proto::world::{RetrieveEntitiesResponse, RetrieveEventsResponse};
use torii_grpc::proto::world::{
RetrieveEntitiesResponse, RetrieveEventsResponse, RetrieveTokenBalancesResponse,
RetrieveTokensResponse,
};
use torii_grpc::types::schema::Entity;
use torii_grpc::types::{EntityKeysClause, Event, EventQuery, Query};
use torii_grpc::types::{EntityKeysClause, Event, EventQuery, Query, Token, TokenBalance};
use torii_relay::client::EventLoop;
use torii_relay::types::Message;

Expand Down Expand Up @@ -85,6 +88,26 @@
self.metadata.read()
}

/// Retrieves tokens matching contract addresses.
pub async fn tokens(&self, contract_addresses: Vec<Felt>) -> Result<Vec<Token>, Error> {
let mut grpc_client = self.inner.write().await;
let RetrieveTokensResponse { tokens } =
grpc_client.retrieve_tokens(contract_addresses).await?;
Ok(tokens.into_iter().map(TryInto::try_into).collect::<Result<Vec<Token>, _>>()?)
}

Check warning on line 97 in crates/torii/client/src/client/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/client/src/client/mod.rs#L92-L97

Added lines #L92 - L97 were not covered by tests

/// Retrieves token balances for account addresses and contract addresses.
pub async fn token_balances(
&self,
account_addresses: Vec<Felt>,
contract_addresses: Vec<Felt>,
) -> Result<Vec<TokenBalance>, Error> {
let mut grpc_client = self.inner.write().await;
let RetrieveTokenBalancesResponse { balances } =
grpc_client.retrieve_token_balances(account_addresses, contract_addresses).await?;
Ok(balances.into_iter().map(TryInto::try_into).collect::<Result<Vec<TokenBalance>, _>>()?)
}

Check warning on line 109 in crates/torii/client/src/client/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/client/src/client/mod.rs#L100-L109

Added lines #L100 - L109 were not covered by tests

/// Retrieves entities matching query parameter.
///
/// The query param includes an optional clause for filtering. Without clause, it fetches ALL
Expand Down
22 changes: 22 additions & 0 deletions crates/torii/core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,28 @@
pub executed_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}

#[derive(FromRow, Deserialize, Debug, Clone)]

Check warning on line 125 in crates/torii/core/src/types.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/core/src/types.rs#L125

Added line #L125 was not covered by tests
#[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)]

Check warning on line 136 in crates/torii/core/src/types.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/core/src/types.rs#L136

Added line #L136 was not covered by tests
#[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,
Expand Down
15 changes: 15 additions & 0 deletions crates/torii/grpc/proto/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
30 changes: 30 additions & 0 deletions crates/torii/grpc/proto/world.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
49 changes: 44 additions & 5 deletions crates/torii/grpc/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@

use crate::proto::world::{
world_client, RetrieveEntitiesRequest, RetrieveEntitiesResponse, RetrieveEventMessagesRequest,
RetrieveEventsRequest, RetrieveEventsResponse, SubscribeEntitiesRequest,
SubscribeEntityResponse, SubscribeEventMessagesRequest, SubscribeEventsRequest,
SubscribeEventsResponse, SubscribeIndexerRequest, SubscribeIndexerResponse,
SubscribeModelsRequest, SubscribeModelsResponse, UpdateEntitiesSubscriptionRequest,
UpdateEventMessagesSubscriptionRequest, WorldMetadataRequest,
RetrieveEventsRequest, RetrieveEventsResponse, RetrieveTokenBalancesRequest,
RetrieveTokenBalancesResponse, RetrieveTokensRequest, RetrieveTokensResponse,
SubscribeEntitiesRequest, SubscribeEntityResponse, SubscribeEventMessagesRequest,
SubscribeEventsRequest, SubscribeEventsResponse, SubscribeIndexerRequest,
SubscribeIndexerResponse, SubscribeModelsRequest, SubscribeModelsResponse,
UpdateEntitiesSubscriptionRequest, UpdateEventMessagesSubscriptionRequest,
WorldMetadataRequest,
};
use crate::types::schema::{Entity, SchemaError};
use crate::types::{EntityKeysClause, Event, EventQuery, IndexerUpdate, ModelKeysClause, Query};
Expand Down Expand Up @@ -90,6 +92,43 @@
.and_then(|metadata| metadata.try_into().map_err(Error::ParseStr))
}

pub async fn retrieve_tokens(
&mut self,
contract_addresses: Vec<Felt>,
) -> Result<RetrieveTokensResponse, Error> {
self.inner
.retrieve_tokens(RetrieveTokensRequest {
contract_addresses: contract_addresses
.into_iter()
.map(|c| c.to_bytes_be().to_vec())
.collect(),
})
.await
.map_err(Error::Grpc)
.map(|res| res.into_inner())
}

Check warning on line 109 in crates/torii/grpc/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/client.rs#L95-L109

Added lines #L95 - L109 were not covered by tests

pub async fn retrieve_token_balances(
&mut self,
account_addresses: Vec<Felt>,
contract_addresses: Vec<Felt>,
) -> Result<RetrieveTokenBalancesResponse, Error> {
self.inner
.retrieve_token_balances(RetrieveTokenBalancesRequest {
account_addresses: account_addresses
.into_iter()
.map(|a| a.to_bytes_be().to_vec())
.collect(),
contract_addresses: contract_addresses
.into_iter()
.map(|c| c.to_bytes_be().to_vec())
.collect(),
})
.await
.map_err(Error::Grpc)
.map(|res| res.into_inner())
}

Check warning on line 130 in crates/torii/grpc/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/client.rs#L111-L130

Added lines #L111 - L130 were not covered by tests

pub async fn retrieve_entities(
&mut self,
query: Query,
Expand Down
140 changes: 136 additions & 4 deletions crates/torii/grpc/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
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;
Expand All @@ -53,10 +54,11 @@
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;
Expand Down Expand Up @@ -87,6 +89,29 @@
}
}

impl From<Token> 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(),
Larkooo marked this conversation as resolved.
Show resolved Hide resolved
}
}

Check warning on line 101 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L93-L101

Added lines #L93 - L101 were not covered by tests
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential serialization errors in Token conversion.

The serde_json::to_string(&value.metadata).unwrap() call could panic if the metadata is not valid JSON. Consider proper error handling.

-            metadata: serde_json::to_string(&value.metadata).unwrap(),
+            metadata: serde_json::to_string(&value.metadata)
+                .map_err(|e| Status::internal(format!("Failed to serialize metadata: {}", e)))?,

Committable suggestion skipped: line range outside the PR's diff.


impl From<TokenBalance> 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,
}
}

Check warning on line 112 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L105-L112

Added lines #L105 - L112 were not covered by tests
}

#[derive(Debug, Clone)]
pub struct DojoWorld {
pool: Pool<Sqlite>,
Expand Down Expand Up @@ -789,6 +814,74 @@
})
}

async fn retrieve_tokens(
&self,
contract_addresses: Vec<Felt>,
) -> Result<RetrieveTokensResponse, Status> {
let query = if contract_addresses.is_empty() {
"SELECT * FROM tokens".to_string()

Check warning on line 822 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L817-L822

Added lines #L817 - L822 were not covered by tests
} else {
format!(
"SELECT * FROM tokens WHERE contract_address IN ({})",
contract_addresses
.iter()
.map(|address| format!("{:#x}", address))
Larkooo marked this conversation as resolved.
Show resolved Hide resolved
.collect::<Vec<_>>()
.join(", ")
)

Check warning on line 831 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L824-L831

Added lines #L824 - L831 were not covered by tests
};

let tokens: Vec<Token> = sqlx::query_as(&query)
.fetch_all(&self.pool)
.await
.map_err(|e| Status::internal(e.to_string()))?;

Check warning on line 837 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L834-L837

Added lines #L834 - L837 were not covered by tests

let tokens = tokens.iter().map(|token| token.clone().into()).collect();
Ok(RetrieveTokensResponse { tokens })
}

Check warning on line 841 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L839-L841

Added lines #L839 - L841 were not covered by tests
Comment on lines +817 to +841
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ohayo sensei! Address SQL injection and input validation in retrieve_tokens.

The current implementation constructs SQL queries using format!, which can lead to SQL injection vulnerabilities. Additionally, there's no input validation or limit on the number of contract addresses.

Apply this diff to fix the issues:

 async fn retrieve_tokens(
     &self,
     contract_addresses: Vec<Felt>,
 ) -> Result<RetrieveTokensResponse, Status> {
+    const MAX_ADDRESSES: usize = 100;
+    if contract_addresses.len() > MAX_ADDRESSES {
+        return Err(Status::invalid_argument(
+            format!("Too many addresses. Maximum allowed: {}", MAX_ADDRESSES)
+        ));
+    }
+
+    if contract_addresses.is_empty() {
+        return Err(Status::invalid_argument("No contract addresses provided"));
+    }
+
+    let placeholders = contract_addresses.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
+    let query = format!("SELECT * FROM tokens WHERE contract_address IN ({})", placeholders);
+
+    let mut query_builder = sqlx::query_as(&query);
+    for address in contract_addresses {
+        query_builder = query_builder.bind(format!("{:#x}", address));
+    }
-
-    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::<Vec<_>>()
-                .join(", ")
-        )
-    };
-
-    let tokens: Vec<Token> = sqlx::query_as(&query)
+    let tokens: Vec<Token> = query_builder
         .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 })
 }

Would you like assistance implementing these changes?

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn retrieve_tokens(
&self,
contract_addresses: Vec<Felt>,
) -> Result<RetrieveTokensResponse, Status> {
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::<Vec<_>>()
.join(", ")
)
};
let tokens: Vec<Token> = 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_tokens(
&self,
contract_addresses: Vec<Felt>,
) -> Result<RetrieveTokensResponse, Status> {
const MAX_ADDRESSES: usize = 100;
if contract_addresses.len() > MAX_ADDRESSES {
return Err(Status::invalid_argument(
format!("Too many addresses. Maximum allowed: {}", MAX_ADDRESSES)
));
}
if contract_addresses.is_empty() {
return Err(Status::invalid_argument("No contract addresses provided"));
}
let placeholders = contract_addresses.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
let query = format!("SELECT * FROM tokens WHERE contract_address IN ({})", placeholders);
let mut query_builder = sqlx::query_as(&query);
for address in contract_addresses {
query_builder = query_builder.bind(format!("{:#x}", address));
}
let tokens: Vec<Token> = query_builder
.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<Felt>,
contract_addresses: Vec<Felt>,
) -> Result<RetrieveTokenBalancesResponse, Status> {
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::<Vec<_>>()
.join(", ")
));
}
if !contract_addresses.is_empty() {
conditions.push(format!(
"contract_address IN ({})",
contract_addresses
.iter()
.map(|address| format!("{:#x}", address))
.collect::<Vec<_>>()
.join(", ")
));
}

Check warning on line 870 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L843-L870

Added lines #L843 - L870 were not covered by tests

if !conditions.is_empty() {
query += &format!(" WHERE {}", conditions.join(" AND "));
}

Check warning on line 874 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L872-L874

Added lines #L872 - L874 were not covered by tests

let balances: Vec<TokenBalance> = sqlx::query_as(&query)
.fetch_all(&self.pool)
.await
.map_err(|e| Status::internal(e.to_string()))?;

Check warning on line 879 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L876-L879

Added lines #L876 - L879 were not covered by tests

let balances = balances.iter().map(|balance| balance.clone().into()).collect();
Ok(RetrieveTokenBalancesResponse { balances })
}

Check warning on line 883 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L881-L883

Added lines #L881 - L883 were not covered by tests
Comment on lines +843 to +883
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ohayo sensei! Secure retrieve_token_balances and improve error handling.

Similar to retrieve_tokens, constructing SQL queries using format! can cause SQL injection vulnerabilities. Additionally, there's insufficient input validation and no limit on the number of addresses.

Apply this diff to address the issues:

 async fn retrieve_token_balances(
     &self,
     account_addresses: Vec<Felt>,
     contract_addresses: Vec<Felt>,
 ) -> Result<RetrieveTokenBalancesResponse, Status> {
+    const MAX_ADDRESSES: usize = 100;
+    if account_addresses.is_empty() || contract_addresses.is_empty() {
+        return Err(Status::invalid_argument("Both account and contract addresses are required"));
+    }
+    if account_addresses.len() > MAX_ADDRESSES || contract_addresses.len() > MAX_ADDRESSES {
+        return Err(Status::invalid_argument(
+            format!("Too many addresses. Maximum allowed: {}", MAX_ADDRESSES)
+        ));
+    }
+
+    let account_placeholders = account_addresses.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
+    let contract_placeholders = contract_addresses.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
+
+    let query = format!(
+        "SELECT * FROM token_balances WHERE account_address IN ({}) AND contract_address IN ({})",
+        account_placeholders, contract_placeholders
+    );
+
+    let mut query_builder = sqlx::query_as(&query);
+    for address in account_addresses {
+        query_builder = query_builder.bind(format!("{:#x}", address));
+    }
+    for address in contract_addresses {
+        query_builder = query_builder.bind(format!("{:#x}", address));
+    }
-
-    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::<Vec<_>>()
-                .join(", ")
-        ));
-    }
-    if !contract_addresses.is_empty() {
-        conditions.push(format!(
-            "contract_address IN ({})",
-            contract_addresses
-                .iter()
-                .map(|address| format!("{:#x}", address))
-                .collect::<Vec<_>>()
-                .join(", ")
-        ));
-    }
-
-    if !conditions.is_empty() {
-        query += &format!(" WHERE {}", conditions.join(" AND "));
-    }
-
-    let balances: Vec<TokenBalance> = sqlx::query_as(&query)
+    let balances: Vec<TokenBalance> = query_builder
         .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 })
 }

Let me know if you'd like help applying these changes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn retrieve_token_balances(
&self,
account_addresses: Vec<Felt>,
contract_addresses: Vec<Felt>,
) -> Result<RetrieveTokenBalancesResponse, Status> {
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::<Vec<_>>()
.join(", ")
));
}
if !contract_addresses.is_empty() {
conditions.push(format!(
"contract_address IN ({})",
contract_addresses
.iter()
.map(|address| format!("{:#x}", address))
.collect::<Vec<_>>()
.join(", ")
));
}
if !conditions.is_empty() {
query += &format!(" WHERE {}", conditions.join(" AND "));
}
let balances: Vec<TokenBalance> = 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 retrieve_token_balances(
&self,
account_addresses: Vec<Felt>,
contract_addresses: Vec<Felt>,
) -> Result<RetrieveTokenBalancesResponse, Status> {
const MAX_ADDRESSES: usize = 100;
if account_addresses.is_empty() || contract_addresses.is_empty() {
return Err(Status::invalid_argument("Both account and contract addresses are required"));
}
if account_addresses.len() > MAX_ADDRESSES || contract_addresses.len() > MAX_ADDRESSES {
return Err(Status::invalid_argument(
format!("Too many addresses. Maximum allowed: {}", MAX_ADDRESSES)
));
}
let account_placeholders = account_addresses.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
let contract_placeholders = contract_addresses.iter().map(|_| "?").collect::<Vec<_>>().join(", ");
let query = format!(
"SELECT * FROM token_balances WHERE account_address IN ({}) AND contract_address IN ({})",
account_placeholders, contract_placeholders
);
let mut query_builder = sqlx::query_as(&query);
for address in account_addresses {
query_builder = query_builder.bind(format!("{:#x}", address));
}
for address in contract_addresses {
query_builder = query_builder.bind(format!("{:#x}", address));
}
let balances: Vec<TokenBalance> = query_builder
.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,
Expand Down Expand Up @@ -1165,6 +1258,45 @@
Ok(Response::new(WorldMetadataResponse { metadata }))
}

async fn retrieve_tokens(
&self,
request: Request<RetrieveTokensRequest>,
) -> Result<Response<RetrieveTokensResponse>, Status> {
let RetrieveTokensRequest { contract_addresses } = request.into_inner();
let contract_addresses = contract_addresses
.iter()
.map(|address| Felt::from_bytes_be_slice(address))
.collect::<Vec<_>>();

Comment on lines +1265 to +1270
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ohayo sensei! Enhance error handling for retrieve_tokens endpoint.

The current implementation doesn't validate input contract addresses and lacks error handling for Felt conversions, which can cause silent failures.

Apply this diff to improve error handling:

 let RetrieveTokensRequest { contract_addresses } = request.into_inner();
+if contract_addresses.is_empty() {
+    return Err(Status::invalid_argument("No contract addresses provided"));
+}
 
 let contract_addresses = contract_addresses
     .iter()
-    .map(|address| Felt::from_bytes_be_slice(address))
-    .collect::<Vec<_>>();
+    .map(|address| {
+        Felt::from_bytes_be_slice(address).map_err(|e| {
+            Status::invalid_argument(format!("Invalid contract address: {}", e))
+        })
+    })
+    .collect::<Result<Vec<_>, Status>>()?;
 
 let tokens = self
     .retrieve_tokens(contract_addresses)
     .await
     .map_err(|e| Status::internal(e.to_string()))?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let RetrieveTokensRequest { contract_addresses } = request.into_inner();
let contract_addresses = contract_addresses
.iter()
.map(|address| Felt::from_bytes_be_slice(address))
.collect::<Vec<_>>();
let RetrieveTokensRequest { contract_addresses } = request.into_inner();
if contract_addresses.is_empty() {
return Err(Status::invalid_argument("No contract addresses provided"));
}
let contract_addresses = contract_addresses
.iter()
.map(|address| {
Felt::from_bytes_be_slice(address).map_err(|e| {
Status::invalid_argument(format!("Invalid contract address: {}", e))
})
})
.collect::<Result<Vec<_>, Status>>()?;
let tokens = self
.retrieve_tokens(contract_addresses)
.await
.map_err(|e| Status::internal(e.to_string()))?;

let tokens = self
.retrieve_tokens(contract_addresses)
.await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(tokens))
}

Check warning on line 1276 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L1264-L1276

Added lines #L1264 - L1276 were not covered by tests
Comment on lines +1261 to +1276
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ohayo sensei! Enhance error handling for the gRPC endpoint.

The implementation needs better input validation and error handling:

  1. No validation of input contract addresses
  2. Silent failures during Felt conversion

Consider this improved implementation:

async fn retrieve_tokens(
    &self,
    request: Request<RetrieveTokensRequest>,
) -> Result<Response<RetrieveTokensResponse>, Status> {
    let RetrieveTokensRequest { contract_addresses } = request.into_inner();
+    if contract_addresses.is_empty() {
+        return Err(Status::invalid_argument("No contract addresses provided"));
+    }
+
    let contract_addresses = contract_addresses
        .iter()
-        .map(|address| Felt::from_bytes_be_slice(address))
+        .map(|address| {
+            Felt::from_bytes_be_slice(address).map_err(|e| {
+                Status::invalid_argument(format!("Invalid contract address: {}", e))
+            })
+        })
+        .collect::<Result<Vec<_>, Status>>()?;

    let tokens = self
        .retrieve_tokens(contract_addresses)
        .await
        .map_err(|e| Status::internal(e.to_string()))?;
    Ok(Response::new(tokens))
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn retrieve_tokens(
&self,
request: Request<RetrieveTokensRequest>,
) -> Result<Response<RetrieveTokensResponse>, Status> {
let RetrieveTokensRequest { contract_addresses } = request.into_inner();
let contract_addresses = contract_addresses
.iter()
.map(|address| Felt::from_bytes_be_slice(address))
.collect::<Vec<_>>();
let tokens = self
.retrieve_tokens(contract_addresses)
.await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(tokens))
}
async fn retrieve_tokens(
&self,
request: Request<RetrieveTokensRequest>,
) -> Result<Response<RetrieveTokensResponse>, Status> {
let RetrieveTokensRequest { contract_addresses } = request.into_inner();
if contract_addresses.is_empty() {
return Err(Status::invalid_argument("No contract addresses provided"));
}
let contract_addresses = contract_addresses
.iter()
.map(|address| {
Felt::from_bytes_be_slice(address).map_err(|e| {
Status::invalid_argument(format!("Invalid contract address: {}", e))
})
})
.collect::<Result<Vec<_>, Status>>()?;
let tokens = self
.retrieve_tokens(contract_addresses)
.await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(tokens))
}


Comment on lines +1261 to +1277
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enhance error handling for the gRPC endpoint.

The implementation needs better input validation and error handling:

  1. No validation of input contract addresses
  2. Silent failures during Felt conversion

Consider this improved implementation:

 async fn retrieve_tokens(
     &self,
     request: Request<RetrieveTokensRequest>,
 ) -> Result<Response<RetrieveTokensResponse>, Status> {
     let RetrieveTokensRequest { contract_addresses } = request.into_inner();
+    if contract_addresses.is_empty() {
+        return Err(Status::invalid_argument("No contract addresses provided"));
+    }
+
     let contract_addresses = contract_addresses
         .iter()
-        .map(|address| Felt::from_bytes_be_slice(address))
+        .map(|address| {
+            Felt::from_bytes_be_slice(address).map_err(|e| {
+                Status::invalid_argument(format!("Invalid contract address: {}", e))
+            })
+        })
+        .collect::<Result<Vec<_>, Status>>()?;

     let tokens = self
         .retrieve_tokens(contract_addresses)
         .await
         .map_err(|e| Status::internal(e.to_string()))?;
     Ok(Response::new(tokens))
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn retrieve_tokens(
&self,
request: Request<RetrieveTokensRequest>,
) -> Result<Response<RetrieveTokensResponse>, Status> {
let RetrieveTokensRequest { contract_addresses } = request.into_inner();
let contract_addresses = contract_addresses
.iter()
.map(|address| Felt::from_bytes_be_slice(address))
.collect::<Vec<_>>();
let tokens = self
.retrieve_tokens(contract_addresses)
.await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(tokens))
}
async fn retrieve_tokens(
&self,
request: Request<RetrieveTokensRequest>,
) -> Result<Response<RetrieveTokensResponse>, Status> {
let RetrieveTokensRequest { contract_addresses } = request.into_inner();
if contract_addresses.is_empty() {
return Err(Status::invalid_argument("No contract addresses provided"));
}
let contract_addresses = contract_addresses
.iter()
.map(|address| {
Felt::from_bytes_be_slice(address).map_err(|e| {
Status::invalid_argument(format!("Invalid contract address: {}", e))
})
})
.collect::<Result<Vec<_>, Status>>()?;
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<RetrieveTokenBalancesRequest>,
) -> Result<Response<RetrieveTokenBalancesResponse>, 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::<Vec<_>>();
let contract_addresses = contract_addresses
.iter()
.map(|address| Felt::from_bytes_be_slice(address))
.collect::<Vec<_>>();

Comment on lines +1282 to +1292
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ohayo sensei! Improve error handling for retrieve_token_balances endpoint.

There's a need to validate input addresses and handle potential conversion errors to prevent silent failures.

Apply this diff to enhance error handling:

 let RetrieveTokenBalancesRequest { account_addresses, contract_addresses } =
     request.into_inner();
+if account_addresses.is_empty() || contract_addresses.is_empty() {
+    return Err(Status::invalid_argument("Both account and contract addresses are required"));
+}
 
 let account_addresses = account_addresses
     .iter()
-    .map(|address| Felt::from_bytes_be_slice(address))
-    .collect::<Vec<_>>();
+    .map(|address| {
+        Felt::from_bytes_be_slice(address).map_err(|e| {
+            Status::invalid_argument(format!("Invalid account address: {}", e))
+        })
+    })
+    .collect::<Result<Vec<_>, Status>>()?;
 
 let contract_addresses = contract_addresses
     .iter()
-    .map(|address| Felt::from_bytes_be_slice(address))
-    .collect::<Vec<_>>();
+    .map(|address| {
+        Felt::from_bytes_be_slice(address).map_err(|e| {
+            Status::invalid_argument(format!("Invalid contract address: {}", e))
+        })
+    })
+    .collect::<Result<Vec<_>, Status>>()?;
 
 let balances = self
     .retrieve_token_balances(account_addresses, contract_addresses)
     .await
     .map_err(|e| Status::internal(e.to_string()))?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let RetrieveTokenBalancesRequest { account_addresses, contract_addresses } =
request.into_inner();
let account_addresses = account_addresses
.iter()
.map(|address| Felt::from_bytes_be_slice(address))
.collect::<Vec<_>>();
let contract_addresses = contract_addresses
.iter()
.map(|address| Felt::from_bytes_be_slice(address))
.collect::<Vec<_>>();
let RetrieveTokenBalancesRequest { account_addresses, contract_addresses } =
request.into_inner();
if account_addresses.is_empty() || contract_addresses.is_empty() {
return Err(Status::invalid_argument("Both account and contract addresses are required"));
}
let account_addresses = account_addresses
.iter()
.map(|address| {
Felt::from_bytes_be_slice(address).map_err(|e| {
Status::invalid_argument(format!("Invalid account address: {}", e))
})
})
.collect::<Result<Vec<_>, Status>>()?;
let contract_addresses = contract_addresses
.iter()
.map(|address| {
Felt::from_bytes_be_slice(address).map_err(|e| {
Status::invalid_argument(format!("Invalid contract address: {}", e))
})
})
.collect::<Result<Vec<_>, Status>>()?;

let balances = self
.retrieve_token_balances(account_addresses, contract_addresses)
.await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(balances))
}

Check warning on line 1298 in crates/torii/grpc/src/server/mod.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/grpc/src/server/mod.rs#L1281-L1298

Added lines #L1281 - L1298 were not covered by tests
Comment on lines +1278 to +1298
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ohayo sensei! Enhance error handling for address conversions.

The implementation needs better error handling for Felt conversions:

Consider this improved implementation:

async fn retrieve_token_balances(
    &self,
    request: Request<RetrieveTokenBalancesRequest>,
) -> Result<Response<RetrieveTokenBalancesResponse>, Status> {
    let RetrieveTokenBalancesRequest { account_addresses, contract_addresses } =
        request.into_inner();
+    if account_addresses.is_empty() || contract_addresses.is_empty() {
+        return Err(Status::invalid_argument("Both account and contract addresses are required"));
+    }
    let account_addresses = account_addresses
        .iter()
-        .map(|address| Felt::from_bytes_be_slice(address))
+        .map(|address| {
+            Felt::from_bytes_be_slice(address).map_err(|e| {
+                Status::invalid_argument(format!("Invalid account address: {}", e))
+            })
+        })
+        .collect::<Result<Vec<_>, Status>>()?;
    let contract_addresses = contract_addresses
        .iter()
-        .map(|address| Felt::from_bytes_be_slice(address))
+        .map(|address| {
+            Felt::from_bytes_be_slice(address).map_err(|e| {
+                Status::invalid_argument(format!("Invalid contract address: {}", e))
+            })
+        })
+        .collect::<Result<Vec<_>, Status>>()?;

    let balances = self
        .retrieve_token_balances(account_addresses, contract_addresses)
        .await
        .map_err(|e| Status::internal(e.to_string()))?;
    Ok(Response::new(balances))
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn retrieve_token_balances(
&self,
request: Request<RetrieveTokenBalancesRequest>,
) -> Result<Response<RetrieveTokenBalancesResponse>, 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::<Vec<_>>();
let contract_addresses = contract_addresses
.iter()
.map(|address| Felt::from_bytes_be_slice(address))
.collect::<Vec<_>>();
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 retrieve_token_balances(
&self,
request: Request<RetrieveTokenBalancesRequest>,
) -> Result<Response<RetrieveTokenBalancesResponse>, Status> {
let RetrieveTokenBalancesRequest { account_addresses, contract_addresses } =
request.into_inner();
if account_addresses.is_empty() || contract_addresses.is_empty() {
return Err(Status::invalid_argument("Both account and contract addresses are required"));
}
let account_addresses = account_addresses
.iter()
.map(|address| {
Felt::from_bytes_be_slice(address).map_err(|e| {
Status::invalid_argument(format!("Invalid account address: {}", e))
})
})
.collect::<Result<Vec<_>, Status>>()?;
let contract_addresses = contract_addresses
.iter()
.map(|address| {
Felt::from_bytes_be_slice(address).map_err(|e| {
Status::invalid_argument(format!("Invalid contract address: {}", e))
})
})
.collect::<Result<Vec<_>, Status>>()?;
let balances = self
.retrieve_token_balances(account_addresses, contract_addresses)
.await
.map_err(|e| Status::internal(e.to_string()))?;
Ok(Response::new(balances))
}


Comment on lines +1278 to +1299
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ohayo sensei! Enhance error handling for address conversions.

The implementation needs better error handling for Felt conversions:

Consider this improved implementation:

async fn retrieve_token_balances(
    &self,
    request: Request<RetrieveTokenBalancesRequest>,
) -> Result<Response<RetrieveTokenBalancesResponse>, Status> {
    let RetrieveTokenBalancesRequest { account_addresses, contract_addresses } =
        request.into_inner();
    let account_addresses = account_addresses
        .iter()
-        .map(|address| Felt::from_bytes_be_slice(address))
+        .map(|address| {
+            Felt::from_bytes_be_slice(address).map_err(|e| {
+                Status::invalid_argument(format!("Invalid account address: {}", e))
+            })
+        })
+        .collect::<Result<Vec<_>, Status>>()?;
-        .collect::<Vec<_>>();
    let contract_addresses = contract_addresses
        .iter()
-        .map(|address| Felt::from_bytes_be_slice(address))
+        .map(|address| {
+            Felt::from_bytes_be_slice(address).map_err(|e| {
+                Status::invalid_argument(format!("Invalid contract address: {}", e))
+            })
+        })
+        .collect::<Result<Vec<_>, Status>>()?;
-        .collect::<Vec<_>>();

    let balances = self
        .retrieve_token_balances(account_addresses, contract_addresses)
        .await
        .map_err(|e| Status::internal(e.to_string()))?;
    Ok(Response::new(balances))
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn retrieve_token_balances(
&self,
request: Request<RetrieveTokenBalancesRequest>,
) -> Result<Response<RetrieveTokenBalancesResponse>, 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::<Vec<_>>();
let contract_addresses = contract_addresses
.iter()
.map(|address| Felt::from_bytes_be_slice(address))
.collect::<Vec<_>>();
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 retrieve_token_balances(
&self,
request: Request<RetrieveTokenBalancesRequest>,
) -> Result<Response<RetrieveTokenBalancesResponse>, Status> {
let RetrieveTokenBalancesRequest { account_addresses, contract_addresses } =
request.into_inner();
let account_addresses = account_addresses
.iter()
.map(|address| {
Felt::from_bytes_be_slice(address).map_err(|e| {
Status::invalid_argument(format!("Invalid account address: {}", e))
})
})
.collect::<Result<Vec<_>, Status>>()?;
let contract_addresses = contract_addresses
.iter()
.map(|address| {
Felt::from_bytes_be_slice(address).map_err(|e| {
Status::invalid_argument(format!("Invalid contract address: {}", e))
})
})
.collect::<Result<Vec<_>, Status>>()?;
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<SubscribeIndexerRequest>,
Expand Down
Loading
Loading