Skip to main content

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::vault_v1::MarketStateV1;
24use crate::types::vault_v2::{MarketStateV2, MetaMorphoAllocation, MorphoMarketPosition, VaultAdapterData};
25use crate::types::{
26    Asset, MarketInfo, NamedChain, UserAccountOverview, UserMarketPosition,
27    UserState, UserVaultPositions, UserVaultV1Position, UserVaultV2Position, Vault, VaultAdapter,
28    VaultAllocation, VaultAllocator, VaultInfo, VaultPositionState,
29    VaultReward, VaultStateV1, VaultV1, VaultV2, VaultV2Warning,
30    VaultWarning, SUPPORTED_CHAINS,
31};
32
33/// Default Morpho GraphQL API endpoint.
34pub const DEFAULT_API_URL: &str = "https://api.morpho.org/graphql";
35
36/// Default page size for paginated queries.
37pub const DEFAULT_PAGE_SIZE: i64 = 100;
38
39/// Configuration for vault clients.
40#[derive(Debug, Clone)]
41pub struct ClientConfig {
42    /// GraphQL API URL.
43    pub api_url: Url,
44    /// Default page size for queries.
45    pub page_size: i64,
46}
47
48impl Default for ClientConfig {
49    fn default() -> Self {
50        Self {
51            api_url: Url::parse(DEFAULT_API_URL).expect("Invalid default API URL"),
52            page_size: DEFAULT_PAGE_SIZE,
53        }
54    }
55}
56
57impl ClientConfig {
58    /// Create a new configuration with default values.
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Set a custom API URL.
64    pub fn with_api_url(mut self, url: Url) -> Self {
65        self.api_url = url;
66        self
67    }
68
69    /// Set a custom page size.
70    pub fn with_page_size(mut self, size: i64) -> Self {
71        self.page_size = size;
72        self
73    }
74}
75
76/// Client for querying V1 (MetaMorpho) vaults.
77#[derive(Debug, Clone)]
78pub struct VaultV1Client {
79    http_client: Client,
80    config: ClientConfig,
81}
82
83impl Default for VaultV1Client {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl VaultV1Client {
90    /// Create a new V1 vault client with default configuration.
91    pub fn new() -> Self {
92        Self {
93            http_client: Client::new(),
94            config: ClientConfig::default(),
95        }
96    }
97
98    /// Create a new V1 vault client with custom configuration.
99    pub fn with_config(config: ClientConfig) -> Self {
100        Self {
101            http_client: Client::new(),
102            config,
103        }
104    }
105
106    /// Execute a GraphQL query.
107    async fn execute<Q: GraphQLQuery>(
108        &self,
109        variables: Q::Variables,
110    ) -> Result<Q::ResponseData> {
111        let request_body = Q::build_query(variables);
112        let response = self
113            .http_client
114            .post(self.config.api_url.as_str())
115            .json(&request_body)
116            .send()
117            .await?;
118
119        let response_body: Response<Q::ResponseData> = response.json().await?;
120
121        if let Some(errors) = response_body.errors {
122            if !errors.is_empty() {
123                return Err(ApiError::GraphQL(
124                    errors
125                        .iter()
126                        .map(|e| e.message.clone())
127                        .collect::<Vec<_>>()
128                        .join("; "),
129                ));
130            }
131        }
132
133        response_body
134            .data
135            .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
136    }
137
138    /// Get V1 vaults with optional filters.
139    pub async fn get_vaults(&self, filters: Option<VaultFiltersV1>) -> Result<Vec<VaultV1>> {
140        let variables = get_vaults_v1::Variables {
141            first: Some(self.config.page_size),
142            skip: Some(0),
143            where_: filters.map(|f| f.to_gql()),
144            order_by: Some(VaultOrderByV1::default().to_gql()),
145            order_direction: Some(OrderDirection::default().to_gql_v1()),
146        };
147
148        let data = self.execute::<GetVaultsV1>(variables).await?;
149
150        let items = match data.vaults.items {
151            Some(items) => items,
152            None => return Ok(Vec::new()),
153        };
154
155        let vaults: Vec<VaultV1> = items
156            .into_iter()
157            .filter_map(convert_v1_vault)
158            .collect();
159
160        Ok(vaults)
161    }
162
163    /// Get a single V1 vault by address and chain.
164    pub async fn get_vault(&self, address: &str, chain: NamedChain) -> Result<VaultV1> {
165        let variables = get_vault_v1_by_address::Variables {
166            address: address.to_string(),
167            chain_id: u64::from(chain) as i64,
168        };
169
170        let data = self.execute::<GetVaultV1ByAddress>(variables).await?;
171
172        convert_v1_vault_single(data.vault_by_address).ok_or_else(|| ApiError::VaultNotFound {
173            address: address.to_string(),
174            chain_id: u64::from(chain) as i64,
175        })
176    }
177
178    /// Get V1 vaults on a specific chain.
179    pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<VaultV1>> {
180        let filters = VaultFiltersV1::new().chain(chain);
181        self.get_vaults(Some(filters)).await
182    }
183
184    /// Get V1 vaults by curator address.
185    pub async fn get_vaults_by_curator(
186        &self,
187        curator: &str,
188        chain: Option<NamedChain>,
189    ) -> Result<Vec<VaultV1>> {
190        let mut filters = VaultFiltersV1::new().curators([curator]);
191        if let Some(c) = chain {
192            filters = filters.chain(c);
193        }
194        self.get_vaults(Some(filters)).await
195    }
196
197    /// Get whitelisted (listed) V1 vaults.
198    pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<VaultV1>> {
199        let mut filters = VaultFiltersV1::new().listed(true);
200        if let Some(c) = chain {
201            filters = filters.chain(c);
202        }
203        self.get_vaults(Some(filters)).await
204    }
205
206    /// Get V1 vaults with query options (filters, ordering, and limit).
207    ///
208    /// This method provides full control over the query parameters including
209    /// ordering by various fields like APY, total assets, etc.
210    ///
211    /// # Example
212    ///
213    /// ```no_run
214    /// use morpho_rs_api::{VaultV1Client, VaultQueryOptionsV1, VaultFiltersV1, VaultOrderByV1, OrderDirection, NamedChain};
215    ///
216    /// #[tokio::main]
217    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
218    ///     let client = VaultV1Client::new();
219    ///
220    ///     // Get top 10 USDC vaults by APY on Ethereum
221    ///     let options = VaultQueryOptionsV1::new()
222    ///         .filters(VaultFiltersV1::new()
223    ///             .chain(NamedChain::Mainnet)
224    ///             .asset_symbols(["USDC"]))
225    ///         .order_by(VaultOrderByV1::NetApy)
226    ///         .order_direction(OrderDirection::Desc)
227    ///         .limit(10);
228    ///
229    ///     let vaults = client.get_vaults_with_options(options).await?;
230    ///     Ok(())
231    /// }
232    /// ```
233    pub async fn get_vaults_with_options(
234        &self,
235        options: VaultQueryOptionsV1,
236    ) -> Result<Vec<VaultV1>> {
237        let variables = get_vaults_v1::Variables {
238            first: options.limit.or(Some(self.config.page_size)),
239            skip: Some(0),
240            where_: options.filters.map(|f| f.to_gql()),
241            order_by: Some(options.order_by.unwrap_or_default().to_gql()),
242            order_direction: Some(options.order_direction.unwrap_or_default().to_gql_v1()),
243        };
244
245        let data = self.execute::<GetVaultsV1>(variables).await?;
246
247        let items = match data.vaults.items {
248            Some(items) => items,
249            None => return Ok(Vec::new()),
250        };
251
252        let vaults: Vec<VaultV1> = items
253            .into_iter()
254            .filter_map(convert_v1_vault)
255            .collect();
256
257        Ok(vaults)
258    }
259
260    /// Get top N V1 vaults ordered by APY (highest first).
261    ///
262    /// # Example
263    ///
264    /// ```no_run
265    /// use morpho_rs_api::{VaultV1Client, VaultFiltersV1, NamedChain};
266    ///
267    /// #[tokio::main]
268    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
269    ///     let client = VaultV1Client::new();
270    ///
271    ///     // Get top 10 vaults by APY on Ethereum
272    ///     let filters = VaultFiltersV1::new().chain(NamedChain::Mainnet);
273    ///     let vaults = client.get_top_vaults_by_apy(10, Some(filters)).await?;
274    ///     Ok(())
275    /// }
276    /// ```
277    pub async fn get_top_vaults_by_apy(
278        &self,
279        limit: i64,
280        filters: Option<VaultFiltersV1>,
281    ) -> Result<Vec<VaultV1>> {
282        let options = VaultQueryOptionsV1 {
283            filters,
284            order_by: Some(VaultOrderByV1::NetApy),
285            order_direction: Some(OrderDirection::Desc),
286            limit: Some(limit),
287        };
288        self.get_vaults_with_options(options).await
289    }
290
291    /// Get V1 vaults for a specific deposit asset.
292    ///
293    /// # Example
294    ///
295    /// ```no_run
296    /// use morpho_rs_api::{VaultV1Client, NamedChain};
297    ///
298    /// #[tokio::main]
299    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
300    ///     let client = VaultV1Client::new();
301    ///
302    ///     // Get all USDC vaults
303    ///     let vaults = client.get_vaults_by_asset("USDC", None).await?;
304    ///     Ok(())
305    /// }
306    /// ```
307    pub async fn get_vaults_by_asset(
308        &self,
309        asset_symbol: &str,
310        chain: Option<NamedChain>,
311    ) -> Result<Vec<VaultV1>> {
312        let mut filters = VaultFiltersV1::new().asset_symbols([asset_symbol]);
313        if let Some(c) = chain {
314            filters = filters.chain(c);
315        }
316        self.get_vaults(Some(filters)).await
317    }
318}
319
320/// Client for querying V2 vaults.
321#[derive(Debug, Clone)]
322pub struct VaultV2Client {
323    http_client: Client,
324    config: ClientConfig,
325}
326
327impl Default for VaultV2Client {
328    fn default() -> Self {
329        Self::new()
330    }
331}
332
333impl VaultV2Client {
334    /// Create a new V2 vault client with default configuration.
335    pub fn new() -> Self {
336        Self {
337            http_client: Client::new(),
338            config: ClientConfig::default(),
339        }
340    }
341
342    /// Create a new V2 vault client with custom configuration.
343    pub fn with_config(config: ClientConfig) -> Self {
344        Self {
345            http_client: Client::new(),
346            config,
347        }
348    }
349
350    /// Execute a GraphQL query.
351    async fn execute<Q: GraphQLQuery>(
352        &self,
353        variables: Q::Variables,
354    ) -> Result<Q::ResponseData> {
355        let request_body = Q::build_query(variables);
356        let response = self
357            .http_client
358            .post(self.config.api_url.as_str())
359            .json(&request_body)
360            .send()
361            .await?;
362
363        let response_body: Response<Q::ResponseData> = response.json().await?;
364
365        if let Some(errors) = response_body.errors {
366            if !errors.is_empty() {
367                return Err(ApiError::GraphQL(
368                    errors
369                        .iter()
370                        .map(|e| e.message.clone())
371                        .collect::<Vec<_>>()
372                        .join("; "),
373                ));
374            }
375        }
376
377        response_body
378            .data
379            .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
380    }
381
382    /// Get V2 vaults with optional filters.
383    pub async fn get_vaults(&self, filters: Option<VaultFiltersV2>) -> Result<Vec<VaultV2>> {
384        let variables = get_vaults_v2::Variables {
385            first: Some(self.config.page_size),
386            skip: Some(0),
387            where_: filters.map(|f| f.to_gql()),
388            order_by: Some(VaultOrderByV2::default().to_gql()),
389            order_direction: Some(OrderDirection::default().to_gql_v2()),
390        };
391
392        let data = self.execute::<GetVaultsV2>(variables).await?;
393
394        let items = match data.vault_v2s.items {
395            Some(items) => items,
396            None => return Ok(Vec::new()),
397        };
398
399        let vaults: Vec<VaultV2> = items
400            .into_iter()
401            .filter_map(convert_v2_vault)
402            .collect();
403
404        Ok(vaults)
405    }
406
407    /// Get a single V2 vault by address and chain.
408    pub async fn get_vault(&self, address: &str, chain: NamedChain) -> Result<VaultV2> {
409        let variables = get_vault_v2_by_address::Variables {
410            address: address.to_string(),
411            chain_id: u64::from(chain) as i64,
412        };
413
414        let data = self.execute::<GetVaultV2ByAddress>(variables).await?;
415
416        convert_v2_vault_single(data.vault_v2_by_address).ok_or_else(|| ApiError::VaultNotFound {
417            address: address.to_string(),
418            chain_id: u64::from(chain) as i64,
419        })
420    }
421
422    /// Get V2 vaults on a specific chain.
423    pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<VaultV2>> {
424        let filters = VaultFiltersV2::new().chain(chain);
425        self.get_vaults(Some(filters)).await
426    }
427
428    /// Get whitelisted (listed) V2 vaults.
429    pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<VaultV2>> {
430        let mut filters = VaultFiltersV2::new().listed(true);
431        if let Some(c) = chain {
432            filters = filters.chain(c);
433        }
434        self.get_vaults(Some(filters)).await
435    }
436
437    /// Get V2 vaults with query options (filters, ordering, and limit).
438    ///
439    /// This method provides full control over the query parameters including
440    /// ordering by various fields like APY, total assets, liquidity, etc.
441    ///
442    /// Note: Asset filtering (by symbol or address) is done client-side since
443    /// the Morpho V2 API doesn't support server-side asset filtering.
444    ///
445    /// # Example
446    ///
447    /// ```no_run
448    /// use morpho_rs_api::{VaultV2Client, VaultQueryOptionsV2, VaultFiltersV2, VaultOrderByV2, OrderDirection, NamedChain};
449    ///
450    /// #[tokio::main]
451    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
452    ///     let client = VaultV2Client::new();
453    ///
454    ///     // Get top 10 USDC vaults by APY on Ethereum
455    ///     let options = VaultQueryOptionsV2::new()
456    ///         .filters(VaultFiltersV2::new()
457    ///             .chain(NamedChain::Mainnet))
458    ///         .order_by(VaultOrderByV2::NetApy)
459    ///         .order_direction(OrderDirection::Desc)
460    ///         .asset_symbols(["USDC"])  // Client-side filtering
461    ///         .limit(10);
462    ///
463    ///     let vaults = client.get_vaults_with_options(options).await?;
464    ///     Ok(())
465    /// }
466    /// ```
467    pub async fn get_vaults_with_options(
468        &self,
469        options: VaultQueryOptionsV2,
470    ) -> Result<Vec<VaultV2>> {
471        // When using client-side asset filtering, we may need to fetch more results
472        // to ensure we have enough after filtering
473        let fetch_limit = if options.has_asset_filter() {
474            // Fetch more if we're going to filter client-side
475            options.limit.map(|l| l * 3).or(Some(self.config.page_size))
476        } else {
477            options.limit.or(Some(self.config.page_size))
478        };
479
480        let variables = get_vaults_v2::Variables {
481            first: fetch_limit,
482            skip: Some(0),
483            where_: options.filters.map(|f| f.to_gql()),
484            order_by: Some(options.order_by.unwrap_or_default().to_gql()),
485            order_direction: Some(options.order_direction.unwrap_or_default().to_gql_v2()),
486        };
487
488        let data = self.execute::<GetVaultsV2>(variables).await?;
489
490        let items = match data.vault_v2s.items {
491            Some(items) => items,
492            None => return Ok(Vec::new()),
493        };
494
495        let mut vaults: Vec<VaultV2> = items
496            .into_iter()
497            .filter_map(convert_v2_vault)
498            .collect();
499
500        // Apply client-side asset filtering
501        if let Some(ref symbols) = options.asset_symbols {
502            vaults.retain(|v| symbols.iter().any(|s| s.eq_ignore_ascii_case(&v.asset.symbol)));
503        }
504        if let Some(ref addresses) = options.asset_addresses {
505            vaults.retain(|v| {
506                addresses
507                    .iter()
508                    .any(|a| v.asset.address.to_string().eq_ignore_ascii_case(a))
509            });
510        }
511
512        // Apply limit after client-side filtering
513        if let Some(limit) = options.limit {
514            vaults.truncate(limit as usize);
515        }
516
517        Ok(vaults)
518    }
519
520    /// Get top N V2 vaults ordered by APY (highest first).
521    ///
522    /// # Example
523    ///
524    /// ```no_run
525    /// use morpho_rs_api::{VaultV2Client, VaultFiltersV2, NamedChain};
526    ///
527    /// #[tokio::main]
528    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
529    ///     let client = VaultV2Client::new();
530    ///
531    ///     // Get top 10 vaults by APY on Ethereum
532    ///     let filters = VaultFiltersV2::new().chain(NamedChain::Mainnet);
533    ///     let vaults = client.get_top_vaults_by_apy(10, Some(filters)).await?;
534    ///     Ok(())
535    /// }
536    /// ```
537    pub async fn get_top_vaults_by_apy(
538        &self,
539        limit: i64,
540        filters: Option<VaultFiltersV2>,
541    ) -> Result<Vec<VaultV2>> {
542        let options = VaultQueryOptionsV2 {
543            filters,
544            order_by: Some(VaultOrderByV2::NetApy),
545            order_direction: Some(OrderDirection::Desc),
546            limit: Some(limit),
547            asset_addresses: None,
548            asset_symbols: None,
549        };
550        self.get_vaults_with_options(options).await
551    }
552
553    /// Get V2 vaults for a specific deposit asset.
554    ///
555    /// Note: This filtering is done client-side since the Morpho V2 API
556    /// doesn't support server-side asset filtering.
557    ///
558    /// # Example
559    ///
560    /// ```no_run
561    /// use morpho_rs_api::{VaultV2Client, NamedChain};
562    ///
563    /// #[tokio::main]
564    /// async fn main() -> Result<(), morpho_rs_api::ApiError> {
565    ///     let client = VaultV2Client::new();
566    ///
567    ///     // Get all USDC vaults
568    ///     let vaults = client.get_vaults_by_asset("USDC", None).await?;
569    ///     Ok(())
570    /// }
571    /// ```
572    pub async fn get_vaults_by_asset(
573        &self,
574        asset_symbol: &str,
575        chain: Option<NamedChain>,
576    ) -> Result<Vec<VaultV2>> {
577        let filters = chain.map(|c| VaultFiltersV2::new().chain(c));
578        let options = VaultQueryOptionsV2 {
579            filters,
580            order_by: None,
581            order_direction: None,
582            limit: None,
583            asset_addresses: None,
584            asset_symbols: Some(vec![asset_symbol.to_string()]),
585        };
586        self.get_vaults_with_options(options).await
587    }
588}
589
590/// Combined client for querying both V1 and V2 vaults via the GraphQL API.
591#[derive(Debug, Clone)]
592pub struct MorphoApiClient {
593    /// V1 vault client.
594    pub v1: VaultV1Client,
595    /// V2 vault client.
596    pub v2: VaultV2Client,
597}
598
599impl Default for MorphoApiClient {
600    fn default() -> Self {
601        Self::new()
602    }
603}
604
605impl MorphoApiClient {
606    /// Create a new combined vault client with default configuration.
607    pub fn new() -> Self {
608        Self {
609            v1: VaultV1Client::new(),
610            v2: VaultV2Client::new(),
611        }
612    }
613
614    /// Create a new combined vault client with custom configuration.
615    pub fn with_config(config: ClientConfig) -> Self {
616        Self {
617            v1: VaultV1Client::with_config(config.clone()),
618            v2: VaultV2Client::with_config(config),
619        }
620    }
621
622    /// Get vaults (V1 and V2) on a specific chain as unified Vault type.
623    pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<Vault>> {
624        let (v1_vaults, v2_vaults) = tokio::try_join!(
625            self.v1.get_vaults_by_chain(chain),
626            self.v2.get_vaults_by_chain(chain),
627        )?;
628
629        let mut vaults: Vec<Vault> = Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
630        vaults.extend(v1_vaults.into_iter().map(Vault::from));
631        vaults.extend(v2_vaults.into_iter().map(Vault::from));
632
633        Ok(vaults)
634    }
635
636    /// Get whitelisted vaults (V1 and V2) as unified Vault type.
637    pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<Vault>> {
638        let (v1_vaults, v2_vaults) = tokio::try_join!(
639            self.v1.get_whitelisted_vaults(chain),
640            self.v2.get_whitelisted_vaults(chain),
641        )?;
642
643        let mut vaults: Vec<Vault> = Vec::with_capacity(v1_vaults.len() + v2_vaults.len());
644        vaults.extend(v1_vaults.into_iter().map(Vault::from));
645        vaults.extend(v2_vaults.into_iter().map(Vault::from));
646
647        Ok(vaults)
648    }
649
650    /// Execute a GraphQL query.
651    async fn execute<Q: GraphQLQuery>(&self, variables: Q::Variables) -> Result<Q::ResponseData> {
652        let request_body = Q::build_query(variables);
653        let response = self
654            .v1
655            .http_client
656            .post(self.v1.config.api_url.as_str())
657            .json(&request_body)
658            .send()
659            .await?;
660
661        let response_body: Response<Q::ResponseData> = response.json().await?;
662
663        if let Some(errors) = response_body.errors {
664            if !errors.is_empty() {
665                return Err(ApiError::GraphQL(
666                    errors
667                        .iter()
668                        .map(|e| e.message.clone())
669                        .collect::<Vec<_>>()
670                        .join("; "),
671                ));
672            }
673        }
674
675        response_body
676            .data
677            .ok_or_else(|| ApiError::Parse("No data in response".to_string()))
678    }
679
680    /// Get all vault positions (V1 and V2) for a user.
681    ///
682    /// If `chain` is `Some`, queries only that chain.
683    /// If `chain` is `None`, queries all supported chains and aggregates results.
684    pub async fn get_user_vault_positions(
685        &self,
686        address: &str,
687        chain: Option<NamedChain>,
688    ) -> Result<UserVaultPositions> {
689        match chain {
690            Some(c) => self.get_user_vault_positions_single_chain(address, c).await,
691            None => self.get_user_vault_positions_all_chains(address).await,
692        }
693    }
694
695    /// Get vault positions for a user on a single chain.
696    async fn get_user_vault_positions_single_chain(
697        &self,
698        address: &str,
699        chain: NamedChain,
700    ) -> Result<UserVaultPositions> {
701        let variables = get_user_vault_positions::Variables {
702            address: address.to_string(),
703            chain_id: u64::from(chain) as i64,
704        };
705
706        let data = self.execute::<GetUserVaultPositions>(variables).await?;
707        let user = data.user_by_address;
708
709        let vault_positions: Vec<UserVaultV1Position> = user
710            .vault_positions
711            .into_iter()
712            .filter_map(convert_user_vault_v1_position)
713            .collect();
714
715        let vault_v2_positions: Vec<UserVaultV2Position> = user
716            .vault_v2_positions
717            .into_iter()
718            .filter_map(convert_user_vault_v2_position)
719            .collect();
720
721        Ok(UserVaultPositions {
722            address: user
723                .address
724                .parse()
725                .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
726            vault_positions,
727            vault_v2_positions,
728        })
729    }
730
731    /// Get vault positions for a user across all chains.
732    async fn get_user_vault_positions_all_chains(
733        &self,
734        address: &str,
735    ) -> Result<UserVaultPositions> {
736        use futures::future::join_all;
737
738        // Filter chains to those with IDs that fit in GraphQL Int (32-bit signed)
739        let valid_chains: Vec<_> = SUPPORTED_CHAINS
740            .iter()
741            .filter(|chain| u64::from(**chain) <= i32::MAX as u64)
742            .copied()
743            .collect();
744
745        let futures: Vec<_> = valid_chains
746            .iter()
747            .map(|chain| self.get_user_vault_positions_single_chain(address, *chain))
748            .collect();
749
750        let results = join_all(futures).await;
751
752        let parsed_address = address
753            .parse()
754            .map_err(|_| ApiError::Parse("Invalid address".to_string()))?;
755
756        let mut all_v1_positions = Vec::new();
757        let mut all_v2_positions = Vec::new();
758
759        for result in results {
760            match result {
761                Ok(positions) => {
762                    all_v1_positions.extend(positions.vault_positions);
763                    all_v2_positions.extend(positions.vault_v2_positions);
764                }
765                // Ignore "No results" errors - user just has no positions on that chain
766                Err(ApiError::GraphQL(msg)) if msg.contains("No results") => continue,
767                Err(e) => return Err(e),
768            }
769        }
770
771        Ok(UserVaultPositions {
772            address: parsed_address,
773            vault_positions: all_v1_positions,
774            vault_v2_positions: all_v2_positions,
775        })
776    }
777
778    /// Get complete account overview for a user on a specific chain.
779    pub async fn get_user_account_overview(
780        &self,
781        address: &str,
782        chain: NamedChain,
783    ) -> Result<UserAccountOverview> {
784        let variables = get_user_account_overview::Variables {
785            address: address.to_string(),
786            chain_id: u64::from(chain) as i64,
787        };
788
789        let data = self.execute::<GetUserAccountOverview>(variables).await?;
790        let user = data.user_by_address;
791
792        let state = UserState::from_gql(
793            user.state.vaults_pnl_usd,
794            user.state.vaults_roe_usd,
795            user.state.vaults_assets_usd,
796            user.state.vault_v2s_pnl_usd,
797            user.state.vault_v2s_roe_usd,
798            user.state.vault_v2s_assets_usd,
799            user.state.markets_pnl_usd,
800            user.state.markets_roe_usd,
801            user.state.markets_supply_pnl_usd,
802            user.state.markets_supply_roe_usd,
803            user.state.markets_borrow_pnl_usd,
804            user.state.markets_borrow_roe_usd,
805            user.state.markets_collateral_pnl_usd,
806            user.state.markets_collateral_roe_usd,
807            user.state.markets_margin_pnl_usd,
808            user.state.markets_margin_roe_usd,
809            user.state.markets_collateral_usd,
810            user.state.markets_supply_assets_usd,
811            user.state.markets_borrow_assets_usd,
812            user.state.markets_margin_usd,
813        );
814
815        let vault_positions: Vec<UserVaultV1Position> = user
816            .vault_positions
817            .into_iter()
818            .filter_map(convert_user_vault_v1_position_overview)
819            .collect();
820
821        let vault_v2_positions: Vec<UserVaultV2Position> = user
822            .vault_v2_positions
823            .into_iter()
824            .filter_map(convert_user_vault_v2_position_overview)
825            .collect();
826
827        let market_positions: Vec<UserMarketPosition> = user
828            .market_positions
829            .into_iter()
830            .filter_map(convert_user_market_position)
831            .collect();
832
833        Ok(UserAccountOverview {
834            address: user
835                .address
836                .parse()
837                .map_err(|_| ApiError::Parse("Invalid address".to_string()))?,
838            state,
839            vault_positions,
840            vault_v2_positions,
841            market_positions,
842        })
843    }
844}
845
846/// Configuration for the unified MorphoClient.
847#[derive(Debug, Clone)]
848pub struct MorphoClientConfig {
849    /// API configuration.
850    pub api_config: Option<ClientConfig>,
851    /// RPC URL for on-chain interactions.
852    pub rpc_url: Option<String>,
853    /// Private key for signing transactions.
854    pub private_key: Option<String>,
855    /// Whether to automatically approve tokens before deposit if allowance is insufficient.
856    /// When true, approves the exact minimal amount needed for the deposit.
857    /// Defaults to true.
858    pub auto_approve: bool,
859}
860
861impl Default for MorphoClientConfig {
862    fn default() -> Self {
863        Self {
864            api_config: None,
865            rpc_url: None,
866            private_key: None,
867            auto_approve: true,
868        }
869    }
870}
871
872impl MorphoClientConfig {
873    /// Create a new configuration with default values.
874    pub fn new() -> Self {
875        Self::default()
876    }
877
878    /// Set the API configuration.
879    pub fn with_api_config(mut self, config: ClientConfig) -> Self {
880        self.api_config = Some(config);
881        self
882    }
883
884    /// Set the RPC URL.
885    pub fn with_rpc_url(mut self, rpc_url: impl Into<String>) -> Self {
886        self.rpc_url = Some(rpc_url.into());
887        self
888    }
889
890    /// Set the private key.
891    pub fn with_private_key(mut self, private_key: impl Into<String>) -> Self {
892        self.private_key = Some(private_key.into());
893        self
894    }
895
896    /// Set whether to automatically approve tokens before deposit.
897    /// When true, approves the exact minimal amount needed for the deposit.
898    /// Defaults to true.
899    pub fn with_auto_approve(mut self, auto_approve: bool) -> Self {
900        self.auto_approve = auto_approve;
901        self
902    }
903}
904
905/// Wrapper for V1 vault operations that automatically uses the signer's address.
906pub struct VaultV1Operations<'a> {
907    client: &'a VaultV1TransactionClient,
908    auto_approve: bool,
909}
910
911impl<'a> VaultV1Operations<'a> {
912    /// Create a new V1 operations wrapper.
913    fn new(client: &'a VaultV1TransactionClient, auto_approve: bool) -> Self {
914        Self { client, auto_approve }
915    }
916
917    /// Deposit assets into a vault, receiving shares to the signer's address.
918    ///
919    /// If `auto_approve` is enabled (default), this will check the current allowance
920    /// and approve the exact minimal amount needed for the deposit if insufficient.
921    pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
922        if self.auto_approve {
923            let asset = self.client.get_asset(vault).await?;
924            let current_allowance = self
925                .client
926                .get_allowance(asset, self.client.signer_address(), vault)
927                .await?;
928            if current_allowance < amount {
929                let needed = amount - current_allowance;
930                if let Some(approval) = self.client.approve_if_needed(asset, vault, needed).await? {
931                    approval.send().await?;
932                }
933            }
934        }
935
936        let receipt = self
937            .client
938            .deposit(vault, amount, self.client.signer_address())
939            .send()
940            .await?;
941        Ok(receipt)
942    }
943
944    /// Withdraw assets from a vault to the signer's address (withdrawing signer's shares).
945    pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
946        let signer = self.client.signer_address();
947        let receipt = self.client.withdraw(vault, amount, signer, signer).send().await?;
948        Ok(receipt)
949    }
950
951    /// Get the signer's vault share balance.
952    pub async fn balance(&self, vault: Address) -> Result<U256> {
953        let balance = self
954            .client
955            .get_balance(vault, self.client.signer_address())
956            .await?;
957        Ok(balance)
958    }
959
960    /// Approve a vault to spend the signer's tokens if needed.
961    /// Returns the transaction receipt if approval was performed, None if already approved.
962    pub async fn approve(
963        &self,
964        vault: Address,
965        amount: U256,
966    ) -> Result<Option<TransactionReceipt>> {
967        let asset = self.client.get_asset(vault).await?;
968        if let Some(approval) = self.client.approve_if_needed(asset, vault, amount).await? {
969            let receipt = approval.send().await?;
970            Ok(Some(receipt))
971        } else {
972            Ok(None)
973        }
974    }
975
976    /// Get the current allowance for the vault to spend the signer's tokens.
977    pub async fn get_allowance(&self, vault: Address) -> Result<U256> {
978        let asset = self.client.get_asset(vault).await?;
979        let allowance = self
980            .client
981            .get_allowance(asset, self.client.signer_address(), vault)
982            .await?;
983        Ok(allowance)
984    }
985
986    /// Get the underlying asset address of a vault.
987    pub async fn get_asset(&self, vault: Address) -> Result<Address> {
988        let asset = self.client.get_asset(vault).await?;
989        Ok(asset)
990    }
991
992    /// Get the decimals of a token.
993    pub async fn get_decimals(&self, token: Address) -> Result<u8> {
994        let decimals = self.client.get_decimals(token).await?;
995        Ok(decimals)
996    }
997
998    /// Get the signer's address.
999    pub fn signer_address(&self) -> Address {
1000        self.client.signer_address()
1001    }
1002
1003    /// Check if auto_approve is enabled.
1004    pub fn auto_approve(&self) -> bool {
1005        self.auto_approve
1006    }
1007}
1008
1009/// Wrapper for V2 vault operations that automatically uses the signer's address.
1010pub struct VaultV2Operations<'a> {
1011    client: &'a VaultV2TransactionClient,
1012    auto_approve: bool,
1013}
1014
1015impl<'a> VaultV2Operations<'a> {
1016    /// Create a new V2 operations wrapper.
1017    fn new(client: &'a VaultV2TransactionClient, auto_approve: bool) -> Self {
1018        Self { client, auto_approve }
1019    }
1020
1021    /// Deposit assets into a vault, receiving shares to the signer's address.
1022    ///
1023    /// If `auto_approve` is enabled (default), this will check the current allowance
1024    /// and approve the exact minimal amount needed for the deposit if insufficient.
1025    pub async fn deposit(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1026        if self.auto_approve {
1027            let asset = self.client.get_asset(vault).await?;
1028            let current_allowance = self
1029                .client
1030                .get_allowance(asset, self.client.signer_address(), vault)
1031                .await?;
1032            if current_allowance < amount {
1033                let needed = amount - current_allowance;
1034                if let Some(approval) = self.client.approve_if_needed(asset, vault, needed).await? {
1035                    approval.send().await?;
1036                }
1037            }
1038        }
1039
1040        let receipt = self
1041            .client
1042            .deposit(vault, amount, self.client.signer_address())
1043            .send()
1044            .await?;
1045        Ok(receipt)
1046    }
1047
1048    /// Withdraw assets from a vault to the signer's address (withdrawing signer's shares).
1049    pub async fn withdraw(&self, vault: Address, amount: U256) -> Result<TransactionReceipt> {
1050        let signer = self.client.signer_address();
1051        let receipt = self.client.withdraw(vault, amount, signer, signer).send().await?;
1052        Ok(receipt)
1053    }
1054
1055    /// Get the signer's vault share balance.
1056    pub async fn balance(&self, vault: Address) -> Result<U256> {
1057        let balance = self
1058            .client
1059            .get_balance(vault, self.client.signer_address())
1060            .await?;
1061        Ok(balance)
1062    }
1063
1064    /// Approve a vault to spend the signer's tokens if needed.
1065    /// Returns the transaction receipt if approval was performed, None if already approved.
1066    pub async fn approve(
1067        &self,
1068        vault: Address,
1069        amount: U256,
1070    ) -> Result<Option<TransactionReceipt>> {
1071        let asset = self.client.get_asset(vault).await?;
1072        if let Some(approval) = self.client.approve_if_needed(asset, vault, amount).await? {
1073            let receipt = approval.send().await?;
1074            Ok(Some(receipt))
1075        } else {
1076            Ok(None)
1077        }
1078    }
1079
1080    /// Get the current allowance for the vault to spend the signer's tokens.
1081    pub async fn get_allowance(&self, vault: Address) -> Result<U256> {
1082        let asset = self.client.get_asset(vault).await?;
1083        let allowance = self
1084            .client
1085            .get_allowance(asset, self.client.signer_address(), vault)
1086            .await?;
1087        Ok(allowance)
1088    }
1089
1090    /// Get the underlying asset address of a vault.
1091    pub async fn get_asset(&self, vault: Address) -> Result<Address> {
1092        let asset = self.client.get_asset(vault).await?;
1093        Ok(asset)
1094    }
1095
1096    /// Get the decimals of a token.
1097    pub async fn get_decimals(&self, token: Address) -> Result<u8> {
1098        let decimals = self.client.get_decimals(token).await?;
1099        Ok(decimals)
1100    }
1101
1102    /// Get the signer's address.
1103    pub fn signer_address(&self) -> Address {
1104        self.client.signer_address()
1105    }
1106
1107    /// Check if auto_approve is enabled.
1108    pub fn auto_approve(&self) -> bool {
1109        self.auto_approve
1110    }
1111}
1112
1113/// Unified Morpho client combining API queries and on-chain transactions.
1114///
1115/// This client provides a namespace-style API for interacting with Morpho vaults:
1116/// - `client.api()` - Access to GraphQL API queries
1117/// - `client.vault_v1()` - V1 vault transaction operations
1118/// - `client.vault_v2()` - V2 vault transaction operations
1119///
1120/// # Example
1121///
1122/// ```no_run
1123/// use morpho_rs_api::{MorphoClient, MorphoClientConfig, NamedChain};
1124/// use alloy::primitives::{Address, U256};
1125///
1126/// #[tokio::main]
1127/// async fn main() -> Result<(), morpho_rs_api::ApiError> {
1128///     // API-only client
1129///     let client = MorphoClient::new();
1130///     let vaults = client.get_vaults_by_chain(NamedChain::Mainnet).await?;
1131///
1132///     // Full client with transaction support
1133///     let config = MorphoClientConfig::new()
1134///         .with_rpc_url("https://eth.llamarpc.com")
1135///         .with_private_key("0x...");
1136///     let client = MorphoClient::with_config(config)?;
1137///
1138///     // V1 vault operations
1139///     let vault: Address = "0x...".parse().unwrap();
1140///     let balance = client.vault_v1()?.balance(vault).await?;
1141///
1142///     Ok(())
1143/// }
1144/// ```
1145pub struct MorphoClient {
1146    api: MorphoApiClient,
1147    vault_v1_tx: Option<VaultV1TransactionClient>,
1148    vault_v2_tx: Option<VaultV2TransactionClient>,
1149    auto_approve: bool,
1150}
1151
1152impl Default for MorphoClient {
1153    fn default() -> Self {
1154        Self::new()
1155    }
1156}
1157
1158impl MorphoClient {
1159    /// Create a new MorphoClient with default API configuration (no transaction support).
1160    pub fn new() -> Self {
1161        Self {
1162            api: MorphoApiClient::new(),
1163            vault_v1_tx: None,
1164            vault_v2_tx: None,
1165            auto_approve: true,
1166        }
1167    }
1168
1169    /// Create a MorphoClient with custom configuration.
1170    ///
1171    /// If both `rpc_url` and `private_key` are provided, transaction support is enabled.
1172    pub fn with_config(config: MorphoClientConfig) -> Result<Self> {
1173        let api = match config.api_config {
1174            Some(api_config) => MorphoApiClient::with_config(api_config),
1175            None => MorphoApiClient::new(),
1176        };
1177
1178        let (vault_v1_tx, vault_v2_tx) = match (&config.rpc_url, &config.private_key) {
1179            (Some(rpc_url), Some(private_key)) => {
1180                let v1 = VaultV1TransactionClient::new(rpc_url, private_key)?;
1181                let v2 = VaultV2TransactionClient::new(rpc_url, private_key)?;
1182                (Some(v1), Some(v2))
1183            }
1184            _ => (None, None),
1185        };
1186
1187        Ok(Self {
1188            api,
1189            vault_v1_tx,
1190            vault_v2_tx,
1191            auto_approve: config.auto_approve,
1192        })
1193    }
1194
1195    /// Get V1 vault operations.
1196    ///
1197    /// Returns an error if transaction support is not configured.
1198    pub fn vault_v1(&self) -> Result<VaultV1Operations<'_>> {
1199        match &self.vault_v1_tx {
1200            Some(client) => Ok(VaultV1Operations::new(client, self.auto_approve)),
1201            None => Err(ApiError::TransactionNotConfigured),
1202        }
1203    }
1204
1205    /// Get V2 vault operations.
1206    ///
1207    /// Returns an error if transaction support is not configured.
1208    pub fn vault_v2(&self) -> Result<VaultV2Operations<'_>> {
1209        match &self.vault_v2_tx {
1210            Some(client) => Ok(VaultV2Operations::new(client, self.auto_approve)),
1211            None => Err(ApiError::TransactionNotConfigured),
1212        }
1213    }
1214
1215    /// Check if auto_approve is enabled.
1216    pub fn auto_approve(&self) -> bool {
1217        self.auto_approve
1218    }
1219
1220    /// Get the API client for GraphQL queries.
1221    pub fn api(&self) -> &MorphoApiClient {
1222        &self.api
1223    }
1224
1225    /// Get vaults (V1 and V2) on a specific chain as unified Vault type.
1226    pub async fn get_vaults_by_chain(&self, chain: NamedChain) -> Result<Vec<Vault>> {
1227        self.api.get_vaults_by_chain(chain).await
1228    }
1229
1230    /// Get whitelisted vaults (V1 and V2) as unified Vault type.
1231    pub async fn get_whitelisted_vaults(&self, chain: Option<NamedChain>) -> Result<Vec<Vault>> {
1232        self.api.get_whitelisted_vaults(chain).await
1233    }
1234
1235    /// Get all vault positions (V1 and V2) for a user.
1236    pub async fn get_user_vault_positions(
1237        &self,
1238        address: &str,
1239        chain: Option<NamedChain>,
1240    ) -> Result<UserVaultPositions> {
1241        self.api.get_user_vault_positions(address, chain).await
1242    }
1243
1244    /// Get complete account overview for a user on a specific chain.
1245    pub async fn get_user_account_overview(
1246        &self,
1247        address: &str,
1248        chain: NamedChain,
1249    ) -> Result<UserAccountOverview> {
1250        self.api.get_user_account_overview(address, chain).await
1251    }
1252
1253    /// Check if transaction support is configured.
1254    pub fn has_transaction_support(&self) -> bool {
1255        self.vault_v1_tx.is_some()
1256    }
1257
1258    /// Get the signer's address if transaction support is configured.
1259    pub fn signer_address(&self) -> Option<Address> {
1260        self.vault_v1_tx.as_ref().map(|c| c.signer_address())
1261    }
1262}
1263
1264// Conversion functions from GraphQL types to our types
1265
1266fn convert_v1_vault(v: get_vaults_v1::GetVaultsV1VaultsItems) -> Option<VaultV1> {
1267    let chain_id = v.chain.id;
1268    let asset = &v.asset;
1269
1270    VaultV1::from_gql(
1271        &v.address,
1272        v.name,
1273        v.symbol,
1274        chain_id,
1275        v.listed,
1276        v.featured,
1277        v.whitelisted,
1278        Asset::from_gql(
1279            &asset.address,
1280            asset.symbol.clone(),
1281            Some(asset.name.clone()),
1282            asset.decimals,
1283            asset.price_usd,
1284        )?,
1285        v.state.as_ref().and_then(convert_v1_state),
1286        v.allocators
1287            .into_iter()
1288            .filter_map(|a| VaultAllocator::from_gql(&a.address))
1289            .collect(),
1290        v.warnings
1291            .into_iter()
1292            .map(|w| VaultWarning {
1293                warning_type: w.type_.clone(),
1294                level: format!("{:?}", w.level),
1295            })
1296            .collect(),
1297    )
1298}
1299
1300fn convert_v1_vault_single(
1301    v: get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddress,
1302) -> Option<VaultV1> {
1303    let chain_id = v.chain.id;
1304    let asset = &v.asset;
1305
1306    VaultV1::from_gql(
1307        &v.address,
1308        v.name,
1309        v.symbol,
1310        chain_id,
1311        v.listed,
1312        v.featured,
1313        v.whitelisted,
1314        Asset::from_gql(
1315            &asset.address,
1316            asset.symbol.clone(),
1317            Some(asset.name.clone()),
1318            asset.decimals,
1319            asset.price_usd,
1320        )?,
1321        v.state.as_ref().and_then(convert_v1_state_single),
1322        v.allocators
1323            .into_iter()
1324            .filter_map(|a| VaultAllocator::from_gql(&a.address))
1325            .collect(),
1326        v.warnings
1327            .into_iter()
1328            .map(|w| VaultWarning {
1329                warning_type: w.type_.clone(),
1330                level: format!("{:?}", w.level),
1331            })
1332            .collect(),
1333    )
1334}
1335
1336// Helper imports for conversion
1337use crate::types::scalars::parse_bigint;
1338use alloy_primitives::B256;
1339use std::str::FromStr;
1340
1341/// Convert f64 fee (0.1 = 10%) to WAD-scaled U256.
1342fn fee_to_wad(fee: f64) -> U256 {
1343    let fee_wad = (fee * 1e18) as u128;
1344    U256::from(fee_wad)
1345}
1346
1347/// Convert V1 market state from GraphQL response.
1348fn convert_v1_market_state(
1349    market: &get_vaults_v1::GetVaultsV1VaultsItemsStateAllocationMarket,
1350) -> Option<MarketStateV1> {
1351    let market_id = B256::from_str(&market.unique_key).ok()?;
1352    let ms = market.state.as_ref()?;
1353    let lltv = parse_bigint(&market.lltv)?;
1354    let timestamp: u64 = ms.timestamp.0.parse().ok()?;
1355
1356    Some(MarketStateV1 {
1357        id: market_id,
1358        total_supply_assets: parse_bigint(&ms.supply_assets)?,
1359        total_borrow_assets: parse_bigint(&ms.borrow_assets)?,
1360        total_supply_shares: parse_bigint(&ms.supply_shares)?,
1361        total_borrow_shares: parse_bigint(&ms.borrow_shares)?,
1362        last_update: timestamp,
1363        fee: fee_to_wad(ms.fee),
1364        rate_at_target: ms.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1365        price: ms.price.as_ref().and_then(|p| parse_bigint(p)),
1366        lltv,
1367    })
1368}
1369
1370/// Convert V1 market state from single vault GraphQL response.
1371fn convert_v1_market_state_single(
1372    market: &get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddressStateAllocationMarket,
1373) -> Option<MarketStateV1> {
1374    let market_id = B256::from_str(&market.unique_key).ok()?;
1375    let ms = market.state.as_ref()?;
1376    let lltv = parse_bigint(&market.lltv)?;
1377    let timestamp: u64 = ms.timestamp.0.parse().ok()?;
1378
1379    Some(MarketStateV1 {
1380        id: market_id,
1381        total_supply_assets: parse_bigint(&ms.supply_assets)?,
1382        total_borrow_assets: parse_bigint(&ms.borrow_assets)?,
1383        total_supply_shares: parse_bigint(&ms.supply_shares)?,
1384        total_borrow_shares: parse_bigint(&ms.borrow_shares)?,
1385        last_update: timestamp,
1386        fee: fee_to_wad(ms.fee),
1387        rate_at_target: ms.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1388        price: ms.price.as_ref().and_then(|p| parse_bigint(p)),
1389        lltv,
1390    })
1391}
1392
1393fn convert_v1_state(s: &get_vaults_v1::GetVaultsV1VaultsItemsState) -> Option<VaultStateV1> {
1394    VaultStateV1::from_gql(
1395        Some(s.curator.as_str()),
1396        Some(s.owner.as_str()),
1397        Some(s.guardian.as_str()),
1398        &s.total_assets,
1399        s.total_assets_usd,
1400        &s.total_supply,
1401        s.fee,
1402        &s.timelock,
1403        s.apy,
1404        s.net_apy,
1405        s.share_price.as_deref().unwrap_or("0"),
1406        s.allocation
1407            .iter()
1408            .filter_map(|a| {
1409                let market = &a.market;
1410                let market_state = convert_v1_market_state(market);
1411                VaultAllocation::from_gql(
1412                    market.unique_key.clone(),
1413                    Some(market.loan_asset.symbol.clone()),
1414                    Some(market.loan_asset.address.as_str()),
1415                    market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
1416                    market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
1417                    &a.supply_assets,
1418                    a.supply_assets_usd,
1419                    &a.supply_cap,
1420                    a.enabled,
1421                    a.supply_queue_index.map(|i| i as i32),
1422                    a.withdraw_queue_index.map(|i| i as i32),
1423                    market_state,
1424                )
1425            })
1426            .collect(),
1427    )
1428}
1429
1430fn convert_v1_state_single(
1431    s: &get_vault_v1_by_address::GetVaultV1ByAddressVaultByAddressState,
1432) -> Option<VaultStateV1> {
1433    VaultStateV1::from_gql(
1434        Some(s.curator.as_str()),
1435        Some(s.owner.as_str()),
1436        Some(s.guardian.as_str()),
1437        &s.total_assets,
1438        s.total_assets_usd,
1439        &s.total_supply,
1440        s.fee,
1441        &s.timelock,
1442        s.apy,
1443        s.net_apy,
1444        s.share_price.as_deref().unwrap_or("0"),
1445        s.allocation
1446            .iter()
1447            .filter_map(|a| {
1448                let market = &a.market;
1449                let market_state = convert_v1_market_state_single(market);
1450                VaultAllocation::from_gql(
1451                    market.unique_key.clone(),
1452                    Some(market.loan_asset.symbol.clone()),
1453                    Some(market.loan_asset.address.as_str()),
1454                    market.collateral_asset.as_ref().map(|ca| ca.symbol.clone()),
1455                    market.collateral_asset.as_ref().map(|ca| ca.address.as_str()),
1456                    &a.supply_assets,
1457                    a.supply_assets_usd,
1458                    &a.supply_cap,
1459                    a.enabled,
1460                    a.supply_queue_index.map(|i| i as i32),
1461                    a.withdraw_queue_index.map(|i| i as i32),
1462                    market_state,
1463                )
1464            })
1465            .collect(),
1466    )
1467}
1468
1469fn convert_v2_vault(v: get_vaults_v2::GetVaultsV2VaultV2sItems) -> Option<VaultV2> {
1470    let chain_id = v.chain.id;
1471    let asset = &v.asset;
1472
1473    VaultV2::from_gql(
1474        &v.address,
1475        v.name,
1476        v.symbol,
1477        chain_id,
1478        v.listed,
1479        v.whitelisted,
1480        Asset::from_gql(
1481            &asset.address,
1482            asset.symbol.clone(),
1483            Some(asset.name.clone()),
1484            asset.decimals,
1485            asset.price_usd,
1486        )?,
1487        Some(v.curator.address.as_str()),
1488        Some(v.owner.address.as_str()),
1489        v.total_assets.as_deref().unwrap_or("0"),
1490        v.total_assets_usd,
1491        &v.total_supply,
1492        Some(v.share_price),
1493        Some(v.performance_fee),
1494        Some(v.management_fee),
1495        v.avg_apy,
1496        v.avg_net_apy,
1497        v.apy,
1498        v.net_apy,
1499        &v.liquidity,
1500        v.liquidity_usd,
1501        v.adapters
1502            .items
1503            .map(|items| {
1504                items
1505                    .into_iter()
1506                    .filter_map(convert_v2_adapter)
1507                    .collect()
1508            })
1509            .unwrap_or_default(),
1510        v.rewards
1511            .into_iter()
1512            .filter_map(|r| {
1513                VaultReward::from_gql(
1514                    &r.asset.address,
1515                    r.asset.symbol.clone(),
1516                    r.supply_apr,
1517                    parse_yearly_supply(&r.yearly_supply_tokens),
1518                )
1519            })
1520            .collect(),
1521        v.warnings
1522            .into_iter()
1523            .map(|w| VaultV2Warning {
1524                warning_type: w.type_.clone(),
1525                level: format!("{:?}", w.level),
1526            })
1527            .collect(),
1528    )
1529}
1530
1531/// Parse yearly supply tokens from string to f64.
1532fn parse_yearly_supply(s: &str) -> Option<f64> {
1533    s.parse::<f64>().ok()
1534}
1535
1536fn convert_v2_vault_single(
1537    v: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddress,
1538) -> Option<VaultV2> {
1539    let chain_id = v.chain.id;
1540    let asset = &v.asset;
1541
1542    VaultV2::from_gql(
1543        &v.address,
1544        v.name,
1545        v.symbol,
1546        chain_id,
1547        v.listed,
1548        v.whitelisted,
1549        Asset::from_gql(
1550            &asset.address,
1551            asset.symbol.clone(),
1552            Some(asset.name.clone()),
1553            asset.decimals,
1554            asset.price_usd,
1555        )?,
1556        Some(v.curator.address.as_str()),
1557        Some(v.owner.address.as_str()),
1558        v.total_assets.as_deref().unwrap_or("0"),
1559        v.total_assets_usd,
1560        &v.total_supply,
1561        Some(v.share_price),
1562        Some(v.performance_fee),
1563        Some(v.management_fee),
1564        v.avg_apy,
1565        v.avg_net_apy,
1566        v.apy,
1567        v.net_apy,
1568        &v.liquidity,
1569        v.liquidity_usd,
1570        v.adapters
1571            .items
1572            .map(|items| {
1573                items
1574                    .into_iter()
1575                    .filter_map(convert_v2_adapter_single)
1576                    .collect()
1577            })
1578            .unwrap_or_default(),
1579        v.rewards
1580            .into_iter()
1581            .filter_map(|r| {
1582                VaultReward::from_gql(
1583                    &r.asset.address,
1584                    r.asset.symbol.clone(),
1585                    r.supply_apr,
1586                    parse_yearly_supply(&r.yearly_supply_tokens),
1587                )
1588            })
1589            .collect(),
1590        v.warnings
1591            .into_iter()
1592            .map(|w| VaultV2Warning {
1593                warning_type: w.type_.clone(),
1594                level: format!("{:?}", w.level),
1595            })
1596            .collect(),
1597    )
1598}
1599
1600#[allow(unreachable_patterns)]
1601fn convert_v2_adapter(
1602    a: get_vaults_v2::GetVaultsV2VaultV2sItemsAdaptersItems,
1603) -> Option<VaultAdapter> {
1604    use get_vaults_v2::GetVaultsV2VaultV2sItemsAdaptersItemsOn::*;
1605
1606    // The `on` field contains the inline fragment enum
1607    let data = match &a.on {
1608        MorphoMarketV1Adapter(adapter) => {
1609            let positions: Vec<MorphoMarketPosition> = adapter.positions.items.as_ref()
1610                .map(|items| {
1611                    items.iter().filter_map(|pos| {
1612                        let market_id = B256::from_str(&pos.market.unique_key).ok()?;
1613                        let market_state = pos.market.state.as_ref().and_then(|ms| {
1614                            let lltv = parse_bigint(&pos.market.lltv)?;
1615                            let timestamp: u64 = ms.timestamp.0.parse().ok()?;
1616                            Some(MarketStateV2 {
1617                                id: market_id,
1618                                total_supply_assets: parse_bigint(&ms.supply_assets)?,
1619                                total_borrow_assets: parse_bigint(&ms.borrow_assets)?,
1620                                total_supply_shares: parse_bigint(&ms.supply_shares)?,
1621                                total_borrow_shares: parse_bigint(&ms.borrow_shares)?,
1622                                last_update: timestamp,
1623                                fee: fee_to_wad(ms.fee),
1624                                rate_at_target: ms.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1625                                price: ms.price.as_ref().and_then(|p| parse_bigint(p)),
1626                                lltv,
1627                            })
1628                        });
1629                        Some(MorphoMarketPosition {
1630                            supply_assets: parse_bigint(&pos.supply_assets)?,
1631                            supply_shares: parse_bigint(&pos.supply_shares)?,
1632                            market_id,
1633                            market_state,
1634                        })
1635                    }).collect()
1636                })
1637                .unwrap_or_default();
1638            Some(VaultAdapterData::MorphoMarketV1 { positions })
1639        }
1640        MetaMorphoAdapter(adapter) => {
1641            let allocations: Vec<MetaMorphoAllocation> = adapter.meta_morpho.state.as_ref()
1642                .map(|state| {
1643                    state.allocation.iter().filter_map(|alloc| {
1644                        let market_id = B256::from_str(&alloc.market.unique_key).ok()?;
1645                        let market_state = alloc.market.state.as_ref().and_then(|ms| {
1646                            let lltv = parse_bigint(&alloc.market.lltv)?;
1647                            let timestamp: u64 = ms.timestamp.0.parse().ok()?;
1648                            Some(MarketStateV2 {
1649                                id: market_id,
1650                                total_supply_assets: parse_bigint(&ms.supply_assets)?,
1651                                total_borrow_assets: parse_bigint(&ms.borrow_assets)?,
1652                                total_supply_shares: parse_bigint(&ms.supply_shares)?,
1653                                total_borrow_shares: parse_bigint(&ms.borrow_shares)?,
1654                                last_update: timestamp,
1655                                fee: fee_to_wad(ms.fee),
1656                                rate_at_target: ms.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1657                                price: ms.price.as_ref().and_then(|p| parse_bigint(p)),
1658                                lltv,
1659                            })
1660                        });
1661                        Some(MetaMorphoAllocation {
1662                            supply_assets: parse_bigint(&alloc.supply_assets)?,
1663                            supply_cap: parse_bigint(&alloc.supply_cap)?,
1664                            enabled: alloc.enabled,
1665                            supply_queue_index: alloc.supply_queue_index.map(|i| i as i32),
1666                            withdraw_queue_index: alloc.withdraw_queue_index.map(|i| i as i32),
1667                            market_id,
1668                            market_state,
1669                        })
1670                    }).collect()
1671                })
1672                .unwrap_or_default();
1673            Some(VaultAdapterData::MetaMorpho { allocations })
1674        }
1675        _ => None,
1676    };
1677
1678    VaultAdapter::from_gql(
1679        a.id,
1680        &a.address,
1681        format!("{:?}", a.type_),
1682        &a.assets,
1683        a.assets_usd,
1684        data,
1685    )
1686}
1687
1688#[allow(unreachable_patterns)]
1689fn convert_v2_adapter_single(
1690    a: get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddressAdaptersItems,
1691) -> Option<VaultAdapter> {
1692    use get_vault_v2_by_address::GetVaultV2ByAddressVaultV2ByAddressAdaptersItemsOn::*;
1693
1694    // The `on` field contains the inline fragment enum
1695    let data = match &a.on {
1696        MorphoMarketV1Adapter(adapter) => {
1697            let positions: Vec<MorphoMarketPosition> = adapter.positions.items.as_ref()
1698                .map(|items| {
1699                    items.iter().filter_map(|pos| {
1700                        let market_id = B256::from_str(&pos.market.unique_key).ok()?;
1701                        let market_state = pos.market.state.as_ref().and_then(|ms| {
1702                            let lltv = parse_bigint(&pos.market.lltv)?;
1703                            let timestamp: u64 = ms.timestamp.0.parse().ok()?;
1704                            Some(MarketStateV2 {
1705                                id: market_id,
1706                                total_supply_assets: parse_bigint(&ms.supply_assets)?,
1707                                total_borrow_assets: parse_bigint(&ms.borrow_assets)?,
1708                                total_supply_shares: parse_bigint(&ms.supply_shares)?,
1709                                total_borrow_shares: parse_bigint(&ms.borrow_shares)?,
1710                                last_update: timestamp,
1711                                fee: fee_to_wad(ms.fee),
1712                                rate_at_target: ms.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1713                                price: ms.price.as_ref().and_then(|p| parse_bigint(p)),
1714                                lltv,
1715                            })
1716                        });
1717                        Some(MorphoMarketPosition {
1718                            supply_assets: parse_bigint(&pos.supply_assets)?,
1719                            supply_shares: parse_bigint(&pos.supply_shares)?,
1720                            market_id,
1721                            market_state,
1722                        })
1723                    }).collect()
1724                })
1725                .unwrap_or_default();
1726            Some(VaultAdapterData::MorphoMarketV1 { positions })
1727        }
1728        MetaMorphoAdapter(adapter) => {
1729            let allocations: Vec<MetaMorphoAllocation> = adapter.meta_morpho.state.as_ref()
1730                .map(|state| {
1731                    state.allocation.iter().filter_map(|alloc| {
1732                        let market_id = B256::from_str(&alloc.market.unique_key).ok()?;
1733                        let market_state = alloc.market.state.as_ref().and_then(|ms| {
1734                            let lltv = parse_bigint(&alloc.market.lltv)?;
1735                            let timestamp: u64 = ms.timestamp.0.parse().ok()?;
1736                            Some(MarketStateV2 {
1737                                id: market_id,
1738                                total_supply_assets: parse_bigint(&ms.supply_assets)?,
1739                                total_borrow_assets: parse_bigint(&ms.borrow_assets)?,
1740                                total_supply_shares: parse_bigint(&ms.supply_shares)?,
1741                                total_borrow_shares: parse_bigint(&ms.borrow_shares)?,
1742                                last_update: timestamp,
1743                                fee: fee_to_wad(ms.fee),
1744                                rate_at_target: ms.rate_at_target.as_ref().and_then(|r| parse_bigint(r)),
1745                                price: ms.price.as_ref().and_then(|p| parse_bigint(p)),
1746                                lltv,
1747                            })
1748                        });
1749                        Some(MetaMorphoAllocation {
1750                            supply_assets: parse_bigint(&alloc.supply_assets)?,
1751                            supply_cap: parse_bigint(&alloc.supply_cap)?,
1752                            enabled: alloc.enabled,
1753                            supply_queue_index: alloc.supply_queue_index.map(|i| i as i32),
1754                            withdraw_queue_index: alloc.withdraw_queue_index.map(|i| i as i32),
1755                            market_id,
1756                            market_state,
1757                        })
1758                    }).collect()
1759                })
1760                .unwrap_or_default();
1761            Some(VaultAdapterData::MetaMorpho { allocations })
1762        }
1763        _ => None,
1764    };
1765
1766    VaultAdapter::from_gql(
1767        a.id,
1768        &a.address,
1769        format!("{:?}", a.type_),
1770        &a.assets,
1771        a.assets_usd,
1772        data,
1773    )
1774}
1775
1776// User position conversion functions
1777
1778fn convert_user_vault_v1_position(
1779    p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultPositions,
1780) -> Option<UserVaultV1Position> {
1781    let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1782
1783    let state = p.state.as_ref().and_then(|s| {
1784        VaultPositionState::from_gql(
1785            &s.shares,
1786            s.assets.as_deref(),
1787            s.assets_usd,
1788            s.pnl.as_deref(),
1789            s.pnl_usd,
1790            s.roe,
1791            s.roe_usd,
1792        )
1793    });
1794
1795    UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1796}
1797
1798fn convert_user_vault_v2_position(
1799    p: get_user_vault_positions::GetUserVaultPositionsUserByAddressVaultV2Positions,
1800) -> Option<UserVaultV2Position> {
1801    let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1802
1803    UserVaultV2Position::from_gql(
1804        p.id,
1805        &p.shares,
1806        &p.assets,
1807        p.assets_usd,
1808        p.pnl.as_deref(),
1809        p.pnl_usd,
1810        p.roe,
1811        p.roe_usd,
1812        vault,
1813    )
1814}
1815
1816fn convert_user_vault_v1_position_overview(
1817    p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultPositions,
1818) -> Option<UserVaultV1Position> {
1819    let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1820
1821    let state = p.state.as_ref().and_then(|s| {
1822        VaultPositionState::from_gql(
1823            &s.shares,
1824            s.assets.as_deref(),
1825            s.assets_usd,
1826            s.pnl.as_deref(),
1827            s.pnl_usd,
1828            s.roe,
1829            s.roe_usd,
1830        )
1831    });
1832
1833    UserVaultV1Position::from_gql(p.id, &p.shares, &p.assets, p.assets_usd, vault, state)
1834}
1835
1836fn convert_user_vault_v2_position_overview(
1837    p: get_user_account_overview::GetUserAccountOverviewUserByAddressVaultV2Positions,
1838) -> Option<UserVaultV2Position> {
1839    let vault = VaultInfo::from_gql(&p.vault.address, p.vault.name, p.vault.symbol, p.vault.chain.id)?;
1840
1841    UserVaultV2Position::from_gql(
1842        p.id,
1843        &p.shares,
1844        &p.assets,
1845        p.assets_usd,
1846        p.pnl.as_deref(),
1847        p.pnl_usd,
1848        p.roe,
1849        p.roe_usd,
1850        vault,
1851    )
1852}
1853
1854fn convert_user_market_position(
1855    p: get_user_account_overview::GetUserAccountOverviewUserByAddressMarketPositions,
1856) -> Option<UserMarketPosition> {
1857    let market = MarketInfo::from_gql(
1858        p.market.unique_key,
1859        Some(p.market.loan_asset.symbol),
1860        Some(p.market.loan_asset.address.as_str()),
1861        p.market.collateral_asset.as_ref().map(|c| c.symbol.clone()),
1862        p.market.collateral_asset.as_ref().map(|c| c.address.as_str()),
1863    );
1864
1865    UserMarketPosition::from_gql(
1866        p.id,
1867        &p.supply_shares,
1868        &p.supply_assets,
1869        p.supply_assets_usd,
1870        &p.borrow_shares,
1871        &p.borrow_assets,
1872        p.borrow_assets_usd,
1873        &p.collateral,
1874        p.collateral_usd,
1875        p.health_factor,
1876        market,
1877    )
1878}
1879