1use graphql_client::{GraphQLQuery, Response};
4use reqwest::Client;
5use url::Url;
6
7use crate::error::{ApiError, Result};
8use crate::filters::{VaultFiltersV1, VaultFiltersV2};
9use crate::queries::v1::{
10 get_vault_v1_by_address, get_vaults_v1, GetVaultV1ByAddress, GetVaultsV1,
11};
12use crate::queries::user::{
13 get_user_account_overview, get_user_vault_positions, GetUserAccountOverview,
14 GetUserVaultPositions,
15};
16use crate::queries::v2::{
17 get_vault_v2_by_address, get_vaults_v2, GetVaultV2ByAddress, GetVaultsV2,
18};
19use crate::types::{
20 Asset, Chain, MarketInfo, UserAccountOverview, UserMarketPosition, UserState,
21 UserVaultPositions, UserVaultV1Position, UserVaultV2Position, Vault, VaultAdapter,
22 VaultAllocation, VaultAllocator, VaultInfo, VaultPositionState, VaultReward, VaultStateV1,
23 VaultV1, VaultV2, VaultV2Warning, VaultWarning,
24};
25
26pub const DEFAULT_API_URL: &str = "https://api.morpho.org/graphql";
28
29pub const DEFAULT_PAGE_SIZE: i64 = 100;
31
32#[derive(Debug, Clone)]
34pub struct ClientConfig {
35 pub api_url: Url,
37 pub page_size: i64,
39}
40
41impl Default for ClientConfig {
42 fn default() -> Self {
43 Self {
44 api_url: Url::parse(DEFAULT_API_URL).expect("Invalid default API URL"),
45 page_size: DEFAULT_PAGE_SIZE,
46 }
47 }
48}
49
50impl ClientConfig {
51 pub fn new() -> Self {
53 Self::default()
54 }
55
56 pub fn with_api_url(mut self, url: Url) -> Self {
58 self.api_url = url;
59 self
60 }
61
62 pub fn with_page_size(mut self, size: i64) -> Self {
64 self.page_size = size;
65 self
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct VaultV1Client {
72 http_client: Client,
73 config: ClientConfig,
74}
75
76impl Default for VaultV1Client {
77 fn default() -> Self {
78 Self::new()
79 }
80}
81
82impl VaultV1Client {
83 pub fn new() -> Self {
85 Self {
86 http_client: Client::new(),
87 config: ClientConfig::default(),
88 }
89 }
90
91 pub fn with_config(config: ClientConfig) -> Self {
93 Self {
94 http_client: Client::new(),
95 config,
96 }
97 }
98
99 async fn execute<Q: GraphQLQuery>(
101 &self,
102 variables: Q::Variables,
103 ) -> Result<Q::ResponseData> {
104 let request_body = Q::build_query(variables);
105 let response = self
106 .http_client
107 .post(self.config.api_url.as_str())
108 .json(&request_body)
109 .send()
110 .await?;
111
112 let response_body: Response<Q::ResponseData> = response.json().await?;
113
114 if let Some(errors) = response_body.errors {
115 if !errors.is_empty() {
116 return Err(ApiError::GraphQL(
117 errors
118 .iter()
119 .map(|e| e.message.clone())
120 .collect::<Vec<_>>()
121 .join("; "),
122 ));
123 }
124 }
125
126 response_body
127 .data
128 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
129 }
130
131 pub async fn get_vaults(&self, filters: Option<VaultFiltersV1>) -> Result<Vec<VaultV1>> {
133 let variables = get_vaults_v1::Variables {
134 first: Some(self.config.page_size),
135 skip: Some(0),
136 where_: filters.map(|f| f.to_gql()),
137 };
138
139 let data = self.execute::<GetVaultsV1>(variables).await?;
140
141 let items = match data.vaults.items {
142 Some(items) => items,
143 None => return Ok(Vec::new()),
144 };
145
146 let vaults: Vec<VaultV1> = items
147 .into_iter()
148 .filter_map(convert_v1_vault)
149 .collect();
150
151 Ok(vaults)
152 }
153
154 pub async fn get_vault(&self, address: &str, chain: Chain) -> Result<VaultV1> {
156 let variables = get_vault_v1_by_address::Variables {
157 address: address.to_string(),
158 chain_id: chain.id() as i64,
159 };
160
161 let data = self.execute::<GetVaultV1ByAddress>(variables).await?;
162
163 convert_v1_vault_single(data.vault_by_address).ok_or_else(|| ApiError::VaultNotFound {
164 address: address.to_string(),
165 chain_id: chain.id(),
166 })
167 }
168
169 pub async fn get_vaults_by_chain(&self, chain: Chain) -> Result<Vec<VaultV1>> {
171 let filters = VaultFiltersV1::new().chain(chain);
172 self.get_vaults(Some(filters)).await
173 }
174
175 pub async fn get_vaults_by_curator(
177 &self,
178 curator: &str,
179 chain: Option<Chain>,
180 ) -> Result<Vec<VaultV1>> {
181 let mut filters = VaultFiltersV1::new().curators([curator]);
182 if let Some(c) = chain {
183 filters = filters.chain(c);
184 }
185 self.get_vaults(Some(filters)).await
186 }
187
188 pub async fn get_whitelisted_vaults(&self, chain: Option<Chain>) -> Result<Vec<VaultV1>> {
190 let mut filters = VaultFiltersV1::new().listed(true);
191 if let Some(c) = chain {
192 filters = filters.chain(c);
193 }
194 self.get_vaults(Some(filters)).await
195 }
196}
197
198#[derive(Debug, Clone)]
200pub struct VaultV2Client {
201 http_client: Client,
202 config: ClientConfig,
203}
204
205impl Default for VaultV2Client {
206 fn default() -> Self {
207 Self::new()
208 }
209}
210
211impl VaultV2Client {
212 pub fn new() -> Self {
214 Self {
215 http_client: Client::new(),
216 config: ClientConfig::default(),
217 }
218 }
219
220 pub fn with_config(config: ClientConfig) -> Self {
222 Self {
223 http_client: Client::new(),
224 config,
225 }
226 }
227
228 async fn execute<Q: GraphQLQuery>(
230 &self,
231 variables: Q::Variables,
232 ) -> Result<Q::ResponseData> {
233 let request_body = Q::build_query(variables);
234 let response = self
235 .http_client
236 .post(self.config.api_url.as_str())
237 .json(&request_body)
238 .send()
239 .await?;
240
241 let response_body: Response<Q::ResponseData> = response.json().await?;
242
243 if let Some(errors) = response_body.errors {
244 if !errors.is_empty() {
245 return Err(ApiError::GraphQL(
246 errors
247 .iter()
248 .map(|e| e.message.clone())
249 .collect::<Vec<_>>()
250 .join("; "),
251 ));
252 }
253 }
254
255 response_body
256 .data
257 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
258 }
259
260 pub async fn get_vaults(&self, filters: Option<VaultFiltersV2>) -> Result<Vec<VaultV2>> {
262 let variables = get_vaults_v2::Variables {
263 first: Some(self.config.page_size),
264 skip: Some(0),
265 where_: filters.map(|f| f.to_gql()),
266 };
267
268 let data = self.execute::<GetVaultsV2>(variables).await?;
269
270 let items = match data.vault_v2s.items {
271 Some(items) => items,
272 None => return Ok(Vec::new()),
273 };
274
275 let vaults: Vec<VaultV2> = items
276 .into_iter()
277 .filter_map(convert_v2_vault)
278 .collect();
279
280 Ok(vaults)
281 }
282
283 pub async fn get_vault(&self, address: &str, chain: Chain) -> Result<VaultV2> {
285 let variables = get_vault_v2_by_address::Variables {
286 address: address.to_string(),
287 chain_id: chain.id() as i64,
288 };
289
290 let data = self.execute::<GetVaultV2ByAddress>(variables).await?;
291
292 convert_v2_vault_single(data.vault_v2_by_address).ok_or_else(|| ApiError::VaultNotFound {
293 address: address.to_string(),
294 chain_id: chain.id(),
295 })
296 }
297
298 pub async fn get_vaults_by_chain(&self, chain: Chain) -> Result<Vec<VaultV2>> {
300 let filters = VaultFiltersV2::new().chain(chain);
301 self.get_vaults(Some(filters)).await
302 }
303
304 pub async fn get_whitelisted_vaults(&self, chain: Option<Chain>) -> Result<Vec<VaultV2>> {
306 let mut filters = VaultFiltersV2::new().listed(true);
307 if let Some(c) = chain {
308 filters = filters.chain(c);
309 }
310 self.get_vaults(Some(filters)).await
311 }
312}
313
314#[derive(Debug, Clone)]
316pub struct VaultClient {
317 pub v1: VaultV1Client,
319 pub v2: VaultV2Client,
321}
322
323impl Default for VaultClient {
324 fn default() -> Self {
325 Self::new()
326 }
327}
328
329impl VaultClient {
330 pub fn new() -> Self {
332 Self {
333 v1: VaultV1Client::new(),
334 v2: VaultV2Client::new(),
335 }
336 }
337
338 pub fn with_config(config: ClientConfig) -> Self {
340 Self {
341 v1: VaultV1Client::with_config(config.clone()),
342 v2: VaultV2Client::with_config(config),
343 }
344 }
345
346 pub async fn get_vaults_by_chain(&self, chain: Chain) -> Result<Vec<Vault>> {
348 let (v1_vaults, v2_vaults) = tokio::try_join!(
349 self.v1.get_vaults_by_chain(chain),
350 self.v2.get_vaults_by_chain(chain),
351 )?;
352
353 let mut vaults: Vec<Vault> = Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
354 vaults.extend(v1_vaults.into_iter().map(Vault::from));
355 vaults.extend(v2_vaults.into_iter().map(Vault::from));
356
357 Ok(vaults)
358 }
359
360 pub async fn get_whitelisted_vaults(&self, chain: Option<Chain>) -> Result<Vec<Vault>> {
362 let (v1_vaults, v2_vaults) = tokio::try_join!(
363 self.v1.get_whitelisted_vaults(chain),
364 self.v2.get_whitelisted_vaults(chain),
365 )?;
366
367 let mut vaults: Vec<Vault> = Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
368 vaults.extend(v1_vaults.into_iter().map(Vault::from));
369 vaults.extend(v2_vaults.into_iter().map(Vault::from));
370
371 Ok(vaults)
372 }
373
374 async fn execute<Q: GraphQLQuery>(&self, variables: Q::Variables) -> Result<Q::ResponseData> {
376 let request_body = Q::build_query(variables);
377 let response = self
378 .v1
379 .http_client
380 .post(self.v1.config.api_url.as_str())
381 .json(&request_body)
382 .send()
383 .await?;
384
385 let response_body: Response<Q::ResponseData> = response.json().await?;
386
387 if let Some(errors) = response_body.errors {
388 if !errors.is_empty() {
389 return Err(ApiError::GraphQL(
390 errors
391 .iter()
392 .map(|e| e.message.clone())
393 .collect::<Vec<_>>()
394 .join("; "),
395 ));
396 }
397 }
398
399 response_body
400 .data
401 .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
402 }
403
404 pub async fn get_user_vault_positions(
409 &self,
410 address: &str,
411 chain: Option<Chain>,
412 ) -> Result<UserVaultPositions> {
413 match chain {
414 Some(c) => self.get_user_vault_positions_single_chain(address, c).await,
415 None => self.get_user_vault_positions_all_chains(address).await,
416 }
417 }
418
419 async fn get_user_vault_positions_single_chain(
421 &self,
422 address: &str,
423 chain: Chain,
424 ) -> Result<UserVaultPositions> {
425 let variables = get_user_vault_positions::Variables {
426 address: address.to_string(),
427 chain_id: chain.id(),
428 };
429
430 let data = self.execute::<GetUserVaultPositions>(variables).await?;
431 let user = data.user_by_address;
432
433 let vault_positions: Vec<UserVaultV1Position> = user
434 .vault_positions
435 .into_iter()
436 .filter_map(convert_user_vault_v1_position)
437 .collect();
438
439 let vault_v2_positions: Vec<UserVaultV2Position> = user
440 .vault_v2_positions
441 .into_iter()
442 .filter_map(convert_user_vault_v2_position)
443 .collect();
444
445 Ok(UserVaultPositions {
446 address: user
447 .address
448 .parse()
449 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
450 vault_positions,
451 vault_v2_positions,
452 })
453 }
454
455 async fn get_user_vault_positions_all_chains(
457 &self,
458 address: &str,
459 ) -> Result<UserVaultPositions> {
460 use futures::future::join_all;
461
462 let valid_chains: Vec<_> = Chain::all()
464 .iter()
465 .filter(|chain| chain.id() <= i32::MAX as i64)
466 .copied()
467 .collect();
468
469 let futures: Vec<_> = valid_chains
470 .iter()
471 .map(|chain| self.get_user_vault_positions_single_chain(address, *chain))
472 .collect();
473
474 let results = join_all(futures).await;
475
476 let parsed_address = address
477 .parse()
478 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?;
479
480 let mut all_v1_positions = Vec::new();
481 let mut all_v2_positions = Vec::new();
482
483 for result in results {
484 match result {
485 Ok(positions) => {
486 all_v1_positions.extend(positions.vault_positions);
487 all_v2_positions.extend(positions.vault_v2_positions);
488 }
489 Err(ApiError::GraphQL(msg)) if msg.contains("No results") => continue,
491 Err(e) => return Err(e),
492 }
493 }
494
495 Ok(UserVaultPositions {
496 address: parsed_address,
497 vault_positions: all_v1_positions,
498 vault_v2_positions: all_v2_positions,
499 })
500 }
501
502 pub async fn get_user_account_overview(
504 &self,
505 address: &str,
506 chain: Chain,
507 ) -> Result<UserAccountOverview> {
508 let variables = get_user_account_overview::Variables {
509 address: address.to_string(),
510 chain_id: chain.id(),
511 };
512
513 let data = self.execute::<GetUserAccountOverview>(variables).await?;
514 let user = data.user_by_address;
515
516 let state = UserState::from_gql(
517 user.state.vaults_pnl_usd,
518 user.state.vaults_roe_usd,
519 user.state.vaults_assets_usd,
520 user.state.vault_v2s_pnl_usd,
521 user.state.vault_v2s_roe_usd,
522 user.state.vault_v2s_assets_usd,
523 user.state.markets_pnl_usd,
524 user.state.markets_roe_usd,
525 user.state.markets_supply_pnl_usd,
526 user.state.markets_supply_roe_usd,
527 user.state.markets_borrow_pnl_usd,
528 user.state.markets_borrow_roe_usd,
529 user.state.markets_collateral_pnl_usd,
530 user.state.markets_collateral_roe_usd,
531 user.state.markets_margin_pnl_usd,
532 user.state.markets_margin_roe_usd,
533 user.state.markets_collateral_usd,
534 user.state.markets_supply_assets_usd,
535 user.state.markets_borrow_assets_usd,
536 user.state.markets_margin_usd,
537 );
538
539 let vault_positions: Vec<UserVaultV1Position> = user
540 .vault_positions
541 .into_iter()
542 .filter_map(convert_user_vault_v1_position_overview)
543 .collect();
544
545 let vault_v2_positions: Vec<UserVaultV2Position> = user
546 .vault_v2_positions
547 .into_iter()
548 .filter_map(convert_user_vault_v2_position_overview)
549 .collect();
550
551 let market_positions: Vec<UserMarketPosition> = user
552 .market_positions
553 .into_iter()
554 .filter_map(convert_user_market_position)
555 .collect();
556
557 Ok(UserAccountOverview {
558 address: user
559 .address
560 .parse()
561 .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
562 state,
563 vault_positions,
564 vault_v2_positions,
565 market_positions,
566 })
567 }
568}
569
570fn convert_v1_vault(v: get_vaults_v1::GetVaultsV1VaultsItems) -> Option<VaultV1> {
573 let chain_id = v.chain.id;
574 let asset = &v.asset;
575
576 VaultV1::from_gql(
577 &v.address,
578 v.name,
579 v.symbol,
580 chain_id,
581 v.listed,
582 v.featured,
583 v.whitelisted,
584 Asset::from_gql(
585 &asset.address,
586 asset.symbol.clone(),
587 Some(asset.name.clone()),
588 asset.decimals,
589 asset.price_usd,
590 )?,
591 v.state.as_ref().and_then(convert_v1_state),
592 v.allocators
593 .into_iter()
594 .filter_map(|a| VaultAllocator::from_gql(&a.address))
595 .collect(),
596 v.warnings
597 .into_iter()
598 .map(|w| VaultWarning {
599 warning_type: w.type_.clone(),
600 level: format!("{:?}", w.level),
601 })
602 .collect(),
603 )
604}
605
606fn convert_v1_vault_single(
607 v: get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddress,
608) -> Option<VaultV1> {
609 let chain_id = v.chain.id;
610 let asset = &v.asset;
611
612 VaultV1::from_gql(
613 &v.address,
614 v.name,
615 v.symbol,
616 chain_id,
617 v.listed,
618 v.featured,
619 v.whitelisted,
620 Asset::from_gql(
621 &asset.address,
622 asset.symbol.clone(),
623 Some(asset.name.clone()),
624 asset.decimals,
625 asset.price_usd,
626 )?,
627 v.state.as_ref().and_then(convert_v1_state_single),
628 v.allocators
629 .into_iter()
630 .filter_map(|a| VaultAllocator::from_gql(&a.address))
631 .collect(),
632 v.warnings
633 .into_iter()
634 .map(|w| VaultWarning {
635 warning_type: w.type_.clone(),
636 level: format!("{:?}", w.level),
637 })
638 .collect(),
639 )
640}
641
642fn convert_v1_state(s: &get_vaults_v1::GetVaultsV1VaultsItemsState) -> Option<VaultStateV1> {
643 VaultStateV1::from_gql(
644 Some(s.curator.as_str()),
645 Some(s.owner.as_str()),
646 Some(s.guardian.as_str()),
647 &s.total_assets,
648 s.total_assets_usd,
649 &s.total_supply,
650 s.fee,
651 &s.timelock,
652 s.apy,
653 s.net_apy,
654 s.share_price.as_deref().unwrap_or("0"),
655 s.allocation
656 .iter()
657 .filter_map(|a| {
658 let market = &a.market;
659 VaultAllocation::from_gql(
660 market.unique_key.clone(),
661 Some(market.loan_asset.symbol.clone()),
662 Some(market.loan_asset.address.as_str()),
663 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
664 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
665 &a.supply_assets,
666 a.supply_assets_usd,
667 &a.supply_cap,
668 )
669 })
670 .collect(),
671 )
672}
673
674fn convert_v1_state_single(
675 s: &get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddressState,
676) -> Option<VaultStateV1> {
677 VaultStateV1::from_gql(
678 Some(s.curator.as_str()),
679 Some(s.owner.as_str()),
680 Some(s.guardian.as_str()),
681 &s.total_assets,
682 s.total_assets_usd,
683 &s.total_supply,
684 s.fee,
685 &s.timelock,
686 s.apy,
687 s.net_apy,
688 s.share_price.as_deref().unwrap_or("0"),
689 s.allocation
690 .iter()
691 .filter_map(|a| {
692 let market = &a.market;
693 VaultAllocation::from_gql(
694 market.unique_key.clone(),
695 Some(market.loan_asset.symbol.clone()),
696 Some(market.loan_asset.address.as_str()),
697 market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
698 market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
699 &a.supply_assets,
700 a.supply_assets_usd,
701 &a.supply_cap,
702 )
703 })
704 .collect(),
705 )
706}
707
708fn convert_v2_vault(v: get_vaults_v2::GetVaultsV2VaultV2sItems) -> Option<VaultV2> {
709 let chain_id = v.chain.id;
710 let asset = &v.asset;
711
712 VaultV2::from_gql(
713 &v.address,
714 v.name,
715 v.symbol,
716 chain_id,
717 v.listed,
718 v.whitelisted,
719 Asset::from_gql(
720 &asset.address,
721 asset.symbol.clone(),
722 Some(asset.name.clone()),
723 asset.decimals,
724 asset.price_usd,
725 )?,
726 Some(v.curator.address.as_str()),
727 Some(v.owner.address.as_str()),
728 v.total_assets.as_deref().unwrap_or("0"),
729 v.total_assets_usd,
730 &v.total_supply,
731 Some(v.share_price),
732 Some(v.performance_fee),
733 Some(v.management_fee),
734 v.avg_apy,
735 v.avg_net_apy,
736 v.apy,
737 v.net_apy,
738 &v.liquidity,
739 v.liquidity_usd,
740 v.adapters
741 .items
742 .map(|items| {
743 items
744 .into_iter()
745 .filter_map(convert_v2_adapter)
746 .collect()
747 })
748 .unwrap_or_default(),
749 v.rewards
750 .into_iter()
751 .filter_map(|r| {
752 VaultReward::from_gql(
753 &r.asset.address,
754 r.asset.symbol.clone(),
755 r.supply_apr,
756 parse_yearly_supply(&r.yearly_supply_tokens),
757 )
758 })
759 .collect(),
760 v.warnings
761 .into_iter()
762 .map(|w| VaultV2Warning {
763 warning_type: w.type_.clone(),
764 level: format!("{:?}", w.level),
765 })
766 .collect(),
767 )
768}
769
770fn parse_yearly_supply(s: &str) -> Option<f64> {
772 s.parse::<f64>().ok()
773}
774
775fn convert_v2_vault_single(
776 v: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddress,
777) -> Option<VaultV2> {
778 let chain_id = v.chain.id;
779 let asset = &v.asset;
780
781 VaultV2::from_gql(
782 &v.address,
783 v.name,
784 v.symbol,
785 chain_id,
786 v.listed,
787 v.whitelisted,
788 Asset::from_gql(
789 &asset.address,
790 asset.symbol.clone(),
791 Some(asset.name.clone()),
792 asset.decimals,
793 asset.price_usd,
794 )?,
795 Some(v.curator.address.as_str()),
796 Some(v.owner.address.as_str()),
797 v.total_assets.as_deref().unwrap_or("0"),
798 v.total_assets_usd,
799 &v.total_supply,
800 Some(v.share_price),
801 Some(v.performance_fee),
802 Some(v.management_fee),
803 v.avg_apy,
804 v.avg_net_apy,
805 v.apy,
806 v.net_apy,
807 &v.liquidity,
808 v.liquidity_usd,
809 v.adapters
810 .items
811 .map(|items| {
812 items
813 .into_iter()
814 .filter_map(convert_v2_adapter_single)
815 .collect()
816 })
817 .unwrap_or_default(),
818 v.rewards
819 .into_iter()
820 .filter_map(|r| {
821 VaultReward::from_gql(
822 &r.asset.address,
823 r.asset.symbol.clone(),
824 r.supply_apr,
825 parse_yearly_supply(&r.yearly_supply_tokens),
826 )
827 })
828 .collect(),
829 v.warnings
830 .into_iter()
831 .map(|w| VaultV2Warning {
832 warning_type: w.type_.clone(),
833 level: format!("{:?}", w.level),
834 })
835 .collect(),
836 )
837}
838
839fn convert_v2_adapter(
840 a: get_vaults_v2::GetVaultsV2VaultV2sItemsAdaptersItems,
841) -> Option<VaultAdapter> {
842 VaultAdapter::from_gql(
843 a.id,
844 &a.address,
845 format!("{:?}", a.type_),
846 &a.assets,
847 a.assets_usd,
848 )
849}
850
851fn convert_v2_adapter_single(
852 a: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddressAdaptersItems,
853) -> Option<VaultAdapter> {
854 VaultAdapter::from_gql(
855 a.id,
856 &a.address,
857 format!("{:?}", a.type_),
858 &a.assets,
859 a.assets_usd,
860 )
861}
862
863fn convert_user_vault_v1_position(
866 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultPositions,
867) -> Option<UserVaultV1Position> {
868 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol)?;
869
870 let state = p.state.as_ref().and_then(|s| {
871 VaultPositionState::from_gql(
872 &s.shares,
873 s.assets.as_deref(),
874 s.assets_usd,
875 s.pnl.as_deref(),
876 s.pnl_usd,
877 s.roe,
878 s.roe_usd,
879 )
880 });
881
882 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
883}
884
885fn convert_user_vault_v2_position(
886 p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultV2Positions,
887) -> Option<UserVaultV2Position> {
888 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol)?;
889
890 UserVaultV2Position::from_gql(
891 p.id,
892 &p.shares,
893 &p.assets,
894 p.assets_usd,
895 p.pnl.as_deref(),
896 p.pnl_usd,
897 p.roe,
898 p.roe_usd,
899 vault,
900 )
901}
902
903fn convert_user_vault_v1_position_overview(
904 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultPositions,
905) -> Option<UserVaultV1Position> {
906 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol)?;
907
908 let state = p.state.as_ref().and_then(|s| {
909 VaultPositionState::from_gql(
910 &s.shares,
911 s.assets.as_deref(),
912 s.assets_usd,
913 s.pnl.as_deref(),
914 s.pnl_usd,
915 s.roe,
916 s.roe_usd,
917 )
918 });
919
920 UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
921}
922
923fn convert_user_vault_v2_position_overview(
924 p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultV2Positions,
925) -> Option<UserVaultV2Position> {
926 let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol)?;
927
928 UserVaultV2Position::from_gql(
929 p.id,
930 &p.shares,
931 &p.assets,
932 p.assets_usd,
933 p.pnl.as_deref(),
934 p.pnl_usd,
935 p.roe,
936 p.roe_usd,
937 vault,
938 )
939}
940
941fn convert_user_market_position(
942 p: get_user_account_overview::GetUserAccountOverviewUserByAddressMarketPositions,
943) -> Option<UserMarketPosition> {
944 let market = MarketInfo::from_gql(
945 p.market.unique_key,
946 Some(p.market.loan_asset.symbol),
947 Some(p.market.loan_asset.address.as_str()),
948 p.market.collateral_asset.as_ref().map(|c| c.symbol.clone()),
949 p.market.collateral_asset.as_ref().map(|c| c.address.as_str()),
950 );
951
952 UserMarketPosition::from_gql(
953 p.id,
954 &p.supply_shares,
955 &p.supply_assets,
956 p.supply_assets_usd,
957 &p.borrow_shares,
958 &p.borrow_assets,
959 p.borrow_assets_usd,
960 &p.collateral,
961 p.collateral_usd,
962 p.health_factor,
963 market,
964 )
965}