morpho_rs_api/
client.rs

1//! Vault client implementations for V1 and V2 vaults.
2
3use 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
30/// Default Morpho GraphQL API endpoint.
31pub const DEFAULT_API_URL: &str = "https://api.morpho.org/graphql";
32
33/// Default page size for paginated queries.
34pub const DEFAULT_PAGE_SIZE: i64 = 100;
35
36/// Configuration for vault clients.
37#[derive(Debug, Clone)]
38pub struct ClientConfig {
39    /// GraphQL API URL.
40    pub api_url: Url,
41    /// Default page size for queries.
42    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    /// Create a new configuration with default values.
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Set a custom API URL.
61    pub fn with_api_url(mut self, url: Url) -> Self {
62        self.api_url = url;
63        self
64    }
65
66    /// Set a custom page size.
67    pub fn with_page_size(mut self, size: i64) -> Self {
68        self.page_size = size;
69        self
70    }
71}
72
73/// Client for querying V1 (MetaMorpho) vaults.
74#[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    /// Create a new V1 vault client with default configuration.
88    pub fn new() -> Self {
89        Self {
90            http_client: Client::new(),
91            config: ClientConfig::default(),
92        }
93    }
94
95    /// Create a new V1 vault client with custom configuration.
96    pub fn with_config(config: ClientConfig) -> Self {
97        Self {
98            http_client: Client::new(),
99            config,
100        }
101    }
102
103    /// Execute a GraphQL query.
104    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    /// Get V1 vaults with optional filters.
136    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    /// Get a single V1 vault by address and chain.
161    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    /// Get V1 vaults on a specific chain.
176    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    /// Get V1 vaults by curator address.
182    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    /// Get whitelisted (listed) V1 vaults.
195    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    /// Get V1 vaults with query options (filters, ordering, and limit).
204    ///
205    /// This method provides full control over the query parameters including
206    /// ordering by various fields like APY, total assets, etc.
207    ///
208    /// # Example
209    ///
210    /// ```no_run
211    /// use morpho_rs_api::{VaultV1Client, VaultQueryOptionsV1, VaultFiltersV1, VaultOrderByV1, OrderDirection, NamedChain};
212    ///
213    /// #[tokio::main]
214    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
215    ///     let client = VaultV1Client::new();
216    ///
217    ///     // Get top 10 USDC vaults by APY on Ethereum
218    ///     let options = VaultQueryOptionsV1::new()
219    ///         .filters(VaultFiltersV1::new()
220    ///             .chain(NamedChain::Mainnet)
221    ///             .asset_symbols(["USDC"]))
222    ///         .order_by(VaultOrderByV1::NetApy)
223    ///         .order_direction(OrderDirection::Desc)
224    ///         .limit(10);
225    ///
226    ///     let vaults = client.get_vaults_with_options(options).await?;
227    ///     Ok(())
228    /// }
229    /// ```
230    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    /// Get top N V1 vaults ordered by APY (highest first).
258    ///
259    /// # Example
260    ///
261    /// ```no_run
262    /// use morpho_rs_api::{VaultV1Client, VaultFiltersV1, NamedChain};
263    ///
264    /// #[tokio::main]
265    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
266    ///     let client = VaultV1Client::new();
267    ///
268    ///     // Get top 10 vaults by APY on Ethereum
269    ///     let filters = VaultFiltersV1::new().chain(NamedChain::Mainnet);
270    ///     let vaults = client.get_top_vaults_by_apy(10, Some(filters)).await?;
271    ///     Ok(())
272    /// }
273    /// ```
274    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    /// Get V1 vaults for a specific deposit asset.
289    ///
290    /// # Example
291    ///
292    /// ```no_run
293    /// use morpho_rs_api::{VaultV1Client, NamedChain};
294    ///
295    /// #[tokio::main]
296    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
297    ///     let client = VaultV1Client::new();
298    ///
299    ///     // Get all USDC vaults
300    ///     let vaults = client.get_vaults_by_asset("USDC", None).await?;
301    ///     Ok(())
302    /// }
303    /// ```
304    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/// Client for querying V2 vaults.
318#[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    /// Create a new V2 vault client with default configuration.
332    pub fn new() -> Self {
333        Self {
334            http_client: Client::new(),
335            config: ClientConfig::default(),
336        }
337    }
338
339    /// Create a new V2 vault client with custom configuration.
340    pub fn with_config(config: ClientConfig) -> Self {
341        Self {
342            http_client: Client::new(),
343            config,
344        }
345    }
346
347    /// Execute a GraphQL query.
348    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    /// Get V2 vaults with optional filters.
380    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    /// Get a single V2 vault by address and chain.
405    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    /// Get V2 vaults on a specific chain.
420    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    /// Get whitelisted (listed) V2 vaults.
426    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    /// Get V2 vaults with query options (filters, ordering, and limit).
435    ///
436    /// This method provides full control over the query parameters including
437    /// ordering by various fields like APY, total assets, liquidity, etc.
438    ///
439    /// Note: Asset filtering (by symbol or address) is done client-side since
440    /// the Morpho V2 API doesn't support server-side asset filtering.
441    ///
442    /// # Example
443    ///
444    /// ```no_run
445    /// use morpho_rs_api::{VaultV2Client, VaultQueryOptionsV2, VaultFiltersV2, VaultOrderByV2, OrderDirection, NamedChain};
446    ///
447    /// #[tokio::main]
448    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
449    ///     let client = VaultV2Client::new();
450    ///
451    ///     // Get top 10 USDC vaults by APY on Ethereum
452    ///     let options = VaultQueryOptionsV2::new()
453    ///         .filters(VaultFiltersV2::new()
454    ///             .chain(NamedChain::Mainnet))
455    ///         .order_by(VaultOrderByV2::NetApy)
456    ///         .order_direction(OrderDirection::Desc)
457    ///         .asset_symbols(["USDC"])  // Client-side filtering
458    ///         .limit(10);
459    ///
460    ///     let vaults = client.get_vaults_with_options(options).await?;
461    ///     Ok(())
462    /// }
463    /// ```
464    pub async fn get_vaults_with_options(
465        &self,
466        options: VaultQueryOptionsV2,
467    ) -> Result<Vec<VaultV2>> {
468        // When using client-side asset filtering, we may need to fetch more results
469        // to ensure we have enough after filtering
470        let fetch_limit = if options.has_asset_filter() {
471            // Fetch more if we're going to filter client-side
472            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        // Apply client-side asset filtering
498        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        // Apply limit after client-side filtering
510        if let Some(limit) = options.limit {
511            vaults.truncate(limit as usize);
512        }
513
514        Ok(vaults)
515    }
516
517    /// Get top N V2 vaults ordered by APY (highest first).
518    ///
519    /// # Example
520    ///
521    /// ```no_run
522    /// use morpho_rs_api::{VaultV2Client, VaultFiltersV2, NamedChain};
523    ///
524    /// #[tokio::main]
525    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
526    ///     let client = VaultV2Client::new();
527    ///
528    ///     // Get top 10 vaults by APY on Ethereum
529    ///     let filters = VaultFiltersV2::new().chain(NamedChain::Mainnet);
530    ///     let vaults = client.get_top_vaults_by_apy(10, Some(filters)).await?;
531    ///     Ok(())
532    /// }
533    /// ```
534    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    /// Get V2 vaults for a specific deposit asset.
551    ///
552    /// Note: This filtering is done client-side since the Morpho V2 API
553    /// doesn't support server-side asset filtering.
554    ///
555    /// # Example
556    ///
557    /// ```no_run
558    /// use morpho_rs_api::{VaultV2Client, NamedChain};
559    ///
560    /// #[tokio::main]
561    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
562    ///     let client = VaultV2Client::new();
563    ///
564    ///     // Get all USDC vaults
565    ///     let vaults = client.get_vaults_by_asset("USDC", None).await?;
566    ///     Ok(())
567    /// }
568    /// ```
569    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/// Combined client for querying both V1 and V2 vaults via the GraphQL API.
588#[derive(Debug, Clone)]
589pub struct MorphoApiClient {
590    /// V1 vault client.
591    pub v1: VaultV1Client,
592    /// V2 vault client.
593    pub v2: VaultV2Client,
594}
595
596impl Default for MorphoApiClient {
597    fn default() -> Self {
598        Self::new()
599    }
600}
601
602impl MorphoApiClient {
603    /// Create a new combined vault client with default configuration.
604    pub fn new() -> Self {
605        Self {
606            v1: VaultV1Client::new(),
607            v2: VaultV2Client::new(),
608        }
609    }
610
611    /// Create a new combined vault client with custom configuration.
612    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    /// Get vaults (V1 and V2) on a specific chain as unified Vault type.
620    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    /// Get whitelisted vaults (V1 and V2) as unified Vault type.
634    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    /// Execute a GraphQL query.
648    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    /// Get all vault positions (V1 and V2) for a user.
678    ///
679    /// If `chain` is `Some`, queries only that chain.
680    /// If `chain` is `None`, queries all supported chains and aggregates results.
681    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    /// Get vault positions for a user on a single chain.
693    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    /// Get vault positions for a user across all chains.
729    async fn get_user_vault_positions_all_chains(
730        &self,
731        address: &str,
732    ) -> Result<UserVaultPositions> {
733        use futures::future::join_all;
734
735        // Filter chains to those with IDs that fit in GraphQL Int (32-bit signed)
736        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                // Ignore "No results" errors - user just has no positions on that chain
763                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    /// Get complete account overview for a user on a specific chain.
776    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/// Configuration for the unified MorphoClient.
844#[derive(Debug, Clone)]
845pub struct MorphoClientConfig {
846    /// API configuration.
847    pub api_config: Option<ClientConfig>,
848    /// RPC URL for on-chain interactions.
849    pub rpc_url: Option<String>,
850    /// Private key for signing transactions.
851    pub private_key: Option<String>,
852    /// Whether to automatically approve tokens before deposit if allowance is insufficient.
853    /// When true, approves the exact minimal amount needed for the deposit.
854    /// Defaults to true.
855    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    /// Create a new configuration with default values.
871    pub fn new() -> Self {
872        Self::default()
873    }
874
875    /// Set the API configuration.
876    pub fn with_api_config(mut self, config: ClientConfig) -> Self {
877        self.api_config = Some(config);
878        self
879    }
880
881    /// Set the RPC URL.
882    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    /// Set the private key.
888    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    /// Set whether to automatically approve tokens before deposit.
894    /// When true, approves the exact minimal amount needed for the deposit.
895    /// Defaults to true.
896    pub fn with_auto_approve(mut self, auto_approve: bool) -> Self {
897        self.auto_approve = auto_approve;
898        self
899    }
900}
901
902/// Wrapper for V1 vault operations that automatically uses the signer's address.
903pub struct VaultV1Operations<'a> {
904    client: &'a VaultV1TransactionClient,
905    auto_approve: bool,
906}
907
908impl<'a> VaultV1Operations<'a> {
909    /// Create a new V1 operations wrapper.
910    fn new(client: &'a VaultV1TransactionClient, auto_approve: bool) -> Self {
911        Self { client, auto_approve }
912    }
913
914    /// Deposit assets into a vault, receiving shares to the signer's address.
915    ///
916    /// If `auto_approve` is enabled (default), this will check the current allowance
917    /// and approve the exact minimal amount needed for the deposit if insufficient.
918    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    /// Withdraw assets from a vault to the signer's address (withdrawing signer's shares).
942    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    /// Get the signer's vault share balance.
949    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    /// Approve a vault to spend the signer's tokens if needed.
958    /// Returns the transaction receipt if approval was performed, None if already approved.
959    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    /// Get the current allowance for the vault to spend the signer's tokens.
974    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    /// Get the underlying asset address of a vault.
984    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    /// Get the decimals of a token.
990    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    /// Get the signer's address.
996    pub fn signer_address(&self) -> Address {
997        self.client.signer_address()
998    }
999
1000    /// Check if auto_approve is enabled.
1001    pub fn auto_approve(&self) -> bool {
1002        self.auto_approve
1003    }
1004}
1005
1006/// Wrapper for V2 vault operations that automatically uses the signer's address.
1007pub struct VaultV2Operations<'a> {
1008    client: &'a VaultV2TransactionClient,
1009    auto_approve: bool,
1010}
1011
1012impl<'a> VaultV2Operations<'a> {
1013    /// Create a new V2 operations wrapper.
1014    fn new(client: &'a VaultV2TransactionClient, auto_approve: bool) -> Self {
1015        Self { client, auto_approve }
1016    }
1017
1018    /// Deposit assets into a vault, receiving shares to the signer's address.
1019    ///
1020    /// If `auto_approve` is enabled (default), this will check the current allowance
1021    /// and approve the exact minimal amount needed for the deposit if insufficient.
1022    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    /// Withdraw assets from a vault to the signer's address (withdrawing signer's shares).
1046    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    /// Get the signer's vault share balance.
1053    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    /// Approve a vault to spend the signer's tokens if needed.
1062    /// Returns the transaction receipt if approval was performed, None if already approved.
1063    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    /// Get the current allowance for the vault to spend the signer's tokens.
1078    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    /// Get the underlying asset address of a vault.
1088    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    /// Get the decimals of a token.
1094    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    /// Get the signer's address.
1100    pub fn signer_address(&self) -> Address {
1101        self.client.signer_address()
1102    }
1103
1104    /// Check if auto_approve is enabled.
1105    pub fn auto_approve(&self) -> bool {
1106        self.auto_approve
1107    }
1108}
1109
1110/// Unified Morpho client combining API queries and on-chain transactions.
1111///
1112/// This client provides a namespace-style API for interacting with Morpho vaults:
1113/// - `client.api()` - Access to GraphQL API queries
1114/// - `client.vault_v1()` - V1 vault transaction operations
1115/// - `client.vault_v2()` - V2 vault transaction operations
1116///
1117/// # Example
1118///
1119/// ```no_run
1120/// use morpho_rs_api::{MorphoClient, MorphoClientConfig, NamedChain};
1121/// use alloy::primitives::{Address, U256};
1122///
1123/// #[tokio::main]
1124/// async fn main() -> Result<(), morpho_rs_api::ApiError> {
1125///     // API-only client
1126///     let client = MorphoClient::new();
1127///     let vaults = client.get_vaults_by_chain(NamedChain::Mainnet).await?;
1128///
1129///     // Full client with transaction support
1130///     let config = MorphoClientConfig::new()
1131///         .with_rpc_url("https://eth.llamarpc.com")
1132///         .with_private_key("0x...");
1133///     let client = MorphoClient::with_config(config)?;
1134///
1135///     // V1 vault operations
1136///     let vault: Address = "0x...".parse().unwrap();
1137///     let balance = client.vault_v1()?.balance(vault).await?;
1138///
1139///     Ok(())
1140/// }
1141/// ```
1142pub 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    /// Create a new MorphoClient with default API configuration (no transaction support).
1157    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    /// Create a MorphoClient with custom configuration.
1167    ///
1168    /// If both `rpc_url` and `private_key` are provided, transaction support is enabled.
1169    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    /// Get V1 vault operations.
1193    ///
1194    /// Returns an error if transaction support is not configured.
1195    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    /// Get V2 vault operations.
1203    ///
1204    /// Returns an error if transaction support is not configured.
1205    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    /// Check if auto_approve is enabled.
1213    pub fn auto_approve(&self) -> bool {
1214        self.auto_approve
1215    }
1216
1217    /// Get the API client for GraphQL queries.
1218    pub fn api(&self) -> &MorphoApiClient {
1219        &self.api
1220    }
1221
1222    /// Get vaults (V1 and V2) on a specific chain as unified Vault type.
1223    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    /// Get whitelisted vaults (V1 and V2) as unified Vault type.
1228    pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<Vault>> {
1229        self.api.get_whitelisted_vaults(chain).await
1230    }
1231
1232    /// Get all vault positions (V1 and V2) for a user.
1233    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    /// Get complete account overview for a user on a specific chain.
1242    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    /// Check if transaction support is configured.
1251    pub fn has_transaction_support(&self) -> bool {
1252        self.vault_v1_tx.is_some()
1253    }
1254
1255    /// Get the signer's address if transaction support is configured.
1256    pub fn signer_address(&self) -> Option<Address> {
1257        self.vault_v1_tx.as_ref().map(|c| c.signer_address())
1258    }
1259}
1260
1261// Conversion functions from GraphQL types to our types
1262
1263fn 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
1461/// Parse yearly supply tokens from string to f64.
1462fn 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
1554// User position conversion functions
1555
1556fn 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}