Websocket request-response correlation (#328)

* Establishes new websocket functionality. Begins the creation of the websocket request

* Adding a wrapper over gorilla websocket connect,send,receive to handle ID messages. Doesn't work

* Successfully moved exchange_websocket into its own wshandler namespace. oof

* Sets up ZB to use a round trip WS request system

* Adds Kraken ID support to subscriptions. Renames duplicate func name UnsubscribeToChannels to RemoveSubscribedChannels. Adds some helper methods in the WebsocketConn to reduce duplicate code. Cleans up ZB implementation

* Fixes double locking which caused no websocket data to be read. Fixes requestid for kraken subscriptions

* Completes Huobi and Hadax implementation. Extends ZB error handling. Adds GZip support for reading messages

* Adds HitBTC support. Adds GetCurrencies, GetSymbols, GetTrades WS funcs. Adds super fun new parameter to GenerateMessageID for Unix and UnixNano

* Adds GateIO id support

* Adds Coinut support. Prevents nil reference error in constatus when there isnt one

* Standardises all Exchange websockets to use the wshandler websocket. Removes the wsRequestMtx as wshandler handles that now. Makes the Dialer a dialer, its not externally referenced that I can see.

* Fixes issue with coinut implementation. Updates bitmex currencies. Removes redundant log messages which are used to log messages

* Starts testing. Renames files

* Adds tests for websocket connection

* Reverts request.go change

* Linting everything

* Fixes rebase issue

* Final changes. Fixes variable names, removes log.Debug, removes lines, rearranges test types, removes order correlation websocket type

* Final final commit, fixing ZB issues.

* Adds traffic alerts where missed. Changes empty struct pointer addresses to nil instead. Removes empty lines

* Fixed string conversion

* Fixes issue with ZB not sending success codes

* Fixes issue with coinut processing due to nonce handling with subscriptions

* Fixes issue where ZB test failure was not caught. Removes unnecessary error handling from other ZB tests

* Removes unused interface

* Renames wshandler.Init() to wshandler.Run()

* Updates template file

* Capitalises cryptocurrencies in struct. Moves websocketResponseCheckTimeout and websocketResponseMaxLimit into config options. Moves connection configuration to main exchange Setup (where appropriate). Reverts currencylastupdated ticks. Improves reader close error checking

* Fixes two inconsistent websocket delay times

* Creates a default variable for websocket ResponseMaxLimit and ResponseCheckTimeout, then applies it to setdefaults and all tests

* Updates exchange template to set and use default websocket response limits
This commit is contained in:
Scott
2019-08-07 15:15:01 +10:00
committed by Adrian Gallagher
parent 6e70f0642a
commit e209d85d0d
113 changed files with 3269 additions and 2594 deletions

View File

@@ -14,16 +14,15 @@ import (
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/request"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
"github.com/thrasher-/gocryptotrader/exchanges/wshandler"
log "github.com/thrasher-/gocryptotrader/logger"
)
@@ -69,9 +68,8 @@ const (
type HUOBI struct {
exchange.Base
AccountID string
WebsocketConn *websocket.Conn
AuthenticatedWebsocketConn *websocket.Conn
wsRequestMtx sync.Mutex
WebsocketConn *wshandler.WebsocketConnection
AuthenticatedWebsocketConn *wshandler.WebsocketConnection
}
// SetDefaults sets default values for the exchange
@@ -96,14 +94,17 @@ func (h *HUOBI) SetDefaults() {
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
h.APIUrlDefault = huobiAPIURL
h.APIUrl = h.APIUrlDefault
h.WebsocketInit()
h.Websocket.Functionality = exchange.WebsocketKlineSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketTradeDataSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported |
exchange.WebsocketAccountDataSupported
h.Websocket = wshandler.New()
h.Websocket.Functionality = wshandler.WebsocketKlineSupported |
wshandler.WebsocketOrderbookSupported |
wshandler.WebsocketTradeDataSupported |
wshandler.WebsocketSubscribeSupported |
wshandler.WebsocketUnsubscribeSupported |
wshandler.WebsocketAuthenticatedEndpointsSupported |
wshandler.WebsocketAccountDataSupported |
wshandler.WebsocketMessageCorrelationSupported
h.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
h.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
}
// Setup sets user configuration
@@ -146,17 +147,36 @@ func (h *HUOBI) Setup(exch *config.ExchangeConfig) {
if err != nil {
log.Fatal(err)
}
err = h.WebsocketSetup(h.WsConnect,
err = h.Websocket.Setup(h.WsConnect,
h.Subscribe,
h.Unsubscribe,
exch.Name,
exch.Websocket,
exch.Verbose,
wsMarketURL,
exch.WebsocketURL)
exch.WebsocketURL,
exch.AuthenticatedWebsocketAPISupport)
if err != nil {
log.Fatal(err)
}
h.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: h.Name,
URL: wsMarketURL,
ProxyURL: h.Websocket.GetProxyAddress(),
Verbose: h.Verbose,
RateLimit: rateLimit,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
h.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: h.Name,
URL: wsAccountsOrdersURL,
ProxyURL: h.Websocket.GetProxyAddress(),
Verbose: h.Verbose,
RateLimit: rateLimit,
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
}
}
}

View File

@@ -9,7 +9,6 @@ import (
"strconv"
"strings"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
@@ -17,6 +16,7 @@ import (
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-/gocryptotrader/exchanges/wshandler"
)
// Please supply you own test keys here for due diligence testing.
@@ -56,37 +56,36 @@ func setupWsTests(t *testing.T) {
TestSetDefaults(t)
TestSetup(t)
if !h.Websocket.IsEnabled() && !h.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
t.Skip(wshandler.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
comms = make(chan WsMessage, sharedtestvalues.WebsocketChannelOverrideCapacity)
h.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
h.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
go h.WsHandleData()
err = h.wsAuthenticatedDial(&dialer)
h.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: h.Name,
URL: wsAccountsOrdersURL,
Verbose: h.Verbose,
ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit,
ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout,
}
h.WebsocketConn = &wshandler.WebsocketConnection{
ExchangeName: h.Name,
URL: wsMarketURL,
Verbose: h.Verbose,
ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit,
ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout,
}
var dialer websocket.Dialer
err := h.wsAuthenticatedDial(&dialer)
if err != nil {
t.Error(err)
t.Fatal(err)
}
err = h.wsLogin()
if err != nil {
t.Error(err)
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case response := <-h.Websocket.DataHandler:
switch respType := response.(type) {
case WsAuthenticatedDataResponse:
if respType.ErrorCode > 0 {
t.Error(respType)
}
case error:
t.Error(respType)
}
case <-timer.C:
t.Error("Websocket did not receive a response")
}
timer.Stop()
wsSetupRan = true
}
@@ -656,46 +655,36 @@ func TestGetDepositAddress(t *testing.T) {
// TestWsGetAccountsList connects to WS, logs in, gets account list
func TestWsGetAccountsList(t *testing.T) {
setupWsTests(t)
h.wsGetAccountsList(currency.NewPairFromString("ethbtc"))
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case response := <-h.Websocket.DataHandler:
switch respType := response.(type) {
case WsAuthenticatedAccountsListResponse:
if respType.ErrorCode > 0 {
t.Error(respType)
}
case error:
t.Error(respType)
}
case <-timer.C:
t.Error("Websocket did not receive a response")
resp, err := h.wsGetAccountsList(currency.NewPairFromString("ethbtc"))
if err != nil {
t.Fatal(err)
}
if resp.ErrorCode > 0 {
t.Error(resp.ErrorMessage)
}
timer.Stop()
}
// TestWsGetOrderList connects to WS, logs in, gets order list
func TestWsGetOrderList(t *testing.T) {
setupWsTests(t)
h.wsGetOrdersList(1, currency.NewPairFromString("ethbtc"))
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Websocket did not receive a response")
resp, err := h.wsGetOrdersList(1, currency.NewPairFromString("ethbtc"))
if err != nil {
t.Fatal(err)
}
if resp.ErrorCode > 0 {
t.Error(resp.ErrorMessage)
}
timer.Stop()
}
// TestWsGetOrderDetails connects to WS, logs in, gets order details
func TestWsGetOrderDetails(t *testing.T) {
setupWsTests(t)
h.wsGetOrderDetails("123")
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Websocket did not receive a response")
orderID := "123"
resp, err := h.wsGetOrderDetails(orderID)
if err != nil {
t.Fatal(err)
}
if resp.ErrorCode > 0 && (orderID == "123" && resp.ErrorCode != 10022) {
t.Error(resp.ErrorMessage)
}
timer.Stop()
}

View File

@@ -270,10 +270,10 @@ var (
// WsRequest defines a request data structure
type WsRequest struct {
Topic string `json:"req,omitempty"`
Subscribe string `json:"sub,omitempty"`
Unsubscribe string `json:"unsub,omitempty"`
ClientGeneratedID string `json:"id,omitempty"`
Topic string `json:"req,omitempty"`
Subscribe string `json:"sub,omitempty"`
Unsubscribe string `json:"unsub,omitempty"`
ClientID int64 `json:"cid,string,omitempty"`
}
// WsResponse defines a response from the websocket connection when there
@@ -286,6 +286,7 @@ type WsResponse struct {
Ping int64 `json:"ping"`
Channel string `json:"ch"`
Subscribed string `json:"subbed"`
ClientID int64 `json:"cid,string,omitempty"`
}
// WsHeartBeat defines a heartbeat request
@@ -346,6 +347,7 @@ type WsAuthenticationRequest struct {
SignatureVersion string `json:"SignatureVersion"`
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
ClientID int64 `json:"cid,string,omitempty"`
}
// WsMessage defines read data from the websocket connection
@@ -363,6 +365,7 @@ type WsAuthenticatedSubscriptionRequest struct {
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
Topic string `json:"topic"`
ClientID int64 `json:"cid,string,omitempty"`
}
// WsAuthenticatedAccountsListRequest request for account list authenticated connection
@@ -375,6 +378,7 @@ type WsAuthenticatedAccountsListRequest struct {
Signature string `json:"Signature"`
Topic string `json:"topic"`
Symbol currency.Pair `json:"symbol"`
ClientID int64 `json:"cid,string,omitempty"`
}
// WsAuthenticatedOrderDetailsRequest request for order details authenticated connection
@@ -387,6 +391,7 @@ type WsAuthenticatedOrderDetailsRequest struct {
Signature string `json:"Signature"`
Topic string `json:"topic"`
OrderID string `json:"order-id"`
ClientID int64 `json:"cid,string,omitempty"`
}
// WsAuthenticatedOrdersListRequest request for orderslist authenticated connection
@@ -401,6 +406,7 @@ type WsAuthenticatedOrdersListRequest struct {
States string `json:"states"`
AccountID int64 `json:"account-id"`
Symbol currency.Pair `json:"symbol"`
ClientID int64 `json:"cid,string,omitempty"`
}
// WsAuthenticatedDataResponse response from authenticated connection
@@ -411,7 +417,7 @@ type WsAuthenticatedDataResponse struct {
ErrorCode int64 `json:"err-code,omitempty"`
ErrorMessage string `json:"err-msg,omitempty"`
Ping int64 `json:"ping,omitempty"`
CID string `json:"cid,omitempty"`
ClientID int64 `json:"cid,string,omitempty"`
}
// WsAuthenticatedAccountsResponse response from Accounts authenticated subscription
@@ -529,3 +535,8 @@ type WsAuthenticatedOrderDetailResponse struct {
WsAuthenticatedDataResponse
Data WsAuthenticatedOrdersListResponseData `json:"data"`
}
// WsPong sent for pong messages
type WsPong struct {
Pong int64 `json:"pong"`
}

View File

@@ -1,11 +1,8 @@
package huobi
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
@@ -16,6 +13,7 @@ import (
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-/gocryptotrader/exchanges/wshandler"
log "github.com/thrasher-/gocryptotrader/logger"
)
@@ -42,6 +40,9 @@ const (
signatureVersion = "2"
requestOp = "req"
authOp = "auth"
loginDelay = 50 * time.Millisecond
rateLimit = 20
)
// Instantiates a communications channel between websocket connections
@@ -50,20 +51,9 @@ var comms = make(chan WsMessage, 1)
// WsConnect initiates a new websocket connection
func (h *HUOBI) WsConnect() error {
if !h.Websocket.IsEnabled() || !h.IsEnabled() {
return errors.New(exchange.WebsocketNotEnabled)
return errors.New(wshandler.WebsocketNotEnabled)
}
var dialer websocket.Dialer
if h.Websocket.GetProxyAddress() != "" {
proxy, err := url.Parse(h.Websocket.GetProxyAddress())
if err != nil {
return err
}
dialer.Proxy = http.ProxyURL(proxy)
}
err := h.wsDial(&dialer)
if err != nil {
return err
@@ -84,11 +74,9 @@ func (h *HUOBI) WsConnect() error {
}
func (h *HUOBI) wsDial(dialer *websocket.Dialer) error {
var err error
var conStatus *http.Response
h.WebsocketConn, conStatus, err = dialer.Dial(wsMarketURL, http.Header{})
err := h.WebsocketConn.Dial(dialer, http.Header{})
if err != nil {
return fmt.Errorf("%v %v %v Error: %v", wsMarketURL, conStatus, conStatus.StatusCode, err)
return err
}
go h.wsMultiConnectionFunnel(h.WebsocketConn, wsMarketURL)
return nil
@@ -98,18 +86,16 @@ func (h *HUOBI) wsAuthenticatedDial(dialer *websocket.Dialer) error {
if !h.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", h.Name)
}
var err error
var conStatus *http.Response
h.AuthenticatedWebsocketConn, conStatus, err = dialer.Dial(wsAccountsOrdersURL, http.Header{})
err := h.AuthenticatedWebsocketConn.Dial(dialer, http.Header{})
if err != nil {
return fmt.Errorf("%v %v %v Error: %v", wsAccountsOrdersURL, conStatus, conStatus.StatusCode, err)
return err
}
go h.wsMultiConnectionFunnel(h.AuthenticatedWebsocketConn, wsAccountsOrdersURL)
return nil
}
// wsMultiConnectionFunnel manages data from multiple endpoints and passes it to a channel
func (h *HUOBI) wsMultiConnectionFunnel(ws *websocket.Conn, url string) {
func (h *HUOBI) wsMultiConnectionFunnel(ws *wshandler.WebsocketConnection, url string) {
h.Websocket.Wg.Add(1)
defer h.Websocket.Wg.Done()
for {
@@ -117,29 +103,13 @@ func (h *HUOBI) wsMultiConnectionFunnel(ws *websocket.Conn, url string) {
case <-h.Websocket.ShutdownC:
return
default:
_, resp, err := ws.ReadMessage()
resp, err := ws.ReadMessage()
if err != nil {
h.Websocket.DataHandler <- err
return
}
h.Websocket.TrafficAlert <- struct{}{}
b := bytes.NewReader(resp)
gReader, err := gzip.NewReader(b)
if err != nil {
h.Websocket.DataHandler <- err
return
}
unzipped, err := ioutil.ReadAll(gReader)
if err != nil {
h.Websocket.DataHandler <- err
return
}
err = gReader.Close()
if err != nil {
h.Websocket.DataHandler <- err
return
}
comms <- WsMessage{Raw: unzipped, URL: url}
comms <- WsMessage{Raw: resp.Raw, URL: url}
}
}
}
@@ -153,9 +123,6 @@ func (h *HUOBI) WsHandleData() {
case <-h.Websocket.ShutdownC:
return
case resp := <-comms:
if h.Verbose {
log.Debugf("%v: %v: %v", h.Name, resp.URL, string(resp.Raw))
}
switch resp.URL {
case wsMarketURL:
h.wsHandleMarketData(resp)
@@ -173,31 +140,26 @@ func (h *HUOBI) wsHandleAuthenticatedData(resp WsMessage) {
h.Websocket.DataHandler <- err
return
}
if init.ErrorCode > 0 {
if init.ErrorMessage == "api-signature-not-valid" {
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
h.Websocket.DataHandler <- fmt.Errorf("%v %v Websocket error %v %s",
h.Name,
resp.URL,
init.ErrorCode,
init.ErrorMessage)
return
}
if init.Ping != 0 {
err = h.WebsocketConn.WriteJSON(`{"pong":1337}`)
err = h.WebsocketConn.SendMessage(WsPong{Pong: init.Ping})
if err != nil {
log.Error(err)
}
return
}
if init.ErrorMessage == "api-signature-not-valid" {
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
if init.Op == "sub" {
if h.Verbose {
log.Debugf("%v: %v: Successfully subscribed to %v", h.Name, resp.URL, init.Topic)
}
return
}
if init.ClientID > 0 {
h.AuthenticatedWebsocketConn.AddResponseWithID(init.ClientID, resp.Raw)
return
}
switch {
case strings.EqualFold(init.Op, authOp):
@@ -230,27 +192,6 @@ func (h *HUOBI) wsHandleAuthenticatedData(resp WsMessage) {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case strings.EqualFold(init.Topic, wsAccountsList):
var response WsAuthenticatedAccountsListResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case strings.EqualFold(init.Topic, wsOrdersList):
var response WsAuthenticatedOrdersListResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case strings.EqualFold(init.Topic, wsOrdersDetail):
var response WsAuthenticatedOrderDetailResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
}
}
@@ -273,7 +214,7 @@ func (h *HUOBI) wsHandleMarketData(resp WsMessage) {
return
}
if init.Ping != 0 {
err = h.WebsocketConn.WriteJSON(`{"pong":1337}`)
err = h.WebsocketConn.SendMessage(WsPong{Pong: init.Ping})
if err != nil {
log.Error(err)
}
@@ -298,7 +239,7 @@ func (h *HUOBI) wsHandleMarketData(resp WsMessage) {
return
}
data := common.SplitStrings(kline.Channel, ".")
h.Websocket.DataHandler <- exchange.KlineData{
h.Websocket.DataHandler <- wshandler.KlineData{
Timestamp: time.Unix(0, kline.Timestamp),
Exchange: h.GetName(),
AssetType: "SPOT",
@@ -317,7 +258,7 @@ func (h *HUOBI) wsHandleMarketData(resp WsMessage) {
return
}
data := common.SplitStrings(trade.Channel, ".")
h.Websocket.DataHandler <- exchange.TradeData{
h.Websocket.DataHandler <- wshandler.TradeData{
Exchange: h.GetName(),
AssetType: "SPOT",
CurrencyPair: currency.NewPairFromString(data[1]),
@@ -354,7 +295,7 @@ func (h *HUOBI) WsProcessOrderbook(ob *WsDepth, symbol string) error {
return err
}
h.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
h.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{
Pair: p,
Exchange: h.GetName(),
Asset: "SPOT",
@@ -366,10 +307,10 @@ func (h *HUOBI) WsProcessOrderbook(ob *WsDepth, symbol string) error {
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (h *HUOBI) GenerateDefaultSubscriptions() {
var channels = []string{wsMarketKline, wsMarketDepth, wsMarketTrade}
var subscriptions []exchange.WebsocketChannelSubscription
var subscriptions []wshandler.WebsocketChannelSubscription
if h.Websocket.CanUseAuthenticatedEndpoints() {
channels = append(channels, "orders.%v", "orders.%v.update")
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: "accounts",
})
}
@@ -378,7 +319,7 @@ func (h *HUOBI) GenerateDefaultSubscriptions() {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = ""
channel := fmt.Sprintf(channels[i], enabledCurrencies[j].Lower().String())
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{
Channel: channel,
Currency: enabledCurrencies[j],
})
@@ -388,39 +329,33 @@ func (h *HUOBI) GenerateDefaultSubscriptions() {
}
// Subscribe sends a websocket message to receive data from the channel
func (h *HUOBI) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
func (h *HUOBI) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
if common.StringContains(channelToSubscribe.Channel, "orders.") ||
common.StringContains(channelToSubscribe.Channel, "accounts") {
return h.wsAuthenticatedSubscribe("sub", wsAccountsOrdersEndPoint+channelToSubscribe.Channel, channelToSubscribe.Channel)
}
subscription, err := common.JSONEncode(WsRequest{Subscribe: channelToSubscribe.Channel})
if err != nil {
return err
}
return h.wsSend(subscription)
return h.WebsocketConn.SendMessage(WsRequest{Subscribe: channelToSubscribe.Channel})
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (h *HUOBI) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
func (h *HUOBI) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error {
if common.StringContains(channelToSubscribe.Channel, "orders.") ||
common.StringContains(channelToSubscribe.Channel, "accounts") {
return h.wsAuthenticatedSubscribe("unsub", wsAccountsOrdersEndPoint+channelToSubscribe.Channel, channelToSubscribe.Channel)
}
subscription, err := common.JSONEncode(WsRequest{Unsubscribe: channelToSubscribe.Channel})
if err != nil {
return err
}
return h.wsSend(subscription)
return h.WebsocketConn.SendMessage(WsRequest{Unsubscribe: channelToSubscribe.Channel})
}
// WsSend sends data to the websocket server
func (h *HUOBI) wsSend(data []byte) error {
h.wsRequestMtx.Lock()
defer h.wsRequestMtx.Unlock()
if h.Verbose {
log.Debugf("%v sending message to websocket %s", h.Name, string(data))
}
return h.WebsocketConn.WriteMessage(websocket.TextMessage, data)
func (h *HUOBI) wsGenerateSignature(timestamp, endpoint string) []byte {
values := url.Values{}
values.Set("AccessKeyId", h.APIKey)
values.Set("SignatureMethod", signatureMethod)
values.Set("SignatureVersion", signatureVersion)
values.Set("Timestamp", timestamp)
host := "api.huobi.pro"
payload := fmt.Sprintf("%s\n%s\n%s\n%s",
"GET", host, endpoint, values.Encode())
return common.GetHMAC(common.HashSHA256, []byte(payload), []byte(h.APISecret))
}
func (h *HUOBI) wsLogin() error {
@@ -438,39 +373,16 @@ func (h *HUOBI) wsLogin() error {
}
hmac := h.wsGenerateSignature(timestamp, wsAccountsOrdersEndPoint)
request.Signature = common.Base64Encode(hmac)
err := h.wsAuthenticatedSend(request)
err := h.AuthenticatedWebsocketConn.SendMessage(request)
if err != nil {
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
time.Sleep(loginDelay)
return nil
}
func (h *HUOBI) wsAuthenticatedSend(request interface{}) error {
h.wsRequestMtx.Lock()
defer h.wsRequestMtx.Unlock()
encodedRequest, err := common.JSONEncode(request)
if err != nil {
return err
}
if h.Verbose {
log.Debugf("%v sending Authenticated message to websocket %s", h.Name, string(encodedRequest))
}
return h.AuthenticatedWebsocketConn.WriteMessage(websocket.TextMessage, encodedRequest)
}
func (h *HUOBI) wsGenerateSignature(timestamp, endpoint string) []byte {
values := url.Values{}
values.Set("AccessKeyId", h.APIKey)
values.Set("SignatureMethod", signatureMethod)
values.Set("SignatureVersion", signatureVersion)
values.Set("Timestamp", timestamp)
host := "api.huobi.pro"
payload := fmt.Sprintf("%s\n%s\n%s\n%s",
"GET", host, endpoint, values.Encode())
return common.GetHMAC(common.HashSHA256, []byte(payload), []byte(h.APISecret))
}
func (h *HUOBI) wsAuthenticatedSubscribe(operation, endpoint, topic string) error {
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedSubscriptionRequest{
@@ -483,12 +395,12 @@ func (h *HUOBI) wsAuthenticatedSubscribe(operation, endpoint, topic string) erro
}
hmac := h.wsGenerateSignature(timestamp, endpoint)
request.Signature = common.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
return h.AuthenticatedWebsocketConn.SendMessage(request)
}
func (h *HUOBI) wsGetAccountsList(pair currency.Pair) error {
func (h *HUOBI) wsGetAccountsList(pair currency.Pair) (*WsAuthenticatedAccountsListResponse, error) {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated cannot get accounts list", h.Name)
return nil, fmt.Errorf("%v not authenticated cannot get accounts list", h.Name)
}
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedAccountsListRequest{
@@ -502,12 +414,19 @@ func (h *HUOBI) wsGetAccountsList(pair currency.Pair) error {
}
hmac := h.wsGenerateSignature(timestamp, wsAccountListEndpoint)
request.Signature = common.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
request.ClientID = h.AuthenticatedWebsocketConn.GenerateMessageID(true)
resp, err := h.AuthenticatedWebsocketConn.SendMessageReturnResponse(request.ClientID, request)
if err != nil {
return nil, err
}
var response WsAuthenticatedAccountsListResponse
err = common.JSONDecode(resp, &response)
return &response, err
}
func (h *HUOBI) wsGetOrdersList(accountID int64, pair currency.Pair) error {
func (h *HUOBI) wsGetOrdersList(accountID int64, pair currency.Pair) (*WsAuthenticatedOrdersResponse, error) {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated cannot get orders list", h.Name)
return nil, fmt.Errorf("%v not authenticated cannot get orders list", h.Name)
}
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedOrdersListRequest{
@@ -523,12 +442,19 @@ func (h *HUOBI) wsGetOrdersList(accountID int64, pair currency.Pair) error {
}
hmac := h.wsGenerateSignature(timestamp, wsOrdersListEndpoint)
request.Signature = common.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
request.ClientID = h.AuthenticatedWebsocketConn.GenerateMessageID(true)
resp, err := h.AuthenticatedWebsocketConn.SendMessageReturnResponse(request.ClientID, request)
if err != nil {
return nil, err
}
var response WsAuthenticatedOrdersResponse
err = common.JSONDecode(resp, &response)
return &response, err
}
func (h *HUOBI) wsGetOrderDetails(orderID string) error {
func (h *HUOBI) wsGetOrderDetails(orderID string) (*WsAuthenticatedOrderDetailResponse, error) {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated cannot get order details", h.Name)
return nil, fmt.Errorf("%v not authenticated cannot get order details", h.Name)
}
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedOrderDetailsRequest{
@@ -542,5 +468,12 @@ func (h *HUOBI) wsGetOrderDetails(orderID string) error {
}
hmac := h.wsGenerateSignature(timestamp, wsOrdersDetailEndpoint)
request.Signature = common.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
request.ClientID = h.AuthenticatedWebsocketConn.GenerateMessageID(true)
resp, err := h.AuthenticatedWebsocketConn.SendMessageReturnResponse(request.ClientID, request)
if err != nil {
return nil, err
}
var response WsAuthenticatedOrderDetailResponse
err = common.JSONDecode(resp, &response)
return &response, err
}

View File

@@ -14,6 +14,7 @@ import (
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-/gocryptotrader/exchanges/ticker"
"github.com/thrasher-/gocryptotrader/exchanges/wshandler"
log "github.com/thrasher-/gocryptotrader/logger"
)
@@ -385,7 +386,7 @@ func (h *HUOBI) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.W
}
// GetWebsocket returns a pointer to the exchange websocket
func (h *HUOBI) GetWebsocket() (*exchange.Websocket, error) {
func (h *HUOBI) GetWebsocket() (*wshandler.Websocket, error) {
return h.Websocket, nil
}
@@ -512,20 +513,20 @@ func setOrderSideAndType(requestType string, orderDetail *exchange.OrderDetail)
// SubscribeToWebsocketChannels appends to ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle subscribing
func (h *HUOBI) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
func (h *HUOBI) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
h.Websocket.SubscribeToChannels(channels)
return nil
}
// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe
// which lets websocket.manageSubscriptions handle unsubscribing
func (h *HUOBI) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
h.Websocket.UnsubscribeToChannels(channels)
func (h *HUOBI) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error {
h.Websocket.RemoveSubscribedChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (h *HUOBI) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
func (h *HUOBI) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return h.Websocket.GetSubscriptions(), nil
}