kiteconnect_async_wasm/
connect.rs

1//! # KiteConnect API Client
2//! 
3//! This module provides the main [`KiteConnect`] struct and associated methods for
4//! interacting with the Zerodha KiteConnect REST API.
5//! 
6//! ## Overview
7//! 
8//! The KiteConnect API allows you to build trading applications and manage portfolios
9//! programmatically. This module provides async methods for all supported endpoints.
10//! 
11//! ## Authentication Flow
12//! 
13//! 1. **Get Login URL**: Use [`KiteConnect::login_url`] to generate a login URL
14//! 2. **User Login**: Direct user to the URL to complete login
15//! 3. **Generate Session**: Use [`KiteConnect::generate_session`] with the request token
16//! 4. **API Access**: Use any API method with the authenticated client
17//! 
18//! ## Example Usage
19//! 
20//! ```rust,no_run
21//! use kiteconnect_async_wasm::connect::KiteConnect;
22//! 
23//! # #[tokio::main]
24//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! let mut client = KiteConnect::new("your_api_key", "");
26//! 
27//! // Authentication
28//! let login_url = client.login_url();
29//! // ... user completes login and returns request_token ...
30//! 
31//! let session = client.generate_session("request_token", "api_secret").await?;
32//! 
33//! // Portfolio operations
34//! let holdings = client.holdings().await?;
35//! let positions = client.positions().await?;
36//! let margins = client.margins(None).await?;
37//! 
38//! // Order operations  
39//! let orders = client.orders().await?;
40//! let trades = client.trades().await?;
41//! 
42//! // Market data
43//! let instruments = client.instruments(None).await?;
44//! # Ok(())
45//! # }
46//! ```
47
48use serde_json::Value as JsonValue;
49use anyhow::{anyhow, Context, Result};
50use std::collections::HashMap;
51use reqwest::header::{HeaderMap, AUTHORIZATION, USER_AGENT};
52
53// Conditional imports for different targets
54#[cfg(not(target_arch = "wasm32"))]
55use {csv::ReaderBuilder, sha2::{Sha256, Digest}};
56
57#[cfg(target_arch = "wasm32")]
58use {
59    js_sys::Uint8Array,
60    wasm_bindgen_futures::JsFuture,
61    web_sys::window,
62};
63
64#[cfg(not(test))]
65const URL: &str = "https://api.kite.trade";
66
67#[cfg(test)]
68const URL: &str = "http://127.0.0.1:1234";
69
70/// Async trait for handling HTTP requests across different platforms
71trait RequestHandler {
72    async fn send_request(
73        &self,
74        url: reqwest::Url,
75        method: &str,
76        data: Option<HashMap<&str, &str>>,
77    ) -> Result<reqwest::Response>;
78}
79
80/// Main client for interacting with the KiteConnect API
81/// 
82/// This struct provides async methods for all KiteConnect REST API endpoints.
83/// It handles authentication, request formatting, and response parsing automatically.
84/// 
85/// ## Thread Safety
86/// 
87/// `KiteConnect` implements `Clone + Send + Sync`, making it safe to use across
88/// multiple threads and async tasks. The underlying HTTP client uses connection
89/// pooling for optimal performance.
90/// 
91/// ## Example
92/// 
93/// ```rust,no_run
94/// use kiteconnect_async_wasm::connect::KiteConnect;
95/// 
96/// # #[tokio::main]
97/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
98/// // Create a new client
99/// let mut client = KiteConnect::new("your_api_key", "");
100/// 
101/// // Set access token (usually done via generate_session)
102/// client.set_access_token("your_access_token");
103/// 
104/// // Use the API
105/// let holdings = client.holdings().await?;
106/// println!("Holdings: {:?}", holdings);
107/// # Ok(())
108/// # }
109/// ```
110/// 
111/// ## Cloning for Concurrent Use
112/// 
113/// ```rust,no_run
114/// use kiteconnect_async_wasm::connect::KiteConnect;
115/// 
116/// # #[tokio::main]
117/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
118/// let client = KiteConnect::new("api_key", "access_token");
119/// 
120/// // Clone for use in different tasks
121/// let client1 = client.clone();
122/// let client2 = client.clone();
123/// 
124/// // Fetch data concurrently
125/// let (holdings, positions) = tokio::try_join!(
126///     client1.holdings(),
127///     client2.positions()
128/// )?;
129/// # Ok(())
130/// # }
131/// ```
132#[derive(Clone, Debug)]
133pub struct KiteConnect {
134    /// API key for authentication
135    api_key: String,
136    /// Access token for authenticated requests
137    access_token: String,
138    /// Optional callback for session expiry handling
139    session_expiry_hook: Option<fn() -> ()>,
140    /// HTTP client for making requests (shared and reusable)
141    client: reqwest::Client,
142}
143
144impl Default for KiteConnect {
145    fn default() -> Self {
146        KiteConnect {
147            api_key: "<API-KEY>".to_string(),
148            access_token: "<ACCESS-TOKEN>".to_string(),
149            session_expiry_hook: None,
150            client: reqwest::Client::new(),
151        }
152    }
153}
154
155impl KiteConnect {
156    /// Constructs url for the given path and query params
157    pub(crate) fn build_url(&self, path: &str, param: Option<Vec<(&str, &str)>>) -> reqwest::Url {
158        let url: &str = &format!("{}/{}", URL, &path[1..]);
159        let mut url = reqwest::Url::parse(url).unwrap();
160
161        if let Some(data) = param {
162            url.query_pairs_mut().extend_pairs(data.iter());
163        }
164        url
165    }
166
167    /// Creates a new KiteConnect client instance
168    /// 
169    /// # Arguments
170    /// 
171    /// * `api_key` - Your KiteConnect API key
172    /// * `access_token` - Access token (can be empty string if using `generate_session`)
173    /// 
174    /// # Example
175    /// 
176    /// ```rust
177    /// use kiteconnect_async_wasm::connect::KiteConnect;
178    /// 
179    /// // Create client for authentication flow
180    /// let mut client = KiteConnect::new("your_api_key", "");
181    /// 
182    /// // Or create with existing access token
183    /// let client = KiteConnect::new("your_api_key", "your_access_token");
184    /// ```
185    pub fn new(api_key: &str, access_token: &str) -> Self {
186        Self {
187            api_key: api_key.to_string(),
188            access_token: access_token.to_string(),
189            client: reqwest::Client::new(),
190            ..Default::default()
191        }
192    }
193
194    /// Helper method to raise or return json response for async responses
195    async fn raise_or_return_json(&self, resp: reqwest::Response) -> Result<JsonValue> {
196        if resp.status().is_success() {
197            let jsn: JsonValue = resp.json().await.with_context(|| "Serialization failed")?;
198            Ok(jsn)
199        } else {
200            let error_text = resp.text().await?;
201            Err(anyhow!(error_text))
202        }
203    }
204
205    /// Sets a session expiry callback hook for this instance
206    /// 
207    /// This hook will be called when a session expires, allowing you to handle
208    /// re-authentication or cleanup logic.
209    /// 
210    /// # Arguments
211    /// 
212    /// * `method` - Callback function to execute on session expiry
213    /// 
214    /// # Example
215    /// 
216    /// ```rust
217    /// use kiteconnect_async_wasm::connect::KiteConnect;
218    /// 
219    /// fn handle_session_expiry() {
220    ///     println!("Session expired! Please re-authenticate.");
221    /// }
222    /// 
223    /// let mut client = KiteConnect::new("api_key", "access_token");
224    /// client.set_session_expiry_hook(handle_session_expiry);
225    /// ```
226    pub fn set_session_expiry_hook(&mut self, method: fn() -> ()) {
227        self.session_expiry_hook = Some(method);
228    }
229
230    /// Gets the current session expiry hook
231    /// 
232    /// Returns the session expiry callback function if one has been set.
233    /// 
234    /// # Returns
235    /// 
236    /// `Option<fn() -> ()>` - The callback function, or `None` if not set
237    pub fn session_expiry_hook(&self) -> Option<fn() -> ()> {
238        self.session_expiry_hook
239    }
240
241    /// Sets the access token for authenticated API requests
242    /// 
243    /// This is typically called automatically by `generate_session`, but can
244    /// be used manually if you have a pre-existing access token.
245    /// 
246    /// # Arguments
247    /// 
248    /// * `access_token` - The access token string
249    /// 
250    /// # Example
251    /// 
252    /// ```rust
253    /// use kiteconnect_async_wasm::connect::KiteConnect;
254    /// 
255    /// let mut client = KiteConnect::new("api_key", "");
256    /// client.set_access_token("your_access_token");
257    /// ```
258    pub fn set_access_token(&mut self, access_token: &str) {
259        self.access_token = access_token.to_string();
260    }
261
262    /// Gets the access token for this instance
263    pub fn access_token(&self) -> &str {
264        &self.access_token
265    }
266
267    /// Generates the KiteConnect login URL for user authentication
268    /// 
269    /// This URL should be opened in a browser to allow the user to log in to their
270    /// Zerodha account. After successful login, the user will be redirected to your
271    /// redirect URL with a `request_token` parameter.
272    /// 
273    /// # Returns
274    /// 
275    /// A login URL string that can be opened in a browser
276    /// 
277    /// # Example
278    /// 
279    /// ```rust
280    /// use kiteconnect_async_wasm::connect::KiteConnect;
281    /// 
282    /// let client = KiteConnect::new("your_api_key", "");
283    /// let login_url = client.login_url();
284    /// 
285    /// println!("Please visit: {}", login_url);
286    /// // User visits URL, logs in, and is redirected with request_token
287    /// ```
288    /// 
289    /// # Authentication Flow
290    /// 
291    /// 1. Generate login URL with this method
292    /// 2. Direct user to the URL in a browser
293    /// 3. User completes login and is redirected with `request_token`
294    /// 4. Use `generate_session()` with the request token to get access token
295    pub fn login_url(&self) -> String {
296        format!("https://kite.trade/connect/login?api_key={}&v3", self.api_key)
297    }
298
299    /// Compute checksum for authentication - different implementations for native vs WASM
300    #[cfg(not(target_arch = "wasm32"))]
301    async fn compute_checksum(&self, input: &str) -> Result<String> {
302        let mut hasher = Sha256::new();
303        hasher.update(input.as_bytes());
304        let result = hasher.finalize();
305        Ok(hex::encode(result))
306    }
307
308    #[cfg(target_arch = "wasm32")]
309    async fn compute_checksum(&self, input: &str) -> Result<String> {
310        // WASM implementation using Web Crypto API
311        let window = window().ok_or_else(|| anyhow!("No window object"))?;
312        let crypto = window.crypto().map_err(|_| anyhow!("No crypto object"))?;
313        let subtle = crypto.subtle();
314
315        let data = Uint8Array::from(input.as_bytes());
316        let digest_promise = subtle
317            .digest_with_str_and_u8_array("SHA-256", &data.to_vec())
318            .map_err(|_| anyhow!("Failed to create digest"))?;
319
320        let digest_result = JsFuture::from(digest_promise)
321            .await
322            .map_err(|_| anyhow!("Failed to compute hash"))?;
323
324        let digest_array = Uint8Array::new(&digest_result);
325        let digest_vec: Vec<u8> = digest_array.to_vec();
326        Ok(hex::encode(digest_vec))
327    }
328
329    /// Generates an access token using the request token from login
330    /// 
331    /// This method completes the authentication flow by exchanging the request token
332    /// (obtained after user login) for an access token that can be used for API calls.
333    /// The access token is automatically stored in the client instance.
334    /// 
335    /// # Arguments
336    /// 
337    /// * `request_token` - The request token received after user login
338    /// * `api_secret` - Your KiteConnect API secret
339    /// 
340    /// # Returns
341    /// 
342    /// A `Result<JsonValue>` containing the session information including access token
343    /// 
344    /// # Errors
345    /// 
346    /// Returns an error if:
347    /// - The request token is invalid or expired
348    /// - The API secret is incorrect
349    /// - Network request fails
350    /// - Response parsing fails
351    /// 
352    /// # Example
353    /// 
354    /// ```rust,no_run
355    /// use kiteconnect_async_wasm::connect::KiteConnect;
356    /// 
357    /// # #[tokio::main]
358    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
359    /// let mut client = KiteConnect::new("your_api_key", "");
360    /// 
361    /// // After user completes login and you receive the request_token
362    /// let session_data = client
363    ///     .generate_session("request_token_from_callback", "your_api_secret")
364    ///     .await?;
365    /// 
366    /// println!("Session created: {:?}", session_data);
367    /// // Access token is now automatically set in the client
368    /// # Ok(())
369    /// # }
370    /// ```
371    /// 
372    /// # Authentication Flow
373    /// 
374    /// 1. Call `login_url()` to get login URL
375    /// 2. User visits URL and completes login
376    /// 3. User is redirected with `request_token` parameter
377    /// 4. Call this method with the request token and API secret
378    /// 5. Access token is automatically set for subsequent API calls
379    pub async fn generate_session(
380        &mut self,
381        request_token: &str,
382        api_secret: &str,
383    ) -> Result<JsonValue> {
384        // Create a hex digest from api key, request token, api secret
385        let input = format!("{}{}{}", self.api_key, request_token, api_secret);
386        let checksum = self.compute_checksum(&input).await?;
387
388        let api_key: &str = &self.api_key.clone();
389        let mut data = HashMap::new();
390        data.insert("api_key", api_key);
391        data.insert("request_token", request_token);
392        data.insert("checksum", checksum.as_str());
393
394        let url = self.build_url("/session/token", None);
395        let resp = self.send_request(url, "POST", Some(data)).await?;
396
397        if resp.status().is_success() {
398            let jsn: JsonValue = resp.json().await?;
399            self.set_access_token(jsn["data"]["access_token"].as_str().unwrap());
400            Ok(jsn)
401        } else {
402            let error_text = resp.text().await?;
403            Err(anyhow!(error_text))
404        }
405    }
406
407    /// Invalidates the access token
408    pub async fn invalidate_access_token(&self, access_token: &str) -> Result<reqwest::Response> {
409        let url = self.build_url("/session/token", None);
410        let mut data = HashMap::new();
411        data.insert("access_token", access_token);
412
413        self.send_request(url, "DELETE", Some(data)).await
414    }
415
416    /// Request for new access token
417    pub async fn renew_access_token(
418        &mut self,
419        access_token: &str,
420        api_secret: &str,
421    ) -> Result<JsonValue> {
422        // Create a hex digest from api key, request token, api secret
423        let input = format!("{}{}{}", self.api_key, access_token, api_secret);
424        let checksum = self.compute_checksum(&input).await?;
425
426        let api_key: &str = &self.api_key.clone();
427        let mut data = HashMap::new();
428        data.insert("api_key", api_key);
429        data.insert("access_token", access_token);
430        data.insert("checksum", checksum.as_str());
431
432        let url = self.build_url("/session/refresh_token", None);
433        let resp = self.send_request(url, "POST", Some(data)).await?;
434
435        if resp.status().is_success() {
436            let jsn: JsonValue = resp.json().await?;
437            self.set_access_token(jsn["access_token"].as_str().unwrap());
438            Ok(jsn)
439        } else {
440            let error_text = resp.text().await?;
441            Err(anyhow!(error_text))
442        }
443    }
444
445    /// Invalidates the refresh token
446    pub async fn invalidate_refresh_token(&self, refresh_token: &str) -> Result<reqwest::Response> {
447        let url = self.build_url("/session/refresh_token", None);
448        let mut data = HashMap::new();
449        data.insert("refresh_token", refresh_token);
450
451        self.send_request(url, "DELETE", Some(data)).await
452    }
453
454    /// Retrieves account balance and margin details
455    /// 
456    /// Returns margin information for trading segments including available cash,
457    /// used margins, and available margins for different product types.
458    /// 
459    /// # Arguments
460    /// 
461    /// * `segment` - Optional trading segment ("equity" or "commodity"). If None, returns all segments
462    /// 
463    /// # Returns
464    /// 
465    /// A `Result<JsonValue>` containing margin data with fields like:
466    /// - `available` - Available margin for trading
467    /// - `utilised` - Currently utilized margin
468    /// - `net` - Net available margin
469    /// - `enabled` - Whether the segment is enabled
470    /// 
471    /// # Errors
472    /// 
473    /// Returns an error if the API request fails or the user is not authenticated.
474    /// 
475    /// # Example
476    /// 
477    /// ```rust,no_run
478    /// use kiteconnect_async_wasm::connect::KiteConnect;
479    /// 
480    /// # #[tokio::main]
481    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
482    /// let client = KiteConnect::new("api_key", "access_token");
483    /// 
484    /// // Get margins for all segments
485    /// let all_margins = client.margins(None).await?;
486    /// println!("All margins: {:?}", all_margins);
487    /// 
488    /// // Get margins for specific segment
489    /// let equity_margins = client.margins(Some("equity".to_string())).await?;
490    /// println!("Equity available margin: {}", 
491    ///     equity_margins["data"]["available"]["live_balance"]);
492    /// # Ok(())
493    /// # }
494    /// ```
495    pub async fn margins(&self, segment: Option<String>) -> Result<JsonValue> {
496        let url: reqwest::Url = if let Some(segment) = segment {
497            self.build_url(&format!("/user/margins/{}", segment), None)
498        } else {
499            self.build_url("/user/margins", None)
500        };
501
502        let resp = self.send_request(url, "GET", None).await?;
503        self.raise_or_return_json(resp).await
504    }
505
506    /// Get user profile details
507    pub async fn profile(&self) -> Result<JsonValue> {
508        let url = self.build_url("/user/profile", None);
509        let resp = self.send_request(url, "GET", None).await?;
510        self.raise_or_return_json(resp).await
511    }
512
513    /// Retrieves the user's holdings (stocks held in demat account)
514    /// 
515    /// Holdings represent stocks that are held in the user's demat account.
516    /// This includes information about quantity, average price, current market value,
517    /// profit/loss, and more.
518    /// 
519    /// # Returns
520    /// 
521    /// A `Result<JsonValue>` containing holdings data with fields like:
522    /// - `tradingsymbol` - Trading symbol of the instrument
523    /// - `quantity` - Total quantity held
524    /// - `average_price` - Average buying price
525    /// - `last_price` - Current market price
526    /// - `pnl` - Profit and loss
527    /// - `product` - Product type (CNC, MIS, etc.)
528    /// 
529    /// # Errors
530    /// 
531    /// Returns an error if the API request fails or the user is not authenticated.
532    /// 
533    /// # Example
534    /// 
535    /// ```rust,no_run
536    /// use kiteconnect_async_wasm::connect::KiteConnect;
537    /// 
538    /// # #[tokio::main]
539    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
540    /// let client = KiteConnect::new("api_key", "access_token");
541    /// 
542    /// let holdings = client.holdings().await?;
543    /// println!("Holdings: {:?}", holdings);
544    /// 
545    /// // Access specific fields
546    /// if let Some(data) = holdings["data"].as_array() {
547    ///     for holding in data {
548    ///         println!("Symbol: {}, Quantity: {}", 
549    ///             holding["tradingsymbol"], holding["quantity"]);
550    ///     }
551    /// }
552    /// # Ok(())
553    /// # }
554    /// ```
555    pub async fn holdings(&self) -> Result<JsonValue> {
556        let url = self.build_url("/portfolio/holdings", None);
557        let resp = self.send_request(url, "GET", None).await?;
558        self.raise_or_return_json(resp).await
559    }
560
561    /// Retrieves the user's positions (open positions for the day)
562    /// 
563    /// Positions represent open trading positions for the current trading day.
564    /// This includes both intraday and carry-forward positions with details about
565    /// profit/loss, margin requirements, and position status.
566    /// 
567    /// # Returns
568    /// 
569    /// A `Result<JsonValue>` containing positions data with fields like:
570    /// - `tradingsymbol` - Trading symbol of the instrument
571    /// - `quantity` - Net position quantity
572    /// - `buy_quantity` - Total buy quantity
573    /// - `sell_quantity` - Total sell quantity
574    /// - `average_price` - Average position price
575    /// - `pnl` - Realized and unrealized P&L
576    /// - `product` - Product type (MIS, CNC, NRML)
577    /// 
578    /// # Errors
579    /// 
580    /// Returns an error if the API request fails or the user is not authenticated.
581    /// 
582    /// # Example
583    /// 
584    /// ```rust,no_run
585    /// use kiteconnect_async_wasm::connect::KiteConnect;
586    /// 
587    /// # #[tokio::main]
588    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
589    /// let client = KiteConnect::new("api_key", "access_token");
590    /// 
591    /// let positions = client.positions().await?;
592    /// println!("Positions: {:?}", positions);
593    /// 
594    /// // Check for open positions
595    /// if let Some(day_positions) = positions["data"]["day"].as_array() {
596    ///     for position in day_positions {
597    ///         if position["quantity"].as_i64().unwrap_or(0) != 0 {
598    ///             println!("Open position: {} qty {}", 
599    ///                 position["tradingsymbol"], position["quantity"]);
600    ///         }
601    ///     }
602    /// }
603    /// # Ok(())
604    /// # }
605    /// ```
606    pub async fn positions(&self) -> Result<JsonValue> {
607        let url = self.build_url("/portfolio/positions", None);
608        let resp = self.send_request(url, "GET", None).await?;
609        self.raise_or_return_json(resp).await
610    }
611
612    /// Place an order
613    pub async fn place_order(
614        &self,
615        variety: &str,
616        exchange: &str,
617        tradingsymbol: &str,
618        transaction_type: &str,
619        quantity: &str,
620        product: Option<&str>,
621        order_type: Option<&str>,
622        price: Option<&str>,
623        validity: Option<&str>,
624        disclosed_quantity: Option<&str>,
625        trigger_price: Option<&str>,
626        squareoff: Option<&str>,
627        stoploss: Option<&str>,
628        trailing_stoploss: Option<&str>,
629        tag: Option<&str>,
630    ) -> Result<JsonValue> {
631        let mut params = HashMap::new();
632        params.insert("variety", variety);
633        params.insert("exchange", exchange);
634        params.insert("tradingsymbol", tradingsymbol);
635        params.insert("transaction_type", transaction_type);
636        params.insert("quantity", quantity);
637        
638        if let Some(product) = product { params.insert("product", product); }
639        if let Some(order_type) = order_type { params.insert("order_type", order_type); }
640        if let Some(price) = price { params.insert("price", price); }
641        if let Some(validity) = validity { params.insert("validity", validity); }
642        if let Some(disclosed_quantity) = disclosed_quantity { params.insert("disclosed_quantity", disclosed_quantity); }
643        if let Some(trigger_price) = trigger_price { params.insert("trigger_price", trigger_price); }
644        if let Some(squareoff) = squareoff { params.insert("squareoff", squareoff); }
645        if let Some(stoploss) = stoploss { params.insert("stoploss", stoploss); }
646        if let Some(trailing_stoploss) = trailing_stoploss { params.insert("trailing_stoploss", trailing_stoploss); }
647        if let Some(tag) = tag { params.insert("tag", tag); }
648
649        let url = self.build_url(&format!("/orders/{}", variety), None);
650        let resp = self.send_request(url, "POST", Some(params)).await?;
651        self.raise_or_return_json(resp).await
652    }
653
654    /// Modify an open order
655    pub async fn modify_order(
656        &self,
657        order_id: &str,
658        variety: &str,
659        quantity: Option<&str>,
660        price: Option<&str>,
661        order_type: Option<&str>,
662        validity: Option<&str>,
663        disclosed_quantity: Option<&str>,
664        trigger_price: Option<&str>,
665        parent_order_id: Option<&str>,
666    ) -> Result<JsonValue> {
667        let mut params = HashMap::new();
668        params.insert("order_id", order_id);
669        params.insert("variety", variety);
670        
671        if let Some(quantity) = quantity { params.insert("quantity", quantity); }
672        if let Some(price) = price { params.insert("price", price); }
673        if let Some(order_type) = order_type { params.insert("order_type", order_type); }
674        if let Some(validity) = validity { params.insert("validity", validity); }
675        if let Some(disclosed_quantity) = disclosed_quantity { params.insert("disclosed_quantity", disclosed_quantity); }
676        if let Some(trigger_price) = trigger_price { params.insert("trigger_price", trigger_price); }
677        if let Some(parent_order_id) = parent_order_id { params.insert("parent_order_id", parent_order_id); }
678
679        let url = self.build_url(&format!("/orders/{}/{}", variety, order_id), None);
680        let resp = self.send_request(url, "PUT", Some(params)).await?;
681        self.raise_or_return_json(resp).await
682    }
683
684    /// Cancel an order
685    pub async fn cancel_order(
686        &self,
687        order_id: &str,
688        variety: &str,
689        parent_order_id: Option<&str>,
690    ) -> Result<JsonValue> {
691        let mut params = HashMap::new();
692        params.insert("order_id", order_id);
693        params.insert("variety", variety);
694        if let Some(parent_order_id) = parent_order_id {
695            params.insert("parent_order_id", parent_order_id);
696        }
697
698        let url = self.build_url(&format!("/orders/{}/{}", variety, order_id), None);
699        let resp = self.send_request(url, "DELETE", Some(params)).await?;
700        self.raise_or_return_json(resp).await
701    }
702
703    /// Exit a BO/CO order
704    pub async fn exit_order(
705        &self,
706        order_id: &str,
707        variety: &str,
708        parent_order_id: Option<&str>,
709    ) -> Result<JsonValue> {
710        self.cancel_order(order_id, variety, parent_order_id).await
711    }
712
713    /// Retrieves a list of all orders for the current trading day
714    /// 
715    /// Returns all orders placed by the user for the current trading day,
716    /// including pending, completed, rejected, and cancelled orders.
717    /// 
718    /// # Returns
719    /// 
720    /// A `Result<JsonValue>` containing orders data with fields like:
721    /// - `order_id` - Unique order identifier
722    /// - `tradingsymbol` - Trading symbol
723    /// - `quantity` - Order quantity
724    /// - `price` - Order price
725    /// - `status` - Order status (OPEN, COMPLETE, CANCELLED, REJECTED)
726    /// - `order_type` - Order type (MARKET, LIMIT, SL, SL-M)
727    /// - `product` - Product type (MIS, CNC, NRML)
728    /// 
729    /// # Errors
730    /// 
731    /// Returns an error if the API request fails or the user is not authenticated.
732    /// 
733    /// # Example
734    /// 
735    /// ```rust,no_run
736    /// use kiteconnect_async_wasm::connect::KiteConnect;
737    /// 
738    /// # #[tokio::main]
739    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
740    /// let client = KiteConnect::new("api_key", "access_token");
741    /// 
742    /// let orders = client.orders().await?;
743    /// println!("Orders: {:?}", orders);
744    /// 
745    /// // Check order statuses
746    /// if let Some(data) = orders["data"].as_array() {
747    ///     for order in data {
748    ///         println!("Order {}: {} - {}", 
749    ///             order["order_id"], 
750    ///             order["tradingsymbol"], 
751    ///             order["status"]);
752    ///     }
753    /// }
754    /// # Ok(())
755    /// # }
756    /// ```
757    pub async fn orders(&self) -> Result<JsonValue> {
758        let url = self.build_url("/orders", None);
759        let resp = self.send_request(url, "GET", None).await?;
760        self.raise_or_return_json(resp).await
761    }
762
763    /// Get the list of order history
764    pub async fn order_history(&self, order_id: &str) -> Result<JsonValue> {
765        let params = vec![("order_id", order_id)];
766        let url = self.build_url("/orders", Some(params));
767        let resp = self.send_request(url, "GET", None).await?;
768        self.raise_or_return_json(resp).await
769    }
770
771    /// Get all trades
772    pub async fn trades(&self) -> Result<JsonValue> {
773        let url = self.build_url("/trades", None);
774        let resp = self.send_request(url, "GET", None).await?;
775        self.raise_or_return_json(resp).await
776    }
777
778    /// Get all trades for a specific order
779    pub async fn order_trades(&self, order_id: &str) -> Result<JsonValue> {
780        let url = self.build_url(&format!("/orders/{}/trades", order_id), None);
781        let resp = self.send_request(url, "GET", None).await?;
782        self.raise_or_return_json(resp).await
783    }
784
785    /// Modify an open position product type
786    pub async fn convert_position(
787        &self,
788        exchange: &str,
789        tradingsymbol: &str,
790        transaction_type: &str,
791        position_type: &str,
792        quantity: &str,
793        old_product: &str,
794        new_product: &str,
795    ) -> Result<JsonValue> {
796        let mut params = HashMap::new();
797        params.insert("exchange", exchange);
798        params.insert("tradingsymbol", tradingsymbol);
799        params.insert("transaction_type", transaction_type);
800        params.insert("position_type", position_type);
801        params.insert("quantity", quantity);
802        params.insert("old_product", old_product);
803        params.insert("new_product", new_product);
804
805        let url = self.build_url("/portfolio/positions", None);
806        let resp = self.send_request(url, "PUT", Some(params)).await?;
807        self.raise_or_return_json(resp).await
808    }
809
810    /// Get all mutual fund orders or individual order info
811    pub async fn mf_orders(&self, order_id: Option<&str>) -> Result<JsonValue> {
812        let url: reqwest::Url = if let Some(order_id) = order_id {
813            self.build_url(&format!("/mf/orders/{}", order_id), None)
814        } else {
815            self.build_url("/mf/orders", None)
816        };
817
818        let resp = self.send_request(url, "GET", None).await?;
819        self.raise_or_return_json(resp).await
820    }
821
822    /// Get the trigger range for a list of instruments
823    pub async fn trigger_range(
824        &self,
825        transaction_type: &str,
826        instruments: Vec<&str>,
827    ) -> Result<JsonValue> {
828        let mut params: Vec<(&str, &str)> = Vec::new();
829        params.push(("transaction_type", transaction_type));
830        
831        for instrument in instruments {
832            params.push(("instruments", instrument));
833        }
834
835        let url = self.build_url("/instruments/trigger_range", Some(params));
836        let resp = self.send_request(url, "GET", None).await?;
837        self.raise_or_return_json(resp).await
838    }
839
840    /// Get instruments list
841    #[cfg(not(target_arch = "wasm32"))]
842    pub async fn instruments(&self, exchange: Option<&str>) -> Result<JsonValue> {
843        let url: reqwest::Url = if let Some(exchange) = exchange {
844            self.build_url(&format!("/instruments/{}", exchange), None)
845        } else {
846            self.build_url("/instruments", None)
847        };
848
849        let resp = self.send_request(url, "GET", None).await?;
850        let body = resp.text().await?;
851        
852        // Parse CSV response
853        let mut rdr = ReaderBuilder::new().from_reader(body.as_bytes());
854        let mut result = Vec::new();
855        
856        let headers = rdr.headers()?.clone();
857        for record in rdr.records() {
858            let record = record?;
859            let mut obj = serde_json::Map::new();
860            
861            for (i, field) in record.iter().enumerate() {
862                if let Some(header) = headers.get(i) {
863                    obj.insert(header.to_string(), JsonValue::String(field.to_string()));
864                }
865            }
866            result.push(JsonValue::Object(obj));
867        }
868        
869        Ok(JsonValue::Array(result))
870    }
871
872    /// Get instruments list (WASM version - returns raw CSV as string)
873    #[cfg(target_arch = "wasm32")]
874    pub async fn instruments(&self, exchange: Option<&str>) -> Result<JsonValue> {
875        let url: reqwest::Url = if let Some(exchange) = exchange {
876            self.build_url(&format!("/instruments/{}", exchange), None)
877        } else {
878            self.build_url("/instruments", None)
879        };
880
881        let resp = self.send_request(url, "GET", None).await?;
882        let body = resp.text().await?;
883        
884        // For WASM, return the raw CSV data as a string
885        // Users can parse it client-side using JS CSV libraries
886        Ok(JsonValue::String(body))
887    }
888
889    /// Get mutual fund instruments list
890    #[cfg(not(target_arch = "wasm32"))]
891    pub async fn mf_instruments(&self) -> Result<JsonValue> {
892        let url = self.build_url("/mf/instruments", None);
893        let resp = self.send_request(url, "GET", None).await?;
894        let body = resp.text().await?;
895        
896        // Parse CSV response
897        let mut rdr = ReaderBuilder::new().from_reader(body.as_bytes());
898        let mut result = Vec::new();
899        
900        let headers = rdr.headers()?.clone();
901        for record in rdr.records() {
902            let record = record?;
903            let mut obj = serde_json::Map::new();
904            
905            for (i, field) in record.iter().enumerate() {
906                if let Some(header) = headers.get(i) {
907                    obj.insert(header.to_string(), JsonValue::String(field.to_string()));
908                }
909            }
910            result.push(JsonValue::Object(obj));
911        }
912        
913        Ok(JsonValue::Array(result))
914    }
915
916    /// Get mutual fund instruments list (WASM version - returns raw CSV as string)
917    #[cfg(target_arch = "wasm32")]
918    pub async fn mf_instruments(&self) -> Result<JsonValue> {
919        let url = self.build_url("/mf/instruments", None);
920        let resp = self.send_request(url, "GET", None).await?;
921        let body = resp.text().await?;
922        
923        // For WASM, return the raw CSV data as a string
924        // Users can parse it client-side using JS CSV libraries
925        Ok(JsonValue::String(body))
926    }
927}
928
929/// Implement the async request handler for KiteConnect struct
930impl RequestHandler for KiteConnect {
931    async fn send_request(
932        &self,
933        url: reqwest::Url,
934        method: &str,
935        data: Option<HashMap<&str, &str>>,
936    ) -> Result<reqwest::Response> {
937        let mut headers = HeaderMap::new();
938        headers.insert("XKiteVersion", "3".parse().unwrap());
939        headers.insert(
940            AUTHORIZATION,
941            format!("token {}:{}", self.api_key, self.access_token)
942                .parse()
943                .unwrap(),
944        );
945        headers.insert(USER_AGENT, "Rust".parse().unwrap());
946
947        let response = match method {
948            "GET" => self.client.get(url).headers(headers).send().await?,
949            "POST" => self.client.post(url).headers(headers).form(&data).send().await?,
950            "DELETE" => self.client.delete(url).headers(headers).json(&data).send().await?,
951            "PUT" => self.client.put(url).headers(headers).form(&data).send().await?,
952            _ => return Err(anyhow!("Unknown method!")),
953        };
954
955        Ok(response)
956    }
957}
958
959#[cfg(test)]
960mod tests {
961    use super::*;
962    use mockito::{Server, Matcher};
963
964    #[tokio::test]
965    async fn test_build_url() {
966        let kiteconnect = KiteConnect::new("key", "token");
967        let url = kiteconnect.build_url("/my-holdings", None);
968        assert_eq!(url.as_str(), format!("{}/my-holdings", URL).as_str());
969
970        let mut params: Vec<(&str, &str)> = Vec::new();
971        params.push(("one", "1"));
972        let url = kiteconnect.build_url("/my-holdings", Some(params));
973        assert_eq!(url.as_str(), format!("{}/my-holdings?one=1", URL).as_str());
974    }
975
976    #[tokio::test]
977    async fn test_set_access_token() {
978        let mut kiteconnect = KiteConnect::new("key", "token");
979        assert_eq!(kiteconnect.access_token(), "token");
980        kiteconnect.set_access_token("my_token");
981        assert_eq!(kiteconnect.access_token(), "my_token");
982    }
983
984    #[tokio::test]
985    async fn test_session_expiry_hook() {
986        let mut kiteconnect = KiteConnect::new("key", "token");
987        assert_eq!(kiteconnect.session_expiry_hook(), None);
988
989        fn mock_hook() { 
990            println!("Session expired");
991        }
992
993        kiteconnect.set_session_expiry_hook(mock_hook);
994        assert_ne!(kiteconnect.session_expiry_hook(), None);
995    }
996
997    #[tokio::test]
998    async fn test_login_url() {
999        let kiteconnect = KiteConnect::new("key", "token");
1000        assert_eq!(kiteconnect.login_url(), "https://kite.trade/connect/login?api_key=key&v3");
1001    }
1002
1003    #[tokio::test]
1004    async fn test_margins() {
1005        // Create a new mock server
1006        let mut server = Server::new_async().await;
1007        
1008        // Create KiteConnect instance that uses the mock server URL
1009        let kiteconnect = TestKiteConnect::new("API_KEY", "ACCESS_TOKEN", &server.url());
1010
1011        let _mock1 = server.mock("GET", Matcher::Regex(r"^/user/margins".to_string()))
1012            .with_body_from_file("mocks/margins.json")
1013            .create_async()
1014            .await;
1015        let _mock2 = server.mock("GET", Matcher::Regex(r"^/user/margins/commodity".to_string()))
1016            .with_body_from_file("mocks/margins.json")
1017            .create_async()
1018            .await;
1019
1020        let data: JsonValue = kiteconnect.margins(None).await.unwrap();
1021        println!("{:?}", data);
1022        assert!(data.is_object());
1023        let data: JsonValue = kiteconnect.margins(Some("commodity".to_string())).await.unwrap();
1024        println!("{:?}", data);
1025        assert!(data.is_object());
1026    }
1027
1028    #[tokio::test]
1029    async fn test_holdings() {
1030        let mut server = Server::new_async().await;
1031        let kiteconnect = TestKiteConnect::new("API_KEY", "ACCESS_TOKEN", &server.url());
1032
1033        let _mock = server.mock("GET", Matcher::Regex(r"^/portfolio/holdings".to_string()))
1034            .with_body_from_file("mocks/holdings.json")
1035            .create_async()
1036            .await;
1037
1038        let data: JsonValue = kiteconnect.holdings().await.unwrap();
1039        println!("{:?}", data);
1040        assert!(data.is_object());
1041    }
1042
1043    #[tokio::test]
1044    async fn test_positions() {
1045        let mut server = Server::new_async().await;
1046        let kiteconnect = TestKiteConnect::new("API_KEY", "ACCESS_TOKEN", &server.url());
1047
1048        let _mock = server.mock("GET", Matcher::Regex(r"^/portfolio/positions".to_string()))
1049            .with_body_from_file("mocks/positions.json")
1050            .create_async()
1051            .await;
1052
1053        let data: JsonValue = kiteconnect.positions().await.unwrap();
1054        println!("{:?}", data);
1055        assert!(data.is_object());
1056    }
1057
1058    #[tokio::test]
1059    async fn test_order_trades() {
1060        let mut server = Server::new_async().await;
1061        let kiteconnect = TestKiteConnect::new("API_KEY", "ACCESS_TOKEN", &server.url());
1062
1063        let _mock2 = server.mock(
1064            "GET", Matcher::Regex(r"^/orders/171229000724687/trades".to_string())
1065        )
1066        .with_body_from_file("mocks/order_trades.json")
1067        .create_async()
1068        .await;
1069
1070        let data: JsonValue = kiteconnect.order_trades("171229000724687").await.unwrap();
1071        println!("{:?}", data);
1072        assert!(data.is_object());
1073    }
1074
1075    #[tokio::test]
1076    async fn test_orders() {
1077        let mut server = Server::new_async().await;
1078        let kiteconnect = TestKiteConnect::new("API_KEY", "ACCESS_TOKEN", &server.url());
1079
1080        let _mock2 = server.mock(
1081            "GET", Matcher::Regex(r"^/orders".to_string())
1082        )
1083        .with_body_from_file("mocks/orders.json")
1084        .with_status(200)
1085        .create_async()
1086        .await;
1087
1088        let data: JsonValue = kiteconnect.orders().await.unwrap();
1089        println!("{:?}", data);
1090        assert!(data.is_object());
1091    }
1092
1093    #[tokio::test]
1094    async fn test_order_history() {
1095        let mut server = Server::new_async().await;
1096        let kiteconnect = TestKiteConnect::new("API_KEY", "ACCESS_TOKEN", &server.url());
1097
1098        let _mock2 = server.mock(
1099            "GET", Matcher::Regex(r"^/orders".to_string())
1100        )
1101        .with_body_from_file("mocks/order_info.json")
1102        .create_async()
1103        .await;
1104
1105        let data: JsonValue = kiteconnect.order_history("171229000724687").await.unwrap();
1106        println!("{:?}", data);
1107        assert!(data.is_object());
1108    }
1109
1110    #[tokio::test]
1111    async fn test_trades() {
1112        let mut server = Server::new_async().await;
1113        let kiteconnect = TestKiteConnect::new("API_KEY", "ACCESS_TOKEN", &server.url());
1114
1115        let _mock1 = server.mock("GET", Matcher::Regex(r"^/trades".to_string()))
1116            .with_body_from_file("mocks/trades.json")
1117            .create_async()
1118            .await;
1119
1120        let data: JsonValue = kiteconnect.trades().await.unwrap();
1121        println!("{:?}", data);
1122        assert!(data.is_object());
1123    }
1124
1125    #[tokio::test]
1126    async fn test_mf_orders() {
1127        let mut server = Server::new_async().await;
1128        let kiteconnect = TestKiteConnect::new("API_KEY", "ACCESS_TOKEN", &server.url());
1129
1130        let _mock1 = server.mock(
1131            "GET", Matcher::Regex(r"^/mf/orders$".to_string())
1132        )
1133        .with_body_from_file("mocks/mf_orders.json")
1134        .create_async()
1135        .await;
1136
1137        let _mock2 = server.mock(
1138            "GET", Matcher::Regex(r"^/mf/orders".to_string())
1139        )
1140        .with_body_from_file("mocks/mf_orders_info.json")
1141        .create_async()
1142        .await;
1143
1144        let data: JsonValue = kiteconnect.mf_orders(None).await.unwrap();
1145        println!("{:?}", data);
1146        assert!(data.is_object());
1147        let data: JsonValue = kiteconnect.mf_orders(Some("171229000724687")).await.unwrap();
1148        println!("{:?}", data);
1149        assert!(data.is_object());
1150    }
1151
1152    #[tokio::test]
1153    async fn test_trigger_range() {
1154        let mut server = Server::new_async().await;
1155        let kiteconnect = TestKiteConnect::new("API_KEY", "ACCESS_TOKEN", &server.url());
1156
1157        let _mock2 = server.mock(
1158            "GET", Matcher::Regex(r"^/instruments/trigger_range".to_string())
1159        )
1160        .with_body_from_file("mocks/trigger_range.json")
1161        .create_async()
1162        .await;
1163
1164        let data: JsonValue = kiteconnect.trigger_range("BUY", vec!["NSE:INFY", "NSE:RELIANCE"]).await.unwrap();
1165        println!("{:?}", data);
1166        assert!(data.is_object());
1167    }
1168
1169    #[tokio::test]
1170    async fn test_instruments() {
1171        let mut server = Server::new_async().await;
1172        let kiteconnect = TestKiteConnect::new("API_KEY", "ACCESS_TOKEN", &server.url());
1173
1174        let _mock2 = server.mock(
1175            "GET", Matcher::Regex(r"^/instruments".to_string())
1176        )
1177        .with_body_from_file("mocks/instruments.csv")
1178        .create_async()
1179        .await;
1180
1181        let data: JsonValue = kiteconnect.instruments(None).await.unwrap();
1182        println!("{:?}", data);
1183        assert_eq!(data[0]["instrument_token"].as_str(), Some("408065"));
1184    }
1185
1186    #[tokio::test]
1187    async fn test_mf_instruments() {
1188        let mut server = Server::new_async().await;
1189        let kiteconnect = TestKiteConnect::new("API_KEY", "ACCESS_TOKEN", &server.url());
1190
1191        let _mock2 = server.mock(
1192            "GET", Matcher::Regex(r"^/mf/instruments".to_string())
1193        )
1194        .with_body_from_file("mocks/mf_instruments.csv")
1195        .create_async()
1196        .await;
1197
1198        let data: JsonValue = kiteconnect.mf_instruments().await.unwrap();
1199        println!("{:?}", data);
1200        assert_eq!(data[0]["tradingsymbol"].as_str(), Some("INF846K01DP8"));
1201    }
1202
1203    // Helper struct to override the URL for testing
1204    #[derive(Clone, Debug)]
1205    struct TestKiteConnect {
1206        api_key: String,
1207        access_token: String,
1208        client: reqwest::Client,
1209        base_url: String,
1210    }
1211
1212    impl TestKiteConnect {
1213        fn new(api_key: &str, access_token: &str, base_url: &str) -> Self {
1214            Self {
1215                api_key: api_key.to_string(),
1216                access_token: access_token.to_string(),
1217                client: reqwest::Client::new(),
1218                base_url: base_url.to_string(),
1219            }
1220        }
1221
1222        fn build_url(&self, path: &str, param: Option<Vec<(&str, &str)>>) -> reqwest::Url {
1223            let url: &str = &format!("{}/{}", self.base_url, &path[1..]);
1224            let mut url = reqwest::Url::parse(url).unwrap();
1225
1226            if let Some(data) = param {
1227                url.query_pairs_mut().extend_pairs(data.iter());
1228            }
1229            url
1230        }
1231
1232        async fn send_request(
1233            &self,
1234            url: reqwest::Url,
1235            method: &str,
1236            data: Option<HashMap<&str, &str>>,
1237        ) -> Result<reqwest::Response> {
1238            let mut headers = HeaderMap::new();
1239            headers.insert("XKiteVersion", "3".parse().unwrap());
1240            headers.insert(
1241                AUTHORIZATION,
1242                format!("token {}:{}", self.api_key, self.access_token)
1243                    .parse()
1244                    .unwrap(),
1245            );
1246            headers.insert(USER_AGENT, "Rust".parse().unwrap());
1247
1248            let response = match method {
1249                "GET" => self.client.get(url).headers(headers).send().await?,
1250                "POST" => self.client.post(url).headers(headers).form(&data).send().await?,
1251                "DELETE" => self.client.delete(url).headers(headers).json(&data).send().await?,
1252                "PUT" => self.client.put(url).headers(headers).form(&data).send().await?,
1253                _ => return Err(anyhow!("Unknown method!")),
1254            };
1255
1256            Ok(response)
1257        }
1258
1259        async fn raise_or_return_json(&self, resp: reqwest::Response) -> Result<JsonValue> {
1260            if resp.status().is_success() {
1261                let jsn: JsonValue = resp.json().await.with_context(|| "Serialization failed")?;
1262                Ok(jsn)
1263            } else {
1264                let error_text = resp.text().await?;
1265                Err(anyhow!(error_text))
1266            }
1267        }
1268
1269        async fn holdings(&self) -> Result<JsonValue> {
1270            let url = self.build_url("/portfolio/holdings", None);
1271            let resp = self.send_request(url, "GET", None).await?;
1272            self.raise_or_return_json(resp).await
1273        }
1274
1275        async fn positions(&self) -> Result<JsonValue> {
1276            let url = self.build_url("/portfolio/positions", None);
1277            let resp = self.send_request(url, "GET", None).await?;
1278            self.raise_or_return_json(resp).await
1279        }
1280
1281        async fn orders(&self) -> Result<JsonValue> {
1282            let url = self.build_url("/orders", None);
1283            let resp = self.send_request(url, "GET", None).await?;
1284            self.raise_or_return_json(resp).await
1285        }
1286
1287        async fn margins(&self, segment: Option<String>) -> Result<JsonValue> {
1288            let url: reqwest::Url = if let Some(segment) = segment {
1289                self.build_url(&format!("/user/margins/{}", segment), None)
1290            } else {
1291                self.build_url("/user/margins", None)
1292            };
1293
1294            let resp = self.send_request(url, "GET", None).await?;
1295            self.raise_or_return_json(resp).await
1296        }
1297
1298        async fn order_trades(&self, order_id: &str) -> Result<JsonValue> {
1299            let url = self.build_url(&format!("/orders/{}/trades", order_id), None);
1300            let resp = self.send_request(url, "GET", None).await?;
1301            self.raise_or_return_json(resp).await
1302        }
1303
1304        async fn order_history(&self, order_id: &str) -> Result<JsonValue> {
1305            let params = vec![("order_id", order_id)];
1306            let url = self.build_url("/orders", Some(params));
1307            let resp = self.send_request(url, "GET", None).await?;
1308            self.raise_or_return_json(resp).await
1309        }
1310
1311        async fn trades(&self) -> Result<JsonValue> {
1312            let url = self.build_url("/trades", None);
1313            let resp = self.send_request(url, "GET", None).await?;
1314            self.raise_or_return_json(resp).await
1315        }
1316
1317        async fn mf_orders(&self, order_id: Option<&str>) -> Result<JsonValue> {
1318            let url: reqwest::Url = if let Some(order_id) = order_id {
1319                self.build_url(&format!("/mf/orders/{}", order_id), None)
1320            } else {
1321                self.build_url("/mf/orders", None)
1322            };
1323
1324            let resp = self.send_request(url, "GET", None).await?;
1325            self.raise_or_return_json(resp).await
1326        }
1327
1328        async fn trigger_range(
1329            &self,
1330            transaction_type: &str,
1331            instruments: Vec<&str>,
1332        ) -> Result<JsonValue> {
1333            let mut params: Vec<(&str, &str)> = Vec::new();
1334            params.push(("transaction_type", transaction_type));
1335            
1336            for instrument in instruments {
1337                params.push(("instruments", instrument));
1338            }
1339
1340            let url = self.build_url("/instruments/trigger_range", Some(params));
1341            let resp = self.send_request(url, "GET", None).await?;
1342            self.raise_or_return_json(resp).await
1343        }
1344
1345        async fn instruments(&self, exchange: Option<&str>) -> Result<JsonValue> {
1346            let url: reqwest::Url = if let Some(exchange) = exchange {
1347                self.build_url(&format!("/instruments/{}", exchange), None)
1348            } else {
1349                self.build_url("/instruments", None)
1350            };
1351
1352            let resp = self.send_request(url, "GET", None).await?;
1353            let body = resp.text().await?;
1354            
1355            // Parse CSV response
1356            #[cfg(not(target_arch = "wasm32"))]
1357            {
1358                use csv::ReaderBuilder;
1359                let mut rdr = ReaderBuilder::new().from_reader(body.as_bytes());
1360                let mut result = Vec::new();
1361                
1362                let headers = rdr.headers()?.clone();
1363                for record in rdr.records() {
1364                    let record = record?;
1365                    let mut obj = serde_json::Map::new();
1366                    
1367                    for (i, field) in record.iter().enumerate() {
1368                        if let Some(header) = headers.get(i) {
1369                            obj.insert(header.to_string(), JsonValue::String(field.to_string()));
1370                        }
1371                    }
1372                    result.push(JsonValue::Object(obj));
1373                }
1374                
1375                Ok(JsonValue::Array(result))
1376            }
1377            
1378            #[cfg(target_arch = "wasm32")]
1379            {
1380                Ok(JsonValue::String(body))
1381            }
1382        }
1383
1384        async fn mf_instruments(&self) -> Result<JsonValue> {
1385            let url = self.build_url("/mf/instruments", None);
1386            let resp = self.send_request(url, "GET", None).await?;
1387            let body = resp.text().await?;
1388            
1389            // Parse CSV response
1390            #[cfg(not(target_arch = "wasm32"))]
1391            {
1392                use csv::ReaderBuilder;
1393                let mut rdr = ReaderBuilder::new().from_reader(body.as_bytes());
1394                let mut result = Vec::new();
1395                
1396                let headers = rdr.headers()?.clone();
1397                for record in rdr.records() {
1398                    let record = record?;
1399                    let mut obj = serde_json::Map::new();
1400                    
1401                    for (i, field) in record.iter().enumerate() {
1402                        if let Some(header) = headers.get(i) {
1403                            obj.insert(header.to_string(), JsonValue::String(field.to_string()));
1404                        }
1405                    }
1406                    result.push(JsonValue::Object(obj));
1407                }
1408                
1409                Ok(JsonValue::Array(result))
1410            }
1411            
1412            #[cfg(target_arch = "wasm32")]
1413            {
1414                Ok(JsonValue::String(body))
1415            }
1416        }
1417    }
1418}