async_connect_example/
async_connect_example.rs

1// Example of refactored async connect.rs
2
3use anyhow::{anyhow, Context, Result};
4use serde_json::Value as JsonValue;
5use std::collections::HashMap;
6use url::Url;
7
8#[cfg(not(target_arch = "wasm32"))]
9use reqwest;
10#[cfg(not(target_arch = "wasm32"))]
11use sha2::{Sha256, Digest};
12
13#[cfg(target_arch = "wasm32")]
14use wasm_bindgen_futures::JsFuture;
15#[cfg(target_arch = "wasm32")]
16use web_sys::{window, SubtleCrypto};
17
18#[cfg(not(test))]
19const URL: &str = "https://api.kite.trade";
20
21#[cfg(test)]
22const URL: &str = "http://localhost:1234"; // Mock server URL
23
24#[async_trait::async_trait]
25trait RequestHandler {
26    async fn send_request(
27        &self,
28        url: Url,
29        method: &str,
30        data: Option<HashMap<&str, &str>>,
31    ) -> Result<reqwest::Response>;
32}
33
34pub struct KiteConnect {
35    api_key: String,
36    access_token: String,
37    session_expiry_hook: Option<fn() -> ()>,
38    #[cfg(not(target_arch = "wasm32"))]
39    client: reqwest::Client,
40    #[cfg(target_arch = "wasm32")]
41    client: reqwest::Client,
42}
43
44impl KiteConnect {
45    /// Constructor
46    pub fn new(api_key: &str, access_token: &str) -> Self {
47        Self {
48            api_key: api_key.to_string(),
49            access_token: access_token.to_string(),
50            session_expiry_hook: None,
51            client: reqwest::Client::new(),
52        }
53    }
54
55    /// Constructs url for the given path and query params
56    fn build_url(&self, path: &str, param: Option<Vec<(&str, &str)>>) -> Url {
57        let url_str = format!("{}{}", URL, path);
58        let mut url = Url::parse(&url_str).unwrap();
59
60        if let Some(data) = param {
61            url.query_pairs_mut().extend_pairs(data.iter());
62        }
63        url
64    }
65
66    /// Sets an access token for this instance
67    pub fn set_access_token(&mut self, access_token: &str) {
68        self.access_token = access_token.to_string();
69    }
70
71    /// Returns the login url
72    pub fn login_url(&self) -> String {
73        format!("https://kite.trade/connect/login?api_key={}&v3", self.api_key)
74    }
75
76    /// Async SHA256 hash computation (platform-specific)
77    #[cfg(not(target_arch = "wasm32"))]
78    async fn compute_checksum(&self, input: &str) -> Result<String> {
79        // Native implementation using sha2
80        let mut hasher = Sha256::new();
81        hasher.update(input.as_bytes());
82        Ok(format!("{:x}", hasher.finalize()))
83    }
84
85    #[cfg(target_arch = "wasm32")]
86    async fn compute_checksum(&self, input: &str) -> Result<String> {
87        // WASM implementation using Web Crypto API
88        use js_sys::Uint8Array;
89        use wasm_bindgen::JsCast;
90
91        let window = window().ok_or_else(|| anyhow!("No window object"))?;
92        let crypto = window.crypto().map_err(|_| anyhow!("No crypto object"))?;
93        let subtle = crypto.subtle();
94
95        let data = Uint8Array::from(input.as_bytes());
96        let digest_promise = subtle
97            .digest_with_str_and_u8_array("SHA-256", &data)
98            .map_err(|_| anyhow!("Failed to create digest"))?;
99
100        let digest_result = JsFuture::from(digest_promise)
101            .await
102            .map_err(|_| anyhow!("Failed to compute hash"))?;
103
104        let digest_array = Uint8Array::new(&digest_result);
105        let digest_vec: Vec<u8> = digest_array.to_vec();
106
107        Ok(hex::encode(digest_vec))
108    }
109
110    /// Request for access token (now async)
111    pub async fn generate_session(
112        &mut self,
113        request_token: &str,
114        api_secret: &str,
115    ) -> Result<JsonValue> {
116        // Create a hex digest from api key, request token, api secret
117        let input = format!("{}{}{}", self.api_key, request_token, api_secret);
118        let checksum = self.compute_checksum(&input).await?;
119
120        let mut data = HashMap::new();
121        data.insert("api_key", self.api_key.as_str());
122        data.insert("request_token", request_token);
123        data.insert("checksum", checksum.as_str());
124
125        let url = self.build_url("/session/token", None);
126        let mut resp = self.send_request(url, "POST", Some(data)).await?;
127
128        if resp.status().is_success() {
129            let jsn: JsonValue = resp.json().await?;
130            if let Some(access_token) = jsn["access_token"].as_str() {
131                self.set_access_token(access_token);
132            }
133            Ok(jsn)
134        } else {
135            let error_text = resp.text().await?;
136            Err(anyhow!(error_text))
137        }
138    }
139
140    /// Get all holdings (now async)
141    pub async fn holdings(&self) -> Result<JsonValue> {
142        let url = self.build_url("/portfolio/holdings", None);
143        let resp = self.send_request(url, "GET", None).await?;
144        self.raise_or_return_json(resp).await
145    }
146
147    /// Get all positions (now async)
148    pub async fn positions(&self) -> Result<JsonValue> {
149        let url = self.build_url("/portfolio/positions", None);
150        let resp = self.send_request(url, "GET", None).await?;
151        self.raise_or_return_json(resp).await
152    }
153
154    /// Get a list of orders (now async)
155    pub async fn orders(&self) -> Result<JsonValue> {
156        let url = self.build_url("/orders", None);
157        let resp = self.send_request(url, "GET", None).await?;
158        self.raise_or_return_json(resp).await
159    }
160
161    /// Place an order (now async)
162    pub async fn place_order(
163        &self,
164        variety: &str,
165        exchange: &str,
166        tradingsymbol: &str,
167        transaction_type: &str,
168        quantity: &str,
169        product: Option<&str>,
170        order_type: Option<&str>,
171        price: Option<&str>,
172        validity: Option<&str>,
173        disclosed_quantity: Option<&str>,
174        trigger_price: Option<&str>,
175        squareoff: Option<&str>,
176        stoploss: Option<&str>,
177        trailing_stoploss: Option<&str>,
178        tag: Option<&str>,
179    ) -> Result<JsonValue> {
180        let mut params = HashMap::new();
181        params.insert("exchange", exchange);
182        params.insert("tradingsymbol", tradingsymbol);
183        params.insert("transaction_type", transaction_type);
184        params.insert("quantity", quantity);
185
186        if let Some(p) = product { params.insert("product", p); }
187        if let Some(ot) = order_type { params.insert("order_type", ot); }
188        if let Some(pr) = price { params.insert("price", pr); }
189        if let Some(v) = validity { params.insert("validity", v); }
190        if let Some(dq) = disclosed_quantity { params.insert("disclosed_quantity", dq); }
191        if let Some(tp) = trigger_price { params.insert("trigger_price", tp); }
192        if let Some(so) = squareoff { params.insert("squareoff", so); }
193        if let Some(sl) = stoploss { params.insert("stoploss", sl); }
194        if let Some(tsl) = trailing_stoploss { params.insert("trailing_stoploss", tsl); }
195        if let Some(t) = tag { params.insert("tag", t); }
196
197        let url = self.build_url(&format!("/orders/{}", variety), None);
198        let resp = self.send_request(url, "POST", Some(params)).await?;
199        self.raise_or_return_json(resp).await
200    }
201
202    /// Helper method to raise or return json response
203    async fn raise_or_return_json(&self, resp: reqwest::Response) -> Result<JsonValue> {
204        if resp.status().is_success() {
205            let jsn: JsonValue = resp.json().await.with_context(|| "Serialization failed")?;
206            Ok(jsn)
207        } else {
208            let error_text = resp.text().await?;
209            Err(anyhow!(error_text))
210        }
211    }
212}
213
214#[async_trait::async_trait]
215impl RequestHandler for KiteConnect {
216    async fn send_request(
217        &self,
218        url: Url,
219        method: &str,
220        data: Option<HashMap<&str, &str>>,
221    ) -> Result<reqwest::Response> {
222        let mut request = match method {
223            "GET" => self.client.get(url),
224            "POST" => {
225                let mut req = self.client.post(url);
226                if let Some(form_data) = data {
227                    req = req.form(&form_data);
228                }
229                req
230            }
231            "PUT" => {
232                let mut req = self.client.put(url);
233                if let Some(form_data) = data {
234                    req = req.form(&form_data);
235                }
236                req
237            }
238            "DELETE" => {
239                let mut req = self.client.delete(url);
240                if let Some(json_data) = data {
241                    req = req.json(&json_data);
242                }
243                req
244            }
245            _ => return Err(anyhow!("Unknown HTTP method: {}", method)),
246        };
247
248        // Add headers
249        request = request
250            .header("X-Kite-Version", "3")
251            .header("Authorization", format!("token {}:{}", self.api_key, self.access_token))
252            .header("User-Agent", "Rust");
253
254        let response = request.send().await?;
255        Ok(response)
256    }
257}
258
259// Usage example for both native and WASM
260#[cfg(not(target_arch = "wasm32"))]
261pub async fn example_usage() -> Result<()> {
262    let mut kite = KiteConnect::new("your_api_key", "");
263    
264    // Generate session
265    let session = kite.generate_session("request_token", "api_secret").await?;
266    println!("Session: {:?}", session);
267    
268    // Get holdings
269    let holdings = kite.holdings().await?;
270    println!("Holdings: {:?}", holdings);
271    
272    // Place an order
273    let order = kite.place_order(
274        "regular",
275        "NSE", 
276        "INFY",
277        "BUY",
278        "1",
279        Some("CNC"),
280        Some("LIMIT"),
281        Some("1500.00"),
282        Some("DAY"),
283        None, None, None, None, None, None
284    ).await?;
285    println!("Order placed: {:?}", order);
286    
287    Ok(())
288}
289
290#[cfg(target_arch = "wasm32")]
291pub async fn example_usage_wasm() -> Result<()> {
292    use wasm_bindgen_futures::spawn_local;
293    
294    spawn_local(async {
295        let mut kite = KiteConnect::new("your_api_key", "");
296        
297        match kite.holdings().await {
298            Ok(holdings) => {
299                web_sys::console::log_1(&format!("Holdings: {:?}", holdings).into());
300            }
301            Err(e) => {
302                web_sys::console::error_1(&format!("Error: {:?}", e).into());
303            }
304        }
305    });
306    
307    Ok(())
308}
309
310#[tokio::main]
311async fn main() -> Result<()> {
312    // Example usage for native environment
313    let mut kite = KiteConnect::new("your_api_key", "your_access_token");
314    
315    // Generate session
316    match kite.generate_session("request_token", "api_secret").await {
317        Ok(session) => println!("Session generated: {:?}", session),
318        Err(e) => println!("Error generating session: {:?}", e),
319    }
320    
321    // Get holdings
322    match kite.holdings().await {
323        Ok(holdings) => println!("Holdings: {:?}", holdings),
324        Err(e) => println!("Error getting holdings: {:?}", e),
325    }
326    
327    Ok(())
328}