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-graphql): subscriptions for tokens and token balances #2999

Merged
merged 9 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion crates/torii/graphql/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub const MODEL_ORDER_TYPE_NAME: &str = "World__ModelOrder";
pub const MODEL_ORDER_FIELD_TYPE_NAME: &str = "World__ModelOrderField";
pub const TOKEN_BALANCE_TYPE_NAME: &str = "Token__Balance";
pub const TOKEN_TRANSFER_TYPE_NAME: &str = "Token__Transfer";
pub const TOKEN_TYPE_NAME: &str = "ERC__Token";
pub const TOKEN_UNION_TYPE_NAME: &str = "ERC__Token";
// pub const ERC721_METADATA_TYPE_NAME: &str = "ERC721__Metadata";

pub const ERC20_TYPE_NAME: &str = "ERC20__Token";
Expand Down
15 changes: 12 additions & 3 deletions crates/torii/graphql/src/mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use async_graphql::Name;
use dojo_types::primitive::Primitive;
use lazy_static::lazy_static;

use crate::constants::{CONTENT_TYPE_NAME, SOCIAL_TYPE_NAME, TOKEN_TYPE_NAME};
use crate::constants::{CONTENT_TYPE_NAME, SOCIAL_TYPE_NAME, TOKEN_UNION_TYPE_NAME};
use crate::types::{GraphqlType, TypeData, TypeMapping};

lazy_static! {
Expand Down Expand Up @@ -146,14 +146,14 @@ lazy_static! {
]);

pub static ref TOKEN_BALANCE_TYPE_MAPPING: TypeMapping = IndexMap::from([
(Name::new("tokenMetadata"), TypeData::Nested((TypeRef::named_nn(TOKEN_TYPE_NAME), IndexMap::new()))),
(Name::new("tokenMetadata"), TypeData::Nested((TypeRef::named_nn(TOKEN_UNION_TYPE_NAME), IndexMap::new()))),
]);

pub static ref TOKEN_TRANSFER_TYPE_MAPPING: TypeMapping = IndexMap::from([
(Name::new("from"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))),
(Name::new("to"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))),
(Name::new("executedAt"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))),
(Name::new("tokenMetadata"), TypeData::Nested((TypeRef::named_nn(TOKEN_TYPE_NAME), IndexMap::new()))),
(Name::new("tokenMetadata"), TypeData::Nested((TypeRef::named_nn(TOKEN_UNION_TYPE_NAME), IndexMap::new()))),
(Name::new("transactionHash"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))),
]);

Expand Down Expand Up @@ -190,4 +190,13 @@ lazy_static! {
TypeData::Simple(TypeRef::named_nn(GraphqlType::DateTime.to_string())),
),
]);

pub static ref TOKEN_TYPE_MAPPING: TypeMapping = IndexMap::from([
(Name::new("id"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))),
(Name::new("contractAddress"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))),
(Name::new("name"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))),
(Name::new("symbol"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))),
(Name::new("decimals"), TypeData::Simple(TypeRef::named_nn(TypeRef::INT))),
(Name::new("tokenMetadata"), TypeData::Nested((TypeRef::named_nn(TOKEN_UNION_TYPE_NAME), IndexMap::new()))),
]);
}
150 changes: 147 additions & 3 deletions crates/torii/graphql/src/object/erc/erc_token.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
use async_graphql::dynamic::FieldValue;
use async_graphql::dynamic::{
Field, FieldValue, SubscriptionField, SubscriptionFieldFuture, TypeRef,
};
use async_graphql::{Name, Value};
use sqlx::{Pool, Row, Sqlite};
use tokio_stream::StreamExt;
use torii_sqlite::simple_broker::SimpleBroker;
use torii_sqlite::types::Token;

use crate::constants::{ERC20_TOKEN_NAME, ERC20_TYPE_NAME, ERC721_TOKEN_NAME, ERC721_TYPE_NAME};
use crate::mapping::{ERC20_TOKEN_TYPE_MAPPING, ERC721_TOKEN_TYPE_MAPPING};
use crate::object::BasicObject;
use crate::mapping::{ERC20_TOKEN_TYPE_MAPPING, ERC721_TOKEN_TYPE_MAPPING, TOKEN_TYPE_MAPPING};
use crate::object::{BasicObject, ResolvableObject};
use crate::types::{TypeMapping, ValueMapping};

#[derive(Debug)]
Expand Down Expand Up @@ -107,3 +113,141 @@
}
}
}

#[derive(Debug)]
pub struct TokenObject;

impl BasicObject for TokenObject {
fn name(&self) -> (&str, &str) {
("tokens", "token")
}

fn type_name(&self) -> &str {
"Token"
}

fn type_mapping(&self) -> &TypeMapping {
&TOKEN_TYPE_MAPPING
}
}

impl ResolvableObject for TokenObject {
fn resolvers(&self) -> Vec<Field> {
vec![]
}

fn subscriptions(&self) -> Option<Vec<SubscriptionField>> {
Some(vec![SubscriptionField::new(
"tokenUpdated",
TypeRef::named_nn(self.type_name()),
|ctx| {
SubscriptionFieldFuture::new(async move {
let pool = ctx.data::<Pool<Sqlite>>()?;
Ok(SimpleBroker::<Token>::subscribe()
.then(move |token| {
let pool = pool.clone();
async move {
// Fetch complete token data including contract type
let query = "SELECT t.*, c.contract_type
FROM tokens t
JOIN contracts c ON t.contract_address = \
c.contract_address
WHERE t.id = ?";

Check warning on line 155 in crates/torii/graphql/src/object/erc/erc_token.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/erc_token.rs#L144-L155

Added lines #L144 - L155 were not covered by tests

let row =
match sqlx::query(query).bind(&token.id).fetch_one(&pool).await

Check warning on line 158 in crates/torii/graphql/src/object/erc/erc_token.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/erc_token.rs#L157-L158

Added lines #L157 - L158 were not covered by tests
{
Ok(row) => row,
Err(_) => return None,

Check warning on line 161 in crates/torii/graphql/src/object/erc/erc_token.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/erc_token.rs#L160-L161

Added lines #L160 - L161 were not covered by tests
};

let contract_type: String = row.get("contract_type");
let token_metadata = match contract_type.to_lowercase().as_str() {
"erc20" => {
let token = Erc20Token {
contract_address: row.get("contract_address"),
name: row.get("name"),
symbol: row.get("symbol"),
decimals: row.get("decimals"),
amount: "0".to_string(), // New token has no balance
};
ErcTokenType::Erc20(token)

Check warning on line 174 in crates/torii/graphql/src/object/erc/erc_token.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/erc_token.rs#L164-L174

Added lines #L164 - L174 were not covered by tests
}
"erc721" => {
let metadata_str: String = row.get("metadata");

Check warning on line 177 in crates/torii/graphql/src/object/erc/erc_token.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/erc_token.rs#L176-L177

Added lines #L176 - L177 were not covered by tests
let (
metadata_str,
metadata_name,
metadata_description,
metadata_attributes,
image_path,
) = if metadata_str.is_empty() {
(String::new(), None, None, None, String::new())

Check warning on line 185 in crates/torii/graphql/src/object/erc/erc_token.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/erc_token.rs#L179-L185

Added lines #L179 - L185 were not covered by tests
} else {
let metadata: serde_json::Value =
serde_json::from_str(&metadata_str)
.expect("metadata is always json");
let metadata_name = metadata.get("name").map(|v| {
Comment on lines +411 to +413
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 JSON parsing errors gracefully, sensei!

Using expect for JSON parsing could cause panic in production.

Replace with proper error handling:

-                                            let metadata: serde_json::Value =
-                                                serde_json::from_str(&metadata_str)
-                                                    .expect("metadata is always json");
+                                            let metadata: serde_json::Value = match serde_json::from_str(&metadata_str) {
+                                                Ok(m) => m,
+                                                Err(e) => {
+                                                    tracing::error!("Failed to parse metadata JSON: {}", e);
+                                                    return None;
+                                                }
+                                            };
📝 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
serde_json::from_str(&metadata_str)
.expect("metadata is always json");
let metadata_name = metadata.get("name").map(|v| {
let metadata: serde_json::Value = match serde_json::from_str(&metadata_str) {
Ok(m) => m,
Err(e) => {
tracing::error!("Failed to parse metadata JSON: {}", e);
return None;
}
};
let metadata_name = metadata.get("name").map(|v| {

v.to_string().trim_matches('"').to_string()
});
let metadata_description =
metadata.get("description").map(|v| {
v.to_string().trim_matches('"').to_string()
});
let metadata_attributes =
metadata.get("attributes").map(|v| {
v.to_string().trim_matches('"').to_string()
});

let contract_address: String =
row.get("contract_address");
let image_path = format!("{}/image", contract_address);

(
metadata_str,
metadata_name,
metadata_description,
metadata_attributes,
image_path,
)

Check warning on line 212 in crates/torii/graphql/src/object/erc/erc_token.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/erc_token.rs#L187-L212

Added lines #L187 - L212 were not covered by tests
};

let token = Erc721Token {
name: row.get("name"),
metadata: metadata_str,
contract_address: row.get("contract_address"),
symbol: row.get("symbol"),
token_id: "0".to_string(), /* New token has no
* specific token_id */
metadata_name,
metadata_description,
metadata_attributes,
image_path,
};
ErcTokenType::Erc721(token)

Check warning on line 227 in crates/torii/graphql/src/object/erc/erc_token.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/erc_token.rs#L215-L227

Added lines #L215 - L227 were not covered by tests
}
_ => return None,

Check warning on line 229 in crates/torii/graphql/src/object/erc/erc_token.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/erc_token.rs#L229

Added line #L229 was not covered by tests
};

Some(Ok(FieldValue::value(Value::Object(ValueMapping::from([
(Name::new("id"), Value::String(token.id)),
(
Name::new("contractAddress"),
Value::String(token.contract_address),
),
(Name::new("name"), Value::String(token.name)),
(Name::new("symbol"), Value::String(token.symbol)),
(Name::new("decimals"), Value::Number(token.decimals.into())),
(
Name::new("tokenMetadata"),
token_metadata.to_field_value().as_value().unwrap().clone(),
),
])))))
}
})
.filter_map(|result| result))
})

Check warning on line 249 in crates/torii/graphql/src/object/erc/erc_token.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/erc_token.rs#L232-L249

Added lines #L232 - L249 were not covered by tests
},
)])
}
}
77 changes: 76 additions & 1 deletion crates/torii/graphql/src/object/erc/token_balance.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use async_graphql::connection::PageInfo;
use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef};
use async_graphql::dynamic::{
Field, FieldFuture, FieldValue, InputValue, SubscriptionField, SubscriptionFieldFuture, TypeRef,
};
use convert_case::{Case, Casing};
use serde::Deserialize;
use sqlx::sqlite::SqliteRow;
use sqlx::{FromRow, Pool, Row, Sqlite, SqliteConnection};
use starknet_crypto::Felt;
use tokio_stream::StreamExt;
use torii_sqlite::constants::TOKEN_BALANCE_TABLE;
use torii_sqlite::simple_broker::SimpleBroker;
use torii_sqlite::types::TokenBalance;
use torii_sqlite::utils::felt_to_sql_string;
use tracing::warn;

Expand Down Expand Up @@ -85,6 +90,76 @@
field = connection_arguments(field);
vec![field]
}

fn subscriptions(&self) -> Option<Vec<SubscriptionField>> {
Some(vec![
SubscriptionField::new(
"tokenBalanceUpdated",
TypeRef::named_nn(format!("{}Connection", self.type_name())),
|ctx| {
SubscriptionFieldFuture::new(async move {
let address = match ctx.args.get("accountAddress") {
Some(addr) => Some(addr.string()?.to_string()),
None => None,

Check warning on line 103 in crates/torii/graphql/src/object/erc/token_balance.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/token_balance.rs#L100-L103

Added lines #L100 - L103 were not covered by tests
};

let pool = ctx.data::<Pool<Sqlite>>()?;
Ok(SimpleBroker::<TokenBalance>::subscribe()
.then(move |token_balance| {
let address = address.clone();
let pool = pool.clone();
async move {

Check warning on line 111 in crates/torii/graphql/src/object/erc/token_balance.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/token_balance.rs#L106-L111

Added lines #L106 - L111 were not covered by tests
// Filter by account address if provided
if let Some(addr) = &address {
if token_balance.account_address != *addr {
return None;
}
}

Check warning on line 117 in crates/torii/graphql/src/object/erc/token_balance.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/token_balance.rs#L113-L117

Added lines #L113 - L117 were not covered by tests
// Fetch associated token data
let query = format!(
"SELECT b.id, t.contract_address, t.name, t.symbol, \
t.decimals, b.balance, b.token_id, t.metadata, \
c.contract_type
FROM {} b
JOIN tokens t ON b.token_id = t.id
JOIN contracts c ON t.contract_address = \
c.contract_address
WHERE b.id = ?",
TOKEN_BALANCE_TABLE
);

Check warning on line 129 in crates/torii/graphql/src/object/erc/token_balance.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/token_balance.rs#L119-L129

Added lines #L119 - L129 were not covered by tests

let row = match sqlx::query(&query)
.bind(&token_balance.id)
.fetch_one(&pool)
.await

Check warning on line 134 in crates/torii/graphql/src/object/erc/token_balance.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/token_balance.rs#L131-L134

Added lines #L131 - L134 were not covered by tests
{
Ok(row) => row,
Err(_) => return None,

Check warning on line 137 in crates/torii/graphql/src/object/erc/token_balance.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/token_balance.rs#L136-L137

Added lines #L136 - L137 were not covered by tests
};

// Create connection with single edge
match token_balances_connection_output(
&[row],
1, // total_count
PageInfo {
has_previous_page: false,
has_next_page: false,
start_cursor: None,
end_cursor: None,
},
) {
Ok(value) => Some(Ok(value)),
Err(_) => None,

Check warning on line 152 in crates/torii/graphql/src/object/erc/token_balance.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/token_balance.rs#L141-L152

Added lines #L141 - L152 were not covered by tests
}
}
})
.filter_map(|result| result))
})

Check warning on line 157 in crates/torii/graphql/src/object/erc/token_balance.rs

View check run for this annotation

Codecov / codecov/patch

crates/torii/graphql/src/object/erc/token_balance.rs#L154-L157

Added lines #L154 - L157 were not covered by tests
},
)
.argument(InputValue::new("accountAddress", TypeRef::named(TypeRef::STRING))),
])
}
}

async fn fetch_token_balances(
Expand Down
10 changes: 6 additions & 4 deletions crates/torii/graphql/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ use super::types::ScalarType;
use super::utils;
use crate::constants::{
EMPTY_TYPE_NAME, ERC20_TYPE_NAME, ERC721_TYPE_NAME, QUERY_TYPE_NAME, SUBSCRIPTION_TYPE_NAME,
TOKEN_TYPE_NAME,
TOKEN_UNION_TYPE_NAME,
};
use crate::object::controller::ControllerObject;
use crate::object::empty::EmptyObject;
use crate::object::erc::erc_token::{Erc20TokenObject, Erc721TokenObject};
use crate::object::erc::erc_token::{Erc20TokenObject, Erc721TokenObject, TokenObject};
use crate::object::erc::token_balance::ErcBalanceObject;
use crate::object::erc::token_transfer::ErcTransferObject;
use crate::object::event_message::EventMessageObject;
Expand Down Expand Up @@ -131,15 +131,17 @@ async fn build_objects(pool: &SqlitePool) -> Result<(Vec<ObjectVariant>, Vec<Uni
ObjectVariant::Basic(Box::new(Erc721TokenObject)),
ObjectVariant::Basic(Box::new(Erc20TokenObject)),
ObjectVariant::Basic(Box::new(EmptyObject)),
ObjectVariant::Resolvable(Box::new(TokenObject)),
];

// model union object
let mut unions: Vec<Union> = Vec::new();
let mut model_union = Union::new("ModelUnion");

// erc_token union object
let erc_token_union =
Union::new(TOKEN_TYPE_NAME).possible_type(ERC20_TYPE_NAME).possible_type(ERC721_TYPE_NAME);
let erc_token_union = Union::new(TOKEN_UNION_TYPE_NAME)
.possible_type(ERC20_TYPE_NAME)
.possible_type(ERC721_TYPE_NAME);

unions.push(erc_token_union);

Expand Down
Loading