├── .github └── workflows │ └── main.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── README.md ├── endpoints.rs └── ws.rs ├── src ├── client │ ├── account.rs │ ├── general.rs │ ├── market.rs │ ├── mod.rs │ ├── userstream.rs │ └── websocket.rs ├── error.rs ├── lib.rs ├── model │ ├── mod.rs │ └── websocket.rs └── transport.rs └── tests └── ping.rs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v1 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | 34 | fmt: 35 | name: Rustfmt 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v1 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: stable 43 | override: true 44 | - run: rustup component add rustfmt 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: fmt 48 | args: --all -- --check 49 | 50 | clippy: 51 | name: Clippy 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v1 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | - run: rustup component add clippy 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- -D warnings 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | 4 | *.fmt 5 | *.iml 6 | .env -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "binance-async" 3 | version = "0.2.0" 4 | edition = "2018" 5 | license = "MIT OR Apache-2.0" 6 | authors = [ 7 | "Flavio Oliveira ", 8 | "Weiyüen Wu ", 9 | "Artem Vorotnikov ", 10 | ] 11 | 12 | description = "Rust Library for the Binance API (Async)" 13 | keywords = ["cryptocurrency", "trading", "binance", "async"] 14 | documentation = "https://docs.rs/crate/binance-async" 15 | repository = "https://github.com/dovahcrow/binance-async-rs" 16 | readme = "README.md" 17 | 18 | [badges] 19 | travis-ci = { repository = "dovahcrow/binance-async-rs" } 20 | 21 | [lib] 22 | name = "binance_async" 23 | path = "src/lib.rs" 24 | 25 | [dependencies] 26 | failure = "0.1" 27 | tracing = "0.1" 28 | 29 | tungstenite = "0.10" 30 | tokio-tungstenite = { version = "0.10", features = ["tls"] } 31 | 32 | url = "2" 33 | futures = "0.3" 34 | headers = "0.3" 35 | http = "0.2" 36 | maplit = "1" 37 | once_cell = "1" 38 | reqwest = { version = "0.10", features = ["json"] } 39 | reqwest-ext = { git = "https://github.com/vorot93/reqwest-ext" } 40 | snafu = "0.6" 41 | streamunordered = "0.5" 42 | tokio = { version = "0.2", features = ["tcp"] } 43 | 44 | chrono = { version = "0.4", features = ["serde"] } 45 | 46 | serde = { version = "1", features = ["derive"] } 47 | serde_json = "1" 48 | 49 | hex = "0.4" 50 | sha2 = "0.8" 51 | hmac = "0.7" 52 | 53 | [dev-dependencies] 54 | csv = "1" 55 | tokio = { version = "0.2", features = ["full"] } 56 | tracing-subscriber = "0.2" 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Flavio Oliveira (flavio@wisespace.io), Weiyüen Wu (doomsplayer@gmail.com) 2 | 3 | Licensed under either of 4 | 5 | * Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0) 6 | * MIT license (http://opensource.org/licenses/MIT) 7 | 8 | at your option. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # binance-async-rs 2 | 3 | Unofficial Rust Library (Async) for the [Binance API](https://github.com/binance-exchange/binance-official-api-docs) 4 | 5 | [![Crates.io](https://img.shields.io/crates/v/binance-async.svg)](https://crates.io/crates/binance-async) 6 | [![Build Status](https://travis-ci.org/dovahcrow/binance-async-rs.png?branch=master)](https://travis-ci.org/dovahcrow/binance-async-rs) 7 | [![MIT licensed](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE-MIT) 8 | [![Apache-2.0 licensed](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE-APACHE) 9 | 10 | [Documentation](https://docs.rs/crate/binance-async) 11 | 12 | This is an async version of Flavio Oliveira (wisespace-io)'s [work](https://github.com/wisespace-io/binance-rs). 13 | ## Binance API Telegram 14 | 15 | 16 | 17 | ## Risk Warning 18 | 19 | It is a personal project, use at your own risk. I will not be responsible for your investment losses. 20 | Cryptocurrency investment is subject to high market risk. 21 | 22 | ## Usage 23 | 24 | Add this to your Cargo.toml 25 | 26 | ```toml 27 | [dependencies] 28 | binance-async = 0.2 29 | ``` 30 | 31 | Examples located in the examples folder. 32 | 33 | ## Other Exchanges 34 | 35 | If you use [Bitfinex](https://www.bitfinex.com/) check out my [Rust library for bitfinex API](https://github.com/wisespace-io/bitfinex-rs) 36 | 37 | If you use [BitMEX](https://www.bitmex.com/) check out my [Rust library for bitmex API](https://github.com/dovahcrow/bitmex-rs) 38 | 39 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Binance Endpoints 4 | 5 | cargo run --release --example "endpoints" 6 | 7 | ## Binance Websockets 8 | 9 | cargo run --release --example "ws" -------------------------------------------------------------------------------- /examples/endpoints.rs: -------------------------------------------------------------------------------- 1 | use crate::binance::Binance; 2 | use binance_async as binance; 3 | use failure::Fallible; 4 | use std::env::var; 5 | 6 | #[tokio::main] 7 | async fn main() -> Fallible<()> { 8 | tracing::subscriber::set_global_default(tracing_subscriber::FmtSubscriber::new()).unwrap(); 9 | 10 | let api_key = var("BINANCE_KEY")?; 11 | let secret_key = var("BINANCE_SECRET")?; 12 | 13 | let bn = Binance::with_credential(&api_key, &secret_key); 14 | 15 | // General 16 | match bn.ping()?.await { 17 | Ok(answer) => println!("{:?}", answer), 18 | Err(e) => println!("Error: {}", e), 19 | } 20 | 21 | match bn.get_server_time()?.await { 22 | Ok(answer) => println!("Server Time: {}", answer.server_time), 23 | Err(e) => println!("Error: {}", e), 24 | } 25 | 26 | // Account 27 | match bn.get_account()?.await { 28 | Ok(answer) => println!("{:?}", answer.balances), 29 | Err(e) => println!("Error: {}", e), 30 | } 31 | 32 | match bn.get_open_orders("WTCETH")?.await { 33 | Ok(answer) => println!("{:?}", answer), 34 | Err(e) => println!("Error: {}", e), 35 | } 36 | 37 | match bn.limit_buy("ETHBTC", 1., 0.1)?.await { 38 | Ok(answer) => println!("{:?}", answer), 39 | Err(e) => println!("Error: {}", e), 40 | } 41 | 42 | match bn.market_buy("WTCETH", 5.)?.await { 43 | Ok(answer) => println!("{:?}", answer), 44 | Err(e) => println!("Error: {}", e), 45 | } 46 | 47 | match bn.limit_sell("WTCETH", 10., 0.035_000)?.await { 48 | Ok(answer) => println!("{:?}", answer), 49 | Err(e) => println!("Error: {}", e), 50 | } 51 | 52 | match bn.market_sell("WTCETH", 5.)?.await { 53 | Ok(answer) => println!("{:?}", answer), 54 | Err(e) => println!("Error: {}", e), 55 | } 56 | 57 | match bn.order_status("WTCETH", 1_957_528)?.await { 58 | Ok(answer) => println!("{:?}", answer), 59 | Err(e) => println!("Error: {}", e), 60 | } 61 | 62 | match bn.cancel_order("WTCETH", 1_957_528)?.await { 63 | Ok(answer) => println!("{:?}", answer), 64 | Err(e) => println!("Error: {}", e), 65 | } 66 | 67 | match bn.get_balance("KNC")?.await { 68 | Ok(answer) => println!("{:?}", answer), 69 | Err(e) => println!("Error: {}", e), 70 | } 71 | 72 | match bn.trade_history("WTCETH")?.await { 73 | Ok(answer) => println!("{:?}", answer), 74 | Err(e) => println!("Error: {}", e), 75 | } 76 | 77 | // Market 78 | 79 | // Order book 80 | match bn.get_depth("BNBETH", None)?.await { 81 | Ok(answer) => println!("{:?}", answer), 82 | Err(e) => println!("Error: {}", e), 83 | } 84 | 85 | // Latest price for ALL symbols 86 | match bn.get_all_prices()?.await { 87 | Ok(answer) => println!("{:?}", answer), 88 | Err(e) => println!("Error: {}", e), 89 | } 90 | 91 | // Latest price for ONE symbol 92 | match bn.get_price("KNCETH")?.await { 93 | Ok(answer) => println!("{:?}", answer), 94 | Err(e) => println!("Error: {}", e), 95 | } 96 | 97 | // Best price/qty on the order book for ALL symbols 98 | match bn.get_all_book_tickers()?.await { 99 | Ok(answer) => println!("{:?}", answer), 100 | Err(e) => println!("Error: {}", e), 101 | } 102 | 103 | // Best price/qty on the order book for ONE symbol 104 | match bn.get_book_ticker("BNBETH")?.await { 105 | Ok(answer) => println!( 106 | "Bid Price: {}, Ask Price: {}", 107 | answer.bid_price, answer.ask_price 108 | ), 109 | Err(e) => println!("Error: {}", e), 110 | } 111 | 112 | // 24hr ticker price change statistics 113 | match bn.get_24h_price_stats("BNBETH")?.await { 114 | Ok(answer) => println!( 115 | "Open Price: {}, Higher Price: {}, Lower Price: {:?}", 116 | answer.open_price, answer.high_price, answer.low_price 117 | ), 118 | Err(e) => println!("Error: {}", e), 119 | } 120 | 121 | // last 10 5min klines (candlesticks) for a symbol: 122 | match bn.get_klines("BNBETH", "5m", 10, None, None)?.await { 123 | Ok(answer) => println!("{:?}", answer), 124 | Err(e) => println!("Error: {}", e), 125 | } 126 | 127 | Ok(()) 128 | } 129 | -------------------------------------------------------------------------------- /examples/ws.rs: -------------------------------------------------------------------------------- 1 | use crate::binance::{model::websocket::Subscription, Binance, BinanceWebsocket}; 2 | use binance_async as binance; 3 | use failure::Fallible; 4 | use std::env::var; 5 | use tokio::stream::StreamExt; 6 | 7 | #[tokio::main] 8 | async fn main() -> Fallible<()> { 9 | tracing::subscriber::set_global_default(tracing_subscriber::FmtSubscriber::new()).unwrap(); 10 | 11 | let api_key_user = var("BINANCE_KEY")?; 12 | let api_secret_user = var("BINANCE_SECRET")?; 13 | 14 | let bn = Binance::with_credential(&api_key_user, &api_secret_user); 15 | match bn.user_stream_start()?.await { 16 | Ok(answer) => { 17 | println!("Data Stream Started ..."); 18 | let listen_key = answer.listen_key; 19 | 20 | let mut ws = BinanceWebsocket::default(); 21 | 22 | for sub in vec![ 23 | Subscription::Ticker("ethbtc".to_string()), 24 | Subscription::AggregateTrade("eosbtc".to_string()), 25 | Subscription::Candlestick("ethbtc".to_string(), "1m".to_string()), 26 | Subscription::Depth("xrpbtc".to_string()), 27 | Subscription::MiniTicker("zrxbtc".to_string()), 28 | Subscription::OrderBook("trxbtc".to_string(), 5), 29 | Subscription::Trade("adabtc".to_string()), 30 | Subscription::UserData(listen_key), 31 | Subscription::MiniTickerAll, 32 | Subscription::TickerAll, 33 | ] { 34 | ws.subscribe(sub).await?; 35 | } 36 | 37 | while let Some(msg) = ws.try_next().await? { 38 | println!("{:?}", msg) 39 | } 40 | } 41 | Err(e) => println!("Error obtaining userstream: {}", e), 42 | } 43 | 44 | Ok(()) 45 | } 46 | -------------------------------------------------------------------------------- /src/client/account.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | client::Binance, 3 | error::Error, 4 | model::{ 5 | AccountInformation, AssetDetail, Balance, DepositAddressData, DepositHistory, Order, 6 | OrderCanceled, TradeHistory, Transaction, 7 | }, 8 | }; 9 | use chrono::prelude::*; 10 | use failure::Fallible; 11 | use futures::prelude::*; 12 | use serde_json::json; 13 | use std::collections::HashMap; 14 | 15 | const ORDER_TYPE_LIMIT: &str = "LIMIT"; 16 | const ORDER_TYPE_MARKET: &str = "MARKET"; 17 | const ORDER_SIDE_BUY: &str = "BUY"; 18 | const ORDER_SIDE_SELL: &str = "SELL"; 19 | const TIME_IN_FORCE_GTC: &str = "GTC"; 20 | 21 | const API_V3_ORDER: &str = "/api/v3/order"; 22 | 23 | struct OrderRequest { 24 | pub symbol: String, 25 | pub qty: f64, 26 | pub price: f64, 27 | pub order_side: String, 28 | pub order_type: String, 29 | pub time_in_force: String, 30 | } 31 | 32 | impl Binance { 33 | // Account Information 34 | pub fn get_account(&self) -> Fallible>> { 35 | let account_info = self 36 | .transport 37 | .signed_get::<_, ()>("/api/v3/account", None)?; 38 | Ok(account_info) 39 | } 40 | 41 | // Balance for ONE Asset 42 | pub fn get_balance(&self, asset: &str) -> Fallible>> { 43 | let asset = asset.to_string(); 44 | let search = move |account: AccountInformation| { 45 | let balance = account 46 | .balances 47 | .into_iter() 48 | .find(|balance| balance.asset == asset); 49 | future::ready(balance.ok_or_else(|| Error::AssetsNotFound.into())) 50 | }; 51 | 52 | let balance = self.get_account()?.and_then(search); 53 | Ok(balance) 54 | } 55 | 56 | // Current open orders for ONE symbol 57 | pub fn get_open_orders( 58 | &self, 59 | symbol: &str, 60 | ) -> Fallible>>> { 61 | let params = json! {{"symbol": symbol}}; 62 | let orders = self 63 | .transport 64 | .signed_get("/api/v3/openOrders", Some(params))?; 65 | Ok(orders) 66 | } 67 | 68 | // All current open orders 69 | pub fn get_all_open_orders(&self) -> Fallible>>> { 70 | let orders = self 71 | .transport 72 | .signed_get::<_, ()>("/api/v3/openOrders", None)?; 73 | Ok(orders) 74 | } 75 | 76 | // Check an order's status 77 | pub fn order_status( 78 | &self, 79 | symbol: &str, 80 | order_id: u64, 81 | ) -> Fallible>> { 82 | let params = json! {{"symbol": symbol, "orderId": order_id}}; 83 | 84 | let order = self.transport.signed_get(API_V3_ORDER, Some(params))?; 85 | Ok(order) 86 | } 87 | 88 | // Place a LIMIT order - BUY 89 | pub fn limit_buy( 90 | &self, 91 | symbol: &str, 92 | qty: f64, 93 | price: f64, 94 | ) -> Fallible>> { 95 | let order = OrderRequest { 96 | symbol: symbol.into(), 97 | qty, 98 | price, 99 | order_side: ORDER_SIDE_BUY.to_string(), 100 | order_type: ORDER_TYPE_LIMIT.to_string(), 101 | time_in_force: TIME_IN_FORCE_GTC.to_string(), 102 | }; 103 | let params = Self::build_order(order); 104 | 105 | let transaction = self.transport.signed_post(API_V3_ORDER, Some(params))?; 106 | 107 | Ok(transaction) 108 | } 109 | 110 | // Place a LIMIT order - SELL 111 | pub fn limit_sell( 112 | &self, 113 | symbol: &str, 114 | qty: f64, 115 | price: f64, 116 | ) -> Fallible>> { 117 | let order = OrderRequest { 118 | symbol: symbol.into(), 119 | qty, 120 | price, 121 | order_side: ORDER_SIDE_SELL.to_string(), 122 | order_type: ORDER_TYPE_LIMIT.to_string(), 123 | time_in_force: TIME_IN_FORCE_GTC.to_string(), 124 | }; 125 | let params = Self::build_order(order); 126 | let transaction = self.transport.signed_post(API_V3_ORDER, Some(params))?; 127 | 128 | Ok(transaction) 129 | } 130 | 131 | // Place a MARKET order - BUY 132 | pub fn market_buy( 133 | &self, 134 | symbol: &str, 135 | qty: f64, 136 | ) -> Fallible>> { 137 | let order = OrderRequest { 138 | symbol: symbol.into(), 139 | qty, 140 | price: 0.0, 141 | order_side: ORDER_SIDE_BUY.to_string(), 142 | order_type: ORDER_TYPE_MARKET.to_string(), 143 | time_in_force: TIME_IN_FORCE_GTC.to_string(), 144 | }; 145 | let params = Self::build_order(order); 146 | let transaction = self.transport.signed_post(API_V3_ORDER, Some(params))?; 147 | 148 | Ok(transaction) 149 | } 150 | 151 | // Place a MARKET order - SELL 152 | pub fn market_sell( 153 | &self, 154 | symbol: &str, 155 | qty: f64, 156 | ) -> Fallible>> { 157 | let order = OrderRequest { 158 | symbol: symbol.into(), 159 | qty, 160 | price: 0.0, 161 | order_side: ORDER_SIDE_SELL.to_string(), 162 | order_type: ORDER_TYPE_MARKET.to_string(), 163 | time_in_force: TIME_IN_FORCE_GTC.to_string(), 164 | }; 165 | let params = Self::build_order(order); 166 | let transaction = self.transport.signed_post(API_V3_ORDER, Some(params))?; 167 | Ok(transaction) 168 | } 169 | 170 | // Check an order's status 171 | pub fn cancel_order( 172 | &self, 173 | symbol: &str, 174 | order_id: u64, 175 | ) -> Fallible>> { 176 | let params = json! {{"symbol":symbol, "orderId":order_id}}; 177 | let order_canceled = self.transport.signed_delete(API_V3_ORDER, Some(params))?; 178 | Ok(order_canceled) 179 | } 180 | 181 | // Trade history 182 | pub fn trade_history( 183 | &self, 184 | symbol: &str, 185 | ) -> Fallible>>> { 186 | let params = json! {{"symbol":symbol}}; 187 | let trade_history = self 188 | .transport 189 | .signed_get("/api/v3/myTrades", Some(params))?; 190 | 191 | Ok(trade_history) 192 | } 193 | 194 | pub fn get_deposit_address( 195 | &self, 196 | symbol: &str, 197 | ) -> Fallible>> { 198 | let params = json! {{"asset":symbol}}; 199 | let deposit_address = self 200 | .transport 201 | .signed_get("/wapi/v3/depositAddress.html", Some(params))?; 202 | 203 | Ok(deposit_address) 204 | } 205 | 206 | pub fn get_deposit_history( 207 | &self, 208 | symbol: Option<&str>, 209 | start_time: Option>, 210 | end_time: Option>, 211 | ) -> Fallible>> { 212 | let params = json! {{"asset":symbol, "startTime":start_time.map(|t| t.timestamp_millis()), "endTime":end_time.map(|t| t.timestamp_millis())}}; 213 | let deposit_history = self 214 | .transport 215 | .signed_get("/wapi/v3/depositHistory.html", Some(params))?; 216 | 217 | Ok(deposit_history) 218 | } 219 | 220 | pub fn asset_detail(&self) -> Fallible>> { 221 | let asset_detail = self 222 | .transport 223 | .signed_get::<_, ()>("/wapi/v3/assetDetail.html", None)?; 224 | 225 | Ok(asset_detail) 226 | } 227 | 228 | fn build_order(order: OrderRequest) -> HashMap<&'static str, String> { 229 | let mut params: HashMap<&str, String> = maplit::hashmap! { 230 | "symbol" => order.symbol, 231 | "side" => order.order_side, 232 | "type" => order.order_type, 233 | "quantity" => order.qty.to_string(), 234 | }; 235 | 236 | if order.price != 0.0 { 237 | params.insert("price", order.price.to_string()); 238 | params.insert("timeInForce", order.time_in_force.to_string()); 239 | } 240 | 241 | params 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/client/general.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | client::Binance, 3 | model::{ExchangeInfo, ExchangeInformation, ServerTime}, 4 | }; 5 | use failure::Fallible; 6 | use futures::prelude::*; 7 | use serde_json::Value; 8 | 9 | impl Binance { 10 | // Test connectivity 11 | pub fn ping(&self) -> Fallible>> { 12 | Ok(self 13 | .transport 14 | .get::<_, ()>("/api/v1/ping", None)? 15 | .map_ok(|_: Value| "pong".into())) 16 | } 17 | 18 | // Check server time 19 | pub fn get_server_time(&self) -> Fallible>> { 20 | Ok(self.transport.get::<_, ()>("/api/v1/time", None)?) 21 | } 22 | 23 | pub fn get_exchange_info(&self) -> Fallible>> { 24 | Ok(self.transport.get::<_, ()>("/api/v1/exchangeInfo", None)?) 25 | } 26 | 27 | // Obtain exchange information (rate limits, symbol metadata etc) 28 | pub fn exchange_info(&self) -> Fallible>> { 29 | let info = self.transport.get::<_, ()>("/api/v1/exchangeInfo", None)?; 30 | Ok(info) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/client/market.rs: -------------------------------------------------------------------------------- 1 | use super::Binance; 2 | use crate::{ 3 | error::Error, 4 | model::{BookTickers, KlineSummaries, KlineSummary, OrderBook, PriceStats, Prices, Ticker}, 5 | }; 6 | use failure::Fallible; 7 | use futures::prelude::*; 8 | use serde_json::{json, Value}; 9 | use std::{collections::HashMap, iter::FromIterator}; 10 | 11 | // Market Data endpoints 12 | impl Binance { 13 | // Order book (Default 100; max 100) 14 | pub fn get_depth( 15 | &self, 16 | symbol: &str, 17 | limit: I, 18 | ) -> Fallible>> 19 | where 20 | I: Into>, 21 | { 22 | let limit = limit.into().unwrap_or(100); 23 | let params = json! {{"symbol": symbol, "limit": limit}}; 24 | 25 | Ok(self.transport.get("/api/v1/depth", Some(params))?) 26 | } 27 | 28 | // Latest price for ALL symbols. 29 | pub fn get_all_prices(&self) -> Fallible>> { 30 | Ok(self 31 | .transport 32 | .get::<_, ()>("/api/v1/ticker/allPrices", None)?) 33 | } 34 | 35 | // Latest price for ONE symbol. 36 | pub fn get_price(&self, symbol: &str) -> Fallible>> { 37 | let symbol = symbol.to_string(); 38 | let all_prices = self.get_all_prices()?; 39 | Ok(async move { 40 | let Prices::AllPrices(prices) = all_prices.await?; 41 | Ok(prices 42 | .into_iter() 43 | .find_map(|obj| { 44 | if obj.symbol == symbol { 45 | Some(obj.price) 46 | } else { 47 | None 48 | } 49 | }) 50 | .ok_or_else(|| Error::SymbolNotFound)?) 51 | }) 52 | } 53 | 54 | // Symbols order book ticker 55 | // -> Best price/qty on the order book for ALL symbols. 56 | pub fn get_all_book_tickers(&self) -> Fallible>> { 57 | Ok(self 58 | .transport 59 | .get::<_, ()>("/api/v1/ticker/allBookTickers", None)?) 60 | } 61 | 62 | // -> Best price/qty on the order book for ONE symbol 63 | pub fn get_book_ticker( 64 | &self, 65 | symbol: &str, 66 | ) -> Fallible>> { 67 | let symbol = symbol.to_string(); 68 | let all_book_tickers = self.get_all_book_tickers()?; 69 | 70 | Ok(async move { 71 | let BookTickers::AllBookTickers(book_tickers) = all_book_tickers.await?; 72 | 73 | Ok(book_tickers 74 | .into_iter() 75 | .find(|obj| obj.symbol == symbol) 76 | .ok_or_else(|| Error::SymbolNotFound)?) 77 | }) 78 | } 79 | 80 | // 24hr ticker price change statistics 81 | pub fn get_24h_price_stats( 82 | &self, 83 | symbol: &str, 84 | ) -> Fallible>> { 85 | let params = json! {{"symbol": symbol}}; 86 | Ok(self.transport.get("/api/v1/ticker/24hr", Some(params))?) 87 | } 88 | 89 | // Returns up to 'limit' klines for given symbol and interval ("1m", "5m", ...) 90 | // https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#klinecandlestick-data 91 | pub fn get_klines( 92 | &self, 93 | symbol: &str, 94 | interval: &str, 95 | limit: S3, 96 | start_time: S4, 97 | end_time: S5, 98 | ) -> Fallible>> 99 | where 100 | S3: Into>, 101 | S4: Into>, 102 | S5: Into>, 103 | { 104 | let mut params = vec![ 105 | ("symbol", symbol.to_string()), 106 | ("interval", interval.to_string()), 107 | ]; 108 | 109 | // Add three optional parameters 110 | if let Some(lt) = limit.into() { 111 | params.push(("limit", lt.to_string())); 112 | } 113 | if let Some(st) = start_time.into() { 114 | params.push(("startTime", st.to_string())); 115 | } 116 | if let Some(et) = end_time.into() { 117 | params.push(("endTime", et.to_string())); 118 | } 119 | let params: HashMap<&str, String> = HashMap::from_iter(params); 120 | 121 | let f = self.transport.get("/api/v1/klines", Some(params))?; 122 | 123 | Ok({ 124 | async move { 125 | let data: Vec> = f.await?; 126 | 127 | Ok(KlineSummaries::AllKlineSummaries( 128 | data.iter() 129 | .map(|row| KlineSummary { 130 | open_time: to_i64(&row[0]), 131 | open: to_f64(&row[1]), 132 | high: to_f64(&row[2]), 133 | low: to_f64(&row[3]), 134 | close: to_f64(&row[4]), 135 | volume: to_f64(&row[5]), 136 | close_time: to_i64(&row[6]), 137 | quote_asset_volume: to_f64(&row[7]), 138 | number_of_trades: to_i64(&row[8]), 139 | taker_buy_base_asset_volume: to_f64(&row[9]), 140 | taker_buy_quote_asset_volume: to_f64(&row[10]), 141 | }) 142 | .collect(), 143 | )) 144 | } 145 | }) 146 | } 147 | 148 | // 24hr ticker price change statistics 149 | pub fn get_24h_price_stats_all( 150 | &self, 151 | ) -> Fallible>>> { 152 | Ok(self.transport.get::<_, ()>("/api/v1/ticker/24hr", None)?) 153 | } 154 | } 155 | 156 | fn to_i64(v: &Value) -> i64 { 157 | v.as_i64().unwrap() 158 | } 159 | 160 | fn to_f64(v: &Value) -> f64 { 161 | v.as_str().unwrap().parse().unwrap() 162 | } 163 | -------------------------------------------------------------------------------- /src/client/mod.rs: -------------------------------------------------------------------------------- 1 | mod account; 2 | mod general; 3 | mod market; 4 | mod userstream; 5 | pub mod websocket; 6 | 7 | use crate::transport::Transport; 8 | 9 | #[derive(Clone, Default)] 10 | pub struct Binance { 11 | pub transport: Transport, 12 | } 13 | 14 | impl Binance { 15 | #[must_use] 16 | pub fn new() -> Self { 17 | Self::default() 18 | } 19 | 20 | #[must_use] 21 | pub fn with_credential(api_key: &str, api_secret: &str) -> Self { 22 | Self { 23 | transport: Transport::with_credential(api_key, api_secret), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/client/userstream.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | client::Binance, 3 | model::{Success, UserDataStream}, 4 | }; 5 | use failure::Fallible; 6 | use futures::prelude::*; 7 | 8 | const USER_DATA_STREAM: &str = "/api/v1/userDataStream"; 9 | 10 | impl Binance { 11 | // User Stream 12 | pub fn user_stream_start(&self) -> Fallible>> { 13 | let user_data_stream = self.transport.post::<_, ()>(USER_DATA_STREAM, None)?; 14 | Ok(user_data_stream) 15 | } 16 | 17 | // Current open orders on a symbol 18 | pub fn user_stream_keep_alive( 19 | &self, 20 | listen_key: &str, 21 | ) -> Fallible>> { 22 | let success = self.transport.put( 23 | USER_DATA_STREAM, 24 | Some(vec![("listen_key", listen_key.to_string())]), 25 | )?; 26 | Ok(success) 27 | } 28 | 29 | pub fn user_stream_close( 30 | &self, 31 | listen_key: &str, 32 | ) -> Fallible>> { 33 | let success = self.transport.delete( 34 | USER_DATA_STREAM, 35 | Some(vec![("listen_key", listen_key.to_string())]), 36 | )?; 37 | Ok(success) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/client/websocket.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::Error, 3 | model::websocket::{AccountUpdate, BinanceWebsocketMessage, Subscription, UserOrderUpdate}, 4 | }; 5 | use failure::Fallible; 6 | use futures::{prelude::*, stream::SplitStream}; 7 | use serde::{Deserialize, Serialize}; 8 | use serde_json::from_str; 9 | use std::{ 10 | collections::HashMap, 11 | pin::Pin, 12 | task::{Context, Poll}, 13 | }; 14 | use streamunordered::{StreamUnordered, StreamYield}; 15 | use tokio::net::TcpStream; 16 | use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream}; 17 | use tracing::*; 18 | use tungstenite::Message; 19 | use url::Url; 20 | 21 | const WS_URL: &str = "wss://stream.binance.com:9443/ws"; 22 | 23 | #[allow(dead_code)] 24 | type WSStream = WebSocketStream>; 25 | 26 | pub type StoredStream = SplitStream; 27 | 28 | #[allow(clippy::module_name_repetitions)] 29 | #[derive(Default)] 30 | pub struct BinanceWebsocket { 31 | subscriptions: HashMap, 32 | tokens: HashMap, 33 | streams: StreamUnordered, 34 | } 35 | 36 | impl BinanceWebsocket { 37 | pub async fn subscribe(&mut self, subscription: Subscription) -> Fallible<()> { 38 | let sub = match subscription { 39 | Subscription::AggregateTrade(ref symbol) => format!("{}@aggTrade", symbol), 40 | Subscription::Candlestick(ref symbol, ref interval) => { 41 | format!("{}@kline_{}", symbol, interval) 42 | } 43 | Subscription::Depth(ref symbol) => format!("{}@depth", symbol), 44 | Subscription::MiniTicker(ref symbol) => format!("{}@miniTicker", symbol), 45 | Subscription::MiniTickerAll => "!miniTicker@arr".to_string(), 46 | Subscription::OrderBook(ref symbol, depth) => format!("{}@depth{}", symbol, depth), 47 | Subscription::Ticker(ref symbol) => format!("{}@ticker", symbol), 48 | Subscription::TickerAll => "!ticker@arr".to_string(), 49 | Subscription::Trade(ref symbol) => format!("{}@trade", symbol), 50 | Subscription::UserData(ref key) => key.clone(), 51 | }; 52 | 53 | trace!("[Websocket] Subscribing to '{:?}'", subscription); 54 | 55 | let endpoint = Url::parse(&format!("{}/{}", WS_URL, sub)).unwrap(); 56 | 57 | let token = self 58 | .streams 59 | .push(connect_async(endpoint).await?.0.split().1); 60 | 61 | self.subscriptions.insert(subscription.clone(), token); 62 | self.tokens.insert(token, subscription); 63 | Ok(()) 64 | } 65 | 66 | pub fn unsubscribe(&mut self, subscription: &Subscription) -> Option { 67 | let streams = Pin::new(&mut self.streams); 68 | self.subscriptions 69 | .get(subscription) 70 | .and_then(|token| StreamUnordered::take(streams, *token)) 71 | } 72 | } 73 | 74 | impl Stream for BinanceWebsocket { 75 | type Item = Fallible; 76 | 77 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 78 | match Pin::new(&mut self.as_mut().get_mut().streams).poll_next(cx) { 79 | Poll::Ready(Some((y, token))) => match y { 80 | StreamYield::Item(item) => { 81 | let sub = self.tokens.get(&token).unwrap(); 82 | Poll::Ready({ 83 | Some( 84 | item.map_err(failure::Error::from) 85 | .and_then(|m| parse_message(sub, m)), 86 | ) 87 | }) 88 | } 89 | StreamYield::Finished(_) => Poll::Pending, 90 | }, 91 | Poll::Ready(None) => Poll::Ready(Some(Err(Error::NoStreamSubscribed.into()))), 92 | Poll::Pending => Poll::Pending, 93 | } 94 | } 95 | } 96 | 97 | fn parse_message(sub: &Subscription, msg: Message) -> Fallible { 98 | let msg = match msg { 99 | Message::Text(msg) => msg, 100 | Message::Binary(b) => return Ok(BinanceWebsocketMessage::Binary(b)), 101 | Message::Pong(..) => return Ok(BinanceWebsocketMessage::Pong), 102 | Message::Ping(..) => return Ok(BinanceWebsocketMessage::Ping), 103 | Message::Close(..) => return Err(failure::format_err!("Socket closed")), 104 | }; 105 | 106 | trace!("Incoming websocket message {}", msg); 107 | let message = match sub { 108 | Subscription::AggregateTrade(..) => { 109 | BinanceWebsocketMessage::AggregateTrade(from_str(&msg)?) 110 | } 111 | Subscription::Candlestick(..) => BinanceWebsocketMessage::Candlestick(from_str(&msg)?), 112 | Subscription::Depth(..) => BinanceWebsocketMessage::Depth(from_str(&msg)?), 113 | Subscription::MiniTicker(..) => BinanceWebsocketMessage::MiniTicker(from_str(&msg)?), 114 | Subscription::MiniTickerAll => BinanceWebsocketMessage::MiniTickerAll(from_str(&msg)?), 115 | Subscription::OrderBook(..) => BinanceWebsocketMessage::OrderBook(from_str(&msg)?), 116 | Subscription::Ticker(..) => BinanceWebsocketMessage::Ticker(from_str(&msg)?), 117 | Subscription::TickerAll => BinanceWebsocketMessage::TickerAll(from_str(&msg)?), 118 | Subscription::Trade(..) => BinanceWebsocketMessage::Trade(from_str(&msg)?), 119 | Subscription::UserData(..) => { 120 | let msg: Either = from_str(&msg)?; 121 | match msg { 122 | Either::Left(m) => BinanceWebsocketMessage::UserAccountUpdate(m), 123 | Either::Right(m) => BinanceWebsocketMessage::UserOrderUpdate(m), 124 | } 125 | } 126 | }; 127 | Ok(message) 128 | } 129 | 130 | #[derive(Debug, Clone, Serialize, Deserialize)] 131 | #[serde(untagged)] 132 | enum Either { 133 | Left(L), 134 | Right(R), 135 | } 136 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use snafu::*; 3 | 4 | #[allow(clippy::pub_enum_variant_names)] 5 | #[derive(Deserialize, Serialize, Debug, Clone, Snafu)] 6 | pub enum Error { 7 | #[snafu(display("Binance error: {}: {}", code, msg))] 8 | BinanceError { code: i64, msg: String }, 9 | #[snafu(display("Assets not found"))] 10 | AssetsNotFound, 11 | #[snafu(display("Symbol not found"))] 12 | SymbolNotFound, 13 | #[snafu(display("No Api key set for private api"))] 14 | NoApiKeySet, 15 | #[snafu(display("No stream is subscribed"))] 16 | NoStreamSubscribed, 17 | } 18 | 19 | #[derive(Deserialize, Serialize, Debug, Clone)] 20 | pub struct BinanceErrorData { 21 | pub code: i64, 22 | pub msg: String, 23 | } 24 | 25 | #[derive(Deserialize, Serialize, Debug, Clone)] 26 | #[serde(untagged)] 27 | pub enum BinanceResponse { 28 | Success(T), 29 | Error(BinanceErrorData), 30 | } 31 | 32 | impl Deserialize<'a>> BinanceResponse { 33 | pub fn into_result(self) -> Result { 34 | match self { 35 | Self::Success(t) => Result::Ok(t), 36 | Self::Error(BinanceErrorData { code, msg }) => { 37 | Result::Err(Error::BinanceError { code, msg }) 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::pedantic, clippy::nursery)] 2 | #![allow(clippy::missing_errors_doc)] 3 | 4 | mod client; 5 | pub mod error; 6 | pub mod model; 7 | mod transport; 8 | 9 | pub use crate::client::{websocket::BinanceWebsocket, Binance}; 10 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod websocket; 2 | 3 | use chrono::prelude::*; 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::HashMap; 6 | 7 | #[derive(Serialize, Deserialize, Clone)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct ServerTime { 10 | pub server_time: u64, 11 | } 12 | 13 | #[derive(Debug, Serialize, Deserialize, Clone)] 14 | #[serde(rename_all = "camelCase")] 15 | pub struct ExchangeInformation { 16 | pub timezone: String, 17 | pub server_time: u64, 18 | pub rate_limits: Vec, 19 | pub symbols: Vec, 20 | } 21 | 22 | #[derive(Debug, Serialize, Deserialize, Clone)] 23 | #[serde(rename_all = "camelCase")] 24 | pub struct AccountInformation { 25 | pub maker_commission: f32, 26 | pub taker_commission: f32, 27 | pub buyer_commission: f32, 28 | pub seller_commission: f32, 29 | pub can_trade: bool, 30 | pub can_withdraw: bool, 31 | pub can_deposit: bool, 32 | pub balances: Vec, 33 | } 34 | 35 | #[derive(Debug, Serialize, Deserialize, Clone)] 36 | #[serde(rename_all = "camelCase")] 37 | pub struct Balance { 38 | pub asset: String, 39 | pub free: String, 40 | pub locked: String, 41 | } 42 | 43 | #[derive(Debug, Serialize, Deserialize, Clone)] 44 | #[serde(rename_all = "camelCase")] 45 | pub struct Order { 46 | pub symbol: String, 47 | pub order_id: u64, 48 | pub client_order_id: String, 49 | #[serde(with = "string_or_float")] 50 | pub price: f64, 51 | pub orig_qty: String, 52 | pub executed_qty: String, 53 | pub status: String, 54 | pub time_in_force: String, 55 | #[serde(rename = "type")] 56 | pub type_name: String, 57 | pub side: String, 58 | #[serde(with = "string_or_float")] 59 | pub stop_price: f64, 60 | pub iceberg_qty: String, 61 | pub time: u64, 62 | } 63 | 64 | #[derive(Debug, Serialize, Deserialize, Clone)] 65 | #[serde(rename_all = "camelCase")] 66 | pub struct OrderCanceled { 67 | pub symbol: String, 68 | pub orig_client_order_id: String, 69 | pub order_id: u64, 70 | pub client_order_id: String, 71 | } 72 | 73 | #[derive(Debug, Serialize, Deserialize, Clone)] 74 | #[serde(rename_all = "camelCase")] 75 | pub struct Transaction { 76 | pub symbol: String, 77 | pub order_id: u64, 78 | pub client_order_id: String, 79 | pub transact_time: u64, 80 | } 81 | 82 | #[derive(Debug, Serialize, Deserialize, Clone)] 83 | pub struct Bids { 84 | #[serde(with = "string_or_float")] 85 | pub price: f64, 86 | #[serde(with = "string_or_float")] 87 | pub qty: f64, 88 | 89 | // Never serialized. 90 | #[serde(skip_serializing)] 91 | ignore: Vec, 92 | } 93 | 94 | #[derive(Debug, Serialize, Deserialize, Clone)] 95 | pub struct Asks { 96 | #[serde(with = "string_or_float")] 97 | pub price: f64, 98 | #[serde(with = "string_or_float")] 99 | pub qty: f64, 100 | 101 | // Never serialized. 102 | #[serde(skip_serializing)] 103 | ignore: Vec, 104 | } 105 | 106 | #[derive(Debug, Serialize, Deserialize, Clone)] 107 | #[serde(rename_all = "camelCase")] 108 | pub struct UserDataStream { 109 | pub listen_key: String, 110 | } 111 | 112 | #[derive(Debug, Serialize, Deserialize, Clone)] 113 | pub struct Success {} 114 | 115 | #[derive(Debug, Serialize, Deserialize, Clone)] 116 | #[serde(rename_all = "camelCase")] 117 | #[serde(untagged)] 118 | pub enum Prices { 119 | AllPrices(Vec), 120 | } 121 | 122 | #[derive(Debug, Serialize, Deserialize, Clone)] 123 | pub struct SymbolPrice { 124 | pub symbol: String, 125 | #[serde(with = "string_or_float")] 126 | pub price: f64, 127 | } 128 | 129 | #[derive(Debug, Serialize, Deserialize, Clone)] 130 | #[serde(rename_all = "camelCase")] 131 | #[serde(untagged)] 132 | pub enum BookTickers { 133 | AllBookTickers(Vec), 134 | } 135 | 136 | #[derive(Debug, Clone)] 137 | pub enum KlineSummaries { 138 | AllKlineSummaries(Vec), 139 | } 140 | 141 | #[derive(Debug, Serialize, Deserialize, Clone)] 142 | #[serde(rename_all = "camelCase")] 143 | pub struct Ticker { 144 | pub symbol: String, 145 | #[serde(with = "string_or_float")] 146 | pub bid_price: f64, 147 | #[serde(with = "string_or_float")] 148 | pub bid_qty: f64, 149 | #[serde(with = "string_or_float")] 150 | pub ask_price: f64, 151 | #[serde(with = "string_or_float")] 152 | pub ask_qty: f64, 153 | } 154 | 155 | #[derive(Debug, Serialize, Deserialize, Clone)] 156 | #[serde(rename_all = "camelCase")] 157 | pub struct TradeHistory { 158 | pub symbol: String, 159 | pub id: u64, 160 | pub order_id: u64, 161 | #[serde(with = "string_or_float")] 162 | pub price: f64, 163 | #[serde(with = "string_or_float")] 164 | pub qty: f64, 165 | #[serde(with = "string_or_float")] 166 | pub commission: f64, 167 | pub commission_asset: String, 168 | pub time: u64, 169 | pub is_buyer: bool, 170 | pub is_maker: bool, 171 | pub is_best_match: bool, 172 | } 173 | 174 | #[derive(Debug, Serialize, Deserialize, Clone)] 175 | #[serde(rename_all = "camelCase")] 176 | pub struct PriceStats { 177 | pub symbol: String, 178 | #[serde(with = "string_or_float")] 179 | pub price_change: f64, 180 | #[serde(with = "string_or_float")] 181 | pub price_change_percent: f64, 182 | #[serde(with = "string_or_float")] 183 | pub weighted_avg_price: f64, 184 | #[serde(with = "string_or_float")] 185 | pub prev_close_price: f64, 186 | #[serde(with = "string_or_float")] 187 | pub last_price: f64, 188 | #[serde(with = "string_or_float")] 189 | pub bid_price: f64, 190 | #[serde(with = "string_or_float")] 191 | pub ask_price: f64, 192 | #[serde(with = "string_or_float")] 193 | pub open_price: f64, 194 | #[serde(with = "string_or_float")] 195 | pub high_price: f64, 196 | #[serde(with = "string_or_float")] 197 | pub low_price: f64, 198 | #[serde(with = "string_or_float")] 199 | pub volume: f64, 200 | pub open_time: u64, 201 | pub close_time: u64, 202 | pub first_id: i64, // For dummy symbol "123456", it is -1 203 | pub last_id: i64, // Same as above 204 | pub count: u64, 205 | } 206 | 207 | #[derive(Debug, Clone)] 208 | pub struct KlineSummary { 209 | pub open_time: i64, 210 | 211 | pub open: f64, 212 | 213 | pub high: f64, 214 | 215 | pub low: f64, 216 | 217 | pub close: f64, 218 | 219 | pub volume: f64, 220 | 221 | pub close_time: i64, 222 | 223 | pub quote_asset_volume: f64, 224 | 225 | pub number_of_trades: i64, 226 | 227 | pub taker_buy_base_asset_volume: f64, 228 | 229 | pub taker_buy_quote_asset_volume: f64, 230 | } 231 | 232 | #[derive(Debug, Serialize, Deserialize, Clone)] 233 | #[serde(rename_all = "camelCase")] 234 | pub struct Kline { 235 | #[serde(rename = "t")] 236 | pub start_time: i64, 237 | #[serde(rename = "T")] 238 | pub end_time: i64, 239 | #[serde(rename = "s")] 240 | pub symbol: String, 241 | #[serde(rename = "i")] 242 | pub interval: String, 243 | #[serde(rename = "f")] 244 | pub first_trade_id: i32, 245 | #[serde(rename = "L")] 246 | pub last_trade_id: i32, 247 | #[serde(rename = "o")] 248 | pub open: String, 249 | #[serde(rename = "c")] 250 | pub close: String, 251 | #[serde(rename = "h")] 252 | pub high: String, 253 | #[serde(rename = "l")] 254 | pub low: String, 255 | #[serde(rename = "v")] 256 | pub volume: String, 257 | #[serde(rename = "n")] 258 | pub number_of_trades: i32, 259 | #[serde(rename = "x")] 260 | pub is_final_bar: bool, 261 | #[serde(rename = "q")] 262 | pub quote_volume: String, 263 | #[serde(rename = "V")] 264 | pub active_buy_volume: String, 265 | #[serde(rename = "Q")] 266 | pub active_volume_buy_quote: String, 267 | #[serde(skip_serializing, rename = "B")] 268 | pub ignore_me: String, 269 | } 270 | // "timezone": "UTC", 271 | // "serverTime": 1508631584636, 272 | // "rateLimits": [{ 273 | // "rateLimitType": "REQUESTS", 274 | // "interval": "MINUTE", 275 | // "limit": 1200 276 | // }, 277 | // { 278 | // "rateLimitType": "ORDERS", 279 | // "interval": "SECOND", 280 | // "limit": 10 281 | // }, 282 | // { 283 | // "rateLimitType": "ORDERS", 284 | // "interval": "DAY", 285 | // "limit": 100000 286 | // } 287 | // ], 288 | // "exchangeFilters": [], 289 | // "symbols": [{ 290 | // "symbol": "ETHBTC", 291 | // "status": "TRADING", 292 | // "baseAsset": "ETH", 293 | // "baseAssetPrecision": 8, 294 | // "quoteAsset": "BTC", 295 | // "quotePrecision": 8, 296 | // "orderTypes": ["LIMIT", "MARKET"], 297 | // "icebergAllowed": false, 298 | // "filters": [{ 299 | // "filterType": "PRICE_FILTER", 300 | // "minPrice": "0.00000100", 301 | // "maxPrice": "100000.00000000", 302 | // "tickSize": "0.00000100" 303 | // }, { 304 | // "filterType": "LOT_SIZE", 305 | // "minQty": "0.00100000", 306 | // "maxQty": "100000.00000000", 307 | // "stepSize": "0.00100000" 308 | // }, { 309 | // "filterType": "MIN_NOTIONAL", 310 | // "minNotional": "0.00100000" 311 | // }] 312 | // }] 313 | // } 314 | 315 | #[derive(Debug, Serialize, Deserialize, Clone)] 316 | #[serde(rename_all = "camelCase")] 317 | pub struct ExchangeInfo { 318 | pub timezone: String, 319 | pub server_time: u64, 320 | pub rate_limits: Vec, 321 | pub exchange_filters: Vec, 322 | pub symbols: Vec, 323 | } 324 | 325 | // { 326 | // "rateLimitType": "ORDERS", 327 | // "interval": "DAY", 328 | // "limit": 100000 329 | // } 330 | #[derive(Debug, Serialize, Deserialize, Clone)] 331 | #[serde(rename_all = "camelCase")] 332 | pub struct RateLimit { 333 | rate_limit_type: RateLimitType, 334 | interval: Interval, 335 | limit: u64, 336 | } 337 | 338 | #[derive(Debug, Serialize, Deserialize, Clone)] 339 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 340 | pub enum RateLimitType { 341 | Orders, 342 | RequestWeight, 343 | } 344 | 345 | #[derive(Debug, Serialize, Deserialize, Clone)] 346 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 347 | pub enum Interval { 348 | Second, 349 | Minute, 350 | Day, 351 | } 352 | 353 | // { 354 | // "filterType": "LOT_SIZE", 355 | // "minQty": "0.00100000", 356 | // "maxQty": "100000.00000000", 357 | // "stepSize": "0.00100000" 358 | // } 359 | #[derive(Debug, Serialize, Deserialize, Clone)] 360 | #[serde(tag = "filterType", rename_all = "SCREAMING_SNAKE_CASE")] 361 | pub enum SymbolFilter { 362 | #[serde(rename_all = "camelCase")] 363 | LotSize { 364 | min_qty: String, 365 | max_qty: String, 366 | step_size: String, 367 | }, 368 | #[serde(rename_all = "camelCase")] 369 | PriceFilter { 370 | min_price: String, 371 | max_price: String, 372 | tick_size: String, 373 | }, 374 | #[serde(rename_all = "camelCase")] 375 | MinNotional { min_notional: String }, 376 | #[serde(rename_all = "camelCase")] 377 | MaxNumAlgoOrders { max_num_algo_orders: u64 }, 378 | #[serde(rename_all = "camelCase")] 379 | MaxNumOrders { limit: u64 }, 380 | #[serde(rename_all = "camelCase")] 381 | IcebergParts { limit: u64 }, 382 | } 383 | 384 | // { 385 | // "symbol": "ETHBTC", 386 | // "status": "TRADING", 387 | // "baseAsset": "ETH", 388 | // "baseAssetPrecision": 8, 389 | // "quoteAsset": "BTC", 390 | // "quotePrecision": 8, 391 | // "orderTypes": ["LIMIT", "MARKET"], 392 | // "icebergAllowed": false, 393 | // "filters": [{ 394 | // "filterType": "PRICE_FILTER", 395 | // "minPrice": "0.00000100", 396 | // "maxPrice": "100000.00000000", 397 | // "tickSize": "0.00000100" 398 | // }, { 399 | // "filterType": "LOT_SIZE", 400 | // "minQty": "0.00100000", 401 | // "maxQty": "100000.00000000", 402 | // "stepSize": "0.00100000" 403 | // }, { 404 | // "filterType": "MIN_NOTIONAL", 405 | // "minNotional": "0.00100000" 406 | // }] 407 | // } 408 | #[derive(Debug, Serialize, Deserialize, Clone)] 409 | #[serde(tag = "filterType", rename_all = "SCREAMING_SNAKE_CASE")] 410 | pub enum ExchangeFilter { 411 | ExchangeMaxNumOrders { limit: u64 }, 412 | ExchangeMaxAlgoOrders { limit: u64 }, 413 | } 414 | 415 | #[derive(Debug, Serialize, Deserialize, Clone)] 416 | #[serde(rename_all = "camelCase")] 417 | pub struct Symbol { 418 | pub symbol: String, 419 | pub status: String, 420 | pub base_asset: String, 421 | pub base_asset_precision: u64, 422 | pub quote_asset: String, 423 | pub quote_precision: u64, 424 | pub order_types: Vec, 425 | pub iceberg_allowed: bool, 426 | pub filters: Vec, 427 | } 428 | 429 | #[derive(Debug, Serialize, Deserialize, Clone)] 430 | #[serde(rename_all = "camelCase")] 431 | pub struct OrderBook { 432 | pub last_update_id: u64, 433 | pub bids: Vec, 434 | pub asks: Vec, 435 | } 436 | 437 | #[derive(Serialize, Deserialize, Clone, Debug)] 438 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 439 | pub enum Side { 440 | Buy, 441 | Sell, 442 | } 443 | 444 | #[derive(Serialize, Deserialize, Clone, Debug)] 445 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 446 | pub enum OrderType { 447 | Market, 448 | Limit, 449 | StopLoss, 450 | StopLossLimit, 451 | TakeProfit, 452 | TakeProfitLimit, 453 | LimitMaker, 454 | } 455 | 456 | #[derive(Serialize, Deserialize, Clone, Debug)] 457 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 458 | pub enum TimeInForce { 459 | GTC, 460 | IOC, 461 | FOK, 462 | } 463 | 464 | #[derive(Serialize, Deserialize, Clone, Debug)] 465 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 466 | pub enum OrderExecType { 467 | New, 468 | } 469 | 470 | #[derive(Serialize, Deserialize, Clone, Debug)] 471 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 472 | pub enum OrderStatus { 473 | New, 474 | PartiallyFilled, 475 | Filled, 476 | Canceled, 477 | PendingCancel, 478 | Rejected, 479 | Expired, 480 | } 481 | 482 | #[derive(Serialize, Deserialize, Clone, Debug)] 483 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 484 | pub enum OrderRejectReason { 485 | None, 486 | } 487 | 488 | #[derive(Serialize, Deserialize, Clone, Debug)] 489 | #[serde(rename_all = "camelCase")] 490 | pub struct DepositAddressData { 491 | pub address: String, 492 | pub address_tag: String, 493 | } 494 | 495 | #[derive(Serialize, Deserialize, Clone, Debug)] 496 | #[serde(rename_all = "camelCase")] 497 | pub struct DepositHistoryEntry { 498 | #[serde(with = "chrono::serde::ts_milliseconds")] 499 | pub insert_time: DateTime, 500 | pub amount: f64, 501 | pub asset: String, 502 | pub address: String, 503 | pub address_tag: Option, 504 | pub tx_id: String, 505 | pub status: u8, 506 | } 507 | 508 | #[derive(Serialize, Deserialize, Clone, Debug)] 509 | #[serde(rename_all = "camelCase")] 510 | pub struct DepositHistory { 511 | pub deposit_list: Vec, 512 | } 513 | 514 | #[derive(Serialize, Deserialize, Clone, Debug)] 515 | #[serde(rename_all = "camelCase")] 516 | pub struct AssetDetailEntry { 517 | pub min_withdraw_amount: f64, 518 | pub deposit_status: bool, 519 | pub withdraw_fee: f64, 520 | pub withdraw_status: bool, 521 | pub deposit_tip: Option, 522 | } 523 | 524 | #[derive(Serialize, Deserialize, Clone, Debug)] 525 | #[serde(rename_all = "camelCase")] 526 | pub struct AssetDetail { 527 | pub asset_detail: HashMap, 528 | } 529 | 530 | mod string_or_float { 531 | use std::fmt; 532 | 533 | use serde::{de, Deserialize, Deserializer, Serializer}; 534 | 535 | pub fn serialize(value: &T, serializer: S) -> Result 536 | where 537 | T: fmt::Display, 538 | S: Serializer, 539 | { 540 | serializer.collect_str(value) 541 | } 542 | 543 | pub fn deserialize<'de, D>(deserializer: D) -> Result 544 | where 545 | D: Deserializer<'de>, 546 | { 547 | #[derive(Deserialize)] 548 | #[serde(untagged)] 549 | enum StringOrFloat { 550 | String(String), 551 | Float(f64), 552 | } 553 | 554 | match StringOrFloat::deserialize(deserializer)? { 555 | StringOrFloat::String(s) => s.parse().map_err(de::Error::custom), 556 | StringOrFloat::Float(i) => Ok(i), 557 | } 558 | } 559 | } 560 | -------------------------------------------------------------------------------- /src/model/websocket.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | string_or_float, Asks, Bids, Kline, OrderBook, OrderExecType, OrderRejectReason, OrderStatus, 3 | OrderType, Side, TimeInForce, 4 | }; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 8 | pub enum Subscription { 9 | UserData(String), // listen key 10 | AggregateTrade(String), //symbol 11 | Trade(String), //symbol 12 | Candlestick(String, String), //symbol, interval 13 | MiniTicker(String), //symbol 14 | MiniTickerAll, 15 | Ticker(String), // symbol 16 | TickerAll, 17 | OrderBook(String, i64), //symbol, depth 18 | Depth(String), //symbol 19 | } 20 | 21 | #[derive(Debug, Clone, Serialize)] 22 | #[allow(clippy::large_enum_variant)] 23 | pub enum BinanceWebsocketMessage { 24 | UserOrderUpdate(UserOrderUpdate), 25 | UserAccountUpdate(AccountUpdate), 26 | AggregateTrade(AggregateTrade), 27 | Trade(TradeMessage), 28 | Candlestick(CandelStickMessage), 29 | MiniTicker(MiniTicker), 30 | MiniTickerAll(Vec), 31 | Ticker(Ticker), 32 | TickerAll(Vec), 33 | OrderBook(OrderBook), 34 | Depth(Depth), 35 | Ping, 36 | Pong, 37 | Binary(Vec), // Unexpected, unparsed 38 | } 39 | 40 | #[derive(Debug, Serialize, Deserialize, Clone)] 41 | #[serde(rename_all = "camelCase")] 42 | pub struct TradeMessage { 43 | #[serde(rename = "e")] 44 | pub event_type: String, 45 | #[serde(rename = "E")] 46 | pub event_time: u64, 47 | #[serde(rename = "s")] 48 | pub symbol: String, 49 | #[serde(rename = "t")] 50 | pub trade_id: u64, 51 | #[serde(rename = "p", with = "string_or_float")] 52 | pub price: f64, 53 | #[serde(rename = "q", with = "string_or_float")] 54 | pub qty: f64, 55 | #[serde(rename = "b")] 56 | pub buyer_order_id: u64, 57 | #[serde(rename = "a")] 58 | pub seller_order_id: u64, 59 | #[serde(rename = "T")] 60 | pub trade_order_time: u64, 61 | #[serde(rename = "m")] 62 | pub is_buyer_maker: bool, 63 | #[serde(skip_serializing, rename = "M")] 64 | pub m_ignore: bool, 65 | } 66 | 67 | #[derive(Debug, Serialize, Deserialize, Clone)] 68 | #[serde(rename_all = "camelCase")] 69 | pub struct AggregateTrade { 70 | #[serde(rename = "e")] 71 | pub event_type: String, 72 | #[serde(rename = "E")] 73 | pub event_time: u64, 74 | #[serde(rename = "s")] 75 | pub symbol: String, 76 | #[serde(rename = "a")] 77 | pub aggregated_trade_id: u64, 78 | #[serde(rename = "p", with = "string_or_float")] 79 | pub price: f64, 80 | #[serde(rename = "q", with = "string_or_float")] 81 | pub qty: f64, 82 | #[serde(rename = "f")] 83 | pub first_break_trade_id: u64, 84 | #[serde(rename = "l")] 85 | pub last_break_trade_id: u64, 86 | #[serde(rename = "T")] 87 | pub trade_order_time: u64, 88 | #[serde(rename = "m")] 89 | pub is_buyer_maker: bool, 90 | #[serde(skip_serializing, rename = "M")] 91 | pub m_ignore: bool, 92 | } 93 | 94 | #[derive(Debug, Serialize, Deserialize, Clone)] 95 | #[serde(rename_all = "camelCase")] 96 | pub struct UserOrderUpdate { 97 | #[serde(rename = "e")] 98 | pub event_type: String, 99 | #[serde(rename = "E")] 100 | pub event_time: u64, 101 | #[serde(rename = "s")] 102 | pub symbol: String, 103 | #[serde(rename = "c")] 104 | pub new_client_order_id: String, 105 | #[serde(rename = "S")] 106 | pub side: Side, 107 | #[serde(rename = "o")] 108 | pub order_type: OrderType, 109 | #[serde(rename = "f")] 110 | pub time_in_force: TimeInForce, 111 | #[serde(rename = "q", with = "string_or_float")] 112 | pub qty: f64, 113 | #[serde(rename = "p", with = "string_or_float")] 114 | pub price: f64, 115 | #[serde(rename = "P", with = "string_or_float")] 116 | pub stop_price: f64, 117 | #[serde(rename = "F", with = "string_or_float")] 118 | pub iceberg_qty: f64, 119 | #[serde(skip_serializing)] 120 | pub g: i32, 121 | #[serde(skip_serializing, rename = "C")] 122 | pub c_ignore: Option, 123 | #[serde(rename = "x")] 124 | pub execution_type: OrderExecType, 125 | #[serde(rename = "X")] 126 | pub order_status: OrderStatus, 127 | #[serde(rename = "r")] 128 | pub order_reject_reason: OrderRejectReason, 129 | #[serde(rename = "i")] 130 | pub order_id: u64, 131 | #[serde(rename = "l", with = "string_or_float")] 132 | pub qty_last_filled_trade: f64, 133 | #[serde(rename = "z", with = "string_or_float")] 134 | pub accumulated_qty_filled_trades: f64, 135 | #[serde(rename = "L", with = "string_or_float")] 136 | pub price_last_filled_trade: f64, 137 | #[serde(rename = "n", with = "string_or_float")] 138 | pub commission: f64, 139 | #[serde(skip_serializing, rename = "N")] 140 | pub asset_commisioned: Option, 141 | #[serde(rename = "T")] 142 | pub trade_order_time: u64, 143 | #[serde(rename = "t")] 144 | pub trade_id: i64, 145 | #[serde(skip_serializing, rename = "I")] 146 | pub i_ignore: u64, 147 | #[serde(skip_serializing)] 148 | pub w: bool, 149 | #[serde(rename = "m")] 150 | pub is_buyer_maker: bool, 151 | #[serde(skip_serializing, rename = "M")] 152 | pub m_ignore: bool, 153 | #[serde(skip_serializing, rename = "O")] 154 | pub order_creation_time: u64, 155 | #[serde(skip_serializing, rename = "Z", with = "string_or_float")] 156 | pub cumulative_quote_asset_transacted_qty: f64, 157 | } 158 | 159 | #[derive(Debug, Serialize, Deserialize, Clone)] 160 | #[serde(rename_all = "camelCase")] 161 | pub struct Depth { 162 | #[serde(rename = "e")] 163 | pub event_type: String, 164 | #[serde(rename = "E")] 165 | pub event_time: u64, 166 | #[serde(rename = "s")] 167 | pub symbol: String, 168 | #[serde(rename = "U")] 169 | pub first_update_id: u64, 170 | #[serde(rename = "u")] 171 | pub final_update_id: u64, 172 | #[serde(rename = "b")] 173 | pub bids: Vec, 174 | #[serde(rename = "a")] 175 | pub asks: Vec, 176 | } 177 | 178 | #[derive(Debug, Serialize, Deserialize, Clone)] 179 | #[serde(rename_all = "camelCase")] 180 | pub struct Ticker { 181 | #[serde(rename = "e")] 182 | pub event_type: String, 183 | #[serde(rename = "E")] 184 | pub event_time: u64, 185 | #[serde(rename = "s")] 186 | pub symbol: String, 187 | #[serde(rename = "p", with = "string_or_float")] 188 | pub price_change: f64, 189 | #[serde(rename = "P", with = "string_or_float")] 190 | pub price_change_percent: f64, 191 | #[serde(rename = "w", with = "string_or_float")] 192 | pub average_price: f64, 193 | #[serde(rename = "x", with = "string_or_float")] 194 | pub prev_close: f64, 195 | #[serde(rename = "c", with = "string_or_float")] 196 | pub current_close: f64, 197 | #[serde(rename = "Q", with = "string_or_float")] 198 | pub current_close_qty: f64, 199 | #[serde(rename = "b", with = "string_or_float")] 200 | pub best_bid: f64, 201 | #[serde(rename = "B", with = "string_or_float")] 202 | pub best_bid_qty: f64, 203 | #[serde(rename = "a", with = "string_or_float")] 204 | pub best_ask: f64, 205 | #[serde(rename = "A", with = "string_or_float")] 206 | pub best_ask_qty: f64, 207 | #[serde(rename = "o", with = "string_or_float")] 208 | pub open: f64, 209 | #[serde(rename = "h", with = "string_or_float")] 210 | pub high: f64, 211 | #[serde(rename = "l", with = "string_or_float")] 212 | pub low: f64, 213 | #[serde(rename = "v", with = "string_or_float")] 214 | pub volume: f64, 215 | #[serde(rename = "q", with = "string_or_float")] 216 | pub quote_volume: f64, 217 | #[serde(rename = "O")] 218 | pub open_time: u64, 219 | #[serde(rename = "C")] 220 | pub close_time: u64, 221 | #[serde(rename = "F")] 222 | pub first_trade_id: u64, 223 | #[serde(rename = "L")] 224 | pub last_trade_id: u64, 225 | #[serde(rename = "n")] 226 | pub num_trades: u64, 227 | } 228 | 229 | #[derive(Debug, Serialize, Deserialize, Clone)] 230 | #[serde(rename_all = "camelCase")] 231 | pub struct CandelStickMessage { 232 | #[serde(rename = "e")] 233 | pub event_type: String, 234 | #[serde(rename = "E")] 235 | pub event_time: u64, 236 | #[serde(rename = "s")] 237 | pub symbol: String, 238 | #[serde(rename = "k")] 239 | pub kline: Kline, 240 | } 241 | 242 | #[derive(Debug, Serialize, Deserialize, Clone)] 243 | #[serde(rename_all = "camelCase")] 244 | pub struct AccountUpdate { 245 | #[serde(rename = "e")] 246 | pub event_type: String, 247 | #[serde(rename = "E")] 248 | pub event_time: u64, 249 | #[serde(rename = "m")] 250 | pub maker_commision_rate: u64, 251 | #[serde(rename = "t")] 252 | pub taker_commision_rate: u64, 253 | #[serde(rename = "b")] 254 | pub buyer_commision_rate: u64, 255 | #[serde(rename = "s")] 256 | pub seller_commision_rate: u64, 257 | #[serde(rename = "T")] 258 | pub can_trade: bool, 259 | #[serde(rename = "W")] 260 | pub can_withdraw: bool, 261 | #[serde(rename = "D")] 262 | pub can_deposit: bool, 263 | #[serde(rename = "u")] 264 | pub last_account_update: u64, 265 | #[serde(rename = "B")] 266 | pub balance: Vec, 267 | } 268 | 269 | #[derive(Debug, Serialize, Deserialize, Clone)] 270 | #[serde(rename_all = "camelCase")] 271 | pub struct AccountUpdateBalance { 272 | #[serde(rename = "a")] 273 | pub asset: String, 274 | #[serde(rename = "f", with = "string_or_float")] 275 | pub free: f64, 276 | #[serde(rename = "l", with = "string_or_float")] 277 | pub locked: f64, 278 | } 279 | 280 | #[derive(Debug, Serialize, Deserialize, Clone)] 281 | #[serde(rename_all = "camelCase")] 282 | pub struct MiniTicker { 283 | #[serde(rename = "e")] 284 | pub event_type: String, 285 | #[serde(rename = "E")] 286 | pub event_time: u64, 287 | #[serde(rename = "s")] 288 | pub symbol: String, 289 | #[serde(rename = "c", with = "string_or_float")] 290 | pub close: f64, 291 | #[serde(rename = "o", with = "string_or_float")] 292 | pub open: f64, 293 | #[serde(rename = "l", with = "string_or_float")] 294 | pub low: f64, 295 | #[serde(rename = "h", with = "string_or_float")] 296 | pub high: f64, 297 | #[serde(rename = "v", with = "string_or_float")] 298 | pub volume: f64, 299 | #[serde(rename = "q", with = "string_or_float")] 300 | pub quote_volume: f64, 301 | } 302 | -------------------------------------------------------------------------------- /src/transport.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{BinanceResponse, Error}; 2 | use chrono::Utc; 3 | use failure::Fallible; 4 | use futures::prelude::*; 5 | use headers::*; 6 | use hex::encode as hexify; 7 | use hmac::{Hmac, Mac}; 8 | use http::Method; 9 | use once_cell::sync::OnceCell; 10 | use reqwest_ext::*; 11 | use serde::{de::DeserializeOwned, Serialize}; 12 | use serde_json::{to_string, to_value, Value}; 13 | use sha2::Sha256; 14 | use std::str::FromStr; 15 | use tracing::*; 16 | use url::Url; 17 | 18 | const BASE: &str = "https://www.binance.com"; 19 | const RECV_WINDOW: usize = 5000; 20 | 21 | pub struct BinanceApiKey(pub String); 22 | 23 | impl headers::Header for BinanceApiKey { 24 | fn name() -> &'static HeaderName { 25 | static H: OnceCell = OnceCell::new(); 26 | 27 | H.get_or_init(|| HeaderName::from_str("X-MBX-APIKEY").unwrap()) 28 | } 29 | 30 | fn decode<'i, I>(values: &mut I) -> Result 31 | where 32 | Self: Sized, 33 | I: Iterator, 34 | { 35 | values 36 | .next() 37 | .and_then(|v| v.to_str().map(ToString::to_string).ok()) 38 | .map(Self) 39 | .ok_or_else(headers::Error::invalid) 40 | } 41 | 42 | fn encode>(&self, values: &mut E) { 43 | values.extend(Some(self.0.parse().unwrap())); 44 | } 45 | } 46 | 47 | #[derive(Clone)] 48 | pub struct Transport { 49 | credential: Option<(String, String)>, 50 | client: reqwest::Client, 51 | pub recv_window: usize, 52 | } 53 | 54 | impl Default for Transport { 55 | fn default() -> Self { 56 | Self::new() 57 | } 58 | } 59 | 60 | impl Transport { 61 | pub fn new() -> Self { 62 | Self { 63 | credential: None, 64 | client: reqwest::Client::builder().build().unwrap(), 65 | recv_window: RECV_WINDOW, 66 | } 67 | } 68 | 69 | pub fn with_credential(api_key: &str, api_secret: &str) -> Self { 70 | Self { 71 | client: reqwest::Client::builder().build().unwrap(), 72 | credential: Some((api_key.into(), api_secret.into())), 73 | recv_window: RECV_WINDOW, 74 | } 75 | } 76 | 77 | pub fn get( 78 | &self, 79 | endpoint: &str, 80 | params: Option, 81 | ) -> Fallible>> 82 | where 83 | O: DeserializeOwned, 84 | Q: Serialize, 85 | { 86 | self.request::<_, _, ()>(Method::GET, endpoint, params, None) 87 | } 88 | 89 | pub fn post( 90 | &self, 91 | endpoint: &str, 92 | data: Option, 93 | ) -> Fallible>> 94 | where 95 | O: DeserializeOwned, 96 | D: Serialize, 97 | { 98 | self.request::<_, (), _>(Method::POST, endpoint, None, data) 99 | } 100 | 101 | pub fn put( 102 | &self, 103 | endpoint: &str, 104 | data: Option, 105 | ) -> Fallible>> 106 | where 107 | O: DeserializeOwned, 108 | D: Serialize, 109 | { 110 | self.request::<_, (), _>(Method::PUT, endpoint, None, data) 111 | } 112 | 113 | pub fn delete( 114 | &self, 115 | endpoint: &str, 116 | params: Option, 117 | ) -> Fallible>> 118 | where 119 | O: DeserializeOwned, 120 | Q: Serialize, 121 | { 122 | self.request::<_, _, ()>(Method::DELETE, endpoint, params, None) 123 | } 124 | 125 | pub fn signed_get( 126 | &self, 127 | endpoint: &str, 128 | params: Option, 129 | ) -> Fallible>> 130 | where 131 | O: DeserializeOwned, 132 | Q: Serialize, 133 | { 134 | self.signed_request::<_, _, ()>(Method::GET, endpoint, params, None) 135 | } 136 | 137 | pub fn signed_post( 138 | &self, 139 | endpoint: &str, 140 | data: Option, 141 | ) -> Fallible>> 142 | where 143 | O: DeserializeOwned, 144 | D: Serialize, 145 | { 146 | self.signed_request::<_, (), _>(Method::POST, endpoint, None, data) 147 | } 148 | 149 | pub fn signed_put( 150 | &self, 151 | endpoint: &str, 152 | params: Option, 153 | ) -> Fallible>> 154 | where 155 | O: DeserializeOwned, 156 | Q: Serialize, 157 | { 158 | self.signed_request::<_, _, ()>(Method::PUT, endpoint, params, None) 159 | } 160 | 161 | pub fn signed_delete( 162 | &self, 163 | endpoint: &str, 164 | params: Option, 165 | ) -> Fallible>> 166 | where 167 | O: DeserializeOwned, 168 | Q: Serialize, 169 | { 170 | self.signed_request::<_, _, ()>(Method::DELETE, endpoint, params, None) 171 | } 172 | 173 | pub fn request( 174 | &self, 175 | method: Method, 176 | endpoint: &str, 177 | params: Option, 178 | data: Option, 179 | ) -> Fallible>> 180 | where 181 | O: DeserializeOwned, 182 | Q: Serialize, 183 | D: Serialize, 184 | { 185 | let url = format!("{}{}", BASE, endpoint); 186 | let url = match params { 187 | Some(p) => Url::parse_with_params(&url, p.to_url_query())?, 188 | None => Url::parse(&url)?, 189 | }; 190 | 191 | let body = match data { 192 | Some(data) => data.to_url_query_string(), 193 | None => "".to_string(), 194 | }; 195 | 196 | let mut req = self 197 | .client 198 | .request(method, url.as_str()) 199 | .typed_header(headers::UserAgent::from_static("binance-rs")) 200 | .typed_header(headers::ContentType::form_url_encoded()); 201 | 202 | if let Ok((key, _)) = self.check_key() { 203 | // This is for user stream: user stream requests need api key in the header but no signature. WEIRD 204 | req = req.typed_header(BinanceApiKey(key.to_string())); 205 | } 206 | 207 | let req = req.body(body); 208 | 209 | Ok(async move { 210 | Ok(req 211 | .send() 212 | .await? 213 | .json::>() 214 | .await? 215 | .into_result()?) 216 | }) 217 | } 218 | 219 | pub fn signed_request( 220 | &self, 221 | method: Method, 222 | endpoint: &str, 223 | params: Option, 224 | data: Option, 225 | ) -> Fallible>> 226 | where 227 | O: DeserializeOwned, 228 | Q: Serialize, 229 | D: Serialize, 230 | { 231 | let query = params.map_or_else(Vec::new, |q| q.to_url_query()); 232 | let url = format!("{}{}", BASE, endpoint); 233 | let mut url = Url::parse_with_params(&url, &query)?; 234 | url.query_pairs_mut() 235 | .append_pair("timestamp", &Utc::now().timestamp_millis().to_string()); 236 | url.query_pairs_mut() 237 | .append_pair("recvWindow", &self.recv_window.to_string()); 238 | 239 | let body = data.map_or_else(String::new, |data| data.to_url_query_string()); 240 | 241 | let (key, signature) = self.signature(&url, &body)?; 242 | url.query_pairs_mut().append_pair("signature", &signature); 243 | 244 | let req = self 245 | .client 246 | .request(method, url.as_str()) 247 | .typed_header(headers::UserAgent::from_static("binance-rs")) 248 | .typed_header(headers::ContentType::form_url_encoded()) 249 | .typed_header(BinanceApiKey(key.to_string())) 250 | .body(body); 251 | 252 | Ok(async move { 253 | Ok(req 254 | .send() 255 | .await? 256 | .json::>() 257 | .await? 258 | .into_result()?) 259 | }) 260 | } 261 | 262 | fn check_key(&self) -> Fallible<(&str, &str)> { 263 | match self.credential.as_ref() { 264 | None => Err(Error::NoApiKeySet.into()), 265 | Some((k, s)) => Ok((k, s)), 266 | } 267 | } 268 | 269 | pub(self) fn signature(&self, url: &Url, body: &str) -> Fallible<(&str, String)> { 270 | let (key, secret) = self.check_key()?; 271 | // Signature: hex(HMAC_SHA256(queries + data)) 272 | let mut mac = Hmac::::new_varkey(secret.as_bytes()).unwrap(); 273 | let sign_message = format!("{}{}", url.query().unwrap_or(""), body); 274 | trace!("Sign message: {}", sign_message); 275 | mac.input(sign_message.as_bytes()); 276 | let signature = hexify(mac.result().code()); 277 | Ok((key, signature)) 278 | } 279 | } 280 | 281 | trait ToUrlQuery: Serialize { 282 | fn to_url_query_string(&self) -> String { 283 | let vec = self.to_url_query(); 284 | 285 | vec.into_iter() 286 | .map(|(k, v)| format!("{}={}", k, v)) 287 | .collect::>() 288 | .join("&") 289 | } 290 | 291 | fn to_url_query(&self) -> Vec<(String, String)> { 292 | let v = to_value(self).unwrap(); 293 | let v = v.as_object().unwrap(); 294 | let mut vec = vec![]; 295 | 296 | for (key, value) in v { 297 | match value { 298 | Value::Null => continue, 299 | Value::String(s) => vec.push((key.clone(), s.clone())), 300 | other => vec.push((key.clone(), to_string(other).unwrap())), 301 | } 302 | } 303 | 304 | vec 305 | } 306 | } 307 | 308 | impl ToUrlQuery for S {} 309 | 310 | #[cfg(test)] 311 | mod test { 312 | use super::Transport; 313 | use failure::Fallible; 314 | use url::{form_urlencoded::Serializer, Url}; 315 | 316 | #[test] 317 | fn signature_query() -> Fallible<()> { 318 | let tr = Transport::with_credential( 319 | "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A", 320 | "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j", 321 | ); 322 | let (_, sig) = tr.signature( 323 | &Url::parse_with_params( 324 | "http://a.com/api/v1/test", 325 | &[ 326 | ("symbol", "LTCBTC"), 327 | ("side", "BUY"), 328 | ("type", "LIMIT"), 329 | ("timeInForce", "GTC"), 330 | ("quantity", "1"), 331 | ("price", "0.1"), 332 | ("recvWindow", "5000"), 333 | ("timestamp", "1499827319559"), 334 | ], 335 | )?, 336 | "", 337 | )?; 338 | assert_eq!( 339 | sig, 340 | "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71" 341 | ); 342 | Ok(()) 343 | } 344 | 345 | #[test] 346 | fn signature_body() -> Fallible<()> { 347 | let tr = Transport::with_credential( 348 | "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A", 349 | "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j", 350 | ); 351 | let mut s = Serializer::new(String::new()); 352 | s.extend_pairs(&[ 353 | ("symbol", "LTCBTC"), 354 | ("side", "BUY"), 355 | ("type", "LIMIT"), 356 | ("timeInForce", "GTC"), 357 | ("quantity", "1"), 358 | ("price", "0.1"), 359 | ("recvWindow", "5000"), 360 | ("timestamp", "1499827319559"), 361 | ]); 362 | 363 | let (_, sig) = tr.signature(&Url::parse("http://a.com/api/v1/test")?, &s.finish())?; 364 | assert_eq!( 365 | sig, 366 | "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71" 367 | ); 368 | Ok(()) 369 | } 370 | 371 | #[test] 372 | fn signature_query_body() -> Fallible<()> { 373 | let tr = Transport::with_credential( 374 | "vmPUZE6mv9SD5VNHk4HlWFsOr6aKE2zvsw0MuIgwCIPy6utIco14y7Ju91duEh8A", 375 | "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j", 376 | ); 377 | 378 | let mut s = Serializer::new(String::new()); 379 | s.extend_pairs(&[ 380 | ("quantity", "1"), 381 | ("price", "0.1"), 382 | ("recvWindow", "5000"), 383 | ("timestamp", "1499827319559"), 384 | ]); 385 | 386 | let (_, sig) = tr.signature( 387 | &Url::parse_with_params( 388 | "http://a.com/api/v1/order", 389 | &[ 390 | ("symbol", "LTCBTC"), 391 | ("side", "BUY"), 392 | ("type", "LIMIT"), 393 | ("timeInForce", "GTC"), 394 | ], 395 | )?, 396 | &s.finish(), 397 | )?; 398 | assert_eq!( 399 | sig, 400 | "0fd168b8ddb4876a0358a8d14d0c9f3da0e9b20c5d52b2a00fcf7d1c602f9a77" 401 | ); 402 | Ok(()) 403 | } 404 | 405 | #[test] 406 | fn signature_body2() -> Fallible<()> { 407 | let tr = Transport::with_credential( 408 | "vj1e6h50pFN9CsXT5nsL25JkTuBHkKw3zJhsA6OPtruIRalm20vTuXqF3htCZeWW", 409 | "5Cjj09rLKWNVe7fSalqgpilh5I3y6pPplhOukZChkusLqqi9mQyFk34kJJBTdlEJ", 410 | ); 411 | 412 | let q = &mut [ 413 | ("symbol", "ETHBTC"), 414 | ("side", "BUY"), 415 | ("type", "LIMIT"), 416 | ("timeInForce", "GTC"), 417 | ("quantity", "1"), 418 | ("price", "0.1"), 419 | ("recvWindow", "5000"), 420 | ("timestamp", "1540687064555"), 421 | ]; 422 | q.sort(); 423 | let q: Vec<_> = q.iter_mut().map(|(k, v)| format!("{}={}", k, v)).collect(); 424 | let q = q.join("&"); 425 | let (_, sig) = tr.signature(&Url::parse("http://a.com/api/v1/test")?, &q)?; 426 | assert_eq!( 427 | sig, 428 | "1ee5a75760b9496a2144a22116e02bc0b7fdcf828781fa87ca273540dfcf2cb0" 429 | ); 430 | Ok(()) 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /tests/ping.rs: -------------------------------------------------------------------------------- 1 | use binance_async as binance; 2 | 3 | use failure::Fallible; 4 | 5 | use crate::binance::Binance; 6 | 7 | #[tokio::test] 8 | async fn ping() -> Fallible<()> { 9 | tracing::subscriber::set_global_default(tracing_subscriber::FmtSubscriber::new()).unwrap(); 10 | 11 | let binance = Binance::new(); 12 | 13 | binance.ping()?.await?; 14 | 15 | Ok(()) 16 | } 17 | --------------------------------------------------------------------------------