From d50e51b75ba4ae4461178447232423cd9ffcc6e8 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Sat, 12 Oct 2024 22:00:38 +0530 Subject: [PATCH 1/9] feat(torii/graphql): use `Connection` abstraction to return data for ercBalance and ercTransfer query commit-id:bc10539f --- crates/torii/graphql/src/constants.rs | 2 + .../graphql/src/object/erc/erc_balance.rs | 201 ++++++++++++++-- .../graphql/src/object/erc/erc_transfer.rs | 220 ++++++++++++++---- crates/torii/graphql/src/object/erc/mod.rs | 14 ++ 4 files changed, 372 insertions(+), 65 deletions(-) diff --git a/crates/torii/graphql/src/constants.rs b/crates/torii/graphql/src/constants.rs index 2d851f07b1..fc5a376f56 100644 --- a/crates/torii/graphql/src/constants.rs +++ b/crates/torii/graphql/src/constants.rs @@ -9,6 +9,8 @@ pub const EVENT_MESSAGE_TABLE: &str = "event_messages"; pub const MODEL_TABLE: &str = "models"; pub const TRANSACTION_TABLE: &str = "transactions"; pub const METADATA_TABLE: &str = "metadata"; +pub const ERC_BALANCE_TABLE: &str = "balances"; +pub const ERC_TRANSFER_TABLE: &str = "erc_transfers"; pub const ID_COLUMN: &str = "id"; pub const EVENT_ID_COLUMN: &str = "event_id"; diff --git a/crates/torii/graphql/src/object/erc/erc_balance.rs b/crates/torii/graphql/src/object/erc/erc_balance.rs index a749350fee..848303e71a 100644 --- a/crates/torii/graphql/src/object/erc/erc_balance.rs +++ b/crates/torii/graphql/src/object/erc/erc_balance.rs @@ -1,15 +1,27 @@ +use async_graphql::connection::PageInfo; use async_graphql::dynamic::{Field, FieldFuture, InputValue, TypeRef}; use async_graphql::{Name, Value}; use convert_case::{Case, Casing}; use serde::Deserialize; -use sqlx::{FromRow, Pool, Sqlite, SqliteConnection}; +use sqlx::sqlite::SqliteRow; +use sqlx::{FromRow, Pool, Row, Sqlite, SqliteConnection}; use starknet_crypto::Felt; use torii_core::sql::utils::felt_to_sql_string; use tracing::warn; -use crate::constants::{ERC_BALANCE_NAME, ERC_BALANCE_TYPE_NAME}; +use super::handle_cursor; +use crate::constants::{ + DEFAULT_LIMIT, ERC_BALANCE_NAME, ERC_BALANCE_TABLE, ERC_BALANCE_TYPE_NAME, ID_COLUMN, +}; use crate::mapping::ERC_BALANCE_TYPE_MAPPING; +use crate::object::connection::page_info::PageInfoObject; +use crate::object::connection::{ + connection_arguments, cursor, parse_connection_arguments, ConnectionArguments, +}; 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::utils::extract; @@ -38,20 +50,39 @@ impl ResolvableObject for ErcBalanceObject { TypeRef::named_nn(TypeRef::STRING), ); - let field = Field::new(self.name().0, TypeRef::named_list(self.type_name()), move |ctx| { - FieldFuture::new(async move { - let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?; - let address = extract::<Felt>( - ctx.args.as_index_map(), - &account_address.to_case(Case::Camel), - )?; + let mut field = Field::new( + self.name().0, + TypeRef::named(format!("{}Connection", self.type_name())), + move |ctx| { + FieldFuture::new(async move { + let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?; + let connection = parse_connection_arguments(&ctx)?; + let address = extract::<Felt>( + ctx.args.as_index_map(), + &account_address.to_case(Case::Camel), + )?; - let erc_balances = fetch_erc_balances(&mut conn, address).await?; + let filter = vec![Filter { + field: "account_address".to_string(), + comparator: Comparator::Eq, + value: FilterValue::String(felt_to_sql_string(&address)), + }]; - Ok(Some(Value::List(erc_balances))) - }) - }) + let total_count = + count_rows(&mut conn, ERC_BALANCE_TABLE, &None, &Some(filter)).await?; + + let (data, page_info) = + fetch_erc_balances(&mut conn, address, &connection, total_count).await?; + + let results = erc_balance_connection_output(&data, total_count, page_info)?; + + Ok(Some(Value::Object(results))) + }) + }, + ) .argument(argument); + + field = connection_arguments(field); vec![field] } } @@ -59,20 +90,132 @@ impl ResolvableObject for ErcBalanceObject { async fn fetch_erc_balances( conn: &mut SqliteConnection, address: Felt, -) -> sqlx::Result<Vec<Value>> { - let query = "SELECT t.contract_address, t.name, t.symbol, t.decimals, b.balance, b.token_id, \ - c.contract_type - FROM balances b + connection: &ConnectionArguments, + total_count: i64, +) -> sqlx::Result<(Vec<SqliteRow>, PageInfo)> { + let table_name = ERC_BALANCE_TABLE; + let id_column = format!("b.{}", ID_COLUMN); + + let mut query = format!( + "SELECT b.id, t.contract_address, t.name, t.symbol, t.decimals, b.balance, b.token_id, \ + c.contract_type + FROM {table_name} b JOIN tokens t ON b.token_id = t.id - JOIN contracts c ON t.contract_address = c.contract_address - WHERE b.account_address = ?"; + JOIN contracts c ON t.contract_address = c.contract_address" + ); + let mut conditions = vec!["b.account_address = ?".to_string()]; + + let mut cursor_param = &connection.after; + if let Some(after_cursor) = &connection.after { + conditions.push(handle_cursor(after_cursor, CursorDirection::After, ID_COLUMN)?); + } + + if let Some(before_cursor) = &connection.before { + cursor_param = &connection.before; + conditions.push(handle_cursor(before_cursor, CursorDirection::Before, ID_COLUMN)?); + } + + if !conditions.is_empty() { + query.push_str(&format!(" WHERE {}", conditions.join(" AND "))); + } + + let is_cursor_based = connection.first.or(connection.last).is_some() || cursor_param.is_some(); + + let data_limit = + connection.first.or(connection.last).or(connection.limit).unwrap_or(DEFAULT_LIMIT); + let limit = if is_cursor_based { + match &cursor_param { + Some(_) => data_limit + 2, + None => data_limit + 1, // prev page does not exist + } + } else { + data_limit + }; - let rows = sqlx::query(query).bind(felt_to_sql_string(&address)).fetch_all(conn).await?; + let order_direction = match (connection.first, connection.last) { + (Some(_), _) => Direction::Desc, + (_, Some(_)) => Direction::Asc, + _ => Direction::Desc, + }; - let mut erc_balances = Vec::new(); + query.push_str(&format!(" ORDER BY {id_column} {} LIMIT {limit}", order_direction.as_ref())); + + if let Some(offset) = connection.offset { + query.push_str(&format!(" OFFSET {}", offset)); + } + + let mut data = sqlx::query(&query).bind(felt_to_sql_string(&address)).fetch_all(conn).await?; + let mut page_info = PageInfo { + has_previous_page: false, + has_next_page: false, + start_cursor: None, + end_cursor: None, + }; + + if data.is_empty() { + Ok((data, page_info)) + } else if is_cursor_based { + match cursor_param { + Some(cursor_query) => { + let first_cursor = cursor::encode( + &data[0].try_get::<String, &str>(&id_column)?, + &data[0].try_get_unchecked::<String, &str>(&id_column)?, + ); + + if &first_cursor == cursor_query && data.len() != 1 { + data.remove(0); + page_info.has_previous_page = true; + } else { + data.pop(); + } + + if data.len() as u64 == limit - 1 { + page_info.has_next_page = true; + data.pop(); + } + } + None => { + if data.len() as u64 == limit { + page_info.has_next_page = true; + data.pop(); + } + } + } + + if !data.is_empty() { + page_info.start_cursor = Some(cursor::encode( + &data[0].try_get::<String, &str>(ID_COLUMN)?, + &data[0].try_get_unchecked::<String, &str>(ID_COLUMN)?, + )); + page_info.end_cursor = Some(cursor::encode( + &data[data.len() - 1].try_get::<String, &str>(ID_COLUMN)?, + &data[data.len() - 1].try_get_unchecked::<String, &str>(ID_COLUMN)?, + )); + } + + Ok((data, page_info)) + } else { + let offset = connection.offset.unwrap_or(0); + if 1 < offset && offset < total_count as u64 { + page_info.has_previous_page = true; + } + if limit + offset < total_count as u64 { + page_info.has_next_page = true; + } + + Ok((data, page_info)) + } +} - for row in rows { - let row = BalanceQueryResultRaw::from_row(&row)?; +fn erc_balance_connection_output( + data: &[SqliteRow], + total_count: i64, + page_info: PageInfo, +) -> sqlx::Result<ValueMapping> { + let mut edges = Vec::new(); + for row in data { + let row = BalanceQueryResultRaw::from_row(row)?; + let cursor = cursor::encode(&row.id, &row.id); let balance_value = match row.contract_type.to_lowercase().as_str() { "erc20" => { @@ -116,10 +259,17 @@ async fn fetch_erc_balances( } }; - erc_balances.push(balance_value); + edges.push(Value::Object(ValueMapping::from([ + (Name::new("node"), balance_value), + (Name::new("cursor"), Value::String(cursor)), + ]))); } - Ok(erc_balances) + Ok(ValueMapping::from([ + (Name::new("totalCount"), Value::from(total_count)), + (Name::new("edges"), Value::List(edges)), + (Name::new("pageInfo"), PageInfoObject::value(page_info)), + ])) } // TODO: This would be required when subscriptions are needed @@ -133,6 +283,7 @@ async fn fetch_erc_balances( #[derive(FromRow, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] struct BalanceQueryResultRaw { + pub id: String, pub contract_address: String, pub name: String, pub symbol: String, diff --git a/crates/torii/graphql/src/object/erc/erc_transfer.rs b/crates/torii/graphql/src/object/erc/erc_transfer.rs index ee522fef20..e1c950db39 100644 --- a/crates/torii/graphql/src/object/erc/erc_transfer.rs +++ b/crates/torii/graphql/src/object/erc/erc_transfer.rs @@ -1,16 +1,26 @@ +use async_graphql::connection::PageInfo; use async_graphql::dynamic::{Field, FieldFuture, InputValue, TypeRef}; use async_graphql::{Name, Value}; use convert_case::{Case, Casing}; use serde::Deserialize; -use sqlx::{FromRow, Pool, Sqlite, SqliteConnection}; +use sqlx::sqlite::SqliteRow; +use sqlx::{FromRow, Pool, Row, Sqlite, SqliteConnection}; use starknet_crypto::Felt; use torii_core::engine::get_transaction_hash_from_event_id; use torii_core::sql::utils::felt_to_sql_string; use tracing::warn; -use crate::constants::{ERC_TRANSFER_NAME, ERC_TRANSFER_TYPE_NAME}; +use super::handle_cursor; +use crate::constants::{ + DEFAULT_LIMIT, ERC_TRANSFER_NAME, ERC_TRANSFER_TABLE, ERC_TRANSFER_TYPE_NAME, ID_COLUMN, +}; use crate::mapping::ERC_TRANSFER_TYPE_MAPPING; +use crate::object::connection::page_info::PageInfoObject; +use crate::object::connection::{ + connection_arguments, cursor, parse_connection_arguments, ConnectionArguments, +}; use crate::object::{BasicObject, ResolvableObject}; +use crate::query::order::{CursorDirection, Direction}; use crate::types::{TypeMapping, ValueMapping}; use crate::utils::extract; @@ -34,31 +44,44 @@ impl BasicObject for ErcTransferObject { impl ResolvableObject for ErcTransferObject { fn resolvers(&self) -> Vec<Field> { let account_address = "account_address"; - let limit = "limit"; let arg_addr = InputValue::new( account_address.to_case(Case::Camel), TypeRef::named_nn(TypeRef::STRING), ); - let arg_limit = - InputValue::new(limit.to_case(Case::Camel), TypeRef::named_nn(TypeRef::INT)); - - let field = Field::new(self.name().0, TypeRef::named_list(self.type_name()), move |ctx| { - FieldFuture::new(async move { - let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?; - let address = extract::<Felt>( - ctx.args.as_index_map(), - &account_address.to_case(Case::Camel), - )?; - let limit = extract::<u64>(ctx.args.as_index_map(), &limit.to_case(Case::Camel))?; - let limit: u32 = limit.try_into()?; - - let erc_transfers = fetch_erc_transfers(&mut conn, address, limit).await?; - - Ok(Some(Value::List(erc_transfers))) - }) - }) - .argument(arg_addr) - .argument(arg_limit); + + let mut field = Field::new( + self.name().0, + TypeRef::named(format!("{}Connection", self.type_name())), + move |ctx| { + FieldFuture::new(async move { + let mut conn = ctx.data::<Pool<Sqlite>>()?.acquire().await?; + let connection = parse_connection_arguments(&ctx)?; + let address = extract::<Felt>( + ctx.args.as_index_map(), + &account_address.to_case(Case::Camel), + )?; + + let total_count: (i64,) = sqlx::query_as( + "SELECT COUNT(*) FROM erc_transfers WHERE from_address = ? OR to_address \ + = ?", + ) + .bind(felt_to_sql_string(&address)) + .bind(felt_to_sql_string(&address)) + .fetch_one(&mut *conn) + .await?; + let total_count = total_count.0; + + let (data, page_info) = + fetch_erc_transfers(&mut conn, address, &connection, total_count).await?; + let results = erc_transfer_connection_output(&data, total_count, page_info)?; + + Ok(Some(Value::Object(results))) + }) + }, + ) + .argument(arg_addr); + + field = connection_arguments(field); vec![field] } } @@ -66,9 +89,13 @@ impl ResolvableObject for ErcTransferObject { async fn fetch_erc_transfers( conn: &mut SqliteConnection, address: Felt, - limit: u32, -) -> sqlx::Result<Vec<Value>> { - let query = format!( + connection: &ConnectionArguments, + total_count: i64, +) -> sqlx::Result<(Vec<SqliteRow>, PageInfo)> { + let table_name = ERC_TRANSFER_TABLE; + let id_column = format!("et.{}", ID_COLUMN); + + let mut query = format!( r#" SELECT et.id, @@ -83,28 +110,134 @@ SELECT t.decimals, c.contract_type FROM - erc_transfers et + {table_name} et JOIN tokens t ON et.token_id = t.id JOIN contracts c ON t.contract_address = c.contract_address -WHERE - et.from_address = ? OR et.to_address = ? -ORDER BY - et.executed_at DESC -LIMIT {}; "#, - limit ); - let address = felt_to_sql_string(&address); - let rows = sqlx::query(&query).bind(&address).bind(&address).fetch_all(conn).await?; + let mut conditions = vec!["et.from_address = ? OR et.to_address = ?".to_string()]; + + let mut cursor_param = &connection.after; + if let Some(after_cursor) = &connection.after { + conditions.push(handle_cursor(after_cursor, CursorDirection::After, ID_COLUMN)?); + } + + if let Some(before_cursor) = &connection.before { + cursor_param = &connection.before; + conditions.push(handle_cursor(before_cursor, CursorDirection::Before, ID_COLUMN)?); + } + + if !conditions.is_empty() { + query.push_str(&format!(" WHERE {}", conditions.join(" AND "))); + } + + let is_cursor_based = connection.first.or(connection.last).is_some() || cursor_param.is_some(); + + let data_limit = + connection.first.or(connection.last).or(connection.limit).unwrap_or(DEFAULT_LIMIT); + let limit = if is_cursor_based { + match &cursor_param { + Some(_) => data_limit + 2, + None => data_limit + 1, // prev page does not exist + } + } else { + data_limit + }; + + let order_direction = match (connection.first, connection.last) { + (Some(_), _) => Direction::Desc, + (_, Some(_)) => Direction::Asc, + _ => Direction::Desc, + }; + + query.push_str(&format!(" ORDER BY {id_column} {} LIMIT {limit}", order_direction.as_ref())); + + if let Some(offset) = connection.offset { + query.push_str(&format!(" OFFSET {}", offset)); + } + + let mut data = sqlx::query(&query) + .bind(felt_to_sql_string(&address)) + .bind(felt_to_sql_string(&address)) + .fetch_all(conn) + .await?; + + let mut page_info = PageInfo { + has_previous_page: false, + has_next_page: false, + start_cursor: None, + end_cursor: None, + }; + + if data.is_empty() { + Ok((data, page_info)) + } else if is_cursor_based { + match cursor_param { + Some(cursor_query) => { + let first_cursor = cursor::encode( + &data[0].try_get::<String, &str>(ID_COLUMN)?, + &data[0].try_get_unchecked::<String, &str>(ID_COLUMN)?, + ); + + if &first_cursor == cursor_query && data.len() != 1 { + data.remove(0); + page_info.has_previous_page = true; + } else { + data.pop(); + } + + if data.len() as u64 == limit - 1 { + page_info.has_next_page = true; + data.pop(); + } + } + None => { + if data.len() as u64 == limit { + page_info.has_next_page = true; + data.pop(); + } + } + } + + if !data.is_empty() { + page_info.start_cursor = Some(cursor::encode( + &data[0].try_get::<String, &str>(ID_COLUMN)?, + &data[0].try_get_unchecked::<String, &str>(ID_COLUMN)?, + )); + page_info.end_cursor = Some(cursor::encode( + &data[data.len() - 1].try_get::<String, &str>(ID_COLUMN)?, + &data[data.len() - 1].try_get_unchecked::<String, &str>(ID_COLUMN)?, + )); + } + + Ok((data, page_info)) + } else { + let offset = connection.offset.unwrap_or(0); + if 1 < offset && offset < total_count as u64 { + page_info.has_previous_page = true; + } + if limit + offset < total_count as u64 { + page_info.has_next_page = true; + } + + Ok((data, page_info)) + } +} - let mut erc_balances = Vec::new(); +fn erc_transfer_connection_output( + data: &[SqliteRow], + total_count: i64, + page_info: PageInfo, +) -> sqlx::Result<ValueMapping> { + let mut edges = Vec::new(); - for row in rows { - let row = TransferQueryResultRaw::from_row(&row)?; + for row in data { + let row = TransferQueryResultRaw::from_row(row)?; 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() { "erc20" => { @@ -156,10 +289,17 @@ LIMIT {}; } }; - erc_balances.push(transfer_value); + edges.push(Value::Object(ValueMapping::from([ + (Name::new("node"), transfer_value), + (Name::new("cursor"), Value::String(cursor)), + ]))); } - Ok(erc_balances) + Ok(ValueMapping::from([ + (Name::new("totalCount"), Value::from(total_count)), + (Name::new("edges"), Value::List(edges)), + (Name::new("pageInfo"), PageInfoObject::value(page_info)), + ])) } // TODO: This would be required when subscriptions are needed diff --git a/crates/torii/graphql/src/object/erc/mod.rs b/crates/torii/graphql/src/object/erc/mod.rs index eac2c5510b..3e85722cca 100644 --- a/crates/torii/graphql/src/object/erc/mod.rs +++ b/crates/torii/graphql/src/object/erc/mod.rs @@ -1,3 +1,17 @@ +use super::connection::cursor; +use crate::query::order::CursorDirection; + pub mod erc_balance; pub mod erc_token; pub mod erc_transfer; + +fn handle_cursor( + cursor: &str, + direction: CursorDirection, + id_column: &str, +) -> sqlx::Result<String> { + match cursor::decode(cursor) { + Ok((event_id, _)) => Ok(format!("{} {} '{}'", id_column, direction.as_ref(), event_id)), + Err(_) => Err(sqlx::Error::Decode("Invalid cursor format".into())), + } +} From 76240e31cf8ee4230585a4e0422bdd2e1da47933 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Wed, 16 Oct 2024 15:44:31 +0530 Subject: [PATCH 2/9] feat(torii): fetch and process erc721 metadata and image commit-id:274aaa3a --- Cargo.lock | 454 +++++++++++++++++- Cargo.toml | 6 + bin/torii/Cargo.toml | 6 +- bin/torii/src/main.rs | 130 ++++- crates/dojo-world/Cargo.toml | 55 +++ crates/sozo/ops/Cargo.toml | 8 +- crates/torii/core/Cargo.toml | 6 +- crates/torii/core/src/engine.rs | 12 +- crates/torii/core/src/executor/erc.rs | 297 ++++++++++++ .../core/src/{executor.rs => executor/mod.rs} | 390 +++++++++------ .../src/processors/erc721_legacy_transfer.rs | 14 +- .../core/src/processors/erc721_transfer.rs | 14 +- .../core/src/processors/metadata_update.rs | 38 +- crates/torii/core/src/sql/erc.rs | 144 ++---- crates/torii/core/src/sql/mod.rs | 6 + crates/torii/core/src/sql/test.rs | 12 +- crates/torii/core/src/utils.rs | 40 ++ .../torii/graphql/src/tests/metadata_test.rs | 15 +- crates/torii/graphql/src/tests/mod.rs | 3 +- .../graphql/src/tests/subscription_test.rs | 31 +- .../grpc/src/server/tests/entities_test.rs | 4 +- crates/torii/libp2p/Cargo.toml | 2 +- crates/torii/libp2p/src/tests.rs | 6 +- .../20241014085532_add_metadata_field.sql | 1 + crates/torii/server/Cargo.toml | 17 +- crates/torii/server/src/artifacts.rs | 336 +++++++++++++ crates/torii/server/src/lib.rs | 1 + crates/torii/server/src/proxy.rs | 36 +- 28 files changed, 1738 insertions(+), 346 deletions(-) create mode 100644 crates/dojo-world/Cargo.toml create mode 100644 crates/torii/core/src/executor/erc.rs rename crates/torii/core/src/{executor.rs => executor/mod.rs} (64%) create mode 100644 crates/torii/migrations/20241014085532_add_metadata_field.sql create mode 100644 crates/torii/server/src/artifacts.rs diff --git a/Cargo.lock b/Cargo.lock index 3af88cd098..e4f92c298e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,12 @@ dependencies = [ "gimli", ] +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler2" version = "2.0.0" @@ -142,6 +148,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -997,6 +1009,17 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "ark-ec" version = "0.4.2" @@ -1669,6 +1692,29 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec 0.7.6", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +dependencies = [ + "arrayvec 0.7.6", +] + [[package]] name = "aws-lc-rs" version = "1.9.0" @@ -1805,7 +1851,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.0", "object", "rustc-demangle", "windows-targets 0.52.6", @@ -1973,6 +2019,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -1988,6 +2040,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitstream-io" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" + [[package]] name = "bitvec" version = "1.0.1" @@ -2195,6 +2253,12 @@ dependencies = [ "serde", ] +[[package]] +name = "built" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" + [[package]] name = "bumpalo" version = "3.16.0" @@ -2258,6 +2322,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.7.2" @@ -3394,7 +3464,7 @@ checksum = "9a95746c5221a74d7b913a415fdbb9e7c90e1b4d818dbbff59bddc034cfce2ec" dependencies = [ "bytes", "flex-error", - "num-derive", + "num-derive 0.3.3", "num-traits 0.2.19", "prost 0.12.6", "prost-types 0.12.6", @@ -3448,6 +3518,16 @@ dependencies = [ "nom", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -3655,6 +3735,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.2" @@ -4311,6 +4397,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "data-url" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" + [[package]] name = "debugid" version = "0.8.0" @@ -5163,6 +5255,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half 2.4.1", + "lebe", + "miniz_oxide 0.7.4", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "eyre" version = "0.6.12" @@ -5214,6 +5322,15 @@ dependencies = [ "bytes", ] +[[package]] +name = "fdeflate" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.0" @@ -5279,7 +5396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -5605,6 +5722,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.0" @@ -7300,6 +7427,39 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits 0.2.19", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +dependencies = [ + "byteorder-lite", + "quick-error 2.0.1", +] + [[package]] name = "imara-diff" version = "0.1.7" @@ -7310,6 +7470,12 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "imgref" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" + [[package]] name = "impl-codec" version = "0.6.0" @@ -7502,6 +7668,17 @@ dependencies = [ "webrtc-util 0.8.1", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "io-close" version = "0.3.7" @@ -7752,6 +7929,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.70" @@ -8655,6 +8838,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "leopard-codec" version = "0.1.0" @@ -8672,6 +8861,17 @@ version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + [[package]] name = "libloading" version = "0.8.5" @@ -9280,6 +9480,15 @@ dependencies = [ "value-bag", ] +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.12.4" @@ -9359,6 +9568,15 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", +] + [[package]] name = "md-5" version = "0.10.6" @@ -9512,6 +9730,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -9519,6 +9746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -9865,6 +10093,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "notify" version = "7.0.0" @@ -9968,6 +10202,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "num-format" version = "0.4.4" @@ -10666,6 +10911,19 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "png" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.0", +] + [[package]] name = "polling" version = "2.8.0" @@ -10966,6 +11224,25 @@ dependencies = [ "human_format", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn 2.0.77", +] + [[package]] name = "prometheus-client" version = "0.22.3" @@ -11184,6 +11461,15 @@ dependencies = [ "psl-types", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quanta" version = "0.12.3" @@ -11205,6 +11491,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-protobuf" version = "0.8.1" @@ -11386,6 +11678,55 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec 0.7.6", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive 0.4.2", + "num-traits 0.2.19", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error 2.0.1", + "rav1e", + "rgb", +] + [[package]] name = "raw-cpuid" version = "11.1.0" @@ -11673,7 +12014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" dependencies = [ "hostname", - "quick-error", + "quick-error 1.2.3", ] [[package]] @@ -12302,7 +12643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", - "quick-error", + "quick-error 1.2.3", "tempfile", "wait-timeout", ] @@ -13137,6 +13478,21 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -14379,6 +14735,19 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.19", + "version-compare", +] + [[package]] name = "tabled" version = "0.16.0" @@ -14421,6 +14790,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempdir" version = "0.3.7" @@ -14530,6 +14905,17 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.36" @@ -15027,6 +15413,7 @@ dependencies = [ "cainome 0.4.6", "chrono", "crypto-bigint", + "data-url", "dojo-test-utils", "dojo-types 1.0.0", "dojo-utils", @@ -15034,6 +15421,7 @@ dependencies = [ "futures-channel", "futures-util", "hashlink", + "ipfs-api-backend-hyper", "katana-runner", "num-traits 0.2.19", "once_cell", @@ -15179,20 +15567,29 @@ dependencies = [ name = "torii-server" version = "1.0.0" dependencies = [ + "anyhow", "base64 0.21.7", + "camino", + "data-url", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.30", "hyper-reverse-proxy", + "image", "indexmap 2.5.0", "lazy_static", + "mime_guess", + "reqwest 0.11.27", "serde", "serde_json", + "sqlx", "tokio", "tokio-util", + "torii-core", "tower 0.4.13", "tower-http 0.4.4", "tracing", + "warp", ] [[package]] @@ -15776,6 +16173,17 @@ dependencies = [ "getrandom", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits 0.2.19", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.0" @@ -15849,6 +16257,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" @@ -16387,6 +16801,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "which" version = "4.4.2" @@ -17082,3 +17502,27 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index a5b69bf2e2..fa4a365206 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -169,14 +169,17 @@ colored_json = "3.2.0" console = "0.15.7" convert_case = "0.6.0" crypto-bigint = { version = "0.5.3", features = [ "serde" ] } +data-url = "0.3" derive_more = "0.99.17" flate2 = "1.0.24" +fluent-uri = "0.3" futures = "0.3.30" futures-util = "0.3.30" hashlink = "0.9.1" hex = "0.4.3" hex-literal = "0.4.1" http = "0.2.9" +image = "0.25.2" indexmap = "2.2.5" indoc = "1.0.7" itertools = "0.12.1" @@ -224,6 +227,9 @@ tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.16", features = [ "env-filter", "json" ] } url = { version = "2.4.0", features = [ "serde" ] } walkdir = "2.5.0" +# TODO: see if we still need the git version +ipfs-api-backend-hyper = { git = "https://github.com/ferristseng/rust-ipfs-api", rev = "af2c17f7b19ef5b9898f458d97a90055c3605633", features = [ "with-hyper-rustls", "with-send-sync" ] } +mime_guess = "2.0" # server hyper = "0.14.27" diff --git a/bin/torii/Cargo.toml b/bin/torii/Cargo.toml index b9bf3cf1cf..2e652a89ca 100644 --- a/bin/torii/Cargo.toml +++ b/bin/torii/Cargo.toml @@ -33,6 +33,7 @@ starknet.workspace = true tokio-stream = "0.1.11" tokio-util = "0.7.7" tokio.workspace = true +toml.workspace = true torii-cli.workspace = true torii-core.workspace = true torii-graphql.workspace = true @@ -40,15 +41,14 @@ torii-grpc = { workspace = true, features = [ "server" ] } torii-relay.workspace = true torii-server.workspace = true tower.workspace = true -toml.workspace = true +clap_config = "0.1.1" +tempfile.workspace = true tower-http.workspace = true tracing-subscriber.workspace = true tracing.workspace = true url.workspace = true webbrowser = "0.8" -tempfile.workspace = true -clap_config = "0.1.1" [dev-dependencies] assert_matches.workspace = true diff --git a/bin/torii/src/main.rs b/bin/torii/src/main.rs index 57415c48e6..1d5b2f1322 100644 --- a/bin/torii/src/main.rs +++ b/bin/torii/src/main.rs @@ -16,7 +16,10 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; +use anyhow::Context; +use camino::Utf8PathBuf; use clap::Parser; +use clap::{ArgAction, Parser}; use dojo_metrics::exporters::prometheus::PrometheusRecorder; use dojo_world::contracts::world::WorldContractReader; use sqlx::sqlite::{ @@ -25,7 +28,7 @@ use sqlx::sqlite::{ use sqlx::SqlitePool; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; -use tempfile::NamedTempFile; +use tempfile::{NamedTempFile, TempDir}; use tokio::sync::broadcast; use tokio::sync::broadcast::Sender; use tokio_stream::StreamExt; @@ -45,6 +48,111 @@ use url::{form_urlencoded, Url}; pub(crate) const LOG_TARGET: &str = "torii::cli"; +/// Dojo World Indexer +#[derive(Parser, Debug)] +#[command(name = "torii", author, version, about, long_about = None)] +struct Args { + /// The world to index + #[arg(short, long = "world", env = "DOJO_WORLD_ADDRESS")] + world_address: Option<Felt>, + + /// The sequencer rpc endpoint to index. + #[arg(long, value_name = "URL", default_value = ":5050", value_parser = parse_url)] + rpc: Url, + + /// Database filepath (ex: indexer.db). If specified file doesn't exist, it will be + /// created. Defaults to in-memory database + #[arg(short, long, default_value = "")] + database: String, + + /// Address to serve api endpoints at. + #[arg(long, value_name = "SOCKET", default_value = "0.0.0.0:8080", value_parser = parse_socket_address)] + addr: SocketAddr, + + /// Port to serve Libp2p TCP & UDP Quic transports + #[arg(long, value_name = "PORT", default_value = "9090")] + relay_port: u16, + + /// Port to serve Libp2p WebRTC transport + #[arg(long, value_name = "PORT", default_value = "9091")] + relay_webrtc_port: u16, + + /// Port to serve Libp2p WebRTC transport + #[arg(long, value_name = "PORT", default_value = "9092")] + relay_websocket_port: u16, + + /// Path to a local identity key file. If not specified, a new identity will be generated + #[arg(long, value_name = "PATH")] + relay_local_key_path: Option<String>, + + /// Path to a local certificate file. If not specified, a new certificate will be generated + /// for WebRTC connections + #[arg(long, value_name = "PATH")] + relay_cert_path: Option<String>, + + /// Specify allowed origins for api endpoints (comma-separated list of allowed origins, or "*" + /// for all) + #[arg(long)] + #[arg(value_delimiter = ',')] + allowed_origins: Option<Vec<String>>, + + /// The external url of the server, used for configuring the GraphQL Playground in a hosted + /// environment + #[arg(long, value_parser = parse_url)] + external_url: Option<Url>, + + /// Enable Prometheus metrics. + /// + /// The metrics will be served at the given interface and port. + #[arg(long, value_name = "SOCKET", value_parser = parse_socket_address, help_heading = "Metrics")] + metrics: Option<SocketAddr>, + + /// Open World Explorer on the browser. + #[arg(long)] + explorer: bool, + + /// Chunk size of the events page when indexing using events + #[arg(long, default_value = "1024")] + events_chunk_size: u64, + + /// Number of blocks to process before commiting to DB + #[arg(long, default_value = "10240")] + blocks_chunk_size: u64, + + /// Enable indexing pending blocks + #[arg(long, action = ArgAction::Set, default_value_t = true)] + index_pending: bool, + + /// Polling interval in ms + #[arg(long, default_value = "500")] + polling_interval: u64, + + /// Max concurrent tasks + #[arg(long, default_value = "100")] + max_concurrent_tasks: usize, + + /// Whether or not to index world transactions + #[arg(long, action = ArgAction::Set, default_value_t = false)] + index_transactions: bool, + + /// Whether or not to index raw events + #[arg(long, action = ArgAction::Set, default_value_t = true)] + index_raw_events: bool, + + /// ERC contract addresses to index + #[arg(long, value_parser = parse_erc_contracts)] + #[arg(conflicts_with = "config")] + contracts: Option<std::vec::Vec<Contract>>, + + /// Configuration file + #[arg(long)] + config: Option<PathBuf>, + + /// Path to a directory to store ERC artifacts + #[arg(long)] + artifacts_path: Option<Utf8PathBuf>, +} + #[tokio::main] async fn main() -> anyhow::Result<()> { let mut args = ToriiArgs::parse().with_config_file()?; @@ -109,7 +217,21 @@ async fn main() -> anyhow::Result<()> { // Get world address let world = WorldContractReader::new(world_address, provider.clone()); - let (mut executor, sender) = Executor::new(pool.clone(), shutdown_tx.clone()).await?; + // let (mut executor, sender) = Executor::new(pool.clone(), shutdown_tx.clone()).await?; + let contracts = args + .indexing + .contracts + .iter() + .map(|contract| (contract.address, contract.r#type)) + .collect(); + + let (mut executor, sender) = Executor::new( + pool.clone(), + shutdown_tx.clone(), + provider.clone(), + args.max_concurrent_tasks, + ) + .await?; tokio::spawn(async move { executor.run().await.unwrap(); }); @@ -184,6 +306,7 @@ async fn main() -> anyhow::Result<()> { args.server.http_cors_origins.filter(|cors_origins| !cors_origins.is_empty()), Some(grpc_addr), None, + Some(artifacts_addr), )); let graphql_server = spawn_rebuilding_graphql_server( @@ -201,6 +324,7 @@ async fn main() -> anyhow::Result<()> { info!(target: LOG_TARGET, endpoint = %addr, "Starting torii endpoint."); info!(target: LOG_TARGET, endpoint = %gql_endpoint, "Serving Graphql playground."); info!(target: LOG_TARGET, url = %explorer_url, "Serving World Explorer."); + info!(target: LOG_TARGET, path = %artifacts_path, "Serving ERC artifacts at path"); if args.explorer { if let Err(e) = webbrowser::open(&explorer_url) { @@ -222,6 +346,7 @@ async fn main() -> anyhow::Result<()> { let graphql_server_handle = tokio::spawn(graphql_server); let grpc_server_handle = tokio::spawn(grpc_server); let libp2p_relay_server_handle = tokio::spawn(async move { libp2p_relay_server.run().await }); + let artifacts_server_handle = tokio::spawn(artifacts_server); tokio::select! { res = engine_handle => res??, @@ -229,6 +354,7 @@ async fn main() -> anyhow::Result<()> { res = graphql_server_handle => res?, res = grpc_server_handle => res??, res = libp2p_relay_server_handle => res?, + res = artifacts_server_handle => res?, _ = dojo_utils::signal::wait_signals() => {}, }; diff --git a/crates/dojo-world/Cargo.toml b/crates/dojo-world/Cargo.toml new file mode 100644 index 0000000000..39301853ca --- /dev/null +++ b/crates/dojo-world/Cargo.toml @@ -0,0 +1,55 @@ +[package] +description = "Dojo world specification. For example, crates and flags used for compilation." +edition.workspace = true +license-file.workspace = true +name = "dojo-world" +repository.workspace = true +version.workspace = true + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +cairo-lang-filesystem.workspace = true +cairo-lang-project.workspace = true +cairo-lang-starknet-classes.workspace = true +cairo-lang-starknet.workspace = true +camino.workspace = true +convert_case.workspace = true +dojo-utils = { workspace = true, optional = true } +num-traits = { workspace = true, optional = true } +regex.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_with.workspace = true +smol_str.workspace = true +starknet-crypto.workspace = true +starknet.workspace = true +thiserror.workspace = true +topological-sort.workspace = true +tracing.workspace = true + +cainome.workspace = true +dojo-types = { path = "../dojo-types", optional = true } +http = { workspace = true, optional = true } +ipfs-api-backend-hyper = { workspace = true, optional = true } +scarb = { workspace = true, optional = true } +tokio = { version = "1.32.0", features = [ "time" ], default-features = false, optional = true } +toml.workspace = true +url = { workspace = true, optional = true } +walkdir.workspace = true + +[dev-dependencies] +assert_fs.workspace = true +assert_matches.workspace = true +dojo-lang.workspace = true +dojo-test-utils = { path = "../dojo-test-utils" } +katana-runner.workspace = true +similar-asserts.workspace = true +tempfile.workspace = true +tokio.workspace = true + +[features] +contracts = [ "dep:dojo-types", "dep:http", "dep:num-traits" ] +manifest = [ "contracts", "dep:dojo-types", "dep:scarb", "dep:url" ] +metadata = [ "dep:ipfs-api-backend-hyper", "dep:scarb", "dep:url" ] +migration = [ "dep:dojo-utils", "dep:scarb", "dep:tokio", "manifest" ] diff --git a/crates/sozo/ops/Cargo.toml b/crates/sozo/ops/Cargo.toml index 170612e2ec..54a6982f8f 100644 --- a/crates/sozo/ops/Cargo.toml +++ b/crates/sozo/ops/Cargo.toml @@ -11,8 +11,8 @@ async-trait.workspace = true cainome.workspace = true colored.workspace = true colored_json.workspace = true -dojo-utils.workspace = true dojo-types.workspace = true +dojo-utils.workspace = true dojo-world.workspace = true futures.workspace = true num-traits.workspace = true @@ -21,8 +21,8 @@ serde_json.workspace = true serde_with.workspace = true sozo-walnut = { workspace = true, optional = true } spinoff.workspace = true -starknet.workspace = true starknet-crypto.workspace = true +starknet.workspace = true thiserror.workspace = true toml.workspace = true tracing.workspace = true @@ -33,11 +33,11 @@ katana-runner = { workspace = true, optional = true } [dev-dependencies] assert_fs.workspace = true dojo-test-utils = { workspace = true, features = [ "build-examples" ] } -ipfs-api-backend-hyper = { git = "https://github.com/ferristseng/rust-ipfs-api", rev = "af2c17f7b19ef5b9898f458d97a90055c3605633", features = [ "with-hyper-rustls" ] } +ipfs-api-backend-hyper.workspace = true katana-runner.workspace = true -tokio.workspace = true scarb.workspace = true sozo-scarbext.workspace = true +tokio.workspace = true [features] test-utils = [ "dep:dojo-test-utils", "dep:katana-runner" ] diff --git a/crates/torii/core/Cargo.toml b/crates/torii/core/Cargo.toml index aac4d9010f..09b25d8cca 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/core/Cargo.toml @@ -16,6 +16,7 @@ bitflags = "2.6.0" cainome.workspace = true chrono.workspace = true crypto-bigint.workspace = true +data-url.workspace = true dojo-types.workspace = true dojo-world.workspace = true futures-channel = "0.3.0" @@ -31,8 +32,9 @@ sqlx.workspace = true starknet-crypto.workspace = true starknet.workspace = true thiserror.workspace = true -tokio = { version = "1.32.0", features = [ "sync", "macros" ], default-features = true } +tokio = { version = "1.32.0", features = [ "macros", "sync" ], default-features = true } # tokio-stream = "0.1.11" +ipfs-api-backend-hyper.workspace = true tokio-util.workspace = true tracing.workspace = true @@ -41,5 +43,5 @@ dojo-test-utils.workspace = true dojo-utils.workspace = true katana-runner.workspace = true scarb.workspace = true -tempfile.workspace = true sozo-scarbext.workspace = true +tempfile.workspace = true diff --git a/crates/torii/core/src/engine.rs b/crates/torii/core/src/engine.rs index ccac372feb..edbbea147f 100644 --- a/crates/torii/core/src/engine.rs +++ b/crates/torii/core/src/engine.rs @@ -277,8 +277,9 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> { match self.process(fetch_result).await { Ok(_) => { - self.db.execute().await?; + self.db.flush().await?; self.db.apply_cache_diff().await?; + self.db.execute().await?; }, Err(e) => { error!(target: LOG_TARGET, error = %e, "Processing fetched data."); @@ -646,8 +647,7 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> { unique_contracts.insert(event.from_address); - Self::process_event( - self, + self.process_event( block_number, block_timestamp, &event_id, @@ -707,8 +707,7 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> { let event_id = format!("{:#064x}:{:#x}:{:#04x}", block_number, *transaction_hash, event_idx); - Self::process_event( - self, + self.process_event( block_number, block_timestamp, &event_id, @@ -720,8 +719,7 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> { } if self.config.flags.contains(IndexingFlags::TRANSACTIONS) { - Self::process_transaction( - self, + self.process_transaction( block_number, block_timestamp, *transaction_hash, diff --git a/crates/torii/core/src/executor/erc.rs b/crates/torii/core/src/executor/erc.rs new file mode 100644 index 0000000000..eb4a3c19bc --- /dev/null +++ b/crates/torii/core/src/executor/erc.rs @@ -0,0 +1,297 @@ +use std::str::FromStr; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use cainome::cairo_serde::{ByteArray, CairoSerde}; +use data_url::mime::Mime; +use data_url::DataUrl; +use reqwest::Client; +use starknet::core::types::{BlockId, BlockTag, FunctionCall, U256}; +use starknet::core::utils::{get_selector_from_name, parse_cairo_short_string}; +use starknet::providers::Provider; +use starknet_crypto::Felt; +use tracing::{debug, trace}; + +use super::{ApplyBalanceDiffQuery, Executor}; +use crate::constants::TOKEN_BALANCE_TABLE; +use crate::sql::utils::{felt_to_sql_string, sql_string_to_u256, u256_to_sql_string, I256}; +use crate::sql::FELT_DELIMITER; +use crate::types::ContractType; +use crate::utils::{fetch_content_from_ipfs, MAX_RETRY}; + +#[derive(Debug, Clone)] +pub struct RegisterErc721TokenQuery { + pub token_id: String, + pub contract_address: Felt, + pub actual_token_id: U256, +} + +#[derive(Debug, Clone)] +pub struct RegisterErc721TokenMetadata { + pub query: RegisterErc721TokenQuery, + pub name: String, + pub symbol: String, + pub metadata: String, +} + +#[derive(Debug, Clone)] +pub struct RegisterErc20TokenQuery { + pub token_id: String, + pub contract_address: Felt, + pub name: String, + pub symbol: String, + pub decimals: u8, +} + +impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { + pub async fn apply_balance_diff( + &mut self, + apply_balance_diff: ApplyBalanceDiffQuery, + ) -> Result<()> { + let erc_cache = apply_balance_diff.erc_cache; + for ((contract_type, id_str), balance) in erc_cache.iter() { + let id = id_str.split(FELT_DELIMITER).collect::<Vec<&str>>(); + match contract_type { + ContractType::WORLD => unreachable!(), + ContractType::ERC721 => { + // account_address/contract_address:id => ERC721 + assert!(id.len() == 2); + let account_address = id[0]; + let token_id = id[1]; + let mid = token_id.split(":").collect::<Vec<&str>>(); + let contract_address = mid[0]; + + self.apply_balance_diff_helper( + id_str, + account_address, + contract_address, + token_id, + balance, + ) + .await + .with_context(|| "Failed to apply balance diff in apply_cache_diff")?; + } + ContractType::ERC20 => { + // account_address/contract_address/ => ERC20 + assert!(id.len() == 3); + let account_address = id[0]; + let contract_address = id[1]; + let token_id = id[1]; + + self.apply_balance_diff_helper( + id_str, + account_address, + contract_address, + token_id, + balance, + ) + .await + .with_context(|| "Failed to apply balance diff in apply_cache_diff")?; + } + } + } + + Ok(()) + } + + pub async fn apply_balance_diff_helper( + &mut self, + id: &str, + account_address: &str, + contract_address: &str, + token_id: &str, + balance_diff: &I256, + ) -> Result<()> { + let tx = &mut self.transaction; + let balance: Option<(String,)> = + sqlx::query_as(&format!("SELECT balance FROM {TOKEN_BALANCE_TABLE} WHERE id = ?")) + .bind(id) + .fetch_optional(&mut **tx) + .await?; + + let mut balance = if let Some(balance) = balance { + sql_string_to_u256(&balance.0) + } else { + U256::from(0u8) + }; + + if balance_diff.is_negative { + if balance < balance_diff.value { + dbg!(&balance_diff, balance, id); + } + balance -= balance_diff.value; + } else { + balance += balance_diff.value; + } + + // write the new balance to the database + sqlx::query(&format!( + "INSERT OR REPLACE INTO {TOKEN_BALANCE_TABLE} (id, contract_address, account_address, \ + token_id, balance) VALUES (?, ?, ?, ?, ?)", + )) + .bind(id) + .bind(contract_address) + .bind(account_address) + .bind(token_id) + .bind(u256_to_sql_string(&balance)) + .execute(&mut **tx) + .await?; + + Ok(()) + } + + pub async fn process_register_erc721_token_query( + register_erc721_token: RegisterErc721TokenQuery, + provider: Arc<P>, + name: String, + symbol: String, + ) -> Result<RegisterErc721TokenMetadata> { + let token_uri = if let Ok(token_uri) = provider + .call( + FunctionCall { + contract_address: register_erc721_token.contract_address, + entry_point_selector: get_selector_from_name("token_uri").unwrap(), + calldata: vec![ + register_erc721_token.actual_token_id.low().into(), + register_erc721_token.actual_token_id.high().into(), + ], + }, + BlockId::Tag(BlockTag::Pending), + ) + .await + { + token_uri + } else if let Ok(token_uri) = provider + .call( + FunctionCall { + contract_address: register_erc721_token.contract_address, + entry_point_selector: get_selector_from_name("tokenURI").unwrap(), + calldata: vec![ + register_erc721_token.actual_token_id.low().into(), + register_erc721_token.actual_token_id.high().into(), + ], + }, + BlockId::Tag(BlockTag::Pending), + ) + .await + { + token_uri + } else { + return Err(anyhow::anyhow!("Failed to fetch token_uri")); + }; + + let token_uri = if let Ok(byte_array) = ByteArray::cairo_deserialize(&token_uri, 0) { + byte_array.to_string().expect("Return value not String") + } else if let Ok(felt_array) = Vec::<Felt>::cairo_deserialize(&token_uri, 0) { + felt_array + .iter() + .map(parse_cairo_short_string) + .collect::<Result<Vec<String>, _>>() + .map(|strings| strings.join("")) + .map_err(|_| anyhow::anyhow!("Failed parsing Array<Felt> to String"))? + } else { + return Err(anyhow::anyhow!("token_uri is neither ByteArray nor Array<Felt>")); + }; + + let metadata = Self::fetch_metadata(&token_uri).await.with_context(|| { + format!( + "Failed to fetch metadata for token_id: {}", + register_erc721_token.actual_token_id + ) + })?; + let metadata = serde_json::to_string(&metadata).context("Failed to serialize metadata")?; + Ok(RegisterErc721TokenMetadata { query: register_erc721_token, metadata, name, symbol }) + } + + // given a uri which can be either http/https url or data uri, fetch the metadata erc721 + // metadata json schema + pub async fn fetch_metadata(token_uri: &str) -> Result<serde_json::Value> { + // Parse the token_uri + + match token_uri { + uri if uri.starts_with("http") || uri.starts_with("https") => { + // Fetch metadata from HTTP/HTTPS URL + debug!(token_uri = %token_uri, "Fetching metadata from http/https URL"); + let client = Client::new(); + let response = client + .get(token_uri) + .send() + .await + .context("Failed to fetch metadata from URL")?; + + let bytes = response.bytes().await.context("Failed to read response bytes")?; + let json: serde_json::Value = serde_json::from_slice(&bytes) + .context(format!("Failed to parse metadata JSON from response: {:?}", bytes))?; + + Ok(json) + } + uri if uri.starts_with("ipfs") => { + let cid = uri.strip_prefix("ipfs://").unwrap(); + debug!(cid = %cid, "Fetching metadata from IPFS"); + let response = fetch_content_from_ipfs(cid, MAX_RETRY) + .await + .context("Failed to fetch metadata from IPFS")?; + + let json: serde_json::Value = + serde_json::from_slice(&response).context(format!( + "Failed to parse metadata JSON from IPFS: {:?}, data: {:?}", + cid, &response + ))?; + + Ok(json) + } + uri if uri.starts_with("data") => { + // Parse and decode data URI + debug!("Parsing metadata from data URI"); + trace!(data_uri = %token_uri); + + // HACK: https://github.com/servo/rust-url/issues/908 + let uri = token_uri.replace("#", "%23"); + + let data_url = DataUrl::process(&uri).context("Failed to parse data URI")?; + + // Ensure the MIME type is JSON + if data_url.mime_type() != &Mime::from_str("application/json").unwrap() { + return Err(anyhow::anyhow!("Data URI is not of JSON type")); + } + + let decoded = data_url.decode_to_vec().context("Failed to decode data URI")?; + // HACK: Loot Survior NFT metadata contains control characters which makes the json + // DATA invalid so filter them out + let decoded_str = String::from_utf8_lossy(&decoded.0) + .chars() + .filter(|c| !c.is_ascii_control()) + .collect::<String>(); + + let json: serde_json::Value = serde_json::from_str(&decoded_str) + .context(format!("Failed to parse metadata JSON from data URI: {}", &uri))?; + + Ok(json) + } + uri => Err(anyhow::anyhow!("Unsupported URI scheme found in token URI: {}", uri)), + } + } + + pub async fn handle_erc721_token_metadata( + &mut self, + result: RegisterErc721TokenMetadata, + ) -> Result<()> { + let query = sqlx::query( + "INSERT INTO tokens (id, contract_address, name, symbol, decimals, metadata) VALUES \ + (?, ?, ?, ?, ?, ?)", + ) + .bind(&result.query.token_id) + .bind(felt_to_sql_string(&result.query.contract_address)) + .bind(&result.name) + .bind(&result.symbol) + .bind(0) + .bind(&result.metadata); + + query + .execute(&mut *self.transaction) + .await + .with_context(|| format!("Failed to execute721Token query: {:?}", result))?; + + Ok(()) + } +} diff --git a/crates/torii/core/src/executor.rs b/crates/torii/core/src/executor/mod.rs similarity index 64% rename from crates/torii/core/src/executor.rs rename to crates/torii/core/src/executor/mod.rs index a8c0a19c70..b2c3bf7093 100644 --- a/crates/torii/core/src/executor.rs +++ b/crates/torii/core/src/executor/mod.rs @@ -1,29 +1,34 @@ use std::collections::HashMap; use std::mem; use std::str::FromStr; +use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{Context, Result}; +use cainome::cairo_serde::{ByteArray, CairoSerde}; use dojo_types::schema::{Struct, Ty}; -use sqlx::query::Query; -use sqlx::sqlite::SqliteArguments; use sqlx::{FromRow, Pool, Sqlite, Transaction}; -use starknet::core::types::{Felt, U256}; +use starknet::core::types::{BlockId, BlockTag, Felt, FunctionCall}; +use starknet::core::utils::{get_selector_from_name, parse_cairo_short_string}; +use starknet::providers::Provider; use tokio::sync::broadcast::{Receiver, Sender}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; -use tokio::sync::oneshot; +use tokio::sync::{oneshot, Semaphore}; +use tokio::task::JoinSet; use tokio::time::Instant; use tracing::{debug, error}; use crate::simple_broker::SimpleBroker; -use crate::sql::utils::{felt_to_sql_string, sql_string_to_u256, u256_to_sql_string, I256}; -use crate::sql::FELT_DELIMITER; +use crate::sql::utils::{felt_to_sql_string, I256}; use crate::types::{ ContractCursor, ContractType, Entity as EntityUpdated, Event as EventEmitted, EventMessage as EventMessageUpdated, Model as ModelRegistered, OptimisticEntity, OptimisticEventMessage, }; +pub mod erc; +pub use erc::{RegisterErc20TokenQuery, RegisterErc721TokenMetadata, RegisterErc721TokenQuery}; + pub(crate) const LOG_TARGET: &str = "torii_core::executor"; #[derive(Debug, Clone)] @@ -102,14 +107,19 @@ pub enum QueryType { DeleteEntity(DeleteEntityQuery), EventMessage(EventMessageQuery), ApplyBalanceDiff(ApplyBalanceDiffQuery), + RegisterErc721Token(RegisterErc721TokenQuery), + RegisterErc20Token(RegisterErc20TokenQuery), + TokenTransfer, RegisterModel, StoreEvent, + // similar to execute but doesn't create a new transaction + Flush, Execute, Other, } #[derive(Debug)] -pub struct Executor<'c> { +pub struct Executor<'c, P: Provider + Sync + Send + 'static> { // Queries should use `transaction` instead of `pool` // This `pool` is only used to create a new `transaction` pool: Pool<Sqlite>, @@ -117,6 +127,16 @@ pub struct Executor<'c> { publish_queue: Vec<BrokerMessage>, rx: UnboundedReceiver<QueryMessage>, shutdown_rx: Receiver<()>, + // These tasks are spawned to fetch ERC721 token metadata from the chain + // to not block the main loop + register_tasks: JoinSet<Result<RegisterErc721TokenMetadata>>, + // Some queries depends on the metadata being registered, so we defer them + // until the metadata is fetched + deferred_query_messages: Vec<QueryMessage>, + // It is used to make RPC calls to fetch token_uri data for erc721 contracts + provider: Arc<P>, + // Used to limit number of tasks that run in parallel to fetch metadata + semaphore: Arc<Semaphore>, } #[derive(Debug)] @@ -174,19 +194,48 @@ impl QueryMessage { rx, ) } + + pub fn flush_recv() -> (Self, oneshot::Receiver<Result<()>>) { + let (tx, rx) = oneshot::channel(); + ( + Self { + statement: "".to_string(), + arguments: vec![], + query_type: QueryType::Flush, + tx: Some(tx), + }, + rx, + ) + } } -impl<'c> Executor<'c> { +impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { pub async fn new( pool: Pool<Sqlite>, shutdown_tx: Sender<()>, + provider: Arc<P>, + max_concurrent_tasks: usize, ) -> Result<(Self, UnboundedSender<QueryMessage>)> { let (tx, rx) = unbounded_channel(); let transaction = pool.begin().await?; let publish_queue = Vec::new(); let shutdown_rx = shutdown_tx.subscribe(); - - Ok((Executor { pool, transaction, publish_queue, rx, shutdown_rx }, tx)) + let semaphore = Arc::new(Semaphore::new(max_concurrent_tasks)); + + Ok(( + Executor { + pool, + transaction, + publish_queue, + rx, + shutdown_rx, + register_tasks: JoinSet::new(), + deferred_query_messages: Vec::new(), + provider, + semaphore, + }, + tx, + )) } pub async fn run(&mut self) -> Result<()> { @@ -197,41 +246,38 @@ impl<'c> Executor<'c> { break Ok(()); } Some(msg) = self.rx.recv() => { - let QueryMessage { statement, arguments, query_type, tx } = msg; - let mut query = sqlx::query(&statement); - - for arg in &arguments { - query = match arg { - Argument::Null => query.bind(None::<String>), - Argument::Int(integer) => query.bind(integer), - Argument::Bool(bool) => query.bind(bool), - Argument::String(string) => query.bind(string), - Argument::FieldElement(felt) => query.bind(format!("{:#x}", felt)), - } - } - - match self.handle_query_type(query, query_type.clone(), &statement, &arguments, tx).await { + let query_type = msg.query_type.clone(); + match self.handle_query_message(msg).await { Ok(()) => {}, Err(e) => { error!(target: LOG_TARGET, r#type = ?query_type, error = %e, "Failed to execute query."); } } } + Some(result) = self.register_tasks.join_next() => { + let result = result??; + self.handle_erc721_token_metadata(result).await?; + } } } } - async fn handle_query_type<'a>( - &mut self, - query: Query<'a, Sqlite, SqliteArguments<'a>>, - query_type: QueryType, - statement: &str, - arguments: &[Argument], - sender: Option<oneshot::Sender<Result<()>>>, - ) -> Result<()> { + async fn handle_query_message(&mut self, query_message: QueryMessage) -> Result<()> { let tx = &mut self.transaction; - match query_type { + let mut query = sqlx::query(&query_message.statement); + + for arg in &query_message.arguments { + query = match arg { + Argument::Null => query.bind(None::<String>), + Argument::Int(integer) => query.bind(integer), + Argument::Bool(bool) => query.bind(bool), + Argument::String(string) => query.bind(string), + Argument::FieldElement(felt) => query.bind(format!("{:#x}", felt)), + } + } + + match query_message.query_type { QueryType::SetHead(set_head) => { let previous_block_timestamp: u64 = sqlx::query_scalar::<_, i64>( "SELECT last_block_timestamp FROM contracts WHERE id = ?", @@ -249,7 +295,10 @@ impl<'c> Executor<'c> { }; query.execute(&mut **tx).await.with_context(|| { - format!("Failed to execute query: {:?}, args: {:?}", statement, arguments) + format!( + "Failed to execute query: {:?}, args: {:?}", + query_message.statement, query_message.arguments + ) })?; let row = sqlx::query("UPDATE contracts SET tps = ? WHERE id = ? RETURNING *") @@ -373,7 +422,10 @@ impl<'c> Executor<'c> { } QueryType::SetEntity(entity) => { let row = query.fetch_one(&mut **tx).await.with_context(|| { - format!("Failed to execute query: {:?}, args: {:?}", statement, arguments) + format!( + "Failed to execute query: {:?}, args: {:?}", + query_message.statement, query_message.arguments + ) })?; let mut entity_updated = EntityUpdated::from_row(&row)?; entity_updated.updated_model = Some(entity); @@ -396,7 +448,10 @@ impl<'c> Executor<'c> { } QueryType::DeleteEntity(entity) => { let delete_model = query.execute(&mut **tx).await.with_context(|| { - format!("Failed to execute query: {:?}, args: {:?}", statement, arguments) + format!( + "Failed to execute query: {:?}, args: {:?}", + query_message.statement, query_message.arguments + ) })?; if delete_model.rows_affected() == 0 { return Ok(()); @@ -447,7 +502,10 @@ impl<'c> Executor<'c> { } QueryType::RegisterModel => { let row = query.fetch_one(&mut **tx).await.with_context(|| { - format!("Failed to execute query: {:?}, args: {:?}", statement, arguments) + format!( + "Failed to execute query: {:?}, args: {:?}", + query_message.statement, query_message.arguments + ) })?; let model_registered = ModelRegistered::from_row(&row)?; self.publish_queue.push(BrokerMessage::ModelRegistered(model_registered)); @@ -455,7 +513,10 @@ impl<'c> Executor<'c> { QueryType::EventMessage(em_query) => { // Must be executed first since other tables have foreign keys on event_messages.id. let event_messages_row = query.fetch_one(&mut **tx).await.with_context(|| { - format!("Failed to execute query: {:?}, args: {:?}", statement, arguments) + format!( + "Failed to execute query: {:?}, args: {:?}", + query_message.statement, query_message.arguments + ) })?; let mut event_counter: i64 = sqlx::query_scalar::<_, i64>( @@ -525,7 +586,10 @@ impl<'c> Executor<'c> { } QueryType::StoreEvent => { let row = query.fetch_one(&mut **tx).await.with_context(|| { - format!("Failed to execute query: {:?}, args: {:?}", statement, arguments) + format!( + "Failed to execute query: {:?}, args: {:?}", + query_message.statement, query_message.arguments + ) })?; let event = EventEmitted::from_row(&row)?; self.publish_queue.push(BrokerMessage::EventEmitted(event)); @@ -536,13 +600,127 @@ impl<'c> Executor<'c> { self.apply_balance_diff(apply_balance_diff).await?; debug!(target: LOG_TARGET, duration = ?instant.elapsed(), "Applied balance diff."); } + QueryType::RegisterErc721Token(register_erc721_token) => { + let semaphore = self.semaphore.clone(); + let provider = self.provider.clone(); + let res = sqlx::query_as::<_, (String, String)>( + "SELECT name, symbol FROM tokens WHERE contract_address = ?", + ) + .bind(felt_to_sql_string(®ister_erc721_token.contract_address)) + .fetch_one(&mut **tx) + .await; + + // If we find a token already registered for this contract_address we dont need to + // refetch the data since its same for all ERC721 tokens + let (name, symbol) = match res { + Ok((name, symbol)) => { + debug!( + contract_address = %felt_to_sql_string(®ister_erc721_token.contract_address), + "Token already registered for contract_address, so reusing fetched data", + ); + (name, symbol) + } + Err(_) => { + // Fetch token information from the chain + let name = provider + .call( + FunctionCall { + contract_address: register_erc721_token.contract_address, + entry_point_selector: get_selector_from_name("name").unwrap(), + calldata: vec![], + }, + BlockId::Tag(BlockTag::Pending), + ) + .await?; + + // len = 1 => return value felt (i.e. legacy erc721 token) + // len > 1 => return value ByteArray (i.e. new erc721 token) + let name = if name.len() == 1 { + parse_cairo_short_string(&name[0]).unwrap() + } else { + ByteArray::cairo_deserialize(&name, 0) + .expect("Return value not ByteArray") + .to_string() + .expect("Return value not String") + }; + + let symbol = provider + .call( + FunctionCall { + contract_address: register_erc721_token.contract_address, + entry_point_selector: get_selector_from_name("symbol").unwrap(), + calldata: vec![], + }, + BlockId::Tag(BlockTag::Pending), + ) + .await?; + let symbol = if symbol.len() == 1 { + parse_cairo_short_string(&symbol[0]).unwrap() + } else { + ByteArray::cairo_deserialize(&symbol, 0) + .expect("Return value not ByteArray") + .to_string() + .expect("Return value not String") + }; + + (name, symbol) + } + }; + + self.register_tasks.spawn(async move { + let permit = semaphore.acquire().await.unwrap(); + + let result = Self::process_register_erc721_token_query( + register_erc721_token, + provider, + name, + symbol, + ) + .await; + + drop(permit); + result + }); + } + QueryType::RegisterErc20Token(register_erc20_token) => { + let query = sqlx::query( + "INSERT INTO tokens (id, contract_address, name, symbol, decimals) VALUES (?, \ + ?, ?, ?, ?)", + ) + .bind(®ister_erc20_token.token_id) + .bind(felt_to_sql_string(®ister_erc20_token.contract_address)) + .bind(®ister_erc20_token.name) + .bind(®ister_erc20_token.symbol) + .bind(register_erc20_token.decimals); + + query.execute(&mut **tx).await.with_context(|| { + format!( + "Failed to execute RegisterErc20Token query: {:?}", + register_erc20_token + ) + })?; + } + QueryType::Flush => { + debug!(target: LOG_TARGET, "Flushing query."); + let instant = Instant::now(); + let res = self.execute(false).await; + debug!(target: LOG_TARGET, duration = ?instant.elapsed(), "Flushed query."); + + if let Some(sender) = query_message.tx { + sender + .send(res) + .map_err(|_| anyhow::anyhow!("Failed to send execute result"))?; + } else { + res?; + } + } QueryType::Execute => { debug!(target: LOG_TARGET, "Executing query."); let instant = Instant::now(); - let res = self.execute().await; + let res = self.execute(true).await; debug!(target: LOG_TARGET, duration = ?instant.elapsed(), "Executed query."); - if let Some(sender) = sender { + if let Some(sender) = query_message.tx { sender .send(res) .map_err(|_| anyhow::anyhow!("Failed to send execute result"))?; @@ -550,9 +728,16 @@ impl<'c> Executor<'c> { res?; } } + QueryType::TokenTransfer => { + // defer executing these queries since they depend on TokenRegister queries + self.deferred_query_messages.push(query_message); + } QueryType::Other => { query.execute(&mut **tx).await.with_context(|| { - format!("Failed to execute query: {:?}, args: {:?}", statement, arguments) + format!( + "Failed to execute query: {:?}, args: {:?}", + query_message.statement, query_message.arguments + ) })?; } } @@ -560,109 +745,42 @@ impl<'c> Executor<'c> { Ok(()) } - async fn execute(&mut self) -> Result<()> { - let transaction = mem::replace(&mut self.transaction, self.pool.begin().await?); - transaction.commit().await?; + async fn execute(&mut self, new_transaction: bool) -> Result<()> { + if new_transaction { + let transaction = mem::replace(&mut self.transaction, self.pool.begin().await?); + transaction.commit().await?; + } for message in self.publish_queue.drain(..) { send_broker_message(message); } - Ok(()) - } - - async fn apply_balance_diff( - &mut self, - apply_balance_diff: ApplyBalanceDiffQuery, - ) -> Result<()> { - let erc_cache = apply_balance_diff.erc_cache; - for ((contract_type, id_str), balance) in erc_cache.iter() { - let id = id_str.split(FELT_DELIMITER).collect::<Vec<&str>>(); - match contract_type { - ContractType::WORLD => unreachable!(), - ContractType::ERC721 => { - // account_address/contract_address:id => ERC721 - assert!(id.len() == 2); - let account_address = id[0]; - let token_id = id[1]; - let mid = token_id.split(":").collect::<Vec<&str>>(); - let contract_address = mid[0]; - - self.apply_balance_diff_helper( - id_str, - account_address, - contract_address, - token_id, - balance, - ) - .await - .with_context(|| "Failed to apply balance diff in apply_cache_diff")?; - } - ContractType::ERC20 => { - // account_address/contract_address/ => ERC20 - assert!(id.len() == 3); - let account_address = id[0]; - let contract_address = id[1]; - let token_id = id[1]; - - self.apply_balance_diff_helper( - id_str, - account_address, - contract_address, - token_id, - balance, - ) - .await - .with_context(|| "Failed to apply balance diff in apply_cache_diff")?; - } - } + while let Some(result) = self.register_tasks.join_next().await { + let result = result??; + self.handle_erc721_token_metadata(result).await?; } - Ok(()) - } - - async fn apply_balance_diff_helper( - &mut self, - id: &str, - account_address: &str, - contract_address: &str, - token_id: &str, - balance_diff: &I256, - ) -> Result<()> { - let tx = &mut self.transaction; - let balance: Option<(String,)> = - sqlx::query_as("SELECT balance FROM balances WHERE id = ?") - .bind(id) - .fetch_optional(&mut **tx) - .await?; - - let mut balance = if let Some(balance) = balance { - sql_string_to_u256(&balance.0) - } else { - U256::from(0u8) - }; - - if balance_diff.is_negative { - if balance < balance_diff.value { - dbg!(&balance_diff, balance, id); + let mut deferred_query_messages = mem::take(&mut self.deferred_query_messages); + + for query_message in deferred_query_messages.drain(..) { + let mut query = sqlx::query(&query_message.statement); + for arg in &query_message.arguments { + query = match arg { + Argument::Null => query.bind(None::<String>), + Argument::Int(integer) => query.bind(integer), + Argument::Bool(bool) => query.bind(bool), + Argument::String(string) => query.bind(string), + Argument::FieldElement(felt) => query.bind(format!("{:#x}", felt)), + }; } - balance -= balance_diff.value; - } else { - balance += balance_diff.value; - } - // write the new balance to the database - sqlx::query( - "INSERT OR REPLACE INTO balances (id, contract_address, account_address, token_id, \ - balance) VALUES (?, ?, ?, ?, ?)", - ) - .bind(id) - .bind(contract_address) - .bind(account_address) - .bind(token_id) - .bind(u256_to_sql_string(&balance)) - .execute(&mut **tx) - .await?; + query.execute(&mut *self.transaction).await.with_context(|| { + format!( + "Failed to execute query: {:?}, args: {:?}", + query_message.statement, query_message.arguments + ) + })?; + } Ok(()) } diff --git a/crates/torii/core/src/processors/erc721_legacy_transfer.rs b/crates/torii/core/src/processors/erc721_legacy_transfer.rs index b3fdcbbfe8..ebbf4d6b53 100644 --- a/crates/torii/core/src/processors/erc721_legacy_transfer.rs +++ b/crates/torii/core/src/processors/erc721_legacy_transfer.rs @@ -36,7 +36,7 @@ where async fn process( &self, - world: &WorldContractReader<P>, + _world: &WorldContractReader<P>, db: &mut Sql, _block_number: u64, block_timestamp: u64, @@ -51,16 +51,8 @@ where let token_id = U256Cainome::cairo_deserialize(&event.data, 2)?; let token_id = U256::from_words(token_id.low, token_id.high); - db.handle_erc721_transfer( - token_address, - from, - to, - token_id, - world.provider(), - block_timestamp, - event_id, - ) - .await?; + db.handle_erc721_transfer(token_address, from, to, token_id, block_timestamp, event_id) + .await?; debug!(target: LOG_TARGET, from = ?from, to = ?to, token_id = ?token_id, "ERC721 Transfer"); Ok(()) diff --git a/crates/torii/core/src/processors/erc721_transfer.rs b/crates/torii/core/src/processors/erc721_transfer.rs index 266ea18e51..a0f56479a9 100644 --- a/crates/torii/core/src/processors/erc721_transfer.rs +++ b/crates/torii/core/src/processors/erc721_transfer.rs @@ -36,7 +36,7 @@ where async fn process( &self, - world: &WorldContractReader<P>, + _world: &WorldContractReader<P>, db: &mut Sql, _block_number: u64, block_timestamp: u64, @@ -51,16 +51,8 @@ where let token_id = U256Cainome::cairo_deserialize(&event.keys, 3)?; let token_id = U256::from_words(token_id.low, token_id.high); - db.handle_erc721_transfer( - token_address, - from, - to, - token_id, - world.provider(), - block_timestamp, - event_id, - ) - .await?; + db.handle_erc721_transfer(token_address, from, to, token_id, block_timestamp, event_id) + .await?; debug!(target: LOG_TARGET, from = ?from, to = ?to, token_id = ?token_id, "ERC721 Transfer"); Ok(()) diff --git a/crates/torii/core/src/processors/metadata_update.rs b/crates/torii/core/src/processors/metadata_update.rs index 76a9f37c12..00cedd800a 100644 --- a/crates/torii/core/src/processors/metadata_update.rs +++ b/crates/torii/core/src/processors/metadata_update.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use anyhow::{Error, Result}; use async_trait::async_trait; use base64::engine::general_purpose; @@ -9,17 +7,13 @@ use dojo_world::config::WorldMetadata; use dojo_world::contracts::abigen::world::Event as WorldEvent; use dojo_world::contracts::world::WorldContractReader; use dojo_world::uri::Uri; -use reqwest::Client; use starknet::core::types::{Event, Felt}; use starknet::providers::Provider; -use tokio_util::bytes::Bytes; use tracing::{error, info}; use super::{EventProcessor, EventProcessorConfig}; use crate::sql::Sql; - -const IPFS_URL: &str = "https://cartridge.infura-ipfs.io/ipfs/"; -const MAX_RETRY: u8 = 3; +use crate::utils::{fetch_content_from_ipfs, MAX_RETRY}; pub(crate) const LOG_TARGET: &str = "torii_core::processors::metadata_update"; @@ -112,7 +106,7 @@ async fn metadata(uri_str: String) -> Result<(WorldMetadata, Option<String>, Opt let uri = Uri::Ipfs(uri_str); let cid = uri.cid().ok_or("Uri is malformed").map_err(Error::msg)?; - let bytes = fetch_content(cid, MAX_RETRY).await?; + let bytes = fetch_content_from_ipfs(cid, MAX_RETRY).await?; let metadata: WorldMetadata = serde_json::from_str(std::str::from_utf8(&bytes)?)?; let icon_img = fetch_image(&metadata.icon_uri).await; @@ -123,36 +117,10 @@ async fn metadata(uri_str: String) -> Result<(WorldMetadata, Option<String>, Opt async fn fetch_image(image_uri: &Option<Uri>) -> Option<String> { if let Some(uri) = image_uri { - let data = fetch_content(uri.cid()?, MAX_RETRY).await.ok()?; + let data = fetch_content_from_ipfs(uri.cid()?, MAX_RETRY).await.ok()?; let encoded = general_purpose::STANDARD.encode(data); return Some(encoded); } None } - -async fn fetch_content(cid: &str, mut retries: u8) -> Result<Bytes> { - while retries > 0 { - let response = Client::new().get(format!("{IPFS_URL}{}", cid)).send().await; - - match response { - Ok(response) => return response.bytes().await.map_err(|e| e.into()), - Err(e) => { - retries -= 1; - if retries > 0 { - info!( - target: LOG_TARGET, - error = %e, - "Fetch uri." - ); - tokio::time::sleep(Duration::from_secs(3)).await; - } - } - } - } - - Err(Error::msg(format!( - "Failed to pull data from IPFS after {} attempts, cid: {}", - MAX_RETRY, cid - ))) -} diff --git a/crates/torii/core/src/sql/erc.rs b/crates/torii/core/src/sql/erc.rs index f82eacb1a2..0a7acc643d 100644 --- a/crates/torii/core/src/sql/erc.rs +++ b/crates/torii/core/src/sql/erc.rs @@ -6,11 +6,13 @@ use cainome::cairo_serde::{ByteArray, CairoSerde}; use starknet::core::types::{BlockId, BlockTag, Felt, FunctionCall, U256}; use starknet::core::utils::{get_selector_from_name, parse_cairo_short_string}; use starknet::providers::Provider; -use tracing::debug; use super::utils::{u256_to_sql_string, I256}; use super::{Sql, FELT_DELIMITER}; -use crate::executor::{ApplyBalanceDiffQuery, Argument, QueryMessage, QueryType}; +use crate::executor::{ + ApplyBalanceDiffQuery, Argument, QueryMessage, QueryType, RegisterErc20TokenQuery, + RegisterErc721TokenQuery, +}; use crate::sql::utils::{felt_and_u256_to_sql_string, felt_to_sql_string, felts_to_sql_string}; use crate::types::ContractType; use crate::utils::utc_dt_string_from_timestamp; @@ -34,7 +36,6 @@ impl Sql { if !token_exists { self.register_erc20_token_metadata(contract_address, &token_id, provider).await?; - self.execute().await.with_context(|| "Failed to execute in handle_erc20_transfer")?; } self.store_erc_transfer_event( @@ -66,6 +67,7 @@ impl Sql { } if self.local_cache.erc_cache.len() >= 100000 { + self.flush().await.with_context(|| "Failed to flush in handle_erc20_transfer")?; self.apply_cache_diff().await?; } @@ -73,23 +75,23 @@ impl Sql { } #[allow(clippy::too_many_arguments)] - pub async fn handle_erc721_transfer<P: Provider + Sync>( + pub async fn handle_erc721_transfer( &mut self, contract_address: Felt, from_address: Felt, to_address: Felt, token_id: U256, - provider: &P, block_timestamp: u64, event_id: &str, ) -> Result<()> { // contract_address:id + let actual_token_id = token_id; let token_id = felt_and_u256_to_sql_string(&contract_address, &token_id); let token_exists: bool = self.local_cache.contains_token_id(&token_id); if !token_exists { - self.register_erc721_token_metadata(contract_address, &token_id, provider).await?; - self.execute().await?; + self.register_erc721_token_metadata(contract_address, &token_id, actual_token_id) + .await?; } self.store_erc_transfer_event( @@ -126,6 +128,7 @@ impl Sql { } if self.local_cache.erc_cache.len() >= 100000 { + self.flush().await.with_context(|| "Failed to flush in handle_erc721_transfer")?; self.apply_cache_diff().await?; } @@ -193,18 +196,16 @@ impl Sql { .await?; let decimals = u8::cairo_deserialize(&decimals, 0).expect("Return value not u8"); - // Insert the token into the tokens table - self.executor.send(QueryMessage::other( - "INSERT INTO tokens (id, contract_address, name, symbol, decimals) VALUES (?, ?, ?, \ - ?, ?)" - .to_string(), - vec![ - Argument::String(token_id.to_string()), - Argument::FieldElement(contract_address), - Argument::String(name), - Argument::String(symbol), - Argument::Int(decimals.into()), - ], + self.executor.send(QueryMessage::new( + "".to_string(), + vec![], + QueryType::RegisterErc20Token(RegisterErc20TokenQuery { + token_id: token_id.to_string(), + contract_address, + name, + symbol, + decimals, + }), ))?; self.local_cache.register_token_id(token_id.to_string()); @@ -212,100 +213,26 @@ impl Sql { Ok(()) } - async fn register_erc721_token_metadata<P: Provider + Sync>( + async fn register_erc721_token_metadata( &mut self, contract_address: Felt, token_id: &str, - provider: &P, + actual_token_id: U256, ) -> Result<()> { - let res = sqlx::query_as::<_, (String, String, u8)>( - "SELECT name, symbol, decimals FROM tokens WHERE contract_address = ?", - ) - .bind(felt_to_sql_string(&contract_address)) - .fetch_one(&self.pool) - .await; - - // If we find a token already registered for this contract_address we dont need to refetch - // the data since its same for all ERC721 tokens - if let Ok((name, symbol, decimals)) = res { - debug!( - contract_address = %felt_to_sql_string(&contract_address), - "Token already registered for contract_address, so reusing fetched data", - ); - self.executor.send(QueryMessage::other( - "INSERT INTO tokens (id, contract_address, name, symbol, decimals) VALUES (?, ?, \ - ?, ?, ?)" - .to_string(), - vec![ - Argument::String(token_id.to_string()), - Argument::FieldElement(contract_address), - Argument::String(name), - Argument::String(symbol), - Argument::Int(decimals.into()), - ], - ))?; - self.local_cache.register_token_id(token_id.to_string()); - return Ok(()); - } - - // Fetch token information from the chain - let name = provider - .call( - FunctionCall { - contract_address, - entry_point_selector: get_selector_from_name("name").unwrap(), - calldata: vec![], - }, - BlockId::Tag(BlockTag::Pending), - ) - .await?; - - // len = 1 => return value felt (i.e. legacy erc721 token) - // len > 1 => return value ByteArray (i.e. new erc721 token) - let name = if name.len() == 1 { - parse_cairo_short_string(&name[0]).unwrap() - } else { - ByteArray::cairo_deserialize(&name, 0) - .expect("Return value not ByteArray") - .to_string() - .expect("Return value not String") - }; - - let symbol = provider - .call( - FunctionCall { - contract_address, - entry_point_selector: get_selector_from_name("symbol").unwrap(), - calldata: vec![], - }, - BlockId::Tag(BlockTag::Pending), - ) - .await?; - let symbol = if symbol.len() == 1 { - parse_cairo_short_string(&symbol[0]).unwrap() - } else { - ByteArray::cairo_deserialize(&symbol, 0) - .expect("Return value not ByteArray") - .to_string() - .expect("Return value not String") - }; - - let decimals = 0; - - // Insert the token into the tokens table - self.executor.send(QueryMessage::other( - "INSERT INTO tokens (id, contract_address, name, symbol, decimals) VALUES (?, ?, ?, \ - ?, ?)" - .to_string(), - vec![ - Argument::String(token_id.to_string()), - Argument::FieldElement(contract_address), - Argument::String(name), - Argument::String(symbol), - Argument::Int(decimals.into()), - ], + self.executor.send(QueryMessage::new( + "".to_string(), + vec![], + QueryType::RegisterErc721Token(RegisterErc721TokenQuery { + token_id: token_id.to_string(), + contract_address, + actual_token_id, + }), ))?; + // optimistically add the token_id to cache + // this cache is used while applying the cache diff + // so we need to make sure that all RegisterErc*Token queries + // are applied before the cache diff is applied self.local_cache.register_token_id(token_id.to_string()); Ok(()) @@ -326,7 +253,7 @@ impl Sql { to_address, amount, token_id, executed_at) VALUES (?, ?, ?, ?, ?, ?, \ ?)"; - self.executor.send(QueryMessage::other( + self.executor.send(QueryMessage::new( insert_query.to_string(), vec![ Argument::String(event_id.to_string()), @@ -337,6 +264,7 @@ impl Sql { Argument::String(token_id.to_string()), Argument::String(utc_dt_string_from_timestamp(block_timestamp)), ], + QueryType::TokenTransfer, ))?; Ok(()) diff --git a/crates/torii/core/src/sql/mod.rs b/crates/torii/core/src/sql/mod.rs index 8c6302447d..11eedf6a3c 100644 --- a/crates/torii/core/src/sql/mod.rs +++ b/crates/torii/core/src/sql/mod.rs @@ -1305,4 +1305,10 @@ impl Sql { self.executor.send(execute)?; recv.await? } + + pub async fn flush(&self) -> Result<()> { + let (flush, recv) = QueryMessage::flush_recv(); + self.executor.send(flush)?; + recv.await? + } } diff --git a/crates/torii/core/src/sql/test.rs b/crates/torii/core/src/sql/test.rs index dde4934ee1..7d79f3ba4a 100644 --- a/crates/torii/core/src/sql/test.rs +++ b/crates/torii/core/src/sql/test.rs @@ -120,7 +120,8 @@ async fn test_load_from_remote(sequencer: &RunnerCtx) { sqlx::migrate!("../migrations").run(&pool).await.unwrap(); let (shutdown_tx, _) = broadcast::channel(1); - let (mut executor, sender) = Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + let (mut executor, sender) = + Executor::new(pool.clone(), shutdown_tx.clone(), Arc::clone(&provider), 100).await.unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); @@ -280,7 +281,8 @@ async fn test_load_from_remote_del(sequencer: &RunnerCtx) { sqlx::migrate!("../migrations").run(&pool).await.unwrap(); let (shutdown_tx, _) = broadcast::channel(1); - let (mut executor, sender) = Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + let (mut executor, sender) = + Executor::new(pool.clone(), shutdown_tx.clone(), Arc::clone(&provider), 100).await.unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); @@ -295,7 +297,7 @@ async fn test_load_from_remote_del(sequencer: &RunnerCtx) { .await .unwrap(); - let _ = bootstrap_engine(world_reader, db.clone(), provider).await; + let _ = bootstrap_engine(world_reader, db.clone(), Arc::clone(&provider)).await.unwrap(); // TODO: seems that we don't delete the record after delete only values are zeroed? assert_eq!(count_table("ns-PlayerConfig", &pool).await, 0); @@ -368,7 +370,9 @@ async fn test_update_with_set_record(sequencer: &RunnerCtx) { sqlx::migrate!("../migrations").run(&pool).await.unwrap(); let (shutdown_tx, _) = broadcast::channel(1); - let (mut executor, sender) = Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + + let (mut executor, sender) = + Executor::new(pool.clone(), shutdown_tx.clone(), Arc::clone(&provider), 100).await.unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); diff --git a/crates/torii/core/src/utils.rs b/crates/torii/core/src/utils.rs index 55f7ee563e..d66d294509 100644 --- a/crates/torii/core/src/utils.rs +++ b/crates/torii/core/src/utils.rs @@ -1,4 +1,19 @@ +use std::time::Duration; + +use anyhow::Result; use chrono::{DateTime, Utc}; +use futures_util::TryStreamExt; +use ipfs_api_backend_hyper::{IpfsApi, IpfsClient, TryFromUri}; +use tokio_util::bytes::Bytes; +use tracing::info; + +// pub const IPFS_URL: &str = "https://cartridge.infura-ipfs.io/ipfs/"; +pub const IPFS_URL: &str = "https://ipfs.io/ipfs/"; +pub const MAX_RETRY: u8 = 3; + +pub const IPFS_CLIENT_URL: &str = "https://ipfs.infura.io:5001"; +pub const IPFS_USERNAME: &str = "2EBrzr7ZASQZKH32sl2xWauXPSA"; +pub const IPFS_PASSWORD: &str = "12290b883db9138a8ae3363b6739d220"; pub fn must_utc_datetime_from_timestamp(timestamp: u64) -> DateTime<Utc> { let naive_dt = DateTime::from_timestamp(timestamp as i64, 0) @@ -10,6 +25,31 @@ pub fn utc_dt_string_from_timestamp(timestamp: u64) -> String { must_utc_datetime_from_timestamp(timestamp).to_rfc3339() } +pub async fn fetch_content_from_ipfs(cid: &str, mut retries: u8) -> Result<Bytes> { + let client = + IpfsClient::from_str(IPFS_CLIENT_URL)?.with_credentials(IPFS_USERNAME, IPFS_PASSWORD); + while retries > 0 { + let response = client.cat(cid).map_ok(|chunk| chunk.to_vec()).try_concat().await; + match response { + Ok(stream) => return Ok(Bytes::from(stream)), + Err(e) => { + retries -= 1; + if retries > 0 { + info!( + error = %e, + "Fetch uri." + ); + tokio::time::sleep(Duration::from_secs(3)).await; + } + } + } + } + + Err(anyhow::anyhow!(format!( + "Failed to pull data from IPFS after {} attempts, cid: {}", + MAX_RETRY, cid + ))) +} // tests #[cfg(test)] mod tests { diff --git a/crates/torii/graphql/src/tests/metadata_test.rs b/crates/torii/graphql/src/tests/metadata_test.rs index 408731d569..b9cfab8a93 100644 --- a/crates/torii/graphql/src/tests/metadata_test.rs +++ b/crates/torii/graphql/src/tests/metadata_test.rs @@ -5,11 +5,14 @@ mod tests { use dojo_world::config::{ProfileConfig, WorldMetadata}; use sqlx::SqlitePool; use starknet::core::types::Felt; + use starknet::providers::jsonrpc::HttpTransport; + use starknet::providers::JsonRpcClient; use tokio::sync::broadcast; use torii_core::executor::Executor; use torii_core::sql::cache::ModelCache; use torii_core::sql::Sql; use torii_core::types::{Contract, ContractType}; + use url::Url; use crate::schema::build_schema; use crate::tests::{run_graphql_query, Connection, Content, Metadata as SqlMetadata, Social}; @@ -54,8 +57,12 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn test_metadata(pool: SqlitePool) { let (shutdown_tx, _) = broadcast::channel(1); + let url: Url = "https://www.example.com".parse().unwrap(); + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(url))); let (mut executor, sender) = - Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + Executor::new(pool.clone(), shutdown_tx.clone(), Arc::clone(&provider), 100) + .await + .unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); @@ -120,8 +127,12 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn test_empty_content(pool: SqlitePool) { let (shutdown_tx, _) = broadcast::channel(1); + let url: Url = "https://www.example.com".parse().unwrap(); + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(url))); let (mut executor, sender) = - Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + Executor::new(pool.clone(), shutdown_tx.clone(), Arc::clone(&provider), 100) + .await + .unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index d351651b6c..06d7afa747 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -343,7 +343,8 @@ pub async fn spinup_types_test(path: &str) -> Result<SqlitePool> { let world = WorldContractReader::new(world_address, Arc::clone(&provider)); let (shutdown_tx, _) = broadcast::channel(1); - let (mut executor, sender) = Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + let (mut executor, sender) = + Executor::new(pool.clone(), shutdown_tx.clone(), Arc::clone(&provider), 100).await.unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); diff --git a/crates/torii/graphql/src/tests/subscription_test.rs b/crates/torii/graphql/src/tests/subscription_test.rs index f1b6455b91..39146feddf 100644 --- a/crates/torii/graphql/src/tests/subscription_test.rs +++ b/crates/torii/graphql/src/tests/subscription_test.rs @@ -12,13 +12,17 @@ mod tests { use serial_test::serial; use sqlx::SqlitePool; use starknet::core::types::Event; + use starknet::providers::jsonrpc::HttpTransport; + use starknet::providers::JsonRpcClient; use starknet_crypto::{poseidon_hash_many, Felt}; use tokio::sync::{broadcast, mpsc}; use torii_core::executor::Executor; use torii_core::sql::cache::ModelCache; use torii_core::sql::utils::felts_to_sql_string; use torii_core::sql::Sql; + use torii_core::types::ContractType; use torii_core::types::{Contract, ContractType}; + use url::Url; use crate::tests::{model_fixtures, run_graphql_subscription}; use crate::utils; @@ -27,8 +31,11 @@ mod tests { #[serial] async fn test_entity_subscription(pool: SqlitePool) { let (shutdown_tx, _) = broadcast::channel(1); + // used to fetch token_uri data for erc721 tokens so pass dummy for the test + let url: Url = "https://www.example.com".parse().unwrap(); + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(url))); let (mut executor, sender) = - Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + Executor::new(pool.clone(), shutdown_tx.clone(), provider, 100).await.unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); @@ -177,8 +184,13 @@ mod tests { #[serial] async fn test_entity_subscription_with_id(pool: SqlitePool) { let (shutdown_tx, _) = broadcast::channel(1); + + // dummy provider since its required to query data for erc721 tokens + let url: Url = "https://www.example.com".parse().unwrap(); + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(url))); + let (mut executor, sender) = - Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + Executor::new(pool.clone(), shutdown_tx.clone(), provider, 100).await.unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); @@ -307,8 +319,11 @@ mod tests { #[serial] async fn test_model_subscription(pool: SqlitePool) { let (shutdown_tx, _) = broadcast::channel(1); + + let url: Url = "https://www.example.com".parse().unwrap(); + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(url))); let (mut executor, sender) = - Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + Executor::new(pool.clone(), shutdown_tx.clone(), provider, 100).await.unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); @@ -388,8 +403,11 @@ mod tests { #[serial] async fn test_model_subscription_with_id(pool: SqlitePool) { let (shutdown_tx, _) = broadcast::channel(1); + + let url: Url = "https://www.example.com".parse().unwrap(); + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(url))); let (mut executor, sender) = - Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + Executor::new(pool.clone(), shutdown_tx.clone(), provider, 100).await.unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); @@ -470,8 +488,11 @@ mod tests { #[serial] async fn test_event_emitted(pool: SqlitePool) { let (shutdown_tx, _) = broadcast::channel(1); + + let url: Url = "https://www.example.com".parse().unwrap(); + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(url))); let (mut executor, sender) = - Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + Executor::new(pool.clone(), shutdown_tx.clone(), provider, 100).await.unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); diff --git a/crates/torii/grpc/src/server/tests/entities_test.rs b/crates/torii/grpc/src/server/tests/entities_test.rs index b5f1e83e1c..031779da58 100644 --- a/crates/torii/grpc/src/server/tests/entities_test.rs +++ b/crates/torii/grpc/src/server/tests/entities_test.rs @@ -89,7 +89,9 @@ async fn test_entities_queries(sequencer: &RunnerCtx) { TransactionWaiter::new(tx.transaction_hash, &provider).await.unwrap(); let (shutdown_tx, _) = broadcast::channel(1); - let (mut executor, sender) = Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + + let (mut executor, sender) = + Executor::new(pool.clone(), shutdown_tx.clone(), Arc::clone(&provider), 100).await.unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); diff --git a/crates/torii/libp2p/Cargo.toml b/crates/torii/libp2p/Cargo.toml index 5e356d81f7..05cf945115 100644 --- a/crates/torii/libp2p/Cargo.toml +++ b/crates/torii/libp2p/Cargo.toml @@ -20,8 +20,8 @@ dojo-types.workspace = true dojo-world.workspace = true indexmap.workspace = true serde_json.workspace = true -starknet.workspace = true starknet-crypto.workspace = true +starknet.workspace = true thiserror.workspace = true tracing.workspace = true diff --git a/crates/torii/libp2p/src/tests.rs b/crates/torii/libp2p/src/tests.rs index dc14d131fa..5dc6ae6bf0 100644 --- a/crates/torii/libp2p/src/tests.rs +++ b/crates/torii/libp2p/src/tests.rs @@ -569,13 +569,15 @@ mod test { let sequencer = KatanaRunner::new().expect("Failed to create Katana sequencer"); - let provider = JsonRpcClient::new(HttpTransport::new(sequencer.url())); + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(sequencer.url()))); let account = sequencer.account_data(0); let (shutdown_tx, _) = broadcast::channel(1); let (mut executor, sender) = - Executor::new(pool.clone(), shutdown_tx.clone()).await.unwrap(); + Executor::new(pool.clone(), shutdown_tx.clone(), Arc::clone(&provider), 100) + .await + .unwrap(); tokio::spawn(async move { executor.run().await.unwrap(); }); diff --git a/crates/torii/migrations/20241014085532_add_metadata_field.sql b/crates/torii/migrations/20241014085532_add_metadata_field.sql new file mode 100644 index 0000000000..f6f81432c2 --- /dev/null +++ b/crates/torii/migrations/20241014085532_add_metadata_field.sql @@ -0,0 +1 @@ +ALTER TABLE tokens ADD COLUMN metadata TEXT; \ No newline at end of file diff --git a/crates/torii/server/Cargo.toml b/crates/torii/server/Cargo.toml index 1ca82c911c..14d503748d 100644 --- a/crates/torii/server/Cargo.toml +++ b/crates/torii/server/Cargo.toml @@ -6,17 +6,26 @@ version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow.workspace = true base64.workspace = true -http.workspace = true +camino.workspace = true +data-url.workspace = true http-body = "0.4.5" -hyper.workspace = true +http.workspace = true hyper-reverse-proxy = { git = "https://github.com/tarrencev/hyper-reverse-proxy" } +hyper.workspace = true +image.workspace = true indexmap.workspace = true lazy_static.workspace = true +mime_guess.workspace = true +reqwest.workspace = true serde.workspace = true serde_json.workspace = true -tokio.workspace = true +sqlx.workspace = true tokio-util = "0.7.7" -tower.workspace = true +tokio.workspace = true +torii-core.workspace = true tower-http.workspace = true +tower.workspace = true tracing.workspace = true +warp.workspace = true diff --git a/crates/torii/server/src/artifacts.rs b/crates/torii/server/src/artifacts.rs new file mode 100644 index 0000000000..260d1e48d0 --- /dev/null +++ b/crates/torii/server/src/artifacts.rs @@ -0,0 +1,336 @@ +use std::future::Future; +use std::io::Cursor; +use std::net::SocketAddr; +use std::str::FromStr; + +use anyhow::{Context, Result}; +use camino::Utf8PathBuf; +use data_url::mime::Mime; +use data_url::DataUrl; +use image::{DynamicImage, ImageFormat}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use sqlx::{Pool, Sqlite}; +use tokio::fs; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::sync::broadcast::Receiver; +use torii_core::utils::{fetch_content_from_ipfs, MAX_RETRY}; +use tracing::{debug, error, trace}; +use warp::http::Response; +use warp::path::Tail; +use warp::{reject, Filter}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ImageQuery { + #[serde(alias = "h")] + height: Option<u32>, + #[serde(alias = "w")] + width: Option<u32>, +} + +async fn serve_static_file( + path: Tail, + artifacts_dir: Utf8PathBuf, + pool: Pool<Sqlite>, + query: ImageQuery, +) -> Result<impl warp::Reply, warp::Rejection> { + let path = path.as_str(); + + // Split the path and validate format + let parts: Vec<&str> = path.split('/').collect(); + + if parts.len() != 3 || parts[2] != "image" { + return Err(reject::not_found()); + } + + // Validate contract_address format + if !parts[0].starts_with("0x") { + return Err(reject::not_found()); + } + + // Validate token_id format + if !parts[1].starts_with("0x") { + return Err(reject::not_found()); + } + + let token_image_dir = artifacts_dir.join(parts[0]).join(parts[1]); + + let token_id = format!("{}:{}", parts[0], parts[1]); + if !token_image_dir.exists() { + match fetch_and_process_image(&artifacts_dir, &token_id, pool) + .await + .context(format!("Failed to fetch and process image for token_id: {}", token_id)) + { + Ok(path) => path, + Err(e) => { + error!(error = %e, "Failed to fetch and process image for token_id: {}", token_id); + return Err(warp::reject::not_found()); + } + }; + } + let file_name = match file_name_from_dir_and_query(token_image_dir, &query) { + Ok(file_name) => file_name, + Err(e) => { + error!(error = %e, "Failed to get file name from directory and query"); + return Err(reject::not_found()); + } + }; + + match File::open(&file_name).await { + Ok(mut file) => { + let mut contents = vec![]; + if file.read_to_end(&mut contents).await.is_ok() { + let mime = mime_guess::from_path(&file_name).first_or_octet_stream().to_string(); + + Ok(Response::builder().header("content-type", mime).body(contents)) + } else { + Err(reject::not_found()) + } + } + Err(_) => Err(reject::not_found()), + } +} + +fn file_name_from_dir_and_query( + token_image_dir: Utf8PathBuf, + query: &ImageQuery, +) -> Result<Utf8PathBuf> { + let mut entries = std::fs::read_dir(&token_image_dir).ok().into_iter().flatten().flatten(); + + // Find the base image (without @medium or @small) + let base_image = entries + .find(|entry| { + entry + .file_name() + .to_str() + .map(|name| name.starts_with("image") && !name.contains('@')) + .unwrap_or(false) + }) + .with_context(|| "Failed to find base image")?; + + let base_filename = base_image.file_name(); + let base_filename = base_filename.to_str().unwrap(); + let base_ext = base_filename.split('.').last().unwrap(); + + let suffix = match (query.width, query.height) { + // If either dimension is <= 100px, use small version + (Some(w), _) if w <= 100 => "@small", + (_, Some(h)) if h <= 100 => "@small", + // If either dimension is <= 250px, use medium version + (Some(w), _) if w <= 250 => "@medium", + (_, Some(h)) if h <= 250 => "@medium", + // If no dimensions specified or larger than 250px, use original + _ => "", + }; + + let target_filename = format!("image{}.{}", suffix, base_ext); + Ok(token_image_dir.join(target_filename)) +} + +pub async fn new( + mut shutdown_rx: Receiver<()>, + static_dir: &Utf8PathBuf, + pool: Pool<Sqlite>, +) -> Result<(SocketAddr, impl Future<Output = ()> + 'static), std::io::Error> { + let static_dir = static_dir.clone(); + + let routes = warp::get() + .and(warp::path("static")) + .and(warp::path::tail()) + .and(warp::any().map(move || static_dir.clone())) + .and(warp::any().map(move || pool.clone())) + .and(warp::any().and(warp::query::<ImageQuery>())) + .and_then(serve_static_file); + + Ok(warp::serve(routes).bind_with_graceful_shutdown(([127, 0, 0, 1], 0), async move { + shutdown_rx.recv().await.ok(); + })) +} + +async fn fetch_and_process_image( + artifacts_path: &Utf8PathBuf, + token_id: &str, + pool: Pool<Sqlite>, +) -> anyhow::Result<String> { + let query = sqlx::query_as::<_, (String,)>("SELECT metadata FROM tokens WHERE id = ?") + .bind(token_id) + .fetch_one(&pool) + .await + .with_context(|| { + format!("Failed to fetch metadata from database for token_id: {}", token_id) + })?; + + let metadata: serde_json::Value = + serde_json::from_str(&query.0).context("Failed to parse metadata")?; + let image_uri = metadata + .get("image") + .with_context(|| format!("Image URL not found in metadata for token_id: {}", token_id))? + .as_str() + .with_context(|| format!("Image field not a string for token_id: {}", token_id))? + .to_string(); + + let image_type = match image_uri { + uri if uri.starts_with("http") || uri.starts_with("https") => { + debug!(image_uri = %uri, "Fetching image from http/https URL"); + // Fetch image from HTTP/HTTPS URL + let client = Client::new(); + let response = client + .get(uri) + .send() + .await + .context("Failed to fetch image from URL")? + .bytes() + .await + .context("Failed to read image bytes from response")?; + + // svg files typically start with <svg or <?xml + if response.starts_with(b"<svg") || response.starts_with(b"<?xml") { + ErcImageType::Svg(response.to_vec()) + } else { + let format = image::guess_format(&response).with_context(|| { + format!("Unknown file format for token_id: {}, data: {:?}", token_id, &response) + })?; + ErcImageType::DynamicImage(( + image::load_from_memory(&response) + .context("Failed to load image from bytes")?, + format, + )) + } + } + uri if uri.starts_with("ipfs") => { + debug!(image_uri = %uri, "Fetching image from IPFS"); + let cid = uri.strip_prefix("ipfs://").unwrap(); + let response = fetch_content_from_ipfs(cid, MAX_RETRY) + .await + .context("Failed to read image bytes from IPFS response")?; + + if response.starts_with(b"<svg") || response.starts_with(b"<?xml") { + ErcImageType::Svg(response.to_vec()) + } else { + let format = image::guess_format(&response).with_context(|| { + format!( + "Unknown file format for token_id: {}, cid: {}, data: {:?}", + token_id, cid, &response + ) + })?; + ErcImageType::DynamicImage(( + image::load_from_memory(&response) + .context("Failed to load image from bytes")?, + format, + )) + } + } + uri if uri.starts_with("data") => { + debug!("Parsing image from data URI"); + trace!(data_uri = %uri); + // Parse and decode data URI + let data_url = DataUrl::process(&uri).context("Failed to parse data URI")?; + + // Check if it's an SVG + if data_url.mime_type() == &Mime::from_str("image/svg+xml").unwrap() { + let decoded = data_url.decode_to_vec().context("Failed to decode data URI")?; + ErcImageType::Svg(decoded.0) + } else { + let decoded = data_url.decode_to_vec().context("Failed to decode data URI")?; + let format = image::guess_format(&decoded.0) + .with_context(|| format!("Unknown file format for token_id: {}", token_id))?; + ErcImageType::DynamicImage(( + image::load_from_memory(&decoded.0) + .context("Failed to load image from bytes")?, + format, + )) + } + } + uri => { + return Err(anyhow::anyhow!("Unsupported URI scheme: {}", uri)); + } + }; + + // Extract contract_address and token_id from token_id + let parts: Vec<&str> = token_id.split(':').collect(); + if parts.len() != 2 { + return Err(anyhow::anyhow!("token_id must be in format contract_address:token_id")); + } + let contract_address = parts[0]; + let token_id_part = parts[1]; + + let dir_path = artifacts_path.join(contract_address).join(token_id_part); + + // Create directories if they don't exist + fs::create_dir_all(&dir_path) + .await + .context("Failed to create directories for image storage")?; + + // Define base image name + let base_image_name = "image"; + + let relative_path = Utf8PathBuf::new().join(contract_address).join(token_id_part); + + match image_type { + ErcImageType::DynamicImage((img, format)) => { + let format_ext = format.extensions_str()[0]; + + let target_sizes = [("medium", 250, 250), ("small", 100, 100)]; + + // Save original image + let original_file_name = format!("{}.{}", base_image_name, format_ext); + let original_file_path = dir_path.join(&original_file_name); + let mut file = fs::File::create(&original_file_path) + .await + .with_context(|| format!("Failed to create file: {:?}", original_file_path))?; + let encoded_image = encode_image_to_vec(&img, format) + .with_context(|| format!("Failed to encode image: {:?}", original_file_path))?; + file.write_all(&encoded_image).await.with_context(|| { + format!("Failed to write image to file: {:?}", original_file_path) + })?; + + // Save resized images + for (label, max_width, max_height) in &target_sizes { + let resized_image = resize_image_to_fit(&img, *max_width, *max_height); + let file_name = format!("@{}.{}", label, format_ext); + let file_path = dir_path.join(format!("{}{}", base_image_name, file_name)); + let mut file = fs::File::create(&file_path) + .await + .with_context(|| format!("Failed to create file: {:?}", file_path))?; + let encoded_image = encode_image_to_vec(&resized_image, format) + .context("Failed to encode image")?; + file.write_all(&encoded_image) + .await + .with_context(|| format!("Failed to write image to file: {:?}", file_path))?; + } + + Ok(format!("{}/{}", relative_path, base_image_name)) + } + ErcImageType::Svg(svg_data) => { + let file_name = format!("{}.svg", base_image_name); + let file_path = dir_path.join(&file_name); + + // Save the SVG file + let mut file = File::create(&file_path) + .await + .with_context(|| format!("Failed to create file: {:?}", file_path))?; + file.write_all(&svg_data) + .await + .with_context(|| format!("Failed to write SVG to file: {:?}", file_path))?; + + Ok(format!("{}/{}", relative_path, file_name)) + } + } +} + +fn resize_image_to_fit(image: &DynamicImage, max_width: u32, max_height: u32) -> DynamicImage { + image.resize_to_fill(max_width, max_height, image::imageops::FilterType::Lanczos3) +} + +fn encode_image_to_vec(image: &DynamicImage, format: ImageFormat) -> Result<Vec<u8>> { + let mut buf = Vec::new(); + image.write_to(&mut Cursor::new(&mut buf), format).with_context(|| "Failed to encode image")?; + Ok(buf) +} + +#[derive(Debug)] +pub enum ErcImageType { + DynamicImage((DynamicImage, ImageFormat)), + Svg(Vec<u8>), +} diff --git a/crates/torii/server/src/lib.rs b/crates/torii/server/src/lib.rs index 44dcc92d61..621f66d155 100644 --- a/crates/torii/server/src/lib.rs +++ b/crates/torii/server/src/lib.rs @@ -1 +1,2 @@ +pub mod artifacts; pub mod proxy; diff --git a/crates/torii/server/src/proxy.rs b/crates/torii/server/src/proxy.rs index 4c759e8b1a..a43fa71c28 100644 --- a/crates/torii/server/src/proxy.rs +++ b/crates/torii/server/src/proxy.rs @@ -58,6 +58,7 @@ pub struct Proxy { addr: SocketAddr, allowed_origins: Option<Vec<String>>, grpc_addr: Option<SocketAddr>, + artifacts_addr: Option<SocketAddr>, graphql_addr: Arc<RwLock<Option<SocketAddr>>>, } @@ -67,8 +68,15 @@ impl Proxy { allowed_origins: Option<Vec<String>>, grpc_addr: Option<SocketAddr>, graphql_addr: Option<SocketAddr>, + artifacts_addr: Option<SocketAddr>, ) -> Self { - Self { addr, allowed_origins, grpc_addr, graphql_addr: Arc::new(RwLock::new(graphql_addr)) } + Self { + addr, + allowed_origins, + grpc_addr, + graphql_addr: Arc::new(RwLock::new(graphql_addr)), + artifacts_addr, + } } pub async fn set_graphql_addr(&self, addr: SocketAddr) { @@ -84,6 +92,7 @@ impl Proxy { let allowed_origins = self.allowed_origins.clone(); let grpc_addr = self.grpc_addr; let graphql_addr = self.graphql_addr.clone(); + let artifacts_addr = self.artifacts_addr; let make_svc = make_service_fn(move |conn: &AddrStream| { let remote_addr = conn.remote_addr().ip(); @@ -125,7 +134,7 @@ impl Proxy { let graphql_addr = graphql_addr_clone.clone(); async move { let graphql_addr = graphql_addr.read().await; - handle(remote_addr, grpc_addr, *graphql_addr, req).await + handle(remote_addr, grpc_addr, artifacts_addr, *graphql_addr, req).await } }); @@ -145,9 +154,32 @@ impl Proxy { async fn handle( client_ip: IpAddr, grpc_addr: Option<SocketAddr>, + artifacts_addr: Option<SocketAddr>, graphql_addr: Option<SocketAddr>, req: Request<Body>, ) -> Result<Response<Body>, Infallible> { + if req.uri().path().starts_with("/static") { + if let Some(artifacts_addr) = artifacts_addr { + let artifacts_addr = format!("http://{}", artifacts_addr); + + return match GRAPHQL_PROXY_CLIENT.call(client_ip, &artifacts_addr, req).await { + Ok(response) => Ok(response), + Err(_error) => { + error!("{:?}", _error); + Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .unwrap()) + } + }; + } else { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::empty()) + .unwrap()); + } + } + if req.uri().path().starts_with("/graphql") { if let Some(graphql_addr) = graphql_addr { let graphql_addr = format!("http://{}", graphql_addr); From 2cbb18b6706b6548f6f54476521d68a8e6e3d960 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Wed, 23 Oct 2024 16:55:41 +0530 Subject: [PATCH 3/9] feat(torii/graphql): cleanup names and add imagePath and metadata to erc_token commit-id:21a1c0a5 --- bin/torii/src/main.rs | 5 +- crates/torii/core/src/constants.rs | 3 + crates/torii/core/src/executor/mod.rs | 7 +- crates/torii/core/src/lib.rs | 1 + crates/torii/core/src/sql/cache.rs | 16 ++--- crates/torii/core/src/sql/erc.rs | 8 ++- crates/torii/core/src/utils.rs | 1 - crates/torii/graphql/src/constants.rs | 16 ++--- crates/torii/graphql/src/mapping.rs | 30 +++++--- .../torii/graphql/src/object/erc/erc_token.rs | 29 ++++++-- crates/torii/graphql/src/object/erc/mod.rs | 4 +- .../erc/{erc_balance.rs => token_balance.rs} | 65 ++++++++++++----- .../{erc_transfer.rs => token_transfer.rs} | 70 ++++++++++++++----- crates/torii/graphql/src/schema.rs | 7 +- ..._rename_tokens_and_erc_balances_tables.sql | 3 + crates/torii/server/src/artifacts.rs | 17 +++-- 16 files changed, 193 insertions(+), 89 deletions(-) create mode 100644 crates/torii/core/src/constants.rs rename crates/torii/graphql/src/object/erc/{erc_balance.rs => token_balance.rs} (78%) rename crates/torii/graphql/src/object/erc/{erc_transfer.rs => token_transfer.rs} (79%) create mode 100644 crates/torii/migrations/20241018073644_rename_tokens_and_erc_balances_tables.sql diff --git a/bin/torii/src/main.rs b/bin/torii/src/main.rs index 1d5b2f1322..c605d58079 100644 --- a/bin/torii/src/main.rs +++ b/bin/torii/src/main.rs @@ -232,9 +232,7 @@ async fn main() -> anyhow::Result<()> { args.max_concurrent_tasks, ) .await?; - tokio::spawn(async move { - executor.run().await.unwrap(); - }); + let executor_handle = tokio::spawn(async move { executor.run().await }); let model_cache = Arc::new(ModelCache::new(pool.clone())); let db = Sql::new(pool.clone(), sender.clone(), &args.indexing.contracts, model_cache.clone()) @@ -350,6 +348,7 @@ async fn main() -> anyhow::Result<()> { tokio::select! { res = engine_handle => res??, + res = executor_handle => res??, res = proxy_server_handle => res??, res = graphql_server_handle => res?, res = grpc_server_handle => res??, diff --git a/crates/torii/core/src/constants.rs b/crates/torii/core/src/constants.rs new file mode 100644 index 0000000000..8248b09f7d --- /dev/null +++ b/crates/torii/core/src/constants.rs @@ -0,0 +1,3 @@ +pub const TOKEN_BALANCE_TABLE: &str = "token_balances"; +pub const TOKEN_TRANSFER_TABLE: &str = "token_transfers"; +pub const TOKENS_TABLE: &str = "tokens"; diff --git a/crates/torii/core/src/executor/mod.rs b/crates/torii/core/src/executor/mod.rs index b2c3bf7093..c823aa1255 100644 --- a/crates/torii/core/src/executor/mod.rs +++ b/crates/torii/core/src/executor/mod.rs @@ -18,6 +18,7 @@ use tokio::task::JoinSet; use tokio::time::Instant; use tracing::{debug, error}; +use crate::constants::TOKENS_TABLE; use crate::simple_broker::SimpleBroker; use crate::sql::utils::{felt_to_sql_string, I256}; use crate::types::{ @@ -603,9 +604,9 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { QueryType::RegisterErc721Token(register_erc721_token) => { let semaphore = self.semaphore.clone(); let provider = self.provider.clone(); - let res = sqlx::query_as::<_, (String, String)>( - "SELECT name, symbol FROM tokens WHERE contract_address = ?", - ) + let res = sqlx::query_as::<_, (String, String)>(&format!( + "SELECT name, symbol FROM {TOKENS_TABLE} WHERE contract_address = ?" + )) .bind(felt_to_sql_string(®ister_erc721_token.contract_address)) .fetch_one(&mut **tx) .await; diff --git a/crates/torii/core/src/lib.rs b/crates/torii/core/src/lib.rs index 0615f98b4e..fbf9a1e14b 100644 --- a/crates/torii/core/src/lib.rs +++ b/crates/torii/core/src/lib.rs @@ -1,5 +1,6 @@ #![warn(unused_crate_dependencies)] +pub mod constants; pub mod engine; pub mod error; pub mod executor; diff --git a/crates/torii/core/src/sql/cache.rs b/crates/torii/core/src/sql/cache.rs index 23da95bd34..76ea4a0574 100644 --- a/crates/torii/core/src/sql/cache.rs +++ b/crates/torii/core/src/sql/cache.rs @@ -6,6 +6,7 @@ use sqlx::{Pool, Sqlite, SqlitePool}; use starknet_crypto::Felt; use tokio::sync::RwLock; +use crate::constants::TOKEN_BALANCE_TABLE; use crate::error::{Error, ParseError, QueryError}; use crate::model::{parse_sql_model_members, SqlModelMember}; use crate::sql::utils::I256; @@ -133,27 +134,24 @@ pub struct LocalCache { impl Clone for LocalCache { fn clone(&self) -> Self { - Self { erc_cache: HashMap::new(), token_id_registry: HashSet::new() } + Self { erc_cache: HashMap::new(), token_id_registry: self.token_id_registry.clone() } } } impl LocalCache { pub async fn new(pool: Pool<Sqlite>) -> Self { // read existing token_id's from balances table and cache them - let token_id_registry: Vec<(String,)> = sqlx::query_as("SELECT token_id FROM balances") - .fetch_all(&pool) - .await - .expect("Should be able to read token_id's from blances table"); + let token_id_registry: Vec<(String,)> = + sqlx::query_as(&format!("SELECT token_id FROM {TOKEN_BALANCE_TABLE}")) + .fetch_all(&pool) + .await + .expect("Should be able to read token_id's from blances table"); let token_id_registry = token_id_registry.into_iter().map(|token_id| token_id.0).collect(); Self { erc_cache: HashMap::new(), token_id_registry } } - pub fn empty() -> Self { - Self { erc_cache: HashMap::new(), token_id_registry: HashSet::new() } - } - pub fn contains_token_id(&self, token_id: &str) -> bool { self.token_id_registry.contains(token_id) } diff --git a/crates/torii/core/src/sql/erc.rs b/crates/torii/core/src/sql/erc.rs index 0a7acc643d..cef58f281a 100644 --- a/crates/torii/core/src/sql/erc.rs +++ b/crates/torii/core/src/sql/erc.rs @@ -9,6 +9,7 @@ use starknet::providers::Provider; use super::utils::{u256_to_sql_string, I256}; use super::{Sql, FELT_DELIMITER}; +use crate::constants::TOKEN_TRANSFER_TABLE; use crate::executor::{ ApplyBalanceDiffQuery, Argument, QueryMessage, QueryType, RegisterErc20TokenQuery, RegisterErc721TokenQuery, @@ -249,9 +250,10 @@ impl Sql { block_timestamp: u64, event_id: &str, ) -> Result<()> { - let insert_query = "INSERT INTO erc_transfers (id, contract_address, from_address, \ - to_address, amount, token_id, executed_at) VALUES (?, ?, ?, ?, ?, ?, \ - ?)"; + let insert_query = format!( + "INSERT INTO {TOKEN_TRANSFER_TABLE} (id, contract_address, from_address, to_address, \ + amount, token_id, executed_at) VALUES (?, ?, ?, ?, ?, ?, ?)" + ); self.executor.send(QueryMessage::new( insert_query.to_string(), diff --git a/crates/torii/core/src/utils.rs b/crates/torii/core/src/utils.rs index d66d294509..2556caa8f0 100644 --- a/crates/torii/core/src/utils.rs +++ b/crates/torii/core/src/utils.rs @@ -7,7 +7,6 @@ use ipfs_api_backend_hyper::{IpfsApi, IpfsClient, TryFromUri}; use tokio_util::bytes::Bytes; use tracing::info; -// pub const IPFS_URL: &str = "https://cartridge.infura-ipfs.io/ipfs/"; pub const IPFS_URL: &str = "https://ipfs.io/ipfs/"; pub const MAX_RETRY: u8 = 3; diff --git a/crates/torii/graphql/src/constants.rs b/crates/torii/graphql/src/constants.rs index fc5a376f56..1e89a0bea5 100644 --- a/crates/torii/graphql/src/constants.rs +++ b/crates/torii/graphql/src/constants.rs @@ -9,8 +9,6 @@ pub const EVENT_MESSAGE_TABLE: &str = "event_messages"; pub const MODEL_TABLE: &str = "models"; pub const TRANSACTION_TABLE: &str = "transactions"; pub const METADATA_TABLE: &str = "metadata"; -pub const ERC_BALANCE_TABLE: &str = "balances"; -pub const ERC_TRANSFER_TABLE: &str = "erc_transfers"; pub const ID_COLUMN: &str = "id"; pub const EVENT_ID_COLUMN: &str = "event_id"; @@ -35,9 +33,10 @@ pub const QUERY_TYPE_NAME: &str = "World__Query"; pub const SUBSCRIPTION_TYPE_NAME: &str = "World__Subscription"; pub const MODEL_ORDER_TYPE_NAME: &str = "World__ModelOrder"; pub const MODEL_ORDER_FIELD_TYPE_NAME: &str = "World__ModelOrderField"; -pub const ERC_BALANCE_TYPE_NAME: &str = "ERC__Balance"; -pub const ERC_TRANSFER_TYPE_NAME: &str = "ERC__Transfer"; -pub const ERC_TOKEN_TYPE_NAME: &str = "ERC__Token"; +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"; // objects' single and plural names pub const ENTITY_NAMES: (&str, &str) = ("entity", "entities"); @@ -49,10 +48,11 @@ 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 TOKEN_BALANCE_NAME: (&str, &str) = ("", "tokenBalances"); +pub const TOKEN_TRANSFER_NAME: (&str, &str) = ("", "tokenTransfers"); -pub const ERC_BALANCE_NAME: (&str, &str) = ("ercBalance", ""); -pub const ERC_TOKEN_NAME: (&str, &str) = ("ercToken", ""); -pub const ERC_TRANSFER_NAME: (&str, &str) = ("ercTransfer", ""); +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 089d2a6b51..adb8b37bc1 100644 --- a/crates/torii/graphql/src/mapping.rs +++ b/crates/torii/graphql/src/mapping.rs @@ -4,7 +4,9 @@ use async_graphql::Name; use dojo_types::primitive::Primitive; use lazy_static::lazy_static; -use crate::constants::{CONTENT_TYPE_NAME, ERC_TOKEN_TYPE_NAME, SOCIAL_TYPE_NAME}; +use crate::constants::{ + CONTENT_TYPE_NAME, ERC721_METADATA_TYPE_NAME, SOCIAL_TYPE_NAME, TOKEN_TYPE_NAME, +}; use crate::types::{GraphqlType, TypeData, TypeMapping}; lazy_static! { @@ -145,27 +147,39 @@ lazy_static! { ), ]); - pub static ref ERC_BALANCE_TYPE_MAPPING: TypeMapping = IndexMap::from([ + 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(ERC_TOKEN_TYPE_NAME))), + (Name::new("tokenMetadata"), TypeData::Simple(TypeRef::named_nn(TOKEN_TYPE_NAME))), ]); - pub static ref ERC_TRANSFER_TYPE_MAPPING: TypeMapping = IndexMap::from([ + 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(ERC_TOKEN_TYPE_NAME))), + (Name::new("tokenMetadata"), TypeData::Simple(TypeRef::named_nn(TOKEN_TYPE_NAME))), (Name::new("transactionHash"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), ]); - pub static ref ERC_TOKEN_TYPE_MAPPING: TypeMapping = IndexMap::from([ + pub static ref 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("tokenId"), 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())) + ), + ]); + + pub static ref ERC721_METADATA_TYPE_MAPPING: TypeMapping = IndexMap::from([ + (Name::new("tokenId"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), + (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("metadata"), 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 14b8de7877..d1a312b932 100644 --- a/crates/torii/graphql/src/object/erc/erc_token.rs +++ b/crates/torii/graphql/src/object/erc/erc_token.rs @@ -1,5 +1,7 @@ -use crate::constants::{ERC_TOKEN_NAME, ERC_TOKEN_TYPE_NAME}; -use crate::mapping::ERC_TOKEN_TYPE_MAPPING; +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 crate::object::BasicObject; use crate::types::TypeMapping; @@ -8,14 +10,31 @@ pub struct ErcTokenObject; impl BasicObject for ErcTokenObject { fn name(&self) -> (&str, &str) { - ERC_TOKEN_NAME + TOKEN_NAME } fn type_name(&self) -> &str { - ERC_TOKEN_TYPE_NAME + TOKEN_TYPE_NAME } fn type_mapping(&self) -> &TypeMapping { - &ERC_TOKEN_TYPE_MAPPING + &TOKEN_TYPE_MAPPING + } +} + +#[derive(Debug)] +pub struct Erc721MetadataObject; + +impl BasicObject for Erc721MetadataObject { + fn name(&self) -> (&str, &str) { + ERC721_METADATA_NAME + } + + fn type_name(&self) -> &str { + ERC721_METADATA_TYPE_NAME + } + + fn type_mapping(&self) -> &TypeMapping { + &ERC721_METADATA_TYPE_MAPPING } } diff --git a/crates/torii/graphql/src/object/erc/mod.rs b/crates/torii/graphql/src/object/erc/mod.rs index 3e85722cca..72d0eb1fab 100644 --- a/crates/torii/graphql/src/object/erc/mod.rs +++ b/crates/torii/graphql/src/object/erc/mod.rs @@ -1,9 +1,9 @@ use super::connection::cursor; use crate::query::order::CursorDirection; -pub mod erc_balance; pub mod erc_token; -pub mod erc_transfer; +pub mod token_balance; +pub mod token_transfer; fn handle_cursor( cursor: &str, diff --git a/crates/torii/graphql/src/object/erc/erc_balance.rs b/crates/torii/graphql/src/object/erc/token_balance.rs similarity index 78% rename from crates/torii/graphql/src/object/erc/erc_balance.rs rename to crates/torii/graphql/src/object/erc/token_balance.rs index 848303e71a..44818d9eee 100644 --- a/crates/torii/graphql/src/object/erc/erc_balance.rs +++ b/crates/torii/graphql/src/object/erc/token_balance.rs @@ -6,14 +6,13 @@ use serde::Deserialize; use sqlx::sqlite::SqliteRow; use sqlx::{FromRow, Pool, Row, Sqlite, SqliteConnection}; use starknet_crypto::Felt; +use torii_core::constants::TOKEN_BALANCE_TABLE; use torii_core::sql::utils::felt_to_sql_string; use tracing::warn; use super::handle_cursor; -use crate::constants::{ - DEFAULT_LIMIT, ERC_BALANCE_NAME, ERC_BALANCE_TABLE, ERC_BALANCE_TYPE_NAME, ID_COLUMN, -}; -use crate::mapping::ERC_BALANCE_TYPE_MAPPING; +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, @@ -30,15 +29,15 @@ pub struct ErcBalanceObject; impl BasicObject for ErcBalanceObject { fn name(&self) -> (&str, &str) { - ERC_BALANCE_NAME + TOKEN_BALANCE_NAME } fn type_name(&self) -> &str { - ERC_BALANCE_TYPE_NAME + TOKEN_BALANCE_TYPE_NAME } fn type_mapping(&self) -> &TypeMapping { - &ERC_BALANCE_TYPE_MAPPING + &TOKEN_BALANCE_TYPE_MAPPING } } @@ -51,7 +50,7 @@ impl ResolvableObject for ErcBalanceObject { ); let mut field = Field::new( - self.name().0, + self.name().1, TypeRef::named(format!("{}Connection", self.type_name())), move |ctx| { FieldFuture::new(async move { @@ -69,12 +68,12 @@ impl ResolvableObject for ErcBalanceObject { }]; let total_count = - count_rows(&mut conn, ERC_BALANCE_TABLE, &None, &Some(filter)).await?; + count_rows(&mut conn, TOKEN_BALANCE_TABLE, &None, &Some(filter)).await?; let (data, page_info) = - fetch_erc_balances(&mut conn, address, &connection, total_count).await?; + fetch_token_balances(&mut conn, address, &connection, total_count).await?; - let results = erc_balance_connection_output(&data, total_count, page_info)?; + let results = token_balances_connection_output(&data, total_count, page_info)?; Ok(Some(Value::Object(results))) }) @@ -87,18 +86,18 @@ impl ResolvableObject for ErcBalanceObject { } } -async fn fetch_erc_balances( +async fn fetch_token_balances( conn: &mut SqliteConnection, address: Felt, connection: &ConnectionArguments, total_count: i64, ) -> sqlx::Result<(Vec<SqliteRow>, PageInfo)> { - let table_name = ERC_BALANCE_TABLE; + let table_name = TOKEN_BALANCE_TABLE; let id_column = format!("b.{}", ID_COLUMN); let mut query = format!( "SELECT b.id, t.contract_address, t.name, t.symbol, t.decimals, b.balance, b.token_id, \ - c.contract_type + t.metadata, c.contract_type FROM {table_name} b JOIN tokens t ON b.token_id = t.id JOIN contracts c ON t.contract_address = c.contract_address" @@ -207,7 +206,7 @@ async fn fetch_erc_balances( } } -fn erc_balance_connection_output( +fn token_balances_connection_output( data: &[SqliteRow], total_count: i64, page_info: PageInfo, @@ -222,10 +221,9 @@ fn erc_balance_connection_output( 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([ @@ -239,12 +237,42 @@ fn erc_balance_connection_output( let token_id = row.token_id.split(':').collect::<Vec<&str>>(); assert!(token_id.len() == 2); + let metadata: serde_json::Value = + serde_json::from_str(&row.metadata).expect("metadata is always json"); + let erc721_name = + metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string()); + let erc721_description = metadata + .get("description") + .map(|v| v.to_string().trim_matches('"').to_string()); + let erc721_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("tokenId"), Value::String(token_id[1].to_string())), (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([ @@ -291,4 +319,5 @@ struct BalanceQueryResultRaw { pub token_id: String, pub balance: String, pub contract_type: String, + pub metadata: String, } diff --git a/crates/torii/graphql/src/object/erc/erc_transfer.rs b/crates/torii/graphql/src/object/erc/token_transfer.rs similarity index 79% rename from crates/torii/graphql/src/object/erc/erc_transfer.rs rename to crates/torii/graphql/src/object/erc/token_transfer.rs index e1c950db39..2dc8d8bd13 100644 --- a/crates/torii/graphql/src/object/erc/erc_transfer.rs +++ b/crates/torii/graphql/src/object/erc/token_transfer.rs @@ -6,15 +6,14 @@ use serde::Deserialize; use sqlx::sqlite::SqliteRow; use sqlx::{FromRow, Pool, Row, Sqlite, SqliteConnection}; use starknet_crypto::Felt; +use torii_core::constants::TOKEN_TRANSFER_TABLE; 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 crate::constants::{ - DEFAULT_LIMIT, ERC_TRANSFER_NAME, ERC_TRANSFER_TABLE, ERC_TRANSFER_TYPE_NAME, ID_COLUMN, -}; -use crate::mapping::ERC_TRANSFER_TYPE_MAPPING; +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, @@ -29,15 +28,15 @@ pub struct ErcTransferObject; impl BasicObject for ErcTransferObject { fn name(&self) -> (&str, &str) { - ERC_TRANSFER_NAME + TOKEN_TRANSFER_NAME } fn type_name(&self) -> &str { - ERC_TRANSFER_TYPE_NAME + TOKEN_TRANSFER_TYPE_NAME } fn type_mapping(&self) -> &TypeMapping { - &ERC_TRANSFER_TYPE_MAPPING + &TOKEN_TRANSFER_TYPE_MAPPING } } @@ -50,7 +49,7 @@ impl ResolvableObject for ErcTransferObject { ); let mut field = Field::new( - self.name().0, + self.name().1, TypeRef::named(format!("{}Connection", self.type_name())), move |ctx| { FieldFuture::new(async move { @@ -61,10 +60,10 @@ impl ResolvableObject for ErcTransferObject { &account_address.to_case(Case::Camel), )?; - let total_count: (i64,) = sqlx::query_as( - "SELECT COUNT(*) FROM erc_transfers WHERE from_address = ? OR to_address \ - = ?", - ) + let total_count: (i64,) = sqlx::query_as(&format!( + "SELECT COUNT(*) FROM {TOKEN_TRANSFER_TABLE} WHERE from_address = ? OR \ + to_address = ?" + )) .bind(felt_to_sql_string(&address)) .bind(felt_to_sql_string(&address)) .fetch_one(&mut *conn) @@ -72,8 +71,8 @@ impl ResolvableObject for ErcTransferObject { let total_count = total_count.0; let (data, page_info) = - fetch_erc_transfers(&mut conn, address, &connection, total_count).await?; - let results = erc_transfer_connection_output(&data, total_count, page_info)?; + 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))) }) @@ -86,13 +85,13 @@ impl ResolvableObject for ErcTransferObject { } } -async fn fetch_erc_transfers( +async fn fetch_token_transfers( conn: &mut SqliteConnection, address: Felt, connection: &ConnectionArguments, total_count: i64, ) -> sqlx::Result<(Vec<SqliteRow>, PageInfo)> { - let table_name = ERC_TRANSFER_TABLE; + let table_name = TOKEN_TRANSFER_TABLE; let id_column = format!("et.{}", ID_COLUMN); let mut query = format!( @@ -108,7 +107,8 @@ SELECT t.name, t.symbol, t.decimals, - c.contract_type + c.contract_type, + t.metadata FROM {table_name} et JOIN @@ -227,7 +227,7 @@ JOIN } } -fn erc_transfer_connection_output( +fn token_transfers_connection_output( data: &[SqliteRow], total_count: i64, page_info: PageInfo, @@ -248,6 +248,7 @@ fn erc_transfer_connection_output( (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([ @@ -265,12 +266,42 @@ fn erc_transfer_connection_output( let token_id = row.token_id.split(':').collect::<Vec<&str>>(); 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 = + metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string()); + let erc721_description = metadata + .get("description") + .map(|v| v.to_string().trim_matches('"').to_string()); + let erc721_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("tokenId"), Value::String(token_id[1].to_string())), (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([ @@ -324,4 +355,5 @@ struct TransferQueryResultRaw { pub symbol: String, pub decimals: u8, pub contract_type: String, + pub metadata: String, } diff --git a/crates/torii/graphql/src/schema.rs b/crates/torii/graphql/src/schema.rs index 5f70c49908..7d9b90b883 100644 --- a/crates/torii/graphql/src/schema.rs +++ b/crates/torii/graphql/src/schema.rs @@ -10,9 +10,9 @@ 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_balance::ErcBalanceObject; -use crate::object::erc::erc_token::ErcTokenObject; -use crate::object::erc::erc_transfer::ErcTransferObject; +use crate::object::erc::erc_token::{Erc721MetadataObject, ErcTokenObject}; +use crate::object::erc::token_balance::ErcBalanceObject; +use crate::object::erc::token_transfer::ErcTransferObject; use crate::object::event_message::EventMessageObject; use crate::object::metadata::content::ContentObject; use crate::object::metadata::social::SocialObject; @@ -122,6 +122,7 @@ async fn build_objects(pool: &SqlitePool) -> Result<(Vec<ObjectVariant>, Vec<Uni ObjectVariant::Basic(Box::new(ContentObject)), ObjectVariant::Basic(Box::new(PageInfoObject)), ObjectVariant::Basic(Box::new(ErcTokenObject)), + ObjectVariant::Basic(Box::new(Erc721MetadataObject)), ]; // model union object diff --git a/crates/torii/migrations/20241018073644_rename_tokens_and_erc_balances_tables.sql b/crates/torii/migrations/20241018073644_rename_tokens_and_erc_balances_tables.sql new file mode 100644 index 0000000000..d3d95f80ac --- /dev/null +++ b/crates/torii/migrations/20241018073644_rename_tokens_and_erc_balances_tables.sql @@ -0,0 +1,3 @@ +-- Add migration script here +ALTER TABLE balances RENAME TO token_balances; +ALTER TABLE erc_transfers RENAME TO token_transfers; diff --git a/crates/torii/server/src/artifacts.rs b/crates/torii/server/src/artifacts.rs index 260d1e48d0..f98a3e2108 100644 --- a/crates/torii/server/src/artifacts.rs +++ b/crates/torii/server/src/artifacts.rs @@ -15,6 +15,7 @@ use tokio::fs; use tokio::fs::File; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::sync::broadcast::Receiver; +use torii_core::constants::TOKENS_TABLE; use torii_core::utils::{fetch_content_from_ipfs, MAX_RETRY}; use tracing::{debug, error, trace}; use warp::http::Response; @@ -153,13 +154,15 @@ async fn fetch_and_process_image( token_id: &str, pool: Pool<Sqlite>, ) -> anyhow::Result<String> { - let query = sqlx::query_as::<_, (String,)>("SELECT metadata FROM tokens WHERE id = ?") - .bind(token_id) - .fetch_one(&pool) - .await - .with_context(|| { - format!("Failed to fetch metadata from database for token_id: {}", token_id) - })?; + let query = sqlx::query_as::<_, (String,)>(&format!( + "SELECT metadata FROM {TOKENS_TABLE} WHERE id = ?" + )) + .bind(token_id) + .fetch_one(&pool) + .await + .with_context(|| { + format!("Failed to fetch metadata from database for token_id: {}", token_id) + })?; let metadata: serde_json::Value = serde_json::from_str(&query.0).context("Failed to parse metadata")?; From 17fd08424c319d896fcb360b5b0ae701873ce456 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Wed, 30 Oct 2024 23:51:28 +0530 Subject: [PATCH 4/9] fix(torii/core): handle an edge case with pending block processing commit-id:6f679e25 --- crates/torii/core/src/engine.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/torii/core/src/engine.rs b/crates/torii/core/src/engine.rs index edbbea147f..36d7d788fb 100644 --- a/crates/torii/core/src/engine.rs +++ b/crates/torii/core/src/engine.rs @@ -10,9 +10,9 @@ use dojo_world::contracts::world::WorldContractReader; use futures_util::future::{join_all, try_join_all}; use hashlink::LinkedHashMap; use starknet::core::types::{ - BlockId, BlockTag, EmittedEvent, Event, EventFilter, EventsPage, MaybePendingBlockWithReceipts, - MaybePendingBlockWithTxHashes, PendingBlockWithReceipts, Transaction, TransactionReceipt, - TransactionWithReceipt, + BlockHashAndNumber, BlockId, BlockTag, EmittedEvent, Event, EventFilter, EventsPage, + MaybePendingBlockWithReceipts, MaybePendingBlockWithTxHashes, PendingBlockWithReceipts, + Transaction, TransactionReceipt, TransactionWithReceipt, }; use starknet::core::utils::get_selector_from_name; use starknet::providers::Provider; @@ -309,23 +309,23 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> { // TODO: since we now process blocks in chunks we can parallelize the fetching of data pub async fn fetch_data(&mut self, cursors: &Cursors) -> Result<FetchDataResult> { - let latest_block_number = self.provider.block_hash_and_number().await?.block_number; + let latest_block = self.provider.block_hash_and_number().await?; let from = cursors.head.unwrap_or(0); - let total_remaining_blocks = latest_block_number - from; + let total_remaining_blocks = latest_block.block_number - from; let blocks_to_process = total_remaining_blocks.min(self.config.blocks_chunk_size); let to = from + blocks_to_process; let instant = Instant::now(); - let result = if from < latest_block_number { + let result = if from < latest_block.block_number { let from = if from == 0 { from } else { from + 1 }; let data = self.fetch_range(from, to, &cursors.cursor_map).await?; debug!(target: LOG_TARGET, duration = ?instant.elapsed(), from = %from, to = %to, "Fetched data for range."); FetchDataResult::Range(data) } else if self.config.index_pending { let data = - self.fetch_pending(latest_block_number + 1, cursors.last_pending_block_tx).await?; - debug!(target: LOG_TARGET, duration = ?instant.elapsed(), latest_block_number = %latest_block_number, "Fetched pending data."); + self.fetch_pending(latest_block.clone(), cursors.last_pending_block_tx).await?; + debug!(target: LOG_TARGET, duration = ?instant.elapsed(), latest_block_number = %latest_block.block_number, "Fetched pending data."); if let Some(data) = data { FetchDataResult::Pending(data) } else { @@ -453,12 +453,18 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> { async fn fetch_pending( &self, - block_number: u64, + block: BlockHashAndNumber, last_pending_block_tx: Option<Felt>, ) -> Result<Option<FetchPendingResult>> { - let block = if let MaybePendingBlockWithReceipts::PendingBlock(pending) = + let pending_block = if let MaybePendingBlockWithReceipts::PendingBlock(pending) = self.provider.get_block_with_receipts(BlockId::Tag(BlockTag::Pending)).await? { + // if the parent hash is not the hash of the latest block that we fetched, then it means + // a new block got mined just after we fetched the latest block information + if block.block_hash != pending.parent_hash { + return Ok(None); + } + pending } else { // TODO: change this to unreachable once katana is updated to return PendingBlockWithTxs @@ -468,8 +474,8 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> { }; Ok(Some(FetchPendingResult { - pending_block: Box::new(block), - block_number, + pending_block: Box::new(pending_block), + block_number: block.block_number + 1, last_pending_block_tx, })) } 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 5/9] 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<String>, + pub metadata_description: Option<String>, + pub metadata_attributes: Option<String>, + 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<T> { + pub node: T, + pub cursor: String, +} + +#[derive(Debug, Clone)] +pub struct Connection<T> { + pub total_count: i64, + pub edges: Vec<ConnectionEdge<T>>, + 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<ValueMapping> { +) -> sqlx::Result<FieldValue<'a>> { 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<ValueMapping> { +) -> sqlx::Result<FieldValue<'a>> { 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::<Vec<&str>>(); 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::<Connection<ErcTokenType>>() + { + match field_name.as_str() { + "edges" => { + return Ok(Some(FieldValue::list( + values + .edges + .iter() + .map(|v| FieldValue::owned_any(v.clone())) + .collect::<Vec<FieldValue<'_>>>(), + ))); + } + "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::<ConnectionEdge<ErcTokenType>>() + { + 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::<Connection<TokenTransferNode>>() + { + match field_name.as_str() { + "edges" => { + return Ok(Some(FieldValue::list( + values + .edges + .iter() + .map(|v| FieldValue::owned_any(v.clone())) + .collect::<Vec<FieldValue<'_>>>(), + ))); + } + "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::<ConnectionEdge<TokenTransferNode>>() + { + 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::<TokenTransferNode>() { + 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::<ErcTokenType>() { + 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<ObjectVariant>, Vec<Uni ObjectVariant::Basic(Box::new(SocialObject)), ObjectVariant::Basic(Box::new(ContentObject)), ObjectVariant::Basic(Box::new(PageInfoObject)), - ObjectVariant::Basic(Box::new(ErcTokenObject)), - ObjectVariant::Basic(Box::new(Erc721MetadataObject)), + ObjectVariant::Basic(Box::new(Erc721TokenObject)), + ObjectVariant::Basic(Box::new(Erc20TokenObject)), ]; // 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); + + unions.push(erc_token_union); + // model data objects for model in models { let type_mapping = type_mapping_query(&mut conn, &model.id).await?; From ae4eabb8ee93b6b57024bbd04915b2e2b939cfb3 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Mon, 4 Nov 2024 02:30:41 +0530 Subject: [PATCH 6/9] fix(torii/core): properly update transaction hash while processing pending block commit-id:fd50508d --- crates/torii/core/src/engine.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/torii/core/src/engine.rs b/crates/torii/core/src/engine.rs index 36d7d788fb..6ea55ce344 100644 --- a/crates/torii/core/src/engine.rs +++ b/crates/torii/core/src/engine.rs @@ -737,6 +737,7 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> { for contract in unique_contracts { let entry = cursor_map.entry(contract).or_insert((*transaction_hash, 0)); + entry.0 = *transaction_hash; entry.1 += 1; } From d4daef35a5013f2daffd120577aeef6d7e38e6cb Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Mon, 4 Nov 2024 02:37:24 +0530 Subject: [PATCH 7/9] fix(torii/core): rollback transaction when engine retries commit-id:16f58fa9 --- crates/torii/core/src/engine.rs | 2 ++ crates/torii/core/src/executor/mod.rs | 39 +++++++++++++++++++++++++++ crates/torii/core/src/sql/mod.rs | 6 +++++ 3 files changed, 47 insertions(+) diff --git a/crates/torii/core/src/engine.rs b/crates/torii/core/src/engine.rs index 6ea55ce344..0d91e95ed3 100644 --- a/crates/torii/core/src/engine.rs +++ b/crates/torii/core/src/engine.rs @@ -284,6 +284,8 @@ impl<P: Provider + Send + Sync + std::fmt::Debug + 'static> Engine<P> { Err(e) => { error!(target: LOG_TARGET, error = %e, "Processing fetched data."); erroring_out = true; + // incase of error rollback the transaction + self.db.rollback().await?; sleep(backoff_delay).await; if backoff_delay < max_backoff_delay { backoff_delay *= 2; diff --git a/crates/torii/core/src/executor/mod.rs b/crates/torii/core/src/executor/mod.rs index c823aa1255..18c76f6d64 100644 --- a/crates/torii/core/src/executor/mod.rs +++ b/crates/torii/core/src/executor/mod.rs @@ -116,6 +116,8 @@ pub enum QueryType { // similar to execute but doesn't create a new transaction Flush, Execute, + // rollback's the current transaction and starts a new one + Rollback, Other, } @@ -208,6 +210,19 @@ impl QueryMessage { rx, ) } + + pub fn rollback_recv() -> (Self, oneshot::Receiver<Result<()>>) { + let (tx, rx) = oneshot::channel(); + ( + Self { + statement: "".to_string(), + arguments: vec![], + query_type: QueryType::Rollback, + tx: Some(tx), + }, + rx, + ) + } } impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { @@ -733,6 +748,20 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { // defer executing these queries since they depend on TokenRegister queries self.deferred_query_messages.push(query_message); } + QueryType::Rollback => { + debug!(target: LOG_TARGET, "Rolling back the transaction."); + // rollback's the current transaction and starts a new one + let res = self.rollback().await; + debug!(target: LOG_TARGET, "Rolled back the transaction."); + + if let Some(sender) = query_message.tx { + sender + .send(res) + .map_err(|_| anyhow::anyhow!("Failed to send rollback result"))?; + } else { + res?; + } + } QueryType::Other => { query.execute(&mut **tx).await.with_context(|| { format!( @@ -785,6 +814,16 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { Ok(()) } + + async fn rollback(&mut self) -> Result<()> { + let transaction = mem::replace(&mut self.transaction, self.pool.begin().await?); + transaction.rollback().await?; + + // NOTE: clear doesn't reset the capacity + self.publish_queue.clear(); + self.deferred_query_messages.clear(); + Ok(()) + } } fn send_broker_message(message: BrokerMessage) { diff --git a/crates/torii/core/src/sql/mod.rs b/crates/torii/core/src/sql/mod.rs index 11eedf6a3c..9c61405d99 100644 --- a/crates/torii/core/src/sql/mod.rs +++ b/crates/torii/core/src/sql/mod.rs @@ -1311,4 +1311,10 @@ impl Sql { self.executor.send(flush)?; recv.await? } + + pub async fn rollback(&self) -> Result<()> { + let (rollback, recv) = QueryMessage::rollback_recv(); + self.executor.send(rollback)?; + recv.await? + } } From 8a7a9a23806dd1c0a2347e6d1a8b5e2c83549c62 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Thu, 14 Nov 2024 06:10:33 +0700 Subject: [PATCH 8/9] fix: git rebase commit-id:ddf37cb7 --- Cargo.lock | 28 ++++ bin/torii/src/main.rs | 128 ++---------------- crates/torii/cli/Cargo.toml | 5 +- crates/torii/cli/src/args.rs | 5 + .../graphql/src/tests/subscription_test.rs | 1 - 5 files changed, 48 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84744fddd2..e4f92c298e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10099,6 +10099,34 @@ 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/bin/torii/src/main.rs b/bin/torii/src/main.rs index c605d58079..ff15a88644 100644 --- a/bin/torii/src/main.rs +++ b/bin/torii/src/main.rs @@ -16,10 +16,8 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use anyhow::Context; use camino::Utf8PathBuf; use clap::Parser; -use clap::{ArgAction, Parser}; use dojo_metrics::exporters::prometheus::PrometheusRecorder; use dojo_world::contracts::world::WorldContractReader; use sqlx::sqlite::{ @@ -48,111 +46,6 @@ use url::{form_urlencoded, Url}; pub(crate) const LOG_TARGET: &str = "torii::cli"; -/// Dojo World Indexer -#[derive(Parser, Debug)] -#[command(name = "torii", author, version, about, long_about = None)] -struct Args { - /// The world to index - #[arg(short, long = "world", env = "DOJO_WORLD_ADDRESS")] - world_address: Option<Felt>, - - /// The sequencer rpc endpoint to index. - #[arg(long, value_name = "URL", default_value = ":5050", value_parser = parse_url)] - rpc: Url, - - /// Database filepath (ex: indexer.db). If specified file doesn't exist, it will be - /// created. Defaults to in-memory database - #[arg(short, long, default_value = "")] - database: String, - - /// Address to serve api endpoints at. - #[arg(long, value_name = "SOCKET", default_value = "0.0.0.0:8080", value_parser = parse_socket_address)] - addr: SocketAddr, - - /// Port to serve Libp2p TCP & UDP Quic transports - #[arg(long, value_name = "PORT", default_value = "9090")] - relay_port: u16, - - /// Port to serve Libp2p WebRTC transport - #[arg(long, value_name = "PORT", default_value = "9091")] - relay_webrtc_port: u16, - - /// Port to serve Libp2p WebRTC transport - #[arg(long, value_name = "PORT", default_value = "9092")] - relay_websocket_port: u16, - - /// Path to a local identity key file. If not specified, a new identity will be generated - #[arg(long, value_name = "PATH")] - relay_local_key_path: Option<String>, - - /// Path to a local certificate file. If not specified, a new certificate will be generated - /// for WebRTC connections - #[arg(long, value_name = "PATH")] - relay_cert_path: Option<String>, - - /// Specify allowed origins for api endpoints (comma-separated list of allowed origins, or "*" - /// for all) - #[arg(long)] - #[arg(value_delimiter = ',')] - allowed_origins: Option<Vec<String>>, - - /// The external url of the server, used for configuring the GraphQL Playground in a hosted - /// environment - #[arg(long, value_parser = parse_url)] - external_url: Option<Url>, - - /// Enable Prometheus metrics. - /// - /// The metrics will be served at the given interface and port. - #[arg(long, value_name = "SOCKET", value_parser = parse_socket_address, help_heading = "Metrics")] - metrics: Option<SocketAddr>, - - /// Open World Explorer on the browser. - #[arg(long)] - explorer: bool, - - /// Chunk size of the events page when indexing using events - #[arg(long, default_value = "1024")] - events_chunk_size: u64, - - /// Number of blocks to process before commiting to DB - #[arg(long, default_value = "10240")] - blocks_chunk_size: u64, - - /// Enable indexing pending blocks - #[arg(long, action = ArgAction::Set, default_value_t = true)] - index_pending: bool, - - /// Polling interval in ms - #[arg(long, default_value = "500")] - polling_interval: u64, - - /// Max concurrent tasks - #[arg(long, default_value = "100")] - max_concurrent_tasks: usize, - - /// Whether or not to index world transactions - #[arg(long, action = ArgAction::Set, default_value_t = false)] - index_transactions: bool, - - /// Whether or not to index raw events - #[arg(long, action = ArgAction::Set, default_value_t = true)] - index_raw_events: bool, - - /// ERC contract addresses to index - #[arg(long, value_parser = parse_erc_contracts)] - #[arg(conflicts_with = "config")] - contracts: Option<std::vec::Vec<Contract>>, - - /// Configuration file - #[arg(long)] - config: Option<PathBuf>, - - /// Path to a directory to store ERC artifacts - #[arg(long)] - artifacts_path: Option<Utf8PathBuf>, -} - #[tokio::main] async fn main() -> anyhow::Result<()> { let mut args = ToriiArgs::parse().with_config_file()?; @@ -217,19 +110,11 @@ async fn main() -> anyhow::Result<()> { // Get world address let world = WorldContractReader::new(world_address, provider.clone()); - // let (mut executor, sender) = Executor::new(pool.clone(), shutdown_tx.clone()).await?; - let contracts = args - .indexing - .contracts - .iter() - .map(|contract| (contract.address, contract.r#type)) - .collect(); - let (mut executor, sender) = Executor::new( pool.clone(), shutdown_tx.clone(), provider.clone(), - args.max_concurrent_tasks, + args.indexing.max_concurrent_tasks, ) .await?; let executor_handle = tokio::spawn(async move { executor.run().await }); @@ -287,6 +172,16 @@ async fn main() -> anyhow::Result<()> { ) .await?; + let temp_dir = TempDir::new()?; + let artifacts_path = + args.artifacts_path.unwrap_or_else(|| Utf8PathBuf::from(temp_dir.path().to_str().unwrap())); + + tokio::fs::create_dir_all(&artifacts_path).await?; + let absolute_path = artifacts_path.canonicalize_utf8()?; + + let (artifacts_addr, artifacts_server) = + torii_server::artifacts::new(shutdown_tx.subscribe(), &absolute_path, pool.clone()).await?; + let mut libp2p_relay_server = torii_relay::server::Relay::new( db, provider.clone(), @@ -299,6 +194,7 @@ async fn main() -> anyhow::Result<()> { .expect("Failed to start libp2p relay server"); let addr = SocketAddr::new(args.server.http_addr, args.server.http_port); + let proxy_server = Arc::new(Proxy::new( addr, args.server.http_cors_origins.filter(|cors_origins| !cors_origins.is_empty()), diff --git a/crates/torii/cli/Cargo.toml b/crates/torii/cli/Cargo.toml index 0f8b708b33..e4ed7ab09e 100644 --- a/crates/torii/cli/Cargo.toml +++ b/crates/torii/cli/Cargo.toml @@ -7,12 +7,13 @@ version.workspace = true [dependencies] anyhow.workspace = true +camino.workspace = true clap.workspace = true dojo-utils.workspace = true serde.workspace = true starknet.workspace = true -torii-core.workspace = true toml.workspace = true +torii-core.workspace = true url.workspace = true [dev-dependencies] @@ -21,4 +22,4 @@ camino.workspace = true [features] default = [ "server" ] -server = [ ] +server = [ ] diff --git a/crates/torii/cli/src/args.rs b/crates/torii/cli/src/args.rs index 64dfb58b0c..8749ab5e2d 100644 --- a/crates/torii/cli/src/args.rs +++ b/crates/torii/cli/src/args.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use anyhow::Result; +use camino::Utf8PathBuf; use clap::Parser; use dojo_utils::parse::parse_url; use serde::{Deserialize, Serialize}; @@ -47,6 +48,10 @@ pub struct ToriiArgs { #[arg(long, help = "Configuration file to setup Torii.")] pub config: Option<PathBuf>, + /// Path to a directory to store ERC artifacts + #[arg(long)] + pub artifacts_path: Option<Utf8PathBuf>, + #[command(flatten)] pub indexing: IndexingOptions, diff --git a/crates/torii/graphql/src/tests/subscription_test.rs b/crates/torii/graphql/src/tests/subscription_test.rs index 39146feddf..bc054f059c 100644 --- a/crates/torii/graphql/src/tests/subscription_test.rs +++ b/crates/torii/graphql/src/tests/subscription_test.rs @@ -20,7 +20,6 @@ mod tests { use torii_core::sql::cache::ModelCache; use torii_core::sql::utils::felts_to_sql_string; use torii_core::sql::Sql; - use torii_core::types::ContractType; use torii_core::types::{Contract, ContractType}; use url::Url; From 705de8837113338088c320cf5872fa242c7adb66 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Thu, 14 Nov 2024 15:40:55 +0530 Subject: [PATCH 9/9] fix(torii/graphql): use borrowed_any instead of owned_any commit-id:eaaead2c --- crates/torii/graphql/src/object/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/torii/graphql/src/object/mod.rs b/crates/torii/graphql/src/object/mod.rs index 36c1f9b754..b4201e0989 100644 --- a/crates/torii/graphql/src/object/mod.rs +++ b/crates/torii/graphql/src/object/mod.rs @@ -85,7 +85,7 @@ pub trait BasicObject: Send + Sync { values .edges .iter() - .map(|v| FieldValue::owned_any(v.clone())) + .map(FieldValue::borrowed_any) .collect::<Vec<FieldValue<'_>>>(), ))); } @@ -105,7 +105,7 @@ pub trait BasicObject: Send + Sync { ctx.parent_value.try_downcast_ref::<ConnectionEdge<ErcTokenType>>() { match field_name.as_str() { - "node" => return Ok(Some(FieldValue::owned_any(values.node.clone()))), + "node" => return Ok(Some(FieldValue::borrowed_any(&values.node))), "cursor" => { return Ok(Some(FieldValue::value(Value::String( values.cursor.clone(), @@ -124,7 +124,7 @@ pub trait BasicObject: Send + Sync { values .edges .iter() - .map(|v| FieldValue::owned_any(v.clone())) + .map(FieldValue::borrowed_any) .collect::<Vec<FieldValue<'_>>>(), ))); } @@ -144,7 +144,7 @@ pub trait BasicObject: Send + Sync { ctx.parent_value.try_downcast_ref::<ConnectionEdge<TokenTransferNode>>() { match field_name.as_str() { - "node" => return Ok(Some(FieldValue::owned_any(values.node.clone()))), + "node" => return Ok(Some(FieldValue::borrowed_any(&values.node))), "cursor" => { return Ok(Some(FieldValue::value(Value::String( values.cursor.clone(),