1use alloy::primitives::{Address, U256};
4use alloy::rpc::types::TransactionReceipt;
5use graphql_client::{GraphQLQuery, Response};
6use morpho_rs_contracts::{Erc4626Client, VaultV1TransactionClient, VaultV2TransactionClient};
7use reqwest::Client;
8use url::Url;
9
10use crate::error::{ApiError, Result};
11use crate::filters::{VaultFiltersV1, VaultFiltersV2, VaultQueryOptionsV1, VaultQueryOptionsV2};
12use crate::types::ordering::{OrderDirection, VaultOrderByV1, VaultOrderByV2};
13use crate::queries::v1::{
14 get_vault_v1_by_address, get_vaults_v1, GetVaultV1ByAddress, GetVaultsV1,
15};
16use crate::queries::user::{
17 get_user_account_overview, get_user_vault_positions, GetUserAccountOverview,
18 GetUserVaultPositions,
19};
20use crate::queries::v2::{
21 get_vault_v2_by_address, get_vaults_v2, GetVaultV2ByAddress, GetVaultsV2,
22};
23use crate::types::vault_v1::MarketStateV1;
24use crate::types::vault_v2::{MarketStateV2, MetaMorphoAllocation, MorphoMarketPosition, VaultAdapterData};
25use crate::types::{
26 Asset, MarketInfo, NamedChain, UserAccountOverview, UserMarketPosition,
27 UserState, UserVaultPositions, UserVaultV1Position, UserVaultV2Position, VaultAdapter,
28 VaultAllocation, VaultAllocator, VaultInfo, VaultPositionState,
29 VaultReward, VaultStateV1, VaultV1, VaultV2, VaultV2Warning,
30 VaultWarning, SUPPORTED_CHAINS,
31};
32use crate::types::vault::Vault as VaultTrait;
33
34pub const DEFAULT_API_URL: &str = "https://api.morpho.org/graphql";
36
37pub const DEFAULT_PAGE_SIZE: i64 = 100;
39
40#[derive(Debug, Clone)]
42pub struct ClientConfig {
43 pub api_url: Url,
45 pub page_size: i64,
47}
48
49impl Default for ClientConfig {
50 fn default() -> Self {
51 Self {
52 api_url: Url::parse(DEFAULT_API_URL).expect("Invalid default API URL"),
53 page_size: DEFAULT_PAGE_SIZE,
54 }
55 }
56}
57
58impl ClientConfig {
59 pub fn new() -> Self {
61 Self::default()
62 }
63
64 pub fn with_api_url(mut self, url: Url) -> Self {
66 self.api_url = url;
67 self
68 }
69
70 pub fn with_page_size(mut self, size: i64) -> Self {
72 self.page_size = size;
73 self
74 }
75}
76
77#[derive(Debug, Clone)]
79pub struct VaultV1Client {
80 http_client: Client,
81 config: ClientConfig,
82}
83
84impl Default for VaultV1Client {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl VaultV1Client {
91 pub fn new() -> Self {
93 Self {
94 http_client: Client::new(),
95 config: ClientConfig::default(),
96 }
97 }
98
99 pub fn with_config(config: ClientConfig) -> Self {
101 Self {
102 http_client: Client::new(),
103 config,
104 }
105 }
106
107 async fn execute<Q: GraphQLQuery>(
109 &self,
110 variables: Q::Variables,
111 ) -> Result<Q::ResponseData> {
112 let request_body = Q::build_query(variables);
113 let response = self
114 .http_client
115 .post(self.config.api_url.as_str())
116 .json(&request_body)
117 .send()
118 .await?;
119
120 let response_body: Response<Q::ResponseData> = response.json().await?;
121
122 if let Some(errors) = response_body.errors {
123 if !errors.is_empty() {
124 return Err(ApiError::GraphQL(
125 errors
126 .iter()
127 .map(|e| e.message.clone())
128 .collect::<Vec<_>>()
129 .join("; "),
130 ));
131 }
132 }
133
134 response_body
135 .data
136 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
137 }
138
139 pub async fn get_vaults(&self, filters: Option<VaultFiltersV1>) -> Result<Vec<VaultV1>> {
141 let variables = get_vaults_v1::Variables {
142 first: Some(self.config.page_size),
143 skip: Some(0),
144 where_: filters.map(|f| f.to_gql()),
145 order_by: Some(VaultOrderByV1::default().to_gql()),
146 order_direction: Some(OrderDirection::default().to_gql_v1()),
147 };
148
149 let data = self.execute::<GetVaultsV1>(variables).await?;
150
151 let items = match data.vaults.items {
152 Some(items) => items,
153 None => return Ok(Vec::new()),
154 };
155
156 let vaults: Vec<VaultV1> = items
157 .into_iter()
158 .filter_map(convert_v1_vault)
159 .collect();
160
161 Ok(vaults)
162 }
163
164 pub async fn get_vault(&self, address: &str, chain: NamedChain) -> Result<VaultV1> {
166 let variables = get_vault_v1_by_address::Variables {
167 address: address.to_string(),
168 chain_id: u64::from(chain) as i64,
169 };
170
171 let data = self.execute::<GetVaultV1ByAddress>(variables).await?;
172
173 convert_v1_vault_single(data.vault_by_address).ok_or_else(|| ApiError::VaultNotFound {
174 address: address.to_string(),
175 chain_id: u64::from(chain) as i64,
176 })
177 }
178
179 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<VaultV1>> {
181 let filters = VaultFiltersV1::new().chain(chain);
182 self.get_vaults(Some(filters)).await
183 }
184
185 pub async fn get_vaults_by_curator(
187 &self,
188 curator: &str,
189 chain: Option<NamedChain>,
190 ) -> Result<Vec<VaultV1>> {
191 let mut filters = VaultFiltersV1::new().curators([curator]);
192 if let Some(c) = chain {
193 filters = filters.chain(c);
194 }
195 self.get_vaults(Some(filters)).await
196 }
197
198 pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<VaultV1>> {
200 let mut filters = VaultFiltersV1::new().listed(true);
201 if let Some(c) = chain {
202 filters = filters.chain(c);
203 }
204 self.get_vaults(Some(filters)).await
205 }
206
207 pub async fn get_vaults_with_options(
235 &self,
236 options: VaultQueryOptionsV1,
237 ) -> Result<Vec<VaultV1>> {
238 let variables = get_vaults_v1::Variables {
239 first: options.limit.or(Some(self.config.page_size)),
240 skip: Some(0),
241 where_: options.filters.map(|f| f.to_gql()),
242 order_by: Some(options.order_by.unwrap_or_default().to_gql()),
243 order_direction: Some(options.order_direction.unwrap_or_default().to_gql_v1()),
244 };
245
246 let data = self.execute::<GetVaultsV1>(variables).await?;
247
248 let items = match data.vaults.items {
249 Some(items) => items,
250 None => return Ok(Vec::new()),
251 };
252
253 let vaults: Vec<VaultV1> = items
254 .into_iter()
255 .filter_map(convert_v1_vault)
256 .collect();
257
258 Ok(vaults)
259 }
260
261 pub async fn get_top_vaults_by_apy(
279 &self,
280 limit: i64,
281 filters: Option<VaultFiltersV1>,
282 ) -> Result<Vec<VaultV1>> {
283 let options = VaultQueryOptionsV1 {
284 filters,
285 order_by: Some(VaultOrderByV1::NetApy),
286 order_direction: Some(OrderDirection::Desc),
287 limit: Some(limit),
288 };
289 self.get_vaults_with_options(options).await
290 }
291
292 pub async fn get_vaults_by_asset(
309 &self,
310 asset_symbol: &str,
311 chain: Option<NamedChain>,
312 ) -> Result<Vec<VaultV1>> {
313 let mut filters = VaultFiltersV1::new().asset_symbols([asset_symbol]);
314 if let Some(c) = chain {
315 filters = filters.chain(c);
316 }
317 self.get_vaults(Some(filters)).await
318 }
319}
320
321#[derive(Debug, Clone)]
323pub struct VaultV2Client {
324 http_client: Client,
325 config: ClientConfig,
326}
327
328impl Default for VaultV2Client {
329 fn default() -> Self {
330 Self::new()
331 }
332}
333
334impl VaultV2Client {
335 pub fn new() -> Self {
337 Self {
338 http_client: Client::new(),
339 config: ClientConfig::default(),
340 }
341 }
342
343 pub fn with_config(config: ClientConfig) -> Self {
345 Self {
346 http_client: Client::new(),
347 config,
348 }
349 }
350
351 async fn execute<Q: GraphQLQuery>(
353 &self,
354 variables: Q::Variables,
355 ) -> Result<Q::ResponseData> {
356 let request_body = Q::build_query(variables);
357 let response = self
358 .http_client
359 .post(self.config.api_url.as_str())
360 .json(&request_body)
361 .send()
362 .await?;
363
364 let response_body: Response<Q::ResponseData> = response.json().await?;
365
366 if let Some(errors) = response_body.errors {
367 if !errors.is_empty() {
368 return Err(ApiError::GraphQL(
369 errors
370 .iter()
371 .map(|e| e.message.clone())
372 .collect::<Vec<_>>()
373 .join("; "),
374 ));
375 }
376 }
377
378 response_body
379 .data
380 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
381 }
382
383 pub async fn get_vaults(&self, filters: Option<VaultFiltersV2>) -> Result<Vec<VaultV2>> {
385 let variables = get_vaults_v2::Variables {
386 first: Some(self.config.page_size),
387 skip: Some(0),
388 where_: filters.map(|f| f.to_gql()),
389 order_by: Some(VaultOrderByV2::default().to_gql()),
390 order_direction: Some(OrderDirection::default().to_gql_v2()),
391 };
392
393 let data = self.execute::<GetVaultsV2>(variables).await?;
394
395 let items = match data.vault_v2s.items {
396 Some(items) => items,
397 None => return Ok(Vec::new()),
398 };
399
400 let vaults: Vec<VaultV2> = items
401 .into_iter()
402 .filter_map(convert_v2_vault)
403 .collect();
404
405 Ok(vaults)
406 }
407
408 pub async fn get_vault(&self, address: &str, chain: NamedChain) -> Result<VaultV2> {
410 let variables = get_vault_v2_by_address::Variables {
411 address: address.to_string(),
412 chain_id: u64::from(chain) as i64,
413 };
414
415 let data = self.execute::<GetVaultV2ByAddress>(variables).await?;
416
417 convert_v2_vault_single(data.vault_v2_by_address).ok_or_else(|| ApiError::VaultNotFound {
418 address: address.to_string(),
419 chain_id: u64::from(chain) as i64,
420 })
421 }
422
423 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<VaultV2>> {
425 let filters = VaultFiltersV2::new().chain(chain);
426 self.get_vaults(Some(filters)).await
427 }
428
429 pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<VaultV2>> {
431 let mut filters = VaultFiltersV2::new().listed(true);
432 if let Some(c) = chain {
433 filters = filters.chain(c);
434 }
435 self.get_vaults(Some(filters)).await
436 }
437
438 pub async fn get_vaults_with_options(
469 &self,
470 options: VaultQueryOptionsV2,
471 ) -> Result<Vec<VaultV2>> {
472 let fetch_limit = if options.has_asset_filter() {
475 options.limit.map(|l| l * 3).or(Some(self.config.page_size))
477 } else {
478 options.limit.or(Some(self.config.page_size))
479 };
480
481 let variables = get_vaults_v2::Variables {
482 first: fetch_limit,
483 skip: Some(0),
484 where_: options.filters.map(|f| f.to_gql()),
485 order_by: Some(options.order_by.unwrap_or_default().to_gql()),
486 order_direction: Some(options.order_direction.unwrap_or_default().to_gql_v2()),
487 };
488
489 let data = self.execute::<GetVaultsV2>(variables).await?;
490
491 let items = match data.vault_v2s.items {
492 Some(items) => items,
493 None => return Ok(Vec::new()),
494 };
495
496 let mut vaults: Vec<VaultV2> = items
497 .into_iter()
498 .filter_map(convert_v2_vault)
499 .collect();
500
501 if let Some(ref symbols) = options.asset_symbols {
503 vaults.retain(|v| symbols.iter().any(|s| s.eq_ignore_ascii_case(&v.asset.symbol)));
504 }
505 if let Some(ref addresses) = options.asset_addresses {
506 vaults.retain(|v| {
507 addresses
508 .iter()
509 .any(|a| v.asset.address.to_string().eq_ignore_ascii_case(a))
510 });
511 }
512
513 if let Some(limit) = options.limit {
515 vaults.truncate(limit as usize);
516 }
517
518 Ok(vaults)
519 }
520
521 pub async fn get_top_vaults_by_apy(
539 &self,
540 limit: i64,
541 filters: Option<VaultFiltersV2>,
542 ) -> Result<Vec<VaultV2>> {
543 let options = VaultQueryOptionsV2 {
544 filters,
545 order_by: Some(VaultOrderByV2::NetApy),
546 order_direction: Some(OrderDirection::Desc),
547 limit: Some(limit),
548 asset_addresses: None,
549 asset_symbols: None,
550 };
551 self.get_vaults_with_options(options).await
552 }
553
554 pub async fn get_vaults_by_asset(
574 &self,
575 asset_symbol: &str,
576 chain: Option<NamedChain>,
577 ) -> Result<Vec<VaultV2>> {
578 let filters = chain.map(|c| VaultFiltersV2::new().chain(c));
579 let options = VaultQueryOptionsV2 {
580 filters,
581 order_by: None,
582 order_direction: None,
583 limit: None,
584 asset_addresses: None,
585 asset_symbols: Some(vec![asset_symbol.to_string()]),
586 };
587 self.get_vaults_with_options(options).await
588 }
589}
590
591#[derive(Debug, Clone)]
593pub struct MorphoApiClient {
594 pub v1: VaultV1Client,
596 pub v2: VaultV2Client,
598}
599
600impl Default for MorphoApiClient {
601 fn default() -> Self {
602 Self::new()
603 }
604}
605
606impl MorphoApiClient {
607 pub fn new() -> Self {
609 Self {
610 v1: VaultV1Client::new(),
611 v2: VaultV2Client::new(),
612 }
613 }
614
615 pub fn with_config(config: ClientConfig) -> Self {
617 Self {
618 v1: VaultV1Client::with_config(config.clone()),
619 v2: VaultV2Client::with_config(config),
620 }
621 }
622
623 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<Box<dyn VaultTrait>>> {
625 let (v1_vaults, v2_vaults) = tokio::try_join!(
626 self.v1.get_vaults_by_chain(chain),
627 self.v2.get_vaults_by_chain(chain),
628 )?;
629
630 let mut vaults: Vec<Box<dyn VaultTrait>> =
631 Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
632 vaults.extend(
633 v1_vaults
634 .into_iter()
635 .map(|v| Box::new(v) as Box<dyn VaultTrait>),
636 );
637 vaults.extend(
638 v2_vaults
639 .into_iter()
640 .map(|v| Box::new(v) as Box<dyn VaultTrait>),
641 );
642
643 Ok(vaults)
644 }
645
646 pub async fn get_whitelisted_vaults(
648 &self,
649 chain: Option<NamedChain>,
650 ) -> Result<Vec<Box<dyn VaultTrait>>> {
651 let (v1_vaults, v2_vaults) = tokio::try_join!(
652 self.v1.get_whitelisted_vaults(chain),
653 self.v2.get_whitelisted_vaults(chain),
654 )?;
655
656 let mut vaults: Vec<Box<dyn VaultTrait>> =
657 Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
658 vaults.extend(
659 v1_vaults
660 .into_iter()
661 .map(|v| Box::new(v) as Box<dyn VaultTrait>),
662 );
663 vaults.extend(
664 v2_vaults
665 .into_iter()
666 .map(|v| Box::new(v) as Box<dyn VaultTrait>),
667 );
668
669 Ok(vaults)
670 }
671
672 async fn execute<Q: GraphQLQuery>(&self, variables: Q::Variables) -> Result<Q::ResponseData> {
674 let request_body = Q::build_query(variables);
675 let response = self
676 .v1
677 .http_client
678 .post(self.v1.config.api_url.as_str())
679 .json(&request_body)
680 .send()
681 .await?;
682
683 let response_body: Response<Q::ResponseData> = response.json().await?;
684
685 if let Some(errors) = response_body.errors {
686 if !errors.is_empty() {
687 return Err(ApiError::GraphQL(
688 errors
689 .iter()
690 .map(|e| e.message.clone())
691 .collect::<Vec<_>>()
692 .join("; "),
693 ));
694 }
695 }
696
697 response_body
698 .data
699 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
700 }
701
702 pub async fn get_user_vault_positions(
707 &self,
708 address: &str,
709 chain: Option<NamedChain>,
710 ) -> Result<UserVaultPositions> {
711 match chain {
712 Some(c) => self.get_user_vault_positions_single_chain(address, c).await,
713 None => self.get_user_vault_positions_all_chains(address).await,
714 }
715 }
716
717 async fn get_user_vault_positions_single_chain(
719 &self,
720 address: &str,
721 chain: NamedChain,
722 ) -> Result<UserVaultPositions> {
723 let variables = get_user_vault_positions::Variables {
724 address: address.to_string(),
725 chain_id: u64::from(chain) as i64,
726 };
727
728 let data = self.execute::<GetUserVaultPositions>(variables).await?;
729 let user = data.user_by_address;
730
731 let vault_positions: Vec<UserVaultV1Position> = user
732 .vault_positions
733 .into_iter()
734 .filter_map(convert_user_vault_v1_position)
735 .collect();
736
737 let vault_v2_positions: Vec<UserVaultV2Position> = user
738 .vault_v2_positions
739 .into_iter()
740 .filter_map(convert_user_vault_v2_position)
741 .collect();
742
743 Ok(UserVaultPositions {
744 address: user
745 .address
746 .parse()
747 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
748 vault_positions,
749 vault_v2_positions,
750 })
751 }
752
753 async fn get_user_vault_positions_all_chains(
755 &self,
756 address: &str,
757 ) -> Result<UserVaultPositions> {
758 use futures::future::join_all;
759
760 let valid_chains: Vec<_> = SUPPORTED_CHAINS
762 .iter()
763 .filter(|chain| u64::from(**chain) <= i32::MAX as u64)
764 .copied()
765 .collect();
766
767 let futures: Vec<_> = valid_chains
768 .iter()
769 .map(|chain| self.get_user_vault_positions_single_chain(address, *chain))
770 .collect();
771
772 let results = join_all(futures).await;
773
774 let parsed_address = address
775 .parse()
776 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?;
777
778 let mut all_v1_positions = Vec::new();
779 let mut all_v2_positions = Vec::new();
780
781 for result in results {
782 match result {
783 Ok(positions) => {
784 all_v1_positions.extend(positions.vault_positions);
785 all_v2_positions.extend(positions.vault_v2_positions);
786 }
787 Err(ApiError::GraphQL(msg)) if msg.contains("No results") => continue,
789 Err(e) => return Err(e),
790 }
791 }
792
793 Ok(UserVaultPositions {
794 address: parsed_address,
795 vault_positions: all_v1_positions,
796 vault_v2_positions: all_v2_positions,
797 })
798 }
799
800 pub async fn get_user_account_overview(
802 &self,
803 address: &str,
804 chain: NamedChain,
805 ) -> Result<UserAccountOverview> {
806 let variables = get_user_account_overview::Variables {
807 address: address.to_string(),
808 chain_id: u64::from(chain) as i64,
809 };
810
811 let data = self.execute::<GetUserAccountOverview>(variables).await?;
812 let user = data.user_by_address;
813
814 let state = UserState::from_gql(
815 user.state.vaults_pnl_usd,
816 user.state.vaults_roe_usd,
817 user.state.vaults_assets_usd,
818 user.state.vault_v2s_pnl_usd,
819 user.state.vault_v2s_roe_usd,
820 user.state.vault_v2s_assets_usd,
821 user.state.markets_pnl_usd,
822 user.state.markets_roe_usd,
823 user.state.markets_supply_pnl_usd,
824 user.state.markets_supply_roe_usd,
825 user.state.markets_borrow_pnl_usd,
826 user.state.markets_borrow_roe_usd,
827 user.state.markets_collateral_pnl_usd,
828 user.state.markets_collateral_roe_usd,
829 user.state.markets_margin_pnl_usd,
830 user.state.markets_margin_roe_usd,
831 user.state.markets_collateral_usd,
832 user.state.markets_supply_assets_usd,
833 user.state.markets_borrow_assets_usd,
834 user.state.markets_margin_usd,
835 );
836
837 let vault_positions: Vec<UserVaultV1Position> = user
838 .vault_positions
839 .into_iter()
840 .filter_map(convert_user_vault_v1_position_overview)
841 .collect();
842
843 let vault_v2_positions: Vec<UserVaultV2Position> = user
844 .vault_v2_positions
845 .into_iter()
846 .filter_map(convert_user_vault_v2_position_overview)
847 .collect();
848
849 let market_positions: Vec<UserMarketPosition> = user
850 .market_positions
851 .into_iter()
852 .filter_map(convert_user_market_position)
853 .collect();
854
855 Ok(UserAccountOverview {
856 address: user
857 .address
858 .parse()
859 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
860 state,
861 vault_positions,
862 vault_v2_positions,
863 market_positions,
864 })
865 }
866}
867
868#[derive(Debug, Clone)]
870pub struct MorphoClientConfig {
871 pub api_config: Option<ClientConfig>,
873 pub rpc_url: Option<String>,
875 pub private_key: Option<String>,
877 pub auto_approve: bool,
881}
882
883impl Default for MorphoClientConfig {
884 fn default() -> Self {
885 Self {
886 api_config: None,
887 rpc_url: None,
888 private_key: None,
889 auto_approve: true,
890 }
891 }
892}
893
894impl MorphoClientConfig {
895 pub fn new() -> Self {
897 Self::default()
898 }
899
900 pub fn with_api_config(mut self, config: ClientConfig) -> Self {
902 self.api_config = Some(config);
903 self
904 }
905
906 pub fn with_rpc_url(mut self, rpc_url: impl Into<String>) -> Self {
908 self.rpc_url = Some(rpc_url.into());
909 self
910 }
911
912 pub fn with_private_key(mut self, private_key: impl Into<String>) -> Self {
914 self.private_key = Some(private_key.into());
915 self
916 }
917
918 pub fn with_auto_approve(mut self, auto_approve: bool) -> Self {
922 self.auto_approve = auto_approve;
923 self
924 }
925}
926
927pub struct VaultV1Operations<'a> {
929 client: &'a VaultV1TransactionClient,
930 auto_approve: bool,
931}
932
933impl<'a> VaultV1Operations<'a> {
934 fn new(client: &'a VaultV1TransactionClient, auto_approve: bool) -> Self {
936 Self { client, auto_approve }
937 }
938
939 pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
944 if self.auto_approve {
945 let asset = self.client.get_asset(vault).await?;
946 if let Some(approval) = self.client.approve_if_needed(asset, vault, amount).await? {
947 approval.send().await?;
948 }
949 }
950
951 let receipt = self
952 .client
953 .deposit(vault, amount, self.client.signer_address())
954 .send()
955 .await?;
956 Ok(receipt)
957 }
958
959 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
961 let signer = self.client.signer_address();
962 let receipt = self.client.withdraw(vault, amount, signer, signer).send().await?;
963 Ok(receipt)
964 }
965
966 pub async fn balance(&self, vault: Address) -> Result<U256> {
968 let balance = self
969 .client
970 .get_balance(vault, self.client.signer_address())
971 .await?;
972 Ok(balance)
973 }
974
975 pub async fn approve(
978 &self,
979 vault: Address,
980 amount: U256,
981 ) -> Result<Option<TransactionReceipt>> {
982 let asset = self.client.get_asset(vault).await?;
983 if let Some(approval) = self.client.approve_if_needed(asset, vault, amount).await? {
984 let receipt = approval.send().await?;
985 Ok(Some(receipt))
986 } else {
987 Ok(None)
988 }
989 }
990
991 pub async fn get_allowance(&self, vault: Address) -> Result<U256> {
993 let asset = self.client.get_asset(vault).await?;
994 let allowance = self
995 .client
996 .get_allowance(asset, self.client.signer_address(), vault)
997 .await?;
998 Ok(allowance)
999 }
1000
1001 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
1003 let asset = self.client.get_asset(vault).await?;
1004 Ok(asset)
1005 }
1006
1007 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
1009 let decimals = self.client.get_decimals(token).await?;
1010 Ok(decimals)
1011 }
1012
1013 pub fn signer_address(&self) -> Address {
1015 self.client.signer_address()
1016 }
1017
1018 pub fn auto_approve(&self) -> bool {
1020 self.auto_approve
1021 }
1022}
1023
1024pub struct VaultV2Operations<'a> {
1026 client: &'a VaultV2TransactionClient,
1027 auto_approve: bool,
1028}
1029
1030impl<'a> VaultV2Operations<'a> {
1031 fn new(client: &'a VaultV2TransactionClient, auto_approve: bool) -> Self {
1033 Self { client, auto_approve }
1034 }
1035
1036 pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1041 if self.auto_approve {
1042 let asset = self.client.get_asset(vault).await?;
1043 if let Some(approval) = self.client.approve_if_needed(asset, vault, amount).await? {
1044 approval.send().await?;
1045 }
1046 }
1047
1048 let receipt = self
1049 .client
1050 .deposit(vault, amount, self.client.signer_address())
1051 .send()
1052 .await?;
1053 Ok(receipt)
1054 }
1055
1056 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1058 let signer = self.client.signer_address();
1059 let receipt = self.client.withdraw(vault, amount, signer, signer).send().await?;
1060 Ok(receipt)
1061 }
1062
1063 pub async fn balance(&self, vault: Address) -> Result<U256> {
1065 let balance = self
1066 .client
1067 .get_balance(vault, self.client.signer_address())
1068 .await?;
1069 Ok(balance)
1070 }
1071
1072 pub async fn approve(
1075 &self,
1076 vault: Address,
1077 amount: U256,
1078 ) -> Result<Option<TransactionReceipt>> {
1079 let asset = self.client.get_asset(vault).await?;
1080 if let Some(approval) = self.client.approve_if_needed(asset, vault, amount).await? {
1081 let receipt = approval.send().await?;
1082 Ok(Some(receipt))
1083 } else {
1084 Ok(None)
1085 }
1086 }
1087
1088 pub async fn get_allowance(&self, vault: Address) -> Result<U256> {
1090 let asset = self.client.get_asset(vault).await?;
1091 let allowance = self
1092 .client
1093 .get_allowance(asset, self.client.signer_address(), vault)
1094 .await?;
1095 Ok(allowance)
1096 }
1097
1098 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
1100 let asset = self.client.get_asset(vault).await?;
1101 Ok(asset)
1102 }
1103
1104 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
1106 let decimals = self.client.get_decimals(token).await?;
1107 Ok(decimals)
1108 }
1109
1110 pub fn signer_address(&self) -> Address {
1112 self.client.signer_address()
1113 }
1114
1115 pub fn auto_approve(&self) -> bool {
1117 self.auto_approve
1118 }
1119}
1120
1121pub struct MorphoClient {
1154 api: MorphoApiClient,
1155 vault_v1_tx: Option<VaultV1TransactionClient>,
1156 vault_v2_tx: Option<VaultV2TransactionClient>,
1157 auto_approve: bool,
1158}
1159
1160impl Default for MorphoClient {
1161 fn default() -> Self {
1162 Self::new()
1163 }
1164}
1165
1166impl MorphoClient {
1167 pub fn new() -> Self {
1169 Self {
1170 api: MorphoApiClient::new(),
1171 vault_v1_tx: None,
1172 vault_v2_tx: None,
1173 auto_approve: true,
1174 }
1175 }
1176
1177 pub fn with_config(config: MorphoClientConfig) -> Result<Self> {
1181 let api = match config.api_config {
1182 Some(api_config) => MorphoApiClient::with_config(api_config),
1183 None => MorphoApiClient::new(),
1184 };
1185
1186 let (vault_v1_tx, vault_v2_tx) = match (&config.rpc_url, &config.private_key) {
1187 (Some(rpc_url), Some(private_key)) => {
1188 let v1 = VaultV1TransactionClient::new(rpc_url, private_key)?;
1189 let v2 = VaultV2TransactionClient::new(rpc_url, private_key)?;
1190 (Some(v1), Some(v2))
1191 }
1192 _ => (None, None),
1193 };
1194
1195 Ok(Self {
1196 api,
1197 vault_v1_tx,
1198 vault_v2_tx,
1199 auto_approve: config.auto_approve,
1200 })
1201 }
1202
1203 pub fn vault_v1(&self) -> Result<VaultV1Operations<'_>> {
1207 match &self.vault_v1_tx {
1208 Some(client) => Ok(VaultV1Operations::new(client, self.auto_approve)),
1209 None => Err(ApiError::TransactionNotConfigured),
1210 }
1211 }
1212
1213 pub fn vault_v2(&self) -> Result<VaultV2Operations<'_>> {
1217 match &self.vault_v2_tx {
1218 Some(client) => Ok(VaultV2Operations::new(client, self.auto_approve)),
1219 None => Err(ApiError::TransactionNotConfigured),
1220 }
1221 }
1222
1223 pub fn auto_approve(&self) -> bool {
1225 self.auto_approve
1226 }
1227
1228 pub fn api(&self) -> &MorphoApiClient {
1230 &self.api
1231 }
1232
1233 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<Box<dyn VaultTrait>>> {
1235 self.api.get_vaults_by_chain(chain).await
1236 }
1237
1238 pub async fn get_whitelisted_vaults(
1240 &self,
1241 chain: Option<NamedChain>,
1242 ) -> Result<Vec<Box<dyn VaultTrait>>> {
1243 self.api.get_whitelisted_vaults(chain).await
1244 }
1245
1246 pub async fn get_user_vault_positions(
1248 &self,
1249 address: &str,
1250 chain: Option<NamedChain>,
1251 ) -> Result<UserVaultPositions> {
1252 self.api.get_user_vault_positions(address, chain).await
1253 }
1254
1255 pub async fn get_user_account_overview(
1257 &self,
1258 address: &str,
1259 chain: NamedChain,
1260 ) -> Result<UserAccountOverview> {
1261 self.api.get_user_account_overview(address, chain).await
1262 }
1263
1264 pub fn has_transaction_support(&self) -> bool {
1266 self.vault_v1_tx.is_some()
1267 }
1268
1269 pub fn signer_address(&self) -> Option<Address> {
1271 self.vault_v1_tx.as_ref().map(|c| c.signer_address())
1272 }
1273}
1274
1275use crate::types::scalars::parse_bigint;
1279use alloy_primitives::B256;
1280use std::str::FromStr;
1281
1282fn fee_to_wad(fee: f64) -> U256 {
1284 let fee_wad = (fee * 1e18) as u128;
1285 U256::from(fee_wad)
1286}
1287
1288macro_rules! impl_v1_vault_conversion {
1291 ($fn_name:ident, $market_state_fn:ident, $state_fn:ident, $mod:ident) => {
1292 fn $market_state_fn(
1293 market: &$mod::MarketFields,
1294 ) -> Option<MarketStateV1> {
1295 let market_id = B256::from_str(&market.unique_key).ok()?;
1296 let ms = market.state.as_ref()?;
1297 let lltv = parse_bigint(&market.lltv)?;
1298 let timestamp: u64 = ms.timestamp.0.parse().ok()?;
1299 let total_supply_assets = parse_bigint(&ms.supply_assets)?;
1300 let total_borrow_assets = parse_bigint(&ms.borrow_assets)?;
1301 let liquidity = total_supply_assets.saturating_sub(total_borrow_assets);
1302
1303 Some(MarketStateV1 {
1304 id: market_id,
1305 total_supply_assets,
1306 total_borrow_assets,
1307 total_supply_shares: parse_bigint(&ms.supply_shares)?,
1308 total_borrow_shares: parse_bigint(&ms.borrow_shares)?,
1309 last_update: timestamp,
1310 fee: fee_to_wad(ms.fee),
1311 rate_at_target: ms.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1312 price: ms.price.as_ref().and_then(|p| parse_bigint(p)),
1313 lltv,
1314 liquidity,
1315 })
1316 }
1317
1318 fn $state_fn(s: &$mod::VaultStateFields) -> Option<VaultStateV1> {
1319 VaultStateV1::from_gql(
1320 Some(s.curator.as_str()),
1321 Some(s.owner.as_str()),
1322 Some(s.guardian.as_str()),
1323 &s.total_assets,
1324 s.total_assets_usd,
1325 &s.total_supply,
1326 s.fee,
1327 &s.timelock,
1328 s.apy,
1329 s.net_apy,
1330 s.share_price.as_deref().unwrap_or("0"),
1331 s.allocation
1332 .iter()
1333 .filter_map(|a| {
1334 let market = &a.market;
1335 let market_state = $market_state_fn(market);
1336 VaultAllocation::from_gql(
1337 market.unique_key.clone(),
1338 Some(market.loan_asset.symbol.clone()),
1339 Some(market.loan_asset.address.as_str()),
1340 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
1341 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
1342 &a.supply_assets,
1343 a.supply_assets_usd,
1344 &a.supply_cap,
1345 a.enabled,
1346 a.supply_queue_index.map(|i| i as i32),
1347 a.withdraw_queue_index.map(|i| i as i32),
1348 market_state,
1349 )
1350 })
1351 .collect(),
1352 )
1353 }
1354
1355 fn $fn_name(v: $mod::VaultFields) -> Option<VaultV1> {
1356 let chain_id = v.chain.id;
1357 let asset = &v.asset;
1358
1359 VaultV1::from_gql(
1360 &v.address,
1361 v.name,
1362 v.symbol,
1363 chain_id,
1364 v.listed,
1365 v.featured,
1366 v.whitelisted,
1367 Asset::from_gql(
1368 &asset.address,
1369 asset.symbol.clone(),
1370 Some(asset.name.clone()),
1371 asset.decimals,
1372 asset.price_usd,
1373 )?,
1374 v.state.as_ref().and_then($state_fn),
1375 v.allocators
1376 .into_iter()
1377 .filter_map(|a| VaultAllocator::from_gql(&a.address))
1378 .collect(),
1379 v.warnings
1380 .into_iter()
1381 .map(|w| VaultWarning {
1382 warning_type: w.type_.clone(),
1383 level: format!("{:?}", w.level),
1384 })
1385 .collect(),
1386 )
1387 }
1388 };
1389}
1390
1391impl_v1_vault_conversion!(
1393 convert_v1_vault,
1394 convert_v1_market_state,
1395 convert_v1_state,
1396 get_vaults_v1
1397);
1398
1399impl_v1_vault_conversion!(
1401 convert_v1_vault_single,
1402 convert_v1_market_state_single,
1403 convert_v1_state_single,
1404 get_vault_v1_by_address
1405);
1406
1407fn convert_v2_vault(v: get_vaults_v2::GetVaultsV2VaultV2sItems) -> Option<VaultV2> {
1408 let chain_id = v.chain.id;
1409 let asset = &v.asset;
1410
1411 VaultV2::from_gql(
1412 &v.address,
1413 v.name,
1414 v.symbol,
1415 chain_id,
1416 v.listed,
1417 v.whitelisted,
1418 Asset::from_gql(
1419 &asset.address,
1420 asset.symbol.clone(),
1421 Some(asset.name.clone()),
1422 asset.decimals,
1423 asset.price_usd,
1424 )?,
1425 Some(v.curator.address.as_str()),
1426 Some(v.owner.address.as_str()),
1427 v.total_assets.as_deref().unwrap_or("0"),
1428 v.total_assets_usd,
1429 &v.total_supply,
1430 Some(v.share_price),
1431 Some(v.performance_fee),
1432 Some(v.management_fee),
1433 v.avg_apy,
1434 v.avg_net_apy,
1435 v.apy,
1436 v.net_apy,
1437 &v.liquidity,
1438 v.liquidity_usd,
1439 v.adapters
1440 .items
1441 .map(|items| {
1442 items
1443 .into_iter()
1444 .filter_map(convert_v2_adapter)
1445 .collect()
1446 })
1447 .unwrap_or_default(),
1448 v.rewards
1449 .into_iter()
1450 .filter_map(|r| {
1451 VaultReward::from_gql(
1452 &r.asset.address,
1453 r.asset.symbol.clone(),
1454 r.supply_apr,
1455 parse_yearly_supply(&r.yearly_supply_tokens),
1456 )
1457 })
1458 .collect(),
1459 v.warnings
1460 .into_iter()
1461 .map(|w| VaultV2Warning {
1462 warning_type: w.type_.clone(),
1463 level: format!("{:?}", w.level),
1464 })
1465 .collect(),
1466 )
1467}
1468
1469fn parse_yearly_supply(s: &str) -> Option<f64> {
1471 s.parse::<f64>().ok()
1472}
1473
1474fn convert_v2_vault_single(
1475 v: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddress,
1476) -> Option<VaultV2> {
1477 let chain_id = v.chain.id;
1478 let asset = &v.asset;
1479
1480 VaultV2::from_gql(
1481 &v.address,
1482 v.name,
1483 v.symbol,
1484 chain_id,
1485 v.listed,
1486 v.whitelisted,
1487 Asset::from_gql(
1488 &asset.address,
1489 asset.symbol.clone(),
1490 Some(asset.name.clone()),
1491 asset.decimals,
1492 asset.price_usd,
1493 )?,
1494 Some(v.curator.address.as_str()),
1495 Some(v.owner.address.as_str()),
1496 v.total_assets.as_deref().unwrap_or("0"),
1497 v.total_assets_usd,
1498 &v.total_supply,
1499 Some(v.share_price),
1500 Some(v.performance_fee),
1501 Some(v.management_fee),
1502 v.avg_apy,
1503 v.avg_net_apy,
1504 v.apy,
1505 v.net_apy,
1506 &v.liquidity,
1507 v.liquidity_usd,
1508 v.adapters
1509 .items
1510 .map(|items| {
1511 items
1512 .into_iter()
1513 .filter_map(convert_v2_adapter_single)
1514 .collect()
1515 })
1516 .unwrap_or_default(),
1517 v.rewards
1518 .into_iter()
1519 .filter_map(|r| {
1520 VaultReward::from_gql(
1521 &r.asset.address,
1522 r.asset.symbol.clone(),
1523 r.supply_apr,
1524 parse_yearly_supply(&r.yearly_supply_tokens),
1525 )
1526 })
1527 .collect(),
1528 v.warnings
1529 .into_iter()
1530 .map(|w| VaultV2Warning {
1531 warning_type: w.type_.clone(),
1532 level: format!("{:?}", w.level),
1533 })
1534 .collect(),
1535 )
1536}
1537
1538#[allow(unreachable_patterns)]
1539fn convert_v2_adapter(
1540 a: get_vaults_v2::GetVaultsV2VaultV2sItemsAdaptersItems,
1541) -> Option<VaultAdapter> {
1542 use get_vaults_v2::GetVaultsV2VaultV2sItemsAdaptersItemsOn::*;
1543
1544 let data = match &a.on {
1546 MorphoMarketV1Adapter(adapter) => {
1547 let positions: Vec<MorphoMarketPosition> = adapter.positions.items.as_ref()
1548 .map(|items| {
1549 items.iter().filter_map(|pos| {
1550 let market_id = B256::from_str(&pos.market.unique_key).ok()?;
1551 let market_state = pos.market.state.as_ref().and_then(|ms| {
1552 let lltv = parse_bigint(&pos.market.lltv)?;
1553 let timestamp: u64 = ms.timestamp.0.parse().ok()?;
1554 Some(MarketStateV2 {
1555 id: market_id,
1556 total_supply_assets: parse_bigint(&ms.supply_assets)?,
1557 total_borrow_assets: parse_bigint(&ms.borrow_assets)?,
1558 total_supply_shares: parse_bigint(&ms.supply_shares)?,
1559 total_borrow_shares: parse_bigint(&ms.borrow_shares)?,
1560 last_update: timestamp,
1561 fee: fee_to_wad(ms.fee),
1562 rate_at_target: ms.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1563 price: ms.price.as_ref().and_then(|p| parse_bigint(p)),
1564 lltv,
1565 })
1566 });
1567 Some(MorphoMarketPosition {
1568 supply_assets: parse_bigint(&pos.supply_assets)?,
1569 supply_shares: parse_bigint(&pos.supply_shares)?,
1570 market_id,
1571 market_state,
1572 })
1573 }).collect()
1574 })
1575 .unwrap_or_default();
1576 Some(VaultAdapterData::MorphoMarketV1 { positions })
1577 }
1578 MetaMorphoAdapter(adapter) => {
1579 let allocations: Vec<MetaMorphoAllocation> = adapter.meta_morpho.state.as_ref()
1580 .map(|state| {
1581 state.allocation.iter().filter_map(|alloc| {
1582 let market_id = B256::from_str(&alloc.market.unique_key).ok()?;
1583 let market_state = alloc.market.state.as_ref().and_then(|ms| {
1584 let lltv = parse_bigint(&alloc.market.lltv)?;
1585 let timestamp: u64 = ms.timestamp.0.parse().ok()?;
1586 Some(MarketStateV2 {
1587 id: market_id,
1588 total_supply_assets: parse_bigint(&ms.supply_assets)?,
1589 total_borrow_assets: parse_bigint(&ms.borrow_assets)?,
1590 total_supply_shares: parse_bigint(&ms.supply_shares)?,
1591 total_borrow_shares: parse_bigint(&ms.borrow_shares)?,
1592 last_update: timestamp,
1593 fee: fee_to_wad(ms.fee),
1594 rate_at_target: ms.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1595 price: ms.price.as_ref().and_then(|p| parse_bigint(p)),
1596 lltv,
1597 })
1598 });
1599 Some(MetaMorphoAllocation {
1600 supply_assets: parse_bigint(&alloc.supply_assets)?,
1601 supply_cap: parse_bigint(&alloc.supply_cap)?,
1602 enabled: alloc.enabled,
1603 supply_queue_index: alloc.supply_queue_index.map(|i| i as i32),
1604 withdraw_queue_index: alloc.withdraw_queue_index.map(|i| i as i32),
1605 market_id,
1606 market_state,
1607 })
1608 }).collect()
1609 })
1610 .unwrap_or_default();
1611 Some(VaultAdapterData::MetaMorpho { allocations })
1612 }
1613 _ => None,
1614 };
1615
1616 VaultAdapter::from_gql(
1617 a.id,
1618 &a.address,
1619 format!("{:?}", a.type_),
1620 &a.assets,
1621 a.assets_usd,
1622 data,
1623 )
1624}
1625
1626#[allow(unreachable_patterns)]
1627fn convert_v2_adapter_single(
1628 a: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddressAdaptersItems,
1629) -> Option<VaultAdapter> {
1630 use get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddressAdaptersItemsOn::*;
1631
1632 let data = match &a.on {
1634 MorphoMarketV1Adapter(adapter) => {
1635 let positions: Vec<MorphoMarketPosition> = adapter.positions.items.as_ref()
1636 .map(|items| {
1637 items.iter().filter_map(|pos| {
1638 let market_id = B256::from_str(&pos.market.unique_key).ok()?;
1639 let market_state = pos.market.state.as_ref().and_then(|ms| {
1640 let lltv = parse_bigint(&pos.market.lltv)?;
1641 let timestamp: u64 = ms.timestamp.0.parse().ok()?;
1642 Some(MarketStateV2 {
1643 id: market_id,
1644 total_supply_assets: parse_bigint(&ms.supply_assets)?,
1645 total_borrow_assets: parse_bigint(&ms.borrow_assets)?,
1646 total_supply_shares: parse_bigint(&ms.supply_shares)?,
1647 total_borrow_shares: parse_bigint(&ms.borrow_shares)?,
1648 last_update: timestamp,
1649 fee: fee_to_wad(ms.fee),
1650 rate_at_target: ms.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1651 price: ms.price.as_ref().and_then(|p| parse_bigint(p)),
1652 lltv,
1653 })
1654 });
1655 Some(MorphoMarketPosition {
1656 supply_assets: parse_bigint(&pos.supply_assets)?,
1657 supply_shares: parse_bigint(&pos.supply_shares)?,
1658 market_id,
1659 market_state,
1660 })
1661 }).collect()
1662 })
1663 .unwrap_or_default();
1664 Some(VaultAdapterData::MorphoMarketV1 { positions })
1665 }
1666 MetaMorphoAdapter(adapter) => {
1667 let allocations: Vec<MetaMorphoAllocation> = adapter.meta_morpho.state.as_ref()
1668 .map(|state| {
1669 state.allocation.iter().filter_map(|alloc| {
1670 let market_id = B256::from_str(&alloc.market.unique_key).ok()?;
1671 let market_state = alloc.market.state.as_ref().and_then(|ms| {
1672 let lltv = parse_bigint(&alloc.market.lltv)?;
1673 let timestamp: u64 = ms.timestamp.0.parse().ok()?;
1674 Some(MarketStateV2 {
1675 id: market_id,
1676 total_supply_assets: parse_bigint(&ms.supply_assets)?,
1677 total_borrow_assets: parse_bigint(&ms.borrow_assets)?,
1678 total_supply_shares: parse_bigint(&ms.supply_shares)?,
1679 total_borrow_shares: parse_bigint(&ms.borrow_shares)?,
1680 last_update: timestamp,
1681 fee: fee_to_wad(ms.fee),
1682 rate_at_target: ms.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1683 price: ms.price.as_ref().and_then(|p| parse_bigint(p)),
1684 lltv,
1685 })
1686 });
1687 Some(MetaMorphoAllocation {
1688 supply_assets: parse_bigint(&alloc.supply_assets)?,
1689 supply_cap: parse_bigint(&alloc.supply_cap)?,
1690 enabled: alloc.enabled,
1691 supply_queue_index: alloc.supply_queue_index.map(|i| i as i32),
1692 withdraw_queue_index: alloc.withdraw_queue_index.map(|i| i as i32),
1693 market_id,
1694 market_state,
1695 })
1696 }).collect()
1697 })
1698 .unwrap_or_default();
1699 Some(VaultAdapterData::MetaMorpho { allocations })
1700 }
1701 _ => None,
1702 };
1703
1704 VaultAdapter::from_gql(
1705 a.id,
1706 &a.address,
1707 format!("{:?}", a.type_),
1708 &a.assets,
1709 a.assets_usd,
1710 data,
1711 )
1712}
1713
1714fn convert_user_vault_v1_position(
1717 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultPositions,
1718) -> Option<UserVaultV1Position> {
1719 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1720
1721 let state = p.state.as_ref().and_then(|s| {
1722 VaultPositionState::from_gql(
1723 &s.shares,
1724 s.assets.as_deref(),
1725 s.assets_usd,
1726 s.pnl.as_deref(),
1727 s.pnl_usd,
1728 s.roe,
1729 s.roe_usd,
1730 )
1731 });
1732
1733 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1734}
1735
1736fn convert_user_vault_v2_position(
1737 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultV2Positions,
1738) -> Option<UserVaultV2Position> {
1739 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1740
1741 UserVaultV2Position::from_gql(
1742 p.id,
1743 &p.shares,
1744 &p.assets,
1745 p.assets_usd,
1746 p.pnl.as_deref(),
1747 p.pnl_usd,
1748 p.roe,
1749 p.roe_usd,
1750 vault,
1751 )
1752}
1753
1754fn convert_user_vault_v1_position_overview(
1755 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultPositions,
1756) -> Option<UserVaultV1Position> {
1757 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1758
1759 let state = p.state.as_ref().and_then(|s| {
1760 VaultPositionState::from_gql(
1761 &s.shares,
1762 s.assets.as_deref(),
1763 s.assets_usd,
1764 s.pnl.as_deref(),
1765 s.pnl_usd,
1766 s.roe,
1767 s.roe_usd,
1768 )
1769 });
1770
1771 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1772}
1773
1774fn convert_user_vault_v2_position_overview(
1775 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultV2Positions,
1776) -> Option<UserVaultV2Position> {
1777 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1778
1779 UserVaultV2Position::from_gql(
1780 p.id,
1781 &p.shares,
1782 &p.assets,
1783 p.assets_usd,
1784 p.pnl.as_deref(),
1785 p.pnl_usd,
1786 p.roe,
1787 p.roe_usd,
1788 vault,
1789 )
1790}
1791
1792fn convert_user_market_position(
1793 p: get_user_account_overview::GetUserAccountOverviewUserByAddressMarketPositions,
1794) -> Option<UserMarketPosition> {
1795 let market = MarketInfo::from_gql(
1796 p.market.unique_key,
1797 Some(p.market.loan_asset.symbol),
1798 Some(p.market.loan_asset.address.as_str()),
1799 p.market.collateral_asset.as_ref().map(|c| c.symbol.clone()),
1800 p.market.collateral_asset.as_ref().map(|c| c.address.as_str()),
1801 );
1802
1803 UserMarketPosition::from_gql(
1804 p.id,
1805 &p.supply_shares,
1806 &p.supply_assets,
1807 p.supply_assets_usd,
1808 &p.borrow_shares,
1809 &p.borrow_assets,
1810 p.borrow_assets_usd,
1811 &p.collateral,
1812 p.collateral_usd,
1813 p.health_factor,
1814 market,
1815 )
1816}
1817