1use crate::errors::*;
2use chrono::Local;
3use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE, USER_AGENT};
4use reqwest::{self, Response, StatusCode};
5use ring::hmac;
6use std::io::Read;
7
8static API_HOST: &'static str = "https://ftx.com/api";
9
10#[derive(Clone)]
11pub struct Client {
12 api_key: String,
13 secret_key: String,
14 client: reqwest::Client,
15}
16
17impl Client {
18 pub fn new(api_key: Option<String>, secret_key: Option<String>) -> Self {
19 Client {
20 api_key: api_key.unwrap_or("".into()),
21 secret_key: secret_key.unwrap_or("".into()),
22 client: reqwest::Client::new(),
23 }
24 }
25
26 pub fn get(&self, endpoint: String, request: String) -> Result<String> {
27 let mut url: String = format!("{}{}", API_HOST, String::from(endpoint));
28 if !request.is_empty() {
29 url.push_str(format!("?{}", request).as_str());
30 }
31 let response = self.client.get(url.as_str()).send()?;
33
34 self.handler(response)
35 }
36
37 pub fn get_signed(
38 &self,
39 endpoint: String,
40 payload: String,
41 subaccount: Option<String>,
42 ) -> Result<String> {
43 let url: String = format!("{}{}", API_HOST, String::from(&endpoint));
44 let response = self
45 .client
46 .get(url.as_str())
47 .headers(self.build_headers(
48 String::from("GET"),
49 endpoint,
50 payload.clone(),
51 subaccount,
52 )?)
53 .body(payload)
54 .send()?;
55
56 self.handler(response)
57 }
58
59 pub fn post_signed(
60 &self,
61 endpoint: String,
62 payload: String,
63 subaccount: Option<String>,
64 ) -> Result<String> {
65 let url: String = format!("{}{}", API_HOST, String::from(&endpoint));
66 let response = self
68 .client
69 .post(url.as_str())
70 .headers(self.build_headers(
71 String::from("POST"),
72 endpoint,
73 payload.clone(),
74 subaccount.clone(),
75 )?)
76 .body(payload)
77 .send()?;
78
79 self.handler(response)
80 }
81
82 pub fn delete_signed(
83 &self,
84 endpoint: String,
85 payload: String,
86 subaccount: Option<String>,
87 ) -> Result<String> {
88 let url: String = format!("{}{}", API_HOST, String::from(&endpoint));
89 let response = self
90 .client
91 .delete(url.as_str())
92 .headers(self.build_headers(
93 String::from("DELETE"),
94 endpoint,
95 payload.clone(),
96 subaccount.clone(),
97 )?)
98 .body(payload)
99 .send()?;
100
101 self.handler(response)
102 }
103
104 fn build_headers(
105 &self,
106 method: String,
107 endpoint: String,
108 payload: String,
109 subaccount: Option<String>,
110 ) -> Result<HeaderMap> {
111 let mut headers = HeaderMap::new();
112 headers.insert(USER_AGENT, HeaderValue::from_static("ftx-rs"));
113 headers.insert(
114 HeaderName::from_static("ftx-key"),
115 HeaderValue::from_str(self.api_key.as_str())?,
116 );
117
118 let ts = Local::now().timestamp() * 1000;
120 let signature_payload = format!("{}{}/api{}{}", ts, method, endpoint, payload);
121 let signed_key = hmac::Key::new(hmac::HMAC_SHA256, self.secret_key.as_bytes());
122 let signature = hex::encode(hmac::sign(&signed_key, signature_payload.as_bytes()).as_ref());
123 headers.insert(
124 HeaderName::from_static("ftx-sign"),
125 HeaderValue::from_str(signature.as_str())?,
126 );
127 headers.insert(HeaderName::from_static("ftx-ts"), HeaderValue::from(ts));
128
129 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
131
132 if let Some(s) = subaccount {
134 headers.insert(
135 HeaderName::from_static("ftx-subaccount"),
136 HeaderValue::from_str(s.as_str())?,
137 );
138 }
139
140 Ok(headers)
143 }
144
145 fn handler(&self, mut response: Response) -> Result<String> {
146 match response.status() {
147 StatusCode::OK => {
148 let mut body = String::new();
149 response.read_to_string(&mut body)?;
150 return Ok(body);
151 }
152 StatusCode::INTERNAL_SERVER_ERROR => {
153 bail!("Internal Server Error");
154 }
155 StatusCode::SERVICE_UNAVAILABLE => {
156 bail!("Service Unavailable");
157 }
158 StatusCode::UNAUTHORIZED => {
159 bail!("Unauthorized");
160 }
161 StatusCode::BAD_REQUEST => {
162 bail!(format!("Bad Request: {:?}", response));
163 }
164 s => {
165 bail!(format!("Received response: {:?}", s));
166 }
167 };
168 }
169}