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