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::simulation::{
14 get_vault_for_simulation, get_vaults_for_simulation, GetVaultForSimulation,
15 GetVaultsForSimulation,
16};
17use crate::queries::v1::{
18 get_vault_v1_by_address, get_vaults_v1, GetVaultV1ByAddress, GetVaultsV1,
19};
20use crate::queries::user::{
21 get_user_account_overview, get_user_vault_positions, GetUserAccountOverview,
22 GetUserVaultPositions,
23};
24use crate::queries::v2::{
25 get_vault_v2_by_address, get_vaults_v2, GetVaultV2ByAddress, GetVaultsV2,
26};
27use crate::types::{
28 Asset, MarketInfo, MarketStateForSim, NamedChain, UserAccountOverview, UserMarketPosition,
29 UserState, UserVaultPositions, UserVaultV1Position, UserVaultV2Position, Vault, VaultAdapter,
30 VaultAllocation, VaultAllocationForSim, VaultAllocator, VaultInfo, VaultPositionState,
31 VaultReward, VaultSimulationData, VaultStateV1, VaultV1, VaultV2, VaultV2Warning,
32 VaultWarning, SUPPORTED_CHAINS,
33};
34
35pub const DEFAULT_API_URL: &str = "https://api.morpho.org/graphql";
37
38pub const DEFAULT_PAGE_SIZE: i64 = 100;
40
41#[derive(Debug, Clone)]
43pub struct ClientConfig {
44 pub api_url: Url,
46 pub page_size: i64,
48}
49
50impl Default for ClientConfig {
51 fn default() -> Self {
52 Self {
53 api_url: Url::parse(DEFAULT_API_URL).expect("Invalid default API URL"),
54 page_size: DEFAULT_PAGE_SIZE,
55 }
56 }
57}
58
59impl ClientConfig {
60 pub fn new() -> Self {
62 Self::default()
63 }
64
65 pub fn with_api_url(mut self, url: Url) -> Self {
67 self.api_url = url;
68 self
69 }
70
71 pub fn with_page_size(mut self, size: i64) -> Self {
73 self.page_size = size;
74 self
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct VaultV1Client {
81 http_client: Client,
82 config: ClientConfig,
83}
84
85impl Default for VaultV1Client {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91impl VaultV1Client {
92 pub fn new() -> Self {
94 Self {
95 http_client: Client::new(),
96 config: ClientConfig::default(),
97 }
98 }
99
100 pub fn with_config(config: ClientConfig) -> Self {
102 Self {
103 http_client: Client::new(),
104 config,
105 }
106 }
107
108 async fn execute<Q: GraphQLQuery>(
110 &self,
111 variables: Q::Variables,
112 ) -> Result<Q::ResponseData> {
113 let request_body = Q::build_query(variables);
114 let response = self
115 .http_client
116 .post(self.config.api_url.as_str())
117 .json(&request_body)
118 .send()
119 .await?;
120
121 let response_body: Response<Q::ResponseData> = response.json().await?;
122
123 if let Some(errors) = response_body.errors {
124 if !errors.is_empty() {
125 return Err(ApiError::GraphQL(
126 errors
127 .iter()
128 .map(|e| e.message.clone())
129 .collect::<Vec<_>>()
130 .join("; "),
131 ));
132 }
133 }
134
135 response_body
136 .data
137 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
138 }
139
140 pub async fn get_vaults(&self, filters: Option<VaultFiltersV1>) -> Result<Vec<VaultV1>> {
142 let variables = get_vaults_v1::Variables {
143 first: Some(self.config.page_size),
144 skip: Some(0),
145 where_: filters.map(|f| f.to_gql()),
146 order_by: Some(VaultOrderByV1::default().to_gql()),
147 order_direction: Some(OrderDirection::default().to_gql_v1()),
148 };
149
150 let data = self.execute::<GetVaultsV1>(variables).await?;
151
152 let items = match data.vaults.items {
153 Some(items) => items,
154 None => return Ok(Vec::new()),
155 };
156
157 let vaults: Vec<VaultV1> = items
158 .into_iter()
159 .filter_map(convert_v1_vault)
160 .collect();
161
162 Ok(vaults)
163 }
164
165 pub async fn get_vault(&self, address: &str, chain: NamedChain) -> Result<VaultV1> {
167 let variables = get_vault_v1_by_address::Variables {
168 address: address.to_string(),
169 chain_id: u64::from(chain) as i64,
170 };
171
172 let data = self.execute::<GetVaultV1ByAddress>(variables).await?;
173
174 convert_v1_vault_single(data.vault_by_address).ok_or_else(|| ApiError::VaultNotFound {
175 address: address.to_string(),
176 chain_id: u64::from(chain) as i64,
177 })
178 }
179
180 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<VaultV1>> {
182 let filters = VaultFiltersV1::new().chain(chain);
183 self.get_vaults(Some(filters)).await
184 }
185
186 pub async fn get_vaults_by_curator(
188 &self,
189 curator: &str,
190 chain: Option<NamedChain>,
191 ) -> Result<Vec<VaultV1>> {
192 let mut filters = VaultFiltersV1::new().curators([curator]);
193 if let Some(c) = chain {
194 filters = filters.chain(c);
195 }
196 self.get_vaults(Some(filters)).await
197 }
198
199 pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<VaultV1>> {
201 let mut filters = VaultFiltersV1::new().listed(true);
202 if let Some(c) = chain {
203 filters = filters.chain(c);
204 }
205 self.get_vaults(Some(filters)).await
206 }
207
208 pub async fn get_vaults_with_options(
236 &self,
237 options: VaultQueryOptionsV1,
238 ) -> Result<Vec<VaultV1>> {
239 let variables = get_vaults_v1::Variables {
240 first: options.limit.or(Some(self.config.page_size)),
241 skip: Some(0),
242 where_: options.filters.map(|f| f.to_gql()),
243 order_by: Some(options.order_by.unwrap_or_default().to_gql()),
244 order_direction: Some(options.order_direction.unwrap_or_default().to_gql_v1()),
245 };
246
247 let data = self.execute::<GetVaultsV1>(variables).await?;
248
249 let items = match data.vaults.items {
250 Some(items) => items,
251 None => return Ok(Vec::new()),
252 };
253
254 let vaults: Vec<VaultV1> = items
255 .into_iter()
256 .filter_map(convert_v1_vault)
257 .collect();
258
259 Ok(vaults)
260 }
261
262 pub async fn get_top_vaults_by_apy(
280 &self,
281 limit: i64,
282 filters: Option<VaultFiltersV1>,
283 ) -> Result<Vec<VaultV1>> {
284 let options = VaultQueryOptionsV1 {
285 filters,
286 order_by: Some(VaultOrderByV1::NetApy),
287 order_direction: Some(OrderDirection::Desc),
288 limit: Some(limit),
289 };
290 self.get_vaults_with_options(options).await
291 }
292
293 pub async fn get_vaults_by_asset(
310 &self,
311 asset_symbol: &str,
312 chain: Option<NamedChain>,
313 ) -> Result<Vec<VaultV1>> {
314 let mut filters = VaultFiltersV1::new().asset_symbols([asset_symbol]);
315 if let Some(c) = chain {
316 filters = filters.chain(c);
317 }
318 self.get_vaults(Some(filters)).await
319 }
320
321 pub async fn get_vault_for_simulation(
346 &self,
347 address: &str,
348 chain: NamedChain,
349 ) -> Result<VaultSimulationData> {
350 let variables = get_vault_for_simulation::Variables {
351 address: address.to_string(),
352 chain_id: u64::from(chain) as i64,
353 };
354
355 let data = self.execute::<GetVaultForSimulation>(variables).await?;
356
357 convert_vault_for_simulation_single(data.vault_by_address).ok_or_else(|| {
358 ApiError::VaultNotFound {
359 address: address.to_string(),
360 chain_id: u64::from(chain) as i64,
361 }
362 })
363 }
364
365 pub async fn get_vaults_for_simulation(
391 &self,
392 filters: Option<VaultFiltersV1>,
393 limit: i64,
394 ) -> Result<Vec<VaultSimulationData>> {
395 let variables = get_vaults_for_simulation::Variables {
396 first: Some(limit),
397 skip: Some(0),
398 where_: filters.map(|f| f.to_gql_sim()),
399 order_by: Some(VaultOrderByV1::default().to_gql_sim()),
400 order_direction: Some(OrderDirection::default().to_gql_sim()),
401 };
402
403 let data = self.execute::<GetVaultsForSimulation>(variables).await?;
404
405 let items = match data.vaults.items {
406 Some(items) => items,
407 None => return Ok(Vec::new()),
408 };
409
410 let vaults: Vec<VaultSimulationData> = items
411 .into_iter()
412 .filter_map(convert_vault_for_simulation)
413 .collect();
414
415 Ok(vaults)
416 }
417}
418
419#[derive(Debug, Clone)]
421pub struct VaultV2Client {
422 http_client: Client,
423 config: ClientConfig,
424}
425
426impl Default for VaultV2Client {
427 fn default() -> Self {
428 Self::new()
429 }
430}
431
432impl VaultV2Client {
433 pub fn new() -> Self {
435 Self {
436 http_client: Client::new(),
437 config: ClientConfig::default(),
438 }
439 }
440
441 pub fn with_config(config: ClientConfig) -> Self {
443 Self {
444 http_client: Client::new(),
445 config,
446 }
447 }
448
449 async fn execute<Q: GraphQLQuery>(
451 &self,
452 variables: Q::Variables,
453 ) -> Result<Q::ResponseData> {
454 let request_body = Q::build_query(variables);
455 let response = self
456 .http_client
457 .post(self.config.api_url.as_str())
458 .json(&request_body)
459 .send()
460 .await?;
461
462 let response_body: Response<Q::ResponseData> = response.json().await?;
463
464 if let Some(errors) = response_body.errors {
465 if !errors.is_empty() {
466 return Err(ApiError::GraphQL(
467 errors
468 .iter()
469 .map(|e| e.message.clone())
470 .collect::<Vec<_>>()
471 .join("; "),
472 ));
473 }
474 }
475
476 response_body
477 .data
478 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
479 }
480
481 pub async fn get_vaults(&self, filters: Option<VaultFiltersV2>) -> Result<Vec<VaultV2>> {
483 let variables = get_vaults_v2::Variables {
484 first: Some(self.config.page_size),
485 skip: Some(0),
486 where_: filters.map(|f| f.to_gql()),
487 order_by: Some(VaultOrderByV2::default().to_gql()),
488 order_direction: Some(OrderDirection::default().to_gql_v2()),
489 };
490
491 let data = self.execute::<GetVaultsV2>(variables).await?;
492
493 let items = match data.vault_v2s.items {
494 Some(items) => items,
495 None => return Ok(Vec::new()),
496 };
497
498 let vaults: Vec<VaultV2> = items
499 .into_iter()
500 .filter_map(convert_v2_vault)
501 .collect();
502
503 Ok(vaults)
504 }
505
506 pub async fn get_vault(&self, address: &str, chain: NamedChain) -> Result<VaultV2> {
508 let variables = get_vault_v2_by_address::Variables {
509 address: address.to_string(),
510 chain_id: u64::from(chain) as i64,
511 };
512
513 let data = self.execute::<GetVaultV2ByAddress>(variables).await?;
514
515 convert_v2_vault_single(data.vault_v2_by_address).ok_or_else(|| ApiError::VaultNotFound {
516 address: address.to_string(),
517 chain_id: u64::from(chain) as i64,
518 })
519 }
520
521 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<VaultV2>> {
523 let filters = VaultFiltersV2::new().chain(chain);
524 self.get_vaults(Some(filters)).await
525 }
526
527 pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<VaultV2>> {
529 let mut filters = VaultFiltersV2::new().listed(true);
530 if let Some(c) = chain {
531 filters = filters.chain(c);
532 }
533 self.get_vaults(Some(filters)).await
534 }
535
536 pub async fn get_vaults_with_options(
567 &self,
568 options: VaultQueryOptionsV2,
569 ) -> Result<Vec<VaultV2>> {
570 let fetch_limit = if options.has_asset_filter() {
573 options.limit.map(|l| l * 3).or(Some(self.config.page_size))
575 } else {
576 options.limit.or(Some(self.config.page_size))
577 };
578
579 let variables = get_vaults_v2::Variables {
580 first: fetch_limit,
581 skip: Some(0),
582 where_: options.filters.map(|f| f.to_gql()),
583 order_by: Some(options.order_by.unwrap_or_default().to_gql()),
584 order_direction: Some(options.order_direction.unwrap_or_default().to_gql_v2()),
585 };
586
587 let data = self.execute::<GetVaultsV2>(variables).await?;
588
589 let items = match data.vault_v2s.items {
590 Some(items) => items,
591 None => return Ok(Vec::new()),
592 };
593
594 let mut vaults: Vec<VaultV2> = items
595 .into_iter()
596 .filter_map(convert_v2_vault)
597 .collect();
598
599 if let Some(ref symbols) = options.asset_symbols {
601 vaults.retain(|v| symbols.iter().any(|s| s.eq_ignore_ascii_case(&v.asset.symbol)));
602 }
603 if let Some(ref addresses) = options.asset_addresses {
604 vaults.retain(|v| {
605 addresses
606 .iter()
607 .any(|a| v.asset.address.to_string().eq_ignore_ascii_case(a))
608 });
609 }
610
611 if let Some(limit) = options.limit {
613 vaults.truncate(limit as usize);
614 }
615
616 Ok(vaults)
617 }
618
619 pub async fn get_top_vaults_by_apy(
637 &self,
638 limit: i64,
639 filters: Option<VaultFiltersV2>,
640 ) -> Result<Vec<VaultV2>> {
641 let options = VaultQueryOptionsV2 {
642 filters,
643 order_by: Some(VaultOrderByV2::NetApy),
644 order_direction: Some(OrderDirection::Desc),
645 limit: Some(limit),
646 asset_addresses: None,
647 asset_symbols: None,
648 };
649 self.get_vaults_with_options(options).await
650 }
651
652 pub async fn get_vaults_by_asset(
672 &self,
673 asset_symbol: &str,
674 chain: Option<NamedChain>,
675 ) -> Result<Vec<VaultV2>> {
676 let filters = chain.map(|c| VaultFiltersV2::new().chain(c));
677 let options = VaultQueryOptionsV2 {
678 filters,
679 order_by: None,
680 order_direction: None,
681 limit: None,
682 asset_addresses: None,
683 asset_symbols: Some(vec![asset_symbol.to_string()]),
684 };
685 self.get_vaults_with_options(options).await
686 }
687}
688
689#[derive(Debug, Clone)]
691pub struct MorphoApiClient {
692 pub v1: VaultV1Client,
694 pub v2: VaultV2Client,
696}
697
698impl Default for MorphoApiClient {
699 fn default() -> Self {
700 Self::new()
701 }
702}
703
704impl MorphoApiClient {
705 pub fn new() -> Self {
707 Self {
708 v1: VaultV1Client::new(),
709 v2: VaultV2Client::new(),
710 }
711 }
712
713 pub fn with_config(config: ClientConfig) -> Self {
715 Self {
716 v1: VaultV1Client::with_config(config.clone()),
717 v2: VaultV2Client::with_config(config),
718 }
719 }
720
721 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<Vault>> {
723 let (v1_vaults, v2_vaults) = tokio::try_join!(
724 self.v1.get_vaults_by_chain(chain),
725 self.v2.get_vaults_by_chain(chain),
726 )?;
727
728 let mut vaults: Vec<Vault> = Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
729 vaults.extend(v1_vaults.into_iter().map(Vault::from));
730 vaults.extend(v2_vaults.into_iter().map(Vault::from));
731
732 Ok(vaults)
733 }
734
735 pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<Vault>> {
737 let (v1_vaults, v2_vaults) = tokio::try_join!(
738 self.v1.get_whitelisted_vaults(chain),
739 self.v2.get_whitelisted_vaults(chain),
740 )?;
741
742 let mut vaults: Vec<Vault> = Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
743 vaults.extend(v1_vaults.into_iter().map(Vault::from));
744 vaults.extend(v2_vaults.into_iter().map(Vault::from));
745
746 Ok(vaults)
747 }
748
749 async fn execute<Q: GraphQLQuery>(&self, variables: Q::Variables) -> Result<Q::ResponseData> {
751 let request_body = Q::build_query(variables);
752 let response = self
753 .v1
754 .http_client
755 .post(self.v1.config.api_url.as_str())
756 .json(&request_body)
757 .send()
758 .await?;
759
760 let response_body: Response<Q::ResponseData> = response.json().await?;
761
762 if let Some(errors) = response_body.errors {
763 if !errors.is_empty() {
764 return Err(ApiError::GraphQL(
765 errors
766 .iter()
767 .map(|e| e.message.clone())
768 .collect::<Vec<_>>()
769 .join("; "),
770 ));
771 }
772 }
773
774 response_body
775 .data
776 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
777 }
778
779 pub async fn get_user_vault_positions(
784 &self,
785 address: &str,
786 chain: Option<NamedChain>,
787 ) -> Result<UserVaultPositions> {
788 match chain {
789 Some(c) => self.get_user_vault_positions_single_chain(address, c).await,
790 None => self.get_user_vault_positions_all_chains(address).await,
791 }
792 }
793
794 async fn get_user_vault_positions_single_chain(
796 &self,
797 address: &str,
798 chain: NamedChain,
799 ) -> Result<UserVaultPositions> {
800 let variables = get_user_vault_positions::Variables {
801 address: address.to_string(),
802 chain_id: u64::from(chain) as i64,
803 };
804
805 let data = self.execute::<GetUserVaultPositions>(variables).await?;
806 let user = data.user_by_address;
807
808 let vault_positions: Vec<UserVaultV1Position> = user
809 .vault_positions
810 .into_iter()
811 .filter_map(convert_user_vault_v1_position)
812 .collect();
813
814 let vault_v2_positions: Vec<UserVaultV2Position> = user
815 .vault_v2_positions
816 .into_iter()
817 .filter_map(convert_user_vault_v2_position)
818 .collect();
819
820 Ok(UserVaultPositions {
821 address: user
822 .address
823 .parse()
824 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
825 vault_positions,
826 vault_v2_positions,
827 })
828 }
829
830 async fn get_user_vault_positions_all_chains(
832 &self,
833 address: &str,
834 ) -> Result<UserVaultPositions> {
835 use futures::future::join_all;
836
837 let valid_chains: Vec<_> = SUPPORTED_CHAINS
839 .iter()
840 .filter(|chain| u64::from(**chain) <= i32::MAX as u64)
841 .copied()
842 .collect();
843
844 let futures: Vec<_> = valid_chains
845 .iter()
846 .map(|chain| self.get_user_vault_positions_single_chain(address, *chain))
847 .collect();
848
849 let results = join_all(futures).await;
850
851 let parsed_address = address
852 .parse()
853 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?;
854
855 let mut all_v1_positions = Vec::new();
856 let mut all_v2_positions = Vec::new();
857
858 for result in results {
859 match result {
860 Ok(positions) => {
861 all_v1_positions.extend(positions.vault_positions);
862 all_v2_positions.extend(positions.vault_v2_positions);
863 }
864 Err(ApiError::GraphQL(msg)) if msg.contains("No results") => continue,
866 Err(e) => return Err(e),
867 }
868 }
869
870 Ok(UserVaultPositions {
871 address: parsed_address,
872 vault_positions: all_v1_positions,
873 vault_v2_positions: all_v2_positions,
874 })
875 }
876
877 pub async fn get_user_account_overview(
879 &self,
880 address: &str,
881 chain: NamedChain,
882 ) -> Result<UserAccountOverview> {
883 let variables = get_user_account_overview::Variables {
884 address: address.to_string(),
885 chain_id: u64::from(chain) as i64,
886 };
887
888 let data = self.execute::<GetUserAccountOverview>(variables).await?;
889 let user = data.user_by_address;
890
891 let state = UserState::from_gql(
892 user.state.vaults_pnl_usd,
893 user.state.vaults_roe_usd,
894 user.state.vaults_assets_usd,
895 user.state.vault_v2s_pnl_usd,
896 user.state.vault_v2s_roe_usd,
897 user.state.vault_v2s_assets_usd,
898 user.state.markets_pnl_usd,
899 user.state.markets_roe_usd,
900 user.state.markets_supply_pnl_usd,
901 user.state.markets_supply_roe_usd,
902 user.state.markets_borrow_pnl_usd,
903 user.state.markets_borrow_roe_usd,
904 user.state.markets_collateral_pnl_usd,
905 user.state.markets_collateral_roe_usd,
906 user.state.markets_margin_pnl_usd,
907 user.state.markets_margin_roe_usd,
908 user.state.markets_collateral_usd,
909 user.state.markets_supply_assets_usd,
910 user.state.markets_borrow_assets_usd,
911 user.state.markets_margin_usd,
912 );
913
914 let vault_positions: Vec<UserVaultV1Position> = user
915 .vault_positions
916 .into_iter()
917 .filter_map(convert_user_vault_v1_position_overview)
918 .collect();
919
920 let vault_v2_positions: Vec<UserVaultV2Position> = user
921 .vault_v2_positions
922 .into_iter()
923 .filter_map(convert_user_vault_v2_position_overview)
924 .collect();
925
926 let market_positions: Vec<UserMarketPosition> = user
927 .market_positions
928 .into_iter()
929 .filter_map(convert_user_market_position)
930 .collect();
931
932 Ok(UserAccountOverview {
933 address: user
934 .address
935 .parse()
936 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
937 state,
938 vault_positions,
939 vault_v2_positions,
940 market_positions,
941 })
942 }
943}
944
945#[derive(Debug, Clone)]
947pub struct MorphoClientConfig {
948 pub api_config: Option<ClientConfig>,
950 pub rpc_url: Option<String>,
952 pub private_key: Option<String>,
954 pub auto_approve: bool,
958}
959
960impl Default for MorphoClientConfig {
961 fn default() -> Self {
962 Self {
963 api_config: None,
964 rpc_url: None,
965 private_key: None,
966 auto_approve: true,
967 }
968 }
969}
970
971impl MorphoClientConfig {
972 pub fn new() -> Self {
974 Self::default()
975 }
976
977 pub fn with_api_config(mut self, config: ClientConfig) -> Self {
979 self.api_config = Some(config);
980 self
981 }
982
983 pub fn with_rpc_url(mut self, rpc_url: impl Into<String>) -> Self {
985 self.rpc_url = Some(rpc_url.into());
986 self
987 }
988
989 pub fn with_private_key(mut self, private_key: impl Into<String>) -> Self {
991 self.private_key = Some(private_key.into());
992 self
993 }
994
995 pub fn with_auto_approve(mut self, auto_approve: bool) -> Self {
999 self.auto_approve = auto_approve;
1000 self
1001 }
1002}
1003
1004pub struct VaultV1Operations<'a> {
1006 client: &'a VaultV1TransactionClient,
1007 auto_approve: bool,
1008}
1009
1010impl<'a> VaultV1Operations<'a> {
1011 fn new(client: &'a VaultV1TransactionClient, auto_approve: bool) -> Self {
1013 Self { client, auto_approve }
1014 }
1015
1016 pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1021 if self.auto_approve {
1022 let asset = self.client.get_asset(vault).await?;
1023 let current_allowance = self
1024 .client
1025 .get_allowance(asset, self.client.signer_address(), vault)
1026 .await?;
1027 if current_allowance < amount {
1028 let needed = amount - current_allowance;
1029 if let Some(approval) = self.client.approve_if_needed(asset, vault, needed).await? {
1030 approval.send().await?;
1031 }
1032 }
1033 }
1034
1035 let receipt = self
1036 .client
1037 .deposit(vault, amount, self.client.signer_address())
1038 .send()
1039 .await?;
1040 Ok(receipt)
1041 }
1042
1043 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1045 let signer = self.client.signer_address();
1046 let receipt = self.client.withdraw(vault, amount, signer, signer).send().await?;
1047 Ok(receipt)
1048 }
1049
1050 pub async fn balance(&self, vault: Address) -> Result<U256> {
1052 let balance = self
1053 .client
1054 .get_balance(vault, self.client.signer_address())
1055 .await?;
1056 Ok(balance)
1057 }
1058
1059 pub async fn approve(
1062 &self,
1063 vault: Address,
1064 amount: U256,
1065 ) -> Result<Option<TransactionReceipt>> {
1066 let asset = self.client.get_asset(vault).await?;
1067 if let Some(approval) = self.client.approve_if_needed(asset, vault, amount).await? {
1068 let receipt = approval.send().await?;
1069 Ok(Some(receipt))
1070 } else {
1071 Ok(None)
1072 }
1073 }
1074
1075 pub async fn get_allowance(&self, vault: Address) -> Result<U256> {
1077 let asset = self.client.get_asset(vault).await?;
1078 let allowance = self
1079 .client
1080 .get_allowance(asset, self.client.signer_address(), vault)
1081 .await?;
1082 Ok(allowance)
1083 }
1084
1085 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
1087 let asset = self.client.get_asset(vault).await?;
1088 Ok(asset)
1089 }
1090
1091 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
1093 let decimals = self.client.get_decimals(token).await?;
1094 Ok(decimals)
1095 }
1096
1097 pub fn signer_address(&self) -> Address {
1099 self.client.signer_address()
1100 }
1101
1102 pub fn auto_approve(&self) -> bool {
1104 self.auto_approve
1105 }
1106}
1107
1108pub struct VaultV2Operations<'a> {
1110 client: &'a VaultV2TransactionClient,
1111 auto_approve: bool,
1112}
1113
1114impl<'a> VaultV2Operations<'a> {
1115 fn new(client: &'a VaultV2TransactionClient, auto_approve: bool) -> Self {
1117 Self { client, auto_approve }
1118 }
1119
1120 pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1125 if self.auto_approve {
1126 let asset = self.client.get_asset(vault).await?;
1127 let current_allowance = self
1128 .client
1129 .get_allowance(asset, self.client.signer_address(), vault)
1130 .await?;
1131 if current_allowance < amount {
1132 let needed = amount - current_allowance;
1133 if let Some(approval) = self.client.approve_if_needed(asset, vault, needed).await? {
1134 approval.send().await?;
1135 }
1136 }
1137 }
1138
1139 let receipt = self
1140 .client
1141 .deposit(vault, amount, self.client.signer_address())
1142 .send()
1143 .await?;
1144 Ok(receipt)
1145 }
1146
1147 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1149 let signer = self.client.signer_address();
1150 let receipt = self.client.withdraw(vault, amount, signer, signer).send().await?;
1151 Ok(receipt)
1152 }
1153
1154 pub async fn balance(&self, vault: Address) -> Result<U256> {
1156 let balance = self
1157 .client
1158 .get_balance(vault, self.client.signer_address())
1159 .await?;
1160 Ok(balance)
1161 }
1162
1163 pub async fn approve(
1166 &self,
1167 vault: Address,
1168 amount: U256,
1169 ) -> Result<Option<TransactionReceipt>> {
1170 let asset = self.client.get_asset(vault).await?;
1171 if let Some(approval) = self.client.approve_if_needed(asset, vault, amount).await? {
1172 let receipt = approval.send().await?;
1173 Ok(Some(receipt))
1174 } else {
1175 Ok(None)
1176 }
1177 }
1178
1179 pub async fn get_allowance(&self, vault: Address) -> Result<U256> {
1181 let asset = self.client.get_asset(vault).await?;
1182 let allowance = self
1183 .client
1184 .get_allowance(asset, self.client.signer_address(), vault)
1185 .await?;
1186 Ok(allowance)
1187 }
1188
1189 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
1191 let asset = self.client.get_asset(vault).await?;
1192 Ok(asset)
1193 }
1194
1195 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
1197 let decimals = self.client.get_decimals(token).await?;
1198 Ok(decimals)
1199 }
1200
1201 pub fn signer_address(&self) -> Address {
1203 self.client.signer_address()
1204 }
1205
1206 pub fn auto_approve(&self) -> bool {
1208 self.auto_approve
1209 }
1210}
1211
1212pub struct MorphoClient {
1245 api: MorphoApiClient,
1246 vault_v1_tx: Option<VaultV1TransactionClient>,
1247 vault_v2_tx: Option<VaultV2TransactionClient>,
1248 auto_approve: bool,
1249}
1250
1251impl Default for MorphoClient {
1252 fn default() -> Self {
1253 Self::new()
1254 }
1255}
1256
1257impl MorphoClient {
1258 pub fn new() -> Self {
1260 Self {
1261 api: MorphoApiClient::new(),
1262 vault_v1_tx: None,
1263 vault_v2_tx: None,
1264 auto_approve: true,
1265 }
1266 }
1267
1268 pub fn with_config(config: MorphoClientConfig) -> Result<Self> {
1272 let api = match config.api_config {
1273 Some(api_config) => MorphoApiClient::with_config(api_config),
1274 None => MorphoApiClient::new(),
1275 };
1276
1277 let (vault_v1_tx, vault_v2_tx) = match (&config.rpc_url, &config.private_key) {
1278 (Some(rpc_url), Some(private_key)) => {
1279 let v1 = VaultV1TransactionClient::new(rpc_url, private_key)?;
1280 let v2 = VaultV2TransactionClient::new(rpc_url, private_key)?;
1281 (Some(v1), Some(v2))
1282 }
1283 _ => (None, None),
1284 };
1285
1286 Ok(Self {
1287 api,
1288 vault_v1_tx,
1289 vault_v2_tx,
1290 auto_approve: config.auto_approve,
1291 })
1292 }
1293
1294 pub fn vault_v1(&self) -> Result<VaultV1Operations<'_>> {
1298 match &self.vault_v1_tx {
1299 Some(client) => Ok(VaultV1Operations::new(client, self.auto_approve)),
1300 None => Err(ApiError::TransactionNotConfigured),
1301 }
1302 }
1303
1304 pub fn vault_v2(&self) -> Result<VaultV2Operations<'_>> {
1308 match &self.vault_v2_tx {
1309 Some(client) => Ok(VaultV2Operations::new(client, self.auto_approve)),
1310 None => Err(ApiError::TransactionNotConfigured),
1311 }
1312 }
1313
1314 pub fn auto_approve(&self) -> bool {
1316 self.auto_approve
1317 }
1318
1319 pub fn api(&self) -> &MorphoApiClient {
1321 &self.api
1322 }
1323
1324 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<Vault>> {
1326 self.api.get_vaults_by_chain(chain).await
1327 }
1328
1329 pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<Vault>> {
1331 self.api.get_whitelisted_vaults(chain).await
1332 }
1333
1334 pub async fn get_user_vault_positions(
1336 &self,
1337 address: &str,
1338 chain: Option<NamedChain>,
1339 ) -> Result<UserVaultPositions> {
1340 self.api.get_user_vault_positions(address, chain).await
1341 }
1342
1343 pub async fn get_user_account_overview(
1345 &self,
1346 address: &str,
1347 chain: NamedChain,
1348 ) -> Result<UserAccountOverview> {
1349 self.api.get_user_account_overview(address, chain).await
1350 }
1351
1352 pub fn has_transaction_support(&self) -> bool {
1354 self.vault_v1_tx.is_some()
1355 }
1356
1357 pub fn signer_address(&self) -> Option<Address> {
1359 self.vault_v1_tx.as_ref().map(|c| c.signer_address())
1360 }
1361}
1362
1363fn convert_v1_vault(v: get_vaults_v1::GetVaultsV1VaultsItems) -> Option<VaultV1> {
1366 let chain_id = v.chain.id;
1367 let asset = &v.asset;
1368
1369 VaultV1::from_gql(
1370 &v.address,
1371 v.name,
1372 v.symbol,
1373 chain_id,
1374 v.listed,
1375 v.featured,
1376 v.whitelisted,
1377 Asset::from_gql(
1378 &asset.address,
1379 asset.symbol.clone(),
1380 Some(asset.name.clone()),
1381 asset.decimals,
1382 asset.price_usd,
1383 )?,
1384 v.state.as_ref().and_then(convert_v1_state),
1385 v.allocators
1386 .into_iter()
1387 .filter_map(|a| VaultAllocator::from_gql(&a.address))
1388 .collect(),
1389 v.warnings
1390 .into_iter()
1391 .map(|w| VaultWarning {
1392 warning_type: w.type_.clone(),
1393 level: format!("{:?}", w.level),
1394 })
1395 .collect(),
1396 )
1397}
1398
1399fn convert_v1_vault_single(
1400 v: get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddress,
1401) -> Option<VaultV1> {
1402 let chain_id = v.chain.id;
1403 let asset = &v.asset;
1404
1405 VaultV1::from_gql(
1406 &v.address,
1407 v.name,
1408 v.symbol,
1409 chain_id,
1410 v.listed,
1411 v.featured,
1412 v.whitelisted,
1413 Asset::from_gql(
1414 &asset.address,
1415 asset.symbol.clone(),
1416 Some(asset.name.clone()),
1417 asset.decimals,
1418 asset.price_usd,
1419 )?,
1420 v.state.as_ref().and_then(convert_v1_state_single),
1421 v.allocators
1422 .into_iter()
1423 .filter_map(|a| VaultAllocator::from_gql(&a.address))
1424 .collect(),
1425 v.warnings
1426 .into_iter()
1427 .map(|w| VaultWarning {
1428 warning_type: w.type_.clone(),
1429 level: format!("{:?}", w.level),
1430 })
1431 .collect(),
1432 )
1433}
1434
1435fn convert_v1_state(s: &get_vaults_v1::GetVaultsV1VaultsItemsState) -> Option<VaultStateV1> {
1436 VaultStateV1::from_gql(
1437 Some(s.curator.as_str()),
1438 Some(s.owner.as_str()),
1439 Some(s.guardian.as_str()),
1440 &s.total_assets,
1441 s.total_assets_usd,
1442 &s.total_supply,
1443 s.fee,
1444 &s.timelock,
1445 s.apy,
1446 s.net_apy,
1447 s.share_price.as_deref().unwrap_or("0"),
1448 s.allocation
1449 .iter()
1450 .filter_map(|a| {
1451 let market = &a.market;
1452 VaultAllocation::from_gql(
1453 market.unique_key.clone(),
1454 Some(market.loan_asset.symbol.clone()),
1455 Some(market.loan_asset.address.as_str()),
1456 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
1457 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
1458 &a.supply_assets,
1459 a.supply_assets_usd,
1460 &a.supply_cap,
1461 )
1462 })
1463 .collect(),
1464 )
1465}
1466
1467fn convert_v1_state_single(
1468 s: &get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddressState,
1469) -> Option<VaultStateV1> {
1470 VaultStateV1::from_gql(
1471 Some(s.curator.as_str()),
1472 Some(s.owner.as_str()),
1473 Some(s.guardian.as_str()),
1474 &s.total_assets,
1475 s.total_assets_usd,
1476 &s.total_supply,
1477 s.fee,
1478 &s.timelock,
1479 s.apy,
1480 s.net_apy,
1481 s.share_price.as_deref().unwrap_or("0"),
1482 s.allocation
1483 .iter()
1484 .filter_map(|a| {
1485 let market = &a.market;
1486 VaultAllocation::from_gql(
1487 market.unique_key.clone(),
1488 Some(market.loan_asset.symbol.clone()),
1489 Some(market.loan_asset.address.as_str()),
1490 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
1491 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
1492 &a.supply_assets,
1493 a.supply_assets_usd,
1494 &a.supply_cap,
1495 )
1496 })
1497 .collect(),
1498 )
1499}
1500
1501fn convert_v2_vault(v: get_vaults_v2::GetVaultsV2VaultV2sItems) -> Option<VaultV2> {
1502 let chain_id = v.chain.id;
1503 let asset = &v.asset;
1504
1505 VaultV2::from_gql(
1506 &v.address,
1507 v.name,
1508 v.symbol,
1509 chain_id,
1510 v.listed,
1511 v.whitelisted,
1512 Asset::from_gql(
1513 &asset.address,
1514 asset.symbol.clone(),
1515 Some(asset.name.clone()),
1516 asset.decimals,
1517 asset.price_usd,
1518 )?,
1519 Some(v.curator.address.as_str()),
1520 Some(v.owner.address.as_str()),
1521 v.total_assets.as_deref().unwrap_or("0"),
1522 v.total_assets_usd,
1523 &v.total_supply,
1524 Some(v.share_price),
1525 Some(v.performance_fee),
1526 Some(v.management_fee),
1527 v.avg_apy,
1528 v.avg_net_apy,
1529 v.apy,
1530 v.net_apy,
1531 &v.liquidity,
1532 v.liquidity_usd,
1533 v.adapters
1534 .items
1535 .map(|items| {
1536 items
1537 .into_iter()
1538 .filter_map(convert_v2_adapter)
1539 .collect()
1540 })
1541 .unwrap_or_default(),
1542 v.rewards
1543 .into_iter()
1544 .filter_map(|r| {
1545 VaultReward::from_gql(
1546 &r.asset.address,
1547 r.asset.symbol.clone(),
1548 r.supply_apr,
1549 parse_yearly_supply(&r.yearly_supply_tokens),
1550 )
1551 })
1552 .collect(),
1553 v.warnings
1554 .into_iter()
1555 .map(|w| VaultV2Warning {
1556 warning_type: w.type_.clone(),
1557 level: format!("{:?}", w.level),
1558 })
1559 .collect(),
1560 )
1561}
1562
1563fn parse_yearly_supply(s: &str) -> Option<f64> {
1565 s.parse::<f64>().ok()
1566}
1567
1568fn convert_v2_vault_single(
1569 v: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddress,
1570) -> Option<VaultV2> {
1571 let chain_id = v.chain.id;
1572 let asset = &v.asset;
1573
1574 VaultV2::from_gql(
1575 &v.address,
1576 v.name,
1577 v.symbol,
1578 chain_id,
1579 v.listed,
1580 v.whitelisted,
1581 Asset::from_gql(
1582 &asset.address,
1583 asset.symbol.clone(),
1584 Some(asset.name.clone()),
1585 asset.decimals,
1586 asset.price_usd,
1587 )?,
1588 Some(v.curator.address.as_str()),
1589 Some(v.owner.address.as_str()),
1590 v.total_assets.as_deref().unwrap_or("0"),
1591 v.total_assets_usd,
1592 &v.total_supply,
1593 Some(v.share_price),
1594 Some(v.performance_fee),
1595 Some(v.management_fee),
1596 v.avg_apy,
1597 v.avg_net_apy,
1598 v.apy,
1599 v.net_apy,
1600 &v.liquidity,
1601 v.liquidity_usd,
1602 v.adapters
1603 .items
1604 .map(|items| {
1605 items
1606 .into_iter()
1607 .filter_map(convert_v2_adapter_single)
1608 .collect()
1609 })
1610 .unwrap_or_default(),
1611 v.rewards
1612 .into_iter()
1613 .filter_map(|r| {
1614 VaultReward::from_gql(
1615 &r.asset.address,
1616 r.asset.symbol.clone(),
1617 r.supply_apr,
1618 parse_yearly_supply(&r.yearly_supply_tokens),
1619 )
1620 })
1621 .collect(),
1622 v.warnings
1623 .into_iter()
1624 .map(|w| VaultV2Warning {
1625 warning_type: w.type_.clone(),
1626 level: format!("{:?}", w.level),
1627 })
1628 .collect(),
1629 )
1630}
1631
1632fn convert_v2_adapter(
1633 a: get_vaults_v2::GetVaultsV2VaultV2sItemsAdaptersItems,
1634) -> Option<VaultAdapter> {
1635 VaultAdapter::from_gql(
1636 a.id,
1637 &a.address,
1638 format!("{:?}", a.type_),
1639 &a.assets,
1640 a.assets_usd,
1641 )
1642}
1643
1644fn convert_v2_adapter_single(
1645 a: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddressAdaptersItems,
1646) -> Option<VaultAdapter> {
1647 VaultAdapter::from_gql(
1648 a.id,
1649 &a.address,
1650 format!("{:?}", a.type_),
1651 &a.assets,
1652 a.assets_usd,
1653 )
1654}
1655
1656fn convert_user_vault_v1_position(
1659 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultPositions,
1660) -> Option<UserVaultV1Position> {
1661 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1662
1663 let state = p.state.as_ref().and_then(|s| {
1664 VaultPositionState::from_gql(
1665 &s.shares,
1666 s.assets.as_deref(),
1667 s.assets_usd,
1668 s.pnl.as_deref(),
1669 s.pnl_usd,
1670 s.roe,
1671 s.roe_usd,
1672 )
1673 });
1674
1675 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1676}
1677
1678fn convert_user_vault_v2_position(
1679 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultV2Positions,
1680) -> Option<UserVaultV2Position> {
1681 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1682
1683 UserVaultV2Position::from_gql(
1684 p.id,
1685 &p.shares,
1686 &p.assets,
1687 p.assets_usd,
1688 p.pnl.as_deref(),
1689 p.pnl_usd,
1690 p.roe,
1691 p.roe_usd,
1692 vault,
1693 )
1694}
1695
1696fn convert_user_vault_v1_position_overview(
1697 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultPositions,
1698) -> Option<UserVaultV1Position> {
1699 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1700
1701 let state = p.state.as_ref().and_then(|s| {
1702 VaultPositionState::from_gql(
1703 &s.shares,
1704 s.assets.as_deref(),
1705 s.assets_usd,
1706 s.pnl.as_deref(),
1707 s.pnl_usd,
1708 s.roe,
1709 s.roe_usd,
1710 )
1711 });
1712
1713 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1714}
1715
1716fn convert_user_vault_v2_position_overview(
1717 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultV2Positions,
1718) -> Option<UserVaultV2Position> {
1719 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1720
1721 UserVaultV2Position::from_gql(
1722 p.id,
1723 &p.shares,
1724 &p.assets,
1725 p.assets_usd,
1726 p.pnl.as_deref(),
1727 p.pnl_usd,
1728 p.roe,
1729 p.roe_usd,
1730 vault,
1731 )
1732}
1733
1734fn convert_user_market_position(
1735 p: get_user_account_overview::GetUserAccountOverviewUserByAddressMarketPositions,
1736) -> Option<UserMarketPosition> {
1737 let market = MarketInfo::from_gql(
1738 p.market.unique_key,
1739 Some(p.market.loan_asset.symbol),
1740 Some(p.market.loan_asset.address.as_str()),
1741 p.market.collateral_asset.as_ref().map(|c| c.symbol.clone()),
1742 p.market.collateral_asset.as_ref().map(|c| c.address.as_str()),
1743 );
1744
1745 UserMarketPosition::from_gql(
1746 p.id,
1747 &p.supply_shares,
1748 &p.supply_assets,
1749 p.supply_assets_usd,
1750 &p.borrow_shares,
1751 &p.borrow_assets,
1752 p.borrow_assets_usd,
1753 &p.collateral,
1754 p.collateral_usd,
1755 p.health_factor,
1756 market,
1757 )
1758}
1759
1760use crate::types::scalars::parse_bigint;
1763use alloy_primitives::B256;
1764use std::str::FromStr;
1765
1766fn fee_to_wad(fee: f64) -> U256 {
1768 let fee_wad = (fee * 1e18) as u128;
1769 U256::from(fee_wad)
1770}
1771
1772fn convert_vault_for_simulation(
1773 v: get_vaults_for_simulation::GetVaultsForSimulationVaultsItems,
1774) -> Option<VaultSimulationData> {
1775 let address = Address::from_str(&v.address).ok()?;
1776 let asset_decimals = v.asset.decimals as u8;
1777 let state = v.state.as_ref()?;
1778
1779 let fee = fee_to_wad(state.fee);
1780 let total_assets = parse_bigint(&state.total_assets)?;
1781 let total_supply = parse_bigint(&state.total_supply)?;
1782
1783 let mut allocations = Vec::new();
1784 let mut markets = Vec::new();
1785
1786 for alloc in &state.allocation {
1787 let market_id = B256::from_str(&alloc.market.unique_key).ok()?;
1788
1789 allocations.push(VaultAllocationForSim {
1790 market_id,
1791 supply_assets: parse_bigint(&alloc.supply_assets)?,
1792 supply_cap: parse_bigint(&alloc.supply_cap)?,
1793 enabled: alloc.enabled,
1794 supply_queue_index: alloc.supply_queue_index.map(|i| i as i32),
1795 withdraw_queue_index: alloc.withdraw_queue_index.map(|i| i as i32),
1796 });
1797
1798 if let Some(ref market_state) = alloc.market.state {
1799 let lltv = parse_bigint(&alloc.market.lltv)?;
1800 let timestamp: u64 = market_state.timestamp.0.parse().ok()?;
1801 markets.push(MarketStateForSim {
1802 id: market_id,
1803 total_supply_assets: parse_bigint(&market_state.supply_assets)?,
1804 total_borrow_assets: parse_bigint(&market_state.borrow_assets)?,
1805 total_supply_shares: parse_bigint(&market_state.supply_shares)?,
1806 total_borrow_shares: parse_bigint(&market_state.borrow_shares)?,
1807 last_update: timestamp,
1808 fee: fee_to_wad(market_state.fee),
1809 rate_at_target: market_state.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1810 price: market_state.price.as_ref().and_then(|p| parse_bigint(p)),
1811 lltv,
1812 });
1813 }
1814 }
1815
1816 Some(VaultSimulationData {
1817 address,
1818 asset_decimals,
1819 fee,
1820 total_assets,
1821 total_assets_usd: state.total_assets_usd,
1822 total_supply,
1823 allocations,
1824 markets,
1825 })
1826}
1827
1828fn convert_vault_for_simulation_single(
1829 v: get_vault_for_simulation::GetVaultForSimulationVaultByAddress,
1830) -> Option<VaultSimulationData> {
1831 let address = Address::from_str(&v.address).ok()?;
1832 let asset_decimals = v.asset.decimals as u8;
1833 let state = v.state.as_ref()?;
1834
1835 let fee = fee_to_wad(state.fee);
1836 let total_assets = parse_bigint(&state.total_assets)?;
1837 let total_supply = parse_bigint(&state.total_supply)?;
1838
1839 let mut allocations = Vec::new();
1840 let mut markets = Vec::new();
1841
1842 for alloc in &state.allocation {
1843 let market_id = B256::from_str(&alloc.market.unique_key).ok()?;
1844
1845 allocations.push(VaultAllocationForSim {
1846 market_id,
1847 supply_assets: parse_bigint(&alloc.supply_assets)?,
1848 supply_cap: parse_bigint(&alloc.supply_cap)?,
1849 enabled: alloc.enabled,
1850 supply_queue_index: alloc.supply_queue_index.map(|i| i as i32),
1851 withdraw_queue_index: alloc.withdraw_queue_index.map(|i| i as i32),
1852 });
1853
1854 if let Some(ref market_state) = alloc.market.state {
1855 let lltv = parse_bigint(&alloc.market.lltv)?;
1856 let timestamp: u64 = market_state.timestamp.0.parse().ok()?;
1857 markets.push(MarketStateForSim {
1858 id: market_id,
1859 total_supply_assets: parse_bigint(&market_state.supply_assets)?,
1860 total_borrow_assets: parse_bigint(&market_state.borrow_assets)?,
1861 total_supply_shares: parse_bigint(&market_state.supply_shares)?,
1862 total_borrow_shares: parse_bigint(&market_state.borrow_shares)?,
1863 last_update: timestamp,
1864 fee: fee_to_wad(market_state.fee),
1865 rate_at_target: market_state.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1866 price: market_state.price.as_ref().and_then(|p| parse_bigint(p)),
1867 lltv,
1868 });
1869 }
1870 }
1871
1872 Some(VaultSimulationData {
1873 address,
1874 asset_decimals,
1875 fee,
1876 total_assets,
1877 total_assets_usd: state.total_assets_usd,
1878 total_supply,
1879 allocations,
1880 markets,
1881 })
1882}