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::{
24 Asset, MarketInfo, NamedChain, UserAccountOverview, UserMarketPosition, UserState,
25 UserVaultPositions, UserVaultV1Position, UserVaultV2Position, Vault, VaultAdapter,
26 VaultAllocation, VaultAllocator, VaultInfo, VaultPositionState, VaultReward, VaultStateV1,
27 VaultV1, VaultV2, VaultV2Warning, VaultWarning, SUPPORTED_CHAINS,
28};
29
30pub const DEFAULT_API_URL: &str = "https://api.morpho.org/graphql";
32
33pub const DEFAULT_PAGE_SIZE: i64 = 100;
35
36#[derive(Debug, Clone)]
38pub struct ClientConfig {
39 pub api_url: Url,
41 pub page_size: i64,
43}
44
45impl Default for ClientConfig {
46 fn default() -> Self {
47 Self {
48 api_url: Url::parse(DEFAULT_API_URL).expect("Invalid default API URL"),
49 page_size: DEFAULT_PAGE_SIZE,
50 }
51 }
52}
53
54impl ClientConfig {
55 pub fn new() -> Self {
57 Self::default()
58 }
59
60 pub fn with_api_url(mut self, url: Url) -> Self {
62 self.api_url = url;
63 self
64 }
65
66 pub fn with_page_size(mut self, size: i64) -> Self {
68 self.page_size = size;
69 self
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct VaultV1Client {
76 http_client: Client,
77 config: ClientConfig,
78}
79
80impl Default for VaultV1Client {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl VaultV1Client {
87 pub fn new() -> Self {
89 Self {
90 http_client: Client::new(),
91 config: ClientConfig::default(),
92 }
93 }
94
95 pub fn with_config(config: ClientConfig) -> Self {
97 Self {
98 http_client: Client::new(),
99 config,
100 }
101 }
102
103 async fn execute<Q: GraphQLQuery>(
105 &self,
106 variables: Q::Variables,
107 ) -> Result<Q::ResponseData> {
108 let request_body = Q::build_query(variables);
109 let response = self
110 .http_client
111 .post(self.config.api_url.as_str())
112 .json(&request_body)
113 .send()
114 .await?;
115
116 let response_body: Response<Q::ResponseData> = response.json().await?;
117
118 if let Some(errors) = response_body.errors {
119 if !errors.is_empty() {
120 return Err(ApiError::GraphQL(
121 errors
122 .iter()
123 .map(|e| e.message.clone())
124 .collect::<Vec<_>>()
125 .join("; "),
126 ));
127 }
128 }
129
130 response_body
131 .data
132 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
133 }
134
135 pub async fn get_vaults(&self, filters: Option<VaultFiltersV1>) -> Result<Vec<VaultV1>> {
137 let variables = get_vaults_v1::Variables {
138 first: Some(self.config.page_size),
139 skip: Some(0),
140 where_: filters.map(|f| f.to_gql()),
141 order_by: Some(VaultOrderByV1::default().to_gql()),
142 order_direction: Some(OrderDirection::default().to_gql_v1()),
143 };
144
145 let data = self.execute::<GetVaultsV1>(variables).await?;
146
147 let items = match data.vaults.items {
148 Some(items) => items,
149 None => return Ok(Vec::new()),
150 };
151
152 let vaults: Vec<VaultV1> = items
153 .into_iter()
154 .filter_map(convert_v1_vault)
155 .collect();
156
157 Ok(vaults)
158 }
159
160 pub async fn get_vault(&self, address: &str, chain: NamedChain) -> Result<VaultV1> {
162 let variables = get_vault_v1_by_address::Variables {
163 address: address.to_string(),
164 chain_id: u64::from(chain) as i64,
165 };
166
167 let data = self.execute::<GetVaultV1ByAddress>(variables).await?;
168
169 convert_v1_vault_single(data.vault_by_address).ok_or_else(|| ApiError::VaultNotFound {
170 address: address.to_string(),
171 chain_id: u64::from(chain) as i64,
172 })
173 }
174
175 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<VaultV1>> {
177 let filters = VaultFiltersV1::new().chain(chain);
178 self.get_vaults(Some(filters)).await
179 }
180
181 pub async fn get_vaults_by_curator(
183 &self,
184 curator: &str,
185 chain: Option<NamedChain>,
186 ) -> Result<Vec<VaultV1>> {
187 let mut filters = VaultFiltersV1::new().curators([curator]);
188 if let Some(c) = chain {
189 filters = filters.chain(c);
190 }
191 self.get_vaults(Some(filters)).await
192 }
193
194 pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<VaultV1>> {
196 let mut filters = VaultFiltersV1::new().listed(true);
197 if let Some(c) = chain {
198 filters = filters.chain(c);
199 }
200 self.get_vaults(Some(filters)).await
201 }
202
203 pub async fn get_vaults_with_options(
231 &self,
232 options: VaultQueryOptionsV1,
233 ) -> Result<Vec<VaultV1>> {
234 let variables = get_vaults_v1::Variables {
235 first: options.limit.or(Some(self.config.page_size)),
236 skip: Some(0),
237 where_: options.filters.map(|f| f.to_gql()),
238 order_by: Some(options.order_by.unwrap_or_default().to_gql()),
239 order_direction: Some(options.order_direction.unwrap_or_default().to_gql_v1()),
240 };
241
242 let data = self.execute::<GetVaultsV1>(variables).await?;
243
244 let items = match data.vaults.items {
245 Some(items) => items,
246 None => return Ok(Vec::new()),
247 };
248
249 let vaults: Vec<VaultV1> = items
250 .into_iter()
251 .filter_map(convert_v1_vault)
252 .collect();
253
254 Ok(vaults)
255 }
256
257 pub async fn get_top_vaults_by_apy(
275 &self,
276 limit: i64,
277 filters: Option<VaultFiltersV1>,
278 ) -> Result<Vec<VaultV1>> {
279 let options = VaultQueryOptionsV1 {
280 filters,
281 order_by: Some(VaultOrderByV1::NetApy),
282 order_direction: Some(OrderDirection::Desc),
283 limit: Some(limit),
284 };
285 self.get_vaults_with_options(options).await
286 }
287
288 pub async fn get_vaults_by_asset(
305 &self,
306 asset_symbol: &str,
307 chain: Option<NamedChain>,
308 ) -> Result<Vec<VaultV1>> {
309 let mut filters = VaultFiltersV1::new().asset_symbols([asset_symbol]);
310 if let Some(c) = chain {
311 filters = filters.chain(c);
312 }
313 self.get_vaults(Some(filters)).await
314 }
315}
316
317#[derive(Debug, Clone)]
319pub struct VaultV2Client {
320 http_client: Client,
321 config: ClientConfig,
322}
323
324impl Default for VaultV2Client {
325 fn default() -> Self {
326 Self::new()
327 }
328}
329
330impl VaultV2Client {
331 pub fn new() -> Self {
333 Self {
334 http_client: Client::new(),
335 config: ClientConfig::default(),
336 }
337 }
338
339 pub fn with_config(config: ClientConfig) -> Self {
341 Self {
342 http_client: Client::new(),
343 config,
344 }
345 }
346
347 async fn execute<Q: GraphQLQuery>(
349 &self,
350 variables: Q::Variables,
351 ) -> Result<Q::ResponseData> {
352 let request_body = Q::build_query(variables);
353 let response = self
354 .http_client
355 .post(self.config.api_url.as_str())
356 .json(&request_body)
357 .send()
358 .await?;
359
360 let response_body: Response<Q::ResponseData> = response.json().await?;
361
362 if let Some(errors) = response_body.errors {
363 if !errors.is_empty() {
364 return Err(ApiError::GraphQL(
365 errors
366 .iter()
367 .map(|e| e.message.clone())
368 .collect::<Vec<_>>()
369 .join("; "),
370 ));
371 }
372 }
373
374 response_body
375 .data
376 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
377 }
378
379 pub async fn get_vaults(&self, filters: Option<VaultFiltersV2>) -> Result<Vec<VaultV2>> {
381 let variables = get_vaults_v2::Variables {
382 first: Some(self.config.page_size),
383 skip: Some(0),
384 where_: filters.map(|f| f.to_gql()),
385 order_by: Some(VaultOrderByV2::default().to_gql()),
386 order_direction: Some(OrderDirection::default().to_gql_v2()),
387 };
388
389 let data = self.execute::<GetVaultsV2>(variables).await?;
390
391 let items = match data.vault_v2s.items {
392 Some(items) => items,
393 None => return Ok(Vec::new()),
394 };
395
396 let vaults: Vec<VaultV2> = items
397 .into_iter()
398 .filter_map(convert_v2_vault)
399 .collect();
400
401 Ok(vaults)
402 }
403
404 pub async fn get_vault(&self, address: &str, chain: NamedChain) -> Result<VaultV2> {
406 let variables = get_vault_v2_by_address::Variables {
407 address: address.to_string(),
408 chain_id: u64::from(chain) as i64,
409 };
410
411 let data = self.execute::<GetVaultV2ByAddress>(variables).await?;
412
413 convert_v2_vault_single(data.vault_v2_by_address).ok_or_else(|| ApiError::VaultNotFound {
414 address: address.to_string(),
415 chain_id: u64::from(chain) as i64,
416 })
417 }
418
419 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<VaultV2>> {
421 let filters = VaultFiltersV2::new().chain(chain);
422 self.get_vaults(Some(filters)).await
423 }
424
425 pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<VaultV2>> {
427 let mut filters = VaultFiltersV2::new().listed(true);
428 if let Some(c) = chain {
429 filters = filters.chain(c);
430 }
431 self.get_vaults(Some(filters)).await
432 }
433
434 pub async fn get_vaults_with_options(
465 &self,
466 options: VaultQueryOptionsV2,
467 ) -> Result<Vec<VaultV2>> {
468 let fetch_limit = if options.has_asset_filter() {
471 options.limit.map(|l| l * 3).or(Some(self.config.page_size))
473 } else {
474 options.limit.or(Some(self.config.page_size))
475 };
476
477 let variables = get_vaults_v2::Variables {
478 first: fetch_limit,
479 skip: Some(0),
480 where_: options.filters.map(|f| f.to_gql()),
481 order_by: Some(options.order_by.unwrap_or_default().to_gql()),
482 order_direction: Some(options.order_direction.unwrap_or_default().to_gql_v2()),
483 };
484
485 let data = self.execute::<GetVaultsV2>(variables).await?;
486
487 let items = match data.vault_v2s.items {
488 Some(items) => items,
489 None => return Ok(Vec::new()),
490 };
491
492 let mut vaults: Vec<VaultV2> = items
493 .into_iter()
494 .filter_map(convert_v2_vault)
495 .collect();
496
497 if let Some(ref symbols) = options.asset_symbols {
499 vaults.retain(|v| symbols.iter().any(|s| s.eq_ignore_ascii_case(&v.asset.symbol)));
500 }
501 if let Some(ref addresses) = options.asset_addresses {
502 vaults.retain(|v| {
503 addresses
504 .iter()
505 .any(|a| v.asset.address.to_string().eq_ignore_ascii_case(a))
506 });
507 }
508
509 if let Some(limit) = options.limit {
511 vaults.truncate(limit as usize);
512 }
513
514 Ok(vaults)
515 }
516
517 pub async fn get_top_vaults_by_apy(
535 &self,
536 limit: i64,
537 filters: Option<VaultFiltersV2>,
538 ) -> Result<Vec<VaultV2>> {
539 let options = VaultQueryOptionsV2 {
540 filters,
541 order_by: Some(VaultOrderByV2::NetApy),
542 order_direction: Some(OrderDirection::Desc),
543 limit: Some(limit),
544 asset_addresses: None,
545 asset_symbols: None,
546 };
547 self.get_vaults_with_options(options).await
548 }
549
550 pub async fn get_vaults_by_asset(
570 &self,
571 asset_symbol: &str,
572 chain: Option<NamedChain>,
573 ) -> Result<Vec<VaultV2>> {
574 let filters = chain.map(|c| VaultFiltersV2::new().chain(c));
575 let options = VaultQueryOptionsV2 {
576 filters,
577 order_by: None,
578 order_direction: None,
579 limit: None,
580 asset_addresses: None,
581 asset_symbols: Some(vec![asset_symbol.to_string()]),
582 };
583 self.get_vaults_with_options(options).await
584 }
585}
586
587#[derive(Debug, Clone)]
589pub struct MorphoApiClient {
590 pub v1: VaultV1Client,
592 pub v2: VaultV2Client,
594}
595
596impl Default for MorphoApiClient {
597 fn default() -> Self {
598 Self::new()
599 }
600}
601
602impl MorphoApiClient {
603 pub fn new() -> Self {
605 Self {
606 v1: VaultV1Client::new(),
607 v2: VaultV2Client::new(),
608 }
609 }
610
611 pub fn with_config(config: ClientConfig) -> Self {
613 Self {
614 v1: VaultV1Client::with_config(config.clone()),
615 v2: VaultV2Client::with_config(config),
616 }
617 }
618
619 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<Vault>> {
621 let (v1_vaults, v2_vaults) = tokio::try_join!(
622 self.v1.get_vaults_by_chain(chain),
623 self.v2.get_vaults_by_chain(chain),
624 )?;
625
626 let mut vaults: Vec<Vault> = Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
627 vaults.extend(v1_vaults.into_iter().map(Vault::from));
628 vaults.extend(v2_vaults.into_iter().map(Vault::from));
629
630 Ok(vaults)
631 }
632
633 pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<Vault>> {
635 let (v1_vaults, v2_vaults) = tokio::try_join!(
636 self.v1.get_whitelisted_vaults(chain),
637 self.v2.get_whitelisted_vaults(chain),
638 )?;
639
640 let mut vaults: Vec<Vault> = Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
641 vaults.extend(v1_vaults.into_iter().map(Vault::from));
642 vaults.extend(v2_vaults.into_iter().map(Vault::from));
643
644 Ok(vaults)
645 }
646
647 async fn execute<Q: GraphQLQuery>(&self, variables: Q::Variables) -> Result<Q::ResponseData> {
649 let request_body = Q::build_query(variables);
650 let response = self
651 .v1
652 .http_client
653 .post(self.v1.config.api_url.as_str())
654 .json(&request_body)
655 .send()
656 .await?;
657
658 let response_body: Response<Q::ResponseData> = response.json().await?;
659
660 if let Some(errors) = response_body.errors {
661 if !errors.is_empty() {
662 return Err(ApiError::GraphQL(
663 errors
664 .iter()
665 .map(|e| e.message.clone())
666 .collect::<Vec<_>>()
667 .join("; "),
668 ));
669 }
670 }
671
672 response_body
673 .data
674 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
675 }
676
677 pub async fn get_user_vault_positions(
682 &self,
683 address: &str,
684 chain: Option<NamedChain>,
685 ) -> Result<UserVaultPositions> {
686 match chain {
687 Some(c) => self.get_user_vault_positions_single_chain(address, c).await,
688 None => self.get_user_vault_positions_all_chains(address).await,
689 }
690 }
691
692 async fn get_user_vault_positions_single_chain(
694 &self,
695 address: &str,
696 chain: NamedChain,
697 ) -> Result<UserVaultPositions> {
698 let variables = get_user_vault_positions::Variables {
699 address: address.to_string(),
700 chain_id: u64::from(chain) as i64,
701 };
702
703 let data = self.execute::<GetUserVaultPositions>(variables).await?;
704 let user = data.user_by_address;
705
706 let vault_positions: Vec<UserVaultV1Position> = user
707 .vault_positions
708 .into_iter()
709 .filter_map(convert_user_vault_v1_position)
710 .collect();
711
712 let vault_v2_positions: Vec<UserVaultV2Position> = user
713 .vault_v2_positions
714 .into_iter()
715 .filter_map(convert_user_vault_v2_position)
716 .collect();
717
718 Ok(UserVaultPositions {
719 address: user
720 .address
721 .parse()
722 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
723 vault_positions,
724 vault_v2_positions,
725 })
726 }
727
728 async fn get_user_vault_positions_all_chains(
730 &self,
731 address: &str,
732 ) -> Result<UserVaultPositions> {
733 use futures::future::join_all;
734
735 let valid_chains: Vec<_> = SUPPORTED_CHAINS
737 .iter()
738 .filter(|chain| u64::from(**chain) <= i32::MAX as u64)
739 .copied()
740 .collect();
741
742 let futures: Vec<_> = valid_chains
743 .iter()
744 .map(|chain| self.get_user_vault_positions_single_chain(address, *chain))
745 .collect();
746
747 let results = join_all(futures).await;
748
749 let parsed_address = address
750 .parse()
751 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?;
752
753 let mut all_v1_positions = Vec::new();
754 let mut all_v2_positions = Vec::new();
755
756 for result in results {
757 match result {
758 Ok(positions) => {
759 all_v1_positions.extend(positions.vault_positions);
760 all_v2_positions.extend(positions.vault_v2_positions);
761 }
762 Err(ApiError::GraphQL(msg)) if msg.contains("No results") => continue,
764 Err(e) => return Err(e),
765 }
766 }
767
768 Ok(UserVaultPositions {
769 address: parsed_address,
770 vault_positions: all_v1_positions,
771 vault_v2_positions: all_v2_positions,
772 })
773 }
774
775 pub async fn get_user_account_overview(
777 &self,
778 address: &str,
779 chain: NamedChain,
780 ) -> Result<UserAccountOverview> {
781 let variables = get_user_account_overview::Variables {
782 address: address.to_string(),
783 chain_id: u64::from(chain) as i64,
784 };
785
786 let data = self.execute::<GetUserAccountOverview>(variables).await?;
787 let user = data.user_by_address;
788
789 let state = UserState::from_gql(
790 user.state.vaults_pnl_usd,
791 user.state.vaults_roe_usd,
792 user.state.vaults_assets_usd,
793 user.state.vault_v2s_pnl_usd,
794 user.state.vault_v2s_roe_usd,
795 user.state.vault_v2s_assets_usd,
796 user.state.markets_pnl_usd,
797 user.state.markets_roe_usd,
798 user.state.markets_supply_pnl_usd,
799 user.state.markets_supply_roe_usd,
800 user.state.markets_borrow_pnl_usd,
801 user.state.markets_borrow_roe_usd,
802 user.state.markets_collateral_pnl_usd,
803 user.state.markets_collateral_roe_usd,
804 user.state.markets_margin_pnl_usd,
805 user.state.markets_margin_roe_usd,
806 user.state.markets_collateral_usd,
807 user.state.markets_supply_assets_usd,
808 user.state.markets_borrow_assets_usd,
809 user.state.markets_margin_usd,
810 );
811
812 let vault_positions: Vec<UserVaultV1Position> = user
813 .vault_positions
814 .into_iter()
815 .filter_map(convert_user_vault_v1_position_overview)
816 .collect();
817
818 let vault_v2_positions: Vec<UserVaultV2Position> = user
819 .vault_v2_positions
820 .into_iter()
821 .filter_map(convert_user_vault_v2_position_overview)
822 .collect();
823
824 let market_positions: Vec<UserMarketPosition> = user
825 .market_positions
826 .into_iter()
827 .filter_map(convert_user_market_position)
828 .collect();
829
830 Ok(UserAccountOverview {
831 address: user
832 .address
833 .parse()
834 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
835 state,
836 vault_positions,
837 vault_v2_positions,
838 market_positions,
839 })
840 }
841}
842
843#[derive(Debug, Clone)]
845pub struct MorphoClientConfig {
846 pub api_config: Option<ClientConfig>,
848 pub rpc_url: Option<String>,
850 pub private_key: Option<String>,
852 pub auto_approve: bool,
856}
857
858impl Default for MorphoClientConfig {
859 fn default() -> Self {
860 Self {
861 api_config: None,
862 rpc_url: None,
863 private_key: None,
864 auto_approve: true,
865 }
866 }
867}
868
869impl MorphoClientConfig {
870 pub fn new() -> Self {
872 Self::default()
873 }
874
875 pub fn with_api_config(mut self, config: ClientConfig) -> Self {
877 self.api_config = Some(config);
878 self
879 }
880
881 pub fn with_rpc_url(mut self, rpc_url: impl Into<String>) -> Self {
883 self.rpc_url = Some(rpc_url.into());
884 self
885 }
886
887 pub fn with_private_key(mut self, private_key: impl Into<String>) -> Self {
889 self.private_key = Some(private_key.into());
890 self
891 }
892
893 pub fn with_auto_approve(mut self, auto_approve: bool) -> Self {
897 self.auto_approve = auto_approve;
898 self
899 }
900}
901
902pub struct VaultV1Operations<'a> {
904 client: &'a VaultV1TransactionClient,
905 auto_approve: bool,
906}
907
908impl<'a> VaultV1Operations<'a> {
909 fn new(client: &'a VaultV1TransactionClient, auto_approve: bool) -> Self {
911 Self { client, auto_approve }
912 }
913
914 pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
919 if self.auto_approve {
920 let asset = self.client.get_asset(vault).await?;
921 let current_allowance = self
922 .client
923 .get_allowance(asset, self.client.signer_address(), vault)
924 .await?;
925 if current_allowance < amount {
926 let needed = amount - current_allowance;
927 self.client.approve_if_needed(asset, vault, needed).await?;
928 }
929 }
930
931 let receipt = self
932 .client
933 .deposit(vault, amount, self.client.signer_address())
934 .await?;
935 Ok(receipt)
936 }
937
938 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
940 let signer = self.client.signer_address();
941 let receipt = self.client.withdraw(vault, amount, signer, signer).await?;
942 Ok(receipt)
943 }
944
945 pub async fn balance(&self, vault: Address) -> Result<U256> {
947 let balance = self
948 .client
949 .get_balance(vault, self.client.signer_address())
950 .await?;
951 Ok(balance)
952 }
953
954 pub async fn approve(
957 &self,
958 vault: Address,
959 amount: U256,
960 ) -> Result<Option<TransactionReceipt>> {
961 let asset = self.client.get_asset(vault).await?;
962 let receipt = self.client.approve_if_needed(asset, vault, amount).await?;
963 Ok(receipt)
964 }
965
966 pub async fn get_allowance(&self, vault: Address) -> Result<U256> {
968 let asset = self.client.get_asset(vault).await?;
969 let allowance = self
970 .client
971 .get_allowance(asset, self.client.signer_address(), vault)
972 .await?;
973 Ok(allowance)
974 }
975
976 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
978 let asset = self.client.get_asset(vault).await?;
979 Ok(asset)
980 }
981
982 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
984 let decimals = self.client.get_decimals(token).await?;
985 Ok(decimals)
986 }
987
988 pub fn signer_address(&self) -> Address {
990 self.client.signer_address()
991 }
992
993 pub fn auto_approve(&self) -> bool {
995 self.auto_approve
996 }
997}
998
999pub struct VaultV2Operations<'a> {
1001 client: &'a VaultV2TransactionClient,
1002 auto_approve: bool,
1003}
1004
1005impl<'a> VaultV2Operations<'a> {
1006 fn new(client: &'a VaultV2TransactionClient, auto_approve: bool) -> Self {
1008 Self { client, auto_approve }
1009 }
1010
1011 pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1016 if self.auto_approve {
1017 let asset = self.client.get_asset(vault).await?;
1018 let current_allowance = self
1019 .client
1020 .get_allowance(asset, self.client.signer_address(), vault)
1021 .await?;
1022 if current_allowance < amount {
1023 let needed = amount - current_allowance;
1024 self.client.approve_if_needed(asset, vault, needed).await?;
1025 }
1026 }
1027
1028 let receipt = self
1029 .client
1030 .deposit(vault, amount, self.client.signer_address())
1031 .await?;
1032 Ok(receipt)
1033 }
1034
1035 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1037 let signer = self.client.signer_address();
1038 let receipt = self.client.withdraw(vault, amount, signer, signer).await?;
1039 Ok(receipt)
1040 }
1041
1042 pub async fn balance(&self, vault: Address) -> Result<U256> {
1044 let balance = self
1045 .client
1046 .get_balance(vault, self.client.signer_address())
1047 .await?;
1048 Ok(balance)
1049 }
1050
1051 pub async fn approve(
1054 &self,
1055 vault: Address,
1056 amount: U256,
1057 ) -> Result<Option<TransactionReceipt>> {
1058 let asset = self.client.get_asset(vault).await?;
1059 let receipt = self.client.approve_if_needed(asset, vault, amount).await?;
1060 Ok(receipt)
1061 }
1062
1063 pub async fn get_allowance(&self, vault: Address) -> Result<U256> {
1065 let asset = self.client.get_asset(vault).await?;
1066 let allowance = self
1067 .client
1068 .get_allowance(asset, self.client.signer_address(), vault)
1069 .await?;
1070 Ok(allowance)
1071 }
1072
1073 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
1075 let asset = self.client.get_asset(vault).await?;
1076 Ok(asset)
1077 }
1078
1079 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
1081 let decimals = self.client.get_decimals(token).await?;
1082 Ok(decimals)
1083 }
1084
1085 pub fn signer_address(&self) -> Address {
1087 self.client.signer_address()
1088 }
1089
1090 pub fn auto_approve(&self) -> bool {
1092 self.auto_approve
1093 }
1094}
1095
1096pub struct MorphoClient {
1129 api: MorphoApiClient,
1130 vault_v1_tx: Option<VaultV1TransactionClient>,
1131 vault_v2_tx: Option<VaultV2TransactionClient>,
1132 auto_approve: bool,
1133}
1134
1135impl Default for MorphoClient {
1136 fn default() -> Self {
1137 Self::new()
1138 }
1139}
1140
1141impl MorphoClient {
1142 pub fn new() -> Self {
1144 Self {
1145 api: MorphoApiClient::new(),
1146 vault_v1_tx: None,
1147 vault_v2_tx: None,
1148 auto_approve: true,
1149 }
1150 }
1151
1152 pub fn with_config(config: MorphoClientConfig) -> Result<Self> {
1156 let api = match config.api_config {
1157 Some(api_config) => MorphoApiClient::with_config(api_config),
1158 None => MorphoApiClient::new(),
1159 };
1160
1161 let (vault_v1_tx, vault_v2_tx) = match (&config.rpc_url, &config.private_key) {
1162 (Some(rpc_url), Some(private_key)) => {
1163 let v1 = VaultV1TransactionClient::new(rpc_url, private_key)?;
1164 let v2 = VaultV2TransactionClient::new(rpc_url, private_key)?;
1165 (Some(v1), Some(v2))
1166 }
1167 _ => (None, None),
1168 };
1169
1170 Ok(Self {
1171 api,
1172 vault_v1_tx,
1173 vault_v2_tx,
1174 auto_approve: config.auto_approve,
1175 })
1176 }
1177
1178 pub fn vault_v1(&self) -> Result<VaultV1Operations<'_>> {
1182 match &self.vault_v1_tx {
1183 Some(client) => Ok(VaultV1Operations::new(client, self.auto_approve)),
1184 None => Err(ApiError::TransactionNotConfigured),
1185 }
1186 }
1187
1188 pub fn vault_v2(&self) -> Result<VaultV2Operations<'_>> {
1192 match &self.vault_v2_tx {
1193 Some(client) => Ok(VaultV2Operations::new(client, self.auto_approve)),
1194 None => Err(ApiError::TransactionNotConfigured),
1195 }
1196 }
1197
1198 pub fn auto_approve(&self) -> bool {
1200 self.auto_approve
1201 }
1202
1203 pub fn api(&self) -> &MorphoApiClient {
1205 &self.api
1206 }
1207
1208 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<Vault>> {
1210 self.api.get_vaults_by_chain(chain).await
1211 }
1212
1213 pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<Vault>> {
1215 self.api.get_whitelisted_vaults(chain).await
1216 }
1217
1218 pub async fn get_user_vault_positions(
1220 &self,
1221 address: &str,
1222 chain: Option<NamedChain>,
1223 ) -> Result<UserVaultPositions> {
1224 self.api.get_user_vault_positions(address, chain).await
1225 }
1226
1227 pub async fn get_user_account_overview(
1229 &self,
1230 address: &str,
1231 chain: NamedChain,
1232 ) -> Result<UserAccountOverview> {
1233 self.api.get_user_account_overview(address, chain).await
1234 }
1235
1236 pub fn has_transaction_support(&self) -> bool {
1238 self.vault_v1_tx.is_some()
1239 }
1240
1241 pub fn signer_address(&self) -> Option<Address> {
1243 self.vault_v1_tx.as_ref().map(|c| c.signer_address())
1244 }
1245}
1246
1247fn convert_v1_vault(v: get_vaults_v1::GetVaultsV1VaultsItems) -> Option<VaultV1> {
1250 let chain_id = v.chain.id;
1251 let asset = &v.asset;
1252
1253 VaultV1::from_gql(
1254 &v.address,
1255 v.name,
1256 v.symbol,
1257 chain_id,
1258 v.listed,
1259 v.featured,
1260 v.whitelisted,
1261 Asset::from_gql(
1262 &asset.address,
1263 asset.symbol.clone(),
1264 Some(asset.name.clone()),
1265 asset.decimals,
1266 asset.price_usd,
1267 )?,
1268 v.state.as_ref().and_then(convert_v1_state),
1269 v.allocators
1270 .into_iter()
1271 .filter_map(|a| VaultAllocator::from_gql(&a.address))
1272 .collect(),
1273 v.warnings
1274 .into_iter()
1275 .map(|w| VaultWarning {
1276 warning_type: w.type_.clone(),
1277 level: format!("{:?}", w.level),
1278 })
1279 .collect(),
1280 )
1281}
1282
1283fn convert_v1_vault_single(
1284 v: get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddress,
1285) -> Option<VaultV1> {
1286 let chain_id = v.chain.id;
1287 let asset = &v.asset;
1288
1289 VaultV1::from_gql(
1290 &v.address,
1291 v.name,
1292 v.symbol,
1293 chain_id,
1294 v.listed,
1295 v.featured,
1296 v.whitelisted,
1297 Asset::from_gql(
1298 &asset.address,
1299 asset.symbol.clone(),
1300 Some(asset.name.clone()),
1301 asset.decimals,
1302 asset.price_usd,
1303 )?,
1304 v.state.as_ref().and_then(convert_v1_state_single),
1305 v.allocators
1306 .into_iter()
1307 .filter_map(|a| VaultAllocator::from_gql(&a.address))
1308 .collect(),
1309 v.warnings
1310 .into_iter()
1311 .map(|w| VaultWarning {
1312 warning_type: w.type_.clone(),
1313 level: format!("{:?}", w.level),
1314 })
1315 .collect(),
1316 )
1317}
1318
1319fn convert_v1_state(s: &get_vaults_v1::GetVaultsV1VaultsItemsState) -> Option<VaultStateV1> {
1320 VaultStateV1::from_gql(
1321 Some(s.curator.as_str()),
1322 Some(s.owner.as_str()),
1323 Some(s.guardian.as_str()),
1324 &s.total_assets,
1325 s.total_assets_usd,
1326 &s.total_supply,
1327 s.fee,
1328 &s.timelock,
1329 s.apy,
1330 s.net_apy,
1331 s.share_price.as_deref().unwrap_or("0"),
1332 s.allocation
1333 .iter()
1334 .filter_map(|a| {
1335 let market = &a.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 )
1346 })
1347 .collect(),
1348 )
1349}
1350
1351fn convert_v1_state_single(
1352 s: &get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddressState,
1353) -> Option<VaultStateV1> {
1354 VaultStateV1::from_gql(
1355 Some(s.curator.as_str()),
1356 Some(s.owner.as_str()),
1357 Some(s.guardian.as_str()),
1358 &s.total_assets,
1359 s.total_assets_usd,
1360 &s.total_supply,
1361 s.fee,
1362 &s.timelock,
1363 s.apy,
1364 s.net_apy,
1365 s.share_price.as_deref().unwrap_or("0"),
1366 s.allocation
1367 .iter()
1368 .filter_map(|a| {
1369 let market = &a.market;
1370 VaultAllocation::from_gql(
1371 market.unique_key.clone(),
1372 Some(market.loan_asset.symbol.clone()),
1373 Some(market.loan_asset.address.as_str()),
1374 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
1375 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
1376 &a.supply_assets,
1377 a.supply_assets_usd,
1378 &a.supply_cap,
1379 )
1380 })
1381 .collect(),
1382 )
1383}
1384
1385fn convert_v2_vault(v: get_vaults_v2::GetVaultsV2VaultV2sItems) -> Option<VaultV2> {
1386 let chain_id = v.chain.id;
1387 let asset = &v.asset;
1388
1389 VaultV2::from_gql(
1390 &v.address,
1391 v.name,
1392 v.symbol,
1393 chain_id,
1394 v.listed,
1395 v.whitelisted,
1396 Asset::from_gql(
1397 &asset.address,
1398 asset.symbol.clone(),
1399 Some(asset.name.clone()),
1400 asset.decimals,
1401 asset.price_usd,
1402 )?,
1403 Some(v.curator.address.as_str()),
1404 Some(v.owner.address.as_str()),
1405 v.total_assets.as_deref().unwrap_or("0"),
1406 v.total_assets_usd,
1407 &v.total_supply,
1408 Some(v.share_price),
1409 Some(v.performance_fee),
1410 Some(v.management_fee),
1411 v.avg_apy,
1412 v.avg_net_apy,
1413 v.apy,
1414 v.net_apy,
1415 &v.liquidity,
1416 v.liquidity_usd,
1417 v.adapters
1418 .items
1419 .map(|items| {
1420 items
1421 .into_iter()
1422 .filter_map(convert_v2_adapter)
1423 .collect()
1424 })
1425 .unwrap_or_default(),
1426 v.rewards
1427 .into_iter()
1428 .filter_map(|r| {
1429 VaultReward::from_gql(
1430 &r.asset.address,
1431 r.asset.symbol.clone(),
1432 r.supply_apr,
1433 parse_yearly_supply(&r.yearly_supply_tokens),
1434 )
1435 })
1436 .collect(),
1437 v.warnings
1438 .into_iter()
1439 .map(|w| VaultV2Warning {
1440 warning_type: w.type_.clone(),
1441 level: format!("{:?}", w.level),
1442 })
1443 .collect(),
1444 )
1445}
1446
1447fn parse_yearly_supply(s: &str) -> Option<f64> {
1449 s.parse::<f64>().ok()
1450}
1451
1452fn convert_v2_vault_single(
1453 v: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddress,
1454) -> Option<VaultV2> {
1455 let chain_id = v.chain.id;
1456 let asset = &v.asset;
1457
1458 VaultV2::from_gql(
1459 &v.address,
1460 v.name,
1461 v.symbol,
1462 chain_id,
1463 v.listed,
1464 v.whitelisted,
1465 Asset::from_gql(
1466 &asset.address,
1467 asset.symbol.clone(),
1468 Some(asset.name.clone()),
1469 asset.decimals,
1470 asset.price_usd,
1471 )?,
1472 Some(v.curator.address.as_str()),
1473 Some(v.owner.address.as_str()),
1474 v.total_assets.as_deref().unwrap_or("0"),
1475 v.total_assets_usd,
1476 &v.total_supply,
1477 Some(v.share_price),
1478 Some(v.performance_fee),
1479 Some(v.management_fee),
1480 v.avg_apy,
1481 v.avg_net_apy,
1482 v.apy,
1483 v.net_apy,
1484 &v.liquidity,
1485 v.liquidity_usd,
1486 v.adapters
1487 .items
1488 .map(|items| {
1489 items
1490 .into_iter()
1491 .filter_map(convert_v2_adapter_single)
1492 .collect()
1493 })
1494 .unwrap_or_default(),
1495 v.rewards
1496 .into_iter()
1497 .filter_map(|r| {
1498 VaultReward::from_gql(
1499 &r.asset.address,
1500 r.asset.symbol.clone(),
1501 r.supply_apr,
1502 parse_yearly_supply(&r.yearly_supply_tokens),
1503 )
1504 })
1505 .collect(),
1506 v.warnings
1507 .into_iter()
1508 .map(|w| VaultV2Warning {
1509 warning_type: w.type_.clone(),
1510 level: format!("{:?}", w.level),
1511 })
1512 .collect(),
1513 )
1514}
1515
1516fn convert_v2_adapter(
1517 a: get_vaults_v2::GetVaultsV2VaultV2sItemsAdaptersItems,
1518) -> Option<VaultAdapter> {
1519 VaultAdapter::from_gql(
1520 a.id,
1521 &a.address,
1522 format!("{:?}", a.type_),
1523 &a.assets,
1524 a.assets_usd,
1525 )
1526}
1527
1528fn convert_v2_adapter_single(
1529 a: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddressAdaptersItems,
1530) -> Option<VaultAdapter> {
1531 VaultAdapter::from_gql(
1532 a.id,
1533 &a.address,
1534 format!("{:?}", a.type_),
1535 &a.assets,
1536 a.assets_usd,
1537 )
1538}
1539
1540fn convert_user_vault_v1_position(
1543 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultPositions,
1544) -> Option<UserVaultV1Position> {
1545 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1546
1547 let state = p.state.as_ref().and_then(|s| {
1548 VaultPositionState::from_gql(
1549 &s.shares,
1550 s.assets.as_deref(),
1551 s.assets_usd,
1552 s.pnl.as_deref(),
1553 s.pnl_usd,
1554 s.roe,
1555 s.roe_usd,
1556 )
1557 });
1558
1559 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1560}
1561
1562fn convert_user_vault_v2_position(
1563 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultV2Positions,
1564) -> Option<UserVaultV2Position> {
1565 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1566
1567 UserVaultV2Position::from_gql(
1568 p.id,
1569 &p.shares,
1570 &p.assets,
1571 p.assets_usd,
1572 p.pnl.as_deref(),
1573 p.pnl_usd,
1574 p.roe,
1575 p.roe_usd,
1576 vault,
1577 )
1578}
1579
1580fn convert_user_vault_v1_position_overview(
1581 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultPositions,
1582) -> Option<UserVaultV1Position> {
1583 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1584
1585 let state = p.state.as_ref().and_then(|s| {
1586 VaultPositionState::from_gql(
1587 &s.shares,
1588 s.assets.as_deref(),
1589 s.assets_usd,
1590 s.pnl.as_deref(),
1591 s.pnl_usd,
1592 s.roe,
1593 s.roe_usd,
1594 )
1595 });
1596
1597 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1598}
1599
1600fn convert_user_vault_v2_position_overview(
1601 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultV2Positions,
1602) -> Option<UserVaultV2Position> {
1603 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1604
1605 UserVaultV2Position::from_gql(
1606 p.id,
1607 &p.shares,
1608 &p.assets,
1609 p.assets_usd,
1610 p.pnl.as_deref(),
1611 p.pnl_usd,
1612 p.roe,
1613 p.roe_usd,
1614 vault,
1615 )
1616}
1617
1618fn convert_user_market_position(
1619 p: get_user_account_overview::GetUserAccountOverviewUserByAddressMarketPositions,
1620) -> Option<UserMarketPosition> {
1621 let market = MarketInfo::from_gql(
1622 p.market.unique_key,
1623 Some(p.market.loan_asset.symbol),
1624 Some(p.market.loan_asset.address.as_str()),
1625 p.market.collateral_asset.as_ref().map(|c| c.symbol.clone()),
1626 p.market.collateral_asset.as_ref().map(|c| c.address.as_str()),
1627 );
1628
1629 UserMarketPosition::from_gql(
1630 p.id,
1631 &p.supply_shares,
1632 &p.supply_assets,
1633 p.supply_assets_usd,
1634 &p.borrow_shares,
1635 &p.borrow_assets,
1636 p.borrow_assets_usd,
1637 &p.collateral,
1638 p.collateral_usd,
1639 p.health_factor,
1640 market,
1641 )
1642}