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 if let Some(approval) = self.client.approve_if_needed(asset, vault, needed).await? {
928 approval.send().await?;
929 }
930 }
931 }
932
933 let receipt = self
934 .client
935 .deposit(vault, amount, self.client.signer_address())
936 .send()
937 .await?;
938 Ok(receipt)
939 }
940
941 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
943 let signer = self.client.signer_address();
944 let receipt = self.client.withdraw(vault, amount, signer, signer).send().await?;
945 Ok(receipt)
946 }
947
948 pub async fn balance(&self, vault: Address) -> Result<U256> {
950 let balance = self
951 .client
952 .get_balance(vault, self.client.signer_address())
953 .await?;
954 Ok(balance)
955 }
956
957 pub async fn approve(
960 &self,
961 vault: Address,
962 amount: U256,
963 ) -> Result<Option<TransactionReceipt>> {
964 let asset = self.client.get_asset(vault).await?;
965 if let Some(approval) = self.client.approve_if_needed(asset, vault, amount).await? {
966 let receipt = approval.send().await?;
967 Ok(Some(receipt))
968 } else {
969 Ok(None)
970 }
971 }
972
973 pub async fn get_allowance(&self, vault: Address) -> Result<U256> {
975 let asset = self.client.get_asset(vault).await?;
976 let allowance = self
977 .client
978 .get_allowance(asset, self.client.signer_address(), vault)
979 .await?;
980 Ok(allowance)
981 }
982
983 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
985 let asset = self.client.get_asset(vault).await?;
986 Ok(asset)
987 }
988
989 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
991 let decimals = self.client.get_decimals(token).await?;
992 Ok(decimals)
993 }
994
995 pub fn signer_address(&self) -> Address {
997 self.client.signer_address()
998 }
999
1000 pub fn auto_approve(&self) -> bool {
1002 self.auto_approve
1003 }
1004}
1005
1006pub struct VaultV2Operations<'a> {
1008 client: &'a VaultV2TransactionClient,
1009 auto_approve: bool,
1010}
1011
1012impl<'a> VaultV2Operations<'a> {
1013 fn new(client: &'a VaultV2TransactionClient, auto_approve: bool) -> Self {
1015 Self { client, auto_approve }
1016 }
1017
1018 pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1023 if self.auto_approve {
1024 let asset = self.client.get_asset(vault).await?;
1025 let current_allowance = self
1026 .client
1027 .get_allowance(asset, self.client.signer_address(), vault)
1028 .await?;
1029 if current_allowance < amount {
1030 let needed = amount - current_allowance;
1031 if let Some(approval) = self.client.approve_if_needed(asset, vault, needed).await? {
1032 approval.send().await?;
1033 }
1034 }
1035 }
1036
1037 let receipt = self
1038 .client
1039 .deposit(vault, amount, self.client.signer_address())
1040 .send()
1041 .await?;
1042 Ok(receipt)
1043 }
1044
1045 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1047 let signer = self.client.signer_address();
1048 let receipt = self.client.withdraw(vault, amount, signer, signer).send().await?;
1049 Ok(receipt)
1050 }
1051
1052 pub async fn balance(&self, vault: Address) -> Result<U256> {
1054 let balance = self
1055 .client
1056 .get_balance(vault, self.client.signer_address())
1057 .await?;
1058 Ok(balance)
1059 }
1060
1061 pub async fn approve(
1064 &self,
1065 vault: Address,
1066 amount: U256,
1067 ) -> Result<Option<TransactionReceipt>> {
1068 let asset = self.client.get_asset(vault).await?;
1069 if let Some(approval) = self.client.approve_if_needed(asset, vault, amount).await? {
1070 let receipt = approval.send().await?;
1071 Ok(Some(receipt))
1072 } else {
1073 Ok(None)
1074 }
1075 }
1076
1077 pub async fn get_allowance(&self, vault: Address) -> Result<U256> {
1079 let asset = self.client.get_asset(vault).await?;
1080 let allowance = self
1081 .client
1082 .get_allowance(asset, self.client.signer_address(), vault)
1083 .await?;
1084 Ok(allowance)
1085 }
1086
1087 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
1089 let asset = self.client.get_asset(vault).await?;
1090 Ok(asset)
1091 }
1092
1093 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
1095 let decimals = self.client.get_decimals(token).await?;
1096 Ok(decimals)
1097 }
1098
1099 pub fn signer_address(&self) -> Address {
1101 self.client.signer_address()
1102 }
1103
1104 pub fn auto_approve(&self) -> bool {
1106 self.auto_approve
1107 }
1108}
1109
1110pub struct MorphoClient {
1143 api: MorphoApiClient,
1144 vault_v1_tx: Option<VaultV1TransactionClient>,
1145 vault_v2_tx: Option<VaultV2TransactionClient>,
1146 auto_approve: bool,
1147}
1148
1149impl Default for MorphoClient {
1150 fn default() -> Self {
1151 Self::new()
1152 }
1153}
1154
1155impl MorphoClient {
1156 pub fn new() -> Self {
1158 Self {
1159 api: MorphoApiClient::new(),
1160 vault_v1_tx: None,
1161 vault_v2_tx: None,
1162 auto_approve: true,
1163 }
1164 }
1165
1166 pub fn with_config(config: MorphoClientConfig) -> Result<Self> {
1170 let api = match config.api_config {
1171 Some(api_config) => MorphoApiClient::with_config(api_config),
1172 None => MorphoApiClient::new(),
1173 };
1174
1175 let (vault_v1_tx, vault_v2_tx) = match (&config.rpc_url, &config.private_key) {
1176 (Some(rpc_url), Some(private_key)) => {
1177 let v1 = VaultV1TransactionClient::new(rpc_url, private_key)?;
1178 let v2 = VaultV2TransactionClient::new(rpc_url, private_key)?;
1179 (Some(v1), Some(v2))
1180 }
1181 _ => (None, None),
1182 };
1183
1184 Ok(Self {
1185 api,
1186 vault_v1_tx,
1187 vault_v2_tx,
1188 auto_approve: config.auto_approve,
1189 })
1190 }
1191
1192 pub fn vault_v1(&self) -> Result<VaultV1Operations<'_>> {
1196 match &self.vault_v1_tx {
1197 Some(client) => Ok(VaultV1Operations::new(client, self.auto_approve)),
1198 None => Err(ApiError::TransactionNotConfigured),
1199 }
1200 }
1201
1202 pub fn vault_v2(&self) -> Result<VaultV2Operations<'_>> {
1206 match &self.vault_v2_tx {
1207 Some(client) => Ok(VaultV2Operations::new(client, self.auto_approve)),
1208 None => Err(ApiError::TransactionNotConfigured),
1209 }
1210 }
1211
1212 pub fn auto_approve(&self) -> bool {
1214 self.auto_approve
1215 }
1216
1217 pub fn api(&self) -> &MorphoApiClient {
1219 &self.api
1220 }
1221
1222 pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<Vault>> {
1224 self.api.get_vaults_by_chain(chain).await
1225 }
1226
1227 pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<Vault>> {
1229 self.api.get_whitelisted_vaults(chain).await
1230 }
1231
1232 pub async fn get_user_vault_positions(
1234 &self,
1235 address: &str,
1236 chain: Option<NamedChain>,
1237 ) -> Result<UserVaultPositions> {
1238 self.api.get_user_vault_positions(address, chain).await
1239 }
1240
1241 pub async fn get_user_account_overview(
1243 &self,
1244 address: &str,
1245 chain: NamedChain,
1246 ) -> Result<UserAccountOverview> {
1247 self.api.get_user_account_overview(address, chain).await
1248 }
1249
1250 pub fn has_transaction_support(&self) -> bool {
1252 self.vault_v1_tx.is_some()
1253 }
1254
1255 pub fn signer_address(&self) -> Option<Address> {
1257 self.vault_v1_tx.as_ref().map(|c| c.signer_address())
1258 }
1259}
1260
1261fn convert_v1_vault(v: get_vaults_v1::GetVaultsV1VaultsItems) -> Option<VaultV1> {
1264 let chain_id = v.chain.id;
1265 let asset = &v.asset;
1266
1267 VaultV1::from_gql(
1268 &v.address,
1269 v.name,
1270 v.symbol,
1271 chain_id,
1272 v.listed,
1273 v.featured,
1274 v.whitelisted,
1275 Asset::from_gql(
1276 &asset.address,
1277 asset.symbol.clone(),
1278 Some(asset.name.clone()),
1279 asset.decimals,
1280 asset.price_usd,
1281 )?,
1282 v.state.as_ref().and_then(convert_v1_state),
1283 v.allocators
1284 .into_iter()
1285 .filter_map(|a| VaultAllocator::from_gql(&a.address))
1286 .collect(),
1287 v.warnings
1288 .into_iter()
1289 .map(|w| VaultWarning {
1290 warning_type: w.type_.clone(),
1291 level: format!("{:?}", w.level),
1292 })
1293 .collect(),
1294 )
1295}
1296
1297fn convert_v1_vault_single(
1298 v: get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddress,
1299) -> Option<VaultV1> {
1300 let chain_id = v.chain.id;
1301 let asset = &v.asset;
1302
1303 VaultV1::from_gql(
1304 &v.address,
1305 v.name,
1306 v.symbol,
1307 chain_id,
1308 v.listed,
1309 v.featured,
1310 v.whitelisted,
1311 Asset::from_gql(
1312 &asset.address,
1313 asset.symbol.clone(),
1314 Some(asset.name.clone()),
1315 asset.decimals,
1316 asset.price_usd,
1317 )?,
1318 v.state.as_ref().and_then(convert_v1_state_single),
1319 v.allocators
1320 .into_iter()
1321 .filter_map(|a| VaultAllocator::from_gql(&a.address))
1322 .collect(),
1323 v.warnings
1324 .into_iter()
1325 .map(|w| VaultWarning {
1326 warning_type: w.type_.clone(),
1327 level: format!("{:?}", w.level),
1328 })
1329 .collect(),
1330 )
1331}
1332
1333fn convert_v1_state(s: &get_vaults_v1::GetVaultsV1VaultsItemsState) -> Option<VaultStateV1> {
1334 VaultStateV1::from_gql(
1335 Some(s.curator.as_str()),
1336 Some(s.owner.as_str()),
1337 Some(s.guardian.as_str()),
1338 &s.total_assets,
1339 s.total_assets_usd,
1340 &s.total_supply,
1341 s.fee,
1342 &s.timelock,
1343 s.apy,
1344 s.net_apy,
1345 s.share_price.as_deref().unwrap_or("0"),
1346 s.allocation
1347 .iter()
1348 .filter_map(|a| {
1349 let market = &a.market;
1350 VaultAllocation::from_gql(
1351 market.unique_key.clone(),
1352 Some(market.loan_asset.symbol.clone()),
1353 Some(market.loan_asset.address.as_str()),
1354 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
1355 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
1356 &a.supply_assets,
1357 a.supply_assets_usd,
1358 &a.supply_cap,
1359 )
1360 })
1361 .collect(),
1362 )
1363}
1364
1365fn convert_v1_state_single(
1366 s: &get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddressState,
1367) -> Option<VaultStateV1> {
1368 VaultStateV1::from_gql(
1369 Some(s.curator.as_str()),
1370 Some(s.owner.as_str()),
1371 Some(s.guardian.as_str()),
1372 &s.total_assets,
1373 s.total_assets_usd,
1374 &s.total_supply,
1375 s.fee,
1376 &s.timelock,
1377 s.apy,
1378 s.net_apy,
1379 s.share_price.as_deref().unwrap_or("0"),
1380 s.allocation
1381 .iter()
1382 .filter_map(|a| {
1383 let market = &a.market;
1384 VaultAllocation::from_gql(
1385 market.unique_key.clone(),
1386 Some(market.loan_asset.symbol.clone()),
1387 Some(market.loan_asset.address.as_str()),
1388 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
1389 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
1390 &a.supply_assets,
1391 a.supply_assets_usd,
1392 &a.supply_cap,
1393 )
1394 })
1395 .collect(),
1396 )
1397}
1398
1399fn convert_v2_vault(v: get_vaults_v2::GetVaultsV2VaultV2sItems) -> Option<VaultV2> {
1400 let chain_id = v.chain.id;
1401 let asset = &v.asset;
1402
1403 VaultV2::from_gql(
1404 &v.address,
1405 v.name,
1406 v.symbol,
1407 chain_id,
1408 v.listed,
1409 v.whitelisted,
1410 Asset::from_gql(
1411 &asset.address,
1412 asset.symbol.clone(),
1413 Some(asset.name.clone()),
1414 asset.decimals,
1415 asset.price_usd,
1416 )?,
1417 Some(v.curator.address.as_str()),
1418 Some(v.owner.address.as_str()),
1419 v.total_assets.as_deref().unwrap_or("0"),
1420 v.total_assets_usd,
1421 &v.total_supply,
1422 Some(v.share_price),
1423 Some(v.performance_fee),
1424 Some(v.management_fee),
1425 v.avg_apy,
1426 v.avg_net_apy,
1427 v.apy,
1428 v.net_apy,
1429 &v.liquidity,
1430 v.liquidity_usd,
1431 v.adapters
1432 .items
1433 .map(|items| {
1434 items
1435 .into_iter()
1436 .filter_map(convert_v2_adapter)
1437 .collect()
1438 })
1439 .unwrap_or_default(),
1440 v.rewards
1441 .into_iter()
1442 .filter_map(|r| {
1443 VaultReward::from_gql(
1444 &r.asset.address,
1445 r.asset.symbol.clone(),
1446 r.supply_apr,
1447 parse_yearly_supply(&r.yearly_supply_tokens),
1448 )
1449 })
1450 .collect(),
1451 v.warnings
1452 .into_iter()
1453 .map(|w| VaultV2Warning {
1454 warning_type: w.type_.clone(),
1455 level: format!("{:?}", w.level),
1456 })
1457 .collect(),
1458 )
1459}
1460
1461fn parse_yearly_supply(s: &str) -> Option<f64> {
1463 s.parse::<f64>().ok()
1464}
1465
1466fn convert_v2_vault_single(
1467 v: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddress,
1468) -> Option<VaultV2> {
1469 let chain_id = v.chain.id;
1470 let asset = &v.asset;
1471
1472 VaultV2::from_gql(
1473 &v.address,
1474 v.name,
1475 v.symbol,
1476 chain_id,
1477 v.listed,
1478 v.whitelisted,
1479 Asset::from_gql(
1480 &asset.address,
1481 asset.symbol.clone(),
1482 Some(asset.name.clone()),
1483 asset.decimals,
1484 asset.price_usd,
1485 )?,
1486 Some(v.curator.address.as_str()),
1487 Some(v.owner.address.as_str()),
1488 v.total_assets.as_deref().unwrap_or("0"),
1489 v.total_assets_usd,
1490 &v.total_supply,
1491 Some(v.share_price),
1492 Some(v.performance_fee),
1493 Some(v.management_fee),
1494 v.avg_apy,
1495 v.avg_net_apy,
1496 v.apy,
1497 v.net_apy,
1498 &v.liquidity,
1499 v.liquidity_usd,
1500 v.adapters
1501 .items
1502 .map(|items| {
1503 items
1504 .into_iter()
1505 .filter_map(convert_v2_adapter_single)
1506 .collect()
1507 })
1508 .unwrap_or_default(),
1509 v.rewards
1510 .into_iter()
1511 .filter_map(|r| {
1512 VaultReward::from_gql(
1513 &r.asset.address,
1514 r.asset.symbol.clone(),
1515 r.supply_apr,
1516 parse_yearly_supply(&r.yearly_supply_tokens),
1517 )
1518 })
1519 .collect(),
1520 v.warnings
1521 .into_iter()
1522 .map(|w| VaultV2Warning {
1523 warning_type: w.type_.clone(),
1524 level: format!("{:?}", w.level),
1525 })
1526 .collect(),
1527 )
1528}
1529
1530fn convert_v2_adapter(
1531 a: get_vaults_v2::GetVaultsV2VaultV2sItemsAdaptersItems,
1532) -> Option<VaultAdapter> {
1533 VaultAdapter::from_gql(
1534 a.id,
1535 &a.address,
1536 format!("{:?}", a.type_),
1537 &a.assets,
1538 a.assets_usd,
1539 )
1540}
1541
1542fn convert_v2_adapter_single(
1543 a: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddressAdaptersItems,
1544) -> Option<VaultAdapter> {
1545 VaultAdapter::from_gql(
1546 a.id,
1547 &a.address,
1548 format!("{:?}", a.type_),
1549 &a.assets,
1550 a.assets_usd,
1551 )
1552}
1553
1554fn convert_user_vault_v1_position(
1557 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultPositions,
1558) -> Option<UserVaultV1Position> {
1559 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1560
1561 let state = p.state.as_ref().and_then(|s| {
1562 VaultPositionState::from_gql(
1563 &s.shares,
1564 s.assets.as_deref(),
1565 s.assets_usd,
1566 s.pnl.as_deref(),
1567 s.pnl_usd,
1568 s.roe,
1569 s.roe_usd,
1570 )
1571 });
1572
1573 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1574}
1575
1576fn convert_user_vault_v2_position(
1577 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultV2Positions,
1578) -> Option<UserVaultV2Position> {
1579 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1580
1581 UserVaultV2Position::from_gql(
1582 p.id,
1583 &p.shares,
1584 &p.assets,
1585 p.assets_usd,
1586 p.pnl.as_deref(),
1587 p.pnl_usd,
1588 p.roe,
1589 p.roe_usd,
1590 vault,
1591 )
1592}
1593
1594fn convert_user_vault_v1_position_overview(
1595 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultPositions,
1596) -> Option<UserVaultV1Position> {
1597 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1598
1599 let state = p.state.as_ref().and_then(|s| {
1600 VaultPositionState::from_gql(
1601 &s.shares,
1602 s.assets.as_deref(),
1603 s.assets_usd,
1604 s.pnl.as_deref(),
1605 s.pnl_usd,
1606 s.roe,
1607 s.roe_usd,
1608 )
1609 });
1610
1611 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1612}
1613
1614fn convert_user_vault_v2_position_overview(
1615 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultV2Positions,
1616) -> Option<UserVaultV2Position> {
1617 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1618
1619 UserVaultV2Position::from_gql(
1620 p.id,
1621 &p.shares,
1622 &p.assets,
1623 p.assets_usd,
1624 p.pnl.as_deref(),
1625 p.pnl_usd,
1626 p.roe,
1627 p.roe_usd,
1628 vault,
1629 )
1630}
1631
1632fn convert_user_market_position(
1633 p: get_user_account_overview::GetUserAccountOverviewUserByAddressMarketPositions,
1634) -> Option<UserMarketPosition> {
1635 let market = MarketInfo::from_gql(
1636 p.market.unique_key,
1637 Some(p.market.loan_asset.symbol),
1638 Some(p.market.loan_asset.address.as_str()),
1639 p.market.collateral_asset.as_ref().map(|c| c.symbol.clone()),
1640 p.market.collateral_asset.as_ref().map(|c| c.address.as_str()),
1641 );
1642
1643 UserMarketPosition::from_gql(
1644 p.id,
1645 &p.supply_shares,
1646 &p.supply_assets,
1647 p.supply_assets_usd,
1648 &p.borrow_shares,
1649 &p.borrow_assets,
1650 p.borrow_assets_usd,
1651 &p.collateral,
1652 p.collateral_usd,
1653 p.health_factor,
1654 market,
1655 )
1656}