From 79f5f406dbabbd5b5692904c71bbd7fc3bf52f50 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Wed, 16 Oct 2024 04:58:35 +0000 Subject: [PATCH 01/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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, From 9eeb517ef440df443d5b97cf1b2d3b72b94a432d Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 14 Nov 2024 23:25:06 +0000 Subject: [PATCH 29/50] Delete error.rs --- crates/subgraph/src/error.rs | 65 ------------------------------------ 1 file changed, 65 deletions(-) delete mode 100644 crates/subgraph/src/error.rs diff --git a/crates/subgraph/src/error.rs b/crates/subgraph/src/error.rs deleted file mode 100644 index cc146e7ce..000000000 --- a/crates/subgraph/src/error.rs +++ /dev/null @@ -1,65 +0,0 @@ -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() - } -} From 32df562e8657b166013a62c57b4e25aecc8121b0 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 18 Nov 2024 01:58:53 +0000 Subject: [PATCH 30/50] update --- Cargo.lock | 2 + crates/subgraph/Cargo.toml | 4 +- crates/subgraph/src/lib.rs | 3 +- crates/subgraph/src/orderbook_client.rs | 24 +- crates/subgraph/src/{ => performance}/apy.rs | 190 +++-- crates/subgraph/src/performance/mod.rs | 20 + .../src/performance/order_performance.rs | 759 ++++++++++++++++++ crates/subgraph/src/{ => performance}/vol.rs | 163 ++-- crates/subgraph/src/types/impls.rs | 100 +-- crates/subgraph/src/types/order.rs | 613 +------------- crates/subgraph/src/utils/mod.rs | 73 +- flake.nix | 2 +- tauri-app/src-tauri/Cargo.lock | 11 + .../src-tauri/src/commands/order_take.rs | 4 +- .../lib/components/tables/OrderAPY.test.ts | 1 + .../tables/OrderVaultsVolTable.svelte | 13 +- .../tables/OrderVaultsVolTable.test.ts | 28 +- 17 files changed, 1120 insertions(+), 890 deletions(-) rename crates/subgraph/src/{ => performance}/apy.rs (72%) create mode 100644 crates/subgraph/src/performance/mod.rs create mode 100644 crates/subgraph/src/performance/order_performance.rs rename crates/subgraph/src/{ => performance}/vol.rs (71%) diff --git a/Cargo.lock b/Cargo.lock index f12f89f10..10120bc22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6523,7 +6523,9 @@ dependencies = [ "httpmock", "insta", "js-sys", + "once_cell", "rain_orderbook_bindings", + "rain_orderbook_math", "reqwest 0.12.5", "serde", "serde-wasm-bindgen 0.6.5", diff --git a/crates/subgraph/Cargo.toml b/crates/subgraph/Cargo.toml index d5758b98e..e53a08c0f 100644 --- a/crates/subgraph/Cargo.toml +++ b/crates/subgraph/Cargo.toml @@ -17,9 +17,11 @@ serde = { workspace = true } serde_json = { workspace = true } alloy = { workspace = true, features = ["rand"] } rain_orderbook_bindings = { workspace = true } +rain_orderbook_math = { workspace = true } chrono = { workspace = true } -url = { workspace = true } +url = { workspace = true, features = ["serde"] } cynic-introspection = "3.7.3" +once_cell = { workspace = true } futures = "0.3.17" tsify = { version = "0.4.5", default-features = false, features = ["js", "wasm-bindgen"] } wasm-bindgen = { version = "0.2.92" } diff --git a/crates/subgraph/src/lib.rs b/crates/subgraph/src/lib.rs index 2b0c95c51..424fb1c31 100644 --- a/crates/subgraph/src/lib.rs +++ b/crates/subgraph/src/lib.rs @@ -1,13 +1,12 @@ -pub mod apy; mod cynic_client; mod multi_orderbook_client; mod orderbook_client; mod pagination; +pub mod performance; pub mod types; pub mod utils; pub mod validate; mod vault_balance_changes_query; -pub mod vol; #[cynic::schema("orderbook")] pub mod schema {} diff --git a/crates/subgraph/src/orderbook_client.rs b/crates/subgraph/src/orderbook_client.rs index 897524b50..7c9b4e938 100644 --- a/crates/subgraph/src/orderbook_client.rs +++ b/crates/subgraph/src/orderbook_client.rs @@ -1,18 +1,19 @@ -use crate::cynic_client::CynicClient; -use crate::error::OrderbookSubgraphClientError; -use crate::pagination::{PaginationArgs, PaginationClient}; +use crate::cynic_client::{CynicClient, CynicClientError}; +use crate::pagination::{PaginationArgs, PaginationClient, PaginationClientError}; +use crate::performance::vol::{get_vaults_vol, VaultVolume}; +use crate::performance::OrderPerformance; use crate::types::common::*; use crate::types::order::{ BatchOrderDetailQuery, BatchOrderDetailQueryVariables, OrderDetailQuery, OrderIdList, - OrderPerformance, OrdersListQuery, + OrdersListQuery, }; use crate::types::order_trade::{OrderTradeDetailQuery, OrderTradesListQuery}; use crate::types::vault::{VaultDetailQuery, VaultsListQuery}; use crate::vault_balance_changes_query::VaultBalanceChangesListPageQueryClient; -use crate::vol::{get_vaults_vol, VaultVolume}; use cynic::Id; use reqwest::Url; - +use std::num::ParseIntError; +use thiserror::Error; #[cfg(target_family = "wasm")] use wasm_bindgen::{JsError, JsValue}; @@ -30,6 +31,10 @@ pub enum OrderbookSubgraphClientError { ParseError(#[from] alloy::primitives::ruint::ParseError), #[error(transparent)] UrlParseError(#[from] url::ParseError), + #[error(transparent)] + PerformanceError(#[from] crate::performance::PerformanceError), + #[error(transparent)] + ParseIntError(#[from] ParseIntError), #[cfg(target_family = "wasm")] #[error(transparent)] SerdeWasmBindgenError(#[from] serde_wasm_bindgen::Error), @@ -242,7 +247,12 @@ impl OrderbookSubgraphClient { let trades = self .order_trades_list_all(order_id, start_timestamp, end_timestamp) .await?; - OrderPerformance::measure(&order, &trades, start_timestamp, end_timestamp) + Ok(OrderPerformance::measure( + &order, + &trades, + start_timestamp, + end_timestamp, + )?) } /// Fetch single vault diff --git a/crates/subgraph/src/apy.rs b/crates/subgraph/src/performance/apy.rs similarity index 72% rename from crates/subgraph/src/apy.rs rename to crates/subgraph/src/performance/apy.rs index 56d3b7c9a..5395f17db 100644 --- a/crates/subgraph/src/apy.rs +++ b/crates/subgraph/src/performance/apy.rs @@ -1,34 +1,40 @@ +use super::PerformanceError; use crate::{ + performance::vol::VaultVolume, types::common::{Erc20, Trade}, - utils::{annual_rate, one_18, to_18_decimals}, - vol::VaultVolume, - OrderbookSubgraphClientError, + utils::annual_rate, }; -use alloy::primitives::{utils::ParseUnits, I256, U256}; +use alloy::primitives::U256; use chrono::TimeDelta; +use rain_orderbook_math::BigUintMath; use serde::{Deserialize, Serialize}; use std::str::FromStr; use typeshare::typeshare; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(rename_all = "camelCase")] #[typeshare] -pub struct VaultAPY { - pub id: String, - pub token: Erc20, +pub struct APYDetails { #[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: I256, - #[typeshare(typescript(type = "string"))] - pub apy: Option, + pub net_vol: U256, + pub capital: U256, + pub apy: U256, + pub is_neg: bool, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct VaultAPY { + pub id: String, + pub token: Erc20, + pub apy_details: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(rename_all = "camelCase")] #[typeshare] pub struct TokenPair { @@ -44,10 +50,10 @@ pub fn get_vaults_apy( vols: &[VaultVolume], start_timestamp: Option, end_timestamp: Option, -) -> Result, OrderbookSubgraphClientError> { +) -> Result, PerformanceError> { let mut token_vaults_apy: Vec = vec![]; for vol in vols { - let vol = vol.to_18_decimals()?; + let vol = vol.scale_18()?; // this token vault trades in desc order by timestamp let vault_trades = trades .iter() @@ -59,6 +65,15 @@ pub fn get_vaults_apy( }) .collect::>(); + if vault_trades.is_empty() { + token_vaults_apy.push(VaultAPY { + id: vol.id.clone(), + token: vol.token.clone(), + apy_details: None, + }); + continue; + } + // 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]; @@ -88,21 +103,18 @@ pub fn get_vaults_apy( } else { &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() - }); + let starting_capital = U256::from_str(&vault_balance_change.new_vault_balance.0)? + .scale_18( + vault_balance_change + .vault + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18") + .parse()?, + ) + .map_err(PerformanceError::from)?; // the time range for this token vault let mut start = u64::from_str(&first_trade.timestamp.0)?; @@ -114,30 +126,41 @@ pub fn get_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.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() - }); + let apy = if !starting_capital.is_zero() { + match annual_rate(start, end) { + Err(_) => None, + Ok(annual_rate_18) => vol + .vol_details + .net_vol + .div_18(starting_capital) + .ok() + .and_then(|v| v.div_18(annual_rate_18).ok()), + } + } else { + None + }; // this token vault apy - token_vaults_apy.push(VaultAPY { - id: vol.id.clone(), - token: vol.token.clone(), - start_time: start, - end_time: end, - apy, - net_vol: vol.net_vol, - capital: starting_capital - .unwrap_or(ParseUnits::I256(I256::ZERO)) - .get_signed(), - }); + if let Some(apy) = apy { + token_vaults_apy.push(VaultAPY { + id: vol.id.clone(), + token: vol.token.clone(), + apy_details: Some(APYDetails { + start_time: start, + end_time: end, + apy, + is_neg: vol.is_net_vol_negative(), + net_vol: vol.vol_details.net_vol, + capital: starting_capital, + }), + }); + } else { + token_vaults_apy.push(VaultAPY { + id: vol.id.clone(), + token: vol.token.clone(), + apy_details: None, + }); + } } Ok(token_vaults_apy) @@ -146,9 +169,12 @@ pub fn get_vaults_apy( #[cfg(test)] mod test { use super::*; - use crate::types::common::{ - BigInt, Bytes, Orderbook, TradeEvent, TradeStructPartialOrder, TradeVaultBalanceChange, - Transaction, VaultBalanceChangeVault, + use crate::{ + performance::vol::VolumeDetails, + types::common::{ + BigInt, Bytes, Orderbook, TradeEvent, TradeStructPartialOrder, TradeVaultBalanceChange, + Transaction, VaultBalanceChangeVault, + }, }; use alloy::primitives::{Address, B256}; @@ -160,18 +186,22 @@ mod test { 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(), + vol_details: VolumeDetails { + total_in: U256::ZERO, + total_out: U256::ZERO, + total_vol: U256::ZERO, + net_vol: U256::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(), + vol_details: VolumeDetails { + total_in: U256::ZERO, + total_out: U256::ZERO, + total_vol: U256::ZERO, + net_vol: U256::from_str("2000000000000000000").unwrap(), + }, }; let result = get_vaults_apy(&trades, &[vault_vol1, vault_vol2], Some(1), Some(10000001)).unwrap(); @@ -179,22 +209,28 @@ mod test { VaultAPY { id: vault1.to_string(), token: token1.clone(), - start_time: 1, - end_time: 10000001, - net_vol: I256::from_str("1000000000000000000").unwrap(), - capital: I256::from_str("5000000000000000000").unwrap(), - // (1/5) / (10000001_end - 1_start / 31_536_00_year) - apy: Some(I256::from_str("630720000000000000").unwrap()), + apy_details: Some(APYDetails { + start_time: 1, + end_time: 10000001, + net_vol: U256::from_str("1000000000000000000").unwrap(), + capital: U256::from_str("5000000000000000000").unwrap(), + // (1/5) / (10000001_end - 1_start / 31_536_00_year) + apy: U256::from_str("630720000000000000").unwrap(), + is_neg: false, + }), }, VaultAPY { id: vault2.to_string(), token: token2.clone(), - start_time: 1, - end_time: 10000001, - net_vol: I256::from_str("2000000000000000000").unwrap(), - capital: I256::from_str("5000000000000000000").unwrap(), - // (2/5) / ((10000001_end - 1_start) / 31_536_00_year) - apy: Some(I256::from_str("1261440000000000000").unwrap()), + apy_details: Some(APYDetails { + start_time: 1, + end_time: 10000001, + net_vol: U256::from_str("2000000000000000000").unwrap(), + capital: U256::from_str("5000000000000000000").unwrap(), + // (2/5) / ((10000001_end - 1_start) / 31_536_00_year) + apy: U256::from_str("1261440000000000000").unwrap(), + is_neg: false, + }), }, ]; diff --git a/crates/subgraph/src/performance/mod.rs b/crates/subgraph/src/performance/mod.rs new file mode 100644 index 000000000..c07a8ff5a --- /dev/null +++ b/crates/subgraph/src/performance/mod.rs @@ -0,0 +1,20 @@ +use alloy::primitives::ruint::ParseError; +use rain_orderbook_math::MathError; +use std::num::ParseIntError; +use thiserror::Error; + +pub mod apy; +mod order_performance; +pub mod vol; + +pub use order_performance::*; + +#[derive(Error, Debug)] +pub enum PerformanceError { + #[error(transparent)] + MathError(#[from] MathError), + #[error(transparent)] + ParseUnsignedError(#[from] ParseError), + #[error(transparent)] + ParseIntError(#[from] ParseIntError), +} diff --git a/crates/subgraph/src/performance/order_performance.rs b/crates/subgraph/src/performance/order_performance.rs new file mode 100644 index 000000000..393f6d8b7 --- /dev/null +++ b/crates/subgraph/src/performance/order_performance.rs @@ -0,0 +1,759 @@ +use super::apy::APYDetails; +use super::vol::VolumeDetails; +use super::PerformanceError; +use crate::performance::apy::{get_vaults_apy, TokenPair}; +use crate::utils::annual_rate; +use crate::{ + performance::vol::get_vaults_vol, + types::common::{Erc20, Order, Trade}, +}; +use alloy::primitives::U256; +use rain_orderbook_math::BigUintMath; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use std::collections::HashMap; +use typeshare::typeshare; + +#[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, + /// vol segment + pub vol_details: VolumeDetails, + /// apy segment + pub apy_details: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct DenominatedPerformance { + /// The denomination token + pub token: Erc20, + /// Order's APY raw value + pub apy: U256, + /// Order's net vol + pub net_vol: U256, + /// Determines if apy and net_vol are negative or not + pub is_neg: bool, + /// Order's starting capital + pub starting_capital: U256, +} + +#[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 vaults_vol = get_vaults_vol(trades)?; + let vaults_apy = get_vaults_apy(trades, &vaults_vol, 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(vaults_vol) { + vault_apy.apy_details.inspect(|v| { + if v.start_time < start_time { + start_time = v.start_time; + } + if v.end_time > end_time { + end_time = v.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(), + apy_details: vault_apy.apy_details, + vol_details: vault_vol.vol_details, + }); + } + 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(), + apy_details: vault_apy.apy_details, + vol_details: vault_vol.vol_details, + }); + } + } + 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 processed_tokens: Vec<&Erc20> = vec![]; + let mut all_tokens_vols_list: Vec = vec![]; + let mut full_apy_in_distinct_token_denominations = vec![]; + for token in &vaults_apy { + // skip if token is alreaedy processed + if processed_tokens.contains(&&token.token) { + continue; + } else { + processed_tokens.push(&token.token); + } + let mut noway = false; + let mut net_vol_is_neg = false; + let mut net_vol_rate_is_neg = false; + let mut combined_capital = U256::ZERO; + let mut combined_net_vol = U256::ZERO; + let mut combined_annual_rate_vol = U256::ZERO; + let mut current_token_vol_list: Vec = vec![]; + for token_vault in &vaults_apy { + if let Some(apy_details) = token_vault.apy_details { + // time to year ratio + let annual_rate = annual_rate(apy_details.start_time, apy_details.end_time) + .map_err(PerformanceError::from)?; + + // 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 += apy_details.capital; + if apy_details.is_neg == net_vol_is_neg { + combined_net_vol += apy_details.net_vol; + } else if net_vol_is_neg { + if apy_details.net_vol >= combined_net_vol { + net_vol_is_neg = false; + combined_net_vol = apy_details.net_vol - combined_net_vol; + } else { + combined_net_vol -= apy_details.net_vol; + } + } else if combined_net_vol >= apy_details.net_vol { + combined_net_vol -= apy_details.net_vol; + } else { + net_vol_is_neg = true; + combined_net_vol = apy_details.net_vol - combined_net_vol; + } + + let annual_rate_vol = apy_details + .net_vol + .div_18(annual_rate) + .map_err(PerformanceError::from)?; + if apy_details.is_neg == net_vol_rate_is_neg { + combined_annual_rate_vol += annual_rate_vol; + } else if net_vol_rate_is_neg { + if annual_rate_vol >= combined_annual_rate_vol { + net_vol_rate_is_neg = false; + combined_annual_rate_vol = + annual_rate_vol - combined_annual_rate_vol; + } else { + combined_annual_rate_vol -= annual_rate_vol; + } + } else if combined_annual_rate_vol >= annual_rate_vol { + combined_annual_rate_vol -= annual_rate_vol; + } else { + net_vol_is_neg = true; + combined_annual_rate_vol = annual_rate_vol - combined_annual_rate_vol; + } + + current_token_vol_list.push(TokenDenominationVol { + net_vol: apy_details.net_vol, + is_neg: apy_details.is_neg, + token: &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) { + let capital_converted = apy_details + .capital + .mul_18(*ratio) + .map_err(PerformanceError::from)?; + combined_capital += capital_converted; + + let net_vol_converted = apy_details + .net_vol + .mul_18(*ratio) + .map_err(PerformanceError::from)?; + if apy_details.is_neg == net_vol_is_neg { + combined_net_vol += net_vol_converted; + } else if net_vol_is_neg { + if net_vol_converted >= combined_net_vol { + net_vol_is_neg = false; + combined_net_vol = net_vol_converted - combined_net_vol; + } else { + combined_net_vol -= net_vol_converted; + } + } else if combined_net_vol >= net_vol_converted { + combined_net_vol -= net_vol_converted; + } else { + net_vol_is_neg = true; + combined_net_vol = net_vol_converted - combined_net_vol; + } + + let annual_rate_vol_converted = net_vol_converted + .div_18(annual_rate) + .map_err(PerformanceError::from)?; + if apy_details.is_neg == net_vol_rate_is_neg { + combined_annual_rate_vol += annual_rate_vol_converted; + } else if net_vol_rate_is_neg { + if annual_rate_vol_converted >= combined_annual_rate_vol { + net_vol_rate_is_neg = false; + combined_annual_rate_vol = + annual_rate_vol_converted - combined_annual_rate_vol; + } else { + combined_annual_rate_vol -= annual_rate_vol_converted; + } + } else if combined_annual_rate_vol >= annual_rate_vol_converted { + combined_annual_rate_vol -= annual_rate_vol_converted; + } else { + net_vol_is_neg = true; + combined_annual_rate_vol = + annual_rate_vol_converted - combined_annual_rate_vol; + } + + current_token_vol_list.push(TokenDenominationVol { + net_vol: net_vol_converted, + is_neg: apy_details.is_neg, + token: &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 Ok(apy) = combined_annual_rate_vol.div_18(combined_capital) { + full_apy_in_distinct_token_denominations.push(DenominatedPerformance { + apy, + token: token.token.clone(), + starting_capital: combined_capital, + net_vol: combined_net_vol, + is_neg: net_vol_rate_is_neg, + }); + } + } else { + current_token_vol_list.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 all_tokens_vols_list.is_empty() { + all_tokens_vols_list.extend(current_token_vol_list); + } + } + + // pick the denomination with highest net vol by iterating over tokens with + // highest vol to lowest and pick the first matching one + all_tokens_vols_list.sort(); + for token in all_tokens_vols_list.iter().rev() { + if let Some(denominated_apy) = full_apy_in_distinct_token_denominations + .iter() + .find(|&v| &v.token == 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)| [ratio, 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 +} + +/// helper struct that provides sorting tokens based on their combined net vol +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct TokenDenominationVol<'a> { + token: &'a Erc20, + net_vol: U256, + is_neg: bool, +} +impl<'a> Ord for TokenDenominationVol<'a> { + fn clamp(self, _min: Self, _max: Self) -> Self + where + Self: Sized, + Self: PartialOrd, + { + self + } + fn cmp(&self, other: &Self) -> Ordering { + if self.is_neg == other.is_neg { + match self.net_vol.cmp(&other.net_vol) { + Ordering::Greater => { + if self.is_neg { + Ordering::Less + } else { + Ordering::Greater + } + } + Ordering::Less => { + if self.is_neg { + Ordering::Greater + } else { + Ordering::Less + } + } + Ordering::Equal => Ordering::Equal, + } + } else if self.is_neg { + Ordering::Less + } else { + Ordering::Greater + } + } + fn min(self, other: Self) -> Self + where + Self: Sized, + { + match self.cmp(&other) { + Ordering::Greater => other, + Ordering::Less => self, + Ordering::Equal => self, + } + } + fn max(self, other: Self) -> Self + where + Self: Sized, + { + match self.cmp(&other) { + Ordering::Greater => self, + Ordering::Less => other, + Ordering::Equal => self, + } + } +} +impl<'a> PartialOrd for TokenDenominationVol<'a> { + fn ge(&self, other: &Self) -> bool { + !matches!(self.cmp(other), Ordering::Less) + } + fn le(&self, other: &Self) -> bool { + !matches!(self.cmp(other), Ordering::Greater) + } + fn gt(&self, other: &Self) -> bool { + matches!(self.cmp(other), Ordering::Greater) + } + fn lt(&self, other: &Self) -> bool { + matches!(self.cmp(other), Ordering::Less) + } + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[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(U256::from_str("285714285714285714").unwrap()), + ); + expected.insert( + TokenPair { + input: token1.clone(), + output: token2.clone(), + }, + Some(U256::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(), + apy_details: Some(APYDetails { + start_time: 1, + end_time: 10000001, + net_vol: U256::from_str("5000000000000000000").unwrap(), + capital: U256::from_str("5000000000000000000").unwrap(), + apy: U256::from_str("3153600000000000000").unwrap(), + is_neg: false, + }), + vol_details: VolumeDetails { + net_vol: U256::from_str("5000000000000000000").unwrap(), + total_in: U256::from_str("7000000000000000000").unwrap(), + total_out: U256::from_str("2000000000000000000").unwrap(), + total_vol: U256::from_str("9000000000000000000").unwrap(), + }, + }; + let token2_perf = VaultPerformance { + id: vault2.to_string(), + token: token2.clone(), + apy_details: Some(APYDetails { + start_time: 1, + end_time: 10000001, + net_vol: U256::from_str("3000000000000000000").unwrap(), + capital: U256::from_str("5000000000000000000").unwrap(), + apy: U256::from_str("1892160000000000000").unwrap(), + is_neg: false, + }), + vol_details: VolumeDetails { + net_vol: U256::from_str("3000000000000000000").unwrap(), + total_in: U256::from_str("5000000000000000000").unwrap(), + total_out: 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: U256::from_str("2172479999999999999").unwrap(), + token: token2, + net_vol: U256::from_str("4428571428571428570").unwrap(), + starting_capital: U256::from_str("6428571428571428570").unwrap(), + is_neg: false, + }), + }; + + 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/vol.rs b/crates/subgraph/src/performance/vol.rs similarity index 71% rename from crates/subgraph/src/vol.rs rename to crates/subgraph/src/performance/vol.rs index fa897ffa9..80d7086f5 100644 --- a/crates/subgraph/src/vol.rs +++ b/crates/subgraph/src/performance/vol.rs @@ -1,27 +1,30 @@ use crate::{ - error::ParseNumberError, + performance::PerformanceError, types::common::{Erc20, Trade}, - utils::to_18_decimals, }; -use alloy::primitives::{ruint::ParseError, utils::ParseUnits, I256, U256}; +use alloy::primitives::{ruint::ParseError, U256}; +use rain_orderbook_math::BigUintMath; use serde::{Deserialize, Serialize}; -use std::str::FromStr; +use std::{cmp::Ordering, str::FromStr}; use typeshare::typeshare; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +#[typeshare] +pub struct VolumeDetails { + pub total_in: U256, + pub total_out: U256, + pub total_vol: U256, + pub net_vol: U256, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(rename_all = "camelCase")] #[typeshare] pub struct VaultVolume { pub id: String, pub token: Erc20, - #[typeshare(typescript(type = "string"))] - pub total_in: U256, - #[typeshare(typescript(type = "string"))] - pub total_out: U256, - #[typeshare(typescript(type = "string"))] - pub total_vol: U256, - #[typeshare(typescript(type = "string"))] - pub net_vol: I256, + pub vol_details: VolumeDetails, } /// Get the vaults volume from array of trades of an owner @@ -34,38 +37,45 @@ pub fn get_vaults_vol(trades: &[Trade]) -> Result, ParseError> }) { if trade.input_vault_balance_change.amount.0.starts_with('-') { let amount = U256::from_str(&trade.input_vault_balance_change.amount.0[1..])?; - vault_vol.total_out += amount; - vault_vol.total_vol += amount; - vault_vol.net_vol -= I256::from_raw(amount); + vault_vol.vol_details.total_out += amount; + vault_vol.vol_details.total_vol += amount; } else { let amount = U256::from_str(&trade.input_vault_balance_change.amount.0)?; - vault_vol.total_in += amount; - vault_vol.total_vol += amount; - vault_vol.net_vol += I256::from_raw(amount); + vault_vol.vol_details.total_in += amount; + vault_vol.vol_details.total_vol += amount; } + vault_vol.vol_details.net_vol = + if vault_vol.vol_details.total_in >= vault_vol.vol_details.total_out { + vault_vol.vol_details.total_in - vault_vol.vol_details.total_out + } else { + vault_vol.vol_details.total_out - vault_vol.vol_details.total_in + }; } else { let mut total_in = U256::ZERO; let mut total_out = U256::ZERO; let mut total_vol = U256::ZERO; - let mut net_vol = I256::ZERO; if trade.input_vault_balance_change.amount.0.starts_with('-') { let amount = U256::from_str(&trade.input_vault_balance_change.amount.0[1..])?; total_out += amount; total_vol += amount; - net_vol -= I256::from_raw(amount); } else { let amount = U256::from_str(&trade.input_vault_balance_change.amount.0)?; total_in += amount; total_vol += amount; - net_vol += I256::from_raw(amount); } vaults_vol.push(VaultVolume { id: trade.input_vault_balance_change.vault.vault_id.0.clone(), token: trade.input_vault_balance_change.vault.token.clone(), - total_in, - total_out, - total_vol, - net_vol, + vol_details: VolumeDetails { + total_in, + total_out, + total_vol, + net_vol: if total_in >= total_out { + total_in - total_out + } else { + total_out - total_in + }, + }, }) } if let Some(vault_vol) = vaults_vol.iter_mut().find(|v| { @@ -74,38 +84,45 @@ pub fn get_vaults_vol(trades: &[Trade]) -> Result, ParseError> }) { if trade.output_vault_balance_change.amount.0.starts_with('-') { let amount = U256::from_str(&trade.output_vault_balance_change.amount.0[1..])?; - vault_vol.total_out += amount; - vault_vol.total_vol += amount; - vault_vol.net_vol -= I256::from_raw(amount); + vault_vol.vol_details.total_out += amount; + vault_vol.vol_details.total_vol += amount; } else { let amount = U256::from_str(&trade.output_vault_balance_change.amount.0)?; - vault_vol.total_in += amount; - vault_vol.total_vol += amount; - vault_vol.net_vol += I256::from_raw(amount); + vault_vol.vol_details.total_in += amount; + vault_vol.vol_details.total_vol += amount; } + vault_vol.vol_details.net_vol = + if vault_vol.vol_details.total_in >= vault_vol.vol_details.total_out { + vault_vol.vol_details.total_in - vault_vol.vol_details.total_out + } else { + vault_vol.vol_details.total_out - vault_vol.vol_details.total_in + }; } else { let mut total_in = U256::ZERO; let mut total_out = U256::ZERO; let mut total_vol = U256::ZERO; - let mut net_vol = I256::ZERO; if trade.output_vault_balance_change.amount.0.starts_with('-') { let amount = U256::from_str(&trade.output_vault_balance_change.amount.0[1..])?; total_out += amount; total_vol += amount; - net_vol -= I256::from_raw(amount); } else { let amount = U256::from_str(&trade.output_vault_balance_change.amount.0)?; total_in += amount; total_vol += amount; - net_vol += I256::from_raw(amount); } vaults_vol.push(VaultVolume { id: trade.output_vault_balance_change.vault.vault_id.0.clone(), token: trade.output_vault_balance_change.vault.token.clone(), - total_in, - total_out, - total_vol, - net_vol, + vol_details: VolumeDetails { + total_in, + total_out, + total_vol, + net_vol: if total_in >= total_out { + total_in - total_out + } else { + total_out - total_in + }, + }, }) } } @@ -113,24 +130,32 @@ pub fn get_vaults_vol(trades: &[Trade]) -> Result, ParseError> } impl VaultVolume { + pub fn is_net_vol_negative(&self) -> bool { + match self.vol_details.total_in.cmp(&self.vol_details.total_out) { + Ordering::Greater => false, + Ordering::Less => true, + Ordering::Equal => false, + } + } + /// 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 + pub fn scale_18(&self) -> Result { + let token_decimals: u8 = self .token .decimals .as_ref() .map(|v| v.0.as_str()) - .unwrap_or("18"); + .unwrap_or("18") + .parse()?; 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(), + vol_details: VolumeDetails { + total_in: self.vol_details.total_in.scale_18(token_decimals)?, + total_out: self.vol_details.total_out.scale_18(token_decimals)?, + total_vol: self.vol_details.total_vol.scale_18(token_decimals)?, + net_vol: self.vol_details.net_vol.scale_18(token_decimals)?, + }, }) } } @@ -288,18 +313,22 @@ mod test { VaultVolume { id: vault_id2.to_string(), token: token2, - total_in: U256::from(5), - total_out: U256::from(7), - total_vol: U256::from(12), - net_vol: I256::from_str("-2").unwrap(), + vol_details: VolumeDetails { + total_in: U256::from(5), + total_out: U256::from(7), + total_vol: U256::from(12), + net_vol: U256::from(2), + }, }, VaultVolume { id: vault_id1.to_string(), token: token1, - total_in: U256::from(3), - total_out: U256::from(2), - total_vol: U256::from(5), - net_vol: I256::from_str("1").unwrap(), + vol_details: VolumeDetails { + total_in: U256::from(3), + total_out: U256::from(2), + total_vol: U256::from(5), + net_vol: U256::from(1), + }, }, ]; @@ -319,20 +348,24 @@ mod test { 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(), + vol_details: VolumeDetails { + total_in: U256::from(20_500_000), + total_out: U256::from(30_000_000), + total_vol: U256::from(50_500_000), + net_vol: U256::from(9_500_000), + }, }; - let result = vault_vol.to_18_decimals().unwrap(); + let result = vault_vol.scale_18().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(), + vol_details: VolumeDetails { + 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: U256::from_str("9_500_000_000_000_000_000").unwrap(), + }, }; assert_eq!(result, expected); diff --git a/crates/subgraph/src/types/impls.rs b/crates/subgraph/src/types/impls.rs index 490880739..afc080450 100644 --- a/crates/subgraph/src/types/impls.rs +++ b/crates/subgraph/src/types/impls.rs @@ -1,67 +1,72 @@ use super::common::*; -use crate::{ - error::ParseNumberError, - utils::{one_18, to_18_decimals}, -}; -use alloy::primitives::{utils::ParseUnits, I256, U256}; +use crate::performance::PerformanceError; +use alloy::primitives::U256; +use rain_orderbook_math::BigUintMath; use std::str::FromStr; impl Trade { - /// Converts this trade's input to 18 point decimals in U256/I256 - 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 - .vault - .token - .decimals - .as_ref() - .map(|v| v.0.as_str()) - .unwrap_or("18"), - )?) + /// Converts this trade's input to 18 point decimals in U256 + pub fn scale_18_input(&self) -> Result { + Ok( + U256::from_str(&self.input_vault_balance_change.amount.0)?.scale_18( + self.input_vault_balance_change + .vault + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18") + .parse()?, + )?, + ) } - /// Converts this trade's output to 18 point decimals in U256/I256 - pub fn output_as_18_decimals(&self) -> Result { - Ok(to_18_decimals( - ParseUnits::I256(I256::from_str(&self.output_vault_balance_change.amount.0)?), + /// Converts this trade's output to 18 point decimals in U256 + pub fn scale_18_output(&self) -> Result { + let amount = if self.output_vault_balance_change.amount.0.starts_with('-') { + &self.output_vault_balance_change.amount.0[1..] + } else { + &self.output_vault_balance_change.amount.0 + }; + Ok(U256::from_str(amount)?.scale_18( self.output_vault_balance_change .vault .token .decimals .as_ref() .map(|v| v.0.as_str()) - .unwrap_or("18"), + .unwrap_or("18") + .parse()?, )?) } /// Calculates the trade's I/O ratio - pub fn ratio(&self) -> Result { - Ok(self - .input_as_18_decimals()? - .get_absolute() - .saturating_mul(one_18().get_absolute()) - .checked_div( - self.output_as_18_decimals()? - .get_signed() - .saturating_neg() - .try_into()?, - ) - .unwrap_or(U256::MAX)) + pub fn ratio(&self) -> Result { + let output = self.scale_18_output()?; + let input = self.scale_18_input()?; + if output.is_zero() && input.is_zero() { + Ok(U256::ZERO) + } else if output.is_zero() { + Ok(U256::MAX) + } else { + Ok(input.div_18(output)?) + } } /// Calculates the trade's O/I ratio (inverse) - pub fn inverse_ratio(&self) -> Result { - Ok( - TryInto::::try_into(self.output_as_18_decimals()?.get_signed().saturating_neg())? - .saturating_mul(one_18().get_absolute()) - .checked_div(self.input_as_18_decimals()?.get_absolute()) - .unwrap_or(U256::MAX), - ) + pub fn inverse_ratio(&self) -> Result { + let output = self.scale_18_output()?; + let input = self.scale_18_input()?; + if output.is_zero() && input.is_zero() { + Ok(U256::ZERO) + } else if input.is_zero() { + Ok(U256::MAX) + } else { + Ok(output.div_18(input)?) + } } } - #[cfg(target_family = "wasm")] mod js_api { use super::super::common::{ @@ -94,7 +99,6 @@ mod js_api { impl_wasm_traits!(Bytes); } - #[cfg(test)] mod test { use super::*; @@ -106,16 +110,16 @@ mod test { #[test] fn test_input_to_18_decimals() { - let result = get_trade().input_as_18_decimals().unwrap(); + let result = get_trade().scale_18_input().unwrap(); let expected = U256::from_str("3000000000000000000").unwrap(); - assert_eq!(result.get_absolute(), expected); + assert_eq!(result, expected); } #[test] fn test_output_to_18_decimals() { - let result = get_trade().output_as_18_decimals().unwrap(); - let expected = I256::from_str("-6000000000000000000").unwrap(); - assert_eq!(result.get_signed(), expected); + let result = get_trade().scale_18_output().unwrap(); + let expected = U256::from_str("6000000000000000000").unwrap(); + assert_eq!(result, expected); } #[test] diff --git a/crates/subgraph/src/types/order.rs b/crates/subgraph/src/types/order.rs index 8c7924657..233c566a9 100644 --- a/crates/subgraph/src/types/order.rs +++ b/crates/subgraph/src/types/order.rs @@ -1,16 +1,6 @@ use super::common::*; -use crate::apy::{get_vaults_apy, TokenPair}; use crate::schema; -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 serde::Serialize; use typeshare::typeshare; #[derive(cynic::QueryVariables, Debug)] @@ -51,604 +41,3 @@ 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 9cc9633d8..c1bfabfff 100644 --- a/crates/subgraph/src/utils/mod.rs +++ b/crates/subgraph/src/utils/mod.rs @@ -1,8 +1,7 @@ -use alloy::primitives::{ - utils::{format_units, parse_units, ParseUnits, Unit, UnitsError}, - I256, U256, -}; +use alloy::primitives::U256; use chrono::TimeDelta; +use once_cell::sync::Lazy; +use rain_orderbook_math::{BigUintMath, MathError, ONE18}; mod order_id; mod slice_list; @@ -10,70 +9,26 @@ mod slice_list; pub use order_id::*; pub use slice_list::*; -/// Returns 18 point decimals 1 as I256/U256 -pub fn one_18() -> ParseUnits { - 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 { - 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 -pub fn to_18_decimals>( - amount: ParseUnits, - decimals: T, -) -> Result { - parse_units(&format_units(amount, decimals)?, 18) -} +/// a year length timestamp in seconds as 18 point decimals as U256 +pub static YEAR18: Lazy = + Lazy::new(|| U256::from(TimeDelta::days(365).num_seconds()).saturating_mul(ONE18)); /// 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()) +pub fn annual_rate(start: u64, end: u64) -> Result { + U256::from(end - start) + .saturating_mul(ONE18) + .div_18(*YEAR18) } #[cfg(test)] mod test { use super::*; - use alloy::primitives::{I256, U256}; - use std::str::FromStr; + use alloy::primitives::U256; #[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() { - const YEAR: u64 = 60 * 60 * 24 * 365; - 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()); + fn test_annual_rate() { + let result = annual_rate(1, 101).unwrap(); + let expected = U256::from(3_170_979_198_376_u64); assert_eq!(result, expected); } } diff --git a/flake.nix b/flake.nix index 2787c3575..214abe19b 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 crates/subgraph/src/apy.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/performance/vol.rs crates/subgraph/src/performance/apy.rs crates/subgraph/src/performance/order_performance.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; diff --git a/tauri-app/src-tauri/Cargo.lock b/tauri-app/src-tauri/Cargo.lock index cc7a06a39..e1742f87e 100644 --- a/tauri-app/src-tauri/Cargo.lock +++ b/tauri-app/src-tauri/Cargo.lock @@ -8337,6 +8337,15 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "rain_orderbook_math" +version = "0.0.0-alpha.0" +dependencies = [ + "alloy", + "once_cell", + "thiserror", +] + [[package]] name = "rain_orderbook_quote" version = "0.0.0-alpha.0" @@ -8377,7 +8386,9 @@ dependencies = [ "cynic-introspection", "futures", "js-sys", + "once_cell", "rain_orderbook_bindings", + "rain_orderbook_math", "reqwest 0.12.5", "serde", "serde-wasm-bindgen 0.6.5", diff --git a/tauri-app/src-tauri/src/commands/order_take.rs b/tauri-app/src-tauri/src/commands/order_take.rs index ffe4eee0c..f3f1287db 100644 --- a/tauri-app/src-tauri/src/commands/order_take.rs +++ b/tauri-app/src-tauri/src/commands/order_take.rs @@ -3,8 +3,8 @@ use rain_orderbook_common::{ csv::TryIntoCsv, subgraph::SubgraphArgs, types::FlattenError, types::OrderTakeFlattened, }; -use rain_orderbook_subgraph_client::types::order::OrderPerformance; -use rain_orderbook_subgraph_client::vol::VaultVolume; +use rain_orderbook_subgraph_client::performance::vol::VaultVolume; +use rain_orderbook_subgraph_client::performance::OrderPerformance; use rain_orderbook_subgraph_client::{types::common::*, PaginationArgs}; use std::fs; use std::path::PathBuf; diff --git a/tauri-app/src/lib/components/tables/OrderAPY.test.ts b/tauri-app/src/lib/components/tables/OrderAPY.test.ts index 8100c8940..a902f7824 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.test.ts +++ b/tauri-app/src/lib/components/tables/OrderAPY.test.ts @@ -46,6 +46,7 @@ const mockOrderApy: OrderPerformance[] = [ symbol: 'output_token', decimals: '0', }, + isNeg: false, netVol: '0', startingCapital: '0', }, diff --git a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.svelte b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.svelte index 59e67c0c4..db6a6e02e 100644 --- a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.svelte +++ b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.svelte @@ -49,16 +49,21 @@ - {formatUnits(BigInt(item.totalIn), Number(item.token.decimals ?? 0))} + {formatUnits(BigInt(item.volDetails.totalIn), Number(item.token.decimals ?? 0))} - {formatUnits(BigInt(item.totalOut), Number(item.token.decimals ?? 0))} + {formatUnits(BigInt(item.volDetails.totalOut), Number(item.token.decimals ?? 0))} - {formatUnits(BigInt(item.netVol), Number(item.token.decimals ?? 0))} + {formatUnits( + BigInt(item.volDetails.totalIn) >= BigInt(item.volDetails.totalOut) + ? BigInt(item.volDetails.netVol) + : -BigInt(item.volDetails.netVol), + Number(item.token.decimals ?? 0), + )} - {formatUnits(BigInt(item.totalVol), Number(item.token.decimals ?? 0))} + {formatUnits(BigInt(item.volDetails.totalVol), Number(item.token.decimals ?? 0))} diff --git a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts index 678bf1e9e..26d815e71 100644 --- a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts +++ b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts @@ -42,10 +42,12 @@ const mockVaultsVol: VaultVolume[] = [ symbol: 'output_token', decimals: '0', }, - totalIn: '1', - totalOut: '2', - totalVol: '3', - netVol: '-1', + volDetails: { + totalIn: '1', + totalOut: '2', + totalVol: '3', + netVol: '-1', + }, }, { id: '2', @@ -56,10 +58,12 @@ const mockVaultsVol: VaultVolume[] = [ symbol: 'output_token', decimals: '0', }, - totalIn: '2', - totalOut: '5', - totalVol: '7', - netVol: '-3', + volDetails: { + totalIn: '2', + totalOut: '5', + totalVol: '7', + netVol: '-3', + }, }, ]; @@ -84,7 +88,7 @@ test('renders table with correct data', async () => { // checking the total ins for (let i = 0; i < mockVaultsVol.length; i++) { const display = formatUnits( - BigInt(mockVaultsVol[i].totalIn), + BigInt(mockVaultsVol[i].volDetails.totalIn), Number(mockVaultsVol[i].token.decimals), ); expect(rows[i]).toHaveTextContent(display.toString()); @@ -98,7 +102,7 @@ test('renders table with correct data', async () => { // checking the total outs for (let i = 0; i < mockVaultsVol.length; i++) { const display = formatUnits( - BigInt(mockVaultsVol[i].totalOut), + BigInt(mockVaultsVol[i].volDetails.totalOut), Number(mockVaultsVol[i].token.decimals), ); expect(rows[i]).toHaveTextContent(display.toString()); @@ -112,7 +116,7 @@ test('renders table with correct data', async () => { // checking the net vols for (let i = 0; i < mockVaultsVol.length; i++) { const display = formatUnits( - BigInt(mockVaultsVol[i].netVol), + BigInt(mockVaultsVol[i].volDetails.netVol), Number(mockVaultsVol[i].token.decimals), ); expect(rows[i]).toHaveTextContent(display.toString()); @@ -126,7 +130,7 @@ test('renders table with correct data', async () => { // checking the total vols for (let i = 0; i < mockVaultsVol.length; i++) { const display = formatUnits( - BigInt(mockVaultsVol[i].totalVol), + BigInt(mockVaultsVol[i].volDetails.totalVol), Number(mockVaultsVol[i].token.decimals), ); expect(rows[i]).toHaveTextContent(display.toString()); From dccd4ea96c6018afef70217bf978a82d7e419ad1 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 18 Nov 2024 02:15:02 +0000 Subject: [PATCH 31/50] update --- crates/subgraph/src/performance/apy.rs | 10 +-- crates/subgraph/src/performance/mod.rs | 10 ++- .../src/performance/order_performance.rs | 9 +-- crates/subgraph/src/types/impls.rs | 65 ++++++++----------- crates/subgraph/src/utils/mod.rs | 29 --------- 5 files changed, 46 insertions(+), 77 deletions(-) diff --git a/crates/subgraph/src/performance/apy.rs b/crates/subgraph/src/performance/apy.rs index 5395f17db..260832674 100644 --- a/crates/subgraph/src/performance/apy.rs +++ b/crates/subgraph/src/performance/apy.rs @@ -1,12 +1,11 @@ -use super::PerformanceError; +use super::{PerformanceError, YEAR18}; use crate::{ performance::vol::VaultVolume, types::common::{Erc20, Trade}, - utils::annual_rate, }; use alloy::primitives::U256; use chrono::TimeDelta; -use rain_orderbook_math::BigUintMath; +use rain_orderbook_math::{BigUintMath, ONE18}; use serde::{Deserialize, Serialize}; use std::str::FromStr; use typeshare::typeshare; @@ -127,7 +126,10 @@ pub fn get_vaults_apy( // this token vault apy in 18 decimals point let apy = if !starting_capital.is_zero() { - match annual_rate(start, end) { + match U256::from(end - start) + .saturating_mul(ONE18) + .div_18(*YEAR18) + { Err(_) => None, Ok(annual_rate_18) => vol .vol_details diff --git a/crates/subgraph/src/performance/mod.rs b/crates/subgraph/src/performance/mod.rs index c07a8ff5a..78a6b17e4 100644 --- a/crates/subgraph/src/performance/mod.rs +++ b/crates/subgraph/src/performance/mod.rs @@ -1,5 +1,7 @@ -use alloy::primitives::ruint::ParseError; -use rain_orderbook_math::MathError; +use alloy::primitives::{ruint::ParseError, U256}; +use chrono::TimeDelta; +use once_cell::sync::Lazy; +use rain_orderbook_math::{MathError, ONE18}; use std::num::ParseIntError; use thiserror::Error; @@ -9,6 +11,10 @@ pub mod vol; pub use order_performance::*; +/// a year length timestamp in seconds as 18 point decimals as U256 +pub static YEAR18: Lazy = + Lazy::new(|| U256::from(TimeDelta::days(365).num_seconds()).saturating_mul(ONE18)); + #[derive(Error, Debug)] pub enum PerformanceError { #[error(transparent)] diff --git a/crates/subgraph/src/performance/order_performance.rs b/crates/subgraph/src/performance/order_performance.rs index 393f6d8b7..122ac630c 100644 --- a/crates/subgraph/src/performance/order_performance.rs +++ b/crates/subgraph/src/performance/order_performance.rs @@ -1,14 +1,13 @@ use super::apy::APYDetails; use super::vol::VolumeDetails; -use super::PerformanceError; +use super::{PerformanceError, YEAR18}; use crate::performance::apy::{get_vaults_apy, TokenPair}; -use crate::utils::annual_rate; use crate::{ performance::vol::get_vaults_vol, types::common::{Erc20, Order, Trade}, }; use alloy::primitives::U256; -use rain_orderbook_math::BigUintMath; +use rain_orderbook_math::{BigUintMath, ONE18}; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::HashMap; @@ -174,7 +173,9 @@ impl OrderPerformance { for token_vault in &vaults_apy { if let Some(apy_details) = token_vault.apy_details { // time to year ratio - let annual_rate = annual_rate(apy_details.start_time, apy_details.end_time) + let annual_rate = U256::from(apy_details.end_time - apy_details.start_time) + .saturating_mul(ONE18) + .div_18(*YEAR18) .map_err(PerformanceError::from)?; // sum up all token vaults' capitals and vols in the current's iteration diff --git a/crates/subgraph/src/types/impls.rs b/crates/subgraph/src/types/impls.rs index afc080450..d8a5fd7b5 100644 --- a/crates/subgraph/src/types/impls.rs +++ b/crates/subgraph/src/types/impls.rs @@ -5,9 +5,14 @@ use rain_orderbook_math::BigUintMath; use std::str::FromStr; impl Trade { - /// Converts this trade's input to 18 point decimals in U256 - pub fn scale_18_input(&self) -> Result { - Ok( + /// Converts this trade's io to 18 point decimals in U256 + pub fn scale_18_io(&self) -> Result<(U256, U256), PerformanceError> { + let amount = if self.output_vault_balance_change.amount.0.starts_with('-') { + &self.output_vault_balance_change.amount.0[1..] + } else { + &self.output_vault_balance_change.amount.0 + }; + Ok(( U256::from_str(&self.input_vault_balance_change.amount.0)?.scale_18( self.input_vault_balance_change .vault @@ -18,32 +23,22 @@ impl Trade { .unwrap_or("18") .parse()?, )?, - ) - } - - /// Converts this trade's output to 18 point decimals in U256 - pub fn scale_18_output(&self) -> Result { - let amount = if self.output_vault_balance_change.amount.0.starts_with('-') { - &self.output_vault_balance_change.amount.0[1..] - } else { - &self.output_vault_balance_change.amount.0 - }; - Ok(U256::from_str(amount)?.scale_18( - self.output_vault_balance_change - .vault - .token - .decimals - .as_ref() - .map(|v| v.0.as_str()) - .unwrap_or("18") - .parse()?, - )?) + U256::from_str(amount)?.scale_18( + self.output_vault_balance_change + .vault + .token + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18") + .parse()?, + )?, + )) } /// Calculates the trade's I/O ratio pub fn ratio(&self) -> Result { - let output = self.scale_18_output()?; - let input = self.scale_18_input()?; + let (input, output) = self.scale_18_io()?; if output.is_zero() && input.is_zero() { Ok(U256::ZERO) } else if output.is_zero() { @@ -55,8 +50,7 @@ impl Trade { /// Calculates the trade's O/I ratio (inverse) pub fn inverse_ratio(&self) -> Result { - let output = self.scale_18_output()?; - let input = self.scale_18_input()?; + let (input, output) = self.scale_18_io()?; if output.is_zero() && input.is_zero() { Ok(U256::ZERO) } else if input.is_zero() { @@ -109,17 +103,12 @@ mod test { use alloy::primitives::Address; #[test] - fn test_input_to_18_decimals() { - let result = get_trade().scale_18_input().unwrap(); - let expected = U256::from_str("3000000000000000000").unwrap(); - assert_eq!(result, expected); - } - - #[test] - fn test_output_to_18_decimals() { - let result = get_trade().scale_18_output().unwrap(); - let expected = U256::from_str("6000000000000000000").unwrap(); - assert_eq!(result, expected); + fn test_scale_18_io() { + let (input, output) = get_trade().scale_18_io().unwrap(); + let expected_input = U256::from_str("3000000000000000000").unwrap(); + let expected_output = U256::from_str("6000000000000000000").unwrap(); + assert_eq!(input, expected_input); + assert_eq!(output, expected_output); } #[test] diff --git a/crates/subgraph/src/utils/mod.rs b/crates/subgraph/src/utils/mod.rs index c1bfabfff..145715232 100644 --- a/crates/subgraph/src/utils/mod.rs +++ b/crates/subgraph/src/utils/mod.rs @@ -1,34 +1,5 @@ -use alloy::primitives::U256; -use chrono::TimeDelta; -use once_cell::sync::Lazy; -use rain_orderbook_math::{BigUintMath, MathError, ONE18}; - mod order_id; mod slice_list; pub use order_id::*; pub use slice_list::*; - -/// a year length timestamp in seconds as 18 point decimals as U256 -pub static YEAR18: Lazy = - Lazy::new(|| U256::from(TimeDelta::days(365).num_seconds()).saturating_mul(ONE18)); - -/// Returns annual rate as 18 point decimals as I256 -pub fn annual_rate(start: u64, end: u64) -> Result { - U256::from(end - start) - .saturating_mul(ONE18) - .div_18(*YEAR18) -} - -#[cfg(test)] -mod test { - use super::*; - use alloy::primitives::U256; - - #[test] - fn test_annual_rate() { - let result = annual_rate(1, 101).unwrap(); - let expected = U256::from(3_170_979_198_376_u64); - assert_eq!(result, expected); - } -} From 89932d5ce562115426c86aac74421cfe5e5feb89 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 18 Nov 2024 02:18:31 +0000 Subject: [PATCH 32/50] Update vol.rs --- crates/subgraph/src/performance/vol.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/subgraph/src/performance/vol.rs b/crates/subgraph/src/performance/vol.rs index 80d7086f5..c24c88c00 100644 --- a/crates/subgraph/src/performance/vol.rs +++ b/crates/subgraph/src/performance/vol.rs @@ -27,7 +27,7 @@ pub struct VaultVolume { pub vol_details: VolumeDetails, } -/// Get the vaults volume from array of trades of an owner +/// Get the vaults volume from array of trades of an order pub fn get_vaults_vol(trades: &[Trade]) -> Result, ParseError> { let mut vaults_vol: Vec = vec![]; for trade in trades { From f9cad84790e87e5981cf55b47e2e800112191445 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 18 Nov 2024 02:21:02 +0000 Subject: [PATCH 33/50] Update impls.rs --- crates/subgraph/src/types/impls.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/subgraph/src/types/impls.rs b/crates/subgraph/src/types/impls.rs index d8a5fd7b5..9e370b50f 100644 --- a/crates/subgraph/src/types/impls.rs +++ b/crates/subgraph/src/types/impls.rs @@ -5,15 +5,20 @@ use rain_orderbook_math::BigUintMath; use std::str::FromStr; impl Trade { - /// Converts this trade's io to 18 point decimals in U256 + /// Scales this trade's io to 18 point decimals in U256 pub fn scale_18_io(&self) -> Result<(U256, U256), PerformanceError> { - let amount = if self.output_vault_balance_change.amount.0.starts_with('-') { + let input_amount = if self.input_vault_balance_change.amount.0.starts_with('-') { + &self.input_vault_balance_change.amount.0[1..] + } else { + &self.input_vault_balance_change.amount.0 + }; + let output_amount = if self.output_vault_balance_change.amount.0.starts_with('-') { &self.output_vault_balance_change.amount.0[1..] } else { &self.output_vault_balance_change.amount.0 }; Ok(( - U256::from_str(&self.input_vault_balance_change.amount.0)?.scale_18( + U256::from_str(input_amount)?.scale_18( self.input_vault_balance_change .vault .token @@ -23,7 +28,7 @@ impl Trade { .unwrap_or("18") .parse()?, )?, - U256::from_str(amount)?.scale_18( + U256::from_str(output_amount)?.scale_18( self.output_vault_balance_change .vault .token From 0f1e192d1bb3dde9103d4735f12004bae0bd03c0 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 18 Nov 2024 03:08:06 +0000 Subject: [PATCH 34/50] Update OrderVaultsVolTable.svelte --- .../src/lib/components/tables/OrderVaultsVolTable.svelte | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.svelte b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.svelte index db6a6e02e..d5f615a64 100644 --- a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.svelte +++ b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.svelte @@ -55,12 +55,8 @@ {formatUnits(BigInt(item.volDetails.totalOut), Number(item.token.decimals ?? 0))} - {formatUnits( - BigInt(item.volDetails.totalIn) >= BigInt(item.volDetails.totalOut) - ? BigInt(item.volDetails.netVol) - : -BigInt(item.volDetails.netVol), - Number(item.token.decimals ?? 0), - )} + {(BigInt(item.volDetails.totalIn) >= BigInt(item.volDetails.totalOut) ? '' : '-') + + formatUnits(BigInt(item.volDetails.netVol), Number(item.token.decimals ?? 0))} {formatUnits(BigInt(item.volDetails.totalVol), Number(item.token.decimals ?? 0))} From b5c585bcf55ac5ad1cdc88fe626d4f2df7fbe27b Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 18 Nov 2024 03:09:13 +0000 Subject: [PATCH 35/50] Update OrderVaultsVolTable.test.ts --- tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts index 26d815e71..f1ec41166 100644 --- a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts +++ b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts @@ -119,7 +119,7 @@ test('renders table with correct data', async () => { BigInt(mockVaultsVol[i].volDetails.netVol), Number(mockVaultsVol[i].token.decimals), ); - expect(rows[i]).toHaveTextContent(display.toString()); + expect(rows[i]).toHaveTextContent(display); } }); From 7a8659a21a4d1cea0f0391869ab361ae223bbb0c Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Mon, 18 Nov 2024 03:47:37 +0000 Subject: [PATCH 36/50] Update OrderVaultsVolTable.test.ts --- .../src/lib/components/tables/OrderVaultsVolTable.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts index f1ec41166..6551204fc 100644 --- a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts +++ b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts @@ -46,7 +46,7 @@ const mockVaultsVol: VaultVolume[] = [ totalIn: '1', totalOut: '2', totalVol: '3', - netVol: '-1', + netVol: '1', }, }, { @@ -62,7 +62,7 @@ const mockVaultsVol: VaultVolume[] = [ totalIn: '2', totalOut: '5', totalVol: '7', - netVol: '-3', + netVol: '3', }, }, ]; @@ -116,7 +116,7 @@ test('renders table with correct data', async () => { // checking the net vols for (let i = 0; i < mockVaultsVol.length; i++) { const display = formatUnits( - BigInt(mockVaultsVol[i].volDetails.netVol), + -BigInt(mockVaultsVol[i].volDetails.netVol), Number(mockVaultsVol[i].token.decimals), ); expect(rows[i]).toHaveTextContent(display); From b3f11931ab41763aa6968ce450744f0e9ff64a9a Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 21 Nov 2024 20:00:49 +0000 Subject: [PATCH 37/50] update --- tauri-app/src/lib/components/tables/OrderAPY.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tauri-app/src/lib/components/tables/OrderAPY.test.ts b/tauri-app/src/lib/components/tables/OrderAPY.test.ts index a902f7824..945fb1f32 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.test.ts +++ b/tauri-app/src/lib/components/tables/OrderAPY.test.ts @@ -9,7 +9,7 @@ import { bigintStringToPercentage } from '$lib/utils/number'; vi.mock('$lib/stores/settings', async (importOriginal) => { const { writable } = await import('svelte/store'); - const { mockSettingsStore } = await import('$lib/mocks/settings'); + const { mockSettingsStore } = await import('@rainlanguage/ui-components'); const _activeOrderbook = writable(); From f64c1756076f4ff88a41bcd82d597dc9f02f90a2 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 21 Nov 2024 21:09:29 +0000 Subject: [PATCH 38/50] update --- crates/subgraph/src/performance/apy.rs | 2 +- .../src/performance/order_performance.rs | 142 ++++++++---------- .../src/lib/components/tables/OrderAPY.svelte | 3 +- .../lib/components/tables/OrderAPY.test.ts | 7 +- 4 files changed, 73 insertions(+), 81 deletions(-) diff --git a/crates/subgraph/src/performance/apy.rs b/crates/subgraph/src/performance/apy.rs index 260832674..7d058b088 100644 --- a/crates/subgraph/src/performance/apy.rs +++ b/crates/subgraph/src/performance/apy.rs @@ -42,7 +42,7 @@ pub struct TokenPair { } /// Calculates each token vault apy at the given timeframe -/// Trades must be sorted indesc order by timestamp, this is +/// Trades must be sorted in desc order by timestamp, this is /// the case if queried from subgraph using this lib functionalities pub fn get_vaults_apy( trades: &[Trade], diff --git a/crates/subgraph/src/performance/order_performance.rs b/crates/subgraph/src/performance/order_performance.rs index 122ac630c..1c5849945 100644 --- a/crates/subgraph/src/performance/order_performance.rs +++ b/crates/subgraph/src/performance/order_performance.rs @@ -21,9 +21,9 @@ pub struct VaultPerformance { pub id: String, /// vault token pub token: Erc20, - /// vol segment + /// vault vol segment pub vol_details: VolumeDetails, - /// apy segment + /// vault apy segment pub apy_details: Option, } @@ -35,10 +35,12 @@ pub struct DenominatedPerformance { pub token: Erc20, /// Order's APY raw value pub apy: U256, - /// Order's net vol + /// Determines if apy is negative or not + pub apy_is_neg: bool, + /// Order's net vol raw value pub net_vol: U256, - /// Determines if apy and net_vol are negative or not - pub is_neg: bool, + /// Determines if net_vol is negative or not + pub net_vol_is_neg: bool, /// Order's starting capital pub starting_capital: U256, } @@ -61,16 +63,16 @@ pub struct OrderPerformance { /// End timestamp of the performance measuring timeframe #[typeshare(typescript(type = "number"))] pub end_time: u64, - /// Ordder's input vaults isolated performance + /// Order's input vaults isolated performance pub inputs_vaults: Vec, - /// Ordder's output vaults isolated performance + /// Order'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 + /// Trades must be sorted in desc order by timestamp, this is the case if /// queried from subgraph using this lib functionalities pub fn measure( order: &Order, @@ -78,6 +80,7 @@ impl OrderPerformance { start_timestamp: Option, end_timestamp: Option, ) -> Result { + // return early if there are no trades if trades.is_empty() { return Ok(OrderPerformance { order_id: order.id.0.clone(), @@ -94,6 +97,8 @@ impl OrderPerformance { let vaults_apy = get_vaults_apy(trades, &vaults_vol, start_timestamp, end_timestamp)?; // build an OrderPerformance struct + // pick the order's whole performance timeframe from the vaults biggest timeframe + // and put the calculated vaults vol and apy into inputs and outputs vaults fields let mut start_time = u64::MAX; let mut end_time = 0_u64; let mut inputs: Vec = vec![]; @@ -172,52 +177,63 @@ impl OrderPerformance { let mut current_token_vol_list: Vec = vec![]; for token_vault in &vaults_apy { if let Some(apy_details) = token_vault.apy_details { - // time to year ratio - let annual_rate = U256::from(apy_details.end_time - apy_details.start_time) - .saturating_mul(ONE18) - .div_18(*YEAR18) - .map_err(PerformanceError::from)?; - - // 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 += apy_details.capital; + // a closure fn handles net vol combination + let mut handle_combined_net_vol = |new_net_vol: U256| { if apy_details.is_neg == net_vol_is_neg { - combined_net_vol += apy_details.net_vol; + combined_net_vol += new_net_vol; } else if net_vol_is_neg { - if apy_details.net_vol >= combined_net_vol { + if new_net_vol >= combined_net_vol { net_vol_is_neg = false; - combined_net_vol = apy_details.net_vol - combined_net_vol; + combined_net_vol = new_net_vol - combined_net_vol; } else { - combined_net_vol -= apy_details.net_vol; + combined_net_vol -= new_net_vol; } - } else if combined_net_vol >= apy_details.net_vol { - combined_net_vol -= apy_details.net_vol; + } else if combined_net_vol >= new_net_vol { + combined_net_vol -= new_net_vol; } else { net_vol_is_neg = true; - combined_net_vol = apy_details.net_vol - combined_net_vol; + combined_net_vol = new_net_vol - combined_net_vol; } + }; - let annual_rate_vol = apy_details - .net_vol - .div_18(annual_rate) - .map_err(PerformanceError::from)?; + // a closure fn handles annual rate vol combination + let mut handle_combined_annual_rate_vol = |new_annual_rate_vol: U256| { if apy_details.is_neg == net_vol_rate_is_neg { - combined_annual_rate_vol += annual_rate_vol; + combined_annual_rate_vol += new_annual_rate_vol; } else if net_vol_rate_is_neg { - if annual_rate_vol >= combined_annual_rate_vol { + if new_annual_rate_vol >= combined_annual_rate_vol { net_vol_rate_is_neg = false; combined_annual_rate_vol = - annual_rate_vol - combined_annual_rate_vol; + new_annual_rate_vol - combined_annual_rate_vol; } else { - combined_annual_rate_vol -= annual_rate_vol; + combined_annual_rate_vol -= new_annual_rate_vol; } - } else if combined_annual_rate_vol >= annual_rate_vol { - combined_annual_rate_vol -= annual_rate_vol; + } else if combined_annual_rate_vol >= new_annual_rate_vol { + combined_annual_rate_vol -= new_annual_rate_vol; } else { - net_vol_is_neg = true; - combined_annual_rate_vol = annual_rate_vol - combined_annual_rate_vol; + net_vol_rate_is_neg = true; + combined_annual_rate_vol = + new_annual_rate_vol - combined_annual_rate_vol; } + }; + + // this vault's timeframe to year ratio + let annual_rate = U256::from(apy_details.end_time - apy_details.start_time) + .saturating_mul(ONE18) + .div_18(*YEAR18) + .map_err(PerformanceError::from)?; + + // sum up all token vaults' capitals and vols in the current's iteration's + // token denomination by using the direct ratio between the tokens + if token_vault.token == token.token { + combined_capital += apy_details.capital; + handle_combined_net_vol(apy_details.net_vol); + + let annual_rate_vol = apy_details + .net_vol + .div_18(annual_rate) + .map_err(PerformanceError::from)?; + handle_combined_annual_rate_vol(annual_rate_vol); current_token_vol_list.push(TokenDenominationVol { net_vol: apy_details.net_vol, @@ -241,42 +257,12 @@ impl OrderPerformance { .net_vol .mul_18(*ratio) .map_err(PerformanceError::from)?; - if apy_details.is_neg == net_vol_is_neg { - combined_net_vol += net_vol_converted; - } else if net_vol_is_neg { - if net_vol_converted >= combined_net_vol { - net_vol_is_neg = false; - combined_net_vol = net_vol_converted - combined_net_vol; - } else { - combined_net_vol -= net_vol_converted; - } - } else if combined_net_vol >= net_vol_converted { - combined_net_vol -= net_vol_converted; - } else { - net_vol_is_neg = true; - combined_net_vol = net_vol_converted - combined_net_vol; - } + handle_combined_net_vol(net_vol_converted); let annual_rate_vol_converted = net_vol_converted .div_18(annual_rate) .map_err(PerformanceError::from)?; - if apy_details.is_neg == net_vol_rate_is_neg { - combined_annual_rate_vol += annual_rate_vol_converted; - } else if net_vol_rate_is_neg { - if annual_rate_vol_converted >= combined_annual_rate_vol { - net_vol_rate_is_neg = false; - combined_annual_rate_vol = - annual_rate_vol_converted - combined_annual_rate_vol; - } else { - combined_annual_rate_vol -= annual_rate_vol_converted; - } - } else if combined_annual_rate_vol >= annual_rate_vol_converted { - combined_annual_rate_vol -= annual_rate_vol_converted; - } else { - net_vol_is_neg = true; - combined_annual_rate_vol = - annual_rate_vol_converted - combined_annual_rate_vol; - } + handle_combined_annual_rate_vol(annual_rate_vol_converted); current_token_vol_list.push(TokenDenominationVol { net_vol: net_vol_converted, @@ -291,17 +277,18 @@ impl OrderPerformance { } } - // for every success apy calc in a token denomination, gather them in BTreeMap + // 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 order from highest to lowest. + // the order's io tokens in order from highest to lowest when sorted. if !noway { if let Ok(apy) = combined_annual_rate_vol.div_18(combined_capital) { full_apy_in_distinct_token_denominations.push(DenominatedPerformance { apy, + apy_is_neg: net_vol_rate_is_neg, token: token.token.clone(), starting_capital: combined_capital, net_vol: combined_net_vol, - is_neg: net_vol_rate_is_neg, + net_vol_is_neg, }); } } else { @@ -310,14 +297,14 @@ impl OrderPerformance { // 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 + // the highest vol token as settlement denomination if all_tokens_vols_list.is_empty() { all_tokens_vols_list.extend(current_token_vol_list); } } - // pick the denomination with highest net vol by iterating over tokens with - // highest vol to lowest and pick the first matching one + // after array is sorted, pick the denomination with highest net vol by + // iterating over tokens with highest vol to lowest and pick the first matching one all_tokens_vols_list.sort(); for token in all_tokens_vols_list.iter().rev() { if let Some(denominated_apy) = full_apy_in_distinct_token_denominations @@ -335,7 +322,7 @@ impl OrderPerformance { } /// 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 +/// Trades must be sorted in desc 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(); @@ -552,10 +539,11 @@ mod test { outputs_vaults: vec![token1_perf.clone(), token2_perf.clone()], denominated_performance: Some(DenominatedPerformance { apy: U256::from_str("2172479999999999999").unwrap(), + apy_is_neg: false, token: token2, net_vol: U256::from_str("4428571428571428570").unwrap(), starting_capital: U256::from_str("6428571428571428570").unwrap(), - is_neg: false, + net_vol_is_neg: false, }), }; diff --git a/tauri-app/src/lib/components/tables/OrderAPY.svelte b/tauri-app/src/lib/components/tables/OrderAPY.svelte index f4baad1af..cf46692e7 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.svelte +++ b/tauri-app/src/lib/components/tables/OrderAPY.svelte @@ -33,7 +33,8 @@ {item.denominatedPerformance - ? bigintStringToPercentage(item.denominatedPerformance.apy, 18, 5) + + ? (item.denominatedPerformance.apyIsNeg ? '-' : '') + + bigintStringToPercentage(item.denominatedPerformance.apy, 18, 5) + '% in ' + (item.denominatedPerformance.token.symbol ?? item.denominatedPerformance.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 945fb1f32..c9d3829be 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.test.ts +++ b/tauri-app/src/lib/components/tables/OrderAPY.test.ts @@ -39,6 +39,7 @@ const mockOrderApy: OrderPerformance[] = [ orderbook: '1', denominatedPerformance: { apy: '1200000000000000000', + apyIsNeg: true, token: { id: 'output_token', address: 'output_token', @@ -46,8 +47,8 @@ const mockOrderApy: OrderPerformance[] = [ symbol: 'output_token', decimals: '0', }, - isNeg: false, netVol: '0', + netVolIsNeg: false, startingCapital: '0', }, startTime: 1, @@ -77,7 +78,9 @@ test('renders table with correct data', async () => { // checking for (let i = 0; i < mockOrderApy.length; i++) { - const display = bigintStringToPercentage(mockOrderApy[i].denominatedPerformance!.apy, 18, 5); + const display = + (mockOrderApy[i].denominatedPerformance!.apyIsNeg ? '-' : '') + + bigintStringToPercentage(mockOrderApy[i].denominatedPerformance!.apy, 18, 5); expect(rows[i]).toHaveTextContent(display); } }); From 27a1b9751252b34baa8ec4098885cfd49d7ba346 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 21 Nov 2024 23:29:51 +0000 Subject: [PATCH 39/50] fix minor issues --- .../subgraph/src/performance/order_performance.rs | 14 +++++++++++--- packages/ui-components/src/lib/index.ts | 3 ++- .../src/lib/components/tables/OrderAPY.svelte | 3 +-- .../src/lib/components/tables/OrderAPY.test.ts | 3 ++- .../components/tables/OrderVaultsVolTable.test.ts | 3 ++- tauri-app/src/lib/queries/orderTradesList.ts | 2 +- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/subgraph/src/performance/order_performance.rs b/crates/subgraph/src/performance/order_performance.rs index 1c5849945..f967325ff 100644 --- a/crates/subgraph/src/performance/order_performance.rs +++ b/crates/subgraph/src/performance/order_performance.rs @@ -341,8 +341,8 @@ pub fn get_order_pairs_ratio(order: &Order, trades: &[Trade]) -> HashMap HashMap import { createInfiniteQuery } from '@tanstack/svelte-query'; - import TanstackAppTable from './TanstackAppTable.svelte'; - import { QKEY_ORDER_APY } from '$lib/queries/keys'; + import { TanstackAppTable, QKEY_ORDER_APY } from '@rainlanguage/ui-components'; import { getOrderApy } from '$lib/queries/orderTradesList'; import { subgraphUrl } from '$lib/stores/settings'; import { TableBodyCell, TableHeadCell } from 'flowbite-svelte'; diff --git a/tauri-app/src/lib/components/tables/OrderAPY.test.ts b/tauri-app/src/lib/components/tables/OrderAPY.test.ts index c9d3829be..2ecb84f91 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.test.ts +++ b/tauri-app/src/lib/components/tables/OrderAPY.test.ts @@ -69,7 +69,8 @@ test('renders table with correct data', async () => { render(OrderApy, { context: new Map([['$$_queryClient', queryClient]]), - props: { id: '1' }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props: { id: '1' } as any, }); await waitFor(async () => { diff --git a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts index 9a59fe1ed..5fedaf586 100644 --- a/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts +++ b/tauri-app/src/lib/components/tables/OrderVaultsVolTable.test.ts @@ -78,7 +78,8 @@ test('renders table with correct data', async () => { render(OrderVaultsVolTable, { context: new Map([['$$_queryClient', queryClient]]), - props: { id: '1' }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props: { id: '1' } as any, }); await waitFor(async () => { diff --git a/tauri-app/src/lib/queries/orderTradesList.ts b/tauri-app/src/lib/queries/orderTradesList.ts index 784a5b896..ca2165c36 100644 --- a/tauri-app/src/lib/queries/orderTradesList.ts +++ b/tauri-app/src/lib/queries/orderTradesList.ts @@ -103,7 +103,7 @@ export const getOrderApy = async ( return []; } return [ - await invoke('order_apy', { + await invoke('order_performance', { orderId: id, subgraphArgs: { url }, startTimestamp, From 0b8ae38c49639c9869134d4c78c1c10f72d1f7bc Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Fri, 22 Nov 2024 00:04:03 +0000 Subject: [PATCH 40/50] Update order_performance.rs --- .../src/performance/order_performance.rs | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/crates/subgraph/src/performance/order_performance.rs b/crates/subgraph/src/performance/order_performance.rs index f967325ff..f7f6cc39d 100644 --- a/crates/subgraph/src/performance/order_performance.rs +++ b/crates/subgraph/src/performance/order_performance.rs @@ -159,8 +159,8 @@ impl OrderPerformance { // if there was no success with any of the order's tokens, simply return None // for the APY. let mut processed_tokens: Vec<&Erc20> = vec![]; - let mut all_tokens_vols_list: Vec = vec![]; - let mut full_apy_in_distinct_token_denominations = vec![]; + let mut tokens_vol_list: Vec = vec![]; + let mut token_denominated_performance = vec![]; for token in &vaults_apy { // skip if token is alreaedy processed if processed_tokens.contains(&&token.token) { @@ -174,7 +174,7 @@ impl OrderPerformance { let mut combined_capital = U256::ZERO; let mut combined_net_vol = U256::ZERO; let mut combined_annual_rate_vol = U256::ZERO; - let mut current_token_vol_list: Vec = vec![]; + let mut current_token_vol_list: Vec = vec![]; for token_vault in &vaults_apy { if let Some(apy_details) = token_vault.apy_details { // a closure fn handles net vol combination @@ -223,8 +223,7 @@ impl OrderPerformance { .div_18(*YEAR18) .map_err(PerformanceError::from)?; - // sum up all token vaults' capitals and vols in the current's iteration's - // token denomination by using the direct ratio between the tokens + // sum up all token vaults' capitals and vols by using the direct ratio between the tokens if token_vault.token == token.token { combined_capital += apy_details.capital; handle_combined_net_vol(apy_details.net_vol); @@ -235,7 +234,7 @@ impl OrderPerformance { .map_err(PerformanceError::from)?; handle_combined_annual_rate_vol(annual_rate_vol); - current_token_vol_list.push(TokenDenominationVol { + current_token_vol_list.push(TokenBasedVol { net_vol: apy_details.net_vol, is_neg: apy_details.is_neg, token: &token.token, @@ -245,7 +244,7 @@ impl OrderPerformance { input: token.token.clone(), output: token_vault.token.clone(), }; - // convert to current denomination by the direct pair ratio if exists + // convert to current denomination by the direct pair ratio if it exists if let Some(Some(ratio)) = pair_ratio_map.get(&pair) { let capital_converted = apy_details .capital @@ -264,12 +263,14 @@ impl OrderPerformance { .map_err(PerformanceError::from)?; handle_combined_annual_rate_vol(annual_rate_vol_converted); - current_token_vol_list.push(TokenDenominationVol { + current_token_vol_list.push(TokenBasedVol { net_vol: net_vol_converted, is_neg: apy_details.is_neg, token: &token_vault.token, }); } else { + // if found no way to convert (there were no direct ratio between the tokens), + // break the loop and go to the next token and try that noway = true; break; } @@ -279,10 +280,11 @@ impl OrderPerformance { // 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 order from highest to lowest when sorted. + // the order's io tokens, and will pick the one that its token had the highest + // net vol amon all other vaults if !noway { if let Ok(apy) = combined_annual_rate_vol.div_18(combined_capital) { - full_apy_in_distinct_token_denominations.push(DenominatedPerformance { + token_denominated_performance.push(DenominatedPerformance { apy, apy_is_neg: net_vol_rate_is_neg, token: token.token.clone(), @@ -291,27 +293,27 @@ impl OrderPerformance { net_vol_is_neg, }); } - } else { - current_token_vol_list.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 settlement denomination - if all_tokens_vols_list.is_empty() { - all_tokens_vols_list.extend(current_token_vol_list); + // if we found a way to calculate apy in the current token denomination, + // we'll include all the tokens vaults net vol in this array to have a list + // of tokens net vols converted to current token + // later, sorting this array will give us the highest to lowest tokens net vols + // to pick the denomination from + if tokens_vol_list.is_empty() { + tokens_vol_list.extend(current_token_vol_list); + } } } // after array is sorted, pick the denomination with highest net vol by // iterating over tokens with highest vol to lowest and pick the first matching one - all_tokens_vols_list.sort(); - for token in all_tokens_vols_list.iter().rev() { - if let Some(denominated_apy) = full_apy_in_distinct_token_denominations + tokens_vol_list.sort(); + for token in tokens_vol_list.iter().rev() { + if let Some(denominated_performance) = token_denominated_performance .iter() .find(|&v| &v.token == token.token) { - order_performance.denominated_performance = Some(denominated_apy.clone()); + order_performance.denominated_performance = Some(denominated_performance.clone()); // return early as soon as a match is found return Ok(order_performance); } @@ -381,14 +383,14 @@ pub fn get_order_pairs_ratio(order: &Order, trades: &[Trade]) -> HashMap { +struct TokenBasedVol<'a> { token: &'a Erc20, net_vol: U256, is_neg: bool, } -impl<'a> Ord for TokenDenominationVol<'a> { +impl<'a> Ord for TokenBasedVol<'a> { fn clamp(self, _min: Self, _max: Self) -> Self where Self: Sized, @@ -442,7 +444,7 @@ impl<'a> Ord for TokenDenominationVol<'a> { } } } -impl<'a> PartialOrd for TokenDenominationVol<'a> { +impl<'a> PartialOrd for TokenBasedVol<'a> { fn ge(&self, other: &Self) -> bool { !matches!(self.cmp(other), Ordering::Less) } From a3b5ed7032118a1f1bd76c88acb5606c2b38a877 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Fri, 22 Nov 2024 01:33:59 +0000 Subject: [PATCH 41/50] update --- .../src/performance/order_performance.rs | 81 +++++++++---------- flake.nix | 2 +- tauri-app/src-tauri/src/commands/order.rs | 2 - .../lib/components/tables/OrderAPY.test.ts | 2 +- 4 files changed, 39 insertions(+), 48 deletions(-) diff --git a/crates/subgraph/src/performance/order_performance.rs b/crates/subgraph/src/performance/order_performance.rs index f7f6cc39d..c5eb0f56d 100644 --- a/crates/subgraph/src/performance/order_performance.rs +++ b/crates/subgraph/src/performance/order_performance.rs @@ -177,46 +177,6 @@ impl OrderPerformance { let mut current_token_vol_list: Vec = vec![]; for token_vault in &vaults_apy { if let Some(apy_details) = token_vault.apy_details { - // a closure fn handles net vol combination - let mut handle_combined_net_vol = |new_net_vol: U256| { - if apy_details.is_neg == net_vol_is_neg { - combined_net_vol += new_net_vol; - } else if net_vol_is_neg { - if new_net_vol >= combined_net_vol { - net_vol_is_neg = false; - combined_net_vol = new_net_vol - combined_net_vol; - } else { - combined_net_vol -= new_net_vol; - } - } else if combined_net_vol >= new_net_vol { - combined_net_vol -= new_net_vol; - } else { - net_vol_is_neg = true; - combined_net_vol = new_net_vol - combined_net_vol; - } - }; - - // a closure fn handles annual rate vol combination - let mut handle_combined_annual_rate_vol = |new_annual_rate_vol: U256| { - if apy_details.is_neg == net_vol_rate_is_neg { - combined_annual_rate_vol += new_annual_rate_vol; - } else if net_vol_rate_is_neg { - if new_annual_rate_vol >= combined_annual_rate_vol { - net_vol_rate_is_neg = false; - combined_annual_rate_vol = - new_annual_rate_vol - combined_annual_rate_vol; - } else { - combined_annual_rate_vol -= new_annual_rate_vol; - } - } else if combined_annual_rate_vol >= new_annual_rate_vol { - combined_annual_rate_vol -= new_annual_rate_vol; - } else { - net_vol_rate_is_neg = true; - combined_annual_rate_vol = - new_annual_rate_vol - combined_annual_rate_vol; - } - }; - // this vault's timeframe to year ratio let annual_rate = U256::from(apy_details.end_time - apy_details.start_time) .saturating_mul(ONE18) @@ -226,13 +186,19 @@ impl OrderPerformance { // sum up all token vaults' capitals and vols by using the direct ratio between the tokens if token_vault.token == token.token { combined_capital += apy_details.capital; - handle_combined_net_vol(apy_details.net_vol); + (combined_net_vol, net_vol_is_neg) = combine( + (apy_details.net_vol, apy_details.is_neg), + (combined_net_vol, net_vol_is_neg), + ); let annual_rate_vol = apy_details .net_vol .div_18(annual_rate) .map_err(PerformanceError::from)?; - handle_combined_annual_rate_vol(annual_rate_vol); + (combined_annual_rate_vol, net_vol_rate_is_neg) = combine( + (annual_rate_vol, apy_details.is_neg), + (combined_annual_rate_vol, net_vol_rate_is_neg), + ); current_token_vol_list.push(TokenBasedVol { net_vol: apy_details.net_vol, @@ -256,12 +222,18 @@ impl OrderPerformance { .net_vol .mul_18(*ratio) .map_err(PerformanceError::from)?; - handle_combined_net_vol(net_vol_converted); + (combined_net_vol, net_vol_is_neg) = combine( + (net_vol_converted, apy_details.is_neg), + (combined_net_vol, net_vol_is_neg), + ); let annual_rate_vol_converted = net_vol_converted .div_18(annual_rate) .map_err(PerformanceError::from)?; - handle_combined_annual_rate_vol(annual_rate_vol_converted); + (combined_annual_rate_vol, net_vol_rate_is_neg) = combine( + (annual_rate_vol_converted, apy_details.is_neg), + (combined_annual_rate_vol, net_vol_rate_is_neg), + ); current_token_vol_list.push(TokenBasedVol { net_vol: net_vol_converted, @@ -383,6 +355,27 @@ pub fn get_order_pairs_ratio(order: &Order, trades: &[Trade]) -> HashMap (U256, bool) { + let mut acc = old_val.0; + let mut sign = old_val.1; + if new_val.1 == sign { + acc += new_val.0; + } else if sign { + if new_val.0 >= acc { + sign = false; + acc = new_val.0 - acc; + } else { + acc -= new_val.0; + } + } else if acc >= new_val.0 { + acc -= new_val.0; + } else { + sign = true; + acc = new_val.0 - acc; + } + (acc, sign) +} + /// helper struct that provides sorting tokens based on a given net vol #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct TokenBasedVol<'a> { diff --git a/flake.nix b/flake.nix index 2ffaccf9a..408bf523f 100644 --- a/flake.nix +++ b/flake.nix @@ -96,7 +96,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=packages/ui-components/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/performance/vol.rs crates/subgraph/src/performance/apy.rs crates/subgraph/src/performance/order_performance.rs --lang=typescript --output-file=packages/ui-components/src/lib/typeshare/subgraphTypes.ts; typeshare crates/settings/src/parse.rs --lang=typescript --output-file=packages/ui-components/src/lib/typeshare/appSettings.ts; diff --git a/tauri-app/src-tauri/src/commands/order.rs b/tauri-app/src-tauri/src/commands/order.rs index f3d916139..679a735a3 100644 --- a/tauri-app/src-tauri/src/commands/order.rs +++ b/tauri-app/src-tauri/src/commands/order.rs @@ -7,8 +7,6 @@ use rain_orderbook_common::{ remove_order::RemoveOrderArgs, subgraph::SubgraphArgs, transaction::TransactionArgs, types::FlattenError, types::OrderDetailExtended, types::OrderFlattened, }; -use rain_orderbook_subgraph_client::{types::common::*, PaginationArgs}; -use rain_orderbook_subgraph_client::{MultiOrderbookSubgraphClient, MultiSubgraphArgs}; use std::fs; use std::path::PathBuf; use tauri::AppHandle; diff --git a/tauri-app/src/lib/components/tables/OrderAPY.test.ts b/tauri-app/src/lib/components/tables/OrderAPY.test.ts index 2ecb84f91..c52ca9ba2 100644 --- a/tauri-app/src/lib/components/tables/OrderAPY.test.ts +++ b/tauri-app/src/lib/components/tables/OrderAPY.test.ts @@ -62,7 +62,7 @@ test('renders table with correct data', async () => { const queryClient = new QueryClient(); mockIPC((cmd) => { - if (cmd === 'order_apy') { + if (cmd === 'order_performance') { return mockOrderApy[0]; } }); From 6f9eb78959e1c5a5f47cb525fdd541913c0c3a7e Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Fri, 22 Nov 2024 17:37:54 +0000 Subject: [PATCH 42/50] apply requested changes --- crates/subgraph/src/performance/apy.rs | 11 +--- crates/subgraph/src/performance/mod.rs | 2 + .../src/performance/order_performance.rs | 36 +++++++---- crates/subgraph/src/performance/vol.rs | 8 +-- crates/subgraph/src/types/impls.rs | 60 +++++++++++-------- .../components/charts/APYTimeFilters.svelte | 6 +- .../components/charts/TableTimeFilters.svelte | 6 +- tauri-app/src/lib/services/time.ts | 16 ++++- tauri-app/src/lib/utils/number.ts | 15 +++++ 9 files changed, 100 insertions(+), 60 deletions(-) diff --git a/crates/subgraph/src/performance/apy.rs b/crates/subgraph/src/performance/apy.rs index 7d058b088..b689cc92c 100644 --- a/crates/subgraph/src/performance/apy.rs +++ b/crates/subgraph/src/performance/apy.rs @@ -103,16 +103,7 @@ pub fn get_vaults_apy( &first_day_last_trade.output_vault_balance_change }; let starting_capital = U256::from_str(&vault_balance_change.new_vault_balance.0)? - .scale_18( - vault_balance_change - .vault - .token - .decimals - .as_ref() - .map(|v| v.0.as_str()) - .unwrap_or("18") - .parse()?, - ) + .scale_18(vault_balance_change.vault.token.get_decimals()?) .map_err(PerformanceError::from)?; // the time range for this token vault diff --git a/crates/subgraph/src/performance/mod.rs b/crates/subgraph/src/performance/mod.rs index 78a6b17e4..685ac2b7f 100644 --- a/crates/subgraph/src/performance/mod.rs +++ b/crates/subgraph/src/performance/mod.rs @@ -23,4 +23,6 @@ pub enum PerformanceError { ParseUnsignedError(#[from] ParseError), #[error(transparent)] ParseIntError(#[from] ParseIntError), + #[error("divide by zero")] + DivByZero, } diff --git a/crates/subgraph/src/performance/order_performance.rs b/crates/subgraph/src/performance/order_performance.rs index c5eb0f56d..195301c91 100644 --- a/crates/subgraph/src/performance/order_performance.rs +++ b/crates/subgraph/src/performance/order_performance.rs @@ -329,19 +329,33 @@ pub fn get_order_pairs_ratio(order: &Order, trades: &[Trade]) -> HashMap Some(v), + Err(e) => { + if let PerformanceError::DivByZero = e { + Some(U256::MAX) } else { - [inverse_ratio, ratio] + None } - }) + } + }; + let inverse_ratio = match latest_trade.inverse_ratio() { + Ok(v) => Some(v), + Err(e) => { + if let PerformanceError::DivByZero = e { + Some(U256::MAX) + } else { + None + } + } + }; + ratio.zip(inverse_ratio).map(|(ratio, inverse_ratio)| { + if latest_trade.input_vault_balance_change.vault.token == input.token { + [ratio, inverse_ratio] + } else { + [inverse_ratio, ratio] + } + }) }); // io diff --git a/crates/subgraph/src/performance/vol.rs b/crates/subgraph/src/performance/vol.rs index c24c88c00..82e4f98a6 100644 --- a/crates/subgraph/src/performance/vol.rs +++ b/crates/subgraph/src/performance/vol.rs @@ -140,13 +140,7 @@ impl VaultVolume { /// Creates a new instance of self with all volume values as 18 decimals point pub fn scale_18(&self) -> Result { - let token_decimals: u8 = self - .token - .decimals - .as_ref() - .map(|v| v.0.as_str()) - .unwrap_or("18") - .parse()?; + let token_decimals: u8 = self.token.get_decimals()?; Ok(VaultVolume { id: self.id.clone(), token: self.token.clone(), diff --git a/crates/subgraph/src/types/impls.rs b/crates/subgraph/src/types/impls.rs index 9e370b50f..2f7980320 100644 --- a/crates/subgraph/src/types/impls.rs +++ b/crates/subgraph/src/types/impls.rs @@ -4,6 +4,17 @@ use alloy::primitives::U256; use rain_orderbook_math::BigUintMath; use std::str::FromStr; +impl Erc20 { + pub fn get_decimals(&self) -> Result { + Ok(self + .decimals + .as_ref() + .map(|v| v.0.as_str()) + .unwrap_or("18") + .parse()?) + } +} + impl Trade { /// Scales this trade's io to 18 point decimals in U256 pub fn scale_18_io(&self) -> Result<(U256, U256), PerformanceError> { @@ -18,25 +29,13 @@ impl Trade { &self.output_vault_balance_change.amount.0 }; Ok(( - U256::from_str(input_amount)?.scale_18( - self.input_vault_balance_change - .vault - .token - .decimals - .as_ref() - .map(|v| v.0.as_str()) - .unwrap_or("18") - .parse()?, - )?, + U256::from_str(input_amount)? + .scale_18(self.input_vault_balance_change.vault.token.get_decimals()?)?, U256::from_str(output_amount)?.scale_18( self.output_vault_balance_change .vault .token - .decimals - .as_ref() - .map(|v| v.0.as_str()) - .unwrap_or("18") - .parse()?, + .get_decimals()?, )?, )) } @@ -44,10 +43,8 @@ impl Trade { /// Calculates the trade's I/O ratio pub fn ratio(&self) -> Result { let (input, output) = self.scale_18_io()?; - if output.is_zero() && input.is_zero() { - Ok(U256::ZERO) - } else if output.is_zero() { - Ok(U256::MAX) + if output.is_zero() { + Err(PerformanceError::DivByZero) } else { Ok(input.div_18(output)?) } @@ -56,10 +53,8 @@ impl Trade { /// Calculates the trade's O/I ratio (inverse) pub fn inverse_ratio(&self) -> Result { let (input, output) = self.scale_18_io()?; - if output.is_zero() && input.is_zero() { - Ok(U256::ZERO) - } else if input.is_zero() { - Ok(U256::MAX) + if output.is_zero() { + Err(PerformanceError::DivByZero) } else { Ok(output.div_18(input)?) } @@ -117,12 +112,19 @@ mod test { } #[test] - fn test_ratio() { + fn test_ratio_happy() { let result = get_trade().ratio().unwrap(); let expected = U256::from_str("500000000000000000").unwrap(); assert_eq!(result, expected); } + #[test] + fn test_ratio_unhappy() { + let mut trade = get_trade(); + trade.output_vault_balance_change.amount = BigInt("0".to_string()); + matches!(trade.ratio().unwrap_err(), PerformanceError::DivByZero); + } + #[test] fn test_inverse_ratio() { let result = get_trade().inverse_ratio().unwrap(); @@ -130,6 +132,16 @@ mod test { assert_eq!(result, expected); } + #[test] + fn test_inverse_ratio_unhappy() { + let mut trade = get_trade(); + trade.input_vault_balance_change.amount = BigInt("0".to_string()); + matches!( + trade.inverse_ratio().unwrap_err(), + PerformanceError::DivByZero + ); + } + // helper to get trade struct fn get_trade() -> Trade { let token_address = Address::from_slice(&[0x11u8; 20]); diff --git a/tauri-app/src/lib/components/charts/APYTimeFilters.svelte b/tauri-app/src/lib/components/charts/APYTimeFilters.svelte index 103fd0d79..07a8b06c4 100644 --- a/tauri-app/src/lib/components/charts/APYTimeFilters.svelte +++ b/tauri-app/src/lib/components/charts/APYTimeFilters.svelte @@ -1,13 +1,13 @@ + + + + + + + APY + + + + + {item.denominatedPerformance + ? (item.denominatedPerformance.apyIsNeg ? '-' : '') + + bigintStringToPercentage(item.denominatedPerformance.apy, 18, 5) + + '% in ' + + (item.denominatedPerformance.token.symbol ?? + item.denominatedPerformance.token.name ?? + item.denominatedPerformance.token.address) + : 'Unavailable APY'} + + + diff --git a/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte b/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte index c261ebec2..ca3a5b88d 100644 --- a/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte +++ b/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte @@ -52,16 +52,17 @@ - {formatUnits(BigInt(item.totalIn), Number(item.token.decimals ?? 0))} + {formatUnits(BigInt(item.volDetails.totalIn), Number(item.token.decimals ?? 0))} - {formatUnits(BigInt(item.totalOut), Number(item.token.decimals ?? 0))} + {formatUnits(BigInt(item.volDetails.totalOut), Number(item.token.decimals ?? 0))} - {formatUnits(BigInt(item.netVol), Number(item.token.decimals ?? 0))} + {(BigInt(item.volDetails.totalIn) >= BigInt(item.volDetails.totalOut) ? '' : '-') + + formatUnits(BigInt(item.volDetails.netVol), Number(item.token.decimals ?? 0))} - {formatUnits(BigInt(item.totalVol), Number(item.token.decimals ?? 0))} + {formatUnits(BigInt(item.volDetails.totalVol), Number(item.token.decimals ?? 0))} diff --git a/tauri-app/src/lib/services/time.ts b/packages/ui-components/src/lib/services/time.ts similarity index 52% rename from tauri-app/src/lib/services/time.ts rename to packages/ui-components/src/lib/services/time.ts index e493489af..88539bba3 100644 --- a/tauri-app/src/lib/services/time.ts +++ b/packages/ui-components/src/lib/services/time.ts @@ -5,17 +5,17 @@ export const TIME_DELTA_30_DAYS = TIME_DELTA_24_HOURS * 30; export const TIME_DELTA_1_YEAR = TIME_DELTA_24_HOURS * 365; export function dateTimestamp(date: Date): number { - return Math.floor(date.getTime() / 1000); + return Math.floor(date.getTime() / 1000); } if (import.meta.vitest) { - const { it, expect } = import.meta.vitest; + const { it, expect } = import.meta.vitest; - it('should get date timestamp in seconds', () => { - const date = new Date(2022, 1, 16, 17, 32, 11, 168); - const result = dateTimestamp(date); - const expected = Math.floor(date.getTime() / 1000); + it('should get date timestamp in seconds', () => { + const date = new Date(2022, 1, 16, 17, 32, 11, 168); + const result = dateTimestamp(date); + const expected = Math.floor(date.getTime() / 1000); - expect(result).toEqual(expected); - }); + expect(result).toEqual(expected); + }); } diff --git a/packages/ui-components/src/lib/utils/number.ts b/packages/ui-components/src/lib/utils/number.ts new file mode 100644 index 000000000..7652029ac --- /dev/null +++ b/packages/ui-components/src/lib/utils/number.ts @@ -0,0 +1,37 @@ +import { formatUnits } from 'viem'; + +/** + * 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 + */ +export function bigintStringToPercentage( + value: string, + valueDecimals: number, + finalDecimalsDigits?: number +): string { + const finalDecimals = + typeof finalDecimalsDigits !== 'undefined' ? finalDecimalsDigits : valueDecimals; + let valueString = formatUnits(BigInt(value) * 100n, valueDecimals); + const index = valueString.indexOf('.'); + if (index > -1) { + valueString = valueString.substring(0, finalDecimals === 0 ? index : index + finalDecimals + 1); + } + return valueString; +} + +if (import.meta.vitest) { + const { it, expect } = import.meta.vitest; + + it('should get percentage string from bigint string', () => { + const value = '123456000000000000'; + const decimals = 18; + const finalDecimalsDigits = 4; + + const result = bigintStringToPercentage(value, decimals, finalDecimalsDigits); + const expected = '12.3456'; + + expect(result).toEqual(expected); + }); +} diff --git a/tauri-app/src-tauri/src/commands/order_take.rs b/tauri-app/src-tauri/src/commands/order_take.rs index f3f1287db..c929e8696 100644 --- a/tauri-app/src-tauri/src/commands/order_take.rs +++ b/tauri-app/src-tauri/src/commands/order_take.rs @@ -4,7 +4,6 @@ use rain_orderbook_common::{ }; use rain_orderbook_subgraph_client::performance::vol::VaultVolume; -use rain_orderbook_subgraph_client::performance::OrderPerformance; use rain_orderbook_subgraph_client::{types::common::*, PaginationArgs}; use std::fs; use std::path::PathBuf; @@ -81,16 +80,3 @@ pub async fn order_trades_count( .await? .len()) } - -#[tauri::command] -pub async fn order_performance( - order_id: String, - subgraph_args: SubgraphArgs, - start_timestamp: Option, - end_timestamp: Option, -) -> CommandResult { - let client = subgraph_args.to_subgraph_client().await?; - 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 93c455190..77af0bbcd 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_performance, order_trades_count, order_trades_list, order_trades_list_write_csv, + order_trades_count, order_trades_list, order_trades_list_write_csv, order_vaults_volume, }; use commands::trade_debug::debug_trade; @@ -83,7 +83,6 @@ fn run_tauri_app() { validate_raindex_version, order_vaults_volume, order_trades_count, - order_performance, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/tauri-app/src/lib/queries/orderTradesList.ts b/tauri-app/src/lib/queries/orderTradesList.ts deleted file mode 100644 index 484d4b77b..000000000 --- a/tauri-app/src/lib/queries/orderTradesList.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { VaultVolume, OrderPerformance } from '$lib/typeshare/subgraphTypes'; -import { invoke } from '@tauri-apps/api'; - -export type OrderTradesListArgs = { - orderId: string; - subgraphArgs: { - url: string; - }; - paginationArgs: { - page: number; - pageSize: number; - }; - startTimestamp?: number; - endTimestamp?: number; -}; - -export const orderVaultsVolume = async ( - id: string, - url: string | undefined, - startTimestamp?: number, - endTimestamp?: number, -) => { - if (!url) { - return []; - } - return await invoke('order_vaults_volume', { - orderId: id, - subgraphArgs: { url }, - startTimestamp, - endTimestamp, - } as OrderTradesListArgs); -}; - -export const getOrderApy = async ( - id: string, - url: string | undefined, - startTimestamp?: number, - endTimestamp?: number, -) => { - if (!url) { - return []; - } - return [ - await invoke('order_performance', { - orderId: id, - subgraphArgs: { url }, - startTimestamp, - endTimestamp, - } as OrderTradesListArgs), - ]; -}; diff --git a/tauri-app/src/lib/utils/number.ts b/tauri-app/src/lib/utils/number.ts index 08a23f23f..79417b775 100644 --- a/tauri-app/src/lib/utils/number.ts +++ b/tauri-app/src/lib/utils/number.ts @@ -3,39 +3,3 @@ import { formatUnits } from 'viem'; export function bigintToFloat(value: bigint, decimals: number) { return parseFloat(formatUnits(value, decimals)); } - -/** - * 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 - */ -export function bigintStringToPercentage( - value: string, - valueDecimals: number, - finalDecimalsDigits?: number, -): string { - const finalDecimals = - typeof finalDecimalsDigits !== 'undefined' ? finalDecimalsDigits : valueDecimals; - let valueString = formatUnits(BigInt(value) * 100n, valueDecimals); - const index = valueString.indexOf('.'); - if (index > -1) { - valueString = valueString.substring(0, finalDecimals === 0 ? index : index + finalDecimals + 1); - } - return valueString; -} - -if (import.meta.vitest) { - const { it, expect } = import.meta.vitest; - - it('should get percentage string from bigint string', () => { - const value = '123456000000000000'; - const decimals = 18; - const finalDecimalsDigits = 4; - - const result = bigintStringToPercentage(value, decimals, finalDecimalsDigits); - const expected = '12.3456'; - - expect(result).toEqual(expected); - }); -} From 0fce59b34f09bc2202da79258f763b6ceaf17e43 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 19 Dec 2024 01:58:24 +0000 Subject: [PATCH 48/50] fix --- crates/js_api/src/subgraph/order.rs | 2 +- crates/subgraph/src/performance/apy.rs | 8 +- crates/subgraph/src/performance/mod.rs | 2 + .../src/performance/order_performance.rs | 256 ++++++++++++++++-- crates/subgraph/src/performance/vol.rs | 58 +++- crates/subgraph/src/types/impls.rs | 27 +- flake.nix | 4 +- packages/orderbook/test/js_api/order.test.ts | 187 ++++++++++++- .../src/__tests__/OrderAPY.test.ts | 99 +++---- .../src/__tests__/OrderVaultsVolTable.test.ts | 2 +- .../components/charts/APYTimeFilters.svelte | 86 +++--- .../src/lib/components/tables/OrderAPY.svelte | 23 +- .../tables/OrderVaultsVolTable.svelte | 3 +- .../src/lib/components/tables/OrderAPY.svelte | 44 --- .../lib/components/tables/OrderAPY.test.ts | 88 ------ 15 files changed, 600 insertions(+), 289 deletions(-) delete mode 100644 tauri-app/src/lib/components/tables/OrderAPY.svelte delete mode 100644 tauri-app/src/lib/components/tables/OrderAPY.test.ts diff --git a/crates/js_api/src/subgraph/order.rs b/crates/js_api/src/subgraph/order.rs index eeef86188..2c8b9b5e5 100644 --- a/crates/js_api/src/subgraph/order.rs +++ b/crates/js_api/src/subgraph/order.rs @@ -119,7 +119,7 @@ pub async fn order_vaults_volume( } /// Measures an order's performance (including vaults apy and vol and total apy and vol) -#[wasm_bindgen(js_name = "getOrderApy")] +#[wasm_bindgen(js_name = "getOrderPerformance")] pub async fn order_performance( url: &str, order_id: &str, diff --git a/crates/subgraph/src/performance/apy.rs b/crates/subgraph/src/performance/apy.rs index 598c70ffd..fdd2987b7 100644 --- a/crates/subgraph/src/performance/apy.rs +++ b/crates/subgraph/src/performance/apy.rs @@ -5,10 +5,11 @@ use crate::{ }; use alloy::primitives::U256; use chrono::TimeDelta; +#[cfg(target_family = "wasm")] +use rain_orderbook_bindings::{impl_all_wasm_traits, wasm_traits::prelude::*}; use rain_orderbook_math::{BigUintMath, ONE18}; use serde::{Deserialize, Serialize}; use std::str::FromStr; -use rain_orderbook_bindings::{impl_all_wasm_traits, wasm_traits::prelude::*}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(rename_all = "camelCase")] @@ -16,8 +17,11 @@ use rain_orderbook_bindings::{impl_all_wasm_traits, wasm_traits::prelude::*}; pub struct APYDetails { pub start_time: u64, pub end_time: u64, + #[cfg_attr(target_family = "wasm", tsify(type = "string"))] pub net_vol: U256, + #[cfg_attr(target_family = "wasm", tsify(type = "string"))] pub capital: U256, + #[cfg_attr(target_family = "wasm", tsify(type = "string"))] pub apy: U256, pub is_neg: bool, } @@ -42,7 +46,7 @@ pub struct TokenPair { #[cfg(target_family = "wasm")] mod impls { use super::*; - impl_all_wasm_traits!(ToeknPair); + impl_all_wasm_traits!(TokenPair); impl_all_wasm_traits!(VaultAPY); impl_all_wasm_traits!(APYDetails); } diff --git a/crates/subgraph/src/performance/mod.rs b/crates/subgraph/src/performance/mod.rs index 685ac2b7f..ef2a2c32f 100644 --- a/crates/subgraph/src/performance/mod.rs +++ b/crates/subgraph/src/performance/mod.rs @@ -25,4 +25,6 @@ pub enum PerformanceError { ParseIntError(#[from] ParseIntError), #[error("divide by zero")] DivByZero, + #[error("Found no trades")] + NoTrades, } diff --git a/crates/subgraph/src/performance/order_performance.rs b/crates/subgraph/src/performance/order_performance.rs index d27ed1ce5..feff118f2 100644 --- a/crates/subgraph/src/performance/order_performance.rs +++ b/crates/subgraph/src/performance/order_performance.rs @@ -7,11 +7,12 @@ use crate::{ types::common::{Erc20, Order, Trade}, }; use alloy::primitives::U256; +#[cfg(target_family = "wasm")] +use rain_orderbook_bindings::{impl_all_wasm_traits, wasm_traits::prelude::*}; use rain_orderbook_math::{BigUintMath, ONE18}; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::HashMap; -use rain_orderbook_bindings::{impl_all_wasm_traits, wasm_traits::prelude::*}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(rename_all = "camelCase")] @@ -34,14 +35,17 @@ pub struct DenominatedPerformance { /// The denomination token pub token: Erc20, /// Order's APY raw value + #[cfg_attr(target_family = "wasm", tsify(type = "string"))] pub apy: U256, /// Determines if apy is negative or not pub apy_is_neg: bool, /// Order's net vol raw value + #[cfg_attr(target_family = "wasm", tsify(type = "string"))] pub net_vol: U256, /// Determines if net_vol is negative or not pub net_vol_is_neg: bool, /// Order's starting capital + #[cfg_attr(target_family = "wasm", tsify(type = "string"))] pub starting_capital: U256, } @@ -88,16 +92,7 @@ impl OrderPerformance { ) -> Result { // return early if there are no trades 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, - }); + return Err(PerformanceError::NoTrades); } let vaults_vol = get_vaults_vol(trades)?; let vaults_apy = get_vaults_apy(trades, &vaults_vol, start_timestamp, end_timestamp)?; @@ -177,9 +172,9 @@ impl OrderPerformance { let mut noway = false; let mut net_vol_is_neg = false; let mut net_vol_rate_is_neg = false; - let mut combined_capital = U256::ZERO; - let mut combined_net_vol = U256::ZERO; - let mut combined_annual_rate_vol = U256::ZERO; + let mut acc_capital = U256::ZERO; + let mut acc_net_vol = U256::ZERO; + let mut acc_annual_rate_vol = U256::ZERO; let mut current_token_vol_list: Vec = vec![]; for token_vault in &vaults_apy { if let Some(apy_details) = token_vault.apy_details { @@ -191,19 +186,19 @@ impl OrderPerformance { // sum up all token vaults' capitals and vols by using the direct ratio between the tokens if token_vault.token == token.token { - combined_capital += apy_details.capital; - (combined_net_vol, net_vol_is_neg) = combine( + acc_capital += apy_details.capital; + (acc_net_vol, net_vol_is_neg) = accumulate( (apy_details.net_vol, apy_details.is_neg), - (combined_net_vol, net_vol_is_neg), + (acc_net_vol, net_vol_is_neg), ); let annual_rate_vol = apy_details .net_vol .div_18(annual_rate) .map_err(PerformanceError::from)?; - (combined_annual_rate_vol, net_vol_rate_is_neg) = combine( + (acc_annual_rate_vol, net_vol_rate_is_neg) = accumulate( (annual_rate_vol, apy_details.is_neg), - (combined_annual_rate_vol, net_vol_rate_is_neg), + (acc_annual_rate_vol, net_vol_rate_is_neg), ); current_token_vol_list.push(TokenBasedVol { @@ -222,23 +217,23 @@ impl OrderPerformance { .capital .mul_18(*ratio) .map_err(PerformanceError::from)?; - combined_capital += capital_converted; + acc_capital += capital_converted; let net_vol_converted = apy_details .net_vol .mul_18(*ratio) .map_err(PerformanceError::from)?; - (combined_net_vol, net_vol_is_neg) = combine( + (acc_net_vol, net_vol_is_neg) = accumulate( (net_vol_converted, apy_details.is_neg), - (combined_net_vol, net_vol_is_neg), + (acc_net_vol, net_vol_is_neg), ); let annual_rate_vol_converted = net_vol_converted .div_18(annual_rate) .map_err(PerformanceError::from)?; - (combined_annual_rate_vol, net_vol_rate_is_neg) = combine( + (acc_annual_rate_vol, net_vol_rate_is_neg) = accumulate( (annual_rate_vol_converted, apy_details.is_neg), - (combined_annual_rate_vol, net_vol_rate_is_neg), + (acc_annual_rate_vol, net_vol_rate_is_neg), ); current_token_vol_list.push(TokenBasedVol { @@ -261,13 +256,13 @@ impl OrderPerformance { // the order's io tokens, and will pick the one that its token had the highest // net vol amon all other vaults if !noway { - if let Ok(apy) = combined_annual_rate_vol.div_18(combined_capital) { + if let Ok(apy) = acc_annual_rate_vol.div_18(acc_capital) { token_denominated_performance.push(DenominatedPerformance { apy, apy_is_neg: net_vol_rate_is_neg, token: token.token.clone(), - starting_capital: combined_capital, - net_vol: combined_net_vol, + starting_capital: acc_capital, + net_vol: acc_net_vol, net_vol_is_neg, }); } @@ -375,7 +370,7 @@ pub fn get_order_pairs_ratio(order: &Order, trades: &[Trade]) -> HashMap (U256, bool) { +fn accumulate(new_val: (U256, bool), old_val: (U256, bool)) -> (U256, bool) { let mut acc = old_val.0; let mut sign = old_val.1; if new_val.1 == sign { @@ -485,6 +480,183 @@ mod test { use alloy::primitives::{Address, B256}; use std::str::FromStr; + #[test] + fn test_token_based_vol_ord_parial_ord() { + 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())), + }; + + // positive == positive + let a = TokenBasedVol { + token: &token, + net_vol: U256::from(1), + is_neg: false, + }; + let b = TokenBasedVol { + token: &token, + net_vol: U256::from(1), + is_neg: false, + }; + assert!(matches!(a.cmp(&b), Ordering::Equal)); + assert!(matches!(a.partial_cmp(&b), Some(Ordering::Equal))); + assert_eq!(a.clone().min(b.clone()), a); + assert_eq!(a.clone().max(b.clone()), a); + assert!(!a.gt(&b)); + assert!(a.ge(&b)); + assert!(a.le(&b)); + assert!(!a.lt(&b)); + + // negative == negative + let a = TokenBasedVol { + token: &token, + net_vol: U256::from(1), + is_neg: true, + }; + let b = TokenBasedVol { + token: &token, + net_vol: U256::from(1), + is_neg: true, + }; + assert!(matches!(a.cmp(&b), Ordering::Equal)); + assert!(matches!(a.partial_cmp(&b), Some(Ordering::Equal))); + assert_eq!(a.clone().min(b.clone()), a); + assert_eq!(a.clone().max(b.clone()), a); + assert!(!a.gt(&b)); + assert!(a.ge(&b)); + assert!(a.le(&b)); + assert!(!a.lt(&b)); + + // positive > positive + let a = TokenBasedVol { + token: &token, + net_vol: U256::from(2), + is_neg: false, + }; + let b = TokenBasedVol { + token: &token, + net_vol: U256::from(1), + is_neg: false, + }; + assert!(matches!(a.cmp(&b), Ordering::Greater)); + assert!(matches!(a.partial_cmp(&b), Some(Ordering::Greater))); + assert_eq!(a.clone().min(b.clone()), b); + assert_eq!(a.clone().max(b.clone()), a); + assert!(a.gt(&b)); + assert!(a.ge(&b)); + assert!(!a.le(&b)); + assert!(!a.lt(&b)); + + // positive < positive + let a = TokenBasedVol { + token: &token, + net_vol: U256::from(1), + is_neg: false, + }; + let b = TokenBasedVol { + token: &token, + net_vol: U256::from(2), + is_neg: false, + }; + assert!(matches!(a.cmp(&b), Ordering::Less)); + assert!(matches!(a.partial_cmp(&b), Some(Ordering::Less))); + assert_eq!(a.clone().min(b.clone()), a); + assert_eq!(a.clone().max(b.clone()), b); + assert!(!a.gt(&b)); + assert!(!a.ge(&b)); + assert!(a.le(&b)); + assert!(a.lt(&b)); + + // negative > negative + let a = TokenBasedVol { + token: &token, + net_vol: U256::from(1), + is_neg: true, + }; + let b = TokenBasedVol { + token: &token, + net_vol: U256::from(2), + is_neg: true, + }; + assert!(matches!(a.cmp(&b), Ordering::Greater)); + assert!(matches!(a.partial_cmp(&b), Some(Ordering::Greater))); + assert_eq!(a.clone().min(b.clone()), b); + assert_eq!(a.clone().max(b.clone()), a); + assert!(a.gt(&b)); + assert!(a.ge(&b)); + assert!(!a.le(&b)); + assert!(!a.lt(&b)); + + // negative < negative + let a = TokenBasedVol { + token: &token, + net_vol: U256::from(2), + is_neg: true, + }; + let b = TokenBasedVol { + token: &token, + net_vol: U256::from(1), + is_neg: true, + }; + assert!(matches!(a.cmp(&b), Ordering::Less)); + assert!(matches!(a.partial_cmp(&b), Some(Ordering::Less))); + assert_eq!(a.clone().min(b.clone()), a); + assert_eq!(a.clone().max(b.clone()), b); + assert!(!a.gt(&b)); + assert!(!a.ge(&b)); + assert!(a.le(&b)); + assert!(a.lt(&b)); + } + + #[test] + fn test_accumulate() { + // both positive + let new_val = (U256::from(1), false); + let old_val = (U256::from(2), false); + let result = accumulate(new_val, old_val); + let expected = (U256::from(3), false); + assert_eq!(result, expected); + + // both negative + let new_val = (U256::from(1), true); + let old_val = (U256::from(2), true); + let result = accumulate(new_val, old_val); + let expected = (U256::from(3), true); + assert_eq!(result, expected); + + // negative < positive + let new_val = (U256::from(1), true); + let old_val = (U256::from(2), false); + let result = accumulate(new_val, old_val); + let expected = (U256::from(1), false); + assert_eq!(result, expected); + + // negative > positive + let new_val = (U256::from(2), true); + let old_val = (U256::from(1), false); + let result = accumulate(new_val, old_val); + let expected = (U256::from(1), true); + assert_eq!(result, expected); + + // positive < negative + let new_val = (U256::from(1), false); + let old_val = (U256::from(2), true); + let result = accumulate(new_val, old_val); + let expected = (U256::from(1), true); + assert_eq!(result, expected); + + // positive > negative + let new_val = (U256::from(2), false); + let old_val = (U256::from(1), true); + let result = accumulate(new_val, old_val); + let expected = (U256::from(1), false); + assert_eq!(result, expected); + } + #[test] fn test_get_pairs_ratio() { let trades = get_trades(); @@ -510,7 +682,33 @@ mod test { } #[test] - fn test_get_order_performance() { + fn test_get_pairs_ratio_unhappy() { + let mut trades = get_trades(); + // set some corrupt value + trades[0].input_vault_balance_change.amount = BigInt("abcd".to_string()); + 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(), + }, + None, + ); + expected.insert( + TokenPair { + input: token1.clone(), + output: token2.clone(), + }, + None, + ); + + assert_eq!(result, expected); + } + + #[test] + fn test_measure_order_performance() { let order = get_order(); let trades = get_trades(); let [token1, token2] = get_tokens(); diff --git a/crates/subgraph/src/performance/vol.rs b/crates/subgraph/src/performance/vol.rs index 2f23615c2..135933ddd 100644 --- a/crates/subgraph/src/performance/vol.rs +++ b/crates/subgraph/src/performance/vol.rs @@ -3,18 +3,23 @@ use crate::{ types::common::{Erc20, Trade}, }; use alloy::primitives::{ruint::ParseError, U256}; +#[cfg(target_family = "wasm")] +use rain_orderbook_bindings::{impl_all_wasm_traits, wasm_traits::prelude::*}; use rain_orderbook_math::BigUintMath; use serde::{Deserialize, Serialize}; use std::{cmp::Ordering, str::FromStr}; -use rain_orderbook_bindings::{impl_all_wasm_traits, wasm_traits::prelude::*}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)] #[serde(rename_all = "camelCase")] #[cfg_attr(target_family = "wasm", derive(Tsify))] pub struct VolumeDetails { + #[cfg_attr(target_family = "wasm", tsify(type = "string"))] pub total_in: U256, + #[cfg_attr(target_family = "wasm", tsify(type = "string"))] pub total_out: U256, + #[cfg_attr(target_family = "wasm", tsify(type = "string"))] pub total_vol: U256, + #[cfg_attr(target_family = "wasm", tsify(type = "string"))] pub net_vol: U256, } @@ -170,6 +175,57 @@ mod test { }; use alloy::primitives::{Address, B256}; + #[test] + fn test_is_net_vol_negative() { + 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())), + }; + + // negative vol + let vault_vol = VaultVolume { + id: "vault-id".to_string(), + token: token.clone(), + vol_details: VolumeDetails { + total_in: U256::from(20_500_000), + total_out: U256::from(30_000_000), + total_vol: U256::from(50_500_000), + net_vol: U256::from(9_500_000), + }, + }; + assert!(vault_vol.is_net_vol_negative()); + + // positive vol + let vault_vol = VaultVolume { + id: "vault-id".to_string(), + token: token.clone(), + vol_details: VolumeDetails { + total_in: U256::from(40_500_000), + total_out: U256::from(30_000_000), + total_vol: U256::from(50_500_000), + net_vol: U256::from(9_500_000), + }, + }; + assert!(!vault_vol.is_net_vol_negative()); + + // equal vol + let vault_vol = VaultVolume { + id: "vault-id".to_string(), + token: token.clone(), + vol_details: VolumeDetails { + total_in: U256::from(30_000_000), + total_out: U256::from(30_000_000), + total_vol: U256::from(50_500_000), + net_vol: U256::from(9_500_000), + }, + }; + assert!(!vault_vol.is_net_vol_negative()); + } + #[test] fn test_vaults_vol() { let bytes = Bytes("".to_string()); diff --git a/crates/subgraph/src/types/impls.rs b/crates/subgraph/src/types/impls.rs index 4afe537a7..6cb557dbc 100644 --- a/crates/subgraph/src/types/impls.rs +++ b/crates/subgraph/src/types/impls.rs @@ -70,6 +70,31 @@ mod test { }; use alloy::primitives::Address; + #[test] + fn test_token_get_decimals() { + // known decimals + let token = Erc20 { + id: Bytes(Address::from_slice(&[0x11u8; 20]).to_string()), + address: Bytes(Address::from_slice(&[0x11u8; 20]).to_string()), + name: Some("Token1".to_string()), + symbol: Some("Token1".to_string()), + decimals: Some(BigInt(6.to_string())), + }; + let result = token.get_decimals().unwrap(); + assert_eq!(result, 6); + + // unknown decimals, defaults to 18 + let token = Erc20 { + id: Bytes(Address::from_slice(&[0x11u8; 20]).to_string()), + address: Bytes(Address::from_slice(&[0x11u8; 20]).to_string()), + name: Some("Token1".to_string()), + symbol: Some("Token1".to_string()), + decimals: None, + }; + let result = token.get_decimals().unwrap(); + assert_eq!(result, 18); + } + #[test] fn test_scale_18_io() { let (input, output) = get_trade().scale_18_io().unwrap(); @@ -94,7 +119,7 @@ mod test { } #[test] - fn test_inverse_ratio() { + fn test_inverse_ratio_happy() { let result = get_trade().inverse_ratio().unwrap(); let expected = U256::from_str("2000000000000000000").unwrap(); assert_eq!(result, expected); diff --git a/flake.nix b/flake.nix index e61cdf86a..96f530b57 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/performance/vol.rs crates/subgraph/src/performance/apy.rs crates/subgraph/src/performance/order_performance.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 --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; @@ -96,7 +96,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/performance/vol.rs crates/subgraph/src/performance/apy.rs crates/subgraph/src/performance/order_performance.rs --lang=typescript --output-file=packages/ui-components/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 --lang=typescript --output-file=packages/ui-components/src/lib/typeshare/subgraphTypes.ts; typeshare crates/settings/src/parse.rs --lang=typescript --output-file=packages/ui-components/src/lib/typeshare/appSettings.ts; diff --git a/packages/orderbook/test/js_api/order.test.ts b/packages/orderbook/test/js_api/order.test.ts index c13cba1c6..b50ff9847 100644 --- a/packages/orderbook/test/js_api/order.test.ts +++ b/packages/orderbook/test/js_api/order.test.ts @@ -1,13 +1,14 @@ import assert from 'assert'; import { getLocal } from 'mockttp'; import { describe, it, beforeEach, afterEach } from 'vitest'; -import { Order, OrderWithSubgraphName, Trade } from '../../dist/types/js_api.js'; +import { Order, OrderPerformance, OrderWithSubgraphName, Trade } from '../../dist/types/js_api.js'; import { getOrders, getOrder, getOrderTradesList, getOrderTradeDetail, - getOrderTradesCount + getOrderTradesCount, + getOrderPerformance } from '../../dist/cjs/js_api.js'; const order1 = { @@ -143,6 +144,73 @@ const order2: Order = { trades: [] } as unknown as Order; +const order3 = { + id: 'order1', + orderBytes: + '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + orderHash: '0x1', + owner: '0x0000000000000000000000000000000000000000', + outputs: [ + { + id: '0x0000000000000000000000000000000000000000', + token: { + id: 'token-1', + address: '0x1111111111111111111111111111111111111111', + name: 'Token One', + symbol: 'TK1', + decimals: '18' + }, + balance: '0', + vaultId: '1', + owner: '0x0000000000000000000000000000000000000000', + ordersAsOutput: [], + ordersAsInput: [], + balanceChanges: [], + orderbook: { + id: '0x0000000000000000000000000000000000000000' + } + } + ], + inputs: [ + { + id: '0x0000000000000000000000000000000000000000', + token: { + id: 'token-2', + address: '0x2222222222222222222222222222222222222222', + name: 'Token Two', + symbol: 'TK2', + decimals: '18' + }, + balance: '0', + vaultId: '2', + owner: '0x0000000000000000000000000000000000000000', + ordersAsOutput: [], + ordersAsInput: [], + balanceChanges: [], + orderbook: { + id: '0x0000000000000000000000000000000000000000' + } + } + ], + active: true, + addEvents: [ + { + transaction: { + blockNumber: '0', + timestamp: '0', + id: '0x0000000000000000000000000000000000000000', + from: '0x0000000000000000000000000000000000000000' + } + } + ], + meta: null, + timestampAdded: '0', + orderbook: { + id: '0x0000000000000000000000000000000000000000' + }, + trades: [] +}; + const mockOrderTradesList: Trade[] = [ { id: '0x07db8b3f3e7498f9d4d0e40b98f57c020d3d277516e86023a8200a20464d4894', @@ -157,7 +225,7 @@ const mockOrderTradesList: Trade[] = [ } }, outputVaultBalanceChange: { - amount: '-100', + amount: '-100000000000000000000', vault: { id: 'vault-1', vaultId: '1', @@ -187,7 +255,7 @@ const mockOrderTradesList: Trade[] = [ orderHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' }, inputVaultBalanceChange: { - amount: '50', + amount: '50000000000000000000', vault: { id: 'vault-2', vaultId: '2', @@ -399,6 +467,7 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Order', async function ( ); } }); + it('should fetch trade count for a single order', async () => { await mockServer.forPost('/sg1').thenReply( 200, @@ -439,4 +508,114 @@ describe('Rain Orderbook JS API Package Bindgen Tests - Order', async function ( ); } }); + + it('should measure order performance given an order id and subgraph', async () => { + const mockServer = getLocal(); + mockServer.start(8088); + await mockServer + .forPost('/sg1') + .once() + .thenReply(200, JSON.stringify({ data: { order: order3 } })); + await mockServer + .forPost('/sg1') + .once() + .thenReply( + 200, + JSON.stringify({ + data: { + trades: mockOrderTradesList + } + }) + ); + await mockServer.forPost('/sg1').thenReply( + 200, + JSON.stringify({ + data: { + trades: [] + } + }) + ); + + const result = await getOrderPerformance( + mockServer.url + '/sg1', + '0x07db8b3f3e7498f9d4d0e40b98f57c020d3d277516e86023a8200a20464d4894', + BigInt(1632000000), + BigInt(1734571449) + ); + const expected: OrderPerformance = { + orderId: 'order1', + orderHash: '0x1', + orderbook: '0x0000000000000000000000000000000000000000', + denominatedPerformance: { + token: { + id: 'token-2', + address: '0x2222222222222222222222222222222222222222', + name: 'Token Two', + symbol: 'TK2', + decimals: '18' + }, + apy: '0x0', + apyIsNeg: false, + netVol: '0x0', + netVolIsNeg: false, + startingCapital: '0x258' + }, + startTime: 1632000000, + endTime: 1734571449, + inputsVaults: [ + { + id: '2', + token: { + id: 'token-2', + address: '0x2222222222222222222222222222222222222222', + name: 'Token Two', + symbol: 'TK2', + decimals: '18' + }, + volDetails: { + totalIn: '0x2b5e3af16b1880000', + totalOut: '0x0', + totalVol: '0x2b5e3af16b1880000', + netVol: '0x2b5e3af16b1880000' + }, + apyDetails: { + startTime: 1632000000, + endTime: 1734571449, + netVol: '0x2b5e3af16b1880000', + capital: '0x96', + apy: '0x13bce241d361f7aa7687c05aa7a4e5', + isNeg: false + } + } + ], + outputsVaults: [ + { + id: '1', + token: { + id: 'token-1', + address: '0x1111111111111111111111111111111111111111', + name: 'Token One', + symbol: 'TK1', + decimals: '18' + }, + volDetails: { + totalIn: '0x0', + totalOut: '0x56bc75e2d63100000', + totalVol: '0x56bc75e2d63100000', + netVol: '0x56bc75e2d63100000' + }, + apyDetails: { + startTime: 1632000000, + endTime: 1734571449, + netVol: '0x56bc75e2d63100000', + capital: '0x384', + apy: '0x6944b6b4675fd38d22d401e37e1a1', + isNeg: true + } + } + ] + }; + mockServer.stop(); + assert.deepEqual(result, expected); + }); }); diff --git a/packages/ui-components/src/__tests__/OrderAPY.test.ts b/packages/ui-components/src/__tests__/OrderAPY.test.ts index 60b478990..fda041d20 100644 --- a/packages/ui-components/src/__tests__/OrderAPY.test.ts +++ b/packages/ui-components/src/__tests__/OrderAPY.test.ts @@ -1,76 +1,44 @@ 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 { OrderPerformance } from '$lib/typeshare/subgraphTypes'; +import type { OrderPerformance } from '@rainlanguage/orderbook/js_api'; import { QueryClient } from '@tanstack/svelte-query'; import OrderApy from '../lib/components/tables/OrderAPY.svelte'; import { bigintStringToPercentage } from '../lib/utils/number'; -vi.mock('$lib/stores/settings', async (importOriginal) => { - const { writable } = await import('svelte/store'); - const { mockSettingsStore } = await import('@rainlanguage/ui-components'); - - 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: OrderPerformance[] = [ - { - orderId: '1', - orderHash: '1', - orderbook: '1', - denominatedPerformance: { - apy: '1200000000000000000', - apyIsNeg: true, - token: { - id: 'output_token', - address: 'output_token', - name: 'output_token', - symbol: 'output_token', - decimals: '0' - }, - netVol: '0', - netVolIsNeg: false, - startingCapital: '0' +vi.mock('@rainlanguage/orderbook/js_api', () => ({ + getOrderPerformance: vi.fn(() => Promise.resolve(mockOrderApy)) +})); + +const mockOrderApy: OrderPerformance = { + orderId: '1', + orderHash: '1', + orderbook: '1', + denominatedPerformance: { + apy: '1200000000000000000', + apyIsNeg: true, + token: { + id: 'output_token', + address: 'output_token', + name: 'output_token', + symbol: 'output_token', + decimals: '0' }, - startTime: 1, - endTime: 2, - inputsVaults: [], - outputsVaults: [] - } -]; - + netVol: '0', + netVolIsNeg: false, + startingCapital: '0' + }, + startTime: 1, + endTime: 2, + inputsVaults: [], + outputsVaults: [] +}; test('renders table with correct data', async () => { const queryClient = new QueryClient(); - mockIPC((cmd) => { - if (cmd === 'order_performance') { - return mockOrderApy[0]; - } - }); - render(OrderApy, { context: new Map([['$$_queryClient', queryClient]]), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - props: { id: '1' } as any + props: { id: '1', subgraphUrl: 'https://example.com' } }); await waitFor(async () => { @@ -78,11 +46,10 @@ test('renders table with correct data', async () => { const rows = screen.getAllByTestId('apy-field'); // checking - for (let i = 0; i < mockOrderApy.length; i++) { - const display = - (mockOrderApy[i].denominatedPerformance!.apyIsNeg ? '-' : '') + - bigintStringToPercentage(mockOrderApy[i].denominatedPerformance!.apy, 18, 5); - expect(rows[i]).toHaveTextContent(display); - } + const display = + (mockOrderApy.denominatedPerformance!.apyIsNeg ? '-' : '') + + bigintStringToPercentage(mockOrderApy.denominatedPerformance!.apy, 18, 5); + + expect(rows[0]).toHaveTextContent(display); }); }); diff --git a/packages/ui-components/src/__tests__/OrderVaultsVolTable.test.ts b/packages/ui-components/src/__tests__/OrderVaultsVolTable.test.ts index f9c99dd35..949ddfeb5 100644 --- a/packages/ui-components/src/__tests__/OrderVaultsVolTable.test.ts +++ b/packages/ui-components/src/__tests__/OrderVaultsVolTable.test.ts @@ -1,7 +1,7 @@ import { render, screen, waitFor } from '@testing-library/svelte'; import { test, vi } from 'vitest'; import { expect } from '$lib/test/matchers'; -import type { VaultVolume } from '$lib/typeshare/subgraphTypes'; +import type { VaultVolume } from '@rainlanguage/orderbook/js_api'; import { formatUnits } from 'viem'; import OrderVaultsVolTable from '../lib/components/tables/OrderVaultsVolTable.svelte'; import { QueryClient } from '@tanstack/svelte-query'; diff --git a/packages/ui-components/src/lib/components/charts/APYTimeFilters.svelte b/packages/ui-components/src/lib/components/charts/APYTimeFilters.svelte index 5ce19b803..daf54eaee 100644 --- a/packages/ui-components/src/lib/components/charts/APYTimeFilters.svelte +++ b/packages/ui-components/src/lib/components/charts/APYTimeFilters.svelte @@ -1,51 +1,51 @@ - { - 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">Last Year - { - setNow(); - timeDelta = TIME_DELTA_30_DAYS; - startTimestamp = now - TIME_DELTA_30_DAYS; - endTimestamp = now; - }} - active={timeDelta === TIME_DELTA_30_DAYS} - size="xs" - class="px-2 py-1">Last Month + { + 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">Last Year + { + setNow(); + timeDelta = TIME_DELTA_30_DAYS; + startTimestamp = now - TIME_DELTA_30_DAYS; + endTimestamp = now; + }} + active={timeDelta === TIME_DELTA_30_DAYS} + size="xs" + class="px-2 py-1">Last Month diff --git a/packages/ui-components/src/lib/components/tables/OrderAPY.svelte b/packages/ui-components/src/lib/components/tables/OrderAPY.svelte index ca3959b49..373645d7b 100644 --- a/packages/ui-components/src/lib/components/tables/OrderAPY.svelte +++ b/packages/ui-components/src/lib/components/tables/OrderAPY.svelte @@ -1,7 +1,8 @@ - + diff --git a/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte b/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte index ca3a5b88d..e94ec9817 100644 --- a/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte +++ b/packages/ui-components/src/lib/components/tables/OrderVaultsVolTable.svelte @@ -2,13 +2,12 @@ import { createInfiniteQuery } from '@tanstack/svelte-query'; import TanstackAppTable from '../TanstackAppTable.svelte'; import { QKEY_VAULTS_VOL_LIST } from '../../queries/keys'; - import { getOrderVaultsVolume } from '@rainlanguage/orderbook/js_api'; + import { getOrderVaultsVolume, type VaultVolume } from '@rainlanguage/orderbook/js_api'; import { TableBodyCell, TableHeadCell } from 'flowbite-svelte'; import Hash, { HashType } from '../Hash.svelte'; import { formatUnits } from 'viem'; import TableTimeFilters from '../charts/TableTimeFilters.svelte'; import { bigintStringToHex } from '../../utils/hex'; - import type { VaultVolume } from '$lib/typeshare/subgraphTypes'; export let id: string; export let subgraphUrl: string; diff --git a/tauri-app/src/lib/components/tables/OrderAPY.svelte b/tauri-app/src/lib/components/tables/OrderAPY.svelte deleted file mode 100644 index 9eab111d7..000000000 --- a/tauri-app/src/lib/components/tables/OrderAPY.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - APY - - - - - {item.denominatedPerformance - ? (item.denominatedPerformance.apyIsNeg ? '-' : '') + - bigintStringToPercentage(item.denominatedPerformance.apy, 18, 5) + - '% in ' + - (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 deleted file mode 100644 index c52ca9ba2..000000000 --- a/tauri-app/src/lib/components/tables/OrderAPY.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -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 { OrderPerformance } from '$lib/typeshare/subgraphTypes'; -import { QueryClient } from '@tanstack/svelte-query'; -import OrderApy from './OrderAPY.svelte'; -import { bigintStringToPercentage } from '$lib/utils/number'; - -vi.mock('$lib/stores/settings', async (importOriginal) => { - const { writable } = await import('svelte/store'); - const { mockSettingsStore } = await import('@rainlanguage/ui-components'); - - 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: OrderPerformance[] = [ - { - orderId: '1', - orderHash: '1', - orderbook: '1', - denominatedPerformance: { - apy: '1200000000000000000', - apyIsNeg: true, - token: { - id: 'output_token', - address: 'output_token', - name: 'output_token', - symbol: 'output_token', - decimals: '0', - }, - netVol: '0', - netVolIsNeg: false, - startingCapital: '0', - }, - startTime: 1, - endTime: 2, - inputsVaults: [], - outputsVaults: [], - }, -]; - -test('renders table with correct data', async () => { - const queryClient = new QueryClient(); - - mockIPC((cmd) => { - if (cmd === 'order_performance') { - return mockOrderApy[0]; - } - }); - - render(OrderApy, { - context: new Map([['$$_queryClient', queryClient]]), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - props: { id: '1' } as any, - }); - - await waitFor(async () => { - // get apy row - const rows = screen.getAllByTestId('apy-field'); - - // checking - for (let i = 0; i < mockOrderApy.length; i++) { - const display = - (mockOrderApy[i].denominatedPerformance!.apyIsNeg ? '-' : '') + - bigintStringToPercentage(mockOrderApy[i].denominatedPerformance!.apy, 18, 5); - expect(rows[i]).toHaveTextContent(display); - } - }); -}); From 481adf2094a4f4dde22284b223573bc3a76275d1 Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 19 Dec 2024 02:17:57 +0000 Subject: [PATCH 49/50] fix --- .../ui-components/src/lib/components/tables/OrderAPY.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-components/src/lib/components/tables/OrderAPY.svelte b/packages/ui-components/src/lib/components/tables/OrderAPY.svelte index 373645d7b..0e7ad287f 100644 --- a/packages/ui-components/src/lib/components/tables/OrderAPY.svelte +++ b/packages/ui-components/src/lib/components/tables/OrderAPY.svelte @@ -21,8 +21,8 @@ queryFn: async () => { return [ (await getOrderPerformance( - id, subgraphUrl || '', + id, queryStartTime, queryEndTime )) as OrderPerformance From 9fe1d05f5a4710e5adb367004855b54bab871c3f Mon Sep 17 00:00:00 2001 From: rouzwelt Date: Thu, 19 Dec 2024 13:39:28 +0000 Subject: [PATCH 50/50] fix div by zero of trade ratios --- .../src/performance/order_performance.rs | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/crates/subgraph/src/performance/order_performance.rs b/crates/subgraph/src/performance/order_performance.rs index feff118f2..d47eef4c3 100644 --- a/crates/subgraph/src/performance/order_performance.rs +++ b/crates/subgraph/src/performance/order_performance.rs @@ -328,35 +328,19 @@ pub fn get_order_pairs_ratio(order: &Order, trades: &[Trade]) -> HashMap Some(v), - Err(e) => { - if let PerformanceError::DivByZero = e { - Some(U256::MAX) + latest_trade + .ratio() + .ok() + .zip(latest_trade.inverse_ratio().ok()) + .map(|(ratio, inverse_ratio)| { + if latest_trade.input_vault_balance_change.vault.token + == input.token + { + [ratio, inverse_ratio] } else { - None + [inverse_ratio, ratio] } - } - }; - let inverse_ratio = match latest_trade.inverse_ratio() { - Ok(v) => Some(v), - Err(e) => { - if let PerformanceError::DivByZero = e { - Some(U256::MAX) - } else { - None - } - } - }; - ratio.zip(inverse_ratio).map(|(ratio, inverse_ratio)| { - if latest_trade.input_vault_balance_change.vault.token == input.token { - [ratio, inverse_ratio] - } else { - [inverse_ratio, ratio] - } - }) + }) }); // io