morpho_rs_api/
client.rs

1//! Vault client implementations for V1 and V2 vaults.
2
3use 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
26/// Default Morpho GraphQL API endpoint.
27pub const DEFAULT_API_URL: &str = "https://api.morpho.org/graphql";
28
29/// Default page size for paginated queries.
30pub const DEFAULT_PAGE_SIZE: i64 = 100;
31
32/// Configuration for vault clients.
33#[derive(Debug, Clone)]
34pub struct ClientConfig {
35    /// GraphQL API URL.
36    pub api_url: Url,
37    /// Default page size for queries.
38    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    /// Create a new configuration with default values.
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Set a custom API URL.
57    pub fn with_api_url(mut self, url: Url) -> Self {
58        self.api_url = url;
59        self
60    }
61
62    /// Set a custom page size.
63    pub fn with_page_size(mut self, size: i64) -> Self {
64        self.page_size = size;
65        self
66    }
67}
68
69/// Client for querying V1 (MetaMorpho) vaults.
70#[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    /// Create a new V1 vault client with default configuration.
84    pub fn new() -> Self {
85        Self {
86            http_client: Client::new(),
87            config: ClientConfig::default(),
88        }
89    }
90
91    /// Create a new V1 vault client with custom configuration.
92    pub fn with_config(config: ClientConfig) -> Self {
93        Self {
94            http_client: Client::new(),
95            config,
96        }
97    }
98
99    /// Execute a GraphQL query.
100    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    /// Get V1 vaults with optional filters.
132    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    /// Get a single V1 vault by address and chain.
155    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    /// Get V1 vaults on a specific chain.
170    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    /// Get V1 vaults by curator address.
176    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    /// Get whitelisted (listed) V1 vaults.
189    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/// Client for querying V2 vaults.
199#[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    /// Create a new V2 vault client with default configuration.
213    pub fn new() -> Self {
214        Self {
215            http_client: Client::new(),
216            config: ClientConfig::default(),
217        }
218    }
219
220    /// Create a new V2 vault client with custom configuration.
221    pub fn with_config(config: ClientConfig) -> Self {
222        Self {
223            http_client: Client::new(),
224            config,
225        }
226    }
227
228    /// Execute a GraphQL query.
229    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    /// Get V2 vaults with optional filters.
261    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    /// Get a single V2 vault by address and chain.
284    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    /// Get V2 vaults on a specific chain.
299    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    /// Get whitelisted (listed) V2 vaults.
305    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/// Combined client for querying both V1 and V2 vaults.
315#[derive(Debug, Clone)]
316pub struct VaultClient {
317    /// V1 vault client.
318    pub v1: VaultV1Client,
319    /// V2 vault client.
320    pub v2: VaultV2Client,
321}
322
323impl Default for VaultClient {
324    fn default() -> Self {
325        Self::new()
326    }
327}
328
329impl VaultClient {
330    /// Create a new combined vault client with default configuration.
331    pub fn new() -> Self {
332        Self {
333            v1: VaultV1Client::new(),
334            v2: VaultV2Client::new(),
335        }
336    }
337
338    /// Create a new combined vault client with custom configuration.
339    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    /// Get vaults (V1 and V2) on a specific chain as unified Vault type.
347    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    /// Get whitelisted vaults (V1 and V2) as unified Vault type.
361    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    /// Execute a GraphQL query.
375    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    /// Get all vault positions (V1 and V2) for a user.
405    ///
406    /// If `chain` is `Some`, queries only that chain.
407    /// If `chain` is `None`, queries all supported chains and aggregates results.
408    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    /// Get vault positions for a user on a single chain.
420    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    /// Get vault positions for a user across all chains.
456    async fn get_user_vault_positions_all_chains(
457        &self,
458        address: &str,
459    ) -> Result<UserVaultPositions> {
460        use futures::future::join_all;
461
462        // Filter chains to those with IDs that fit in GraphQL Int (32-bit signed)
463        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                // Ignore "No results" errors - user just has no positions on that chain
490                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    /// Get complete account overview for a user on a specific chain.
503    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
570// Conversion functions from GraphQL types to our types
571
572fn 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
770/// Parse yearly supply tokens from string to f64.
771fn 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
863// User position conversion functions
864
865fn 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}