From 013f994524e2f94d3441f9de511d2ff903567119 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Sun, 3 Nov 2024 20:38:47 +0530 Subject: [PATCH] feat(torii/graphql): use union types for token balances and transfers commit-id:73b9b2b4 --- Cargo.lock | 28 ---- crates/torii/graphql/src/constants.rs | 12 +- crates/torii/graphql/src/mapping.rs | 35 ++--- .../torii/graphql/src/object/erc/erc_token.rs | 99 +++++++++++-- crates/torii/graphql/src/object/erc/mod.rs | 15 ++ .../graphql/src/object/erc/token_balance.rs | 104 +++++-------- .../graphql/src/object/erc/token_transfer.rs | 139 ++++++++---------- crates/torii/graphql/src/object/mod.rs | 135 ++++++++++++++++- crates/torii/graphql/src/schema.rs | 16 +- 9 files changed, 368 insertions(+), 215 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4f92c298e..84744fddd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10099,34 +10099,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" -[[package]] -name = "notify" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" -dependencies = [ - "bitflags 2.6.0", - "filetime", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.52.0", -] - -[[package]] -name = "notify-types" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7393c226621f817964ffb3dc5704f9509e107a8b024b489cc2c1b217378785df" -dependencies = [ - "instant", -] - [[package]] name = "ntapi" version = "0.4.1" diff --git a/crates/torii/graphql/src/constants.rs b/crates/torii/graphql/src/constants.rs index 1e89a0bea5..f309b82b91 100644 --- a/crates/torii/graphql/src/constants.rs +++ b/crates/torii/graphql/src/constants.rs @@ -36,7 +36,10 @@ 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 ERC721_METADATA_TYPE_NAME: &str = "ERC721__Metadata"; +// pub const ERC721_METADATA_TYPE_NAME: &str = "ERC721__Metadata"; + +pub const ERC20_TYPE_NAME: &str = "ERC20__Token"; +pub const ERC721_TYPE_NAME: &str = "ERC721__Token"; // objects' single and plural names pub const ENTITY_NAMES: (&str, &str) = ("entity", "entities"); @@ -48,11 +51,14 @@ pub const CONTENT_NAMES: (&str, &str) = ("content", "contents"); pub const METADATA_NAMES: (&str, &str) = ("metadata", "metadatas"); pub const TRANSACTION_NAMES: (&str, &str) = ("transaction", "transactions"); pub const PAGE_INFO_NAMES: (&str, &str) = ("pageInfo", ""); -pub const TOKEN_NAME: (&str, &str) = ("token", "tokens"); + +pub const ERC20_TOKEN_NAME: (&str, &str) = ("erc20Token", ""); +pub const ERC721_TOKEN_NAME: (&str, &str) = ("erc721Token", ""); + pub const TOKEN_BALANCE_NAME: (&str, &str) = ("", "tokenBalances"); pub const TOKEN_TRANSFER_NAME: (&str, &str) = ("", "tokenTransfers"); -pub const ERC721_METADATA_NAME: (&str, &str) = ("erc721Metadata", ""); +// pub const ERC721_METADATA_NAME: (&str, &str) = ("erc721Metadata", ""); // misc pub const ORDER_DIR_TYPE_NAME: &str = "OrderDirection"; diff --git a/crates/torii/graphql/src/mapping.rs b/crates/torii/graphql/src/mapping.rs index adb8b37bc1..190cbf4b62 100644 --- a/crates/torii/graphql/src/mapping.rs +++ b/crates/torii/graphql/src/mapping.rs @@ -4,9 +4,7 @@ use async_graphql::Name; use dojo_types::primitive::Primitive; use lazy_static::lazy_static; -use crate::constants::{ - CONTENT_TYPE_NAME, ERC721_METADATA_TYPE_NAME, SOCIAL_TYPE_NAME, TOKEN_TYPE_NAME, -}; +use crate::constants::{CONTENT_TYPE_NAME, SOCIAL_TYPE_NAME, TOKEN_TYPE_NAME}; use crate::types::{GraphqlType, TypeData, TypeMapping}; lazy_static! { @@ -148,38 +146,35 @@ lazy_static! { ]); pub static ref TOKEN_BALANCE_TYPE_MAPPING: TypeMapping = IndexMap::from([ - (Name::new("balance"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("type"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("tokenMetadata"), TypeData::Simple(TypeRef::named_nn(TOKEN_TYPE_NAME))), + (Name::new("tokenMetadata"), TypeData::Nested((TypeRef::named_nn(TOKEN_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("amount"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("type"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), (Name::new("executedAt"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("tokenMetadata"), TypeData::Simple(TypeRef::named_nn(TOKEN_TYPE_NAME))), + (Name::new("tokenMetadata"), TypeData::Nested((TypeRef::named_nn(TOKEN_TYPE_NAME), IndexMap::new()))), (Name::new("transactionHash"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), ]); - pub static ref TOKEN_TYPE_MAPPING: TypeMapping = IndexMap::from([ + pub static ref ERC20_TOKEN_TYPE_MAPPING: TypeMapping = IndexMap::from([ (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::STRING))), (Name::new("contractAddress"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("decimals"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), - ( - Name::new("erc721"), - TypeData::Nested((TypeRef::named(ERC721_METADATA_TYPE_NAME), IndexMap::new())) - ), + (Name::new("amount"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), ]); - pub static ref ERC721_METADATA_TYPE_MAPPING: TypeMapping = IndexMap::from([ - (Name::new("tokenId"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), + pub static ref ERC721_TOKEN_TYPE_MAPPING: TypeMapping = IndexMap::from([ (Name::new("name"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("description"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("attributes"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("imagePath"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), + (Name::new("symbol"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), + (Name::new("tokenId"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), + (Name::new("contractAddress"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), (Name::new("metadata"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), + (Name::new("metadataName"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), + (Name::new("metadataDescription"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), + (Name::new("metadataAttributes"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), + (Name::new("imagePath"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), ]); + } diff --git a/crates/torii/graphql/src/object/erc/erc_token.rs b/crates/torii/graphql/src/object/erc/erc_token.rs index d1a312b932..a3baeb10a6 100644 --- a/crates/torii/graphql/src/object/erc/erc_token.rs +++ b/crates/torii/graphql/src/object/erc/erc_token.rs @@ -1,40 +1,109 @@ -use crate::constants::{ - ERC721_METADATA_NAME, ERC721_METADATA_TYPE_NAME, TOKEN_NAME, TOKEN_TYPE_NAME, -}; -use crate::mapping::{ERC721_METADATA_TYPE_MAPPING, TOKEN_TYPE_MAPPING}; +use async_graphql::dynamic::FieldValue; +use async_graphql::{Name, Value}; + +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::types::TypeMapping; +use crate::types::{TypeMapping, ValueMapping}; #[derive(Debug)] -pub struct ErcTokenObject; +pub struct Erc20TokenObject; -impl BasicObject for ErcTokenObject { +impl BasicObject for Erc20TokenObject { fn name(&self) -> (&str, &str) { - TOKEN_NAME + ERC20_TOKEN_NAME } fn type_name(&self) -> &str { - TOKEN_TYPE_NAME + ERC20_TYPE_NAME } fn type_mapping(&self) -> &TypeMapping { - &TOKEN_TYPE_MAPPING + &ERC20_TOKEN_TYPE_MAPPING } } #[derive(Debug)] -pub struct Erc721MetadataObject; +pub struct Erc721TokenObject; -impl BasicObject for Erc721MetadataObject { +impl BasicObject for Erc721TokenObject { fn name(&self) -> (&str, &str) { - ERC721_METADATA_NAME + ERC721_TOKEN_NAME } fn type_name(&self) -> &str { - ERC721_METADATA_TYPE_NAME + ERC721_TYPE_NAME } fn type_mapping(&self) -> &TypeMapping { - &ERC721_METADATA_TYPE_MAPPING + &ERC721_TOKEN_TYPE_MAPPING + } +} + +#[derive(Debug, Clone)] +pub enum ErcTokenType { + Erc20(Erc20Token), + Erc721(Erc721Token), +} + +#[derive(Debug, Clone)] +pub struct Erc20Token { + pub name: String, + pub symbol: String, + pub decimals: u8, + pub contract_address: String, + pub amount: String, +} + +#[derive(Debug, Clone)] +pub struct Erc721Token { + pub name: String, + pub symbol: String, + pub token_id: String, + pub contract_address: String, + pub metadata: String, + pub metadata_name: Option, + pub metadata_description: Option, + pub metadata_attributes: Option, + pub image_path: String, +} + +impl ErcTokenType { + pub fn to_field_value<'a>(self) -> FieldValue<'a> { + match self { + ErcTokenType::Erc20(token) => FieldValue::with_type( + FieldValue::value(Value::Object(ValueMapping::from([ + (Name::new("name"), Value::String(token.name)), + (Name::new("symbol"), Value::String(token.symbol)), + (Name::new("decimals"), Value::from(token.decimals)), + (Name::new("contractAddress"), Value::String(token.contract_address)), + (Name::new("amount"), Value::String(token.amount)), + ]))), + ERC20_TYPE_NAME.to_string(), + ), + ErcTokenType::Erc721(token) => FieldValue::with_type( + FieldValue::value(Value::Object(ValueMapping::from([ + (Name::new("name"), Value::String(token.name)), + (Name::new("symbol"), Value::String(token.symbol)), + (Name::new("tokenId"), Value::String(token.token_id)), + (Name::new("contractAddress"), Value::String(token.contract_address)), + (Name::new("metadata"), Value::String(token.metadata)), + ( + Name::new("metadataName"), + token.metadata_name.map(Value::String).unwrap_or(Value::Null), + ), + ( + Name::new("metadataDescription"), + token.metadata_description.map(Value::String).unwrap_or(Value::Null), + ), + ( + Name::new("metadataAttributes"), + token.metadata_attributes.map(Value::String).unwrap_or(Value::Null), + ), + (Name::new("imagePath"), Value::String(token.image_path)), + ]))), + ERC721_TYPE_NAME.to_string(), + ), + } } } diff --git a/crates/torii/graphql/src/object/erc/mod.rs b/crates/torii/graphql/src/object/erc/mod.rs index 72d0eb1fab..b5a4827582 100644 --- a/crates/torii/graphql/src/object/erc/mod.rs +++ b/crates/torii/graphql/src/object/erc/mod.rs @@ -1,3 +1,5 @@ +use async_graphql::Value; + use super::connection::cursor; use crate::query::order::CursorDirection; @@ -15,3 +17,16 @@ fn handle_cursor( Err(_) => Err(sqlx::Error::Decode("Invalid cursor format".into())), } } + +#[derive(Debug, Clone)] +pub struct ConnectionEdge { + pub node: T, + pub cursor: String, +} + +#[derive(Debug, Clone)] +pub struct Connection { + pub total_count: i64, + pub edges: Vec>, + pub page_info: Value, +} diff --git a/crates/torii/graphql/src/object/erc/token_balance.rs b/crates/torii/graphql/src/object/erc/token_balance.rs index 44818d9eee..7fdcca4fe5 100644 --- a/crates/torii/graphql/src/object/erc/token_balance.rs +++ b/crates/torii/graphql/src/object/erc/token_balance.rs @@ -1,6 +1,5 @@ use async_graphql::connection::PageInfo; -use async_graphql::dynamic::{Field, FieldFuture, InputValue, TypeRef}; -use async_graphql::{Name, Value}; +use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}; use convert_case::{Case, Casing}; use serde::Deserialize; use sqlx::sqlite::SqliteRow; @@ -10,18 +9,20 @@ use torii_core::constants::TOKEN_BALANCE_TABLE; use torii_core::sql::utils::felt_to_sql_string; use tracing::warn; -use super::handle_cursor; +use super::erc_token::{Erc20Token, ErcTokenType}; +use super::{handle_cursor, Connection, ConnectionEdge}; use crate::constants::{DEFAULT_LIMIT, ID_COLUMN, TOKEN_BALANCE_NAME, TOKEN_BALANCE_TYPE_NAME}; use crate::mapping::TOKEN_BALANCE_TYPE_MAPPING; use crate::object::connection::page_info::PageInfoObject; use crate::object::connection::{ connection_arguments, cursor, parse_connection_arguments, ConnectionArguments, }; +use crate::object::erc::erc_token::Erc721Token; use crate::object::{BasicObject, ResolvableObject}; use crate::query::data::count_rows; use crate::query::filter::{Comparator, Filter, FilterValue}; use crate::query::order::{CursorDirection, Direction}; -use crate::types::{TypeMapping, ValueMapping}; +use crate::types::TypeMapping; use crate::utils::extract; #[derive(Debug)] @@ -75,7 +76,7 @@ impl ResolvableObject for ErcBalanceObject { let results = token_balances_connection_output(&data, total_count, page_info)?; - Ok(Some(Value::Object(results))) + Ok(Some(results)) }) }, ) @@ -206,11 +207,11 @@ async fn fetch_token_balances( } } -fn token_balances_connection_output( +fn token_balances_connection_output<'a>( data: &[SqliteRow], total_count: i64, page_info: PageInfo, -) -> sqlx::Result { +) -> sqlx::Result> { let mut edges = Vec::new(); for row in data { let row = BalanceQueryResultRaw::from_row(row)?; @@ -218,19 +219,15 @@ fn token_balances_connection_output( let balance_value = match row.contract_type.to_lowercase().as_str() { "erc20" => { - let token_metadata = Value::Object(ValueMapping::from([ - (Name::new("name"), Value::String(row.name)), - (Name::new("symbol"), Value::String(row.symbol)), - (Name::new("decimals"), Value::String(row.decimals.to_string())), - (Name::new("contractAddress"), Value::String(row.contract_address.clone())), - (Name::new("erc721"), Value::Null), - ])); - - Value::Object(ValueMapping::from([ - (Name::new("balance"), Value::String(row.balance)), - (Name::new("type"), Value::String(row.contract_type)), - (Name::new("tokenMetadata"), token_metadata), - ])) + let token_metadata = Erc20Token { + contract_address: row.contract_address, + name: row.name, + symbol: row.symbol, + decimals: row.decimals, + amount: row.balance, + }; + + ErcTokenType::Erc20(token_metadata) } "erc721" => { // contract_address:token_id @@ -239,47 +236,29 @@ fn token_balances_connection_output( let metadata: serde_json::Value = serde_json::from_str(&row.metadata).expect("metadata is always json"); - let erc721_name = + let metadata_name = metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string()); - let erc721_description = metadata + let metadata_description = metadata .get("description") .map(|v| v.to_string().trim_matches('"').to_string()); - let erc721_attributes = + let metadata_attributes = metadata.get("attributes").map(|v| v.to_string().trim_matches('"').to_string()); let image_path = format!("{}/{}", token_id.join("/"), "image"); - let token_metadata = Value::Object(ValueMapping::from([ - (Name::new("contractAddress"), Value::String(row.contract_address.clone())), - (Name::new("name"), Value::String(row.name)), - (Name::new("symbol"), Value::String(row.symbol)), - (Name::new("decimals"), Value::String(row.decimals.to_string())), - ( - Name::new("erc721"), - Value::Object(ValueMapping::from([ - (Name::new("imagePath"), Value::String(image_path)), - (Name::new("tokenId"), Value::String(token_id[1].to_string())), - (Name::new("metadata"), Value::String(row.metadata)), - ( - Name::new("name"), - erc721_name.map(Value::String).unwrap_or(Value::Null), - ), - ( - Name::new("description"), - erc721_description.map(Value::String).unwrap_or(Value::Null), - ), - ( - Name::new("attributes"), - erc721_attributes.map(Value::String).unwrap_or(Value::Null), - ), - ])), - ), - ])); - - Value::Object(ValueMapping::from([ - (Name::new("balance"), Value::String(row.balance)), - (Name::new("type"), Value::String(row.contract_type)), - (Name::new("tokenMetadata"), token_metadata), - ])) + + let token_metadata = Erc721Token { + name: row.name, + metadata: row.metadata, + contract_address: row.contract_address, + symbol: row.symbol, + token_id: token_id[1].to_string(), + metadata_name, + metadata_description, + metadata_attributes, + image_path, + }; + + ErcTokenType::Erc721(token_metadata) } _ => { warn!("Unknown contract type: {}", row.contract_type); @@ -287,17 +266,14 @@ fn token_balances_connection_output( } }; - edges.push(Value::Object(ValueMapping::from([ - (Name::new("node"), balance_value), - (Name::new("cursor"), Value::String(cursor)), - ]))); + edges.push(ConnectionEdge { node: balance_value, cursor }); } - Ok(ValueMapping::from([ - (Name::new("totalCount"), Value::from(total_count)), - (Name::new("edges"), Value::List(edges)), - (Name::new("pageInfo"), PageInfoObject::value(page_info)), - ])) + Ok(FieldValue::owned_any(Connection { + total_count, + edges, + page_info: PageInfoObject::value(page_info), + })) } // TODO: This would be required when subscriptions are needed diff --git a/crates/torii/graphql/src/object/erc/token_transfer.rs b/crates/torii/graphql/src/object/erc/token_transfer.rs index 2dc8d8bd13..1441937c2e 100644 --- a/crates/torii/graphql/src/object/erc/token_transfer.rs +++ b/crates/torii/graphql/src/object/erc/token_transfer.rs @@ -1,6 +1,5 @@ use async_graphql::connection::PageInfo; -use async_graphql::dynamic::{Field, FieldFuture, InputValue, TypeRef}; -use async_graphql::{Name, Value}; +use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}; use convert_case::{Case, Casing}; use serde::Deserialize; use sqlx::sqlite::SqliteRow; @@ -11,16 +10,18 @@ use torii_core::engine::get_transaction_hash_from_event_id; use torii_core::sql::utils::felt_to_sql_string; use tracing::warn; -use super::handle_cursor; +use super::erc_token::{Erc20Token, ErcTokenType}; +use super::{handle_cursor, Connection, ConnectionEdge}; use crate::constants::{DEFAULT_LIMIT, ID_COLUMN, TOKEN_TRANSFER_NAME, TOKEN_TRANSFER_TYPE_NAME}; use crate::mapping::TOKEN_TRANSFER_TYPE_MAPPING; use crate::object::connection::page_info::PageInfoObject; use crate::object::connection::{ connection_arguments, cursor, parse_connection_arguments, ConnectionArguments, }; +use crate::object::erc::erc_token::Erc721Token; use crate::object::{BasicObject, ResolvableObject}; use crate::query::order::{CursorDirection, Direction}; -use crate::types::{TypeMapping, ValueMapping}; +use crate::types::TypeMapping; use crate::utils::extract; #[derive(Debug)] @@ -74,7 +75,7 @@ impl ResolvableObject for ErcTransferObject { fetch_token_transfers(&mut conn, address, &connection, total_count).await?; let results = token_transfers_connection_output(&data, total_count, page_info)?; - Ok(Some(Value::Object(results))) + Ok(Some(results)) }) }, ) @@ -227,11 +228,11 @@ JOIN } } -fn token_transfers_connection_output( +fn token_transfers_connection_output<'a>( data: &[SqliteRow], total_count: i64, page_info: PageInfo, -) -> sqlx::Result { +) -> sqlx::Result> { let mut edges = Vec::new(); for row in data { @@ -239,80 +240,60 @@ fn token_transfers_connection_output( let transaction_hash = get_transaction_hash_from_event_id(&row.id); let cursor = cursor::encode(&row.id, &row.id); - let transfer_value = match row.contract_type.to_lowercase().as_str() { + let transfer_node = match row.contract_type.to_lowercase().as_str() { "erc20" => { - let token_metadata = Value::Object(ValueMapping::from([ - (Name::new("name"), Value::String(row.name)), - (Name::new("symbol"), Value::String(row.symbol)), - // for erc20 there is no token_id - (Name::new("tokenId"), Value::Null), - (Name::new("decimals"), Value::String(row.decimals.to_string())), - (Name::new("contractAddress"), Value::String(row.contract_address.clone())), - (Name::new("erc721"), Value::Null), - ])); - - Value::Object(ValueMapping::from([ - (Name::new("from"), Value::String(row.from_address)), - (Name::new("to"), Value::String(row.to_address)), - (Name::new("amount"), Value::String(row.amount)), - (Name::new("type"), Value::String(row.contract_type)), - (Name::new("executedAt"), Value::String(row.executed_at)), - (Name::new("tokenMetadata"), token_metadata), - (Name::new("transactionHash"), Value::String(transaction_hash)), - ])) + let token_metadata = ErcTokenType::Erc20(Erc20Token { + contract_address: row.contract_address, + name: row.name, + symbol: row.symbol, + decimals: row.decimals, + amount: row.amount, + }); + + TokenTransferNode { + from: row.from_address, + to: row.to_address, + executed_at: row.executed_at, + token_metadata, + transaction_hash, + } } "erc721" => { // contract_address:token_id let token_id = row.token_id.split(':').collect::>(); assert!(token_id.len() == 2); - let image_path = format!("{}/{}", token_id.join("/"), "image"); let metadata: serde_json::Value = serde_json::from_str(&row.metadata).expect("metadata is always json"); - let erc721_name = + let metadata_name = metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string()); - let erc721_description = metadata + let metadata_description = metadata .get("description") .map(|v| v.to_string().trim_matches('"').to_string()); - let erc721_attributes = + let metadata_attributes = metadata.get("attributes").map(|v| v.to_string().trim_matches('"').to_string()); - let token_metadata = Value::Object(ValueMapping::from([ - (Name::new("name"), Value::String(row.name)), - (Name::new("symbol"), Value::String(row.symbol)), - (Name::new("decimals"), Value::String(row.decimals.to_string())), - (Name::new("contractAddress"), Value::String(row.contract_address.clone())), - ( - Name::new("erc721"), - Value::Object(ValueMapping::from([ - (Name::new("imagePath"), Value::String(image_path)), - (Name::new("tokenId"), Value::String(token_id[1].to_string())), - (Name::new("metadata"), Value::String(row.metadata)), - ( - Name::new("name"), - erc721_name.map(Value::String).unwrap_or(Value::Null), - ), - ( - Name::new("description"), - erc721_description.map(Value::String).unwrap_or(Value::Null), - ), - ( - Name::new("attributes"), - erc721_attributes.map(Value::String).unwrap_or(Value::Null), - ), - ])), - ), - ])); - - Value::Object(ValueMapping::from([ - (Name::new("from"), Value::String(row.from_address)), - (Name::new("to"), Value::String(row.to_address)), - (Name::new("amount"), Value::String(row.amount)), - (Name::new("type"), Value::String(row.contract_type)), - (Name::new("executedAt"), Value::String(row.executed_at)), - (Name::new("tokenMetadata"), token_metadata), - (Name::new("transactionHash"), Value::String(transaction_hash)), - ])) + let image_path = format!("{}/{}", token_id.join("/"), "image"); + + let token_metadata = ErcTokenType::Erc721(Erc721Token { + name: row.name, + metadata: row.metadata, + contract_address: row.contract_address, + symbol: row.symbol, + token_id: token_id[1].to_string(), + metadata_name, + metadata_description, + metadata_attributes, + image_path, + }); + + TokenTransferNode { + from: row.from_address, + to: row.to_address, + executed_at: row.executed_at, + token_metadata, + transaction_hash, + } } _ => { warn!("Unknown contract type: {}", row.contract_type); @@ -320,17 +301,14 @@ fn token_transfers_connection_output( } }; - edges.push(Value::Object(ValueMapping::from([ - (Name::new("node"), transfer_value), - (Name::new("cursor"), Value::String(cursor)), - ]))); + edges.push(ConnectionEdge { node: transfer_node, cursor }); } - Ok(ValueMapping::from([ - (Name::new("totalCount"), Value::from(total_count)), - (Name::new("edges"), Value::List(edges)), - (Name::new("pageInfo"), PageInfoObject::value(page_info)), - ])) + Ok(FieldValue::owned_any(Connection { + total_count, + edges, + page_info: PageInfoObject::value(page_info), + })) } // TODO: This would be required when subscriptions are needed @@ -357,3 +335,12 @@ struct TransferQueryResultRaw { pub contract_type: String, pub metadata: String, } + +#[derive(Debug, Clone)] +pub struct TokenTransferNode { + pub from: String, + pub to: String, + pub executed_at: String, + pub token_metadata: ErcTokenType, + pub transaction_hash: String, +} diff --git a/crates/torii/graphql/src/object/mod.rs b/crates/torii/graphql/src/object/mod.rs index 8997cdabe3..36c1f9b754 100644 --- a/crates/torii/graphql/src/object/mod.rs +++ b/crates/torii/graphql/src/object/mod.rs @@ -10,10 +10,14 @@ pub mod model_data; pub mod transaction; use async_graphql::dynamic::{ - Enum, Field, FieldFuture, InputObject, InputValue, Object, SubscriptionField, TypeRef, + Enum, Field, FieldFuture, FieldValue, InputObject, InputValue, Object, SubscriptionField, + TypeRef, }; use async_graphql::Value; use convert_case::{Case, Casing}; +use erc::erc_token::ErcTokenType; +use erc::token_transfer::TokenTransferNode; +use erc::{Connection, ConnectionEdge}; use sqlx::{Pool, Sqlite}; use self::connection::edge::EdgeObject; @@ -57,12 +61,133 @@ pub trait BasicObject: Send + Sync { let field_name = field_name.clone(); FieldFuture::new(async move { - match ctx.parent_value.try_to_value()? { - Value::Object(values) => { - Ok(Some(values.get(&field_name).unwrap().clone())) // safe unwrap + match ctx.parent_value.try_to_value() { + Ok(Value::Object(values)) => { + // safe unwrap + return Ok(Some(FieldValue::value( + values.get(&field_name).unwrap().clone(), + ))); + } + // if the parent is `Value` then it must be a Object + Ok(_) => return Err("incorrect value, requires Value::Object".into()), + _ => {} + }; + + // if its not we try to downcast to known types which is a special case for + // tokenBalances and tokenTransfers queries + + if let Ok(values) = + ctx.parent_value.try_downcast_ref::>() + { + match field_name.as_str() { + "edges" => { + return Ok(Some(FieldValue::list( + values + .edges + .iter() + .map(|v| FieldValue::owned_any(v.clone())) + .collect::>>(), + ))); + } + "pageInfo" => { + return Ok(Some(FieldValue::value(values.page_info.clone()))); + } + "totalCount" => { + return Ok(Some(FieldValue::value(Value::from( + values.total_count, + )))); + } + _ => return Err("incorrect value, requires Value::Object".into()), + } + } + + if let Ok(values) = + ctx.parent_value.try_downcast_ref::>() + { + match field_name.as_str() { + "node" => return Ok(Some(FieldValue::owned_any(values.node.clone()))), + "cursor" => { + return Ok(Some(FieldValue::value(Value::String( + values.cursor.clone(), + )))); + } + _ => return Err("incorrect value, requires Value::Object".into()), + } + } + + if let Ok(values) = + ctx.parent_value.try_downcast_ref::>() + { + match field_name.as_str() { + "edges" => { + return Ok(Some(FieldValue::list( + values + .edges + .iter() + .map(|v| FieldValue::owned_any(v.clone())) + .collect::>>(), + ))); + } + "pageInfo" => { + return Ok(Some(FieldValue::value(values.page_info.clone()))); + } + "totalCount" => { + return Ok(Some(FieldValue::value(Value::from( + values.total_count, + )))); + } + _ => return Err("incorrect value, requires Value::Object".into()), } - _ => Err("incorrect value, requires Value::Object".into()), } + + if let Ok(values) = + ctx.parent_value.try_downcast_ref::>() + { + match field_name.as_str() { + "node" => return Ok(Some(FieldValue::owned_any(values.node.clone()))), + "cursor" => { + return Ok(Some(FieldValue::value(Value::String( + values.cursor.clone(), + )))); + } + _ => return Err("incorrect value, requires Value::Object".into()), + } + } + + if let Ok(values) = ctx.parent_value.try_downcast_ref::() { + match field_name.as_str() { + "from" => { + return Ok(Some(FieldValue::value(Value::String( + values.from.clone(), + )))); + } + "to" => { + return Ok(Some(FieldValue::value(Value::String( + values.to.clone(), + )))); + } + "executedAt" => { + return Ok(Some(FieldValue::value(Value::String( + values.executed_at.clone(), + )))); + } + "tokenMetadata" => { + return Ok(Some(values.clone().token_metadata.to_field_value())); + } + "transactionHash" => { + return Ok(Some(FieldValue::value(Value::String( + values.transaction_hash.clone(), + )))); + } + _ => return Err("incorrect value, requires Value::Object".into()), + } + } + + if let Ok(values) = ctx.parent_value.try_downcast_ref::() { + return Ok(Some(values.clone().to_field_value())); + } + + Err("unexpected parent value".into()) }) }); diff --git a/crates/torii/graphql/src/schema.rs b/crates/torii/graphql/src/schema.rs index 7d9b90b883..79ec29e15d 100644 --- a/crates/torii/graphql/src/schema.rs +++ b/crates/torii/graphql/src/schema.rs @@ -9,8 +9,10 @@ use super::object::event::EventObject; use super::object::model_data::ModelDataObject; use super::types::ScalarType; use super::utils; -use crate::constants::{QUERY_TYPE_NAME, SUBSCRIPTION_TYPE_NAME}; -use crate::object::erc::erc_token::{Erc721MetadataObject, ErcTokenObject}; +use crate::constants::{ + ERC20_TYPE_NAME, ERC721_TYPE_NAME, QUERY_TYPE_NAME, SUBSCRIPTION_TYPE_NAME, TOKEN_TYPE_NAME, +}; +use crate::object::erc::erc_token::{Erc20TokenObject, Erc721TokenObject}; use crate::object::erc::token_balance::ErcBalanceObject; use crate::object::erc::token_transfer::ErcTransferObject; use crate::object::event_message::EventMessageObject; @@ -121,14 +123,20 @@ async fn build_objects(pool: &SqlitePool) -> Result<(Vec, Vec = 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); + + unions.push(erc_token_union); + // model data objects for model in models { let type_mapping = type_mapping_query(&mut conn, &model.id).await?;