Skip to main content

ftx_async/
rest.rs

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
26/// Returns the current time as an Unix EPOX timestamp in milliseconds and as a string.
27fn 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
36/// Returns a message signature for the provided payload to enable authenticated GET/POST requests to FTX.
37fn 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); // big enough to avoid a realloc on the subsequent push_str's
47    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
66/// An asynchronouse client to make REST API requests to FTX.
67///
68/// Example
69/// Initialise the REST client and retrieve the list of available markets.
70/// ```rust
71/// #[tokio::main]
72/// async fn main() {
73///     let api_key = ""; // A valid api key for a FTX account
74///     let api_secret = ""; // The secret corresponding to the provided API key
75///     let client = ftx_async::rest::RestApi::new(api_key, api_secret);
76///     let markets = client.get_markets().await.unwrap();
77/// }
78pub struct RestApi {
79    client: reqwest::Client,
80    api_key: String,
81    api_secret: String,
82}
83
84impl RestApi {
85    /// Construct a new RestApi.
86    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    /// Build and send a FTX GET request and returns a future for the response.
98    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    /// Returns all positions in futures contracts in the account wallet.
114    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    /// Returns all balances in the account wallet.
132    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    /// Returns a list of all markets on the exchange.
144    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    /// Returns the list of active orders on the exchange for the current account.
157    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    /// Submit an order to the ['Place Order'](https://docs.ftx.com/reference/place-order) endpoint.  
171    ///
172    /// * 'market' - Market to trade. e.g. BTC-PERP
173    /// * 'side' - SideOfBook::BUY or SideOfBook::SELL
174    /// * 'price' - Order price; Ignored for market orders
175    /// * 'order_type' - OrderType::MARKET or OrderType::LIMIT
176    /// * 'size' - Order size
177    /// * 'reduce_only' - Only place order if it will reduce current position size
178    /// * 'ioc' - Immediate-or-cancel
179    /// * 'post_only' - Only place order if it will enter the orderbook (maker only)
180    /// * 'client_id' - (Optional) Client-assigned order iD; Max length 64 characters; must be unique on a per subaccount basis
181    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    /// Submit an order to the ['Cancel Order'](https://docs.ftx.com/reference/cancel-order) endpoint.  
246    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}