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, Chain, MarketInfo, UserAccountOverview, UserMarketPosition, UserState,
25 UserVaultPositions, UserVaultV1Position, UserVaultV2Position, Vault, VaultAdapter,
26 VaultAllocation, VaultAllocator, VaultInfo, VaultPositionState, VaultReward, VaultStateV1,
27 VaultV1, VaultV2, VaultV2Warning, VaultWarning,
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: Chain) -> Result<VaultV1> {
162 let variables = get_vault_v1_by_address::Variables {
163 address: address.to_string(),
164 chain_id: chain.id() 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: chain.id(),
172 })
173 }
174
175 pub async fn get_vaults_by_chain(&self, chain: Chain) -> 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<Chain>,
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<Chain>) -> 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<Chain>,
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: Chain) -> Result<VaultV2> {
406 let variables = get_vault_v2_by_address::Variables {
407 address: address.to_string(),
408 chain_id: chain.id() 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: chain.id(),
416 })
417 }
418
419 pub async fn get_vaults_by_chain(&self, chain: Chain) -> 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<Chain>) -> 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<Chain>,
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: Chain) -> 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<Chain>) -> 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<Chain>,
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: Chain,
697 ) -> Result<UserVaultPositions> {
698 let variables = get_user_vault_positions::Variables {
699 address: address.to_string(),
700 chain_id: chain.id(),
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<_> = Chain::all()
737 .iter()
738 .filter(|chain| chain.id() <= i32::MAX as i64)
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: Chain,
780 ) -> Result<UserAccountOverview> {
781 let variables = get_user_account_overview::Variables {
782 address: address.to_string(),
783 chain_id: chain.id(),
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, Default)]
845pub struct MorphoClientConfig {
846 pub api_config: Option<ClientConfig>,
848 pub rpc_url: Option<String>,
850 pub private_key: Option<String>,
852}
853
854impl MorphoClientConfig {
855 pub fn new() -> Self {
857 Self::default()
858 }
859
860 pub fn with_api_config(mut self, config: ClientConfig) -> Self {
862 self.api_config = Some(config);
863 self
864 }
865
866 pub fn with_rpc_url(mut self, rpc_url: impl Into<String>) -> Self {
868 self.rpc_url = Some(rpc_url.into());
869 self
870 }
871
872 pub fn with_private_key(mut self, private_key: impl Into<String>) -> Self {
874 self.private_key = Some(private_key.into());
875 self
876 }
877}
878
879pub struct VaultV1Operations<'a> {
881 client: &'a VaultV1TransactionClient,
882}
883
884impl<'a> VaultV1Operations<'a> {
885 fn new(client: &'a VaultV1TransactionClient) -> Self {
887 Self { client }
888 }
889
890 pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
892 let receipt = self
893 .client
894 .deposit(vault, amount, self.client.signer_address())
895 .await?;
896 Ok(receipt)
897 }
898
899 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
901 let signer = self.client.signer_address();
902 let receipt = self.client.withdraw(vault, amount, signer, signer).await?;
903 Ok(receipt)
904 }
905
906 pub async fn balance(&self, vault: Address) -> Result<U256> {
908 let balance = self
909 .client
910 .get_balance(vault, self.client.signer_address())
911 .await?;
912 Ok(balance)
913 }
914
915 pub async fn approve(
918 &self,
919 vault: Address,
920 amount: U256,
921 ) -> Result<Option<TransactionReceipt>> {
922 let asset = self.client.get_asset(vault).await?;
923 let receipt = self.client.approve_if_needed(asset, vault, amount).await?;
924 Ok(receipt)
925 }
926
927 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
929 let asset = self.client.get_asset(vault).await?;
930 Ok(asset)
931 }
932
933 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
935 let decimals = self.client.get_decimals(token).await?;
936 Ok(decimals)
937 }
938
939 pub fn signer_address(&self) -> Address {
941 self.client.signer_address()
942 }
943}
944
945pub struct VaultV2Operations<'a> {
947 client: &'a VaultV2TransactionClient,
948}
949
950impl<'a> VaultV2Operations<'a> {
951 fn new(client: &'a VaultV2TransactionClient) -> Self {
953 Self { client }
954 }
955
956 pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
958 let receipt = self
959 .client
960 .deposit(vault, amount, self.client.signer_address())
961 .await?;
962 Ok(receipt)
963 }
964
965 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
967 let signer = self.client.signer_address();
968 let receipt = self.client.withdraw(vault, amount, signer, signer).await?;
969 Ok(receipt)
970 }
971
972 pub async fn balance(&self, vault: Address) -> Result<U256> {
974 let balance = self
975 .client
976 .get_balance(vault, self.client.signer_address())
977 .await?;
978 Ok(balance)
979 }
980
981 pub async fn approve(
984 &self,
985 vault: Address,
986 amount: U256,
987 ) -> Result<Option<TransactionReceipt>> {
988 let asset = self.client.get_asset(vault).await?;
989 let receipt = self.client.approve_if_needed(asset, vault, amount).await?;
990 Ok(receipt)
991 }
992
993 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
995 let asset = self.client.get_asset(vault).await?;
996 Ok(asset)
997 }
998
999 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
1001 let decimals = self.client.get_decimals(token).await?;
1002 Ok(decimals)
1003 }
1004
1005 pub fn signer_address(&self) -> Address {
1007 self.client.signer_address()
1008 }
1009}
1010
1011pub struct MorphoClient {
1044 api: MorphoApiClient,
1045 vault_v1_tx: Option<VaultV1TransactionClient>,
1046 vault_v2_tx: Option<VaultV2TransactionClient>,
1047}
1048
1049impl Default for MorphoClient {
1050 fn default() -> Self {
1051 Self::new()
1052 }
1053}
1054
1055impl MorphoClient {
1056 pub fn new() -> Self {
1058 Self {
1059 api: MorphoApiClient::new(),
1060 vault_v1_tx: None,
1061 vault_v2_tx: None,
1062 }
1063 }
1064
1065 pub fn with_config(config: MorphoClientConfig) -> Result<Self> {
1069 let api = match config.api_config {
1070 Some(api_config) => MorphoApiClient::with_config(api_config),
1071 None => MorphoApiClient::new(),
1072 };
1073
1074 let (vault_v1_tx, vault_v2_tx) = match (&config.rpc_url, &config.private_key) {
1075 (Some(rpc_url), Some(private_key)) => {
1076 let v1 = VaultV1TransactionClient::new(rpc_url, private_key)?;
1077 let v2 = VaultV2TransactionClient::new(rpc_url, private_key)?;
1078 (Some(v1), Some(v2))
1079 }
1080 _ => (None, None),
1081 };
1082
1083 Ok(Self {
1084 api,
1085 vault_v1_tx,
1086 vault_v2_tx,
1087 })
1088 }
1089
1090 pub fn vault_v1(&self) -> Result<VaultV1Operations<'_>> {
1094 match &self.vault_v1_tx {
1095 Some(client) => Ok(VaultV1Operations::new(client)),
1096 None => Err(ApiError::TransactionNotConfigured),
1097 }
1098 }
1099
1100 pub fn vault_v2(&self) -> Result<VaultV2Operations<'_>> {
1104 match &self.vault_v2_tx {
1105 Some(client) => Ok(VaultV2Operations::new(client)),
1106 None => Err(ApiError::TransactionNotConfigured),
1107 }
1108 }
1109
1110 pub fn api(&self) -> &MorphoApiClient {
1112 &self.api
1113 }
1114
1115 pub async fn get_vaults_by_chain(&self, chain: Chain) -> Result<Vec<Vault>> {
1117 self.api.get_vaults_by_chain(chain).await
1118 }
1119
1120 pub async fn get_whitelisted_vaults(&self, chain: Option<Chain>) -> Result<Vec<Vault>> {
1122 self.api.get_whitelisted_vaults(chain).await
1123 }
1124
1125 pub async fn get_user_vault_positions(
1127 &self,
1128 address: &str,
1129 chain: Option<Chain>,
1130 ) -> Result<UserVaultPositions> {
1131 self.api.get_user_vault_positions(address, chain).await
1132 }
1133
1134 pub async fn get_user_account_overview(
1136 &self,
1137 address: &str,
1138 chain: Chain,
1139 ) -> Result<UserAccountOverview> {
1140 self.api.get_user_account_overview(address, chain).await
1141 }
1142
1143 pub fn has_transaction_support(&self) -> bool {
1145 self.vault_v1_tx.is_some()
1146 }
1147
1148 pub fn signer_address(&self) -> Option<Address> {
1150 self.vault_v1_tx.as_ref().map(|c| c.signer_address())
1151 }
1152}
1153
1154fn convert_v1_vault(v: get_vaults_v1::GetVaultsV1VaultsItems) -> Option<VaultV1> {
1157 let chain_id = v.chain.id;
1158 let asset = &v.asset;
1159
1160 VaultV1::from_gql(
1161 &v.address,
1162 v.name,
1163 v.symbol,
1164 chain_id,
1165 v.listed,
1166 v.featured,
1167 v.whitelisted,
1168 Asset::from_gql(
1169 &asset.address,
1170 asset.symbol.clone(),
1171 Some(asset.name.clone()),
1172 asset.decimals,
1173 asset.price_usd,
1174 )?,
1175 v.state.as_ref().and_then(convert_v1_state),
1176 v.allocators
1177 .into_iter()
1178 .filter_map(|a| VaultAllocator::from_gql(&a.address))
1179 .collect(),
1180 v.warnings
1181 .into_iter()
1182 .map(|w| VaultWarning {
1183 warning_type: w.type_.clone(),
1184 level: format!("{:?}", w.level),
1185 })
1186 .collect(),
1187 )
1188}
1189
1190fn convert_v1_vault_single(
1191 v: get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddress,
1192) -> Option<VaultV1> {
1193 let chain_id = v.chain.id;
1194 let asset = &v.asset;
1195
1196 VaultV1::from_gql(
1197 &v.address,
1198 v.name,
1199 v.symbol,
1200 chain_id,
1201 v.listed,
1202 v.featured,
1203 v.whitelisted,
1204 Asset::from_gql(
1205 &asset.address,
1206 asset.symbol.clone(),
1207 Some(asset.name.clone()),
1208 asset.decimals,
1209 asset.price_usd,
1210 )?,
1211 v.state.as_ref().and_then(convert_v1_state_single),
1212 v.allocators
1213 .into_iter()
1214 .filter_map(|a| VaultAllocator::from_gql(&a.address))
1215 .collect(),
1216 v.warnings
1217 .into_iter()
1218 .map(|w| VaultWarning {
1219 warning_type: w.type_.clone(),
1220 level: format!("{:?}", w.level),
1221 })
1222 .collect(),
1223 )
1224}
1225
1226fn convert_v1_state(s: &get_vaults_v1::GetVaultsV1VaultsItemsState) -> Option<VaultStateV1> {
1227 VaultStateV1::from_gql(
1228 Some(s.curator.as_str()),
1229 Some(s.owner.as_str()),
1230 Some(s.guardian.as_str()),
1231 &s.total_assets,
1232 s.total_assets_usd,
1233 &s.total_supply,
1234 s.fee,
1235 &s.timelock,
1236 s.apy,
1237 s.net_apy,
1238 s.share_price.as_deref().unwrap_or("0"),
1239 s.allocation
1240 .iter()
1241 .filter_map(|a| {
1242 let market = &a.market;
1243 VaultAllocation::from_gql(
1244 market.unique_key.clone(),
1245 Some(market.loan_asset.symbol.clone()),
1246 Some(market.loan_asset.address.as_str()),
1247 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
1248 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
1249 &a.supply_assets,
1250 a.supply_assets_usd,
1251 &a.supply_cap,
1252 )
1253 })
1254 .collect(),
1255 )
1256}
1257
1258fn convert_v1_state_single(
1259 s: &get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddressState,
1260) -> Option<VaultStateV1> {
1261 VaultStateV1::from_gql(
1262 Some(s.curator.as_str()),
1263 Some(s.owner.as_str()),
1264 Some(s.guardian.as_str()),
1265 &s.total_assets,
1266 s.total_assets_usd,
1267 &s.total_supply,
1268 s.fee,
1269 &s.timelock,
1270 s.apy,
1271 s.net_apy,
1272 s.share_price.as_deref().unwrap_or("0"),
1273 s.allocation
1274 .iter()
1275 .filter_map(|a| {
1276 let market = &a.market;
1277 VaultAllocation::from_gql(
1278 market.unique_key.clone(),
1279 Some(market.loan_asset.symbol.clone()),
1280 Some(market.loan_asset.address.as_str()),
1281 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
1282 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
1283 &a.supply_assets,
1284 a.supply_assets_usd,
1285 &a.supply_cap,
1286 )
1287 })
1288 .collect(),
1289 )
1290}
1291
1292fn convert_v2_vault(v: get_vaults_v2::GetVaultsV2VaultV2sItems) -> Option<VaultV2> {
1293 let chain_id = v.chain.id;
1294 let asset = &v.asset;
1295
1296 VaultV2::from_gql(
1297 &v.address,
1298 v.name,
1299 v.symbol,
1300 chain_id,
1301 v.listed,
1302 v.whitelisted,
1303 Asset::from_gql(
1304 &asset.address,
1305 asset.symbol.clone(),
1306 Some(asset.name.clone()),
1307 asset.decimals,
1308 asset.price_usd,
1309 )?,
1310 Some(v.curator.address.as_str()),
1311 Some(v.owner.address.as_str()),
1312 v.total_assets.as_deref().unwrap_or("0"),
1313 v.total_assets_usd,
1314 &v.total_supply,
1315 Some(v.share_price),
1316 Some(v.performance_fee),
1317 Some(v.management_fee),
1318 v.avg_apy,
1319 v.avg_net_apy,
1320 v.apy,
1321 v.net_apy,
1322 &v.liquidity,
1323 v.liquidity_usd,
1324 v.adapters
1325 .items
1326 .map(|items| {
1327 items
1328 .into_iter()
1329 .filter_map(convert_v2_adapter)
1330 .collect()
1331 })
1332 .unwrap_or_default(),
1333 v.rewards
1334 .into_iter()
1335 .filter_map(|r| {
1336 VaultReward::from_gql(
1337 &r.asset.address,
1338 r.asset.symbol.clone(),
1339 r.supply_apr,
1340 parse_yearly_supply(&r.yearly_supply_tokens),
1341 )
1342 })
1343 .collect(),
1344 v.warnings
1345 .into_iter()
1346 .map(|w| VaultV2Warning {
1347 warning_type: w.type_.clone(),
1348 level: format!("{:?}", w.level),
1349 })
1350 .collect(),
1351 )
1352}
1353
1354fn parse_yearly_supply(s: &str) -> Option<f64> {
1356 s.parse::<f64>().ok()
1357}
1358
1359fn convert_v2_vault_single(
1360 v: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddress,
1361) -> Option<VaultV2> {
1362 let chain_id = v.chain.id;
1363 let asset = &v.asset;
1364
1365 VaultV2::from_gql(
1366 &v.address,
1367 v.name,
1368 v.symbol,
1369 chain_id,
1370 v.listed,
1371 v.whitelisted,
1372 Asset::from_gql(
1373 &asset.address,
1374 asset.symbol.clone(),
1375 Some(asset.name.clone()),
1376 asset.decimals,
1377 asset.price_usd,
1378 )?,
1379 Some(v.curator.address.as_str()),
1380 Some(v.owner.address.as_str()),
1381 v.total_assets.as_deref().unwrap_or("0"),
1382 v.total_assets_usd,
1383 &v.total_supply,
1384 Some(v.share_price),
1385 Some(v.performance_fee),
1386 Some(v.management_fee),
1387 v.avg_apy,
1388 v.avg_net_apy,
1389 v.apy,
1390 v.net_apy,
1391 &v.liquidity,
1392 v.liquidity_usd,
1393 v.adapters
1394 .items
1395 .map(|items| {
1396 items
1397 .into_iter()
1398 .filter_map(convert_v2_adapter_single)
1399 .collect()
1400 })
1401 .unwrap_or_default(),
1402 v.rewards
1403 .into_iter()
1404 .filter_map(|r| {
1405 VaultReward::from_gql(
1406 &r.asset.address,
1407 r.asset.symbol.clone(),
1408 r.supply_apr,
1409 parse_yearly_supply(&r.yearly_supply_tokens),
1410 )
1411 })
1412 .collect(),
1413 v.warnings
1414 .into_iter()
1415 .map(|w| VaultV2Warning {
1416 warning_type: w.type_.clone(),
1417 level: format!("{:?}", w.level),
1418 })
1419 .collect(),
1420 )
1421}
1422
1423fn convert_v2_adapter(
1424 a: get_vaults_v2::GetVaultsV2VaultV2sItemsAdaptersItems,
1425) -> Option<VaultAdapter> {
1426 VaultAdapter::from_gql(
1427 a.id,
1428 &a.address,
1429 format!("{:?}", a.type_),
1430 &a.assets,
1431 a.assets_usd,
1432 )
1433}
1434
1435fn convert_v2_adapter_single(
1436 a: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddressAdaptersItems,
1437) -> Option<VaultAdapter> {
1438 VaultAdapter::from_gql(
1439 a.id,
1440 &a.address,
1441 format!("{:?}", a.type_),
1442 &a.assets,
1443 a.assets_usd,
1444 )
1445}
1446
1447fn convert_user_vault_v1_position(
1450 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultPositions,
1451) -> Option<UserVaultV1Position> {
1452 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1453
1454 let state = p.state.as_ref().and_then(|s| {
1455 VaultPositionState::from_gql(
1456 &s.shares,
1457 s.assets.as_deref(),
1458 s.assets_usd,
1459 s.pnl.as_deref(),
1460 s.pnl_usd,
1461 s.roe,
1462 s.roe_usd,
1463 )
1464 });
1465
1466 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1467}
1468
1469fn convert_user_vault_v2_position(
1470 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultV2Positions,
1471) -> Option<UserVaultV2Position> {
1472 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1473
1474 UserVaultV2Position::from_gql(
1475 p.id,
1476 &p.shares,
1477 &p.assets,
1478 p.assets_usd,
1479 p.pnl.as_deref(),
1480 p.pnl_usd,
1481 p.roe,
1482 p.roe_usd,
1483 vault,
1484 )
1485}
1486
1487fn convert_user_vault_v1_position_overview(
1488 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultPositions,
1489) -> Option<UserVaultV1Position> {
1490 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1491
1492 let state = p.state.as_ref().and_then(|s| {
1493 VaultPositionState::from_gql(
1494 &s.shares,
1495 s.assets.as_deref(),
1496 s.assets_usd,
1497 s.pnl.as_deref(),
1498 s.pnl_usd,
1499 s.roe,
1500 s.roe_usd,
1501 )
1502 });
1503
1504 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1505}
1506
1507fn convert_user_vault_v2_position_overview(
1508 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultV2Positions,
1509) -> Option<UserVaultV2Position> {
1510 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1511
1512 UserVaultV2Position::from_gql(
1513 p.id,
1514 &p.shares,
1515 &p.assets,
1516 p.assets_usd,
1517 p.pnl.as_deref(),
1518 p.pnl_usd,
1519 p.roe,
1520 p.roe_usd,
1521 vault,
1522 )
1523}
1524
1525fn convert_user_market_position(
1526 p: get_user_account_overview::GetUserAccountOverviewUserByAddressMarketPositions,
1527) -> Option<UserMarketPosition> {
1528 let market = MarketInfo::from_gql(
1529 p.market.unique_key,
1530 Some(p.market.loan_asset.symbol),
1531 Some(p.market.loan_asset.address.as_str()),
1532 p.market.collateral_asset.as_ref().map(|c| c.symbol.clone()),
1533 p.market.collateral_asset.as_ref().map(|c| c.address.as_str()),
1534 );
1535
1536 UserMarketPosition::from_gql(
1537 p.id,
1538 &p.supply_shares,
1539 &p.supply_assets,
1540 p.supply_assets_usd,
1541 &p.borrow_shares,
1542 &p.borrow_assets,
1543 p.borrow_assets_usd,
1544 &p.collateral,
1545 p.collateral_usd,
1546 p.health_factor,
1547 market,
1548 )
1549}