diff --git a/README.md b/README.md index d255409b..3f99dcda 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | COINUT | Yes | Yes | NA | | Exmo | Yes | NA | NA | | CoinbasePro | Yes | Yes | No| +| Coinbene | Yes | No | No | | GateIO | Yes | Yes | NA | | Gemini | Yes | Yes | No | | HitBTC | Yes | Yes | No | diff --git a/cmd/documentation/exchanges_templates/coinbene.tmpl b/cmd/documentation/exchanges_templates/coinbene.tmpl new file mode 100644 index 00000000..4efb55ba --- /dev/null +++ b/cmd/documentation/exchanges_templates/coinbene.tmpl @@ -0,0 +1,106 @@ +{{define "exchanges coinbene" -}} +{{template "header" .}} +## Coinbene Exchange + +### Current Features + ++ REST Support ++ Websocket Support + +### How to enable + ++ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example) + ++ Individual package example below: + +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### How to do REST public/private calls + ++ If enabled via "configuration".json file the exchange will be added to the +IBotExchange array in the ```go var bot Bot``` and you will only be able to use +the wrapper interface functions for accessing exchange data. View routines.go +for an example of integration usage with GoCryptoTrader. Rudimentary example +below: + +main.go +```go +var c exchange.IBotExchange + +for i := range bot.exchanges { + if bot.exchanges[i].GetName() == "Coinbene" { + c = bot.exchanges[i] + } +} + +// Public calls - wrapper functions + +// Fetches current ticker information +tick, err := c.FetchTicker() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := c.FetchOrderbook() +if err != nil { + // Handle error +} + +// Private calls - wrapper functions - make sure your APIKEY and APISECRET are +// set and AuthenticatedAPISupport is set to true + +// Fetches current account information +accountInfo, err := c.GetAccountInfo() +if err != nil { + // Handle error +} +``` + ++ If enabled via individually importing package, rudimentary example below: + +```go +// Public calls + +// Fetches current ticker information +ticker, err := c.GetTicker() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := c.GetOrderbook() +if err != nil { + // Handle error +} + +// Private calls - make sure your APIKEY and APISECRET are set and +// AuthenticatedAPISupport is set to true + +// GetUserInfo returns account info +accountInfo, err := c.GetUserInfo(...) +if err != nil { + // Handle error +} + +// Submits an order and the exchange and returns its tradeID +resp, err := c.SubmitOrder(...) +if err != nil { + // Handle error +} +``` + +### How to do Websocket public/private calls + +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations"}} +{{end}} diff --git a/cmd/documentation/root_templates/root_readme.tmpl b/cmd/documentation/root_templates/root_readme.tmpl index 9666a073..3d7ca831 100644 --- a/cmd/documentation/root_templates/root_readme.tmpl +++ b/cmd/documentation/root_templates/root_readme.tmpl @@ -33,6 +33,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | COINUT | Yes | Yes | NA | | Exmo | Yes | NA | NA | | CoinbasePro | Yes | Yes | No| +| Coinbene | Yes | No | No | | GateIO | Yes | Yes | NA | | Gemini | Yes | Yes | No | | HitBTC | Yes | Yes | No | diff --git a/config/config_test.go b/config/config_test.go index 984f013e..a3ce1747 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -16,7 +16,7 @@ import ( const ( // Default number of enabled exchanges. Modify this whenever an exchange is // added or removed - defaultEnabledExchanges = 27 + defaultEnabledExchanges = 28 testFakeExchangeName = "Stampbit" ) diff --git a/config_example.json b/config_example.json index 98ac51e8..2506e16f 100644 --- a/config_example.json +++ b/config_example.json @@ -778,6 +778,53 @@ } ] }, + { + "name": "Coinbene", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "websocketOrderbookBufferLimit": 5, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "clientId": "ClientID", + "availablePairs": "ABBC/BTC,ABT/ETH,ABT/USDT,ABYSS/ETH,ACDC/BTC,ACDC/USDT,ADI/ETH,ADK/BTC,ADN/BTC,AE/BTC,AE/USDT,AID/BTC,AIDOC/BTC,AION/BTC,AIPE/USDT,AIT/USDT,ALGO/USDT,ALI/ETH,ALX/ETH,APL/ETH,ATX/BTC,B2G/BTC,B91/USDT,BAAS/BTC,BAT/BTC,BCHABC/USDT,BCHSV/USDT,BEAUTY/ETH,BETHER/ETH,BEZ/BTC,BGC/USDT,BKG/BTC,BNT/BTC,BOA/USDT,BSTN/ETH,BTC/USDT,BTFM/USDT,BTNT/BTC,BTSC/BTC,BTT/USDT,BU/ETH,BVT/ETH,C3W/ETH,CAN/ETH,CCC/ETH,CCE/USDT,CC/USDT,CEDEX/ETH,CENT/BTC,CFT/USDT,CLO/BTC,CMT/ETH,CMT/USDT,CNN/BTC,CNN/ETH,CNN/USDT,CONI/USDT,COSM/BTC,COSM/ETH,COZP/BTC,CPC/BTC,CPMS/USDT,CREDO/ETH,CRN/BTC,CS/ETH,CS/USDT,CTXC/ETH,CUST/USDT,CVC/BTC,CXC/USDT,CXP/BTC,DCA/ETH,DCT/BTC,DENT/BTC,DGD/BTC,DOCK/ETH,DSCB/USDT,DTA/ETH,DUC/BTC,DVC/ETH,EBC/BTC,EBC/ETH,EBC/USDT,ECA/BTC,EDC/BTC,EDR/ETH,ELF/BTC,EMT/USDT,EOS/BTC,EOS/USDT,EQUAD/BTC,ETC/BTC,ETC/USDT,ETH/BTC,ETH/USDT,ETK/BTC,ETN/BTC,FAB/ETH,FACC/ETH,FCC/BTC,FDS/USDT,FND/ETH,FNKOS/ETH,FTN/BTC,FTN/USDT,FTT/BTC,FXT/ETH,GETX/ETH,GLDR/ETH,GMTK/ETH,GOM/USDT,GRAM/USDT,GRIN/BTC,GRN/BTC,GSTT/USDT,GUSD/USDT,GVT/BTC,HAPPY/BTC,HDAC/BTC,HMB/USDT,HNB/USDT,HPT/ETH,HUP/USDT,INCX/ETH,IOST/BTC,IOTE/USDT,ISR/BTC,ISR/ETH,IVY/ETH,JOB/BTC,KBC/BTC,KBC/USDT,KMD/BTC,KNT/ETH,KST/BTC,KUE/BTC,KUE/ETH,KUKY/BTC,LAMB/USDT,LATX/BTC,LBK/BTC,LINK/BTC,LOOM/BTC,LTC/BTC,LTC/USDT,LUC/ETH,LUX/BTC,LVTC/ETH,MDC/USDT,MGC/USDT,MIB/BTC,MINX/BTC,MINX/ETH,MOAC/USDT,MPL/BTC,MTC/BTC,MT/ETH,MTN/ETH,MT/USDT,MVL/ETH,MVPT/ETH,MWT/USDT,NANO/BTC,NBAI/ETH,NCASH/BTC,NEO/BTC,NEO/USDT,NOBS/BTC,NPXS/ETH,NPXS/USDT,NTY/ETH,ODC/USDT,OMG/BTC,OMX/ETH,OVC/ETH,OZX/ETH,PAL/ETH,PAT/ETH,PAX/USDT,PKX/BTC,PLAY/BTC,PMA/ETH,POLL/BTC,POLY/BTC,PPT/BTC,PSM/BTC,QKC/BTC,QTUM/BTC,QTUM/USDT,RBG/BTC,RBG/ETH,RBG/USDT,RBTC/BTC,RBZ/USDT,RCOIN/BTC,RCOIN/USDT,REP/BTC,REV/BTC,RIF/BTC,SALT/BTC,SCC/BTC,SCO/BTC,SEN/BTC,SENC/ETH,SHE/BTC,SHVR/BTC,SIM/BTC,SKB/BTC,SKM/ETH,SKYM/USDT,SLT/ETH,SMARTUP/ETH,SMARTUP/USDT,SMART/USDT,SORO/USDT,SRCOIN/BTC,SRCOIN/ETH,STORJ/BTC,STQ/BTC,SWET/BTC,SWTC/USDT,TCT/BTC,TEMCO/USDT,TEN/BTC,TEN/ETH,THM/ETH,TIB/BTC,TIMO/USDT,TMTG/BTC,TOC/ETH,TOSC/BTC,TRUE/ETH,TRX/BTC,TRX/USDT,TSL/BTC,TVB/USDT,UTNP/BTC,VBT/USDT,VEEN/BTC,VME/BTC,VME/ETH,VOLLAR/USDT,VSC/ETH,W12/BTC,W12/ETH,WBL/BTC,WFX/BTC,XEM/BTC,XLM/BTC,XMCT/ETH,XMCT/USDT,XMR/BTC,XNK/ETH,XRP/BTC,XRP/USDT,XSR/USDT,YTA/USDT,ZAT/ETH,ZDC/BTC,ZEC/BTC,ZGC/BTC,ZRX/BTC", + "enabledPairs": "BTC/USDT", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "/" + }, + "requestCurrencyPairFormat": { + "uppercase": true, + "delimiter": "/" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, { "name": "GateIO", "enabled": true, diff --git a/engine/exchange.go b/engine/exchange.go index f52e9214..e6dd36c0 100644 --- a/engine/exchange.go +++ b/engine/exchange.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/btcmarkets" "github.com/thrasher-corp/gocryptotrader/exchanges/btse" "github.com/thrasher-corp/gocryptotrader/exchanges/coinbasepro" + "github.com/thrasher-corp/gocryptotrader/exchanges/coinbene" "github.com/thrasher-corp/gocryptotrader/exchanges/coinut" "github.com/thrasher-corp/gocryptotrader/exchanges/exmo" "github.com/thrasher-corp/gocryptotrader/exchanges/gateio" @@ -151,6 +152,8 @@ func LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error { exch = new(btcmarkets.BTCMarkets) case "btse": exch = new(btse.BTSE) + case "coinbene": + exch = new(coinbene.Coinbene) case "coinut": exch = new(coinut.COINUT) case "exmo": diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index 80b3541d..16a1eb21 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -166,9 +166,7 @@ func (b *Bitfinex) WsConnect() error { func (b *Bitfinex) WsDataHandler() { b.Websocket.Wg.Add(1) - defer func() { - b.Websocket.Wg.Done() - }() + defer b.Websocket.Wg.Done() for { select { diff --git a/exchanges/coinbene/README.md b/exchanges/coinbene/README.md new file mode 100644 index 00000000..44f6ef31 --- /dev/null +++ b/exchanges/coinbene/README.md @@ -0,0 +1,141 @@ +# GoCryptoTrader package Coinbene + + + + +[![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/exchanges/coinbene) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This coinbene package is part of the GoCryptoTrader codebase. + +## This is still in active development + +You can track ideas, planned features and what's in progresss on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Coinbene Exchange + +### Current Features + ++ REST Support ++ Websocket Support + +### How to enable + ++ [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example) + ++ Individual package example below: + +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### How to do REST public/private calls + ++ If enabled via "configuration".json file the exchange will be added to the +IBotExchange array in the ```go var bot Bot``` and you will only be able to use +the wrapper interface functions for accessing exchange data. View routines.go +for an example of integration usage with GoCryptoTrader. Rudimentary example +below: + +main.go +```go +var c exchange.IBotExchange + +for i := range bot.exchanges { + if bot.exchanges[i].GetName() == "Coinbene" { + c = bot.exchanges[i] + } +} + +// Public calls - wrapper functions + +// Fetches current ticker information +tick, err := c.GetTickerPrice() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := c.GetOrderbookEx() +if err != nil { + // Handle error +} + +// Private calls - wrapper functions - make sure your APIKEY and APISECRET are +// set and AuthenticatedAPISupport is set to true + +// Fetches current account information +accountInfo, err := c.GetAccountInfo() +if err != nil { + // Handle error +} +``` + ++ If enabled via individually importing package, rudimentary example below: + +```go +// Public calls + +// Fetches current ticker information +ticker, err := c.GetTicker() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := c.GetOrderBook() +if err != nil { + // Handle error +} + +// Private calls - make sure your APIKEY and APISECRET are set and +// AuthenticatedAPISupport is set to true + +// GetUserInfo returns account info +accountInfo, err := c.GetUserInfo(...) +if err != nil { + // Handle error +} + +// Submits an order and the exchange and returns its tradeID +tradeID, err := c.Trade(...) +if err != nil { + // Handle error +} +``` + +### How to do Websocket public/private calls + +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** + diff --git a/exchanges/coinbene/coinbene.go b/exchanges/coinbene/coinbene.go new file mode 100644 index 00000000..75c88419 --- /dev/null +++ b/exchanges/coinbene/coinbene.go @@ -0,0 +1,292 @@ +package coinbene + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" +) + +// Coinbene is the overarching type across this package +type Coinbene struct { + exchange.Base + WebsocketConn *wshandler.WebsocketConnection +} + +const ( + coinbeneAPIURL = "https://openapi-exchange.coinbene.com/api/exchange/" + coinbeneAuthPath = "/api/exchange/v2" + coinbeneAPIVersion = "v2" + buy = "buy" + sell = "sell" + + // Public endpoints + coinbeneFetchTicker = "/market/ticker/one" + coinbeneFetchOrderBook = "/market/orderBook" + coinbeneGetTrades = "/market/trades" + coinbeneGetAllPairs = "/market/tradePair/list" + coinbenePairInfo = "/market/tradePair/one" + + // Authenticated endpoints + coinbeneGetUserBalance = "/account/list" + coinbenePlaceOrder = "/order/place" + coinbeneOrderInfo = "/order/info" + coinbeneRemoveOrder = "/order/cancel" + coinbeneOpenOrders = "/order/openOrders" + coinbeneClosedOrders = "/order/closedOrders" + + authRateLimit = 150 + unauthRateLimit = 10 +) + +// GetTicker gets and stores ticker data for a currency pair +func (c *Coinbene) GetTicker(symbol string) (TickerResponse, error) { + var t TickerResponse + params := url.Values{} + params.Set("symbol", symbol) + path := common.EncodeURLValues(c.API.Endpoints.URL+coinbeneAPIVersion+coinbeneFetchTicker, params) + return t, c.SendHTTPRequest(path, &t) +} + +// GetOrderbook gets and stores orderbook data for given pair +func (c *Coinbene) GetOrderbook(symbol string, size int64) (OrderbookResponse, error) { + var o OrderbookResponse + params := url.Values{} + params.Set("symbol", symbol) + params.Set("depth", strconv.FormatInt(size, 10)) + path := common.EncodeURLValues(c.API.Endpoints.URL+coinbeneAPIVersion+coinbeneFetchOrderBook, params) + return o, c.SendHTTPRequest(path, &o) +} + +// GetTrades gets recent trades from the exchange +func (c *Coinbene) GetTrades(symbol string) (TradeResponse, error) { + var t TradeResponse + params := url.Values{} + params.Set("symbol", symbol) + path := common.EncodeURLValues(c.API.Endpoints.URL+coinbeneAPIVersion+coinbeneGetTrades, params) + return t, c.SendHTTPRequest(path, &t) +} + +// GetPairInfo gets info about a single pair +func (c *Coinbene) GetPairInfo(symbol string) (PairResponse, error) { + var resp PairResponse + params := url.Values{} + params.Set("symbol", symbol) + path := common.EncodeURLValues(c.API.Endpoints.URL+coinbeneAPIVersion+coinbenePairInfo, params) + return resp, c.SendHTTPRequest(path, &resp) +} + +// GetAllPairs gets all pairs on the exchange +func (c *Coinbene) GetAllPairs() (AllPairResponse, error) { + var a AllPairResponse + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneGetAllPairs + return a, c.SendHTTPRequest(path, &a) +} + +// GetUserBalance gets user balanace info +func (c *Coinbene) GetUserBalance() (UserBalanceResponse, error) { + var resp UserBalanceResponse + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneGetUserBalance + err := c.SendAuthHTTPRequest(http.MethodGet, path, coinbeneGetUserBalance, nil, &resp) + if err != nil { + return resp, err + } + if resp.Code != 200 { + return resp, fmt.Errorf(resp.Message) + } + return resp, nil +} + +// PlaceOrder creates an order +func (c *Coinbene) PlaceOrder(price, quantity float64, symbol, direction, clientID string) (PlaceOrderResponse, error) { + var resp PlaceOrderResponse + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbenePlaceOrder + params := url.Values{} + params.Set("symbol", symbol) + switch direction { + case sell: + params.Set("direction", "2") + case buy: + params.Set("direction", "1") + default: + return resp, + fmt.Errorf("passed in direction %s is invalid must be 'buy' or 'sell'", + direction) + } + + params.Set("price", strconv.FormatFloat(price, 'f', -1, 64)) + params.Set("quantity", strconv.FormatFloat(quantity, 'f', -1, 64)) + params.Set("clientId", clientID) + err := c.SendAuthHTTPRequest(http.MethodPost, + path, + coinbenePlaceOrder, + params, + &resp) + if err != nil { + return resp, err + } + if resp.Code != 200 { + return resp, fmt.Errorf(resp.Message) + } + return resp, nil +} + +// FetchOrderInfo gets order info +func (c *Coinbene) FetchOrderInfo(orderID string) (OrderInfoResponse, error) { + var resp OrderInfoResponse + params := url.Values{} + params.Set("orderId", orderID) + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneOrderInfo + err := c.SendAuthHTTPRequest(http.MethodGet, path, coinbeneOrderInfo, params, &resp) + if err != nil { + return resp, err + } + if resp.Code != 200 { + return resp, fmt.Errorf(resp.Message) + } + if resp.Order.OrderID != orderID { + return resp, fmt.Errorf("%s orderID doesn't match the returned orderID %s", + orderID, resp.Order.OrderID) + } + return resp, nil +} + +// RemoveOrder removes a given order +func (c *Coinbene) RemoveOrder(orderID string) (RemoveOrderResponse, error) { + var resp RemoveOrderResponse + params := url.Values{} + params.Set("orderId", orderID) + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneRemoveOrder + err := c.SendAuthHTTPRequest(http.MethodPost, path, coinbeneRemoveOrder, params, &resp) + if err != nil { + return resp, err + } + if resp.Code != 200 { + return resp, fmt.Errorf(resp.Message) + } + return resp, nil +} + +// FetchOpenOrders finds open orders +func (c *Coinbene) FetchOpenOrders(symbol string) (OpenOrderResponse, error) { + var resp OpenOrderResponse + params := url.Values{} + params.Set("symbol", symbol) + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneOpenOrders + for i := int64(1); ; i++ { + var temp OpenOrderResponse + params.Set("pageNum", strconv.FormatInt(i, 10)) + err := c.SendAuthHTTPRequest(http.MethodGet, path, coinbeneOpenOrders, params, &temp) + if err != nil { + return resp, err + } + if temp.Code != 200 { + return resp, fmt.Errorf(temp.Message) + } + for j := range temp.OpenOrders { + resp.OpenOrders = append(resp.OpenOrders, temp.OpenOrders[j]) + } + + if len(temp.OpenOrders) != 20 { + break + } + } + return resp, nil +} + +// FetchClosedOrders finds open orders +func (c *Coinbene) FetchClosedOrders(symbol, latestID string) (ClosedOrderResponse, error) { + var resp ClosedOrderResponse + params := url.Values{} + params.Set("symbol", symbol) + params.Set("latestOrderId", latestID) + path := c.API.Endpoints.URL + coinbeneAPIVersion + coinbeneClosedOrders + for i := int64(1); ; i++ { + var temp ClosedOrderResponse + params.Set("pageNum", strconv.FormatInt(i, 10)) + err := c.SendAuthHTTPRequest(http.MethodGet, path, coinbeneClosedOrders, params, &temp) + if err != nil { + return resp, err + } + if temp.Code != 200 { + return resp, fmt.Errorf(temp.Message) + } + for j := range temp.Data { + resp.Data = append(resp.Data, temp.Data[j]) + } + if len(temp.Data) != 20 { + break + } + } + return resp, nil +} + +// SendHTTPRequest sends an unauthenticated HTTP request +func (c *Coinbene) SendHTTPRequest(path string, result interface{}) error { + return c.SendPayload(http.MethodGet, + path, + nil, + nil, + &result, + false, + false, + c.Verbose, + c.HTTPDebugging, + c.HTTPRecording) +} + +// SendAuthHTTPRequest sends an authenticated HTTP request +func (c *Coinbene) SendAuthHTTPRequest(method, path, epPath string, params url.Values, result interface{}) error { + if params == nil { + params = url.Values{} + } + timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.999Z") + var finalBody io.Reader + var preSign string + switch { + case len(params) != 0 && method == http.MethodGet: + preSign = fmt.Sprintf("%s%s%s%s?%s", timestamp, method, coinbeneAuthPath, epPath, params.Encode()) + path = common.EncodeURLValues(path, params) + case len(params) != 0: + m := make(map[string]string) + for k, v := range params { + m[k] = strings.Join(v, "") + } + tempBody, err := json.Marshal(m) + if err != nil { + return err + } + finalBody = bytes.NewBufferString(string(tempBody)) + preSign = timestamp + method + coinbeneAuthPath + epPath + string(tempBody) + case len(params) == 0: + preSign = timestamp + method + coinbeneAuthPath + epPath + } + tempSign := crypto.GetHMAC(crypto.HashSHA256, + []byte(preSign), + []byte(c.API.Credentials.Secret)) + headers := make(map[string]string) + headers["Content-Type"] = "application/json" + headers["ACCESS-KEY"] = c.API.Credentials.Key + headers["ACCESS-SIGN"] = crypto.HexEncodeToString(tempSign) + headers["ACCESS-TIMESTAMP"] = timestamp + return c.SendPayload(method, + path, + headers, + finalBody, + &result, + true, + false, + c.Verbose, + c.HTTPDebugging, + c.HTTPRecording) +} diff --git a/exchanges/coinbene/coinbene_test.go b/exchanges/coinbene/coinbene_test.go new file mode 100644 index 00000000..f24255a0 --- /dev/null +++ b/exchanges/coinbene/coinbene_test.go @@ -0,0 +1,179 @@ +package coinbene + +import ( + "log" + "os" + "testing" + + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" +) + +// Please supply your own keys here for due diligence testing +const ( + testAPIKey = "" + testAPISecret = "" + canManipulateRealOrders = false + btcusdt = "BTC/USDT" +) + +var c Coinbene + +func TestMain(m *testing.M) { + c.SetDefaults() + cfg := config.GetConfig() + err := cfg.LoadConfig("../../testdata/configtest.json", true) + if err != nil { + log.Fatal(err) + } + coinbeneConfig, err := cfg.GetExchangeConfig("Coinbene") + if err != nil { + log.Fatal(err) + } + coinbeneConfig.API.AuthenticatedWebsocketSupport = true + coinbeneConfig.API.AuthenticatedSupport = true + coinbeneConfig.API.Credentials.Secret = testAPISecret + coinbeneConfig.API.Credentials.Key = testAPIKey + c.Setup(coinbeneConfig) + + os.Exit(m.Run()) +} + +func areTestAPIKeysSet() bool { + return c.AllowAuthenticatedRequest() +} + +func TestGetTicker(t *testing.T) { + t.Parallel() + _, err := c.GetTicker(btcusdt) + if err != nil { + t.Error(err) + } +} + +func TestGetOrderbook(t *testing.T) { + t.Parallel() + _, err := c.GetOrderbook(btcusdt, 100) + if err != nil { + t.Error(err) + } +} + +func TestGetTrades(t *testing.T) { + t.Parallel() + _, err := c.GetTrades(btcusdt) + if err != nil { + t.Error(err) + } +} + +func TestGetAllPairs(t *testing.T) { + t.Parallel() + _, err := c.GetAllPairs() + if err != nil { + t.Error(err) + } +} + +func TestGetPairInfo(t *testing.T) { + t.Parallel() + _, err := c.GetPairInfo(btcusdt) + if err != nil { + t.Error(err) + } +} + +func TestGetUserBalance(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := c.GetUserBalance() + if err != nil { + t.Error(err) + } +} + +func TestPlaceOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + _, err := c.PlaceOrder(140, 1, btcusdt, "buy", "") + if err != nil { + t.Error(err) + } +} + +func TestFetchOrderInfo(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := c.FetchOrderInfo("adfjashjgsag") + if err != nil { + t.Error(err) + } +} + +func TestRemoveOrder(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + _, err := c.RemoveOrder("adfjashjgsag") + if err != nil { + t.Error(err) + } +} + +func TestFetchOpenOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := c.FetchOpenOrders(btcusdt) + if err != nil { + t.Error(err) + } +} + +func TestFetchClosedOrders(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := c.FetchClosedOrders(btcusdt, "") + if err != nil { + t.Error(err) + } +} + +func TestUpdateTicker(t *testing.T) { + t.Parallel() + cp := currency.NewPairWithDelimiter("BTC", "USDT", "/") + _, err := c.UpdateTicker(cp, "spot") + if err != nil { + t.Error(err) + } +} + +func TestGetAccountInfo(t *testing.T) { + t.Parallel() + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := c.GetAccountInfo() + if err != nil { + t.Error(err) + } +} + +func TestUpdateOrderbook(t *testing.T) { + t.Parallel() + cp := currency.NewPairWithDelimiter("BTC", "USDT", "/") + _, err := c.UpdateOrderbook(cp, "spot") + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/coinbene/coinbene_types.go b/exchanges/coinbene/coinbene_types.go new file mode 100644 index 00000000..04dc583c --- /dev/null +++ b/exchanges/coinbene/coinbene_types.go @@ -0,0 +1,248 @@ +package coinbene + +// TickerData stores ticker data +type TickerData struct { + Symbol string `json:"symbol"` + LatestPrice float64 `json:"latestPrice,string"` + BestBid float64 `json:"bestBid,string"` + BestAsk float64 `json:"bestAsk,string"` + DailyHigh float64 `json:"high24h,string"` + DailyLow float64 `json:"low24h,string"` + DailyVolume float64 `json:"volume24h,string"` +} + +// TickerResponse stores ticker response data +type TickerResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + TickerData `json:"data"` +} + +// Orderbook stores orderbook info +type Orderbook struct { + Asks [][]string `json:"asks"` + Bids [][]string `json:"bids"` +} + +// OrderbookResponse stores data from fetched orderbooks +type OrderbookResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + Orderbook `json:"data"` +} + +// TradeResponse stores trade data +type TradeResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + Trades [][]string `json:"data"` +} + +// AllPairData stores pair data +type AllPairData struct { + Symbol string `json:"symbol"` + BaseAsset string `json:"baseAsset"` + QuoteAsset string `json:"quoteAsset"` + PricePrecision int64 `json:"pricePrecision,string"` + AmountPrecision int64 `json:"amountPrecision,string"` + TakerFeeRate float64 `json:"takerFeeRate,string"` + MakerFeeRate float64 `json:"makerFeeRate,string"` + MinAmount float64 `json:"minAmount,string"` + Site string `json:"site"` + PriceFluctuation float64 `json:"priceFluctuation,string"` +} + +// AllPairResponse stores data for all pairs enabled on exchange +type AllPairResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + Data []AllPairData `json:"data"` +} + +// PairResponse stores data for a single queried pair +type PairResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + Data AllPairData `json:"data"` +} + +// UserBalanceData stores user balance data +type UserBalanceData struct { + Asset string `json:"asset"` + Available float64 `json:"available,string"` + Reserved float64 `json:"reserved,string"` + Total float64 `json:"total,string"` +} + +// UserBalanceResponse stores user balance data +type UserBalanceResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + Data []UserBalanceData `json:"data"` +} + +// PlaceOrderResponse stores data for a placed order +type PlaceOrderResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + Status string `json:"status"` + Timestamp int64 `json:"timestamp"` + OrderID string `json:"orderid"` +} + +// OrderInfoData stores order info +type OrderInfoData struct { + OrderID string `json:"orderId"` + BaseAsset string `json:"baseAsset"` + QuoteAsset string `json:"quoteAsset"` + OrderType string `json:"orderDirection"` + Quantity float64 `json:"quntity,string"` + Amount float64 `json:"amout,string"` + FilledAmount float64 `json:"filledAmount"` + TakerRate float64 `json:"takerFeeRate,string"` + MakerRate float64 `json:"makerRate,string"` + AvgPrice float64 `json:"avgPrice,string"` + OrderPrice float64 `json:"orderPrice,string"` + OrderStatus string `json:"orderStatus"` + OrderTime string `json:"orderTime"` + TotalFee float64 `json:"totalFee"` +} + +// OrderInfoResponse stores orderinfo data +type OrderInfoResponse struct { + Order OrderInfoData `json:"data"` + Code int64 `json:"code"` + Message string `json:"message"` +} + +// RemoveOrderResponse stores data for the remove request +type RemoveOrderResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + OrderID string `json:"data"` +} + +// OpenOrderResponse stores data for open orders +type OpenOrderResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + OpenOrders []OrderInfoData `json:"data"` +} + +// ClosedOrderResponse stores data for closed orders +type ClosedOrderResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + Data []OrderInfoData `json:"data"` +} + +// WsSub stores subscription data +type WsSub struct { + Operation string `json:"op"` + Arguments []string `json:"args"` +} + +// WsTickerData stores websocket ticker data +type WsTickerData struct { + Symbol string `json:"symbol"` + LastPrice float64 `json:"lastPrice,string"` + MarkPrice float64 `json:"markPrice,string"` + BestAskPrice float64 `json:"bestAskPrice,string"` + BestBidPrice float64 `json:"bestBidPrice,string"` + BestAskVolume float64 `json:"bestAskVolume,string"` + BestBidVolume float64 `json:"bestBidVolume,string"` + High24h float64 `json:"high24h,string"` + Low24h float64 `json:"low24h,string"` + Volume24h float64 `json:"volume,string"` + Timestamp string `json:"timestamp"` +} + +// WsTicker stores websocket ticker +type WsTicker struct { + Topic string `json:"topic"` + Data []WsTickerData `json:"data"` +} + +// WsTradeList stores websocket tradelist data +type WsTradeList struct { + Topic string `json:"topic"` + Data [][]string `json:"data"` +} + +// WsOrderbook stores websocket orderbook data +type WsOrderbook struct { + Topic string `json:"topic"` + Action string `json:"action"` + Data []Orderbook `json:"data"` + Version int64 `json:"version,string"` + Timestamp string `json:"timestamp"` +} + +// WsKline stores websocket kline data +type WsKline struct { + Topic string `json:"topic"` + Data [][]interface{} `json:"data"` +} + +// WsUserData stores websocket user data +type WsUserData struct { + Asset string `json:"string"` + Available float64 `json:"availableBalance"` + Locked float64 `json:"frozenBalance"` + Total float64 `json:"balance"` + Timestamp string `json:"timestamp"` +} + +// WsUserInfo stores websocket user info +type WsUserInfo struct { + Topic string `json:"topic"` + Data []WsUserData `json:"data"` +} + +// WsPositionData stores websocket info on user's position +type WsPositionData struct { + AvailableQuantity float64 `json:"availableQuantity"` + AveragePrice float64 `json:"avgPrice"` + Leverage float64 `json:"leverage"` + LiquidationPrice float64 `json:"liquidationPrice"` + MarkPrice float64 `json:"markPrice"` + PositionMargin float64 `json:"positionMargin"` + Quantity float64 `json:"quantity"` + RealisedPNL float64 `json:"realisedPnl"` + Side string `json:"side"` + Symbol string `json:"symbol"` + MarginMode int64 `json:"marginMode"` + CreateTime string `json:"createTime"` +} + +// WsPosition stores websocket info on user's positions +type WsPosition struct { + Topic string `json:"topic"` + Data []WsPositionData `json:"data"` +} + +// WsOrderData stores websocket user order data +type WsOrderData struct { + OrderID string `json:"orderId"` + Direction string `json:"direction"` + Leverage float64 `json:"leverage"` + Symbol string `json:"symbol"` + OrderType string `json:"orderType"` + Quantity float64 `json:"quantity"` + OrderPrice float64 `json:"orderPrice"` + OrderValue float64 `json:"orderValue"` + Fee float64 `json:"fee"` + FilledQuantity float64 `json:"filledQuantity"` + AveragePrice float64 `json:"averagePrice"` + OrderTime string `json:"orderTime"` + Status string `json:"status"` + LastFillQuantity float64 `json:"lastFillQuantity"` + LastFillPrice float64 `json:"lastFillPrice"` + LastFillTime string `json:"lastFillTime"` +} + +// WsUserOrders stores websocket user orders' data +type WsUserOrders struct { + Topic string `json:"topic"` + Data []WsOrderData `json:"data"` +} diff --git a/exchanges/coinbene/coinbene_websocket.go b/exchanges/coinbene/coinbene_websocket.go new file mode 100644 index 00000000..791cabae --- /dev/null +++ b/exchanges/coinbene/coinbene_websocket.go @@ -0,0 +1,353 @@ +package coinbene + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wsorderbook" +) + +const ( + coinbeneWsURL = "wss://ws-contract.coinbene.vip/openapi/ws" + event = "event" + topic = "topic" +) + +// WsConnect connects to websocket +func (c *Coinbene) WsConnect() error { + if !c.Websocket.IsEnabled() || !c.IsEnabled() { + return errors.New(wshandler.WebsocketNotEnabled) + } + var dialer websocket.Dialer + err := c.WebsocketConn.Dial(&dialer, http.Header{}) + if err != nil { + return err + } + go c.WsDataHandler() + if c.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + err = c.Login() + if err != nil { + c.Websocket.DataHandler <- err + c.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + } + c.GenerateDefaultSubscriptions() + + return nil +} + +// GenerateDefaultSubscriptions generates stuff +func (c *Coinbene) GenerateDefaultSubscriptions() { + var channels = []string{"orderBook.%s.100", "tradeList.%s", "ticker.%s", "kline.%s"} + var subscriptions []wshandler.WebsocketChannelSubscription + pairs := c.GetEnabledPairs(asset.PerpetualSwap) + for x := range channels { + for y := range pairs { + pairs[y].Delimiter = "" + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ + Channel: fmt.Sprintf(channels[x], pairs[y]), + Currency: pairs[y], + }) + } + } + c.Websocket.SubscribeToChannels(subscriptions) +} + +// GenerateAuthSubs generates auth subs +func (c *Coinbene) GenerateAuthSubs() { + var subscriptions []wshandler.WebsocketChannelSubscription + var sub wshandler.WebsocketChannelSubscription + var userChannels = []string{"user.account", "user.position", "user.order"} + for z := range userChannels { + sub.Channel = userChannels[z] + subscriptions = append(subscriptions, sub) + } + c.Websocket.SubscribeToChannels(subscriptions) +} + +// WsDataHandler handles websocket data +func (c *Coinbene) WsDataHandler() { + c.Websocket.Wg.Add(1) + + defer c.Websocket.Wg.Done() + + for { + select { + case <-c.Websocket.ShutdownC: + return + + default: + stream, err := c.WebsocketConn.ReadMessage() + if err != nil { + c.Websocket.DataHandler <- err + return + } + c.Websocket.TrafficAlert <- struct{}{} + if string(stream.Raw) == "ping" { + c.WebsocketConn.Lock() + c.WebsocketConn.Connection.WriteMessage(websocket.TextMessage, []byte("pong")) + c.WebsocketConn.Unlock() + continue + } + var result map[string]interface{} + err = common.JSONDecode(stream.Raw, &result) + if err != nil { + c.Websocket.DataHandler <- err + } + _, ok := result[event] + switch { + case ok && (result[event].(string) == "subscribe" || result[event].(string) == "unsubscribe"): + continue + case ok && result[event].(string) == "error": + c.Websocket.DataHandler <- fmt.Errorf("message: %s. code: %v", result["message"], result["code"]) + continue + } + if ok && strings.Contains(result[event].(string), "login") { + if result["success"].(bool) { + c.Websocket.SetCanUseAuthenticatedEndpoints(true) + c.GenerateAuthSubs() + continue + } + c.Websocket.SetCanUseAuthenticatedEndpoints(false) + c.Websocket.DataHandler <- fmt.Errorf("message: %s. code: %v", result["message"], result["code"]) + continue + } + switch { + case strings.Contains(result[topic].(string), "ticker"): + var ticker WsTicker + err = common.JSONDecode(stream.Raw, &ticker) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + for x := range ticker.Data { + c.Websocket.DataHandler <- wshandler.TickerData{ + Volume: ticker.Data[x].Volume24h, + Last: ticker.Data[x].LastPrice, + High: ticker.Data[x].High24h, + Low: ticker.Data[x].Low24h, + Pair: currency.NewPairFromFormattedPairs(ticker.Data[x].Symbol, + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)), + Exchange: c.Name, + AssetType: asset.PerpetualSwap, + } + } + case strings.Contains(result[topic].(string), "tradeList"): + var tradeList WsTradeList + err = common.JSONDecode(stream.Raw, &tradeList) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + var t time.Time + var price, amount float64 + t, err = time.Parse(time.RFC3339, tradeList.Data[0][3]) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + price, err = strconv.ParseFloat(tradeList.Data[0][0], 64) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + amount, err = strconv.ParseFloat(tradeList.Data[0][2], 64) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + p := strings.Replace(tradeList.Topic, "tradeList.", "", 1) + c.Websocket.DataHandler <- wshandler.TradeData{ + CurrencyPair: currency.NewPairFromFormattedPairs(p, + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)), + Timestamp: t, + Price: price, + Amount: amount, + Exchange: c.Name, + AssetType: asset.PerpetualSwap, + Side: tradeList.Data[0][1], + } + case strings.Contains(result[topic].(string), "orderBook"): + var orderBook WsOrderbook + err = common.JSONDecode(stream.Raw, &orderBook) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + p := strings.Replace(orderBook.Topic, "tradeList.", "", 1) + cp := currency.NewPairFromFormattedPairs(p, + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)) + var amount, price float64 + var asks, bids []orderbook.Item + for i := range orderBook.Data[0].Asks { + amount, err = strconv.ParseFloat(orderBook.Data[0].Asks[i][1], 64) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + price, err = strconv.ParseFloat(orderBook.Data[0].Asks[i][0], 64) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + asks = append(asks, orderbook.Item{ + Amount: amount, + Price: price, + }) + } + for j := range orderBook.Data[0].Bids { + amount, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][1], 64) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + price, err = strconv.ParseFloat(orderBook.Data[0].Bids[j][0], 64) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + bids = append(bids, orderbook.Item{ + Amount: amount, + Price: price, + }) + } + if orderBook.Action == "insert" { + var newOB orderbook.Base + newOB.Asks = asks + newOB.Bids = bids + newOB.AssetType = asset.PerpetualSwap + newOB.Pair = cp + newOB.ExchangeName = c.Name + err = c.Websocket.Orderbook.LoadSnapshot(&newOB) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, + Asset: asset.PerpetualSwap, + Exchange: c.Name, + } + } else if orderBook.Action == "update" { + newOB := wsorderbook.WebsocketOrderbookUpdate{ + Asks: asks, + Bids: bids, + Asset: asset.PerpetualSwap, + Pair: cp, + UpdateID: orderBook.Version, + } + err = c.Websocket.Orderbook.Update(&newOB) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: newOB.Pair, + Asset: asset.PerpetualSwap, + Exchange: c.Name, + } + } + case strings.Contains(result[topic].(string), "kline"): + var kline WsKline + var tempFloat float64 + var tempKline []float64 + err = common.JSONDecode(stream.Raw, &kline) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + for x := 2; x < len(kline.Data[0]); x++ { + tempFloat, err = strconv.ParseFloat(kline.Data[0][x].(string), 64) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + tempKline = append(tempKline, tempFloat) + } + p := currency.NewPairFromFormattedPairs(kline.Data[0][0].(string), + c.GetEnabledPairs(asset.PerpetualSwap), + c.GetPairFormat(asset.PerpetualSwap, true)) + c.Websocket.DataHandler <- wshandler.KlineData{ + Timestamp: time.Unix(int64(kline.Data[0][1].(float64)), 0), + Pair: p, + AssetType: asset.PerpetualSwap, + Exchange: c.Name, + OpenPrice: tempKline[0], + ClosePrice: tempKline[1], + HighPrice: tempKline[2], + LowPrice: tempKline[3], + Volume: tempKline[4], + } + case strings.Contains(result[topic].(string), "user.account"): + var userinfo WsUserInfo + err = common.JSONDecode(stream.Raw, &userinfo) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + c.Websocket.DataHandler <- userinfo + case strings.Contains(result[topic].(string), "user.position"): + var position WsPosition + err = common.JSONDecode(stream.Raw, &position) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + c.Websocket.DataHandler <- position + case strings.Contains(result[topic].(string), "user.order"): + var orders WsUserOrders + err = common.JSONDecode(stream.Raw, &orders) + if err != nil { + c.Websocket.DataHandler <- err + continue + } + c.Websocket.DataHandler <- orders + default: + c.Websocket.DataHandler <- fmt.Errorf("%s - unhandled response '%s'", c.Name, stream.Raw) + } + } + } +} + +// Subscribe sends a websocket message to receive data from the channel +func (c *Coinbene) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { + var sub WsSub + sub.Operation = "subscribe" + sub.Arguments = []string{channelToSubscribe.Channel} + return c.WebsocketConn.SendMessage(sub) +} + +// Unsubscribe sends a websocket message to receive data from the channel +func (c *Coinbene) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { + var sub WsSub + sub.Operation = "unsubscribe" + sub.Arguments = []string{channelToSubscribe.Channel} + return c.WebsocketConn.SendMessage(sub) +} + +// Login logs in +func (c *Coinbene) Login() error { + var sub WsSub + expTime := time.Now().Add(time.Minute * 10).Format("2006-01-02T15:04:05Z") + signMsg := expTime + http.MethodGet + "/login" + tempSign := crypto.GetHMAC(crypto.HashSHA256, + []byte(signMsg), + []byte(c.API.Credentials.Secret)) + sign := crypto.HexEncodeToString(tempSign) + sub.Operation = "login" + sub.Arguments = []string{c.API.Credentials.Key, expTime, sign} + return c.WebsocketConn.SendMessage(sub) +} diff --git a/exchanges/coinbene/coinbene_wrapper.go b/exchanges/coinbene/coinbene_wrapper.go new file mode 100644 index 00000000..d88c751f --- /dev/null +++ b/exchanges/coinbene/coinbene_wrapper.go @@ -0,0 +1,620 @@ +package coinbene + +import ( + "fmt" + "strconv" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// GetDefaultConfig returns a default exchange config +func (c *Coinbene) GetDefaultConfig() (*config.ExchangeConfig, error) { + c.SetDefaults() + exchCfg := new(config.ExchangeConfig) + exchCfg.Name = c.Name + exchCfg.HTTPTimeout = exchange.DefaultHTTPTimeout + exchCfg.BaseCurrencies = c.BaseCurrencies + + err := c.SetupDefaults(exchCfg) + if err != nil { + return nil, err + } + + if c.Features.Supports.RESTCapabilities.AutoPairUpdates { + err = c.UpdateTradablePairs(true) + if err != nil { + return nil, err + } + } + + return exchCfg, nil +} + +// SetDefaults sets the basic defaults for Coinbene +func (c *Coinbene) SetDefaults() { + c.Name = "Coinbene" + c.Enabled = true + c.Verbose = true + c.API.CredentialsValidator.RequiresKey = true + c.API.CredentialsValidator.RequiresSecret = true + + c.CurrencyPairs = currency.PairsManager{ + AssetTypes: asset.Items{ + asset.Spot, + }, + UseGlobalFormat: true, + RequestFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "/", + }, + ConfigFormat: ¤cy.PairFormat{ + Uppercase: true, + Delimiter: "/", + }, + } + + c.Features = exchange.Features{ + Supports: exchange.FeaturesSupported{ + REST: true, + Websocket: false, // Purposely disabled until SWAP is supported + RESTCapabilities: protocol.Features{ + TickerFetching: true, + TradeFetching: true, + OrderbookFetching: true, + AccountBalance: true, + AutoPairUpdates: true, + GetOrder: true, + GetOrders: true, + CancelOrder: true, + CancelOrders: true, + SubmitOrder: true, + TradeFee: true, + }, + WebsocketCapabilities: protocol.Features{ + TickerFetching: true, + AccountBalance: true, + AccountInfo: true, + OrderbookFetching: true, + TradeFetching: true, + KlineFetching: true, + Subscribe: true, + Unsubscribe: true, + AuthenticatedEndpoints: true, + }, + WithdrawPermissions: exchange.NoFiatWithdrawals | + exchange.WithdrawCryptoViaWebsiteOnly, + }, + Enabled: exchange.FeaturesEnabled{ + AutoPairUpdates: true, + }, + } + c.Requester = request.New(c.Name, + request.NewRateLimit(time.Minute, authRateLimit), + request.NewRateLimit(time.Second, unauthRateLimit), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + + c.API.Endpoints.URLDefault = coinbeneAPIURL + c.API.Endpoints.URL = c.API.Endpoints.URLDefault + c.API.Endpoints.WebsocketURL = coinbeneWsURL + c.Websocket = wshandler.New() + c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + c.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit +} + +// Setup takes in the supplied exchange configuration details and sets params +func (c *Coinbene) Setup(exch *config.ExchangeConfig) error { + if !exch.Enabled { + c.SetEnabled(false) + return nil + } + + err := c.SetupDefaults(exch) + if err != nil { + return err + } + + // TO-DO: Remove this once SWAP is supported + if exch.Features.Enabled.Websocket { + log.Warnf(log.ExchangeSys, + "%s websocket only supports SWAP which GoCryptoTrader currently "+ + "does not. Disabling.\n", + c.Name) + exch.Features.Enabled.Websocket = false + } + + err = c.Websocket.Setup( + &wshandler.WebsocketSetup{ + Enabled: exch.Features.Enabled.Websocket, + Verbose: exch.Verbose, + AuthenticatedWebsocketAPISupport: exch.API.AuthenticatedWebsocketSupport, + WebsocketTimeout: exch.WebsocketTrafficTimeout, + DefaultURL: coinbeneWsURL, + ExchangeName: exch.Name, + RunningURL: exch.API.Endpoints.WebsocketURL, + Connector: c.WsConnect, + Subscriber: c.Subscribe, + UnSubscriber: c.Unsubscribe, + }) + if err != nil { + return err + } + + c.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: c.Name, + URL: c.Websocket.GetWebsocketURL(), + ProxyURL: c.Websocket.GetProxyAddress(), + Verbose: c.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + + c.Websocket.Orderbook.Setup( + exch.WebsocketOrderbookBufferLimit, + true, + true, + false, + false, + exch.Name) + + return nil +} + +// Start starts the Coinbene go routine +func (c *Coinbene) Start(wg *sync.WaitGroup) { + wg.Add(1) + go func() { + c.Run() + wg.Done() + }() +} + +// Run implements the Coinbene wrapper +func (c *Coinbene) Run() { + if c.Verbose { + log.Debugf(log.ExchangeSys, + "%s Websocket: %s. (url: %s).\n", + c.Name, + common.IsEnabled(c.Websocket.IsEnabled()), + c.Websocket.GetWebsocketURL(), + ) + c.PrintEnabledPairs() + } + + if !c.GetEnabledFeatures().AutoPairUpdates { + return + } + + err := c.UpdateTradablePairs(false) + if err != nil { + log.Errorf(log.ExchangeSys, + "%s Failed to update tradable pairs. Error: %s", + c.Name, + err) + } +} + +// FetchTradablePairs returns a list of exchange tradable pairs +func (c *Coinbene) FetchTradablePairs(a asset.Item) ([]string, error) { + pairs, err := c.GetAllPairs() + if err != nil { + return nil, err + } + + var currencies []string + for x := range pairs.Data { + currencies = append(currencies, pairs.Data[x].Symbol) + } + return currencies, nil +} + +// UpdateTradablePairs updates the exchanges available pairs and stores +// them +func (c *Coinbene) UpdateTradablePairs(forceUpdate bool) error { + pairs, err := c.FetchTradablePairs(asset.Spot) + if err != nil { + return err + } + + return c.UpdatePairs(currency.NewPairsFromStrings(pairs), + asset.Spot, + false, + forceUpdate) +} + +// UpdateTicker updates and returns the ticker for a currency pair +func (c *Coinbene) UpdateTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + var resp ticker.Price + allPairs := c.GetEnabledPairs(assetType) + for x := range allPairs { + tempResp, err := c.GetTicker(c.FormatExchangeCurrency(allPairs[x], + assetType).String()) + if err != nil { + return resp, err + } + resp.Pair = allPairs[x] + resp.Last = tempResp.TickerData.LatestPrice + resp.High = tempResp.TickerData.DailyHigh + resp.Low = tempResp.TickerData.DailyLow + resp.Bid = tempResp.TickerData.BestBid + resp.Ask = tempResp.TickerData.BestAsk + resp.Volume = tempResp.TickerData.DailyVolume + resp.LastUpdated = time.Now() + err = ticker.ProcessTicker(c.Name, &resp, assetType) + if err != nil { + return resp, err + } + } + return ticker.GetTicker(c.Name, p, assetType) +} + +// FetchTicker returns the ticker for a currency pair +func (c *Coinbene) FetchTicker(p currency.Pair, assetType asset.Item) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(c.Name, p, assetType) + if err != nil { + return c.UpdateTicker(p, assetType) + } + return tickerNew, nil +} + +// FetchOrderbook returns orderbook base on the currency pair +func (c *Coinbene) FetchOrderbook(currency currency.Pair, assetType asset.Item) (orderbook.Base, error) { + ob, err := orderbook.Get(c.Name, currency, assetType) + if err != nil { + return c.UpdateOrderbook(currency, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (c *Coinbene) UpdateOrderbook(p currency.Pair, assetType asset.Item) (orderbook.Base, error) { + var resp orderbook.Base + tempResp, err := c.GetOrderbook( + c.FormatExchangeCurrency(p, assetType).String(), + 100, + ) + if err != nil { + return resp, err + } + resp.ExchangeName = c.Name + resp.Pair = p + resp.AssetType = assetType + var amount, price float64 + for i := range tempResp.Orderbook.Asks { + amount, err = strconv.ParseFloat(tempResp.Orderbook.Asks[i][1], 64) + if err != nil { + return resp, err + } + price, err = strconv.ParseFloat(tempResp.Orderbook.Asks[i][0], 64) + if err != nil { + return resp, err + } + resp.Asks = append(resp.Asks, orderbook.Item{ + Price: price, + Amount: amount}) + } + for j := range tempResp.Orderbook.Bids { + amount, err = strconv.ParseFloat(tempResp.Orderbook.Bids[j][1], 64) + if err != nil { + return resp, err + } + price, err = strconv.ParseFloat(tempResp.Orderbook.Bids[j][0], 64) + if err != nil { + return resp, err + } + resp.Bids = append(resp.Bids, orderbook.Item{ + Price: price, + Amount: amount}) + } + err = resp.Process() + if err != nil { + return resp, err + } + return orderbook.Get(c.Name, p, assetType) +} + +// GetAccountInfo retrieves balances for all enabled currencies for the +// Coinbene exchange +func (c *Coinbene) GetAccountInfo() (exchange.AccountInfo, error) { + var info exchange.AccountInfo + data, err := c.GetUserBalance() + if err != nil { + return info, err + } + var account exchange.Account + for key := range data.Data { + c := currency.NewCode(data.Data[key].Asset) + hold := data.Data[key].Reserved + available := data.Data[key].Available + account.Currencies = append(account.Currencies, + exchange.AccountCurrencyInfo{CurrencyName: c, + TotalValue: hold + available, + Hold: hold}) + } + info.Accounts = append(info.Accounts, account) + info.Exchange = c.Name + return info, nil +} + +// GetFundingHistory returns funding history, deposits and +// withdrawals +func (c *Coinbene) GetFundingHistory() ([]exchange.FundHistory, error) { + return nil, common.ErrFunctionNotSupported +} + +// GetExchangeHistory returns historic trade data since exchange opening. +func (c *Coinbene) GetExchangeHistory(p currency.Pair, assetType asset.Item) ([]exchange.TradeHistory, error) { + return nil, common.ErrFunctionNotSupported +} + +// SubmitOrder submits a new order +func (c *Coinbene) SubmitOrder(s *order.Submit) (order.SubmitResponse, error) { + var resp order.SubmitResponse + if err := s.Validate(); err != nil { + return resp, err + } + + if s.OrderSide != order.Buy && s.OrderSide != order.Sell { + return resp, + fmt.Errorf("%s orderside is not supported by this exchange", + s.OrderSide) + } + + if s.OrderType != order.Limit { + return resp, fmt.Errorf("only limit order is supported by this exchange") + } + tempResp, err := c.PlaceOrder(s.Price, + s.Amount, + c.FormatExchangeCurrency(s.Pair, asset.Spot).String(), + s.OrderType.String(), + s.ClientID) + if err != nil { + return resp, err + } + resp.IsOrderPlaced = true + resp.OrderID = tempResp.OrderID + return resp, nil +} + +// ModifyOrder will allow of changing orderbook placement and limit to +// market conversion +func (c *Coinbene) ModifyOrder(action *order.Modify) (string, error) { + return "", common.ErrFunctionNotSupported +} + +// CancelOrder cancels an order by its corresponding ID number +func (c *Coinbene) CancelOrder(order *order.Cancel) error { + _, err := c.RemoveOrder(order.OrderID) + return err +} + +// CancelAllOrders cancels all orders associated with a currency pair +func (c *Coinbene) CancelAllOrders(orderCancellation *order.Cancel) (order.CancelAllResponse, error) { + var resp order.CancelAllResponse + tempMap := make(map[string]string) + orders, err := c.FetchOpenOrders( + c.FormatExchangeCurrency(orderCancellation.CurrencyPair, + asset.Spot).String(), + ) + if err != nil { + return resp, err + } + for x := range orders.OpenOrders { + _, err := c.RemoveOrder(orders.OpenOrders[x].OrderID) + if err != nil { + tempMap[orders.OpenOrders[x].OrderID] = "Failed" + } else { + tempMap[orders.OpenOrders[x].OrderID] = "Success" + } + } + resp.Status = tempMap + return resp, nil +} + +// GetOrderInfo returns information on a current open order +func (c *Coinbene) GetOrderInfo(orderID string) (order.Detail, error) { + var resp order.Detail + tempResp, err := c.FetchOrderInfo(orderID) + if err != nil { + return resp, err + } + var t time.Time + resp.Exchange = c.Name + resp.ID = orderID + resp.CurrencyPair = currency.NewPairWithDelimiter(tempResp.Order.BaseAsset, + "/", + tempResp.Order.QuoteAsset) + t, err = time.Parse(time.RFC3339, tempResp.Order.OrderTime) + if err != nil { + return resp, err + } + resp.Price = tempResp.Order.OrderPrice + resp.OrderDate = t + resp.ExecutedAmount = tempResp.Order.FilledAmount + resp.Fee = tempResp.Order.TotalFee + return resp, nil +} + +// GetDepositAddress returns a deposit address for a specified currency +func (c *Coinbene) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) { + return "", common.ErrFunctionNotSupported +} + +// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is +// submitted +func (c *Coinbene) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.CryptoWithdrawRequest) (string, error) { + return "", common.ErrFunctionNotSupported +} + +// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is +// submitted +func (c *Coinbene) WithdrawFiatFunds(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { + return "", common.ErrFunctionNotSupported +} + +// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is +// submitted +func (c *Coinbene) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.FiatWithdrawRequest) (string, error) { + return "", common.ErrFunctionNotSupported +} + +// GetWebsocket returns a pointer to the exchange websocket +func (c *Coinbene) GetWebsocket() (*wshandler.Websocket, error) { + return c.Websocket, nil +} + +// GetActiveOrders retrieves any orders that are active/open +func (c *Coinbene) GetActiveOrders(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { + var resp []order.Detail + var tempResp order.Detail + var tempData OpenOrderResponse + if len(getOrdersRequest.Currencies) == 0 { + allPairs, err := c.GetAllPairs() + if err != nil { + return resp, err + } + for a := range allPairs.Data { + getOrdersRequest.Currencies = append(getOrdersRequest.Currencies, currency.NewPairFromString(allPairs.Data[a].Symbol)) + } + } + var err error + for x := range getOrdersRequest.Currencies { + tempData, err = c.FetchOpenOrders( + c.FormatExchangeCurrency( + getOrdersRequest.Currencies[x], + asset.Spot).String(), + ) + if err != nil { + return resp, err + } + var t time.Time + for y := range tempData.OpenOrders { + tempResp.Exchange = c.Name + tempResp.CurrencyPair = getOrdersRequest.Currencies[x] + tempResp.OrderSide = buy + if tempData.OpenOrders[y].OrderType == sell { + tempResp.OrderSide = sell + } + t, err = time.Parse(time.RFC3339, tempData.OpenOrders[y].OrderTime) + if err != nil { + return resp, err + } + tempResp.OrderDate = t + tempResp.Status = order.Status(tempData.OpenOrders[y].OrderStatus) + tempResp.Price = tempData.OpenOrders[y].OrderPrice + tempResp.Amount = tempData.OpenOrders[y].Amount + tempResp.ExecutedAmount = tempData.OpenOrders[y].FilledAmount + tempResp.RemainingAmount = tempData.OpenOrders[y].Amount - tempData.OpenOrders[y].FilledAmount + tempResp.Fee = tempData.OpenOrders[y].TotalFee + resp = append(resp, tempResp) + } + } + return resp, nil +} + +// GetOrderHistory retrieves account order information +// Can Limit response to specific order status +func (c *Coinbene) GetOrderHistory(getOrdersRequest *order.GetOrdersRequest) ([]order.Detail, error) { + var resp []order.Detail + var tempResp order.Detail + var tempData ClosedOrderResponse + if len(getOrdersRequest.Currencies) == 0 { + allPairs, err := c.GetAllPairs() + if err != nil { + return resp, err + } + for a := range allPairs.Data { + getOrdersRequest.Currencies = append(getOrdersRequest.Currencies, currency.NewPairFromString(allPairs.Data[a].Symbol)) + } + } + var err error + for x := range getOrdersRequest.Currencies { + tempData, err = c.FetchClosedOrders( + c.FormatExchangeCurrency( + getOrdersRequest.Currencies[x], + asset.Spot).String(), + "", + ) + if err != nil { + return resp, err + } + var t time.Time + for y := range tempData.Data { + tempResp.Exchange = c.Name + tempResp.CurrencyPair = getOrdersRequest.Currencies[x] + tempResp.OrderSide = order.Buy + if tempData.Data[y].OrderType == sell { + tempResp.OrderSide = order.Sell + } + t, err = time.Parse(time.RFC3339, tempData.Data[y].OrderTime) + if err != nil { + return resp, err + } + tempResp.OrderDate = t + tempResp.Status = order.Status(tempData.Data[y].OrderStatus) + tempResp.Price = tempData.Data[y].OrderPrice + tempResp.Amount = tempData.Data[y].Amount + tempResp.ExecutedAmount = tempData.Data[y].FilledAmount + tempResp.RemainingAmount = tempData.Data[y].Amount - tempData.Data[y].FilledAmount + tempResp.Fee = tempData.Data[y].TotalFee + resp = append(resp, tempResp) + } + } + return resp, nil +} + +// GetFeeByType returns an estimate of fee based on the type of transaction +func (c *Coinbene) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { + var fee float64 + tempData, err := c.GetPairInfo( + c.FormatExchangeCurrency( + feeBuilder.Pair, asset.Spot).String(), + ) + if err != nil { + return fee, err + } + switch feeBuilder.IsMaker { + case true: + fee = feeBuilder.PurchasePrice * feeBuilder.Amount * tempData.Data.MakerFeeRate + case false: + fee = feeBuilder.PurchasePrice * feeBuilder.Amount * tempData.Data.TakerFeeRate + } + return fee, nil +} + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (c *Coinbene) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + c.Websocket.SubscribeToChannels(channels) + return nil +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (c *Coinbene) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + c.Websocket.RemoveSubscribedChannels(channels) + return nil +} + +// GetSubscriptions returns a copied list of subscriptions +func (c *Coinbene) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { + return c.Websocket.GetSubscriptions(), nil +} + +// AuthenticateWebsocket sends an authentication message to the websocket +func (c *Coinbene) AuthenticateWebsocket() error { + return c.Login() +} diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 37eabdc7..42cac143 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -623,11 +623,25 @@ func (e *Base) SetAPIURL() error { if e.Config.API.Endpoints.URL == "" || e.Config.API.Endpoints.URLSecondary == "" { return fmt.Errorf("exchange %s: SetAPIURL error. URL vals are empty", e.Name) } + + checkInsecureEndpoint := func(endpoint string) { + if !strings.Contains(endpoint, "https") { + return + } + log.Warnf(log.ExchangeSys, + "%s is using HTTP instead of HTTPS [%s] for API functionality, an"+ + " attacker could eavesdrop on this connection. Use at your"+ + " own risk.", + e.Name, endpoint) + } + if e.Config.API.Endpoints.URL != config.APIURLNonDefaultMessage { e.API.Endpoints.URL = e.Config.API.Endpoints.URL + checkInsecureEndpoint(e.API.Endpoints.URL) } if e.Config.API.Endpoints.URLSecondary != config.APIURLNonDefaultMessage { e.API.Endpoints.URLSecondary = e.Config.API.Endpoints.URLSecondary + checkInsecureEndpoint(e.API.Endpoints.URLSecondary) } return nil } diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index ae1c53ea..5aab5f4a 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -1176,6 +1176,13 @@ func TestSetAPIURL(t *testing.T) { if tester.GetAPIURLSecondaryDefault() != testURLSecondaryDefault { t.Error("incorrect return URL") } + + tester.Config.API.Endpoints.URL = "http://insecureino.com" + tester.Config.API.Endpoints.URLSecondary = tester.Config.API.Endpoints.URL + err = tester.SetAPIURL() + if err != nil { + t.Error(err) + } } func BenchmarkSetAPIURL(b *testing.B) { diff --git a/testdata/configtest.json b/testdata/configtest.json index 58e54a13..041a3654 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -1400,6 +1400,52 @@ "supportedCurrencies": "" } ] + }, + { + "name": "Coinbene", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 0, + "websocketResponseCheckTimeout": 0, + "websocketResponseMaxLimit": 0, + "websocketOrderbookBufferLimit": 0, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "", + "apiUrlSecondary": "", + "proxyAddress": "", + "websocketUrl": "", + "availablePairs": "BTC/USDT", + "enabledPairs": "BTC/USDT", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": true, + "delimiter": "/" + }, + "requestCurrencyPairFormat": { + "uppercase": true, + "delimiter": "/" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] } ], "bankAccounts": [