From 79f5f406dbabbd5b5692904c71bbd7fc3bf52f50 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 16 Oct 2024 04:58:35 +0000 Subject: [PATCH 01/28] init --- crates/cli/src/commands/order/detail.rs | 8 +- crates/cli/src/commands/order/list.rs | 8 +- crates/cli/src/commands/trade/detail.rs | 6 + crates/cli/src/commands/trade/list.rs | 6 + crates/cli/src/commands/vault/detail.rs | 4 +- crates/cli/src/commands/vault/list.rs | 5 +- .../commands/vault/list_balance_changes.rs | 3 + crates/quote/src/cli/mod.rs | 8 +- crates/quote/src/quote.rs | 10 + crates/subgraph/schema/orderbook.graphql | 36 +++ crates/subgraph/src/apy.rs | 232 ++++++++++++++++++ crates/subgraph/src/lib.rs | 1 + crates/subgraph/src/orderbook_client.rs | 49 +++- crates/subgraph/src/types/common.rs | 9 + crates/subgraph/src/types/vault.rs | 21 ++ crates/subgraph/src/vol.rs | 44 +++- ...er_test__batch_order_query_gql_output.snap | 28 +++ .../order_test__orders_query_gql_output.snap | 29 +++ ...r_trade_test__vaults_query_gql_output.snap | 6 + ..._trades_test__vaults_query_gql_output.snap | 6 + .../orders_test__orders_query_gql_output.snap | 28 +++ ...balance_changes_list_query_gql_output.snap | 3 + .../vault_test__vaults_query_gql_output.snap | 14 ++ .../vaults_test__vaults_query_gql_output.snap | 14 ++ subgraph/schema.graphql | 4 + subgraph/src/clear.ts | 10 +- subgraph/src/takeorder.ts | 6 +- subgraph/src/vault.ts | 26 ++ subgraph/tests/vault.test.ts | 117 ++++++++- .../src-tauri/src/commands/order_quote.rs | 4 + 30 files changed, 723 insertions(+), 22 deletions(-) create mode 100644 crates/subgraph/src/apy.rs diff --git a/crates/cli/src/commands/order/detail.rs b/crates/cli/src/commands/order/detail.rs index 77719c3df..2779d85ad 100644 --- a/crates/cli/src/commands/order/detail.rs +++ b/crates/cli/src/commands/order/detail.rs @@ -103,7 +103,9 @@ mod tests { "owner": encode_prefixed(order.owner), "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [] + "balanceChanges": [], + "totalVolumeIn": "1", + "totalVolumeOut": "1", }], "inputs": [{ "id": encode_prefixed(B256::random()), @@ -120,7 +122,9 @@ mod tests { "owner": encode_prefixed(order.owner), "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [] + "balanceChanges": [], + "totalVolumeIn": "1", + "totalVolumeOut": "1", }], "orderbook": { "id": encode_prefixed(B256::random()), diff --git a/crates/cli/src/commands/order/list.rs b/crates/cli/src/commands/order/list.rs index e5dbd4427..a2e66403e 100644 --- a/crates/cli/src/commands/order/list.rs +++ b/crates/cli/src/commands/order/list.rs @@ -231,7 +231,9 @@ mod tests { "owner": encode_prefixed(order.owner), "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [] + "balanceChanges": [], + "totalVolumeIn": "1", + "totalVolumeOut": "1", }], "inputs": [{ "id": encode_prefixed(B256::random()), @@ -248,7 +250,9 @@ mod tests { "owner": encode_prefixed(order.owner), "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [] + "balanceChanges": [], + "totalVolumeIn": "1", + "totalVolumeOut": "1", }], "orderbook": { "id": encode_prefixed(B256::random()), diff --git a/crates/cli/src/commands/trade/detail.rs b/crates/cli/src/commands/trade/detail.rs index 87a185f4e..b649e26a8 100644 --- a/crates/cli/src/commands/trade/detail.rs +++ b/crates/cli/src/commands/trade/detail.rs @@ -104,6 +104,9 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), + "totalVolumeIn": "1", + "totalVolumeOut": "1", + "balance": "1", "token": { "name": "T1", "symbol": "T1", @@ -132,6 +135,9 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), + "totalVolumeIn": "1", + "totalVolumeOut": "1", + "balance": "1", "token": { "name": "T2", "symbol": "T2", diff --git a/crates/cli/src/commands/trade/list.rs b/crates/cli/src/commands/trade/list.rs index 9e904ea4e..b6766b88b 100644 --- a/crates/cli/src/commands/trade/list.rs +++ b/crates/cli/src/commands/trade/list.rs @@ -200,6 +200,9 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), + "totalVolumeIn": "1", + "totalVolumeOut": "1", + "balance": "1", "token": { "name": "T1", "symbol": "T1", @@ -228,6 +231,9 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), + "totalVolumeIn": "1", + "totalVolumeOut": "1", + "balance": "1", "token": { "name": "T2", "symbol": "T2", diff --git a/crates/cli/src/commands/vault/detail.rs b/crates/cli/src/commands/vault/detail.rs index f364d2efd..b9450763d 100644 --- a/crates/cli/src/commands/vault/detail.rs +++ b/crates/cli/src/commands/vault/detail.rs @@ -104,7 +104,9 @@ mod tests { "orderbook": { "id": encode_prefixed(B256::random()), }, - "balanceChanges": [] + "balanceChanges": [], + "totalVolumeIn": "1", + "totalVolumeOut": "1", } } }) diff --git a/crates/cli/src/commands/vault/list.rs b/crates/cli/src/commands/vault/list.rs index af7d48b8f..0c420a499 100644 --- a/crates/cli/src/commands/vault/list.rs +++ b/crates/cli/src/commands/vault/list.rs @@ -210,7 +210,10 @@ mod tests { "orderbook": { "id": encode_prefixed(B256::random()), }, - "balanceChanges": [] + "balanceChanges": [], + "totalVolumeIn": "1", + "totalVolumeOut": "1", + "balance": "1", }] } }) diff --git a/crates/cli/src/commands/vault/list_balance_changes.rs b/crates/cli/src/commands/vault/list_balance_changes.rs index 95b25d6f7..ce344bad8 100644 --- a/crates/cli/src/commands/vault/list_balance_changes.rs +++ b/crates/cli/src/commands/vault/list_balance_changes.rs @@ -178,6 +178,9 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), + "totalVolumeIn": "1", + "totalVolumeOut": "1", + "balance": "1", "token": { "name": "T1", "symbol": "T1", diff --git a/crates/quote/src/cli/mod.rs b/crates/quote/src/cli/mod.rs index 71676932b..e93906150 100644 --- a/crates/quote/src/cli/mod.rs +++ b/crates/quote/src/cli/mod.rs @@ -341,7 +341,9 @@ mod tests { "orderbook": { "id": encode_prefixed(B256::random()) }, "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [] + "balanceChanges": [], + "totalVolumeIn": "1", + "totalVolumeOut": "1", }], "inputs": [{ "id": encode_prefixed(Address::random().0.0), @@ -358,7 +360,9 @@ mod tests { "orderbook": { "id": encode_prefixed(B256::random()) }, "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [] + "balanceChanges": [], + "totalVolumeIn": "1", + "totalVolumeOut": "1", }], "orderbook": { "id": encode_prefixed(B256::random()) }, "active": true, diff --git a/crates/quote/src/quote.rs b/crates/quote/src/quote.rs index 4a10272d1..d3c593175 100644 --- a/crates/quote/src/quote.rs +++ b/crates/quote/src/quote.rs @@ -321,6 +321,8 @@ mod tests { "orderHash": encode_prefixed(B256::random()), "active": true }], + "totalVolumeIn": "1", + "totalVolumeOut": "1", "balanceChanges": [{ "__typename": "Withdrawal", "id": encode_prefixed(B256::random()), @@ -330,6 +332,9 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), + "totalVolumeIn": "1", + "totalVolumeOut": "1", + "balance": "1", "token": { "id": encode_prefixed(order.validOutputs[0].token.0.0), "address": encode_prefixed(order.validOutputs[0].token.0.0), @@ -371,6 +376,8 @@ mod tests { "orderHash": encode_prefixed(B256::random()), "active": true }], + "totalVolumeIn": "1", + "totalVolumeOut": "1", "balanceChanges": [{ "__typename": "Withdrawal", "id": encode_prefixed(B256::random()), @@ -380,6 +387,9 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), + "totalVolumeIn": "1", + "totalVolumeOut": "1", + "balance": "1", "token": { "id": encode_prefixed(order.validOutputs[0].token.0.0), "address": encode_prefixed(order.validOutputs[0].token.0.0), diff --git a/crates/subgraph/schema/orderbook.graphql b/crates/subgraph/schema/orderbook.graphql index 655b4e437..19287e9e1 100644 --- a/crates/subgraph/schema/orderbook.graphql +++ b/crates/subgraph/schema/orderbook.graphql @@ -368,6 +368,8 @@ enum ClearBounty_orderBy { vault__owner vault__vaultId vault__balance + vault__totalVolumeIn + vault__totalVolumeOut amount oldVaultBalance newVaultBalance @@ -917,6 +919,8 @@ enum Deposit_orderBy { vault__owner vault__vaultId vault__balance + vault__totalVolumeIn + vault__totalVolumeOut amount oldVaultBalance newVaultBalance @@ -3032,6 +3036,8 @@ enum TradeVaultBalanceChange_orderBy { vault__owner vault__vaultId vault__balance + vault__totalVolumeIn + vault__totalVolumeOut amount oldVaultBalance newVaultBalance @@ -3326,6 +3332,14 @@ type Vault { orderDirection: OrderDirection where: VaultBalanceChange_filter ): [VaultBalanceChange!]! + """ + All time vault total volume in, includes only trades volumes + """ + totalVolumeIn: BigInt! + """ + All time vault total volume out, includes only trades volumes + """ + totalVolumeOut: BigInt! } interface VaultBalanceChange { @@ -3471,6 +3485,8 @@ enum VaultBalanceChange_orderBy { vault__owner vault__vaultId vault__balance + vault__totalVolumeIn + vault__totalVolumeOut amount oldVaultBalance newVaultBalance @@ -3564,6 +3580,22 @@ input Vault_filter { balance_in: [BigInt!] balance_not_in: [BigInt!] balanceChanges_: VaultBalanceChange_filter + totalVolumeIn: BigInt + totalVolumeIn_not: BigInt + totalVolumeIn_gt: BigInt + totalVolumeIn_lt: BigInt + totalVolumeIn_gte: BigInt + totalVolumeIn_lte: BigInt + totalVolumeIn_in: [BigInt!] + totalVolumeIn_not_in: [BigInt!] + totalVolumeOut: BigInt + totalVolumeOut_not: BigInt + totalVolumeOut_gt: BigInt + totalVolumeOut_lt: BigInt + totalVolumeOut_gte: BigInt + totalVolumeOut_lte: BigInt + totalVolumeOut_in: [BigInt!] + totalVolumeOut_not_in: [BigInt!] """ Filter for the block changed event. """ @@ -3588,6 +3620,8 @@ enum Vault_orderBy { ordersAsOutput balance balanceChanges + totalVolumeIn + totalVolumeOut } type Withdrawal implements Event & VaultBalanceChange { @@ -3763,6 +3797,8 @@ enum Withdrawal_orderBy { vault__owner vault__vaultId vault__balance + vault__totalVolumeIn + vault__totalVolumeOut amount oldVaultBalance newVaultBalance diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs new file mode 100644 index 000000000..ca16b3215 --- /dev/null +++ b/crates/subgraph/src/apy.rs @@ -0,0 +1,232 @@ +use crate::{ + types::common::Erc20, vol::VaultVolume, OrderbookSubgraphClient, OrderbookSubgraphClientError, +}; +use alloy::primitives::{I256, U256}; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use typeshare::typeshare; + +pub const YEAR: u64 = 60 * 60 * 24 * 365; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct VaultAPY { + pub id: String, + pub token: Erc20, + pub start_time: Option, + pub end_time: Option, + #[typeshare(typescript(type = "string"))] + pub net_vol: I256, + #[typeshare(typescript(type = "string"))] + pub capital: U256, + #[typeshare(typescript(type = "string"))] + pub apy: i64, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct APYTimeframe { + pub start_time: u64, + pub end_time: u64, +} + +/// Given a subgraph and an order id and optionally a timeframe, will fetch data +/// and calculates the APY for each of the order's vaults +pub async fn get_order_vaults_apy( + subgraph_url: Url, + order_id: &str, + timeframe: Option, +) -> Result, OrderbookSubgraphClientError> { + let client = OrderbookSubgraphClient::new(subgraph_url); + let vols = if let Some(timeframe) = timeframe { + client + .order_vaults_volume( + cynic::Id::new(order_id), + Some(timeframe.start_time), + Some(timeframe.end_time), + ) + .await? + } else { + let order = client.order_detail(cynic::Id::new(order_id)).await?; + let mut vols: Vec = vec![]; + for vault in &order.inputs { + if !vols + .iter() + .any(|v| v.id == vault.vault_id.0 && v.token.address.0 == vault.token.address.0) + { + let total_in = U256::from_str(&vault.total_volume_in.0)?; + let total_out = U256::from_str(&vault.total_volume_out.0)?; + vols.push(VaultVolume { + id: vault.vault_id.0.clone(), + token: vault.token.clone(), + total_in, + total_out, + total_vol: total_in.saturating_add(total_out), + net_vol: I256::from_raw(total_in).saturating_sub(I256::from_raw(total_out)), + all_time_vol_in: total_in, + all_time_vol_out: total_out, + }) + } + } + for vault in &order.outputs { + if !vols + .iter() + .any(|v| v.id == vault.vault_id.0 && v.token.address.0 == vault.token.address.0) + { + let total_in = U256::from_str(&vault.total_volume_in.0)?; + let total_out = U256::from_str(&vault.total_volume_out.0)?; + vols.push(VaultVolume { + id: vault.vault_id.0.clone(), + token: vault.token.clone(), + total_in, + total_out, + total_vol: total_in.saturating_add(total_out), + net_vol: I256::from_raw(total_in).saturating_sub(I256::from_raw(total_out)), + all_time_vol_in: total_in, + all_time_vol_out: total_out, + }) + } + } + vols + }; + + let mut vaults_apy: Vec = vec![]; + for vol in vols { + let vault_bal_change = client + .first_day_vault_balance_change( + cynic::Id::new(&vol.id), + timeframe.map(|v| v.start_time), + ) + .await?; + let capital = U256::from_str( + &vault_bal_change + .as_ref() + .map(|v| v.old_vault_balance.0.clone()) + .unwrap_or("0".to_string()), + )?; + let start = u64::from_str( + &timeframe + .map(|v: APYTimeframe| v.start_time.to_string()) + .unwrap_or( + vault_bal_change + .as_ref() + .map(|v| v.timestamp.0.clone()) + .unwrap_or("0".to_string()), + ), + )?; + let end = timeframe + .map(|v| v.end_time) + .unwrap_or(chrono::Utc::now().timestamp() as u64); + let apy = if capital.is_zero() || start == 0 { + 0_i64 + } else { + let change_ratio = i64::try_from( + vol.net_vol + .saturating_mul(I256::from_raw(U256::from(10000))) + .saturating_div(I256::from_raw(capital)), + )? / 10000; + let time_to_year_ratio = ((end - start) / YEAR) as i64; + (change_ratio * time_to_year_ratio) * 100 + }; + vaults_apy.push(VaultAPY { + id: vol.id.clone(), + token: vol.token.clone(), + start_time: timeframe.map(|v| v.start_time), + end_time: timeframe.map(|v| v.end_time), + net_vol: vol.net_vol, + apy, + capital, + }); + } + + Ok(vaults_apy) +} + +// #[cfg(test)] +// mod test { +// use super::*; +// use crate::types::common::{ +// BigInt, Bytes, Orderbook, TradeEvent, TradeStructPartialOrder, TradeVaultBalanceChange, +// Transaction, VaultBalanceChangeVault, +// }; +// use alloy::primitives::{Address, B256}; + +// // helper function that returns mocked sg response in json +// fn get_sg_response() -> Value { +// let io = IO::default(); +// let order = OrderV3 { +// validInputs: vec![io.clone()], +// validOutputs: vec![io.clone()], +// ..Default::default() +// }; +// json!({ +// "data": { +// "order": { +// "id": encode_prefixed(B256::random()), +// "owner": encode_prefixed(order.owner), +// "orderHash": encode_prefixed(B256::random()), +// "orderBytes": encode_prefixed(order.abi_encode()), +// "outputs": [{ +// "id": encode_prefixed(B256::random()), +// "balance": "0", +// "vaultId": io.vaultId.to_string(), +// "token": { +// "name": "T1", +// "symbol": "T1", +// "id": encode_prefixed(io.token), +// "address": encode_prefixed(io.token), +// "decimals": io.decimals.to_string(), +// }, +// "orderbook": { "id": encode_prefixed(B256::random()) }, +// "owner": encode_prefixed(order.owner), +// "ordersAsOutput": [], +// "ordersAsInput": [], +// "balanceChanges": [] +// "totalVolumeIn": "1", +// "totalVolumeOut": "1", +// }], +// "inputs": [{ +// "id": encode_prefixed(B256::random()), +// "balance": "0", +// "vaultId": io.vaultId.to_string(), +// "token": { +// "name": "T2", +// "symbol": "T2", +// "id": encode_prefixed(io.token), +// "address": encode_prefixed(io.token), +// "decimals": io.decimals.to_string(), +// }, +// "orderbook": { "id": encode_prefixed(B256::random()) }, +// "owner": encode_prefixed(order.owner), +// "ordersAsOutput": [], +// "ordersAsInput": [], +// "balanceChanges": [], +// "totalVolumeIn": "1", +// "totalVolumeOut": "1", +// }], +// "orderbook": { +// "id": encode_prefixed(B256::random()), +// }, +// "meta": null, +// "active": true, +// "timestampAdded": "0", +// "addEvents": [{ +// "transaction": { +// "id": encode_prefixed(B256::random()), +// "blockNumber": "0", +// "timestamp": "0", +// "from": encode_prefixed(alloy::primitives::Address::random()) +// } +// }], +// "trades": [] +// } +// } +// }) +// } + +// #[test] +// fn test_get_order_vaults_vol() {} +// } diff --git a/crates/subgraph/src/lib.rs b/crates/subgraph/src/lib.rs index 2ca4cc6f5..309a49362 100644 --- a/crates/subgraph/src/lib.rs +++ b/crates/subgraph/src/lib.rs @@ -1,3 +1,4 @@ +pub mod apy; mod cynic_client; mod orderbook_client; mod pagination; diff --git a/crates/subgraph/src/orderbook_client.rs b/crates/subgraph/src/orderbook_client.rs index 4663f8ce7..c3e7bee38 100644 --- a/crates/subgraph/src/orderbook_client.rs +++ b/crates/subgraph/src/orderbook_client.rs @@ -6,7 +6,7 @@ use crate::types::order::{ OrdersListQuery, }; use crate::types::order_trade::{OrderTradeDetailQuery, OrderTradesListQuery}; -use crate::types::vault::{VaultDetailQuery, VaultsListQuery}; +use crate::types::vault::{VaultBalanceChangesByTimeListQuery, VaultDetailQuery, VaultsListQuery}; use crate::vault_balance_changes_query::VaultBalanceChangesListPageQueryClient; use crate::vol::{get_vaults_vol, VaultVolume}; use cynic::Id; @@ -25,6 +25,10 @@ pub enum OrderbookSubgraphClientError { PaginationClientError(#[from] PaginationClientError), #[error(transparent)] ParseError(#[from] alloy::primitives::ruint::ParseError), + #[error(transparent)] + ParseNumError(#[from] std::num::ParseIntError), + #[error(transparent)] + BigintConversionError(#[from] alloy::primitives::BigIntConversionError), } pub struct OrderbookSubgraphClient { @@ -338,4 +342,47 @@ impl OrderbookSubgraphClient { } Ok(all_pages_merged) } + + /// Fetch end of first day vault balance change from a given string timestamp + pub async fn first_day_vault_balance_change( + &self, + id: cynic::Id, + start_timestamp: Option, + ) -> Result, OrderbookSubgraphClientError> { + let day = 60 * 60 * 24; + let first_vault_change_data = self + .query::( + PaginationWithTimestampQueryVariables { + id: Bytes(id.inner().to_string()), + first: Some(1), + skip: None, + timestamp_lte: None, + timestamp_gte: Some( + start_timestamp.map_or(BigInt("0".to_string()), |v| BigInt(v.to_string())), + ), + }, + ) + .await?; + + let first_vault_change_timestamp = first_vault_change_data + .vault_balance_changes + .first() + .ok_or(OrderbookSubgraphClientError::Empty)? + .timestamp + .0 + .parse::()?; + let data = self + .query::( + PaginationWithTimestampQueryVariables { + id: Bytes(id.inner().to_string()), + first: Some(1), + skip: None, + timestamp_lte: None, + timestamp_gte: Some(BigInt((first_vault_change_timestamp + day).to_string())), + }, + ) + .await?; + + Ok(data.vault_balance_changes.first().cloned()) + } } diff --git a/crates/subgraph/src/types/common.rs b/crates/subgraph/src/types/common.rs index bef02ad12..f88b66b03 100644 --- a/crates/subgraph/src/types/common.rs +++ b/crates/subgraph/src/types/common.rs @@ -151,6 +151,8 @@ pub struct Vault { #[arguments(orderBy: timestampAdded, orderDirection: desc)] pub orders_as_input: Vec, pub balance_changes: Vec, + pub total_volume_in: BigInt, + pub total_volume_out: BigInt, } #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] @@ -160,6 +162,9 @@ pub struct VaultBalanceChangeVault { pub id: Bytes, pub vault_id: BigInt, pub token: Erc20, + pub balance: BigInt, + pub total_volume_in: BigInt, + pub total_volume_out: BigInt, } #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] @@ -496,4 +501,8 @@ pub enum VaultOrderBy { Balance, #[cynic(rename = "balanceChanges")] BalanceChanges, + #[cynic(rename = "totalVolumeIn")] + TotalVolumeIn, + #[cynic(rename = "totalVolumeOut")] + TotalVolumeOut, } diff --git a/crates/subgraph/src/types/vault.rs b/crates/subgraph/src/types/vault.rs index 0d55f4daa..aebdc3406 100644 --- a/crates/subgraph/src/types/vault.rs +++ b/crates/subgraph/src/types/vault.rs @@ -26,3 +26,24 @@ pub struct VaultBalanceChangesListQuery { #[arguments(orderDirection: "desc", orderBy: "timestamp", where: { vault_: { id: $id } }, skip: $skip, first: $first)] pub vault_balance_changes: Vec, } + +#[derive(cynic::QueryFragment, Debug, Clone, Serialize)] +#[cynic( + graphql_type = "Query", + variables = "PaginationWithTimestampQueryVariables" +)] +#[typeshare] +pub struct VaultBalanceChangesByTimeListQuery { + #[arguments( + skip: $skip, + first: $first, + orderDirection: "asc", + orderBy: "timestamp", + where: { + vault_: { id: $id }, + timestamp_gte: $timestamp_gte, + timestamp_lte: $timestamp_lte + } + )] + pub vault_balance_changes: Vec, +} diff --git a/crates/subgraph/src/vol.rs b/crates/subgraph/src/vol.rs index a88798249..6c15d3304 100644 --- a/crates/subgraph/src/vol.rs +++ b/crates/subgraph/src/vol.rs @@ -8,16 +8,20 @@ use typeshare::typeshare; #[serde(rename_all = "camelCase")] #[typeshare] pub struct VaultVolume { - id: String, - token: Erc20, + pub id: String, + pub token: Erc20, #[typeshare(typescript(type = "string"))] - total_in: U256, + pub total_in: U256, #[typeshare(typescript(type = "string"))] - total_out: U256, + pub total_out: U256, #[typeshare(typescript(type = "string"))] - total_vol: U256, + pub total_vol: U256, #[typeshare(typescript(type = "string"))] - net_vol: I256, + pub net_vol: I256, + #[typeshare(typescript(type = "string"))] + pub all_time_vol_in: U256, + #[typeshare(typescript(type = "string"))] + pub all_time_vol_out: U256, } /// Get the vaults volume from array of trades @@ -62,6 +66,12 @@ pub fn get_vaults_vol(trades: &[Trade]) -> Result, ParseError> total_out, total_vol, net_vol, + all_time_vol_in: U256::from_str( + &trade.input_vault_balance_change.vault.total_volume_in.0, + )?, + all_time_vol_out: U256::from_str( + &trade.input_vault_balance_change.vault.total_volume_out.0, + )?, }) } if let Some(vault_vol) = vaults_vol.iter_mut().find(|v| { @@ -102,6 +112,12 @@ pub fn get_vaults_vol(trades: &[Trade]) -> Result, ParseError> total_out, total_vol, net_vol, + all_time_vol_in: U256::from_str( + &trade.output_vault_balance_change.vault.total_volume_in.0, + )?, + all_time_vol_out: U256::from_str( + &trade.output_vault_balance_change.vault.total_volume_out.0, + )?, }) } } @@ -166,6 +182,9 @@ mod test { id: bytes.clone(), token: token1.clone(), vault_id: BigInt(vault_id1.to_string()), + balance: BigInt("0".to_string()), + total_volume_in: BigInt("0".to_string()), + total_volume_out: BigInt("0".to_string()), }, timestamp: bigint.clone(), transaction: Transaction { @@ -186,6 +205,9 @@ mod test { id: bytes.clone(), token: token2.clone(), vault_id: BigInt(vault_id2.to_string()), + balance: BigInt("0".to_string()), + total_volume_in: BigInt("0".to_string()), + total_volume_out: BigInt("0".to_string()), }, timestamp: bigint.clone(), transaction: Transaction { @@ -224,6 +246,9 @@ mod test { id: bytes.clone(), token: token2.clone(), vault_id: BigInt(vault_id2.to_string()), + balance: BigInt("0".to_string()), + total_volume_in: BigInt("0".to_string()), + total_volume_out: BigInt("0".to_string()), }, timestamp: bigint.clone(), transaction: Transaction { @@ -244,6 +269,9 @@ mod test { id: bytes.clone(), token: token1.clone(), vault_id: BigInt(vault_id1.to_string()), + balance: BigInt("0".to_string()), + total_volume_in: BigInt("0".to_string()), + total_volume_out: BigInt("0".to_string()), }, timestamp: bigint.clone(), transaction: Transaction { @@ -265,6 +293,8 @@ mod test { total_out: U256::from(7), total_vol: U256::from(12), net_vol: I256::from_str("-2").unwrap(), + all_time_vol_in: U256::from(0), + all_time_vol_out: U256::from(0), }, VaultVolume { id: vault_id1.to_string(), @@ -273,6 +303,8 @@ mod test { total_out: U256::from(2), total_vol: U256::from(5), net_vol: I256::from_str("1").unwrap(), + all_time_vol_in: U256::from(0), + all_time_vol_out: U256::from(0), }, ]; diff --git a/crates/subgraph/tests/snapshots/batch_order_test__batch_order_query_gql_output.snap b/crates/subgraph/tests/snapshots/batch_order_test__batch_order_query_gql_output.snap index b02666f22..a919723a6 100644 --- a/crates/subgraph/tests/snapshots/batch_order_test__batch_order_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/batch_order_test__batch_order_query_gql_output.snap @@ -51,6 +51,9 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -79,6 +82,9 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -107,6 +113,9 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -135,6 +144,9 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -149,6 +161,8 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { sender } } + totalVolumeIn + totalVolumeOut } inputs { id @@ -193,6 +207,9 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -221,6 +238,9 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -249,6 +269,9 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -277,6 +300,9 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -291,6 +317,8 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { sender } } + totalVolumeIn + totalVolumeOut } orderbook { id diff --git a/crates/subgraph/tests/snapshots/order_test__orders_query_gql_output.snap b/crates/subgraph/tests/snapshots/order_test__orders_query_gql_output.snap index 754ed1164..d2a4cdf2f 100644 --- a/crates/subgraph/tests/snapshots/order_test__orders_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/order_test__orders_query_gql_output.snap @@ -1,5 +1,6 @@ --- source: crates/subgraph/tests/order_test.rs +assertion_line: 13 expression: request_body.query --- query OrderDetailQuery($id: ID!) { @@ -51,6 +52,9 @@ query OrderDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -79,6 +83,9 @@ query OrderDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -107,6 +114,9 @@ query OrderDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -135,6 +145,9 @@ query OrderDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -149,6 +162,8 @@ query OrderDetailQuery($id: ID!) { sender } } + totalVolumeIn + totalVolumeOut } inputs { id @@ -193,6 +208,9 @@ query OrderDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -221,6 +239,9 @@ query OrderDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -249,6 +270,9 @@ query OrderDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -277,6 +301,9 @@ query OrderDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -291,6 +318,8 @@ query OrderDetailQuery($id: ID!) { sender } } + totalVolumeIn + totalVolumeOut } orderbook { id diff --git a/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap index 9d0565381..4744b19b5 100644 --- a/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap @@ -30,6 +30,9 @@ query OrderTradeDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -62,6 +65,9 @@ query OrderTradeDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { diff --git a/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap index 6c8382206..480ca4a02 100644 --- a/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap @@ -30,6 +30,9 @@ query OrderTradesListQuery($first: Int, $id: Bytes!, $skip: Int, $timestampGte: symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -62,6 +65,9 @@ query OrderTradesListQuery($first: Int, $id: Bytes!, $skip: Int, $timestampGte: symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { diff --git a/crates/subgraph/tests/snapshots/orders_test__orders_query_gql_output.snap b/crates/subgraph/tests/snapshots/orders_test__orders_query_gql_output.snap index 7e6dd6dcb..f8b8ddeca 100644 --- a/crates/subgraph/tests/snapshots/orders_test__orders_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/orders_test__orders_query_gql_output.snap @@ -51,6 +51,9 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -79,6 +82,9 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -107,6 +113,9 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -135,6 +144,9 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -149,6 +161,8 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { sender } } + totalVolumeIn + totalVolumeOut } inputs { id @@ -193,6 +207,9 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -221,6 +238,9 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -249,6 +269,9 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -277,6 +300,9 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -291,6 +317,8 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { sender } } + totalVolumeIn + totalVolumeOut } orderbook { id diff --git a/crates/subgraph/tests/snapshots/vault_balance_changes_test__vault_balance_changes_list_query_gql_output.snap b/crates/subgraph/tests/snapshots/vault_balance_changes_test__vault_balance_changes_list_query_gql_output.snap index f8c373a85..3dab7fdd9 100644 --- a/crates/subgraph/tests/snapshots/vault_balance_changes_test__vault_balance_changes_list_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/vault_balance_changes_test__vault_balance_changes_list_query_gql_output.snap @@ -18,6 +18,9 @@ query VaultBalanceChangesListQuery($first: Int, $id: Bytes!, $skip: Int) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { diff --git a/crates/subgraph/tests/snapshots/vault_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/vault_test__vaults_query_gql_output.snap index e3beb593c..b61d24ea6 100644 --- a/crates/subgraph/tests/snapshots/vault_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/vault_test__vaults_query_gql_output.snap @@ -46,6 +46,9 @@ query VaultDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -74,6 +77,9 @@ query VaultDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -102,6 +108,9 @@ query VaultDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -130,6 +139,9 @@ query VaultDetailQuery($id: ID!) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -144,5 +156,7 @@ query VaultDetailQuery($id: ID!) { sender } } + totalVolumeIn + totalVolumeOut } } diff --git a/crates/subgraph/tests/snapshots/vaults_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/vaults_test__vaults_query_gql_output.snap index c0cd662ca..bc7d92dfd 100644 --- a/crates/subgraph/tests/snapshots/vaults_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/vaults_test__vaults_query_gql_output.snap @@ -46,6 +46,9 @@ query VaultsListQuery($first: Int, $skip: Int, $filters: Vault_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -74,6 +77,9 @@ query VaultsListQuery($first: Int, $skip: Int, $filters: Vault_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -102,6 +108,9 @@ query VaultsListQuery($first: Int, $skip: Int, $filters: Vault_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -130,6 +139,9 @@ query VaultsListQuery($first: Int, $skip: Int, $filters: Vault_filter) { symbol decimals } + balance + totalVolumeIn + totalVolumeOut } timestamp transaction { @@ -144,5 +156,7 @@ query VaultsListQuery($first: Int, $skip: Int, $filters: Vault_filter) { sender } } + totalVolumeIn + totalVolumeOut } } diff --git a/subgraph/schema.graphql b/subgraph/schema.graphql index c5d52bd6d..7c8502ee1 100644 --- a/subgraph/schema.graphql +++ b/subgraph/schema.graphql @@ -40,6 +40,10 @@ type Vault @entity { balance: BigInt! "All balance changes for this vault" balanceChanges: [VaultBalanceChange!]! @derivedFrom(field: "vault") + "All time vault total volume in, includes only trades volumes" + totalVolumeIn: BigInt! + "All time vault total volume out, includes only trades volumes" + totalVolumeOut: BigInt! } interface VaultBalanceChange { diff --git a/subgraph/src/clear.ts b/subgraph/src/clear.ts index 1aafdc176..827d1b316 100644 --- a/subgraph/src/clear.ts +++ b/subgraph/src/clear.ts @@ -3,7 +3,11 @@ import { Clear, ClearBounty, ClearTemporaryData } from "../generated/schema"; import { eventId } from "./interfaces/event"; import { createTradeEntity } from "./trade"; import { createTradeVaultBalanceChangeEntity } from "./tradevaultbalancechange"; -import { handleVaultBalanceChange, vaultEntityId } from "./vault"; +import { + vaultEntityId, + handleVaultBalanceChange, + handleTradeVaultBalanceChange, +} from "./vault"; import { log } from "@graphprotocol/graph-ts"; import { BigInt, @@ -50,7 +54,7 @@ export function createTrade( outputVaultId: BigInt, outputAmount: BigInt ): void { - let oldInputVaultBalance = handleVaultBalanceChange( + let oldInputVaultBalance = handleTradeVaultBalanceChange( event.address, inputVaultId, inputToken, @@ -65,7 +69,7 @@ export function createTrade( inputAmount ); - let oldOutputVaultBalance = handleVaultBalanceChange( + let oldOutputVaultBalance = handleTradeVaultBalanceChange( event.address, outputVaultId, outputToken, diff --git a/subgraph/src/takeorder.ts b/subgraph/src/takeorder.ts index 56f30c553..4e1a7754d 100644 --- a/subgraph/src/takeorder.ts +++ b/subgraph/src/takeorder.ts @@ -2,7 +2,7 @@ import { Bytes, ethereum } from "@graphprotocol/graph-ts"; import { TakeOrderV2 } from "../generated/OrderBook/OrderBook"; import { TakeOrder } from "../generated/schema"; import { eventId } from "./interfaces/event"; -import { handleVaultBalanceChange, vaultEntityId } from "./vault"; +import { handleTradeVaultBalanceChange, vaultEntityId } from "./vault"; import { createTradeVaultBalanceChangeEntity } from "./tradevaultbalancechange"; import { createTradeEntity } from "./trade"; import { crypto } from "@graphprotocol/graph-ts"; @@ -22,7 +22,7 @@ export function handleTakeOrder(event: TakeOrderV2): void { let orderOutput = order.validOutputs[event.params.config.outputIOIndex.toU32()]; - let oldOutputVaultBalance = handleVaultBalanceChange( + let oldOutputVaultBalance = handleTradeVaultBalanceChange( event.address, orderOutput.vaultId, orderOutput.token, @@ -47,7 +47,7 @@ export function handleTakeOrder(event: TakeOrderV2): void { // Credit the input vault let orderInput = order.validInputs[event.params.config.inputIOIndex.toU32()]; - let oldInputVaultBalance = handleVaultBalanceChange( + let oldInputVaultBalance = handleTradeVaultBalanceChange( event.address, orderInput.vaultId, orderInput.token, diff --git a/subgraph/src/vault.ts b/subgraph/src/vault.ts index 708d74560..bd63bf889 100644 --- a/subgraph/src/vault.ts +++ b/subgraph/src/vault.ts @@ -26,6 +26,8 @@ export function createEmptyVault( vault.token = getERC20Entity(token); vault.owner = owner; vault.balance = BigInt.fromI32(0); + vault.totalVolumeIn = BigInt.fromI32(0); + vault.totalVolumeOut = BigInt.fromI32(0); vault.save(); return vault; } @@ -56,3 +58,27 @@ export function handleVaultBalanceChange( vault.save(); return oldVaultBalance; } + +export function handleTradeVaultBalanceChange( + orderbook: Bytes, + vaultId: BigInt, + token: Bytes, + amount: BigInt, + owner: Bytes +): BigInt { + let oldVaultBalance = handleVaultBalanceChange( + orderbook, + vaultId, + token, + amount, + owner + ); + let vault = getVault(orderbook, owner, vaultId, token); + if (amount.lt(BigInt.fromI32(0))) { + vault.totalVolumeOut = vault.totalVolumeOut.plus(amount.neg()); + } else { + vault.totalVolumeIn = vault.totalVolumeIn.plus(amount); + } + vault.save(); + return oldVaultBalance; +} diff --git a/subgraph/tests/vault.test.ts b/subgraph/tests/vault.test.ts index f5e70ad7e..d571b8205 100644 --- a/subgraph/tests/vault.test.ts +++ b/subgraph/tests/vault.test.ts @@ -6,7 +6,11 @@ import { afterEach, clearInBlockStore, } from "matchstick-as"; -import { handleVaultBalanceChange, vaultEntityId } from "../src/vault"; +import { + vaultEntityId, + handleVaultBalanceChange, + handleTradeVaultBalanceChange, +} from "../src/vault"; import { Bytes, BigInt, Address } from "@graphprotocol/graph-ts"; import { createDepositEvent, createWithdrawEvent } from "./event-mocks.test"; import { createMockERC20Functions } from "./erc20.test"; @@ -282,4 +286,115 @@ describe("Vault balance changes", () => { assert.bigIntEquals(oldBalance, BigInt.fromI32(100)); }); + + test("handleTradeVaultBalanceChange()", () => { + createMockERC20Functions( + Address.fromString("0x1234567890123456789012345678901234567890") + ); + + let vaultId = vaultEntityId( + Bytes.fromHexString("0x0987654321098765432109876543210987654321"), + Address.fromString("0x0987654321098765432109876543210987654321"), + BigInt.fromI32(1), + Address.fromString("0x1234567890123456789012345678901234567890") + ); + + handleTradeVaultBalanceChange( + Address.fromString("0x0987654321098765432109876543210987654321"), + BigInt.fromI32(1), + Bytes.fromHexString("0x1234567890123456789012345678901234567890"), + BigInt.fromI32(100), + Bytes.fromHexString("0x0987654321098765432109876543210987654321") + ); + + assert.entityCount("Vault", 1); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "balance", + BigInt.fromI32(100).toString() + ); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "token", + "0x1234567890123456789012345678901234567890" + ); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "vaultId", + BigInt.fromI32(1).toString() + ); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "owner", + "0x0987654321098765432109876543210987654321" + ); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "balance", + BigInt.fromI32(100).toString() + ); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "totalVolumeIn", + BigInt.fromI32(100).toString() + ); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "totalVolumeOut", + BigInt.fromI32(0).toString() + ); + + handleTradeVaultBalanceChange( + Address.fromString("0x0987654321098765432109876543210987654321"), + BigInt.fromI32(1), + Bytes.fromHexString("0x1234567890123456789012345678901234567890"), + BigInt.fromI32(-50), + Bytes.fromHexString("0x0987654321098765432109876543210987654321") + ); + + assert.entityCount("Vault", 1); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "balance", + BigInt.fromI32(50).toString() + ); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "token", + "0x1234567890123456789012345678901234567890" + ); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "vaultId", + BigInt.fromI32(1).toString() + ); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "owner", + "0x0987654321098765432109876543210987654321" + ); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "totalVolumeIn", + BigInt.fromI32(100).toString() + ); + assert.fieldEquals( + "Vault", + vaultId.toHexString(), + "totalVolumeOut", + BigInt.fromI32(50).toString() + ); + }); }); diff --git a/tauri-app/src-tauri/src/commands/order_quote.rs b/tauri-app/src-tauri/src/commands/order_quote.rs index 1c4ba36ed..2a740cd5e 100644 --- a/tauri-app/src-tauri/src/commands/order_quote.rs +++ b/tauri-app/src-tauri/src/commands/order_quote.rs @@ -434,6 +434,8 @@ amount price: context<3 0>() context<4 0>(); orders_as_input: vec![], orders_as_output: vec![], balance_changes: vec![], + total_volume_in: BigInt("123".to_string()), + total_volume_out: BigInt("123".to_string()), }; let vault2 = Vault { id: Bytes(B256::random().to_string()), @@ -453,6 +455,8 @@ amount price: context<3 0>() context<4 0>(); orders_as_input: vec![], orders_as_output: vec![], balance_changes: vec![], + total_volume_in: BigInt("123".to_string()), + total_volume_out: BigInt("123".to_string()), }; // does not follow the actual original order's io order From 22c3e03c35fcb0964dfae2027ede7801323e67bf Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 17 Oct 2024 01:06:01 +0000 Subject: [PATCH 02/28] Revert "init" This reverts commit 79f5f406dbabbd5b5692904c71bbd7fc3bf52f50. --- crates/cli/src/commands/order/detail.rs | 8 +- crates/cli/src/commands/order/list.rs | 8 +- crates/cli/src/commands/trade/detail.rs | 6 - crates/cli/src/commands/trade/list.rs | 6 - crates/cli/src/commands/vault/detail.rs | 4 +- crates/cli/src/commands/vault/list.rs | 5 +- .../commands/vault/list_balance_changes.rs | 3 - crates/quote/src/cli/mod.rs | 8 +- crates/quote/src/quote.rs | 10 - crates/subgraph/schema/orderbook.graphql | 36 --- crates/subgraph/src/apy.rs | 232 ------------------ crates/subgraph/src/lib.rs | 1 - crates/subgraph/src/orderbook_client.rs | 49 +--- crates/subgraph/src/types/common.rs | 9 - crates/subgraph/src/types/vault.rs | 21 -- crates/subgraph/src/vol.rs | 44 +--- ...er_test__batch_order_query_gql_output.snap | 28 --- .../order_test__orders_query_gql_output.snap | 29 --- ...r_trade_test__vaults_query_gql_output.snap | 6 - ..._trades_test__vaults_query_gql_output.snap | 6 - .../orders_test__orders_query_gql_output.snap | 28 --- ...balance_changes_list_query_gql_output.snap | 3 - .../vault_test__vaults_query_gql_output.snap | 14 -- .../vaults_test__vaults_query_gql_output.snap | 14 -- subgraph/schema.graphql | 4 - subgraph/src/clear.ts | 10 +- subgraph/src/takeorder.ts | 6 +- subgraph/src/vault.ts | 26 -- subgraph/tests/vault.test.ts | 117 +-------- .../src-tauri/src/commands/order_quote.rs | 4 - 30 files changed, 22 insertions(+), 723 deletions(-) delete mode 100644 crates/subgraph/src/apy.rs diff --git a/crates/cli/src/commands/order/detail.rs b/crates/cli/src/commands/order/detail.rs index 2779d85ad..77719c3df 100644 --- a/crates/cli/src/commands/order/detail.rs +++ b/crates/cli/src/commands/order/detail.rs @@ -103,9 +103,7 @@ mod tests { "owner": encode_prefixed(order.owner), "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [], - "totalVolumeIn": "1", - "totalVolumeOut": "1", + "balanceChanges": [] }], "inputs": [{ "id": encode_prefixed(B256::random()), @@ -122,9 +120,7 @@ mod tests { "owner": encode_prefixed(order.owner), "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [], - "totalVolumeIn": "1", - "totalVolumeOut": "1", + "balanceChanges": [] }], "orderbook": { "id": encode_prefixed(B256::random()), diff --git a/crates/cli/src/commands/order/list.rs b/crates/cli/src/commands/order/list.rs index a2e66403e..e5dbd4427 100644 --- a/crates/cli/src/commands/order/list.rs +++ b/crates/cli/src/commands/order/list.rs @@ -231,9 +231,7 @@ mod tests { "owner": encode_prefixed(order.owner), "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [], - "totalVolumeIn": "1", - "totalVolumeOut": "1", + "balanceChanges": [] }], "inputs": [{ "id": encode_prefixed(B256::random()), @@ -250,9 +248,7 @@ mod tests { "owner": encode_prefixed(order.owner), "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [], - "totalVolumeIn": "1", - "totalVolumeOut": "1", + "balanceChanges": [] }], "orderbook": { "id": encode_prefixed(B256::random()), diff --git a/crates/cli/src/commands/trade/detail.rs b/crates/cli/src/commands/trade/detail.rs index b649e26a8..87a185f4e 100644 --- a/crates/cli/src/commands/trade/detail.rs +++ b/crates/cli/src/commands/trade/detail.rs @@ -104,9 +104,6 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), - "totalVolumeIn": "1", - "totalVolumeOut": "1", - "balance": "1", "token": { "name": "T1", "symbol": "T1", @@ -135,9 +132,6 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), - "totalVolumeIn": "1", - "totalVolumeOut": "1", - "balance": "1", "token": { "name": "T2", "symbol": "T2", diff --git a/crates/cli/src/commands/trade/list.rs b/crates/cli/src/commands/trade/list.rs index b6766b88b..9e904ea4e 100644 --- a/crates/cli/src/commands/trade/list.rs +++ b/crates/cli/src/commands/trade/list.rs @@ -200,9 +200,6 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), - "totalVolumeIn": "1", - "totalVolumeOut": "1", - "balance": "1", "token": { "name": "T1", "symbol": "T1", @@ -231,9 +228,6 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), - "totalVolumeIn": "1", - "totalVolumeOut": "1", - "balance": "1", "token": { "name": "T2", "symbol": "T2", diff --git a/crates/cli/src/commands/vault/detail.rs b/crates/cli/src/commands/vault/detail.rs index b9450763d..f364d2efd 100644 --- a/crates/cli/src/commands/vault/detail.rs +++ b/crates/cli/src/commands/vault/detail.rs @@ -104,9 +104,7 @@ mod tests { "orderbook": { "id": encode_prefixed(B256::random()), }, - "balanceChanges": [], - "totalVolumeIn": "1", - "totalVolumeOut": "1", + "balanceChanges": [] } } }) diff --git a/crates/cli/src/commands/vault/list.rs b/crates/cli/src/commands/vault/list.rs index 0c420a499..af7d48b8f 100644 --- a/crates/cli/src/commands/vault/list.rs +++ b/crates/cli/src/commands/vault/list.rs @@ -210,10 +210,7 @@ mod tests { "orderbook": { "id": encode_prefixed(B256::random()), }, - "balanceChanges": [], - "totalVolumeIn": "1", - "totalVolumeOut": "1", - "balance": "1", + "balanceChanges": [] }] } }) diff --git a/crates/cli/src/commands/vault/list_balance_changes.rs b/crates/cli/src/commands/vault/list_balance_changes.rs index ce344bad8..95b25d6f7 100644 --- a/crates/cli/src/commands/vault/list_balance_changes.rs +++ b/crates/cli/src/commands/vault/list_balance_changes.rs @@ -178,9 +178,6 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), - "totalVolumeIn": "1", - "totalVolumeOut": "1", - "balance": "1", "token": { "name": "T1", "symbol": "T1", diff --git a/crates/quote/src/cli/mod.rs b/crates/quote/src/cli/mod.rs index e93906150..71676932b 100644 --- a/crates/quote/src/cli/mod.rs +++ b/crates/quote/src/cli/mod.rs @@ -341,9 +341,7 @@ mod tests { "orderbook": { "id": encode_prefixed(B256::random()) }, "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [], - "totalVolumeIn": "1", - "totalVolumeOut": "1", + "balanceChanges": [] }], "inputs": [{ "id": encode_prefixed(Address::random().0.0), @@ -360,9 +358,7 @@ mod tests { "orderbook": { "id": encode_prefixed(B256::random()) }, "ordersAsOutput": [], "ordersAsInput": [], - "balanceChanges": [], - "totalVolumeIn": "1", - "totalVolumeOut": "1", + "balanceChanges": [] }], "orderbook": { "id": encode_prefixed(B256::random()) }, "active": true, diff --git a/crates/quote/src/quote.rs b/crates/quote/src/quote.rs index d3c593175..4a10272d1 100644 --- a/crates/quote/src/quote.rs +++ b/crates/quote/src/quote.rs @@ -321,8 +321,6 @@ mod tests { "orderHash": encode_prefixed(B256::random()), "active": true }], - "totalVolumeIn": "1", - "totalVolumeOut": "1", "balanceChanges": [{ "__typename": "Withdrawal", "id": encode_prefixed(B256::random()), @@ -332,9 +330,6 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), - "totalVolumeIn": "1", - "totalVolumeOut": "1", - "balance": "1", "token": { "id": encode_prefixed(order.validOutputs[0].token.0.0), "address": encode_prefixed(order.validOutputs[0].token.0.0), @@ -376,8 +371,6 @@ mod tests { "orderHash": encode_prefixed(B256::random()), "active": true }], - "totalVolumeIn": "1", - "totalVolumeOut": "1", "balanceChanges": [{ "__typename": "Withdrawal", "id": encode_prefixed(B256::random()), @@ -387,9 +380,6 @@ mod tests { "vault": { "id": encode_prefixed(B256::random()), "vaultId": encode_prefixed(B256::random()), - "totalVolumeIn": "1", - "totalVolumeOut": "1", - "balance": "1", "token": { "id": encode_prefixed(order.validOutputs[0].token.0.0), "address": encode_prefixed(order.validOutputs[0].token.0.0), diff --git a/crates/subgraph/schema/orderbook.graphql b/crates/subgraph/schema/orderbook.graphql index 19287e9e1..655b4e437 100644 --- a/crates/subgraph/schema/orderbook.graphql +++ b/crates/subgraph/schema/orderbook.graphql @@ -368,8 +368,6 @@ enum ClearBounty_orderBy { vault__owner vault__vaultId vault__balance - vault__totalVolumeIn - vault__totalVolumeOut amount oldVaultBalance newVaultBalance @@ -919,8 +917,6 @@ enum Deposit_orderBy { vault__owner vault__vaultId vault__balance - vault__totalVolumeIn - vault__totalVolumeOut amount oldVaultBalance newVaultBalance @@ -3036,8 +3032,6 @@ enum TradeVaultBalanceChange_orderBy { vault__owner vault__vaultId vault__balance - vault__totalVolumeIn - vault__totalVolumeOut amount oldVaultBalance newVaultBalance @@ -3332,14 +3326,6 @@ type Vault { orderDirection: OrderDirection where: VaultBalanceChange_filter ): [VaultBalanceChange!]! - """ - All time vault total volume in, includes only trades volumes - """ - totalVolumeIn: BigInt! - """ - All time vault total volume out, includes only trades volumes - """ - totalVolumeOut: BigInt! } interface VaultBalanceChange { @@ -3485,8 +3471,6 @@ enum VaultBalanceChange_orderBy { vault__owner vault__vaultId vault__balance - vault__totalVolumeIn - vault__totalVolumeOut amount oldVaultBalance newVaultBalance @@ -3580,22 +3564,6 @@ input Vault_filter { balance_in: [BigInt!] balance_not_in: [BigInt!] balanceChanges_: VaultBalanceChange_filter - totalVolumeIn: BigInt - totalVolumeIn_not: BigInt - totalVolumeIn_gt: BigInt - totalVolumeIn_lt: BigInt - totalVolumeIn_gte: BigInt - totalVolumeIn_lte: BigInt - totalVolumeIn_in: [BigInt!] - totalVolumeIn_not_in: [BigInt!] - totalVolumeOut: BigInt - totalVolumeOut_not: BigInt - totalVolumeOut_gt: BigInt - totalVolumeOut_lt: BigInt - totalVolumeOut_gte: BigInt - totalVolumeOut_lte: BigInt - totalVolumeOut_in: [BigInt!] - totalVolumeOut_not_in: [BigInt!] """ Filter for the block changed event. """ @@ -3620,8 +3588,6 @@ enum Vault_orderBy { ordersAsOutput balance balanceChanges - totalVolumeIn - totalVolumeOut } type Withdrawal implements Event & VaultBalanceChange { @@ -3797,8 +3763,6 @@ enum Withdrawal_orderBy { vault__owner vault__vaultId vault__balance - vault__totalVolumeIn - vault__totalVolumeOut amount oldVaultBalance newVaultBalance diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs deleted file mode 100644 index ca16b3215..000000000 --- a/crates/subgraph/src/apy.rs +++ /dev/null @@ -1,232 +0,0 @@ -use crate::{ - types::common::Erc20, vol::VaultVolume, OrderbookSubgraphClient, OrderbookSubgraphClientError, -}; -use alloy::primitives::{I256, U256}; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; -use typeshare::typeshare; - -pub const YEAR: u64 = 60 * 60 * 24 * 365; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[typeshare] -pub struct VaultAPY { - pub id: String, - pub token: Erc20, - pub start_time: Option, - pub end_time: Option, - #[typeshare(typescript(type = "string"))] - pub net_vol: I256, - #[typeshare(typescript(type = "string"))] - pub capital: U256, - #[typeshare(typescript(type = "string"))] - pub apy: i64, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[typeshare] -pub struct APYTimeframe { - pub start_time: u64, - pub end_time: u64, -} - -/// Given a subgraph and an order id and optionally a timeframe, will fetch data -/// and calculates the APY for each of the order's vaults -pub async fn get_order_vaults_apy( - subgraph_url: Url, - order_id: &str, - timeframe: Option, -) -> Result, OrderbookSubgraphClientError> { - let client = OrderbookSubgraphClient::new(subgraph_url); - let vols = if let Some(timeframe) = timeframe { - client - .order_vaults_volume( - cynic::Id::new(order_id), - Some(timeframe.start_time), - Some(timeframe.end_time), - ) - .await? - } else { - let order = client.order_detail(cynic::Id::new(order_id)).await?; - let mut vols: Vec = vec![]; - for vault in &order.inputs { - if !vols - .iter() - .any(|v| v.id == vault.vault_id.0 && v.token.address.0 == vault.token.address.0) - { - let total_in = U256::from_str(&vault.total_volume_in.0)?; - let total_out = U256::from_str(&vault.total_volume_out.0)?; - vols.push(VaultVolume { - id: vault.vault_id.0.clone(), - token: vault.token.clone(), - total_in, - total_out, - total_vol: total_in.saturating_add(total_out), - net_vol: I256::from_raw(total_in).saturating_sub(I256::from_raw(total_out)), - all_time_vol_in: total_in, - all_time_vol_out: total_out, - }) - } - } - for vault in &order.outputs { - if !vols - .iter() - .any(|v| v.id == vault.vault_id.0 && v.token.address.0 == vault.token.address.0) - { - let total_in = U256::from_str(&vault.total_volume_in.0)?; - let total_out = U256::from_str(&vault.total_volume_out.0)?; - vols.push(VaultVolume { - id: vault.vault_id.0.clone(), - token: vault.token.clone(), - total_in, - total_out, - total_vol: total_in.saturating_add(total_out), - net_vol: I256::from_raw(total_in).saturating_sub(I256::from_raw(total_out)), - all_time_vol_in: total_in, - all_time_vol_out: total_out, - }) - } - } - vols - }; - - let mut vaults_apy: Vec = vec![]; - for vol in vols { - let vault_bal_change = client - .first_day_vault_balance_change( - cynic::Id::new(&vol.id), - timeframe.map(|v| v.start_time), - ) - .await?; - let capital = U256::from_str( - &vault_bal_change - .as_ref() - .map(|v| v.old_vault_balance.0.clone()) - .unwrap_or("0".to_string()), - )?; - let start = u64::from_str( - &timeframe - .map(|v: APYTimeframe| v.start_time.to_string()) - .unwrap_or( - vault_bal_change - .as_ref() - .map(|v| v.timestamp.0.clone()) - .unwrap_or("0".to_string()), - ), - )?; - let end = timeframe - .map(|v| v.end_time) - .unwrap_or(chrono::Utc::now().timestamp() as u64); - let apy = if capital.is_zero() || start == 0 { - 0_i64 - } else { - let change_ratio = i64::try_from( - vol.net_vol - .saturating_mul(I256::from_raw(U256::from(10000))) - .saturating_div(I256::from_raw(capital)), - )? / 10000; - let time_to_year_ratio = ((end - start) / YEAR) as i64; - (change_ratio * time_to_year_ratio) * 100 - }; - vaults_apy.push(VaultAPY { - id: vol.id.clone(), - token: vol.token.clone(), - start_time: timeframe.map(|v| v.start_time), - end_time: timeframe.map(|v| v.end_time), - net_vol: vol.net_vol, - apy, - capital, - }); - } - - Ok(vaults_apy) -} - -// #[cfg(test)] -// mod test { -// use super::*; -// use crate::types::common::{ -// BigInt, Bytes, Orderbook, TradeEvent, TradeStructPartialOrder, TradeVaultBalanceChange, -// Transaction, VaultBalanceChangeVault, -// }; -// use alloy::primitives::{Address, B256}; - -// // helper function that returns mocked sg response in json -// fn get_sg_response() -> Value { -// let io = IO::default(); -// let order = OrderV3 { -// validInputs: vec![io.clone()], -// validOutputs: vec![io.clone()], -// ..Default::default() -// }; -// json!({ -// "data": { -// "order": { -// "id": encode_prefixed(B256::random()), -// "owner": encode_prefixed(order.owner), -// "orderHash": encode_prefixed(B256::random()), -// "orderBytes": encode_prefixed(order.abi_encode()), -// "outputs": [{ -// "id": encode_prefixed(B256::random()), -// "balance": "0", -// "vaultId": io.vaultId.to_string(), -// "token": { -// "name": "T1", -// "symbol": "T1", -// "id": encode_prefixed(io.token), -// "address": encode_prefixed(io.token), -// "decimals": io.decimals.to_string(), -// }, -// "orderbook": { "id": encode_prefixed(B256::random()) }, -// "owner": encode_prefixed(order.owner), -// "ordersAsOutput": [], -// "ordersAsInput": [], -// "balanceChanges": [] -// "totalVolumeIn": "1", -// "totalVolumeOut": "1", -// }], -// "inputs": [{ -// "id": encode_prefixed(B256::random()), -// "balance": "0", -// "vaultId": io.vaultId.to_string(), -// "token": { -// "name": "T2", -// "symbol": "T2", -// "id": encode_prefixed(io.token), -// "address": encode_prefixed(io.token), -// "decimals": io.decimals.to_string(), -// }, -// "orderbook": { "id": encode_prefixed(B256::random()) }, -// "owner": encode_prefixed(order.owner), -// "ordersAsOutput": [], -// "ordersAsInput": [], -// "balanceChanges": [], -// "totalVolumeIn": "1", -// "totalVolumeOut": "1", -// }], -// "orderbook": { -// "id": encode_prefixed(B256::random()), -// }, -// "meta": null, -// "active": true, -// "timestampAdded": "0", -// "addEvents": [{ -// "transaction": { -// "id": encode_prefixed(B256::random()), -// "blockNumber": "0", -// "timestamp": "0", -// "from": encode_prefixed(alloy::primitives::Address::random()) -// } -// }], -// "trades": [] -// } -// } -// }) -// } - -// #[test] -// fn test_get_order_vaults_vol() {} -// } diff --git a/crates/subgraph/src/lib.rs b/crates/subgraph/src/lib.rs index 309a49362..2ca4cc6f5 100644 --- a/crates/subgraph/src/lib.rs +++ b/crates/subgraph/src/lib.rs @@ -1,4 +1,3 @@ -pub mod apy; mod cynic_client; mod orderbook_client; mod pagination; diff --git a/crates/subgraph/src/orderbook_client.rs b/crates/subgraph/src/orderbook_client.rs index c3e7bee38..4663f8ce7 100644 --- a/crates/subgraph/src/orderbook_client.rs +++ b/crates/subgraph/src/orderbook_client.rs @@ -6,7 +6,7 @@ use crate::types::order::{ OrdersListQuery, }; use crate::types::order_trade::{OrderTradeDetailQuery, OrderTradesListQuery}; -use crate::types::vault::{VaultBalanceChangesByTimeListQuery, VaultDetailQuery, VaultsListQuery}; +use crate::types::vault::{VaultDetailQuery, VaultsListQuery}; use crate::vault_balance_changes_query::VaultBalanceChangesListPageQueryClient; use crate::vol::{get_vaults_vol, VaultVolume}; use cynic::Id; @@ -25,10 +25,6 @@ pub enum OrderbookSubgraphClientError { PaginationClientError(#[from] PaginationClientError), #[error(transparent)] ParseError(#[from] alloy::primitives::ruint::ParseError), - #[error(transparent)] - ParseNumError(#[from] std::num::ParseIntError), - #[error(transparent)] - BigintConversionError(#[from] alloy::primitives::BigIntConversionError), } pub struct OrderbookSubgraphClient { @@ -342,47 +338,4 @@ impl OrderbookSubgraphClient { } Ok(all_pages_merged) } - - /// Fetch end of first day vault balance change from a given string timestamp - pub async fn first_day_vault_balance_change( - &self, - id: cynic::Id, - start_timestamp: Option, - ) -> Result, OrderbookSubgraphClientError> { - let day = 60 * 60 * 24; - let first_vault_change_data = self - .query::( - PaginationWithTimestampQueryVariables { - id: Bytes(id.inner().to_string()), - first: Some(1), - skip: None, - timestamp_lte: None, - timestamp_gte: Some( - start_timestamp.map_or(BigInt("0".to_string()), |v| BigInt(v.to_string())), - ), - }, - ) - .await?; - - let first_vault_change_timestamp = first_vault_change_data - .vault_balance_changes - .first() - .ok_or(OrderbookSubgraphClientError::Empty)? - .timestamp - .0 - .parse::()?; - let data = self - .query::( - PaginationWithTimestampQueryVariables { - id: Bytes(id.inner().to_string()), - first: Some(1), - skip: None, - timestamp_lte: None, - timestamp_gte: Some(BigInt((first_vault_change_timestamp + day).to_string())), - }, - ) - .await?; - - Ok(data.vault_balance_changes.first().cloned()) - } } diff --git a/crates/subgraph/src/types/common.rs b/crates/subgraph/src/types/common.rs index f88b66b03..bef02ad12 100644 --- a/crates/subgraph/src/types/common.rs +++ b/crates/subgraph/src/types/common.rs @@ -151,8 +151,6 @@ pub struct Vault { #[arguments(orderBy: timestampAdded, orderDirection: desc)] pub orders_as_input: Vec, pub balance_changes: Vec, - pub total_volume_in: BigInt, - pub total_volume_out: BigInt, } #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] @@ -162,9 +160,6 @@ pub struct VaultBalanceChangeVault { pub id: Bytes, pub vault_id: BigInt, pub token: Erc20, - pub balance: BigInt, - pub total_volume_in: BigInt, - pub total_volume_out: BigInt, } #[derive(cynic::QueryFragment, Debug, Clone, Serialize)] @@ -501,8 +496,4 @@ pub enum VaultOrderBy { Balance, #[cynic(rename = "balanceChanges")] BalanceChanges, - #[cynic(rename = "totalVolumeIn")] - TotalVolumeIn, - #[cynic(rename = "totalVolumeOut")] - TotalVolumeOut, } diff --git a/crates/subgraph/src/types/vault.rs b/crates/subgraph/src/types/vault.rs index aebdc3406..0d55f4daa 100644 --- a/crates/subgraph/src/types/vault.rs +++ b/crates/subgraph/src/types/vault.rs @@ -26,24 +26,3 @@ pub struct VaultBalanceChangesListQuery { #[arguments(orderDirection: "desc", orderBy: "timestamp", where: { vault_: { id: $id } }, skip: $skip, first: $first)] pub vault_balance_changes: Vec, } - -#[derive(cynic::QueryFragment, Debug, Clone, Serialize)] -#[cynic( - graphql_type = "Query", - variables = "PaginationWithTimestampQueryVariables" -)] -#[typeshare] -pub struct VaultBalanceChangesByTimeListQuery { - #[arguments( - skip: $skip, - first: $first, - orderDirection: "asc", - orderBy: "timestamp", - where: { - vault_: { id: $id }, - timestamp_gte: $timestamp_gte, - timestamp_lte: $timestamp_lte - } - )] - pub vault_balance_changes: Vec, -} diff --git a/crates/subgraph/src/vol.rs b/crates/subgraph/src/vol.rs index 6c15d3304..a88798249 100644 --- a/crates/subgraph/src/vol.rs +++ b/crates/subgraph/src/vol.rs @@ -8,20 +8,16 @@ use typeshare::typeshare; #[serde(rename_all = "camelCase")] #[typeshare] pub struct VaultVolume { - pub id: String, - pub token: Erc20, + id: String, + token: Erc20, #[typeshare(typescript(type = "string"))] - pub total_in: U256, + total_in: U256, #[typeshare(typescript(type = "string"))] - pub total_out: U256, + total_out: U256, #[typeshare(typescript(type = "string"))] - pub total_vol: U256, + total_vol: U256, #[typeshare(typescript(type = "string"))] - pub net_vol: I256, - #[typeshare(typescript(type = "string"))] - pub all_time_vol_in: U256, - #[typeshare(typescript(type = "string"))] - pub all_time_vol_out: U256, + net_vol: I256, } /// Get the vaults volume from array of trades @@ -66,12 +62,6 @@ pub fn get_vaults_vol(trades: &[Trade]) -> Result, ParseError> total_out, total_vol, net_vol, - all_time_vol_in: U256::from_str( - &trade.input_vault_balance_change.vault.total_volume_in.0, - )?, - all_time_vol_out: U256::from_str( - &trade.input_vault_balance_change.vault.total_volume_out.0, - )?, }) } if let Some(vault_vol) = vaults_vol.iter_mut().find(|v| { @@ -112,12 +102,6 @@ pub fn get_vaults_vol(trades: &[Trade]) -> Result, ParseError> total_out, total_vol, net_vol, - all_time_vol_in: U256::from_str( - &trade.output_vault_balance_change.vault.total_volume_in.0, - )?, - all_time_vol_out: U256::from_str( - &trade.output_vault_balance_change.vault.total_volume_out.0, - )?, }) } } @@ -182,9 +166,6 @@ mod test { id: bytes.clone(), token: token1.clone(), vault_id: BigInt(vault_id1.to_string()), - balance: BigInt("0".to_string()), - total_volume_in: BigInt("0".to_string()), - total_volume_out: BigInt("0".to_string()), }, timestamp: bigint.clone(), transaction: Transaction { @@ -205,9 +186,6 @@ mod test { id: bytes.clone(), token: token2.clone(), vault_id: BigInt(vault_id2.to_string()), - balance: BigInt("0".to_string()), - total_volume_in: BigInt("0".to_string()), - total_volume_out: BigInt("0".to_string()), }, timestamp: bigint.clone(), transaction: Transaction { @@ -246,9 +224,6 @@ mod test { id: bytes.clone(), token: token2.clone(), vault_id: BigInt(vault_id2.to_string()), - balance: BigInt("0".to_string()), - total_volume_in: BigInt("0".to_string()), - total_volume_out: BigInt("0".to_string()), }, timestamp: bigint.clone(), transaction: Transaction { @@ -269,9 +244,6 @@ mod test { id: bytes.clone(), token: token1.clone(), vault_id: BigInt(vault_id1.to_string()), - balance: BigInt("0".to_string()), - total_volume_in: BigInt("0".to_string()), - total_volume_out: BigInt("0".to_string()), }, timestamp: bigint.clone(), transaction: Transaction { @@ -293,8 +265,6 @@ mod test { total_out: U256::from(7), total_vol: U256::from(12), net_vol: I256::from_str("-2").unwrap(), - all_time_vol_in: U256::from(0), - all_time_vol_out: U256::from(0), }, VaultVolume { id: vault_id1.to_string(), @@ -303,8 +273,6 @@ mod test { total_out: U256::from(2), total_vol: U256::from(5), net_vol: I256::from_str("1").unwrap(), - all_time_vol_in: U256::from(0), - all_time_vol_out: U256::from(0), }, ]; diff --git a/crates/subgraph/tests/snapshots/batch_order_test__batch_order_query_gql_output.snap b/crates/subgraph/tests/snapshots/batch_order_test__batch_order_query_gql_output.snap index a919723a6..b02666f22 100644 --- a/crates/subgraph/tests/snapshots/batch_order_test__batch_order_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/batch_order_test__batch_order_query_gql_output.snap @@ -51,9 +51,6 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -82,9 +79,6 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -113,9 +107,6 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -144,9 +135,6 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -161,8 +149,6 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { sender } } - totalVolumeIn - totalVolumeOut } inputs { id @@ -207,9 +193,6 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -238,9 +221,6 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -269,9 +249,6 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -300,9 +277,6 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -317,8 +291,6 @@ query BatchOrderDetailQuery($id_list: Order_filter!) { sender } } - totalVolumeIn - totalVolumeOut } orderbook { id diff --git a/crates/subgraph/tests/snapshots/order_test__orders_query_gql_output.snap b/crates/subgraph/tests/snapshots/order_test__orders_query_gql_output.snap index d2a4cdf2f..754ed1164 100644 --- a/crates/subgraph/tests/snapshots/order_test__orders_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/order_test__orders_query_gql_output.snap @@ -1,6 +1,5 @@ --- source: crates/subgraph/tests/order_test.rs -assertion_line: 13 expression: request_body.query --- query OrderDetailQuery($id: ID!) { @@ -52,9 +51,6 @@ query OrderDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -83,9 +79,6 @@ query OrderDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -114,9 +107,6 @@ query OrderDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -145,9 +135,6 @@ query OrderDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -162,8 +149,6 @@ query OrderDetailQuery($id: ID!) { sender } } - totalVolumeIn - totalVolumeOut } inputs { id @@ -208,9 +193,6 @@ query OrderDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -239,9 +221,6 @@ query OrderDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -270,9 +249,6 @@ query OrderDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -301,9 +277,6 @@ query OrderDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -318,8 +291,6 @@ query OrderDetailQuery($id: ID!) { sender } } - totalVolumeIn - totalVolumeOut } orderbook { id diff --git a/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap index 4744b19b5..9d0565381 100644 --- a/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/order_trade_test__vaults_query_gql_output.snap @@ -30,9 +30,6 @@ query OrderTradeDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -65,9 +62,6 @@ query OrderTradeDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { diff --git a/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap index 480ca4a02..6c8382206 100644 --- a/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/order_trades_test__vaults_query_gql_output.snap @@ -30,9 +30,6 @@ query OrderTradesListQuery($first: Int, $id: Bytes!, $skip: Int, $timestampGte: symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -65,9 +62,6 @@ query OrderTradesListQuery($first: Int, $id: Bytes!, $skip: Int, $timestampGte: symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { diff --git a/crates/subgraph/tests/snapshots/orders_test__orders_query_gql_output.snap b/crates/subgraph/tests/snapshots/orders_test__orders_query_gql_output.snap index f8b8ddeca..7e6dd6dcb 100644 --- a/crates/subgraph/tests/snapshots/orders_test__orders_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/orders_test__orders_query_gql_output.snap @@ -51,9 +51,6 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -82,9 +79,6 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -113,9 +107,6 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -144,9 +135,6 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -161,8 +149,6 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { sender } } - totalVolumeIn - totalVolumeOut } inputs { id @@ -207,9 +193,6 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -238,9 +221,6 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -269,9 +249,6 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -300,9 +277,6 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -317,8 +291,6 @@ query OrdersListQuery($first: Int, $skip: Int, $filters: Order_filter) { sender } } - totalVolumeIn - totalVolumeOut } orderbook { id diff --git a/crates/subgraph/tests/snapshots/vault_balance_changes_test__vault_balance_changes_list_query_gql_output.snap b/crates/subgraph/tests/snapshots/vault_balance_changes_test__vault_balance_changes_list_query_gql_output.snap index 3dab7fdd9..f8c373a85 100644 --- a/crates/subgraph/tests/snapshots/vault_balance_changes_test__vault_balance_changes_list_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/vault_balance_changes_test__vault_balance_changes_list_query_gql_output.snap @@ -18,9 +18,6 @@ query VaultBalanceChangesListQuery($first: Int, $id: Bytes!, $skip: Int) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { diff --git a/crates/subgraph/tests/snapshots/vault_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/vault_test__vaults_query_gql_output.snap index b61d24ea6..e3beb593c 100644 --- a/crates/subgraph/tests/snapshots/vault_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/vault_test__vaults_query_gql_output.snap @@ -46,9 +46,6 @@ query VaultDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -77,9 +74,6 @@ query VaultDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -108,9 +102,6 @@ query VaultDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -139,9 +130,6 @@ query VaultDetailQuery($id: ID!) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -156,7 +144,5 @@ query VaultDetailQuery($id: ID!) { sender } } - totalVolumeIn - totalVolumeOut } } diff --git a/crates/subgraph/tests/snapshots/vaults_test__vaults_query_gql_output.snap b/crates/subgraph/tests/snapshots/vaults_test__vaults_query_gql_output.snap index bc7d92dfd..c0cd662ca 100644 --- a/crates/subgraph/tests/snapshots/vaults_test__vaults_query_gql_output.snap +++ b/crates/subgraph/tests/snapshots/vaults_test__vaults_query_gql_output.snap @@ -46,9 +46,6 @@ query VaultsListQuery($first: Int, $skip: Int, $filters: Vault_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -77,9 +74,6 @@ query VaultsListQuery($first: Int, $skip: Int, $filters: Vault_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -108,9 +102,6 @@ query VaultsListQuery($first: Int, $skip: Int, $filters: Vault_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -139,9 +130,6 @@ query VaultsListQuery($first: Int, $skip: Int, $filters: Vault_filter) { symbol decimals } - balance - totalVolumeIn - totalVolumeOut } timestamp transaction { @@ -156,7 +144,5 @@ query VaultsListQuery($first: Int, $skip: Int, $filters: Vault_filter) { sender } } - totalVolumeIn - totalVolumeOut } } diff --git a/subgraph/schema.graphql b/subgraph/schema.graphql index 7c8502ee1..c5d52bd6d 100644 --- a/subgraph/schema.graphql +++ b/subgraph/schema.graphql @@ -40,10 +40,6 @@ type Vault @entity { balance: BigInt! "All balance changes for this vault" balanceChanges: [VaultBalanceChange!]! @derivedFrom(field: "vault") - "All time vault total volume in, includes only trades volumes" - totalVolumeIn: BigInt! - "All time vault total volume out, includes only trades volumes" - totalVolumeOut: BigInt! } interface VaultBalanceChange { diff --git a/subgraph/src/clear.ts b/subgraph/src/clear.ts index 827d1b316..1aafdc176 100644 --- a/subgraph/src/clear.ts +++ b/subgraph/src/clear.ts @@ -3,11 +3,7 @@ import { Clear, ClearBounty, ClearTemporaryData } from "../generated/schema"; import { eventId } from "./interfaces/event"; import { createTradeEntity } from "./trade"; import { createTradeVaultBalanceChangeEntity } from "./tradevaultbalancechange"; -import { - vaultEntityId, - handleVaultBalanceChange, - handleTradeVaultBalanceChange, -} from "./vault"; +import { handleVaultBalanceChange, vaultEntityId } from "./vault"; import { log } from "@graphprotocol/graph-ts"; import { BigInt, @@ -54,7 +50,7 @@ export function createTrade( outputVaultId: BigInt, outputAmount: BigInt ): void { - let oldInputVaultBalance = handleTradeVaultBalanceChange( + let oldInputVaultBalance = handleVaultBalanceChange( event.address, inputVaultId, inputToken, @@ -69,7 +65,7 @@ export function createTrade( inputAmount ); - let oldOutputVaultBalance = handleTradeVaultBalanceChange( + let oldOutputVaultBalance = handleVaultBalanceChange( event.address, outputVaultId, outputToken, diff --git a/subgraph/src/takeorder.ts b/subgraph/src/takeorder.ts index 4e1a7754d..56f30c553 100644 --- a/subgraph/src/takeorder.ts +++ b/subgraph/src/takeorder.ts @@ -2,7 +2,7 @@ import { Bytes, ethereum } from "@graphprotocol/graph-ts"; import { TakeOrderV2 } from "../generated/OrderBook/OrderBook"; import { TakeOrder } from "../generated/schema"; import { eventId } from "./interfaces/event"; -import { handleTradeVaultBalanceChange, vaultEntityId } from "./vault"; +import { handleVaultBalanceChange, vaultEntityId } from "./vault"; import { createTradeVaultBalanceChangeEntity } from "./tradevaultbalancechange"; import { createTradeEntity } from "./trade"; import { crypto } from "@graphprotocol/graph-ts"; @@ -22,7 +22,7 @@ export function handleTakeOrder(event: TakeOrderV2): void { let orderOutput = order.validOutputs[event.params.config.outputIOIndex.toU32()]; - let oldOutputVaultBalance = handleTradeVaultBalanceChange( + let oldOutputVaultBalance = handleVaultBalanceChange( event.address, orderOutput.vaultId, orderOutput.token, @@ -47,7 +47,7 @@ export function handleTakeOrder(event: TakeOrderV2): void { // Credit the input vault let orderInput = order.validInputs[event.params.config.inputIOIndex.toU32()]; - let oldInputVaultBalance = handleTradeVaultBalanceChange( + let oldInputVaultBalance = handleVaultBalanceChange( event.address, orderInput.vaultId, orderInput.token, diff --git a/subgraph/src/vault.ts b/subgraph/src/vault.ts index bd63bf889..708d74560 100644 --- a/subgraph/src/vault.ts +++ b/subgraph/src/vault.ts @@ -26,8 +26,6 @@ export function createEmptyVault( vault.token = getERC20Entity(token); vault.owner = owner; vault.balance = BigInt.fromI32(0); - vault.totalVolumeIn = BigInt.fromI32(0); - vault.totalVolumeOut = BigInt.fromI32(0); vault.save(); return vault; } @@ -58,27 +56,3 @@ export function handleVaultBalanceChange( vault.save(); return oldVaultBalance; } - -export function handleTradeVaultBalanceChange( - orderbook: Bytes, - vaultId: BigInt, - token: Bytes, - amount: BigInt, - owner: Bytes -): BigInt { - let oldVaultBalance = handleVaultBalanceChange( - orderbook, - vaultId, - token, - amount, - owner - ); - let vault = getVault(orderbook, owner, vaultId, token); - if (amount.lt(BigInt.fromI32(0))) { - vault.totalVolumeOut = vault.totalVolumeOut.plus(amount.neg()); - } else { - vault.totalVolumeIn = vault.totalVolumeIn.plus(amount); - } - vault.save(); - return oldVaultBalance; -} diff --git a/subgraph/tests/vault.test.ts b/subgraph/tests/vault.test.ts index d571b8205..f5e70ad7e 100644 --- a/subgraph/tests/vault.test.ts +++ b/subgraph/tests/vault.test.ts @@ -6,11 +6,7 @@ import { afterEach, clearInBlockStore, } from "matchstick-as"; -import { - vaultEntityId, - handleVaultBalanceChange, - handleTradeVaultBalanceChange, -} from "../src/vault"; +import { handleVaultBalanceChange, vaultEntityId } from "../src/vault"; import { Bytes, BigInt, Address } from "@graphprotocol/graph-ts"; import { createDepositEvent, createWithdrawEvent } from "./event-mocks.test"; import { createMockERC20Functions } from "./erc20.test"; @@ -286,115 +282,4 @@ describe("Vault balance changes", () => { assert.bigIntEquals(oldBalance, BigInt.fromI32(100)); }); - - test("handleTradeVaultBalanceChange()", () => { - createMockERC20Functions( - Address.fromString("0x1234567890123456789012345678901234567890") - ); - - let vaultId = vaultEntityId( - Bytes.fromHexString("0x0987654321098765432109876543210987654321"), - Address.fromString("0x0987654321098765432109876543210987654321"), - BigInt.fromI32(1), - Address.fromString("0x1234567890123456789012345678901234567890") - ); - - handleTradeVaultBalanceChange( - Address.fromString("0x0987654321098765432109876543210987654321"), - BigInt.fromI32(1), - Bytes.fromHexString("0x1234567890123456789012345678901234567890"), - BigInt.fromI32(100), - Bytes.fromHexString("0x0987654321098765432109876543210987654321") - ); - - assert.entityCount("Vault", 1); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "balance", - BigInt.fromI32(100).toString() - ); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "token", - "0x1234567890123456789012345678901234567890" - ); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "vaultId", - BigInt.fromI32(1).toString() - ); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "owner", - "0x0987654321098765432109876543210987654321" - ); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "balance", - BigInt.fromI32(100).toString() - ); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "totalVolumeIn", - BigInt.fromI32(100).toString() - ); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "totalVolumeOut", - BigInt.fromI32(0).toString() - ); - - handleTradeVaultBalanceChange( - Address.fromString("0x0987654321098765432109876543210987654321"), - BigInt.fromI32(1), - Bytes.fromHexString("0x1234567890123456789012345678901234567890"), - BigInt.fromI32(-50), - Bytes.fromHexString("0x0987654321098765432109876543210987654321") - ); - - assert.entityCount("Vault", 1); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "balance", - BigInt.fromI32(50).toString() - ); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "token", - "0x1234567890123456789012345678901234567890" - ); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "vaultId", - BigInt.fromI32(1).toString() - ); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "owner", - "0x0987654321098765432109876543210987654321" - ); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "totalVolumeIn", - BigInt.fromI32(100).toString() - ); - assert.fieldEquals( - "Vault", - vaultId.toHexString(), - "totalVolumeOut", - BigInt.fromI32(50).toString() - ); - }); }); diff --git a/tauri-app/src-tauri/src/commands/order_quote.rs b/tauri-app/src-tauri/src/commands/order_quote.rs index 2a740cd5e..1c4ba36ed 100644 --- a/tauri-app/src-tauri/src/commands/order_quote.rs +++ b/tauri-app/src-tauri/src/commands/order_quote.rs @@ -434,8 +434,6 @@ amount price: context<3 0>() context<4 0>(); orders_as_input: vec![], orders_as_output: vec![], balance_changes: vec![], - total_volume_in: BigInt("123".to_string()), - total_volume_out: BigInt("123".to_string()), }; let vault2 = Vault { id: Bytes(B256::random().to_string()), @@ -455,8 +453,6 @@ amount price: context<3 0>() context<4 0>(); orders_as_input: vec![], orders_as_output: vec![], balance_changes: vec![], - total_volume_in: BigInt("123".to_string()), - total_volume_out: BigInt("123".to_string()), }; // does not follow the actual original order's io order From 4fdb5a6b55d3e2a551de03a1a9bcfeb0bbb88ca3 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Fri, 18 Oct 2024 04:30:51 +0000 Subject: [PATCH 03/28] apy logic --- crates/subgraph/src/apy.rs | 769 ++++++++++++++++++++++++ crates/subgraph/src/lib.rs | 1 + crates/subgraph/src/orderbook_client.rs | 6 + crates/subgraph/src/types/common.rs | 6 +- crates/subgraph/src/vol.rs | 14 +- 5 files changed, 786 insertions(+), 10 deletions(-) create mode 100644 crates/subgraph/src/apy.rs diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs new file mode 100644 index 000000000..81ab882db --- /dev/null +++ b/crates/subgraph/src/apy.rs @@ -0,0 +1,769 @@ +use crate::{ + types::common::{Erc20, Order, Trade}, + vol::{get_vaults_vol, VaultVolume}, + OrderbookSubgraphClientError, +}; +use alloy::primitives::{ + utils::{format_units, parse_units, ParseUnits, Unit, UnitsError}, + I256, U256, +}; +use core::f64; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, str::FromStr}; +use typeshare::typeshare; + +pub const ONE: &str = "1000000000000000000"; +pub const DAY: u64 = 60 * 60 * 24; +pub const YEAR: u64 = DAY * 365; +pub const PREFERED_DENOMINATIONS: [&str; 11] = [ + "usdt", "usdc", "dai", "frax", "mim", "usdp", "weth", "wbtc", "wpol", "wmatic", "wbnb", +]; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct TokenVaultAPY { + pub id: String, + pub token: Erc20, + pub start_time: u64, + pub end_time: u64, + #[typeshare(typescript(type = "string"))] + pub net_vol: I256, + #[typeshare(typescript(type = "string"))] + pub capital: U256, + pub apy: f64, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct DenominatedAPY { + pub apy: f64, + pub token: Erc20, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct OrderAPY { + pub order_id: String, + pub order_hash: String, + pub apy: Option, + pub start_time: u64, + pub end_time: u64, + pub inputs_token_vault_apy: Vec, + pub outputs_token_vault_apy: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct TokenPair { + input: Erc20, + output: Erc20, +} + +/// Given an order and its trades and optionally a timeframe, will calculates +/// the APY for each of the entire order and for each of its vaults +pub fn get_order_apy( + order: Order, + trades: &[Trade], + start_timestamp: Option, + end_timestamp: Option, +) -> Result { + let one = I256::from_str(ONE).unwrap(); + if trades.is_empty() { + return Ok(OrderAPY { + order_id: order.id.0.clone(), + order_hash: order.order_hash.0.clone(), + start_time: start_timestamp.unwrap_or(0), + end_time: end_timestamp.unwrap_or(chrono::Utc::now().timestamp() as u64), + inputs_token_vault_apy: vec![], + outputs_token_vault_apy: vec![], + apy: None, + }); + } + let vols = get_vaults_vol(trades)?; + let token_vaults_apy = get_token_vaults_apy(trades, &vols, start_timestamp, end_timestamp)?; + + // build an OrderApy struct + let mut start_time = u64::MAX; + let mut end_time = 0_u64; + let mut inputs: Vec = vec![]; + let mut outputs: Vec = vec![]; + for item in &token_vaults_apy { + if item.start_time < start_time { + start_time = item.start_time; + } + if item.end_time > end_time { + end_time = item.end_time; + } + if order + .inputs + .iter() + .any(|v| v.vault_id.0 == item.id && v.token == item.token) + { + inputs.push(item.clone()); + } + if order + .outputs + .iter() + .any(|v| v.vault_id.0 == item.id && v.token == item.token) + { + outputs.push(item.clone()); + } + } + let mut order_apy = OrderAPY { + order_id: order.id.0.clone(), + order_hash: order.order_hash.0.clone(), + start_time, + end_time, + inputs_token_vault_apy: inputs, + outputs_token_vault_apy: outputs, + apy: None, + }; + + // get pairs ratios + let pair_ratio_map = get_pairs_ratio(&order_apy, trades); + + // try to calculate all vaults capital and volume denominated into any of + // the order's tokens by checking if there is direct ratio between the tokens, + // multi path ratios are ignored currently and results in None for the APY. + // if there is a success for any of the denomination tokens, checks if it is + // among the prefered ones, if not continues the process with remaining tokens. + // if none of the successfull calcs fulfills any of the prefered denominations + // will end up picking the first one. + // if there was no success with any of the order's tokens, simply return None + // for the APY. + let mut apy_denominations = vec![]; + for token in &token_vaults_apy { + let mut noway = false; + let mut combined_capital = I256::ZERO; + let mut combined_annual_rate_vol = I256::ZERO; + for token_vault in &token_vaults_apy { + // time to year ratio with 4 point decimals + let annual_rate = I256::from_raw(U256::from( + ((token_vault.end_time - token_vault.start_time) * 10_000) / YEAR, + )); + let token_decimals = token_vault + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18"); + + // convert to 18 point decimals + let vault_capital = + to_18_decimals(ParseUnits::U256(token_vault.capital), token_decimals); + let vault_net_vol = + to_18_decimals(ParseUnits::I256(token_vault.net_vol), token_decimals); + if vault_capital.is_err() || vault_net_vol.is_err() { + noway = true; + break; + } + let vault_capital = vault_capital.unwrap().get_signed(); + let vault_net_vol = vault_net_vol.unwrap().get_signed(); + + // sum up all capitals and vols in one denomination + if token_vault.token == token.token { + combined_capital += vault_capital; + combined_annual_rate_vol += vault_net_vol.saturating_mul(annual_rate); + } else { + let pair = TokenPair { + input: token.token.clone(), + output: token_vault.token.clone(), + }; + // convert to current denomination by the direct pair ratio if exists + if let Some(Some(ratio)) = pair_ratio_map.get(&pair) { + combined_capital += vault_capital.saturating_mul(*ratio).saturating_div(one); + combined_annual_rate_vol += token_vault + .net_vol + .saturating_mul(*ratio) + .saturating_div(one) + .saturating_mul(annual_rate); + } else { + noway = true; + break; + } + } + } + + // success + if !noway { + // by 4 point decimals + let int_apy = i64::try_from( + combined_annual_rate_vol + .saturating_mul(I256::from_raw(U256::from(10_000))) + .saturating_div(combined_capital), + )?; + // div by 10_000 to convert to actual float and again by 10_000 to + // factor in the anuual rate and then mul by 100 to convert to + // percentage, so equals to div by 1_000_000, + let apy = int_apy as f64 / 1_000_000f64; + let denominated_apy = DenominatedAPY { + apy, + token: token.token.clone(), + }; + // chcek if this token is one of prefered ones and if so return early + // if not continue to next token denomination + for denomination in PREFERED_DENOMINATIONS { + if token + .token + .symbol + .as_ref() + .is_some_and(|sym| sym.to_ascii_lowercase().contains(denomination)) + { + order_apy.apy = Some(denominated_apy.clone()); + return Ok(order_apy); + } + } + apy_denominations.push(denominated_apy); + } + } + + // none of the order's tokens fulfilled any of the prefered denominations + // so just pick the first one if there was any success at all + if !apy_denominations.is_empty() { + order_apy.apy = Some(apy_denominations[0].clone()); + } + + Ok(order_apy) +} + +/// Calculates each token vault apy at the given timeframe +pub fn get_token_vaults_apy( + trades: &[Trade], + vols: &[VaultVolume], + start_timestamp: Option, + end_timestamp: Option, +) -> Result, OrderbookSubgraphClientError> { + let mut token_vaults_apy: Vec = vec![]; + for vol in vols { + // this token vault trades in desc order by timestamp + let vault_trades = trades + .iter() + .filter(|v| { + (v.input_vault_balance_change.vault.vault_id.0 == vol.id + && v.input_vault_balance_change.vault.token == vol.token) + || (v.output_vault_balance_change.vault.vault_id.0 == vol.id + && v.output_vault_balance_change.vault.token == vol.token) + }) + .collect::>(); + + // this token vault first trade, indictaes the start time + // to find the end of the first day to find the starting capital + let first_trade = vault_trades[vault_trades.len() - 1]; + let first_day_last_trade = vault_trades + .iter() + .filter(|v| { + u64::from_str(&v.timestamp.0).unwrap() + <= u64::from_str(&first_trade.timestamp.0).unwrap() + DAY + }) + .collect::>()[0]; + + // vaults starting capital at end of first day of its first ever trade + let starting_capital = if first_day_last_trade + .input_vault_balance_change + .vault + .vault_id + .0 + == vol.id + && first_day_last_trade.input_vault_balance_change.vault.token == vol.token + { + U256::from_str( + &first_day_last_trade + .input_vault_balance_change + .new_vault_balance + .0, + )? + } else { + U256::from_str( + &first_day_last_trade + .output_vault_balance_change + .new_vault_balance + .0, + )? + }; + + // the time range for this token vault + let mut start = u64::from_str(&first_trade.timestamp.0)?; + start_timestamp.inspect(|t| { + if start > *t { + start = *t; + } + }); + let end = end_timestamp.unwrap_or(chrono::Utc::now().timestamp() as u64); + + // this token vault apy + let apy = if starting_capital.is_zero() { + 0_f64 + } else { + // by 4 point decimals + let change_ratio = i64::try_from( + vol.net_vol + .saturating_mul(I256::from_raw(U256::from(10_000))) + .saturating_div(I256::from_raw(starting_capital)), + )? as f64; + let time_to_year_ratio = ((end - start) as f64) / YEAR as f64; + (change_ratio * time_to_year_ratio) / 100f64 + }; + token_vaults_apy.push(TokenVaultAPY { + id: vol.id.clone(), + token: vol.token.clone(), + start_time: start, + end_time: end, + net_vol: vol.net_vol, + apy, + capital: starting_capital, + }); + } + + Ok(token_vaults_apy) +} + +/// Calculates an order's pairs' ratios from their last trades in a given list of trades +fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap> { + let one = I256::from_str(ONE).unwrap(); + let mut pair_ratio_map: HashMap> = HashMap::new(); + for input in &order_apy.inputs_token_vault_apy { + for output in &order_apy.outputs_token_vault_apy { + if input.token != output.token { + // find this pairs trades from list of order's trades + let pair_trades = trades + .iter() + .filter(|v| { + v.input_vault_balance_change.vault.token == input.token + && v.output_vault_balance_change.vault.token == output.token + && v.input_vault_balance_change.vault.vault_id.0 == input.id + && v.output_vault_balance_change.vault.vault_id.0 == output.id + }) + .collect::>(); + + // calculate the pair ratio (in amount/out amount) + let ratio = if pair_trades.is_empty() { + None + } else { + // convert input and output amounts to 18 decimals point + // and then calculate the pair ratio + let input_amount = to_18_decimals( + ParseUnits::U256( + U256::from_str(&pair_trades[0].input_vault_balance_change.amount.0) + .unwrap(), + ), + pair_trades[0] + .input_vault_balance_change + .vault + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18"), + ); + let output_amount = to_18_decimals( + ParseUnits::U256( + U256::from_str( + &pair_trades[0].output_vault_balance_change.amount.0[1..], + ) + .unwrap(), + ), + pair_trades[0] + .output_vault_balance_change + .vault + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18"), + ); + #[allow(clippy::unnecessary_unwrap)] + if input_amount.is_err() || output_amount.is_err() { + None + } else { + Some( + input_amount + .unwrap() + .get_signed() + .saturating_mul(one) + .checked_div(output_amount.unwrap().get_signed()) + .unwrap_or(I256::MAX), + ) + } + }; + pair_ratio_map.insert( + TokenPair { + input: input.token.clone(), + output: output.token.clone(), + }, + ratio, + ); + } + } + } + pair_ratio_map +} + +/// Converts a U256 or I256 to a fixed point U256 or I256 given the decimals point +pub fn to_18_decimals>( + amount: ParseUnits, + decimals: T, +) -> Result { + parse_units(&format_units(amount, decimals)?, 18) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::common::{ + BigInt, Bytes, Orderbook, TradeEvent, TradeStructPartialOrder, TradeVaultBalanceChange, + Transaction, Vault, VaultBalanceChangeVault, + }; + use alloy::primitives::{Address, B256}; + + #[test] + fn test_to_18_decimals() { + let value = ParseUnits::I256(I256::from_str("-123456789").unwrap()); + let result = to_18_decimals(value, 5).unwrap(); + let expected = ParseUnits::I256(I256::from_str("-1234567890000000000000").unwrap()); + assert_eq!(result, expected); + + let value = ParseUnits::U256(U256::from_str("123456789").unwrap()); + let result = to_18_decimals(value, 12).unwrap(); + let expected = ParseUnits::U256(U256::from_str("123456789000000").unwrap()); + assert_eq!(result, expected); + } + + #[test] + fn test_get_pairs_ratio() { + let trades = get_trades(); + let [token1, token2] = get_tokens(); + let [vault1, vault2] = get_vault_ids(); + let token_vault1 = TokenVaultAPY { + id: vault1.to_string(), + token: token1.clone(), + start_time: 0, + end_time: 0, + net_vol: I256::ZERO, + capital: U256::ZERO, + apy: 0f64, + }; + let token_vault2 = TokenVaultAPY { + id: vault2.to_string(), + token: token2.clone(), + start_time: 0, + end_time: 0, + net_vol: I256::ZERO, + capital: U256::ZERO, + apy: 0f64, + }; + let order_apy = OrderAPY { + order_id: "".to_string(), + order_hash: "".to_string(), + apy: None, + start_time: 0, + end_time: 0, + inputs_token_vault_apy: vec![token_vault1.clone(), token_vault2.clone()], + outputs_token_vault_apy: vec![token_vault1, token_vault2], + }; + let result = get_pairs_ratio(&order_apy, &trades); + let mut expected = HashMap::new(); + expected.insert( + TokenPair { + input: token2.clone(), + output: token1.clone(), + }, + Some(I256::from_str("2500000000000000000").unwrap()), + ); + expected.insert( + TokenPair { + input: token1.clone(), + output: token2.clone(), + }, + Some(I256::from_str("3500000000000000000").unwrap()), + ); + + assert_eq!(result, expected); + } + + #[test] + fn test_get_token_vaults_apy() { + let trades = get_trades(); + let [token1, token2] = get_tokens(); + let [vault1, vault2] = get_vault_ids(); + let vault_vol1 = VaultVolume { + id: vault1.to_string(), + token: token1.clone(), + total_in: U256::ZERO, + total_out: U256::ZERO, + total_vol: U256::ZERO, + net_vol: I256::from_str("1000000000000000000").unwrap(), + }; + let vault_vol2 = VaultVolume { + id: vault2.to_string(), + token: token2.clone(), + total_in: U256::ZERO, + total_out: U256::ZERO, + total_vol: U256::ZERO, + net_vol: I256::from_str("2000000000000000000").unwrap(), + }; + let result = + get_token_vaults_apy(&trades, &[vault_vol1, vault_vol2], Some(1), Some(10000001)) + .unwrap(); + let expected = vec![ + TokenVaultAPY { + id: vault1.to_string(), + token: token1.clone(), + start_time: 1, + end_time: 10000001, + net_vol: I256::from_str("1000000000000000000").unwrap(), + capital: U256::from_str("2000000000000000000").unwrap(), + apy: 15.854895991882293, + }, + TokenVaultAPY { + id: vault2.to_string(), + token: token2.clone(), + start_time: 1, + end_time: 10000001, + net_vol: I256::from_str("2000000000000000000").unwrap(), + capital: U256::from_str("2000000000000000000").unwrap(), + apy: 31.709791983764585, + }, + ]; + + assert_eq!(result, expected); + } + + #[test] + fn test_get_order_apy() { + let order = get_order(); + let trades = get_trades(); + let [token1, token2] = get_tokens(); + let [vault1, vault2] = get_vault_ids(); + let token1_apy = TokenVaultAPY { + id: vault1.to_string(), + token: token1.clone(), + start_time: 1, + end_time: 10000001, + net_vol: I256::from_str("5000000000000000000").unwrap(), + capital: U256::from_str("2000000000000000000").unwrap(), + apy: 79.27447995941147, + }; + let token2_apy = TokenVaultAPY { + id: vault2.to_string(), + token: token2.clone(), + start_time: 1, + end_time: 10000001, + net_vol: I256::from_str("3000000000000000000").unwrap(), + capital: U256::from_str("2000000000000000000").unwrap(), + apy: 47.564687975646876, + }; + let result = get_order_apy(order, &trades, Some(1), Some(10000001)).unwrap(); + let expected = OrderAPY { + order_id: "order-id".to_string(), + order_hash: "".to_string(), + start_time: 1, + end_time: 10000001, + inputs_token_vault_apy: vec![token2_apy.clone(), token1_apy.clone()], + outputs_token_vault_apy: vec![token2_apy.clone(), token1_apy.clone()], + apy: Some(DenominatedAPY { + apy: 70.192857, + token: token2, + }), + }; + + assert_eq!(result, expected); + } + + fn get_vault_ids() -> [B256; 2] { + [ + B256::from_slice(&[0x11u8; 32]), + B256::from_slice(&[0x22u8; 32]), + ] + } + fn get_tokens() -> [Erc20; 2] { + let token1_address = Address::from_slice(&[0x11u8; 20]); + let token2_address = Address::from_slice(&[0x22u8; 20]); + let token1 = Erc20 { + id: Bytes(token1_address.to_string()), + address: Bytes(token1_address.to_string()), + name: Some("Token1".to_string()), + symbol: Some("Token1".to_string()), + decimals: Some(BigInt(18.to_string())), + }; + let token2 = Erc20 { + id: Bytes(token2_address.to_string()), + address: Bytes(token2_address.to_string()), + name: Some("Token2".to_string()), + symbol: Some("Token2".to_string()), + decimals: Some(BigInt(18.to_string())), + }; + [token1, token2] + } + fn get_order() -> Order { + let [vault_id1, vault_id2] = get_vault_ids(); + let [token1, token2] = get_tokens(); + let vault1 = Vault { + id: Bytes("".to_string()), + owner: Bytes("".to_string()), + vault_id: BigInt(vault_id1.to_string()), + balance: BigInt("".to_string()), + token: token1, + orderbook: Orderbook { + id: Bytes("".to_string()), + }, + orders_as_output: vec![], + orders_as_input: vec![], + balance_changes: vec![], + }; + let vault2 = Vault { + id: Bytes("".to_string()), + owner: Bytes("".to_string()), + vault_id: BigInt(vault_id2.to_string()), + balance: BigInt("".to_string()), + token: token2, + orderbook: Orderbook { + id: Bytes("".to_string()), + }, + orders_as_output: vec![], + orders_as_input: vec![], + balance_changes: vec![], + }; + Order { + id: Bytes("order-id".to_string()), + order_bytes: Bytes("".to_string()), + order_hash: Bytes("".to_string()), + owner: Bytes("".to_string()), + outputs: vec![vault1.clone(), vault2.clone()], + inputs: vec![vault1, vault2], + orderbook: Orderbook { + id: Bytes("".to_string()), + }, + active: true, + timestamp_added: BigInt("".to_string()), + meta: None, + add_events: vec![], + trades: vec![], + } + } + + fn get_trades() -> Vec { + let bytes = Bytes("".to_string()); + let bigint = BigInt("".to_string()); + let [vault_id1, vault_id2] = get_vault_ids(); + let [token1, token2] = get_tokens(); + let trade1 = Trade { + id: bytes.clone(), + order: TradeStructPartialOrder { + id: bytes.clone(), + order_hash: bytes.clone(), + }, + trade_event: TradeEvent { + sender: bytes.clone(), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: bigint.clone(), + }, + }, + timestamp: BigInt("1".to_string()), + orderbook: Orderbook { id: bytes.clone() }, + output_vault_balance_change: TradeVaultBalanceChange { + id: bytes.clone(), + __typename: "TradeVaultBalanceChange".to_string(), + amount: BigInt("-2000000000000000000".to_string()), + new_vault_balance: BigInt("2000000000000000000".to_string()), + old_vault_balance: bigint.clone(), + vault: VaultBalanceChangeVault { + id: bytes.clone(), + token: token1.clone(), + vault_id: BigInt(vault_id1.to_string()), + }, + timestamp: BigInt("1".to_string()), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: BigInt("1".to_string()), + }, + orderbook: Orderbook { id: bytes.clone() }, + }, + input_vault_balance_change: TradeVaultBalanceChange { + id: bytes.clone(), + __typename: "TradeVaultBalanceChange".to_string(), + amount: BigInt("5000000000000000000".to_string()), + new_vault_balance: BigInt("2000000000000000000".to_string()), + old_vault_balance: bigint.clone(), + vault: VaultBalanceChangeVault { + id: bytes.clone(), + token: token2.clone(), + vault_id: BigInt(vault_id2.to_string()), + }, + timestamp: BigInt("1".to_string()), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: BigInt("1".to_string()), + }, + orderbook: Orderbook { id: bytes.clone() }, + }, + }; + let trade2 = Trade { + id: bytes.clone(), + order: TradeStructPartialOrder { + id: bytes.clone(), + order_hash: bytes.clone(), + }, + trade_event: TradeEvent { + sender: bytes.clone(), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: bigint.clone(), + }, + }, + timestamp: BigInt("1".to_string()), + orderbook: Orderbook { id: bytes.clone() }, + output_vault_balance_change: TradeVaultBalanceChange { + id: bytes.clone(), + __typename: "TradeVaultBalanceChange".to_string(), + amount: BigInt("-2000000000000000000".to_string()), + new_vault_balance: BigInt("5000000000000000000".to_string()), + old_vault_balance: bigint.clone(), + vault: VaultBalanceChangeVault { + id: bytes.clone(), + token: token2.clone(), + vault_id: BigInt(vault_id2.to_string()), + }, + timestamp: BigInt("1".to_string()), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: BigInt("1".to_string()), + }, + orderbook: Orderbook { id: bytes.clone() }, + }, + input_vault_balance_change: TradeVaultBalanceChange { + id: bytes.clone(), + __typename: "TradeVaultBalanceChange".to_string(), + amount: BigInt("7000000000000000000".to_string()), + new_vault_balance: BigInt("5000000000000000000".to_string()), + old_vault_balance: bigint.clone(), + vault: VaultBalanceChangeVault { + id: bytes.clone(), + token: token1.clone(), + vault_id: BigInt(vault_id1.to_string()), + }, + timestamp: BigInt("1".to_string()), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: BigInt("1".to_string()), + }, + orderbook: Orderbook { id: bytes.clone() }, + }, + }; + vec![trade1, trade2] + } +} diff --git a/crates/subgraph/src/lib.rs b/crates/subgraph/src/lib.rs index 2ca4cc6f5..309a49362 100644 --- a/crates/subgraph/src/lib.rs +++ b/crates/subgraph/src/lib.rs @@ -1,3 +1,4 @@ +pub mod apy; mod cynic_client; mod orderbook_client; mod pagination; diff --git a/crates/subgraph/src/orderbook_client.rs b/crates/subgraph/src/orderbook_client.rs index 4663f8ce7..ab637e70c 100644 --- a/crates/subgraph/src/orderbook_client.rs +++ b/crates/subgraph/src/orderbook_client.rs @@ -25,6 +25,12 @@ pub enum OrderbookSubgraphClientError { PaginationClientError(#[from] PaginationClientError), #[error(transparent)] ParseError(#[from] alloy::primitives::ruint::ParseError), + #[error(transparent)] + ParseBigIntConversionError(#[from] alloy::primitives::BigIntConversionError), + #[error(transparent)] + ParseFloatError(#[from] std::num::ParseFloatError), + #[error(transparent)] + ParseIntError(#[from] std::num::ParseIntError), } pub struct OrderbookSubgraphClient { diff --git a/crates/subgraph/src/types/common.rs b/crates/subgraph/src/types/common.rs index bef02ad12..0489778a3 100644 --- a/crates/subgraph/src/types/common.rs +++ b/crates/subgraph/src/types/common.rs @@ -278,7 +278,7 @@ pub struct OrderStructPartialTrade { pub id: Bytes, } -#[derive(cynic::QueryFragment, Debug, Serialize, Clone, PartialEq)] +#[derive(cynic::QueryFragment, Debug, Serialize, Clone, PartialEq, Eq, Hash)] #[cynic(graphql_type = "ERC20")] #[typeshare] pub struct Erc20 { @@ -305,11 +305,11 @@ pub struct AddOrder { pub transaction: Transaction, } -#[derive(cynic::Scalar, Debug, Clone, PartialEq)] +#[derive(cynic::Scalar, Debug, Clone, PartialEq, Eq, Hash)] #[typeshare] pub struct BigInt(pub String); -#[derive(cynic::Scalar, Debug, Clone, PartialEq)] +#[derive(cynic::Scalar, Debug, Clone, PartialEq, Eq, Hash)] #[typeshare] pub struct Bytes(pub String); diff --git a/crates/subgraph/src/vol.rs b/crates/subgraph/src/vol.rs index a88798249..416787da0 100644 --- a/crates/subgraph/src/vol.rs +++ b/crates/subgraph/src/vol.rs @@ -4,20 +4,20 @@ use serde::{Deserialize, Serialize}; use std::str::FromStr; use typeshare::typeshare; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(rename_all = "camelCase")] #[typeshare] pub struct VaultVolume { - id: String, - token: Erc20, + pub id: String, + pub token: Erc20, #[typeshare(typescript(type = "string"))] - total_in: U256, + pub total_in: U256, #[typeshare(typescript(type = "string"))] - total_out: U256, + pub total_out: U256, #[typeshare(typescript(type = "string"))] - total_vol: U256, + pub total_vol: U256, #[typeshare(typescript(type = "string"))] - net_vol: I256, + pub net_vol: I256, } /// Get the vaults volume from array of trades From c61d43badb4e5bd8ddcaefc37bdb40aee42ee775 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Fri, 18 Oct 2024 05:21:20 +0000 Subject: [PATCH 04/28] ui --- crates/subgraph/src/apy.rs | 6 ++ flake.nix | 2 +- .../src-tauri/src/commands/order_take.rs | 23 ++++++ tauri-app/src-tauri/src/main.rs | 6 +- .../components/charts/APYTimeFilters.svelte | 53 +++++++++++++ .../lib/components/detail/OrderDetail.svelte | 4 + .../src/lib/components/tables/OrderAPY.svelte | 37 +++++++++ .../lib/components/tables/OrderAPY.test.ts | 79 +++++++++++++++++++ tauri-app/src/lib/queries/keys.ts | 1 + tauri-app/src/lib/queries/orderTradesList.ts | 21 ++++- 10 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 tauri-app/src/lib/components/charts/APYTimeFilters.svelte create mode 100644 tauri-app/src/lib/components/tables/OrderAPY.svelte create mode 100644 tauri-app/src/lib/components/tables/OrderAPY.test.ts diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 81ab882db..67d005ab2 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -25,12 +25,15 @@ pub const PREFERED_DENOMINATIONS: [&str; 11] = [ pub struct TokenVaultAPY { pub id: String, pub token: Erc20, + #[typeshare(typescript(type = "number"))] pub start_time: u64, + #[typeshare(typescript(type = "number"))] pub end_time: u64, #[typeshare(typescript(type = "string"))] pub net_vol: I256, #[typeshare(typescript(type = "string"))] pub capital: U256, + #[typeshare(typescript(type = "number"))] pub apy: f64, } @@ -38,6 +41,7 @@ pub struct TokenVaultAPY { #[serde(rename_all = "camelCase")] #[typeshare] pub struct DenominatedAPY { + #[typeshare(typescript(type = "number"))] pub apy: f64, pub token: Erc20, } @@ -49,7 +53,9 @@ pub struct OrderAPY { pub order_id: String, pub order_hash: String, pub apy: Option, + #[typeshare(typescript(type = "number"))] pub start_time: u64, + #[typeshare(typescript(type = "number"))] pub end_time: u64, pub inputs_token_vault_apy: Vec, pub outputs_token_vault_apy: Vec, diff --git a/flake.nix b/flake.nix index 510db2a3e..852a411cf 100644 --- a/flake.nix +++ b/flake.nix @@ -63,7 +63,7 @@ cargo install --git https://github.com/tomjw64/typeshare --rev 556b44aafd5304eedf17206800f69834e3820b7c export PATH=$PATH:$CARGO_HOME/bin - typeshare crates/subgraph/src/types/common.rs crates/subgraph/src/types/order.rs crates/subgraph/src/types/vault.rs crates/subgraph/src/types/order_trade.rs crates/common/src/types/order_detail_extended.rs crates/subgraph/src/vol.rs --lang=typescript --output-file=tauri-app/src/lib/typeshare/subgraphTypes.ts; + typeshare crates/subgraph/src/types/common.rs crates/subgraph/src/types/order.rs crates/subgraph/src/types/vault.rs crates/subgraph/src/types/order_trade.rs crates/common/src/types/order_detail_extended.rs crates/subgraph/src/vol.rs crates/subgraph/src/apy.rs --lang=typescript --output-file=tauri-app/src/lib/typeshare/subgraphTypes.ts; typeshare crates/settings/src/parse.rs --lang=typescript --output-file=tauri-app/src/lib/typeshare/appSettings.ts; typeshare lib/rain.interpreter/crates/eval/src/trace.rs crates/common/src/fuzz/mod.rs crates/settings/src/config_source.rs crates/settings/src/config.rs crates/settings/src/plot_source.rs crates/settings/src/chart.rs crates/settings/src/deployer.rs crates/settings/src/network.rs crates/settings/src/order.rs crates/settings/src/orderbook.rs crates/settings/src/scenario.rs crates/settings/src/blocks.rs crates/settings/src/token.rs crates/settings/src/deployment.rs --lang=typescript --output-file=tauri-app/src/lib/typeshare/config.ts; diff --git a/tauri-app/src-tauri/src/commands/order_take.rs b/tauri-app/src-tauri/src/commands/order_take.rs index 4b47506ec..3b663e43e 100644 --- a/tauri-app/src-tauri/src/commands/order_take.rs +++ b/tauri-app/src-tauri/src/commands/order_take.rs @@ -2,6 +2,7 @@ use crate::error::CommandResult; use rain_orderbook_common::{ csv::TryIntoCsv, subgraph::SubgraphArgs, types::FlattenError, types::OrderTakeFlattened, }; +use rain_orderbook_subgraph_client::apy::{get_order_apy, OrderAPY}; use rain_orderbook_subgraph_client::vol::VaultVolume; use rain_orderbook_subgraph_client::{types::common::*, PaginationArgs}; use std::fs; @@ -79,3 +80,25 @@ pub async fn order_trades_count( .await? .len()) } + +#[tauri::command] +pub async fn order_apy( + order_id: String, + subgraph_args: SubgraphArgs, + start_timestamp: Option, + end_timestamp: Option, +) -> CommandResult { + let client = subgraph_args.to_subgraph_client().await?; + let order = client.order_detail(order_id.clone().into()).await?; + let trades = subgraph_args + .to_subgraph_client() + .await? + .order_trades_list_all(order_id.into(), start_timestamp, end_timestamp) + .await?; + Ok(get_order_apy( + order, + &trades, + start_timestamp, + end_timestamp, + )?) +} diff --git a/tauri-app/src-tauri/src/main.rs b/tauri-app/src-tauri/src/main.rs index bb237152b..f5b6dc916 100644 --- a/tauri-app/src-tauri/src/main.rs +++ b/tauri-app/src-tauri/src/main.rs @@ -19,7 +19,8 @@ use commands::order::{ }; use commands::order_quote::{batch_order_quotes, debug_order_quote}; use commands::order_take::{ - order_trades_count, order_trades_list, order_trades_list_write_csv, order_vaults_volume, + order_apy, order_trades_count, order_trades_list, order_trades_list_write_csv, + order_vaults_volume, }; use commands::trade_debug::debug_trade; use commands::vault::{ @@ -82,7 +83,8 @@ fn run_tauri_app() { get_app_commit_sha, validate_raindex_version, order_vaults_volume, - order_trades_count + order_trades_count, + order_apy, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/tauri-app/src/lib/components/charts/APYTimeFilters.svelte b/tauri-app/src/lib/components/charts/APYTimeFilters.svelte new file mode 100644 index 000000000..1098182ad --- /dev/null +++ b/tauri-app/src/lib/components/charts/APYTimeFilters.svelte @@ -0,0 +1,53 @@ + + + + { + setNow(); + timeDelta = undefined; + startTimestamp = undefined; + endTimestamp = undefined; + }} + active={timeDelta === undefined} + size="xs" + class="px-2 py-1">All Time + { + setNow(); + timeDelta = TIME_DELTA_1_YEAR; + startTimestamp = now - TIME_DELTA_1_YEAR; + endTimestamp = now; + }} + active={timeDelta === TIME_DELTA_1_YEAR} + size="xs" + class="px-2 py-1">1 Year + { + setNow(); + timeDelta = TIME_DELTA_1_MONTH; + startTimestamp = now - TIME_DELTA_1_MONTH; + endTimestamp = now; + }} + active={timeDelta === TIME_DELTA_1_MONTH} + size="xs" + class="px-2 py-1">1 Month + diff --git a/tauri-app/src/lib/components/detail/OrderDetail.svelte b/tauri-app/src/lib/components/detail/OrderDetail.svelte index 7270e9f43..5cf625ed5 100644 --- a/tauri-app/src/lib/components/detail/OrderDetail.svelte +++ b/tauri-app/src/lib/components/detail/OrderDetail.svelte @@ -20,6 +20,7 @@ import { onDestroy } from 'svelte'; import { queryClient } from '$lib/queries/queryClient'; import OrderVaultsVolTable from '../tables/OrderVaultsVolTable.svelte'; + import OrderApy from '../tables/OrderAPY.svelte'; export let id: string; @@ -143,6 +144,9 @@ + + + diff --git a/tauri-app/src/lib/components/tables/OrderAPY.svelte b/tauri-app/src/lib/components/tables/OrderAPY.svelte new file mode 100644 index 000000000..82b1b8eaf --- /dev/null +++ b/tauri-app/src/lib/components/tables/OrderAPY.svelte @@ -0,0 +1,37 @@ + + + + + + + + APY + + + + + {item.apy ? item.apy.apy : 0} % + + + diff --git a/tauri-app/src/lib/components/tables/OrderAPY.test.ts b/tauri-app/src/lib/components/tables/OrderAPY.test.ts new file mode 100644 index 000000000..1db066939 --- /dev/null +++ b/tauri-app/src/lib/components/tables/OrderAPY.test.ts @@ -0,0 +1,79 @@ +import { render, screen, waitFor } from '@testing-library/svelte'; +import { test, vi } from 'vitest'; +import { expect } from '$lib/test/matchers'; +import { mockIPC } from '@tauri-apps/api/mocks'; +import type { OrderAPY } from '$lib/typeshare/subgraphTypes'; +import OrderApy from './OrderVaultsVolTable.svelte'; +import { QueryClient } from '@tanstack/svelte-query'; + +vi.mock('$lib/stores/settings', async (importOriginal) => { + const { writable } = await import('svelte/store'); + const { mockSettingsStore } = await import('$lib/mocks/settings'); + + const _activeOrderbook = writable(); + + return { + ...((await importOriginal()) as object), + settings: mockSettingsStore, + subgraphUrl: writable('https://example.com'), + activeOrderbook: { + ..._activeOrderbook, + load: vi.fn(() => _activeOrderbook.set(true)), + }, + }; +}); + +vi.mock('$lib/services/modal', async () => { + return { + handleDepositGenericModal: vi.fn(), + handleDepositModal: vi.fn(), + handleWithdrawModal: vi.fn(), + }; +}); + +const mockOrderApy: OrderAPY[] = [ + { + orderId: '1', + orderHash: '1', + apy: { + apy: 1.2, + token: { + id: 'output_token', + address: 'output_token', + name: 'output_token', + symbol: 'output_token', + decimals: '0', + }, + }, + startTime: 1, + endTime: 2, + inputsTokenVaultApy: [], + outputsTokenVaultApy: [], + }, +]; + +test('renders table with correct data', async () => { + const queryClient = new QueryClient(); + + mockIPC((cmd) => { + if (cmd === 'order_apy') { + return mockOrderApy; + } + }); + + render(OrderApy, { + context: new Map([['$$_queryClient', queryClient]]), + props: { id: '1' }, + }); + + await waitFor(async () => { + // get apy row + const rows = screen.getAllByTestId('apy'); + + // checking + for (let i = 0; i < mockOrderApy.length; i++) { + const display = mockOrderApy[i].apy!.apy; + expect(rows[i]).toHaveTextContent(display.toString()); + } + }); +}); diff --git a/tauri-app/src/lib/queries/keys.ts b/tauri-app/src/lib/queries/keys.ts index ab88165c0..1580e284e 100644 --- a/tauri-app/src/lib/queries/keys.ts +++ b/tauri-app/src/lib/queries/keys.ts @@ -6,3 +6,4 @@ export const QKEY_ORDER = 'order'; export const QKEY_ORDER_TRADES_LIST = 'orderTradesList'; export const QKEY_ORDER_QUOTE = 'orderQuote'; export const QKEY_VAULTS_VOL_LIST = 'orderVaultsVolumeList'; +export const QKEY_ORDER_APY = 'orderApy'; diff --git a/tauri-app/src/lib/queries/orderTradesList.ts b/tauri-app/src/lib/queries/orderTradesList.ts index 4272a8dc4..f86ac85dd 100644 --- a/tauri-app/src/lib/queries/orderTradesList.ts +++ b/tauri-app/src/lib/queries/orderTradesList.ts @@ -1,4 +1,4 @@ -import type { Trade, VaultVolume } from '$lib/typeshare/subgraphTypes'; +import type { OrderAPY, Trade, VaultVolume } from '$lib/typeshare/subgraphTypes'; import { invoke } from '@tauri-apps/api'; import { DEFAULT_PAGE_SIZE } from './constants'; import { prepareHistoricalOrderChartData } from '$lib/services/historicalOrderCharts'; @@ -92,3 +92,22 @@ export const orderTradesCount = async ( endTimestamp, } as OrderTradesListArgs); }; + +export const getOrderApy = async ( + id: string, + url: string | undefined, + startTimestamp?: number, + endTimestamp?: number, +) => { + if (!url) { + return []; + } + return [ + await invoke('order_apy', { + orderId: id, + subgraphArgs: { url }, + startTimestamp, + endTimestamp, + } as OrderTradesListArgs), + ]; +}; From 6edab3b8a589d306241781dacec6e82ec4f231a7 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Fri, 18 Oct 2024 05:51:34 +0000 Subject: [PATCH 05/28] update --- crates/subgraph/src/apy.rs | 6 ++++-- tauri-app/src/lib/components/tables/OrderAPY.svelte | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 67d005ab2..b550f8e3b 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -198,7 +198,8 @@ pub fn get_order_apy( let int_apy = i64::try_from( combined_annual_rate_vol .saturating_mul(I256::from_raw(U256::from(10_000))) - .saturating_div(combined_capital), + .checked_div(combined_capital) + .unwrap_or(I256::ZERO), )?; // div by 10_000 to convert to actual float and again by 10_000 to // factor in the anuual rate and then mul by 100 to convert to @@ -306,7 +307,8 @@ pub fn get_token_vaults_apy( let change_ratio = i64::try_from( vol.net_vol .saturating_mul(I256::from_raw(U256::from(10_000))) - .saturating_div(I256::from_raw(starting_capital)), + .checked_div(I256::from_raw(starting_capital)) + .unwrap_or(I256::ZERO), )? as f64; let time_to_year_ratio = ((end - start) as f64) / YEAR as f64; (change_ratio * time_to_year_ratio) / 100f64 diff --git a/tauri-app/src/lib/components/tables/OrderAPY.svelte b/tauri-app/src/lib/components/tables/OrderAPY.svelte index 82b1b8eaf..964c60a3b 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.svelte +++ b/tauri-app/src/lib/components/tables/OrderAPY.svelte @@ -31,7 +31,7 @@ - {item.apy ? item.apy.apy : 0} % + {item.apy?.apy ?? 0} % {item.apy?.token?.symbol ? 'in ' + item.apy.token.symbol : ''} From 7968399f9651601039e69b5df966bced6443eb4f Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sat, 19 Oct 2024 04:50:28 +0000 Subject: [PATCH 06/28] apply requested changes - fix tests --- crates/subgraph/src/apy.rs | 254 +++++++++++------- .../src-tauri/src/commands/order_take.rs | 6 +- .../components/charts/APYTimeFilters.svelte | 18 +- .../components/charts/ChartTimeFilters.svelte | 13 +- .../charts/ChartTimeFilters.test.ts | 11 +- .../components/charts/TableTimeFilters.svelte | 8 +- .../charts/TableTimeFilters.test.ts | 4 +- .../src/lib/components/tables/OrderAPY.svelte | 23 +- .../lib/components/tables/OrderAPY.test.ts | 25 +- tauri-app/src/lib/services/time.ts | 9 + 10 files changed, 237 insertions(+), 134 deletions(-) create mode 100644 tauri-app/src/lib/services/time.ts diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index b550f8e3b..14b944b71 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -7,7 +7,6 @@ use alloy::primitives::{ utils::{format_units, parse_units, ParseUnits, Unit, UnitsError}, I256, U256, }; -use core::f64; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, str::FromStr}; use typeshare::typeshare; @@ -33,16 +32,16 @@ pub struct TokenVaultAPY { pub net_vol: I256, #[typeshare(typescript(type = "string"))] pub capital: U256, - #[typeshare(typescript(type = "number"))] - pub apy: f64, + #[typeshare(typescript(type = "string"))] + pub apy: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[typeshare] pub struct DenominatedAPY { - #[typeshare(typescript(type = "number"))] - pub apy: f64, + #[typeshare(typescript(type = "string"))] + pub apy: I256, pub token: Erc20, } @@ -52,7 +51,7 @@ pub struct DenominatedAPY { pub struct OrderAPY { pub order_id: String, pub order_hash: String, - pub apy: Option, + pub denominated_apy: Option, #[typeshare(typescript(type = "number"))] pub start_time: u64, #[typeshare(typescript(type = "number"))] @@ -70,7 +69,7 @@ struct TokenPair { /// Given an order and its trades and optionally a timeframe, will calculates /// the APY for each of the entire order and for each of its vaults pub fn get_order_apy( - order: Order, + order: &Order, trades: &[Trade], start_timestamp: Option, end_timestamp: Option, @@ -84,7 +83,7 @@ pub fn get_order_apy( end_time: end_timestamp.unwrap_or(chrono::Utc::now().timestamp() as u64), inputs_token_vault_apy: vec![], outputs_token_vault_apy: vec![], - apy: None, + denominated_apy: None, }); } let vols = get_vaults_vol(trades)?; @@ -124,31 +123,40 @@ pub fn get_order_apy( end_time, inputs_token_vault_apy: inputs, outputs_token_vault_apy: outputs, - apy: None, + denominated_apy: None, }; // get pairs ratios let pair_ratio_map = get_pairs_ratio(&order_apy, trades); - // try to calculate all vaults capital and volume denominated into any of + // try to calculate all vaults capital and volume denominated into each of // the order's tokens by checking if there is direct ratio between the tokens, // multi path ratios are ignored currently and results in None for the APY. // if there is a success for any of the denomination tokens, checks if it is - // among the prefered ones, if not continues the process with remaining tokens. + // among the prefered denominations, if not continues the same process with + // remaining order's io tokens. // if none of the successfull calcs fulfills any of the prefered denominations // will end up picking the first one. // if there was no success with any of the order's tokens, simply return None // for the APY. - let mut apy_denominations = vec![]; + let mut full_apy_in_distinct_token_denominations = vec![]; for token in &token_vaults_apy { let mut noway = false; let mut combined_capital = I256::ZERO; let mut combined_annual_rate_vol = I256::ZERO; for token_vault in &token_vaults_apy { - // time to year ratio with 4 point decimals - let annual_rate = I256::from_raw(U256::from( - ((token_vault.end_time - token_vault.start_time) * 10_000) / YEAR, - )); + // time to year ratio + let timeframe = parse_units( + &(token_vault.end_time - token_vault.start_time).to_string(), + 18, + ) + .unwrap() + .get_signed(); + let year = parse_units(&YEAR.to_string(), 18).unwrap().get_signed(); + let annual_rate = timeframe.saturating_mul(one).saturating_div(year); + // let annual_rate = I256::from_raw(U256::from( + // ((token_vault.end_time - token_vault.start_time) * 10_000) / YEAR, + // )); let token_decimals = token_vault .token .decimals @@ -171,7 +179,9 @@ pub fn get_order_apy( // sum up all capitals and vols in one denomination if token_vault.token == token.token { combined_capital += vault_capital; - combined_annual_rate_vol += vault_net_vol.saturating_mul(annual_rate); + combined_annual_rate_vol += vault_net_vol + .saturating_mul(one) + .saturating_div(annual_rate); } else { let pair = TokenPair { input: token.token.clone(), @@ -184,7 +194,8 @@ pub fn get_order_apy( .net_vol .saturating_mul(*ratio) .saturating_div(one) - .saturating_mul(annual_rate); + .saturating_mul(one) + .saturating_div(annual_rate); } else { noway = true; break; @@ -192,44 +203,45 @@ pub fn get_order_apy( } } - // success + // for every success denomination, gather them in an array if !noway { - // by 4 point decimals - let int_apy = i64::try_from( - combined_annual_rate_vol - .saturating_mul(I256::from_raw(U256::from(10_000))) - .checked_div(combined_capital) - .unwrap_or(I256::ZERO), - )?; - // div by 10_000 to convert to actual float and again by 10_000 to - // factor in the anuual rate and then mul by 100 to convert to - // percentage, so equals to div by 1_000_000, - let apy = int_apy as f64 / 1_000_000f64; - let denominated_apy = DenominatedAPY { - apy, - token: token.token.clone(), - }; - // chcek if this token is one of prefered ones and if so return early - // if not continue to next token denomination - for denomination in PREFERED_DENOMINATIONS { - if token + if let Some(apy) = combined_annual_rate_vol + .saturating_mul(one) + .checked_div(combined_capital) + { + full_apy_in_distinct_token_denominations.push(Some(DenominatedAPY { + apy, + token: token.token.clone(), + })); + } + } + } + + // check if this token is one of prefered ones and if so return early + // if not continue to next distinct token denomination and check if that + // satisfies any prefered token + for prefered_token in PREFERED_DENOMINATIONS { + for denominated_apy in full_apy_in_distinct_token_denominations.iter().flatten() { + if denominated_apy + .token + .symbol + .as_ref() + .is_some_and(|sym| sym.to_ascii_lowercase().contains(prefered_token)) + || denominated_apy .token - .symbol + .name .as_ref() - .is_some_and(|sym| sym.to_ascii_lowercase().contains(denomination)) - { - order_apy.apy = Some(denominated_apy.clone()); - return Ok(order_apy); - } + .is_some_and(|name| name.to_ascii_lowercase().contains(prefered_token)) + { + order_apy.denominated_apy = Some(denominated_apy.clone()); + return Ok(order_apy); } - apy_denominations.push(denominated_apy); } } - - // none of the order's tokens fulfilled any of the prefered denominations - // so just pick the first one if there was any success at all - if !apy_denominations.is_empty() { - order_apy.apy = Some(apy_denominations[0].clone()); + // none of the order's distinct tokens denominations matched with any of the + // prefered denominations so just pick the first one if there was any success at all + if !full_apy_in_distinct_token_denominations.is_empty() { + order_apy.denominated_apy = full_apy_in_distinct_token_denominations[0].clone(); } Ok(order_apy) @@ -242,6 +254,7 @@ pub fn get_token_vaults_apy( start_timestamp: Option, end_timestamp: Option, ) -> Result, OrderbookSubgraphClientError> { + let one = I256::from_str(ONE).unwrap(); let mut token_vaults_apy: Vec = vec![]; for vol in vols { // this token vault trades in desc order by timestamp @@ -301,17 +314,18 @@ pub fn get_token_vaults_apy( // this token vault apy let apy = if starting_capital.is_zero() { - 0_f64 + None } else { - // by 4 point decimals - let change_ratio = i64::try_from( - vol.net_vol - .saturating_mul(I256::from_raw(U256::from(10_000))) - .checked_div(I256::from_raw(starting_capital)) - .unwrap_or(I256::ZERO), - )? as f64; - let time_to_year_ratio = ((end - start) as f64) / YEAR as f64; - (change_ratio * time_to_year_ratio) / 100f64 + let change_ratio = vol + .net_vol + .saturating_mul(one) + .saturating_div(I256::from_raw(starting_capital)); + let timeframe = parse_units(&(end - start).to_string(), 18) + .unwrap() + .get_signed(); + let year = parse_units(&YEAR.to_string(), 18).unwrap().get_signed(); + let annual_rate = timeframe.saturating_mul(one).saturating_div(year); + change_ratio.saturating_mul(one).checked_div(annual_rate) }; token_vaults_apy.push(TokenVaultAPY { id: vol.id.clone(), @@ -333,7 +347,19 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap> = HashMap::new(); for input in &order_apy.inputs_token_vault_apy { for output in &order_apy.outputs_token_vault_apy { - if input.token != output.token { + let pair_as_key = TokenPair { + input: input.token.clone(), + output: output.token.clone(), + }; + let reverse_pair_as_key = TokenPair { + input: output.token.clone(), + output: input.token.clone(), + }; + // if not same io token and ratio map doesnt already include them + if input.token != output.token + && !(pair_ratio_map.contains_key(&pair_as_key) + || pair_ratio_map.contains_key(&reverse_pair_as_key)) + { // find this pairs trades from list of order's trades let pair_trades = trades .iter() @@ -344,19 +370,46 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap>(); + let reverse_pair_trades = trades + .iter() + .filter(|v| { + v.output_vault_balance_change.vault.token == input.token + && v.input_vault_balance_change.vault.token == output.token + && v.output_vault_balance_change.vault.vault_id.0 == input.id + && v.input_vault_balance_change.vault.vault_id.0 == output.id + }) + .collect::>(); // calculate the pair ratio (in amount/out amount) - let ratio = if pair_trades.is_empty() { + let ratio = if pair_trades.is_empty() && reverse_pair_trades.is_empty() { None } else { + // pick the latest one between trade and reverese trade + let latest_pair_trade = if let Some(trade) = pair_trades.first() { + let trade_timestamp = u64::from_str(&trade.timestamp.0).unwrap(); + if let Some(reverse_trade) = reverse_pair_trades.first() { + let reverse_trade_timestamp = + u64::from_str(&reverse_trade.timestamp.0).unwrap(); + if trade_timestamp >= reverse_trade_timestamp { + trade + } else { + reverse_trade + } + } else { + trade + } + } else { + reverse_pair_trades.first().unwrap() + }; + // convert input and output amounts to 18 decimals point // and then calculate the pair ratio let input_amount = to_18_decimals( ParseUnits::U256( - U256::from_str(&pair_trades[0].input_vault_balance_change.amount.0) + U256::from_str(&latest_pair_trade.input_vault_balance_change.amount.0) .unwrap(), ), - pair_trades[0] + latest_pair_trade .input_vault_balance_change .vault .token @@ -364,15 +417,16 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap HashMap CommandResult { let client = subgraph_args.to_subgraph_client().await?; let order = client.order_detail(order_id.clone().into()).await?; - let trades = subgraph_args - .to_subgraph_client() - .await? + let trades = client .order_trades_list_all(order_id.into(), start_timestamp, end_timestamp) .await?; Ok(get_order_apy( - order, + &order, &trades, start_timestamp, end_timestamp, diff --git a/tauri-app/src/lib/components/charts/APYTimeFilters.svelte b/tauri-app/src/lib/components/charts/APYTimeFilters.svelte index 1098182ad..103fd0d79 100644 --- a/tauri-app/src/lib/components/charts/APYTimeFilters.svelte +++ b/tauri-app/src/lib/components/charts/APYTimeFilters.svelte @@ -1,15 +1,13 @@ diff --git a/tauri-app/src/lib/components/charts/ChartTimeFilters.test.ts b/tauri-app/src/lib/components/charts/ChartTimeFilters.test.ts index 9efecbe92..acfc1b91a 100644 --- a/tauri-app/src/lib/components/charts/ChartTimeFilters.test.ts +++ b/tauri-app/src/lib/components/charts/ChartTimeFilters.test.ts @@ -2,11 +2,12 @@ import { render, fireEvent, screen } from '@testing-library/svelte'; import { get, writable } from 'svelte/store'; import { test, expect } from 'vitest'; import ChartTimeFiltersTest from './ChartTimeFilters.test.svelte'; - -const TIME_DELTA_24_HOURS = 60 * 60 * 24; -const TIME_DELTA_7_DAYS = TIME_DELTA_24_HOURS * 7; -const TIME_DELTA_30_DAYS = TIME_DELTA_24_HOURS * 30; -const TIME_DELTA_1_YEAR = TIME_DELTA_24_HOURS * 365; +import { + TIME_DELTA_1_YEAR, + TIME_DELTA_24_HOURS, + TIME_DELTA_30_DAYS, + TIME_DELTA_7_DAYS, +} from '$lib/services/time'; test('initial timeDelta is set to 1 year', async () => { const timeDeltaStore = writable(TIME_DELTA_1_YEAR); diff --git a/tauri-app/src/lib/components/charts/TableTimeFilters.svelte b/tauri-app/src/lib/components/charts/TableTimeFilters.svelte index 8439f7355..6ac185bd2 100644 --- a/tauri-app/src/lib/components/charts/TableTimeFilters.svelte +++ b/tauri-app/src/lib/components/charts/TableTimeFilters.svelte @@ -1,15 +1,13 @@ - + @@ -30,8 +41,14 @@ - - {item.apy?.apy ?? 0} % {item.apy?.token?.symbol ? 'in ' + item.apy.token.symbol : ''} + + {item.denominatedApy + ? formatApyToPercentage(item.denominatedApy.apy) + + '% in ' + + (item.denominatedApy.token.symbol ?? + item.denominatedApy.token.name ?? + item.denominatedApy.token.address) + : 'Unavailable APY'} diff --git a/tauri-app/src/lib/components/tables/OrderAPY.test.ts b/tauri-app/src/lib/components/tables/OrderAPY.test.ts index 1db066939..34d13c179 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.test.ts +++ b/tauri-app/src/lib/components/tables/OrderAPY.test.ts @@ -3,8 +3,19 @@ import { test, vi } from 'vitest'; import { expect } from '$lib/test/matchers'; import { mockIPC } from '@tauri-apps/api/mocks'; import type { OrderAPY } from '$lib/typeshare/subgraphTypes'; -import OrderApy from './OrderVaultsVolTable.svelte'; import { QueryClient } from '@tanstack/svelte-query'; +import { formatUnits } from 'viem'; +import OrderApy from './OrderAPY.svelte'; + +function formatApyToPercentage(value: string): string { + let valueString = formatUnits(BigInt(value) * 100n, 18); + const index = valueString.indexOf('.'); + if (index > -1) { + // 5 point decimals to show on UI + valueString = valueString.substring(0, index + 6); + } + return valueString; +} vi.mock('$lib/stores/settings', async (importOriginal) => { const { writable } = await import('svelte/store'); @@ -35,8 +46,8 @@ const mockOrderApy: OrderAPY[] = [ { orderId: '1', orderHash: '1', - apy: { - apy: 1.2, + denominatedApy: { + apy: '1200000000000000000', token: { id: 'output_token', address: 'output_token', @@ -57,7 +68,7 @@ test('renders table with correct data', async () => { mockIPC((cmd) => { if (cmd === 'order_apy') { - return mockOrderApy; + return mockOrderApy[0]; } }); @@ -68,12 +79,12 @@ test('renders table with correct data', async () => { await waitFor(async () => { // get apy row - const rows = screen.getAllByTestId('apy'); + const rows = screen.getAllByTestId('apy-field'); // checking for (let i = 0; i < mockOrderApy.length; i++) { - const display = mockOrderApy[i].apy!.apy; - expect(rows[i]).toHaveTextContent(display.toString()); + const display = formatApyToPercentage(mockOrderApy[i].denominatedApy!.apy); + expect(rows[i]).toHaveTextContent(display); } }); }); diff --git a/tauri-app/src/lib/services/time.ts b/tauri-app/src/lib/services/time.ts new file mode 100644 index 000000000..8ba7f5913 --- /dev/null +++ b/tauri-app/src/lib/services/time.ts @@ -0,0 +1,9 @@ +export const TIME_DELTA_24_HOURS = 60 * 60 * 24; +export const TIME_DELTA_48_HOURS = TIME_DELTA_24_HOURS * 2; +export const TIME_DELTA_7_DAYS = TIME_DELTA_24_HOURS * 7; +export const TIME_DELTA_30_DAYS = TIME_DELTA_24_HOURS * 30; +export const TIME_DELTA_1_YEAR = TIME_DELTA_24_HOURS * 365; + +export function nowTimestamp(): number { + return Math.floor(new Date().getTime() / 1000); +} From 6b27e3e1bd73ee00a422b427d8a2e10c7bb8a7d8 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 20 Oct 2024 03:10:29 +0000 Subject: [PATCH 07/28] Update apy.rs --- crates/subgraph/src/apy.rs | 133 ++++++++++++++++++++----------------- 1 file changed, 72 insertions(+), 61 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 14b944b71..87d2b6769 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -31,7 +31,7 @@ pub struct TokenVaultAPY { #[typeshare(typescript(type = "string"))] pub net_vol: I256, #[typeshare(typescript(type = "string"))] - pub capital: U256, + pub capital: I256, #[typeshare(typescript(type = "string"))] pub apy: Option, } @@ -154,32 +154,13 @@ pub fn get_order_apy( .get_signed(); let year = parse_units(&YEAR.to_string(), 18).unwrap().get_signed(); let annual_rate = timeframe.saturating_mul(one).saturating_div(year); - // let annual_rate = I256::from_raw(U256::from( - // ((token_vault.end_time - token_vault.start_time) * 10_000) / YEAR, - // )); - let token_decimals = token_vault - .token - .decimals - .as_ref() - .map(|v| v.0.as_str()) - .unwrap_or("18"); - - // convert to 18 point decimals - let vault_capital = - to_18_decimals(ParseUnits::U256(token_vault.capital), token_decimals); - let vault_net_vol = - to_18_decimals(ParseUnits::I256(token_vault.net_vol), token_decimals); - if vault_capital.is_err() || vault_net_vol.is_err() { - noway = true; - break; - } - let vault_capital = vault_capital.unwrap().get_signed(); - let vault_net_vol = vault_net_vol.unwrap().get_signed(); - // sum up all capitals and vols in one denomination + // sum up all token vaults' capitals and vols in the current's iteration + // token denomination by using the direct ratio between the tokens if token_vault.token == token.token { - combined_capital += vault_capital; - combined_annual_rate_vol += vault_net_vol + combined_capital += token_vault.capital; + combined_annual_rate_vol += token_vault + .net_vol .saturating_mul(one) .saturating_div(annual_rate); } else { @@ -189,7 +170,10 @@ pub fn get_order_apy( }; // convert to current denomination by the direct pair ratio if exists if let Some(Some(ratio)) = pair_ratio_map.get(&pair) { - combined_capital += vault_capital.saturating_mul(*ratio).saturating_div(one); + combined_capital += token_vault + .capital + .saturating_mul(*ratio) + .saturating_div(one); combined_annual_rate_vol += token_vault .net_vol .saturating_mul(*ratio) @@ -203,7 +187,9 @@ pub fn get_order_apy( } } - // for every success denomination, gather them in an array + // for every success apy calc in a token denomination, gather them in an array + // this means at the end we have all the successful apy calculated in each of + // the order's io tokens in an array. if !noway { if let Some(apy) = combined_annual_rate_vol .saturating_mul(one) @@ -280,7 +266,8 @@ pub fn get_token_vaults_apy( .collect::>()[0]; // vaults starting capital at end of first day of its first ever trade - let starting_capital = if first_day_last_trade + // as 18 point decimals + let vault_balance_change = if first_day_last_trade .input_vault_balance_change .vault .vault_id @@ -288,20 +275,36 @@ pub fn get_token_vaults_apy( == vol.id && first_day_last_trade.input_vault_balance_change.vault.token == vol.token { - U256::from_str( - &first_day_last_trade - .input_vault_balance_change - .new_vault_balance - .0, - )? + &first_day_last_trade.input_vault_balance_change } else { - U256::from_str( - &first_day_last_trade - .output_vault_balance_change - .new_vault_balance - .0, - )? + &first_day_last_trade.output_vault_balance_change }; + let starting_capital = U256::from_str(&vault_balance_change.new_vault_balance.0) + .ok() + .and_then(|amount| { + to_18_decimals( + ParseUnits::U256(amount), + vault_balance_change + .vault + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18"), + ) + .ok() + }); + + // convert net vol to 18 decimals point + let net_vol = to_18_decimals( + ParseUnits::I256(vol.net_vol), + vol.token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18"), + ) + .ok(); // the time range for this token vault let mut start = u64::from_str(&first_trade.timestamp.0)?; @@ -312,29 +315,37 @@ pub fn get_token_vaults_apy( }); let end = end_timestamp.unwrap_or(chrono::Utc::now().timestamp() as u64); - // this token vault apy - let apy = if starting_capital.is_zero() { - None + // this token vault apy in 18 decimals point + let apy = if let Some((starting_capital, net_vol)) = starting_capital.zip(net_vol) { + if starting_capital.is_zero() { + None + } else { + let change_ratio = net_vol + .get_signed() + .saturating_mul(one) + .saturating_div(starting_capital.get_signed()); + let timeframe = parse_units(&(end - start).to_string(), 18) + .unwrap() + .get_signed(); + let year = parse_units(&YEAR.to_string(), 18).unwrap().get_signed(); + let annual_rate = timeframe.saturating_mul(one).saturating_div(year); + change_ratio.saturating_mul(one).checked_div(annual_rate) + } } else { - let change_ratio = vol - .net_vol - .saturating_mul(one) - .saturating_div(I256::from_raw(starting_capital)); - let timeframe = parse_units(&(end - start).to_string(), 18) - .unwrap() - .get_signed(); - let year = parse_units(&YEAR.to_string(), 18).unwrap().get_signed(); - let annual_rate = timeframe.saturating_mul(one).saturating_div(year); - change_ratio.saturating_mul(one).checked_div(annual_rate) + None }; + + // this token vault apy token_vaults_apy.push(TokenVaultAPY { id: vol.id.clone(), token: vol.token.clone(), start_time: start, end_time: end, - net_vol: vol.net_vol, apy, - capital: starting_capital, + net_vol: net_vol.unwrap_or(ParseUnits::I256(I256::ZERO)).get_signed(), + capital: starting_capital + .unwrap_or(ParseUnits::I256(I256::ZERO)) + .get_signed(), }); } @@ -519,7 +530,7 @@ mod test { start_time: 0, end_time: 0, net_vol: I256::ZERO, - capital: U256::ZERO, + capital: I256::ZERO, apy: Some(I256::ZERO), }; let token_vault2 = TokenVaultAPY { @@ -528,7 +539,7 @@ mod test { start_time: 0, end_time: 0, net_vol: I256::ZERO, - capital: U256::ZERO, + capital: I256::ZERO, apy: Some(I256::ZERO), }; let order_apy = OrderAPY { @@ -591,7 +602,7 @@ mod test { start_time: 1, end_time: 10000001, net_vol: I256::from_str("1000000000000000000").unwrap(), - capital: U256::from_str("2000000000000000000").unwrap(), + capital: I256::from_str("2000000000000000000").unwrap(), // (1/2) / (10000001_end - 1_start / 31_536_00_year) apy: Some(I256::from_str("1576800000000000000").unwrap()), }, @@ -601,7 +612,7 @@ mod test { start_time: 1, end_time: 10000001, net_vol: I256::from_str("2000000000000000000").unwrap(), - capital: U256::from_str("2000000000000000000").unwrap(), + capital: I256::from_str("2000000000000000000").unwrap(), // (2/2) / ((10000001_end - 1_start) / 31_536_00_year) apy: Some(I256::from_str("3153600000000000000").unwrap()), }, @@ -622,7 +633,7 @@ mod test { start_time: 1, end_time: 10000001, net_vol: I256::from_str("5000000000000000000").unwrap(), - capital: U256::from_str("2000000000000000000").unwrap(), + capital: I256::from_str("2000000000000000000").unwrap(), apy: Some(I256::from_str("7884000000000000001").unwrap()), }; let token2_apy = TokenVaultAPY { @@ -631,7 +642,7 @@ mod test { start_time: 1, end_time: 10000001, net_vol: I256::from_str("3000000000000000000").unwrap(), - capital: U256::from_str("2000000000000000000").unwrap(), + capital: I256::from_str("2000000000000000000").unwrap(), apy: Some(I256::from_str("4730400000000000000").unwrap()), }; let result = get_order_apy(&order, &trades, Some(1), Some(10000001)).unwrap(); From 04155e8839f5ef40578fab3f11f2da7ed85c9f4b Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 20 Oct 2024 04:58:03 +0000 Subject: [PATCH 08/28] Update apy.rs --- crates/subgraph/src/apy.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 87d2b6769..b2b02fc12 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -377,8 +377,6 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap>(); let reverse_pair_trades = trades @@ -386,8 +384,6 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap>(); From 621a180da8f1648a89f8656c26cedcf50644ea81 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 20 Oct 2024 05:21:06 +0000 Subject: [PATCH 09/28] Update apy.rs --- crates/subgraph/src/apy.rs | 153 ++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 88 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index b2b02fc12..85d82ff67 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -371,97 +371,74 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap>(); - let reverse_pair_trades = trades - .iter() - .filter(|v| { - v.output_vault_balance_change.vault.token == input.token - && v.input_vault_balance_change.vault.token == output.token - }) - .collect::>(); - - // calculate the pair ratio (in amount/out amount) - let ratio = if pair_trades.is_empty() && reverse_pair_trades.is_empty() { - None - } else { - // pick the latest one between trade and reverese trade - let latest_pair_trade = if let Some(trade) = pair_trades.first() { - let trade_timestamp = u64::from_str(&trade.timestamp.0).unwrap(); - if let Some(reverse_trade) = reverse_pair_trades.first() { - let reverse_trade_timestamp = - u64::from_str(&reverse_trade.timestamp.0).unwrap(); - if trade_timestamp >= reverse_trade_timestamp { - trade - } else { - reverse_trade - } - } else { - trade - } - } else { - reverse_pair_trades.first().unwrap() - }; - - // convert input and output amounts to 18 decimals point - // and then calculate the pair ratio - let input_amount = to_18_decimals( - ParseUnits::U256( - U256::from_str(&latest_pair_trade.input_vault_balance_change.amount.0) + .and_then(|latest_trade| { + // convert input and output amounts to 18 decimals point + // and then calculate the pair ratio + let input_amount = to_18_decimals( + ParseUnits::U256( + U256::from_str(&latest_trade.input_vault_balance_change.amount.0) + .unwrap(), + ), + latest_trade + .input_vault_balance_change + .vault + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18"), + ) + .ok(); + let output_amount = to_18_decimals( + ParseUnits::U256( + U256::from_str( + &latest_trade.output_vault_balance_change.amount.0[1..], + ) .unwrap(), - ), - latest_pair_trade - .input_vault_balance_change - .vault - .token - .decimals - .as_ref() - .map(|v| v.0.as_str()) - .unwrap_or("18"), - ) - .ok(); - let output_amount = to_18_decimals( - ParseUnits::U256( - U256::from_str( - &latest_pair_trade.output_vault_balance_change.amount.0[1..], - ) - .unwrap(), - ), - latest_pair_trade - .output_vault_balance_change - .vault - .token - .decimals - .as_ref() - .map(|v| v.0.as_str()) - .unwrap_or("18"), - ) - .ok(); - if let Some((input_amount, output_amount)) = input_amount.zip(output_amount) { - Some([ - // io ratio - input_amount - .get_signed() - .saturating_mul(one) - .checked_div(output_amount.get_signed()) - .unwrap_or(I256::MAX), - // oi ratio - output_amount - .get_signed() - .saturating_mul(one) - .checked_div(input_amount.get_signed()) - .unwrap_or(I256::MAX), - ]) - } else { - None - } - }; + ), + latest_trade + .output_vault_balance_change + .vault + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18"), + ) + .ok(); + input_amount + .zip(output_amount) + .map(|(input_amount, output_amount)| { + [ + // io ratio + input_amount + .get_signed() + .saturating_mul(one) + .checked_div(output_amount.get_signed()) + .unwrap_or(I256::MAX), + // oi ratio + output_amount + .get_signed() + .saturating_mul(one) + .checked_div(input_amount.get_signed()) + .unwrap_or(I256::MAX), + ] + }) + }); + // io pair_ratio_map.insert( TokenPair { From c2804e4209003df46b861a566d25a0e33241b39a Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 20 Oct 2024 05:52:28 +0000 Subject: [PATCH 10/28] Update apy.rs --- crates/subgraph/src/apy.rs | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 85d82ff67..228591c23 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -68,6 +68,8 @@ struct TokenPair { /// Given an order and its trades and optionally a timeframe, will calculates /// the APY for each of the entire order and for each of its vaults +/// Trades must be sorted indesc order by timestamp, this is the case if +/// queried from subgraph using this lib functionalities pub fn get_order_apy( order: &Order, trades: &[Trade], @@ -234,6 +236,8 @@ pub fn get_order_apy( } /// Calculates each token vault apy at the given timeframe +/// Trades must be sorted indesc order by timestamp, this is +/// the case if queried from subgraph using this lib functionalities pub fn get_token_vaults_apy( trades: &[Trade], vols: &[VaultVolume], @@ -353,6 +357,8 @@ pub fn get_token_vaults_apy( } /// Calculates an order's pairs' ratios from their last trades in a given list of trades +/// Trades must be sorted indesc order by timestamp, this is the case if queried from subgraph +/// using this lib functionalities fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap> { let one = I256::from_str(ONE).unwrap(); let mut pair_ratio_map: HashMap> = HashMap::new(); @@ -575,9 +581,9 @@ mod test { start_time: 1, end_time: 10000001, net_vol: I256::from_str("1000000000000000000").unwrap(), - capital: I256::from_str("2000000000000000000").unwrap(), - // (1/2) / (10000001_end - 1_start / 31_536_00_year) - apy: Some(I256::from_str("1576800000000000000").unwrap()), + capital: I256::from_str("5000000000000000000").unwrap(), + // (1/5) / (10000001_end - 1_start / 31_536_00_year) + apy: Some(I256::from_str("630720000000000000").unwrap()), }, TokenVaultAPY { id: vault2.to_string(), @@ -585,9 +591,9 @@ mod test { start_time: 1, end_time: 10000001, net_vol: I256::from_str("2000000000000000000").unwrap(), - capital: I256::from_str("2000000000000000000").unwrap(), - // (2/2) / ((10000001_end - 1_start) / 31_536_00_year) - apy: Some(I256::from_str("3153600000000000000").unwrap()), + capital: I256::from_str("5000000000000000000").unwrap(), + // (2/5) / ((10000001_end - 1_start) / 31_536_00_year) + apy: Some(I256::from_str("1261440000000000000").unwrap()), }, ]; @@ -606,8 +612,8 @@ mod test { start_time: 1, end_time: 10000001, net_vol: I256::from_str("5000000000000000000").unwrap(), - capital: I256::from_str("2000000000000000000").unwrap(), - apy: Some(I256::from_str("7884000000000000001").unwrap()), + capital: I256::from_str("5000000000000000000").unwrap(), + apy: Some(I256::from_str("3153600000000000000").unwrap()), }; let token2_apy = TokenVaultAPY { id: vault2.to_string(), @@ -615,8 +621,8 @@ mod test { start_time: 1, end_time: 10000001, net_vol: I256::from_str("3000000000000000000").unwrap(), - capital: I256::from_str("2000000000000000000").unwrap(), - apy: Some(I256::from_str("4730400000000000000").unwrap()), + capital: I256::from_str("5000000000000000000").unwrap(), + apy: Some(I256::from_str("1892160000000000000").unwrap()), }; let result = get_order_apy(&order, &trades, Some(1), Some(10000001)).unwrap(); let expected = OrderAPY { @@ -624,11 +630,11 @@ mod test { order_hash: "".to_string(), start_time: 1, end_time: 10000001, - inputs_token_vault_apy: vec![token2_apy.clone(), token1_apy.clone()], - outputs_token_vault_apy: vec![token2_apy.clone(), token1_apy.clone()], + inputs_token_vault_apy: vec![token1_apy.clone(), token2_apy.clone()], + outputs_token_vault_apy: vec![token1_apy.clone(), token2_apy.clone()], denominated_apy: Some(DenominatedAPY { - apy: I256::from_str("7183200000000000000").unwrap(), - token: token2, + apy: I256::from_str("2172480000000000000").unwrap(), + token: token1, }), }; @@ -828,6 +834,6 @@ mod test { orderbook: Orderbook { id: bytes.clone() }, }, }; - vec![trade1, trade2] + vec![trade2, trade1] } } From 4c690024bd188740c1fba6da5ab1409c285d74e2 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 20 Oct 2024 06:10:14 +0000 Subject: [PATCH 11/28] Update apy.rs --- crates/subgraph/src/apy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 228591c23..0b47c7dd8 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -468,7 +468,7 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap>( amount: ParseUnits, decimals: T, From b88b0c29acfbf58fc56de4777e5fd8204a3a1e91 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 20 Oct 2024 06:15:19 +0000 Subject: [PATCH 12/28] Update apy.rs --- crates/subgraph/src/apy.rs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 0b47c7dd8..a0d570ded 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -76,7 +76,6 @@ pub fn get_order_apy( start_timestamp: Option, end_timestamp: Option, ) -> Result { - let one = I256::from_str(ONE).unwrap(); if trades.is_empty() { return Ok(OrderAPY { order_id: order.id.0.clone(), @@ -155,7 +154,7 @@ pub fn get_order_apy( .unwrap() .get_signed(); let year = parse_units(&YEAR.to_string(), 18).unwrap().get_signed(); - let annual_rate = timeframe.saturating_mul(one).saturating_div(year); + let annual_rate = timeframe.saturating_mul(one()).saturating_div(year); // sum up all token vaults' capitals and vols in the current's iteration // token denomination by using the direct ratio between the tokens @@ -163,7 +162,7 @@ pub fn get_order_apy( combined_capital += token_vault.capital; combined_annual_rate_vol += token_vault .net_vol - .saturating_mul(one) + .saturating_mul(one()) .saturating_div(annual_rate); } else { let pair = TokenPair { @@ -175,12 +174,12 @@ pub fn get_order_apy( combined_capital += token_vault .capital .saturating_mul(*ratio) - .saturating_div(one); + .saturating_div(one()); combined_annual_rate_vol += token_vault .net_vol .saturating_mul(*ratio) - .saturating_div(one) - .saturating_mul(one) + .saturating_div(one()) + .saturating_mul(one()) .saturating_div(annual_rate); } else { noway = true; @@ -194,7 +193,7 @@ pub fn get_order_apy( // the order's io tokens in an array. if !noway { if let Some(apy) = combined_annual_rate_vol - .saturating_mul(one) + .saturating_mul(one()) .checked_div(combined_capital) { full_apy_in_distinct_token_denominations.push(Some(DenominatedAPY { @@ -244,7 +243,6 @@ pub fn get_token_vaults_apy( start_timestamp: Option, end_timestamp: Option, ) -> Result, OrderbookSubgraphClientError> { - let one = I256::from_str(ONE).unwrap(); let mut token_vaults_apy: Vec = vec![]; for vol in vols { // this token vault trades in desc order by timestamp @@ -326,14 +324,14 @@ pub fn get_token_vaults_apy( } else { let change_ratio = net_vol .get_signed() - .saturating_mul(one) + .saturating_mul(one()) .saturating_div(starting_capital.get_signed()); let timeframe = parse_units(&(end - start).to_string(), 18) .unwrap() .get_signed(); let year = parse_units(&YEAR.to_string(), 18).unwrap().get_signed(); - let annual_rate = timeframe.saturating_mul(one).saturating_div(year); - change_ratio.saturating_mul(one).checked_div(annual_rate) + let annual_rate = timeframe.saturating_mul(one()).saturating_div(year); + change_ratio.saturating_mul(one()).checked_div(annual_rate) } } else { None @@ -360,7 +358,6 @@ pub fn get_token_vaults_apy( /// Trades must be sorted indesc order by timestamp, this is the case if queried from subgraph /// using this lib functionalities fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap> { - let one = I256::from_str(ONE).unwrap(); let mut pair_ratio_map: HashMap> = HashMap::new(); for input in &order_apy.inputs_token_vault_apy { for output in &order_apy.outputs_token_vault_apy { @@ -432,13 +429,13 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap>( parse_units(&format_units(amount, decimals)?, 18) } +/// Returns 18 point decimals 1 as I256 +fn one() -> I256 { + I256::from_str(ONE).unwrap() +} + #[cfg(test)] mod test { use super::*; From 59a2a3905f0300f7e8196e811bfebc1226431c2d Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 20 Oct 2024 06:18:07 +0000 Subject: [PATCH 13/28] Update apy.rs --- crates/subgraph/src/apy.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index a0d570ded..deccc2d0f 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -153,8 +153,7 @@ pub fn get_order_apy( ) .unwrap() .get_signed(); - let year = parse_units(&YEAR.to_string(), 18).unwrap().get_signed(); - let annual_rate = timeframe.saturating_mul(one()).saturating_div(year); + let annual_rate = timeframe.saturating_mul(one()).saturating_div(year()); // sum up all token vaults' capitals and vols in the current's iteration // token denomination by using the direct ratio between the tokens @@ -329,8 +328,7 @@ pub fn get_token_vaults_apy( let timeframe = parse_units(&(end - start).to_string(), 18) .unwrap() .get_signed(); - let year = parse_units(&YEAR.to_string(), 18).unwrap().get_signed(); - let annual_rate = timeframe.saturating_mul(one()).saturating_div(year); + let annual_rate = timeframe.saturating_mul(one()).saturating_div(year()); change_ratio.saturating_mul(one()).checked_div(annual_rate) } } else { @@ -478,6 +476,11 @@ fn one() -> I256 { I256::from_str(ONE).unwrap() } +/// Returns YEAR as 18 point decimals as I256 +fn year() -> I256 { + parse_units(&YEAR.to_string(), 18).unwrap().get_signed() +} + #[cfg(test)] mod test { use super::*; From f509d868355840c2ac2f39435f385fb9266c05e8 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 20 Oct 2024 06:25:40 +0000 Subject: [PATCH 14/28] Update apy.rs --- crates/subgraph/src/apy.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index deccc2d0f..daad0e811 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -147,13 +147,7 @@ pub fn get_order_apy( let mut combined_annual_rate_vol = I256::ZERO; for token_vault in &token_vaults_apy { // time to year ratio - let timeframe = parse_units( - &(token_vault.end_time - token_vault.start_time).to_string(), - 18, - ) - .unwrap() - .get_signed(); - let annual_rate = timeframe.saturating_mul(one()).saturating_div(year()); + let annual_rate = annual_rate(token_vault.start_time, token_vault.end_time); // sum up all token vaults' capitals and vols in the current's iteration // token denomination by using the direct ratio between the tokens @@ -325,11 +319,9 @@ pub fn get_token_vaults_apy( .get_signed() .saturating_mul(one()) .saturating_div(starting_capital.get_signed()); - let timeframe = parse_units(&(end - start).to_string(), 18) - .unwrap() - .get_signed(); - let annual_rate = timeframe.saturating_mul(one()).saturating_div(year()); - change_ratio.saturating_mul(one()).checked_div(annual_rate) + change_ratio + .saturating_mul(one()) + .checked_div(annual_rate(start, end)) } } else { None @@ -481,6 +473,14 @@ fn year() -> I256 { parse_units(&YEAR.to_string(), 18).unwrap().get_signed() } +/// Returns annual rate as 18 point decimals as I256 +fn annual_rate(start: u64, end: u64) -> I256 { + let timeframe = parse_units(&(end - start).to_string(), 18) + .unwrap() + .get_signed(); + timeframe.saturating_mul(one()).saturating_div(year()) +} + #[cfg(test)] mod test { use super::*; From 06cd19a54149e51bcaaf743a3859430cf37d9c1d Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 20 Oct 2024 06:44:42 +0000 Subject: [PATCH 15/28] Update apy.rs --- crates/subgraph/src/apy.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index daad0e811..bd65dc535 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -311,21 +311,20 @@ pub fn get_token_vaults_apy( let end = end_timestamp.unwrap_or(chrono::Utc::now().timestamp() as u64); // this token vault apy in 18 decimals point - let apy = if let Some((starting_capital, net_vol)) = starting_capital.zip(net_vol) { - if starting_capital.is_zero() { - None - } else { - let change_ratio = net_vol - .get_signed() - .saturating_mul(one()) - .saturating_div(starting_capital.get_signed()); - change_ratio - .saturating_mul(one()) - .checked_div(annual_rate(start, end)) - } - } else { - None - }; + let apy = starting_capital + .zip(net_vol) + .and_then(|(starting_capital, net_vol)| { + (!starting_capital.is_zero()) + .then_some( + net_vol + .get_signed() + .saturating_mul(one()) + .saturating_div(starting_capital.get_signed()) + .saturating_mul(one()) + .checked_div(annual_rate(start, end)), + ) + .flatten() + }); // this token vault apy token_vaults_apy.push(TokenVaultAPY { From 8fe210d4433c71ef10b1a38ed7f7a903f89b3e58 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 20 Oct 2024 06:50:13 +0000 Subject: [PATCH 16/28] Update apy.rs --- crates/subgraph/src/apy.rs | 72 +++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index bd65dc535..ec7c304ea 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -379,7 +379,7 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap HashMap Date: Sun, 20 Oct 2024 06:51:58 +0000 Subject: [PATCH 17/28] Update apy.rs --- crates/subgraph/src/apy.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index ec7c304ea..8ba9e3b43 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -474,10 +474,11 @@ fn year() -> I256 { /// Returns annual rate as 18 point decimals as I256 fn annual_rate(start: u64, end: u64) -> I256 { - let timeframe = parse_units(&(end - start).to_string(), 18) + parse_units(&(end - start).to_string(), 18) .unwrap() - .get_signed(); - timeframe.saturating_mul(one()).saturating_div(year()) + .get_signed() + .saturating_mul(one()) + .saturating_div(year()) } #[cfg(test)] From 931bf381b5a5d53a0446cff121e5074d03352ded Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 20 Oct 2024 06:57:26 +0000 Subject: [PATCH 18/28] Update apy.rs --- crates/subgraph/src/apy.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 8ba9e3b43..88bd24805 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -432,21 +432,9 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap Date: Mon, 21 Oct 2024 05:42:57 +0000 Subject: [PATCH 19/28] Update apy.rs --- crates/subgraph/src/apy.rs | 69 +++++++++++++++----------------------- 1 file changed, 27 insertions(+), 42 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 88bd24805..b93495b4a 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -8,15 +8,15 @@ use alloy::primitives::{ I256, U256, }; use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, str::FromStr}; +use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, +}; use typeshare::typeshare; pub const ONE: &str = "1000000000000000000"; pub const DAY: u64 = 60 * 60 * 24; pub const YEAR: u64 = DAY * 365; -pub const PREFERED_DENOMINATIONS: [&str; 11] = [ - "usdt", "usdc", "dai", "frax", "mim", "usdp", "weth", "wbtc", "wpol", "wmatic", "wbnb", -]; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -133,17 +133,15 @@ pub fn get_order_apy( // try to calculate all vaults capital and volume denominated into each of // the order's tokens by checking if there is direct ratio between the tokens, // multi path ratios are ignored currently and results in None for the APY. - // if there is a success for any of the denomination tokens, checks if it is - // among the prefered denominations, if not continues the same process with - // remaining order's io tokens. - // if none of the successfull calcs fulfills any of the prefered denominations - // will end up picking the first one. + // if there is a success for any of the denomination tokens, gather it in order + // of its net vol in a BTreeMap and lastly pick the one with highest net vol. // if there was no success with any of the order's tokens, simply return None // for the APY. - let mut full_apy_in_distinct_token_denominations = vec![]; + let mut full_apy_in_distinct_token_denominations = BTreeMap::new(); for token in &token_vaults_apy { let mut noway = false; let mut combined_capital = I256::ZERO; + let mut combined_net_vol = I256::ZERO; let mut combined_annual_rate_vol = I256::ZERO; for token_vault in &token_vaults_apy { // time to year ratio @@ -153,6 +151,7 @@ pub fn get_order_apy( // token denomination by using the direct ratio between the tokens if token_vault.token == token.token { combined_capital += token_vault.capital; + combined_net_vol += token_vault.net_vol; combined_annual_rate_vol += token_vault .net_vol .saturating_mul(one()) @@ -168,6 +167,10 @@ pub fn get_order_apy( .capital .saturating_mul(*ratio) .saturating_div(one()); + combined_net_vol += token_vault + .net_vol + .saturating_mul(*ratio) + .saturating_div(one()); combined_annual_rate_vol += token_vault .net_vol .saturating_mul(*ratio) @@ -181,48 +184,30 @@ pub fn get_order_apy( } } - // for every success apy calc in a token denomination, gather them in an array + // for every success apy calc in a token denomination, gather them in BTreeMap // this means at the end we have all the successful apy calculated in each of - // the order's io tokens in an array. + // the order's io tokens in order from highest to lowest. if !noway { if let Some(apy) = combined_annual_rate_vol .saturating_mul(one()) .checked_div(combined_capital) { - full_apy_in_distinct_token_denominations.push(Some(DenominatedAPY { - apy, - token: token.token.clone(), - })); + full_apy_in_distinct_token_denominations.insert( + combined_net_vol, + DenominatedAPY { + apy, + token: token.token.clone(), + }, + ); } } } - // check if this token is one of prefered ones and if so return early - // if not continue to next distinct token denomination and check if that - // satisfies any prefered token - for prefered_token in PREFERED_DENOMINATIONS { - for denominated_apy in full_apy_in_distinct_token_denominations.iter().flatten() { - if denominated_apy - .token - .symbol - .as_ref() - .is_some_and(|sym| sym.to_ascii_lowercase().contains(prefered_token)) - || denominated_apy - .token - .name - .as_ref() - .is_some_and(|name| name.to_ascii_lowercase().contains(prefered_token)) - { - order_apy.denominated_apy = Some(denominated_apy.clone()); - return Ok(order_apy); - } - } - } - // none of the order's distinct tokens denominations matched with any of the - // prefered denominations so just pick the first one if there was any success at all - if !full_apy_in_distinct_token_denominations.is_empty() { - order_apy.denominated_apy = full_apy_in_distinct_token_denominations[0].clone(); - } + // pick the denomination with highest net vol + order_apy.denominated_apy = full_apy_in_distinct_token_denominations + .last_key_value() + .map(|(_k, v)| v) + .cloned(); Ok(order_apy) } From 36a34f749afc5c0aaf7c83b0997b3e690e0c5c61 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 21 Oct 2024 18:14:42 +0000 Subject: [PATCH 20/28] fix pick denomination --- crates/subgraph/src/apy.rs | 53 +++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index b93495b4a..c1ace0774 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -134,15 +134,16 @@ pub fn get_order_apy( // the order's tokens by checking if there is direct ratio between the tokens, // multi path ratios are ignored currently and results in None for the APY. // if there is a success for any of the denomination tokens, gather it in order - // of its net vol in a BTreeMap and lastly pick the one with highest net vol. + // of its net vol and pick the one with highest net vol. // if there was no success with any of the order's tokens, simply return None // for the APY. - let mut full_apy_in_distinct_token_denominations = BTreeMap::new(); + let mut token_net_vol_map = BTreeMap::new(); + let mut full_apy_in_distinct_token_denominations = vec![]; for token in &token_vaults_apy { let mut noway = false; let mut combined_capital = I256::ZERO; - let mut combined_net_vol = I256::ZERO; let mut combined_annual_rate_vol = I256::ZERO; + let mut current_token_net_vol_map = BTreeMap::new(); for token_vault in &token_vaults_apy { // time to year ratio let annual_rate = annual_rate(token_vault.start_time, token_vault.end_time); @@ -151,11 +152,11 @@ pub fn get_order_apy( // token denomination by using the direct ratio between the tokens if token_vault.token == token.token { combined_capital += token_vault.capital; - combined_net_vol += token_vault.net_vol; combined_annual_rate_vol += token_vault .net_vol .saturating_mul(one()) .saturating_div(annual_rate); + current_token_net_vol_map.insert(token_vault.net_vol, &token.token); } else { let pair = TokenPair { input: token.token.clone(), @@ -167,16 +168,19 @@ pub fn get_order_apy( .capital .saturating_mul(*ratio) .saturating_div(one()); - combined_net_vol += token_vault - .net_vol - .saturating_mul(*ratio) - .saturating_div(one()); combined_annual_rate_vol += token_vault .net_vol .saturating_mul(*ratio) .saturating_div(one()) .saturating_mul(one()) .saturating_div(annual_rate); + current_token_net_vol_map.insert( + token_vault + .net_vol + .saturating_mul(*ratio) + .saturating_div(one()), + &token_vault.token, + ); } else { noway = true; break; @@ -192,22 +196,29 @@ pub fn get_order_apy( .saturating_mul(one()) .checked_div(combined_capital) { - full_apy_in_distinct_token_denominations.insert( - combined_net_vol, - DenominatedAPY { - apy, - token: token.token.clone(), - }, - ); + full_apy_in_distinct_token_denominations.push(DenominatedAPY { + apy, + token: token.token.clone(), + }); } + } else { + current_token_net_vol_map.clear(); + } + if token_net_vol_map.is_empty() { + token_net_vol_map.extend(current_token_net_vol_map); } } // pick the denomination with highest net vol - order_apy.denominated_apy = full_apy_in_distinct_token_denominations - .last_key_value() - .map(|(_k, v)| v) - .cloned(); + for (_, token) in token_net_vol_map.iter().rev() { + if let Some(denominated_apy) = full_apy_in_distinct_token_denominations + .iter() + .find(|v| &&v.token == token) + { + order_apy.denominated_apy = Some(denominated_apy.clone()); + break; + } + } Ok(order_apy) } @@ -611,8 +622,8 @@ mod test { inputs_token_vault_apy: vec![token1_apy.clone(), token2_apy.clone()], outputs_token_vault_apy: vec![token1_apy.clone(), token2_apy.clone()], denominated_apy: Some(DenominatedAPY { - apy: I256::from_str("2172480000000000000").unwrap(), - token: token1, + apy: I256::from_str("2172479999999999999").unwrap(), + token: token2, }), }; From d4883d705f05d8f044d09e39e01992b68679fe43 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 21 Oct 2024 18:44:42 +0000 Subject: [PATCH 21/28] update [skip ci] --- crates/subgraph/src/apy.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index c1ace0774..de68c7c28 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -137,7 +137,7 @@ pub fn get_order_apy( // of its net vol and pick the one with highest net vol. // if there was no success with any of the order's tokens, simply return None // for the APY. - let mut token_net_vol_map = BTreeMap::new(); + let mut tokens_net_vol_map = BTreeMap::new(); let mut full_apy_in_distinct_token_denominations = vec![]; for token in &token_vaults_apy { let mut noway = false; @@ -204,13 +204,14 @@ pub fn get_order_apy( } else { current_token_net_vol_map.clear(); } - if token_net_vol_map.is_empty() { - token_net_vol_map.extend(current_token_net_vol_map); + + if tokens_net_vol_map.is_empty() { + tokens_net_vol_map.extend(current_token_net_vol_map); } } // pick the denomination with highest net vol - for (_, token) in token_net_vol_map.iter().rev() { + for (_, token) in tokens_net_vol_map.iter().rev() { if let Some(denominated_apy) = full_apy_in_distinct_token_denominations .iter() .find(|v| &&v.token == token) From b659f5b98ad8c4de14eced9cc3b885465b2d8486 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Tue, 22 Oct 2024 02:18:11 +0000 Subject: [PATCH 22/28] update --- crates/subgraph/src/apy.rs | 90 +++++++++++--------------------- crates/subgraph/src/utils/mod.rs | 63 ++++++++++++++++++++++ 2 files changed, 94 insertions(+), 59 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index de68c7c28..8dd0628e4 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -1,10 +1,11 @@ use crate::{ types::common::{Erc20, Order, Trade}, + utils::{one_18, to_18_decimals, year_18, DAY}, vol::{get_vaults_vol, VaultVolume}, OrderbookSubgraphClientError, }; use alloy::primitives::{ - utils::{format_units, parse_units, ParseUnits, Unit, UnitsError}, + utils::{parse_units, ParseUnits}, I256, U256, }; use serde::{Deserialize, Serialize}; @@ -14,10 +15,6 @@ use std::{ }; use typeshare::typeshare; -pub const ONE: &str = "1000000000000000000"; -pub const DAY: u64 = 60 * 60 * 24; -pub const YEAR: u64 = DAY * 365; - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[typeshare] @@ -137,13 +134,13 @@ pub fn get_order_apy( // of its net vol and pick the one with highest net vol. // if there was no success with any of the order's tokens, simply return None // for the APY. - let mut tokens_net_vol_map = BTreeMap::new(); + let mut ordered_token_net_vol_map = BTreeMap::new(); let mut full_apy_in_distinct_token_denominations = vec![]; for token in &token_vaults_apy { let mut noway = false; let mut combined_capital = I256::ZERO; let mut combined_annual_rate_vol = I256::ZERO; - let mut current_token_net_vol_map = BTreeMap::new(); + let mut token_net_vol_map_converted_in_current_denomination = BTreeMap::new(); for token_vault in &token_vaults_apy { // time to year ratio let annual_rate = annual_rate(token_vault.start_time, token_vault.end_time); @@ -154,9 +151,10 @@ pub fn get_order_apy( combined_capital += token_vault.capital; combined_annual_rate_vol += token_vault .net_vol - .saturating_mul(one()) + .saturating_mul(one_18().get_signed()) .saturating_div(annual_rate); - current_token_net_vol_map.insert(token_vault.net_vol, &token.token); + token_net_vol_map_converted_in_current_denomination + .insert(token_vault.net_vol, &token.token); } else { let pair = TokenPair { input: token.token.clone(), @@ -167,18 +165,18 @@ pub fn get_order_apy( combined_capital += token_vault .capital .saturating_mul(*ratio) - .saturating_div(one()); + .saturating_div(one_18().get_signed()); combined_annual_rate_vol += token_vault .net_vol .saturating_mul(*ratio) - .saturating_div(one()) - .saturating_mul(one()) + .saturating_div(one_18().get_signed()) + .saturating_mul(one_18().get_signed()) .saturating_div(annual_rate); - current_token_net_vol_map.insert( + token_net_vol_map_converted_in_current_denomination.insert( token_vault .net_vol .saturating_mul(*ratio) - .saturating_div(one()), + .saturating_div(one_18().get_signed()), &token_vault.token, ); } else { @@ -193,7 +191,7 @@ pub fn get_order_apy( // the order's io tokens in order from highest to lowest. if !noway { if let Some(apy) = combined_annual_rate_vol - .saturating_mul(one()) + .saturating_mul(one_18().get_signed()) .checked_div(combined_capital) { full_apy_in_distinct_token_denominations.push(DenominatedAPY { @@ -202,22 +200,27 @@ pub fn get_order_apy( }); } } else { - current_token_net_vol_map.clear(); + token_net_vol_map_converted_in_current_denomination.clear(); } - if tokens_net_vol_map.is_empty() { - tokens_net_vol_map.extend(current_token_net_vol_map); + // if we already have ordered token net vol in a denomination + // we dont need them in other denominations in order to pick + // the highest vol token as settelement denomination + if ordered_token_net_vol_map.is_empty() { + ordered_token_net_vol_map.extend(token_net_vol_map_converted_in_current_denomination); } } - // pick the denomination with highest net vol - for (_, token) in tokens_net_vol_map.iter().rev() { + // pick the denomination with highest net vol by iterating over tokens with + // highest vol to lowest and pick the first matching matching one + for (_, &token) in ordered_token_net_vol_map.iter().rev() { if let Some(denominated_apy) = full_apy_in_distinct_token_denominations .iter() - .find(|v| &&v.token == token) + .find(|&v| &v.token == token) { order_apy.denominated_apy = Some(denominated_apy.clone()); - break; + // return early as soon as a match is found + return Ok(order_apy); } } @@ -315,9 +318,9 @@ pub fn get_token_vaults_apy( .then_some( net_vol .get_signed() - .saturating_mul(one()) + .saturating_mul(one_18().get_signed()) .saturating_div(starting_capital.get_signed()) - .saturating_mul(one()) + .saturating_mul(one_18().get_signed()) .checked_div(annual_rate(start, end)), ) .flatten() @@ -415,13 +418,13 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap HashMap>( - amount: ParseUnits, - decimals: T, -) -> Result { - parse_units(&format_units(amount, decimals)?, 18) -} - -/// Returns 18 point decimals 1 as I256 -fn one() -> I256 { - I256::from_str(ONE).unwrap() -} - -/// Returns YEAR as 18 point decimals as I256 -fn year() -> I256 { - parse_units(&YEAR.to_string(), 18).unwrap().get_signed() -} - /// Returns annual rate as 18 point decimals as I256 fn annual_rate(start: u64, end: u64) -> I256 { parse_units(&(end - start).to_string(), 18) .unwrap() .get_signed() - .saturating_mul(one()) - .saturating_div(year()) + .saturating_mul(one_18().get_signed()) + .saturating_div(year_18().get_signed()) } #[cfg(test)] @@ -475,19 +460,6 @@ mod test { }; use alloy::primitives::{Address, B256}; - #[test] - fn test_to_18_decimals() { - let value = ParseUnits::I256(I256::from_str("-123456789").unwrap()); - let result = to_18_decimals(value, 5).unwrap(); - let expected = ParseUnits::I256(I256::from_str("-1234567890000000000000").unwrap()); - assert_eq!(result, expected); - - let value = ParseUnits::U256(U256::from_str("123456789").unwrap()); - let result = to_18_decimals(value, 12).unwrap(); - let expected = ParseUnits::U256(U256::from_str("123456789000000").unwrap()); - assert_eq!(result, expected); - } - #[test] fn test_get_pairs_ratio() { let trades = get_trades(); diff --git a/crates/subgraph/src/utils/mod.rs b/crates/subgraph/src/utils/mod.rs index 145715232..5aedc70a4 100644 --- a/crates/subgraph/src/utils/mod.rs +++ b/crates/subgraph/src/utils/mod.rs @@ -1,5 +1,68 @@ +use alloy::primitives::utils::{format_units, parse_units, ParseUnits, Unit, UnitsError}; + mod order_id; mod slice_list; pub use order_id::*; pub use slice_list::*; + +pub const DAY: u64 = 60 * 60 * 24; +pub const YEAR: u64 = DAY * 365; + +/// Returns 18 point decimals 1 as I256/U256 +pub fn one_18() -> ParseUnits { + parse_units("1", 18).unwrap() +} + +/// Returns YEAR as 18 point decimals as I256/U256 +pub fn year_18() -> ParseUnits { + parse_units(&YEAR.to_string(), 18).unwrap() +} + +/// Converts a U256/I256 value to a 18 fixed point U256/I256 given the decimals point +pub fn to_18_decimals>( + amount: ParseUnits, + decimals: T, +) -> Result { + parse_units(&format_units(amount, decimals)?, 18) +} + +#[cfg(test)] +mod test { + use super::*; + use alloy::primitives::{I256, U256}; + use std::str::FromStr; + + #[test] + fn test_one() { + let result = one_18(); + let expected_signed = I256::from_str("1_000_000_000_000_000_000").unwrap(); + let expected_absolute = U256::from_str("1_000_000_000_000_000_000").unwrap(); + assert_eq!(result.get_signed(), expected_signed); + assert_eq!(result.get_absolute(), expected_absolute); + } + + #[test] + fn test_year_18_decimals() { + let result = year_18(); + let expected_signed = I256::try_from(YEAR) + .unwrap() + .saturating_mul(one_18().get_signed()); + let expected_absolute = U256::from(YEAR).saturating_mul(one_18().get_absolute()); + assert_eq!(result.get_signed(), expected_signed); + assert_eq!(result.get_absolute(), expected_absolute); + } + + #[test] + fn test_to_18_decimals() { + let value = ParseUnits::I256(I256::from_str("-123456789").unwrap()); + let result = to_18_decimals(value, 5).unwrap(); + let expected = ParseUnits::I256(I256::from_str("-1234567890000000000000").unwrap()); + assert_eq!(result, expected); + + let value = ParseUnits::U256(U256::from_str("123456789").unwrap()); + let result = to_18_decimals(value, 12).unwrap(); + let expected = ParseUnits::U256(U256::from_str("123456789000000").unwrap()); + assert_eq!(result, expected); + } +} From 31f54d7dd2d5ca7984aca1ced0074e38bf695404 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 23 Oct 2024 01:59:44 +0000 Subject: [PATCH 23/28] apply requested changes --- crates/subgraph/src/apy.rs | 67 ++----- crates/subgraph/src/types/impls.rs | 188 ++++++++++++++++++ crates/subgraph/src/types/mod.rs | 1 + crates/subgraph/src/utils/mod.rs | 7 +- .../src/lib/components/tables/OrderAPY.svelte | 14 +- .../lib/components/tables/OrderAPY.test.ts | 14 +- tauri-app/src/lib/utils/number.ts | 15 ++ 7 files changed, 223 insertions(+), 83 deletions(-) create mode 100644 crates/subgraph/src/types/impls.rs diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 8dd0628e4..272e47369 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -1,6 +1,6 @@ use crate::{ types::common::{Erc20, Order, Trade}, - utils::{one_18, to_18_decimals, year_18, DAY}, + utils::{one_18, to_18_decimals, year_18}, vol::{get_vaults_vol, VaultVolume}, OrderbookSubgraphClientError, }; @@ -8,6 +8,7 @@ use alloy::primitives::{ utils::{parse_units, ParseUnits}, I256, U256, }; +use chrono::TimeDelta; use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, HashMap}, @@ -256,7 +257,8 @@ pub fn get_token_vaults_apy( .iter() .filter(|v| { u64::from_str(&v.timestamp.0).unwrap() - <= u64::from_str(&first_trade.timestamp.0).unwrap() + DAY + <= u64::from_str(&first_trade.timestamp.0).unwrap() + + TimeDelta::days(1).num_seconds() as u64 }) .collect::>()[0]; @@ -354,18 +356,18 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap HashMap Result { + to_18_decimals( + ParseUnits::I256(I256::from_str(&self.input_vault_balance_change.amount.0).unwrap()), + self.input_vault_balance_change + .vault + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18"), + ) + } + + /// Converts this trade's output to 18 point decimals in U256/I256 + pub fn output_to_18_decimals(&self) -> Result { + to_18_decimals( + ParseUnits::I256(I256::from_str(&self.output_vault_balance_change.amount.0).unwrap()), + self.output_vault_balance_change + .vault + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18"), + ) + } + + /// Calculates the trade I/O ratio + pub fn ratio(&self) -> Option { + Some( + self.input_to_18_decimals() + .ok()? + .get_absolute() + .saturating_mul(one_18().get_absolute()) + .checked_div( + self.output_to_18_decimals() + .ok()? + .get_signed() + .saturating_neg() + .try_into() + .ok()?, + ) + .unwrap_or(U256::MAX), + ) + } + + /// Calculates the trade O/I ratio (inverse) + pub fn inverse_ratio(&self) -> Option { + Some( + TryInto::::try_into( + self.output_to_18_decimals() + .ok()? + .get_signed() + .saturating_neg(), + ) + .ok()? + .saturating_mul(one_18().get_absolute()) + .checked_div(self.input_to_18_decimals().ok()?.get_absolute()) + .unwrap_or(U256::MAX), + ) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::common::{ + BigInt, Bytes, Orderbook, TradeEvent, TradeStructPartialOrder, TradeVaultBalanceChange, + Transaction, VaultBalanceChangeVault, + }; + use alloy::primitives::Address; + + #[test] + fn test_input_to_18_decimals() { + let result = get_trade().input_to_18_decimals().unwrap(); + let expected = U256::from_str("3000000000000000000").unwrap(); + assert_eq!(result.get_absolute(), expected); + } + + #[test] + fn test_output_to_18_decimals() { + let result = get_trade().output_to_18_decimals().unwrap(); + let expected = I256::from_str("-6000000000000000000").unwrap(); + assert_eq!(result.get_signed(), expected); + } + + #[test] + fn test_ratio() { + let result = get_trade().ratio().unwrap(); + let expected = U256::from_str("500000000000000000").unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_inverse_ratio() { + let result = get_trade().inverse_ratio().unwrap(); + let expected = U256::from_str("2000000000000000000").unwrap(); + assert_eq!(result, expected); + } + + // helper to get trade struct + fn get_trade() -> Trade { + let token_address = Address::from_slice(&[0x11u8; 20]); + let token = Erc20 { + id: Bytes(token_address.to_string()), + address: Bytes(token_address.to_string()), + name: Some("Token1".to_string()), + symbol: Some("Token1".to_string()), + decimals: Some(BigInt(6.to_string())), + }; + let input_trade_vault_balance_change = TradeVaultBalanceChange { + id: Bytes("".to_string()), + __typename: "".to_string(), + amount: BigInt("3000000".to_string()), + new_vault_balance: BigInt("".to_string()), + old_vault_balance: BigInt("".to_string()), + vault: VaultBalanceChangeVault { + id: Bytes("".to_string()), + vault_id: BigInt("".to_string()), + token: token.clone(), + }, + timestamp: BigInt("".to_string()), + transaction: Transaction { + id: Bytes("".to_string()), + from: Bytes("".to_string()), + block_number: BigInt("".to_string()), + timestamp: BigInt("".to_string()), + }, + orderbook: Orderbook { + id: Bytes("".to_string()), + }, + }; + let output_trade_vault_balance_change = TradeVaultBalanceChange { + id: Bytes("".to_string()), + __typename: "".to_string(), + amount: BigInt("-6000000".to_string()), + new_vault_balance: BigInt("".to_string()), + old_vault_balance: BigInt("".to_string()), + vault: VaultBalanceChangeVault { + id: Bytes("".to_string()), + vault_id: BigInt("".to_string()), + token: token.clone(), + }, + timestamp: BigInt("".to_string()), + transaction: Transaction { + id: Bytes("".to_string()), + from: Bytes("".to_string()), + block_number: BigInt("".to_string()), + timestamp: BigInt("".to_string()), + }, + orderbook: Orderbook { + id: Bytes("".to_string()), + }, + }; + Trade { + id: Bytes("".to_string()), + trade_event: TradeEvent { + transaction: Transaction { + id: Bytes("".to_string()), + from: Bytes("".to_string()), + block_number: BigInt("".to_string()), + timestamp: BigInt("".to_string()), + }, + sender: Bytes("".to_string()), + }, + output_vault_balance_change: output_trade_vault_balance_change, + input_vault_balance_change: input_trade_vault_balance_change, + order: TradeStructPartialOrder { + id: Bytes("".to_string()), + order_hash: Bytes("".to_string()), + }, + timestamp: BigInt("".to_string()), + orderbook: Orderbook { + id: Bytes("".to_string()), + }, + } + } +} diff --git a/crates/subgraph/src/types/mod.rs b/crates/subgraph/src/types/mod.rs index 88b092954..7e27af4e4 100644 --- a/crates/subgraph/src/types/mod.rs +++ b/crates/subgraph/src/types/mod.rs @@ -1,4 +1,5 @@ pub mod common; +pub mod impls; pub mod order; pub mod order_detail_traits; pub mod order_trade; diff --git a/crates/subgraph/src/utils/mod.rs b/crates/subgraph/src/utils/mod.rs index 5aedc70a4..547742a16 100644 --- a/crates/subgraph/src/utils/mod.rs +++ b/crates/subgraph/src/utils/mod.rs @@ -1,4 +1,5 @@ use alloy::primitives::utils::{format_units, parse_units, ParseUnits, Unit, UnitsError}; +use chrono::TimeDelta; mod order_id; mod slice_list; @@ -6,9 +7,6 @@ mod slice_list; pub use order_id::*; pub use slice_list::*; -pub const DAY: u64 = 60 * 60 * 24; -pub const YEAR: u64 = DAY * 365; - /// Returns 18 point decimals 1 as I256/U256 pub fn one_18() -> ParseUnits { parse_units("1", 18).unwrap() @@ -16,7 +14,7 @@ pub fn one_18() -> ParseUnits { /// Returns YEAR as 18 point decimals as I256/U256 pub fn year_18() -> ParseUnits { - parse_units(&YEAR.to_string(), 18).unwrap() + parse_units(&TimeDelta::days(365).num_seconds().to_string(), 18).unwrap() } /// Converts a U256/I256 value to a 18 fixed point U256/I256 given the decimals point @@ -44,6 +42,7 @@ mod test { #[test] fn test_year_18_decimals() { + const YEAR: u64 = 60 * 60 * 24 * 365; let result = year_18(); let expected_signed = I256::try_from(YEAR) .unwrap() diff --git a/tauri-app/src/lib/components/tables/OrderAPY.svelte b/tauri-app/src/lib/components/tables/OrderAPY.svelte index 38e90c34c..2bd280557 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.svelte +++ b/tauri-app/src/lib/components/tables/OrderAPY.svelte @@ -6,7 +6,7 @@ import { subgraphUrl } from '$lib/stores/settings'; import { TableBodyCell, TableHeadCell } from 'flowbite-svelte'; import ApyTimeFilters from '../charts/APYTimeFilters.svelte'; - import { formatUnits } from 'viem'; + import { bigintString18ToPercentage } from '$lib/utils/number'; export let id: string; @@ -20,16 +20,6 @@ getNextPageParam: () => undefined, enabled: !!$subgraphUrl, }); - - function formatApyToPercentage(value: string): string { - let valueString = formatUnits(BigInt(value) * 100n, 18); - const index = valueString.indexOf('.'); - if (index > -1) { - // 5 point decimals to show on UI - valueString = valueString.substring(0, index + 6); - } - return valueString; - } @@ -43,7 +33,7 @@ {item.denominatedApy - ? formatApyToPercentage(item.denominatedApy.apy) + + ? bigintString18ToPercentage(item.denominatedApy.apy, 5) + '% in ' + (item.denominatedApy.token.symbol ?? item.denominatedApy.token.name ?? diff --git a/tauri-app/src/lib/components/tables/OrderAPY.test.ts b/tauri-app/src/lib/components/tables/OrderAPY.test.ts index 34d13c179..1ae57d114 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.test.ts +++ b/tauri-app/src/lib/components/tables/OrderAPY.test.ts @@ -4,18 +4,8 @@ import { expect } from '$lib/test/matchers'; import { mockIPC } from '@tauri-apps/api/mocks'; import type { OrderAPY } from '$lib/typeshare/subgraphTypes'; import { QueryClient } from '@tanstack/svelte-query'; -import { formatUnits } from 'viem'; import OrderApy from './OrderAPY.svelte'; - -function formatApyToPercentage(value: string): string { - let valueString = formatUnits(BigInt(value) * 100n, 18); - const index = valueString.indexOf('.'); - if (index > -1) { - // 5 point decimals to show on UI - valueString = valueString.substring(0, index + 6); - } - return valueString; -} +import { bigintString18ToPercentage } from '$lib/utils/number'; vi.mock('$lib/stores/settings', async (importOriginal) => { const { writable } = await import('svelte/store'); @@ -83,7 +73,7 @@ test('renders table with correct data', async () => { // checking for (let i = 0; i < mockOrderApy.length; i++) { - const display = formatApyToPercentage(mockOrderApy[i].denominatedApy!.apy); + const display = bigintString18ToPercentage(mockOrderApy[i].denominatedApy!.apy, 5); expect(rows[i]).toHaveTextContent(display); } }); diff --git a/tauri-app/src/lib/utils/number.ts b/tauri-app/src/lib/utils/number.ts index 79417b775..4c469d3b1 100644 --- a/tauri-app/src/lib/utils/number.ts +++ b/tauri-app/src/lib/utils/number.ts @@ -3,3 +3,18 @@ import { formatUnits } from 'viem'; export function bigintToFloat(value: bigint, decimals: number) { return parseFloat(formatUnits(value, decimals)); } + +/** + * Converts a bigint string 18point decimals value to a float string, optionally + * keeping the given number of decimals digits after "." + * @param value - The bigint string value + * @param decimalPoint - (optional) the number of digits to keep after "." + */ +export function bigintString18ToPercentage(value: string, decimalPoint?: number): string { + let valueString = formatUnits(BigInt(value) * 100n, 18); + const index = valueString.indexOf('.'); + if (decimalPoint !== undefined && index > -1) { + valueString = valueString.substring(0, decimalPoint === 0 ? index : index + decimalPoint); + } + return valueString; +} From 54d21b32e917ac360e83f35420c47cc19f493f80 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 23 Oct 2024 02:10:05 +0000 Subject: [PATCH 24/28] update --- .../src/lib/components/tables/OrderAPY.svelte | 4 ++-- .../src/lib/components/tables/OrderAPY.test.ts | 4 ++-- tauri-app/src/lib/utils/number.ts | 16 +++++++++++----- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/tauri-app/src/lib/components/tables/OrderAPY.svelte b/tauri-app/src/lib/components/tables/OrderAPY.svelte index 2bd280557..13c0cf7b3 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.svelte +++ b/tauri-app/src/lib/components/tables/OrderAPY.svelte @@ -6,7 +6,7 @@ import { subgraphUrl } from '$lib/stores/settings'; import { TableBodyCell, TableHeadCell } from 'flowbite-svelte'; import ApyTimeFilters from '../charts/APYTimeFilters.svelte'; - import { bigintString18ToPercentage } from '$lib/utils/number'; + import { bigintStringToPercentage } from '$lib/utils/number'; export let id: string; @@ -33,7 +33,7 @@ {item.denominatedApy - ? bigintString18ToPercentage(item.denominatedApy.apy, 5) + + ? bigintStringToPercentage(item.denominatedApy.apy, 18, 5) + '% in ' + (item.denominatedApy.token.symbol ?? item.denominatedApy.token.name ?? diff --git a/tauri-app/src/lib/components/tables/OrderAPY.test.ts b/tauri-app/src/lib/components/tables/OrderAPY.test.ts index 1ae57d114..540e5da01 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.test.ts +++ b/tauri-app/src/lib/components/tables/OrderAPY.test.ts @@ -5,7 +5,7 @@ import { mockIPC } from '@tauri-apps/api/mocks'; import type { OrderAPY } from '$lib/typeshare/subgraphTypes'; import { QueryClient } from '@tanstack/svelte-query'; import OrderApy from './OrderAPY.svelte'; -import { bigintString18ToPercentage } from '$lib/utils/number'; +import { bigintStringToPercentage } from '$lib/utils/number'; vi.mock('$lib/stores/settings', async (importOriginal) => { const { writable } = await import('svelte/store'); @@ -73,7 +73,7 @@ test('renders table with correct data', async () => { // checking for (let i = 0; i < mockOrderApy.length; i++) { - const display = bigintString18ToPercentage(mockOrderApy[i].denominatedApy!.apy, 5); + const display = bigintStringToPercentage(mockOrderApy[i].denominatedApy!.apy, 18, 5); expect(rows[i]).toHaveTextContent(display); } }); diff --git a/tauri-app/src/lib/utils/number.ts b/tauri-app/src/lib/utils/number.ts index 4c469d3b1..2ca9c4962 100644 --- a/tauri-app/src/lib/utils/number.ts +++ b/tauri-app/src/lib/utils/number.ts @@ -8,13 +8,19 @@ export function bigintToFloat(value: bigint, decimals: number) { * Converts a bigint string 18point decimals value to a float string, optionally * keeping the given number of decimals digits after "." * @param value - The bigint string value - * @param decimalPoint - (optional) the number of digits to keep after "." + * @param valueDecimals - The bigint string value decimals point + * @param decimalPoint - (optional) The number of digits to keep after "." in final result, defaults to valueDecimals */ -export function bigintString18ToPercentage(value: string, decimalPoint?: number): string { - let valueString = formatUnits(BigInt(value) * 100n, 18); +export function bigintStringToPercentage( + value: string, + valueDecimals: number, + finalDecimalsDigits?: number, +): string { + const finalDecimals = finalDecimalsDigits !== undefined ? finalDecimalsDigits : valueDecimals; + let valueString = formatUnits(BigInt(value) * 100n, valueDecimals); const index = valueString.indexOf('.'); - if (decimalPoint !== undefined && index > -1) { - valueString = valueString.substring(0, decimalPoint === 0 ? index : index + decimalPoint); + if (index > -1) { + valueString = valueString.substring(0, finalDecimals === 0 ? index : index + finalDecimals + 1); } return valueString; } From 5b65481f7f4ed873d1a592e0f6b33de51630b3e1 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 23 Oct 2024 02:10:59 +0000 Subject: [PATCH 25/28] Update number.ts --- tauri-app/src/lib/utils/number.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tauri-app/src/lib/utils/number.ts b/tauri-app/src/lib/utils/number.ts index 2ca9c4962..59da9ef1e 100644 --- a/tauri-app/src/lib/utils/number.ts +++ b/tauri-app/src/lib/utils/number.ts @@ -16,7 +16,8 @@ export function bigintStringToPercentage( valueDecimals: number, finalDecimalsDigits?: number, ): string { - const finalDecimals = finalDecimalsDigits !== undefined ? finalDecimalsDigits : valueDecimals; + const finalDecimals = + typeof finalDecimalsDigits !== 'undefined' ? finalDecimalsDigits : valueDecimals; let valueString = formatUnits(BigInt(value) * 100n, valueDecimals); const index = valueString.indexOf('.'); if (index > -1) { From 51b5f8632455d1d92a267d02d59b3e53e2ec225e Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 23 Oct 2024 02:12:08 +0000 Subject: [PATCH 26/28] Update number.ts --- tauri-app/src/lib/utils/number.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tauri-app/src/lib/utils/number.ts b/tauri-app/src/lib/utils/number.ts index 59da9ef1e..084a7e554 100644 --- a/tauri-app/src/lib/utils/number.ts +++ b/tauri-app/src/lib/utils/number.ts @@ -5,8 +5,7 @@ export function bigintToFloat(value: bigint, decimals: number) { } /** - * Converts a bigint string 18point decimals value to a float string, optionally - * keeping the given number of decimals digits after "." + * Converts a bigint string value to a percentage with optionally given number of decimal points * @param value - The bigint string value * @param valueDecimals - The bigint string value decimals point * @param decimalPoint - (optional) The number of digits to keep after "." in final result, defaults to valueDecimals From 6aa1bcccaa6e438a7d6453239c0bda3c635cda69 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 23 Oct 2024 02:36:48 +0000 Subject: [PATCH 27/28] update --- crates/subgraph/src/apy.rs | 10 ++-- crates/subgraph/src/types/impls.rs | 79 ++++++++++++++++-------------- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 272e47369..66f2aff2c 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -381,11 +381,13 @@ fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap Result { - to_18_decimals( - ParseUnits::I256(I256::from_str(&self.input_vault_balance_change.amount.0).unwrap()), + pub fn input_to_18_decimals(&self) -> Result { + Ok(to_18_decimals( + ParseUnits::U256(U256::from_str(&self.input_vault_balance_change.amount.0)?), self.input_vault_balance_change .vault .token @@ -18,13 +31,13 @@ impl Trade { .as_ref() .map(|v| v.0.as_str()) .unwrap_or("18"), - ) + )?) } /// Converts this trade's output to 18 point decimals in U256/I256 - pub fn output_to_18_decimals(&self) -> Result { - to_18_decimals( - ParseUnits::I256(I256::from_str(&self.output_vault_balance_change.amount.0).unwrap()), + pub fn output_to_18_decimals(&self) -> Result { + Ok(to_18_decimals( + ParseUnits::I256(I256::from_str(&self.output_vault_balance_change.amount.0)?), self.output_vault_balance_change .vault .token @@ -32,41 +45,31 @@ impl Trade { .as_ref() .map(|v| v.0.as_str()) .unwrap_or("18"), - ) + )?) } - /// Calculates the trade I/O ratio - pub fn ratio(&self) -> Option { - Some( - self.input_to_18_decimals() - .ok()? - .get_absolute() - .saturating_mul(one_18().get_absolute()) - .checked_div( - self.output_to_18_decimals() - .ok()? - .get_signed() - .saturating_neg() - .try_into() - .ok()?, - ) - .unwrap_or(U256::MAX), - ) - } - - /// Calculates the trade O/I ratio (inverse) - pub fn inverse_ratio(&self) -> Option { - Some( - TryInto::::try_into( - self.output_to_18_decimals() - .ok()? + /// Calculates the trade's I/O ratio + pub fn ratio(&self) -> Result { + Ok(self + .input_to_18_decimals()? + .get_absolute() + .saturating_mul(one_18().get_absolute()) + .checked_div( + self.output_to_18_decimals()? .get_signed() - .saturating_neg(), + .saturating_neg() + .try_into()?, ) - .ok()? - .saturating_mul(one_18().get_absolute()) - .checked_div(self.input_to_18_decimals().ok()?.get_absolute()) - .unwrap_or(U256::MAX), + .unwrap_or(U256::MAX)) + } + + /// Calculates the trade's O/I ratio (inverse) + pub fn inverse_ratio(&self) -> Result { + Ok( + TryInto::::try_into(self.output_to_18_decimals()?.get_signed().saturating_neg())? + .saturating_mul(one_18().get_absolute()) + .checked_div(self.input_to_18_decimals()?.get_absolute()) + .unwrap_or(U256::MAX), ) } } From c5adc1c68949d4235a8e3a6cbd8d9be3db7d212f Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Sun, 27 Oct 2024 18:23:36 +0000 Subject: [PATCH 28/28] apply requested changes --- crates/subgraph/src/apy.rs | 483 ++------------ crates/subgraph/src/error.rs | 65 ++ crates/subgraph/src/lib.rs | 4 +- crates/subgraph/src/orderbook_client.rs | 40 +- crates/subgraph/src/types/impls.rs | 41 +- crates/subgraph/src/types/order.rs | 613 +++++++++++++++++- crates/subgraph/src/utils/mod.rs | 18 +- crates/subgraph/src/vol.rs | 67 +- .../src-tauri/src/commands/order_take.rs | 20 +- tauri-app/src-tauri/src/main.rs | 4 +- .../src/lib/components/tables/OrderAPY.svelte | 10 +- .../lib/components/tables/OrderAPY.test.ts | 15 +- tauri-app/src/lib/queries/orderTradesList.ts | 4 +- 13 files changed, 853 insertions(+), 531 deletions(-) create mode 100644 crates/subgraph/src/error.rs diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/apy.rs index 66f2aff2c..56d3b7c9a 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/apy.rs @@ -1,25 +1,19 @@ use crate::{ - types::common::{Erc20, Order, Trade}, - utils::{one_18, to_18_decimals, year_18}, - vol::{get_vaults_vol, VaultVolume}, + types::common::{Erc20, Trade}, + utils::{annual_rate, one_18, to_18_decimals}, + vol::VaultVolume, OrderbookSubgraphClientError, }; -use alloy::primitives::{ - utils::{parse_units, ParseUnits}, - I256, U256, -}; +use alloy::primitives::{utils::ParseUnits, I256, U256}; use chrono::TimeDelta; use serde::{Deserialize, Serialize}; -use std::{ - collections::{BTreeMap, HashMap}, - str::FromStr, -}; +use std::str::FromStr; use typeshare::typeshare; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[typeshare] -pub struct TokenVaultAPY { +pub struct VaultAPY { pub id: String, pub token: Erc20, #[typeshare(typescript(type = "number"))] @@ -34,211 +28,26 @@ pub struct TokenVaultAPY { pub apy: Option, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[typeshare] -pub struct DenominatedAPY { - #[typeshare(typescript(type = "string"))] - pub apy: I256, - pub token: Erc20, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[typeshare] -pub struct OrderAPY { - pub order_id: String, - pub order_hash: String, - pub denominated_apy: Option, - #[typeshare(typescript(type = "number"))] - pub start_time: u64, - #[typeshare(typescript(type = "number"))] - pub end_time: u64, - pub inputs_token_vault_apy: Vec, - pub outputs_token_vault_apy: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct TokenPair { - input: Erc20, - output: Erc20, -} - -/// Given an order and its trades and optionally a timeframe, will calculates -/// the APY for each of the entire order and for each of its vaults -/// Trades must be sorted indesc order by timestamp, this is the case if -/// queried from subgraph using this lib functionalities -pub fn get_order_apy( - order: &Order, - trades: &[Trade], - start_timestamp: Option, - end_timestamp: Option, -) -> Result { - if trades.is_empty() { - return Ok(OrderAPY { - order_id: order.id.0.clone(), - order_hash: order.order_hash.0.clone(), - start_time: start_timestamp.unwrap_or(0), - end_time: end_timestamp.unwrap_or(chrono::Utc::now().timestamp() as u64), - inputs_token_vault_apy: vec![], - outputs_token_vault_apy: vec![], - denominated_apy: None, - }); - } - let vols = get_vaults_vol(trades)?; - let token_vaults_apy = get_token_vaults_apy(trades, &vols, start_timestamp, end_timestamp)?; - - // build an OrderApy struct - let mut start_time = u64::MAX; - let mut end_time = 0_u64; - let mut inputs: Vec = vec![]; - let mut outputs: Vec = vec![]; - for item in &token_vaults_apy { - if item.start_time < start_time { - start_time = item.start_time; - } - if item.end_time > end_time { - end_time = item.end_time; - } - if order - .inputs - .iter() - .any(|v| v.vault_id.0 == item.id && v.token == item.token) - { - inputs.push(item.clone()); - } - if order - .outputs - .iter() - .any(|v| v.vault_id.0 == item.id && v.token == item.token) - { - outputs.push(item.clone()); - } - } - let mut order_apy = OrderAPY { - order_id: order.id.0.clone(), - order_hash: order.order_hash.0.clone(), - start_time, - end_time, - inputs_token_vault_apy: inputs, - outputs_token_vault_apy: outputs, - denominated_apy: None, - }; - - // get pairs ratios - let pair_ratio_map = get_pairs_ratio(&order_apy, trades); - - // try to calculate all vaults capital and volume denominated into each of - // the order's tokens by checking if there is direct ratio between the tokens, - // multi path ratios are ignored currently and results in None for the APY. - // if there is a success for any of the denomination tokens, gather it in order - // of its net vol and pick the one with highest net vol. - // if there was no success with any of the order's tokens, simply return None - // for the APY. - let mut ordered_token_net_vol_map = BTreeMap::new(); - let mut full_apy_in_distinct_token_denominations = vec![]; - for token in &token_vaults_apy { - let mut noway = false; - let mut combined_capital = I256::ZERO; - let mut combined_annual_rate_vol = I256::ZERO; - let mut token_net_vol_map_converted_in_current_denomination = BTreeMap::new(); - for token_vault in &token_vaults_apy { - // time to year ratio - let annual_rate = annual_rate(token_vault.start_time, token_vault.end_time); - - // sum up all token vaults' capitals and vols in the current's iteration - // token denomination by using the direct ratio between the tokens - if token_vault.token == token.token { - combined_capital += token_vault.capital; - combined_annual_rate_vol += token_vault - .net_vol - .saturating_mul(one_18().get_signed()) - .saturating_div(annual_rate); - token_net_vol_map_converted_in_current_denomination - .insert(token_vault.net_vol, &token.token); - } else { - let pair = TokenPair { - input: token.token.clone(), - output: token_vault.token.clone(), - }; - // convert to current denomination by the direct pair ratio if exists - if let Some(Some(ratio)) = pair_ratio_map.get(&pair) { - combined_capital += token_vault - .capital - .saturating_mul(*ratio) - .saturating_div(one_18().get_signed()); - combined_annual_rate_vol += token_vault - .net_vol - .saturating_mul(*ratio) - .saturating_div(one_18().get_signed()) - .saturating_mul(one_18().get_signed()) - .saturating_div(annual_rate); - token_net_vol_map_converted_in_current_denomination.insert( - token_vault - .net_vol - .saturating_mul(*ratio) - .saturating_div(one_18().get_signed()), - &token_vault.token, - ); - } else { - noway = true; - break; - } - } - } - - // for every success apy calc in a token denomination, gather them in BTreeMap - // this means at the end we have all the successful apy calculated in each of - // the order's io tokens in order from highest to lowest. - if !noway { - if let Some(apy) = combined_annual_rate_vol - .saturating_mul(one_18().get_signed()) - .checked_div(combined_capital) - { - full_apy_in_distinct_token_denominations.push(DenominatedAPY { - apy, - token: token.token.clone(), - }); - } - } else { - token_net_vol_map_converted_in_current_denomination.clear(); - } - - // if we already have ordered token net vol in a denomination - // we dont need them in other denominations in order to pick - // the highest vol token as settelement denomination - if ordered_token_net_vol_map.is_empty() { - ordered_token_net_vol_map.extend(token_net_vol_map_converted_in_current_denomination); - } - } - - // pick the denomination with highest net vol by iterating over tokens with - // highest vol to lowest and pick the first matching matching one - for (_, &token) in ordered_token_net_vol_map.iter().rev() { - if let Some(denominated_apy) = full_apy_in_distinct_token_denominations - .iter() - .find(|&v| &v.token == token) - { - order_apy.denominated_apy = Some(denominated_apy.clone()); - // return early as soon as a match is found - return Ok(order_apy); - } - } - - Ok(order_apy) +pub struct TokenPair { + pub input: Erc20, + pub output: Erc20, } /// Calculates each token vault apy at the given timeframe /// Trades must be sorted indesc order by timestamp, this is /// the case if queried from subgraph using this lib functionalities -pub fn get_token_vaults_apy( +pub fn get_vaults_apy( trades: &[Trade], vols: &[VaultVolume], start_timestamp: Option, end_timestamp: Option, -) -> Result, OrderbookSubgraphClientError> { - let mut token_vaults_apy: Vec = vec![]; +) -> Result, OrderbookSubgraphClientError> { + let mut token_vaults_apy: Vec = vec![]; for vol in vols { + let vol = vol.to_18_decimals()?; // this token vault trades in desc order by timestamp let vault_trades = trades .iter() @@ -256,9 +65,12 @@ pub fn get_token_vaults_apy( let first_day_last_trade = vault_trades .iter() .filter(|v| { - u64::from_str(&v.timestamp.0).unwrap() - <= u64::from_str(&first_trade.timestamp.0).unwrap() - + TimeDelta::days(1).num_seconds() as u64 + u64::from_str(&v.timestamp.0) + .ok() + .zip(u64::from_str(&first_trade.timestamp.0).ok()) + .is_some_and(|(trade_time, first_trade_time)| { + trade_time <= first_trade_time + TimeDelta::days(1).num_seconds() as u64 + }) }) .collect::>()[0]; @@ -292,17 +104,6 @@ pub fn get_token_vaults_apy( .ok() }); - // convert net vol to 18 decimals point - let net_vol = to_18_decimals( - ParseUnits::I256(vol.net_vol), - vol.token - .decimals - .as_ref() - .map(|v| v.0.as_str()) - .unwrap_or("18"), - ) - .ok(); - // the time range for this token vault let mut start = u64::from_str(&first_trade.timestamp.0)?; start_timestamp.inspect(|t| { @@ -313,29 +114,26 @@ pub fn get_token_vaults_apy( let end = end_timestamp.unwrap_or(chrono::Utc::now().timestamp() as u64); // this token vault apy in 18 decimals point - let apy = starting_capital - .zip(net_vol) - .and_then(|(starting_capital, net_vol)| { - (!starting_capital.is_zero()) - .then_some( - net_vol - .get_signed() - .saturating_mul(one_18().get_signed()) - .saturating_div(starting_capital.get_signed()) - .saturating_mul(one_18().get_signed()) - .checked_div(annual_rate(start, end)), - ) - .flatten() - }); + let apy = starting_capital.and_then(|starting_capital| { + (!starting_capital.is_zero()) + .then_some( + vol.net_vol + .saturating_mul(one_18().get_signed()) + .saturating_div(starting_capital.get_signed()) + .saturating_mul(one_18().get_signed()) + .checked_div(annual_rate(start, end)), + ) + .flatten() + }); // this token vault apy - token_vaults_apy.push(TokenVaultAPY { + token_vaults_apy.push(VaultAPY { id: vol.id.clone(), token: vol.token.clone(), start_time: start, end_time: end, apy, - net_vol: net_vol.unwrap_or(ParseUnits::I256(I256::ZERO)).get_signed(), + net_vol: vol.net_vol, capital: starting_capital .unwrap_or(ParseUnits::I256(I256::ZERO)) .get_signed(), @@ -345,134 +143,17 @@ pub fn get_token_vaults_apy( Ok(token_vaults_apy) } -/// Calculates an order's pairs' ratios from their last trades in a given list of trades -/// Trades must be sorted indesc order by timestamp, this is the case if queried from subgraph -/// using this lib functionalities -fn get_pairs_ratio(order_apy: &OrderAPY, trades: &[Trade]) -> HashMap> { - let mut pair_ratio_map: HashMap> = HashMap::new(); - for input in &order_apy.inputs_token_vault_apy { - for output in &order_apy.outputs_token_vault_apy { - let pair_as_key = TokenPair { - input: input.token.clone(), - output: output.token.clone(), - }; - let inverse_pair_as_key = TokenPair { - input: output.token.clone(), - output: input.token.clone(), - }; - // if not same io token and ratio map doesnt already include them - if input.token != output.token - && !(pair_ratio_map.contains_key(&pair_as_key) - || pair_ratio_map.contains_key(&inverse_pair_as_key)) - { - // find this pairs(io or oi) latest tradetrades from list of order's - // trades, the calculate the pair ratio (in amount/out amount) and - // its inverse from the latest trade that involes these 2 tokens. - // this assumes the trades are already in desc order by timestamp which - // is the case when used this lib query to get them - let ratio = trades - .iter() - .find(|v| { - (v.input_vault_balance_change.vault.token == input.token - && v.output_vault_balance_change.vault.token == output.token) - || (v.output_vault_balance_change.vault.token == input.token - && v.input_vault_balance_change.vault.token == output.token) - }) - .and_then(|latest_trade| { - // convert input and output amounts to 18 decimals point - // and then calculate the pair ratio - latest_trade - .ratio() - .ok() - .zip(latest_trade.inverse_ratio().ok()) - .map(|(ratio, inverse_ratio)| { - [I256::from_raw(ratio), I256::from_raw(inverse_ratio)] - }) - }); - - // io - pair_ratio_map.insert(pair_as_key, ratio.map(|v| v[0])); - // oi - pair_ratio_map.insert(inverse_pair_as_key, ratio.map(|v| v[1])); - } - } - } - - pair_ratio_map -} - -/// Returns annual rate as 18 point decimals as I256 -fn annual_rate(start: u64, end: u64) -> I256 { - parse_units(&(end - start).to_string(), 18) - .unwrap() - .get_signed() - .saturating_mul(one_18().get_signed()) - .saturating_div(year_18().get_signed()) -} - #[cfg(test)] mod test { use super::*; use crate::types::common::{ BigInt, Bytes, Orderbook, TradeEvent, TradeStructPartialOrder, TradeVaultBalanceChange, - Transaction, Vault, VaultBalanceChangeVault, + Transaction, VaultBalanceChangeVault, }; use alloy::primitives::{Address, B256}; #[test] - fn test_get_pairs_ratio() { - let trades = get_trades(); - let [token1, token2] = get_tokens(); - let [vault1, vault2] = get_vault_ids(); - let token_vault1 = TokenVaultAPY { - id: vault1.to_string(), - token: token1.clone(), - start_time: 0, - end_time: 0, - net_vol: I256::ZERO, - capital: I256::ZERO, - apy: Some(I256::ZERO), - }; - let token_vault2 = TokenVaultAPY { - id: vault2.to_string(), - token: token2.clone(), - start_time: 0, - end_time: 0, - net_vol: I256::ZERO, - capital: I256::ZERO, - apy: Some(I256::ZERO), - }; - let order_apy = OrderAPY { - order_id: "".to_string(), - order_hash: "".to_string(), - denominated_apy: None, - start_time: 0, - end_time: 0, - inputs_token_vault_apy: vec![token_vault1.clone(), token_vault2.clone()], - outputs_token_vault_apy: vec![token_vault1, token_vault2], - }; - let result = get_pairs_ratio(&order_apy, &trades); - let mut expected = HashMap::new(); - expected.insert( - TokenPair { - input: token2.clone(), - output: token1.clone(), - }, - Some(I256::from_str("285714285714285714").unwrap()), - ); - expected.insert( - TokenPair { - input: token1.clone(), - output: token2.clone(), - }, - Some(I256::from_str("3500000000000000000").unwrap()), - ); - - assert_eq!(result, expected); - } - - #[test] - fn test_get_token_vaults_apy() { + fn test_get_vaults_apy() { let trades = get_trades(); let [token1, token2] = get_tokens(); let [vault1, vault2] = get_vault_ids(); @@ -493,10 +174,9 @@ mod test { net_vol: I256::from_str("2000000000000000000").unwrap(), }; let result = - get_token_vaults_apy(&trades, &[vault_vol1, vault_vol2], Some(1), Some(10000001)) - .unwrap(); + get_vaults_apy(&trades, &[vault_vol1, vault_vol2], Some(1), Some(10000001)).unwrap(); let expected = vec![ - TokenVaultAPY { + VaultAPY { id: vault1.to_string(), token: token1.clone(), start_time: 1, @@ -506,7 +186,7 @@ mod test { // (1/5) / (10000001_end - 1_start / 31_536_00_year) apy: Some(I256::from_str("630720000000000000").unwrap()), }, - TokenVaultAPY { + VaultAPY { id: vault2.to_string(), token: token2.clone(), start_time: 1, @@ -521,47 +201,6 @@ mod test { assert_eq!(result, expected); } - #[test] - fn test_get_order_apy() { - let order = get_order(); - let trades = get_trades(); - let [token1, token2] = get_tokens(); - let [vault1, vault2] = get_vault_ids(); - let token1_apy = TokenVaultAPY { - id: vault1.to_string(), - token: token1.clone(), - start_time: 1, - end_time: 10000001, - net_vol: I256::from_str("5000000000000000000").unwrap(), - capital: I256::from_str("5000000000000000000").unwrap(), - apy: Some(I256::from_str("3153600000000000000").unwrap()), - }; - let token2_apy = TokenVaultAPY { - id: vault2.to_string(), - token: token2.clone(), - start_time: 1, - end_time: 10000001, - net_vol: I256::from_str("3000000000000000000").unwrap(), - capital: I256::from_str("5000000000000000000").unwrap(), - apy: Some(I256::from_str("1892160000000000000").unwrap()), - }; - let result = get_order_apy(&order, &trades, Some(1), Some(10000001)).unwrap(); - let expected = OrderAPY { - order_id: "order-id".to_string(), - order_hash: "".to_string(), - start_time: 1, - end_time: 10000001, - inputs_token_vault_apy: vec![token1_apy.clone(), token2_apy.clone()], - outputs_token_vault_apy: vec![token1_apy.clone(), token2_apy.clone()], - denominated_apy: Some(DenominatedAPY { - apy: I256::from_str("2172479999999999999").unwrap(), - token: token2, - }), - }; - - assert_eq!(result, expected); - } - fn get_vault_ids() -> [B256; 2] { [ B256::from_slice(&[0x11u8; 32]), @@ -587,52 +226,6 @@ mod test { }; [token1, token2] } - fn get_order() -> Order { - let [vault_id1, vault_id2] = get_vault_ids(); - let [token1, token2] = get_tokens(); - let vault1 = Vault { - id: Bytes("".to_string()), - owner: Bytes("".to_string()), - vault_id: BigInt(vault_id1.to_string()), - balance: BigInt("".to_string()), - token: token1, - orderbook: Orderbook { - id: Bytes("".to_string()), - }, - orders_as_output: vec![], - orders_as_input: vec![], - balance_changes: vec![], - }; - let vault2 = Vault { - id: Bytes("".to_string()), - owner: Bytes("".to_string()), - vault_id: BigInt(vault_id2.to_string()), - balance: BigInt("".to_string()), - token: token2, - orderbook: Orderbook { - id: Bytes("".to_string()), - }, - orders_as_output: vec![], - orders_as_input: vec![], - balance_changes: vec![], - }; - Order { - id: Bytes("order-id".to_string()), - order_bytes: Bytes("".to_string()), - order_hash: Bytes("".to_string()), - owner: Bytes("".to_string()), - outputs: vec![vault1.clone(), vault2.clone()], - inputs: vec![vault1, vault2], - orderbook: Orderbook { - id: Bytes("".to_string()), - }, - active: true, - timestamp_added: BigInt("".to_string()), - meta: None, - add_events: vec![], - trades: vec![], - } - } fn get_trades() -> Vec { let bytes = Bytes("".to_string()); diff --git a/crates/subgraph/src/error.rs b/crates/subgraph/src/error.rs new file mode 100644 index 000000000..cc146e7ce --- /dev/null +++ b/crates/subgraph/src/error.rs @@ -0,0 +1,65 @@ +use crate::{cynic_client::CynicClientError, pagination::PaginationClientError}; +use alloy::primitives::{ + ruint::ParseError, utils::UnitsError, BigIntConversionError, ParseSignedError, +}; +use std::num::{ParseFloatError, ParseIntError}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum OrderbookSubgraphClientError { + #[error(transparent)] + CynicClientError(#[from] CynicClientError), + #[error("Subgraph query returned no data")] + Empty, + #[error(transparent)] + PaginationClientError(#[from] PaginationClientError), + #[error(transparent)] + ParseNumberError(#[from] crate::error::ParseNumberError), +} + +#[derive(Error, Debug)] +pub enum ParseNumberError { + #[error(transparent)] + UnitsError(#[from] UnitsError), + #[error(transparent)] + ParseUnsignedError(#[from] ParseError), + #[error(transparent)] + ParseSignedError(#[from] ParseSignedError), + #[error(transparent)] + BigIntConversionError(#[from] BigIntConversionError), + #[error(transparent)] + ParseIntError(#[from] ParseIntError), + #[error(transparent)] + ParseFloatError(#[from] ParseFloatError), +} + +impl From for OrderbookSubgraphClientError { + fn from(value: UnitsError) -> Self { + ParseNumberError::from(value).into() + } +} +impl From for OrderbookSubgraphClientError { + fn from(value: ParseError) -> Self { + ParseNumberError::from(value).into() + } +} +impl From for OrderbookSubgraphClientError { + fn from(value: ParseSignedError) -> Self { + ParseNumberError::from(value).into() + } +} +impl From for OrderbookSubgraphClientError { + fn from(value: BigIntConversionError) -> Self { + ParseNumberError::from(value).into() + } +} +impl From for OrderbookSubgraphClientError { + fn from(value: ParseIntError) -> Self { + ParseNumberError::from(value).into() + } +} +impl From for OrderbookSubgraphClientError { + fn from(value: ParseFloatError) -> Self { + ParseNumberError::from(value).into() + } +} diff --git a/crates/subgraph/src/lib.rs b/crates/subgraph/src/lib.rs index 309a49362..c257fab1b 100644 --- a/crates/subgraph/src/lib.rs +++ b/crates/subgraph/src/lib.rs @@ -1,5 +1,6 @@ pub mod apy; mod cynic_client; +pub mod error; mod orderbook_client; mod pagination; pub mod types; @@ -11,5 +12,6 @@ pub mod vol; #[cynic::schema("orderbook")] pub mod schema {} -pub use orderbook_client::{OrderbookSubgraphClient, OrderbookSubgraphClientError}; +pub use error::*; +pub use orderbook_client::OrderbookSubgraphClient; pub use pagination::{PageQueryClient, PaginationArgs}; diff --git a/crates/subgraph/src/orderbook_client.rs b/crates/subgraph/src/orderbook_client.rs index ab637e70c..9612fa1b1 100644 --- a/crates/subgraph/src/orderbook_client.rs +++ b/crates/subgraph/src/orderbook_client.rs @@ -1,9 +1,10 @@ -use crate::cynic_client::{CynicClient, CynicClientError}; -use crate::pagination::{PaginationArgs, PaginationClient, PaginationClientError}; +use crate::cynic_client::CynicClient; +use crate::error::OrderbookSubgraphClientError; +use crate::pagination::{PaginationArgs, PaginationClient}; use crate::types::common::*; use crate::types::order::{ BatchOrderDetailQuery, BatchOrderDetailQueryVariables, OrderDetailQuery, OrderIdList, - OrdersListQuery, + OrderPerformance, OrdersListQuery, }; use crate::types::order_trade::{OrderTradeDetailQuery, OrderTradesListQuery}; use crate::types::vault::{VaultDetailQuery, VaultsListQuery}; @@ -11,28 +12,9 @@ use crate::vault_balance_changes_query::VaultBalanceChangesListPageQueryClient; use crate::vol::{get_vaults_vol, VaultVolume}; use cynic::Id; use reqwest::Url; -use thiserror::Error; const ALL_PAGES_QUERY_PAGE_SIZE: u16 = 200; -#[derive(Error, Debug)] -pub enum OrderbookSubgraphClientError { - #[error(transparent)] - CynicClientError(#[from] CynicClientError), - #[error("Subgraph query returned no data")] - Empty, - #[error(transparent)] - PaginationClientError(#[from] PaginationClientError), - #[error(transparent)] - ParseError(#[from] alloy::primitives::ruint::ParseError), - #[error(transparent)] - ParseBigIntConversionError(#[from] alloy::primitives::BigIntConversionError), - #[error(transparent)] - ParseFloatError(#[from] std::num::ParseFloatError), - #[error(transparent)] - ParseIntError(#[from] std::num::ParseIntError), -} - pub struct OrderbookSubgraphClient { url: Url, } @@ -222,6 +204,20 @@ impl OrderbookSubgraphClient { Ok(get_vaults_vol(&trades)?) } + /// Fetches order data and measures an order's detailed performance (apy and vol) + pub async fn order_performance( + &self, + order_id: cynic::Id, + start_timestamp: Option, + end_timestamp: Option, + ) -> Result { + let order = self.order_detail(order_id.clone()).await?; + let trades = self + .order_trades_list_all(order_id, start_timestamp, end_timestamp) + .await?; + OrderPerformance::measure(&order, &trades, start_timestamp, end_timestamp) + } + /// Fetch single vault pub async fn vault_detail(&self, id: Id) -> Result { let data = self diff --git a/crates/subgraph/src/types/impls.rs b/crates/subgraph/src/types/impls.rs index e092d404a..7ecb055e5 100644 --- a/crates/subgraph/src/types/impls.rs +++ b/crates/subgraph/src/types/impls.rs @@ -1,27 +1,14 @@ use super::common::*; -use crate::utils::{one_18, to_18_decimals}; -use alloy::primitives::{ - utils::{ParseUnits, UnitsError}, - I256, U256, +use crate::{ + error::ParseNumberError, + utils::{one_18, to_18_decimals}, }; +use alloy::primitives::{utils::ParseUnits, I256, U256}; use std::str::FromStr; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum ParseUnitsError { - #[error(transparent)] - UnitsError(#[from] UnitsError), - #[error(transparent)] - ParseUnsignedError(#[from] alloy::primitives::ruint::ParseError), - #[error(transparent)] - ParseSignedError(#[from] alloy::primitives::ParseSignedError), - #[error(transparent)] - BigIntConversionError(#[from] alloy::primitives::BigIntConversionError), -} impl Trade { /// Converts this trade's input to 18 point decimals in U256/I256 - pub fn input_to_18_decimals(&self) -> Result { + pub fn input_as_18_decimals(&self) -> Result { Ok(to_18_decimals( ParseUnits::U256(U256::from_str(&self.input_vault_balance_change.amount.0)?), self.input_vault_balance_change @@ -35,7 +22,7 @@ impl Trade { } /// Converts this trade's output to 18 point decimals in U256/I256 - pub fn output_to_18_decimals(&self) -> Result { + pub fn output_as_18_decimals(&self) -> Result { Ok(to_18_decimals( ParseUnits::I256(I256::from_str(&self.output_vault_balance_change.amount.0)?), self.output_vault_balance_change @@ -49,13 +36,13 @@ impl Trade { } /// Calculates the trade's I/O ratio - pub fn ratio(&self) -> Result { + pub fn ratio(&self) -> Result { Ok(self - .input_to_18_decimals()? + .input_as_18_decimals()? .get_absolute() .saturating_mul(one_18().get_absolute()) .checked_div( - self.output_to_18_decimals()? + self.output_as_18_decimals()? .get_signed() .saturating_neg() .try_into()?, @@ -64,11 +51,11 @@ impl Trade { } /// Calculates the trade's O/I ratio (inverse) - pub fn inverse_ratio(&self) -> Result { + pub fn inverse_ratio(&self) -> Result { Ok( - TryInto::::try_into(self.output_to_18_decimals()?.get_signed().saturating_neg())? + TryInto::::try_into(self.output_as_18_decimals()?.get_signed().saturating_neg())? .saturating_mul(one_18().get_absolute()) - .checked_div(self.input_to_18_decimals()?.get_absolute()) + .checked_div(self.input_as_18_decimals()?.get_absolute()) .unwrap_or(U256::MAX), ) } @@ -85,14 +72,14 @@ mod test { #[test] fn test_input_to_18_decimals() { - let result = get_trade().input_to_18_decimals().unwrap(); + let result = get_trade().input_as_18_decimals().unwrap(); let expected = U256::from_str("3000000000000000000").unwrap(); assert_eq!(result.get_absolute(), expected); } #[test] fn test_output_to_18_decimals() { - let result = get_trade().output_to_18_decimals().unwrap(); + let result = get_trade().output_as_18_decimals().unwrap(); let expected = I256::from_str("-6000000000000000000").unwrap(); assert_eq!(result.get_signed(), expected); } diff --git a/crates/subgraph/src/types/order.rs b/crates/subgraph/src/types/order.rs index 233c566a9..8c7924657 100644 --- a/crates/subgraph/src/types/order.rs +++ b/crates/subgraph/src/types/order.rs @@ -1,6 +1,16 @@ use super::common::*; +use crate::apy::{get_vaults_apy, TokenPair}; use crate::schema; -use serde::Serialize; +use crate::utils::annual_rate; +use crate::{ + types::common::{Erc20, Order, Trade}, + utils::one_18, + vol::get_vaults_vol, + OrderbookSubgraphClientError, +}; +use alloy::primitives::{I256, U256}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; use typeshare::typeshare; #[derive(cynic::QueryVariables, Debug)] @@ -41,3 +51,604 @@ pub struct OrderDetailQuery { #[arguments(id: $id)] pub order: Option, } + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct VaultPerformance { + /// vault id + pub id: String, + /// vault token + pub token: Erc20, + #[typeshare(typescript(type = "number"))] + pub start_time: u64, + #[typeshare(typescript(type = "number"))] + pub end_time: u64, + + // vol segment + #[typeshare(typescript(type = "string"))] + pub total_in_vol: U256, + #[typeshare(typescript(type = "string"))] + pub total_out_vol: U256, + #[typeshare(typescript(type = "string"))] + pub total_vol: U256, + #[typeshare(typescript(type = "string"))] + pub net_vol: I256, + + // apy segment + #[typeshare(typescript(type = "string"))] + pub starting_capital: I256, + #[typeshare(typescript(type = "string"))] + pub apy: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct DenominatedPerformance { + #[typeshare(typescript(type = "string"))] + pub apy: I256, + #[typeshare(typescript(type = "string"))] + pub net_vol: I256, + #[typeshare(typescript(type = "string"))] + pub starting_capital: I256, + pub token: Erc20, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct OrderPerformance { + /// Order subgraph id + pub order_id: String, + /// Order hash + pub order_hash: String, + /// Order's orderbook + pub orderbook: String, + /// Order's measured performance as a whole + pub denominated_performance: Option, + /// Start timestamp of the performance measring timeframe + #[typeshare(typescript(type = "number"))] + pub start_time: u64, + /// End timestamp of the performance measuring timeframe + #[typeshare(typescript(type = "number"))] + pub end_time: u64, + /// Ordder's input vaults isolated performance + pub inputs_vaults: Vec, + /// Ordder's output vaults isolated performance + pub outputs_vaults: Vec, +} + +impl OrderPerformance { + /// Given an order and its trades and optionally a timeframe, will calculates + /// the order performance, (apy and volume) + /// Trades must be sorted indesc order by timestamp, this is the case if + /// queried from subgraph using this lib functionalities + pub fn measure( + order: &Order, + trades: &[Trade], + start_timestamp: Option, + end_timestamp: Option, + ) -> Result { + if trades.is_empty() { + return Ok(OrderPerformance { + order_id: order.id.0.clone(), + order_hash: order.order_hash.0.clone(), + orderbook: order.orderbook.id.0.clone(), + start_time: start_timestamp.unwrap_or(0), + end_time: end_timestamp.unwrap_or(chrono::Utc::now().timestamp() as u64), + inputs_vaults: vec![], + outputs_vaults: vec![], + denominated_performance: None, + }); + } + let vols = get_vaults_vol(trades)?; + let vaults_apy = get_vaults_apy(trades, &vols, start_timestamp, end_timestamp)?; + + // build an OrderPerformance struct + let mut start_time = u64::MAX; + let mut end_time = 0_u64; + let mut inputs: Vec = vec![]; + let mut outputs: Vec = vec![]; + for (vault_apy, vault_vol) in vaults_apy.iter().zip(vols) { + if vault_apy.start_time < start_time { + start_time = vault_apy.start_time; + } + if vault_apy.end_time > end_time { + end_time = vault_apy.end_time; + } + if order + .inputs + .iter() + .any(|v| v.vault_id.0 == vault_apy.id && v.token == vault_apy.token) + { + inputs.push(VaultPerformance { + id: vault_apy.id.clone(), + token: vault_apy.token.clone(), + total_in_vol: vault_vol.total_in, + total_out_vol: vault_vol.total_out, + total_vol: vault_vol.total_vol, + net_vol: vault_vol.net_vol, + start_time: vault_apy.start_time, + end_time: vault_apy.end_time, + starting_capital: vault_apy.capital, + apy: vault_apy.apy, + }); + } + if order + .outputs + .iter() + .any(|v| v.vault_id.0 == vault_apy.id && v.token == vault_apy.token) + { + outputs.push(VaultPerformance { + id: vault_apy.id.clone(), + token: vault_apy.token.clone(), + total_in_vol: vault_vol.total_in, + total_out_vol: vault_vol.total_out, + total_vol: vault_vol.total_vol, + net_vol: vault_vol.net_vol, + start_time: vault_apy.start_time, + end_time: vault_apy.end_time, + starting_capital: vault_apy.capital, + apy: vault_apy.apy, + }); + } + } + let mut order_performance = OrderPerformance { + order_id: order.id.0.clone(), + order_hash: order.order_hash.0.clone(), + orderbook: order.orderbook.id.0.clone(), + start_time, + end_time, + inputs_vaults: inputs, + outputs_vaults: outputs, + denominated_performance: None, + }; + + // get pairs ratios + let pair_ratio_map = get_order_pairs_ratio(order, trades); + + // try to calculate all vaults capital and volume denominated into each of + // the order's tokens by checking if there is direct ratio between the tokens, + // multi path ratios are ignored currently and results in None for the APY. + // if there is a success for any of the denomination tokens, gather it in order + // of its net vol and pick the one with highest net vol. + // if there was no success with any of the order's tokens, simply return None + // for the APY. + let mut ordered_token_net_vol_map = BTreeMap::new(); + let mut full_apy_in_distinct_token_denominations = vec![]; + for token in &vaults_apy { + let mut noway = false; + let mut combined_capital = I256::ZERO; + let mut combined_net_vol = I256::ZERO; + let mut combined_annual_rate_vol = I256::ZERO; + let mut token_net_vol_map_converted_in_current_denomination = BTreeMap::new(); + for token_vault in &vaults_apy { + // time to year ratio + let annual_rate = annual_rate(token_vault.start_time, token_vault.end_time); + + // sum up all token vaults' capitals and vols in the current's iteration + // token denomination by using the direct ratio between the tokens + if token_vault.token == token.token { + combined_capital += token_vault.capital; + combined_net_vol += token_vault.net_vol; + combined_annual_rate_vol += token_vault + .net_vol + .saturating_mul(one_18().get_signed()) + .saturating_div(annual_rate); + token_net_vol_map_converted_in_current_denomination + .insert(token_vault.net_vol, &token.token); + } else { + let pair = TokenPair { + input: token.token.clone(), + output: token_vault.token.clone(), + }; + // convert to current denomination by the direct pair ratio if exists + if let Some(Some(ratio)) = pair_ratio_map.get(&pair) { + combined_capital += token_vault + .capital + .saturating_mul(*ratio) + .saturating_div(one_18().get_signed()); + combined_net_vol += token_vault + .net_vol + .saturating_mul(*ratio) + .saturating_div(one_18().get_signed()); + combined_annual_rate_vol += token_vault + .net_vol + .saturating_mul(*ratio) + .saturating_div(one_18().get_signed()) + .saturating_mul(one_18().get_signed()) + .saturating_div(annual_rate); + token_net_vol_map_converted_in_current_denomination.insert( + token_vault + .net_vol + .saturating_mul(*ratio) + .saturating_div(one_18().get_signed()), + &token_vault.token, + ); + } else { + noway = true; + break; + } + } + } + + // for every success apy calc in a token denomination, gather them in BTreeMap + // this means at the end we have all the successful apy calculated in each of + // the order's io tokens in order from highest to lowest. + if !noway { + if let Some(apy) = combined_annual_rate_vol + .saturating_mul(one_18().get_signed()) + .checked_div(combined_capital) + { + full_apy_in_distinct_token_denominations.push(DenominatedPerformance { + apy, + token: token.token.clone(), + starting_capital: combined_capital, + net_vol: combined_net_vol, + }); + } + } else { + token_net_vol_map_converted_in_current_denomination.clear(); + } + + // if we already have ordered token net vol in a denomination + // we dont need them in other denominations in order to pick + // the highest vol token as settelement denomination + if ordered_token_net_vol_map.is_empty() { + ordered_token_net_vol_map + .extend(token_net_vol_map_converted_in_current_denomination); + } + } + + // pick the denomination with highest net vol by iterating over tokens with + // highest vol to lowest and pick the first matching matching one + for (_, &token) in ordered_token_net_vol_map.iter().rev() { + if let Some(denominated_apy) = full_apy_in_distinct_token_denominations + .iter() + .find(|&v| &v.token == token) + { + order_performance.denominated_performance = Some(denominated_apy.clone()); + // return early as soon as a match is found + return Ok(order_performance); + } + } + + Ok(order_performance) + } +} + +/// Calculates an order's pairs' ratios from their last trades in a given list of trades +/// Trades must be sorted indesc order by timestamp, this is the case if queried from subgraph +/// using this lib functionalities +pub fn get_order_pairs_ratio(order: &Order, trades: &[Trade]) -> HashMap> { + let mut pair_ratio_map: HashMap> = HashMap::new(); + for input in &order.inputs { + for output in &order.outputs { + let pair_as_key = TokenPair { + input: input.token.clone(), + output: output.token.clone(), + }; + let inverse_pair_as_key = TokenPair { + input: output.token.clone(), + output: input.token.clone(), + }; + // if not same io token and ratio map doesnt already include them + if input.token != output.token + && !(pair_ratio_map.contains_key(&pair_as_key) + || pair_ratio_map.contains_key(&inverse_pair_as_key)) + { + // find this pairs(io or oi) latest tradetrades from list of order's + // trades, the calculate the pair ratio (in amount/out amount) and + // its inverse from the latest trade that involes these 2 tokens. + let ratio = trades + .iter() + .find(|v| { + (v.input_vault_balance_change.vault.token == input.token + && v.output_vault_balance_change.vault.token == output.token) + || (v.output_vault_balance_change.vault.token == input.token + && v.input_vault_balance_change.vault.token == output.token) + }) + .and_then(|latest_trade| { + // convert input and output amounts to 18 decimals point + // and then calculate the pair ratio + latest_trade + .ratio() + .ok() + .zip(latest_trade.inverse_ratio().ok()) + .map(|(ratio, inverse_ratio)| { + [I256::from_raw(ratio), I256::from_raw(inverse_ratio)] + }) + }); + + // io + pair_ratio_map.insert(pair_as_key, ratio.map(|v| v[0])); + // oi + pair_ratio_map.insert(inverse_pair_as_key, ratio.map(|v| v[1])); + } + } + } + + pair_ratio_map +} + +#[cfg(test)] +mod test { + use super::*; + use crate::types::common::{ + BigInt, Bytes, Order, Orderbook, TradeEvent, TradeStructPartialOrder, + TradeVaultBalanceChange, Transaction, Vault, VaultBalanceChangeVault, + }; + use alloy::primitives::{Address, B256}; + use std::str::FromStr; + + #[test] + fn test_get_pairs_ratio() { + let trades = get_trades(); + let [token1, token2] = get_tokens(); + let result = get_order_pairs_ratio(&get_order(), &trades); + let mut expected = HashMap::new(); + expected.insert( + TokenPair { + input: token2.clone(), + output: token1.clone(), + }, + Some(I256::from_str("285714285714285714").unwrap()), + ); + expected.insert( + TokenPair { + input: token1.clone(), + output: token2.clone(), + }, + Some(I256::from_str("3500000000000000000").unwrap()), + ); + + assert_eq!(result, expected); + } + + #[test] + fn test_get_order_performance() { + let order = get_order(); + let trades = get_trades(); + let [token1, token2] = get_tokens(); + let [vault1, vault2] = get_vault_ids(); + let token1_perf = VaultPerformance { + id: vault1.to_string(), + token: token1.clone(), + start_time: 1, + end_time: 10000001, + net_vol: I256::from_str("5000000000000000000").unwrap(), + starting_capital: I256::from_str("5000000000000000000").unwrap(), + apy: Some(I256::from_str("3153600000000000000").unwrap()), + total_in_vol: U256::from_str("7000000000000000000").unwrap(), + total_out_vol: U256::from_str("2000000000000000000").unwrap(), + total_vol: U256::from_str("9000000000000000000").unwrap(), + }; + let token2_perf = VaultPerformance { + id: vault2.to_string(), + token: token2.clone(), + start_time: 1, + end_time: 10000001, + net_vol: I256::from_str("3000000000000000000").unwrap(), + starting_capital: I256::from_str("5000000000000000000").unwrap(), + apy: Some(I256::from_str("1892160000000000000").unwrap()), + total_in_vol: U256::from_str("5000000000000000000").unwrap(), + total_out_vol: U256::from_str("2000000000000000000").unwrap(), + total_vol: U256::from_str("7000000000000000000").unwrap(), + }; + let result = OrderPerformance::measure(&order, &trades, Some(1), Some(10000001)).unwrap(); + let expected = OrderPerformance { + order_id: "order-id".to_string(), + order_hash: "".to_string(), + orderbook: "".to_string(), + start_time: 1, + end_time: 10000001, + inputs_vaults: vec![token1_perf.clone(), token2_perf.clone()], + outputs_vaults: vec![token1_perf.clone(), token2_perf.clone()], + denominated_performance: Some(DenominatedPerformance { + apy: I256::from_str("2172479999999999999").unwrap(), + token: token2, + net_vol: I256::from_str("4428571428571428570").unwrap(), + starting_capital: I256::from_str("6428571428571428570").unwrap(), + }), + }; + + assert_eq!(result, expected); + } + + fn get_vault_ids() -> [B256; 2] { + [ + B256::from_slice(&[0x11u8; 32]), + B256::from_slice(&[0x22u8; 32]), + ] + } + fn get_tokens() -> [Erc20; 2] { + let token1_address = Address::from_slice(&[0x11u8; 20]); + let token2_address = Address::from_slice(&[0x22u8; 20]); + let token1 = Erc20 { + id: Bytes(token1_address.to_string()), + address: Bytes(token1_address.to_string()), + name: Some("Token1".to_string()), + symbol: Some("Token1".to_string()), + decimals: Some(BigInt(18.to_string())), + }; + let token2 = Erc20 { + id: Bytes(token2_address.to_string()), + address: Bytes(token2_address.to_string()), + name: Some("Token2".to_string()), + symbol: Some("Token2".to_string()), + decimals: Some(BigInt(18.to_string())), + }; + [token1, token2] + } + fn get_order() -> Order { + let [vault_id1, vault_id2] = get_vault_ids(); + let [token1, token2] = get_tokens(); + let vault1 = Vault { + id: Bytes("".to_string()), + owner: Bytes("".to_string()), + vault_id: BigInt(vault_id1.to_string()), + balance: BigInt("".to_string()), + token: token1, + orderbook: Orderbook { + id: Bytes("".to_string()), + }, + orders_as_output: vec![], + orders_as_input: vec![], + balance_changes: vec![], + }; + let vault2 = Vault { + id: Bytes("".to_string()), + owner: Bytes("".to_string()), + vault_id: BigInt(vault_id2.to_string()), + balance: BigInt("".to_string()), + token: token2, + orderbook: Orderbook { + id: Bytes("".to_string()), + }, + orders_as_output: vec![], + orders_as_input: vec![], + balance_changes: vec![], + }; + Order { + id: Bytes("order-id".to_string()), + order_bytes: Bytes("".to_string()), + order_hash: Bytes("".to_string()), + owner: Bytes("".to_string()), + outputs: vec![vault1.clone(), vault2.clone()], + inputs: vec![vault1, vault2], + orderbook: Orderbook { + id: Bytes("".to_string()), + }, + active: true, + timestamp_added: BigInt("".to_string()), + meta: None, + add_events: vec![], + trades: vec![], + } + } + + fn get_trades() -> Vec { + let bytes = Bytes("".to_string()); + let bigint = BigInt("".to_string()); + let [vault_id1, vault_id2] = get_vault_ids(); + let [token1, token2] = get_tokens(); + let trade1 = Trade { + id: bytes.clone(), + order: TradeStructPartialOrder { + id: bytes.clone(), + order_hash: bytes.clone(), + }, + trade_event: TradeEvent { + sender: bytes.clone(), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: bigint.clone(), + }, + }, + timestamp: BigInt("1".to_string()), + orderbook: Orderbook { id: bytes.clone() }, + output_vault_balance_change: TradeVaultBalanceChange { + id: bytes.clone(), + __typename: "TradeVaultBalanceChange".to_string(), + amount: BigInt("-2000000000000000000".to_string()), + new_vault_balance: BigInt("2000000000000000000".to_string()), + old_vault_balance: bigint.clone(), + vault: VaultBalanceChangeVault { + id: bytes.clone(), + token: token1.clone(), + vault_id: BigInt(vault_id1.to_string()), + }, + timestamp: BigInt("1".to_string()), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: BigInt("1".to_string()), + }, + orderbook: Orderbook { id: bytes.clone() }, + }, + input_vault_balance_change: TradeVaultBalanceChange { + id: bytes.clone(), + __typename: "TradeVaultBalanceChange".to_string(), + amount: BigInt("5000000000000000000".to_string()), + new_vault_balance: BigInt("2000000000000000000".to_string()), + old_vault_balance: bigint.clone(), + vault: VaultBalanceChangeVault { + id: bytes.clone(), + token: token2.clone(), + vault_id: BigInt(vault_id2.to_string()), + }, + timestamp: BigInt("1".to_string()), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: BigInt("1".to_string()), + }, + orderbook: Orderbook { id: bytes.clone() }, + }, + }; + let trade2 = Trade { + id: bytes.clone(), + order: TradeStructPartialOrder { + id: bytes.clone(), + order_hash: bytes.clone(), + }, + trade_event: TradeEvent { + sender: bytes.clone(), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: bigint.clone(), + }, + }, + timestamp: BigInt("2".to_string()), + orderbook: Orderbook { id: bytes.clone() }, + output_vault_balance_change: TradeVaultBalanceChange { + id: bytes.clone(), + __typename: "TradeVaultBalanceChange".to_string(), + amount: BigInt("-2000000000000000000".to_string()), + new_vault_balance: BigInt("5000000000000000000".to_string()), + old_vault_balance: bigint.clone(), + vault: VaultBalanceChangeVault { + id: bytes.clone(), + token: token2.clone(), + vault_id: BigInt(vault_id2.to_string()), + }, + timestamp: BigInt("2".to_string()), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: BigInt("1".to_string()), + }, + orderbook: Orderbook { id: bytes.clone() }, + }, + input_vault_balance_change: TradeVaultBalanceChange { + id: bytes.clone(), + __typename: "TradeVaultBalanceChange".to_string(), + amount: BigInt("7000000000000000000".to_string()), + new_vault_balance: BigInt("5000000000000000000".to_string()), + old_vault_balance: bigint.clone(), + vault: VaultBalanceChangeVault { + id: bytes.clone(), + token: token1.clone(), + vault_id: BigInt(vault_id1.to_string()), + }, + timestamp: BigInt("2".to_string()), + transaction: Transaction { + id: bytes.clone(), + from: bytes.clone(), + block_number: bigint.clone(), + timestamp: BigInt("1".to_string()), + }, + orderbook: Orderbook { id: bytes.clone() }, + }, + }; + vec![trade2, trade1] + } +} diff --git a/crates/subgraph/src/utils/mod.rs b/crates/subgraph/src/utils/mod.rs index 547742a16..9cc9633d8 100644 --- a/crates/subgraph/src/utils/mod.rs +++ b/crates/subgraph/src/utils/mod.rs @@ -1,4 +1,7 @@ -use alloy::primitives::utils::{format_units, parse_units, ParseUnits, Unit, UnitsError}; +use alloy::primitives::{ + utils::{format_units, parse_units, ParseUnits, Unit, UnitsError}, + I256, U256, +}; use chrono::TimeDelta; mod order_id; @@ -9,12 +12,14 @@ pub use slice_list::*; /// Returns 18 point decimals 1 as I256/U256 pub fn one_18() -> ParseUnits { - parse_units("1", 18).unwrap() + ParseUnits::U256(U256::from(1_000_000_000_000_000_000_u64)) } /// Returns YEAR as 18 point decimals as I256/U256 pub fn year_18() -> ParseUnits { - parse_units(&TimeDelta::days(365).num_seconds().to_string(), 18).unwrap() + ParseUnits::U256( + U256::from(TimeDelta::days(365).num_seconds()).saturating_mul(one_18().get_absolute()), + ) } /// Converts a U256/I256 value to a 18 fixed point U256/I256 given the decimals point @@ -25,6 +30,13 @@ pub fn to_18_decimals>( parse_units(&format_units(amount, decimals)?, 18) } +/// Returns annual rate as 18 point decimals as I256 +pub fn annual_rate(start: u64, end: u64) -> I256 { + I256::from_raw(U256::from(end - start).saturating_mul(one_18().get_absolute())) + .saturating_mul(one_18().get_signed()) + .saturating_div(year_18().get_signed()) +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/subgraph/src/vol.rs b/crates/subgraph/src/vol.rs index 416787da0..fa897ffa9 100644 --- a/crates/subgraph/src/vol.rs +++ b/crates/subgraph/src/vol.rs @@ -1,5 +1,9 @@ -use crate::types::common::{Erc20, Trade}; -use alloy::primitives::{ruint::ParseError, I256, U256}; +use crate::{ + error::ParseNumberError, + types::common::{Erc20, Trade}, + utils::to_18_decimals, +}; +use alloy::primitives::{ruint::ParseError, utils::ParseUnits, I256, U256}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use typeshare::typeshare; @@ -20,7 +24,7 @@ pub struct VaultVolume { pub net_vol: I256, } -/// Get the vaults volume from array of trades +/// Get the vaults volume from array of trades of an owner pub fn get_vaults_vol(trades: &[Trade]) -> Result, ParseError> { let mut vaults_vol: Vec = vec![]; for trade in trades { @@ -108,6 +112,29 @@ pub fn get_vaults_vol(trades: &[Trade]) -> Result, ParseError> Ok(vaults_vol) } +impl VaultVolume { + /// Creates a new instance of self with all volume values as 18 decimals point + pub fn to_18_decimals(&self) -> Result { + let token_decimals = self + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18"); + Ok(VaultVolume { + id: self.id.clone(), + token: self.token.clone(), + total_in: to_18_decimals(ParseUnits::U256(self.total_in), token_decimals)? + .get_absolute(), + total_out: to_18_decimals(ParseUnits::U256(self.total_out), token_decimals)? + .get_absolute(), + total_vol: to_18_decimals(ParseUnits::U256(self.total_vol), token_decimals)? + .get_absolute(), + net_vol: to_18_decimals(ParseUnits::I256(self.net_vol), token_decimals)?.get_signed(), + }) + } +} + #[cfg(test)] mod test { use super::*; @@ -118,7 +145,7 @@ mod test { use alloy::primitives::{Address, B256}; #[test] - fn test_get_vaults_vol() { + fn test_vaults_vol() { let bytes = Bytes("".to_string()); let bigint = BigInt("".to_string()); let token1_address = Address::random(); @@ -278,4 +305,36 @@ mod test { assert_eq!(result, expected); } + + #[test] + fn test_to_18_decimals() { + let token_address = Address::random(); + let token = Erc20 { + id: Bytes(token_address.to_string()), + address: Bytes(token_address.to_string()), + name: Some("Token".to_string()), + symbol: Some("Token".to_string()), + decimals: Some(BigInt(6.to_string())), + }; + let vault_vol = VaultVolume { + id: "vault-id".to_string(), + token: token.clone(), + total_in: U256::from(20_500_000), + total_out: U256::from(30_000_000), + total_vol: U256::from(50_500_000), + net_vol: I256::from_str("-9_500_000").unwrap(), + }; + + let result = vault_vol.to_18_decimals().unwrap(); + let expected = VaultVolume { + id: "vault-id".to_string(), + token, + total_in: U256::from_str("20_500_000_000_000_000_000").unwrap(), + total_out: U256::from_str("30_000_000_000_000_000_000").unwrap(), + total_vol: U256::from_str("50_500_000_000_000_000_000").unwrap(), + net_vol: I256::from_str("-9_500_000_000_000_000_000").unwrap(), + }; + + assert_eq!(result, expected); + } } diff --git a/tauri-app/src-tauri/src/commands/order_take.rs b/tauri-app/src-tauri/src/commands/order_take.rs index 586c9f875..ffe4eee0c 100644 --- a/tauri-app/src-tauri/src/commands/order_take.rs +++ b/tauri-app/src-tauri/src/commands/order_take.rs @@ -2,7 +2,8 @@ use crate::error::CommandResult; use rain_orderbook_common::{ csv::TryIntoCsv, subgraph::SubgraphArgs, types::FlattenError, types::OrderTakeFlattened, }; -use rain_orderbook_subgraph_client::apy::{get_order_apy, OrderAPY}; + +use rain_orderbook_subgraph_client::types::order::OrderPerformance; use rain_orderbook_subgraph_client::vol::VaultVolume; use rain_orderbook_subgraph_client::{types::common::*, PaginationArgs}; use std::fs; @@ -82,21 +83,14 @@ pub async fn order_trades_count( } #[tauri::command] -pub async fn order_apy( +pub async fn order_performance( order_id: String, subgraph_args: SubgraphArgs, start_timestamp: Option, end_timestamp: Option, -) -> CommandResult { +) -> CommandResult { let client = subgraph_args.to_subgraph_client().await?; - let order = client.order_detail(order_id.clone().into()).await?; - let trades = client - .order_trades_list_all(order_id.into(), start_timestamp, end_timestamp) - .await?; - Ok(get_order_apy( - &order, - &trades, - start_timestamp, - end_timestamp, - )?) + Ok(client + .order_performance(order_id.into(), start_timestamp, end_timestamp) + .await?) } diff --git a/tauri-app/src-tauri/src/main.rs b/tauri-app/src-tauri/src/main.rs index f5b6dc916..5d9f26598 100644 --- a/tauri-app/src-tauri/src/main.rs +++ b/tauri-app/src-tauri/src/main.rs @@ -19,7 +19,7 @@ use commands::order::{ }; use commands::order_quote::{batch_order_quotes, debug_order_quote}; use commands::order_take::{ - order_apy, order_trades_count, order_trades_list, order_trades_list_write_csv, + order_performance, order_trades_count, order_trades_list, order_trades_list_write_csv, order_vaults_volume, }; use commands::trade_debug::debug_trade; @@ -84,7 +84,7 @@ fn run_tauri_app() { validate_raindex_version, order_vaults_volume, order_trades_count, - order_apy, + order_performance, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/tauri-app/src/lib/components/tables/OrderAPY.svelte b/tauri-app/src/lib/components/tables/OrderAPY.svelte index 13c0cf7b3..f4baad1af 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.svelte +++ b/tauri-app/src/lib/components/tables/OrderAPY.svelte @@ -32,12 +32,12 @@ - {item.denominatedApy - ? bigintStringToPercentage(item.denominatedApy.apy, 18, 5) + + {item.denominatedPerformance + ? bigintStringToPercentage(item.denominatedPerformance.apy, 18, 5) + '% in ' + - (item.denominatedApy.token.symbol ?? - item.denominatedApy.token.name ?? - item.denominatedApy.token.address) + (item.denominatedPerformance.token.symbol ?? + item.denominatedPerformance.token.name ?? + item.denominatedPerformance.token.address) : 'Unavailable APY'} diff --git a/tauri-app/src/lib/components/tables/OrderAPY.test.ts b/tauri-app/src/lib/components/tables/OrderAPY.test.ts index 540e5da01..8100c8940 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.test.ts +++ b/tauri-app/src/lib/components/tables/OrderAPY.test.ts @@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/svelte'; import { test, vi } from 'vitest'; import { expect } from '$lib/test/matchers'; import { mockIPC } from '@tauri-apps/api/mocks'; -import type { OrderAPY } from '$lib/typeshare/subgraphTypes'; +import type { OrderPerformance } from '$lib/typeshare/subgraphTypes'; import { QueryClient } from '@tanstack/svelte-query'; import OrderApy from './OrderAPY.svelte'; import { bigintStringToPercentage } from '$lib/utils/number'; @@ -32,11 +32,12 @@ vi.mock('$lib/services/modal', async () => { }; }); -const mockOrderApy: OrderAPY[] = [ +const mockOrderApy: OrderPerformance[] = [ { orderId: '1', orderHash: '1', - denominatedApy: { + orderbook: '1', + denominatedPerformance: { apy: '1200000000000000000', token: { id: 'output_token', @@ -45,11 +46,13 @@ const mockOrderApy: OrderAPY[] = [ symbol: 'output_token', decimals: '0', }, + netVol: '0', + startingCapital: '0', }, startTime: 1, endTime: 2, - inputsTokenVaultApy: [], - outputsTokenVaultApy: [], + inputsVaults: [], + outputsVaults: [], }, ]; @@ -73,7 +76,7 @@ test('renders table with correct data', async () => { // checking for (let i = 0; i < mockOrderApy.length; i++) { - const display = bigintStringToPercentage(mockOrderApy[i].denominatedApy!.apy, 18, 5); + const display = bigintStringToPercentage(mockOrderApy[i].denominatedPerformance!.apy, 18, 5); expect(rows[i]).toHaveTextContent(display); } }); diff --git a/tauri-app/src/lib/queries/orderTradesList.ts b/tauri-app/src/lib/queries/orderTradesList.ts index f86ac85dd..802a6136b 100644 --- a/tauri-app/src/lib/queries/orderTradesList.ts +++ b/tauri-app/src/lib/queries/orderTradesList.ts @@ -1,4 +1,4 @@ -import type { OrderAPY, Trade, VaultVolume } from '$lib/typeshare/subgraphTypes'; +import type { OrderPerformance, Trade, VaultVolume } from '$lib/typeshare/subgraphTypes'; import { invoke } from '@tauri-apps/api'; import { DEFAULT_PAGE_SIZE } from './constants'; import { prepareHistoricalOrderChartData } from '$lib/services/historicalOrderCharts'; @@ -103,7 +103,7 @@ export const getOrderApy = async ( return []; } return [ - await invoke('order_apy', { + await invoke('order_apy', { orderId: id, subgraphArgs: { url }, startTimestamp,