Repository: hugues31/coinnect Branch: master Commit: 8da907c66d08 Files: 49 Total size: 307.9 KB Directory structure: gitextract_2v2o9zsm/ ├── .github/ │ └── workflows/ │ └── rust.yml ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── TODO.md ├── examples/ │ ├── generic_api.rs │ ├── kraken_trading.rs │ └── simple.rs ├── get_pairs_name.py ├── src/ │ ├── bitstamp/ │ │ ├── api.rs │ │ ├── credentials.rs │ │ ├── generic_api.rs │ │ ├── mod.rs │ │ └── utils.rs │ ├── bittrex/ │ │ ├── api.rs │ │ ├── credentials.rs │ │ ├── generic_api.rs │ │ ├── mod.rs │ │ └── utils.rs │ ├── coinnect.rs │ ├── error.rs │ ├── exchange.rs │ ├── gdax/ │ │ ├── api.rs │ │ ├── credentials.rs │ │ ├── generic_api.rs │ │ ├── mod.rs │ │ └── utils.rs │ ├── helpers/ │ │ └── mod.rs │ ├── kraken/ │ │ ├── api.rs │ │ ├── credentials.rs │ │ ├── generic_api.rs │ │ ├── mod.rs │ │ └── utils.rs │ ├── lib.rs │ ├── poloniex/ │ │ ├── api.rs │ │ ├── credentials.rs │ │ ├── generic_api.rs │ │ ├── mod.rs │ │ └── utils.rs │ └── types.rs └── tests/ ├── bitstamp.rs ├── bittrex.rs ├── coinnect.rs ├── exchange.rs ├── gdax.rs ├── kraken.rs └── poloniex.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/rust.yml ================================================ name: Rust on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Build run: cargo build --verbose - name: Run tests run: cargo test --verbose ================================================ FILE: .gitignore ================================================ target Cargo.lock keys_real.json .idea/ .vscode/ ================================================ FILE: .travis.yml ================================================ language: rust cache: cargo sudo: false rust: - 1.21.0 # oldest version supported - stable - nightly matrix: allow_failures: - rust: nightly before_script: - if [ ${TRAVIS_RUST_VERSION} = "nightly" ]; then cargo install rustfmt --force && cargo install clippy --force; fi script: - if [ ${TRAVIS_RUST_VERSION} = "nightly" ]; then cargo fmt -- --write-mode=diff && cargo clippy; fi - cargo build --verbose - cargo test --verbose ================================================ FILE: Cargo.toml ================================================ [package] name = "coinnect" version = "0.5.12" license = "MIT" authors = ["Hugues Gaillard ", "Alejandro Inestal "] description = """ A Rust library to connect to various crypto-currencies exchanges. """ documentation = "https://docs.rs/coinnect/" homepage = "https://github.com/hugues31/coinnect" repository = "https://github.com/hugues31/coinnect" keywords = [ "bitcoin", "trading", "poloniex", "kraken", "bitstamp" ] readme = "README.md" edition = "2018" [features] default = [] bitstamp_private_tests = [] kraken_private_tests = [] poloniex_private_tests = [] bittrex_private_tests = [] [[example]] name = "simple" path = "examples/simple.rs" [[example]] name = "kraken_trading" path = "examples/kraken_trading.rs" [[example]] name = "generic_api" path = "examples/generic_api.rs" [dependencies] hyper = "0.10.10" serde_json = "1.0.0" hyper-native-tls = "0.3" lazy_static = "1.4" bidir-map = "1.0.0" data-encoding = "2.0.0-rc.1" error-chain = "0.12" sha2 = "0.9.5" hmac = "0.11.0" bigdecimal = "0.2.1" chrono = "0.4.0" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2017 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![Coinnect](https://raw.githubusercontent.com/hugues31/coinnect/master/coinnect.png) =========== [![crates.io](https://img.shields.io/crates/v/coinnect.svg)](https://crates.io/crates/coinnect) [![Downloads from crates.io](https://img.shields.io/crates/d/coinnect.svg)](https://crates.io/crates/coinnect) [![Build Status](https://travis-ci.org/hugues31/coinnect.svg?branch=master)](https://travis-ci.org/hugues31/coinnect) [![doc.rs](https://docs.rs/coinnect/badge.svg)](https://docs.rs/coinnect/) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) Coinnect is a Rust library aiming to provide a complete access to REST APIs for various crypto-currencies exchanges (see below for a list of supported exchanges). All methods consume HTTPS api. The purpose of this crate is not to stream data (you should use websocket/FIX in that case). You basically have 2 choices to retrieve data : use the raw API provided by the platform you target or use the generic Coinnect API, which is more user-friendly and safe. Ideally, use the raw API when the Coinnect API could not retrieve the data/perform the action you want. **NOTE:** A new version with Futures support is coming as soon as async-await syntax will be stabilized ! **WARNING:** This library is highly experimental at the moment. Please do not invest what you can't afford to lose. This is a personal project, I cannot be held responsible for the library malfunction, which can lead to a loss of money. *The project is licensed under the terms of the MIT License.* ### Exchanges support: | Exchange | Raw API supported | Generic API supported | Note | |:--------:|:-----------------:|:---------------------:|:----:| | Bitstamp | X | X | Not every method is implemented for now.| | Kraken | X | X | - | | Poloniex | X | X | - | | Bittrex | X | X | - | If your favorite exchange is not listed above, you can vote [here](https://github.com/hugues31/coinnect/issues/54) to add it in the next release of Coinnect. Generic API supports: - Ticker - Orderbook - Balances - Add a new order - ... more to come! Feel free to make a PR to add support to your favorite exchange ;) ### Documentation - [Master](https://docs.rs/coinnect/) ## Usage Add this to your `Cargo.toml`: ```toml [dependencies] coinnect = "0.5" ``` and this to your crate root: ```rust extern crate coinnect; ``` For optional parameters, most methods require an empty `str` (`""`) or `Option` (`None`) if you don't want to specify them. Since 0.2, you have access to a generic API to communicate across exchanges in the same way. Note that this functionality is under active development. For more informations, look at ExchangeApi trait doc. ## Example The example below shows you how to connect to Poloniex ```rust extern crate coinnect; use coinnect::poloniex::api::PoloniexApi; use coinnect::poloniex::credentials::PoloniexCreds; fn main() { // We create a PoloniexApi by providing API key/secret // You can give an empty str if you only use public methods let creds = PoloniexCreds::new("my_optionnal_name", "api_key", "api_secret"); let mut my_api = PoloniexApi::new(creds).unwrap(); // Let's look at the ticker! let list_coins = my_api.return_ticker().unwrap(); for coin in list_coins { // please visit Poloniex API documentation to know how the data is returned // or look at the coinnect documentation let name = coin.0; let price = coin.1.as_object().unwrap().get("last").unwrap().as_str().unwrap(); println!("Coin {} has price : {}", name, price); } } ``` For more examples, please see [examples](examples/). ## Testing You can run the tests suite with `cargo test` for testing non private data requests (this will ignore tests related to private requests). You can use `cargo test --features "bitstamp_private_tests"` to run private tests related to bitstamp exchange for example. Before running private tests, make sure you have a `keys_real.json` file at the root with the following structure : ```json { "account_kraken": { "api_key" : "123456789ABCDEF", "api_secret": "ABC&EF?abcdef" }, "account_poloniex": { "api_key" : "XYXY-XYXY-XYXY-XY", "api_secret": "A0A0B1B1C2C2" }, "account_bitstamp": { "api_key" : "XYXY-XYXY-XYXY-XY", "api_secret" : "A0A0B1B1C2C2", "customer_id": "123456" } } ``` You must insert your real API keys, otherwise private tests may fail. No action is performed if you run the tests : no test will open position, or withdraw, etc. Tests only check for correct authentication method and correct parsing. You can examine the [tests](tests) folder just to be sure and look at the [Cargo.toml](Cargo.toml) file for a complete list of features. ## Contribution Your contribution is highly appreciated. Do not hesitate to open an issue or a pull request. Note that any contribution submitted for inclusion in the project will be licensed according to the terms given in [LICENSE](LICENSE). ## Disclaimer This SOFTWARE PRODUCT is provided by THE PROVIDER "as is" and "with all faults." THE PROVIDER makes no representations or warranties of any kind concerning the safety, suitability, lack of viruses, inaccuracies, typographical errors, or other harmful components of this SOFTWARE PRODUCT. There are inherent dangers in the use of any software, and you are solely responsible for determining whether this SOFTWARE PRODUCT is compatible with your equipment and other software installed on your equipment. You are also solely responsible for the protection of your equipment and backup of your data, and THE PROVIDER will not be liable for any damages you may suffer in connection with using, modifying, or distributing this SOFTWARE PRODUCT. ================================================ FILE: TODO.md ================================================ TODO ==== - [ ] Implement two-factor auth for supported exchanges - [ ] Add links to the documentation (Kraken use external links for example) - [ ] Remove .clone() for params in Kraken & Poloniex ================================================ FILE: examples/generic_api.rs ================================================ // This example shows how to use the generic API provided by Coinnect. // This method is useful if you have to iterate throught multiple accounts of // different exchanges and perform the same operation (such as get the current account's balance) // You can also use the Coinnect generic API if you want a better error handling since all methods // return Result<_, Error>. extern crate coinnect; use coinnect::coinnect::Coinnect; use coinnect::kraken::KrakenCreds; use coinnect::exchange::Exchange::*; use coinnect::types::Pair::*; fn main() { // We create a Coinnect Generic API // Since Kraken does not need customer_id field, we set it to None let my_creds = KrakenCreds::new("my_optionnal_name", "api_key", "api_secret"); let mut my_api = Coinnect::new(Kraken, my_creds).unwrap(); let ticker = my_api.ticker(ETC_BTC); println!("ETC_BTC last trade price is {}.", ticker.unwrap().last_trade_price); } ================================================ FILE: examples/kraken_trading.rs ================================================ // This example shows how to implement a simple trading strategy. // We are looking for the pair with the highest rise in price over the last 24 hours and we // add a buy order to buy it. // Please do NOT run this example with your real account unless you know what you're doing. extern crate coinnect; use std::path::PathBuf; use coinnect::kraken::{KrakenApi, KrakenCreds}; use std::error::Error; fn main() { // We create a KrakenApi by loading a json file containing API configuration // (see documentation for more info) let path = PathBuf::from("keys_real.json"); let my_creds = KrakenCreds::new_from_file("account_kraken", path).unwrap(); let mut my_api = KrakenApi::new(my_creds).unwrap(); // First, get the list of all pair we can trade with EUR€ as quote // You could use a simple unwrap() or use match to recover from an error for example let pairs_request = match my_api.get_tradable_asset_pairs("", "") { Ok(pairs_request) => pairs_request, Err(err) => panic!("Error : {:?}, description : {}", err, err.description()), }; let list_all_pairs = pairs_request.get("result").unwrap().as_object().unwrap(); let mut list_pairs_eur = Vec::new(); for pair in list_all_pairs { // The map structure is explained in documentation let quote = pair.1 .as_object() .unwrap() .get("quote") .unwrap() .as_str() .unwrap(); if quote == "ZEUR" { let name = pair.0; list_pairs_eur.push(name); } } println!("List {:?}", list_pairs_eur); // Now that we have the pairs, we choose the one with the highest price variation. // We query the ticker to get the opening and closing price over the last 24 hours for each // pairs in list_pairs_eur. KrakenApi has a blocking timer which prevents from ban if you // make rapid succession of requests (inside a for loop for example). Here, the ticker function // can take a list of pairs as parameter, so 1 request should suffice. // Convert Vec into comma separated values String let eur_pairs = format!("{:?}", list_pairs_eur); let eur_pairs = eur_pairs .replace("\"", "") .replace("[", "") .replace("]", "") .replace(" ", ""); // Get ticker let ticker_request = my_api.get_ticker_information(&eur_pairs).unwrap(); let list_ticker = ticker_request.get("result").unwrap().as_object().unwrap(); let mut pair_to_buy = ""; let mut pair_price_var = 0.0; let mut current_price = 0.0; for pair in list_ticker { let name = pair.0; // WARNING: Kraken uses quotes to encapsulate floating value let pair_info = pair.1.as_object().unwrap(); let open_price = pair_info .get("o") .unwrap() .as_str() .unwrap() .parse::() .unwrap(); let close_price_array = pair_info.get("c").unwrap().as_array().unwrap(); let close_price = close_price_array[0] .as_str() .unwrap() .parse::() .unwrap(); let price_var = (close_price / open_price - 1.0) * 100.0; if price_var > pair_price_var { pair_price_var = price_var; pair_to_buy = name; current_price = close_price; } } println!("{} has the highest price variation ({:.2}%).", pair_to_buy, pair_price_var); // Add a buy limit order for an amount of 100€ for a price of: current_price - 2% let buying_price = current_price - (current_price * 2.0 / 100.0); let volume = 100.0 / buying_price; // Specify optional parameters with an empty str ("") // See documentation for more informations let _ = my_api.add_standard_order(pair_to_buy, // name of the pair "buy", // type : buy/sell "limit", // order type : market/limit/... &buying_price.to_string(), // price 1 "", // price 2 &volume.to_string(), // volume "", // leverage "", // oflags (see doc) "", // starttm "", // expiretm "", // userref ""); // validate // In a real case example, you should check if any error occurs. } ================================================ FILE: examples/simple.rs ================================================ // This example shows how to connect to your Poloniex account and perform simple operations extern crate coinnect; use coinnect::poloniex::{PoloniexApi, PoloniexCreds}; fn main() { // We create a PoloniexApi by providing API key/secret // You can give an empty String if you only use public methods let creds = PoloniexCreds::new("my_optionnal_name", "api_key", "api_secret"); let mut my_api = PoloniexApi::new(creds).unwrap(); // Let's look at the ticker! let list_coins = my_api.return_ticker().unwrap(); for coin in list_coins { // please visit Poloniex API documentation to know how the data is returned // or look at the coinnect documentation let name = coin.0; let price = coin.1 .as_object() .unwrap() .get("last") .unwrap() .as_str() .unwrap(); println!("Coin {} has price : {}", name, price); } } ================================================ FILE: get_pairs_name.py ================================================ # This python3 script displays all pairs that can be used # on Kraken, Poloniex and Bitstamp platform. The pairs can then be copied/pasted # into Coinnect. This script does the conversion for Poloniex (see Pair doc). import json import ssl import urllib.request # ugly fix the ssl certificate bug ssl._create_default_https_context = ssl._create_unverified_context # ╔╗ ╦ ╔╦╗ ╔═╗ ╔╦╗ ╔═╗ ╔╦╗ ╔═╗ # ╠╩╗ ║ ║ ╚═╗ ║ ╠═╣ ║║║ ╠═╝ # ╚═╝ ╩ ╩ ╚═╝ ╩ ╩ ╩ ╩ ╩ ╩ raw_bitstamp_pairs = ["btcusd", "btceur", "eurusd", "xrpusd", "xrpeur", "xrpbtc"] standardized_bitstamp_pairs = ["BTC_USD", "BTC_EUR", "EUR_USD", "XRP_USD", "XRP_EUR", "XRP_BTC"] # ╦╔═ ╦═╗ ╔═╗ ╦╔═ ╔═╗ ╔╗╔ # ╠╩╗ ╠╦╝ ╠═╣ ╠╩╗ ║╣ ║║║ # ╩ ╩ ╩╚═ ╩ ╩ ╩ ╩ ╚═╝ ╝╚╝ url = "https://api.kraken.com/0/public/AssetPairs" raw_kraken_pairs = list() standardized_kraken_pairs = list() with urllib.request.urlopen(url) as response: html = response.read().decode("utf-8") json_data = json.loads(html) for currency in json_data["result"]: raw_kraken_pairs.append(currency) quote = json_data["result"][currency]["quote"][1:] # remove the X or Z base = json_data["result"][currency]["base"] old_naming = ("XETH", "XXBT", "XETC", "XLTC", "XICN", "XREP", "XXDG", "XZEC", "XXLM", "XXMR", "XMLN", "XXRP") if base in old_naming: base = base[1:] # remove the X if base == "XBT": base = "BTC" if quote == "XBT": quote = "BTC" if json_data["result"][currency]["altname"][-2:] == ".d": quote += "_d" standardized_kraken_pairs.append(base + "_" + quote) # ╔═╗ ╔═╗ ╦ ╔═╗ ╔╗╔ ╦ ╔═╗ ═╗ ╦ # ╠═╝ ║ ║ ║ ║ ║ ║║║ ║ ║╣ ╔╩╦╝ # ╩ ╚═╝ ╩═╝ ╚═╝ ╝╚╝ ╩ ╚═╝ ╩ ╚═ url = "https://poloniex.com/public?command=returnTicker" raw_poloniex_pairs = list() with urllib.request.urlopen(url) as response: html = response.read().decode("utf-8") json_data = json.loads(html) for currency in json_data: raw_poloniex_pairs.append(currency) # conversion standardized_poloniex_pairs = list() for pair in raw_poloniex_pairs: base, quote = pair.split('_', 1) standardized_poloniex_pairs.append(quote + "_" + base) # ╔╗ ╦ ╔╦╗ ╔╦╗ ╦═╗ ╔═╗ ═╗ ╦ # ╠╩╗ ║ ║ ║ ╠╦╝ ║╣ ╔╩╦╝ # ╚═╝ ╩ ╩ ╩ ╩╚═ ╚═╝ ╩ ╚═ url = "https://bittrex.com/api/v1.1/public/getmarketsummaries" raw_bittrex_pairs = list() with urllib.request.urlopen(url) as response: html = response.read().decode("utf-8") json_data = json.loads(html) for currency in json_data["result"]: raw_bittrex_pairs.append(currency["MarketName"]) # conversion standardized_bittrex_pairs = list() for pair in raw_bittrex_pairs: base, quote = pair.split('-', 1) standardized_bittrex_pairs.append(quote + "_" + base) # Generate all possible pairs exchanges = [standardized_bitstamp_pairs, standardized_kraken_pairs, standardized_poloniex_pairs, standardized_bittrex_pairs] pairs = list() for exchange in exchanges: for pair in exchange: if pair not in pairs: pairs.append(pair) pairs = sorted(pairs) print("SUPPORTED PAIRS") print("===============") for pair in pairs: print(pair + ",") print("\n\n\n") print("BITSTAMP PAIRS") print("==============") for std, raw in zip(standardized_bitstamp_pairs, raw_bitstamp_pairs): print("m.insert({std}, \"{raw}\");".format(std=std, raw=raw)) print("\n\n\n") print("KRAKEN PAIRS") print("============") for std, raw in zip(standardized_kraken_pairs, raw_kraken_pairs): print("m.insert({std}, \"{raw}\");".format(std=std, raw=raw)) print("\n\n\n") print("POLONIEX PAIRS") print("==============") for std, raw in zip(standardized_poloniex_pairs, raw_poloniex_pairs): print("m.insert({std}, \"{raw}\");".format(std=std, raw=raw)) print("\n\n\n") print("BITTREX PAIRS") print("==============") for std, raw in zip(standardized_bittrex_pairs, raw_bittrex_pairs): print("m.insert({std}, \"{raw}\");".format(std=std, raw=raw)) # CURRENCIES # BITTREX url = "https://bittrex.com/api/v1.1/public/getcurrencies" bittrex_currencies = list() with urllib.request.urlopen(url) as response: html = response.read().decode("utf-8") json_data = json.loads(html) for currency in json_data["result"]: print(currency["Currency"] + ",") bittrex_currencies.append(currency["Currency"]) # Currency enum -> Option for currency in bittrex_currencies: print("Currency::" + currency + " => Some(\"" + currency + "\".to_string()),") # Currency str -> Option for currency in bittrex_currencies: print("\"" + currency + "\" => Some(Currency::" + currency + "),") ================================================ FILE: src/bitstamp/api.rs ================================================ //! Use this module to interact with Bitstamp exchange. //! Please see examples for more informations. use hyper_native_tls::NativeTlsClient; use hyper::Client; use hyper::header::ContentType; use hyper::net::HttpsConnector; use serde_json::Value; use serde_json::value::Map; use std::collections::HashMap; use std::io::Read; use std::thread; use std::time::Duration; use crate::coinnect::Credentials; use crate::exchange::Exchange; use crate::error::*; use crate::helpers; use crate::types::Pair; use crate::bitstamp::utils; use crate::types::*; header! { #[doc(hidden)] (KeyHeader, "Key") => [String] } header! { #[doc(hidden)] (SignHeader, "Sign") => [String] } header! { #[doc(hidden)] (ContentHeader, "Content-Type") => [String] } #[derive(Debug)] pub struct BitstampApi { last_request: i64, // unix timestamp in ms, to avoid ban api_key: String, api_secret: String, customer_id: String, http_client: Client, burst: bool, } impl BitstampApi { /// Create a new BitstampApi by providing an API key & API secret pub fn new(creds: C) -> Result { if creds.exchange() != Exchange::Bitstamp { return Err(ErrorKind::InvalidConfigType(Exchange::Bitstamp, creds.exchange()).into()); } //TODO: Handle correctly TLS errors with error_chain. let ssl = match NativeTlsClient::new() { Ok(res) => res, Err(_) => return Err(ErrorKind::TlsError.into()), }; let connector = HttpsConnector::new(ssl); Ok(BitstampApi { last_request: 0, api_key: creds.get("api_key").unwrap_or_default(), api_secret: creds.get("api_secret").unwrap_or_default(), customer_id: creds.get("customer_id").unwrap_or_default(), http_client: Client::with_connector(connector), burst: false, // No burst by default }) } /// The number of calls in a given period is limited. In order to avoid a ban we limit /// by default the number of api requests. /// This function sets or removes the limitation. /// Burst false implies no block. /// Burst true implies there is a control over the number of calls allowed to the exchange pub fn set_burst(&mut self, burst: bool) { self.burst = burst } fn block_or_continue(&self) { if ! self.burst { let threshold: u64 = 1000; // 600 requests per 10 mins = 1 request per second let offset: u64 = helpers::get_unix_timestamp_ms() as u64 - self.last_request as u64; if offset < threshold { let wait_ms = Duration::from_millis(threshold - offset); thread::sleep(wait_ms); } } } fn public_query(&mut self, params: &HashMap<&str, &str>) -> Result> { let method: &str = params .get("method") .ok_or_else(|| "Missing \"method\" field.")?; let pair: &str = params.get("pair").ok_or_else(|| "Missing \"pair\" field.")?; let url: String = utils::build_url(method, pair); self.block_or_continue(); let mut response = self.http_client.get(&url).send()?; self.last_request = helpers::get_unix_timestamp_ms(); let mut buffer = String::new(); response.read_to_string(&mut buffer)?; utils::deserialize_json(&buffer) } /// /// /// #Examples /// /// ```json /// extern crate coinnect; /// use coinnect::bitstamp::BitstampApi; /// let mut api = BitstampApi::new("", ""); /// let result = api.private_query("balance", "btcusd"); /// assert_eq!(true, true); /// ``` fn private_query(&mut self, params: &HashMap<&str, &str>) -> Result> { let method: &str = params .get("method") .ok_or_else(|| "Missing \"method\" field.")?; let pair: &str = params.get("pair").ok_or_else(|| "Missing \"pair\" field.")?; let url: String = utils::build_url(method, pair); let nonce = utils::generate_nonce(None); let signature = utils::build_signature(&nonce, &self.customer_id, &self.api_key, &self.api_secret)?; let copy_api_key = self.api_key.clone(); let mut post_params: &mut HashMap<&str, &str> = &mut HashMap::new(); post_params.insert("key", ©_api_key); post_params.insert("signature", &signature); post_params.insert("nonce", &nonce); // copy params into post_params .... bit of a hack but will do for now params.iter().for_each(|(k,v)| { post_params.insert(k,v); }); helpers::strip_empties(&mut post_params); let post_data = helpers::url_encode_hashmap(post_params); let mut response = self.http_client .post(&url) .header(ContentType::form_url_encoded()) .body(&post_data) .send()?; let mut buffer = String::new(); response.read_to_string(&mut buffer)?; utils::deserialize_json(&buffer) } /// Sample output : /// /// ```json /// { /// "BTC_LTC":{ /// "last":"0.0251","lowestAsk":"0.02589999","highestBid":"0.0251", /// "percentChange":"0.02390438","baseVolume":"6.16485315","quoteVolume":"245.82513926"}, /// "BTC_NXT":{ /// "last":"0.00005730","lowestAsk":"0.00005710","highestBid":"0.00004903", /// "percentChange":"0.16701570","baseVolume":"0.45347489","quoteVolume":"9094"}, /// ... } /// ``` pub fn return_ticker(&mut self, pair: Pair) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let mut params: HashMap<&str, &str> = HashMap::new(); params.insert("pair", pair_name); params.insert("method", "ticker"); self.public_query(¶ms) } /// Sample output : /// /// ```json /// {"asks":[[0.00007600,1164],[0.00007620,1300], ... ], "bids":[[0.00006901,200], /// [0.00006900,408], ... ], "timestamp": "1234567890"} /// ``` pub fn return_order_book(&mut self, pair: Pair) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let mut params: HashMap<&str, &str> = HashMap::new(); params.insert("method", "order_book"); params.insert("pair", pair_name); self.public_query(¶ms) } /// Sample output : /// /// ```json /// [{"date":"2014-02-10 04:23:23","type":"buy","rate":"0.00007600","amount":"140", /// "total":"0.01064"}, /// {"date":"2014-02-10 01:19:37","type":"buy","rate":"0.00007600","amount":"655", /// "total":"0.04978"}, ... ] /// ``` pub fn return_trade_history(&mut self, pair: Pair) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let mut params: HashMap<&str, &str> = HashMap::new(); params.insert("pair", pair_name); params.insert("method", "transactions"); self.public_query(¶ms) } /// Returns all of your available balances. /// /// Sample output: /// /// ```json /// {"BTC":"0.59098578","LTC":"3.31117268", ... } /// ``` pub fn return_balances(&mut self) -> Result> { let mut params = HashMap::new(); params.insert("method", "balance"); params.insert("pair", ""); self.private_query(¶ms) } /// Add a buy limit order to the exchange /// limit_price : If the order gets executed, a new sell order will be placed, /// with "limit_price" as its price. /// daily_order (Optional) : Opens buy limit order which will be canceled /// at 0:00 UTC unless it already has been executed. Possible value: True pub fn buy_limit(&mut self, pair: Pair, amount: Volume, price: Price, price_limit: Option, daily_order: Option) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let amount_string = amount.to_string(); let price_string = price.to_string(); let price_limit_string = match price_limit { Some(limit) => limit.to_string(), None => "".to_string(), }; let mut params = HashMap::new(); params.insert("method", "buy"); params.insert("pair", pair_name); params.insert("amount", &amount_string); params.insert("price", &price_string); params.insert("limit_price", &price_limit_string); if let Some(order) = daily_order { let daily_order_str = if order { "True" } else { "" }; // False is not a possible value params.insert("daily_order", daily_order_str); } self.private_query(¶ms) } /// Add a sell limit order to the exchange /// limit_price : If the order gets executed, a new sell order will be placed, /// with "limit_price" as its price. /// daily_order (Optional) : Opens sell limit order which will be canceled /// at 0:00 UTC unless it already has been executed. Possible value: True pub fn sell_limit(&mut self, pair: Pair, amount: Volume, price: Price, price_limit: Option, daily_order: Option) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let amount_string = amount.to_string(); let price_string = price.to_string(); let price_limit_string = match price_limit { Some(limit) => limit.to_string(), None => "".to_string(), }; let mut params = HashMap::new(); params.insert("method", "sell"); params.insert("pair", pair_name); params.insert("amount", &amount_string); params.insert("price", &price_string); params.insert("limit_price", &price_limit_string); if let Some(order) = daily_order { let daily_order_str = if order { "True" } else { "" }; // False is not a possible value params.insert("daily_order", daily_order_str); } self.private_query(¶ms) } /// Add a market buy order to the exchange /// By placing a market order you acknowledge that the execution of your order depends /// on the market conditions and that these conditions may be subject to sudden changes /// that cannot be foreseen. pub fn buy_market(&mut self, pair: Pair, amount: Volume) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let amount_string = amount.to_string(); let mut params = HashMap::new(); params.insert("method", "buy/market"); params.insert("pair", pair_name); params.insert("amount", &amount_string); self.private_query(¶ms) } /// Add a market sell order to the exchange /// By placing a market order you acknowledge that the execution of your order depends /// on the market conditions and that these conditions may be subject to sudden changes /// that cannot be foreseen. pub fn sell_market(&mut self, pair: Pair, amount: Volume) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let amount_string = amount.to_string(); let mut params = HashMap::new(); params.insert("method", "sell/market"); params.insert("pair", pair_name); params.insert("amount", &amount_string); self.private_query(¶ms) } } #[cfg(test)] mod bitstamp_api_tests { use super::*; #[test] fn should_block_or_not_block_when_enabled_or_disabled() { let mut api = BitstampApi { last_request: helpers::get_unix_timestamp_ms(), api_key: "".to_string(), api_secret: "".to_string(), customer_id: "".to_string(), http_client: Client::new(), burst: false, }; let mut counter = 0; loop { api.set_burst(false); let start = helpers::get_unix_timestamp_ms(); api.block_or_continue(); api.last_request = helpers::get_unix_timestamp_ms(); let difference = api.last_request - start; assert!(difference >= 999); assert!(difference < 10000); api.set_burst(true); let start = helpers::get_unix_timestamp_ms(); api.block_or_continue(); api.last_request = helpers::get_unix_timestamp_ms(); let difference = api.last_request - start; assert!(difference < 10); counter = counter + 1; if counter >= 3 { break; } } } } ================================================ FILE: src/bitstamp/credentials.rs ================================================ //! Contains the Bitstamp credentials. use serde_json; use serde_json::Value; use crate::coinnect::Credentials; use crate::exchange::Exchange; use crate::helpers; use crate::error::*; use std::collections::HashMap; use std::str::FromStr; use std::fs::File; use std::io::Read; use std::path::PathBuf; #[derive(Debug, Clone)] pub struct BitstampCreds { exchange: Exchange, name: String, data: HashMap, } impl BitstampCreds { /// Create a new `BitstampCreds` from arguments. pub fn new(name: &str, api_key: &str, api_secret: &str, customer_id: &str) -> Self { let mut creds = BitstampCreds { data: HashMap::new(), exchange: Exchange::Bitstamp, name: if name.is_empty() { "BitstampClient".to_string() } else { name.to_string() }, }; //if api_key.is_empty() { //warning!("No API key set for the Bitstamp client"); //} creds .data .insert("api_key".to_string(), api_key.to_string()); //if api_secret.is_empty() { //warning!("No API secret set for the Bitstamp client"); //} creds .data .insert("api_secret".to_string(), api_secret.to_string()); //if api_secret.is_empty() { //warning!("No API customer ID set for the Bitstamp client"); //} creds .data .insert("customer_id".to_string(), customer_id.to_string()); creds } /// Create a new `BitstampCreds` from a json configuration file. This file must follow this /// structure: /// /// ```json /// { /// "account_kraken": { /// "exchange" : "kraken", /// "api_key" : "123456789ABCDEF", /// "api_secret": "ABC&EF?abcdef" /// }, /// "account_bitstamp": { /// "exchange" : "bitstamp", /// "api_key" : "1234567890ABCDEF1234567890ABCDEF", /// "api_secret" : "1234567890ABCDEF1234567890ABCDEF", /// "customer_id": "123456" /// } /// } /// ``` /// For this example, you could use load your Bitstamp account with /// `BitstampAPI::new(BitstampCreds::new_from_file("account_bitstamp", Path::new("/keys.json")))` pub fn new_from_file(name: &str, path: PathBuf) -> Result { let mut f = File::open(&path)?; let mut buffer = String::new(); f.read_to_string(&mut buffer)?; let data: Value = serde_json::from_str(&buffer)?; let json_obj = data.as_object() .ok_or_else(|| ErrorKind::BadParse)? .get(name) .ok_or_else(|| ErrorKind::MissingField(name.to_string()))?; let api_key = helpers::get_json_string(json_obj, "api_key")?; let api_secret = helpers::get_json_string(json_obj, "api_secret")?; let customer_id = helpers::get_json_string(json_obj, "customer_id")?; let exchange = { let exchange_str = helpers::get_json_string(json_obj, "exchange")?; Exchange::from_str(exchange_str) .chain_err(|| ErrorKind::InvalidFieldValue("exchange".to_string()))? }; if exchange != Exchange::Bitstamp { return Err(ErrorKind::InvalidConfigType(Exchange::Bitstamp, exchange).into()); } Ok(BitstampCreds::new(name, api_key, api_secret, customer_id)) } } impl Credentials for BitstampCreds { /// Return a value from the credentials. fn get(&self, key: &str) -> Option { if let Some(res) = self.data.get(key) { Some(res.clone()) } else { None } } fn name(&self) -> String { self.name.clone() } fn exchange(&self) -> Exchange { self.exchange } } ================================================ FILE: src/bitstamp/generic_api.rs ================================================ //! Use this module to interact with Bitstamp through a Generic API. //! This a more convenient and safe way to deal with the exchange since methods return a Result<> //! but this generic API does not provide all the functionnality that Bitstamp offers. use crate::exchange::ExchangeApi; use crate::bitstamp::api::BitstampApi; use crate::bitstamp::utils; use crate::error::*; use crate::types::*; use crate::helpers; impl ExchangeApi for BitstampApi { fn ticker(&mut self, pair: Pair) -> Result { let result = self.return_ticker(pair)?; let price = helpers::from_json_bigdecimal(&result["last"], "last")?; let ask = helpers::from_json_bigdecimal(&result["ask"], "ask")?; let bid = helpers::from_json_bigdecimal(&result["bid"], "bid")?; let vol = helpers::from_json_bigdecimal(&result["volume"], "volume")?; Ok(Ticker { timestamp: helpers::get_unix_timestamp_ms(), pair: pair, last_trade_price: price, lowest_ask: ask, highest_bid: bid, volume: Some(vol), }) } fn orderbook(&mut self, pair: Pair) -> Result { let raw_response = self.return_order_book(pair)?; let result = utils::parse_result(&raw_response)?; let mut ask_offers = Vec::new(); let mut bid_offers = Vec::new(); let ask_array = result["asks"] .as_array() .ok_or_else(|| ErrorKind::InvalidFieldFormat(format!("{}", result["asks"])))?; let bid_array = result["bids"] .as_array() .ok_or_else(|| ErrorKind::InvalidFieldFormat(format!("{}", result["asks"])))?; for ask in ask_array { let price = helpers::from_json_bigdecimal(&ask[0], "ask price")?; let volume = helpers::from_json_bigdecimal(&ask[1], "ask volume")?; ask_offers.push((price, volume)); } for bid in bid_array { let price = helpers::from_json_bigdecimal(&bid[0], "bid price")?; let volume = helpers::from_json_bigdecimal(&bid[1], "bid volume")?; bid_offers.push((price, volume)); } Ok(Orderbook { timestamp: helpers::get_unix_timestamp_ms(), pair: pair, asks: ask_offers, bids: bid_offers, }) } fn add_order(&mut self, order_type: OrderType, pair: Pair, quantity: Volume, price: Option) -> Result { //let pair_name = match utils::get_pair_string(&pair) { //Some(name) => name, //None => return Err(ErrorKind::PairUnsupported.into()), //}; let result = match order_type { OrderType::BuyLimit => { if price.is_none() { return Err(ErrorKind::MissingPrice.into()); } // Unwrap safe here with the check above. self.buy_limit(pair, quantity, price.unwrap(), None, None) } OrderType::BuyMarket => self.buy_market(pair, quantity), OrderType::SellLimit => { if price.is_none() { return Err(ErrorKind::MissingPrice.into()); } // Unwrap safe here with the check above. self.sell_limit(pair, quantity, price.unwrap(), None, None) } OrderType::SellMarket => self.sell_market(pair, quantity), }; Ok(OrderInfo { timestamp: helpers::get_unix_timestamp_ms(), identifier: vec![result?["id"] .as_str() .ok_or_else(|| { ErrorKind::MissingField("id".to_string()) })? .to_string()], }) } /// Return the balances for each currency on the account fn balances(&mut self) -> Result { let raw_response = self.return_balances()?; let result = utils::parse_result(&raw_response)?; let mut balances = Balances::new(); for (key, val) in result.iter() { let currency = utils::get_currency_enum(key); match currency { Some(c) => { let amount = helpers::from_json_bigdecimal(&val, "amount")?; balances.insert(c, amount); }, _ => () } } Ok(balances) } } ================================================ FILE: src/bitstamp/mod.rs ================================================ //! Use this module to interact with Bitstamp exchange. pub mod api; pub mod generic_api; pub mod credentials; pub mod utils; pub use self::credentials::BitstampCreds; pub use self::api::BitstampApi; ================================================ FILE: src/bitstamp/utils.rs ================================================ use bidir_map::BidirMap; use hmac::{Hmac, Mac, NewMac}; use sha2::{Sha256}; use serde_json; use serde_json::Value; use serde_json::value::Map; use crate::error::*; use crate::helpers; use crate::types::Currency; use crate::types::Pair; use crate::types::Pair::*; lazy_static! { static ref PAIRS_STRING: BidirMap = { let mut m = BidirMap::new(); m.insert(BTC_USD, "btcusd"); m.insert(BTC_EUR, "btceur"); m.insert(EUR_USD, "eurusd"); m.insert(XRP_USD, "xrpusd"); m.insert(XRP_EUR, "xrpeur"); m.insert(XRP_BTC, "xrpbtc"); m.insert(LTC_USD, "ltcusd"); m.insert(LTC_EUR, "ltceur"); m.insert(LTC_BTC, "ltcbtc"); m.insert(ETH_USD, "ethusd"); m.insert(ETH_EUR, "etheur"); m.insert(ETH_BTC, "ethbtc"); m.insert(BCH_USD, "bchusd"); m.insert(BCH_EUR, "bcheur"); m.insert(BCH_BTC, "bchbtc"); m }; } /// Return the name associated to pair used by Bitstamp /// If the Pair is not supported, None is returned. pub fn get_pair_string(pair: &Pair) -> Option<&&str> { PAIRS_STRING.get_by_first(pair) } /// Return the Pair enum associated to the string used by Bitstamp /// If the Pair is not supported, None is returned. pub fn get_pair_enum(pair: &str) -> Option<&Pair> { PAIRS_STRING.get_by_second(&pair) } pub fn build_signature(nonce: &str, customer_id: &str, api_key: &str, api_secret: &str) -> Result { const C: &'static [u8] = b"0123456789ABCDEF"; let message = nonce.to_owned() + customer_id + api_key; let mut mac = Hmac::::new_from_slice(api_secret.as_bytes()).unwrap(); mac.update(message.as_bytes()); let result = mac.finalize(); let raw_signature = result.into_bytes(); let mut signature = Vec::with_capacity(raw_signature.len() * 2); for &byte in &raw_signature { signature.push(C[(byte >> 4) as usize]); signature.push(C[(byte & 0xf) as usize]); } // TODO: Handle correctly the from_utf8 errors with error_chain. Ok(String::from_utf8(signature)?) } pub fn build_url(method: &str, pair: &str) -> String { "https://www.bitstamp.net/api/v2/".to_string() + method + "/" + pair + "/" } pub fn deserialize_json(json_string: &str) -> Result> { let data: Value = match serde_json::from_str(json_string) { Ok(data) => data, Err(_) => return Err(ErrorKind::BadParse.into()), }; match data.as_object() { Some(value) => Ok(value.clone()), None => Err(ErrorKind::BadParse.into()), } } pub fn generate_nonce(fixed_nonce: Option) -> String { match fixed_nonce { Some(v) => v, None => helpers::get_unix_timestamp_ms().to_string(), } } /// If error array is null, return the result (encoded in a json object) /// else return the error string found in array pub fn parse_result(response: &Map) -> Result> { let error_msg = match response.get("error") { Some(error) => { error .as_str() .ok_or_else(|| ErrorKind::InvalidFieldFormat("error".to_string()))? } None => return Ok(response.clone()), }; match error_msg.as_ref() { "Invalid command." => Err(ErrorKind::InvalidArguments.into()), "Invalid API key/secret pair." => Err(ErrorKind::BadCredentials.into()), "Total must be at least 0.0001." => Err(ErrorKind::InsufficientOrderSize.into()), other => Err(ErrorKind::ExchangeSpecificError(other.to_string()).into()), } } /// Return the currency enum associated with the /// string used by Bitstamp. If no currency is found, /// return None /// # Examples /// /// ``` /// use coinnect::bitstamp::utils::get_currency_enum; /// use coinnect::types::Currency; /// /// let currency = get_currency_enum("usd_balance"); /// assert_eq!(Some(Currency::USD), currency); /// ``` pub fn get_currency_enum(currency: &str) -> Option { match currency { "usd_balance" => Some(Currency::USD), "btc_balance" => Some(Currency::BTC), "eur_balance" => Some(Currency::EUR), "xrp_balance" => Some(Currency::XRP), _ => None, } } /// Return the currency string associated with the /// enum used by Bitstamp. If no currency is found, /// return None /// # Examples /// /// ``` /// use coinnect::bitstamp::utils::get_currency_string; /// use coinnect::types::Currency; /// /// let currency = get_currency_string(Currency::USD); /// assert_eq!(currency, Some("USD".to_string())); /// ``` pub fn get_currency_string(currency: Currency) -> Option { match currency { Currency::USD => Some("USD".to_string()), Currency::BTC => Some("BTC".to_string()), Currency::EUR => Some("EUR".to_string()), Currency::XRP => Some("XRP".to_string()), _ => None, } } ================================================ FILE: src/bittrex/api.rs ================================================ //! Use this module to interact with the raw-original API provided by Bittrex. //! WARNING: Special attention should be paid to error management: parsing number, etc. use hmac::{Hmac, Mac, NewMac}; use sha2::{Sha512}; use hyper_native_tls::NativeTlsClient; use hyper::Client; use hyper::header; use hyper::net::HttpsConnector; use data_encoding::HEXLOWER; use serde_json::Value; use serde_json::value::Map; use std::collections::HashMap; use std::io::Read; use std::thread; use std::time::Duration; use std::str; use crate::error::*; use crate::helpers; use crate::exchange::Exchange; use crate::coinnect::Credentials; use crate::bittrex::utils; header! { #[doc(hidden)] (ApiSign, "apisign") => [String] } #[derive(Debug)] pub struct BittrexApi { last_request: i64, // unix timestamp in ms, to avoid ban api_key: String, api_secret: String, http_client: Client, burst: bool, } impl BittrexApi { /// Create a new BittrexApi by providing an API key & API secret pub fn new(creds: C) -> Result { if creds.exchange() != Exchange::Bittrex { return Err(ErrorKind::InvalidConfigType(Exchange::Bittrex, creds.exchange()).into()); } // TODO: implement correctly the TLS error in error_chain. let ssl = match NativeTlsClient::new() { Ok(res) => res, Err(_) => return Err(ErrorKind::TlsError.into()), }; let connector = HttpsConnector::new(ssl); Ok(BittrexApi { last_request: 0, api_key: creds.get("api_key").unwrap_or_default(), api_secret: creds.get("api_secret").unwrap_or_default(), http_client: Client::with_connector(connector), burst: false, }) } /// The number of calls in a given period is limited. In order to avoid a ban we limit /// by default the number of api requests. /// This function sets or removes the limitation. /// Burst false implies no block. /// Burst true implies there is a control over the number of calls allowed to the exchange pub fn set_burst(&mut self, burst: bool) { self.burst = burst } pub fn block_or_continue(&self) { if ! self.burst { let threshold: u64 = 500; // 1 request/500ms let offset: u64 = helpers::get_unix_timestamp_ms() as u64 - self.last_request as u64; if offset < threshold { let wait_ms = Duration::from_millis(threshold - offset); thread::sleep(wait_ms); } } } fn public_query(&mut self, method: &str, params: &mut HashMap<&str, &str>) -> Result> { helpers::strip_empties(params); let url = "https://bittrex.com/api/v1.1".to_string() + method + "?" + &helpers::url_encode_hashmap(params); self.block_or_continue(); //TODO: Handle correctly http errors with error_chain. let mut response = match self.http_client.get(&url).send() { Ok(response) => response, Err(err) => return Err(ErrorKind::ServiceUnavailable(err.to_string()).into()), }; self.last_request = helpers::get_unix_timestamp_ms(); let mut buffer = String::new(); response.read_to_string(&mut buffer)?; utils::deserialize_json(&buffer) } fn private_query(&mut self, method: &str, mut params: &mut HashMap<&str, &str>) -> Result> { let nonce = helpers::get_unix_timestamp_ms().to_string(); let mut initial_params: HashMap<&str, &str> = HashMap::new(); initial_params.insert("nonce", &nonce); initial_params.insert("apikey", &self.api_key); let base_url = "https://bittrex.com/api/v1.1".to_string() + method + "?apikey=" + &self.api_key + "&nonce=" + &nonce; let url = if params.is_empty() { base_url } else { base_url + "&" + &helpers::url_encode_hashmap(&mut params) }; let hmac_key = self.api_secret.as_bytes(); let mut mac = Hmac::::new_from_slice(&hmac_key[..]).unwrap(); mac.update(url.as_bytes()); let mut custom_header = header::Headers::new(); let signature = HEXLOWER.encode(&mac.finalize().into_bytes()); custom_header.set(ApiSign(signature)); let mut res = match self.http_client .post(&url) .headers(custom_header) .send() { Ok(res) => res, Err(err) => return Err(ErrorKind::ServiceUnavailable(err.to_string()).into()), }; let mut buffer = String::new(); res.read_to_string(&mut buffer)?; utils::deserialize_json(&buffer) } /// Used to get the open and available trading markets at Bittrex along with other meta data. /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : [{ /// "MarketCurrency" : "LTC", /// "BaseCurrency" : "BTC", /// "MarketCurrencyLong" : "Litecoin", /// "BaseCurrencyLong" : "Bitcoin", /// "MinTradeSize" : 0.01000000, /// "MarketName" : "BTC-LTC", /// "IsActive" : true, /// "Created" : "2014-02-13T00:00:00" /// }, { /// "MarketCurrency" : "DOGE", /// "BaseCurrency" : "BTC", /// "MarketCurrencyLong" : "Dogecoin", /// "BaseCurrencyLong" : "Bitcoin", /// "MinTradeSize" : 100.00000000, /// "MarketName" : "BTC-DOGE", /// "IsActive" : true, /// "Created" : "2014-02-13T00:00:00" /// } /// ] /// } /// ``` pub fn get_markets(&mut self) -> Result> { let mut params = HashMap::new(); self.public_query("/public/getmarkets", &mut params) } /// Used to get all supported currencies at Bittrex along with other meta data. /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : [{ /// "Currency" : "BTC", /// "CurrencyLong" : "Bitcoin", /// "MinConfirmation" : 2, /// "TxFee" : 0.00020000, /// "IsActive" : true, /// "CoinType" : "BITCOIN", /// "BaseAddress" : null /// }, { /// "Currency" : "LTC", /// "CurrencyLong" : "Litecoin", /// "MinConfirmation" : 5, /// "TxFee" : 0.00200000, /// "IsActive" : true, /// "CoinType" : "BITCOIN", /// "BaseAddress" : null /// } /// ] /// } /// ``` pub fn get_currencies(&mut self) -> Result> { let mut params = HashMap::new(); self.public_query("/public/getcurrencies", &mut params) } /// Used to get the current tick values for a market. /// "market" required a string literal for the market (ex: BTC-LTC) /// /// ````json /// { /// "success" : true, /// "message" : "", /// "result" : { /// "Bid" : 2.05670368, /// "Ask" : 3.35579531, /// "Last" : 3.35579531 /// } /// } /// ``` pub fn get_ticker(&mut self, market: &str) -> Result> { let mut params = HashMap::new(); params.insert("market", market); self.public_query("/public/getticker", &mut params) } /// Used to get the last 24 hour summary of all active exchanges /// /// ````json /// { /// "success" : true, /// "message" : "", /// "result" : [{ /// "MarketName" : "BTC-888", /// "High" : 0.00000919, /// "Low" : 0.00000820, /// "Volume" : 74339.61396015, /// "Last" : 0.00000820, /// "BaseVolume" : 0.64966963, /// "TimeStamp" : "2014-07-09T07:19:30.15", /// "Bid" : 0.00000820, /// "Ask" : 0.00000831, /// "OpenBuyOrders" : 15, /// "OpenSellOrders" : 15, /// "PrevDay" : 0.00000821, /// "Created" : "2014-03-20T06:00:00", /// "DisplayMarketName" : null /// }, { /// "MarketName" : "BTC-A3C", /// "High" : 0.00000072, /// "Low" : 0.00000001, /// "Volume" : 166340678.42280999, /// "Last" : 0.00000005, /// "BaseVolume" : 17.59720424, /// "TimeStamp" : "2014-07-09T07:21:40.51", /// "Bid" : 0.00000004, /// "Ask" : 0.00000005, /// "OpenBuyOrders" : 18, /// "OpenSellOrders" : 18, /// "PrevDay" : 0.00000002, /// "Created" : "2014-05-30T07:57:49.637", /// "DisplayMarketName" : null /// } /// ] /// } /// ``` pub fn get_market_summaries(&mut self) -> Result> { let mut params = HashMap::new(); self.public_query("/public/getmarketsummaries", &mut params) } /// Used to get the last 24 hour summary of all active exchanges /// "market" required a string literal for the market (ex: BTC-LTC) /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : [{ /// "MarketName" : "BTC-LTC", /// "High" : 0.01350000, /// "Low" : 0.01200000, /// "Volume" : 3833.97619253, /// "Last" : 0.01349998, /// "BaseVolume" : 47.03987026, /// "TimeStamp" : "2014-07-09T07:22:16.72", /// "Bid" : 0.01271001, /// "Ask" : 0.01291100, /// "OpenBuyOrders" : 45, /// "OpenSellOrders" : 45, /// "PrevDay" : 0.01229501, /// "Created" : "2014-02-13T00:00:00", /// "DisplayMarketName" : null /// } /// ] /// } /// ``` pub fn get_market_summary(&mut self, market: &str) -> Result> { let mut params = HashMap::new(); params.insert("market", market); self.public_query("/public/getmarketsummary", &mut params) } /// Used to get retrieve the orderbook for a given market /// "market" required a string literal for the market (ex: BTC-LTC) /// "order_type" required "buy", "sell" or "both" to identify the type of orderbook to return. /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : { /// "buy" : [{ /// "Quantity" : 12.37000000, /// "Rate" : 0.02525000 /// } /// ], /// "sell" : [{ /// "Quantity" : 32.55412402, /// "Rate" : 0.02540000 /// }, { /// "Quantity" : 60.00000000, /// "Rate" : 0.02550000 /// }, { /// "Quantity" : 60.00000000, /// "Rate" : 0.02575000 /// }, { /// "Quantity" : 84.00000000, /// "Rate" : 0.02600000 /// } /// ] /// } /// } /// ``` pub fn get_order_book(&mut self, market: &str, order_type: &str) -> Result> { let mut params = HashMap::new(); params.insert("market", market); params.insert("type", order_type); self.public_query("/public/getorderbook", &mut params) } /// Used to retrieve the latest trades that have occured for a specific market. /// "market" required a string literal for the market (ex: BTC-LTC) /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : [{ /// "Id" : 319435, /// "TimeStamp" : "2014-07-09T03:21:20.08", /// "Quantity" : 0.30802438, /// "Price" : 0.01263400, /// "Total" : 0.00389158, /// "FillType" : "FILL", /// "OrderType" : "BUY" /// }, { /// "Id" : 319433, /// "TimeStamp" : "2014-07-09T03:21:20.08", /// "Quantity" : 0.31820814, /// "Price" : 0.01262800, /// "Total" : 0.00401833, /// "FillType" : "PARTIAL_FILL", /// "OrderType" : "BUY" /// }, { /// "Id" : 319379, /// "TimeStamp" : "2014-07-09T02:58:48.127", /// "Quantity" : 49.64643541, /// "Price" : 0.01263200, /// "Total" : 0.62713377, /// "FillType" : "FILL", /// "OrderType" : "SELL" /// }, { /// "Id" : 319378, /// "TimeStamp" : "2014-07-09T02:58:46.27", /// "Quantity" : 0.35356459, /// "Price" : 0.01263200, /// "Total" : 0.00446622, /// "FillType" : "PARTIAL_FILL", /// "OrderType" : "BUY" /// } /// ] /// } /// ``` pub fn get_market_history(&mut self, market: &str) -> Result> { let mut params = HashMap::new(); params.insert("market", market); self.public_query("/public/getmarkethistory", &mut params) } /// Used to place a buy order in a specific market. Use buylimit to place limit orders. /// Make sure you have the proper permissions set on your API keys for this call to work. /// "market" required a string literal for the market (ex: BTC-LTC) /// "quantity" required the amount to purchase /// "rate" required the rate at which to place the order. /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : { /// "uuid" : "e606d53c-8d70-11e3-94b5-425861b86ab6" /// } /// } /// ``` pub fn buy_limit(&mut self, market: &str, quantity: &str, rate: &str) -> Result> { let mut params = HashMap::new(); params.insert("market", market); params.insert("quantity", quantity); params.insert("rate", rate); self.private_query("/market/buylimit", &mut params) } /// Used to place a sell order in a specific market. Use selllimit to place limit orders. /// Make sure you have the proper permissions set on your API keys for this call to work. /// "market" required a string literal for the market (ex: BTC-LTC) /// "quantity" required the amount to purchase /// "rate" required the rate at which to place the order. /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : { /// "uuid" : "614c34e4-8d71-11e3-94b5-425861b86ab6" /// } /// } /// ``` pub fn sell_limit(&mut self, market: &str, quantity: &str, rate: &str) -> Result> { let mut params = HashMap::new(); params.insert("market", market); params.insert("quantity", quantity); params.insert("rate", rate); self.private_query("/market/selllimit", &mut params) } /// Used to cancel a buy or sell order. /// "uuid" required uuid of buy or sell order /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : null /// } /// ``` pub fn cancel(&mut self, uuid: &str) -> Result> { let mut params = HashMap::new(); params.insert("uuid", uuid); self.private_query("/market/cancel", &mut params) } /// Get all orders that you currently have opened. A specific market can be requested /// "market" optional a string literal for the market (ie. BTC-LTC) /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : [{ /// "Uuid" : null, /// "OrderUuid" : "09aa5bb6-8232-41aa-9b78-a5a1093e0211", /// "Exchange" : "BTC-LTC", /// "OrderType" : "LIMIT_SELL", /// "Quantity" : 5.00000000, /// "QuantityRemaining" : 5.00000000, /// "Limit" : 2.00000000, /// "CommissionPaid" : 0.00000000, /// "Price" : 0.00000000, /// "PricePerUnit" : null, /// "Opened" : "2014-07-09T03:55:48.77", /// "Closed" : null, /// "CancelInitiated" : false, /// "ImmediateOrCancel" : false, /// "IsConditional" : false, /// "Condition" : null, /// "ConditionTarget" : null /// }, { /// "Uuid" : null, /// "OrderUuid" : "8925d746-bc9f-4684-b1aa-e507467aaa99", /// "Exchange" : "BTC-LTC", /// "OrderType" : "LIMIT_BUY", /// "Quantity" : 100000.00000000, /// "QuantityRemaining" : 100000.00000000, /// "Limit" : 0.00000001, /// "CommissionPaid" : 0.00000000, /// "Price" : 0.00000000, /// "PricePerUnit" : null, /// "Opened" : "2014-07-09T03:55:48.583", /// "Closed" : null, /// "CancelInitiated" : false, /// "ImmediateOrCancel" : false, /// "IsConditional" : false, /// "Condition" : null, /// "ConditionTarget" : null /// } /// ] /// } /// ``` pub fn get_open_orders(&mut self, market: &str) -> Result> { let mut params = HashMap::new(); params.insert("market", market); self.private_query("/market/getopenorders", &mut params) } /// Used to retrieve all balances from your account /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : [{ /// "Currency" : "DOGE", /// "Balance" : 0.00000000, /// "Available" : 0.00000000, /// "Pending" : 0.00000000, /// "CryptoAddress" : "DLxcEt3AatMyr2NTatzjsfHNoB9NT62HiF", /// "Requested" : false, /// "Uuid" : null /// /// }, { /// "Currency" : "BTC", /// "Balance" : 14.21549076, /// "Available" : 14.21549076, /// "Pending" : 0.00000000, /// "CryptoAddress" : "1Mrcdr6715hjda34pdXuLqXcju6qgwHA31", /// "Requested" : false, /// "Uuid" : null /// } /// ] /// } /// ``` pub fn get_balances(&mut self) -> Result> { let mut params = HashMap::new(); self.private_query("/account/getbalances", &mut params) } /// Used to retrieve the balance from your account for a specific currency. /// "currency" required a string literal for the currency (ex: LTC) /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : { /// "Currency" : "BTC", /// "Balance" : 4.21549076, /// "Available" : 4.21549076, /// "Pending" : 0.00000000, /// "CryptoAddress" : "1MacMr6715hjds342dXuLqXcju6fgwHA31", /// "Requested" : false, /// "Uuid" : null /// } /// } /// ``` pub fn get_balance(&mut self, currency: &str) -> Result> { let mut params = HashMap::new(); params.insert("currency", currency); self.private_query("/account/getbalance", &mut params) } /// Used to retrieve or generate an address for a specific currency. /// If one does not exist, the call will fail and return ADDRESS_GENERATING until one is available. /// "currency" required a string literal for the currency (ex: LTC) /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : { /// "Currency" : "VTC", /// "Address" : "Vy5SKeKGXUHKS2WVpJ76HYuKAu3URastUo" /// } /// } /// ``` pub fn get_deposit_address(&mut self, currency: &str) -> Result> { let mut params = HashMap::new(); params.insert("currency", currency); self.private_query("/account/getdepositaddress", &mut params) } /// Used to withdraw funds from your account. note: please account for txfee. /// "currency" required a string literal for the currency (ie. BTC) /// "quantity" required the quantity of coins to withdraw /// "address" required the address where to send the funds. /// "paymentid" optional used for CryptoNotes/BitShareX/Nxt optional field (memo/paymentid) /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : { /// "uuid" : "68b5a16c-92de-11e3-ba3b-425861b86ab6" /// } /// } /// ``` pub fn withdraw(&mut self, currency: &str, quantity: &str, address: &str, paymentid: &str) -> Result> { let mut params = HashMap::new(); params.insert("currency", currency); params.insert("quantity", quantity); params.insert("address", address); params.insert("paymentid", paymentid); self.private_query("/account/withdraw", &mut params) } /// Used to retrieve a single order by uuid. /// "uuid" required the uuid of the buy or sell order /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : { /// "AccountId" : null, /// "OrderUuid" : "0cb4c4e4-bdc7-4e13-8c13-430e587d2cc1", /// "Exchange" : "BTC-SHLD", /// "Type" : "LIMIT_BUY", /// "Quantity" : 1000.00000000, /// "QuantityRemaining" : 1000.00000000, /// "Limit" : 0.00000001, /// "Reserved" : 0.00001000, /// "ReserveRemaining" : 0.00001000, /// "CommissionReserved" : 0.00000002, /// "CommissionReserveRemaining" : 0.00000002, /// "CommissionPaid" : 0.00000000, /// "Price" : 0.00000000, /// "PricePerUnit" : null, /// "Opened" : "2014-07-13T07:45:46.27", /// "Closed" : null, /// "IsOpen" : true, /// "Sentinel" : "6c454604-22e2-4fb4-892e-179eede20972", /// "CancelInitiated" : false, /// "ImmediateOrCancel" : false, /// "IsConditional" : false, /// "Condition" : "NONE", /// "ConditionTarget" : null /// } /// } /// ``` pub fn get_order(&mut self, uuid: &str) -> Result> { let mut params = HashMap::new(); params.insert("uuid", uuid); self.private_query("/account/getorder", &mut params) } /// Used to retrieve your order history. /// "market" optional a string literal for the market (ie. BTC-LTC). /// If ommited, will return for all markets /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : [{ /// "OrderUuid" : "fd97d393-e9b9-4dd1-9dbf-f288fc72a185", /// "Exchange" : "BTC-LTC", /// "TimeStamp" : "2014-07-09T04:01:00.667", /// "OrderType" : "LIMIT_BUY", /// "Limit" : 0.00000001, /// "Quantity" : 100000.00000000, /// "QuantityRemaining" : 100000.00000000, /// "Commission" : 0.00000000, /// "Price" : 0.00000000, /// "PricePerUnit" : null, /// "IsConditional" : false, /// "Condition" : null, /// "ConditionTarget" : null, /// "ImmediateOrCancel" : false /// }, { /// "OrderUuid" : "17fd64d1-f4bd-4fb6-adb9-42ec68b8697d", /// "Exchange" : "BTC-ZS", /// "TimeStamp" : "2014-07-08T20:38:58.317", /// "OrderType" : "LIMIT_SELL", /// "Limit" : 0.00002950, /// "Quantity" : 667.03644955, /// "QuantityRemaining" : 0.00000000, /// "Commission" : 0.00004921, /// "Price" : 0.01968424, /// "PricePerUnit" : 0.00002950, /// "IsConditional" : false, /// "Condition" : null, /// "ConditionTarget" : null, /// "ImmediateOrCancel" : false /// } /// ] /// } /// ``` pub fn get_order_history(&mut self, market: &str) -> Result> { let mut params = HashMap::new(); params.insert("market", market); self.private_query("/account/getorderhistory", &mut params) } /// Used to retrieve your withdrawal history. /// "currency" optional a string literal for the currecy (ie. BTC). /// If omitted, will return for all currencies /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : [{ /// "PaymentUuid" : "b52c7a5c-90c6-4c6e-835c-e16df12708b1", /// "Currency" : "BTC", /// "Amount" : 17.00000000, /// "Address" : "1DeaaFBdbB5nrHj87x3NHS4onvw1GPNyAu", /// "Opened" : "2014-07-09T04:24:47.217", /// "Authorized" : true, /// "PendingPayment" : false, /// "TxCost" : 0.00020000, /// "TxId" : null, /// "Canceled" : true, /// "InvalidAddress" : false /// }, { /// "PaymentUuid" : "f293da98-788c-4188-a8f9-8ec2c33fdfcf", /// "Currency" : "XC", /// "Amount" : 7513.75121715, /// "Address" : "XVnSMgAd7EonF2Dgc4c9K14L12RBaW5S5J", /// "Opened" : "2014-07-08T23:13:31.83", /// "Authorized" : true, /// "PendingPayment" : false, /// "TxCost" : 0.00002000, /// "TxId" : "b4a575c2a71c7e56d02ab8e26bb1ef0a2f6cf2094f6ca2116476a569c1e84f6e", /// "Canceled" : false, /// "InvalidAddress" : false /// } /// ] /// } /// ``` pub fn get_withdrawal_history(&mut self, currency: &str) -> Result> { let mut params = HashMap::new(); params.insert("currency", currency); self.private_query("/account/getwithdrawalhistory", &mut params) } /// Used to retrieve your deposit history. /// "currency" optional a string literal for the currecy (ie. BTC). /// If omitted, will return for all currencies /// /// ```json /// { /// "success" : true, /// "message" : "", /// "result" : [{ /// "PaymentUuid" : "554ec664-8842-4fe9-b491-06225becbd59", /// "Currency" : "BTC", /// "Amount" : 0.00156121, /// "Address" : "1K37yQZaGrPKNTZ5KNP792xw8f7XbXxetE", /// "Opened" : "2014-07-11T03:41:25.323", /// "Authorized" : true, /// "PendingPayment" : false, /// "TxCost" : 0.00020000, /// "TxId" : "70cf6fdccb9bd38e1a930e13e4ae6299d678ed6902da710fa3cc8d164f9be126", /// "Canceled" : false, /// "InvalidAddress" : false /// }, { /// "PaymentUuid" : "d3fdf168-3d8e-40b6-8fe4-f46e2a7035ea", /// "Currency" : "BTC", /// "Amount" : 0.11800000, /// "Address" : "1Mrcar6715hjds34pdXuLqXcju6QgwHA31", /// "O /// pened" : "2014-07-03T20:27:07.163", /// "Authorized" : true, /// "PendingPayment" : false, /// "TxCost" : 0.00020000, /// "TxId" : "3efd41b3a051433a888eed3ecc174c1d025a5e2b486eb418eaaec5efddda22de", /// "Canceled" : false, /// "InvalidAddress" : false /// } /// ] /// } /// ``` pub fn get_deposit_history(&mut self, currency: &str) -> Result> { let mut params = HashMap::new(); params.insert("currency", currency); self.private_query("/account/getdeposithistory", &mut params) } } ================================================ FILE: src/bittrex/credentials.rs ================================================ //! Contains the Bittrex credentials. use std::collections::HashMap; use std::str::FromStr; use serde_json; use serde_json::Value; use crate::coinnect::Credentials; use crate::exchange::Exchange; use crate::helpers; use crate::error::*; use std::fs::File; use std::io::Read; use std::path::PathBuf; #[derive(Debug, Clone)] pub struct BittrexCreds { exchange: Exchange, name: String, data: HashMap, } impl BittrexCreds { /// Create a new `BittrexCreds` from arguments. pub fn new(name: &str, api_key: &str, api_secret: &str) -> Self { let mut creds = BittrexCreds { data: HashMap::new(), exchange: Exchange::Bittrex, name: if name.is_empty() { "BittrexClient".to_string() } else { name.to_string() }, }; //if api_key.is_empty() { //warning!("No API key set for the Bitstamp client"); //} creds .data .insert("api_key".to_string(), api_key.to_string()); //if api_secret.is_empty() { //warning!("No API secret set for the Bitstamp client"); //} creds .data .insert("api_secret".to_string(), api_secret.to_string()); creds } /// Create a new `BittrexCreds` from a json configuration file. This file must follow this /// structure: /// /// ```json /// { /// "account_bittrex": { /// "exchange" : "bittrex", /// "api_key" : "123456789ABCDEF", /// "api_secret": "ABC&EF?abcdef" /// }, /// "account_bitstamp": { /// "exchange" : "bitstamp", /// "api_key" : "1234567890ABCDEF1234567890ABCDEF", /// "api_secret" : "1234567890ABCDEF1234567890ABCDEF", /// "customer_id": "123456" /// } /// } /// ``` /// For this example, you could use load your Bittrex account with /// `BittrexAPI::new(BittrexCreds::new_from_file("account_bittrex", Path::new("/keys.json")))` pub fn new_from_file(name: &str, path: PathBuf) -> Result { let mut f = File::open(&path)?; let mut buffer = String::new(); f.read_to_string(&mut buffer)?; let data: Value = serde_json::from_str(&buffer)?; let json_obj = data.as_object() .ok_or_else(|| ErrorKind::BadParse)? .get(name) .ok_or_else(|| ErrorKind::MissingField(name.to_string()))?; let api_key = helpers::get_json_string(json_obj, "api_key")?; let api_secret = helpers::get_json_string(json_obj, "api_secret")?; let exchange = { let exchange_str = helpers::get_json_string(json_obj, "exchange")?; Exchange::from_str(exchange_str) .chain_err(|| ErrorKind::InvalidFieldValue("exchange".to_string()))? }; if exchange != Exchange::Bittrex { return Err(ErrorKind::InvalidConfigType(Exchange::Bittrex, exchange).into()); } Ok(BittrexCreds::new(name, api_key, api_secret)) } } impl Credentials for BittrexCreds { /// Return a value from the credentials. fn get(&self, key: &str) -> Option { if let Some(res) = self.data.get(key) { Some(res.clone()) } else { None } } fn name(&self) -> String { self.name.clone() } fn exchange(&self) -> Exchange { self.exchange } } ================================================ FILE: src/bittrex/generic_api.rs ================================================ //! Use this module to interact with Bittrex through a Generic API. //! This a more convenient and safe way to deal with the exchange since methods return a Result<> //! but this generic API does not provide all the functionnality that Bittrex offers. use bigdecimal::BigDecimal; use std::str::FromStr; use crate::exchange::ExchangeApi; use crate::bittrex::api::BittrexApi; use crate::error::*; use crate::types::*; use crate::bittrex::utils; use crate::helpers; impl ExchangeApi for BittrexApi { fn ticker(&mut self, pair: Pair) -> Result { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let raw_response = self.get_market_summary(pair_name)?; let result = utils::parse_result(&raw_response)?; let result_array = result.as_array(); let result_obj = result_array.unwrap()[0].as_object().unwrap(); let price_str = result_obj.get("Last").unwrap().as_f64().unwrap().to_string(); let price = BigDecimal::from_str(&price_str).unwrap(); let ask_str = result_obj.get("Ask").unwrap().as_f64().unwrap().to_string(); let ask = BigDecimal::from_str(&ask_str).unwrap(); let bid_str = result_obj.get("Bid").unwrap().as_f64().unwrap().to_string(); let bid = BigDecimal::from_str(&bid_str).unwrap(); let volume_str = result_obj.get("Volume").unwrap().as_f64().unwrap().to_string(); let vol = BigDecimal::from_str(&volume_str).unwrap(); Ok(Ticker { timestamp: helpers::get_unix_timestamp_ms(), pair: pair, last_trade_price: price, lowest_ask: ask, highest_bid: bid, volume: Some(vol), }) } fn orderbook(&mut self, pair: Pair) -> Result { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let raw_response = self.get_order_book(pair_name, "both")?; let result = utils::parse_result(&raw_response)?; let mut ask_offers = Vec::new(); // buy orders let mut bid_offers = Vec::new(); // sell orders let buy_orders = result["buy"].as_array() .ok_or_else(|| ErrorKind::InvalidFieldFormat(format!("{}", result["buy"])))?; let sell_orders = result["sell"].as_array() .ok_or_else(|| ErrorKind::InvalidFieldFormat(format!("{}", result["sell"])))?; for ask in buy_orders { let ask_obj = ask.as_object().unwrap(); let price_str = ask_obj.get("Rate").unwrap().as_f64().unwrap().to_string(); let price = BigDecimal::from_str(&price_str).unwrap(); let volume_str = ask_obj.get("Quantity").unwrap().as_f64().unwrap().to_string(); let volume = BigDecimal::from_str(&volume_str).unwrap(); ask_offers.push((price, volume)); } for bid in sell_orders { let bid_obj = bid.as_object().unwrap(); let price_str = bid_obj.get("Rate").unwrap().as_f64().unwrap().to_string(); let price = BigDecimal::from_str(&price_str).unwrap(); let volume_str = bid_obj.get("Quantity").unwrap().as_f64().unwrap().to_string(); let volume = BigDecimal::from_str(&volume_str).unwrap(); bid_offers.push((price, volume)); } Ok(Orderbook { timestamp: helpers::get_unix_timestamp_ms(), pair: pair, asks: ask_offers, bids: bid_offers, }) } fn add_order(&mut self, order_type: OrderType, pair: Pair, quantity: Volume, price: Option) -> Result { let pair_name = match utils::get_pair_string(&pair) { Some(pair_str) => pair_str, None => return Err(ErrorKind::PairUnsupported.into()) }; let raw_response = match order_type { OrderType::BuyLimit => { if price.is_none() { return Err(ErrorKind::MissingPrice.into()); } self.buy_limit(pair_name, &quantity.to_string(), &price.unwrap().to_string()) } OrderType::BuyMarket => { let min_price = "0.000000001"; self.buy_limit(pair_name, &quantity.to_string(), min_price) } OrderType::SellLimit => { if price.is_none() { return Err(ErrorKind::MissingPrice.into()); } self.sell_limit(pair_name, &quantity.to_string(), &price.unwrap().to_string()) } OrderType::SellMarket => { let max_price = "999999999.99"; self.buy_limit(pair_name, &quantity.to_string(), max_price) } }?; let result = utils::parse_result(&raw_response)?; let result_obj = result.as_object().unwrap(); Ok(OrderInfo { timestamp: helpers::get_unix_timestamp_ms(), identifier: vec![result_obj.get("uuid").unwrap().as_str().unwrap().to_string()], }) } fn balances(&mut self) -> Result { let raw_response = self.get_balances()?; let result = utils::parse_result(&raw_response)?; let result_array = result.as_array().unwrap(); let mut balances = Balances::new(); for currency in result_array { let currency_obj = currency.as_object().unwrap(); let currency_str = currency_obj.get("Currency").unwrap().as_str().unwrap(); let currency = utils::get_currency_enum(¤cy_str); match currency { Some(c) => { let amount_str = currency_obj.get("Available").unwrap().as_f64().unwrap().to_string(); let amount = BigDecimal::from_str(&amount_str).unwrap(); balances.insert(c, amount); }, _ => () } } Ok(balances) } } ================================================ FILE: src/bittrex/mod.rs ================================================ //! Use this module to interact with Bittrex exchange. //! See examples for more informations. pub mod api; pub mod generic_api; pub mod credentials; pub mod utils; pub use self::credentials::BittrexCreds; pub use self::api::BittrexApi; ================================================ FILE: src/bittrex/utils.rs ================================================ use bidir_map::BidirMap; use serde_json; use serde_json::Value; use serde_json::value::Map; use crate::error::*; use crate::types::Currency; use crate::types::Pair; use crate::types::Pair::*; lazy_static! { static ref PAIRS_STRING: BidirMap = { let mut m = BidirMap::new(); m.insert(_1ST_BTC, "BTC-1ST"); m.insert(_2GIVE_BTC, "BTC-2GIVE"); m.insert(ABY_BTC, "BTC-ABY"); m.insert(ADA_BTC, "BTC-ADA"); m.insert(ADT_BTC, "BTC-ADT"); m.insert(ADX_BTC, "BTC-ADX"); m.insert(AEON_BTC, "BTC-AEON"); m.insert(AGRS_BTC, "BTC-AGRS"); m.insert(AMP_BTC, "BTC-AMP"); m.insert(ANT_BTC, "BTC-ANT"); m.insert(APX_BTC, "BTC-APX"); m.insert(ARDR_BTC, "BTC-ARDR"); m.insert(ARK_BTC, "BTC-ARK"); m.insert(AUR_BTC, "BTC-AUR"); m.insert(BAT_BTC, "BTC-BAT"); m.insert(BAY_BTC, "BTC-BAY"); m.insert(BCC_BTC, "BTC-BCC"); m.insert(BCY_BTC, "BTC-BCY"); m.insert(BITB_BTC, "BTC-BITB"); m.insert(BLITZ_BTC, "BTC-BLITZ"); m.insert(BLK_BTC, "BTC-BLK"); m.insert(BLOCK_BTC, "BTC-BLOCK"); m.insert(BNT_BTC, "BTC-BNT"); m.insert(BRK_BTC, "BTC-BRK"); m.insert(BRX_BTC, "BTC-BRX"); m.insert(BSD_BTC, "BTC-BSD"); m.insert(BTCD_BTC, "BTC-BTCD"); m.insert(BTS_BTC, "BTC-BTS"); m.insert(BURST_BTC, "BTC-BURST"); m.insert(BYC_BTC, "BTC-BYC"); m.insert(CANN_BTC, "BTC-CANN"); m.insert(CFI_BTC, "BTC-CFI"); m.insert(CLAM_BTC, "BTC-CLAM"); m.insert(CLOAK_BTC, "BTC-CLOAK"); m.insert(CLUB_BTC, "BTC-CLUB"); m.insert(COVAL_BTC, "BTC-COVAL"); m.insert(CPC_BTC, "BTC-CPC"); m.insert(CRB_BTC, "BTC-CRB"); m.insert(CRW_BTC, "BTC-CRW"); m.insert(CURE_BTC, "BTC-CURE"); m.insert(CVC_BTC, "BTC-CVC"); m.insert(DASH_BTC, "BTC-DASH"); m.insert(DCR_BTC, "BTC-DCR"); m.insert(DCT_BTC, "BTC-DCT"); m.insert(DGB_BTC, "BTC-DGB"); m.insert(DGD_BTC, "BTC-DGD"); m.insert(DMD_BTC, "BTC-DMD"); m.insert(DNT_BTC, "BTC-DNT"); m.insert(DOGE_BTC, "BTC-DOGE"); m.insert(DOPE_BTC, "BTC-DOPE"); m.insert(DTB_BTC, "BTC-DTB"); m.insert(DYN_BTC, "BTC-DYN"); m.insert(EBST_BTC, "BTC-EBST"); m.insert(EDG_BTC, "BTC-EDG"); m.insert(EFL_BTC, "BTC-EFL"); m.insert(EGC_BTC, "BTC-EGC"); m.insert(EMC_BTC, "BTC-EMC"); m.insert(EMC2_BTC, "BTC-EMC2"); m.insert(ENRG_BTC, "BTC-ENRG"); m.insert(ERC_BTC, "BTC-ERC"); m.insert(ETC_BTC, "BTC-ETC"); m.insert(ETH_BTC, "BTC-ETH"); m.insert(EXCL_BTC, "BTC-EXCL"); m.insert(EXP_BTC, "BTC-EXP"); m.insert(FAIR_BTC, "BTC-FAIR"); m.insert(FCT_BTC, "BTC-FCT"); m.insert(FLDC_BTC, "BTC-FLDC"); m.insert(FLO_BTC, "BTC-FLO"); m.insert(FTC_BTC, "BTC-FTC"); m.insert(FUN_BTC, "BTC-FUN"); m.insert(GAM_BTC, "BTC-GAM"); m.insert(GAME_BTC, "BTC-GAME"); m.insert(GBG_BTC, "BTC-GBG"); m.insert(GBYTE_BTC, "BTC-GBYTE"); m.insert(GCR_BTC, "BTC-GCR"); m.insert(GEO_BTC, "BTC-GEO"); m.insert(GLD_BTC, "BTC-GLD"); m.insert(GNO_BTC, "BTC-GNO"); m.insert(GNT_BTC, "BTC-GNT"); m.insert(GOLOS_BTC, "BTC-GOLOS"); m.insert(GRC_BTC, "BTC-GRC"); m.insert(GRS_BTC, "BTC-GRS"); m.insert(GUP_BTC, "BTC-GUP"); m.insert(HMQ_BTC, "BTC-HMQ"); m.insert(INCNT_BTC, "BTC-INCNT"); m.insert(INFX_BTC, "BTC-INFX"); m.insert(IOC_BTC, "BTC-IOC"); m.insert(ION_BTC, "BTC-ION"); m.insert(IOP_BTC, "BTC-IOP"); m.insert(KMD_BTC, "BTC-KMD"); m.insert(KORE_BTC, "BTC-KORE"); m.insert(LBC_BTC, "BTC-LBC"); m.insert(LGD_BTC, "BTC-LGD"); m.insert(LMC_BTC, "BTC-LMC"); m.insert(LSK_BTC, "BTC-LSK"); m.insert(LTC_BTC, "BTC-LTC"); m.insert(LUN_BTC, "BTC-LUN"); m.insert(MAID_BTC, "BTC-MAID"); m.insert(MANA_BTC, "BTC-MANA"); m.insert(MCO_BTC, "BTC-MCO"); m.insert(MEME_BTC, "BTC-MEME"); m.insert(MLN_BTC, "BTC-MLN"); m.insert(MONA_BTC, "BTC-MONA"); m.insert(MTL_BTC, "BTC-MTL"); m.insert(MUE_BTC, "BTC-MUE"); m.insert(MUSIC_BTC, "BTC-MUSIC"); m.insert(MYST_BTC, "BTC-MYST"); m.insert(NAV_BTC, "BTC-NAV"); m.insert(NBT_BTC, "BTC-NBT"); m.insert(NEO_BTC, "BTC-NEO"); m.insert(NEOS_BTC, "BTC-NEOS"); m.insert(NLG_BTC, "BTC-NLG"); m.insert(NMR_BTC, "BTC-NMR"); m.insert(NXC_BTC, "BTC-NXC"); m.insert(NXS_BTC, "BTC-NXS"); m.insert(NXT_BTC, "BTC-NXT"); m.insert(OK_BTC, "BTC-OK"); m.insert(OMG_BTC, "BTC-OMG"); m.insert(OMNI_BTC, "BTC-OMNI"); m.insert(PART_BTC, "BTC-PART"); m.insert(PAY_BTC, "BTC-PAY"); m.insert(PDC_BTC, "BTC-PDC"); m.insert(PINK_BTC, "BTC-PINK"); m.insert(PIVX_BTC, "BTC-PIVX"); m.insert(PKB_BTC, "BTC-PKB"); m.insert(POT_BTC, "BTC-POT"); m.insert(PPC_BTC, "BTC-PPC"); m.insert(PTC_BTC, "BTC-PTC"); m.insert(PTOY_BTC, "BTC-PTOY"); m.insert(QRL_BTC, "BTC-QRL"); m.insert(QTUM_BTC, "BTC-QTUM"); m.insert(QWARK_BTC, "BTC-QWARK"); m.insert(RADS_BTC, "BTC-RADS"); m.insert(RBY_BTC, "BTC-RBY"); m.insert(RDD_BTC, "BTC-RDD"); m.insert(REP_BTC, "BTC-REP"); m.insert(RISE_BTC, "BTC-RISE"); m.insert(RLC_BTC, "BTC-RLC"); m.insert(SAFEX_BTC, "BTC-SAFEX"); m.insert(SALT_BTC, "BTC-SALT"); m.insert(SBD_BTC, "BTC-SBD"); m.insert(SC_BTC, "BTC-SC"); m.insert(SEQ_BTC, "BTC-SEQ"); m.insert(SHIFT_BTC, "BTC-SHIFT"); m.insert(SIB_BTC, "BTC-SIB"); m.insert(SLR_BTC, "BTC-SLR"); m.insert(SLS_BTC, "BTC-SLS"); m.insert(SNGLS_BTC, "BTC-SNGLS"); m.insert(SNRG_BTC, "BTC-SNRG"); m.insert(SNT_BTC, "BTC-SNT"); m.insert(SPHR_BTC, "BTC-SPHR"); m.insert(SPR_BTC, "BTC-SPR"); m.insert(START_BTC, "BTC-START"); m.insert(STEEM_BTC, "BTC-STEEM"); m.insert(STORJ_BTC, "BTC-STORJ"); m.insert(STRAT_BTC, "BTC-STRAT"); m.insert(SWIFT_BTC, "BTC-SWIFT"); m.insert(SWT_BTC, "BTC-SWT"); m.insert(SYNX_BTC, "BTC-SYNX"); m.insert(SYS_BTC, "BTC-SYS"); m.insert(THC_BTC, "BTC-THC"); m.insert(TIME_BTC, "BTC-TIME"); m.insert(TIX_BTC, "BTC-TIX"); m.insert(TKN_BTC, "BTC-TKN"); m.insert(TKS_BTC, "BTC-TKS"); m.insert(TRIG_BTC, "BTC-TRIG"); m.insert(TRST_BTC, "BTC-TRST"); m.insert(TRUST_BTC, "BTC-TRUST"); m.insert(TX_BTC, "BTC-TX"); m.insert(UBQ_BTC, "BTC-UBQ"); m.insert(UNB_BTC, "BTC-UNB"); m.insert(VIA_BTC, "BTC-VIA"); m.insert(VOX_BTC, "BTC-VOX"); m.insert(VRC_BTC, "BTC-VRC"); m.insert(VRM_BTC, "BTC-VRM"); m.insert(VTC_BTC, "BTC-VTC"); m.insert(VTR_BTC, "BTC-VTR"); m.insert(WAVES_BTC, "BTC-WAVES"); m.insert(WINGS_BTC, "BTC-WINGS"); m.insert(XAUR_BTC, "BTC-XAUR"); m.insert(XCP_BTC, "BTC-XCP"); m.insert(XDN_BTC, "BTC-XDN"); m.insert(XEL_BTC, "BTC-XEL"); m.insert(XEM_BTC, "BTC-XEM"); m.insert(XLM_BTC, "BTC-XLM"); m.insert(XMG_BTC, "BTC-XMG"); m.insert(XMR_BTC, "BTC-XMR"); m.insert(XMY_BTC, "BTC-XMY"); m.insert(XRP_BTC, "BTC-XRP"); m.insert(XST_BTC, "BTC-XST"); m.insert(XVC_BTC, "BTC-XVC"); m.insert(XVG_BTC, "BTC-XVG"); m.insert(XWC_BTC, "BTC-XWC"); m.insert(XZC_BTC, "BTC-XZC"); m.insert(ZCL_BTC, "BTC-ZCL"); m.insert(ZEC_BTC, "BTC-ZEC"); m.insert(ZEN_BTC, "BTC-ZEN"); m.insert(_1ST_ETH, "ETH-1ST"); m.insert(ADT_ETH, "ETH-ADT"); m.insert(ADX_ETH, "ETH-ADX"); m.insert(ANT_ETH, "ETH-ANT"); m.insert(BAT_ETH, "ETH-BAT"); m.insert(BCC_ETH, "ETH-BCC"); m.insert(BNT_ETH, "ETH-BNT"); m.insert(BTS_ETH, "ETH-BTS"); m.insert(CFI_ETH, "ETH-CFI"); m.insert(CRB_ETH, "ETH-CRB"); m.insert(CVC_ETH, "ETH-CVC"); m.insert(DASH_ETH, "ETH-DASH"); m.insert(DGB_ETH, "ETH-DGB"); m.insert(DGD_ETH, "ETH-DGD"); m.insert(DNT_ETH, "ETH-DNT"); m.insert(ETC_ETH, "ETH-ETC"); m.insert(FCT_ETH, "ETH-FCT"); m.insert(FUN_ETH, "ETH-FUN"); m.insert(GNO_ETH, "ETH-GNO"); m.insert(GNT_ETH, "ETH-GNT"); m.insert(GUP_ETH, "ETH-GUP"); m.insert(HMQ_ETH, "ETH-HMQ"); m.insert(LGD_ETH, "ETH-LGD"); m.insert(LTC_ETH, "ETH-LTC"); m.insert(LUN_ETH, "ETH-LUN"); m.insert(MANA_ETH, "ETH-MANA"); m.insert(MCO_ETH, "ETH-MCO"); m.insert(MTL_ETH, "ETH-MTL"); m.insert(MYST_ETH, "ETH-MYST"); m.insert(NEO_ETH, "ETH-NEO"); m.insert(NMR_ETH, "ETH-NMR"); m.insert(OMG_ETH, "ETH-OMG"); m.insert(PAY_ETH, "ETH-PAY"); m.insert(PTOY_ETH, "ETH-PTOY"); m.insert(QRL_ETH, "ETH-QRL"); m.insert(QTUM_ETH, "ETH-QTUM"); m.insert(REP_ETH, "ETH-REP"); m.insert(RLC_ETH, "ETH-RLC"); m.insert(SALT_ETH, "ETH-SALT"); m.insert(SC_ETH, "ETH-SC"); m.insert(SNGLS_ETH, "ETH-SNGLS"); m.insert(SNT_ETH, "ETH-SNT"); m.insert(STORJ_ETH, "ETH-STORJ"); m.insert(STRAT_ETH, "ETH-STRAT"); m.insert(TIME_ETH, "ETH-TIME"); m.insert(TIX_ETH, "ETH-TIX"); m.insert(TKN_ETH, "ETH-TKN"); m.insert(TRST_ETH, "ETH-TRST"); m.insert(WAVES_ETH, "ETH-WAVES"); m.insert(WINGS_ETH, "ETH-WINGS"); m.insert(XEM_ETH, "ETH-XEM"); m.insert(XLM_ETH, "ETH-XLM"); m.insert(XMR_ETH, "ETH-XMR"); m.insert(XRP_ETH, "ETH-XRP"); m.insert(ZEC_ETH, "ETH-ZEC"); m.insert(BCC_USDT, "USDT-BCC"); m.insert(BTC_USDT, "USDT-BTC"); m.insert(DASH_USDT, "USDT-DASH"); m.insert(ETC_USDT, "USDT-ETC"); m.insert(ETH_USDT, "USDT-ETH"); m.insert(LTC_USDT, "USDT-LTC"); m.insert(NEO_USDT, "USDT-NEO"); m.insert(OMG_USDT, "USDT-OMG"); m.insert(XMR_USDT, "USDT-XMR"); m.insert(XRP_USDT, "USDT-XRP"); m.insert(ZEC_USDT, "USDT-ZEC"); m }; } /// Return the name associated to pair used by Bittrex /// If the Pair is not supported, None is returned. pub fn get_pair_string(pair: &Pair) -> Option<&&str> { PAIRS_STRING.get_by_first(pair) } /// Return the Pair enum associated to the string used by Bittrex /// If the Pair is not supported, None is returned. pub fn get_pair_enum(pair: &str) -> Option<&Pair> { PAIRS_STRING.get_by_second(&pair) } pub fn deserialize_json(json_string: &str) -> Result> { let data: Value = match serde_json::from_str(json_string) { Ok(data) => data, Err(_) => return Err(ErrorKind::BadParse.into()), }; match data.as_object() { Some(value) => Ok(value.clone()), None => Err(ErrorKind::BadParse.into()), } } /// If error array is null, return the result (which can be an array, object or null) /// else return the error string found in array pub fn parse_result(response: &Map) -> Result { let is_success = match response["success"].as_bool() { Some(is_success) => { is_success } None => return Err(ErrorKind::BadParse.into()), }; if is_success { Ok(response.get("result").unwrap().clone()) } else { let error_message = response.get("message") .ok_or_else(|| ErrorKind::MissingField("message".to_string()))? .as_str() .ok_or_else(|| ErrorKind::InvalidFieldFormat("message".to_string()))?; match error_message.as_ref() { "MIN_TRADE_REQUIREMENT_NOT_MET" => Err(ErrorKind::InsufficientOrderSize.into()), "INVALID_PERMISSION" => Err(ErrorKind::PermissionDenied.into()), _ => Err(ErrorKind::ExchangeSpecificError(error_message.to_string()).into()), } } } /// Return the currency enum associated with the /// string used by Bittrex. If no currency is found, /// return None /// # Examples /// /// ``` /// use coinnect::bittrex::utils::get_currency_enum; /// use coinnect::types::Currency; /// /// let currency = get_currency_enum("1ST"); /// assert_eq!(Some(Currency::_1ST), currency); /// ``` pub fn get_currency_enum(currency: &str) -> Option { match currency { "1ST" => Some(Currency::_1ST), "2GIVE" => Some(Currency::_2GIVE), "8BIT" => Some(Currency::_8BIT), "ABY" => Some(Currency::ABY), "ADA" => Some(Currency::ADA), "ADC" => Some(Currency::ADC), "ADT" => Some(Currency::ADT), "ADX" => Some(Currency::ADX), "AEON" => Some(Currency::AEON), "AGRS" => Some(Currency::AGRS), "AM" => Some(Currency::AM), "AMP" => Some(Currency::AMP), "AMS" => Some(Currency::AMS), "ANT" => Some(Currency::ANT), "APEX" => Some(Currency::APEX), "APX" => Some(Currency::APX), "ARB" => Some(Currency::ARB), "ARDR" => Some(Currency::ARDR), "ARK" => Some(Currency::ARK), "AUR" => Some(Currency::AUR), "BAT" => Some(Currency::BAT), "BAY" => Some(Currency::BAY), "BCC" => Some(Currency::BCC), "BCY" => Some(Currency::BCY), "BITB" => Some(Currency::BITB), "BITCNY" => Some(Currency::BITCNY), "BITS" => Some(Currency::BITS), "BITZ" => Some(Currency::BITZ), "BLC" => Some(Currency::BLC), "BLITZ" => Some(Currency::BLITZ), "BLK" => Some(Currency::BLK), "BLOCK" => Some(Currency::BLOCK), "BNT" => Some(Currency::BNT), "BOB" => Some(Currency::BOB), "BRK" => Some(Currency::BRK), "BRX" => Some(Currency::BRX), "BSD" => Some(Currency::BSD), "BSTY" => Some(Currency::BSTY), "BTA" => Some(Currency::BTA), "BTC" => Some(Currency::BTC), "BTCD" => Some(Currency::BTCD), "BTS" => Some(Currency::BTS), "BURST" => Some(Currency::BURST), "BYC" => Some(Currency::BYC), "CANN" => Some(Currency::CANN), "CCN" => Some(Currency::CCN), "CFI" => Some(Currency::CFI), "CLAM" => Some(Currency::CLAM), "CLOAK" => Some(Currency::CLOAK), "CLUB" => Some(Currency::CLUB), "COVAL" => Some(Currency::COVAL), "CPC" => Some(Currency::CPC), "CRB" => Some(Currency::CRB), "CRBIT" => Some(Currency::CRBIT), "CRW" => Some(Currency::CRW), "CRYPT" => Some(Currency::CRYPT), "CURE" => Some(Currency::CURE), "CVC" => Some(Currency::CVC), "DAR" => Some(Currency::DAR), "DASH" => Some(Currency::DASH), "DCR" => Some(Currency::DCR), "DCT" => Some(Currency::DCT), "DGB" => Some(Currency::DGB), "DGC" => Some(Currency::DGC), "DGD" => Some(Currency::DGD), "DMD" => Some(Currency::DMD), "DNT" => Some(Currency::DNT), "DOGE" => Some(Currency::DOGE), "DOPE" => Some(Currency::DOPE), "DRACO" => Some(Currency::DRACO), "DTB" => Some(Currency::DTB), "DTC" => Some(Currency::DTC), "DYN" => Some(Currency::DYN), "EBST" => Some(Currency::EBST), "EDG" => Some(Currency::EDG), "EFL" => Some(Currency::EFL), "EGC" => Some(Currency::EGC), "EMC" => Some(Currency::EMC), "EMC2" => Some(Currency::EMC2), "ENRG" => Some(Currency::ENRG), "ERC" => Some(Currency::ERC), "ETC" => Some(Currency::ETC), "ETH" => Some(Currency::ETH), "EXCL" => Some(Currency::EXCL), "EXP" => Some(Currency::EXP), "FAIR" => Some(Currency::FAIR), "FC2" => Some(Currency::FC2), "FCT" => Some(Currency::FCT), "FLDC" => Some(Currency::FLDC), "FLO" => Some(Currency::FLO), "FRK" => Some(Currency::FRK), "FSC2" => Some(Currency::FSC2), "FTC" => Some(Currency::FTC), "FUN" => Some(Currency::FUN), "GAM" => Some(Currency::GAM), "GAME" => Some(Currency::GAME), "GBG" => Some(Currency::GBG), "GBYTE" => Some(Currency::GBYTE), "GCR" => Some(Currency::GCR), "GEMZ" => Some(Currency::GEMZ), "GEO" => Some(Currency::GEO), "GHC" => Some(Currency::GHC), "GLD" => Some(Currency::GLD), "GNO" => Some(Currency::GNO), "GNT" => Some(Currency::GNT), "GOLOS" => Some(Currency::GOLOS), "GP" => Some(Currency::GP), "GRC" => Some(Currency::GRC), "GRS" => Some(Currency::GRS), "GRT" => Some(Currency::GRT), "GUP" => Some(Currency::GUP), "HKG" => Some(Currency::HKG), "HMQ" => Some(Currency::HMQ), "HYPER" => Some(Currency::HYPER), "HZ" => Some(Currency::HZ), "INCNT" => Some(Currency::INCNT), "INFX" => Some(Currency::INFX), "IOC" => Some(Currency::IOC), "ION" => Some(Currency::ION), "IOP" => Some(Currency::IOP), "J" => Some(Currency::J), "KMD" => Some(Currency::KMD), "KORE" => Some(Currency::KORE), "KR" => Some(Currency::KR), "LBC" => Some(Currency::LBC), "LGD" => Some(Currency::LGD), "LMC" => Some(Currency::LMC), "LSK" => Some(Currency::LSK), "LTC" => Some(Currency::LTC), "LUN" => Some(Currency::LUN), "LXC" => Some(Currency::LXC), "MAID" => Some(Currency::MAID), "MANA" => Some(Currency::MANA), "MAX" => Some(Currency::MAX), "MCO" => Some(Currency::MCO), "MEC" => Some(Currency::MEC), "MEME" => Some(Currency::MEME), "METAL" => Some(Currency::METAL), "MLN" => Some(Currency::MLN), "MND" => Some(Currency::MND), "MONA" => Some(Currency::MONA), "MTL" => Some(Currency::MTL), "MTR" => Some(Currency::MTR), "MUE" => Some(Currency::MUE), "MUSIC" => Some(Currency::MUSIC), "MYST" => Some(Currency::MYST), "MZC" => Some(Currency::MZC), "NAUT" => Some(Currency::NAUT), "NAV" => Some(Currency::NAV), "NBT" => Some(Currency::NBT), "NEO" => Some(Currency::NEO), "NEOS" => Some(Currency::NEOS), "NET" => Some(Currency::NET), "NEU" => Some(Currency::NEU), "NLG" => Some(Currency::NLG), "NMR" => Some(Currency::NMR), "NTRN" => Some(Currency::NTRN), "NXC" => Some(Currency::NXC), "NXS" => Some(Currency::NXS), "NXT" => Some(Currency::NXT), "OC" => Some(Currency::OC), "OK" => Some(Currency::OK), "OMG" => Some(Currency::OMG), "OMNI" => Some(Currency::OMNI), "ORB" => Some(Currency::ORB), "PART" => Some(Currency::PART), "PAY" => Some(Currency::PAY), "PDC" => Some(Currency::PDC), "PINK" => Some(Currency::PINK), "PIVX" => Some(Currency::PIVX), "PKB" => Some(Currency::PKB), "POT" => Some(Currency::POT), "PPC" => Some(Currency::PPC), "PRIME" => Some(Currency::PRIME), "PTC" => Some(Currency::PTC), "PTOY" => Some(Currency::PTOY), "PXI" => Some(Currency::PXI), "QRL" => Some(Currency::QRL), "QTUM" => Some(Currency::QTUM), "QWARK" => Some(Currency::QWARK), "RADS" => Some(Currency::RADS), "RBY" => Some(Currency::RBY), "RDD" => Some(Currency::RDD), "REP" => Some(Currency::REP), "RISE" => Some(Currency::RISE), "RLC" => Some(Currency::RLC), "ROOT" => Some(Currency::ROOT), "SAFEX" => Some(Currency::SAFEX), "SALT" => Some(Currency::SALT), "SBD" => Some(Currency::SBD), "SC" => Some(Currency::SC), "SCOT" => Some(Currency::SCOT), "SCRT" => Some(Currency::SCRT), "SEQ" => Some(Currency::SEQ), "SFR" => Some(Currency::SFR), "SHIFT" => Some(Currency::SHIFT), "SIB" => Some(Currency::SIB), "SLG" => Some(Currency::SLG), "SLING" => Some(Currency::SLING), "SLR" => Some(Currency::SLR), "SLS" => Some(Currency::SLS), "SNGLS" => Some(Currency::SNGLS), "SNRG" => Some(Currency::SNRG), "SNT" => Some(Currency::SNT), "SOON" => Some(Currency::SOON), "SPHR" => Some(Currency::SPHR), "SPR" => Some(Currency::SPR), "SPRTS" => Some(Currency::SPRTS), "SSD" => Some(Currency::SSD), "START" => Some(Currency::START), "STEEM" => Some(Currency::STEEM), "STEPS" => Some(Currency::STEPS), "STORJ" => Some(Currency::STORJ), "STRAT" => Some(Currency::STRAT), "STV" => Some(Currency::STV), "SWIFT" => Some(Currency::SWIFT), "SWING" => Some(Currency::SWING), "SWT" => Some(Currency::SWT), "SYNX" => Some(Currency::SYNX), "SYS" => Some(Currency::SYS), "TES" => Some(Currency::TES), "THC" => Some(Currency::THC), "TIME" => Some(Currency::TIME), "TIT" => Some(Currency::TIT), "TIX" => Some(Currency::TIX), "TKN" => Some(Currency::TKN), "TKS" => Some(Currency::TKS), "TRI" => Some(Currency::TRI), "TRIG" => Some(Currency::TRIG), "TRK" => Some(Currency::TRK), "TROLL" => Some(Currency::TROLL), "TRST" => Some(Currency::TRST), "TRUST" => Some(Currency::TRUST), "TX" => Some(Currency::TX), "U" => Some(Currency::U), "UBQ" => Some(Currency::UBQ), "UFO" => Some(Currency::UFO), "UNB" => Some(Currency::UNB), "UNIQ" => Some(Currency::UNIQ), "UNIT" => Some(Currency::UNIT), "UNO" => Some(Currency::UNO), "USDT" => Some(Currency::USDT), "UTC" => Some(Currency::UTC), "VIA" => Some(Currency::VIA), "VIOR" => Some(Currency::VIOR), "VIRAL" => Some(Currency::VIRAL), "VOX" => Some(Currency::VOX), "VPN" => Some(Currency::VPN), "VRC" => Some(Currency::VRC), "VRM" => Some(Currency::VRM), "VTC" => Some(Currency::VTC), "VTR" => Some(Currency::VTR), "WARP" => Some(Currency::WARP), "WAVES" => Some(Currency::WAVES), "WINGS" => Some(Currency::WINGS), "XAUR" => Some(Currency::XAUR), "XBB" => Some(Currency::XBB), "XC" => Some(Currency::XC), "XCO" => Some(Currency::XCO), "XCP" => Some(Currency::XCP), "XDN" => Some(Currency::XDN), "XDQ" => Some(Currency::XDQ), "XEL" => Some(Currency::XEL), "XEM" => Some(Currency::XEM), "XLM" => Some(Currency::XLM), "XMG" => Some(Currency::XMG), "XMR" => Some(Currency::XMR), "XMY" => Some(Currency::XMY), "XPY" => Some(Currency::XPY), "XQN" => Some(Currency::XQN), "XRP" => Some(Currency::XRP), "XSEED" => Some(Currency::XSEED), "XST" => Some(Currency::XST), "XTC" => Some(Currency::XTC), "XVC" => Some(Currency::XVC), "XVG" => Some(Currency::XVG), "XWC" => Some(Currency::XWC), "XZC" => Some(Currency::XZC), "YBC" => Some(Currency::YBC), "ZCL" => Some(Currency::ZCL), "ZEC" => Some(Currency::ZEC), "ZEN" => Some(Currency::ZEN), _ => None, } } /// Return the currency String associated with the /// string used by Bittrex. If no currency is found, /// return None /// # Examples /// /// ``` /// use coinnect::bittrex::utils::get_currency_string; /// use coinnect::types::Currency; /// /// let currency = get_currency_string(Currency::_1ST); /// assert_eq!(currency, Some("1ST".to_string())); /// ``` pub fn get_currency_string(currency: Currency) -> Option { match currency { Currency::_1ST => Some("1ST".to_string()), Currency::_2GIVE => Some("2GIVE".to_string()), Currency::_8BIT => Some("8BIT".to_string()), Currency::ABY => Some("ABY".to_string()), Currency::ADA => Some("ADA".to_string()), Currency::ADC => Some("ADC".to_string()), Currency::ADT => Some("ADT".to_string()), Currency::ADX => Some("ADX".to_string()), Currency::AEON => Some("AEON".to_string()), Currency::AGRS => Some("AGRS".to_string()), Currency::AM => Some("AM".to_string()), Currency::AMP => Some("AMP".to_string()), Currency::AMS => Some("AMS".to_string()), Currency::ANT => Some("ANT".to_string()), Currency::APEX => Some("APEX".to_string()), Currency::APX => Some("APX".to_string()), Currency::ARB => Some("ARB".to_string()), Currency::ARDR => Some("ARDR".to_string()), Currency::ARK => Some("ARK".to_string()), Currency::AUR => Some("AUR".to_string()), Currency::BAT => Some("BAT".to_string()), Currency::BAY => Some("BAY".to_string()), Currency::BCC => Some("BCC".to_string()), Currency::BCY => Some("BCY".to_string()), Currency::BITB => Some("BITB".to_string()), Currency::BITCNY => Some("BITCNY".to_string()), Currency::BITS => Some("BITS".to_string()), Currency::BITZ => Some("BITZ".to_string()), Currency::BLC => Some("BLC".to_string()), Currency::BLITZ => Some("BLITZ".to_string()), Currency::BLK => Some("BLK".to_string()), Currency::BLOCK => Some("BLOCK".to_string()), Currency::BNT => Some("BNT".to_string()), Currency::BOB => Some("BOB".to_string()), Currency::BRK => Some("BRK".to_string()), Currency::BRX => Some("BRX".to_string()), Currency::BSD => Some("BSD".to_string()), Currency::BSTY => Some("BSTY".to_string()), Currency::BTA => Some("BTA".to_string()), Currency::BTC => Some("BTC".to_string()), Currency::BTCD => Some("BTCD".to_string()), Currency::BTS => Some("BTS".to_string()), Currency::BURST => Some("BURST".to_string()), Currency::BYC => Some("BYC".to_string()), Currency::CANN => Some("CANN".to_string()), Currency::CCN => Some("CCN".to_string()), Currency::CFI => Some("CFI".to_string()), Currency::CLAM => Some("CLAM".to_string()), Currency::CLOAK => Some("CLOAK".to_string()), Currency::CLUB => Some("CLUB".to_string()), Currency::COVAL => Some("COVAL".to_string()), Currency::CPC => Some("CPC".to_string()), Currency::CRB => Some("CRB".to_string()), Currency::CRBIT => Some("CRBIT".to_string()), Currency::CRW => Some("CRW".to_string()), Currency::CRYPT => Some("CRYPT".to_string()), Currency::CURE => Some("CURE".to_string()), Currency::CVC => Some("CVC".to_string()), Currency::DAR => Some("DAR".to_string()), Currency::DASH => Some("DASH".to_string()), Currency::DCR => Some("DCR".to_string()), Currency::DCT => Some("DCT".to_string()), Currency::DGB => Some("DGB".to_string()), Currency::DGC => Some("DGC".to_string()), Currency::DGD => Some("DGD".to_string()), Currency::DMD => Some("DMD".to_string()), Currency::DNT => Some("DNT".to_string()), Currency::DOGE => Some("DOGE".to_string()), Currency::DOPE => Some("DOPE".to_string()), Currency::DRACO => Some("DRACO".to_string()), Currency::DTB => Some("DTB".to_string()), Currency::DTC => Some("DTC".to_string()), Currency::DYN => Some("DYN".to_string()), Currency::EBST => Some("EBST".to_string()), Currency::EDG => Some("EDG".to_string()), Currency::EFL => Some("EFL".to_string()), Currency::EGC => Some("EGC".to_string()), Currency::EMC => Some("EMC".to_string()), Currency::EMC2 => Some("EMC2".to_string()), Currency::ENRG => Some("ENRG".to_string()), Currency::ERC => Some("ERC".to_string()), Currency::ETC => Some("ETC".to_string()), Currency::ETH => Some("ETH".to_string()), Currency::EXCL => Some("EXCL".to_string()), Currency::EXP => Some("EXP".to_string()), Currency::FAIR => Some("FAIR".to_string()), Currency::FC2 => Some("FC2".to_string()), Currency::FCT => Some("FCT".to_string()), Currency::FLDC => Some("FLDC".to_string()), Currency::FLO => Some("FLO".to_string()), Currency::FRK => Some("FRK".to_string()), Currency::FSC2 => Some("FSC2".to_string()), Currency::FTC => Some("FTC".to_string()), Currency::FUN => Some("FUN".to_string()), Currency::GAM => Some("GAM".to_string()), Currency::GAME => Some("GAME".to_string()), Currency::GBG => Some("GBG".to_string()), Currency::GBYTE => Some("GBYTE".to_string()), Currency::GCR => Some("GCR".to_string()), Currency::GEMZ => Some("GEMZ".to_string()), Currency::GEO => Some("GEO".to_string()), Currency::GHC => Some("GHC".to_string()), Currency::GLD => Some("GLD".to_string()), Currency::GNO => Some("GNO".to_string()), Currency::GNT => Some("GNT".to_string()), Currency::GOLOS => Some("GOLOS".to_string()), Currency::GP => Some("GP".to_string()), Currency::GRC => Some("GRC".to_string()), Currency::GRS => Some("GRS".to_string()), Currency::GRT => Some("GRT".to_string()), Currency::GUP => Some("GUP".to_string()), Currency::HKG => Some("HKG".to_string()), Currency::HMQ => Some("HMQ".to_string()), Currency::HYPER => Some("HYPER".to_string()), Currency::HZ => Some("HZ".to_string()), Currency::INCNT => Some("INCNT".to_string()), Currency::INFX => Some("INFX".to_string()), Currency::IOC => Some("IOC".to_string()), Currency::ION => Some("ION".to_string()), Currency::IOP => Some("IOP".to_string()), Currency::J => Some("J".to_string()), Currency::KMD => Some("KMD".to_string()), Currency::KORE => Some("KORE".to_string()), Currency::KR => Some("KR".to_string()), Currency::LBC => Some("LBC".to_string()), Currency::LGD => Some("LGD".to_string()), Currency::LMC => Some("LMC".to_string()), Currency::LSK => Some("LSK".to_string()), Currency::LTC => Some("LTC".to_string()), Currency::LUN => Some("LUN".to_string()), Currency::LXC => Some("LXC".to_string()), Currency::MAID => Some("MAID".to_string()), Currency::MANA => Some("MANA".to_string()), Currency::MAX => Some("MAX".to_string()), Currency::MCO => Some("MCO".to_string()), Currency::MEC => Some("MEC".to_string()), Currency::MEME => Some("MEME".to_string()), Currency::METAL => Some("METAL".to_string()), Currency::MLN => Some("MLN".to_string()), Currency::MND => Some("MND".to_string()), Currency::MONA => Some("MONA".to_string()), Currency::MTL => Some("MTL".to_string()), Currency::MTR => Some("MTR".to_string()), Currency::MUE => Some("MUE".to_string()), Currency::MUSIC => Some("MUSIC".to_string()), Currency::MYST => Some("MYST".to_string()), Currency::MZC => Some("MZC".to_string()), Currency::NAUT => Some("NAUT".to_string()), Currency::NAV => Some("NAV".to_string()), Currency::NBT => Some("NBT".to_string()), Currency::NEO => Some("NEO".to_string()), Currency::NEOS => Some("NEOS".to_string()), Currency::NET => Some("NET".to_string()), Currency::NEU => Some("NEU".to_string()), Currency::NLG => Some("NLG".to_string()), Currency::NMR => Some("NMR".to_string()), Currency::NTRN => Some("NTRN".to_string()), Currency::NXC => Some("NXC".to_string()), Currency::NXS => Some("NXS".to_string()), Currency::NXT => Some("NXT".to_string()), Currency::OC => Some("OC".to_string()), Currency::OK => Some("OK".to_string()), Currency::OMG => Some("OMG".to_string()), Currency::OMNI => Some("OMNI".to_string()), Currency::ORB => Some("ORB".to_string()), Currency::PART => Some("PART".to_string()), Currency::PAY => Some("PAY".to_string()), Currency::PDC => Some("PDC".to_string()), Currency::PINK => Some("PINK".to_string()), Currency::PIVX => Some("PIVX".to_string()), Currency::PKB => Some("PKB".to_string()), Currency::POT => Some("POT".to_string()), Currency::PPC => Some("PPC".to_string()), Currency::PRIME => Some("PRIME".to_string()), Currency::PTC => Some("PTC".to_string()), Currency::PTOY => Some("PTOY".to_string()), Currency::PXI => Some("PXI".to_string()), Currency::QRL => Some("QRL".to_string()), Currency::QTUM => Some("QTUM".to_string()), Currency::QWARK => Some("QWARK".to_string()), Currency::RADS => Some("RADS".to_string()), Currency::RBY => Some("RBY".to_string()), Currency::RDD => Some("RDD".to_string()), Currency::REP => Some("REP".to_string()), Currency::RISE => Some("RISE".to_string()), Currency::RLC => Some("RLC".to_string()), Currency::ROOT => Some("ROOT".to_string()), Currency::SAFEX => Some("SAFEX".to_string()), Currency::SALT => Some("SALT".to_string()), Currency::SBD => Some("SBD".to_string()), Currency::SC => Some("SC".to_string()), Currency::SCOT => Some("SCOT".to_string()), Currency::SCRT => Some("SCRT".to_string()), Currency::SEQ => Some("SEQ".to_string()), Currency::SFR => Some("SFR".to_string()), Currency::SHIFT => Some("SHIFT".to_string()), Currency::SIB => Some("SIB".to_string()), Currency::SLG => Some("SLG".to_string()), Currency::SLING => Some("SLING".to_string()), Currency::SLR => Some("SLR".to_string()), Currency::SLS => Some("SLS".to_string()), Currency::SNGLS => Some("SNGLS".to_string()), Currency::SNRG => Some("SNRG".to_string()), Currency::SNT => Some("SNT".to_string()), Currency::SOON => Some("SOON".to_string()), Currency::SPHR => Some("SPHR".to_string()), Currency::SPR => Some("SPR".to_string()), Currency::SPRTS => Some("SPRTS".to_string()), Currency::SSD => Some("SSD".to_string()), Currency::START => Some("START".to_string()), Currency::STEEM => Some("STEEM".to_string()), Currency::STEPS => Some("STEPS".to_string()), Currency::STORJ => Some("STORJ".to_string()), Currency::STRAT => Some("STRAT".to_string()), Currency::STV => Some("STV".to_string()), Currency::SWIFT => Some("SWIFT".to_string()), Currency::SWING => Some("SWING".to_string()), Currency::SWT => Some("SWT".to_string()), Currency::SYNX => Some("SYNX".to_string()), Currency::SYS => Some("SYS".to_string()), Currency::TES => Some("TES".to_string()), Currency::THC => Some("THC".to_string()), Currency::TIME => Some("TIME".to_string()), Currency::TIT => Some("TIT".to_string()), Currency::TIX => Some("TIX".to_string()), Currency::TKN => Some("TKN".to_string()), Currency::TKS => Some("TKS".to_string()), Currency::TRI => Some("TRI".to_string()), Currency::TRIG => Some("TRIG".to_string()), Currency::TRK => Some("TRK".to_string()), Currency::TROLL => Some("TROLL".to_string()), Currency::TRST => Some("TRST".to_string()), Currency::TRUST => Some("TRUST".to_string()), Currency::TX => Some("TX".to_string()), Currency::U => Some("U".to_string()), Currency::UBQ => Some("UBQ".to_string()), Currency::UFO => Some("UFO".to_string()), Currency::UNB => Some("UNB".to_string()), Currency::UNIQ => Some("UNIQ".to_string()), Currency::UNIT => Some("UNIT".to_string()), Currency::UNO => Some("UNO".to_string()), Currency::USDT => Some("USDT".to_string()), Currency::UTC => Some("UTC".to_string()), Currency::VIA => Some("VIA".to_string()), Currency::VIOR => Some("VIOR".to_string()), Currency::VIRAL => Some("VIRAL".to_string()), Currency::VOX => Some("VOX".to_string()), Currency::VPN => Some("VPN".to_string()), Currency::VRC => Some("VRC".to_string()), Currency::VRM => Some("VRM".to_string()), Currency::VTC => Some("VTC".to_string()), Currency::VTR => Some("VTR".to_string()), Currency::WARP => Some("WARP".to_string()), Currency::WAVES => Some("WAVES".to_string()), Currency::WINGS => Some("WINGS".to_string()), Currency::XAUR => Some("XAUR".to_string()), Currency::XBB => Some("XBB".to_string()), Currency::XC => Some("XC".to_string()), Currency::XCO => Some("XCO".to_string()), Currency::XCP => Some("XCP".to_string()), Currency::XDN => Some("XDN".to_string()), Currency::XDQ => Some("XDQ".to_string()), Currency::XEL => Some("XEL".to_string()), Currency::XEM => Some("XEM".to_string()), Currency::XLM => Some("XLM".to_string()), Currency::XMG => Some("XMG".to_string()), Currency::XMR => Some("XMR".to_string()), Currency::XMY => Some("XMY".to_string()), Currency::XPY => Some("XPY".to_string()), Currency::XQN => Some("XQN".to_string()), Currency::XRP => Some("XRP".to_string()), Currency::XSEED => Some("XSEED".to_string()), Currency::XST => Some("XST".to_string()), Currency::XTC => Some("XTC".to_string()), Currency::XVC => Some("XVC".to_string()), Currency::XVG => Some("XVG".to_string()), Currency::XWC => Some("XWC".to_string()), Currency::XZC => Some("XZC".to_string()), Currency::YBC => Some("YBC".to_string()), Currency::ZCL => Some("ZCL".to_string()), Currency::ZEC => Some("ZEC".to_string()), Currency::ZEN => Some("ZEN".to_string()), _ => None, } } ================================================ FILE: src/coinnect.rs ================================================ //! Use this module to create a generic API. use std::path::PathBuf; use crate::exchange::{Exchange, ExchangeApi}; use crate::bitstamp::{BitstampApi, BitstampCreds}; use crate::kraken::{KrakenApi, KrakenCreds}; use crate::poloniex::{PoloniexApi, PoloniexCreds}; use crate::bittrex::{BittrexApi, BittrexCreds}; use crate::gdax::{GdaxApi, GdaxCreds}; use crate::error::*; pub trait Credentials { /// Get an element from the credentials. fn get(&self, cred: &str) -> Option; /// Return the targeted `Exchange`. fn exchange(&self) -> Exchange; /// Return the client name. fn name(&self) -> String; } #[derive(Debug)] pub struct Coinnect; impl Coinnect { /// Create a new CoinnectApi by providing an API key & API secret pub fn new(exchange: Exchange, creds: C) -> Result> { match exchange { Exchange::Bitstamp => Ok(Box::new(BitstampApi::new(creds)?)), Exchange::Kraken => Ok(Box::new(KrakenApi::new(creds)?)), Exchange::Poloniex => Ok(Box::new(PoloniexApi::new(creds)?)), Exchange::Bittrex => Ok(Box::new(BittrexApi::new(creds)?)), Exchange::Gdax => Ok(Box::new(GdaxApi::new(creds)?)), } } /// Create a new CoinnectApi from a json configuration file. This file must follow this /// structure: /// /// For this example, you could use load your Bitstamp account with /// `new_from_file(Exchange::Bitstamp, "account_bitstamp", Path::new("/keys.json"))` pub fn new_from_file(exchange: Exchange, name: &str, path: PathBuf) -> Result> { match exchange { Exchange::Bitstamp => { Ok(Box::new(BitstampApi::new(BitstampCreds::new_from_file(name, path)?)?)) } Exchange::Kraken => { Ok(Box::new(KrakenApi::new(KrakenCreds::new_from_file(name, path)?)?)) } Exchange::Poloniex => { Ok(Box::new(PoloniexApi::new(PoloniexCreds::new_from_file(name, path)?)?)) } Exchange::Bittrex => { Ok(Box::new(BittrexApi::new(BittrexCreds::new_from_file(name, path)?)?)) } Exchange::Gdax => { Ok(Box::new(GdaxApi::new(GdaxCreds::new_from_file(name, path)?)?)) } } } } ================================================ FILE: src/error.rs ================================================ //! This module contains enum Error. //! Error type represents all possible errors that can occur when dealing //! with the generic or any dedicated-exchange API use serde_json; use hyper; use data_encoding; use crate::exchange::Exchange; error_chain!{ types { Error, ErrorKind, ResultExt, Result; } foreign_links { Json(serde_json::Error); ParseFloat(::std::num::ParseFloatError); ParseString(::std::string::FromUtf8Error); Hyper(hyper::Error); DataDecoding(data_encoding::DecodeError); Io(::std::io::Error); } errors { BadParse { description("ParsingError") display("The response could not be parsed.") } ServiceUnavailable(reason: String) { description("ServiceUnavailable") display("Host could not be reached: {}.", reason) } BadCredentials { description("BadCredentials") display("The informations provided do not allow authentication.") } RateLimitExceeded { description("RateLimitExceeded") display("API call rate limit exceeded.") } PairUnsupported { description("PairUnsupported") display("This pair is not supported.") } InvalidArguments { description("InvalidArguments") display("Arguments passed do not conform to the protocol.") } ExchangeSpecificError(reason: String) { description("ExchangeSpecificError") display("Exchange error: {}", reason) } TlsError { description("TlsError") display("Fail to initialize TLS client.") } InvalidFieldFormat(field: String) { description("InvalidFieldFormat") display("Fail to parse field \"{}\".", field) } InvalidFieldValue(field: String) { description("InvalidFieldValue") display("Invalid value for field \"{}\".", field) } MissingField(field: String) { description("MissingFiled") display("Missing field \"{}\".", field) } InsufficientFunds { description("InsufficientFunds") display("You haven't enough founds.") } InsufficientOrderSize { description("InsufficientOrderSize") display("Your order is not big enough.") } MissingPrice{ description("MissingPrice") display("No price specified.") } InvalidConfigType(expected: Exchange, find: Exchange){ description("InvalidConfigType") display("Invalid config: \nExpected: {:?}\nFind: {:?}", expected, find) } InvalidExchange(value: String) { description("InvalidExchange") display("Invalid exchange: \"{}\"", value) } InvalidNonce { description("InvalidNonce") display("Invalid nonce") } PermissionDenied { description("PermissionDenied") display("The operation cannot be done with the provided credentials") } } } ================================================ FILE: src/exchange.rs ================================================ //! This module contains Exchange enum. use std::fmt::Debug; use std::convert::Into; use std::str::FromStr; use crate::error::*; use crate::types::*; #[derive(Debug, PartialEq, Clone, Copy)] pub enum Exchange { Bitstamp, Kraken, Poloniex, Bittrex, Gdax, } impl Into for Exchange { fn into(self) -> String { match self { Exchange::Bitstamp => "Bitstamp".to_string(), Exchange::Kraken => "Kraken".to_string(), Exchange::Poloniex => "Poloniex".to_string(), Exchange::Bittrex => "Bittrex".to_string(), Exchange::Gdax => "Gdax".to_string(), } } } impl FromStr for Exchange { type Err = Error; fn from_str(input: &str) -> ::std::result::Result { match input.to_lowercase().as_str() { "bitstamp" => Ok(Exchange::Bitstamp), "kraken" => Ok(Exchange::Kraken), "poloniex" => Ok(Exchange::Poloniex), "bittrex" => Ok(Exchange::Bittrex), "gdax" => Ok(Exchange::Gdax), _ => Err(ErrorKind::InvalidExchange(input.to_string()).into()), } } } pub trait ExchangeApi: Debug { /// Return a Ticker for the Pair specified. fn ticker(&mut self, pair: Pair) -> Result; /// Return an Orderbook for the specified Pair. fn orderbook(&mut self, pair: Pair) -> Result; /// Place an order directly to the exchange. /// Quantity is in quote currency. So if you want to buy 1 Bitcoin for X€ (pair BTC_EUR), /// base currency (right member in the pair) is BTC and quote/counter currency is BTC (left /// member in the pair). /// So quantity = 1. /// /// A good practice is to store the return type (OrderInfo) somewhere since it can later be used /// to modify or cancel the order. fn add_order(&mut self, order_type: OrderType, pair: Pair, quantity: Volume, price: Option) -> Result; /// Retrieve the current amounts of all the currencies that the account holds /// The amounts returned are available (not used to open an order) fn balances(&mut self) -> Result; } ================================================ FILE: src/gdax/api.rs ================================================ //! Use this module to interact with Gdax exchange. //! Please see examples for more informations. use hyper_native_tls::NativeTlsClient; use hyper::Client; use hyper::header::{ContentType,UserAgent}; use hyper::net::HttpsConnector; use serde_json::Value; use serde_json::value::Map; use std::collections::HashMap; use std::io::Read; use std::thread; use std::time::Duration; use crate::coinnect::Credentials; use crate::exchange::Exchange; use crate::error::*; use crate::helpers; use crate::types::Pair; use crate::gdax::utils; use crate::types::*; header! { #[doc(hidden)] (KeyHeader, "Key") => [String] } header! { #[doc(hidden)] (SignHeader, "Sign") => [String] } header! { #[doc(hidden)] (ContentHeader, "Content-Type") => [String] } #[derive(Debug)] pub struct GdaxApi { last_request: i64, // unix timestamp in ms, to avoid ban api_key: String, api_secret: String, customer_id: String, http_client: Client, burst: bool, } impl GdaxApi { /// Create a new GdaxApi by providing an API key & API secret pub fn new(creds: C) -> Result { if creds.exchange() != Exchange::Gdax { return Err(ErrorKind::InvalidConfigType(Exchange::Gdax, creds.exchange()).into()); } //TODO: Handle correctly TLS errors with error_chain. let ssl = match NativeTlsClient::new() { Ok(res) => res, Err(_) => return Err(ErrorKind::TlsError.into()), }; let connector = HttpsConnector::new(ssl); Ok(GdaxApi { last_request: 0, api_key: creds.get("api_key").unwrap_or_default(), api_secret: creds.get("api_secret").unwrap_or_default(), customer_id: creds.get("customer_id").unwrap_or_default(), http_client: Client::with_connector(connector), burst: false, // No burst by default }) } /// The number of calls in a given period is limited. In order to avoid a ban we limit /// by default the number of api requests. /// This function sets or removes the limitation. /// Burst false implies no block. /// Burst true implies there is a control over the number of calls allowed to the exchange pub fn set_burst(&mut self, burst: bool) { self.burst = burst } fn block_or_continue(&self) { if ! self.burst { let threshold: u64 = 334; // 3 requests/sec = 1/3*1000 let offset: u64 = helpers::get_unix_timestamp_ms() as u64 - self.last_request as u64; if offset < threshold { let wait_ms = Duration::from_millis(threshold - offset); thread::sleep(wait_ms); } } } fn public_query(&mut self, params: &HashMap<&str, &str>) -> Result> { let method: &str = params .get("method") .ok_or_else(|| "Missing \"method\" field.")?; let pair: &str = params.get("pair").ok_or_else(|| "Missing \"pair\" field.")?; let url: String = utils::build_url(method, pair); self.block_or_continue(); let mut response = self.http_client .get(&url) .header(UserAgent("coinnect".to_string())) .send()?; self.last_request = helpers::get_unix_timestamp_ms(); let mut buffer = String::new(); response.read_to_string(&mut buffer)?; utils::deserialize_json(&buffer) } /// /// /// #Examples /// /// ```json /// extern crate coinnect; /// use coinnect::gdax::GdaxApi; /// let mut api = GdaxApi::new("", ""); /// let result = api.private_query("balance", "btcusd"); /// assert_eq!(true, true); /// ``` fn private_query(&mut self, params: &HashMap<&str, &str>) -> Result> { let method: &str = params .get("method") .ok_or_else(|| "Missing \"method\" field.")?; let pair: &str = params.get("pair").ok_or_else(|| "Missing \"pair\" field.")?; let url: String = utils::build_url(method, pair); let nonce = utils::generate_nonce(None); let signature = utils::build_signature(&nonce, &self.customer_id, &self.api_key, &self.api_secret)?; let copy_api_key = self.api_key.clone(); let mut post_params: &mut HashMap<&str, &str> = &mut HashMap::new(); post_params.insert("key", ©_api_key); post_params.insert("signature", &signature); post_params.insert("nonce", &nonce); // copy params into post_params .... bit of a hack but will do for now params.iter().for_each(|(k,v)| { post_params.insert(k,v); }); helpers::strip_empties(&mut post_params); let post_data = helpers::url_encode_hashmap(post_params); let mut response = self.http_client .post(&url) .header(ContentType::form_url_encoded()) .body(&post_data) .send()?; let mut buffer = String::new(); response.read_to_string(&mut buffer)?; utils::deserialize_json(&buffer) } /// Sample output : /// /// ```json /// { /// "BTC_LTC":{ /// "last":"0.0251","lowestAsk":"0.02589999","highestBid":"0.0251", /// "percentChange":"0.02390438","baseVolume":"6.16485315","quoteVolume":"245.82513926"}, /// "BTC_NXT":{ /// "last":"0.00005730","lowestAsk":"0.00005710","highestBid":"0.00004903", /// "percentChange":"0.16701570","baseVolume":"0.45347489","quoteVolume":"9094"}, /// ... } /// ``` pub fn return_ticker(&mut self, pair: Pair) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let mut params: HashMap<&str, &str> = HashMap::new(); params.insert("pair", pair_name); params.insert("method", "ticker"); self.public_query(¶ms) } /// Sample output : /// /// ```json /// {"asks":[[0.00007600,1164],[0.00007620,1300], ... ], "bids":[[0.00006901,200], /// [0.00006900,408], ... ], "timestamp": "1234567890"} /// ``` pub fn return_order_book(&mut self, pair: Pair) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let mut params: HashMap<&str, &str> = HashMap::new(); params.insert("method", "order_book"); params.insert("pair", pair_name); self.public_query(¶ms) } /// Sample output : /// /// ```json /// [{"date":"2014-02-10 04:23:23","type":"buy","rate":"0.00007600","amount":"140", /// "total":"0.01064"}, /// {"date":"2014-02-10 01:19:37","type":"buy","rate":"0.00007600","amount":"655", /// "total":"0.04978"}, ... ] /// ``` pub fn return_trade_history(&mut self, pair: Pair) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let mut params: HashMap<&str, &str> = HashMap::new(); params.insert("pair", pair_name); params.insert("method", "transactions"); self.public_query(¶ms) } /// Returns all of your available balances. /// /// Sample output: /// /// ```json /// {"BTC":"0.59098578","LTC":"3.31117268", ... } /// ``` pub fn return_balances(&mut self) -> Result> { let mut params = HashMap::new(); params.insert("method", "balance"); params.insert("pair", ""); self.private_query(¶ms) } /// Add a buy limit order to the exchange /// limit_price : If the order gets executed, a new sell order will be placed, /// with "limit_price" as its price. /// daily_order (Optional) : Opens buy limit order which will be canceled /// at 0:00 UTC unless it already has been executed. Possible value: True pub fn buy_limit(&mut self, pair: Pair, amount: Volume, price: Price, price_limit: Option, daily_order: Option) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let amount_string = amount.to_string(); let price_string = price.to_string(); let price_limit_string = match price_limit { Some(limit) => limit.to_string(), None => "".to_string(), }; let mut params = HashMap::new(); params.insert("method", "buy"); params.insert("pair", pair_name); params.insert("amount", &amount_string); params.insert("price", &price_string); params.insert("limit_price", &price_limit_string); if let Some(order) = daily_order { let daily_order_str = if order { "True" } else { "" }; // False is not a possible value params.insert("daily_order", daily_order_str); } self.private_query(¶ms) } /// Add a sell limit order to the exchange /// limit_price : If the order gets executed, a new sell order will be placed, /// with "limit_price" as its price. /// daily_order (Optional) : Opens sell limit order which will be canceled /// at 0:00 UTC unless it already has been executed. Possible value: True pub fn sell_limit(&mut self, pair: Pair, amount: Volume, price: Price, price_limit: Option, daily_order: Option) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let amount_string = amount.to_string(); let price_string = price.to_string(); let price_limit_string = match price_limit { Some(limit) => limit.to_string(), None => "".to_string(), }; let mut params = HashMap::new(); params.insert("method", "sell"); params.insert("pair", pair_name); params.insert("amount", &amount_string); params.insert("price", &price_string); params.insert("limit_price", &price_limit_string); if let Some(order) = daily_order { let daily_order_str = if order { "True" } else { "" }; // False is not a possible value params.insert("daily_order", daily_order_str); } self.private_query(¶ms) } /// Add a market buy order to the exchange /// By placing a market order you acknowledge that the execution of your order depends /// on the market conditions and that these conditions may be subject to sudden changes /// that cannot be foreseen. pub fn buy_market(&mut self, pair: Pair, amount: Volume) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let amount_string = amount.to_string(); let mut params = HashMap::new(); params.insert("method", "buy/market"); params.insert("pair", pair_name); params.insert("amount", &amount_string); self.private_query(¶ms) } /// Add a market sell order to the exchange /// By placing a market order you acknowledge that the execution of your order depends /// on the market conditions and that these conditions may be subject to sudden changes /// that cannot be foreseen. pub fn sell_market(&mut self, pair: Pair, amount: Volume) -> Result> { let pair_name = match utils::get_pair_string(&pair) { Some(name) => name, None => return Err(ErrorKind::PairUnsupported.into()), }; let amount_string = amount.to_string(); let mut params = HashMap::new(); params.insert("method", "sell/market"); params.insert("pair", pair_name); params.insert("amount", &amount_string); self.private_query(¶ms) } } #[cfg(test)] mod gdax_api_tests { use super::*; #[test] fn should_block_or_not_block_when_enabled_or_disabled() { let mut api = GdaxApi { last_request: helpers::get_unix_timestamp_ms(), api_key: "".to_string(), api_secret: "".to_string(), customer_id: "".to_string(), http_client: Client::new(), burst: false, }; let mut counter = 0; loop { api.set_burst(false); let start = helpers::get_unix_timestamp_ms(); api.block_or_continue(); api.last_request = helpers::get_unix_timestamp_ms(); let difference = api.last_request - start; assert!(difference >= 334); assert!(difference < 10000); api.set_burst(true); let start = helpers::get_unix_timestamp_ms(); api.block_or_continue(); api.last_request = helpers::get_unix_timestamp_ms(); let difference = api.last_request - start; assert!(difference < 10); counter = counter + 1; if counter >= 3 { break; } } } } ================================================ FILE: src/gdax/credentials.rs ================================================ //! Contains the Gdax credentials. use serde_json; use serde_json::Value; use crate::coinnect::Credentials; use crate::exchange::Exchange; use crate::helpers; use crate::error::*; use std::collections::HashMap; use std::str::FromStr; use std::fs::File; use std::io::Read; use std::path::PathBuf; #[derive(Debug, Clone)] pub struct GdaxCreds { exchange: Exchange, name: String, data: HashMap, } impl GdaxCreds { /// Create a new `GdaxCreds` from a json configuration file. This file must follow this /// structure: /// /// ```json /// { /// "account_gdax": { /// "exchange" : "gdax", /// "api_key" : "123456789ABCDEF", /// "api_secret": "ABC&EF?abcdef", /// "passphrase": "123456" /// }, /// "account_bitstamp": { /// "exchange" : "bitstamp", /// "api_key" : "1234567890ABCDEF1234567890ABCDEF", /// "api_secret" : "1234567890ABCDEF1234567890ABCDEF", /// "customer_id": "123456" /// } /// } /// ``` /// For this example, you could use load your Gdax account with /// `GdaxAPI::new(GdaxCreds::new_from_file("account_gdax", Path::new("/keys.json")))` pub fn new_from_file(name: &str, path: PathBuf) -> Result { let mut f = File::open(&path)?; let mut buffer = String::new(); f.read_to_string(&mut buffer)?; let data: Value = serde_json::from_str(&buffer)?; let json_obj = data.as_object() .ok_or_else(|| ErrorKind::BadParse)? .get(name) .ok_or_else(|| ErrorKind::MissingField(name.to_string()))?; let api_key = helpers::get_json_string(json_obj, "api_key")?; let api_secret = helpers::get_json_string(json_obj, "api_secret")?; let passphrase = helpers::get_json_string(json_obj, "passphrase")?; let exchange = { let exchange_str = helpers::get_json_string(json_obj, "exchange")?; Exchange::from_str(exchange_str) .chain_err(|| ErrorKind::InvalidFieldValue("exchange".to_string()))? }; if exchange != Exchange::Gdax { return Err(ErrorKind::InvalidConfigType(Exchange::Gdax, exchange).into()); } Ok(GdaxCreds::new(name, api_key, api_secret, passphrase)) } /// Create a new `GdaxCreds` from arguments. pub fn new(name: &str, api_key: &str, api_secret: &str, passphrase: &str) -> Self { let mut creds = GdaxCreds { data: HashMap::new(), exchange: Exchange::Gdax, name: if name.is_empty() { "GdaxClient".to_string() } else { name.to_string() }, }; //if api_key.is_empty() { //warning!("No API key set for the Gdax client"); //} creds .data .insert("api_key".to_string(), api_key.to_string()); //if api_secret.is_empty() { //warning!("No API secret set for the Gdax client"); //} creds .data .insert("api_secret".to_string(), api_secret.to_string()); //if api_secret.is_empty() { //warning!("No API customer ID set for the Gdax client"); //} creds .data .insert("passphrase".to_string(), passphrase.to_string()); creds } } impl Credentials for GdaxCreds { /// Return a value from the credentials. fn get(&self, key: &str) -> Option { if let Some(res) = self.data.get(key) { Some(res.clone()) } else { None } } fn name(&self) -> String { self.name.clone() } fn exchange(&self) -> Exchange { self.exchange } } ================================================ FILE: src/gdax/generic_api.rs ================================================ //! Use this module to interact with Gdax through a Generic API. //! This a more convenient and safe way to deal with the exchange since methods return a Result<> //! but this generic API does not provide all the functionnality that Gdax offers. use crate::exchange::ExchangeApi; use crate::gdax::api::GdaxApi; use crate::gdax::utils; use crate::error::*; use crate::types::*; use crate::helpers; impl ExchangeApi for GdaxApi { fn ticker(&mut self, pair: Pair) -> Result { let result = self.return_ticker(pair)?; let price = helpers::from_json_bigdecimal(&result["price"], "price")?; let ask = helpers::from_json_bigdecimal(&result["ask"], "ask")?; let bid = helpers::from_json_bigdecimal(&result["bid"], "bid")?; let vol = helpers::from_json_bigdecimal(&result["volume"], "volume")?; Ok(Ticker { timestamp: helpers::get_unix_timestamp_ms(), pair, last_trade_price: price, lowest_ask: ask, highest_bid: bid, volume: Some(vol), }) } fn orderbook(&mut self, pair: Pair) -> Result { let raw_response = self.return_order_book(pair)?; let result = utils::parse_result(&raw_response)?; let mut ask_offers = Vec::new(); let mut bid_offers = Vec::new(); let ask_array = result["asks"] .as_array() .ok_or_else(|| ErrorKind::InvalidFieldFormat(format!("{}", result["asks"])))?; let bid_array = result["bids"] .as_array() .ok_or_else(|| ErrorKind::InvalidFieldFormat(format!("{}", result["asks"])))?; for ask in ask_array { let price = helpers::from_json_bigdecimal(&ask[0], "ask price")?; let volume = helpers::from_json_bigdecimal(&ask[1], "ask volume")?; ask_offers.push((price, volume)); } for bid in bid_array { let price = helpers::from_json_bigdecimal(&bid[0], "bid price")?; let volume = helpers::from_json_bigdecimal(&bid[1], "bid volume")?; bid_offers.push((price, volume)); } Ok(Orderbook { timestamp: helpers::get_unix_timestamp_ms(), pair: pair, asks: ask_offers, bids: bid_offers, }) } fn add_order(&mut self, order_type: OrderType, pair: Pair, quantity: Volume, price: Option) -> Result { //let pair_name = match utils::get_pair_string(&pair) { //Some(name) => name, //None => return Err(ErrorKind::PairUnsupported.into()), //}; let result = match order_type { OrderType::BuyLimit => { if price.is_none() { return Err(ErrorKind::MissingPrice.into()); } // Unwrap safe here with the check above. self.buy_limit(pair, quantity, price.unwrap(), None, None) } OrderType::BuyMarket => self.buy_market(pair, quantity), OrderType::SellLimit => { if price.is_none() { return Err(ErrorKind::MissingPrice.into()); } // Unwrap safe here with the check above. self.sell_limit(pair, quantity, price.unwrap(), None, None) } OrderType::SellMarket => self.sell_market(pair, quantity), }; Ok(OrderInfo { timestamp: helpers::get_unix_timestamp_ms(), identifier: vec![result?["id"] .as_str() .ok_or_else(|| { ErrorKind::MissingField("id".to_string()) })? .to_string()], }) } /// Return the balances for each currency on the account fn balances(&mut self) -> Result { let raw_response = self.return_balances()?; let result = utils::parse_result(&raw_response)?; let mut balances = Balances::new(); for (key, val) in result.iter() { let currency = utils::get_currency_enum(key); match currency { Some(c) => { let amount = helpers::from_json_bigdecimal(&val, "amount")?; balances.insert(c, amount); }, _ => () } } Ok(balances) } } ================================================ FILE: src/gdax/mod.rs ================================================ //! Use this module to interact with Bitstamp exchange. pub mod api; pub mod generic_api; pub mod credentials; pub mod utils; pub use self::credentials::GdaxCreds; pub use self::api::GdaxApi; ================================================ FILE: src/gdax/utils.rs ================================================ use bidir_map::BidirMap; use hmac::{Hmac, Mac, NewMac}; use sha2::{Sha256}; use serde_json; use serde_json::Value; use serde_json::value::Map; use crate::error::*; use crate::helpers; use crate::types::Currency; use crate::types::Pair; use crate::types::Pair::*; type HmacSha256 = Hmac; lazy_static! { static ref PAIRS_STRING: BidirMap = { let mut m = BidirMap::new(); m.insert(BCH_USD, "bch-usd"); m.insert(LTC_EUR, "ltc-eur"); m.insert(LTC_USD, "ltc-usd"); m.insert(LTC_BTC, "ltc-btc"); m.insert(ETH_EUR, "eth-eur"); m.insert(ETH_USD, "eth-usd"); m.insert(ETH_BTC, "eth-btc"); m.insert(BTC_GBP, "btc-gbp"); m.insert(BTC_EUR, "btc-eur"); m.insert(BTC_USD, "btc-usd"); m }; } /// Return the name associated to pair used by Bitstamp /// If the Pair is not supported, None is returned. pub fn get_pair_string(pair: &Pair) -> Option<&&str> { PAIRS_STRING.get_by_first(pair) } /// Return the Pair enum associated to the string used by Bitstamp /// If the Pair is not supported, None is returned. pub fn get_pair_enum(pair: &str) -> Option<&Pair> { PAIRS_STRING.get_by_second(&pair) } pub fn build_signature(nonce: &str, passphrase: &str, api_key: &str, api_secret: &str) -> Result { const C: &'static [u8] = b"0123456789ABCDEF"; let message = nonce.to_owned() + passphrase + api_key; let mut mac = HmacSha256::new_from_slice(api_secret.as_bytes()).unwrap(); mac.update(message.as_bytes()); let result = mac.finalize(); let raw_signature = result.into_bytes(); let mut signature = Vec::with_capacity(raw_signature.len() * 2); for &byte in &raw_signature { signature.push(C[(byte >> 4) as usize]); signature.push(C[(byte & 0xf) as usize]); } // TODO: Handle correctly the from_utf8 errors with error_chain. Ok(String::from_utf8(signature)?) } pub fn build_url(method: &str, pair: &str) -> String { match method { "ticker" => "https://api.gdax.com/products/".to_string() + pair + "/ticker", "order_book" => "https://api.gdax.com/products/".to_string() + pair + "/book", "transactions" => "https://api.gdax.com/accounts/".to_string() + pair + "/ledger", _ => "not implemented yet".to_string(), } } pub fn deserialize_json(json_string: &str) -> Result> { let data: Value = match serde_json::from_str(json_string) { Ok(data) => data, Err(_) => return Err(ErrorKind::BadParse.into()), }; match data.as_object() { Some(value) => Ok(value.clone()), None => Err(ErrorKind::BadParse.into()), } } pub fn generate_nonce(fixed_nonce: Option) -> String { match fixed_nonce { Some(v) => v, None => helpers::get_unix_timestamp_ms().to_string(), } } /// If error array is null, return the result (encoded in a json object) /// else return the error string found in array pub fn parse_result(response: &Map) -> Result> { let error_msg = match response.get("error") { Some(error) => { error .as_str() .ok_or_else(|| ErrorKind::InvalidFieldFormat("error".to_string()))? } None => return Ok(response.clone()), }; match error_msg.as_ref() { "Invalid command." => Err(ErrorKind::InvalidArguments.into()), "Invalid API key/secret pair." => Err(ErrorKind::BadCredentials.into()), "Total must be at least 0.0001." => Err(ErrorKind::InsufficientOrderSize.into()), other => Err(ErrorKind::ExchangeSpecificError(other.to_string()).into()), } } /// Return the currency enum associated with the /// string used by Bitstamp. If no currency is found, /// return None /// # Examples /// /// ``` /// use coinnect::gdax::utils::get_currency_enum; /// use coinnect::types::Currency; /// /// let currency = get_currency_enum("usd_balance"); /// assert_eq!(Some(Currency::USD), currency); /// ``` pub fn get_currency_enum(currency: &str) -> Option { match currency { "btc_balance" => Some(Currency::BTC), "eur_balance" => Some(Currency::EUR), "ltc_balance" => Some(Currency::LTC), "gbp_balance" => Some(Currency::GBP), "usd_balance" => Some(Currency::USD), "eth_balance" => Some(Currency::ETH), "bch_balance" => Some(Currency::BCH), _ => None, } } /// Return the currency string associated with the /// enum used by Gdax. If no currency is found, /// return None /// # Examples /// /// ``` /// use coinnect::gdax::utils::get_currency_string; /// use coinnect::types::Currency; /// /// let currency = get_currency_string(Currency::USD); /// assert_eq!(currency, Some("USD".to_string())); /// ``` pub fn get_currency_string(currency: Currency) -> Option { match currency { Currency::BTC => Some("BTC".to_string()), Currency::EUR => Some("EUR".to_string()), Currency::LTC => Some("LTC".to_string()), Currency::GBP => Some("GBP".to_string()), Currency::USD => Some("USD".to_string()), Currency::ETH => Some("ETH".to_string()), Currency::BCH => Some("BCH".to_string()), _ => None, } } ================================================ FILE: src/helpers/mod.rs ================================================ use serde_json::Value; use crate::error::*; use bigdecimal::BigDecimal; use std::str::FromStr; use std::collections::HashMap; use chrono::prelude::*; // Helper functions pub fn url_encode_hashmap(hashmap: &HashMap<&str, &str>) -> String { if hashmap.is_empty() { return "".to_string(); } let mut acc = "".to_string(); for (name, param) in hashmap { acc += &(name.to_string() + "=" + param + "&"); } acc.pop(); // remove the last "&" acc } pub fn get_unix_timestamp_ms() -> i64 { let now = Utc::now(); let seconds: i64 = now.timestamp(); let nanoseconds: i64 = now.nanosecond() as i64; (seconds * 1000) + (nanoseconds / 1000 / 1000) } pub fn get_unix_timestamp_us() -> i64 { let now = Utc::now(); let seconds: i64 = now.timestamp(); let nanoseconds: i64 = now.nanosecond() as i64; (seconds * 1000 * 1000) + (nanoseconds / 1000) } pub fn strip_empties(x: &mut HashMap<&str, &str>) { let empties: Vec<_> = x.iter() .filter(|&(_, &v)| v.is_empty()) .map(|(k, _)| (*k).clone()) .collect(); for empty in empties { x.remove(&empty); } } pub fn get_json_string<'a>(json_obj: &'a Value, key: &str) -> Result<&'a str> { Ok(json_obj .get(key) .ok_or_else(|| ErrorKind::MissingField(key.to_string()))? .as_str() .ok_or_else(|| ErrorKind::InvalidFieldFormat(key.to_string()))?) } pub fn from_json_bigdecimal(json_obj: &Value, key: &str) -> Result { let num = json_obj .as_str() .ok_or_else(|| ErrorKind::MissingField(key.to_string()))?; Ok(BigDecimal::from_str(num).chain_err(|| ErrorKind::InvalidFieldFormat(key.to_string()))?) } ================================================ FILE: src/kraken/api.rs ================================================ //! Use this module to interact with the raw-original API provided by Kraken. //! It is recommended to use a nonce window setting of 5000 for your API key when sending requests in quick succession in order to avoid nonce errors. //! WARNING: Special attention should be paid to error management: parsing number, etc. use hmac::{Hmac, Mac, NewMac}; use sha2::{Sha256, Sha512, Digest}; use hyper_native_tls::NativeTlsClient; use hyper::Client; use hyper::header; use hyper::net::HttpsConnector; use data_encoding::BASE64; use serde_json::Value; use serde_json::value::Map; use std::collections::HashMap; use std::io::Read; use std::thread; use std::time::Duration; use std::str; use crate::error::*; use crate::helpers; use crate::exchange::Exchange; use crate::coinnect::Credentials; use crate::kraken::utils; header! { #[doc(hidden)] (KeyHeader, "API-Key") => [String] } header! { #[doc(hidden)] (SignHeader, "API-Sign") => [String] } #[derive(Debug)] pub struct KrakenApi { last_request: i64, // unix timestamp in ms, to avoid ban api_key: String, api_secret: String, otp: Option, // two-factor password (if two-factor enabled, otherwise not required) http_client: Client, burst: bool, } impl KrakenApi { /// Create a new KrakenApi by providing an API key & API secret pub fn new(creds: C) -> Result { if creds.exchange() != Exchange::Kraken { return Err(ErrorKind::InvalidConfigType(Exchange::Kraken, creds.exchange()).into()); } // TODO: implement correctly the TLS error in error_chain. let ssl = match NativeTlsClient::new() { Ok(res) => res, Err(_) => return Err(ErrorKind::TlsError.into()), }; let connector = HttpsConnector::new(ssl); Ok(KrakenApi { last_request: 0, api_key: creds.get("api_key").unwrap_or_default(), api_secret: creds.get("api_secret").unwrap_or_default(), otp: None, http_client: Client::with_connector(connector), burst: false, }) } /// Use to provide your two-factor password (if two-factor enabled, otherwise not required) pub fn set_two_pass_auth(&mut self, otp: String) { self.otp = Some(otp); } /// The number of calls in a given period is limited. In order to avoid a ban we limit /// by default the number of api requests. /// This function sets or removes the limitation. /// Burst false implies no block. /// Burst true implies there is a control over the number of calls allowed to the exchange pub fn set_burst(&mut self, burst: bool) { self.burst = burst } pub fn block_or_continue(&self) { if ! self.burst { let threshold: u64 = 2000; // 1 request/2sec let offset: u64 = helpers::get_unix_timestamp_ms() as u64 - self.last_request as u64; if offset < threshold { let wait_ms = Duration::from_millis(threshold - offset); thread::sleep(wait_ms); } } } fn public_query(&mut self, method: &str, params: &mut HashMap<&str, &str>) -> Result> { helpers::strip_empties(params); let url = "https://api.kraken.com/0/public/".to_string() + method + "?" + &helpers::url_encode_hashmap(params); self.block_or_continue(); //TODO: Handle correctly http errors with error_chain. let mut response = match self.http_client.get(&url).send() { Ok(response) => response, Err(err) => return Err(ErrorKind::ServiceUnavailable(err.to_string()).into()), }; self.last_request = helpers::get_unix_timestamp_ms(); let mut buffer = String::new(); response.read_to_string(&mut buffer)?; utils::deserialize_json(&buffer) } fn private_query(&mut self, method: &str, mut params: &mut HashMap<&str, &str>) -> Result> { let url = "https://api.kraken.com/0/private/".to_string() + method; let urlpath = "/0/private/".to_string() + method; let nonce = helpers::get_unix_timestamp_ms().to_string(); helpers::strip_empties(&mut params); let mut params = params.clone(); // TODO: Remove .clone() params.insert("nonce", &nonce); if let Some(ref password) = self.otp { params.insert("otp", password); } let postdata = helpers::url_encode_hashmap(¶ms); let signature = self.create_signature(urlpath, &postdata, &nonce)?; let mut custom_header = header::Headers::new(); custom_header.set(KeyHeader(self.api_key.clone())); custom_header.set(SignHeader(signature)); let mut res = match self.http_client .post(&url) .body(&postdata) .headers(custom_header) .send() { Ok(res) => res, Err(err) => return Err(ErrorKind::ServiceUnavailable(err.to_string()).into()), }; let mut buffer = String::new(); res.read_to_string(&mut buffer)?; utils::deserialize_json(&buffer) } fn create_signature(&self, urlpath: String, postdata: &str, nonce: &str) -> Result { let message_presha256 = nonce.to_string() + postdata; let mut sha256 = Sha256::default(); sha256.update(&message_presha256.as_bytes()); let output = sha256.finalize(); let mut concatenated = urlpath.as_bytes().to_vec(); for elem in output { concatenated.push(elem); } let hmac_key = BASE64.decode(self.api_secret.as_bytes())?; let mut mac = Hmac::::new_from_slice(&hmac_key[..]).unwrap(); mac.update(&concatenated); Ok(BASE64.encode(&mac.finalize().into_bytes())) } /// Result: Server's time /// /// ```json /// unixtime = as unix timestamp /// rfc1123 = as RFC 1123 time format /// ``` /// Note: This is to aid in approximating the skew time between the server and client. pub fn get_server_time(&mut self) -> Result> { let mut params = HashMap::new(); self.public_query("Time", &mut params) } /// Input: /// /// ```json /// info = info to retrieve (optional): /// info = all info (default) /// aclass = asset class (optional): /// currency (default) /// asset = comma delimited list of assets to get info on (optional. default = all for /// given asset class) /// ``` /// Result: array of asset names and their info: /// /// ```json /// = asset name /// altname = alternate name /// aclass = asset class /// decimals = scaling decimal places for record keeping /// display_decimals = scaling decimal places for output display /// ``` pub fn get_asset_info(&mut self, info: &str, aclass: &str, asset: &str) -> Result> { let mut params = HashMap::new(); params.insert("info", info); params.insert("aclass", aclass); params.insert("asset", asset); self.public_query("Assets", &mut params) } /// Input: /// /// ```json /// info = info to retrieve (optional): /// info = all info (default) /// leverage = leverage info /// fees = fees schedule /// margin = margin info /// pair = comma delimited list of asset pairs to get info on (optional. default = all) /// ``` /// /// Result: array of pair names and their info /// /// ```json /// = pair name /// altname = alternate pair name /// aclass_base = asset class of base component /// base = asset id of base component /// aclass_quote = asset class of quote component /// quote = asset id of quote component /// lot = volume lot size /// pair_decimals = scaling decimal places for pair /// lot_decimals = scaling decimal places for volume /// lot_multiplier = amount to multiply lot volume by to get currency volume /// leverage_buy = array of leverage amounts available when buying /// leverage_sell = array of leverage amounts available when selling /// fees = fee schedule array in [volume, percent fee] tuples /// fees_maker = maker fee schedule array in [volume, percent fee] tuples (if on /// maker/taker) /// fee_volume_currency = volume discount currency /// margin_call = margin call level /// margin_stop = stop-out/liquidation margin level /// ``` pub fn get_tradable_asset_pairs(&mut self, info: &str, pair: &str) -> Result> { let mut params = HashMap::new(); params.insert("info", info); params.insert("pair", pair); self.public_query("AssetPairs", &mut params) } /// Input: /// /// ```json /// pair = comma delimited list of asset pairs to get info on /// ``` /// /// Result: array of pair names and their ticker info /// /// ```json /// = pair name /// a = ask array(, , ), /// b = bid array(, , ), /// c = last trade closed array(, ), /// v = volume array(, ), /// p = volume weighted average price array(, ), /// t = number of trades array(, ), /// l = low array(, ), /// h = high array(, ), /// o = today's opening price /// ``` pub fn get_ticker_information(&mut self, pair: &str) -> Result> { let mut params = HashMap::new(); params.insert("pair", pair); self.public_query("Ticker", &mut params) } /// Input: /// /// ```json /// pair = asset pair to get OHLC data for /// interval = time frame interval in minutes (optional): /// 1 (default), 5, 15, 30, 60, 240, 1440, 10080, 21600 /// since = return committed OHLC data since given id (optional. exclusive) /// ``` /// /// Result: array of pair name and OHLC data /// /// ```json /// = pair name /// array of array entries(