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