Skip to content

Commit

Permalink
feat(torii): implement graphql for erc
Browse files Browse the repository at this point in the history
commit-id:10465a00
  • Loading branch information
lambda-0x committed Sep 24, 2024
1 parent cbedcce commit 9db7b9b
Show file tree
Hide file tree
Showing 16 changed files with 415 additions and 10 deletions.
2 changes: 2 additions & 0 deletions crates/torii/core/src/sql/erc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ impl Sql {
.fetch_one(&self.pool)
.await?;

// TODO(opt): we dont need to make rpc call for each token id for erc721, metadata for all
// tokens is same is same for a specific contract
if !token_exists {
register_erc721_token_metadata(
contract_address,
Expand Down
8 changes: 4 additions & 4 deletions crates/torii/core/src/sql/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ pub fn felts_to_sql_string(felts: &[Felt]) -> String {
+ FELT_DELIMITER
}

pub(crate) fn felt_to_sql_string(felt: &Felt) -> String {
pub fn felt_to_sql_string(felt: &Felt) -> String {
format!("{:#x}", felt)
}

pub(crate) fn felt_and_u256_to_sql_string(felt: &Felt, u256: &U256) -> String {
pub fn felt_and_u256_to_sql_string(felt: &Felt, u256: &U256) -> String {
format!("{}:{}", felt_to_sql_string(felt), u256_to_sql_string(u256))
}

pub(crate) fn u256_to_sql_string(u256: &U256) -> String {
pub fn u256_to_sql_string(u256: &U256) -> String {
format!("{:#064x}", u256)
}

pub(crate) fn sql_string_to_u256(sql_string: &str) -> U256 {
pub fn sql_string_to_u256(sql_string: &str) -> U256 {
let sql_string = sql_string.strip_prefix("0x").unwrap_or(sql_string);
U256::from(crypto_bigint::U256::from_be_hex(sql_string))
}
2 changes: 1 addition & 1 deletion crates/torii/graphql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ serde.workspace = true
serde_json.workspace = true
sozo-ops.workspace = true
sqlx.workspace = true
starknet-crypto.workspace = true
strum.workspace = true
strum_macros.workspace = true
thiserror.workspace = true
Expand All @@ -46,5 +47,4 @@ dojo-world.workspace = true
katana-runner.workspace = true
scarb.workspace = true
serial_test = "2.0.0"
starknet-crypto.workspace = true
starknet.workspace = true
7 changes: 7 additions & 0 deletions crates/torii/graphql/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ 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";

// objects' single and plural names
pub const ENTITY_NAMES: (&str, &str) = ("entity", "entities");
Expand All @@ -45,6 +48,10 @@ 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 ERC_BALANCE_NAME: (&str, &str) = ("ercBalance", "");
pub const ERC_TOKEN_NAME: (&str, &str) = ("ercToken", "");
pub const ERC_TRANSFER_NAME: (&str, &str) = ("ercTransfer", "");

// misc
pub const ORDER_DIR_TYPE_NAME: &str = "OrderDirection";
pub const ORDER_ASC: &str = "ASC";
Expand Down
2 changes: 2 additions & 0 deletions crates/torii/graphql/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pub enum ExtractError {
NotList(String),
#[error("Not a string: {0}")]
NotString(String),
#[error("Not a felt: {0}")]
NotFelt(String),
#[error("Not a number: {0}")]
NotNumber(String),
}
25 changes: 24 additions & 1 deletion crates/torii/graphql/src/mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use async_graphql::Name;
use dojo_types::primitive::Primitive;
use lazy_static::lazy_static;

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

lazy_static! {
Expand Down Expand Up @@ -144,4 +144,27 @@ lazy_static! {
TypeData::Simple(TypeRef::named(GraphqlType::DateTime.to_string()))
),
]);

pub static ref ERC_BALANCE_TYPE_MAPPING: TypeMapping = IndexMap::from([
(Name::new("balance"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
(Name::new("type"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
(Name::new("token_metadata"), TypeData::Simple(TypeRef::named(ERC_TOKEN_TYPE_NAME))),
]);

pub static ref ERC_TRANSFER_TYPE_MAPPING: TypeMapping = IndexMap::from([
(Name::new("from"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
(Name::new("to"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
(Name::new("amount"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
(Name::new("type"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
(Name::new("executed_at"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
(Name::new("token_metadata"), TypeData::Simple(TypeRef::named(ERC_TOKEN_TYPE_NAME))),
]);

pub static ref ERC_TOKEN_TYPE_MAPPING: TypeMapping = IndexMap::from([
(Name::new("name"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
(Name::new("symbol"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
(Name::new("token_id"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
(Name::new("decimals"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
(Name::new("contract_address"), TypeData::Simple(TypeRef::named(TypeRef::STRING))),
]);
}
143 changes: 143 additions & 0 deletions crates/torii/graphql/src/object/erc/erc_balance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
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 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 crate::mapping::ERC_BALANCE_TYPE_MAPPING;
use crate::object::{BasicObject, ResolvableObject};
use crate::types::{TypeMapping, ValueMapping};
use crate::utils::extract;

#[derive(Debug)]
pub struct ErcBalanceObject;

impl BasicObject for ErcBalanceObject {
fn name(&self) -> (&str, &str) {
ERC_BALANCE_NAME
}

fn type_name(&self) -> &str {
ERC_BALANCE_TYPE_NAME
}

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

impl ResolvableObject for ErcBalanceObject {
fn resolvers(&self) -> Vec<Field> {
let account_address = "account_address";
let argument = InputValue::new(
account_address.to_case(Case::Camel),
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 erc_balances = fetch_erc_balances(&mut conn, address).await?;

Ok(Some(Value::List(erc_balances)))
})
})
.argument(argument);
vec![field]
}
}

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
JOIN tokens t ON b.token_id = t.id
JOIN contracts c ON t.contract_address = c.contract_address
WHERE b.account_address = ?";

let rows = sqlx::query(query).bind(felt_to_sql_string(&address)).fetch_all(conn).await?;

let mut erc_balances = Vec::new();

for row in rows {
let row = BalanceQueryResultRaw::from_row(&row)?;

let balance_value = match row.contract_type.as_str() {
"ERC20" | "Erc20" | "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("token_id"), Value::Null),
(Name::new("decimals"), Value::String(row.decimals.to_string())),
(Name::new("contract_address"), Value::String(row.contract_address.clone())),
]));

Value::Object(ValueMapping::from([
(Name::new("balance"), Value::String(row.balance)),
(Name::new("type"), Value::String(row.contract_type)),
(Name::new("token_metadata"), token_metadata),
]))
}
"ERC721" | "Erc721" | "erc721" => {
// contract_address:token_id
let token_id = row.token_id.split(':').collect::<Vec<&str>>();
assert!(token_id.len() == 2);

let token_metadata = Value::Object(ValueMapping::from([
(Name::new("contract_address"), Value::String(row.contract_address.clone())),
(Name::new("name"), Value::String(row.name)),
(Name::new("symbol"), Value::String(row.symbol)),
(Name::new("token_id"), Value::String(row.token_id)),
(Name::new("decimals"), Value::String(row.decimals.to_string())),
]));

Value::Object(ValueMapping::from([
(Name::new("balance"), Value::String(row.balance)),
(Name::new("type"), Value::String(row.contract_type)),
(Name::new("token_metadata"), token_metadata),
]))
}
_ => {
warn!("Unknown contract type: {}", row.contract_type);
continue;
}
};

erc_balances.push(balance_value);
}

Ok(erc_balances)
}

// TODO: This would be required when subscriptions are needed
// impl ErcBalanceObject {
// pub fn value_mapping(entity: ErcBalance) -> ValueMapping {
// IndexMap::from([
// ])
// }
// }

#[derive(FromRow, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct BalanceQueryResultRaw {
pub contract_address: String,
pub name: String,
pub symbol: String,
pub decimals: u8,
pub token_id: String,
pub balance: String,
pub contract_type: String,
}
21 changes: 21 additions & 0 deletions crates/torii/graphql/src/object/erc/erc_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use crate::constants::{ERC_TOKEN_NAME, ERC_TOKEN_TYPE_NAME};
use crate::mapping::ERC_TOKEN_TYPE_MAPPING;
use crate::object::BasicObject;
use crate::types::TypeMapping;

#[derive(Debug)]
pub struct ErcTokenObject;

impl BasicObject for ErcTokenObject {
fn name(&self) -> (&str, &str) {
ERC_TOKEN_NAME
}

fn type_name(&self) -> &str {
ERC_TOKEN_TYPE_NAME
}

fn type_mapping(&self) -> &TypeMapping {
&ERC_TOKEN_TYPE_MAPPING
}
}
Loading

0 comments on commit 9db7b9b

Please sign in to comment.