1use serde_json::Value as JsonValue;
49use anyhow::{anyhow, Context, Result};
50use std::collections::HashMap;
51use reqwest::header::{HeaderMap, AUTHORIZATION, USER_AGENT};
52
53#[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
70trait 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#[derive(Clone, Debug)]
133pub struct KiteConnect {
134 api_key: String,
136 access_token: String,
138 session_expiry_hook: Option<fn() -> ()>,
140 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 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 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 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 pub fn set_session_expiry_hook(&mut self, method: fn() -> ()) {
227 self.session_expiry_hook = Some(method);
228 }
229
230 pub fn session_expiry_hook(&self) -> Option<fn() -> ()> {
238 self.session_expiry_hook
239 }
240
241 pub fn set_access_token(&mut self, access_token: &str) {
259 self.access_token = access_token.to_string();
260 }
261
262 pub fn access_token(&self) -> &str {
264 &self.access_token
265 }
266
267 pub fn login_url(&self) -> String {
296 format!("https://kite.trade/connect/login?api_key={}&v3", self.api_key)
297 }
298
299 #[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 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 pub async fn generate_session(
380 &mut self,
381 request_token: &str,
382 api_secret: &str,
383 ) -> Result<JsonValue> {
384 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 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 pub async fn renew_access_token(
418 &mut self,
419 access_token: &str,
420 api_secret: &str,
421 ) -> Result<JsonValue> {
422 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 #[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 Ok(JsonValue::String(body))
887 }
888
889 #[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 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 #[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 Ok(JsonValue::String(body))
926 }
927}
928
929impl 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 let mut server = Server::new_async().await;
1007
1008 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 #[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 #[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 #[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}