1#![warn(missing_docs)]
2use const_format::concatcp;
3use hmac::{Hmac, Mac};
4use reqwest::Response;
5use sha2::Sha256;
6use std::time::{SystemTime, UNIX_EPOCH};
7use tracing::{error, warn};
8
9use crate::interface::{
10 AccountInfo, AccountInfoResponse, FtxId, FtxPrice, FtxSize, Market, OrderResponse, PlaceOrder,
11 PlaceOrderResponse, RestResponseMarketList, RestResponseOrderList, WalletBalances,
12};
13
14pub use crate::ws::{OrderType, SideOfBook};
15
16const FTX_REST_URL: &str = "https://ftx.com";
17const URI_GET_ACCOUNT_INFO: &str = "/api/account";
18const URL_GET_ACCOUNT_INFO: &str = concatcp!(FTX_REST_URL, URI_GET_ACCOUNT_INFO);
19const URI_GET_WALLET: &str = "/api/wallet/balances";
20const URL_GET_WALLET: &str = concatcp!(FTX_REST_URL, URI_GET_WALLET);
21const URI_ORDERS: &str = "/api/orders";
22const URL_ORDERS: &str = concatcp!(FTX_REST_URL, URI_ORDERS);
23const URI_MARKETS: &str = "/api/markets";
24const URL_MARKETS: &str = concatcp!(FTX_REST_URL, URI_MARKETS);
25
26fn get_timestamp() -> (u128, String) {
28 let ts = SystemTime::now()
29 .duration_since(UNIX_EPOCH)
30 .unwrap()
31 .as_millis();
32
33 (ts, ts.to_string())
34}
35
36fn build_signature(
38 api_secret: &str,
39 time_stamp: &str,
40 http_cmd: &str,
41 uri: &str,
42 data: Option<&str>,
43) -> String {
44 const S_SIZE: usize = 256;
45 let mut mac = Hmac::<Sha256>::new_from_slice(api_secret.as_bytes()).unwrap();
46 let mut s = String::with_capacity(S_SIZE); s.push_str(time_stamp);
48 s.push_str(http_cmd);
49 s.push_str(uri);
50 if data.is_some() {
51 s.push_str(data.unwrap())
52 };
53 if cfg!(debug_assertions) {
54 if s.len() > S_SIZE {
55 warn!(
56 "build_signature() string buffer too small ({:?} vs {:?})",
57 s.len(),
58 S_SIZE
59 )
60 };
61 }
62 mac.update(&s.into_bytes());
63 hex::encode(mac.finalize().into_bytes())
64}
65
66pub struct RestApi {
79 client: reqwest::Client,
80 api_key: String,
81 api_secret: String,
82}
83
84impl RestApi {
85 pub fn new(api_key: &str, api_secret: &str) -> RestApi {
87 Self {
88 client: reqwest::Client::builder()
89 .tcp_nodelay(true)
90 .build()
91 .unwrap(),
92 api_key: String::from(api_key),
93 api_secret: String::from(api_secret),
94 }
95 }
96
97 fn send_get_request(
99 &self,
100 target_url: &str,
101 endpoint: &str,
102 ) -> impl std::future::Future<Output = Result<Response, reqwest::Error>> {
103 let (_, ts) = get_timestamp();
104 let signature = build_signature(&self.api_secret, &ts, "GET", endpoint, None);
105 self.client
106 .get(target_url)
107 .header("FTX-KEY", &self.api_key)
108 .header("FTX-SIGN", signature)
109 .header("FTX-TS", ts)
110 .send()
111 }
112
113 pub async fn get_account_info(&self) -> Result<AccountInfo, ()> {
115 let res = self
116 .send_get_request(URL_GET_ACCOUNT_INFO, URI_GET_ACCOUNT_INFO)
117 .await;
118
119 let mut result: Result<AccountInfo, ()> = Err(());
120 if let Ok(r) = res {
121 let msg = r.text().await.unwrap();
122 let msg: AccountInfoResponse = serde_json::from_str(&msg[..]).unwrap();
123 result = msg.result.ok_or(());
124 if !msg.success {
125 warn!("get_account_info: {}", msg.error.unwrap())
126 }
127 }
128 result
129 }
130
131 pub async fn get_wallet(&self) -> Result<WalletBalances, ()> {
133 let res = self.send_get_request(URL_GET_WALLET, URI_GET_WALLET).await;
134
135 if let Ok(r) = res {
136 let msg = r.text().await.unwrap();
137 let msg = serde_json::from_str(&msg[..]).unwrap();
138 return Ok(msg);
139 }
140 Err(())
141 }
142
143 pub async fn get_markets(&self) -> Result<Vec<Market>, ()> {
145 let res = self.send_get_request(URL_MARKETS, URI_MARKETS).await;
146
147 if let Ok(r) = res {
148 let msg = r.text().await.unwrap();
149 let msg: RestResponseMarketList = serde_json::from_str(&msg[..]).unwrap();
150 if msg.success {
151 return Ok(msg.result);
152 }
153 }
154 Err(())
155 }
156 pub async fn get_orders(&self) -> Result<RestResponseOrderList, ()> {
158 let res = self.send_get_request(URL_ORDERS, URI_ORDERS).await;
159
160 if let Ok(r) = res {
161 let msg = r.text().await.unwrap();
162 let msg: RestResponseOrderList = serde_json::from_str(&msg[..]).unwrap();
163 if msg.success {
164 return Ok(msg);
165 }
166 }
167 Err(())
168 }
169
170 pub async fn place_order(
182 &self,
183 market: &str,
184 side: SideOfBook,
185 price: FtxPrice,
186 order_type: OrderType,
187 size: FtxSize,
188 reduce_only: bool,
189 ioc: bool,
190 post_only: bool,
191 client_id: Option<&str>,
192 ) -> Result<FtxId, String> {
193 let body = PlaceOrder {
194 market,
195 side: match side {
196 SideOfBook::BUY => "buy",
197 SideOfBook::SELL => "sell",
198 },
199 price: match order_type {
200 OrderType::MARKET => None,
201 OrderType::LIMIT => Some(price),
202 },
203 order_type: match order_type {
204 OrderType::MARKET => "market",
205 OrderType::LIMIT => "limit",
206 },
207 size,
208 reduce_only,
209 ioc,
210 post_only,
211 client_id,
212 };
213 let endpoint = URI_ORDERS;
214 let target_url = URL_ORDERS;
215 let payload = serde_json::to_string(&body).unwrap();
216 let (_, ts) = get_timestamp();
217 let signature = build_signature(&self.api_secret, &ts, "POST", endpoint, Some(&payload));
218
219 let res = self
220 .client
221 .post(target_url)
222 .header("FTX-KEY", &self.api_key)
223 .header("FTX-SIGN", signature)
224 .header("FTX-TS", ts)
225 .body(payload)
226 .send()
227 .await;
228 if let Ok(r) = res {
229 let msg = r.text().await.unwrap();
230 let msg: Result<PlaceOrderResponse, _> = serde_json::from_str(&msg[..]);
231 if let Ok(m) = msg {
232 if m.success {
233 let id = m.result.expect("Unable to convert FTX ID to u64").id;
234 return Ok(id);
235 } else {
236 return Err(m.error.unwrap_or(String::from("No FTX error msg supplied")));
237 }
238 } else {
239 return Err(String::from("Serde error unpacking place order response"));
240 }
241 }
242 return Err(String::from("Unknown place_order error"));
243 }
244
245 pub async fn cancel_order(&self, order_id: FtxId) -> Result<(), ()> {
247 let mut endpoint = String::with_capacity(URI_ORDERS.len() + 20);
248 endpoint.push_str(URI_ORDERS);
249 endpoint.push_str("/");
250 endpoint.push_str(&order_id.to_string());
251 let mut target_url = String::with_capacity(URL_ORDERS.len() + 20);
252 target_url.push_str(URL_ORDERS);
253 target_url.push_str("/");
254 target_url.push_str(&order_id.to_string());
255 let (_, ts) = get_timestamp();
256 let signature = build_signature(&self.api_secret, &ts, "DELETE", &endpoint, None);
257
258 let res = self
259 .client
260 .delete(target_url)
261 .header("FTX-KEY", &self.api_key)
262 .header("FTX-SIGN", signature)
263 .header("FTX-TS", ts)
264 .send()
265 .await;
266
267 if let Ok(r) = res {
268 let msg = r.text().await.unwrap();
269 let msg: Result<OrderResponse, _> = serde_json::from_str(&msg[..]);
270 if let Ok(m) = msg {
271 if m.success {
272 return Ok(());
273 } else {
274 warn!("cancel_order: {}", m.result);
275 }
276 }
277 } else {
278 error!("cancel_order error: {:?}", res);
279 }
280 return Err(());
281 }
282}
283
284mod tests {
285 #[allow(unused_imports)]
286 use super::*;
287 #[allow(unused_imports)]
288 use tokio::time::{sleep, Duration};
289 #[allow(unused_imports)]
290 use tokio_test;
291
292 #[test]
293 fn test_build_signature_get() {
294 let signature = build_signature(
295 "T4lPid48QtjNxjLUFOcUZghD7CUJ7sTVsfuvQZF2",
296 "1588591511721",
297 "GET",
298 "/api/markets",
299 None,
300 );
301 assert_eq!(
302 signature,
303 "dbc62ec300b2624c580611858d94f2332ac636bb86eccfa1167a7777c496ee6f"
304 );
305 }
306
307 #[test]
308 fn test_build_signature_post() {
309 let payload = r#"{"market": "BTC-PERP", "side": "buy", "price": 8500, "size": 1, "type": "limit", "reduceOnly": false, "ioc": false, "postOnly": false, "clientId": null}"#;
310 let signature = build_signature(
311 "T4lPid48QtjNxjLUFOcUZghD7CUJ7sTVsfuvQZF2",
312 "1588591856950",
313 "POST",
314 "/api/orders",
315 Some(payload),
316 );
317 assert_eq!(
318 signature,
319 "c4fbabaf178658a59d7bbf57678d44c369382f3da29138f04cd46d3d582ba4ba"
320 );
321 }
322}