diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..c1eeb82 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,28 @@ +version: 2 +jobs: + build: + docker: + - image: mmquant/bfx-cpp-api-circleci-alpine:0.0.1 + steps: + + - restore_cache: + keys: + - source-{{ .Branch }}-{{ .Revision }} + - source-{{ .Branch }}- + + - checkout + + - save_cache: + key: source-{{ .Branch }}-{{ .Revision }} + paths: + - ".git" + + - run: + name: Build project + command: | + cd ~/project/app/build && cmake .. && make + + - run: + name : Test + command: | + cd ~/project/app/bin && ./test diff --git a/.circleci/images/Dockerfile b/.circleci/images/Dockerfile new file mode 100644 index 0000000..e496c60 --- /dev/null +++ b/.circleci/images/Dockerfile @@ -0,0 +1,16 @@ +FROM alpine:edge as runtime + +# Environment variables +ENV ESSENTIAL_PACKAGES="cmake clang curl curl-dev gcc g++ git gzip make mlocate openssh py-pip tar supervisor" \ + UTILITY_PACKAGES="nano vim ca-certificates" + +# Configure essential and utility packages +RUN apk update && \ + apk --no-cache --progress add $ESSENTIAL_PACKAGES $UTILITY_PACKAGES && \ + pip install --upgrade pip && \ + pip install supervisor-stdout + +# compiling https://github.com/weidai11/cryptopp.git +RUN cd /tmp && \ + git clone https://github.com/weidai11/cryptopp.git && \ + cd cryptopp && make && make install diff --git a/.circleci/run-build-locally.sh b/.circleci/run-build-locally.sh new file mode 100644 index 0000000..67b5b59 --- /dev/null +++ b/.circleci/run-build-locally.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# This script is used to run CircleCI build on local machine without pushing through remote repo. + +curl --user ${CIRCLE_TOKEN}: \ + --request POST \ + --form revision=$1 \ + --form config=@config.yml \ + --form notify=false \ +https://circleci.com/api/v1.1/project/github/MMquant/bfx-cpp-api/tree/develop diff --git a/.gitignore b/.gitignore index 0b7964c..822ea4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store +app/bin/ DerivedData/ bfx-cpp-api.xcodeproj/ include/.DS_Store include/rapidjson/.DS_Store -key-secret +app/doc/key-secret +app/.ash_history diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9ba5adf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,43 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Stage 1 - Installing essential and utility pkgs. +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +FROM alpine:edge as runtime + +# Environment variables +ENV ESSENTIAL_PACKAGES="cmake clang curl curl-dev gcc g++ git gzip make mlocate openssh py-pip tar supervisor" \ + UTILITY_PACKAGES="nano vim ca-certificates" + +# Configure essential and utility packages +RUN apk update && \ + apk --no-cache --progress add $ESSENTIAL_PACKAGES $UTILITY_PACKAGES && \ + pip install --upgrade pip && \ + pip install supervisor-stdout + +# compiling https://github.com/weidai11/cryptopp.git +RUN cd /tmp && \ + git clone https://github.com/weidai11/cryptopp.git && \ + cd cryptopp && make && make install + +RUN mkdir -p /home/bfx-cpp-api + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Stage 2 - Applying needed configurations. +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +COPY ./docker/etc /etc + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Stage 3 - Adding project files into VM. +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# Adding project folder and needed files +ADD ./app /home/bfx-cpp-api/app +RUN chmod -R a+w /home/bfx-cpp-api/app + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Stage 4 - Adding entry point. +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# Adding entry point +ADD ./docker/entrypoint.sh /sbin/entrypoint.sh diff --git a/README.md b/README.md index 68ca8af..de5f336 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,148 @@ -# bfx-cpp-api - -_C++ Bitfinex REST API client_ +![bfx-cpp-api logo](app/doc/logo/bfx-cpp-api_logo.png) *** -### Synopsis +### Build status -This header-only library contains class for interfacing Bitfinex REST API v1 +| [Linux][lin-link] | +| :---------------: | +| ![lin-badge] | -### Installation +[lin-badge]: https://circleci.com/gh/MMquant/bfx-cpp-api/tree/master.svg?style=svg "CircleCI build status" +[lin-link]: https://circleci.com/gh/MMquant/bfx-cpp-api "CircleCI build status" -Just copy content of `include/` directory into your project's `include/` directory and -add `#include "BitfinexAPI.hpp"` to your `.cpp` file. +### Notice -### Usage +***Master*** branch contains new version of the client. For old version checkout ***legacy*** branch. -See self-explanatory `example.cpp` for general usage. +### Synopsis + +This header-only library contains class for interfacing Bitfinex REST API v1. Current version supports response JSON +schema validation. ### Dependencies -*bfx-cpp-api* depends on following external libraries +*bfx-cpp-api* depends on following external libraries/packages + +* *cmake* - [https://cmake.org/download/](https://cmake.org/download/) +* *libcrypto++* - [https://www.cryptopp.com/](https://www.cryptopp.com/) +* *libcrypto++-dev* - [https://www.cryptopp.com/](https://www.cryptopp.com/) +* *curl* - [https://curl.haxx.se/download.html](https://curl.haxx.se/download.html) +* *libcurl4-gnutls-dev* or *libcurl4-openssl-dev* - [https://curl.haxx.se/download.html](https://curl.haxx.se/download.html) +* *gcc* > 7.2 or C++14 compatible *clang* + +### How to Build'n'Run `src/example.cpp` + +1. Install dependencies (via apt, homebrew etc.). +2. Clone or download *bfx-api-cpp* repository. +3. Add `key-secret` file in `bfx-api-cpp/app/doc` directory. (or edit `example.cpp` so that it doesn't use `key-secret` file) +4. Peek into self-documented `/app/src/example.cpp`. +5. Build `example` binary + +```BASH +cd app/build && cmake .. && make +``` + +7. Run `example` binary from `app/bin` + +```BASH +./example +``` + +### How to Build'n'Run `src/example.cpp` in Docker container + +1. Clone or download *bfx-api-cpp* repository. +2. Build docker image + +```BASH +cd +docker-compose build +``` + +3. Start docker image + +```BASH +docker-compose up & +``` + +4. Spawn bash + +```BASH +docker exec -it bfx-cpp-api_dev_1 /bin/sh +``` + +5. Add `key-secret` file in `/home/bfx-cpp-api/app/doc` directory. (or edit `example.cpp` so that it doesn't use `key-secret` file) + +```BASH +cd /home/bfx-cpp-api/app/doc +echo > key-secret +echo >> key-secret +``` + +6. Build example + +```BASH +cd /home/bfx-cpp-api/app/build +cmake .. +make +``` + +7. Run `example` binary + +```BASH +cd /home/bfx-cpp-api/app/bin +./example +``` + +### Quick interface overview + +```C++ +// Create API client for both authenticated and unauthenticated requests +BfxAPI::BitfinexAPI bfxAPI("accessKey", "secretKey"); + +// Create API client for just unauthenticated requests +BfxAPI::BitfinexAPI bfxAPI(); + +// Fetch data +bfxAPI.getTicker("btcusd"); + +// Check for errors +if (!bfxAPI.hasApiError()) +{ + // Get response in string + cout << bfxAPI.strResponse() << endl; +} +else +{ + // Inspect errors + cout << bfxAPI.getBfxApiStatusCode() << endl; + cout << bfxAPI.getCurlStatusCode() << endl; +} +``` + +See self-explanatory `src/example.cpp` for general usage and more requests. + +### Change Log + +- 2018-09-26 Using the small Docker image Alpine instead of Debian. +- 2018-09-26 Using docker-compose to build/up/down the image. +- 2018-09-26 Grouping project files inside the app folder. +- 2018-08-01 Dockerfile added. CircleCI added. +- 2018-07-24 CMakeLists.txt added. Installation instructions changed. +- 2018-07-11 Schema validation logic complete. Client currently validates public requests only. + +### Known issues + +You will not be able to compile *bfx-cpp-api* with GCC<7.2 due to this [bug](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66297). +You can use CLANG to avoid GCC bug. -* libcryptopp - [https://www.cryptopp.com/](https://www.cryptopp.com/) -* libcurl - [https://curl.haxx.se/download.html](https://curl.haxx.se/download.html) +### Contribution -### ToDo +1. Fork `bfx-cpp-api` repository. +2. Commit to your ***develop*** branch. +3. Create pull request from your ***develop*** branch to ***origin/develop*** branch. -- [ ] Integrating RapidJSON -- [ ] Unit tests -- [ ] JSON Scheme validation +No direct pull request to ***master*** branch accepted! ### Author diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..1a203d1 --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,55 @@ +################################################################################ + +cmake_minimum_required (VERSION 3.7) +project (example) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/bin) + +################################################################################ + +# TARGET rapidjson +add_library(rapidjson INTERFACE) +target_include_directories(rapidjson INTERFACE "include/rapidjson") + +################################################################################ + +# TARGET bfxapicpp +add_library(bfxapicpp INTERFACE) +target_include_directories(bfxapicpp INTERFACE "include/bfx-api-cpp") +target_link_libraries(bfxapicpp INTERFACE rapidjson) + +################################################################################ + +# TARGET example +add_executable (example src/example.cpp) +target_include_directories (example PRIVATE include) +target_link_libraries(example +PUBLIC bfxapicpp +PRIVATE -lcryptopp -lcurl) +# Assuming example executable built into /bin directory configuration files +# will have following paths +target_compile_definitions(example PUBLIC +JSON_DEFINITIONS_FILE_PATH="${PROJECT_SOURCE_DIR}/doc/definitions.json" +WITHDRAWAL_CONF_FILE_PATH="${PROJECT_SOURCE_DIR}/doc/withdraw.conf") +# Enable all compiler warnings +target_compile_options(example PRIVATE -Wall) + +################################################################################ + +# TARGET test +add_executable (test src/test.cpp) +target_include_directories (test PRIVATE include) +target_link_libraries(test +PUBLIC bfxapicpp +PRIVATE -lcryptopp -lcurl) +# Assuming test executable built into /bin directory configuration files +# will have following paths +target_compile_definitions(test PUBLIC +JSON_DEFINITIONS_FILE_PATH="${PROJECT_SOURCE_DIR}/doc/definitions.json" +WITHDRAWAL_CONF_FILE_PATH="${PROJECT_SOURCE_DIR}/doc/withdraw.conf") +# Enable all compiler warnings +target_compile_options(test PRIVATE -Wall) diff --git a/app/build/.gitignore b/app/build/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/app/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/app/doc/definitions.json b/app/doc/definitions.json new file mode 100644 index 0000000..4f069e5 --- /dev/null +++ b/app/doc/definitions.json @@ -0,0 +1,306 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "flatJsonSchema": { + "description": "Flat JSON string array scheme used in jsonutils::jsonStrToUset", + "type": "array", + "items": { + "type": "string" + } + }, + "pubticker": { + "description": "JSON schema used in https://api.bitfinex.com/v1/pubticker/ endpoint", + "type": "object", + "properties": { + "mid": { + "type": "string" + }, + "bid": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "last_price": { + "type": "string" + }, + "low": { + "type": "string" + }, + "high": { + "type": "string" + }, + "volume": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "mid", + "bid", + "ask", + "last_price", + "low", + "high", + "volume", + "timestamp" + ] + }, + "stats": { + "description": "JSON schema used in https://api.bitfinex.com/v1/stats/ endpoint", + "type": "array", + "items": { + "type": "object", + "properties": { + "period": { + "type": "integer" + }, + "volume": { + "type": "string" + } + }, + "required": [ + "period", + "volume" + ] + } + }, + "lendbook": { + "description": "JSON schema used in https://api.bitfinex.com/v1/lendbook/ endpoint", + "type": "object", + "properties": { + "bids": { + "type": "array", + "items": { + "type": "object", + "properties": { + "rate": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "period": { + "type": "integer" + }, + "timestamp": { + "type": "string" + }, + "frr": { + "type": "string" + } + }, + "required": [ + "rate", + "amount", + "period", + "timestamp", + "frr" + ] + } + }, + "asks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "rate": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "period": { + "type": "integer" + }, + "timestamp": { + "type": "string" + }, + "frr": { + "type": "string" + } + }, + "required": [ + "rate", + "amount", + "period", + "timestamp", + "frr" + ] + } + } + }, + "required": [ + "bids", + "asks" + ] + }, + "book": { + "description": "JSON schema used in https://api.bitfinex.com/v1/book/ endpoint", + "type": "object", + "properties": { + "bids": { + "type": "array", + "items": { + "type": "object", + "properties": { + "price": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "price", + "amount", + "timestamp" + ] + } + }, + "asks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "price": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "timestamp": { + "type": "string" + } + }, + "required": [ + "price", + "amount", + "timestamp" + ] + } + } + }, + "required": [ + "bids", + "asks" + ] + }, + "trades": { + "description": "JSON schema used in https://api.bitfinex.com/v1/trades/ endpoint", + "type": "array", + "items": { + "type": "object", + "properties": { + "timestamp": { + "type": "integer" + }, + "tid": { + "type": "integer" + }, + "price": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "exchange": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "timestamp", + "tid", + "price", + "amount", + "exchange", + "type" + ] + } + }, + "lends": { + "description": "JSON schema used in https://api.bitfinex.com/v1/lends/ endpoint", + "type": "array", + "items": { + "type": "object", + "properties": { + "rate": { + "type": "string" + }, + "amount_lent": { + "type": "string" + }, + "amount_used": { + "type": "string" + }, + "timestamp": { + "type": "integer" + } + }, + "required": [ + "rate", + "amount_lent", + "amount_used", + "timestamp" + ] + } + }, + "symbols": { + "description": "JSON schema used in https://api.bitfinex.com/v1/symbols/ endpoint", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": false + }, + "symbols_details": { + "description": "JSON schema used in https://api.bitfinex.com/v1/symbols_details/ endpoint", + "type": "array", + "items": { + "type": "object", + "properties": { + "pair": { + "type": "string" + }, + "price_precision": { + "type": "integer" + }, + "initial_margin": { + "type": "string" + }, + "minimum_margin": { + "type": "string" + }, + "maximum_order_size": { + "type": "string" + }, + "minimum_order_size": { + "type": "string" + }, + "expiration": { + "type": "string" + }, + "margin": { + "type": "boolean" + } + }, + "required": [ + "pair", + "price_precision", + "initial_margin", + "minimum_margin", + "maximum_order_size", + "minimum_order_size", + "expiration", + "margin" + ] + } + } +} \ No newline at end of file diff --git a/app/doc/logo/bfx-cpp-api_logo.png b/app/doc/logo/bfx-cpp-api_logo.png new file mode 100644 index 0000000..cdcb7d5 Binary files /dev/null and b/app/doc/logo/bfx-cpp-api_logo.png differ diff --git a/doc/withdraw.conf b/app/doc/withdraw.conf similarity index 93% rename from doc/withdraw.conf rename to app/doc/withdraw.conf index 7c16401..999ae28 100644 --- a/doc/withdraw.conf +++ b/app/doc/withdraw.conf @@ -1,5 +1,4 @@ /////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////// // // CONFIGURATION FILE FOR Withdraw() method // @@ -7,7 +6,6 @@ // if you don't wish to receive confirmation emails. // /////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////// // string, REQUIRED for wire and crypto // Can be one of the following ["bitcoin", "litecoin", "ethereum", "ethereumc", "mastercoin", "zcash", "monero", "wire"]. diff --git a/app/include/bfx-api-cpp/BitfinexAPI.hpp b/app/include/bfx-api-cpp/BitfinexAPI.hpp new file mode 100644 index 0000000..4233539 --- /dev/null +++ b/app/include/bfx-api-cpp/BitfinexAPI.hpp @@ -0,0 +1,1008 @@ +//////////////////////////////////////////////////////////////////////////////// +// BitfinexAPI.hpp +// +// +// Bitfinex REST API C++ client +// +// +// Copyright (C) 2018 Petr Javorik maple@mmquant.net +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +// std +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// internal jsonutils +#include "jsonutils.hpp" + +// internal error +#include "error.hpp" + +// internal HTTPRequest +#include "HTTPRequest.hpp" + +// namespaces +using std::cerr; +using std::cout; +using std::endl; +using std::string; +using std::to_string; +using std::unordered_set; +using std::vector; + +namespace BfxAPI +{ + + class BitfinexAPI + { + + //////////////////////////////////////////////////////////////////////// + // Class constants + //////////////////////////////////////////////////////////////////////// + + static constexpr auto API_URL = "https://api.bitfinex.com/v1"; + #ifndef WITHDRAWAL_CONF_FILE_PATH + static constexpr auto WITHDRAWAL_CONF_FILE_PATH = "withdraw.conf"; + #endif + + //////////////////////////////////////////////////////////////////////// + // Typedefs + //////////////////////////////////////////////////////////////////////// + + // Structure for multiple new orders endpoint + struct sOrder + { + string symbol; + double amount; + double price; + string side; + string type; + }; + using vOrders = vector; + using vIds = vector; + + public: + + //////////////////////////////////////////////////////////////////////// + // Constructor - Destructor + //////////////////////////////////////////////////////////////////////// + + explicit BitfinexAPI():BitfinexAPI("", "") {} + + explicit BitfinexAPI(const string &accessKey, const string &secretKey): + WDconfFilePath_(WITHDRAWAL_CONF_FILE_PATH), + Request(API_URL), + bfxApiStatusCode_(noError) + { + // Internal HTTPRequest set Keys + Request.setAccessKey(accessKey); + Request.setSecretKey(secretKey); + + // populate _symbols directly from Bitfinex getSymbols endpoint + jsonutils::jsonStrToUset(symbols_, getSymbols().strResponse()); + + currencies_ = + { + "BTG", + "DSH", + "ETC", + "ETP", + "EUR", + "GBP", + "IOT", + "JPY", + "LTC", + "NEO", + "OMG", + "SAN", + "USD", + "XMR", + "XRP", + "ZEC" + }; + + schemaValidator_ = jsonutils::BfxSchemaValidator(symbols_, currencies_); + + // As found on + // https://bitfinex.readme.io/v1/reference#rest-auth-deposit + methods_ = + { + "bcash" + "bitcoin", + "ethereum", + "ethereumc", + "ethereumc", + "litecoin", + "mastercoin", + "monero", + "tetheruso", + "zcash", + }; + + walletNames_ = + { + "trading", "exchange", "deposit" + }; + + // New order endpoint "type" parameter + types_ = + { + "market", + "limit", + "stop", + "trailing-stop", + "fill-or-kill", + "exchange market", + "exchange limit", + "exchange stop", + "exchange trailing-stop", + "exchange fill-or-kill" + }; + } + + // BitfinexAPI object cannot be + // copied + BitfinexAPI(const BitfinexAPI&) = delete; + BitfinexAPI& operator = (const BitfinexAPI&) = delete; + // moved + BitfinexAPI(BitfinexAPI&&) = delete; + BitfinexAPI& operator = (BitfinexAPI&&) = delete; + + ~BitfinexAPI() { } + + //////////////////////////////////////////////////////////////////////// + // Accessors + //////////////////////////////////////////////////////////////////////// + + // Getters + const string getWDconfFilePath() const noexcept + { return WDconfFilePath_; } + + const BfxClientErrors& getBfxApiStatusCode() const noexcept + { return bfxApiStatusCode_; } + + const CURLcode getCurlStatusCode() const noexcept + { return Request.getLastStatusCode(); } + + const string strResponse() const noexcept + { return Request.getLastResponse(); } + + bool hasApiError() + { + return (checkErrors() != noError || Request.hasError()); + } + + // Setters + void setWDconfFilePath(const string &path) noexcept + { WDconfFilePath_ = path; } + + void setKeys(const string &accessKey, const string &secretKey) noexcept + { + Request.setAccessKey(accessKey); + Request.setSecretKey(secretKey); + } + + //////////////////////////////////////////////////////////////////////// + // Public endpoints + //////////////////////////////////////////////////////////////////////// + + BitfinexAPI& getTicker(const string &symbol) + { + if (!inArray(symbol, symbols_)) + bfxApiStatusCode_ = badSymbol; + else + Request.get("/pubticker/" + symbol); + + return *this; + }; + + BitfinexAPI& getStats(const string &symbol) + { + if (!inArray(symbol, symbols_)) + bfxApiStatusCode_ = badSymbol; + else + Request.get("/stats/" + symbol); + + return *this; + }; + + BitfinexAPI& getFundingBook(const string ¤cy, + const unsigned &limit_bids = 50, + const unsigned &limit_asks = 50) + { + if (!inArray(currency, currencies_)) + bfxApiStatusCode_ = badCurrency; + else + { + map params; + params["limit_bids"] = to_string(limit_bids); + params["limit_asks"] = to_string(limit_asks); + Request.get("/lendbook/" + currency, params); + } + + return *this; + }; + + BitfinexAPI& getOrderBook(const string &symbol, + const unsigned &limit_bids = 50, + const unsigned &limit_asks = 50, + const bool &group = true) + { + if (!inArray(symbol, symbols_)) + bfxApiStatusCode_ = badSymbol; + else + { + map params; + params["limit_bids"] = to_string(limit_bids); + params["limit_asks"] = to_string(limit_asks); + params["group"] = to_string(group); + Request.get("/book/" + symbol, params); + } + + return *this; + }; + + BitfinexAPI& getTrades(const string &symbol, + const time_t &since = 0, + const unsigned &limit_trades = 50) + { + if (!inArray(symbol, symbols_)) + bfxApiStatusCode_ = badSymbol; + else + { + map params; + params["timestamp"] = to_string(since); + params["limit_trades"] = to_string(limit_trades); + Request.get("/trades/" + symbol, params); + } + + return *this; + }; + + BitfinexAPI& getLends(const string ¤cy, + const time_t &since = 0, + const unsigned &limit_lends = 50) + { + if (!inArray(currency, currencies_)) + bfxApiStatusCode_ = badCurrency; + else + { + map params; + params["timestamp"] = to_string(since); + params["limit_lends"] = to_string(limit_lends); + Request.get("/lends/" + currency, params); + } + + return *this; + }; + + BitfinexAPI& getSymbols() + { + Request.get("/symbols/"); + + return *this; + }; + + BitfinexAPI& getSymbolsDetails() + { + Request.get("/symbols_details/"); + + return *this; + }; + + //////////////////////////////////////////////////////////////////////// + // Authenticated endpoints + //////////////////////////////////////////////////////////////////////// + + // Account + BitfinexAPI& getAccountInfo() + { + string params = "{\"request\":\"/v1/account_infos\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/account_infos/", params); + + return *this; + }; + + BitfinexAPI& getAccountFees() + { + string params = "{\"request\":\"/v1/account_fees\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/account_fees/", params); + + return *this; + }; + + BitfinexAPI& getSummary() + { + string params = "{\"request\":\"/v1/summary\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/summary/", params); + + return *this; + }; + + BitfinexAPI& deposit(const string &method, + const string &walletName, + const bool &renew = false) + { + if (!inArray(method, methods_)) + { bfxApiStatusCode_ = badDepositMethod; return *this; } + + if (!inArray(walletName, walletNames_)) + { bfxApiStatusCode_ = badWalletType; return *this; } + + string params = "{\"request\":\"/v1/deposit/new\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"method\":\"" + method + "\""; + params += ",\"wallet_name\":\"" + walletName + "\""; + params += ",\"renew\":" + to_string(renew); + params += "}"; + Request.post("/deposit/new/", params); + + return *this; + }; + + BitfinexAPI& getKeyPermissions() + { + string params = "{\"request\":\"/v1/key_info\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/key_info/", params); + + return *this; + }; + + BitfinexAPI& getMarginInfos() + { + string params = "{\"request\":\"/v1/margin_infos\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/margin_infos/", params); + + return *this; + }; + + BitfinexAPI& getBalances() + { + string params = "{\"request\":\"/v1/balances\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/balances/", params); + + return *this; + }; + + BitfinexAPI& transfer(const double &amount, + const string ¤cy, + const string &walletfrom, + const string &walletto) + { + if (!inArray(currency, currencies_)) + { bfxApiStatusCode_ = badCurrency; return *this; } + + if (!inArray(walletfrom, walletNames_) || + !inArray(walletto, walletNames_)) + { bfxApiStatusCode_ = badWalletType; return *this; } + + string params = "{\"request\":\"/v1/transfer\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"amount\":\"" + to_string(amount) + "\""; + params += ",\"currency\":\"" + currency + "\""; + params += ",\"walletfrom\":\"" + walletfrom + "\""; + params += ",\"walletto\":\"" + walletto + "\""; + params += "}"; + Request.post("/transfer/", params); + + return *this; + }; + + // configure withdraw.conf file before use + BitfinexAPI& withdraw() + { + string params = "{\"request\":\"/v1/withdraw\",\"nonce\":\"" + + getTonce() + "\""; + + // Add params from withdraw.conf + BfxClientErrors code(parseWDconfParams(params)); + if (code != noError) + bfxApiStatusCode_ = code; + else + { + params += "}"; + Request.post("/withdraw/", params); + } + + return *this; + }; + + // Orders + BitfinexAPI& newOrder(const string &symbol, + const double &amount, + const double &price, + const string &side, + const string &type, + const bool &is_hidden = false, + const bool &is_postonly = false, + const bool &use_all_available = false, + const bool &ocoorder = false, + const double &buy_price_oco = 0) + { + if (!inArray(symbol, symbols_)) + { bfxApiStatusCode_ = badSymbol; return *this; }; + + if (!inArray(type, types_)) + { bfxApiStatusCode_ = badOrderType; return *this; }; + + string params = "{\"request\":\"/v1/order/new\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"symbol\":\"" + symbol + "\""; + params += ",\"amount\":\"" + to_string(amount) + "\""; + params += ",\"price\":\"" + to_string(price) + "\""; + params += ",\"side\":\"" + side + "\""; + params += ",\"type\":\"" + type + "\""; + params += ",\"is_hidden\":" + bool2string(is_hidden); + params += ",\"is_postonly\":" + bool2string(is_postonly); + params += ",\"use_all_available\":" + bool2string(use_all_available); + params += ",\"ocoorder\":" + bool2string(ocoorder); + params += ",\"buy_price_oco\":" + bool2string(buy_price_oco); + params += "}"; + + Request.post("/order/new/", params); + return *this; + }; + + BitfinexAPI& newOrders(const vOrders &orders) + { + string params = "{\"request\":\"/v1/order/new/multi\",\"nonce\":\"" + + getTonce() + "\""; + + // Get pointer to last element in orders. We will not place + // ',' character at the end of the last loop. + auto &last = *(--orders.cend()); + + params += ",\"payload\":["; + for (const auto &order : orders) + { + params += "{\"symbol\":\"" + order.symbol + "\""; + params += ",\"amount\":\"" + to_string(order.amount) + "\""; + params += ",\"price\":\"" + to_string(order.price) + "\""; + params += ",\"side\":\"" + order.side + "\""; + params += ",\"type\":\"" + order.type + "\"}"; + if (&order != &last) + params += ","; + } + params += "]}"; + Request.post("/order/new/multi/", params); + + return *this; + }; + + BitfinexAPI& cancelOrder(const long long &order_id) + { + string params = "{\"request\":\"/v1/order/cancel\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"order_id\":" + to_string(order_id); + params += "}"; + Request.post("/order/cancel/", params); + + return *this; + }; + + BitfinexAPI& cancelOrders(const vIds &vOrderIds) + { + string params = "{\"request\":\"/v1/order/cancel/multi\",\"nonce\":\"" + + getTonce() + "\""; + + // Get pointer to last element in vOrders. We will not place + // ',' character at the end of the last loop. + auto &last = *(--vOrderIds.cend()); + + params += ", \"order_ids\":["; + for (const auto &order_id : vOrderIds) + { + params += to_string(order_id); + if (&order_id != &last) + params += ","; + } + params += "]}"; + Request.post("/order/cancel/multi/", params); + + return *this; + }; + + BitfinexAPI& cancelAllOrders() + { + string params = "{\"request\":\"/v1/order/cancel/all\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/order/cancel/all/", params); + + return *this; + }; + + BitfinexAPI& replaceOrder(const long long &order_id, + const string &symbol, + const double &amount, + const double &price, + const string &side, + const string &type, + const bool &is_hidden = false, + const bool &use_remaining = false) + { + if (!inArray(symbol, symbols_)) + { bfxApiStatusCode_ = badSymbol; return *this; }; + + if (!inArray(type, types_)) + { bfxApiStatusCode_ = badOrderType; return *this; }; + + string params = "{\"request\":\"/v1/order/cancel/replace\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"order_id\":" + to_string(order_id); + params += ",\"symbol\":\"" + symbol + "\""; + params += ",\"amount\":\"" + to_string(amount) + "\""; + params += ",\"price\":\"" + to_string(price) + "\""; + params += ",\"side\":\"" + side + "\""; + params += ",\"type\":\"" + type + "\""; + params += ",\"is_hidden\":" + bool2string(is_hidden); + params += ",\"use_all_available\":" + bool2string(use_remaining); + params += "}"; + Request.post("/order/cancel/replace/", params); + + return *this; + }; + + BitfinexAPI& getOrderStatus(const long long &order_id) + { + string params = "{\"request\":\"/v1/order/status\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"order_id\":" + to_string(order_id); + params += "}"; + Request.post("/order/status/", params); + + return *this; + }; + + BitfinexAPI& getActiveOrders() + { + string params = "{\"request\":\"/v1/orders\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/orders/", params); + + return *this; + }; + + BitfinexAPI& getOrdersHistory(const unsigned &limit = 50) + { + string params = "{\"request\":\"/v1/orders/hist\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"limit\":" + to_string(limit); + params += "}"; + Request.post("/orders/hist/", params); + + return *this; + }; + + + // Positions + BitfinexAPI& getActivePositions() + { + string params = "{\"request\":\"/v1/positions\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/positions/", params); + + return *this; + }; + + BitfinexAPI& claimPosition(long long &position_id, + const double &amount) + { + string params = "{\"request\":\"/v1/position/claim\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"position_id\":" + to_string(position_id); + params += ",\"amount\":\"" + to_string(amount) + "\""; + params += "}"; + Request.post("/position/claim/", params); + + return *this; + }; + + + // Historical data + BitfinexAPI& getBalanceHistory(const string ¤cy, + const time_t &since = 0, + const time_t &until = 0, + const unsigned &limit = 500, + const string &walletType = "all") + { + // Is currency valid ? + if (!inArray(currency, currencies_)) + { bfxApiStatusCode_ = badCurrency; return *this; }; + + // Is wallet type valid ? + // Modified condition which accepts "all" value for all wallets + // balances together.If "all" specified then there is simply no + // wallet parameter in POST request. + if (!inArray(walletType, walletNames_) || walletType != "all") + { bfxApiStatusCode_ = badWalletType; return *this; }; + + string params = "{\"request\":\"/v1/history\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"currency\":\"" + currency + "\""; + params += ",\"since\":\"" + to_string(since) + "\""; + params += ",\"until\":\"" + (!until ? getTonce() : to_string(until)) + + "\""; + params += ",\"limit\":" + to_string(limit); + if (walletType != "all") + params += ",\"wallet\":\"" + walletType + "\""; + params += "}"; + Request.post("/history/", params); + + return *this; + }; + + BitfinexAPI& getWithdrawalHistory(const string ¤cy, + const string &method = "all", + const time_t &since = 0, + const time_t &until = 0, + const unsigned &limit = 500) + { + if (!inArray(currency, currencies_)) + { bfxApiStatusCode_ = badCurrency; return *this; }; + + if (!inArray(method, methods_) && method != "wire" && method != "all") + { bfxApiStatusCode_ = badDepositMethod; return *this; }; + + string params = "{\"request\":\"/v1/history/movements\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"currency\":\"" + currency + "\""; + if (method != "all") + params += ",\"method\":\"" + method + "\""; + params += ",\"since\":\"" + to_string(since) + "\""; + params += ",\"until\":\"" + + (!until ? getTonce() : to_string(until)) + "\""; + params += ",\"limit\":" + to_string(limit); + params += "}"; + Request.post("/history/movements/", params); + + return *this; + }; + + BitfinexAPI& getPastTrades(const string &symbol, + const time_t ×tamp, + const time_t &until = 0, + const unsigned &limit_trades = 500, + const bool reverse = false) + { + if (!inArray(symbol, symbols_)) + bfxApiStatusCode_ = badSymbol; + else + { + string params = "{\"request\":\"/v1/mytrades\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"symbol\":\"" + symbol + "\""; + params += ",\"timestamp\":\"" + to_string(timestamp) + "\""; + params += ",\"until\":\"" + + (!until ? getTonce() : to_string(until)) + "\""; + params += ",\"limit_trades\":" + to_string(limit_trades); + params += ",\"reverse\":" + to_string(reverse); + params += "}"; + Request.post("/mytrades/", params); + } + + return *this; + }; + + // Margin funding + BitfinexAPI& newOffer(const string ¤cy, + const double &amount, + const float &rate, + const unsigned &period, + const string &direction) + { + if(!inArray(currency, currencies_)) + bfxApiStatusCode_ = badCurrency; + else + { + string params = "{\"request\":\"/v1/offer/new\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"currency\":\"" + currency + "\""; + params += ",\"amount\":\"" + to_string(amount) + "\""; + params += ",\"rate\":\"" + to_string(rate) + "\""; + params += ",\"period\":" + to_string(period); + params += ",\"direction\":\"" + direction + "\""; + params += "}"; + Request.post("/offer/new/", params); + } + + return *this; + }; + + BitfinexAPI& cancelOffer(const long long &offer_id) + { + string params = "{\"request\":\"/v1/offer/cancel\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"offer_id\":" + to_string(offer_id); + params += "}"; + Request.post("/offer/cancel/", params); + + return *this; + }; + + BitfinexAPI& getOfferStatus(const long long &offer_id) + { + string params = "{\"request\":\"/v1/offer/status\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"offer_id\":" + to_string(offer_id); + params += "}"; + Request.post("/offer/status/", params); + + return *this; + }; + + BitfinexAPI& getActiveCredits() + { + string params = "{\"request\":\"/v1/credits\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/credits/", params); + + return *this; + }; + + BitfinexAPI& getOffers() + { + string params = "{\"request\":\"/v1/offers\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/offers/", params); + + return *this; + }; + + BitfinexAPI& getOffersHistory(const unsigned &limit) + { + string params = "{\"request\":\"/v1/offers/hist\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"limit\":" + to_string(limit); + params += "}"; + Request.post("/offers/hist/", params); + + return *this; + }; + + // There is ambiguity in the "symbol" parameter value for this call. + // It should be "currency" not "symbol". + // Typical values for "symbol" are trading pairs such as "btcusd", + // "btcltc" ... + // Typical values for "currency" are "btc", "ltc" ... + BitfinexAPI& getPastFundingTrades(const string ¤cy, + const time_t &until = 0, + const unsigned &limit_trades = 50) + { + // Is currency valid ? + if(!inArray(currency, currencies_)) + bfxApiStatusCode_ = badCurrency; + else + { + string params = "{\"request\":\"/v1/mytrades_funding\",\"nonce\":\"" + + getTonce() + "\""; + // param inconsistency in BFX API, "symbol" should be currency + params += ",\"symbol\":\"" + currency + "\""; + params += ",\"until\":" + to_string(until); + params += ",\"limit_trades\":" + to_string(limit_trades); + params += "}"; + Request.post("/mytrades_funding/", params); + } + + return *this; + }; + + BitfinexAPI& getTakenFunds() + { + string params = "{\"request\":\"/v1/taken_funds\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/taken_funds/", params); + + return *this; + }; + + BitfinexAPI& getUnusedTakenFunds() + { + string params = "{\"request\":\"/v1/unused_taken_funds\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/unused_taken_funds/", params); + + return *this; + }; + + BitfinexAPI& getTotalTakenFunds() + { + string params = "{\"request\":\"/v1/total_taken_funds\",\"nonce\":\"" + + getTonce() + "\""; + params += "}"; + Request.post("/total_taken_funds/", params); + + return *this; + }; + + BitfinexAPI& closeLoan(const long long &offer_id) + { + string params = "{\"request\":\"/v1/funding/close\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"swap_id\":" + to_string(offer_id); + params += "}"; + Request.post("/funding/close/", params); + + return *this; + }; + + BitfinexAPI& closePosition(const long long &position_id) + { + string params = "{\"request\":\"/v1/position/close\",\"nonce\":\"" + + getTonce() + "\""; + params += ",\"position_id\":" + to_string(position_id); + params += "}"; + Request.post("/position/close/", params); + + return *this; + }; + + private: + + //////////////////////////////////////////////////////////////////////// + // Private attributes + //////////////////////////////////////////////////////////////////////// + + // containers with supported parameters + unordered_set symbols_; // valid symbol pairs + unordered_set currencies_; // valid currencies + unordered_set methods_; // valid deposit methods + unordered_set walletNames_; // valid walletTypes + unordered_set types_; // valid Types (see new order endpoint) + // BitfinexAPI settings + string WDconfFilePath_; + // internal jsonutils instances + jsonutils::BfxSchemaValidator schemaValidator_; + // internal HTTPRequest instance + HTTPRequest Request; + // dynamic and status variables + BfxClientErrors bfxApiStatusCode_; + + //////////////////////////////////////////////////////////////////////// + // Utility private methods + //////////////////////////////////////////////////////////////////////// + + BfxClientErrors parseWDconfParams(string ¶ms) + { + using std::getline; + using std::ifstream; + using std::map; + using std::regex; + using std::regex_search; + using std::smatch; + + string line; + map mParams; + ifstream inFile(WDconfFilePath_, ifstream::in); + if (!inFile.is_open()) + { + return badWDconfFilePath; + } + regex rgx("^(.*)\\b\\s*=\\s*(\"{0,1}.*\"{0,1})$"); + smatch match; + + // Create map with parameters + while (getline(inFile, line)) + { + // Skip comments, blank lines ... + if (isalpha(line[0])) + { + // ... and keys with empty values + if (regex_search(line, match, rgx) && match[2] != "\"\"") + mParams.emplace(match[1], match[2]); + } + } + + // Check parameters + if (!mParams.count("withdraw_type") || + !mParams.count("walletselected") || + !mParams.count("amount")) + { + return requiredParamsMissing; + } + + if (mParams["withdraw_type"] == "wire") + { + if (!mParams.count("account_number") || + !mParams.count("bank_name") || + !mParams.count("bank_address") || + !mParams.count("bank_city") || + !mParams.count("bank_country")) + { + return wireParamsMissing; + } + } + else if (inArray(mParams["withdraw_type"], methods_)) + { + if(!mParams.count("address")) + { + return addressParamsMissing; + } + } + + // Create JSON string + + for (const auto ¶m : mParams) + { + params += ",\""; + params += param.first; + params += "\":"; + params += param.second; + } + + return noError; + }; + + BfxClientErrors checkErrors() { + bfxApiStatusCode_ = Request.hasError() + ? curlERR + : schemaValidator_.validateSchema( + Request.getLastPath(), + Request.getLastResponse() + ); + return bfxApiStatusCode_; + } + + //////////////////////////////////////////////////////////////////////// + // Utility private static methods + //////////////////////////////////////////////////////////////////////// + + const static string bool2string(const bool &in) noexcept + { return in ? "true" : "false"; }; + + static string getTonce() noexcept + { + using namespace std::chrono; + + milliseconds ms = + duration_cast(system_clock::now().time_since_epoch()); + + return to_string(ms.count()); + }; + + static bool inArray(const string &value, + const unordered_set &inputSet) noexcept + { return (inputSet.find(value) != inputSet.cend()); }; + }; +} diff --git a/app/include/bfx-api-cpp/HTTPRequest.hpp b/app/include/bfx-api-cpp/HTTPRequest.hpp new file mode 100644 index 0000000..1217784 --- /dev/null +++ b/app/include/bfx-api-cpp/HTTPRequest.hpp @@ -0,0 +1,287 @@ +//////////////////////////////////////////////////////////////////////////////// +// HTTPRequest.hpp +// +// +// Bitfinex REST API C++ client +// +// +// Copyright (C) 2018 Blas Vicco blasvicco@gmail.com +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +//////////////////////////////////////////////////////////////////////////////// + +#include +#include + +// curl +#include + +// cryptopp +#include +#include +#include +#include + +using std::string; +using std::map; + +// CRYPTOPP_NO_GLOBAL_BYTE signals byte is at CryptoPP::byte +#ifdef CRYPTOPP_NO_GLOBAL_BYTE +using CryptoPP::byte; +#endif + +namespace BfxAPI { + + class HTTPRequest { + + //////////////////////////////////////////////////////////////////////// + // Class constants + //////////////////////////////////////////////////////////////////////// + + static constexpr auto CURL_TIMEOUT = 30L; + static constexpr auto CURL_DEBUG_VERBOSE = 0L; + + public: + + //////////////////////////////////////////////////////////////////////// + // Constructor / Destructor + //////////////////////////////////////////////////////////////////////// + + HTTPRequest(string inEndpoint) { + endpoint = inEndpoint; + curlGET = curl_easy_init(); + curlPOST = curl_easy_init(); + }; + + ~HTTPRequest() { + curl_easy_cleanup(curlGET); + curl_easy_cleanup(curlPOST); + }; + + //////////////////////////////////////////////////////////////////////// + // Public methods + //////////////////////////////////////////////////////////////////////// + + string get(string inPath, map params = {}) { + response = ""; + if (curlGET) { + path = inPath; + string url = endpoint + path + "?" + parseParams(params); + + setupHeader(); + curl_easy_setopt(curlPOST, CURLOPT_HTTPHEADER, curlHeader); + curl_easy_setopt(curlGET, CURLOPT_TIMEOUT, CURL_TIMEOUT); + curl_easy_setopt(curlGET, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curlGET, CURLOPT_VERBOSE, CURL_DEBUG_VERBOSE); + curl_easy_setopt(curlGET, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curlGET, CURLOPT_WRITEFUNCTION, writeCallback); + + curlStatusCode = curl_easy_perform(curlGET); + // libcurl internal error handling + if (curlStatusCode != CURLE_OK) { + cerr << "libcurl error in Request.get():" << endl; + cerr << "CURLcode: " << curlStatusCode << endl; + } + } else { + cerr << "curl not properly initialized curlGET = nullptr"; + } + return response; + }; + + string post(string inPath, string json = "") { + response = ""; + if (curlPOST) { + path = inPath; + string url = endpoint + path; + string payload; + getBase64(json, payload); + + if (accessKey != "") { + header["X-BFX-APIKEY"] = accessKey; + } + + if (secretKey != "") { + header["X-BFX-SIGNATURE"] = getSignature(payload); + } + + if (payload != "") { + header["X-BFX-PAYLOAD"] = payload; + } + + setupHeader(); + curl_easy_setopt(curlPOST, CURLOPT_HTTPHEADER, curlHeader); + curl_easy_setopt(curlPOST, CURLOPT_POST, 1); + curl_easy_setopt(curlPOST, CURLOPT_POSTFIELDS, "\n"); + curl_easy_setopt(curlPOST, CURLOPT_TIMEOUT, CURL_TIMEOUT); + curl_easy_setopt(curlPOST, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curlPOST, CURLOPT_VERBOSE, CURL_DEBUG_VERBOSE); + curl_easy_setopt(curlPOST, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curlPOST, CURLOPT_WRITEFUNCTION, writeCallback); + + curlStatusCode = curl_easy_perform(curlPOST); + clearHeader(); + // libcurl internal error handling + if (curlStatusCode != CURLE_OK) { + cerr << "libcurl error in Request.post():" << endl; + cerr << "CURLcode: " << curlStatusCode << endl; + } + } else { + cerr << "curl not properly initialized curlPOST = nullptr"; + } + + return response; + }; + + string parseParams(map params) { + string pp = ""; + for (auto it = params.begin(); it != params.end(); it++) { + pp += it->first + "=" + it->second + "&"; + } + return pp; + }; + + string getSignature(string payload) { + string signature; + getHmacSha384(secretKey, payload, signature); + return signature; + } + + const CURLcode getLastStatusCode() const noexcept { + return curlStatusCode; + } + + const string getLastResponse() const noexcept { + return response; + } + + const string getLastPath() const noexcept { + return path; + } + + const bool hasError() const noexcept { + return curlStatusCode != CURLE_OK; + } + + void setSecretKey(string inSecretKey) { + secretKey = inSecretKey; + } + + void setAccessKey(string inAccessKey) { + accessKey = inAccessKey; + } + + void setHeader(map inHeader) { + header = inHeader; + } + + private: + + //////////////////////////////////////////////////////////////////////// + // Private properties + //////////////////////////////////////////////////////////////////////// + + string endpoint, path, secretKey, accessKey, response; + map header; + struct curl_slist *curlHeader = nullptr; + + // Curl properties + CURL *curlGET; + CURL *curlPOST; + CURLcode curlStatusCode; + + //////////////////////////////////////////////////////////////////////// + // Private methods + //////////////////////////////////////////////////////////////////////// + + // Curl write callback function. Appends fetched *content to *userp + // pointer. *userp pointer is set up by + // curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result) line. + // In this case *userp points to result. + static size_t writeCallback( + void *data, + size_t size, + size_t nmemb, + void *userp) noexcept + { + (static_cast (userp)) + ->append(static_cast (data), size * nmemb); + return size * nmemb; + }; + + static void getBase64(const string &content, string &encoded) { + using CryptoPP::Base64Encoder; + using CryptoPP::StringSink; + using CryptoPP::StringSource; + + byte buffer[1024] = {}; + + for (auto i = 0; i < content.length(); ++i) + buffer[i] = content[i]; + + StringSource ss( + buffer, + content.length(), + true, + new Base64Encoder(new StringSink(encoded), false) + ); + }; + + static void getHmacSha384( + const string &key, + const string &content, + string &digest) + { + using CryptoPP::HashFilter; + using CryptoPP::HexEncoder; + using CryptoPP::HMAC; + using CryptoPP::SecByteBlock; + using CryptoPP::StringSink; + using CryptoPP::StringSource; + using CryptoPP::SHA384; + using std::transform; + + SecByteBlock byteKey((const byte*)key.data(), key.size()); + string mac; + digest.clear(); + + HMAC hmac(byteKey, byteKey.size()); + StringSource ss1( + content, true, + new HashFilter(hmac, new StringSink(mac)) + ); + StringSource ss2(mac, true, new HexEncoder(new StringSink(digest))); + transform(digest.cbegin(), digest.cend(), digest.begin(), ::tolower); + }; + + void setupHeader() { + curlHeader = nullptr; + for (auto it = header.begin(); it != header.end(); it++) { + curlHeader = curl_slist_append( + curlHeader, + (it->first + ": " + it->second).c_str() + ); + } + }; + + void clearHeader() { + curlHeader = nullptr; + header.erase("X-BFX-APIKEY"); + header.erase("X-BFX-SIGNATURE"); + header.erase("X-BFX-PAYLOAD"); + } + + }; + +} diff --git a/app/include/bfx-api-cpp/error.hpp b/app/include/bfx-api-cpp/error.hpp new file mode 100644 index 0000000..b1f9687 --- /dev/null +++ b/app/include/bfx-api-cpp/error.hpp @@ -0,0 +1,26 @@ +//////////////////////////////////////////////////////////////////////////////// +// error.hpp +// +// bfx-api-cpp error enumeration +// +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +enum BfxClientErrors +{ + noError = 0, + curlERR, // 1 + badSymbol, // 2 + badCurrency, // 3 + badDepositMethod, // 4 + badWalletType, // 5 + requiredParamsMissing, // 6 + wireParamsMissing, // 7 + addressParamsMissing, // 8 + badOrderType, // 9 + jsonStrToUSetError, // 10 + badWDconfFilePath, // 11 + responseParseError, // 12 + responseSchemaError // 13 +}; diff --git a/app/include/bfx-api-cpp/jsonutils.hpp b/app/include/bfx-api-cpp/jsonutils.hpp new file mode 100644 index 0000000..2b9c23c --- /dev/null +++ b/app/include/bfx-api-cpp/jsonutils.hpp @@ -0,0 +1,338 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// JSON utility routines for BitfinexAPI +// +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +// rapidjson +#include "rapidjson/document.h" +#include "rapidjson/error/en.h" +#include "rapidjson/filereadstream.h" +#include "rapidjson/schema.h" +#include "rapidjson/stringbuffer.h" +#include "rapidjson/writer.h" + +// internal error +#include "error.hpp" + +// std +#include +#include +#include +#include + +// namespaces +using std::cerr; +using std::cout; +using std::endl; +using std::string; +using std::unordered_set; +using std::unordered_map; +namespace rj = rapidjson; + + +namespace jsonutils +{ + + //////////////////////////////////////////////////////////////////////////// + // Classes + //////////////////////////////////////////////////////////////////////////// + + /// Helper class resolving remote schema for schema $ref operator + class MyRemoteSchemaDocumentProvider: public rj::IRemoteSchemaDocumentProvider + { + + #ifndef JSON_DEFINITIONS_FILE_PATH + static constexpr auto JSON_DEFINITIONS_FILE_PATH = "definitions.json"; + #endif + + public: + + MyRemoteSchemaDocumentProvider() + { + FILE *pFileIn = fopen(JSON_DEFINITIONS_FILE_PATH, "r"); // non-Windows use "r" + char readBuffer[65536]; + rj::FileReadStream fReadStream(pFileIn, readBuffer, sizeof(readBuffer)); + rj::Document d; + d.ParseStream(fReadStream); + fclose(pFileIn); + remoteSchemaDoc_ = new rj::SchemaDocument(d); + }; + + // ~MyRemoteSchemaDocumentProvider() + // { + // delete remoteSchemaDoc_; + // }; + + private: + + rj::SchemaDocument *remoteSchemaDoc_; + + virtual const rj::SchemaDocument* + GetRemoteDocument(const char* uri, rj::SizeType length) + { + // Resolve the URI and return a pointer to that schema + return remoteSchemaDoc_; + } + }; + + class BfxSchemaValidator + { + public: + + BfxSchemaValidator() {} + BfxSchemaValidator(unordered_set &symbols, + unordered_set ¤cies) + { + // Mapping is needed because rapidjson implementation of $ref + // keyword in json schema doesn't support json schema names which + // contain special characters thus direct mapping of endpoint such + // "/symbols/" to "/symbols/" schema name is not possible. + // See https://github.com/Tencent/rapidjson/issues/1311 + + //////////////////////////////////////////////////////////////////// + // Public endpoints + //////////////////////////////////////////////////////////////////// + + // Create map for endpoints which use symbols in URL + for (const auto &symbol : symbols) + { + // Map /pubticker/[symbol] endpoints to "pubticker" schema + apiEndPointToSchemaMap_.emplace("/pubticker/" + symbol, + "pubticker"); + // Map /stats/[symbol] endpoints to "stats" schema + apiEndPointToSchemaMap_.emplace("/stats/" + symbol, + "stats"); + // Map /book/[symbol] endpoints to "book" schema + apiEndPointToSchemaMap_.emplace("/book/" + symbol, + "book"); + // Map /trades/[symbol] endpoints to "trades" schema + apiEndPointToSchemaMap_.emplace("/trades/" + symbol, + "trades"); + } + + // Create map for endpoints which use currencies in URL + for (const auto ¤cy : currencies) + { + // Map /lendbook/[currency] endpoints to "lendbook" schema + apiEndPointToSchemaMap_.emplace("/lendbook/" + currency, + "lendbook"); + // Map /lends/[currency] endpoints to "lends" schema + apiEndPointToSchemaMap_.emplace("/lends/" + currency, + "lends"); + } + + // Create map for static endpoints + apiEndPointToSchemaMap_.emplace("/symbols/", "symbols"); + apiEndPointToSchemaMap_.emplace("/symbols_details/", + "symbols_details"); + + //////////////////////////////////////////////////////////////////// + // Authenticated endpoints + //////////////////////////////////////////////////////////////////// + + // Create map for static endpoints + apiEndPointToSchemaMap_.emplace("/account_infos/", "account_infos"); + apiEndPointToSchemaMap_.emplace("/account_fees/", "account_fees"); + apiEndPointToSchemaMap_.emplace("/summary/", "summary"); + apiEndPointToSchemaMap_.emplace("/deposit/new/", "deposit_new"); + apiEndPointToSchemaMap_.emplace("/key_info/", "key_info"); + apiEndPointToSchemaMap_.emplace("/margin_infos/", "margin_infos"); + apiEndPointToSchemaMap_.emplace("/balances/", "balances"); + apiEndPointToSchemaMap_.emplace("/transfer/", "transfer"); + apiEndPointToSchemaMap_.emplace("/withdraw/", "withdraw"); + apiEndPointToSchemaMap_.emplace("/order/new/", "order_new"); + apiEndPointToSchemaMap_.emplace("/order/new/multi/", "order_new_multi"); + apiEndPointToSchemaMap_.emplace("/order/cancel/", "order_cancel"); + apiEndPointToSchemaMap_.emplace("/order/cancel/multi/", + "order_cancel_multi"); + apiEndPointToSchemaMap_.emplace("/order/cancel/all/", + "order_cancel_all"); + apiEndPointToSchemaMap_.emplace("/order/cancel/replace/", + "order_cancel_replace"); + apiEndPointToSchemaMap_.emplace("/order/status/", "order_status"); + apiEndPointToSchemaMap_.emplace("/orders/", "orders"); + apiEndPointToSchemaMap_.emplace("/orders/hist/", "orders_hist"); + apiEndPointToSchemaMap_.emplace("/positions/", "positions"); + apiEndPointToSchemaMap_.emplace("/position/claim/", "position_claim"); + apiEndPointToSchemaMap_.emplace("/history/", "history"); + apiEndPointToSchemaMap_.emplace("/history/movements/", + "history_movements"); + apiEndPointToSchemaMap_.emplace("/mytrades/", "mytrades"); + apiEndPointToSchemaMap_.emplace("/offer/new/", "offer_new"); + apiEndPointToSchemaMap_.emplace("/offer/cancel/", "offer_cancel"); + apiEndPointToSchemaMap_.emplace("/offer/status/", "offer_status"); + apiEndPointToSchemaMap_.emplace("/credits/", "credits"); + apiEndPointToSchemaMap_.emplace("/offers/", "offers"); + apiEndPointToSchemaMap_.emplace("/offers/hist/", "offers_hist"); + apiEndPointToSchemaMap_.emplace("/mytrades_funding/", + "mytrades_funding"); + apiEndPointToSchemaMap_.emplace("/taken_funds/", "taken_funds"); + apiEndPointToSchemaMap_.emplace("/unused_taken_funds/", + "unused_taken_funds"); + apiEndPointToSchemaMap_.emplace("/total_taken_funds/", + "total_taken_funds"); + apiEndPointToSchemaMap_.emplace("/funding/close/", "funding_close"); + apiEndPointToSchemaMap_.emplace("/position/close/", "position_close"); + + } + + auto validateSchema(const string &apiEndPoint, const string &inputJson) + { + const auto schemaName = getApiEndPointSchemaName(apiEndPoint); + + // Create rapidjson schema document + rj::Document sd; + string schema = + "{ \"$ref\": \"definitions.json#/" + schemaName + "\" }"; + sd.Parse(schema.c_str()); + rj::SchemaDocument schemaDocument(sd, 0, 0, &provider_); + + // Create rapidjson document and check for parse errors + rj::Document d; + if (d.Parse(inputJson.c_str()).HasParseError()) + { + cerr << "Invalid json - response:" << endl; + cerr << inputJson << endl; + cerr << "API endpoint: " << apiEndPoint << endl; + return BfxClientErrors::responseParseError; + } + + // Create rapidjson validator and check for schema errors + rj::SchemaValidator validator(schemaDocument); + if (!d.Accept(validator)) + { + // Input JSON is invalid according to the schema + // Output diagnostic information + rj::StringBuffer sb; + validator.GetInvalidSchemaPointer().StringifyUriFragment(sb); + cerr << "Invalid schema: " << sb.GetString() << endl; + cerr << "Invalid keyword: " << validator.GetInvalidSchemaKeyword() << endl; + sb.Clear(); + validator.GetInvalidDocumentPointer().StringifyUriFragment(sb); + cerr << "Invalid document: " << sb.GetString() << endl; + cerr << "Invalid response: " << inputJson << endl; + cerr << "Invalid API endpoint: " << apiEndPoint << endl; + return BfxClientErrors::responseSchemaError; + } + + return BfxClientErrors::noError; + } + + private: + + MyRemoteSchemaDocumentProvider provider_; + unordered_map apiEndPointToSchemaMap_; + + const string& getApiEndPointSchemaName(const string& apiEndpoint) noexcept + { + return apiEndPointToSchemaMap_[apiEndpoint]; + } + + }; + + /// SAX events helper struct for jsonStrToUset() routine + struct jsonStrToUsetHandler: + public rj::BaseReaderHandler, jsonStrToUsetHandler> + { + // Constructor + jsonStrToUsetHandler():state_(State::kExpectArrayStart) {} + + // SAX events handlers + bool StartArray() noexcept + { + switch (state_) + { + case State::kExpectArrayStart: + state_ = State::kExpectValueOrArrayEnd; + return true; + default: + return false; + } + } + + bool String(const char *str, rj::SizeType length, bool) + { + switch (state_) + { + case State::kExpectValueOrArrayEnd: + handlerUSet_.emplace(str); + return true; + default: + return false; + } + } + + bool EndArray(rj::SizeType) noexcept + { + return state_ == State::kExpectValueOrArrayEnd; + } + + bool Default() { return false; } // All other events are invalid. + + // Handler attributes + std::unordered_set handlerUSet_; // output uSet + enum class State // valid states + { + kExpectArrayStart, + kExpectValueOrArrayEnd, + } state_; + }; + + //////////////////////////////////////////////////////////////////////////// + // Routines + //////////////////////////////////////////////////////////////////////////// + + BfxClientErrors jsonStrToUset(unordered_set &uSet, const string &inputJson) + { + // Create schema $ref resolver + rj::Document sd; + string schema = "{ \"$ref\": \"doc/definitions.json#/flatJsonSchema\" }"; + sd.Parse(schema.c_str()); + MyRemoteSchemaDocumentProvider provider; + rj::SchemaDocument schemaDoc(sd, 0, 0, &provider); + + // Create SAX events handler which contains parsed uSet after successful + // parsing + jsonStrToUsetHandler handler; + + // Create schema validator + rj::GenericSchemaValidator + validator(schemaDoc, handler); + + // Create reader + rj::Reader reader; + + // Create input JSON StringStream + rj::StringStream ss(inputJson.c_str()); + + // Parse and validate + if (!reader.Parse(ss, validator)) + { + // Input JSON is invalid according to the schema + // Output diagnostic information + cerr << "Error(offset " << reader.GetErrorOffset() << "): "; + cerr << GetParseError_En(reader.GetParseErrorCode()) << endl; + + if (!validator.IsValid()) + { + rj::StringBuffer sb; + validator.GetInvalidSchemaPointer().StringifyUriFragment(sb); + cerr << "Invalid schema: " << sb.GetString() << endl; + cerr << "Invalid keyword: " << validator.GetInvalidSchemaKeyword() << endl; + sb.Clear(); + validator.GetInvalidDocumentPointer().StringifyUriFragment(sb); + cerr << "Invalid document: " << sb.GetString() << endl; + cerr << "Invalid response: " << inputJson << endl; + } + return BfxClientErrors::jsonStrToUSetError; + } + else + { + uSet.swap(handler.handlerUSet_); + return BfxClientErrors::noError; + } + } +} diff --git a/include/rapidjson/allocators.h b/app/include/rapidjson/allocators.h similarity index 100% rename from include/rapidjson/allocators.h rename to app/include/rapidjson/allocators.h diff --git a/include/rapidjson/cursorstreamwrapper.h b/app/include/rapidjson/cursorstreamwrapper.h similarity index 100% rename from include/rapidjson/cursorstreamwrapper.h rename to app/include/rapidjson/cursorstreamwrapper.h diff --git a/include/rapidjson/document.h b/app/include/rapidjson/document.h similarity index 100% rename from include/rapidjson/document.h rename to app/include/rapidjson/document.h diff --git a/include/rapidjson/encodedstream.h b/app/include/rapidjson/encodedstream.h similarity index 100% rename from include/rapidjson/encodedstream.h rename to app/include/rapidjson/encodedstream.h diff --git a/include/rapidjson/encodings.h b/app/include/rapidjson/encodings.h similarity index 100% rename from include/rapidjson/encodings.h rename to app/include/rapidjson/encodings.h diff --git a/include/rapidjson/error/en.h b/app/include/rapidjson/error/en.h similarity index 100% rename from include/rapidjson/error/en.h rename to app/include/rapidjson/error/en.h diff --git a/include/rapidjson/error/error.h b/app/include/rapidjson/error/error.h similarity index 100% rename from include/rapidjson/error/error.h rename to app/include/rapidjson/error/error.h diff --git a/include/rapidjson/filereadstream.h b/app/include/rapidjson/filereadstream.h similarity index 100% rename from include/rapidjson/filereadstream.h rename to app/include/rapidjson/filereadstream.h diff --git a/include/rapidjson/filewritestream.h b/app/include/rapidjson/filewritestream.h similarity index 100% rename from include/rapidjson/filewritestream.h rename to app/include/rapidjson/filewritestream.h diff --git a/include/rapidjson/fwd.h b/app/include/rapidjson/fwd.h similarity index 100% rename from include/rapidjson/fwd.h rename to app/include/rapidjson/fwd.h diff --git a/include/rapidjson/internal/biginteger.h b/app/include/rapidjson/internal/biginteger.h similarity index 100% rename from include/rapidjson/internal/biginteger.h rename to app/include/rapidjson/internal/biginteger.h diff --git a/include/rapidjson/internal/diyfp.h b/app/include/rapidjson/internal/diyfp.h similarity index 100% rename from include/rapidjson/internal/diyfp.h rename to app/include/rapidjson/internal/diyfp.h diff --git a/include/rapidjson/internal/dtoa.h b/app/include/rapidjson/internal/dtoa.h similarity index 100% rename from include/rapidjson/internal/dtoa.h rename to app/include/rapidjson/internal/dtoa.h diff --git a/include/rapidjson/internal/ieee754.h b/app/include/rapidjson/internal/ieee754.h similarity index 100% rename from include/rapidjson/internal/ieee754.h rename to app/include/rapidjson/internal/ieee754.h diff --git a/include/rapidjson/internal/itoa.h b/app/include/rapidjson/internal/itoa.h similarity index 100% rename from include/rapidjson/internal/itoa.h rename to app/include/rapidjson/internal/itoa.h diff --git a/include/rapidjson/internal/meta.h b/app/include/rapidjson/internal/meta.h similarity index 100% rename from include/rapidjson/internal/meta.h rename to app/include/rapidjson/internal/meta.h diff --git a/include/rapidjson/internal/pow10.h b/app/include/rapidjson/internal/pow10.h similarity index 100% rename from include/rapidjson/internal/pow10.h rename to app/include/rapidjson/internal/pow10.h diff --git a/include/rapidjson/internal/regex.h b/app/include/rapidjson/internal/regex.h similarity index 100% rename from include/rapidjson/internal/regex.h rename to app/include/rapidjson/internal/regex.h diff --git a/include/rapidjson/internal/stack.h b/app/include/rapidjson/internal/stack.h similarity index 100% rename from include/rapidjson/internal/stack.h rename to app/include/rapidjson/internal/stack.h diff --git a/include/rapidjson/internal/strfunc.h b/app/include/rapidjson/internal/strfunc.h similarity index 100% rename from include/rapidjson/internal/strfunc.h rename to app/include/rapidjson/internal/strfunc.h diff --git a/include/rapidjson/internal/strtod.h b/app/include/rapidjson/internal/strtod.h similarity index 100% rename from include/rapidjson/internal/strtod.h rename to app/include/rapidjson/internal/strtod.h diff --git a/include/rapidjson/internal/swap.h b/app/include/rapidjson/internal/swap.h similarity index 100% rename from include/rapidjson/internal/swap.h rename to app/include/rapidjson/internal/swap.h diff --git a/include/rapidjson/istreamwrapper.h b/app/include/rapidjson/istreamwrapper.h similarity index 100% rename from include/rapidjson/istreamwrapper.h rename to app/include/rapidjson/istreamwrapper.h diff --git a/include/rapidjson/memorybuffer.h b/app/include/rapidjson/memorybuffer.h similarity index 100% rename from include/rapidjson/memorybuffer.h rename to app/include/rapidjson/memorybuffer.h diff --git a/include/rapidjson/memorystream.h b/app/include/rapidjson/memorystream.h similarity index 100% rename from include/rapidjson/memorystream.h rename to app/include/rapidjson/memorystream.h diff --git a/include/rapidjson/msinttypes/inttypes.h b/app/include/rapidjson/msinttypes/inttypes.h similarity index 100% rename from include/rapidjson/msinttypes/inttypes.h rename to app/include/rapidjson/msinttypes/inttypes.h diff --git a/include/rapidjson/msinttypes/stdint.h b/app/include/rapidjson/msinttypes/stdint.h similarity index 100% rename from include/rapidjson/msinttypes/stdint.h rename to app/include/rapidjson/msinttypes/stdint.h diff --git a/include/rapidjson/ostreamwrapper.h b/app/include/rapidjson/ostreamwrapper.h similarity index 100% rename from include/rapidjson/ostreamwrapper.h rename to app/include/rapidjson/ostreamwrapper.h diff --git a/include/rapidjson/pointer.h b/app/include/rapidjson/pointer.h similarity index 100% rename from include/rapidjson/pointer.h rename to app/include/rapidjson/pointer.h diff --git a/include/rapidjson/prettywriter.h b/app/include/rapidjson/prettywriter.h similarity index 100% rename from include/rapidjson/prettywriter.h rename to app/include/rapidjson/prettywriter.h diff --git a/include/rapidjson/rapidjson.h b/app/include/rapidjson/rapidjson.h similarity index 100% rename from include/rapidjson/rapidjson.h rename to app/include/rapidjson/rapidjson.h diff --git a/include/rapidjson/reader.h b/app/include/rapidjson/reader.h similarity index 100% rename from include/rapidjson/reader.h rename to app/include/rapidjson/reader.h diff --git a/include/rapidjson/schema.h b/app/include/rapidjson/schema.h similarity index 100% rename from include/rapidjson/schema.h rename to app/include/rapidjson/schema.h diff --git a/include/rapidjson/stream.h b/app/include/rapidjson/stream.h similarity index 100% rename from include/rapidjson/stream.h rename to app/include/rapidjson/stream.h diff --git a/include/rapidjson/stringbuffer.h b/app/include/rapidjson/stringbuffer.h similarity index 100% rename from include/rapidjson/stringbuffer.h rename to app/include/rapidjson/stringbuffer.h diff --git a/include/rapidjson/writer.h b/app/include/rapidjson/writer.h similarity index 100% rename from include/rapidjson/writer.h rename to app/include/rapidjson/writer.h diff --git a/app/src/example.cpp b/app/src/example.cpp new file mode 100644 index 0000000..cc18709 --- /dev/null +++ b/app/src/example.cpp @@ -0,0 +1,158 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// example.cpp +// +// +// Bitfinex REST API C++ client - examples +// +//////////////////////////////////////////////////////////////////////////////// + +// std +#include +#include + +// BitfinexAPI +#include "bfx-api-cpp/BitfinexAPI.hpp" + + +// namespaces +using std::cerr; +using std::cout; +using std::endl; +using std::ifstream; +using std::string; + + +int main(int argc, char *argv[]) +{ + // Create bfxAPI without API keys + BfxAPI::BitfinexAPI bfxAPI; + + // Create bfxAPI with API keys + // BfxAPI::BitfinexAPI bfxAPI("accessKey", "secretKey"); + + // Load API keys from file + ifstream ifs("../doc/key-secret", ifstream::in); + if (ifs.is_open()) { + string accessKey; getline(ifs, accessKey); + string secretKey; getline(ifs, secretKey); + ifs.close(); + bfxAPI.setKeys(accessKey, secretKey); + } + + // Fetch API + cout << "Request with error checking: " << endl; + bfxAPI.getTicker("btcusd"); + if (!bfxAPI.hasApiError()) + cerr << bfxAPI.strResponse() << endl; + else + { + // see bfxERR enum in BitfinexAPI.hpp::BitfinexAPI + cerr << "BfxApiStatusCode: "; + cerr << bfxAPI.getBfxApiStatusCode() << endl; + // see https://curl.haxx.se/libcurl/c/libcurl-errors.html + cerr << "CurlStatusCode: "; + cerr << bfxAPI.getCurlStatusCode() << endl; + } + + cout << "Request without error checking: " << endl; + cout << bfxAPI.getSummary().strResponse() << endl; + + //////////////////////////////////////////////////////////////////////////// + /// Available unauthenticated requests + //////////////////////////////////////////////////////////////////////////// + + // bfxAPI.getTicker("btcusd"); + // bfxAPI.getStats("btcusd"); + // bfxAPI.getFundingBook("USD", 50, 50); + // bfxAPI.getOrderBook("btcusd", 50, 50, true); + // bfxAPI.getTrades("btcusd", 0L, 50); + // bfxAPI.getLends("USD", 0L, 50); + // bfxAPI.getSymbols(); + // bfxAPI.getSymbolsDetails(); + + //////////////////////////////////////////////////////////////////////////// + /// Available authenticated requests + //////////////////////////////////////////////////////////////////////////// + + /// Account /// + // bfxAPI.getAccountInfo(); + // bfxAPI.getAccountFees(); + // bfxAPI.getSummary(); + // bfxAPI.deposit("bitcoin", "deposit", true); + // bfxAPI.getKeyPermissions(); + // bfxAPI.getMarginInfos(); + // bfxAPI.getBalances(); + // bfxAPI.transfer(0.1, "BTC", "trading", "deposit"); + // bfxAPI.withdraw(); // configure withdraw.conf file before use + + /// Orders /// + // bfxAPI.newOrder("btcusd", + // 0.01, + // 983, + // "sell", + // "exchange limit", + // false, + // true, + // false, + // false, + // 0); + // + // How to create vOrders object for newOrders() call + // BitfinexAPI::vOrders orders = + // { + // {"btcusd", 0.1, 950, "sell", "exchange limit"}, + // {"btcusd", 0.1, 950, "sell", "exchange limit"}, + // {"btcusd", 0.1, 950, "sell", "exchange limit"} + // }; + // bfxAPI.newOrders(orders); + // + // bfxAPI.cancelOrder(13265453586LL); + // + // How to create ids object for cancelOrders() call + // BitfinexAPI::vIds ids = + // { + // 12324589754LL, + // 12356754322LL, + // 12354996754LL + // }; + // bfxAPI.cancelOrders(ids); + // + // bfxAPI.cancelAllOrders(); + // bfxAPI.replaceOrder(1321548521LL, + // "btcusd", + // 0.05, + // 1212, + // "sell", + // "exchange limit", + // false, + // false); + // bfxAPI.getOrderStatus(12113548453LL); + // bfxAPI.getActiveOrders(); + // bfxAPI.getOrdersHistory(10); + + /// Positions /// + // bfxAPI.getActivePositions(); + // bfxAPI.claimPosition(156321412LL, 150); + + /// Historical data /// + // bfxAPI.getBalanceHistory("USD", 0L, 0L, 500, "all"); + // bfxAPI.getWithdrawalHistory("BTC", "all", 0L , 0L, 500); + // bfxAPI.getPastTrades("btcusd", 0L, 0L, 500, false); + + /// Margin funding /// + // bfxAPI.newOffer("USD", 12000, 25.2, 30, "lend"); + // bfxAPI.cancelOffer(12354245628LL); + // bfxAPI.getOfferStatus(12313541215LL); + // bfxAPI.getActiveCredits(); + // bfxAPI.getOffers(); + // bfxAPI.getOffersHistory(50); + // bfxAPI.getPastFundingTrades("BTC", 0, 50); + // bfxAPI.getTakenFunds(); + // bfxAPI.getUnusedTakenFunds(); + // bfxAPI.getTotalTakenFunds(); + // bfxAPI.closeLoan(1235845634LL); + // bfxAPI.closePosition(1235845634LL); + + return 0; +} diff --git a/app/src/test.cpp b/app/src/test.cpp new file mode 100644 index 0000000..8696928 --- /dev/null +++ b/app/src/test.cpp @@ -0,0 +1,72 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// test.cpp +// +// +// Bitfinex REST API C++ client - Available unauthenticated requests test +// +//////////////////////////////////////////////////////////////////////////////// + +// std +#include +#include + +// BitfinexAPI +#include "bfx-api-cpp/BitfinexAPI.hpp" + + +// namespaces +using std::cerr; +using std::cout; +using std::endl; +using std::ifstream; +using std::string; + +void check(BfxAPI::BitfinexAPI &bfxAPI) { + if (bfxAPI.hasApiError()) { + cout << "❌" << endl << endl; + cout << "BfxApiStatusCode: "; + cout << bfxAPI.getBfxApiStatusCode() << " - "; + cout << "CurlStatusCode: "; + cout << bfxAPI.getCurlStatusCode() << endl; + cout << "Response: " << bfxAPI.strResponse() << endl << endl; + } else { + cout << "✅" << endl << endl; + } +} + +int main(int argc, char *argv[]) { + // Create bfxAPI without API keys + BfxAPI::BitfinexAPI bfxAPI; + + //////////////////////////////////////////////////////////////////////////// + /// Available unauthenticated requests + //////////////////////////////////////////////////////////////////////////// + cout << "Starting available unauthenticated requests test" << endl << endl; + + cout << "- getTicker(\"btcusd\"): "; + bfxAPI.getTicker("btcusd"); check(bfxAPI); + + cout << "- getStats(\"btcusd\"): "; + bfxAPI.getStats("btcusd"); check(bfxAPI); + + cout << "- getFundingBook(\"USD\", 50, 50): "; + bfxAPI.getFundingBook("USD", 50, 50); check(bfxAPI); + + cout << "- getOrderBook(\"btcusd\", 50, 50, true): "; + bfxAPI.getOrderBook("btcusd", 50, 50, true); check(bfxAPI); + + cout << "- getTrades(\"btcusd\", 0L, 50): "; + bfxAPI.getTrades("btcusd", 0L, 50); check(bfxAPI); + + cout << "- getLends(\"USD\", 0L, 50)): "; + bfxAPI.getLends("USD", 0L, 50); check(bfxAPI); + + cout << "- getSymbols(): "; + bfxAPI.getSymbols(); check(bfxAPI); + + cout << "- getSymbolsDetails(): "; + bfxAPI.getSymbolsDetails(); check(bfxAPI); + + return 0; +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..63b4771 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3' +services: + dev: + build: . + volumes: + - ./app:/home/bfx-cpp-api/app + environment: + - CMAKE_MAKE_PROGRAM=/usr/bin/cmake + - CC=/usr/bin/clang + - CXX=/usr/bin/clang++ + command: ["/sbin/entrypoint.sh"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..1e2ef85 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Indexing files +updatedb + +# Start supervisord and services +/usr/bin/supervisord --nodaemon -c /etc/supervisord.conf diff --git a/docker/etc/supervisord.conf b/docker/etc/supervisord.conf new file mode 100644 index 0000000..a0c0610 --- /dev/null +++ b/docker/etc/supervisord.conf @@ -0,0 +1,36 @@ +[unix_http_server] +file=/var/run/supervisor.sock ; (the path to the socket file) +chmod = 0700 ; +username = supervisor_user ; +password = supervisor_pass ; + +[supervisord] +logfile=/var/log/supervisord.log ; (main log file;default $CWD/supervisord.log) +logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB) +logfile_backups=10 ; (num of main logfile rotation backups;default 10) +loglevel=info ; (log level;default info; others: debug,warn,trace) +pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) +nodaemon=false ; (start in foreground if true;default false) +minfds=1024 ; (min. avail startup file descriptors;default 1024) +minprocs=200 ; (min. avail process descriptors;default 200) +user=root ; + +; the below section must remain in the config file for RPC +; (supervisorctl/web interface) to work, additional interfaces may be +; added by defining them in separate rpcinterface: sections +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket +username = supervisor_user ; +password = supervisor_pass ; + +[eventlistener:stdout] +command = supervisor_stdout +buffer_size = 100 +events = PROCESS_LOG +result_handler = supervisor_stdout:event_handler + +#[include] +#files = /etc/supervisor/conf.d/*.conf diff --git a/example.cpp b/example.cpp deleted file mode 100644 index 5394ead..0000000 --- a/example.cpp +++ /dev/null @@ -1,141 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -// -// example.cpp -// -// -// Bitfinex REST API C++ client - examples -// -////////////////////////////////////////////////////////////////////////////// - - -#include -#include -#include "BitfinexAPI.hpp" - - -using std::cout; -using std::ifstream; -using std::endl; -using std::string; - - -int main(int argc, char *argv[]) -{ - ///////////////////////////////////////////////////////////////////////// - // Examples - // Note that default values are not mandatory. See BitfinexAPI.hpp - // for details. - ///////////////////////////////////////////////////////////////////////// - - ///////////////////////////////////////////////////////////////////////// - // GET REQUESTS - unauthenticated endpoints - ///////////////////////////////////////////////////////////////////////// - - BitfinexAPI bfxAPI; - string response; - int errCode = 0; - - // errCode = bfxAPI.getTicker(response, "btcusd"); - // errCode = bfxAPI.getStats(response, "btcusd"); - // errCode = bfxAPI.getFundingBook(response, "USD", 50, 50); - // errCode = bfxAPI.getOrderBook(response, "btcusd", 50, 50, 1); - // errCode = bfxAPI.getTrades(response, "btcusd", 0L, 50); - // errCode = bfxAPI.getLends(response, "USD", 0L, 50); - // errCode = bfxAPI.getSymbols(response); - // errCode = bfxAPI.getSymbolDetails(response); - - cout << "Response: " << response << endl; - cout << "Error code: " << errCode << endl; - - ///////////////////////////////////////////////////////////////////////// - // POST REQUESTS - authenticated endpoints - ///////////////////////////////////////////////////////////////////////// - - ifstream ifs("key-secret", ifstream::in); - if (!ifs.is_open()) - { - cout << "Can't open 'key-secret' file. " << endl; - return 1; - } - else - { - response.clear(); - string accessKey, secretKey; - getline(ifs, accessKey); - getline(ifs, secretKey); - ifs.close(); - - bfxAPI.setKeys(accessKey, secretKey); - - /// Account /// - // errCode = bfxAPI.getAccountInfo(response); - // errCode = bfxAPI.getAccountFees(response); - // errCode = bfxAPI.getSummary(response); - // errCode = bfxAPI.deposit(response, "bitcoin", "deposit", 1); - // errCode = bfxAPI.getKeyPermissions(response); - // errCode = bfxAPI.getMarginInfos(response); - // errCode = bfxAPI.getBalances(response); - // errCode = bfxAPI.transfer(response, 0.1, "BTC", "trading", "deposit"); - // errCode = bfxAPI.withdraw(response); // configure withdraw.conf file before use - - /// Orders /// - // errCode = bfxAPI.newOrder(response, "btcusd", 0.01, 983, "sell", "exchange limit", 0, 1, - // 0, 0, 0); - // - // How to create vOrders object for newOrders() call - // BitfinexAPI::vOrders orders = - // { - // {"btcusd", 0.1, 950, "sell", "exchange limit"}, - // {"btcusd", 0.1, 950, "sell", "exchange limit"}, - // {"btcusd", 0.1, 950, "sell", "exchange limit"} - // }; - // errCode = bfxAPI.newOrders(response, orders); - // - // errCode = bfxAPI.cancelOrder(response, 13265453586LL); - // - // How to create ids object for cancelOrders() call - // BitfinexAPI::vIds ids = - // { - // 12324589754LL, - // 12356754322LL, - // 12354996754LL - // }; - // errCode = bfxAPI.cancelOrders(response, ids); - // - // errCode = bfxAPI.cancelAllOrders(response); - // errCode = bfxAPI.replaceOrder(response, 1321548521LL, "btcusd", 0.05, 1212, "sell", - // "exchange limit", 0, 0); - // errCode = bfxAPI.getOrderStatus(response, 12113548453LL); - // errCode = bfxAPI.getActiveOrders(response); - // errCode = bfxAPI.getOrdersHistory(response, 10); - - - /// Positions /// - // errCode = bfxAPI.getActivePositions(response); - // errCode = bfxAPI.claimPosition(response, 156321412LL, 150); - - /// Historical data /// - // errCode = bfxAPI.getBalanceHistory(response, "USD", 0L, 0L, 500, "all"); - // errCode = bfxAPI.getWithdrawalHistory(response, "BTC", "all", 0L , 0L, 500); - // errCode = bfxAPI.getPastTrades(response, "btcusd", 0L, 0L, 500, 0); - - /// Margin funding /// - // errCode = bfxAPI.newOffer(response, "USD", 12000, 25.2, 30, "lend"); - // errCode = bfxAPI.cancelOffer(response, 12354245628LL); - // errCode = bfxAPI.getOfferStatus(response, 12313541215LL); - // errCode = bfxAPI.getActiveCredits(response); - // errCode = bfxAPI.getOffers(response); - // errCode = bfxAPI.getOffersHistory(response, 50); - // errCode = bfxAPI.getPastFundingTrades(response, "BTC", 0, 50); - // errCode = bfxAPI.getTakenFunds(response); - // errCode = bfxAPI.getUnusedTakenFunds(response); - // errCode = bfxAPI.getTotalTakenFunds(response); - // errCode = bfxAPI.closeLoan(response, 1235845634LL); - // errCode = bfxAPI.closePosition(response, 1235845634LL); - - cout << "Response: " << response << endl; - cout << "Error code: " << errCode << endl; - - return 0; - } -} diff --git a/include/BitfinexAPI.hpp b/include/BitfinexAPI.hpp deleted file mode 100644 index 40b64a6..0000000 --- a/include/BitfinexAPI.hpp +++ /dev/null @@ -1,1037 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -// -// BitfinexAPI.hpp -// -// -// Bitfinex REST API C++ client -// -// -// Copyright (C) 2018 Petr Javorik maple@mmquant.net -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// -////////////////////////////////////////////////////////////////////////////// - -#pragma once - -// std -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// curl -#include - -// cryptopp -#include -#include -#include -#include - -// internal jsonutils -#include "jsonutils.hpp" - - -using std::cout; -using std::string; -using std::to_string; -using std::unordered_set; -using std::vector; - - -// CRYPTOPP_NO_GLOBAL_BYTE signals byte is at CryptoPP::byte -#if defined(CRYPTOPP_NO_GLOBAL_BYTE) -using CryptoPP::byte; -#endif - - -class BitfinexAPI -{ -public: - - ////////////////////////////////////////////////////////////////////////////// - // Enumerations - ////////////////////////////////////////////////////////////////////////////// - enum bfxERR // positive values for curl internal error codes - { - curlERR = -50, - badSymbol = -40, - badCurrency = -39, - badDepositMethod = -38, - badWalletType = -37, - requiredParamsMissing = -36, - wireParamsMissing = -35, - addressParamsMissing = -34, - badOrderType = -33 - }; - - ////////////////////////////////////////////////////////////////////////////// - // Typedefs - ////////////////////////////////////////////////////////////////////////////// - - // Structure for multiple new orders endpoint - struct sOrder - { - string symbol; - double amount; - double price; - string side; - string type; - }; - typedef vector vOrders; - - typedef vector vIds; - - ////////////////////////////////////////////////////////////////////////////// - // Constructor - destructor - ////////////////////////////////////////////////////////////////////////////// - - explicit BitfinexAPI():BitfinexAPI("", "") {} - - explicit BitfinexAPI(const string &accessKey, const string &secretKey): - _accessKey(accessKey), - _secretKey(secretKey), - _WDconfFilePath("doc/withdraw.conf"), - _APIurl("https://api.bitfinex.com/v1"), - _curl(curl_easy_init()) - { - string temp; - getSymbols(temp); - jsonutils::arrayToUset(_symbols, temp); - - _currencies = - { - "BTG", - "DSH", - "ETC", - "ETP", - "EUR", - "GBP", - "IOT", - "JPY", - "LTC", - "NEO", - "OMG", - "SAN", - "USD", - "XMR", - "XRP", - "ZEC" - }; - // As found on https://bitfinex.readme.io/v1/reference#rest-auth-deposit - _methods = - { - "bcash" - "bitcoin", - "ethereum", - "ethereumc", - "ethereumc", - "litecoin", - "mastercoin", - "monero", - "tetheruso", - "zcash", - }; - _walletNames = - { - "trading", "exchange", "deposit" - }; - // New order endpoint "type" parameter - _types = - { - "market", - "limit", - "stop", - "trailing-stop", - "fill-or-kill", - "exchange market", - "exchange limit", - "exchange stop", - "exchange trailing-stop", - "exchange fill-or-kill" - }; - } - - ~BitfinexAPI() - { - curl_easy_cleanup(_curl); - } - - ////////////////////////////////////////////////////////////////////////////// - // Accessors - ////////////////////////////////////////////////////////////////////////////// - - string getWDconfFilePath() const { return _WDconfFilePath; } - - void setWDconfFilePath(const string &path) { _WDconfFilePath = path; } - - void setKeys(const string &accessKey, const string &secretKey) - { - this->_accessKey = accessKey; - this->_secretKey = secretKey; - } - - ////////////////////////////////////////////////////////////////////////////// - // Public endpoints - ////////////////////////////////////////////////////////////////////////////// - - int getTicker(string &result, const string &symbol) - { - // Is symbol valid ? - if(!inArray(symbol, _symbols)) - { - return badSymbol; - } - - return DoGETrequest("/pubticker/" + symbol, "", result); - }; - - int getStats(string &result, const string &symbol) - { - // Is symbol valid ? - if(!inArray(symbol, _symbols)) - { - return badSymbol; - } - - return DoGETrequest("/stats/" + symbol, "", result); - }; - - int getFundingBook(string &result, - const string ¤cy, - const int &limit_bids = 50, - const int &limit_asks = 50) - { - // Is currency valid ? - if(!inArray(currency, _currencies)) - { - return badCurrency; - } - - string params = - "?limit_bids=" + to_string(limit_bids) + - "&limit_asks=" + to_string(limit_asks); - return DoGETrequest("/lendbook/" + currency, params, result); - }; - - int getOrderBook(string &result, - const string &symbol, - const int &limit_bids = 50, - const int &limit_asks = 50, - const bool &group = 1) - { - // Is symbol valid ? - if(!inArray(symbol, _symbols)) - { - return badSymbol; - } - - string params = - "?limit_bids=" + to_string(limit_bids) + - "&limit_asks=" + to_string(limit_asks) + - "&group=" + to_string(group); - return DoGETrequest("/book/" + symbol, params, result); - }; - - int getTrades(string &result, - const string &symbol, - const time_t &since = 0, - const int &limit_trades = 50) - { - // Is symbol valid ? - if(!inArray(symbol, _symbols)) - { - return badSymbol; - } - - string params = - "?timestamp=" + to_string(since) + - "&limit_trades=" + to_string(limit_trades); - return DoGETrequest("/trades/" + symbol, params, result); - }; - - int getLends(string &result, - const string ¤cy, - const time_t &since = 0, - const int &limit_lends = 50) - { - // Is currency valid ? - if(!inArray(currency, _currencies)) - { - return badCurrency; - } - - string params = - "?timestamp=" + to_string(since) + - "&limit_lends=" + to_string(limit_lends); - return DoGETrequest("/lends/" + currency, params, result); - }; - - int getSymbols(string &result) - { - return DoGETrequest("/symbols/", "", result); - }; - - int getSymbolDetails(string &result) - { - return DoGETrequest("/symbols_details/", "", result); - }; - - ////////////////////////////////////////////////////////////////////////////// - // Authenticated endpoints - ////////////////////////////////////////////////////////////////////////////// - - // Account - int getAccountInfo(string &result) - { - string params = "{\"request\":\"/v1/account_infos\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/account_infos/", params, result); - }; - - int getAccountFees(string &result) - { - string params = "{\"request\":\"/v1/account_fees\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/account_fees/", params, result); - }; - - int getSummary(string &result) - { - string params = "{\"request\":\"/v1/summary\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/summary/", params, result); - }; - - int deposit(string &result, - const string &method, - const string &walletName, - const bool &renew = 0) - { - // Is deposit method valid ? - if(!inArray(method, _methods)) - { - return badDepositMethod; - } - // Is walletType valid ? - if(!inArray(walletName, _walletNames)) - { - return badWalletType; - } - - string params = "{\"request\":\"/v1/deposit/new\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"method\":\"" + method + "\""; - params += ",\"wallet_name\":\"" + walletName + "\""; - params += ",\"renew\":" + to_string(renew); - params += "}"; - return DoPOSTrequest("/deposit/new/", params, result); - }; - - int getKeyPermissions(string &result) - { - string params = "{\"request\":\"/v1/key_info\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/key_info/", params, result); - }; - - int getMarginInfos(string &result) - { - string params = "{\"request\":\"/v1/margin_infos\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/margin_infos/", params, result); - }; - - int getBalances(string &result) - { - string params = "{\"request\":\"/v1/balances\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/balances/", params, result); - }; - - int transfer(string &result, - const double &amount, - const string ¤cy, - const string &walletfrom, - const string &walletto) - { - // Is currency valid ? - if(!inArray(currency, _currencies)) - { - return badCurrency; - } - // Is walletType valid ? - if(!inArray(walletfrom, _walletNames) || !inArray(walletto, _walletNames)) - { - return badWalletType; - } - - string params = "{\"request\":\"/v1/transfer\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"amount\":\"" + to_string(amount) + "\""; - params += ",\"currency\":\"" + currency + "\""; - params += ",\"walletfrom\":\"" + walletfrom + "\""; - params += ",\"walletto\":\"" + walletto + "\""; - params += "}"; - return DoPOSTrequest("/transfer/", params, result); - }; - - int withdraw(string &result) // configure withdraw.conf file before use - { - string params = "{\"request\":\"/v1/withdraw\",\"nonce\":\"" + getTonce() + "\""; - - // Add params from withdraw.conf - int err = parseWDconfParams(params); - if (err != 0) { return err; }; - - params += "}"; - return DoPOSTrequest("/withdraw/", params, result); - }; - - // Orders - int newOrder(string &result, - const string &symbol, - const double &amount, - const double &price, - const string &side, - const string &type, - const bool &is_hidden = 0, - const bool &is_postonly = 0, - const bool &use_all_available = 0, - const bool &ocoorder = 0, - const double &buy_price_oco = 0) - { - // Is symbol valid ? - if(!inArray(symbol, _symbols)) - { - return badSymbol; - } - // Is order type valid ? - if(!inArray(type, _types)) - { - return badOrderType; - } - - string params = "{\"request\":\"/v1/order/new\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"symbol\":\"" + symbol + "\""; - params += ",\"amount\":\"" + to_string(amount) + "\""; - params += ",\"price\":\"" + to_string(price) + "\""; - params += ",\"side\":\"" + side + "\""; - params += ",\"type\":\"" + type + "\""; - params += ",\"is_hidden\":" + bool2string(is_hidden); - params += ",\"is_postonly\":" + bool2string(is_postonly); - params += ",\"use_all_available\":" + bool2string(use_all_available); - params += ",\"ocoorder\":" + bool2string(ocoorder); - params += ",\"buy_price_oco\":" + bool2string(buy_price_oco); - params += "}"; - return DoPOSTrequest("/order/new/", params, result); - }; - - int newOrders(string &result, const vOrders &orders) - { - string params = "{\"request\":\"/v1/order/new/multi\",\"nonce\":\"" + getTonce() + "\""; - - // Get pointer to last element in orders. We will not place - // ',' character at the end of the last loop. - auto &last = *(--orders.cend()); - - params += ",\"payload\":["; - for (const auto &order : orders) - { - params += "{\"symbol\":\"" + order.symbol + "\""; - params += ",\"amount\":\"" + to_string(order.amount) + "\""; - params += ",\"price\":\"" + to_string(order.price) + "\""; - params += ",\"side\":\"" + order.side + "\""; - params += ",\"type\":\"" + order.type + "\"}"; - if (&order != &last) - { - params += ","; - } - } - params += "]"; - params += "}"; - return DoPOSTrequest("/order/new/multi/", params, result); - }; - - int cancelOrder(string &result, const long long &order_id) - { - string params = "{\"request\":\"/v1/order/cancel\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"order_id\":" + to_string(order_id); - params += "}"; - return DoPOSTrequest("/order/cancel/", params, result); - }; - - int cancelOrders(string &result, const vIds &vOrder_ids) - { - string params = "{\"request\":\"/v1/order/cancel/multi\",\"nonce\":\"" + getTonce() + "\""; - - // Get pointer to last element in vOrders. We will not place - // ',' character at the end of the last loop. - auto &last = *(--vOrder_ids.cend()); - - params += ", \"order_ids\":["; - for (const auto &order_id : vOrder_ids) - { - params += to_string(order_id); - if (&order_id != &last) - { - params += ","; - } - } - params += "]"; - params += "}"; - return DoPOSTrequest("/order/cancel/multi/", params, result); - }; - - int cancelAllOrders(string &result) - { - string params = "{\"request\":\"/v1/order/cancel/all\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/order/cancel/all/", params, result); - }; - - int replaceOrder(string &result, - const long long &order_id, - const string &symbol, - const double &amount, - const double &price, - const string &side, - const string &type, - const bool &is_hidden = 0, - const bool &use_remaining = 0) - { - // Is symbol valid ? - if(!inArray(symbol, _symbols)) - { - return badSymbol; - } - // Is order type valid ? - if(!inArray(type, _types)) - { - return badOrderType; - } - - string params = "{\"request\":\"/v1/order/cancel/replace\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"order_id\":" + to_string(order_id); - params += ",\"symbol\":\"" + symbol + "\""; - params += ",\"amount\":\"" + to_string(amount) + "\""; - params += ",\"price\":\"" + to_string(price) + "\""; - params += ",\"side\":\"" + side + "\""; - params += ",\"type\":\"" + type + "\""; - params += ",\"is_hidden\":" + bool2string(is_hidden); - params += ",\"use_all_available\":" + bool2string(use_remaining); - params += "}"; - return DoPOSTrequest("/order/cancel/replace/", params, result); - }; - - int getOrderStatus(string &result, const long long &order_id) - { - string params = "{\"request\":\"/v1/order/status\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"order_id\":" + to_string(order_id); - params += "}"; - return DoPOSTrequest("/order/status/", params, result); - }; - - int getActiveOrders(string &result) - { - string params = "{\"request\":\"/v1/orders\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/orders/", params, result); - }; - - int getOrdersHistory(string &result, const int &limit = 50) - { - string params = "{\"request\":\"/v1/orders/hist\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"limit\":" + to_string(limit); - params += "}"; - return DoPOSTrequest("/orders/hist/", params, result); - }; - - - // Positions - int getActivePositions(string &result) - { - string params = "{\"request\":\"/v1/positions\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/positions/", params, result); - }; - - int claimPosition(string &result, - long long &position_id, - const double &amount) - { - string params = "{\"request\":\"/v1/position/claim\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"position_id\":" + to_string(position_id); - params += ",\"amount\":\"" + to_string(amount) + "\""; - params += "}"; - return DoPOSTrequest("/position/claim/", params, result); - }; - - // Historical data - int getBalanceHistory(string &result, - const string ¤cy, - const time_t &since = 0, - const time_t &until = 0, - const int &limit = 500, - const string &walletType = "all") - { - // Is currency valid ? - if(!inArray(currency, _currencies)) - { - return badCurrency; - } - // Is wallet type valid ? - // Modified condition which accepts "all" value for all wallets balances together. - if(!inArray(walletType, _walletNames) && walletType != "all") - { - return badWalletType; - } - - string params = "{\"request\":\"/v1/history\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"currency\":\"" + currency + "\""; - params += ",\"since\":\"" + to_string(since) + "\""; - params += ",\"until\":\"" + (!until ? getTonce() : to_string(until)) + "\""; - params += ",\"limit\":" + to_string(limit); - if (walletType != "all") - { - params += ",\"wallet\":\"" + walletType + "\""; - } - - params += "}"; - return DoPOSTrequest("/history/", params, result); - }; - - int getWithdrawalHistory(string &result, - const string ¤cy, - const string &method = "all", - const time_t &since = 0, - const time_t &until = 0, - const int &limit = 500) - { - // Is currency valid ? - if(!inArray(currency, _currencies)) - { - return badCurrency; - } - // Is deposit method valid ? - if(!inArray(method, _methods) && method != "wire" && method != "all") - { - return badDepositMethod; - } - - string params = "{\"request\":\"/v1/history/movements\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"currency\":\"" + currency + "\""; - if (method != "all") - { - params += ",\"method\":\"" + method + "\""; - } - params += ",\"since\":\"" + to_string(since) + "\""; - params += ",\"until\":\"" + (!until ? getTonce() : to_string(until)) + "\""; - params += ",\"limit\":" + to_string(limit); - params += "}"; - return DoPOSTrequest("/history/movements/", params, result); - }; - - int getPastTrades(string &result, - const string &symbol, - const time_t ×tamp, - const time_t &until = 0, - const int &limit_trades = 500, - const bool reverse = 0) - { - // Is symbol valid ? - if(!inArray(symbol, _symbols)) - { - return badSymbol; - } - - string params = "{\"request\":\"/v1/mytrades\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"symbol\":\"" + symbol + "\""; - params += ",\"timestamp\":\"" + to_string(timestamp) + "\""; - params += ",\"until\":\"" + (!until ? getTonce() : to_string(until)) + "\""; - params += ",\"limit_trades\":" + to_string(limit_trades); - params += ",\"reverse\":" + to_string(reverse); - params += "}"; - return DoPOSTrequest("/mytrades/", params, result); - }; - - // Margin funding - int newOffer(string &result, - const string ¤cy, - const double &amount, - const float &rate, - const int &period, - const string &direction) - { - // Is currency valid ? - if(!inArray(currency, _currencies)) - { - return badCurrency; - } - - string params = "{\"request\":\"/v1/offer/new\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"currency\":\"" + currency + "\""; - params += ",\"amount\":\"" + to_string(amount) + "\""; - params += ",\"rate\":\"" + to_string(rate) + "\""; - params += ",\"period\":" + to_string(period); - params += ",\"direction\":\"" + direction + "\""; - params += "}"; - return DoPOSTrequest("/offer/new/", params, result); - }; - - int cancelOffer(string &result, const long long &offer_id) - { - string params = "{\"request\":\"/v1/offer/cancel\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"offer_id\":" + to_string(offer_id); - params += "}"; - return DoPOSTrequest("/offer/cancel/", params, result); - }; - - int getOfferStatus(string &result, const long long &offer_id) - { - string params = "{\"request\":\"/v1/offer/status\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"offer_id\":" + to_string(offer_id); - params += "}"; - return DoPOSTrequest("/offer/status/", params, result); - }; - - int getActiveCredits(string &result) - { - string params = "{\"request\":\"/v1/credits\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/credits/", params, result); - }; - - int getOffers(string &result) - { - string params = "{\"request\":\"/v1/offers\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/offers/", params, result); - }; - - int getOffersHistory(string &result, const int &limit) - { - string params = "{\"request\":\"/v1/offers/hist\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"limit\":" + to_string(limit); - params += "}"; - return DoPOSTrequest("/offers/hist/", params, result); - }; - - // There is ambiguity in the "symbol" parameter value for this call. - // It should be "currency" not "symbol". - // Typical values for "symbol" are trading pairs such as "btcusd", "btcltc" ... - // Typical values for "currency" are "btc", "ltc" ... - int getPastFundingTrades(string &result, - const string ¤cy, - const time_t &until = 0, - const int &limit_trades = 50) - { - // Is currency valid ? - if(!inArray(currency, _currencies)) - { - return badCurrency; - } - - string params = "{\"request\":\"/v1/mytrades_funding\",\"nonce\":\"" + getTonce() + "\""; - // param inconsistency in BFX API, symbol should be currency - params += ",\"symbol\":\"" + currency + "\""; - params += ",\"until\":" + to_string(until); - params += ",\"limit_trades\":" + to_string(limit_trades); - params += "}"; - return DoPOSTrequest("/mytrades_funding/", params, result); - }; - - int getTakenFunds(string &result) - { - string params = "{\"request\":\"/v1/taken_funds\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/taken_funds/", params, result); - }; - - int getUnusedTakenFunds(string &result) - { - string params = "{\"request\":\"/v1/unused_taken_funds\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/unused_taken_funds/", params, result); - }; - - int getTotalTakenFunds(string &result) - { - string params = "{\"request\":\"/v1/total_taken_funds\",\"nonce\":\"" + getTonce() + "\""; - params += "}"; - return DoPOSTrequest("/total_taken_funds/", params, result); - }; - - int closeLoan(string &result, const long long &offer_id) - { - string params = "{\"request\":\"/v1/funding/close\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"swap_id\":" + to_string(offer_id); - params += "}"; - return DoPOSTrequest("/funding/close/", params, result); - }; - - int closePosition(string &result, const long long &position_id) - { - string params = "{\"request\":\"/v1/position/close\",\"nonce\":\"" + getTonce() + "\""; - params += ",\"position_id\":" + to_string(position_id); - params += "}"; - return DoPOSTrequest("/position/close/", params, result); - }; - -protected: - - // BitfinexAPI object cannot be copied - BitfinexAPI(const BitfinexAPI&); - BitfinexAPI& operator = (const BitfinexAPI&); - - ////////////////////////////////////////////////////////////////////////////// - // Private attributes - ////////////////////////////////////////////////////////////////////////////// - - unordered_set _symbols; // possible symbol pairs - unordered_set _currencies; // possible currencies - unordered_set _methods; // possible deposit methods - unordered_set _walletNames; // possible walletTypes - unordered_set _types; // possible Types (see new order endpoint) - string _WDconfFilePath; - string _APIurl; - string _accessKey, _secretKey; - CURL *_curl; - CURLcode _res; - - ////////////////////////////////////////////////////////////////////////////// - // Utility private methods - ////////////////////////////////////////////////////////////////////////////// - - int parseWDconfParams(string ¶ms) - { - using std::getline; - using std::ifstream; - using std::map; - using std::regex; - using std::regex_search; - using std::smatch; - - string line; - ifstream inFile; - map mParams; - inFile.open(_WDconfFilePath); - regex rgx("^(.*)\\b\\s*=\\s*(\"{0,1}.*\"{0,1})$"); - smatch match; - - // Create map with parameters - while (getline(inFile, line)) - { - // Skip comments, blank lines ... - if (isalpha(line[0])) - { - // ... and keys with empty values - if (regex_search(line, match, rgx) && match[2] != "\"\"") - mParams.emplace(match[1], match[2]); - } - } - - // Check parameters - if (!mParams.count("withdraw_type") || - !mParams.count("walletselected") || - !mParams.count("amount")) - { - return requiredParamsMissing; - } - - if (mParams["withdraw_type"] == "wire") - { - if (!mParams.count("account_number") || - !mParams.count("bank_name") || - !mParams.count("bank_address") || - !mParams.count("bank_city") || - !mParams.count("bank_country")) - { - return wireParamsMissing; - } - } - else if (inArray(mParams["withdraw_type"], _methods)) - { - if(!mParams.count("address")) - { - return addressParamsMissing; - } - } - - // Create JSON string - - for (const auto ¶m : mParams) - { - params += ",\""; - params += param.first; - params += "\":"; - params += param.second; - } - - return 0; - }; - - int DoGETrequest(const string &UrlEndPoint, const string ¶ms, string &result) - { - if(_curl) - { - string url = _APIurl + UrlEndPoint + params; - - _curl = curl_easy_init(); - curl_easy_setopt(_curl, CURLOPT_TIMEOUT, 30L); - curl_easy_setopt(_curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(_curl, CURLOPT_WRITEDATA, &result); - curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, WriteCallback); - - _res = curl_easy_perform(_curl); - - // libcurl internal error handling - if (_res != CURLE_OK) - { - cout << "Libcurl error in DoGETrequest(), code:\n"; - return _res; - } - return _res; - } - else - { - // curl not properly initialized curl = nullptr - return curlERR; - } - }; - - int DoPOSTrequest(const string &UrlEndPoint, const string ¶ms, string &result) - { - if(_curl) - { - string url = _APIurl + UrlEndPoint; - string payload; - string signature; - getBase64(params, payload); - getHmacSha384(_secretKey, payload, signature); - - // Headers - struct curl_slist *httpHeaders = nullptr; - httpHeaders = curl_slist_append(httpHeaders, - ("X-BFX-APIKEY:" + _accessKey).c_str()); - httpHeaders = curl_slist_append(httpHeaders, - ("X-BFX-PAYLOAD:" + payload).c_str()); - httpHeaders = curl_slist_append(httpHeaders, - ("X-BFX-SIGNATURE:" + signature).c_str()); - - _curl = curl_easy_init(); - curl_easy_setopt(_curl, CURLOPT_HTTPHEADER, httpHeaders); - curl_easy_setopt(_curl, CURLOPT_POST, 1); - curl_easy_setopt(_curl, CURLOPT_POSTFIELDS, "\n"); - curl_easy_setopt(_curl, CURLOPT_TIMEOUT, 30L); - curl_easy_setopt(_curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(_curl, CURLOPT_VERBOSE, 0L); // debug option - curl_easy_setopt(_curl, CURLOPT_WRITEDATA, &result); - curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, WriteCallback); - - _res = curl_easy_perform(_curl); - - // libcurl internal error handling - if (_res != CURLE_OK) - { - cout << "Libcurl error in DoPOSTrequest(), code:\n"; - return _res; - } - return _res; - - } - else - { - // curl not properly initialized curl = NULL - return curlERR; - } - }; - - ////////////////////////////////////////////////////////////////////////////// - // Utility private static methods - ////////////////////////////////////////////////////////////////////////////// - - static string bool2string(const bool &in) { return in ? "true" : "false"; }; - - static string getTonce() - { - using namespace std::chrono; - - milliseconds ms = duration_cast(system_clock::now().time_since_epoch()); - - return to_string(ms.count()); - }; - - static int getBase64(const string &content, string &encoded) - { - using CryptoPP::Base64Encoder; - using CryptoPP::StringSink; - using CryptoPP::StringSource; - - byte buffer[1024] = {}; - - for (int i = 0; i < content.length(); ++i) - { - buffer[i] = content[i]; - }; - - StringSource ss(buffer, - content.length(), - true, - new Base64Encoder(new StringSink(encoded), false)); - - return 0; - }; - - static int getHmacSha384(const string &key, const string &content, string &digest) - { - using CryptoPP::HashFilter; - using CryptoPP::HexEncoder; - using CryptoPP::HMAC; - using CryptoPP::SecByteBlock; - using CryptoPP::StringSink; - using CryptoPP::StringSource; - using CryptoPP::SHA384; - using std::transform; - - SecByteBlock byteKey((const byte*)key.data(), key.size()); - string mac; - digest.clear(); - - HMAC hmac(byteKey, byteKey.size()); - StringSource ss1(content, true, new HashFilter(hmac, new StringSink(mac))); - StringSource ss2(mac, true, new HexEncoder(new StringSink(digest))); - transform(digest.cbegin(), digest.cend(), digest.begin(), ::tolower); - - return 0; - }; - - // Curl write callback function. Appends fetched *content to *userp pointer. - // *userp pointer is set up by curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result) line. - // In this case *userp will point to result. - static size_t WriteCallback(void *response, size_t size, size_t nmemb, void *userp) - { - (static_cast(userp))->append(static_cast(response)); - return size * nmemb; - }; - - static bool inArray(const string &value, const unordered_set &inputSet) - { - return (inputSet.find(value) != inputSet.cend()) ? true : false; - }; -}; diff --git a/include/jsonutils.hpp b/include/jsonutils.hpp deleted file mode 100644 index 8428831..0000000 --- a/include/jsonutils.hpp +++ /dev/null @@ -1,33 +0,0 @@ -// JSON Utility routines for BitfinexAPI - -#pragma once - - -#include -#include - - -using std::unordered_set; -using std::string; -using std::stringstream; - - -namespace jsonutils -{ - void arrayToUset(unordered_set &uSet, string &array) - { - stringstream ss(array); - - while(ss.good()) - { - string substr; - getline(ss, substr, ','); - substr.erase(std::remove_if(substr.begin(), - substr.end(), - [](auto const &c) -> bool { - return !std::isalnum(c);}), - substr.end()); - uSet.emplace(substr); - } - } -}