1use 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"; #[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 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 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 pub fn set_access_token(&mut self, access_token: &str) {
68 self.access_token = access_token.to_string();
69 }
70
71 pub fn login_url(&self) -> String {
73 format!("https://kite.trade/connect/login?api_key={}&v3", self.api_key)
74 }
75
76 #[cfg(not(target_arch = "wasm32"))]
78 async fn compute_checksum(&self, input: &str) -> Result<String> {
79 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 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 pub async fn generate_session(
112 &mut self,
113 request_token: &str,
114 api_secret: &str,
115 ) -> Result<JsonValue> {
116 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 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 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 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 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 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 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#[cfg(not(target_arch = "wasm32"))]
261pub async fn example_usage() -> Result<()> {
262 let mut kite = KiteConnect::new("your_api_key", "");
263
264 let session = kite.generate_session("request_token", "api_secret").await?;
266 println!("Session: {:?}", session);
267
268 let holdings = kite.holdings().await?;
270 println!("Holdings: {:?}", holdings);
271
272 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 let mut kite = KiteConnect::new("your_api_key", "your_access_token");
314
315 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 match kite.holdings().await {
323 Ok(holdings) => println!("Holdings: {:?}", holdings),
324 Err(e) => println!("Error getting holdings: {:?}", e),
325 }
326
327 Ok(())
328}