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};
12use crate::queries::v1::{
13 get_vault_v1_by_address, get_vaults_v1, GetVaultV1ByAddress, GetVaultsV1,
14};
15use crate::queries::user::{
16 get_user_account_overview, get_user_vault_positions, GetUserAccountOverview,
17 GetUserVaultPositions,
18};
19use crate::queries::v2::{
20 get_vault_v2_by_address, get_vaults_v2, GetVaultV2ByAddress, GetVaultsV2,
21};
22use crate::types::{
23 Asset, Chain, MarketInfo, UserAccountOverview, UserMarketPosition, UserState,
24 UserVaultPositions, UserVaultV1Position, UserVaultV2Position, Vault, VaultAdapter,
25 VaultAllocation, VaultAllocator, VaultInfo, VaultPositionState, VaultReward, VaultStateV1,
26 VaultV1, VaultV2, VaultV2Warning, VaultWarning,
27};
28
29pub const DEFAULT_API_URL: &str = "https://api.morpho.org/graphql";
31
32pub const DEFAULT_PAGE_SIZE: i64 = 100;
34
35#[derive(Debug, Clone)]
37pub struct ClientConfig {
38 pub api_url: Url,
40 pub page_size: i64,
42}
43
44impl Default for ClientConfig {
45 fn default() -> Self {
46 Self {
47 api_url: Url::parse(DEFAULT_API_URL).expect("Invalid default API URL"),
48 page_size: DEFAULT_PAGE_SIZE,
49 }
50 }
51}
52
53impl ClientConfig {
54 pub fn new() -> Self {
56 Self::default()
57 }
58
59 pub fn with_api_url(mut self, url: Url) -> Self {
61 self.api_url = url;
62 self
63 }
64
65 pub fn with_page_size(mut self, size: i64) -> Self {
67 self.page_size = size;
68 self
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct VaultV1Client {
75 http_client: Client,
76 config: ClientConfig,
77}
78
79impl Default for VaultV1Client {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85impl VaultV1Client {
86 pub fn new() -> Self {
88 Self {
89 http_client: Client::new(),
90 config: ClientConfig::default(),
91 }
92 }
93
94 pub fn with_config(config: ClientConfig) -> Self {
96 Self {
97 http_client: Client::new(),
98 config,
99 }
100 }
101
102 async fn execute<Q: GraphQLQuery>(
104 &self,
105 variables: Q::Variables,
106 ) -> Result<Q::ResponseData> {
107 let request_body = Q::build_query(variables);
108 let response = self
109 .http_client
110 .post(self.config.api_url.as_str())
111 .json(&request_body)
112 .send()
113 .await?;
114
115 let response_body: Response<Q::ResponseData> = response.json().await?;
116
117 if let Some(errors) = response_body.errors {
118 if !errors.is_empty() {
119 return Err(ApiError::GraphQL(
120 errors
121 .iter()
122 .map(|e| e.message.clone())
123 .collect::<Vec<_>>()
124 .join("; "),
125 ));
126 }
127 }
128
129 response_body
130 .data
131 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
132 }
133
134 pub async fn get_vaults(&self, filters: Option<VaultFiltersV1>) -> Result<Vec<VaultV1>> {
136 let variables = get_vaults_v1::Variables {
137 first: Some(self.config.page_size),
138 skip: Some(0),
139 where_: filters.map(|f| f.to_gql()),
140 };
141
142 let data = self.execute::<GetVaultsV1>(variables).await?;
143
144 let items = match data.vaults.items {
145 Some(items) => items,
146 None => return Ok(Vec::new()),
147 };
148
149 let vaults: Vec<VaultV1> = items
150 .into_iter()
151 .filter_map(convert_v1_vault)
152 .collect();
153
154 Ok(vaults)
155 }
156
157 pub async fn get_vault(&self, address: &str, chain: Chain) -> Result<VaultV1> {
159 let variables = get_vault_v1_by_address::Variables {
160 address: address.to_string(),
161 chain_id: chain.id() as i64,
162 };
163
164 let data = self.execute::<GetVaultV1ByAddress>(variables).await?;
165
166 convert_v1_vault_single(data.vault_by_address).ok_or_else(|| ApiError::VaultNotFound {
167 address: address.to_string(),
168 chain_id: chain.id(),
169 })
170 }
171
172 pub async fn get_vaults_by_chain(&self, chain: Chain) -> Result<Vec<VaultV1>> {
174 let filters = VaultFiltersV1::new().chain(chain);
175 self.get_vaults(Some(filters)).await
176 }
177
178 pub async fn get_vaults_by_curator(
180 &self,
181 curator: &str,
182 chain: Option<Chain>,
183 ) -> Result<Vec<VaultV1>> {
184 let mut filters = VaultFiltersV1::new().curators([curator]);
185 if let Some(c) = chain {
186 filters = filters.chain(c);
187 }
188 self.get_vaults(Some(filters)).await
189 }
190
191 pub async fn get_whitelisted_vaults(&self, chain: Option<Chain>) -> Result<Vec<VaultV1>> {
193 let mut filters = VaultFiltersV1::new().listed(true);
194 if let Some(c) = chain {
195 filters = filters.chain(c);
196 }
197 self.get_vaults(Some(filters)).await
198 }
199}
200
201#[derive(Debug, Clone)]
203pub struct VaultV2Client {
204 http_client: Client,
205 config: ClientConfig,
206}
207
208impl Default for VaultV2Client {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214impl VaultV2Client {
215 pub fn new() -> Self {
217 Self {
218 http_client: Client::new(),
219 config: ClientConfig::default(),
220 }
221 }
222
223 pub fn with_config(config: ClientConfig) -> Self {
225 Self {
226 http_client: Client::new(),
227 config,
228 }
229 }
230
231 async fn execute<Q: GraphQLQuery>(
233 &self,
234 variables: Q::Variables,
235 ) -> Result<Q::ResponseData> {
236 let request_body = Q::build_query(variables);
237 let response = self
238 .http_client
239 .post(self.config.api_url.as_str())
240 .json(&request_body)
241 .send()
242 .await?;
243
244 let response_body: Response<Q::ResponseData> = response.json().await?;
245
246 if let Some(errors) = response_body.errors {
247 if !errors.is_empty() {
248 return Err(ApiError::GraphQL(
249 errors
250 .iter()
251 .map(|e| e.message.clone())
252 .collect::<Vec<_>>()
253 .join("; "),
254 ));
255 }
256 }
257
258 response_body
259 .data
260 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
261 }
262
263 pub async fn get_vaults(&self, filters: Option<VaultFiltersV2>) -> Result<Vec<VaultV2>> {
265 let variables = get_vaults_v2::Variables {
266 first: Some(self.config.page_size),
267 skip: Some(0),
268 where_: filters.map(|f| f.to_gql()),
269 };
270
271 let data = self.execute::<GetVaultsV2>(variables).await?;
272
273 let items = match data.vault_v2s.items {
274 Some(items) => items,
275 None => return Ok(Vec::new()),
276 };
277
278 let vaults: Vec<VaultV2> = items
279 .into_iter()
280 .filter_map(convert_v2_vault)
281 .collect();
282
283 Ok(vaults)
284 }
285
286 pub async fn get_vault(&self, address: &str, chain: Chain) -> Result<VaultV2> {
288 let variables = get_vault_v2_by_address::Variables {
289 address: address.to_string(),
290 chain_id: chain.id() as i64,
291 };
292
293 let data = self.execute::<GetVaultV2ByAddress>(variables).await?;
294
295 convert_v2_vault_single(data.vault_v2_by_address).ok_or_else(|| ApiError::VaultNotFound {
296 address: address.to_string(),
297 chain_id: chain.id(),
298 })
299 }
300
301 pub async fn get_vaults_by_chain(&self, chain: Chain) -> Result<Vec<VaultV2>> {
303 let filters = VaultFiltersV2::new().chain(chain);
304 self.get_vaults(Some(filters)).await
305 }
306
307 pub async fn get_whitelisted_vaults(&self, chain: Option<Chain>) -> Result<Vec<VaultV2>> {
309 let mut filters = VaultFiltersV2::new().listed(true);
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 MorphoApiClient {
320 pub v1: VaultV1Client,
322 pub v2: VaultV2Client,
324}
325
326impl Default for MorphoApiClient {
327 fn default() -> Self {
328 Self::new()
329 }
330}
331
332impl MorphoApiClient {
333 pub fn new() -> Self {
335 Self {
336 v1: VaultV1Client::new(),
337 v2: VaultV2Client::new(),
338 }
339 }
340
341 pub fn with_config(config: ClientConfig) -> Self {
343 Self {
344 v1: VaultV1Client::with_config(config.clone()),
345 v2: VaultV2Client::with_config(config),
346 }
347 }
348
349 pub async fn get_vaults_by_chain(&self, chain: Chain) -> Result<Vec<Vault>> {
351 let (v1_vaults, v2_vaults) = tokio::try_join!(
352 self.v1.get_vaults_by_chain(chain),
353 self.v2.get_vaults_by_chain(chain),
354 )?;
355
356 let mut vaults: Vec<Vault> = Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
357 vaults.extend(v1_vaults.into_iter().map(Vault::from));
358 vaults.extend(v2_vaults.into_iter().map(Vault::from));
359
360 Ok(vaults)
361 }
362
363 pub async fn get_whitelisted_vaults(&self, chain: Option<Chain>) -> Result<Vec<Vault>> {
365 let (v1_vaults, v2_vaults) = tokio::try_join!(
366 self.v1.get_whitelisted_vaults(chain),
367 self.v2.get_whitelisted_vaults(chain),
368 )?;
369
370 let mut vaults: Vec<Vault> = Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
371 vaults.extend(v1_vaults.into_iter().map(Vault::from));
372 vaults.extend(v2_vaults.into_iter().map(Vault::from));
373
374 Ok(vaults)
375 }
376
377 async fn execute<Q: GraphQLQuery>(&self, variables: Q::Variables) -> Result<Q::ResponseData> {
379 let request_body = Q::build_query(variables);
380 let response = self
381 .v1
382 .http_client
383 .post(self.v1.config.api_url.as_str())
384 .json(&request_body)
385 .send()
386 .await?;
387
388 let response_body: Response<Q::ResponseData> = response.json().await?;
389
390 if let Some(errors) = response_body.errors {
391 if !errors.is_empty() {
392 return Err(ApiError::GraphQL(
393 errors
394 .iter()
395 .map(|e| e.message.clone())
396 .collect::<Vec<_>>()
397 .join("; "),
398 ));
399 }
400 }
401
402 response_body
403 .data
404 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
405 }
406
407 pub async fn get_user_vault_positions(
412 &self,
413 address: &str,
414 chain: Option<Chain>,
415 ) -> Result<UserVaultPositions> {
416 match chain {
417 Some(c) => self.get_user_vault_positions_single_chain(address, c).await,
418 None => self.get_user_vault_positions_all_chains(address).await,
419 }
420 }
421
422 async fn get_user_vault_positions_single_chain(
424 &self,
425 address: &str,
426 chain: Chain,
427 ) -> Result<UserVaultPositions> {
428 let variables = get_user_vault_positions::Variables {
429 address: address.to_string(),
430 chain_id: chain.id(),
431 };
432
433 let data = self.execute::<GetUserVaultPositions>(variables).await?;
434 let user = data.user_by_address;
435
436 let vault_positions: Vec<UserVaultV1Position> = user
437 .vault_positions
438 .into_iter()
439 .filter_map(convert_user_vault_v1_position)
440 .collect();
441
442 let vault_v2_positions: Vec<UserVaultV2Position> = user
443 .vault_v2_positions
444 .into_iter()
445 .filter_map(convert_user_vault_v2_position)
446 .collect();
447
448 Ok(UserVaultPositions {
449 address: user
450 .address
451 .parse()
452 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
453 vault_positions,
454 vault_v2_positions,
455 })
456 }
457
458 async fn get_user_vault_positions_all_chains(
460 &self,
461 address: &str,
462 ) -> Result<UserVaultPositions> {
463 use futures::future::join_all;
464
465 let valid_chains: Vec<_> = Chain::all()
467 .iter()
468 .filter(|chain| chain.id() <= i32::MAX as i64)
469 .copied()
470 .collect();
471
472 let futures: Vec<_> = valid_chains
473 .iter()
474 .map(|chain| self.get_user_vault_positions_single_chain(address, *chain))
475 .collect();
476
477 let results = join_all(futures).await;
478
479 let parsed_address = address
480 .parse()
481 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?;
482
483 let mut all_v1_positions = Vec::new();
484 let mut all_v2_positions = Vec::new();
485
486 for result in results {
487 match result {
488 Ok(positions) => {
489 all_v1_positions.extend(positions.vault_positions);
490 all_v2_positions.extend(positions.vault_v2_positions);
491 }
492 Err(ApiError::GraphQL(msg)) if msg.contains("No results") => continue,
494 Err(e) => return Err(e),
495 }
496 }
497
498 Ok(UserVaultPositions {
499 address: parsed_address,
500 vault_positions: all_v1_positions,
501 vault_v2_positions: all_v2_positions,
502 })
503 }
504
505 pub async fn get_user_account_overview(
507 &self,
508 address: &str,
509 chain: Chain,
510 ) -> Result<UserAccountOverview> {
511 let variables = get_user_account_overview::Variables {
512 address: address.to_string(),
513 chain_id: chain.id(),
514 };
515
516 let data = self.execute::<GetUserAccountOverview>(variables).await?;
517 let user = data.user_by_address;
518
519 let state = UserState::from_gql(
520 user.state.vaults_pnl_usd,
521 user.state.vaults_roe_usd,
522 user.state.vaults_assets_usd,
523 user.state.vault_v2s_pnl_usd,
524 user.state.vault_v2s_roe_usd,
525 user.state.vault_v2s_assets_usd,
526 user.state.markets_pnl_usd,
527 user.state.markets_roe_usd,
528 user.state.markets_supply_pnl_usd,
529 user.state.markets_supply_roe_usd,
530 user.state.markets_borrow_pnl_usd,
531 user.state.markets_borrow_roe_usd,
532 user.state.markets_collateral_pnl_usd,
533 user.state.markets_collateral_roe_usd,
534 user.state.markets_margin_pnl_usd,
535 user.state.markets_margin_roe_usd,
536 user.state.markets_collateral_usd,
537 user.state.markets_supply_assets_usd,
538 user.state.markets_borrow_assets_usd,
539 user.state.markets_margin_usd,
540 );
541
542 let vault_positions: Vec<UserVaultV1Position> = user
543 .vault_positions
544 .into_iter()
545 .filter_map(convert_user_vault_v1_position_overview)
546 .collect();
547
548 let vault_v2_positions: Vec<UserVaultV2Position> = user
549 .vault_v2_positions
550 .into_iter()
551 .filter_map(convert_user_vault_v2_position_overview)
552 .collect();
553
554 let market_positions: Vec<UserMarketPosition> = user
555 .market_positions
556 .into_iter()
557 .filter_map(convert_user_market_position)
558 .collect();
559
560 Ok(UserAccountOverview {
561 address: user
562 .address
563 .parse()
564 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
565 state,
566 vault_positions,
567 vault_v2_positions,
568 market_positions,
569 })
570 }
571}
572
573#[derive(Debug, Clone, Default)]
575pub struct MorphoClientConfig {
576 pub api_config: Option<ClientConfig>,
578 pub rpc_url: Option<String>,
580 pub private_key: Option<String>,
582}
583
584impl MorphoClientConfig {
585 pub fn new() -> Self {
587 Self::default()
588 }
589
590 pub fn with_api_config(mut self, config: ClientConfig) -> Self {
592 self.api_config = Some(config);
593 self
594 }
595
596 pub fn with_rpc_url(mut self, rpc_url: impl Into<String>) -> Self {
598 self.rpc_url = Some(rpc_url.into());
599 self
600 }
601
602 pub fn with_private_key(mut self, private_key: impl Into<String>) -> Self {
604 self.private_key = Some(private_key.into());
605 self
606 }
607}
608
609pub struct VaultV1Operations<'a> {
611 client: &'a VaultV1TransactionClient,
612}
613
614impl<'a> VaultV1Operations<'a> {
615 fn new(client: &'a VaultV1TransactionClient) -> Self {
617 Self { client }
618 }
619
620 pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
622 let receipt = self
623 .client
624 .deposit(vault, amount, self.client.signer_address())
625 .await?;
626 Ok(receipt)
627 }
628
629 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
631 let signer = self.client.signer_address();
632 let receipt = self.client.withdraw(vault, amount, signer, signer).await?;
633 Ok(receipt)
634 }
635
636 pub async fn balance(&self, vault: Address) -> Result<U256> {
638 let balance = self
639 .client
640 .get_balance(vault, self.client.signer_address())
641 .await?;
642 Ok(balance)
643 }
644
645 pub async fn approve(
648 &self,
649 vault: Address,
650 amount: U256,
651 ) -> Result<Option<TransactionReceipt>> {
652 let asset = self.client.get_asset(vault).await?;
653 let receipt = self.client.approve_if_needed(asset, vault, amount).await?;
654 Ok(receipt)
655 }
656
657 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
659 let asset = self.client.get_asset(vault).await?;
660 Ok(asset)
661 }
662
663 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
665 let decimals = self.client.get_decimals(token).await?;
666 Ok(decimals)
667 }
668
669 pub fn signer_address(&self) -> Address {
671 self.client.signer_address()
672 }
673}
674
675pub struct VaultV2Operations<'a> {
677 client: &'a VaultV2TransactionClient,
678}
679
680impl<'a> VaultV2Operations<'a> {
681 fn new(client: &'a VaultV2TransactionClient) -> Self {
683 Self { client }
684 }
685
686 pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
688 let receipt = self
689 .client
690 .deposit(vault, amount, self.client.signer_address())
691 .await?;
692 Ok(receipt)
693 }
694
695 pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
697 let signer = self.client.signer_address();
698 let receipt = self.client.withdraw(vault, amount, signer, signer).await?;
699 Ok(receipt)
700 }
701
702 pub async fn balance(&self, vault: Address) -> Result<U256> {
704 let balance = self
705 .client
706 .get_balance(vault, self.client.signer_address())
707 .await?;
708 Ok(balance)
709 }
710
711 pub async fn approve(
714 &self,
715 vault: Address,
716 amount: U256,
717 ) -> Result<Option<TransactionReceipt>> {
718 let asset = self.client.get_asset(vault).await?;
719 let receipt = self.client.approve_if_needed(asset, vault, amount).await?;
720 Ok(receipt)
721 }
722
723 pub async fn get_asset(&self, vault: Address) -> Result<Address> {
725 let asset = self.client.get_asset(vault).await?;
726 Ok(asset)
727 }
728
729 pub async fn get_decimals(&self, token: Address) -> Result<u8> {
731 let decimals = self.client.get_decimals(token).await?;
732 Ok(decimals)
733 }
734
735 pub fn signer_address(&self) -> Address {
737 self.client.signer_address()
738 }
739}
740
741pub struct MorphoClient {
774 api: MorphoApiClient,
775 vault_v1_tx: Option<VaultV1TransactionClient>,
776 vault_v2_tx: Option<VaultV2TransactionClient>,
777}
778
779impl Default for MorphoClient {
780 fn default() -> Self {
781 Self::new()
782 }
783}
784
785impl MorphoClient {
786 pub fn new() -> Self {
788 Self {
789 api: MorphoApiClient::new(),
790 vault_v1_tx: None,
791 vault_v2_tx: None,
792 }
793 }
794
795 pub fn with_config(config: MorphoClientConfig) -> Result<Self> {
799 let api = match config.api_config {
800 Some(api_config) => MorphoApiClient::with_config(api_config),
801 None => MorphoApiClient::new(),
802 };
803
804 let (vault_v1_tx, vault_v2_tx) = match (&config.rpc_url, &config.private_key) {
805 (Some(rpc_url), Some(private_key)) => {
806 let v1 = VaultV1TransactionClient::new(rpc_url, private_key)?;
807 let v2 = VaultV2TransactionClient::new(rpc_url, private_key)?;
808 (Some(v1), Some(v2))
809 }
810 _ => (None, None),
811 };
812
813 Ok(Self {
814 api,
815 vault_v1_tx,
816 vault_v2_tx,
817 })
818 }
819
820 pub fn vault_v1(&self) -> Result<VaultV1Operations<'_>> {
824 match &self.vault_v1_tx {
825 Some(client) => Ok(VaultV1Operations::new(client)),
826 None => Err(ApiError::TransactionNotConfigured),
827 }
828 }
829
830 pub fn vault_v2(&self) -> Result<VaultV2Operations<'_>> {
834 match &self.vault_v2_tx {
835 Some(client) => Ok(VaultV2Operations::new(client)),
836 None => Err(ApiError::TransactionNotConfigured),
837 }
838 }
839
840 pub fn api(&self) -> &MorphoApiClient {
842 &self.api
843 }
844
845 pub async fn get_vaults_by_chain(&self, chain: Chain) -> Result<Vec<Vault>> {
847 self.api.get_vaults_by_chain(chain).await
848 }
849
850 pub async fn get_whitelisted_vaults(&self, chain: Option<Chain>) -> Result<Vec<Vault>> {
852 self.api.get_whitelisted_vaults(chain).await
853 }
854
855 pub async fn get_user_vault_positions(
857 &self,
858 address: &str,
859 chain: Option<Chain>,
860 ) -> Result<UserVaultPositions> {
861 self.api.get_user_vault_positions(address, chain).await
862 }
863
864 pub async fn get_user_account_overview(
866 &self,
867 address: &str,
868 chain: Chain,
869 ) -> Result<UserAccountOverview> {
870 self.api.get_user_account_overview(address, chain).await
871 }
872
873 pub fn has_transaction_support(&self) -> bool {
875 self.vault_v1_tx.is_some()
876 }
877
878 pub fn signer_address(&self) -> Option<Address> {
880 self.vault_v1_tx.as_ref().map(|c| c.signer_address())
881 }
882}
883
884fn convert_v1_vault(v: get_vaults_v1::GetVaultsV1VaultsItems) -> Option<VaultV1> {
887 let chain_id = v.chain.id;
888 let asset = &v.asset;
889
890 VaultV1::from_gql(
891 &v.address,
892 v.name,
893 v.symbol,
894 chain_id,
895 v.listed,
896 v.featured,
897 v.whitelisted,
898 Asset::from_gql(
899 &asset.address,
900 asset.symbol.clone(),
901 Some(asset.name.clone()),
902 asset.decimals,
903 asset.price_usd,
904 )?,
905 v.state.as_ref().and_then(convert_v1_state),
906 v.allocators
907 .into_iter()
908 .filter_map(|a| VaultAllocator::from_gql(&a.address))
909 .collect(),
910 v.warnings
911 .into_iter()
912 .map(|w| VaultWarning {
913 warning_type: w.type_.clone(),
914 level: format!("{:?}", w.level),
915 })
916 .collect(),
917 )
918}
919
920fn convert_v1_vault_single(
921 v: get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddress,
922) -> Option<VaultV1> {
923 let chain_id = v.chain.id;
924 let asset = &v.asset;
925
926 VaultV1::from_gql(
927 &v.address,
928 v.name,
929 v.symbol,
930 chain_id,
931 v.listed,
932 v.featured,
933 v.whitelisted,
934 Asset::from_gql(
935 &asset.address,
936 asset.symbol.clone(),
937 Some(asset.name.clone()),
938 asset.decimals,
939 asset.price_usd,
940 )?,
941 v.state.as_ref().and_then(convert_v1_state_single),
942 v.allocators
943 .into_iter()
944 .filter_map(|a| VaultAllocator::from_gql(&a.address))
945 .collect(),
946 v.warnings
947 .into_iter()
948 .map(|w| VaultWarning {
949 warning_type: w.type_.clone(),
950 level: format!("{:?}", w.level),
951 })
952 .collect(),
953 )
954}
955
956fn convert_v1_state(s: &get_vaults_v1::GetVaultsV1VaultsItemsState) -> Option<VaultStateV1> {
957 VaultStateV1::from_gql(
958 Some(s.curator.as_str()),
959 Some(s.owner.as_str()),
960 Some(s.guardian.as_str()),
961 &s.total_assets,
962 s.total_assets_usd,
963 &s.total_supply,
964 s.fee,
965 &s.timelock,
966 s.apy,
967 s.net_apy,
968 s.share_price.as_deref().unwrap_or("0"),
969 s.allocation
970 .iter()
971 .filter_map(|a| {
972 let market = &a.market;
973 VaultAllocation::from_gql(
974 market.unique_key.clone(),
975 Some(market.loan_asset.symbol.clone()),
976 Some(market.loan_asset.address.as_str()),
977 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
978 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
979 &a.supply_assets,
980 a.supply_assets_usd,
981 &a.supply_cap,
982 )
983 })
984 .collect(),
985 )
986}
987
988fn convert_v1_state_single(
989 s: &get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddressState,
990) -> Option<VaultStateV1> {
991 VaultStateV1::from_gql(
992 Some(s.curator.as_str()),
993 Some(s.owner.as_str()),
994 Some(s.guardian.as_str()),
995 &s.total_assets,
996 s.total_assets_usd,
997 &s.total_supply,
998 s.fee,
999 &s.timelock,
1000 s.apy,
1001 s.net_apy,
1002 s.share_price.as_deref().unwrap_or("0"),
1003 s.allocation
1004 .iter()
1005 .filter_map(|a| {
1006 let market = &a.market;
1007 VaultAllocation::from_gql(
1008 market.unique_key.clone(),
1009 Some(market.loan_asset.symbol.clone()),
1010 Some(market.loan_asset.address.as_str()),
1011 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
1012 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
1013 &a.supply_assets,
1014 a.supply_assets_usd,
1015 &a.supply_cap,
1016 )
1017 })
1018 .collect(),
1019 )
1020}
1021
1022fn convert_v2_vault(v: get_vaults_v2::GetVaultsV2VaultV2sItems) -> Option<VaultV2> {
1023 let chain_id = v.chain.id;
1024 let asset = &v.asset;
1025
1026 VaultV2::from_gql(
1027 &v.address,
1028 v.name,
1029 v.symbol,
1030 chain_id,
1031 v.listed,
1032 v.whitelisted,
1033 Asset::from_gql(
1034 &asset.address,
1035 asset.symbol.clone(),
1036 Some(asset.name.clone()),
1037 asset.decimals,
1038 asset.price_usd,
1039 )?,
1040 Some(v.curator.address.as_str()),
1041 Some(v.owner.address.as_str()),
1042 v.total_assets.as_deref().unwrap_or("0"),
1043 v.total_assets_usd,
1044 &v.total_supply,
1045 Some(v.share_price),
1046 Some(v.performance_fee),
1047 Some(v.management_fee),
1048 v.avg_apy,
1049 v.avg_net_apy,
1050 v.apy,
1051 v.net_apy,
1052 &v.liquidity,
1053 v.liquidity_usd,
1054 v.adapters
1055 .items
1056 .map(|items| {
1057 items
1058 .into_iter()
1059 .filter_map(convert_v2_adapter)
1060 .collect()
1061 })
1062 .unwrap_or_default(),
1063 v.rewards
1064 .into_iter()
1065 .filter_map(|r| {
1066 VaultReward::from_gql(
1067 &r.asset.address,
1068 r.asset.symbol.clone(),
1069 r.supply_apr,
1070 parse_yearly_supply(&r.yearly_supply_tokens),
1071 )
1072 })
1073 .collect(),
1074 v.warnings
1075 .into_iter()
1076 .map(|w| VaultV2Warning {
1077 warning_type: w.type_.clone(),
1078 level: format!("{:?}", w.level),
1079 })
1080 .collect(),
1081 )
1082}
1083
1084fn parse_yearly_supply(s: &str) -> Option<f64> {
1086 s.parse::<f64>().ok()
1087}
1088
1089fn convert_v2_vault_single(
1090 v: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddress,
1091) -> Option<VaultV2> {
1092 let chain_id = v.chain.id;
1093 let asset = &v.asset;
1094
1095 VaultV2::from_gql(
1096 &v.address,
1097 v.name,
1098 v.symbol,
1099 chain_id,
1100 v.listed,
1101 v.whitelisted,
1102 Asset::from_gql(
1103 &asset.address,
1104 asset.symbol.clone(),
1105 Some(asset.name.clone()),
1106 asset.decimals,
1107 asset.price_usd,
1108 )?,
1109 Some(v.curator.address.as_str()),
1110 Some(v.owner.address.as_str()),
1111 v.total_assets.as_deref().unwrap_or("0"),
1112 v.total_assets_usd,
1113 &v.total_supply,
1114 Some(v.share_price),
1115 Some(v.performance_fee),
1116 Some(v.management_fee),
1117 v.avg_apy,
1118 v.avg_net_apy,
1119 v.apy,
1120 v.net_apy,
1121 &v.liquidity,
1122 v.liquidity_usd,
1123 v.adapters
1124 .items
1125 .map(|items| {
1126 items
1127 .into_iter()
1128 .filter_map(convert_v2_adapter_single)
1129 .collect()
1130 })
1131 .unwrap_or_default(),
1132 v.rewards
1133 .into_iter()
1134 .filter_map(|r| {
1135 VaultReward::from_gql(
1136 &r.asset.address,
1137 r.asset.symbol.clone(),
1138 r.supply_apr,
1139 parse_yearly_supply(&r.yearly_supply_tokens),
1140 )
1141 })
1142 .collect(),
1143 v.warnings
1144 .into_iter()
1145 .map(|w| VaultV2Warning {
1146 warning_type: w.type_.clone(),
1147 level: format!("{:?}", w.level),
1148 })
1149 .collect(),
1150 )
1151}
1152
1153fn convert_v2_adapter(
1154 a: get_vaults_v2::GetVaultsV2VaultV2sItemsAdaptersItems,
1155) -> Option<VaultAdapter> {
1156 VaultAdapter::from_gql(
1157 a.id,
1158 &a.address,
1159 format!("{:?}", a.type_),
1160 &a.assets,
1161 a.assets_usd,
1162 )
1163}
1164
1165fn convert_v2_adapter_single(
1166 a: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddressAdaptersItems,
1167) -> Option<VaultAdapter> {
1168 VaultAdapter::from_gql(
1169 a.id,
1170 &a.address,
1171 format!("{:?}", a.type_),
1172 &a.assets,
1173 a.assets_usd,
1174 )
1175}
1176
1177fn convert_user_vault_v1_position(
1180 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultPositions,
1181) -> Option<UserVaultV1Position> {
1182 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol)?;
1183
1184 let state = p.state.as_ref().and_then(|s| {
1185 VaultPositionState::from_gql(
1186 &s.shares,
1187 s.assets.as_deref(),
1188 s.assets_usd,
1189 s.pnl.as_deref(),
1190 s.pnl_usd,
1191 s.roe,
1192 s.roe_usd,
1193 )
1194 });
1195
1196 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1197}
1198
1199fn convert_user_vault_v2_position(
1200 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultV2Positions,
1201) -> Option<UserVaultV2Position> {
1202 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol)?;
1203
1204 UserVaultV2Position::from_gql(
1205 p.id,
1206 &p.shares,
1207 &p.assets,
1208 p.assets_usd,
1209 p.pnl.as_deref(),
1210 p.pnl_usd,
1211 p.roe,
1212 p.roe_usd,
1213 vault,
1214 )
1215}
1216
1217fn convert_user_vault_v1_position_overview(
1218 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultPositions,
1219) -> Option<UserVaultV1Position> {
1220 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol)?;
1221
1222 let state = p.state.as_ref().and_then(|s| {
1223 VaultPositionState::from_gql(
1224 &s.shares,
1225 s.assets.as_deref(),
1226 s.assets_usd,
1227 s.pnl.as_deref(),
1228 s.pnl_usd,
1229 s.roe,
1230 s.roe_usd,
1231 )
1232 });
1233
1234 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1235}
1236
1237fn convert_user_vault_v2_position_overview(
1238 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultV2Positions,
1239) -> Option<UserVaultV2Position> {
1240 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol)?;
1241
1242 UserVaultV2Position::from_gql(
1243 p.id,
1244 &p.shares,
1245 &p.assets,
1246 p.assets_usd,
1247 p.pnl.as_deref(),
1248 p.pnl_usd,
1249 p.roe,
1250 p.roe_usd,
1251 vault,
1252 )
1253}
1254
1255fn convert_user_market_position(
1256 p: get_user_account_overview::GetUserAccountOverviewUserByAddressMarketPositions,
1257) -> Option<UserMarketPosition> {
1258 let market = MarketInfo::from_gql(
1259 p.market.unique_key,
1260 Some(p.market.loan_asset.symbol),
1261 Some(p.market.loan_asset.address.as_str()),
1262 p.market.collateral_asset.as_ref().map(|c| c.symbol.clone()),
1263 p.market.collateral_asset.as_ref().map(|c| c.address.as_str()),
1264 );
1265
1266 UserMarketPosition::from_gql(
1267 p.id,
1268 &p.supply_shares,
1269 &p.supply_assets,
1270 p.supply_assets_usd,
1271 &p.borrow_shares,
1272 &p.borrow_assets,
1273 p.borrow_assets_usd,
1274 &p.collateral,
1275 p.collateral_usd,
1276 p.health_factor,
1277 market,
1278 )
1279}